Put the screen-locker status back on the i3 bar
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run

The GTK tray icon was the only status surface, and it had been commented out
of ~/.config/i3/config since 2026-08-21. With nothing in the bar, the locker
miscounted the week for a fortnight and the machine never said so.

Shows "<count>/<minimum> · <gaming budget>", read from the local status API
(screen-locker-web.service on 127.0.0.1:8770); clicking opens the full UI.

Three things it deliberately will not do quietly:
- a dead status server renders "status server down" in red, or the last known
  value marked (stale) in orange -- never a blank block. That server is what
  the gaming budget reads, so its absence costs two hours;
- a disarmed or unscheduled timer overrides the compliance colour entirely
  and shows "DISARMED" in red. A locker that cannot fire is the worse
  problem and the one that hides;
- clicking only opens the UI. There is no bar action that can clear a lock.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fcwu78wk17FWMHbbfbyicc
This commit is contained in:
Krzysztof Rudnicki 2026-08-29 22:55:24 +02:00
parent 334c0cd904
commit 2713ae9d66
3 changed files with 128 additions and 0 deletions

View File

@ -0,0 +1,40 @@
{
"intent": "Put the screen-locker status back on the i3 bar, so a miscounting or disarmed locker is visible without being asked. The GTK tray icon it replaces had been commented out of ~/.config/i3/config since 2026-08-21, and in that window the locker counted one workout twice, believed the week met at 5/5 when the real total was 3, and skipped enforcement for two ISO weeks with nothing on the machine saying so.",
"scope": [
"linux_configuration/i3blocks/screen_locker.sh (new block script)",
"linux_configuration/i3blocks/config (register the block before shutdown_countdown)",
"Non-goal: the block never mutates locker state; clicking only opens the read-only UI",
"Depends on screen-locker-web.service (127.0.0.1:8770) from the screen-locker repo"
],
"changes": [
"New block renders '<count>/<minimum> · <gaming budget>' from GET /api/status, coloured by compliance_state (green ok / yellow warn / red lock).",
"Arming from GET /api/health overrides the compliance colour: a timer that is disabled or has no next trigger shows 'DISARMED' in red, because a locker that cannot fire is the worse failure and the one that hides.",
"A dead status server renders the last known value marked '(stale)' in orange, or 'status server down' in red when there is no cache — never a blank block.",
"Missing curl/jq degrades to a visible marker rather than an empty block or a bash error."
],
"verification": [
{
"command": "~/.config/i3blocks/screen_locker.sh",
"result": "pass",
"evidence": "Prints '💪 3/5 · 6h' / '💪 3/5' / '#F1FA8C' — 3/5 is the corrected weekly count, 6h the cut gaming budget for a day with no workout."
},
{
"command": "systemctl --user stop screen-locker-web.service && ~/.config/i3blocks/screen_locker.sh",
"result": "pass",
"evidence": "With cache: '💪 3/5 · 6h (stale)' in orange. After rm of the cache: '💪 status server down' in red. Restored to '💪 3/5 · 6h' once the unit was started again."
},
{
"command": "i3-msg reload && scrot",
"result": "pass",
"evidence": "Block visible in the HDMI-0 bar between PC:warn and the 23:00 shutdown countdown, rendering '💪 3/5 · 6h' in yellow. i3blocks does not re-read its config on i3-msg reload alone; the running process had to exit first."
}
],
"risks": [
"Two curl calls every 30s. Both are loopback with --max-time 2, so a hung server delays the block by at most 4s rather than wedging the bar.",
"The block reports the gaming budget using its own 8h/6h constants. If steam-backlog-enforcer's configured values change, this display drifts from the enforcer until updated."
],
"rollback": [
"Remove the [screen_locker] stanza from linux_configuration/i3blocks/config, delete linux_configuration/i3blocks/screen_locker.sh and the ~/.config/i3blocks symlink, then i3-msg reload and kill the running i3blocks so it re-reads the config.",
"After rollback, confirm the bar still renders the remaining blocks (cpu, gpu, memory, disk, volume, ethernet, claude_usage, pc_startup, shutdown_countdown, time)."
]
}

View File

@ -51,6 +51,10 @@ interval=30
color=#FFFFFF
[screen_locker]
command=~/.config/i3blocks/screen_locker.sh
interval=30
[shutdown_countdown]
command=~/.config/i3blocks/shutdown_countdown.sh
interval=60

View File

@ -0,0 +1,84 @@
#!/bin/bash
# ============================================================================
# Screen-locker status for i3blocks.
#
# Replaces the GTK tray icon that sat commented out in ~/.config/i3/config from
# 2026-08-21, during which the locker miscounted the week and nothing on the
# machine said so. The point of this block is that a broken or disarmed locker
# is visible without being asked: it is never blank and never silently green.
#
# Reads the local status API (screen-locker-web.service). Click opens the UI.
# ============================================================================
set -euo pipefail
readonly API='http://127.0.0.1:8770/api/status'
readonly HEALTH='http://127.0.0.1:8770/api/health'
readonly UI='http://127.0.0.1:8770/'
readonly CACHE="${XDG_RUNTIME_DIR:-/tmp}/i3blocks-screen-locker.cache"
# Dracula-ish palette, matching the other blocks in this bar.
readonly RED='#FF5555'
readonly YELLOW='#F1FA8C'
readonly GREEN='#50FA7B'
readonly ORANGE='#FFB86C'
emit() {
# full_text, short_text, color — the three lines i3blocks reads.
printf '%s\n%s\n%s\n' "$1" "$2" "$3"
}
# A click opens the web UI rather than doing anything destructive; there is no
# action here that could clear a lock.
if [[ ${BLOCK_BUTTON:-0} -ne 0 ]]; then
xdg-open "$UI" >/dev/null 2>&1 &
fi
if ! command -v curl >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then
emit '💪 no curl/jq' '💪 ?' "$ORANGE"
exit 0
fi
status_json=$(curl -fsS --max-time 2 "$API" 2>/dev/null || true)
if [[ -z $status_json ]]; then
# The server being down is itself news: it is what the gaming budget reads,
# and it failing closed costs two hours. Say so instead of showing nothing.
if [[ -r $CACHE ]]; then
emit "$(cat "$CACHE") (stale)" '💪 ?' "$ORANGE"
else
emit '💪 status server down' '💪 ?' "$RED"
fi
exit 0
fi
count=$(jq -r '.snapshot.week.counted_count' <<<"$status_json")
minimum=$(jq -r '.snapshot.week.minimum' <<<"$status_json")
state=$(jq -r '.compliance_state' <<<"$status_json")
workout_today=$(jq -r '.gaming.workout_today' <<<"$status_json")
if [[ $workout_today == 'true' ]]; then
hours='8h'
else
hours='6h'
fi
health_json=$(curl -fsS --max-time 2 "$HEALTH" 2>/dev/null || true)
armed=$(jq -r '.armed' <<<"${health_json:-{\}}" 2>/dev/null || echo 'unknown')
text="💪 ${count}/${minimum} · ${hours}"
case $state in
lock) color=$RED ;;
warn) color=$YELLOW ;;
*) color=$GREEN ;;
esac
# Arming beats compliance: a locker that cannot fire is the worse problem, and
# it is the one that hides. Never render it as a green tick.
if [[ $armed != 'true' ]]; then
text="$text ⚠ DISARMED"
color=$RED
fi
printf '%s' "$text" >"$CACHE"
emit "$text" "💪 ${count}/${minimum}" "$color"