The niri compositor layer of the equisdots desktop: a modular KDL config, the
niri session scripts and the session/portal files needed to run the equisdots
stack on niri 26.04+ instead of Hyprland. It
is the niri sibling of the hyprland repository.
niri is a scrollable-tiling Wayland compositor. Windows live in columns on an infinite horizontal strip per monitor, workspaces are dynamic and independent per monitor, and the config is a single KDL document that hot-reloads on save.
| Installed path | Source | Purpose |
|---|---|---|
~/.config/niri/config.kdl |
config/niri/config.kdl |
Entry config; includes the modules and the generated fragments. |
~/.config/niri/modules/*.kdl |
config/niri/modules/ |
The translated Hyprland modules (input, layout, keys, ...). |
~/.config/niri/generated/ |
config/niri/generated/ |
Runtime fragments (palette border colors, saved outputs, effects). |
~/.config/niri/scripts/*.sh |
scripts/ |
The niri session scripts and their shared lib/common.sh. |
~/.config/systemd/user/*.service |
systemd/ |
Optional user units (swayidle, quickshell, xwww, focus-daemon). |
/usr/share/wayland-sessions/niri.desktop |
session/niri.desktop |
The display-manager session entry. |
/usr/share/xdg-desktop-portal/niri-portals.conf |
portals/niri-portals.conf |
Portal backend selection (gnome + gtk). |
install.sh deploys all of the above (--config-only deploys just the config
and the user units). uninstall.sh removes them without touching packages.
This repository uses strategy A: the shared equisdots data root stays at
~/.config/hypr, and only the compositor config moves to ~/.config/niri.
~/.config/hypr/settings.json,palettes/,wallpapers/, the Quickshell shell (~/.config/hypr/scripts/quickshell) and the shared scripts (~/.config/hypr/scripts/*.sh, e.g.lock.sh,volume.sh,qs_manager.sh) are reused unchanged.- The niri scripts read the shared root through
EQUISDOTS_CONFIG_DIR(default~/.config/hypr); seescripts/lib/common.sh. - Hyprland and niri can run side by side from one
settings.jsonand one palette store.
Only the compositor-specific pieces differ: the KDL config, the workspace
event-stream daemon, the monitor/idle/screenshot wrappers and the shell backend
(a Niri.qml compositor adapter in the shell repository).
# 1. Validate the assembled config (niri is not required to be running).
niri validate -c ~/.config/niri/config.kdl
# 2. Start a session from a display manager (choose "Niri") or a TTY:
niri-session
# 3. Follow the session log:
journalctl --user-unit=niri -b --no-pager
# 4. Check the bar data sources:
~/.config/niri/scripts/niri_kb_fetch.sh
~/.config/niri/scripts/niri_workspaces.sh --once
niriis not installed in the authoring environment, soniri validatemust be run by the user on a machine with niri 26.04+. The scripts are checked here withbash -n, and the KDL is checked for brace balance, but that is not a substitute forniri validate.
Portals (screencasting, file choosers) require a real session (niri-session
or a display manager); a bare niri on a TTY does not provide the
graphical-session plumbing.
niri's KDL parser is strict. A block whose last node is not terminated by
; or a newline before } fails to parse:
// INVALID: the last node (open-floating) is not terminated before }
window-rule { match app-id="^(kitty)$"; open-floating true }
// VALID: multi-line block
window-rule {
match app-id="^(kitty)$"
open-floating true
}
// VALID: one-line block with a trailing ;
focus-ring { off; }An invalid config makes niri silently fall back to its default config: a
grey screen with the "Important Hotkeys" overlay, no error on screen. This is
why every rule in modules/window-rules.kdl and every generated fragment uses
multi-line blocks (see docs/kdl-gotchas.md).
Always validate before trusting a running session:
niri validate -c ~/.config/niri/config.kdlSaving a watched file live-reloads it, but a bad fragment can still be loaded
if it is written between validations; the generators run niri validate before
forcing a reload, so a broken runtime fragment is not applied.
config/niri/
config.kdl entry: includes modules, top-level options, generated
modules/
environment.kdl env.lua -> environment {} (see caveats in-file)
layout.kdl general/decoration -> layout {}
workspaces.kdl workspaces.lua -> named workspaces
window-rules.kdl windowrules.lua -> window-rule {}
layer-rules.kdl layers.lua -> layer-rule {} (forced blur removed)
autostart.kdl autostart.lua -> spawn-sh-at-startup
keybinds.kdl keybinds.lua -> binds {}
defaults/
input.kdl seed for generated/user-input.kdl
animations.kdl seed for generated/user-animations.kdl
blur.kdl seed for generated/blur.kdl
generated/
README.md generated fragments and their writers
user-input.kdl* input {} owner (seeded, then Settings panel)
user-animations.kdl* animations {} owner (seeded, then Settings panel)
blur.kdl* blur {} owner (seeded, then effects writer)
theme-colors.kdl* border colors (niri_write_borders.sh)
borders.kdl* live border colors (shell backend, Compositor.setWindowBorders)
theme-effects.kdl* gaps/border/shadow (niri_effects.sh)
outputs.kdl* saved monitor layout (niri_monitor_apply.sh)
user-binds.kdl* shell-generated binds (Config.qml -> Niri backend)
user-startup.kdl* shell-generated startup (Config.qml -> Niri backend)
scripts/
lib/common.sh shared paths/logging (EQUISDOTS_CONFIG_DIR, ...)
caching.sh Quickshell cache/state/run exports
niri_workspaces.sh workspace state daemon for the bar (event stream)
niri_kb_fetch.sh active keyboard layout
niri_ws.sh workspace/window routing
niri_write_borders.sh write generated/theme-colors.kdl and reload
niri_effects.sh read/preview/persist gaps/border/blur/shadow
niri_monitor_apply.sh apply saved layout -> generated/outputs.kdl
niri_monitor_manager.sh list/apply outputs, position presets, save
niri_scale.sh scale the focused output up/down/auto
niri_idle_mode.sh swayidle awake/normal/boot/status
niri_screenshot.sh equisdots overlay (region + still/video + mic)
niri_record.sh gpu-screen-recorder start/stop, region via slurp
niri_pick_color.sh native pick-color -> hex on the clipboard
systemd/ optional user units + README
session/niri.desktop display-manager session entry
portals/niri-portals.conf xdg-desktop-portal selection (gnome + gtk)
install.sh / uninstall.sh idempotent, distro-agnostic
* = generated at runtime and gitignored.
| Script | Usage |
|---|---|
niri_ws.sh |
<index|name> [move], next, prev |
niri_effects.sh |
read, preview KEY=VAL ..., persist, apply KEY=VAL ... |
niri_monitor_manager.sh |
list, apply NAME key=val, layout PRESET, save |
niri_scale.sh |
up, down, auto, <scale> [output] |
niri_idle_mode.sh |
awake, normal, boot, status |
niri_screenshot.sh |
(default overlay), --full, --window, --edit, --geometry G, --record, --scan-qr |
niri_record.sh |
toggle, start [screen], region, region-geom G, stop, status |
niri_workspaces.sh |
(daemon), --once |
Run any script with --help for details.
These are deliberate; see the comments in each module.
- Workspaces are dynamic. The numbered binds still work (1..0 address
indices), but app placement uses named workspaces (
browser,code,chat,media,games,scratch). There is no floating scratchpad overlay. - No
hyprctl eval. Live changes write a KDL fragment undergenerated/and reload; this is why the border/effects writers exist. - Border, not focus-ring. Hyprland drew a border around every window;
niri's
focus-ring(active window) is disabled andborder(all windows) is enabled with width 2. The default border is a neutral grey; the shell refines it from the palette's muted colour (color8), not the loud accent, throughgenerated/borders.kdl. - No forced layer blur. Hyprland's namespace blur used
ignore_alpha, which niri 26.04 does not have. Forcing blur on the large, mostly transparent Quickshell panels painted a full-screen blur sheet, so the layer rules no longer force it. Surfaces that request blur viaext-background-effectare still blurred with the globalblur {}settings. - No separate notification daemon. The Quickshell shell registers
org.freedesktop.Notificationsitself and renders palette-aware notifications; nomako/dunstis installed (a rival daemon would win the D-Bus name and use its own colours). - Key conflicts resolved. Hyprland rebound keys; niri rejects duplicates.
Focus wins for
Super+H/J/L, lock moved toSuper+Alt+L, andmaximize-columnmoved toSuper+Ctrl+MsoSuper+Shift+Mcan open the Plugins panel (the same chord as Hyprland). - No output mirroring. Hardware mirroring does not exist in niri 26.04;
use
wl-mirroras a window if needed. - No live effect preview.
niri_effects.shwrites and reloads; call it on slider release, not per frame. - Screenshots open the equisdots overlay (
Print), the same UI as Hyprland: pick a region and choose screenshot vs. video and microphone on/off.scripts/niri_screenshot.shlaunches the screenshots plugin's Quickshell overlay (with the legacy root path as a fallback) and handles the capture; the overlay calls back into the niri script throughEQUISDOTS_SCREENSHOT_SCRIPT(shipped byniri-shell). - Plugins are Quickshell-side and compositor-neutral.
niri-metadeploys the plugin standard (plugin-api,plugin-sdk, thepluginshub, the default plugins and their registry), so the same 63 plugins, MD3 theme tokens, plugin popups/positions and bar modules run on niri.Super+Shift+Mopens the Plugins panel. Plugin-provided keybind actions are covered by the static binds below (clipboard, quicknotes, RSS, file search, screenshots, timex), because niri reads KDL rather than evaluating pluginhypr.luafragments at runtime. - Recording uses the same engine as Hyprland:
gpu-screen-recorder. It is self-contained inscripts/niri_record.sh(no dependency on the Hyprland repo):Super+Alt+Rtoggles a full-screen recording andSuper+Alt+Shift+Rrecords a region picked withslurp. Recordings go to${XDG_VIDEOS_DIR:-$HOME/Videos}/Recordings, and the pid cache (~/.cache/quickshell/recording/rec_pid) drives the bar's recording indicator, so the widget's stop button works too.install.shaddsgpu-screen-recorderas an optional package (AUR / COPR on some distros). OBS over the screencast portal remains an alternative. GDK_BACKENDis not set. A globalwaylandvalue breaks the screencast portal; the KDLenvironment {}deliberately omits it.
Where a Hyprland visual feature has no niri equivalent, it is replaced by a valid alternative rather than dropped silently.
| Hyprland feature | niri substitute |
|---|---|
Per-window immediate (tearing) |
variable-refresh-rate on-demand=true on the output + variable-refresh-rate true on game windows |
Layer blur per namespace (ignore_alpha) |
No forced layer blur (niri has no ignore_alpha); surfaces that request ext-background-effect are blurred with the global blur {} tuning |
| Accent-coloured active border | Neutral grey default; shell refines it from the palette muted colour (color8) via generated/borders.kdl |
mako / dunst notification daemon |
The Quickshell shell registers org.freedesktop.Notifications itself (palette-aware) |
dim_inactive / dim_strength |
window-rule { match is-active=false; opacity 0.80 } |
borderangle / fade* animation leaves |
springs (window-movement, workspace-switch, horizontal-view-movement) |
| Special workspace (scratchpad overlay) | named workspace scratch focused with Super+A |
center floating rule |
remembered floating position (default-floating-position) |
pin window |
move to a persistent named workspace, or keep it floating |
shadow.render_power |
not exposed; shadow.softness is the control |
| Output mirroring | no hardware mirror; run wl-mirror as a window |
hyprpicker |
niri_pick_color.sh (niri pick-color / screenshot UI) |
Screenshot --edit annotation |
pipe the capture to satty in niri_screenshot.sh |
- niri 26.04+ with
xwayland-satellite >= 0.7. jqandpython3for the scripts.- For capture/annotation:
grim,slurp,satty,wl-clipboard,zbar. - For the plugin set:
cliphist(clipboard),fd(file search) andjq(scan/registry) in addition to the shell's own dependencies. - The shared equisdots payload (Quickshell shell, settings.json, palettes) from
the other repositories;
install.shcalls a meta installer when present.
See LICENSE.