Prev post Next post Archive

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.

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:

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 | | | | | | | | | | |

Conclusions

Written by Francis Wharf
Prev post Next post Archive