i3 focus block: red on miss, suitcase

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWZUTC9Zzdzsx7VW7dm
This commit is contained in:
Krzysztof Rudnicki 2026-11-10 15:45:31 +01:00
parent 7581a904cd
commit 47b4510ac6
4 changed files with 93 additions and 40 deletions

View File

@ -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"
]
}

View File

@ -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=<status.json from a scratch issue-dashboard server on a fixture source> 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'."
]
}

View File

@ -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"
}

View File

@ -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)