From 47b4510ac6d472075d512ee3a3c226660369cf0d Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Tue, 10 Nov 2026 15:45:31 +0100 Subject: [PATCH] i3 focus block: red on miss, suitcase Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WzxdWZUTC9Zzdzsx7VW7dm --- .../i3-focus-block-milestones-2026-11-10.json | 31 +++++++++++++ .../issue-dashboard-awaiting-2026-10-02.json | 37 --------------- linux_configuration/i3blocks/focus_project.sh | 20 +++++++-- .../tests/test_i3blocks_focus_project.sh | 45 +++++++++++++++++++ 4 files changed, 93 insertions(+), 40 deletions(-) create mode 100644 docs/superpowers/evidence/i3-focus-block-milestones-2026-11-10.json delete mode 100644 docs/superpowers/evidence/issue-dashboard-awaiting-2026-10-02.json diff --git a/docs/superpowers/evidence/i3-focus-block-milestones-2026-11-10.json b/docs/superpowers/evidence/i3-focus-block-milestones-2026-11-10.json new file mode 100644 index 00000000..b09da486 --- /dev/null +++ b/docs/superpowers/evidence/i3-focus-block-milestones-2026-11-10.json @@ -0,0 +1,31 @@ +{ + "intent": "The i3 focus block turns red when an interview milestone is missed and shows a suitcase while a trip question is open; no new text.", + "scope": [ + "linux_configuration/i3blocks/focus_project.sh", + "linux_configuration/tests/test_i3blocks_focus_project.sh", + "Non-goal: the block still renders only daily-limits' cached focus field" + ], + "changes": [ + "jq render picks #FF5555 when focus.missed is true, over green and yellow", + "jq render appends the FontAwesome suitcase (U+F0F2) when a trip_ask:* milestone is pending or missed; malformed milestones are ignored" + ], + "verification": [ + { + "command": "bash linux_configuration/tests/test_i3blocks_focus_project.sh", + "result": "pass", + "evidence": "red first (missed block stayed yellow), then all checks passed incl. red, suitcase, no-suitcase and malformed milestone cases" + }, + { + "command": "shellcheck linux_configuration/i3blocks/focus_project.sh linux_configuration/tests/test_i3blocks_focus_project.sh", + "result": "pass", + "evidence": "no findings" + } + ], + "risks": [ + "Suitcase glyph needs the bar's FontAwesome font (installed: Font Awesome 7 Free Solid)" + ], + "rollback": [ + "git revert the commit; the live ~/.config/i3blocks/focus_project.sh is a symlink to the repo file", + "Check the block renders yellow/green again" + ] +} diff --git a/docs/superpowers/evidence/issue-dashboard-awaiting-2026-10-02.json b/docs/superpowers/evidence/issue-dashboard-awaiting-2026-10-02.json deleted file mode 100644 index 4d4423d7..00000000 --- a/docs/superpowers/evidence/issue-dashboard-awaiting-2026-10-02.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "intent": "Merged-but-open issues (Merge only tags the opener, kado-no-akari #21) get their own 'Awaiting opener' column in ~/src/issue-dashboard and an 'awaiting' count in status.json; the i3bar issue_dashboard block shows it as '⏳ N' instead of counting it as to do.", - "scope": [ - "linux_configuration/i3blocks/issue_dashboard.sh - read the optional 'awaiting' field and show '· ⏳ N' between '✓' and '✗' when N > 0", - "linux_configuration/tests/test_i3blocks_issue_dashboard.sh - awaiting text, hidden at zero, ordering, colour, old-format file without the key", - "Non-goal: the dashboard change itself (local repo ~/src/issue-dashboard, commit 0e6077c); the running dashboard server was not restarted." - ], - "changes": [ - "awaiting is optional: a status.json from a server older than the column has no key and still renders '📋 N · ⚙ N · ✓ N', never grey.", - "awaiting never changes the colour (it waits on the opener, not kuhy) and stays out of the short text; zero is hidden like '✗'.", - "Still zero forks on the hot path: one more regex field read and an arithmetic test." - ], - "verification": [ - { - "command": "bash linux_configuration/tests/test_i3blocks_issue_dashboard.sh", - "result": "pass", - "evidence": "awaiting / hidden-at-zero / ordering with ✗ / colour / old-format / stale / missing / broken / empty / click asserted; strace still shows one execve and no clone/fork." - }, - { - "command": "bash linux_configuration/tests/test_i3blocks_efficiency.sh", - "result": "pass", - "evidence": "All i3blocks efficiency regression tests passed." - }, - { - "command": "ISSUE_DASHBOARD_STATUS= issue_dashboard.sh", - "result": "pass", - "evidence": "'📋 1 · ⚙ 0 · ✓ 1 · ⏳ 2 · ✗ 1' #FF5555 from {todo 1, ready 1, awaiting 2, failed 1}; the live old-format status.json still renders '📋 14 · ⚙ 0 · ✓ 0' #FFFFFF." - } - ], - "risks": [ - "'⏳' shows only once kuhy restarts the dashboard server; until then the old server keeps writing merged-open issues into todo." - ], - "rollback": [ - "git revert this commit; the block reads the old fields only.", - "Confirm the bar still renders '📋 N · ⚙ N · ✓ N'." - ] -} diff --git a/linux_configuration/i3blocks/focus_project.sh b/linux_configuration/i3blocks/focus_project.sh index 08d88ebd..b974679d 100755 --- a/linux_configuration/i3blocks/focus_project.sh +++ b/linux_configuration/i3blocks/focus_project.sh @@ -18,6 +18,9 @@ # gaming_budget block already says "⏱?" for the same cache, and old minutes # must never be shown. # +# Interview projects: focus.missed (any milestone missed or unknown) turns the +# block red, over green and yellow; no text is added. +# # Clicks (the block says what to do, so a click goes where the work is): # left `python3 -m focus_project.open`: the tutor focus screen (starts the # focus timer if it is not running) and the project's work URL @@ -34,6 +37,10 @@ readonly STALE_AFTER_SECONDS=180 # Dracula-ish palette, matching gaming_budget.sh. readonly YELLOW='#F1FA8C' readonly GREEN='#50FA7B' +readonly RED='#FF5555' +# FontAwesome suitcase (the bar's icon font): an interview project's trip +# question (milestone trip_ask:*) is still open, pending or missed. +readonly SUITCASE=$'\uf0f2' emit() { # full_text, short_text, color: the three lines i3blocks reads. @@ -60,14 +67,21 @@ hide() { # can never add lines to the block's output. render_focus() { local rendered - rendered=$(jq -r --arg green "$GREEN" --arg yellow "$YELLOW" ' + rendered=$(jq -r --arg green "$GREEN" --arg yellow "$YELLOW" \ + --arg red "$RED" --arg suitcase "$SUITCASE" ' (.generated_at | if type == "number" then floor else "none" end), (.focus | if type == "object" and (.project | type) == "string" then (.minutes // 0 | floor) as $m | (.daily_minutes // 0 | floor) as $d | ((.name // .project) | tostring | gsub("[\u0001-\u001f\u007f]"; " ")) as $name - | "🎯 \($name) \($m)/\($d)", "🎯 \($m)/\($d)", - (if $d > 0 and $m >= $d then $green else $yellow end) + | (if any((.milestones | if type == "array" then .[] else empty end); + type == "object" and ((.id | type) == "string") + and (.id | startswith("trip_ask:")) + and (.state == "pending" or .state == "missed")) + then " " + $suitcase else "" end) as $trip + | "🎯 \($name) \($m)/\($d)\($trip)", "🎯 \($m)/\($d)\($trip)", + (if .missed == true then $red + elif $d > 0 and $m >= $d then $green else $yellow end) else "", "", $yellow end)' "$LIMITS_CACHE" 2>/dev/null) || return 1 printf '%s\n' "$rendered" >"$RENDER_CACHE" } diff --git a/linux_configuration/tests/test_i3blocks_focus_project.sh b/linux_configuration/tests/test_i3blocks_focus_project.sh index 080f0ca6..26447ea5 100755 --- a/linux_configuration/tests/test_i3blocks_focus_project.sh +++ b/linux_configuration/tests/test_i3blocks_focus_project.sh @@ -63,6 +63,51 @@ out=$(run_block) assert_eq '🎯 HackerRank prep 120/120' "$(line 1 "$out")" 'target reached text' assert_eq '#50FA7B' "$(line 3 "$out")" 'green at daily_minutes' +printf 'Checking interview milestones (red when missed, suitcase when a trip question is open)...\n' +RED='#FF5555' +SUITCASE=$'\uf0f2' +# focus_ms MISSED MINUTES MILESTONE_JSON_ARRAY +focus_ms() { + jq -nc --argjson x "$1" --argjson m "$2" --argjson ms "$3" \ + '{project:"p",name:"Interview",minutes:$m,daily_minutes:30,missed:$x,milestones:$ms}' +} +ms_json() { # ID STATE + jq -nc --arg i "$1" --arg s "$2" '[{id:$i,due:"2026-11-12T23:59:00",state:$s}]' +} +write_cache "$NOW" "$(focus_ms true 5 "$(ms_json rules missed)")" +out=$(run_block) +assert_eq "$RED" "$(line 3 "$out")" 'a missed milestone turns the block red' +assert_eq '🎯 Interview 5/30' "$(line 1 "$out")" 'red adds no text' +write_cache "$NOW" "$(focus_ms true 30 "$(ms_json rules missed)")" +assert_eq "$RED" "$(line 3 "$(run_block)")" 'red beats green once the daily target is reached' +write_cache "$NOW" "$(focus_ms false 30 "$(ms_json rules "done")")" +assert_eq '#50FA7B' "$(line 3 "$(run_block)")" 'all milestones done: not red' +write_cache "$NOW" "$(focus_ms false 5 "$(ms_json rules pending)")" +assert_eq '#F1FA8C' "$(line 3 "$(run_block)")" 'only pending milestones: not red' +write_cache "$NOW" "$(focus_json 'No milestones' 5 120)" +assert_eq '#F1FA8C' "$(line 3 "$(run_block)")" 'a project without milestones is not red' +write_cache "$NOW" '{"project":null,"minutes":0,"daily_minutes":0,"missed":true}' +assert_eq '' "$(line 1 "$(run_block)")" 'missed without a project stays hidden' + +write_cache "$NOW" "$(focus_ms false 5 "$(ms_json trip_ask:2026-11-14 pending)")" +out=$(run_block) +assert_eq "🎯 Interview 5/30 $SUITCASE" "$(line 1 "$out")" 'pending trip question shows the suitcase' +assert_eq "🎯 5/30 $SUITCASE" "$(line 2 "$out")" 'suitcase in the short text too' +assert_eq '#F1FA8C' "$(line 3 "$out")" 'a pending trip question alone is not red' +write_cache "$NOW" "$(focus_ms true 5 "$(ms_json trip_ask:2026-11-14 missed)")" +out=$(run_block) +assert_eq "🎯 Interview 5/30 $SUITCASE" "$(line 1 "$out")" 'missed trip question shows the suitcase' +assert_eq "$RED" "$(line 3 "$out")" 'missed trip question is red' +for state in "done" late_done; do + write_cache "$NOW" "$(focus_ms false 5 "$(ms_json trip_ask:2026-11-14 "$state")")" + assert_eq '🎯 Interview 5/30' "$(line 1 "$(run_block)")" "$state trip question shows no suitcase" +done +write_cache "$NOW" "$(focus_ms false 5 "$(ms_json phone:2026-11-14 pending)")" +assert_eq '🎯 Interview 5/30' "$(line 1 "$(run_block)")" 'other open milestones show no suitcase' + +write_cache "$NOW" "$(focus_ms false 5 '["junk",{"id":7},null]')" +assert_eq '🎯 Interview 5/30' "$(line 1 "$(run_block)")" 'malformed milestones are ignored, not fatal' + printf 'Checking the hidden states...\n' write_cache "$NOW" '{"project":null,"minutes":0,"daily_minutes":0}' out=$(run_block)