diff --git a/docs/NIRI.md b/docs/NIRI.md index c3798a2..b1fc2a5 100644 --- a/docs/NIRI.md +++ b/docs/NIRI.md @@ -39,7 +39,7 @@ The shipped bind: ```kdl binds { - Mod+Shift+D repeat=false hotkey-overlay-title="Toggle Voculinux dictation" { + Mod+Shift+M repeat=false hotkey-overlay-title="Toggle Voculinux dictation" { spawn "voculinux" "--toggle"; } } @@ -47,9 +47,11 @@ binds { `repeat=false` is what makes it toggle once per press: without it, holding the key repeats the spawn and toggles dictation back off before you let go. -Pick any key you like (the overlay title keeps it discoverable under -`Mod+H`), and uncomment the `--start` / `--stop` variants in the include -file if you want dedicated keys. +`Mod+Shift+M` was free in niri's stock binds and in Dank Material Shell's +binds when this was written. To use another key, check it first (below). +The bind's title shows in niri's hotkey overlay (`Mod+Shift+Slash`). The +include file's commented `--start` / `--stop` variants use `Mod+Shift+S` and +`Mod+Shift+X`, which were free in the same way. A key is either a compositor bind or an in-app Voculinux shortcut — never both. With External Activation on, do not keep the same key configured @@ -57,6 +59,31 @@ under **Settings → Shortcuts**; with it off, remove the niri bind. A key claimed on both sides fires twice: the compositor toggles over D-Bus while the internal listener fires on its own. +## Check a key before you bind it + +`niri validate` rejects a key bound twice inside one `binds {}` block, but +not a key bound in two blocks: a bind in your include file and one in +`dms/binds.kdl` both pass validation with no warning. So validation cannot +tell you whether a key is free. Check with grep, against your own config and +niri's stock binds. Run this before you copy the include file, or your own +toggle bind will match: + +```bash +KEY_RE='(Mod|Super)\+Shift\+M([[:space:]{]|$)' # the key you want to check +grep -rnE --include='*.kdl' "$KEY_RE" ~/.config/niri/ +strings "$(command -v niri)" | grep -E "^[[:space:]]*(//[[:space:]]*)?$KEY_RE" +``` + +The first command searches your config. `~/.config/niri/` holds +`config.kdl` and the DMS-managed `dms/binds.kdl`, which `config.kdl` +includes. `--include='*.kdl'` skips the `binds.kdl.pre-*` backups. If you +include files from elsewhere, add their paths. The second command searches +the stock defaults embedded in the niri binary. Commented lines match too, so +a hit can be a bind you have commented out. No output from either command +means the key is free. For a hit, remove or move the other bind, because niri does not warn +about a key bound in two places. `Mod` and `Super` are both checked because +niri reads `Mod` as Super on a TTY. + ## First dictation There is no permission prompt on niri. The RemoteDesktop portal is never diff --git a/resources/niri/voculinux.kdl b/resources/niri/voculinux.kdl index 16e610f..d595c6c 100644 --- a/resources/niri/voculinux.kdl +++ b/resources/niri/voculinux.kdl @@ -11,12 +11,16 @@ // // One key is either a compositor bind or an in-app Voculinux shortcut, // never both: do not keep this key configured under Settings → Shortcuts. +// +// The keys below were free in niri's stock binds and in Dank Material Shell's +// binds when this file was written. Before you use one, check it against your +// own config: docs/NIRI.md, "Check a key before you bind it". binds { // Toggle dictation, exactly once per press. repeat=false stops key // repeat from firing the spawn again while the key is held (which // would toggle dictation back off before you let go). - Mod+Shift+D repeat=false hotkey-overlay-title="Toggle Voculinux dictation" { + Mod+Shift+M repeat=false hotkey-overlay-title="Toggle Voculinux dictation" { spawn "voculinux" "--toggle"; } @@ -24,13 +28,13 @@ binds { // Mod+Shift+S repeat=false hotkey-overlay-title="Start Voculinux dictation" { // spawn "voculinux" "--start"; // } - // Mod+Shift+E repeat=false hotkey-overlay-title="Stop Voculinux dictation" { + // Mod+Shift+X repeat=false hotkey-overlay-title="Stop Voculinux dictation" { // spawn "voculinux" "--stop"; // } // Flatpak installs: voculinux is not on the host PATH, so bind the // host D-Bus call instead (same toggle, no extra sandbox per press). - // Mod+Shift+D repeat=false hotkey-overlay-title="Toggle Voculinux dictation" { + // Mod+Shift+M repeat=false hotkey-overlay-title="Toggle Voculinux dictation" { // spawn "gdbus" "call" "--session" "--dest" "com.voculinux.Voculinux" "--object-path" "/com/voculinux/Voculinux" "--method" "com.voculinux.Voculinux.Toggle"; // } } diff --git a/tests/test_niri_pack.py b/tests/test_niri_pack.py index e03cd3e..cca5621 100644 --- a/tests/test_niri_pack.py +++ b/tests/test_niri_pack.py @@ -8,6 +8,7 @@ widening fails loudly. When #32 records DMS evidence, update the doc and this assertion together. """ +import re from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] @@ -18,6 +19,9 @@ USER_GUIDE = REPO_ROOT / "docs" / "USER_GUIDE.md" TRAY_INDICATOR = REPO_ROOT / "src" / "vocalinux" / "ui" / "tray_indicator.py" TOGGLE_SPAWN = 'spawn "voculinux" "--toggle"' +TOGGLE_HEADER = re.compile(r"^(\S+) repeat=false") +#: Move Column Right in Dank Material Shell's binds; the shipped key must not reuse it (#45). +DMS_MOVE_COLUMN_RIGHT_KEY = "Mod+Shift+D" CURRENT_WATCHER = "org.kde.StatusNotifierWatcher" @@ -45,6 +49,19 @@ def test_include_bind_is_non_repeating() -> None: assert "repeat=false" in _read(NIRI_INCLUDE) +def test_doc_and_include_share_a_toggle_key_not_taken_by_dms() -> None: + """The doc's example key is the include file's toggle key, and it is not in use (#45).""" + keys = [ + match.group(1) + for match in (TOGGLE_HEADER.match(line) for line in _active_lines(_read(NIRI_INCLUDE))) + if match + ] + assert len(keys) == 1, "the include file should ship exactly one toggle bind" + key = keys[0] + assert key != DMS_MOVE_COLUMN_RIGHT_KEY + assert f"{key} repeat=false" in _read(NIRI_DOC) + + def test_include_file_uses_no_compositor_internals() -> None: """Only plain spawn actions; no compositor message APIs.""" assert "niri msg" not in _read(NIRI_INCLUDE).lower()