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:
@@ -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 *
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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
@@ -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`:
|
||||||
|
|||||||
@@ -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";
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
Reference in New Issue
Block a user