Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 84 additions & 11 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@ clap = { version = "4.4", features = ["derive"] }
tracing = "0.1"
tracing-subscriber = "0.3.18"
base64 = "0.22.1"
toml = "1.0.3"
serde_json = "1.0.149"
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ In order to download webpages, you need to have `monolith` installed. `cargo ins
Run `textpod` in any directory. It will create a `notes.md` file if it doesn't exist. It will create `attachments` directory for file and image attachments.
Webpages are saved in `attachments/webpages`. You can specify the port with `-p` flag, e.g. `textpod -p 8080` and/or the address with `-l` flag, e.g. `textpod -l 0.0.0.0`.

## Configuration

You can customize keyboard shortcuts by placing a `textpod.toml` file in the working directory (alongside `notes.md`). The file is optional — if absent, defaults apply.

```toml
[shortcuts]
save = "Cmd+Enter"
```

Supported modifiers: `Ctrl`, `Cmd`, `Alt`, `Shift`. Combine them with `+` and a key name (e.g. `Enter`, `s`). The default save shortcut is `Ctrl+Enter`.

## Docker

Docker image is available at [Docker Hub](https://hub.docker.com/r/freetonik/textpod).
Expand Down
20 changes: 18 additions & 2 deletions src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,26 @@
<body>

<textarea id="editor"
placeholder="Ctrl+Enter to save.&#10;Type / to search.&#10;Drag & drop files to attach.&#10;Start links with + to save local copies."></textarea>
placeholder="{{SAVE_SHORTCUT_DISPLAY}} to save.&#10;Type / to search.&#10;Drag & drop files to attach.&#10;Start links with + to save local copies."></textarea>
<div id="submitContainer"><button id="submitButton">Submit</button></div>
<div id="notes"></div>

<script>
const SHORTCUTS = {{SHORTCUTS_CONFIG}};

function matchShortcut(event, binding) {
const parts = binding.split('+');
const key = parts.pop();
const mods = parts.map(m => m.toLowerCase());
return (
event.key === key &&
event.ctrlKey === mods.includes('ctrl') &&
event.metaKey === mods.includes('cmd') &&
event.altKey === mods.includes('alt') &&
event.shiftKey === mods.includes('shift')
);
Comment on lines +123 to +133
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The matchShortcut function has a case-sensitivity issue. The event.key property returns capitalized key names (e.g., "Enter", "Escape") but the function performs a strict equality comparison without normalizing the case. This means a configuration like save = "Cmd+enter" (lowercase) would fail to match. Consider normalizing both sides to lowercase for the key comparison, or document that key names must match the exact case returned by event.key (e.g., "Enter" not "enter").

Copilot uses AI. Check for mistakes.
}

const editor = document.getElementById('editor');
const notesDiv = document.getElementById('notes');
const submitButton = document.getElementById('submitButton');
Expand Down Expand Up @@ -209,7 +224,8 @@
});

editor.addEventListener('keydown', async (e) => {
if (e.ctrlKey && e.key === 'Enter' && !editor.value.startsWith('/')) {
if (matchShortcut(e, SHORTCUTS.save) && !editor.value.startsWith('/')) {
Comment thread
SandroPacella marked this conversation as resolved.
e.preventDefault();
saveNotes();
}
});
Expand Down
48 changes: 44 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,30 @@ struct AppState {
notes_file: PathBuf,
}

#[derive(Deserialize, Default)]
struct Config {
#[serde(default)]
shortcuts: ShortcutsConfig,
}

#[derive(Deserialize, Serialize)]
struct ShortcutsConfig {
#[serde(default = "default_save_shortcut")]
save: String,
}

impl Default for ShortcutsConfig {
fn default() -> Self {
Self {
save: default_save_shortcut(),
}
}
}

fn default_save_shortcut() -> String {
"Ctrl+Enter".to_string()
}
Comment on lines +62 to +84
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no validation of the shortcut configuration value. If a user provides an invalid shortcut string (e.g., empty string, malformed format, or invalid key names), the application will still accept it and inject it into the HTML, potentially causing the shortcut functionality to silently fail. Consider adding validation to ensure the shortcut string follows the expected format (modifiers+key) and providing a warning or error if it's invalid.

Copilot uses AI. Check for mistakes.

const CONTENT_LENGTH_LIMIT: usize = 500 * 1024 * 1024; // allow uploading up to 500mb files... overkill?

#[tokio::main]
Expand All @@ -74,6 +98,17 @@ async fn main() {
}
}

let config: Config = match fs::read_to_string("textpod.toml") {
Ok(contents) => match toml::from_str(&contents) {
Ok(cfg) => cfg,
Err(e) => {
error!("failed to parse textpod.toml: {e}; using defaults");
Config::default()
}
},
Err(_) => Config::default(),
};

if let Err(e) = fs::create_dir_all("attachments") {
error!(
"could not create attachments directory in {}: {e}",
Expand All @@ -83,10 +118,15 @@ async fn main() {
}

let favicon = Base64Display::new(FAVICON_SVG, &STANDARD);
let html = INDEX_HTML.replace(
"{{FAVICON}}",
format!("data:image/svg+xml;base64,{favicon}").as_str(),
);
let shortcuts_json =
serde_json::to_string(&config.shortcuts).expect("failed to serialize shortcuts config");
let html = INDEX_HTML
.replace(
"{{FAVICON}}",
format!("data:image/svg+xml;base64,{favicon}").as_str(),
)
.replace("{{SHORTCUTS_CONFIG}}", &shortcuts_json)
.replace("{{SAVE_SHORTCUT_DISPLAY}}", &config.shortcuts.save);
Comment thread
freetonik marked this conversation as resolved.

let notes = Arc::new(Mutex::new(load_notes(&args.notes_file)));

Expand Down