dms-plugins
August 27, 2026 · View on GitHub
dms-plugins
Plugins for DankMaterialShell.
Written for sitolamix, but tied to nothing —
they are in the official plugin registry, so dms plugins install is all it takes
on any distro, and NixOS gets them as flake packages.
The plugins
dankMenuOne key to every command — a hierarchical, searchable root menu in the shape of
Omarchy's |
mouthGuardWebcam mouth-closure tracker with alerts and session stats, running MediaPipe Face Mesh locally on the NPU. |
virtualKeyboardOn-screen keyboard that types real key events through |
barDropdownOne bar button that drops a panel of real bar widgets below the bar, where a crowded side section has no room to expand along it. |
| Plugin | Type | Needs | Docs |
|---|---|---|---|
dankmenu | daemon | nothing beyond DMS | README |
mouthguard | composite | a webcam, python3 + cv2 + openvino | README |
virtualkeyboard | composite | ydotool, with ydotoold running | README |
bardropdown | widget | nothing beyond DMS | README |
Install
These plugins are listed in the official DMS plugin registry, so the short answer on any distro is:
dms plugins install dankMenu
dms plugins install mouthGuard
dms plugins install virtualKeyboard
dms plugins install barDropdown
Then enable them in DMS under Mod+, → Plugins. dms plugins list shows
what's installed, and dms plugins update pulls newer versions later.
There is no build step for any of them — they are QML and JavaScript, and none
references a path above its own directory. Two need something from outside
DMS, though: virtualKeyboard types through ydotool, so that has to be
installed with its ydotoold daemon running (see
its README), and MouthGuard needs
a Python detector with cv2 and openvino, which on most distros is a package
install and on NixOS is a nix build (see
its README).
From a clone instead — for hacking on a plugin, or pinning it to this repo
DMS loads plugins from ~/.config/DankMaterialShell/plugins/<id>, where <id>
must match the id in the plugin's plugin.json — dankMenu, mouthGuard,
virtualKeyboard and barDropdown, camelCase, note the capital letters.
Symlink the clone and a git pull updates the plugin:
git clone https://github.com/sitolam/dms-plugins ~/src/dms-plugins
mkdir -p ~/.config/DankMaterialShell/plugins
ln -s ~/src/dms-plugins/plugins/dankmenu ~/.config/DankMaterialShell/plugins/dankMenu
ln -s ~/src/dms-plugins/plugins/mouthguard ~/.config/DankMaterialShell/plugins/mouthGuard
ln -s ~/src/dms-plugins/plugins/virtualkeyboard ~/.config/DankMaterialShell/plugins/virtualKeyboard
ln -s ~/src/dms-plugins/plugins/bardropdown ~/.config/DankMaterialShell/plugins/barDropdown
Or copy just the one directory, if you don't want the clone lying around:
git clone --depth 1 https://github.com/sitolam/dms-plugins /tmp/dms-plugins
cp -r /tmp/dms-plugins/plugins/dankmenu ~/.config/DankMaterialShell/plugins/dankMenu
NixOS, via home-manager
dms plugins install writes into ~/.config, which a declarative setup usually
doesn't want, so take the plugins as flake packages instead:
# flake.nix
inputs.dms-plugins.url = "github:sitolam/dms-plugins";
programs.dank-material-shell.plugins = {
dankMenu = {
enable = true;
src = inputs.dms-plugins.packages.${pkgs.system}.dankmenu;
};
mouthGuard = {
enable = true;
src = inputs.dms-plugins.packages.${pkgs.system}.mouthguard;
};
virtualKeyboard = {
enable = true;
src = inputs.dms-plugins.packages.${pkgs.system}.virtualkeyboard;
};
barDropdown = {
enable = true;
src = inputs.dms-plugins.packages.${pkgs.system}.bardropdown;
settings.targets = [ "ambientSound" "systemTray" "usbManager" ];
};
};
barDropdown is the one plugin here whose settings have to line up with the bar
itself: every id in targets must be a widget the bar knows, and none of them
may also appear in the bar's own widget list, or it will be rendered twice. Add
barDropdown to a bar section in its place.
With managePluginSettings = true, plugin_settings.json becomes a read-only
store symlink, so plugins must be enabled declaratively as above — the DMS
settings GUI cannot write to it.
MouthGuard additionally needs its detector binary: the ambient python3 on NixOS
has neither cv2 nor openvino, so link the flake's mouthguard-detector in as
the result the plugin looks for. See
MouthGuard's README.
Using dankMenu
Full documentation lives in its own README; this is the short version — enough to have it working in about two minutes.
1. Bind a key
dankMenu has no default keybind: it exposes IPC verbs, and your compositor decides
what opens it. toggle opens the root menu, or closes it if already open.
niri — ~/.config/niri/config.kdl:
binds {
Mod+Space { spawn "dms" "ipc" "call" "dankMenu" "toggle" "root"; }
}
Hyprland — ~/.config/hypr/hyprland.conf:
bind = SUPER, SPACE, exec, dms ipc call dankMenu toggle root
Sway — ~/.config/sway/config:
bindsym $mod+space exec dms ipc call dankMenu toggle root
river, Wayfire, anything else: bind the same shell command. It is a plain one-shot process, so any launcher, keybind daemon or script can trigger it.
2. Drive it
dms ipc call dankMenu toggle # open at the root, or close if open
dms ipc call dankMenu open system # open straight into a submenu
dms ipc call dankMenu open power-menu # aliases work too
dms ipc call dankMenu close
dms ipc call dankMenu refresh # re-read the menu file
Because open takes a route, any submenu can have its own keybind — a power
menu on Mod+Escape is just dms ipc call dankMenu open system.
3. Navigate
Typing searches the whole subtree below wherever you are, with a breadcrumb showing where each result lives:

