A simple status indicator
Hi! Today I want to show you how to build and use a simple status indicator system.
You can see here the end result, applied on Waybar indicating that build
passed but tests
failed.
It can also be used on AwesomeWM.
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
- Add/Update statuses
- Visualizing it
- Removing entries
- Building on it
- Monitor tasks
- Monitor task we didn't start
- As task queuing
- Special mention: nodemon
- Ideas for improvements
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 yousind
to update a value. - "Bell" sounds: same, just
play
some tune after yousind
. - Phone notifications: Gotify is a straightforward way to send notifications to your phone, send a message after the
sind
onsind-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.