feat(niri): add niri keybind pack and Dank Material Shell notes

Docs-only slice of ticket #22: include file, integration page, index
links. Tray-watcher observation stays open for hardware run (#32); no
probe code changed.

Refs #22
This commit is contained in:
soubarnak
2026-11-06 02:23:40 +05:30
parent 27f157355d
commit c27d204a00
6 changed files with 223 additions and 1 deletions
+1
View File
@@ -1,4 +1,5 @@
include voculinux.desktop include voculinux.desktop
include resources/icons/scalable/*.svg include resources/icons/scalable/*.svg
include resources/sounds/*.wav include resources/sounds/*.wav
include resources/niri/*.kdl
recursive-include src/vocalinux/resources * recursive-include src/vocalinux/resources *
+94
View File
@@ -0,0 +1,94 @@
# niri and Dank Material Shell
Dictation on the [niri](https://github.com/YaLTeR/niri) Wayland compositor,
including sessions running Dank Material Shell (DMS). There is no
compositor-specific code: activation reuses the standard external triggers
over D-Bus, and the tray is the standard StatusNotifier item.
## Prerequisites
1. Install Voculinux first: [INSTALL.md](INSTALL.md).
2. Open **Settings → Shortcuts** and turn on **External Activation (Desktop
Shortcut)**. The built-in key listener stops, and a running instance
answers these triggers on the session bus (`com.voculinux.Voculinux`):
```bash
voculinux --toggle # start if idle, stop if active
voculinux --start # start voice typing
voculinux --stop # stop voice typing
```
## Keybind: toggle once per press
Either paste the bind below into the `binds {}` section of
`~/.config/niri/config.kdl`, or copy the ready-made include file and
reference it (easier to keep across config edits):
```bash
mkdir -p ~/.config/niri
cp resources/niri/voculinux.kdl ~/.config/niri/voculinux.kdl
```
then near the top of `~/.config/niri/config.kdl`:
```kdl
include "~/.config/niri/voculinux.kdl"
```
The shipped bind:
```kdl
binds {
Mod+Shift+D repeat=false hotkey-overlay-title="Toggle Voculinux dictation" {
spawn "voculinux" "--toggle";
}
}
```
`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.
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
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.
## Hold-to-talk stays in the app
A compositor bind only sends the key press; it never sees the release, so
it can only toggle dictation. Push-to-talk (hold Right Alt, release to
stop) keeps working only through the built-in Voculinux shortcut. This
pack ships plain press binds only, so if you dictate with hold-to-talk,
leave the in-app shortcut in place for it and use the niri bind for toggle.
## Flatpak
`voculinux` is not on the host PATH from a sandbox, so bind the host D-Bus
call instead (same toggle, no extra sandbox spawned per press). The include
file carries a commented-out `gdbus` variant; background and the exact
command live under [Running without device
access](../packaging/flatpak/README.md#running-without-device-access).
## Tray under Dank Material Shell
Not yet observed on hardware: the StatusNotifier watcher bus name (or
names) DMS owns will be recorded with ticket #32, the hardware acceptance
run. The tray probe (`_check_status_notifier_watcher` in
`src/vocalinux/ui/tray_indicator.py`) currently accepts only
`org.kde.StatusNotifierWatcher`, and no probe code changed in this ticket.
The observation to record on #32, with a DMS session running:
```bash
busctl --user list | grep -i -E 'watcher|statusnotifier'
```
If DMS owns only `org.freedesktop.StatusNotifierWatcher`, the probe is
widened to accept both watcher names so no false missing-tray warning
appears, with behaviour tests covering both names. If
`org.kde.StatusNotifierWatcher` is present, the current probe stands and
the recorded output closes the question.
+1
View File
@@ -8,6 +8,7 @@ User and maintainer documentation for Voculinux.
| [INSTALL_MANUAL.md](INSTALL_MANUAL.md) | Users | Manual install, system packages, PyPI/pipx | | [INSTALL_MANUAL.md](INSTALL_MANUAL.md) | Users | Manual install, system packages, PyPI/pipx |
| [TROUBLESHOOTING.md](TROUBLESHOOTING.md) | Users | Common install and runtime failures | | [TROUBLESHOOTING.md](TROUBLESHOOTING.md) | Users | Common install and runtime failures |
| [USER_GUIDE.md](USER_GUIDE.md) | Users | Dictation, engines, models, shortcuts, tips | | [USER_GUIDE.md](USER_GUIDE.md) | Users | Dictation, engines, models, shortcuts, tips |
| [NIRI.md](NIRI.md) | Users | niri keybind pack and Dank Material Shell notes |
| [UPDATE.md](UPDATE.md) | Users | How to upgrade; release notes by version | | [UPDATE.md](UPDATE.md) | Users | How to upgrade; release notes by version |
| [DISTRO_COMPATIBILITY.md](DISTRO_COMPATIBILITY.md) | Users | Distro matrix, session caveats | | [DISTRO_COMPATIBILITY.md](DISTRO_COMPATIBILITY.md) | Users | Distro matrix, session caveats |
| [HTTP_REMOTE.md](HTTP_REMOTE.md) | Power users | Remote HTTP transcription | | [HTTP_REMOTE.md](HTTP_REMOTE.md) | Power users | Remote HTTP transcription |
+3 -1
View File
@@ -165,7 +165,9 @@ support push-to-talk because it never sees the release. On KDE Plasma:
Now your chosen key combination toggles dictation, handled by the compositor Now your chosen key combination toggles dictation, handled by the compositor
rather than by Voculinux reading the keyboard directly. This works the same way rather than by Voculinux reading the keyboard directly. This works the same way
on other compositors that support binding a key to a command (e.g. GNOME custom on other compositors that support binding a key to a command (e.g. GNOME custom
shortcuts, Sway/Hyprland `bindsym`/`bind`). shortcuts, Sway/Hyprland `bindsym`/`bind`). On niri, including under Dank
Material Shell, use the ready-made pack: [NIRI.md](NIRI.md) (optional include
file, once-per-press bind, tray note).
As an advanced alternative, you can set the same option in As an advanced alternative, you can set the same option in
`~/.config/voculinux/config.json` under `shortcuts`: `~/.config/voculinux/config.json` under `shortcuts`:
+36
View File
@@ -0,0 +1,36 @@
// Voculinux dictation toggle for the niri Wayland compositor.
//
// Copy this file to ~/.config/niri/voculinux.kdl, then add this line near
// the top of ~/.config/niri/config.kdl:
//
// include "~/.config/niri/voculinux.kdl"
//
// Prerequisite inside Voculinux: open Settings → Shortcuts and turn on
// "External Activation (Desktop Shortcut)". A running instance then answers
// the triggers below over D-Bus; the built-in key listener stays off.
//
// One key is either a compositor bind or an in-app Voculinux shortcut,
// never both: do not keep this key configured under Settings → Shortcuts.
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" {
spawn "voculinux" "--toggle";
}
// Optional explicit start/stop binds. Same rule: keep repeat off.
// 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" {
// 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" {
// spawn "gdbus" "call" "--session" "--dest" "com.voculinux.Voculinux" "--object-path" "/com/voculinux/Voculinux" "--method" "com.voculinux.Voculinux.Toggle";
// }
}
+88
View File
@@ -0,0 +1,88 @@
"""Ticket #22: niri + Dank Material Shell integration pack (docs/keybind seam).
A niri user following only the new documentation gets a working toggle bind
(fires once per press) with no compositor-internal APIs. The tray-watcher
question stays code-untouched until hardware observation (ticket #32);
the probe assertion below pins the current watcher name so a silent
widening fails loudly. When #32 records DMS evidence, update the doc and
this assertion together.
"""
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
NIRI_DOC = REPO_ROOT / "docs" / "NIRI.md"
NIRI_INCLUDE = REPO_ROOT / "resources" / "niri" / "voculinux.kdl"
DOCS_INDEX = REPO_ROOT / "docs" / "README.md"
USER_GUIDE = REPO_ROOT / "docs" / "USER_GUIDE.md"
TRAY_INDICATOR = REPO_ROOT / "src" / "vocalinux" / "ui" / "tray_indicator.py"
TOGGLE_SPAWN = 'spawn "voculinux" "--toggle"'
CURRENT_WATCHER = "org.kde.StatusNotifierWatcher"
def _read(path: Path) -> str:
assert path.is_file(), f"{path.relative_to(REPO_ROOT)} is missing"
return path.read_text(encoding="utf-8")
def _active_lines(text: str) -> list[str]:
"""Non-blank lines that are not niri // comments."""
return [
line.strip()
for line in text.splitlines()
if line.strip() and not line.strip().startswith("//")
]
def test_include_file_spawns_renamed_toggle_trigger() -> None:
"""The shipped bind drives the renamed CLI trigger with split argv."""
assert any(TOGGLE_SPAWN in line for line in _active_lines(_read(NIRI_INCLUDE)))
def test_include_bind_is_non_repeating() -> None:
"""Key repeat must not re-fire the toggle while held."""
assert "repeat=false" in _read(NIRI_INCLUDE)
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()
def test_doc_shows_include_and_once_per_press() -> None:
"""Doc presents the optional include file and the once-per-press promise."""
doc = _read(NIRI_DOC).lower()
assert "include" in doc
assert "repeat=false" in doc
assert "once per press" in doc
def test_doc_warns_compositor_bind_xor_in_app_shortcut() -> None:
"""One key is a compositor bind or an in-app shortcut, never both."""
doc = _read(NIRI_DOC).lower()
assert "never both" in doc
assert "external activation" in doc
def test_doc_keeps_hold_to_talk_in_app() -> None:
"""A press-only bind cannot do push-to-talk; the release never arrives."""
doc = _read(NIRI_DOC).lower()
assert "push-to-talk" in doc or "hold-to-talk" in doc
assert "release" in doc
def test_doc_records_tray_watcher_gap_without_probe_change() -> None:
"""DMS observation is deferred to #32; the probe stays as-is meanwhile."""
doc = _read(NIRI_DOC)
assert CURRENT_WATCHER in doc
assert "#32" in doc
probe = _read(TRAY_INDICATOR)
assert CURRENT_WATCHER in probe
assert "org.freedesktop.StatusNotifierWatcher" not in probe
def test_doc_linked_from_index_and_user_guide() -> None:
"""The niri page is reachable from the docs index and the user guide."""
assert "NIRI.md" in _read(DOCS_INDEX)
assert "NIRI.md" in _read(USER_GUIDE)