11 minute read

The screen time module Omarchy is missing

Nothing tracks screen time on Omarchy, so I built something that does, quietly running in the background and showing me how my days actually add up.

Ever since moving to Omarchy, I’ve had this itch to keep track of my screen time. On macOS (and even iOS), this is just there, baked into the system. You don’t think about it, you just open it and get your numbers.

Tracking screen time on Linux

On Linux, things work a bit differently. If something exists, it’s because someone decided it should exist. Not the system. Not the OS. A person. And honestly, that’s part of the charm—but also part of the friction.

So if I wanted screen time tracking, I had to build it, or find a person that had already built one. But that isn’t as exciting as building it, is it?

Why this even matters

Screen time sounds simple, but it’s surprisingly flexible.

You might just want to know how long you’re sitting in front of your computer. Or maybe you want a rough idea of how much you’re working each day. If you freelance, you could even take it further and track time per client, so you know how much to bill them.

My setup doesn’t go that far, but it wouldn’t be hard to extend it.

At its core, though, I just wanted something truthful. Something that runs quietly in the background and doesn’t depend on me remembering to start or stop anything.

Why a Waybar module

The obvious place for this was the Waybar. It’s always there. Always visible. No friction.

That gives you a few nice properties:

From the user’s point of view, it’s just an icon. You can click it to pause if you really want to, but most of the time you don’t have to think about it at all.

That was the goal.

Making it automatic

The key part of making this work wasn’t the timer itself—it was automation.

When the system locks, the timer pauses.
When it unlocks, it resumes.

That’s it.

No manual tracking. No “oh I forgot to stop it”. No inflated numbers. This one detail made the whole thing actually usable. Accurate by default.

Plain Text
hypridle.conf
1234567891011
general {
  lock_cmd = omarchy-lock-screen
  on_unlock_cmd = /home/pmpinto/scripts/active-timer-toggle resume
  before_sleep_cmd = /home/pmpinto/scripts/active-timer-toggle pause && loginctl lock-session
}

# After 2 min on screensaver
listener {
    timeout = 121
    on-timeout = /home/pmpinto/scripts/active-timer-toggle pause && omarchy-lock-screen
}

The above is how I have set this up with hypridle.

The multi-computer problem

Things got a bit more interesting when I introduced a second machine.

After using a Framework desktop for a while, I picked up a Framework laptop to use as my main work device. Both machines share the same setup, and I use a monitor with a built-in KVM.

That basically means I can plug everything into the monitor—keyboard, mouse, camera, headphones—and just switch between machines instantly.

Super clean setup. But it created a weird problem.

In Omarchy, each display runs its own Waybar instance. So when I connected the laptop, I suddenly had two timers running at the same time.

It created a race condition, where each instance was trying to write the data before the other, ending up completely wiping the data file.

Fixing the data mess

The solution wasn’t to prevent multiple instances—it was to make them cooperate.

After a small revision, the tracker now handles multiple running instances without corrupting the data. No matter how many Waybars are active, they all write safely to the same source of truth.

From the outside, nothing changed. From the inside, everything became reliable.

What it actually shows

The UI stays intentionally simple. You get an icon in the Waybar. That’s it.

But when you hover it, you see:

Preview of what Active Timer looks like in Waybar
Preview of what Active Timer looks like in Waybar

Just enough to understand your habits without overloading you with stats.

How it works under the hood

The tracker writes to a JSON file on every tick. Nothing fancy, just a simple structure that keeps accumulating time safely.

Bash
scripts/active-timer-status
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
#!/usr/bin/env bash

DIR="$HOME/.config/active-timer"
STATE="$DIR/state"
LAST="$DIR/last_tick"
DAYS="$DIR/days.json"
LOCK="$DIR/.lock"
SEP="<span alpha='24%'>$(printf '─%.0s' {1..24})</span>"

mkdir -p "$DIR"

now=$(date +%s)
state=$(cat "$STATE" 2>/dev/null || echo paused)
last=$(cat "$LAST" 2>/dev/null || echo "$now")

delta=$((now - last))
echo "$now" >"$LAST"

workday() {
  if [[ $(date +%H) -lt 6 ]]; then
    date -d "yesterday" +%Y-%m-%d
  else
    date +%Y-%m-%d
  fi
}

today="$(workday)"

