Releases: YaLTeR/niri
v25.05.1
This is a hotfix release for niri v25.05.
- Fixed handling of layer surfaces unmapped through a null buffer commit: they will now receive an initial configure as necessary (thanks @alex-huff). This makes the kitty quick access terminal work.
- Fixed unmapped layer surfaces preventing popups from appearing (thanks @alex-huff).
- Fixed brief hover events when the cursor is hidden (thanks @Duncaen). Also fixed brief cursor hovers when a hidden cursor warps (i.e. with
warp-mouse-to-focus
or withfocus-monitor-right
etc.). - Renamed
un/set/toggle-urgent
toun/set/toggle-window-urgent
, their intended name. This was my oversight. This change is not config-breaking because these actions aren't bindable, but it does mean you need to change the name when callingniri msg action
. I went ahead with this break because I don't believe these actions are used anywhere yet (and others confirmed that). - Fixed screen not always redrawing on
niri msg set-window-urgent
and other urgency actions. - Updated Smithay:
v25.05
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.
Note
Packagers: the niri default config now spawns waybar at startup so as not to start with a blank desktop. Please consider adding Waybar as a recommended dependency (or changing it to some other bar).
The Overview
The big new thing in niri v25.05 is the Overview. It zooms out your workspaces and windows to let you see what's going on at a glance, navigate, and drag windows around, all without having to touch the keyboard.
niri-overview-release.mp4
You can open it with the toggle-overview
bind, via the new top-left hot corner, or using a touchpad four-finger swipe. While in the overview, all keyboard shortcuts keep working, while pointing devices get easier:
- Mouse: left click and drag windows to move them, right click and drag to scroll workspaces left/right, scroll to switch workspaces (no holding Mod required).
- Touchpad: two-finger scrolling that matches the normal three-finger gestures.
- Touchscreen: one-finger scrolling, or one-finger long press to move a window.
niri-overview-touch.mp4
Drag-and-drop will scroll the workspaces up/down in the overview, and will activate a workspace when holding it for a moment. Combined with the hot corner, this lets you do a mouse-only DnD across workspaces.
niri-overview-dnd-across-ws.mp4
By the way, this new drag-and-drop hold will also bring floating windows to the top outside the overview.
You can drag-and-drop a window to a new workspace above, below, or between existing workspaces.
niri-overview-insert-between.mp4
To make the spatial model work for the overview, niri now draws a separate background under each workspace. For layer-shell tools, the background and bottom layers zoom out together with the workspaces, while the top and overlay layers remain on top of the overview. Make sure to put your bar on the top layer.
The background behind the workspaces in the overview is called the backdrop. There are new backdrop-color
settings both globally in the overview {}
section and per-output.
If you want something more interesting in the backdrop, there's a new place-within-backdrop
layer rule. You can use it on a wallpaper tool showing a blurred version of your background, for example.
Since backgrounds are now tied to workspaces, they will also move together with workspaces. If you don't like this, you can combine place-within-backdrop
with a transparent background-color
to get the stationary wallpaper back. Check the overview wiki page for an example configuration. There, you will also find how to change the overview zoom level or disable the hot corner.
There have also been smaller spatial model fixes to accommodate the overview. For example, the top layer-shell layer and the interactively dragged window now render on top of the background- and bottom-layer popups.
Finally, @CharlieQLe added an IPC request and event for monitoring the overview's open state.
Screencasting features
For this release, I worked on several new features for screencasting and screensharing. To keep track of them, I wrote a new wiki page that describes all screencasting-related functionality in niri. Check it out!
I should also mention the new wait-for-frame-completion-in-pipewire
debug flag by @coleleavitt. If you started having glitches when screencasting on NVIDIA (e.g., in Discord), this flag should help, until we support explicit sync for PipeWire screencasts.
Dynamic screencast target
A dynamic cast target is a special target that can change what it streams. You can select it as a special "niri Dynamic Cast Target" in the window selection dialog:
The dynamic target starts as an empty, transparent video stream. Then, you can use the following binds to change what it shows:
set-dynamic-cast-window
to cast the focused window.set-dynamic-cast-monitor
to cast the focused monitor.clear-dynamic-cast-target
to go back to an empty stream.
You can also use these actions from the command line, for example, to interactively pick which window to cast. Check the screencasting wiki page for more details and examples.
niri-dynamic-cast-target.mp4
Windowed fullscreen
A common feature in WMs is fake, or detached, or windowed fullscreen. The compositor tells the window that it went fullscreen, but in reality keeps it as a normal window.
This is useful when screencasting browser-based presentations like Google Slides, where you usually want to hide the browser UI, which requires fullscreening the browser. Real fullscreen is not always convenient, for example, if you have an ultrawide monitor, or just want to leave the browser as a smaller window, without taking up an entire monitor.
Now, niri can help, with the new toggle-windowed-fullscreen
bind. It tells the app that it went fullscreen, while in reality leaving it as a normal window that you can resize and put wherever you want.
binds {
Mod+Ctrl+Shift+F { toggle-windowed-fullscreen; }
}
Here's an example showing a windowed-fullscreen Google Slides presentation, along with the presenter view and a meeting app, all on the same monitor:
Screenshot UI
For this release, I made the screenshot UI support tablet and touchscreen input for drawing the selection, closing this long-standing gap. I also added a small capture button to the panel at the bottom, making it possible to select and save a screenshot without a keyboard.
niri-screenshot-ui-touch.mp4
Keyboard use got better too: the screenshot UI will now recognize some windowing binds like move-column-left/right
, move-window-up/down
and set-window-width/height
, and move the selection region as if it was a floating window. Thanks to @nnyyxxxx for prototyping the implementation! Naturally, this opens the door for making a bunch more windowing actions work on the screenshot UI selection.
Finally, @TobyBridle added a show-pointer
flag to screenshot
and screenshot-screen
actions to control whether the mouse cursor is included in the image.
Window urgency
Urgency is an unspecified but commonly implemented Wayland behavior where a window can request the user's attention. The compositor will then draw it with a red border, and signal this urgency to other desktop components.
Now, @Duncaen implemented window urgency in niri. It comes with a host of urgent-color
settings on borders, focus-rings and tab indicators, an is-urgent
window rule matcher, and urgency indicators for windows and workspaces in the IPC.
Urgency is cleared once you focus a window. You can also manually toggle urgency for a specific window with the new toggle-urgent
, set-urgent
, and unset-urgent
actions.
@Duncaen is also adding niri urgency support to Waybar in this PR.
IPC improvements
We have several new things in the IPC system.
@bbb651 implemented niri msg pick-window
that lets you select a window by clicking. The command then outputs information about this window, which can be used for scripting. For example, you can make a "screenshot clicked window" script:
$ niri msg action screenshot-window --id="$(niri msg --json pick-window | jq .id)"
@nnyyxxxx added niri msg pick-color
that prints the color of the selected pixel. They also hooked it up to the PickColor method of the Screenshot portal, making the color picker work in apps.
niri-pick-color.mp4
The niri IPC socket had so far been conservatively limited to a single reques...
v25.02
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.
Note
Packagers: I fixed the problem where some tests required RAYON_NUM_THREADS=1
on a heavily multithreaded CPU. Please remove that variable if you had it set, so that we don't miss any new bugs.
niri-25-02.mp4
Tabbed columns
Columns can now present windows as tabs, rather than vertically stacked tiles. This is useful when you have limited vertical space, or when you frequently switch between two large windows and want to avoid scrolling.
Add this new bind to your config to switch a column to tabbed mode:
binds {
Mod+W { toggle-column-tabbed-display; }
}
This is the only new bind you will need. All other keyboard and mouse navigation works exactly the same as with regular columns: switch tabs with focus-window-down/up
, add or remove windows with consume-window-into-column
/expel-window-from-column
, and so on. (Thanks @elkowar for this wonderful UX idea!)
niri-tabs.mp4
There are a few new actions that help navigate the tabs. All of them also work on regular columns.
focus-window-top/bottom
focuses the topmost or the bottommost window in a column.focus-window-down-or-top
andfocus-window-up-or-bottom
cycle the navigation so the focus jumps from the last to the first window and vice versa.focus-window-in-column <index>
focuses a specific window by index.
The tab indicator can be customized in several ways and moved to top/bottom/right of the column. See the wiki page for more details.
You can also make windows open as tabbed columns by default globally or with a window rule. This goes well with the hide-when-single-tab
setting for the tab indicator.
Shadows
Niri can now draw shadows behind windows. Apart from being a nice aesthetic effect, shadows help to delineate floating and otherwise overlapping windows. They are especially useful when you disable or clip away client-side decorations (which commonly include shadows of their own).
Niri shadows are not enabled by default to not clash with the shadows coming from client-side decorations. Turning them on is simple enough:
// Enable shadows.
layout {
shadow {
on
}
}
// Also ask windows to omit client-side decorations, so that
// they don't draw their own window shadows.
prefer-no-csd
You can customize properties like softness (blur radius), spread, offset, and color, both globally and for individual windows. Like borders and focus rings, shadows will follow the window corner radius that you set via geometry-corner-radius
.
Shadows also work on layer-shell surfaces. Due to the higher variety among layer-shell components, we don't enable shadows for them automatically; you need to explicitly enable them with a layer rule. For example:
// Add a shadow for fuzzel.
layer-rule {
match namespace="^launcher$"
shadow {
on
}
// Fuzzel defaults to 10 px rounded corners.
geometry-corner-radius 10
}
Drag-and-drop view scrolling
In this release I finally addressed one of the longer-standing UX issues: you can now scroll the view left and right during a drag-and-drop operation by moving the mouse close to the monitor's edge. This is similar to how you can scroll various lists and scrolling views in applications during drag-and-drop.
There's a small debounce delay before the scrolling starts so that it doesn't trigger when quickly moving the mouse across monitors. You can customize this, as well as other parameters like maximum scrolling speed, in the new config section.
In addition to drag-and-drop, this gesture will trigger when dragging a window in the tiling layout. Dragging floating windows however won't scroll the view.
niri-dnd-edge-scroll.mp4
Screencast target window rule
There's a new is-window-cast-target=true
window rule that matches windows "targetted" by an individual-window screencast. You can use it, for example, to highlight the window that you're screensharing by changing its border/focus ring colors.
// Indicate screencasted windows with red colors.
window-rule {
match is-window-cast-target=true
focus-ring {
active-color "#f38ba8"
inactive-color "#7d0d2d"
}
border {
inactive-color "#7d0d2d"
}
shadow {
color "#7d0d2d70"
}
tab-indicator {
active-color "#f38ba8"
inactive-color "#7d0d2d"
}
}
Thanks @elkowar for the suggestion!
Custom titles for Important Hotkeys
We have an Important Hotkeys dialog in niri that pops up at startup with a list of the main binds to get you going. The binds in this list and their titles are hardcoded (so that you're not spammed with all the keys bound by default).
In this release, you can customize this list using the new hotkey-overlay-title
property.
- To add a bind to the dialog, or change an existing bind, set it to the title that you want to show:
binds { Mod+Shift+S hotkey-overlay-title="Toggle Dark/Light Style" { spawn "some-script.sh"; } }
- To hide an existing bind from the dialog, set it to null:
binds { Mod+Q hotkey-overlay-title=null { close-window; } }
This is especially useful for binds that spawn
programs, as niri can't automatically deduce good titles for them. For example, here's my Important Hotkeys list where I gave nice titles to most spawn binds (everything below PrtSc):
These custom titles also support full Pango markup which allows you to change styles, colors, and fonts.
I also made two cosmetic changes to the key combo rendering:
- Ctrl and Shift are now sorted before Alt, matching most other programs. However, when Alt is the Mod key (running niri as a window), then it will be sorted first to emphasize that.
- Space is now rendered with capital S. These names come from xkb in all kinds of spelling variations, and we have to "prettify" them manually in niri. Space seems fairly common, so I added it to the code.
Expand to available width
Sometimes windows don't quite neatly divide into preset widths, making it hard to fill the space on the monitor exactly. The new expand-column-to-available-width
bind addresses this: it expands the focused window to take up all remaining free space on the screen.
Since windows on niri can scroll in and out of view, this bind considers the current window positions. All fully visible windows remain on screen.
niri-expand-to-available-width.mp4
Keyboard shortcuts inhibit protocol
@sodiboo implemented the keyboard-shortcuts-inhibit
Wayland protocol. It is used by apps like virtual machines or remote desktop clients to let them pass compositor bindings to the target system.
You can force-deactivate the inhibiting and get your niri shortcuts back using the following new action. Make sure to add it to your config:
binds {
Mod+Escape { toggle-keyboard-shortcuts-inhibit; }
}
You can also make certain binds ignore inhibiting with the new allow-inhibiting=false
property. They will always be handled by niri and never passed to the window.
binds {
// This bind will always work, even when using a virtual machine.
Super+Alt+L allow-inhibiting=false { spawn "swaylock"; }
}
Screenshot without writing to disk
Thanks to @sornas, you can now capture screenshots only to the clipboard, without writing the image to disk. Simply press CtrlC in the screenshot UI instead of Space or Enter</k...
v25.01
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-dro...
v0.1.10.1
This is a hotfix release for niri v0.1.10.
- Fixed scrolling not working when the
mouse {}
ortouchpad {}
section is omitted from the config file. - Made the mouse cursor show up on scroll which makes scrolling work when the cursor was hidden (thanks @r-vdp).
- Fixed a crash when holding Space in the screenshot UI.
- Bound touch-dragging with held Mod to interactive window move.
v0.1.10
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.
Interactive window moving
While not full-blown floating window support quite yet, this is an important step towards that. You can now move windows by dragging them by title bars, or anywhere while holding Mod.
niri-interactive-move.mp4
To prevent accidental layout changes, the windows rubber-band a little before you drag them out.
niri-interactive-move-rubberband.mp4
Furthermore, I made both interactive moving and resizing work on a touchscreen.
niri-touch-move-resize.mp4
Thanks to @Pajn for implementing a fairly complete proof-of-concept of this feature!
Locked pointer location hint
@sodiboo implemented the pointer location hint request. Apps like Blender use it to tell the compositor the final location after a locked pointer movement so that the compositor can update its own pointer location to match it.
niri-pointer-position-hint.mp4
Laptop lid and tablet mode switch bindings
Thanks to @cmeissl, you can now bind commands to laptop lid opening/closing and tablet mode switching. You can use this to automatically enable an on-screen keyboard when a convertible laptop enters tablet mode. See the switch events wiki page for more information and examples.
Additionally, I implemented disabling of the internal laptop monitor when closing the lid. So your workspaces will automatically move to the external screen. If for some reason this breaks for you, set the new keep-laptop-panel-on-when-lid-is-closed
debug config flag.
Pointer hiding
@yzy-1 implemented new cursor hiding options: hide when typing (on any key press), and hide after a set inactivity period. See the wiki page for more details.
cursor {
hide-when-typing
// Or, after a timeout:
// hide-after-inactive-ms 1000
}
To complement this, there are a few improvements to the hidden pointer behavior. The pointer will now show up on mouse button press, and on the contrary, it will stay hidden on programmatic and keyboard-triggered movement, like focusing a different monitor, or when using warp-mouse-to-focus
.
Input configuration improvements
Thanks to @tazjin, @chillinbythetree and @elipp for:
- Adding a
trackball
input config section. - Adding a
scroll-button
setting to mice, touchpads, trackpoints, and trackballs. - Adding a
scroll-factor
setting to mice and touchpads that you can use to speed up or slow down scrolling.
See the input config wiki page for more information.
Other improvements in this release
- Tablet input no longer follows the monitor rotation: you need to rotate your graphics tablet together with your monitor. This makes convertible laptops work properly; this is also how input works on other desktop environments. Thanks @cmeissl.
- The GTK Access portal is now explicitly set in
niri-portals.conf
, which makes it work. It is required for applications requesting PipeWire webcam and microphone access, such as the Firefox package on Fedora 41. Thanks @cmeissl. - The
niri-ipc
crate is now published to crates.io. - Active workspace is now preserved across monitor disconnects and reconnects.
- Added a window
--id
argument toniri msg action consume-or-expel-window-left/right
and to the IPC. - Added an explicit
power-on-monitors
action that can be useful with certain hardware. Niri still automatically powers on monitors on any input event. - Added support for running niri as a dinit service: files in
resources/dinit/
and corresponding code inniri-session
(thanks @markK24). - Added a
disable-monitor-names
debug config flag as a workaround for niri crashing when plugging in two monitors reporting the exact same make/model/serial. This issue is tracked in #734. - The focused window will now become visually inactive when a layer-shell app in front has keyboard focus.
- Fixed
focus-window-up-or-column-right
focusing left instead of right. - Fixed an animation jump when expelling a narrower window from a column with uneven window widths.
- Fixed the logind power key inhibit file descriptor leaking into processes spawned by niri.
- Fixed window close view position restoration triggering for windows that didn't get focused upon opening.
- Fixed a crash when an output disappears immediately after connecting.
- Fixed used xdg-activation token memory leak.
- Fixed lock screen clients hanging until a monitor is enabled when no monitors are enabled.
- Updated Smithay:
- Fixed memory leak when locking the screen.
- Fixed occasional visual freezing of GTK and other apps.
- Fixed a regression that made it so increasing the output scale in niri v0.1.9 didn't propagate to some clients, keeping them blurry.
v0.1.9
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.
Note
Packagers: niri now requires libdisplay-info.
New IPC functionality
In this release, I designed and implemented an event stream in niri's IPC which lets you continuously listen to compositor events like workspace or window changes. The event stream enables taskbar applications to make correct and efficient widgets for niri.
I implemented the niri modules for workspaces, focused window, and keyboard layout in Waybar, available in its fresh 0.11.0 release. Pull requests are open for yambar and ironbar thanks to their contributors.
niri-waybar-workspaces.mp4
IPC windows and workspaces now have unique IDs, and all individual window and workspace actions can address a specific window or workspace by its ID. On the command line, a new niri msg windows
command lists all windows with their IDs, and window commands accept an --id <ID>
argument to target a specific window, for example:
$ niri msg action fullscreen-window --id 2
Also, there's a new niri msg action focus-window --id <ID>
action and a new niri msg keyboard-layouts
command.
I wrote some documentation on the programmatic access to the niri IPC socket. I also set up an online rustdoc for the niri-ipc crate where I documented every IPC type and request. Please refer there when working with the niri IPC.
Unfortunately, while adding ID arguments to IPC actions, I discovered a backward incompatibility trap in serde-json. The default enum representation—externally tagged—prevents you from changing a unit variant to a struct variant, because the representation gains an extra dictionary. "FullscreenWindow"
becomes {"FullscreenWindow":{}}
, and the former does not parse with the new definition.
I decided to make a JSON breaking change, converting all unit Action
enum variants to struct variants (with or without fields). I doubt anyone used them directly through JSON since these actions could only address the focused window or column. All enum variants that already had fields are unchanged, and the niri msg
CLI is also unaffected.
With this breaking change out of the way, any further JSON additions should remain backward compatible, so that existing scripts and programs communicating with niri will keep working with new niri versions.
Height distribution changes
One common complaint about niri's layout was the ability to make a multi-window column not "add up" to the total height of the monitor. The behavior was also fairly unobvious: with two windows in a column, you resize one, and the other resizes along as expected. Then, you resize the other, but the first window doesn't react. It felt like a bug.
Last time there was a design problem (unwanted scrolling with focus-follows-mouse), we quickly found a solution by brainstorming in a Discussion. So, I made a big write-up about window heights in #593. While there hasn't been much discussion, the act of laying out in writing all considerations and constraints had spawned a potential solution in my mind, which turned out to work quite well.
In this release, I reworked the window height distribution to do the expected thing in more cases. A column of two or more windows will always try to match the monitor height, as long as the minimum window sizes allow that. Resizing one window will resize all other windows in a column proportionally. The window that you resized last retains its height just like before, which lets you size one window in a column exactly to fit something, unaffected by adding more windows into the column, or moving it across monitors.
Keep in mind that a single-window column can still be resized arbitrarily, including shorter or taller than the monitor. Until floating windows are implemented, this is necessary for some uses that require exact-sized windows.
niri-height-distribution.mp4
Additionally, I found and fixed a small issue where windows in a column would occasionally "snap" to a smaller size when resizing.
Preset window heights
@TheAngusMcFire implemented a preset-window-heights
layout option and a corresponding switch-preset-window-height
bind, which work like the existing column width presets.
By default, it's bound to ModShiftR, which is consistent with Shift making resize binds affect the height rather than the width. The default bind to resetting the window height therefore moved to ModCtrlR. (None of this affects you if you already have a niri config; you'll need to add any new binds manually.)
Output names
You might be familiar with this sight:
$ niri msg focused-output
Output "Unknown Unknown Unknown" (DP-1)
...
Thanks to @cmeissl finishing the libdisplay-info bindings, this sight is no more.
$ niri msg focused-output
Output "Acer Technologies XV320QU LV 420615FCD4200" (DP-1)
...
Following this, all throughout niri I implemented the ability to address outputs by name. This includes config output
, map-to-output
, open-on-output
; niri msg output
; wlr-output-management tools (wdisplays, kanshi); and xdg-desktop-portal-gnome screencasting where the screen selector will now show the monitor model and screencast session restore will remember the output name rather than the connector.
The recommended way to configure everything output-related is now by name (as shown in niri msg outputs
). This way, configuration does not depend on the connector name that can be non-deterministic with multiple GPUs or when using thunderbolt docks.
// Previously: output "DP-1" {
output "Dell Inc. Dell S2716DG #ASOwvAqQj0Dd" {
mode "[email protected]"
// ...
}
I was also finally able to change the monitor sorting order to use the output name rather than the connector name, once again making it more deterministic. Note that this may swap your monitor positions if you were using multiple monitors and haven't manually configured them.
Transactional updates
One of Wayland's premises is that "every frame is perfect" except the first one. The compositor is in full control of the display, and window state changes are atomic and correlate to specific compositor requests.
This allows the compositor to synchronize updates for multiple windows: render the old state until all windows update, then switch to the new state all at once, with no broken frame in between.
However, possible doesn't mean easy, and different kinds of transactional updates need different approaches in the code. For this release, I implemented two relatively common cases.
Resizing
Thanks to the scrollable tiling nature, niri doesn't need to synchronize resizes among all windows on a workspace. However, windows in one column must still resize in unison: they must have the same width, and their heights must add up exactly to the monitor height.
niri-synchronized-resizing.mp4
Closing
Closing a window resizes all other windows in the column to take up the freed space. Normally, resize and close animations hide this, but if you disable animations, the flicker becomes very noticeable. The closing transaction fixes this: niri waits until other windows have resized before hiding the closed window.
niri-close-transactions.mp4
On-demand VRR
Thanks to @my4ng, we now have on-demand variable refresh rate as a window rule.
Some monitors flicker at the lowest VRR refresh rate, some drivers have VRR bugs, and some clients don't handle VRR too well. Now, niri can enable VRR only when a specific window is on screen (for example, a video player, or a game), thereby avoiding most of those issues.
Configure your output with on-demand=true
:
output "Acer Technologies XV320QU LV 420615FCD4200" {
// ...
// This will keep VRR off unless enabled by a window rule.
variable-refresh-rate on-demand=true
}
Then, add variable-refresh-rate true
window rules as necessary:
// Enable VRR when mpv is on screen.
window-rule {
match app-id="^mpv$"
variable-refresh-rate true
}
NVIDIA flickering fix
There was a problem with NVIDIA flickering on niri, which the user could fix by enabling the wait-for-frame-completion-before-queueing
debug flag. Turns out, this was only necessary because ages ago I forgot to add a check in the code. 🤦
Starting from this release, you should no longer need to set that debug flag, and NVIDIA GPUs should no longer flicker on niri out of the box (fingers crossed).
Small UX improvements
The horizontal touchpad swipe gesture will no longer go past the first or last column on the workspace.
niri-horizontal-gesture-snap-limit.mp4
And focus-follows-mouse will no longer "catch" windows on workspaces as you're switching away from them, which is especially important when using the...
v0.1.8
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.
Today is a special day. Niri is one year old! 🥳
┌ (main) ~/s/r/niri
└─ git show --format='commit %H%nCommitDate: %cd%n%n %s' --no-patch ad3c3f8
commit ad3c3f8cefd38d2bf26b466d8e34eccde3bca443
CommitDate: Thu Aug 10 14:49:38 2023 +0400
Init from smallvil
We've come a long way since then! I am very happy with how niri is shaping up. I am especially grateful to 45 (!) contributors who have volunteered their time to improve something in the compositor over the year.
We also managed to amass more than 3000 stars, and almost 400 people in our comfy Matrix room!
Nevertheless, there's plenty to be done. Without further ado, here are the improvements from the last release.
Note
Packagers: niri now requires pango >= 1.44 and rust >= 1.77.
Gradient border color spaces
A big thanks to @CaliOn2 for overhauling gradient rendering in niri, adding support for interpolation color spaces! You can now set gradient borders to draw not just in srgb
, but also in srgb-linear
, oklab
and oklch
, the latter with support for {shorter,longer,increasing,decreasing} hue
.
Which means that you can now have beautiful rainbow gradient borders:
layout {
border {
active-gradient from="red" to="orange" angle=45 in="oklch longer hue"
}
}
As usual, niri gradients are rendered the same way as CSS linear-gradient()
, so you can use any browser tool to configure them.
Additionally, @my4ng debugged and fixed gradients rendering with a sharp edge on NVIDIA, and I fixed gradient rendering being reversed at angle=90
.
Screenshot UI pointer toggle
You can now toggle mouse pointer visibility in the screenshot UI by pressing P. I added a new help panel to remind you of this, and to explain how to capture the screenshot.
niri-screenshot-ui-panel.mp4
Also, the screenshot UI now fades in. (As usual, you can disable this animation if you don't like it.) Finally, I fixed some minor regressions with area selection that were introduced in the fractional scaling refactor.
Key repeat for binds
Thanks to @salman-farooq-sh, niri now has key repeat for all binds. This is especially useful for binds that control volume and brightness. Or for having some fun by spawning a ton of windows.
niri-key-repeat.mp4
You can disable key repeat for specific binds using the new repeat=false
property:
binds {
// Disable key repeat for this bind.
Mod+T repeat=false { spawn "alacritty"; }
}
Focus-follows-mouse improvements
Being a scrollable-tiling compositor, niri faces some unique design challenges for otherwise commonplace functionality. One particularly annoying example was unwanted view movement caused by focus-follows-mouse
.
When using niri, you will frequently have windows partially off-screen. With focus-follows-mouse
, moving the cursor over such a window would focus it and scroll it into view. This is especially problematic if you have two monitors side-by-side, and just want to move the mouse to the other monitor.
To figure out the solution, I outlined all problems and possible solutions in a GitHub discussion. Some very productive brainstorming followed, and a solution emerged: a view scroll threshold for focus-follows-mouse
.
In the config, you can now set a property on focus-follows-mouse
:
input {
focus-follows-mouse max-scroll-amount="0%"
}
This number controls how much scrolling is allowed to happen for focus-follows-mouse
to trigger. With 0% (the new suggested default), focus-follows-mouse
will only focus a window if it does not cause any scrolling. You can also set this to bigger values, e.g. 10% will restrict it to when the view scrolls no more than 10% of the screen width.
niri-focus-follows-mouse-threshold.mp4
I've personally been avoiding focus-follows-mouse
in niri in the past precisely because of the undesired scrolling, but max-scroll-amount="0%"
had pretty much solved that problem, so since then I've been using it just fine.
This release has another fix to focus-follows-mouse
: when using the always-centered mode, it will no longer cause rapid window scrolling. The issue was the way the logic interacted with pointer focus update suppression during animations.
wlr-output-management
@gmorer implemented the wlr-output-management protocol, which means that you can now use third-party tools like kanshi or wdisplays to configure the outputs in niri.
Keep in mind that changes applied this way are transient and are not automatically saved into your niri configuration, just like the niri msg output
command.
wlr-screencopy version 3
Niri had supported wlr-screencopy version 1 since v0.1.3. This was enough for screenshot tools like grim, but not for screen recording tools. This was an intentional choice, as the screen recording parts of this protocol are quite complex, and need a very different implementation from the existing PipeWire screencasting.
For this release, @my4ng dived in and implemented it! Now, niri supports wlr-screencopy version 3 and you can use tools like wf-recorder and wl-mirror.
niri-wf-recorder.mp4
As a bonus, I found and fixed a bug in region capture with a fractional scale.
Negative struts
@salman-farooq-sh implemented a small change to allow strut
config values to go negative. It's not obvious at first why this is needed (why would you want windows to peek outside the screen bounds?), but actually, this is an elegant solution for having smaller outer gaps than inner gaps.
In niri, one of the design principles is that opening a new window never causes existing windows to resize. Turns out, this restriction prevents differentiating horizontal inner and outer gaps. Imagine inner gaps = 10 and outer gaps = 0. A single 50%-wide window should then take exactly 50% of the screen. But then, opening a second window introduces an inner gap, so now the first window must occupy (50% minus a half gap), requiring a resize!
Negative struts work around this problem. All gap values remain equal, but you can use left and right strut values equal to negative gap size to "push" the "outer" gaps off-screen. Visually, this looks the same as having no outer gaps, while not causing any unintended window resizes.
layout {
gaps 16
struts {
left -16
right -16
top -16
bottom -16
}
}
PipeWire screencast fixes
I implemented the full DMA-BUF modifier negotiation procedure for PipeWire screencasts (when using xdg-desktop-portal-gnome). As an immediate benefit, it makes screencasting work on NVIDIA. It should also fix GStreamer-based screen recording tools like Kooha, however, I have not been able to get this to work just yet. Perhaps it'll start working with the next PipeWire release? We'll see.
Wiki configuration snippet tests
This is not directly related to running niri, but is very cool nonetheless! @Suyashtnt implemented a test that verifies that every single config example on the wiki successfully parses. Combined with the fact that every single config option has an example code block, we should be very much set to catch any unintentional config parsing regressions.
Nightly COPR
With some help from @my4ng, I set up a COPR with automatic git niri builds: https://copr.fedorainfracloud.org/coprs/yalter/niri-git
Turns out, this is quite easy, and involves adding a special template .spec file into the repository, and setting up a webhook so GitHub can tell COPR to trigger a build when the main branch is updated.
If you run Fedora, you can use this new COPR to stay more up-to-date with niri development.
Other improvements in this release
- Implemented on-demand keyboard focus mode for layer-shell surfaces, which is used by some newer bar applications.
- Added
focus-window-or-monitor-{up,down}
actions (thanks @TheAngusMcFire). - Added
move-column-left-or-to-monitor-left
andmove-column-right-or-to-monitor-right
actions (thanks @brainlessbitch). - Added a
middle-emulation
flag to touchpad, mouse, and trackpoint settings. - Added a
background-color
option to outputs that sets the color of the default niri solid background. You can use this if you don't want to run any third-party background tools. Thanks @anant-357! - Added
Mod3
/ISO_Level5_Shift
modifier support to key bindings (thanks @jpeeler). - Enabled sub-pixel glyph positioning for better kerning in niri panels.
- Added a
profile-with-tracy-ondemand
build feature that produces a build with on-demand Tracy pr...
v0.1.7
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.
Fractional Scaling
The big update this time is fractional scale support. You can set output scale to fractional values like 1.5 and automatic scale factor guessing will now return fractional scale factors.
On the surface this sounds simple, but under the hood, doing it properly required a complete refactor of the layout system to use fractional coordinates and sizes (and then chasing down all of the bugs caused by this).
The result is well worth it though. Borders, gaps and windows are always physical-pixel aligned, and not restricted to integer logical pixel positions. There's no blur or position-dependent +-1 px jank. Fractional-scale-aware clients remain crisp at any scale.
Here's a demo of going through every single currently representable fractional scale factor between 100% and 200% where everything remains crisp, including a 1 px checkerboard in mpv. Watch it in the native 1920×1080 resolution if you want to see the checkerboard correctly.
niri-scale.mp4
As a bonus, you can set the scale to a value below 1, which will make things smaller and give you more space. This could be useful in specific cases like monitors with very big pixel size, but it will lose you some image crispness.
Fractional Layout
As previously mentioned, niri layout now completely operates in floating-point. While fractional scaling benefits the most from this, fractional layout is also useful for integer scales.
Concretely, you can now set border and focus ring width, gaps, struts to fractional values, which will round to physical pixels according to the monitor's scale factor. Which means you can have 1 px wide borders on a 200% monitor for example by setting the border width to 0.5.
The view position is also no longer restricted to integer logical pixels, so when you do a touchpad swipe gesture on a 200% monitor, windows will move in single physical pixel increments.
If you're interested in the technical details of how this works, check this wiki page.
Window Screencasts
You can now select an individual window to screencast through xdg-desktop-portal-gnome. You can resize windows, open pop-ups, use block-out rules, and it will all work correctly.
niri-window-screencast-2.mp4
This involved some refactoring of the PipeWire screencasting code in niri, most notably adding support for changing the video stream size on the fly. As a bonus, monitor screencasts will now also keep running through monitor resolution changes.
I still need to work out some details like frame callback delivery to obscured windows, but the current implementation should already work for a lot of use cases.
xdg-activation
@pcc implemented the xdg-activation-v1 protocol which allows apps to pass focus to other apps. For example, clicking on a link in a GTK 4 app will now automatically focus your browser, switching the workspace if necessary.
niri-xdg-activation.mp4
This protocol is also used by clients to indicate urgency; this part is not implemented yet (but planned).
Workspace Switch Mouse Gesture
Last release I added the horizontal Mod + middle mouse drag gesture to scroll the view. This release I also added the vertical drag gesture to switch workspaces, just like on a touchpad you can swipe both horizontally and vertically.
niri-vertical-mouse-gesture.mp4
Other improvements in this release
- Added four actions
focus-window-{up,down}-or-column-{left,right}
which allow traversing all windows on a workspace in order (thanks @minego). - Added actions
focus-column-right-or-first
,focus-column-left-or-last
which allow the focus to loop around (thanks @sullyj3). - Added actions
focus-column-or-monitor-left
andfocus-column-or-monitor-right
that switch the monitor upon reaching the end of the workspace. - Added
niri msg focused-output
which returns information about the currently focused output (thanks @rustysec). - Added
off
flag to disable input devices (thanks @yuja). - Added
left-handed
flag to touchpad, mouse, tablet input config. - Added
scroll-method
property to touchpad, mouse, trackpoint input config (thanks @yuja). - Added
disabled-on-external-mouse
flag to touchpad input config (thanks @yuja). - Niri now additionally reads the config file path from
$NIRI_CONFIG
, to help with nix wrappers. The--config
flag still takes precedence (thanks @sodiboo). - Changed absolute pointer input to work over a union rectangle across all outputs, rather than picking a single output (thanks @galister).
- Changed tablet input without a specific
map-to-output
to map to a union rectangle across all outputs. This makes Open Tablet Driver work. - Changed foreign-toplevel (i.e. Waybar) window activation to animate the workspace switch.
- Changed output scale setting to no longer require the fractional part, i.e.
scale 2
will work. - Fixed
focus-window-or-workspace-{up,down}
missing the workspace switch animation. - Fixed empty named workspaces disappearing upon output removal.
- Fixed a crash when an (already unfullscreened) window that in a column with other windows requests to be unfullscreened.
- Fixed key repeat not working when the
keyboard
config section is missing. - Fixed some crashes when no outputs are connected. On some devices outputs reconnect themselves upon resuming from sleep, which was triggering these issues.
- Fixed rounded corners rendering blurry on very high scale factors.
- Fixed the automatic draw border with background check to also include the KDE decoration protocol value. This makes it work for some older clients like GTK 3 (thanks @kchibisov).
- Fixed ISO_Level3_Shift modifier not showing up in the Important Hotkeys list.
- Niri now increases the fd limit to the maximum, fixing some fd-heavy clients (e.g. running RustRover in Xwayland).
- Updated Smithay, which fixes running on the NVIDIA 555 driver (explicit sync is still not implemented for now).
v0.1.6
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.
We've now got a small setup showcase thread, be sure to check it out!
And here are the improvements from the last release.
Gestures
In this release, I added mouse gestures for resizing and scrolling the view. I also made a wiki page listing all existing gestures.
Interactive Window Resizing
You can now resize windows interactively with a mouse (yes, finally). Both by edge-dragging windows with client-side decorations, and anywhere on a window by holding Mod together with the right mouse button.
To complement this, there are two new double-click gestures: double-clicking a resize will expand the window to the full monitor width, or reset the window height to take up all available space, depending on the edge that you double-click. Thanks @FreeFull for suggesting these gestures!
Resetting the window height is also available as the new reset-window-height
key binding.
niri-interactive-resize.mp4
Despite the ubiquity, interactive resizing proved quite tricky to implement with plenty of edge cases (tiling makes it harder since multiple things need to coordinate together). The main challenge stems from the fact that when resizing a window by the left edge, its right edge should stay in place, which means that the window itself must move to the left, strictly in sync with changing size. Throw into the mix slow windows (the red rectangle on the video), windows not strictly obeying the given size (e.g. terminals snapping to the cell grid), and multiple windows in a column (which must all resize together), and you've got a wild asynchronous cocktail.
There was even a Chromium bug involved in this one, and a similar Firefox issue is waiting on a recent GTK 3 update.
Mouse View Scrolling
Holding Mod and the middle mouse button (scroll wheel) will now let you scroll the view. This uses the touchpad swipe gesture code with all its decelerated spring animation goodness, but makes sure that the spot that you "grabbed" stays locked to the mouse cursor.
niri-mouse-view-gesture.mp4
Functionality
This release also adds some nice new functionality.
Named Workspaces
You can now declare named workspaces in the config.
workspace "browser"
workspace "chat" {
open-on-output "DP-2"
}
Unlike normal (dynamic) workspaces, named workspaces are persistent (they are not deleted when they have no windows), but otherwise they behave just like normal workspaces: you can reposition them and move to different monitors.
Actions like focus-workspace
or move-column-to-workspace
can refer to workspaces by name in addition to by index. Also, you can use the new open-on-workspace
window rule to make a window open on a specific named workspace:
// Declare a workspace named "chat" that opens on the "DP-2" output.
workspace "chat" {
open-on-output "DP-2"
}
// Open Fractal on the "chat" workspace.
window-rule {
match app-id=r#"^org\.gnome\.Fractal$"#
open-on-workspace "chat"
}
You can find a few more details on the wiki page.
Named workspaces should mostly solve the "shove a bunch of windows on correct monitors at startup" problem while working seamlessly with the dynamic workspace system. Thanks to @algernon for implementing this!
IPC Improvements
The new niri msg output
command lets you apply transient output configuration changes. It uses the same syntax as the config file, e.g. niri msg output eDP-1 scale 2
. These changes will persist until you edit the output settings in the config file (or restart niri).
While adding this, I also made output names case-insensitive, both for niri msg output
and for the config file, which should make things less annoying.
Additionally, @rustysec added a niri msg workspaces
command which will be extra useful now with the introduction of named workspaces:
┌ ~
└─ niri msg workspaces
Output "DP-2":
1 "chat"
2 "browser"
* 3
4
Output "eDP-1":
1 "notes"
* 2
3
Like with other IPC commands, you can use the --json
flag to get the same data in a machine-readable form.
New Window Rules
You can now set focus-ring
and border
properties in window rules to override them for specific windows.
The new is_active_in_column
matcher, added by @TheZoq2, can be used to make a magnifier-like window layout:
window-rule {
match is-active-in-column=false
min-height 100
max-height 100
}
Finally, the new at-startup
matcher will match during the first 60 seconds after niri startup. You can combine it with open-on-output
or open-on-workspace
properties to put windows where they belong when starting the session, but not afterward. I found it quite useful for e.g. browsers where I want new windows to open normally as I go on with my day, rather than keep spawning on the same monitor and workspace.
// Open Firefox maximized on the "browser" workspace, but only at niri startup.
window-rule {
match at-startup=true app-id=r#"^org\.mozilla\.firefox$"#
open-on-workspace "browser"
open-maximized true
}
Debugging Features
There are a few new debugging features:
-
The
debug-toggle-opaque-regions
bind will draw regions marked as opaque in blue and others in red. -
The
debug-toggle-damage
bind will draw the damage computed for the screen. Kind of, mostly. Good enough to tell when something wrong is going on. -
The
disable-direct-scanout
flag disables direct scanout to the primary and the overlay planes.
Eye candy
Of course, there are also new eye candy features!
Rounded Window Corners
Niri can now do corner rounding, a clear must-have feature for any self-respecting Wayland compositor. I've got quite an extensive implementation here, actually. Let's take a look.
You set the radius with the new geometry-corner-radius
window rule.
By itself, it doesn't clip the window but merely informs elements like the border and the focus ring which window radius they should assume. This means that you can keep using client-side-decorated windows with their own rounded corners and shadows, and have the borders drawn with the right radius.
window-rule {
geometry-corner-radius 12
}
This sets the radius of the window geometry—the inner radius of the border. The outer border radius is computed automatically taking the border width into account.
You can even set a separate radius for every corner, for example, to match GTK 3 applications:
window-rule {
match app-id="^gnome-terminal-server$"
geometry-corner-radius 8 8 0 0
}
Next, the new clip-to-geometry
window rule will make niri actually clip windows to their geometry, including the geometry-corner-radius
that you have set.
window-rule {
geometry-corner-radius 12
clip-to-geometry true
}
Combine this with prefer-no-csd
to get the classic rounded corner setup that works on all windows:
prefer-no-csd
layout {
focus-ring {
off
}
border {
width 2
}
}
window-rule {
geometry-corner-radius 12
clip-to-geometry true
}
All of this works correctly with subsurfaces, windows blocked out from screencasts, transparency, resize and other animations. And whenever possible, there's no overhead: opaque regions are preserved (except for the corners themselves), and even overlay plane unredirection still works for subsurfaces completely inside the clipped geometry!
Tricky Cases | Opaque Regions | Unredirection |
---|---|---|
![]() |
![]() |
![]() |
Screen Transition
I added a do-screen-transition
action which lets you switch between light and dark, or between different themes, smoothly like in GNOME Shell.
niri-screen-transition.mp4
The key is to make sure the applications themselves switch their theme without animation and as fast as possible, then niri's own screen transition will make it look nice and synchronized.
If your apps take just a...