Posted on ::

What are we making today?

The idea is an always-on-top persistent widget that integrates with my application Smoker and shows a live counter of how many cigarettes I have smoked today.

This is a nice addition to my existing Stream Deck hotkey which adds a cigarette to the app.

A simple counter in a box should suffice.

widget_example

Word of advice

If you intend to follow along with this, take it carefully. Yea, I know it's just a widget, but still. I haven't tested anything of this on any other OS or desktop environment or whatever. I just did it, thought it was cool and wanted to share.

I'm using KDE Plasma on EndeavourOS.

Framework

Since I'm using KDE Plasma with Wayland, a fitting library is eww.

yay -S eww

Setup

We need two files, located in ~/.config/eww/ called eww.yuck (layout and functionality) and eww.scss (styling).

;; function that listens to a stdin stream
(deflisten smoker_count
  :initial "--"
  "/home/masco/.config/eww/scripts/smoker-stream.sh")

;; bounding box and counter, bound to the 'smoker_count' function
(defwidget counter []
  (box :class "counter-box" :orientation "h"
    (label :class "count" :text {smoker_count})))

;; layout definition on the monitor
(defwindow smoker
  :monitor 0
  :stacking "fg"
  :wm-ignore true
  :windowtype "normal"
  :geometry (geometry :anchor "bottom right" :width "3%" :x "20px" :y "20px")
  (counter))

Then of course we need to style it a bit, so it doesn't look as trash as plain HTML.

* {
    all: unset; // removes all GTK (default) styling
}

.counter-box {
    background-color: rgba(20, 20, 20, 0);
    border: 1px solid #f97316;
    border-radius: 10px;
    padding: 6px 12px;
}

.count { 
    color: #f97316; 
    font-size: 24px; 
    font-weight: bold; 
}

Fetching Data

Now that we have the widget set up, we still need some way to get the actual data. One way would be to use a defpoll component and just let it run a curl task like every 10 seconds.

But that would be kinda inefficient and lazy, but we want ✨efficiency✨ right.

So, for exactly this purpose, the Smoker app already exposes an /api/v1/events endpoint that can be used to update dynamically. That endpoint sends data as text/event-stream so the connection is kept alive by the client. These updates are then captured by the deflisten function and passed into the component.

In other words, every time a cigarette is logged, that endpoint will send a new "refresh" event to the client and update the widget.

So the smoker-stream.sh file looks like this:

Warning

If you're recreating this, make sure the prod.env file contains API_KEY and BASE fields, or set them directly in the script.

Specific to this application:

The base should be a valid Smoker endpoint. In my case that is https://smoker.domain.com/api/v1.

#!/usr/bin/env bash
# load .env file
SCRIPT_DIR="/home/masco/.config/eww/scripts"
set -a; source "$SCRIPT_DIR/prod.env"; set +a

# fetch today's count
get_data () {
  curl -s -H "Authorization: Bearer $API_KEY" "$BASE/stats" | jq -r '.data.session.count'
}

# run once on init
get_data

while true; do
  # subscribe to events and read from stream
  curl -sN -H "Authorization: Bearer $API_KEY" "$BASE/events" | while IFS= read -r line; do
      case "$line" in # if line has data
        data:*)
          get_data # print data to stdout
          ;;
      esac
    done
  sleep 2 # reconnect after 2s
done

Nice. We got the layout, styling and logic done. Last thing is to actually start the widget by running:

eww open smoker

Final words

I really like this implementation of a widget. It's easy to implement, doesn't take long and can be directly connected to bash scripts.

This was more or less a test if I can get a widget to run -- also kinda the first real blog post on here hihi -- and I'm pretty sure that I'm gonna do more stupid stuff with this in the future.

I hope you enjoyed this little adventure. Happy coding! :3