mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 19:07:04 +01:00
i3blocks: 🛋 Leisure label and hidden SHADOW block once leisure is live
Phase 2 of the leisure rollout, dormant until daily-limits' cache says "leisure_active": true. gaming_budget.sh then labels the block "🛋 Leisure" (short "🛋", same colour logic, stale state keeps the label); leisure_shadow.sh prints nothing so i3bar hides it. A missing, false or non-boolean key keeps today's output. Also: gaming_budget.sh floors generated_at and says "no data" for a non-numeric one; a fractional timestamp used to make it exit 1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dsh4Znwh2KnC7q6vbGpX1N
This commit is contained in:
parent
5e6aee64cf
commit
66321e555b
@ -0,0 +1,12 @@
|
||||
{
|
||||
"title": "i3blocks: leisure labels and hidden SHADOW block once the leisure budget is live",
|
||||
"objective": "Build the i3blocks half of leisure Phase 2 dormant: when daily-limits' cache has leisure_active true, gaming_budget.sh shows '🛋 Leisure' with the same colour logic and leisure_shadow.sh prints nothing; a missing key keeps current behaviour; both always exit 0 and stay shellcheck clean.",
|
||||
"acceptance_criteria": [
|
||||
"leisure_active true: gaming_budget full text starts '🛋 Leisure', short text '🛋', colours unchanged (green/yellow/orange/red/grey)",
|
||||
"leisure_active true: leisure_shadow prints 0 lines and exits 0, even for stale/no-data caches",
|
||||
"leisure_active missing/false/null/non-boolean: output identical to before",
|
||||
"gaming_budget exits 0 on a fractional or non-numeric generated_at"
|
||||
],
|
||||
"out_of_scope": ["daily-limits itself (own repo)", "enforcer and extension behaviour"],
|
||||
"verifier": "linux_configuration/tests/test_i3blocks_gaming_budget.sh, test_i3blocks_leisure_shadow.sh, test_i3blocks_efficiency.sh, shellcheck, pre-commit"
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
{
|
||||
"intent": "Phase 2 of the leisure rollout, dormant until go-live: once daily-limits' cache says leisure_active, the gaming block reads '🛋 Leisure ...' (same colours) and the SHADOW block hides itself.",
|
||||
"scope": [
|
||||
"linux_configuration/i3blocks/gaming_budget.sh, leisure_shadow.sh and their two test scripts",
|
||||
"Non-goal: no accounting, no new keys written; a missing/false key keeps today's output"
|
||||
],
|
||||
"changes": [
|
||||
"gaming_budget.sh: jq picks the label (🎮 or '🛋 Leisure') from .leisure_active == true; a 5th render line carries it to the stale state; an old 4-line render still reads as 🎮",
|
||||
"gaming_budget.sh: generated_at is floored and a non-numeric one says 'no data' (it used to exit 1 on a fractional timestamp)",
|
||||
"leisure_shadow.sh: when live the render is empty text + 'hidden' colour sentinel and the block prints nothing, exit 0, whatever the cache age"
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "bash linux_configuration/tests/test_i3blocks_gaming_budget.sh && bash linux_configuration/tests/test_i3blocks_leisure_shadow.sh && bash linux_configuration/tests/test_i3blocks_efficiency.sh",
|
||||
"result": "pass",
|
||||
"evidence": "All gaming_budget checks passed. All leisure_shadow checks passed. All i3blocks efficiency regression tests passed. (both new suites RED before the change)"
|
||||
},
|
||||
{
|
||||
"command": "shellcheck linux_configuration/i3blocks/*.sh linux_configuration/tests/test_i3blocks_{gaming_budget,leisure_shadow}.sh",
|
||||
"result": "pass",
|
||||
"evidence": "no findings"
|
||||
},
|
||||
{
|
||||
"command": "fixture caches (daily-limits sample report, leisure_active false/true) through both blocks",
|
||||
"result": "pass",
|
||||
"evidence": "off: '🎮 1h26 left · 1h34/3h00 ...' + '🛋 SHADOW 3h12 left · 1h48 used'; on: '🛋 Leisure 1h26 left · 1h34/3h00 ...' + (nothing), all exit 0"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"One tick after deploy the old 4-line render is read: shown as 🎮, never an error",
|
||||
"Hidden shadow block depends on daily-limits >= ed274f2 writing leisure_active"
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit; the symlinks in ~/.config/i3blocks pick it up on the next tick",
|
||||
"Check both blocks render from the live cache again"
|
||||
]
|
||||
}
|
||||
@ -10,6 +10,10 @@
|
||||
# when its mtime changes; the rendered block is kept in a small file, so a
|
||||
# steady-state tick forks nothing. A missing or >180 s old cache shows "⏱?",
|
||||
# never old numbers. Left-click opens `daily-limits --gui`.
|
||||
#
|
||||
# Phase 2 (leisure rollout): once the cache's top-level `leisure_active` is
|
||||
# true, the same block is the leisure budget: 🎮 becomes `🛋 Leisure`, colours
|
||||
# unchanged. A missing/false/non-boolean key keeps the 🎮 gaming label.
|
||||
# ============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
@ -41,7 +45,8 @@ if [[ ${BLOCK_BUTTON:-0} -eq 1 ]]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# One jq pass renders four lines: generated_at, full text, short text, color.
|
||||
# One jq pass renders five lines: generated_at, full text, short text, color
|
||||
# and the label (🎮, or `🛋 Leisure` once live) the stale state reuses.
|
||||
# Thresholds mirror the enforcer's own warn_at (1h, 30m, 10m). Bonuses still
|
||||
# not done are listed as " +💪2h +🧩?1h" ("?" = status unknown, still counted);
|
||||
# a gaming day other than the date is shown as " (10-08)"; used_minutes == null means the enforcer
|
||||
@ -54,28 +59,30 @@ render_limits() {
|
||||
def hm: . as $m
|
||||
| "\($m / 60 | floor)h\($m % 60 | tostring | if length < 2 then "0" + . else . end)";
|
||||
def bonus_hm: if . < 60 then "\(.)m" else hm | sub("h00$"; "h") end;
|
||||
{workout: "💪", leetcode: "🧩", reading: "📖", anki: "🗂", automation: "⚙", tutor: "⚙"} as $icon
|
||||
{workout: "💪", leetcode: "🧩", reading: "📖", anki: "🗂", automation: "⚙", tutor: "⚙"} as $earner_icon
|
||||
| .gaming as $g
|
||||
| (if .leisure_active == true then ["🛋 Leisure", "🛋"] else ["🎮", "🎮"] end) as [$label, $icon]
|
||||
| ([.earners[] | select(.status != "done" and .gaming_minutes > 0)
|
||||
| "+\($icon[.name] // .label)\(if .status == "unknown" then "?" else "" end)\(.gaming_minutes | bonus_hm)"]
|
||||
| "+\($earner_icon[.name] // .label)\(if .status == "unknown" then "?" else "" end)\(.gaming_minutes | bonus_hm)"]
|
||||
| if length > 0 then " " + join(" ") else "" end) as $todo
|
||||
| (if (.gaming.day // .date) != .date then " (\((.gaming.day // .date)[5:]))" else "" end) as $day
|
||||
| .generated_at,
|
||||
| (.generated_at | if type == "number" then floor else "none" end),
|
||||
(if $g.used_minutes == null then
|
||||
"🎮 budget \($g.budget_minutes | hm)\($day)\($todo)", "🎮 \($g.budget_minutes | hm)", $grey
|
||||
"\($label) budget \($g.budget_minutes | hm)\($day)\($todo)", "\($icon) \($g.budget_minutes | hm)", $grey
|
||||
else
|
||||
([$g.budget_minutes - $g.used_minutes, 0] | max) as $left
|
||||
| "\($g.used_minutes | hm)/\($g.budget_minutes | hm)\($day)" as $ratio
|
||||
| if $left <= 0 then
|
||||
"🎮 BLOCKED · \($ratio)\($todo)", "🎮 BLOCKED", $red
|
||||
"\($label) BLOCKED · \($ratio)\($todo)", "\($icon) BLOCKED", $red
|
||||
else
|
||||
"🎮 \($left | hm) left · \($ratio)\($todo)", "🎮 \($left | hm)",
|
||||
"\($label) \($left | hm) left · \($ratio)\($todo)", "\($icon) \($left | hm)",
|
||||
(if $left <= 10 then $red
|
||||
elif $left <= 30 then $orange
|
||||
elif $left <= 60 then $yellow
|
||||
else $green end)
|
||||
end
|
||||
end)' "$LIMITS_CACHE" 2>/dev/null) || return 1
|
||||
end),
|
||||
$label' "$LIMITS_CACHE" 2>/dev/null) || return 1
|
||||
printf '%s\n' "$rendered" >"$RENDER_CACHE"
|
||||
}
|
||||
|
||||
@ -100,6 +107,8 @@ fi
|
||||
read -r full_text
|
||||
read -r short_text
|
||||
read -r color
|
||||
# A render from before the label line (one tick after an upgrade) is 🎮.
|
||||
read -r label || label='🎮'
|
||||
} <"$RENDER_CACHE" || {
|
||||
emit '🎮 unreadable cache' '🎮 !' "$RED"
|
||||
exit 0
|
||||
@ -110,9 +119,14 @@ if [[ -n ${NOW_EPOCH:-} ]]; then
|
||||
else
|
||||
printf -v now '%(%s)T' -1
|
||||
fi
|
||||
# A cache without a numeric generated_at cannot be dated: say so, never crash.
|
||||
if [[ ! $generated_at =~ ^-?[0-9]+$ ]]; then
|
||||
emit "$label ⏱? no data" "${label%% *} ⏱?" "$ORANGE"
|
||||
exit 0
|
||||
fi
|
||||
age=$((now - generated_at))
|
||||
if ((age > STALE_AFTER_SECONDS || age < -STALE_AFTER_SECONDS)); then
|
||||
emit "🎮 ⏱? stale $((age / 60))m" '🎮 ⏱?' "$ORANGE"
|
||||
emit "$label ⏱? stale $((age / 60))m" "${label%% *} ⏱?" "$ORANGE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
@ -11,6 +11,11 @@
|
||||
# steady-state tick forks nothing. Missing numbers say "⏱? no data", never 0;
|
||||
# a >180 s old cache says "stale". Yellow under 30 min, red at would-be 0.
|
||||
# Never urgent: it always exits 0 (i3blocks flashes exit 33 as urgent).
|
||||
#
|
||||
# Phase 2 (leisure rollout): once the cache's top-level `leisure_active` is
|
||||
# true the shadow week is over and the block prints nothing (i3bar hides it);
|
||||
# gaming_budget.sh shows the real leisure budget instead. A missing/false key
|
||||
# keeps the shadow block.
|
||||
# ============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
@ -31,6 +36,8 @@ emit() {
|
||||
}
|
||||
|
||||
# One jq pass renders four lines: generated_at, full text, short text, color.
|
||||
# Once live full/short are empty and color is "hidden" (non-empty, so $(...)
|
||||
# cannot strip the lines): the "hidden" render.
|
||||
render_shadow() {
|
||||
local rendered
|
||||
rendered=$(jq -r \
|
||||
@ -40,7 +47,9 @@ render_shadow() {
|
||||
| "\($m / 60 | floor)h\($m % 60 | tostring | if length < 2 then "0" + . else . end)";
|
||||
(.leisure_shadow // {}) as $s
|
||||
| (.generated_at | if type == "number" then floor else "none" end),
|
||||
(if $s.used_minutes == null or $s.budget_minutes == null then
|
||||
(if .leisure_active == true then
|
||||
"", "", "hidden"
|
||||
elif $s.used_minutes == null or $s.budget_minutes == null then
|
||||
"🛋 SHADOW ⏱? no data", "🛋 ⏱?", $orange
|
||||
elif $s.would_block or ($s.left_minutes // 0) <= 0 then
|
||||
"🛋 SHADOW WOULD BLOCK · \($s.used_minutes | hm) used", "🛋 WOULD BLOCK", $red
|
||||
@ -78,6 +87,9 @@ fi
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Hidden (leisure budget live): print nothing, whatever the cache's age.
|
||||
[[ -n $full_text ]] || exit 0
|
||||
|
||||
if [[ -n ${NOW_EPOCH:-} ]]; then
|
||||
now=$NOW_EPOCH
|
||||
else
|
||||
|
||||
@ -104,6 +104,44 @@ assert_eq '🎮 2h00 left · 1h00/3h00 (10-08) +🧩1h' "$(line 1)" 'previous ga
|
||||
write_cache 60 180 "$NOW" "$D" leetcode:todo:60
|
||||
assert_eq '🎮 2h00 left · 1h00/3h00 +🧩1h' "$(line 1)" 'same day: no note'
|
||||
|
||||
# set_live VALUE -- rewrite the cache's top-level leisure_active (Phase 2).
|
||||
set_live() {
|
||||
jq -c ".leisure_active = $1" "$CACHE" >"$CACHE.new" && mv "$CACHE.new" "$CACHE"
|
||||
rm -f "$RENDER"
|
||||
}
|
||||
|
||||
printf 'Checking the leisure budget, once live, is labelled 🛋 Leisure...\n'
|
||||
write_cache 360 480 "$NOW" "$D" workout:done:120 reading:todo:60
|
||||
set_live true
|
||||
assert_eq '🛋 Leisure 2h00 left · 6h00/8h00 +📖1h' "$(line 1)" 'live full text'
|
||||
assert_eq '🛋 2h00' "$(line 2)" 'live short text'
|
||||
assert_eq '#50FA7B' "$(line 3)" 'live keeps the colour logic (green)'
|
||||
assert_eq '3' "$(run_block | wc -l)" 'live emits exactly full/short/colour'
|
||||
write_cache 460 480 "$NOW" "$D"
|
||||
set_live true
|
||||
assert_eq '#FFB86C' "$(line 3)" 'live keeps the colour logic (orange)'
|
||||
write_cache 300 300 "$NOW" "$D" leetcode:todo:60
|
||||
set_live true
|
||||
assert_eq '🛋 Leisure BLOCKED · 5h00/5h00 +🧩1h' "$(line 1)" 'live blocked text'
|
||||
assert_eq '🛋 BLOCKED' "$(line 2)" 'live blocked short text'
|
||||
assert_eq '#FF5555' "$(line 3)" 'live blocked is red'
|
||||
write_cache null 180 "$NOW" "$D"
|
||||
set_live true
|
||||
assert_eq '🛋 Leisure budget 3h00' "$(line 1)" 'live budget only'
|
||||
assert_eq '🛋 3h00' "$(line 2)" 'live budget only short text'
|
||||
write_cache 60 180 "$((NOW - 181))" "$D"
|
||||
set_live true
|
||||
assert_eq '🛋 Leisure ⏱? stale 3m' "$(line 1)" 'live stale keeps the leisure label'
|
||||
assert_eq '🛋 ⏱?' "$(line 2)" 'live stale short text'
|
||||
for value in false null '"yes"'; do
|
||||
write_cache 60 180 "$NOW" "$D"
|
||||
set_live "$value"
|
||||
assert_eq '🎮 2h00 left · 1h00/3h00' "$(line 1)" "leisure_active $value is not live"
|
||||
done
|
||||
printf '%s\n%s\n%s\n%s\n' "$NOW" '🎮 old render' '🎮 old' '#50FA7B' >"$RENDER"
|
||||
touch -d '+1 min' "$RENDER"
|
||||
assert_eq '🎮 old render' "$(line 1)" 'a 4-line render from before the label line still shows'
|
||||
|
||||
printf 'Checking a stale or missing cache never shows numbers as fresh...\n'
|
||||
write_cache 60 180 "$((NOW - 181))" "$D"
|
||||
assert_eq '🎮 ⏱? stale 3m' "$(line 1)" 'older than 180 s'
|
||||
@ -114,6 +152,19 @@ rm -f "$CACHE"
|
||||
assert_eq '🎮 ⏱? no data' "$(line 1)" 'missing cache'
|
||||
assert_eq '🎮 ⏱?' "$(line 2)" 'missing cache short text'
|
||||
|
||||
printf 'Checking an odd generated_at never crashes the block (always exit 0)...\n'
|
||||
write_cache 60 180 "$NOW.75" "$D"
|
||||
assert_eq '🎮 2h00 left · 1h00/3h00' "$(line 1)" 'a fractional generated_at is a timestamp'
|
||||
for generated in null '"soon"'; do
|
||||
write_cache 60 180 "$generated" "$D"
|
||||
set +e
|
||||
output=$(run_block 2>&1)
|
||||
status=$?
|
||||
set -e
|
||||
assert_eq '0' "$status" "generated_at $generated still exits 0"
|
||||
assert_eq '🎮 ⏱? no data' "$(sed -n 1p <<<"$output")" "generated_at $generated says no data"
|
||||
done
|
||||
|
||||
printf 'Checking an unreadable cache is red...\n'
|
||||
printf 'not json' >"$CACHE"
|
||||
rm -f "$RENDER"
|
||||
|
||||
@ -140,6 +140,29 @@ touch "$CACHE"
|
||||
PATH="$TMP_DIR/bin:$PATH" run_block >/dev/null
|
||||
assert_eq '2' "$(wc -l <"$TMP_DIR/jq.calls")" 'a rewritten cache is re-parsed'
|
||||
|
||||
printf 'Checking the block hides itself once the leisure budget is live...\n'
|
||||
# set_live VALUE -- rewrite the cache's top-level leisure_active (Phase 2).
|
||||
set_live() {
|
||||
jq -c ".leisure_active = $1" "$CACHE" >"$CACHE.new" && mv "$CACHE.new" "$CACHE"
|
||||
rm -f "$RENDER"
|
||||
}
|
||||
for spec in '108 300' '300 300' 'null null' "60 300 $((NOW - 600))"; do
|
||||
read -r used budget generated <<<"$spec"
|
||||
write_cache "$used" "$budget" "${generated:-$NOW}"
|
||||
set_live true
|
||||
set +e
|
||||
output=$(run_block)
|
||||
status=$?
|
||||
set -e
|
||||
assert_eq '0' "$status" "live ($spec) exits 0"
|
||||
assert_eq '' "$output" "live ($spec) prints nothing: the block is hidden"
|
||||
done
|
||||
for value in false null; do
|
||||
write_cache 108 300
|
||||
set_live "$value"
|
||||
assert_eq '🛋 SHADOW 3h12 left · 1h48 used' "$(line 1)" "leisure_active $value still shows the shadow"
|
||||
done
|
||||
|
||||
printf 'Checking the config wires the block next to gaming_budget...\n'
|
||||
CONFIG="$REPO_DIR/i3blocks/config"
|
||||
grep -A3 '^\[leisure_shadow\]$' "$CONFIG" | grep -q '^command=~/.config/i3blocks/leisure_shadow.sh$' ||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user