Advent 2022: 18. Bash Script to Display Coloured Plain-Text Beeminder Dashboard

Today’s Advent Calendar post may be of interest only to people who use Linux, BSD, or Mac OS because it’s about a Bash script, but if anyone would like to comment with their own code that does something similar for any platform, that would be awesome! Feel free to link to other forum threads or external sites if you already have code elsewhere. You’re also welcome to post improvements to my code if you wish!

I do almost all my task management and Beeminder data entry on the command line, so this is a script I wrote for myself to display my upcoming goals in a plain-text dashboard. Goals due in more than 3 days are not displayed. The screenshot below shows a) what it looks like and b) that I’m not actually doing at all well on Blue Means Do this week! :laughing:

This script is purely to display data. It doesn’t let you add or edit datapoints.

alt text for screenshot

The image shows the data below but with the lines coloured green, blue, orange, or red, depending on goal deadline.

breathing            +00:10      in 3 days
water                +335.25001  in 2 days
should               +4          in 2 days
pomadayyo            +1          in 2 days
fruitveg             +6.794      in 2 days
stretches            +3          by midnight tomorrow  has DELETE datapoint
inboxes              +3          by midnight tomorrow
gratitude            +1          by midnight tomorrow
thisbe_firefox_tabs  -1          by midnight
single_neck_stretch  +11         by midnight           has DELETE datapoint
exercise             +3          by midnight
eat_responsibly      +0.55       by midnight
clutterbox           +4          by midnight           has #SELFDESTRUCT datapoint
bk_dinosaurs         +1          by midnight
bed_time_good        +0.6        by midnight
aerobic              +00:07      by 23:00
tax                  +00:11      in 57 minutes!

The script requires that you have installed:

  • bash (standard on any *nix system I expect)
  • curl for running an API command to fetch all the recent goal data
  • jq for parsing the data (it’s JSON data)

You need to edit the script to insert your Auth Token and to specify a directory that the downloaded data can be stored in (or use the default one in the script). Script comments tell you where to edit - search for CUSTOMISE. You may also need to adjust the path to bash in the very first line, depending on your system.

After that, just run the script with no command-line arguments to download and display your data. Rerunning it like that will display the same data again (but the remaining time displays may change).
Rerunning it with the ‘-r’ option will cause new data to be fetched (i.e., do this after you’ve added datapoints).
Run it with ‘-h’ for help.

As you can see in the screenshot, the output will identify any goals that have datapoints with ‘#SELFDESTRUCT’ or ‘DELETE’ in their comments. You may find this useful if you use Pessimistic Presumption in Do More Goals. Note that only recent datapoints are searched for those comments. “Recent” is defined by Beeminder’s API; I’m not sure how far back it goes, but it’s at least a couple of days for my goals.

And here is the script:

# Author: Alice Harris, 2022-12-18
# Share and Enjoy

if [[ "$1" == "-h" ]]; then
	echo "Beeminder summary script"
	echo "A command-line tool to fetch your Beeminder goal data and display the most urgent goals with colours."
	echo "Requires curl and jq ( and"
	echo "Goal data will be saved locally - configure the BEEMINDER_TEMP_DIR variable for the storage directory."
	echo "Configure the BEEMINDER_TOKEN variable to hold your Auth Token from"
	echo "Run as '$0' for normal use."
	echo "That will fetch and save the data if not already present but then will use the same saved data every time."
	echo "Run with -r option to erase and refetch the stored data: '$0 -r'"
	echo "Also shows if the goal has recent datapoints commented with '#SELFDESTRUCT' or 'DELETE'."
	echo "See for why."
	exit 0

# use the "-r" command-line option to erase stored data (causes it to be refetched)
if [[ "$1" == "-r" ]]; then

BEEMINDER_TEMP_DIR="/tmp/beeminder-data" # CUSTOMISE THIS - a directory that data about your goals will be downloaded to
# The directory and its parent directories will be created if they don't exist.

BEEMINDER_TOKEN="AbC12-abcd1234efgHIJ" # CUSTOMISE THIS with your Auth Token from


# define colour codes for terminal output - see


all_goals_file="$BEEMINDER_TEMP_DIR/goals-raw-data"  # downloaded JSON data about your goals
all_goals_file_pretty="$BEEMINDER_TEMP_DIR/goals-raw-data-pretty"  # the same data but neatened for easier reading (not needed for this script but may be interesting)
goals_file_coloured_lines="$BEEMINDER_TEMP_DIR/goals-coloured-lines"  # human-friendly, coloured list of the most urgent goals

# Use the "-r" command-line option to erase stored data (causes the data to be refetched):
[[ $erase_stored_data ]] && rm -f -- "$all_goals_file" "$all_goals_file_pretty"

rm -f -- "$goals_file_coloured_lines" # always delete this since we recreate it from the stored data

if [[ ! -f "$all_goals_file" ]]; then
	echo "fetching Beeminder goal data..."

	curl -f -s "$BEEMINDER_TOKEN" -o "$all_goals_file" >/dev/null
	if [[ ! -f "$all_goals_file" ]]; then
		echo "ERROR: file '$all_goals_file' was not created" >&2
		echo "I am about to try to refetch the data and show you the output for troubleshooting." >&2
		echo >&2
		echo >&2
		echo >&2
		exit 1

	cat $all_goals_file | jq . > $all_goals_file_pretty

date_now=$( date "+%s" )
date_start_of_day_0=$( date -d "today 0" "+%s" ) # start of today
date_start_of_day_1=$(( $date_start_of_day_0 + 60*60*24 * 1 ))
date_start_of_day_2=$(( $date_start_of_day_0 + 60*60*24 * 2 ))
date_start_of_day_3=$(( $date_start_of_day_0 + 60*60*24 * 3 ))
date_start_of_day_4=$(( $date_start_of_day_0 + 60*60*24 * 4 ))
# Yeah, that's messy. Ah well. :)
# With those variables, the script will show you all goals due within the next 3 days inclusive.
# If you want to see more days, add more variables and adjust the if-elif block below where the variables are used.

readarray -t all_goals_summarised < <( cat "$all_goals_file" | jq -r ". | .[] | \"\(.losedate)\t\(.slug)\t\(.baremin)\t\(.limsum)\t\(.recent_data)\"" | sort -nr )
# That code uses jq to parse the downloaded goal data,
# extract the goalnames (slugs), time remaining, etc,
# and store the extracted values in a bash array called all_goals_summarised

# Process one goal at a time, working out what colour to use for it, and what data to display:
for one_goal in "${all_goals_summarised[@]}"; do

	goalname="${one_goal_arr[1]}"  # this is 'slug' in the JSON data

	losetime=$( date -d "@$((losedate + 1))" "+%H:%M" )
	# That converts the deadline from seconds since epoch ('@') to HH:MM.
	# The '+ 1' part adds one second to the deadline to convert it from (for example) 22:59 to 23:00.
	# This means the displayed deadline is not strictly accurate but I find it more intuitive and neater.
	# Remove '+ 1" from the line above to use the exact deadline from your data.

	[[ $losetime == "00:00" ]] || [[ $losetime == "23:59" ]] && losetime="midnight"
	# That causes the display to say "midnight" instead of "00:00" or "23:59".
	# Comment out that line if you don't want that.

	if   (( $losedate < $date_start_of_day_1 )); then
		seconds_remaining_till_derail=$(( $losedate - $date_now ))
		if (( $seconds_remaining_till_derail < 60*60*1 )); then
			# derailment is less than an hour away so we change the displayed text to make it more obvious
			due_string=$( date -ud @$seconds_remaining_till_derail "+in %M minutes!" )
			due_string="by $losetime"
	elif (( $losedate < $date_start_of_day_2 )); then
		due_string="by $losetime tomorrow"
	elif (( $losedate < $date_start_of_day_3 )); then
		due_string="in 2 days"
	elif (( $losedate < $date_start_of_day_4 )); then
		due_string="in 3 days"
		due_string="in 4 or more days"
		display_goal=   # turns off the display of this goal

	[[ "$recent_data" =~ DELETE         ]] && delete_flag="\thas DELETE datapoint"
	[[ "$recent_data" =~ \#SELFDESTRUCT ]] && selfdestruct_flag="\thas #SELFDESTRUCT datapoint"

	if [[ $display_goal ]]; then
		echo -e "$colour$goalname\t$baremin\t$due_string$delete_flag$selfdestruct_flag$NORMAL" >> $goals_file_coloured_lines

cat $goals_file_coloured_lines | column -s"	" -t

exit 0

For use on MacOS or BSD, you may run into trouble with GNU date not being present or with an older version of bash not containing readarray. See the comments for a fixes from @adamwolf and @clouedoc


This is really cool. But…

Are you actually running Linux??
Well, not that I would expect anything else than my fellow Beeminder fanatics…
But I’m still surprised.

The reason I say this is that your script it blurts out tons of MacOS-specific errors on my computer.

I won’t try to fix it though, because I’m on my way to writing the official Beeminder for WearOS app.

As I write these words, your script starts trembling in fear.

“I don’t want to be forgotten!”

“don’t worry, I last about 20 hours. They’ll still need you to remember to breathe or drink water”
-pixel watch

1 Like

date -d is in GNU date. It looks to be an extension to POSIX.2. It’s not in the date that ships with macOS (at least on 12.6), which I presume is BSD date.

readarray is part of bash 5 (Bash Builtins (Bash Reference Manual)). I think macOS ships with bash 3.2.

I can take a stab at adjusting it!

1 Like

Well, if you like the challenge, I can guarantee you that I will use this script!

Also, how did you know about this? Was it from experience, did you google it, or is there something I’m missing? Are you some kind of computer guru?

Not that I don’t like the web UI… But I do love my CLIs

1 Like

A little from experience, a little from Google, a little from having smashed my face against computers and dates for far too long :slight_smile:

If you use Homebrew on your mac, you can brew install coreutils, and then you can run the GNU version of date as gdate. You could also install a newer bash that way, come to think of it, and then edit the script so that every instance of date -d is replaced with gdate -d, and then…

at 15:56:04 ❯ /opt/homebrew/bin/bash

espanol_cm       +12  in 3 days
duospanish       +9   in 2 days
deutsch_cm       +6   in 2 days
family_meetings  +1   by midnight tomorrow
esperanto        +10  by midnight tomorrow
duodeutsch       +10  by midnight

Tada! (This isn’t quite what I had in mind–I still think we can wrangle that date command to work on systems with BSD date and GNU date–but sometimes sooner is better!)


Ah damn, I forgot that there’d be significant differences with the bash environment on MacOS / BSD. It’s been years since I used it. I’ve added a comment about it to the bottom of the post. @clouedoc thanks for asking about this and @adamwolf thanks for posting about fixes!


Hello guys,

It works!

# here: download Alys' script to your home folder
# here: replace Beeminder token
brew install bash
brew install coreutils
# here: open and replace all instances of `date -d` with `gdate -d`
# here: open your .zshrc / .bashrc and add: alias bee="/opt/homebrew/bin/bash"
# here: open a new terminal window

Thanks a lot @adamwolf, and also thansk @alys :pray: