niri-dynamic-workspaces

Install
Nix (flake)
# flake.nix inputs
niri-dynamic-workspaces.url = "github:nickolaj-jepsen/niri-dynamic-workspaces";
Build directly:
Home Manager module
# Add to your Home Manager imports
imports = [ inputs.niri-dynamic-workspaces.homeModules.default ];
# Enable
programs.niri-dynamic-workspaces = {
enable = true;
keybind = "Mod+D"; # default — open switcher
deleteKeybind = "Mod+Ctrl+D"; # default — open delete overlay
moveWindowKeybind = "Mod+Shift+D"; # default — open move-window overlay
daemon = true; # default — start daemon at login
settings = {
general.workspace_prefix = "dyn-";
};
};
This installs the package, adds a niri keybind, and writes the config file.
Cargo
Requires GTK4, gtk4-layer-shell, and pkg-config development headers.
Configuration
Config file: ~/.config/niri-dynamic-workspaces/config.toml
All fields are optional with sensible defaults.
[]
= "dyn-" # prefix for dynamic workspace names
= ["kitty"] # programs launched when creating any new workspace
= true # daemon: auto-delete empty unfocused workspaces
= true # preview workspaces by hovering over cards
= "qwerty" # keyboard layout for the overlay (see table below)
[]
= ["Escape", "Ctrl+c", "Ctrl+w", "Ctrl+q"] # keys to dismiss the overlay
[] # key: a-z or 0-9
= "Browser" # optional display name shown on the key
= ["firefox", "slack"] # programs launched on create (replaces defaults)
# Configured workspaces that don't exist yet appear as muted keys with a dashed border.
[]
= ["kitty --title myterm"] # arguments supported via whitespace splitting
[] # digit workspaces work too
= "Comms"
= ["slack", "discord"]
Templates
Templates let you choose from predefined program sets when creating a new workspace. When templates are defined and you press a key for a non-existing workspace, a picker appears instead of creating the workspace immediately.
[]
= ["kitty", "code {{path}}"]
= "d" # optional hotkey shortcut
= "{{path}}" # optional workspace title (see below)
[]
= "Project path" # display label shown in the input form
= "text" # free-form input (default)
[]
= "Git branch"
= "options" # dropdown from static list
= ["main", "develop", "staging"]
[]
= "Build tool"
= "command" # dropdown from shell command output
= "ls ~/dev" # each stdout line = one option
[]
= "Project"
= "dir" # dropdown from directory scan
= ["~/dev", "~/work"] # directories to scan for child dirs
= 1 # scan depth (default 1)
[]
= ["firefox", "slack"]
- Each template needs a
programslist (templates with empty programs are skipped) - The optional
keyfield assigns a hotkey (a-z or 0-9) for quick selection in the picker - Templates without a
keyget one auto-assigned (1-9 then a-z) - The picker always includes an "Empty" option that uses
default_programs - Workspaces with per-key
[workspace.KEY].programsskip the picker and create directly - Templates can define variables with
{{name}}placeholders in program strings - Each variable has a required
name(display label) and optionaltype(defaults to"text") - Variable types:
"text"— free-form text input (default); outputs whatever the user types"options"— dropdown from a static list (optionsfield); outputs the selected option string"command"— dropdown from shell command output (commandfield); outputs the selected stdout line"dir"— dropdown from directory scan (dirsfield; hidden dirs excluded;depthcontrols scan depth, default 1); outputs the absolute path of the selected directory (e.g./home/user/dev/myproject)
- If the source resolves to zero options at runtime, the variable falls back to free-form text input
- Templates with variables show an input form before creating the workspace
- Templates without variables create immediately as before
- The optional
titlefield sets a display name on the workspace (shown on the key card):- Supports
{{var}}substitution from template variables - If omitted and the template has variables, the first variable's value is used automatically
- For
dir-type variables, the basename is extracted (e.g./home/user/dev/myproject→myproject) - The full workspace name becomes
{prefix}{key} {title}(e.g.dyn-a myproject)
- Supports
Hooks
Shell commands that run automatically when workspaces are created or deleted:
[]
= ['notify-send "Created $NDW_WORKSPACE_NAME"']
= ["cleanup-workspace.sh"]
on_create/on_deleteare arrays of shell commands run viash -c- Commands run in the background (fire-and-forget, non-blocking)
- Variables are passed as environment variables — use shell double quotes (not single quotes) to expand them
- Environment variables set for each hook:
NDW_WORKSPACE_NAME— full workspace name (e.g.dyn-a)NDW_WORKSPACE_KEY— single character key (e.g.a)NDW_TEMPLATE— template name if used (empty otherwise)NDW_VAR_<NAME>— template variable values, uppercased (e.g.NDW_VAR_PATH)
- Templates can define additional
on_createhooks that run after the global ones:
[]
= ["kitty", "code {{path}}"]
= ['git -C "$NDW_VAR_PATH" status']
[]
= "Project path"
Available keyboard layouts
| Layout | Value |
|---|---|
| QWERTY | qwerty |
| AZERTY | azerty |
| QWERTZ | qwertz |
| Dvorak | dvorak |
| Colemak | colemak |
All layouts contain the same 36 keys (a–z, 0–9) arranged in the physical positions of each keyboard layout. The value is case-insensitive.
Usage
niri-dynamic-workspacesorniri-dynamic-workspaces switch— opens the switcher overlay (press key to switch/create)niri-dynamic-workspaces delete— opens the delete overlay (press key to delete)niri-dynamic-workspaces move-window— opens the move-window overlay (press key to move the focused window)niri-dynamic-workspaces daemon— starts as a background daemon (no overlay shown)
All modes support toggle behavior: running the same command again closes the overlay.
Direct mode (no overlay)
Pass a workspace key to act immediately without opening the overlay:
Daemon mode
By default the overlay process starts fresh each time a keybind is pressed, which includes GTK and CSS initialization. For faster overlay display, start a background daemon at login:
spawn-at-startup "niri-dynamic-workspaces" "daemon"
The daemon keeps GTK initialized and subsequent switch/delete/move-window invocations are forwarded to it over D-Bus, skipping startup overhead.
The Home Manager module enables daemon mode by default. To disable it:
programs.niri-dynamic-workspaces.daemon = false;
Development
Enter the dev shell and build:
Lint and test: