Niri, Waybar, and Rust
As a long term believer in it being the year of Linux on the desktop, I’ve been using Sway for several years as my main compositor and, by extension, desktop. Like a lot of people, I combine that with Waybar to provide some basic desktop environment functionality like a system tray, clock, etc.
For the last few weeks, I’ve been trying out Niri instead. Niri is another Wayland compositor that takes inspiration from a project I haven’t tried called PaperWM to implement a “scrollable tiling” environment. I’ve been enjoying it: my Sway setup is generally to have one app per workspace, with one “chat” workspace in tabbed mode with whatever communication apps I have running at any given moment.1
I haven’t 100% decided if I’m going to stick with Niri or go back to Sway
(truthfully, I’ll probably switch back and forth for a while), but one gap I
felt pretty quickly in Niri is that the only option for a running app list in
Waybar is the wlr/taskbar
module. Happily, Niri does implement
the required Wayland protocols for this to work, but less happily, apps
essentially show up in random order.2 It’s possible to drag to reorder
the apps, but that’s obviously a manual process, and because I’m crazy I
like things being ordered, I would like everything to be in the same order it’s
on screen: first workspace first, then leftmost window first.
An aside: why wasn’t this an issue for me in Sway, you might ask? That’s because I hacked some terrible code that I’m embarrassed of and hence have never released to rename workspaces based on their active window title3, which then meant that the
sway/workspaces
Waybar module got me close enough to what I wanted.
So, I figured, could I create a Waybar module to do what I wanted?
Waybar has long supported custom modules, which are useful, but limited in what UI they can create: it’s basically just a single label with a single tooltip. That isn’t sufficient here.
About a year ago, I became aware of Waybar’s cffi
module, which
allows you to implement Waybar modules in anything that can build a shared
library with a C API. You get a Gtk 34 container, and can then do
whatever you want within that. I threw together a rough Rust binding, couldn’t
get one of my demos to work5, and then left the code to rot in a
directory.6
Now, though, I had real motivation to get something useful together, so I blew
the dust off the bindings, fixed the bugs, brought them up to date with 2024
edition Rust, and have now published them as the waybar-cffi
crate. You implement a Module
trait, call a
macro, and you have a Waybar module! Obviously I’m biased, but I think
the Hello World example is pretty nifty.
Could I implement the taskbar of my dreams using that?
No suspense here: the answer is obviously yes, otherwise I probably wouldn’t be writing a blog post.
This looks pretty similar to wlr/taskbar
out of the box: I like that module
in general, so there wasn’t much need to change anything. The main differences
are that windows and window events are synced via Niri’s IPC
protocol, and apps are sorted by workspace index, so they’re mostly
in the order they appear on my desktop.
The “mostly” above is because Niri doesn’t yet provide window geometry in its IPC, which means the taskbar can’t order apps within a workspace based on their position.7 In practice, this isn’t a huge issue for me because the window ID ordering basically works as a fallback, but I’d love to add that at some point.
The other main thing I implemented in the initial version is app highlighting, which you can kind of see on the Signal icon there. Basically, I’ve written the module configuration to allow this:
{
"cffi/niri-taskbar": {
"apps": {
"signal": [
{
"match": "\\([0-9]+\\)$",
"class": "unread"
}
]
}
}
}
Which translates to:
- If the app ID is
signal
, and - The window title matches the given regex8, then
- Add the
unread
CSS class to the window button.
At which point I can use Waybar’s normal styling support, built on top of Gtk’s CSS support, to apply a highlighted border.
This is particularly useful in Niri because, unlike Sway, Niri doesn’t currently implement a Wayland protocol to indicate that a toplevel window is “urgent” (or, put another way, has a notification pending). But, even on Sway, I actually prefer to have a middle ground quiet way of being notified that something might be pending.
Anyway, this is the dictionary definition of “works for me” right now. I don’t know if it’ll be useful to anyone else.
I do have some rough plans for the taskbar module. In no particular order:
- I haven’t even considered multiple outputs yet. That’ll probably need something.
- I don’t know if I want this, but some way of putting a border or number between workspaces for additional visual grouping would probably be nice.
- I wonder if it’s possible to fake urgency by peeking9 on notifications on D-Bus and highlighting those apps until they become focused.
But we’ll see how far I get on this. Like I said, I’m not even totally sure I’ll keep using Niri in the longer term, in which case the taskbar module would no longer be useful to me. Regardless, though, I’m glad to finally get the Rust bindings out for Waybar CFFI, and I hope those are more generally useful to others no matter what happens with the taskbar.
-
Signal and IRCCloud are permanent fixtures, Slack and Zulip are usually running unless I’m actively trying to disconnect, and then it’s usually whatever combination of WhatsApp, Messenger, and Discord I need on a given day. ↩
-
OK, so that’s not strictly true: they show up in window ID order, which right now basically matches startup order. (Although Niri explicitly documents that this is not to be relied upon, so who knows how long that will be true.) ↩
-
It’s actually worse than that: I also included some functionality to parse the window titles for chat applications and set the workspace title based on which apps needed attention. ↩
-
Waybar only supports Gtk 3 at present; the hold up on [Gtk 4 support][gtk4] is, as I understand it, system tray support. This was mildly annoying when I had to use fractional scaling, since Gtk 3 doesn’t support it natively, but honestly isn’t an issue for me now that I put a 2x screen in my Framework. ↩
-
This turned out to be because I’d forgotten literally everything I once knew about how Gtk and the Glib event loop work. ↩
-
I do this way too often. ↩
-
I spent quite a bit of time trying to figure out if there was a way to get that information out of a Wayland protocol instead of Niri, and I think the answer right now is “not really”. I may be mistaken! ↩
-
Which matches how Signal indicates unread messages — it appends
(N)
to the window title, whereN
is the number of unreads. ↩ -
I’ve played around with some of this before, and I have a horrible feeling that the actual way of doing this is implementing the entire notification interface and then delegating to whatever actual notification daemon you want, but I’d like to be wrong here. ↩