If you’re looking to use Tarme and how to configure it, please refer to the git
repo linked above
This post is more of a backstory + retrospective dev-log
What is the i3bar-protocol?
Most desktop setups will offer some sort of bar or panel to display some information and a number of actions at all times, usually at the edge of the screen. Some of those panels will handle all the work themselves: gathering information, formatting it, displaying it, windowing tasks such as reserving a chunk of space at the edge of the screen, handling click events etc.
i3wm did things slightly differently. Instead of shipping a monolithic panel, it split the job into two components: the bar (i3bar) and the status generator (i3status). The responsibilities of the two components, along with how the two are to communicate, are defined in the i3bar-protocol.
The panel has limited responsibilities: windowing, catching click events, and displaying the status as dictated by the generator. The status generator does everything else: gathers information, formats it, dictates some metadata as to how the status is to be displayed by the panel, and handles click events.
Since the panel is effectively a thin client for the status generator, there’s little one could change about it besides supporting a different windowing system without introducing changes or additions to the protocol, as such there are only two main panels implementing the protocol: the reference implementation, i3bar, and its Wayland clone, swaybar.
The real magic happens in the status generator, which leaves a lot more room for implementations to go wild. A fact not really showcased by the reference implementation, i3status, which is, for lack of a better word, anemic. It’s functional and does what it needs to, but it’s not pretty, and even less configurable. This gives the entire protocol a rather bad reputation of being anemic, but I’m willing to make the argument that reputation is undeserved.
But that’s why there’s a protocol instead of a monolithic panel: to allow people to make their own, fancier implementations!
Why not use an existing status generator?
Initially I used i3blocks, a popular replacement for i3status which offers far more options for customizing the contents and visuals of the status info. It also offers a large collection of ‘blocklets’ which make it easy to customise it without requiring knowledge of programming, but it can still prove rather limiting for some. In order to make it easy for programmers of all backgrounds to make blocks, each block is an external program, communicating with i3blocks over stdio (although most of that information is just passed along to the panel with few to no changes made by i3blocks). This design allows for each block to be written in a different language, but also leads to each block being a different process, and with no system to communicate or share resources with other blocks beyond what is already present on a UNIX system, it leads to some setups being much harder or even impossible to achieve with i3blocks.
This isn’t an issue for most, as i3blocks' popularity can attest, but it did leave me wishing for something less restrictive.
The first lines of code…
Initially I used i3blocks with whatever I found to my liking from random repos on Github, the result was a bit mismatched but it did what I needed it to. Unfortunately, I couldn’t find blocks for some things, so I had to code some blocks of my own anyway. Initially I had a number of blocks written in Bash, Python, and AWK, but I quickly ran into issues with them. The fact each block had to be started as an external process for each update limited me somewhat, I could only do so much before the overhead of constantly spawning new processes became too much, a fact exacerbated by the blocks being written in interpreted languages, and the blocks themselves doing plenty of external calls of their own to gather some of the information needed. Another issue I had was the blocks themselves were pretty poorly made, as they were mostly five minute hackjobs done during breaks in uni, or at 3am while barely awake. As such I decided to rewrite them.
At the time I was also learning Scheme, so that was the language I went with for the rewrites. It gave me all the fancy tools I’m used to from Python and then some, but unlike Python, Scheme is compiled (or CHICKEN Scheme is, anyway). This gave me a lot more room to stretch as even the most expensive operations I could do weren’t expensive enough to be a problem.
So, I got to work, ported my own custom blocks, all far fancier than before, but also faster, a fact likely helped by the fact I actually spent more than five minutes per block, and actually spent the time to test for edge cases.
When all was done, I had some fancy new blocks. While writing them, I also had some ideas for fancier things I could do, but quickly I realised most of them would quickly run into the limitations imposed by i3blocks (which I had also became far more aware of while working on the blocks). So I did what any programmer who has fun would do: started reading the i3bar-protocol documentation to write my own implementation of the status generator.
Going all in
The protocol is concise without skimping on any important details, so pretty quickly I got to work and that same day I had a working prototype, one that barely resembles the current code but one that could print the famous “Hello, World” to swaybar (did I mention I’m using Sway? Prob not, it’s not very relevant to the rest of the story tho). The road to the first build that was actually useful was a bit bumpy, ran into all sorts of peculiarities of UNIX and Scheme, but after a few days of head scratching and just trying stuff out to see what works I had something that worked enough so I could start working on the blocks. First I ported the blocks I had already written for i3blocks. Those were easy, return the information instead of printing it to stdout. Once I was done with those, I got to work to more blocks.
Most blocks were pretty straightforward so there’s not much to write about
them, although some did demand changes to the core of Tarme (the initial design
was rather limited). Other blocks were a bit more work, usually when gathering
the information. Some of the information I needed was easy to acquire,
Bluetooth info from bluetoothctl
, network connection info from nmcli
,
memory and swap from free
. Other information sources were a bit more finicky.
acpi
is a command line utility found on most Linux systems originating from
the Debian project, and it is a great way to get power and thermal information
about the system. Great if you’re a human using it interactively, that is. If
you’re trying to parse its output, you’re in for a lot of pain. Its output
shifts constantly, uses separators inconsistently, information appears and
disappears, some information is written with a variable number of words. Once
again, most of this isn’t a problem when using the tool interactively, but the
aforementioned issues make it a poor choice to get information from for another
program. Luckily, I only needed battery information, and that is easily
readable from /sys/class/power_supply/bat*
(a fact I didn’t know before
working on Tarme but which has come in useful since I learned it)
But how does it actually work?
Now that I described how this project came to be, and went over some of the more notable things that happened during development, I want to go a bit over how Tarme works for anyone who doesn’t want to go over the source code.
The i3bar-protocol uses JSON for communication between the panel and the status generator, and this ended up influencing the way Tarme works quite a bit. Each status update is represented by an array of objects, each object representing one block. With that in mind, the list of blocks is represented by a Scheme list internally, with each update iterating over the list and calling each block’s update lambda (if present). The JSON object sent to the panel is represented within Tarme by an association list (alist) which is generated from 3 parts: global defaults, block defaults, and the result of the update lambda.
The global default values are defined in a form at the top of Tarme’s configuration file (which is itself a Scheme file), and are applied to all blocks that don’t set the values for themselves.
Each block then has its own set of static values, which shadow the global defaults, and are always set for the block, unless overridden by the update lambda.
The update lambda is the last in the chain. It must return either an empty list or an association list, with all properties present in this alist overriding both the global and block defaults for that update (if the update lambda returns an alist without a property it returned previously, the block reverts to what was defined in its static values, the global defaults, or the panel’s default values if the former 2 sets of properties don’t define anything)
And one last touch I added: Pywal integration.
For those unfamiliar: Pywal is a popular tool for generating colour schemes from the wallpaper and applying them to as many applications on your system as possible. One feature Pywal supports to help with that goal is custom templates: you give Pywal a template file, telling it how to structure the colour scheme for various applications, and Pywal will generate a working scheme using that template. Tarme ships with one such template in the Git repo, so just add it to Pywal’s list of templates, generate a scheme, and load it from Tarme’s configuration file! After that, Tarme can use fancy colours without them ever falling out of sync with the rest of the system, if you use Pywal for the whole system, that is.
Was it worth it?
Yup. I may not get international acclaim from Tarme, but I learned a lot while working on it, and got a fancy panel for my Sway setup as well! (pictured at the top of the article)
All in all, I think it was a worthwhile project.