fix(niri): move the example toggle off Mod+Shift+D
CI / security (pull_request) Successful in 1m8s
CI / lint (pull_request) Successful in 23s
CI / ai-review (pull_request) Successful in 8s

Mod+Shift+D is Move Column Right in Dank Material Shell's niri binds
(dms/binds.kdl), so the shipped example competed with it silently. The
toggle moves to Mod+Shift+M, which is free in niri's stock binds and in
DMS's binds. The start and stop examples move off Mod+Shift+E, which is
stock niri's quit, to Mod+Shift+S and Mod+Shift+X.

niri validate rejects a key bound twice in one binds block but not across
blocks, so validation cannot show a conflict. The docs now give a grep
over the config tree and niri's embedded stock binds for checking a key
before binding it. The overlay pointer is corrected to Mod+Shift+Slash.
A test pins the doc and the include to one toggle key that is not
Mod+Shift+D.

Fixes #45
This commit is contained in:
2026-11-06 23:54:07 +05:30
parent 3776c4de6e
commit 2a5710334b
3 changed files with 55 additions and 7 deletions
+31 -4
View File
@@ -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
+7 -3
View File
@@ -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";
// }
}
+17
View File
@@ -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()