

Discover more from hrbrmstr's Daily Drop
There once was a coder who aimed to see
If she should go out or stay home for tea.
Her laptop's lid went up, and her head arched down
To write some Weather App code.
Soon may the Weather App come
To bring us forecasts with curl, jq, and gum
One day, when the codin' is done
We'll know whether outside we should go.
Now that we know “Weird Al” Yankovic is in no danger of being unseated as the king of parody songs, let's get straight to the business of today. Said business is extending a previous WPE to use the gum utility we talked about this week be a bit more “app”-y.
You are most certainly going to do most of the heavy lifting, but I framed out an approach that we'll go over here, and eventually drop a link to a slightly modified script from the one I provided last time.
The User Experience
If you just used my first example script without many changes, you know that the user experience (UX) was pretty thin. We at least gave them some help, but the output was terrible, and we offered no way to see what locations they had cached.
The modified UX in my new example script still lets folks enter an address/location as a parameter. But, if no parameter is specified, it provides a minimal TUI (terminal user interface) to let them select from a list of cached locations, or create one if no cached entries exist. The output is also more human-friendly, albeit a bit sparse (I mean, it is your weekend project, so you gotta do some of the fiddly bits).
Hopefully, the light UX upgrade in my example script will provide enough fuel for you to design a much more sophisticated and useful personal weather app.
Gum Wrappers
At the start of the script, you'll see that we've cast aside the previous “show help if no args” logic with some gum print wrappers:
TITLE_COLOR="#FEE715"
MESSAGE_COLOR="#DA5A2A"
LIGHT_COLOR="#DCE2F0"
title() {
local text="$1"
gum style --foreground="${TITLE_COLOR}" "${text}"
}
message() {
local text="$1"
gum style --foreground="${MESSAGE_COLOR}" "${text}"
}
light() {
local text="$1"
gum style --foreground="${LIGHT_COLOR}" "${text}"
}
Those semantic wrappers make using gum styling a bit less in situ verbose. Customize them like crazy! I picked colors a bit haphazardly from the 2colors resource we covered way back in Drop #151, so — also — be better than me.
Functional Forecasts
NOTE: You'll 100% want to go to this line in the GL snippet vs suffer through the Substack code block, below.
Next, we pull out the forecast display code from the main part of the script and make it into a function.
The jq
script shows how to target the daily
forecast field from the API response, and also how to extract fields for use in later Bash processing. jq
has support for format strings, and you may find the @sh
one useful depending on how much you're pulling from the forecast response. With that format/escape indicator, the input is escaped suitable for use in a command-line for a POSIX shell. If the input is an array, the output will be a series of space-separated strings.
Note, further, the need to handle the differences between the linux and macOS date
command.
You may also find Bash's printf useful.
# prettify the output
pretty_forecast() {
local forecast pretty_address
forecast="$1"
pretty_address="$2"
message "Forecast for ${pretty_address} at $(date)"
echo
echo "${forecast}" | \
jq -r '.timelines.daily[] | "\(.time) \(.values.precipitationProbabilityMax)"' | \
while IFS= read -r line; do
time=$(echo "$line" | awk '{print $1}')
probability=$(echo "$line" | awk '{print $2}')
if [[ "$(uname)" == "Darwin" ]]; then # handle macOS janky date cmd
day_of_week=$(date -jf "%Y-%m-%dT%H:%M:%SZ" -u "${time}" '+%A')
else
day_of_week=$(date -d "@${time}" '+%A')
fi
echo "$(light "${day_of_week}"): ${probability}% 🌧️"
done
}
The Main Event
Again, the below looks better on GL.
If there is an argument to the CLI, we just do what we did in the first script.
If there is no argument, we check to see if there are any cached locations.
If there are cached locations, we build a menu (with gum) to let folks select from it and then display the new forecast.
If there are no cached locations, then we use gum to get user input, then display the forecast.
title "Welcome To AwesomeWX™"
echo
# Check if any command line input was provided
if [ -z "$*" ]; then
locations=$(sqlite3 cache.db "SELECT '\"' || query || '\"' FROM api_cache")
if [ -z "${locations}" ]; then
message "No cached locations found."
QUERY_STRING=$(gum input --prompt='> ' --placeholder "Enter location for forecast…")
clear
get_forecast "${QUERY_STRING}"
else
QUERY_STRING=$(echo "${locations}" | xargs gum choose --header="Choose location for forecast:")
get_forecast "${QUERY_STRING}"
fi
else
# Get the query string from the command line
QUERY_STRING="$*"
get_forecast "${QUERY_STRING}"
fi
Preview
Here's a preview of what it looks like:
FIN
It's super important to make liberal use of documented Bash functions to help keep code organized.
You can also work in a project directory and spread the functionality across individual scripts, then use a Justfile, or Makefile to `cat` them all together (in the right order) and run the final “production” script. That’s also an opportunity to add tests.
Furthermore, remember, you can use gum from any scripting language that can shell out to the system, or use Golang and Charmbracelet's equivalent styling Go modules.
Consider fully embracing semantic colors and having a JSON config file, so folks can customize them.
If you’re going to create a complex app, perhaps give Penpot a go. It’s an open-source alternative to Figma, and can help you map out the CLI UX before implementing it. Hacking is fine for a toy project, but if you decide to get complex, your future self will thank you for spending up-front design time.
Finally, make sure to save off a copy of the tomorrow.io JSON and use that for fiddling with the fancy pretty_forecast()
formatting vs. burn through your daily API limits.
Depending on how much work you did previously (or are just riffing from my script). A fully working basic pretty weather app should likely only consume an hour or two, leaving plenty of time to go outside and touch some grass! ☮