| key | effect |
|---|---|
Enter | enter a submenu, or run a row and close |
Right | enter a submenu, when the cursor is at the end of the query |
Escape | up one level; at the root, close |
Left / Backspace | up one level, when the query is empty |
Up / Down | move the selection |
| any text | search this level's whole subtree |
Every vim binding is Ctrl-prefixed — the search field is always focused, so bare
hjkl has to reach it as text: Ctrl+J/Ctrl+K move, Ctrl+L/Ctrl+H go in and
out, Ctrl+D/Ctrl+U jump half a page, Ctrl+G closes outright.
4. Make it yours
The whole menu is one JSONC file. The plugin ships
menu.jsonc as a starting point; point the
menuPath setting at your own file to replace it entirely. Whichever file is
live is watched, so saving it updates the menu immediately — no shell restart.
Object keys are dotted ids, and the dots are the hierarchy:
{
// A submenu: no action, no target, no provider.
"system": {"icon":"power_settings_new","label":"System","aliases":["power-menu"]},
// An action: runs a shell command through `bash -lc`, then closes.
"system.lock": {"icon":"lock","label":"Lock","action":"loginctl lock-session"},
// A link: opens in your browser.
"learn.niri": {"icon":"grid_view","label":"Niri","target":"https://github.com/YaLTeR/niri/wiki"},
// A provider: contents generated at open time. "apps" is the only one so far.
"apps": {"icon":"apps","label":"Apps","provider":"apps"},
}
Rows can also reflect the machine rather than just describe it — when, checked
and disabled are shell snippets judged by exit status:

"trigger.toggle.night": {
"icon":"nightlight","label":"Night Mode",
"checked":"dms ipc call night status | grep -q enabled",
"action":"dms ipc call night toggle"
}
A fourth snippet, labelCmd, is judged by its output rather than its exit
status — the first line of stdout becomes the row's label, so a row can show a
live value instead of a fixed string.
Because menuPath is just a path, the file can be generated — see
dankMenu's README for the full
field reference, the condition semantics, and a worked Nix example.
Using MouthGuard
Once enabled it adds a bar pill whose icon and color track the detection state, and a Control Center tile. Left click opens the popout (live lip-gap chart, session counters, last 5 sessions), middle click starts or stops a session, right click mutes alerts without stopping tracking.
Everything else — the detector build, the two MediaPipe model files, NPU setup, every setting, and the detection semantics — is in MouthGuard's README.
Using virtualKeyboard
Install ydotool and start its ydotoold daemon first — the keyboard injects
raw Linux input events rather than Wayland text input, which is what lets it
type into terminals, games and password prompts like a physical keyboard
would. virtualKeyboard's README
covers the socket path, the usual reason it silently does nothing.
Like dankMenu it ships no keybind, only IPC verbs:
dms ipc call virtualKeyboard toggle
dms ipc call virtualKeyboard open
dms ipc call virtualKeyboard close
niri — ~/.config/niri/config.kdl:
binds {
Mod+Shift+K { spawn "dms" "ipc" "call" "virtualKeyboard" "toggle"; }
}
Hyprland — ~/.config/hypr/hyprland.conf:
bind = SUPER SHIFT, K, exec, dms ipc call virtualKeyboard toggle
There is an optional DankBar pill too — a keyboard icon that lights up while the keyboard is open and toggles it on click, which on a touchscreen is the only way in, since a keybind needs a keyboard to press.
It opens docked along the bottom of the screen, overlaying whatever is there rather than reserving space for itself. The pin button swaps it for an ordinary floating window — draggable by the empty padding around the buttons, stackable, and closable from its own titlebar:

Modifiers latch instead of needing two hands: Shift arms for one key, a
second tap caps-locks it, and Ctrl / Alt / Menu stay held until pressed
again. Number and symbol keys print their shifted character in the corner, the
way a physical keycap does.
Development
nix develop
pytest plugins/mouthguard # MouthGuard's Python suite
qmltestrunner -input plugins/dankmenu/tests/tst_menumodel.qml # one file per run
qmltestrunner -input plugins/virtualkeyboard/tests/tst_layout_us.qml
nix flake check # everything, headless
qmltestrunner takes one -input file per run, and its exit code is the
failure count rather than a flat 1.
MouthGuard keeps its own flake.nix under plugins/mouthguard/: its README and
StartupCheck.qml both document nix build .#detector inside the plugin
directory as the supported path for non-flake installs, and that has to keep
working. The root flake is what NixOS consumers pin. Both build the same
detector, from plugins/mouthguard/package.nix — a plain function of a nixpkgs
instance rather than a flake output, so neither flake defines it twice. That
matters more than it looks: the detector carries two hash-pinned MediaPipe model
files and an NPU runtime built around Intel's graph compiler, which nixpkgs does
not package and OpenVINO will only load from beside its own libraries.
Publishing a plugin to the DMS registry
All three plugins here are listed in the
DMS plugin registry — that
is what makes them installable with dms plugins install and findable in the
in-shell plugin browser, rather than only by cloning this repo. Notes for adding
another; the official guide
is the authority:
-
Add
plugins/{github-username}-{plugin-name}.json, lowercase and hyphenated — e.g.sitolam-dankmenu.json. -
Fill in the entry:
{ "id": "dankMenu", "name": "Dank Menu", "capabilities": ["daemon", "ipc"], "category": "utilities", "repo": "https://github.com/sitolam/dms-plugins", "path": "plugins/dankmenu", "author": "sitolam", "description": "Omarchy-style root menu: one key to every command, with built-in search", "dependencies": [], "compositors": ["any"], "distro": ["any"], "screenshot": "https://raw.githubusercontent.com/sitolam/dms-plugins/main/plugins/dankmenu/screenshots/root.png" }pathis what makes a monorepo work — it points the registry at the subdirectory rather than the repo root, so one repo can list several plugins. -
idandnamemust match the plugin's ownplugin.jsonexactly, or the entry is rejected. -
Run the repo's validation scripts, then open a PR. The registry opens a tracking issue for feedback, and listings are ranked by community upvotes.
Keep the description short, point screenshot at a real image, and make sure
the compositors and distro values are ones you have actually tested.
Credits
dankMenu's design is Omarchy's, by Basecamp and its contributors (MIT) — it shares no code, but deliberately keeps their menu file schema field-for-field.
virtualKeyboard's design is end-4's, from
dots-hyprland (GPL-3.0) — the docked
card, the key shapes, the latching modifiers and the pin-to-float idea are all
theirs, and its US QWERTY table is a port of their layouts.js "English (US)"
entry. Thank you, end-4: the whole rice is worth a look, DMS or not.
MouthGuard is a native port of sitolam/mouthguard.
License
GPL-3.0-only — see LICENSE.
The repo was MIT until virtualKeyboard landed. Its key tables are ported from end-4's GPL-3.0 dots-hyprland, which makes that plugin a derivative work, and MIT cannot promise freedoms the GPL withholds — so everything here moved to GPL-3.0 rather than carrying two licenses and a footnote on which applies where. dankMenu and MouthGuard are unencumbered by anyone else's code; they are GPL by choice, not obligation.



