Skip to main content

A simple status indicator

Hi! Today I want to show you how to build and use a simple status indicator system.

Waybar showing "build" in green and "test" in red

You can see here the end result, applied on Waybar indicating that build passed but tests failed.

It can also be used on AwesomeWM.

AwesomeWM showing "build" in green and "test" in red

This is entirely written in bash (and some Lua for the AwesomeWM plugin), and built with real simplicity in mind.

I've worked with them in their current state for the latest 2 years (although just some hours for the Waybar version) and found them enough for me.

Keeping this in mind, there's lots of improvements that may be easy to apply, so I'll write down some ideas at the end. Think of this as a Minimum Viable Product.

You can find all the code on: GitHub.

Table of contents

Basic structure

Ok, so the main idea is to dump all the status to be indicated status on a common JSON file. The file shown on the screenshots is this one

{
  "build": "#0f0",
  "tests": "#f00"
}

Simple, right?

The only tricky part, honestly, is to remember to lock the files to avoid problems when several programs are writing to the indicators at the same time. Keeping that in mind we can just rely on jq to manipulate the JSON file.

Ah, and I call this collection of scripts sind (for simple indicators), so the scripts are named:

  • sind (Adds/updates statuses)
  • sind-read (Reads them to waybar)
  • sind-rm (Removes statuses)
  • sind-run (Runs a program and keeps track of it's status)
  • sind-clear (Clears all statuses=

You get the idea...

Add/Update statuses

We'll start updating the status file. For simplicity I've put the status on /tmp/indicators.json, but you might want to move it somewhere else in multiuser systems (or if you care for proper a proper structuring of the filesystem).

I've named the script that does the most basic updates to the indicators sind, and is run as sind <INDICATOR NAME> <HEX COLOR>. You can find the complete script here. But this is the important part:

NAME=${1:-}
COLOR=${2:-}
FILE=/tmp/indicators.json

# Create it if it's empty
if [ ! -f "$FILE" ];then
    echo '{}' > $FILE
fi

# Lock $FILE and use jq to update the $NAME key to the $COLOR value
# then write the new content to $FILE
flock "$FILE" -c "bash -c '
      set -euo pipefail
      new_value=\`jq <\"$FILE\" \".$NAME=\\\"$COLOR\\\"\"\`;

      echo \"\$new_value\" >\"$FILE\"
'"

This leverages jq to perform the updates to the JSON file, and flock to make sure no two versions of these scripts update the file at the same time (which can get messy very fast).

Visualizing it

We can add new entries with sind so let's now show them on the systray, where they belong.

Waybar

Waybar describes itself as a "Highly customizable Wayland bar for Sway and Wlroots based compositors".

I've used it on Sway as a swaybar replacement for higher customizability. It ships with a very complete configuration.

To visualize our indicators on Waybar we can create a custom block on the configuration (normally stored on ~/.config/waybar/config).

    "custom/sind": {
        "return-type": "json",
        "exec": "bash $HOME/.config/waybar/sind-read.sh ",
    },

As you can see the configuration is pretty basic, just describe that the script will return data JSON-formatted, and point to the script that will read the statuses (note that we'll have to use bash for this, just sh won't work).

The sind-read.sh script that will print out the information in the format that waybar expects is this (complete script here):

# Remove all but the latest sind-read process.
pgrep -f "$0" | head -n-1 | xargs kill

# Enable "strict mode"
set -eu

# Read the indicators and output a waybar-compatible message
PROCESS() {
      DATA=$(cat "$FILE" || true)
      if [ -z "$DATA" ];then
         continue
      fi

      # This might look tricky, but thisjust reads the values and outputs an 
      # HMTL-like list of tags (these tags help to add the color).
      text=""
      for key in $(echo "$DATA" | jq 'keys' |grep '^  "'|cut -d\" -f2);do
                 value=$(echo "$DATA"| jq -r ".$key")
                 text="$text<span foreground='#000' background='$value'> $key </span>"
              done

      # After we're done, just wrap it in JSON
      result="{\"text\": \"$text\", \"class\":\"\", \"tooltip\":\"\"}"
      echo "$result"
}

# Make sure the indicators file exists
if [ ! -f "$FILE" ];then
   echo "{}" > "$FILE"
fi

# Main: After processing the indicators once, wait for changes on it and repeat
PROCESS
inotifywait -m -e close_write "$FILE" | while read _ ;do
      PROCESS                                        
done

The trick in this script is to use inotifywait to watch for updates on the indicators file, and send an update to waybar as soon as there's any change. This makes this "widget" very responsive.

See the "test" status refresh on the top-right corner.

(Remember to restart the waybar to run the new widget.)

AwesomeWM

We already know how to write a simple AwesomeWM widget, but we can make some changes to get a resposiveness like the one above. In this widget we can find how to use inotifywait on a AwesomeWM widget.

I won't go deep into the code of this widget. Honestly, it's a bit more involved than the waybar one as it doesn't just print the text, and has to build the UI elements keeping in mind reasonable aspect ratios.

Just know that you can get it here (indicators.lua). To use it, import it on your rc.lua file

local indicators = require("my-widgets.sind")

And add them to your wibox/wibar.

 s.mytasklist, -- Middle widget
 { -- Right widgets
     layout = wibox.layout.fixed.horizontal,
+    indicators,
     mykeyboardlayout,
     wibox.widget.systray(),
     mytextclock,
     s.mylayoutbox,
 },

(Remember to reload AwesomeWM configuration to run the new widget.)

Removing entries

Now, we've added and shown statuses, the next step is to remove the ones we're no longer interested on. Let's call this sind-rm (full code) and run it like sind-rm <STATUS> [<STATUS> ...]:

# Define the indicator file
FILE=/tmp/indicators.json

# Create it if it's empty
if [ ! -f "$FILE" ];then
    echo '{}' > $FILE
fi

# For each parameter...
for NAME in "$@";do

    # Lock $FILE and use jq to remove the key with the parameter name
    # then, write it back
    flock "$FILE" -c "bash -c '
      set -euo pipefail
      new_value=\`jq <\"$FILE\" \"del(.$NAME)\"\`;

      echo \"\$new_value\" >\"$FILE\"
'"
done

This locks and re-writes the files as many times as statuses are being deleted, so it might not be ideal if we have a really large number of them. In practice I didn't find this to be a problem.

Building on it

Ok, so we have the primitives in place. We can use this to have scripts that update our systray while they run to indicate the status of different components, but there are some things we can build using which will be generally useful.

Monitor tasks

Say, we might want to launch a long-running task, we can now do it like this:

sind MY_TASK '#0ff'
sleep 1 # Do something that takes some time
sind MY_TASK '#0f0'

But it would be simpler if we could just do

sind-run MY_TASK 'sleep 1'

This sind-run would be the following (full code):

NAME=${1:-}
COMMAND=${2:-}

sind "$NAME" "#0ff"

bash -xc "$COMMAND"  # The -x is just my preference :)

RES=$?  # Collect the command exit code 

if [ $RES -eq 0 ];then 
    sind "$NAME" "#0f0"  # If OK, show in green
else
    sind "$NAME" "#f00"  # If failed, show in red
fi

exit $RES  # Propagate error code to the parent process

Monitor task we didn't start

Now, sind-run might be useful when you know a process is going to take long, but some times you just run something and it takes longer than expected. We can monitor already-started tasks! (Although we can't get it's exit code).

sind-attach does this by using tail --pid to wait for a process to finish (full code). This is run as sind-attach <STATUS> <PID>.

NAME=${1:-}
PID=${2:-}

sind "$NAME" "#0ff"

tail "--pid=$PID" -f /dev/null  # Wait for the process to end

sind "$NAME" "#0f0"

As task queuing

Now, we can monitor tasks (either started or already running), so the next scenario is when we need to wait for a process to complete to run the following.

Picture this: we just completed a feature for a new program, the unit tests are running, we have the commit ready, only waiting to git push. We can get off the keyboard if the git push is triggered after the tests are passed.

So, sind-wait should wait for a monitored process (ideally with sind-run to know if it was successful) to complete, and have the current command line show it's success.

This builds on the idea that sind-run marks running processes as blue (#0ff), failed ones as red (#f00) and passing ones as green (#0f0) (full code).

FILE=/tmp/indicators.json

while [ 1 ];do
    ready=1

    # Check all indicators on arguments
    for NAME in "$@";do

        # Get the value of an indicator
        val=`jq -r <"$FILE" "(.$NAME)"`

        # Check if value is one of the allowed ones
        # If not green, it's not yet done
        if [ "$val" != "#00ff00" ];then
            if [ "$val" != "#0f0" ];then
                ready=0
            fi
        fi

        # If red, it has failed (so the wait finishes with an error).
        if [ "$val" == "#f00" ];then
            exit 1
        fi
        if [ "$val" == "#ff0000" ];then
            exit 1
        fi
    done

    # If all processes are ready, quit successfully
    if [ $ready -eq 1 ];then
        break
    fi

    # If not all processes are ready, wait for the next file modification
    inotifywait -q -e modify "$FILE" > /dev/null
done

This one is a bit more complex, but sometimes it comes in handy.

Special mention: nodemon

Ok, so the task queuing doesn't make a lot of sense in isolation. If you know the task will take some time why won't you concatenate the commands with &&.

I've used this repeatedly while building PrograMaker. PrograMaker has 4 automatic tests suites:

  • Unit tests (backend)
  • Unit tests (frontend)
  • Type checks (backend)
  • Type checks (frontend)

And I use nodemon to automatically launch them every time there was a change on the relevant part of the code (there's other alternatives based on Python, for example, but I was already running Node for the frontend, so... ¯\_(ツ)_/¯ ).

The idea is this: have nodemon running sind-run on the tests while you are developing, and you will always have an idea on where the tests fail. Just keep in mind the quoting when building the command:

(Nodemon command in example: nodemon -w . -e sh -x 'sind-run test "bash testme.sh"').

Ideas for improvements

So, that's what I've built until now. But there are lots of things that can be added on top with different degrees of effort:

Minimal script changes

sind-run and sind-attach are a great place to place events we want to take place when a task completes:

  • Notifications: just notify-send after you sind to update a value.
  • "Bell" sounds: same, just play some tune after you sind.
  • Phone notifications: Gotify is a straightforward way to send notifications to your phone, send a message after the sind on sind-run and it's ready.

Stability

If you want this to be more stable and less hackish you could:

  • Add ShellCheck to perform some static checks.
  • Move the indicators.json file to somewhere more appropriate, maybe /run/user/$UID/.

Format changes

And at last, if you want to make this into a proper system it might be interesting to add some semantic to the task results. Maybe convert the STATUS: COLOR JSON format into something like:

{ 
    "SAMPLE_KEY": { 
        "color": "#0f0",
        "text": "Cooler text! 🤯",
        "in_progress": false,
        "exit_code": 0,
    }
}

Specially interesting here would be proper semantics for status and exit code.