What do niri and Dank Material Shell expose for global keybinds that spawn commands, StatusNotifier tray hosting, and bar widgets, and which of those should Voculinux target without forking either project?
Part of #1
## Question
What do niri and Dank Material Shell expose for global keybinds that spawn commands, StatusNotifier tray hosting, and bar widgets, and which of those should Voculinux target without forking either project?
niri and Dank Material Shell integration APIs — research findings
Ticket: "niri and Dank integration APIs" (map: Voculinux port)
Question: What do niri and Dank Material Shell (DMS) expose for spawn
keybinds, StatusNotifier tray hosting, and bar widgets, and which should
Voculinux target without forking?
Researched 2026-11-05 against primary sources (official docs, upstream
repos, this repo's source). No repo files were edited.
Naming note: upstream DMS has rebranded. danklinux.com now redirects to duxlabs.dev, the shell is "Dux Modular Shell (dms)" 2.x, and the dms
CLI name is retained [S8][S9]. "DMS" below means that codebase.
Verdict
Target neither project's internals and fork neither. Target three
already-standard seams, in this order:
Activation: niri's own keybind config spawning the existing
Vocalinux CLI/D-Bus triggers (vocalinux --toggle/--start/--stop).
This is exactly how DMS itself is driven on niri
(spawn "dms" "ipc" "call" ...) [S10]. Ship a documented KDL snippet;
add no compositor code.
Tray: the freedesktop StatusNotifier standard Vocalinux already
speaks via Ayatana AppIndicator. niri hosts nothing; DMS (via
Quickshell) is one conforming host among several [S8][S12]. Nothing
DMS-specific is needed, subject to one watcher-name check below.
Bar widget: optional, later, as a separate DMS QML plugin — not in
core, and only after Vocalinux's D-Bus interface gains a state signal
(today it is write-only: Toggle/Start/Stop methods, no signals or
properties). A widget built before that is a blind button duplicating
the tray icon.
Push-to-hold dictation and text injection stay on the existing
compositor-agnostic paths (evdev, wtype/ydotool). niri's native
press/release binds would eventually allow compositor-driven
push-to-talk, but upstream marks them "Since: next release" [S2] and DMS
likewise gates them behind "Next release (niri main)" vs 26.04 [S10] —
do not depend on them yet.
1. Spawn keybinds
niri (primary)
Binds are user config, KDL: binds { Mod+T { spawn "alacritty"; } }. spawn takes an argv vector (no shell); spawn-sh (since 25.08) runs
one string through sh; spawn-at-startup is the autostart form [S1].
Binds repeat by default; repeat=false and cooldown-ms= rate-limit
them. Spawn binds accept allow-when-locked=true. Bound keys are
consumed by the compositor and not passed to applications by default
[S1].
Press and release binds exist in the wiki but are labelled
"Since: next release", explicitly motivated by push-to-talk, with a
guarantee that a fired press action always gets its release action
[S1][S2]. The implementing PR describes the same push-to-talk use
case [S3]. Until a numbered release ships them, stable niri can only
express toggle activation, not hold.
Every bindable action is also invocable at runtime: niri msg action spawn -- ..., and the IPC socket at $NIRI_SOCKET
accepts one JSON request per line, with a stated backwards-compat
policy for JSON output (fields/variants not renamed or removed;
handle unknown additions) [S4]. Vocalinux does not need this — the
compositor spawns us, not vice versa — except that focused_window.py already shells out to niri msg -j focused-window [L3].
Consequence for Voculinux: the integration is a five-line user snippet,
e.g. Mod+R { spawn "vocalinux" "--toggle"; }, because the receiving
end already exists: --toggle/--start/--stop forward to com.vocalinux.VocalinuxToggle/Start/Stop on the session bus and
exit [L5][L6]. DMS's niri integration uses the same shape plus include optional=true "dms/binds.kdl" fragment files [S10] — a pattern
Voculinux can copy for an optional include file instead of asking users
to hand-edit binds {}.
DMS
DMS exposes no global-keybind registry of its own. Its "Keybinds & IPC"
surface is dms ipc call <target> <function> [args] — a CLI meant to be
spawned by the compositor's binds [S9][S10]. There is no DMS API for a
third-party app to register a hotkey; the compositor owns keys on this
stack. Targeting DMS for activation would add a middleman that itself
just shells out.
Current Vocalinux assumption (conflict to document)
Hold-to-talk today reads raw evdev: EvdevKeyboardBackend opens /dev/input/event*, optionally EVIOCGRABs each keyboard and
re-emits unconsumed events through a /dev/uinput clone [L1]. This is
compositor-agnostic and works under niri, but requires input-group
(keylogger-grade) access [L2]. If a user also binds the same key in
niri, both fire: evdev sees the device event before the compositor, and
niri's "bound keys are not passed to apps" [S1] does not stop the evdev
reader. The niri snippet docs must say: compositor bind or in-app
shortcut for a given key, never both.
2. StatusNotifier tray hosting
niri: none. niri is a compositor only — no bar, no tray, no
StatusNotifierWatcher. Users bring a shell/bar; that is the entire
reason DMS/Waybar pair with it [S10][S11].
DMS: yes. DMS lists "System Tray" as a general feature; its
architecture is a Go backend (dms) spawning a Quickshell (QML) UI,
both integrating with D-Bus independently [S8]. Quickshell implements
the StatusNotifier host side (watcher + item proxies + DBusMenu) per
the freedesktop StatusNotifierItem spec [S12][S13].
Vocalinux: already a client.tray_indicator.py loads AyatanaAppIndicatorGlib → AyatanaAppIndicator3 → AppIndicator3
and registers an APPLICATION_STATUS indicator, which is a
StatusNotifierItem on the session bus [L4]. Any conforming watcher —
DMS, Waybar's tray module, Plasma — hosts it. No DMS-specific tray
work exists to do.
One unverified risk: Vocalinux probes for a watcher by looking for the
exact bus name org.kde.StatusNotifierWatcher
(tray_indicator.py:702, main.py:341) and pops a "tray icon may not
appear" dialog if absent [L4]. If Quickshell's watcher owns only org.freedesktop.StatusNotifierWatcher, Vocalinux would false-positive
that dialog on DMS despite a working tray. Verify on a real DMS box
(busctl --user list | grep StatusNotifierWatcher) before changing
anything; if confirmed, accept both names. This was not testable from
docs alone and is marked unconfirmed.
3. Bar widgets
niri: no widget host. Its IPC/event-stream is compositor state
(windows, workspaces, outputs) "useful for implementing various bars
and indicators" [S4] — data for bar authors, not an extension API in niri. There is nothing Voculinux-shaped to plug into.
DMS: a real plugin system. Plugins are QML directories discovered
from $CONFIGPATH/dms/plugins/, each with a plugin.json manifest
(id, type: widget/daemon/launcher/desktop/dash/dashCard/
composite, capabilities e.g. ["dankbar-widget"], component, requires_dms) [S7]. A bar widget wraps PluginComponent and
provides horizontalBarPill / verticalBarPill components plus
optional popoutContent; clicks can run external commands via
Quickshell Process / Quickshell.execDetached [S7]. So a DMS pill
that shells out to vocalinux --toggle is a small, supported,
no-fork extension — distributed through DMS's plugin registry [S7].
Blocker is on our side, not theirs. A useful pill must show
idle/listening/processing state. com.vocalinux.Vocalinux exposes
only the three methods above — the introspection XML has no signals
and no properties [L6]. A plugin today could only fire-and-forget.
Sequencing: (1) add a state signal/property to dbus_service.py
(compositor-agnostic; also benefits Waybar custom/ modules and
scripts), (2) then optionally ship the QML plugin as a separate
artifact. Do not invert the order.
Churn risk. DMS just rebranded, moved docs domains, and publishes
versioned docs (1.6 / 2.0 / 2.1 / 2.2) with config paths shifting
from ~/.config/DankMaterialShell/plugins/ to $CONFIGPATH/dms/plugins/ [S7][S8]. Pin requires_dms, keep the
plugin thin (icon + click + D-Bus state), and keep it out of the
core package so DMS API drift cannot break the app.
Local assumptions audit (text_injection, keyboard_backends)
text_injector.py already lists "niri" in _IBUS_UNBRIDGED_COMPOSITORS (line 310): IBus commit_text is
treated as unbridged on niri unless the ibus-wayland
input-method-v2 bridge process is running, falling back to the
wtype probe → ydotool (uinput) path [L7]. Backend pinning already
allows wtype/ydotool explicitly [L7]. No niri change needed.
focused_window.py already identifies the focused window on niri
via niri msg -j focused-window (line 441), beside Hyprland/Sway
equivalents [L3]. Terminal detection for Ctrl+Shift+V paste works
by the same path.
remote_desktop_portal.py uses org.freedesktop.portal.RemoteDesktop (NotifyKeyboardKeysym)
[L8]. niri ships niri-portals.conf preferring gnome;gtk
backends [S5]. Whether xdg-desktop-portal-gnome's RemoteDesktop
works against niri (vs Mutter) is unproven from primary docs —
treat portal as unsupported on niri until tested on hardware;
the code already probes and degrades [L8]. Do not pin portal in
niri docs.
keyboard_backends/: pynput is X11-only in practice; evdev is
the Wayland path and is compositor-blind (kernel devices) [L1][L2].
Default gesture is hold Right Alt, push-to-talk
(base.py:95,103) [L9] — the one mode stable-niri binds cannot yet
replace (see §1). XDG GlobalShortcuts portal is not used anywhere
in the codebase and has no niri-side implementation documented;
not a viable target today.
Recommended sequence
Docs + config only: niri page/snippet (spawn "vocalinux" "--toggle", repeat=false, optional include file), with the
evdev-double-trigger warning and "hold mode stays in-app" note.
Hardware verification on niri + DMS: tray icon/menu render,
watcher bus name (§2), wtype vs ydotool selection, toggle-bind
latency, portal probe result. Fix the watcher-name check only if
step 2 proves it wrong.
Add D-Bus state signal to dbus_service.py (agnostic; unblocks
Waybar modules too).
When a numbered niri release ships press/release binds [S2],
document compositor push-to-talk: press { spawn "vocalinux" "--start"; } / release { spawn "vocalinux" "--stop"; } — and re-evaluate whether
evdev remains the default on niri. Until then, do not document it
as supported.
[L6] src/vocalinux/dbus_service.py:24-38,175-198
(com.vocalinux.Vocalinux Toggle/Start/Stop; methods only, no
signals/properties — verified by grep, no matches).
# niri and Dank Material Shell integration APIs — research findings
Ticket: "niri and Dank integration APIs" (map: Voculinux port)
Question: What do niri and Dank Material Shell (DMS) expose for spawn
keybinds, StatusNotifier tray hosting, and bar widgets, and which should
Voculinux target without forking?
Researched 2026-11-05 against primary sources (official docs, upstream
repos, this repo's source). No repo files were edited.
Naming note: upstream DMS has rebranded. `danklinux.com` now redirects to
`duxlabs.dev`, the shell is "Dux Modular Shell (dms)" 2.x, and the `dms`
CLI name is retained [S8][S9]. "DMS" below means that codebase.
## Verdict
Target neither project's internals and fork neither. Target three
already-standard seams, in this order:
1. **Activation: niri's own keybind config spawning the existing
Vocalinux CLI/D-Bus triggers** (`vocalinux --toggle/--start/--stop`).
This is exactly how DMS itself is driven on niri
(`spawn "dms" "ipc" "call" ...`) [S10]. Ship a documented KDL snippet;
add no compositor code.
2. **Tray: the freedesktop StatusNotifier standard Vocalinux already
speaks** via Ayatana AppIndicator. niri hosts nothing; DMS (via
Quickshell) is one conforming host among several [S8][S12]. Nothing
DMS-specific is needed, subject to one watcher-name check below.
3. **Bar widget: optional, later, as a separate DMS QML plugin** — not in
core, and only after Vocalinux's D-Bus interface gains a state signal
(today it is write-only: Toggle/Start/Stop methods, no signals or
properties). A widget built before that is a blind button duplicating
the tray icon.
Push-to-hold dictation and text injection stay on the existing
compositor-agnostic paths (evdev, wtype/ydotool). niri's native
press/release binds would eventually allow compositor-driven
push-to-talk, but upstream marks them "Since: next release" [S2] and DMS
likewise gates them behind "Next release (niri main)" vs 26.04 [S10] —
do not depend on them yet.
## 1. Spawn keybinds
### niri (primary)
- Binds are user config, KDL: `binds { Mod+T { spawn "alacritty"; } }`.
`spawn` takes an argv vector (no shell); `spawn-sh` (since 25.08) runs
one string through `sh`; `spawn-at-startup` is the autostart form [S1].
- Binds repeat by default; `repeat=false` and `cooldown-ms=` rate-limit
them. Spawn binds accept `allow-when-locked=true`. Bound keys are
consumed by the compositor and not passed to applications by default
[S1].
- Press **and release** binds exist in the wiki but are labelled
"Since: next release", explicitly motivated by push-to-talk, with a
guarantee that a fired press action always gets its release action
[S1][S2]. The implementing PR describes the same push-to-talk use
case [S3]. Until a numbered release ships them, stable niri can only
express *toggle* activation, not hold.
- Every bindable action is also invocable at runtime:
`niri msg action spawn -- ...`, and the IPC socket at `$NIRI_SOCKET`
accepts one JSON request per line, with a stated backwards-compat
policy for JSON output (fields/variants not renamed or removed;
handle unknown additions) [S4]. Vocalinux does not need this — the
compositor spawns *us*, not vice versa — except that
`focused_window.py` already shells out to
`niri msg -j focused-window` [L3].
Consequence for Voculinux: the integration is a five-line user snippet,
e.g. `Mod+R { spawn "vocalinux" "--toggle"; }`, because the receiving
end already exists: `--toggle/--start/--stop` forward to
`com.vocalinux.Vocalinux` `Toggle/Start/Stop` on the session bus and
exit [L5][L6]. DMS's niri integration uses the same shape plus
`include optional=true "dms/binds.kdl"` fragment files [S10] — a pattern
Voculinux can copy for an optional include file instead of asking users
to hand-edit `binds {}`.
### DMS
DMS exposes no global-keybind registry of its own. Its "Keybinds & IPC"
surface is `dms ipc call <target> <function> [args]` — a CLI meant to be
spawned *by* the compositor's binds [S9][S10]. There is no DMS API for a
third-party app to register a hotkey; the compositor owns keys on this
stack. Targeting DMS for activation would add a middleman that itself
just shells out.
### Current Vocalinux assumption (conflict to document)
Hold-to-talk today reads raw evdev: `EvdevKeyboardBackend` opens
`/dev/input/event*`, optionally `EVIOCGRAB`s each keyboard and
re-emits unconsumed events through a `/dev/uinput` clone [L1]. This is
compositor-agnostic and works under niri, but requires `input`-group
(keylogger-grade) access [L2]. If a user *also* binds the same key in
niri, both fire: evdev sees the device event before the compositor, and
niri's "bound keys are not passed to apps" [S1] does not stop the evdev
reader. The niri snippet docs must say: compositor bind **or** in-app
shortcut for a given key, never both.
## 2. StatusNotifier tray hosting
- **niri: none.** niri is a compositor only — no bar, no tray, no
StatusNotifierWatcher. Users bring a shell/bar; that is the entire
reason DMS/Waybar pair with it [S10][S11].
- **DMS: yes.** DMS lists "System Tray" as a general feature; its
architecture is a Go backend (`dms`) spawning a Quickshell (QML) UI,
both integrating with D-Bus independently [S8]. Quickshell implements
the StatusNotifier host side (watcher + item proxies + DBusMenu) per
the freedesktop StatusNotifierItem spec [S12][S13].
- **Vocalinux: already a client.** `tray_indicator.py` loads
`AyatanaAppIndicatorGlib` → `AyatanaAppIndicator3` → `AppIndicator3`
and registers an `APPLICATION_STATUS` indicator, which is a
StatusNotifierItem on the session bus [L4]. Any conforming watcher —
DMS, Waybar's tray module, Plasma — hosts it. No DMS-specific tray
work exists to do.
One unverified risk: Vocalinux probes for a watcher by looking for the
exact bus name `org.kde.StatusNotifierWatcher`
(`tray_indicator.py:702`, `main.py:341`) and pops a "tray icon may not
appear" dialog if absent [L4]. If Quickshell's watcher owns only
`org.freedesktop.StatusNotifierWatcher`, Vocalinux would false-positive
that dialog on DMS despite a working tray. Verify on a real DMS box
(`busctl --user list | grep StatusNotifierWatcher`) before changing
anything; if confirmed, accept both names. This was not testable from
docs alone and is marked unconfirmed.
## 3. Bar widgets
- **niri: no widget host.** Its IPC/event-stream is compositor state
(windows, workspaces, outputs) "useful for implementing various bars
and indicators" [S4] — data *for* bar authors, not an extension API
*in* niri. There is nothing Voculinux-shaped to plug into.
- **DMS: a real plugin system.** Plugins are QML directories discovered
from `$CONFIGPATH/dms/plugins/`, each with a `plugin.json` manifest
(`id`, `type`: widget/daemon/launcher/desktop/dash/dashCard/
composite, `capabilities` e.g. `["dankbar-widget"]`, `component`,
`requires_dms`) [S7]. A bar widget wraps `PluginComponent` and
provides `horizontalBarPill` / `verticalBarPill` components plus
optional `popoutContent`; clicks can run external commands via
Quickshell `Process` / `Quickshell.execDetached` [S7]. So a DMS pill
that shells out to `vocalinux --toggle` is a small, supported,
no-fork extension — distributed through DMS's plugin registry [S7].
- **Blocker is on our side, not theirs.** A useful pill must show
idle/listening/processing state. `com.vocalinux.Vocalinux` exposes
only the three methods above — the introspection XML has no signals
and no properties [L6]. A plugin today could only fire-and-forget.
Sequencing: (1) add a state signal/property to `dbus_service.py`
(compositor-agnostic; also benefits Waybar `custom/` modules and
scripts), (2) then optionally ship the QML plugin as a separate
artifact. Do not invert the order.
- **Churn risk.** DMS just rebranded, moved docs domains, and publishes
versioned docs (1.6 / 2.0 / 2.1 / 2.2) with config paths shifting
from `~/.config/DankMaterialShell/plugins/` to
`$CONFIGPATH/dms/plugins/` [S7][S8]. Pin `requires_dms`, keep the
plugin thin (icon + click + D-Bus state), and keep it out of the
core package so DMS API drift cannot break the app.
## Local assumptions audit (text_injection, keyboard_backends)
- `text_injector.py` already lists `"niri"` in
`_IBUS_UNBRIDGED_COMPOSITORS` (line 310): IBus `commit_text` is
treated as unbridged on niri unless the `ibus-wayland`
input-method-v2 bridge process is running, falling back to the
wtype probe → ydotool (uinput) path [L7]. Backend pinning already
allows `wtype`/`ydotool` explicitly [L7]. No niri change needed.
- `focused_window.py` already identifies the focused window on niri
via `niri msg -j focused-window` (line 441), beside Hyprland/Sway
equivalents [L3]. Terminal detection for Ctrl+Shift+V paste works
by the same path.
- `remote_desktop_portal.py` uses
`org.freedesktop.portal.RemoteDesktop` (`NotifyKeyboardKeysym`)
[L8]. niri ships `niri-portals.conf` preferring `gnome;gtk`
backends [S5]. Whether xdg-desktop-portal-gnome's RemoteDesktop
works against niri (vs Mutter) is unproven from primary docs —
treat `portal` as unsupported on niri until tested on hardware;
the code already probes and degrades [L8]. Do not pin `portal` in
niri docs.
- `keyboard_backends/`: `pynput` is X11-only in practice; `evdev` is
the Wayland path and is compositor-blind (kernel devices) [L1][L2].
Default gesture is hold Right Alt, push-to-talk
(`base.py:95,103`) [L9] — the one mode stable-niri binds cannot yet
replace (see §1). XDG GlobalShortcuts portal is not used anywhere
in the codebase and has no niri-side implementation documented;
not a viable target today.
## Recommended sequence
1. Docs + config only: niri page/snippet (`spawn "vocalinux"
"--toggle"`, `repeat=false`, optional include file), with the
evdev-double-trigger warning and "hold mode stays in-app" note.
2. Hardware verification on niri + DMS: tray icon/menu render,
watcher bus name (§2), wtype vs ydotool selection, toggle-bind
latency, portal probe result. Fix the watcher-name check only if
step 2 proves it wrong.
3. Add D-Bus state signal to `dbus_service.py` (agnostic; unblocks
Waybar modules too).
4. Optional DMS QML plugin (separate repo/artifact, `requires_dms`
pinned).
5. When a numbered niri release ships press/release binds [S2],
document compositor push-to-talk:
`press { spawn "vocalinux" "--start"; }` /
`release { spawn "vocalinux" "--stop"; }` — and re-evaluate whether
evdev remains the default on niri. Until then, do not document it
as supported.
## Sources
Primary (upstream):
- [S1] niri wiki, Configuration: Key Bindings — binds{}, spawn,
spawn-sh, repeat/cooldown, allow-when-locked, press/release,
key consumption.
https://raw.githubusercontent.com/wiki/niri-wm/niri/Configuration:-Key-Bindings.md
- [S2] Same page, "Press and Release Bindings — Since: next release"
(push-to-talk guarantee).
https://github.com/niri-wm/niri/wiki/Configuration:-Key-Bindings
- [S3] niri PR #2456, "Implement release keybinds and modifier-only
binds" (push-to-talk motivation).
https://github.com/niri-wm/niri/pull/2456
- [S4] niri wiki, IPC — `$NIRI_SOCKET`, JSON requests, event-stream,
JSON backwards-compatibility policy.
https://raw.githubusercontent.com/wiki/niri-wm/niri/IPC.md
- [S5] niri `resources/niri-portals.conf` (`default=gnome;gtk;`).
https://raw.githubusercontent.com/niri-wm/niri/main/resources/niri-portals.conf
- [S7] DMS plugin system README — discovery path, plugin.json,
PluginComponent pills/popouts, Process/execDetached.
https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/refs/heads/master/quickshell/PLUGINS/README.md
- [S8] Dux/DMS Overview & Architecture — "System Tray" feature,
Quickshell+Go client-server architecture.
https://duxlabs.dev/docs/dms/overview
- [S9] Dux/DMS Keybinds & IPC — `dms ipc call <target> <function>`.
https://duxlabs.dev/docs/dms/keybinds-ipc
- [S10] Dux/DMS Compositor Setup — niri includes
(`dms/binds.kdl`, version-gated `binds-next.kdl`: 26.04 vs
next-release), `spawn-at-startup "dms" "run"`,
`spawn "dms" "ipc" "call" ...` bind examples.
https://duxlabs.dev/docs/dms/compositors
- [S12] Quickshell system-tray implementation notes
(StatusNotifierItem host, DBusMenu).
https://deepwiki.com/quickshell-mirror/quickshell/7.3-system-tray-(status-notifier)
- [S13] freedesktop StatusNotifierItem specification.
https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/StatusNotifierItem/
Secondary (context only):
- [S11] "bars, notification daemons, and app launchers are not
included. Niri is strictly the compositor."
https://itsfoss.com/niri-window-manager/
Local (this repo, read-only inspection):
- [L1] `src/vocalinux/ui/keyboard_backends/evdev_backend.py:2-19`
(evdev / EVIOCGRAB / uinput clone design).
- [L2] `src/vocalinux/ui/keyboard_shortcuts.py:110-125`
(Wayland = python-evdev + `input` group guidance).
- [L3] `src/vocalinux/text_injection/focused_window.py:435-443`
(`niri msg -j focused-window`).
- [L4] `src/vocalinux/ui/tray_indicator.py:22-53,589-617,702`
(Ayatana AppIndicator chain; `org.kde.StatusNotifierWatcher`
probe); `src/vocalinux/main.py:341`.
- [L5] `src/vocalinux/main.py:102-117,138`
(`--toggle/--start/--stop` trigger flags).
- [L6] `src/vocalinux/dbus_service.py:24-38,175-198`
(`com.vocalinux.Vocalinux` Toggle/Start/Stop; methods only, no
signals/properties — verified by grep, no matches).
- [L7] `src/vocalinux/text_injection/text_injector.py:298-318,
395-455` (niri in `_IBUS_UNBRIDGED_COMPOSITORS`; `ibus-wayland`
bridge exception; selectable backends).
- [L8] `src/vocalinux/text_injection/remote_desktop_portal.py:2-6,
67-71,235-259` (RemoteDesktop portal client + probe).
- [L9] `src/vocalinux/ui/keyboard_backends/base.py:93-103`
(default hold-Right-Alt, push_to_talk).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Part of #1
Question
What do niri and Dank Material Shell expose for global keybinds that spawn commands, StatusNotifier tray hosting, and bar widgets, and which of those should Voculinux target without forking either project?
niri and Dank Material Shell integration APIs — research findings
Ticket: "niri and Dank integration APIs" (map: Voculinux port)
Question: What do niri and Dank Material Shell (DMS) expose for spawn
keybinds, StatusNotifier tray hosting, and bar widgets, and which should
Voculinux target without forking?
Researched 2026-11-05 against primary sources (official docs, upstream
repos, this repo's source). No repo files were edited.
Naming note: upstream DMS has rebranded.
danklinux.comnow redirects toduxlabs.dev, the shell is "Dux Modular Shell (dms)" 2.x, and thedmsCLI name is retained [S8][S9]. "DMS" below means that codebase.
Verdict
Target neither project's internals and fork neither. Target three
already-standard seams, in this order:
Vocalinux CLI/D-Bus triggers (
vocalinux --toggle/--start/--stop).This is exactly how DMS itself is driven on niri
(
spawn "dms" "ipc" "call" ...) [S10]. Ship a documented KDL snippet;add no compositor code.
speaks via Ayatana AppIndicator. niri hosts nothing; DMS (via
Quickshell) is one conforming host among several [S8][S12]. Nothing
DMS-specific is needed, subject to one watcher-name check below.
core, and only after Vocalinux's D-Bus interface gains a state signal
(today it is write-only: Toggle/Start/Stop methods, no signals or
properties). A widget built before that is a blind button duplicating
the tray icon.
Push-to-hold dictation and text injection stay on the existing
compositor-agnostic paths (evdev, wtype/ydotool). niri's native
press/release binds would eventually allow compositor-driven
push-to-talk, but upstream marks them "Since: next release" [S2] and DMS
likewise gates them behind "Next release (niri main)" vs 26.04 [S10] —
do not depend on them yet.
1. Spawn keybinds
niri (primary)
binds { Mod+T { spawn "alacritty"; } }.spawntakes an argv vector (no shell);spawn-sh(since 25.08) runsone string through
sh;spawn-at-startupis the autostart form [S1].repeat=falseandcooldown-ms=rate-limitthem. Spawn binds accept
allow-when-locked=true. Bound keys areconsumed by the compositor and not passed to applications by default
[S1].
"Since: next release", explicitly motivated by push-to-talk, with a
guarantee that a fired press action always gets its release action
[S1][S2]. The implementing PR describes the same push-to-talk use
case [S3]. Until a numbered release ships them, stable niri can only
express toggle activation, not hold.
niri msg action spawn -- ..., and the IPC socket at$NIRI_SOCKETaccepts one JSON request per line, with a stated backwards-compat
policy for JSON output (fields/variants not renamed or removed;
handle unknown additions) [S4]. Vocalinux does not need this — the
compositor spawns us, not vice versa — except that
focused_window.pyalready shells out toniri msg -j focused-window[L3].Consequence for Voculinux: the integration is a five-line user snippet,
e.g.
Mod+R { spawn "vocalinux" "--toggle"; }, because the receivingend already exists:
--toggle/--start/--stopforward tocom.vocalinux.VocalinuxToggle/Start/Stopon the session bus andexit [L5][L6]. DMS's niri integration uses the same shape plus
include optional=true "dms/binds.kdl"fragment files [S10] — a patternVoculinux can copy for an optional include file instead of asking users
to hand-edit
binds {}.DMS
DMS exposes no global-keybind registry of its own. Its "Keybinds & IPC"
surface is
dms ipc call <target> <function> [args]— a CLI meant to bespawned by the compositor's binds [S9][S10]. There is no DMS API for a
third-party app to register a hotkey; the compositor owns keys on this
stack. Targeting DMS for activation would add a middleman that itself
just shells out.
Current Vocalinux assumption (conflict to document)
Hold-to-talk today reads raw evdev:
EvdevKeyboardBackendopens/dev/input/event*, optionallyEVIOCGRABs each keyboard andre-emits unconsumed events through a
/dev/uinputclone [L1]. This iscompositor-agnostic and works under niri, but requires
input-group(keylogger-grade) access [L2]. If a user also binds the same key in
niri, both fire: evdev sees the device event before the compositor, and
niri's "bound keys are not passed to apps" [S1] does not stop the evdev
reader. The niri snippet docs must say: compositor bind or in-app
shortcut for a given key, never both.
2. StatusNotifier tray hosting
StatusNotifierWatcher. Users bring a shell/bar; that is the entire
reason DMS/Waybar pair with it [S10][S11].
architecture is a Go backend (
dms) spawning a Quickshell (QML) UI,both integrating with D-Bus independently [S8]. Quickshell implements
the StatusNotifier host side (watcher + item proxies + DBusMenu) per
the freedesktop StatusNotifierItem spec [S12][S13].
tray_indicator.pyloadsAyatanaAppIndicatorGlib→AyatanaAppIndicator3→AppIndicator3and registers an
APPLICATION_STATUSindicator, which is aStatusNotifierItem on the session bus [L4]. Any conforming watcher —
DMS, Waybar's tray module, Plasma — hosts it. No DMS-specific tray
work exists to do.
One unverified risk: Vocalinux probes for a watcher by looking for the
exact bus name
org.kde.StatusNotifierWatcher(
tray_indicator.py:702,main.py:341) and pops a "tray icon may notappear" dialog if absent [L4]. If Quickshell's watcher owns only
org.freedesktop.StatusNotifierWatcher, Vocalinux would false-positivethat dialog on DMS despite a working tray. Verify on a real DMS box
(
busctl --user list | grep StatusNotifierWatcher) before changinganything; if confirmed, accept both names. This was not testable from
docs alone and is marked unconfirmed.
3. Bar widgets
(windows, workspaces, outputs) "useful for implementing various bars
and indicators" [S4] — data for bar authors, not an extension API
in niri. There is nothing Voculinux-shaped to plug into.
from
$CONFIGPATH/dms/plugins/, each with aplugin.jsonmanifest(
id,type: widget/daemon/launcher/desktop/dash/dashCard/composite,
capabilitiese.g.["dankbar-widget"],component,requires_dms) [S7]. A bar widget wrapsPluginComponentandprovides
horizontalBarPill/verticalBarPillcomponents plusoptional
popoutContent; clicks can run external commands viaQuickshell
Process/Quickshell.execDetached[S7]. So a DMS pillthat shells out to
vocalinux --toggleis a small, supported,no-fork extension — distributed through DMS's plugin registry [S7].
idle/listening/processing state.
com.vocalinux.Vocalinuxexposesonly the three methods above — the introspection XML has no signals
and no properties [L6]. A plugin today could only fire-and-forget.
Sequencing: (1) add a state signal/property to
dbus_service.py(compositor-agnostic; also benefits Waybar
custom/modules andscripts), (2) then optionally ship the QML plugin as a separate
artifact. Do not invert the order.
versioned docs (1.6 / 2.0 / 2.1 / 2.2) with config paths shifting
from
~/.config/DankMaterialShell/plugins/to$CONFIGPATH/dms/plugins/[S7][S8]. Pinrequires_dms, keep theplugin thin (icon + click + D-Bus state), and keep it out of the
core package so DMS API drift cannot break the app.
Local assumptions audit (text_injection, keyboard_backends)
text_injector.pyalready lists"niri"in_IBUS_UNBRIDGED_COMPOSITORS(line 310): IBuscommit_textistreated as unbridged on niri unless the
ibus-waylandinput-method-v2 bridge process is running, falling back to the
wtype probe → ydotool (uinput) path [L7]. Backend pinning already
allows
wtype/ydotoolexplicitly [L7]. No niri change needed.focused_window.pyalready identifies the focused window on nirivia
niri msg -j focused-window(line 441), beside Hyprland/Swayequivalents [L3]. Terminal detection for Ctrl+Shift+V paste works
by the same path.
remote_desktop_portal.pyusesorg.freedesktop.portal.RemoteDesktop(NotifyKeyboardKeysym)[L8]. niri ships
niri-portals.confpreferringgnome;gtkbackends [S5]. Whether xdg-desktop-portal-gnome's RemoteDesktop
works against niri (vs Mutter) is unproven from primary docs —
treat
portalas unsupported on niri until tested on hardware;the code already probes and degrades [L8]. Do not pin
portalinniri docs.
keyboard_backends/:pynputis X11-only in practice;evdevisthe Wayland path and is compositor-blind (kernel devices) [L1][L2].
Default gesture is hold Right Alt, push-to-talk
(
base.py:95,103) [L9] — the one mode stable-niri binds cannot yetreplace (see §1). XDG GlobalShortcuts portal is not used anywhere
in the codebase and has no niri-side implementation documented;
not a viable target today.
Recommended sequence
spawn "vocalinux" "--toggle",repeat=false, optional include file), with theevdev-double-trigger warning and "hold mode stays in-app" note.
watcher bus name (§2), wtype vs ydotool selection, toggle-bind
latency, portal probe result. Fix the watcher-name check only if
step 2 proves it wrong.
dbus_service.py(agnostic; unblocksWaybar modules too).
requires_dmspinned).
document compositor push-to-talk:
press { spawn "vocalinux" "--start"; }/release { spawn "vocalinux" "--stop"; }— and re-evaluate whetherevdev remains the default on niri. Until then, do not document it
as supported.
Sources
Primary (upstream):
spawn-sh, repeat/cooldown, allow-when-locked, press/release,
key consumption.
https://raw.githubusercontent.com/wiki/niri-wm/niri/Configuration:-Key-Bindings.md
(push-to-talk guarantee).
https://github.com/niri-wm/niri/wiki/Configuration:-Key-Bindings
binds" (push-to-talk motivation).
https://github.com/niri-wm/niri/pull/2456
$NIRI_SOCKET, JSON requests, event-stream,JSON backwards-compatibility policy.
https://raw.githubusercontent.com/wiki/niri-wm/niri/IPC.md
resources/niri-portals.conf(default=gnome;gtk;).https://raw.githubusercontent.com/niri-wm/niri/main/resources/niri-portals.conf
PluginComponent pills/popouts, Process/execDetached.
https://raw.githubusercontent.com/AvengeMedia/DankMaterialShell/refs/heads/master/quickshell/PLUGINS/README.md
Quickshell+Go client-server architecture.
https://duxlabs.dev/docs/dms/overview
dms ipc call <target> <function>.https://duxlabs.dev/docs/dms/keybinds-ipc
(
dms/binds.kdl, version-gatedbinds-next.kdl: 26.04 vsnext-release),
spawn-at-startup "dms" "run",spawn "dms" "ipc" "call" ...bind examples.https://duxlabs.dev/docs/dms/compositors
(StatusNotifierItem host, DBusMenu).
https://deepwiki.com/quickshell-mirror/quickshell/7.3-system-tray-(status-notifier)
https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/StatusNotifierItem/
Secondary (context only):
included. Niri is strictly the compositor."
https://itsfoss.com/niri-window-manager/
Local (this repo, read-only inspection):
src/vocalinux/ui/keyboard_backends/evdev_backend.py:2-19(evdev / EVIOCGRAB / uinput clone design).
src/vocalinux/ui/keyboard_shortcuts.py:110-125(Wayland = python-evdev +
inputgroup guidance).src/vocalinux/text_injection/focused_window.py:435-443(
niri msg -j focused-window).src/vocalinux/ui/tray_indicator.py:22-53,589-617,702(Ayatana AppIndicator chain;
org.kde.StatusNotifierWatcherprobe);
src/vocalinux/main.py:341.src/vocalinux/main.py:102-117,138(
--toggle/--start/--stoptrigger flags).src/vocalinux/dbus_service.py:24-38,175-198(
com.vocalinux.VocalinuxToggle/Start/Stop; methods only, nosignals/properties — verified by grep, no matches).
src/vocalinux/text_injection/text_injector.py:298-318, 395-455(niri in_IBUS_UNBRIDGED_COMPOSITORS;ibus-waylandbridge exception; selectable backends).
src/vocalinux/text_injection/remote_desktop_portal.py:2-6, 67-71,235-259(RemoteDesktop portal client + probe).src/vocalinux/ui/keyboard_backends/base.py:93-103(default hold-Right-Alt, push_to_talk).