In this fifth installment of our ongoing Linux tutorial series, we're going to customize our i3 experience by modifying i3bar's output!  We're going to integrate our MPD music backend (the setup of which we covered here), into i3bar.  We'll make use of some freely available code, and write a little python of our own.

The man page of i3status describes it succinctly:

"i3status is a small program (about 1500 SLOC) for generating a status bar for i3bar, dzen2, xmobar, lemonbar or similar programs. It is designed to be very efficient by issuing a very small number of system calls, as one generally wants to update such a status line every second."

So without further ado, let's get customizing i3status!  At the end of this tutorial we'll have something that looks like this (focus on the status bar ;)):

i3status with live MPD information

First, we'll copy the basic config that i3status comes packaged with into our local config folder to begin working from, and effectively back up the original settings.

If ~./config/i3status/ doesn't exist create it, then copy the contents of /etc/i3status.conf into ~/.config/i3status/config:

$ mkdir ~/.config/i3status
$ sudo cat /etc/i3status.conf > ~/.config/i3status/config

Next, we'll consider how exactly i3status works.  At a set frequency, the "interval" setting in the general block in i3status' config, it generates i3bar-friendly JSON that dictates what to display and how.  So to modify what gets displayed, one method is to intercept that JSON and edit it to our liking.  There's no need to reinvent the wheel here, and the following script does the nuts and bolts of this for us.  The original can be found here, but the boilerplate components we need look like this:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

# This script is a simple wrapper which prefixes each i3status line with custom
# information. It is a python reimplementation of:
# http://code.stapelberg.de/git/i3status/tree/contrib/wrapper.pl
#
# To use it, ensure your ~/.i3status.conf contains this line:
#     output_format = "i3bar"
# in the 'general' section.
# Then, in your ~/.i3/config, use:
#     status_command i3status | ~/i3status/contrib/wrapper.py
# In the 'bar' section.
#
# In its current version it will display the cpu frequency governor, but you
# are free to change it to display whatever you like, see the comment in the
# source code below.
#
# © 2012 Valentin Haenel <valentin.haenel@gmx.de>
#
# This program is free software. It comes without any warranty, to the extent
# permitted by applicable law. You can redistribute it and/or modify it under
# the terms of the Do What The Fuck You Want To Public License (WTFPL), Version
# 2, as published by Sam Hocevar. See http://sam.zoy.org/wtfpl/COPYING for more
# details.

import sys
import json

def get_governor():
    """ Get the current governor for cpu0, assuming all CPUs use the same. """
    with open('/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor') as fp:
        return fp.readlines()[0].strip()

def print_line(message):
    """ Non-buffered printing to stdout. """
    sys.stdout.write(message + '\n')
    sys.stdout.flush()

def read_line():
    """ Interrupted respecting reader for stdin. """
    # try reading a line, removing any extra whitespace
    try:
        line = sys.stdin.readline().strip()
        # i3status sends EOF, or an empty line
        if not line:
            sys.exit(3)
        return line
    # exit on ctrl-c
    except KeyboardInterrupt:
        sys.exit()

if __name__ == '__main__':
    # Skip the first line which contains the version header.
    print_line(read_line())

    # The second line contains the start of the infinite array.
    print_line(read_line())

    while True:
        line, prefix = read_line(), ''
        # ignore comma at start of lines
        if line.startswith(','):
            line, prefix = line[1:], ','

        j = json.loads(line)
        # insert information into the start of the json, but could be anywhere
        # CHANGE THIS LINE TO INSERT SOMETHING ELSE
        j.insert(0, {'full_text' : '%s' % get_governor(), 'name' : 'gov'})
        # and echo back new encoded json
        print_line(prefix+json.dumps(j))

All we need to do is extract some information from MPD and follow the pattern!  To do this, the mpc command line MPD controller is pretty useful.  So we'll need to install it:

$ sudo apt install mpc

We can now call mpc to get uniformly formatted information from MPD.  Following Valentin's example, this python function produces a color and a string to prepend to the JSON being provided to i3bar:

def get_mpd_info():
        color = "#FFFFFF"
        try:
            output = subprocess.check_output("mpc -f '%artist%:::%title%:::%album%'", shell=True)
            try:
                line,line2 = output.splitlines()[:2]
            except ValueError:
                # MPD Up, but stoppedi(others?):
                return color, "Nothing Playing!"
            state = ""

            try:
                artist,title,album = line.split(":::")
                state = line2.split(" ")[0]
                if "playing" in state:
                    #color = "#FF00FF" #pink
                    color = "#00FF00"
                else:
                    color = "#FFFFFF"
            except ValueError:
                # MPD Up, but stopped:
                return color, "Nothing Playing!"
            # Everything Good:
            return color, state + " " + artist + " - " + title
        except subprocess.CalledProcessError:
            # MPD Down:
            return color, "MPD is Down!"

Okay!  Now we can modify the main loop to use our function.  The resulting main function looks like this:

if __name__ == '__main__':
    # Skip the first line which contains the version header.
    print_line(read_line())

    # The second line contains the start of the infinite array.
    print_line(read_line())

    # get some output:
    #fout = open("/home/bob/i3status.log","w")

    while True:
        line, prefix = read_line(), ''
        # ignore comma at start of lines
        if line.startswith(','):
            line, prefix = line[1:], ','

        j = json.loads(line)
        # insert information into the start of the json, but could be anywhere
        # CHANGE THIS LINE TO INSERT SOMETHING ELSE
        color,s = get_mpd_info()
        j.insert(0, {'full_text' : '%s' % s, 'color' : '%s' % color, 'name' : 'gov'})

        # and echo back new encoded json
        print_line(prefix+json.dumps(j))

Alright!  Now we've got a fully functioning i3status bar wrapper which will place the text we produced, in the color we specified at the beginning of our status bar.

To implement this completely, take the full python script, and place it somewhere safe (link to the full script here).  I've put mine in ~/Scripts/i3bar/wrapper.py.  Make it executable:

$ chmod +x ~/Scripts/i3bar/wrapper.py

Next, modify i3's configuration file as suggested in Valentin's script.  Change the bar { ... } block so that i3status is piped into our wrapper:

bar {
    status_command i3status | ~/Scripts/i3bar/wrapper.py
    ...
}

Next, modify i3status' config file.  Add the following line to the general { ... } block at the top of the file:

output_format = "i3bar"

Okay, we're all set!  To see the effects of our changes, hit <shift> + <mod> + <r> to reload your i3 config.

# Reads: 3136