feat: add customizable keyboard shortcuts via textpod.toml#54
feat: add customizable keyboard shortcuts via textpod.toml#54SandroPacella wants to merge 2 commits intofreetonik:mainfrom
Conversation
Reads an optional textpod.toml config file from the working directory
on startup. Shortcuts are defined under a [shortcuts] section (e.g.
save = "Cmd+Enter") and injected into the frontend via template
variables, reusing the existing {{FAVICON}} replacement pattern.
Defaults to Ctrl+Enter when no config file is present, keeping full
backwards compatibility. Resolves freetonik#46.
There was a problem hiding this comment.
Pull request overview
This pull request adds customizable keyboard shortcuts to Textpod by introducing an optional textpod.toml configuration file. The implementation addresses issue #46, where users reported that the hardcoded Ctrl+Enter shortcut conflicts with Firefox on Mac. The feature maintains full backward compatibility by using Ctrl+Enter as the default when no config file is present.
Changes:
- Added TOML-based configuration system for keyboard shortcuts with graceful fallback to defaults
- Implemented frontend shortcut matching logic that supports Ctrl, Cmd, Alt, and Shift modifiers
- Updated UI placeholder text to dynamically display the configured save shortcut
Reviewed changes
Copilot reviewed 4 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/main.rs | Added Config and ShortcutsConfig structs, TOML parsing at startup, and template injection for shortcuts |
| src/index.html | Added matchShortcut() function and updated keydown handler to use configurable shortcuts |
| README.md | Added Configuration section documenting the textpod.toml format and supported modifiers |
| Cargo.toml | Added toml and serde_json dependencies |
| Cargo.lock | Updated dependency tree with new packages |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 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') | ||
| ); |
There was a problem hiding this comment.
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").
| #[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() | ||
| } |
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
The shortcut configuration is injected directly into the HTML without any sanitization or escaping. While the TOML parser provides some protection, a malicious or malformed textpod.toml file could potentially inject arbitrary JavaScript code into the page via the {{SAVE_SHORTCUT_DISPLAY}} and {{SHORTCUTS_CONFIG}} placeholders. Consider HTML-escaping the save shortcut display string and ensuring the JSON serialization is safe (which it should be with serde_json, but verify the dependencies are legitimate - see other comments).
|
Huh, I've tried this new copilot code review feature. I did not focus-read the suggestions yet, but on a brief scan some of them at least sound relevant. |
… default browser behavior when save shortcut is triggered Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
I like these suggestions copilot made: For e.preventDefault() —> I just pushed a commit. Without it the browser could intercept the shortcut before we ever see it. On the case-sensitivity and validation points: both valid imo, and I want to address them. I’d rather not tack them onto this PR since they’d expand the scope pretty quickly. Would it make more sense to open follow-up issues so we can track properly, or do you want them handled here first? An afterthought: I use StackEdit often and I hate when its key bindings override mine, so that's something I'll keep in mind here. |
Summary
Resolves #46.
textpod.tomlconfig file that users place in the working directory alongsidenotes.md[shortcuts]section (e.g.save = "Cmd+Enter")Ctrl,Cmd,Alt,Shift, combinable with any key nameCtrl+Enter— fully backwards-compatibleImplementation
Reuses the existing
{{FAVICON}}template injection pattern: the backend readstextpod.tomlon startup, serializes the shortcuts config to JSON, and injects it into the HTML via{{SHORTCUTS_CONFIG}}and{{SAVE_SHORTCUT_DISPLAY}}template variables. The frontendmatchShortcut()function parses the binding string and matches it againstKeyboardEventproperties.Config example
Test plan
Ctrl+Entersaves notes, placeholder says "Ctrl+Enter to save."save = "Cmd+Enter": Cmd+Enter saves, Ctrl+Enter does not, placeholder says "Cmd+Enter to save."