mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 20:07:04 +01:00
i3blocks: gaming budget block (time left, used/total, unearned bonuses)
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
Python tests / test (push) Waiting to run
Shell tests / shell-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 (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
New [gaming_budget] block reads steam-backlog-enforcer's /api/budget every 30s: "🎮 2h00 left · 6h00/8h00 +📖1h". Colours follow the enforcer warn_at (1h/30m/10m), blocked is red, a dead server shows the cached value as stale. Unearned bonus sizes come from the enforcer config via jq --slurpfile. Prunes the oldest evidence artifact to stay under the 60-file cap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CD7nSVE2dKvByG8hdSi3gm
This commit is contained in:
parent
6832805e5d
commit
ae99698d5b
1
.github/workflows/shell-tests.yml
vendored
1
.github/workflows/shell-tests.yml
vendored
@ -26,6 +26,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tests=(
|
||||
test_i3blocks_gaming_budget.sh
|
||||
test_i3blocks_persist_common.sh
|
||||
test_setup_dns_blocker.sh
|
||||
)
|
||||
|
||||
@ -0,0 +1,44 @@
|
||||
{
|
||||
"intent": "Show today's gaming budget in the i3 bar: time left, used/total, and which bonuses (workout, LeetCode, reading) can still be earned.",
|
||||
"scope": [
|
||||
"linux_configuration/i3blocks/gaming_budget.sh (new block), linux_configuration/i3blocks/config",
|
||||
"linux_configuration/tests/test_i3blocks_gaming_budget.sh, .github/workflows/shell-tests.yml",
|
||||
"Non-goal: no changes to steam-backlog-enforcer, screen-locker, leetcode-guard or book-guard; the block only reads /api/budget"
|
||||
],
|
||||
"changes": [
|
||||
"New interval=30 block reads steam-backlog-enforcer GET /api/budget with one curl + one jq pass; colours follow the enforcer warn_at (1h yellow, 30m orange, 10m/blocked red)",
|
||||
"Unearned-bonus sizes come from ~/.config/steam_backlog_enforcer/config.json via jq --slurpfile (never on argv, the file holds secrets), falling back to the enforcer defaults 2h/1h/1h",
|
||||
"Never blank: dead server shows cached text marked (stale) in orange or 'budget server down' in red; bad state_status, ok:false and unparseable bodies are red and do not overwrite the cache",
|
||||
"Behaviour test with a PATH-stubbed curl, wired into the shell-tests CI list"
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "~/.config/i3blocks/gaming_budget.sh",
|
||||
"result": "pass",
|
||||
"evidence": "Live API: '🎮 2h00 left · 6h00/8h00 +📖1h' / '🎮 2h00' / #50FA7B, matching /api/budget (21600s used of 28800, reading bonus 0)"
|
||||
},
|
||||
{
|
||||
"command": "i3-msg restart (run by the user)",
|
||||
"result": "pass",
|
||||
"evidence": "User confirmed the block renders in the bar as intended"
|
||||
},
|
||||
{
|
||||
"command": "bash linux_configuration/tests/test_i3blocks_gaming_budget.sh",
|
||||
"result": "pass",
|
||||
"evidence": "All gaming_budget checks passed (thresholds, blocked, overspent, config bonus sizes, stale, down, error states)"
|
||||
},
|
||||
{
|
||||
"command": "bash linux_configuration/tests/test_i3blocks_efficiency.sh",
|
||||
"result": "pass",
|
||||
"evidence": "All i3blocks efficiency regression tests passed"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"Depends on the /api/budget JSON shape (today.*, rules.bonuses.*); a renamed field renders 'unreadable budget' in red rather than a wrong number",
|
||||
"Bonus fallback sizes duplicate the enforcer defaults; they only matter when config.json leaves the fields unset"
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit and remove ~/.config/i3blocks/gaming_budget.sh symlink",
|
||||
"i3-msg restart and confirm the bar renders without the block"
|
||||
]
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
{
|
||||
"intent": "Restore the four test seams in nvidia_config.sh, which were verified and measured but did not reach the previous commit, so the committed suite read and wrote the real system files.",
|
||||
"scope": [
|
||||
"linux_configuration/scripts/single_use/fixes/lib/nvidia_config.sh",
|
||||
"Non-goal: any change to the tests, which were already correct"
|
||||
],
|
||||
"changes": [
|
||||
"Re-applied the XORG_CONF, XORG_CONF_D, PROFILE_FILE and USER_HOME defaults at their assignment sites",
|
||||
"Grepped the file for all four immediately after writing it, before running anything, because a silently-reverted edit is exactly what caused this"
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "ci-mirror during git push",
|
||||
"result": "fail",
|
||||
"evidence": "'lib/tests suite failed: .../fixes/lib/tests/run_all.sh'. ci-mirror runs from a clean checkout of committed files, which is why it caught what the local run could not."
|
||||
},
|
||||
{
|
||||
"command": "bash run_all.sh from a detached worktree at the previous commit",
|
||||
"result": "fail",
|
||||
"evidence": "EXIT 1 with 10 failures, including 'configure_xorg: enables RenderAccel (expected \"RenderAccel\" \"true\", got: # NVIDIA configuration with RenderAccel disabled)' -- the suite was reading this machine's real /etc/X11 config, and 'does not re-clone over a checkout' showed a clone path of /home/testuser/pyroveil rather than the tmpdir."
|
||||
},
|
||||
{
|
||||
"command": "grep -n 'XORG_CONF:-|XORG_CONF_D:-|PROFILE_FILE:-|USER_HOME:-' nvidia_config.sh",
|
||||
"result": "pass",
|
||||
"evidence": "All four present at lines 26, 27, 60 and 85 after re-applying."
|
||||
},
|
||||
{
|
||||
"command": "shellcheck + shfmt -l on nvidia_config.sh",
|
||||
"result": "pass",
|
||||
"evidence": "No findings, zero suppressions, no reformat pending."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"The previous commit, cb1e8c53, is pushed and briefly had a suite that would write to the real /etc/X11/xorg.conf.d on any machine that ran it un-jailed. Nothing ran it in that window on this machine -- the real 20-nvidia.conf still carries its Jun 2025 mtime.",
|
||||
"The local run passed against a stale seamed copy in the measurement worktree, which is why a clean-checkout run is now the check that matters before pushing."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit; nvidia_config.sh returns to hardcoded paths and the suite will fail again from a clean checkout",
|
||||
"Re-run run_all.sh from a detached worktree to confirm the failure mode returns"
|
||||
]
|
||||
}
|
||||
@ -51,6 +51,10 @@ interval=30
|
||||
color=#FFFFFF
|
||||
|
||||
|
||||
[gaming_budget]
|
||||
command=~/.config/i3blocks/gaming_budget.sh
|
||||
interval=30
|
||||
|
||||
[screen_locker]
|
||||
command=~/.config/i3blocks/screen_locker.sh
|
||||
interval=30
|
||||
|
||||
102
linux_configuration/i3blocks/gaming_budget.sh
Executable file
102
linux_configuration/i3blocks/gaming_budget.sh
Executable file
@ -0,0 +1,102 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ============================================================================
|
||||
# Gaming time budget for i3blocks: time left today, used/total, and which
|
||||
# bonuses (workout, LeetCode, reading) are still there to be earned.
|
||||
#
|
||||
# Reads steam-backlog-enforcer's /api/budget, which already folds in the
|
||||
# screen-locker, leetcode-guard and book-guard bonuses. Like screen_locker.sh
|
||||
# it is never blank: a dead server shows the last value marked stale, or red.
|
||||
# Click opens the enforcer's web UI.
|
||||
# ============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
readonly API="${GAMING_BUDGET_API:-http://127.0.0.1:8000/api/budget}"
|
||||
readonly UI='http://127.0.0.1:8000/'
|
||||
# Only for the *size* of bonuses not yet earned; /api/budget reports earned
|
||||
# ones as 0. The file holds secrets, so it goes to jq by path, never on argv.
|
||||
readonly CONFIG="${GAMING_BUDGET_CONFIG:-$HOME/.config/steam_backlog_enforcer/config.json}"
|
||||
readonly CACHE="${GAMING_BUDGET_CACHE:-${XDG_RUNTIME_DIR:-/tmp}/i3blocks-gaming-budget.cache}"
|
||||
|
||||
# Dracula-ish palette, matching screen_locker.sh.
|
||||
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"
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
budget_json=$(curl -fsS --max-time 2 "$API" 2>/dev/null || true)
|
||||
if [[ -z $budget_json ]]; then
|
||||
if [[ -r $CACHE ]]; then
|
||||
emit "$(<"$CACHE") (stale)" '🎮 ?' "$ORANGE"
|
||||
else
|
||||
emit '🎮 budget server down' '🎮 ?' "$RED"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
config_path=/dev/null
|
||||
[[ -r $CONFIG ]] && config_path=$CONFIG
|
||||
|
||||
# One jq pass renders all three lines, so a tick costs curl + jq and no more.
|
||||
# The first line is "cache" or "nocache": error states must not overwrite the
|
||||
# last good value that the stale path shows.
|
||||
# Thresholds mirror the enforcer's own warn_at (1h, 30m, 10m).
|
||||
rendered=$(jq -r \
|
||||
--slurpfile cfg "$config_path" \
|
||||
--arg red "$RED" --arg orange "$ORANGE" --arg yellow "$YELLOW" --arg green "$GREEN" '
|
||||
def hm: (. / 60 | round) as $m
|
||||
| "\($m / 60 | floor)h\($m % 60 | tostring | if length < 2 then "0" + . else . end)";
|
||||
def bonus_hm: hm | sub("h00$"; "h");
|
||||
($cfg[0] // {}) as $c
|
||||
| if .state_status != "ok" then ["nocache", "🎮 state \(.state_status)", "🎮 !", $red]
|
||||
elif .ok != true then ["nocache", "🎮 budget error", "🎮 !", $red]
|
||||
else
|
||||
.today as $t
|
||||
| .rules.bonuses as $b
|
||||
| ([$t.seconds_remaining, 0] | max) as $left
|
||||
| "\($t.seconds_used | hm)/\($t.budget_seconds | hm)" as $ratio
|
||||
# Unearned bonuses: [earned, configured size (enforcer default), icon].
|
||||
| [ [$b.workout, ($c.workout_bonus_seconds // 7200), "💪"],
|
||||
[$b.leetcode, ($c.leetcode_bonus_seconds // 3600), "🧩"],
|
||||
[$b.reading, ($c.reading_bonus_seconds // 3600), "📖"] ]
|
||||
| map(select((.[0] // 0) == 0 and .[1] > 0) | "+\(.[2])\(.[1] | bonus_hm)")
|
||||
| (if length > 0 then " " + join(" ") else "" end) as $todo
|
||||
| if $t.blocked or $left <= 0 then
|
||||
["cache", "🎮 BLOCKED · \($ratio)\($todo)", "🎮 BLOCKED", $red]
|
||||
else
|
||||
["cache", "🎮 \($left | hm) left · \($ratio)\($todo)", "🎮 \($left | hm)",
|
||||
(if $left <= 600 then $red
|
||||
elif $left <= 1800 then $orange
|
||||
elif $left <= 3600 then $yellow
|
||||
else $green end)]
|
||||
end
|
||||
end
|
||||
| .[]' <<<"$budget_json" 2>/dev/null) || {
|
||||
emit '🎮 unreadable budget' '🎮 !' "$RED"
|
||||
exit 0
|
||||
}
|
||||
|
||||
{
|
||||
read -r cache_flag
|
||||
read -r full_text
|
||||
read -r short_text
|
||||
read -r color
|
||||
} <<<"$rendered"
|
||||
|
||||
[[ $cache_flag == 'cache' ]] && printf '%s' "$full_text" >"$CACHE"
|
||||
emit "$full_text" "$short_text" "$color"
|
||||
114
linux_configuration/tests/test_i3blocks_gaming_budget.sh
Executable file
114
linux_configuration/tests/test_i3blocks_gaming_budget.sh
Executable file
@ -0,0 +1,114 @@
|
||||
#!/bin/bash
|
||||
# Behaviour tests for the i3blocks gaming_budget block: text, colour
|
||||
# thresholds, unearned-bonus suffix and every failure state. curl is stubbed on
|
||||
# PATH to serve a fixture file, so nothing talks to the real enforcer.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
REPO_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd)
|
||||
BLOCK="$REPO_DIR/i3blocks/gaming_budget.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 gaming_budget tests (pacman -S jq)'
|
||||
|
||||
# Stub curl: print $FIXTURE, or fail like `curl -f` on a dead server.
|
||||
BIN_DIR="$TMP_DIR/bin"
|
||||
mkdir -p "$BIN_DIR"
|
||||
cat >"$BIN_DIR/curl" <<'STUB'
|
||||
#!/bin/bash
|
||||
[[ -r ${FIXTURE:-} ]] || exit 7
|
||||
cat "$FIXTURE"
|
||||
STUB
|
||||
chmod +x "$BIN_DIR/curl"
|
||||
|
||||
FIXTURE="$TMP_DIR/budget.json"
|
||||
CACHE_FILE="$TMP_DIR/cache"
|
||||
CONFIG_FILE="$TMP_DIR/config.json"
|
||||
|
||||
# write_budget USED BUDGET REMAINING BLOCKED WORKOUT LEETCODE READING
|
||||
write_budget() {
|
||||
printf '{"ok":true,"state_status":"ok","today":{"seconds_used":%s,"budget_seconds":%s,"seconds_remaining":%s,"blocked":%s},"rules":{"bonuses":{"base":18000,"workout":%s,"leetcode":%s,"reading":%s}}}\n' \
|
||||
"$@" >"$FIXTURE"
|
||||
}
|
||||
|
||||
run_block() {
|
||||
PATH="$BIN_DIR:$PATH" FIXTURE="$FIXTURE" GAMING_BUDGET_CACHE="$CACHE_FILE" \
|
||||
GAMING_BUDGET_CONFIG="$CONFIG_FILE" bash "$BLOCK"
|
||||
}
|
||||
|
||||
line() {
|
||||
run_block | sed -n "${1}p"
|
||||
}
|
||||
|
||||
printf 'Checking the normal day: time left, used/total, missing bonus...\n'
|
||||
write_budget 21600.3 28800 7199.7 false 7200 3600 0
|
||||
assert_eq '🎮 2h00 left · 6h00/8h00 +📖1h' "$(line 1)" 'full text'
|
||||
assert_eq '🎮 2h00' "$(line 2)" 'short text'
|
||||
assert_eq '#50FA7B' "$(line 3)" 'green above 1h'
|
||||
|
||||
printf 'Checking colour thresholds follow the enforcer warn_at...\n'
|
||||
write_budget 25800 28800 3000 false 7200 3600 3600
|
||||
assert_eq '#F1FA8C' "$(line 3)" 'yellow at <=1h'
|
||||
assert_eq '🎮 0h50 left · 7h10/8h00' "$(line 1)" 'no suffix when every bonus is earned'
|
||||
write_budget 27600 28800 1200 false 7200 3600 3600
|
||||
assert_eq '#FFB86C' "$(line 3)" 'orange at <=30m'
|
||||
write_budget 28500 28800 300 false 7200 3600 3600
|
||||
assert_eq '#FF5555' "$(line 3)" 'red at <=10m'
|
||||
|
||||
printf 'Checking the blocked state...\n'
|
||||
write_budget 18000 18000 0 true 0 0 0
|
||||
assert_eq '🎮 BLOCKED · 5h00/5h00 +💪2h +🧩1h +📖1h' "$(line 1)" 'blocked full text lists what can still be earned'
|
||||
assert_eq '🎮 BLOCKED' "$(line 2)" 'blocked short text'
|
||||
assert_eq '#FF5555' "$(line 3)" 'blocked is red'
|
||||
write_budget 18100 18000 -100 false 7200 3600 3600
|
||||
assert_eq '🎮 BLOCKED' "$(line 2)" 'overspent counts as blocked even before the flag flips'
|
||||
|
||||
printf 'Checking bonus sizes come from the enforcer config...\n'
|
||||
printf '{"workout_bonus_seconds":5400,"leetcode_bonus_seconds":0}\n' >"$CONFIG_FILE"
|
||||
write_budget 0 18000 18000 false 0 0 0
|
||||
assert_eq '🎮 5h00 left · 0h00/5h00 +💪1h30 +📖1h' "$(line 1)" 'configured sizes, zero-size bonus omitted'
|
||||
rm -f "$CONFIG_FILE"
|
||||
|
||||
printf 'Checking a dead server shows the cached value as stale...\n'
|
||||
write_budget 21600 28800 7200 false 7200 3600 0
|
||||
run_block >/dev/null
|
||||
rm -f "$FIXTURE"
|
||||
assert_eq '🎮 2h00 left · 6h00/8h00 +📖1h (stale)' "$(line 1)" 'stale text'
|
||||
assert_eq '#FFB86C' "$(line 3)" 'stale is orange'
|
||||
rm -f "$CACHE_FILE"
|
||||
assert_eq '🎮 budget server down' "$(line 1)" 'no cache: server down'
|
||||
assert_eq '#FF5555' "$(line 3)" 'server down is red'
|
||||
|
||||
printf 'Checking error states are red and never overwrite the cache...\n'
|
||||
printf 'kept' >"$CACHE_FILE"
|
||||
printf '{"ok":false,"state_status":"corrupt"}\n' >"$FIXTURE"
|
||||
assert_eq '🎮 state corrupt' "$(line 1)" 'bad state_status'
|
||||
assert_eq '#FF5555' "$(line 3)" 'bad state is red'
|
||||
printf '{"ok":false,"state_status":"ok"}\n' >"$FIXTURE"
|
||||
assert_eq '🎮 budget error' "$(line 1)" 'ok:false'
|
||||
printf 'not json' >"$FIXTURE"
|
||||
assert_eq '🎮 unreadable budget' "$(line 1)" 'garbage body'
|
||||
assert_eq 'kept' "$(<"$CACHE_FILE")" 'error states must leave the cache alone'
|
||||
|
||||
printf 'All gaming_budget checks passed.\n'
|
||||
Loading…
Reference in New Issue
Block a user