Niri is a scrollable-tiling Wayland compositor. Windows are arranged in columns on an infinite strip going to the right. Opening a new window never causes existing windows to resize.
Here are the improvements from the last release... hang on, how come we jumped from v0.1 all the way to v25?
Starting now, niri escapes ZeroVer is switching to year.month versioning. In 25.01, the "25" is year 2025, and "01" is month 01 (January). So version 25.01 tells you that this release was tagged in January of 2025.
Hotfix releases will use the third component. For example, the first hotfix for the 25.01 release would be called 25.01.1.
There are a few reasons for this change.
- For niri, semver isn't very useful. Big and small features are added every release, and so far we've managed to avoid any breaking changes to the config file. Calendar versioning at least tells you how old of a version you're running.
- v0.1.x left no place for a hotfix version. I couldn't even put v0.1.10.1 into
Cargo.toml
because it has four components instead of three. The new versioning has just two components, leaving one extra for the hotfix version. - I feel like niri is now sufficiently featureful to graduate from v0.1. :) I expanded the Status section of the README to cover some of the frequently asked "is this thing supported" questions.
Similar versioning is also used in other projects like Helix, NixOS and Ubuntu.
With this change, the niri releases remain unscheduled: once every few months, and not bound to any particular cycle. Whenever I feel that it's a good time for a new version.
Note
Packagers: niri now requires Rust 1.80. Also, there are new environment variables to override the niri version string and commit: NIRI_BUILD_VERSION_STRING
and NIRI_BUILD_COMMIT
. The new Packaging niri wiki page shows how to use them, along with everything else important for packaging.
New niri tests need XDG_RUNTIME_DIR
to be set. You can use export XDG_RUNTIME_DIR="$(mktemp -d)"
.
If some tests fail with Err::AlreadyInUse
on a heavily multi-threaded CPU, set RAYON_NUM_THREADS=1
. This is tracked in #953.
Floating windows
Floating windows are here! It took a big refactor and a good month of hard work, but the most liked niri feature request is done.
Like other WMs, niri will auto-float dialogs and fixed-size windows. With no extra configuration, this release does away with most of the annoying dialog scrolling.
niri-floating-dialogs.mp4
Being a scrolling WM, there were several options and design decisions to consider for how floating windows should work. I opted for a setup familiar from other tiling WMs: floating windows are on a separate "layer" that always shows on top of the tiled windows, and the floating layout does not scroll. Each workspace/monitor has its own floating layout, just like each workspace/monitor has its own tiling layout.
There's a surprising number of features and small details that go into a good floating experience. Things like correct parent-child stacking, focus-follows-mouse activating but not raising the window, or restoring the floating size and position after moving the window to the tiling layout and back.
niri-floating.mp4
Since floating windows live on a workspace, and workspaces can move between monitors, it's important that floating windows never end up "out of bounds" and unreachable outside the monitor.
Internally, niri remembers floating window positions relative to the monitor size, and will always push windows slightly away from the monitor edges. This way, windows are always visible, and moving the workspace to a smaller monitor will roughly preserve the window layout. Furthermore, moving the workspace to a smaller monitor and back will restore the original window positions exactly.
In the following demo, I'm resizing a nested niri with three floating windows, simulating monitor resolution changes.
niri-floating-offscreen.mp4
There's a set of actions for focusing the floating or the tiling layout, and for moving windows around. The updated default config includes switch-focus-between-floating-and-tiling
bound to ModShiftV and toggle-window-floating
bound to ModV. All relevant existing binds keep working when the focus is on the floating layout, e.g. focus-column-right
will activate the next floating window to the right.
Additionally, on a mouse, you can easily move a window between floating and tiling by right-clicking while dragging it. You can tell which of the two it "targets" by the presence of the tiling insertion hint.
niri-interactive-move-floating-switch.mp4
There's a new is-floating
window rule matcher, and new open-floating
and default-floating-position
rules.
You can use open-floating
to float some window that isn't covered by the auto-floating heuristics, like the Firefox Picture-in-Picture player. And default-floating-position
supports putting floating windows relative to the four corners of a monitor:
// Open the Firefox Picture-in-Picture window at the bottom-left corner of the screen
// with a small gap.
window-rule {
match app-id="firefox$" title="^Picture-in-Picture$"
open-floating true
default-floating-position x=32 y=32 relative-to="bottom-left"
}
Meanwhile, real tiling WM users like @algernon can set a blanket open-floating false
rule to disable all auto-floating heuristics. Rest assured that our new set of 3135 snapshot tests across all possible window opening settings will keep this working.
All in all, this release contains a fairly complete per-workspace floating layout. Going forward, we can expand the functionality, for example by adding a sticky/show on all workspaces window flag. Or perhaps by putting modal dialogs as floating right into the scrolling layout.
Also, when resizing tiled windows, their height is now clamped to the monitor height. It used to be unlimited so that you could take window screenshots larger than the monitor size, but now you can do that with a floating window.
Layer-shell improvements
This release has several fixes to layer-shell handling.
- @cmeissl fixed the problem where the pop-up menu on Waybar and other GTK 3 bars would sometimes get stuck and fail to open. The way it was fixed disables keyboard navigation in those menus, but this is consistent with other compositors like Sway.
- @cmeissl also fixed some clients crashing when opening nested pop-up menus (like lxqt-panel app menus).
- @calops fixed niri not always activating the window below when clicking through layer-shell surfaces. Previously, that code didn't account for the surface's input region.
- Pop-up menus from all layer-shell surfaces now render on top of regular windows. So putting Waybar at the top layer is no longer necessary for usable context menus.
- Niri will now give bottom and background layer-shell surfaces on-demand keyboard focus and allow them to take pop-up grabs.
- Certain actions like
focus-column-right
will now move the focus from an on-demand layer-shell surface back to the main layout, allowing you to "escape" layer-shell with just a keyboard.
Combined, these improvements make the desktop icons components from LXQt and Xfce "just work" on niri. Thanks @stefonarch from LXQt for helping me test this and working on the LXQt niri session.
Layer rules
At last, you can block out layer-shell notifications from screencasts just like windows:
// Block out mako notifications from screencasts.
layer-rule {
match namespace="^notifications$"
block-out-from "screencast"
}
Layer rules work very similarly to window rules but with a different set of matchers and properties. See the wiki page for more details.
You can currently match by layer-shell namespace, and set block-out-from
and opacity
. To find out the namespace, use niri msg layers
which lists all currently open layer-shell surfaces.
Drag-and-drop focus switch
Drag-and-dropping something will now focus the output where you dropped it. This makes dragging a Firefox tab to a different output open it on that output.
niri-firefox-dnd-to-different-monitor.mp4
Successful drag-and-dropping (when the target window accepts the drop) will also now activate the target window. This should reduce the confusion when you try to press some app shortcut right after a DnD and it ends up in the wrong (previous) window.
IPC and niri msg
improvements
I've expanded the IPC interface and niri msg
a bit in this release:
- Added the process ID and whether the window is floating to
niri msg windows
. - Added
niri msg layers
which shows information about layer-shell surfaces. - Added the
set-window-width
,switch-preset-window-width
, andcenter-window
actions that mirror the corresponding-column
actions, but can accept their target window by--id
. - Actions that can accept negative sizes like
niri msg set-column-width -10%
no longer require a double-dash before the negative size. (I set the argument parser option that allows these arguments to start with a hyphen.)
Mouse click bindings
Thanks to @bbb651, you can now bind actions to mouse clicks:
binds {
Mod+Ctrl+MouseLeft { close-window; }
Mod+MouseMiddle { maximize-column; }
}
These mouse-click binds will operate on the window that was focused at the time of the click, not the window you're clicking.
Window swapping
@rustn00b added the swap-window-left/right
actions that swap windows between two adjacent columns. They can be useful for a master/stack-like layout.
niri-swap-window.mp4
New window focusing
Thanks to @cmeissl, niri will now pass an xdg-activation token to the processes it spawns (with spawn
binds or niri msg action spawn
). As such, new windows that correctly activate themselves from the token will now get focused even when opening on a different workspace, or from a fullscreen window.
When developing Wayland applications, you can verify that you use the xdg-activation token correctly with the new strict-new-window-focus-policy
debug flag. It disables all heuristic automatic focusing, so only those new windows that use xdg-activation will get focused.
Lazy PipeWire initialization and resilience
Instead of at compositor startup, niri will now initialize PipeWire on-demand (at the first screencast attempt). This makes it possible to run PipeWire after niri startup (e.g. with spawn-at-startup "pipewire"
).
Also, niri now handles PipeWire restarts, and will reinitialize PipeWire next time it's needed. So restarting PipeWire should no longer make it impossible to screencast.
Other improvements in this release
- Added the
empty-workspace-above-first
layout option (thanks @FluxTape). If set, niri will always keep an empty workspace at the very start, in addition to the empty workspace at the very end. - Added the
focus-window-previous
action (thanks @zeroeightysix). - Added the
focus-monitor-next/previous
,move-window/column/workspace-to-monitor-next/previous
actions (thanks @julianschuler). These use internal monitor ordering (that should be consistent across reboots) and loop around. - Added the
un/set-workspace-name
actions that can be useful in scripts to change workspace names at runtime (thanks @rustn00b). - Added the
open-focused true/false
window rule to force or prevent a window from getting focused on opening. - Added the
default-window-height
window rule that works similarly to the existingdefault-column-width
, but for height. expel-window-from-column
now always expels the bottom-most window in a column (rather than the focused one), and doesn't switch focus to the expelled column. This way, it becomes the exact opposite ofconsume-window-from-column
. If you prefer the old behavior, useconsume-or-expel-window-right
.- Changed
is-active-in-column
window rule matcher to matchtrue
during the initial window opening (thanks @TheZoq2). This makes sense as a new window always opens in a separate column, therefore it is active in its column right away. - Niri will now power on monitors when the session is unlocked (thanks @salman-farooq-sh). This helps with fingerprint and other unlocking methods that don't generate an input event.
- Fixed xdg-desktop-portal notifications by setting niri to use the gtk impl for the Notification portal.
- Fixed
border { off; }
unable to override an earlierborder { on; }
window rule. - Fixed a crash when connecting two monitors with identical make/model/serial (or one monitor through both HDMI and DP). Niri will detect this and automatically unname the second monitor. So the
disable-monitor-names
debug flag is no longer necessary for such setups. - Fixed windows on other workspaces losing their Activated state (becoming visually inactive). This was a v0.1.10 regression. Windows keep their Activated state when switching workspaces to reduce unnecessary visual changes.
- Fixed a crash when calling
move-window-to-workspace
to an unfocused monitor. - Fixed several actions applying to the windows below while interactively moving a window.
- Fixed a potential crash when disconnecting a monitor while screencasting it.
- Fixed potentially misaligned borders on the interactively moved window when hot-reloading the output scale config in-flight.
- Fixed a wrong visual position when disconnecting a monitor while the primary monitor is in the middle of a workspace switch.
- Fixed initial window size requested by niri not honoring min/max size from window rules.
- Fixed potential crash when dropping an interactively moved window on a different monitor with a workspace switch in progress.
- Added Xrgb and Xbgr as preferred formats for the primary plane (previously only Argb and Abgr were preferred). I haven't seen it affect anything thus far, but maybe it fixes a bandwidth limitation on some setups.
- Refactored animation timing. This shouldn't have changed much visually (maybe some tiny issues were fixed), but it made the system much more robust, and let me write tests for many more scenarios. I wrote a wiki page about the new system if you're curious about the technical details.
- Added the
force-pipewire-invalid-modifier
debug flag that forces PipeWire screencasting to use the invalid modifier. - Added the
restrict-primary-scanout-to-matching-format
debug flag. - Debug flags for
enable-overlay-planes
anddisable-cursor-plane
now apply immediately, without having to reconnect the monitor. - Updated Smithay:
- Fixed some clients crashing when opening nested pop-up menus (lxqt-panel app menus).
- Implemented presentation-time v2 protocol (well-defined refresh rate reporting for VRR).
- Fixed xdg-foreign assigning window parents in reverse, so now portal dialogs are correctly parented.
- Updated rustix:
- Fixed a crash at startup on M2 Apple and other AArch64 devices.
Funding
I work on niri in the spare time that I have from my university studies. If you like what I do, you can support my work on GitHub Sponsors. Big thanks to all current and past sponsors!