mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 20:27:02 +01:00
i3blocks: shadow-week leisure block (leisure_shadow.sh)
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 (${{ matrix.group }}) (mypy, mypy) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (pylint, pylint) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (rest, ) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (shellcheck, shellcheck) (push) Waiting to run
Python tests / test (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / lib-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
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 (${{ matrix.group }}) (mypy, mypy) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (pylint, pylint) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (rest, ) (push) Waiting to run
Pre-commit checks / pre-commit (${{ matrix.group }}) (shellcheck, shellcheck) (push) Waiting to run
Python tests / test (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / lib-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Renders daily-limits' `leisure_shadow` cache (contract C5): `🛋 SHADOW 3h12 left · 1h48 used`, yellow under 30 min, red `WOULD BLOCK` at 0, never urgent; `⏱? no data` while the enforcer has no C4 fields. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dsh4Znwh2KnC7q6vbGpX1N
This commit is contained in:
parent
09fa296445
commit
e190d9a4bd
1
.github/workflows/shell-tests.yml
vendored
1
.github/workflows/shell-tests.yml
vendored
@ -31,6 +31,7 @@ jobs:
|
||||
set -euo pipefail
|
||||
tests=(
|
||||
test_i3blocks_gaming_budget.sh
|
||||
test_i3blocks_leisure_shadow.sh
|
||||
test_i3blocks_persist_common.sh
|
||||
test_i3blocks_shutdown_hint.sh
|
||||
test_setup_dns_blocker.sh
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
{
|
||||
"title": "i3blocks: shadow-week leisure block (leisure_shadow.sh)",
|
||||
"objective": "Render daily-limits' `leisure_shadow` cache (contract C5) as `🛋 SHADOW <left> left · <used> used`, yellow under 30 min, red `🛋 SHADOW WOULD BLOCK · <used> used` at 0, never urgent.",
|
||||
"acceptance_criteria": [
|
||||
"bash linux_configuration/tests/test_i3blocks_leisure_shadow.sh passes",
|
||||
"null/absent numbers render `🛋 SHADOW ⏱? no data`, never 0",
|
||||
"the block always exits 0 and prints exactly three lines",
|
||||
"[leisure_shadow] sits right after [gaming_budget] in linux_configuration/i3blocks/config"
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Renaming the gaming block or its labels (Phase 2)",
|
||||
"Any accounting: the numbers come from steam-backlog-enforcer via daily-limits"
|
||||
],
|
||||
"verifier": "the test script above, shellcheck, and the block run against a forced fixture cache"
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
{
|
||||
"intent": "Show the shadow week's would-be leisure budget on i3bar: `🛋 SHADOW 3h12 left · 1h48 used`, yellow under 30 min, red `WOULD BLOCK` at 0, never urgent/blinking, honest `⏱? no data` while the enforcer's C4 fields are absent.",
|
||||
"scope": [
|
||||
"linux_configuration/i3blocks/leisure_shadow.sh (new block), its [leisure_shadow] config entry right after [gaming_budget]",
|
||||
"linux_configuration/tests/test_i3blocks_leisure_shadow.sh + shell-tests.yml list",
|
||||
"Non-goal: renaming the gaming block (Phase 2); the accounting (steam-backlog-enforcer) and the cache (daily-limits) live elsewhere"
|
||||
],
|
||||
"changes": [
|
||||
"leisure_shadow.sh renders daily-limits' cache `leisure_shadow` (contract C5) with gaming_budget.sh's render-cache + single jq pass; always exits 0 so i3blocks never marks it urgent",
|
||||
"config: [leisure_shadow] block, interval=30, next to [gaming_budget]"
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "bash linux_configuration/tests/test_i3blocks_leisure_shadow.sh",
|
||||
"result": "pass",
|
||||
"evidence": "All leisure_shadow checks passed (text, 30/29/1/0 boundaries, no data x3, stale, garbage, exit 0 + 3 lines, jq once per mtime, config placement)"
|
||||
},
|
||||
{
|
||||
"command": "DAILY_LIMITS_CACHE=<forced fixture 570/600> bash linux_configuration/i3blocks/leisure_shadow.sh",
|
||||
"result": "pass",
|
||||
"evidence": "🛋 SHADOW 0h30 left · 9h30 used / 🛋 0h30 / #F8F8F2; live cache (no C4 yet): 🛋 SHADOW ⏱? no data"
|
||||
},
|
||||
{
|
||||
"command": "shellcheck linux_configuration/i3blocks/leisure_shadow.sh linux_configuration/tests/test_i3blocks_leisure_shadow.sh; bash linux_configuration/tests/test_i3blocks_efficiency.sh",
|
||||
"result": "pass",
|
||||
"evidence": "shellcheck clean; All i3blocks efficiency regression tests passed"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"Until steam-backlog-enforcer writes the C4 fields the block reads `⏱? no data` (by design)",
|
||||
"One more 30 s block: a steady tick forks nothing (render cache), jq only when the cache changes"
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit and rm ~/.config/i3blocks/leisure_shadow.sh, then pkill -x i3blocks; i3-msg reload",
|
||||
"Check the bar shows the gaming block again with no gap"
|
||||
]
|
||||
}
|
||||
@ -55,6 +55,11 @@ color=#FFFFFF
|
||||
command=~/.config/i3blocks/gaming_budget.sh
|
||||
interval=30
|
||||
|
||||
# Shadow week: the would-be leisure budget. Never urgent (no flashing).
|
||||
[leisure_shadow]
|
||||
command=~/.config/i3blocks/leisure_shadow.sh
|
||||
interval=30
|
||||
|
||||
[screen_locker]
|
||||
command=~/.config/i3blocks/screen_locker.sh
|
||||
interval=30
|
||||
|
||||
92
linux_configuration/i3blocks/leisure_shadow.sh
Executable file
92
linux_configuration/i3blocks/leisure_shadow.sh
Executable file
@ -0,0 +1,92 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ============================================================================
|
||||
# Shadow-week leisure budget for i3blocks: what the leisure budget WOULD say
|
||||
# (`🛋 SHADOW 3h12 left · 1h48 used`) while only games are enforced.
|
||||
#
|
||||
# Renders the `leisure_shadow` part of daily-limits' cache
|
||||
# ($XDG_RUNTIME_DIR/daily-limits.json, rewritten every 60 s; contract C5) and
|
||||
# owns no accounting. Same shape as gaming_budget.sh: jq re-parses only when
|
||||
# the cache's mtime changes, the rendered block is kept in a small file, so a
|
||||
# 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).
|
||||
# ============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
readonly LIMITS_CACHE="${DAILY_LIMITS_CACHE:-${XDG_RUNTIME_DIR:-/run/user/$UID}/daily-limits.json}"
|
||||
readonly RENDER_CACHE="${LEISURE_SHADOW_RENDER_CACHE:-${XDG_RUNTIME_DIR:-/run/user/$UID}/i3blocks-leisure-shadow.render}"
|
||||
readonly STALE_AFTER_SECONDS=180
|
||||
|
||||
# Dracula-ish palette, matching gaming_budget.sh.
|
||||
readonly RED='#FF5555'
|
||||
readonly YELLOW='#F1FA8C'
|
||||
readonly ORANGE='#FFB86C'
|
||||
readonly FOREGROUND='#F8F8F2'
|
||||
|
||||
emit() {
|
||||
# full_text, short_text, color — the three lines i3blocks reads.
|
||||
printf '%s\n%s\n%s\n' "$1" "$2" "$3"
|
||||
}
|
||||
|
||||
# One jq pass renders four lines: generated_at, full text, short text, color.
|
||||
render_shadow() {
|
||||
local rendered
|
||||
rendered=$(jq -r \
|
||||
--arg red "$RED" --arg yellow "$YELLOW" --arg orange "$ORANGE" \
|
||||
--arg fg "$FOREGROUND" '
|
||||
def hm: . as $m
|
||||
| "\($m / 60 | floor)h\($m % 60 | tostring | if length < 2 then "0" + . else . end)";
|
||||
(.leisure_shadow // {}) as $s
|
||||
| .generated_at,
|
||||
(if $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
|
||||
else
|
||||
"🛋 SHADOW \($s.left_minutes | hm) left · \($s.used_minutes | hm) used",
|
||||
"🛋 \($s.left_minutes | hm)",
|
||||
(if $s.left_minutes < 30 then $yellow else $fg end)
|
||||
end)' "$LIMITS_CACHE" 2>/dev/null) || return 1
|
||||
printf '%s\n' "$rendered" >"$RENDER_CACHE"
|
||||
}
|
||||
|
||||
if [[ ! -r $LIMITS_CACHE ]]; then
|
||||
emit '🛋 SHADOW ⏱? no data' '🛋 ⏱?' "$ORANGE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ $LIMITS_CACHE -nt $RENDER_CACHE ]]; then
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
emit '🛋 SHADOW no jq' '🛋 ?' "$ORANGE"
|
||||
exit 0
|
||||
fi
|
||||
render_shadow || {
|
||||
emit '🛋 SHADOW unreadable cache' '🛋 !' "$RED"
|
||||
exit 0
|
||||
}
|
||||
fi
|
||||
|
||||
{
|
||||
read -r generated_at
|
||||
read -r full_text
|
||||
read -r short_text
|
||||
read -r color
|
||||
} <"$RENDER_CACHE" || {
|
||||
emit '🛋 SHADOW unreadable cache' '🛋 !' "$RED"
|
||||
exit 0
|
||||
}
|
||||
|
||||
if [[ -n ${NOW_EPOCH:-} ]]; then
|
||||
now=$NOW_EPOCH
|
||||
else
|
||||
printf -v now '%(%s)T' -1
|
||||
fi
|
||||
age=$((now - generated_at))
|
||||
if ((age > STALE_AFTER_SECONDS || age < -STALE_AFTER_SECONDS)); then
|
||||
emit "🛋 SHADOW ⏱? stale $((age / 60))m" '🛋 ⏱?' "$ORANGE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
emit "$full_text" "$short_text" "$color"
|
||||
136
linux_configuration/tests/test_i3blocks_leisure_shadow.sh
Executable file
136
linux_configuration/tests/test_i3blocks_leisure_shadow.sh
Executable file
@ -0,0 +1,136 @@
|
||||
#!/bin/bash
|
||||
# Behaviour tests for the i3blocks leisure_shadow block: it renders the
|
||||
# `leisure_shadow` part of daily-limits' cache ($DAILY_LIMITS_CACHE, contract
|
||||
# C5). Covers text, the yellow/red thresholds, the "no data" states (no cache,
|
||||
# no key, null numbers), stale/garbage caches, "never urgent" and the
|
||||
# "re-parse only when the cache changes" contract.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
REPO_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd)
|
||||
BLOCK="$REPO_DIR/i3blocks/leisure_shadow.sh"
|
||||
TMP_DIR=$(mktemp -d)
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
fail() {
|
||||
printf 'FAIL: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
assert_eq() {
|
||||
local expected=$1
|
||||
local actual=$2
|
||||
local context=$3
|
||||
if [[ "$expected" != "$actual" ]]; then
|
||||
fail "$context (expected '$expected', actual '$actual')"
|
||||
fi
|
||||
}
|
||||
|
||||
printf '{}' | jq -e . >/dev/null 2>&1 ||
|
||||
fail 'a working jq is required for the leisure_shadow tests (pacman -S jq)'
|
||||
|
||||
CACHE="$TMP_DIR/daily-limits.json"
|
||||
RENDER="$TMP_DIR/render"
|
||||
NOW=1790000000
|
||||
|
||||
# write_cache USED BUDGET [GENERATED_AT] -- USED/BUDGET may be "null";
|
||||
# would_block and left_minutes follow from them as daily-limits writes them.
|
||||
write_cache() {
|
||||
local used=$1 budget=$2 generated=${3:-$NOW} left=null block=false
|
||||
if [[ $used != null && $budget != null ]]; then
|
||||
left=$((budget > used ? budget - used : 0))
|
||||
((used >= budget)) && block=true
|
||||
fi
|
||||
printf '{"date":"2026-10-19","generated_at":%s,"gaming":{"day":"2026-10-19","budget_minutes":300,"used_minutes":0,"ceiling_minutes":480},"leisure_shadow":{"day":"2026-10-19","budget_minutes":%s,"used_minutes":%s,"left_minutes":%s,"would_block":%s,"sources":[]}}\n' \
|
||||
"$generated" "$budget" "$used" "$left" "$block" >"$CACHE"
|
||||
rm -f "$RENDER" # force a re-render for the new content
|
||||
}
|
||||
|
||||
run_block() {
|
||||
NOW_EPOCH="$NOW" DAILY_LIMITS_CACHE="$CACHE" LEISURE_SHADOW_RENDER_CACHE="$RENDER" bash "$BLOCK"
|
||||
}
|
||||
|
||||
line() {
|
||||
run_block | sed -n "${1}p"
|
||||
}
|
||||
|
||||
printf 'Checking the normal day: time left and used...\n'
|
||||
write_cache 108 300
|
||||
assert_eq '🛋 SHADOW 3h12 left · 1h48 used' "$(line 1)" 'full text'
|
||||
assert_eq '🛋 3h12' "$(line 2)" 'short text'
|
||||
assert_eq '#F8F8F2' "$(line 3)" 'plain foreground above 30 min'
|
||||
assert_eq '3' "$(run_block | wc -l)" 'exactly full/short/colour, no urgent line'
|
||||
|
||||
printf 'Checking yellow strictly under 30 min...\n'
|
||||
write_cache 270 300
|
||||
assert_eq '#F8F8F2' "$(line 3)" '30 min left is not yet yellow'
|
||||
write_cache 271 300
|
||||
assert_eq '#F1FA8C' "$(line 3)" '29 min left is yellow'
|
||||
assert_eq '🛋 SHADOW 0h29 left · 4h31 used' "$(line 1)" 'yellow text'
|
||||
write_cache 299 300
|
||||
assert_eq '#F1FA8C' "$(line 3)" '1 min left is still yellow, not red'
|
||||
|
||||
printf 'Checking would-block at and over 0, never urgent...\n'
|
||||
write_cache 300 300
|
||||
assert_eq '🛋 SHADOW WOULD BLOCK · 5h00 used' "$(line 1)" 'at 0'
|
||||
assert_eq '🛋 WOULD BLOCK' "$(line 2)" 'would-block short text'
|
||||
assert_eq '#FF5555' "$(line 3)" 'would-block is red'
|
||||
write_cache 605 600
|
||||
assert_eq '🛋 SHADOW WOULD BLOCK · 10h05 used' "$(line 1)" 'over'
|
||||
set +e
|
||||
run_block >/dev/null
|
||||
status=$?
|
||||
set -e
|
||||
assert_eq '0' "$status" 'exit 0: i3blocks marks exit 33 urgent (flashing)'
|
||||
assert_eq '3' "$(run_block | wc -l)" 'would-block emits no urgent line'
|
||||
|
||||
printf 'Checking "no data" is said, never faked as zero...\n'
|
||||
write_cache null null
|
||||
assert_eq '🛋 SHADOW ⏱? no data' "$(line 1)" 'C4 fields absent'
|
||||
assert_eq '🛋 ⏱?' "$(line 2)" 'no data short text'
|
||||
assert_eq '#FFB86C' "$(line 3)" 'no data is orange'
|
||||
printf '{"date":"2026-10-19","generated_at":%s,"gaming":{}}\n' "$NOW" >"$CACHE"
|
||||
rm -f "$RENDER"
|
||||
assert_eq '🛋 SHADOW ⏱? no data' "$(line 1)" 'a cache without leisure_shadow'
|
||||
rm -f "$CACHE"
|
||||
assert_eq '🛋 SHADOW ⏱? no data' "$(line 1)" 'missing cache'
|
||||
|
||||
printf 'Checking a stale or garbage cache...\n'
|
||||
write_cache 60 300 "$((NOW - 181))"
|
||||
assert_eq '🛋 SHADOW ⏱? stale 3m' "$(line 1)" 'older than 180 s'
|
||||
assert_eq '#FFB86C' "$(line 3)" 'stale is orange'
|
||||
write_cache 60 300 "$((NOW - 180))"
|
||||
assert_eq '🛋 SHADOW 4h00 left · 1h00 used' "$(line 1)" '180 s is still fresh'
|
||||
printf 'not json' >"$CACHE"
|
||||
rm -f "$RENDER"
|
||||
assert_eq '🛋 SHADOW unreadable cache' "$(line 1)" 'garbage body'
|
||||
assert_eq '#FF5555' "$(line 3)" 'garbage is red'
|
||||
|
||||
printf 'Checking jq runs only when the cache file changes...\n'
|
||||
write_cache 60 300
|
||||
mkdir -p "$TMP_DIR/bin"
|
||||
printf '#!/bin/bash\necho x >>"%s/jq.calls"\nexec %s "$@"\n' "$TMP_DIR" "$(command -v jq)" >"$TMP_DIR/bin/jq"
|
||||
chmod +x "$TMP_DIR/bin/jq"
|
||||
PATH="$TMP_DIR/bin:$PATH" run_block >/dev/null
|
||||
PATH="$TMP_DIR/bin:$PATH" run_block >/dev/null
|
||||
assert_eq '1' "$(wc -l <"$TMP_DIR/jq.calls")" 'two ticks, one unchanged cache: one jq run'
|
||||
sleep 0.05
|
||||
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 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$' ||
|
||||
fail '[leisure_shadow] should run leisure_shadow.sh'
|
||||
next_section=$(sed -n '/^\[gaming_budget\]$/,$p' "$CONFIG" | grep -m2 '^\[' | tail -1)
|
||||
assert_eq '[leisure_shadow]' "$next_section" '[leisure_shadow] sits right after [gaming_budget]'
|
||||
! grep -A4 '^\[leisure_shadow\]$' "$CONFIG" | grep -q '^urgent' ||
|
||||
fail '[leisure_shadow] must never be urgent'
|
||||
|
||||
printf 'All leisure_shadow checks passed.\n'
|
||||
Loading…
Reference in New Issue
Block a user