Repository navigation
Additive custom keymaps with conflict detection - #225
Merged
Merged
Conversation
Implements user-facing JSON keymap configuration following the ThemeManager pattern. Users can now customize all keybindings by creating JSON files in ~/.sqlit/keymaps/ and setting custom_keymap in settings.json. Features: - KeymapManager class for loading/validating custom keymaps - FileBasedKeymapProvider implementing KeymapProvider interface - JSON structure with leader_commands and action_keys arrays - Graceful error handling (invalid configs don't crash app) - Comprehensive test suite (11 tests covering all scenarios) - User documentation with examples and troubleshooting - Template keymap showing all default bindings Integration: - Loads during app startup after theme initialization - Settings key: custom_keymap (default/name) - Directory: ~/.sqlit/keymaps/ - Follows established sqlit configuration patterns Files: - sqlit/core/keymap_manager.py: Manager implementation - config/keymap.template.json: User template with examples - docs/custom-keymaps.md: Complete user guide - tests/ui/keybindings/test_keymap_manager.py: Test coverage This enables power users to customize keybindings per CONTRIBUTING.md line 243 which explicitly permits keyboard binding customization.
Builds on #131 by xtrasmal. Overrides now merge over the defaults rather than replacing the whole map, the footer and help screen reflect custom keys, and a config with conflicting bindings is rejected at startup. Also wires more screens (value view, autocomplete, connection editor, etc.) into the keymap so user overrides actually take effect there too. Co-authored-by: Xander <1174877+xtrasmal@users.noreply.github.com>
JSON shape is now a pure key remap:
{ "keymap": { "action_keys": { "<state>": { "<action>": "<key>" } },
"leader_commands": { "<menu>": { "<action>": "<key>" } } } }
Unknown (state, action) pairs are rejected with a helpful error.
Label, category, guard, primary, show, priority are all inherited
from defaults — they are no longer user-facing.
KeymapManager moves from sqlit/core/ to sqlit/domains/shell/app/,
alongside ThemeManager. settings_store is now a required argument;
the lazy import from domains is gone and core no longer depends on
domains for this code path.
Also fixes a bug where ":" in INSERT mode could be re-dispatched
through the action router and start command mode mid-SQL-string;
QueryInsertModeState now forbids enter_command_mode. Strips id=
attributes that had no matching keymap default and were therefore
inert.
# Conflicts: # sqlit/domains/shell/state/machine.py
When a custom keymap fails to load (invalid JSON, unknown action, conflicting bindings), the error now shows up as an error-severity notification in the TUI instead of being buried in stderr where a running-in-a-terminal user never sees it. KeymapManager stores the message on self.load_error; startup_flow picks it up after mount and surfaces it via app.notify, falling back to stderr on headless or when notify fails. Mirrors the existing _warn_on_missing_actions / _warn_on_keyring_error pattern.
app.notify() raises NoMatches mid-run_on_mount because the notification rack's refresh chain touches widgets (#results-area DataTable) that aren't mounted until after refresh. The notify exception was swallowed and the user saw nothing. Wrap the notify call in call_after_refresh so it fires once the initial paint completes. The existing _warn_on_missing_actions and _warn_on_keyring_error have the same latent bug — left untouched here since they rarely fire in practice.
Setting a key to null/""/[] in the keymap removes the default for that
(action, state) — no key triggers it anymore. Makes the natural case
where the user wants a key already taken by another default trivially
solvable:
"query_normal": {
"enter_insert_mode": "u",
"undo": null
}
Previously this would fail with "key 'u' is bound to multiple actions"
and the user had no in-config way to resolve it. The conflict-error
message now points at the null-unbind option, and the modal's
"defaults are in effect" hint uses the resolved CUSTOM_KEYMAP_DIR
instead of the hardcoded legacy ~/.sqlit/keymaps/ path.
Short pointer in the Configuration section with a minimal example and a link to docs/custom-keymaps.md. Doesn't add it to the main Keybindings table — kept advanced-usage voluntary per CONTRIBUTING.md.
The customization flow is now: drop a single JSON file at ~/.config/sqlit/keymap.json, restart sqlit. No settings.json edit, no mkdir. - KeymapManager picks up the default file automatically when no custom_keymap setting is set. The setting still wins for the power-user case where you want several named keymaps in keymaps/. - README's Custom keybindings subsection is now self-contained (path, JSON shape, list/null semantics, multi-keymap escape hatch). - docs/custom-keymaps.md deleted — the workflow + error messages carry the user without separate documentation. Errors already list valid actions/states on typo and suggest the null unbind on conflict. - keymap.template.json points at the new path.
…affold A user editing keymap.json no longer needs to know Textual's internal key names — `:`, `?`, `/`, `$`, `%`, `*`, `^`, `;`, `@`, `#`, `!`, `&`, `~`, brackets, etc. work directly. The loader expands them to canonical Textual variants on read (e.g. `:` covers `colon`, `shift+semicolon`, and `:` to handle every terminal's emission), so a binding written with the friendly form works everywhere. config/keymap.template.json is now the full default keymap in friendly form (~9KB across every state and menu). A user copies lines they want to change rather than memorizing action names. KeymapManager auto-creates an empty keymap.json scaffold in the config dir on first run so the customization file is discoverable. README points at both files. Conflict detection now ignores user-preserved default overlaps (e.g. the intentional `d` → delete_connection vs delete_connection_folder overlap in `tree`, both kept by state-machine disambiguation) — only new user-introduced collisions are flagged. docs/keymap-actions.md and docs/custom-keymaps.md removed — the template + README + error messages carry the user without a separate documentation file to maintain.
…dead code `_expand_user_key` was naively concatenating the user's modifier prefix onto each canonical variant. For inputs like `shift+:` — where `:` already expands to `shift+semicolon` — this produced `shift+shift+semicolon`, which Textual silently ignores. Now the merge unions user modifiers with whatever the canonical already carries, so each modifier appears once. Edge case: input ending in `+` (e.g. `ctrl++`, meaning ctrl + the plus key) is preserved verbatim — caught by a regression test before pushing. Also removes `canonical_to_friendly` and `_CANONICAL_TO_FRIENDLY` — the template-from-defaults generator they were written for was never wired up, so they were dead code.
# Conflicts: # sqlit/domains/shell/state/machine.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Builds on #131 — thanks @xtrasmal for kicking this off.
Overrides now merge over the defaults rather than replacing the whole map, the footer and help screen reflect your custom keys, and a config with conflicting bindings is rejected at startup with a clear error. Also wired more places into the keymap (value view, autocomplete, connection editor, error/file pickers) so overrides actually take effect there too.