(
  flock -n 9 || exit 0

  # ensure JSON exists and is valid
  if [[ ! -s "$DAYS" ]] || ! jq empty "$DAYS" >/dev/null 2>&1; then
    echo "{}" >"$DAYS"
  fi

  if [[ "$state" == "running" && $delta -gt 0 && $delta -lt 3600 ]]; then
    tmp=$(mktemp)

    if jq --arg d "$today" --argjson s "$delta" \
      '.[$d] = (.[$d] // 0) + $s' "$DAYS" >"$tmp"; then
      mv "$tmp" "$DAYS"
    else
      rm -f "$tmp"
    fi
  fi

  tmp=$(mktemp)

  if jq '
  to_entries
  | sort_by(.key)
  | reverse
  | .[:5]
  | from_entries
' "$DAYS" >"$tmp"; then
    mv "$tmp" "$DAYS"
  else
    rm -f "$tmp"
  fi

) 9>"$LOCK"

fmt() {
  printf "%dh%02dm" $(($1 / 3600)) $((($1 % 3600) / 60))
}

today_secs=$(jq -r --arg d "$today" '.[$d] // 0' "$DAYS")

if [[ "$state" == "paused" ]]; then
  lines="<span alpha='64%'> $(printf "%-10s" "Paused ")$(printf "%11s" "$(fmt "$today_secs")")</span>"
else
  lines="<span alpha='96%'> $(printf "%-10s" "Active ")$(printf "%11s" "$(fmt "$today_secs")")</span>"
fi

sum=0
count=0

if jq -e 'length > 0' "$DAYS" >/dev/null; then
  lines="$lines
$SEP"
fi

while read -r d s; do
  dayname=$(date -d "$d" +%A)
  dayname_padded=$(printf "%-9s" "$dayname")

  icon=""
  [[ "$d" == "$today" ]] && icon=""

  lines="$lines
<span alpha='64%'>$icon $dayname_padded</span> <span alpha='96%'>$(printf "%11s" "$(fmt "$s")")</span>"

  sum=$((sum + s))
  count=$((count + 1))

done < <(
  jq -r '
    to_entries
    | sort_by(.key)
    | reverse
    | .[]
    | "\(.key) \(.value)"
  ' "$DAYS"
)

if ((count > 0)); then
  avg=$((sum / count))
  lines="$lines
$SEP
<span alpha='80%'> $(printf "%-10s" "Average ")$(printf "%11s" "$(fmt "$avg")")</span>"
fi

icon="󱫠"
[[ "$state" == "paused" ]] && icon="󰔞"

jq -nc --arg text "$icon" --arg tooltip "$lines" '{text:$text, tooltip:$tooltip}'

And here’s what that data looks like:

JSON
.config/active-timer/days.json
1234567
{
  "2026-05-17": 18640,
  "2026-05-16": 21420,
  "2026-05-15": 9800,
  "2026-05-14": 15210,
  "2026-05-13": 20105
}

And for whatever reason I felt like having the toggling in a separate script. So that one script handles the tooltip and data, and the other handles the toggling—you should probably merge these together if you are reusing them.

So here’s the second script:

Bash
scripts/active-timer-toggle
12345678910111213141516171819202122232425
#!/usr/bin/env bash

STATE_DIR="$HOME/.config/active-timer"
STATE_FILE="$STATE_DIR/state"
LAST_TICK="$STATE_DIR/last_tick"

current="$(cat "$STATE_FILE" 2>/dev/null || echo paused)"

case "$1" in
  pause)
    echo paused > "$STATE_FILE"
    ;;
  resume)
    echo running > "$STATE_FILE"
    ;;
  toggle|"")
    if [[ "$current" == "running" ]]; then
      echo paused > "$STATE_FILE"
    else
      echo running > "$STATE_FILE"
    fi
    ;;
esac

date +%s > "$LAST_TICK"

The only missing part of the puzzle is the Waybar piece. I personally have it somewhere in the modules on the right side of the screen:

JSON
.config/waybar/config.json
12345
{
  "modules-right": [
    "custom/active-timer"
  ]
}

And then define the module as such:

JSON
.config/waybar/config.json
12345678910
{
    “custom/active-timer": {
        "exec": "/home/pmpinto/scripts/active-timer-status",
        "format": "{text}",
        "interval": 10,
        "return-type": "json",
        "tooltip": "{tooltip}",
        "on-click": "/home/pmpinto/scripts/active-timer-toggle toggle"
    }
}

Final thoughts

This ended up being one of those small tools that quietly changes how you see your day.

Not because it’s complex—but because it’s honest. It runs in the background, doesn’t ask for attention, and just tells you the truth when you look at it.

This setup solves a simple problem in a very “Linux way”: build only what you need, and make it fit your workflow. By integrating the tracker directly into the Waybar, it becomes invisible but always available.

Automation is what makes it actually useful. Pausing on lock and resuming on unlock removes human error, which is what usually breaks time tracking.

Handling multiple machines was the real challenge. Once that was solved, the system became reliable enough to trust—something essential for any kind of tracking.

Photo of Pedro