Super ultrawide Niri
After having used practically the same xmonad configuration for a decade and a half I’ve now modernized my setup with the scrollable-tiling Wayland compositor Niri. It’s been a bit of a struggle to unlearn my old workflow but I’m really growing to love Niri’s scrollable workflow, especially on my new super ultrawide display.
Samsung Odyssey Neo G9 G95NC 57”
What kicked off my Niri journey was the purchase of a new super ultrawide monitor.
I bought the 57” Odyssey Neo G9 as it was the largest monitor I could find. (It’s marketed as a “gaming” display but it’s really an amazing productivity display.)
It replaced my old 3-monitor setup:
I was debating whether to replace the center 31.5” monitor or replace all monitors with a single one but I think I made the right choice with the ultrawide. The curvature wasn’t an issue (I’ve come to prefer it) and the extra vertical space the portrait side monitors provided wasn’t as crucial as I thought.
I think an ultrawide is worth it just to get rid of the annoying bezels. Small things can be a big thing sometimes.
A more dynamic workflow
xmonad and Niri are similar yet different. Both automatically lay out windows as you spawn them but xmonad (at least the way I used it) follows a layout algorithm that re-flows using a “master” window and combines the rest of the windows into one space, while Niri lays out windows in columns.
The change is subtle but it implies that a new window won’t change the size of other windows. This is very nice if you spawn a lot of short-lived terminals or web browsers like I do and it reduces the amount of manual reshuffling I spend time on.
My xmonad workflow was more static than my Niri one.
In xmonad I made heavy use of workspaces, mapping ten workspaces mentally to different programs, such as 0 Firefox and 1 terminal logs on the left monitor; 3, 4 and 5 for different Neovim instances on the center monitor; 8 as chat and 9 for music or video on the right monitor.
I had no rules to enforce this; it’s an emergent behaviour that served me well for years.
With Niri it’s more dynamic. I still use workspaces but they no longer have direct shortcuts, I simply go up/down in the workspace list. Maybe I’ll add them in the future but with 3–4 workspaces that’s not as necessary. I spawn workspaces/windows when I need them and remove them when I’m done.
Usually it’s one workspace per project (yes, I’m now one of those who have multiple up at once) with all the related things such as editor, terminals, and browser with docs. I don’t typically utilize the full screen width and I try to keep the things I’m working on in the center, often leaving 10–30% gaps on the sides. Even though I don’t normally use the “endless scrolling” feature of Niri I re-center selected windows all the time so I can look straight ahead as much as possible.
Keyboard shortcuts
As a fan of keyboard layouts of course I have to spend some time tinkering with good keyboard shortcuts (especially as Niri’s recommended keybinds don’t map well with my custom keyboard or custom layout).
Regular keymaps
These are triggered in the “normal” way by first pressing the Super combo and then another key on the base layer (I use autoshift so I shift with a long press).
Window management
-
Super+Ftoggle windowed fullscreen (keep column width) -
Super+Shift+Ffullscreen window (over the entire display) -
Super+Mmaximize column (moves other columns)
Run stuff
-
Super+Enterterminal -
Super+ENoctalia’s launcher (also exists on the navigation layer asLaunch) -
Super+Sshow Noctalia control center -
Super+Shift+Sshow Noctalia settings -
Super+Qpower off monitors (they wake on input) -
Super+Shift+Qshow Noctalia session menu (reboot etc) -
Super+Shift+Llock screen
Misc
-
Super+Hshow hotkey overlay -
Super+Pinteractive screenshot -
Super+Shift+Pscreenshot selected window
Tweaks to the standard CachyOS setup
In the process of moving from xmonad to Niri I also moved from Void Linux to CachyOS and I let the installer install Niri and give me a basic configuration together with Noctalia (that provides a statusbar, notifications, and a bunch of things you apparently need).
Center the status bar and other Noctalia windows
Feels absolutely required on this screen otherwise things end up in the corners.
Firefox on XWayland
Force Firefox onto XWayland as the Wayland popup manager is broken:
environment {
MOZ_ENABLE_WAYLAND "0"
}
Dead keys for Ghostty
For some reason dead keys were broken in Ghostty.
This is bad for me as the OS keyboard is set to Swedish and it uses them to type ~ (quite a crucial character for a programmer).
The fix:
environment {
GTK_IM_MODULE "ibus"
QT_IM_MODULE "ibus"
XMODIFIERS "@im=ibus"
}
This needs ibus installed and running.
Melange colorscheme
Noctalia discovers custom color schemes under ~/.config/noctalia/colorschemes/<Name>/<Name>.json, so I dropped in my trusty Melange colorscheme there:
},
},
}
}
}
Then pick the colorscheme:
"colorSchemes":
}
Layout appearance
The default appearance was pretty I admit but way too much blank space and weirdness. Some tweaks:
layout {
// Required for noctalia-shell to set wallpaper
background-color "transparent"
// Never auto-center focused columns (too much movement)
center-focused-column "never"
// But do center a single window
always-center-single-column
// No extra space around it all
struts {}
// No gaps between windows
gaps 0
// The focus ring was annoying
focus-ring {
off
}
// Use a border with consistent width for all windows instead
border {
on
width 2
active-color "#ebc06d"
inactive-color "#403a36"
}
// Setting widths is important with such a large screen
preset-column-widths {
proportion 0.15
proportion 0.3
proportion 0.4
}
default-column-width { proportion 0.15; }
// Heights too, why not?
preset-window-heights {
proportion 0.15
proportion 0.5
proportion 1.0
}
}
// Prevent the mouse from opening the overview in the corners
gestures {
hot-corners {
off
}
}
Keep windows centered
Niri has the always-center-single-column option, which is nice as I want to keep as much as possible in the center of the monitor when I’m working.
But I very frequently use 2–3 smaller windows and with my frequent opening and closing I’d like them centered too.
Luckily, Niri has an IPC you can use to make a small program that reacts to events and does this for you. I made a small rust project using the niri-ipc crate that does this for me:
The autocenter implementation
[dependencies]
niri-ipc = "26.4.0"
use HashMap;
use io;
use Socket;
use ;
;
;
;
workspace: ,
width: f64,
let mut socket = connect?;
if !matches! eprintln!;
exit;
let mut known: = new;
let mut read_event = socket.read_events;
loop let result = match read_event? // A full snapshot of the current state. Just refresh our state.
WindowsChanged => known = windows
.into_iter
.map WindowId,
WindowState workspace: w.workspace_id.map,
width: w.layout.tile_size.0,
,
.collect;
Ok
WindowOpenedOrChanged => let workspace = window.workspace_id.map;
let entry = WindowState workspace,
width: window.layout.tile_size.0,
;
let prev = known.insert;
match prev // Don't center floats.
_ if window.is_floating => Ok,
// New window, try to re-center.
None => maybe_center_new,
// Window changed workspace, try to re-center.
Some if state.workspace != workspace => center_focused_if_fits,
// Skip other things.
Some => Ok,
WindowClosed => if known.remove.is_some center_focused_if_fits
else Ok
WindowLayoutsChanged => // Only re-center if the width was changed, otherwise our re-center will
// loop back indefinitely.
let mut resized = false;
for in changes if let Some = known.get_mut if .abs > 0.5 state.width = layout.tile_size.0;
resized = true;
if resized center_focused_if_fits
else Ok
_ => Ok,
;
if let Err = result eprintln!;
/// Center a newly created window if the workspace is focused and if there's surrounding free space left.
let Some = window.workspace_id.map else return Ok;
;
let Some = focused_workspace? else return Ok;
;
if WorkspaceId == workspace_id center_if_fits?;
Ok
/// Center windows in the focused workspace if there's surrounding free space left.
if let Some = focused_workspace? center_if_fits?;
Ok
/// Center windows in the workspace if there's surrounding free space left.
let Some = workspace.output.as_deref.map else return Ok;
;
let Some = output_width? else return Ok;
;
if workspace_width? < f64from center_visible_columns?;
Ok
/// Issue a one-shot query to Niri, wait, and return the response.
match connect?.send? Ok => Ok,
Err => Err,
/// Get the focused workspace.
match query? Workspaces => Ok,
_ => Ok,
/// Get the width of an output (monitor).
match query? Outputs => Ok .get
.and_then
.map,
_ => Ok,
/// Calculates the width of all columns in the workspace.
let Windows = query? else return Ok;
;
let mut columns: = new;
for w in windows if w.workspace_id.map != Some continue;
if let Some = w.layout.pos_in_scrolling_layout let width = columns.entry.or_insert;
*width = width.max;
Ok
/// Send a command to center the visible columns.
if let Err = connect?.send? eprintln!;
Ok
One catch is that if a new window overflows the monitor width, the script won’t center the columns even if there would be free space left afterwards.
This is a little weird but it’s consistent with Niri’s center-visible-columns command.
I had a small itch to try to hack around it but in the end I left it alone…
Is Niri worth it?
Yes, absolutely. Niri has been a huge upgrade for me in combination with a single wide screen. My xmonad setup worked really well with three monitors—arguably a better fit in that context than Niri—but for the big-screen use-case Niri is superior.
I’m curious how it holds up on my laptop, once I gather enough energy to install CachyOS on it… But that’s a side quest. The big-screen setup I spend most of my days in is the best I’ve ever had, and I have no desire to go back.