Replacing i3status for Some Reason
My i3status alternative, divebar, can be found on my gitlab.
Motivation
I recently bought a new laptop, installed Arch Linux, and set about making a pleasant desktop environment.
I started the process of getting my dotfiles in order, and chose sway as my window manager.
As usual, the i3status top bar looked irritatingly ugly. In the past, I've replaced it with a pile of bash held together with sellotape, but as this was a fancy new computer, and I was going to the trouble of installing arch properly and all, I decided to choose a more permanent solution.
The i3blocks project looks promising, but using repeatedly executed commands to build the status bar seems wasteful, and I ideally want more precise waiting mechanisms than simply "every n seconds".
Design
I chose the name "divebar" because it was funny to me, and I haven't googled to find out whether someone else thought of it first, because that would be upsetting.
I wanted the bar to be a configurable and extensible executable that is as self-contained as possible, and communicates with sway via the swaybar protocol.
As usual, I also chose to write this with async rust using tokio.
Implementing the SwayBar protocol in Rust
The swaybar protocol transmits infinitely long JSON arrays of objects in both directions.
- Stdin:
[ { click event }, { click event }, ...
- Stdout:
{ header }\n[ { bar status }, { bar status }, ...
The click events are produced by sway every time the bar is clicked, and the bar status objects are produced by the bar whenever it wants to update its contents.
Reading Click Events
To read click events from the stdin, we need to buffer the data into a structure implementing the BufMut
trait. A Vec<u8>
, is the obvious answer, as we need to store arbitrarily long JSON event objects in a single contiguous slice that can be processed by the serde_json
crate.
A simple struct can be used to approximate the behaviour of a VecDeque
while keeping the contents of the buffer contiguous at all times.
#[derive(Default)]
pub struct JsonBuf
{
buf: Vec<u8>,
consumed: usize
}
impl JsonBuf
{
async fn read<I>(&mut self, input: &mut I)
-> io::Result<()>
where I: AsyncRead + Unpin
{
// If we have consumed over half the buffer, but
// are trying to read more, empty the buffer and
// move all un-consumed bytes to the front.
if self.consumed >= self.buf.len() / 2
{
self.buf.drain(..self.consumed);
self.consumed = 0;
}
input.read_buf(&mut self.buf).await?;
Ok(())
}
fn get(&self) -> &[u8]
{
&self.buf[self.consumed..]
}
fn consume(&mut self, bytes: usize)
{
self.consumed = std::cmp::min(self.buf.len(), self.consumed + bytes);
}
}
From protocol.rs.
There should be no real performance penalty to not using a VecDeque
proper, as the JsonBuf
should be completely emptied after each click event, unless we are receiving more click events than we can handle.
Processing the JSON click event objects is a little awkward, as delimiting them requires a knowledge of JSON.
todo - Find out whether or not Sway puts newlines between JSONs!
Luckily, we do know that every JSON object must start with a '{'
character. This does not mean that every '{'
character corresponds to the start of a JSON object - but it does mean that before trying to parse any JSON, we can start by consuming every non-'{'
character. This should also give us a chance to re-synchronize in the case of malformed or corrupted JSON input from sway.
Once we've got a contiguous array of bytes starting with an '{'
character, the next problem is figuring out whether or not we have a complete JSON object. If we do not have a complete JSON object, we want to wait for more input, and if we have a complete JSON object, we want to consume the input up to the end of that object, but no further.
Luckily, serde_json
has The StreamDeserializer
struct that implements the behaviour we need to do this.
We can create a StreamDeserializer
structure from a normal Deserializer
, attempt to parse one object, and if successful, use the byte_offset()
method to find out how many bytes we need to consume.
async fn consume_next_event(&mut self) -> io::Result<Click>
{
loop
{
let json_buf = self.json.get();
let mut deser = serde_json::Deserializer::from_slice(json_buf)
.into_iter();
match deser.next()
{
// Successfully found an event
Some(Ok(clk)) =>
{
self.json.consume(deser.byte_offset());
return Ok(clk);
}
// The JSON object ended early!
None =>
{
self.json.read(&mut self.input).await?;
}
Some(Err(e)) if e.is_eof() =>
{
self.json.read(&mut self.input).await?;
}
// The JSON object was invalid!
Some(Err(e)) =>
{
// Discard the whole buffer!
self.json.consume_all();
// Return an error
return Err(io::Error::new(
io::ErrorKind::Other, format!("{}", e)
));
}
}
}
}
From protocol.rs.
Using this approach, we can robustly read click events from stdin.
Writing Bar status
It is much easier to write events to stdout than read them from stdin. We simply write the JSON, and manually insert commas and newlines where needed.
Implementing Blocks
The status bar is considered as a sequence of blocks, each displaying a different piece of status information.
The Content
trait
By separating the behaviour of statusbar content into a trait, we have the ability to add support for more than just swaybar in the future, and third party types of content are easy to add.
Currently, there are two traits that need to be implemented to define a new type of content:
pub struct Status
{
pub full: String,
pub short: Option<String>,
pub urgent: bool
}
pub trait ContentType
{
type Config: for<'de> serde::Deserialize<'de>;
type Inst: Sized + Content + 'static;
fn new_content(&self, config: Self::Config) -> io::Result<Self::Inst>;
}
#[async_trait]
pub trait Content: Sync + Send
{
fn is_status_cancellable(&self) -> bool { false }
async fn status(&mut self) -> io::Result<block::Status>;
async fn click(&mut self) -> io::Result<()> { Ok(()) }
async fn wait(&mut self) -> io::Result<()> { Ok(()) }
}
The ContentType
trait needs to be implemented for a type corresponding to a named type of content, and creates instances of that content type from a Config
structure.
The Content
trait needs to be implemented by instances of content:
- The
is_status_cancellable()
method hints whether thestatus()
method can be cancelled in order to handle aClick
event. - The
status()
method should return the current information to be displayed by the block in the status bar. - The
click()
method should handle a click by the user. - The
wait()
method should wait for the contents of the bar to change. This can always be cancelled.
By separating the wait()
and status()
methods, not only do we have the ability to improve user responsiveness by waiting in a cancellable method, and reading status in a non-cancellable method, but we can adjust the timing of calls to status()
if we want to override the behaviour of wait()
, with a "only refresh every N seconds" or "refresh at least every N seconds" config option for example
Example - The "time"
Content type
For example, the "time"
content type is implemented like this:
use super::*;
pub struct Time;
pub struct TimeInst;
const SHORT_FORMAT: &'static str = "%e %b %R";
const LONG_FORMAT: &'static str = "%a %e %b %y %T";
impl ContentType for Time
{
type Inst = TimeInst;
type Config = EmptyConfig;
fn new_content(&self, _: EmptyConfig) -> io::Result<TimeInst>
{
Ok(TimeInst)
}
}
async fn sleep_until_next_second()
{
let now = chrono::Local::now();
let millis = now.timestamp_subsec_millis() as u64;
let duration = Duration::from_millis(1000u64 - millis);
sleep(duration).await;
}
#[async_trait]
impl Content for TimeInst
{
async fn wait(&mut self) -> io::Result<()>
{
sleep_until_next_second().await;
Ok(())
}
async fn status(&mut self) -> io::Result<block::Status>
{
let now = chrono::Local::now();
Ok(block::Status {
full: format!("{}", now.format(LONG_FORMAT)),
short: Some(format!("{}", now.format(SHORT_FORMAT))),
urgent: false
})
}
}
From time.rs.
The status()
method simply displays the current time, and the wait()
method waits until the second changes. We don't invoke any subprocesses, and we know that our status bar will update as soon as the second changes.
Benchmarks
todo
| | i3status
| i3blocks
| waybar
| divebar
| | Memory Usage | | | | | | | | | | | | CPU Usage | | | | | | | | | | |