A mode is on because something is true right now: a game is running, a window is fullscreen on a given display, a workspace is focused, the clock is inside a window. That is asked again rather than fired once, and it is the whole reason schedules could be included here without the usual failure modes. A machine asleep at 23:30, rebooted at 02:00, or opened at 08:00 into a window that has already passed all reach the right answer by being asked again; an alarm gets all three wrong. The midnight-crossing rule is the part worth being careful about: a window belongs to the day it STARTS on, so a Friday-only 23:30-07:00 covers Saturday morning and must not cover Saturday night. That arithmetic was tested as pure logic before anything was built on it, including every malformed input failing closed -- silencing someone because a time string was wrong is the worst way this could fail. This does not take over the manual timed session. FocusSession already owns that, with its capsule, shortcut, Quick Settings entry and contracts, so modes defer entirely while one runs. Two writers of Do Not Disturb would each restore whatever the other happened to leave behind. Gaming hands over rather than being duplicated. The hook was silencing notifications itself, which would have made exactly those two owners -- and Gaming.active only polls while its settings page is open, so a mode could not have seen a game reliably in any case. The hook reports the game over IPC now and the mode decides what that means, the Gaming page points at it, and gamingSilenceNotifications is retired from the schema, since a setting nothing reads is the dead row this work keeps removing. Sleep ships disabled. A desktop that starts silencing someone on first boot has overstepped, whatever the default hour. Three contracts moved with it. gaming-contract asserted the hook uses setDnd, which was right before and wrong now; the shell-side assertions that setDnd and dndState exist stay, because a toggle would flip an already-silent machine back on. The new contract is proven to fail by breaking the midnight rule and by letting modes run alongside a manual session. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
104 lines
5.8 KiB
Bash
Executable File
104 lines
5.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# What Panama does while a game runs must be undone afterwards -- to what was
|
|
# there before, not to a default.
|
|
#
|
|
# That distinction is the whole feature. If ending a game forced Do Not Disturb
|
|
# off, it would silently undo a Do Not Disturb someone set by hand; if it forced
|
|
# the power profile to "balanced", it would undo a deliberate choice. Both are
|
|
# worse than doing nothing at all, because both look like the desktop
|
|
# misbehaving rather than a setting being wrong.
|
|
#
|
|
# Read-only: it reads gaming state and never engages Game Mode.
|
|
|
|
set -uo pipefail
|
|
|
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
helper="$repo_dir/config/dot/quickshell/scripts/panama-gaming"
|
|
service="$repo_dir/config/dot/quickshell/services/Gaming.qml"
|
|
page="$repo_dir/config/dot/quickshell/modules/settings/GamingPage.qml"
|
|
shell_file="$repo_dir/config/dot/quickshell/shell.qml"
|
|
|
|
fail() {
|
|
printf 'gaming contract: %s\n' "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
for path in "$helper" "$service" "$page" "$shell_file"; do
|
|
[[ -r "$path" ]] || fail "missing $path"
|
|
done
|
|
[[ -x "$helper" ]] || fail 'panama-gaming is not executable'
|
|
|
|
# ── The hook restores, it does not impose ───────────────────────────────────
|
|
hook_body="$(sed -n '/^def hook/,/^def /p' "$helper")"
|
|
[[ -n "$hook_body" ]] || fail 'the hook is missing'
|
|
grep -q 'state_path' <<<"$hook_body" \
|
|
|| fail 'the hook records nothing about the state before a game, so it cannot restore it'
|
|
grep -qE 'before\.get\("profile"\)' <<<"$hook_body" \
|
|
|| fail 'the power profile is not restored to what it was'
|
|
# Do Not Disturb is no longer the hook's business at all. It belongs to the
|
|
# Gaming focus mode, which records what Do Not Disturb was before it took over
|
|
# and puts that back -- the same protection, in one place instead of two. The
|
|
# assertion is therefore stricter than it was: the hook must not touch it.
|
|
grep -qE 'setDnd|dndState' <<<"$hook_body" \
|
|
&& fail 'the hook writes Do Not Disturb again, so it and the focus mode both own it'
|
|
grep -q 'gameStarted' <<<"$hook_body" \
|
|
|| fail 'the hook does not report the game to the shell, so no mode can react to it'
|
|
grep -qE 'set.*"balanced"' <<<"$hook_body" \
|
|
&& fail 'the hook restores a hardcoded profile rather than the previous one'
|
|
|
|
# ── The shell can be told, not only toggled ─────────────────────────────────
|
|
# A toggle is the wrong primitive here: if notifications were already silenced,
|
|
# toggling at game start would unsilence them.
|
|
grep -q 'function setDnd(enabled: bool)' "$shell_file" \
|
|
|| fail 'there is no explicit way to set Do Not Disturb, only a toggle'
|
|
grep -q 'function dndState()' "$shell_file" \
|
|
|| fail 'there is no way to read Do Not Disturb, so the hook cannot know what to restore'
|
|
# setDnd and dndState remain the right primitives and are still asserted above --
|
|
# a toggle would flip an already-silent machine back on. What changed is who
|
|
# calls them: not this hook, which now reports the game and lets the Gaming
|
|
# focus mode decide. Asserted in the other direction a few lines up.
|
|
|
|
# ── The hook does not depend on the shell being up ──────────────────────────
|
|
# A game can start after a shell restart; a hook that asked the shell for its
|
|
# settings would silently do nothing.
|
|
grep -q 'settings.json' <<<"$hook_body" \
|
|
|| fail 'the hook reads its settings from somewhere other than the settings file'
|
|
|
|
# ── Honest reporting ────────────────────────────────────────────────────────
|
|
grep -q 'governorAlreadyThere' "$service" \
|
|
|| fail 'the service cannot tell when Game Mode would change nothing'
|
|
page_code="$(grep -vE '^\s*//' "$page")"
|
|
grep -q 'already runs that governor' <<<"$page_code" \
|
|
|| fail 'the page does not say when Game Mode has no effect on this machine'
|
|
# Proton is listed, never chosen: Steam owns that per game.
|
|
grep -qiE 'setProton|selectProton|chooseProton' <<<"$page_code" \
|
|
&& fail 'the page claims to choose the Proton build, which Steam owns per game'
|
|
|
|
# ── Polling stops when nobody is looking ────────────────────────────────────
|
|
grep -q 'running: root.watching' "$service" \
|
|
|| fail 'the poll timer runs regardless of whether the page is open'
|
|
grep -q 'Gaming.watching = false' "$page" \
|
|
|| fail 'the page never stops the poll timer, so it would poll forever after being closed'
|
|
|
|
command -v jq >/dev/null 2>&1 || { printf 'gaming contract: SKIP (no jq)\n'; exit 0; }
|
|
|
|
state="$("$helper" snapshot 2>/dev/null)" || fail 'snapshot failed'
|
|
jq -e '(.gpus | type == "array") and (.gameMode | type == "object") and (.library | type == "object")' \
|
|
<<<"$state" >/dev/null || fail 'the snapshot is incomplete'
|
|
jq -e '.gameMode | has("active") and has("daemonRunning") and has("hooksInstalled")' <<<"$state" >/dev/null \
|
|
|| fail 'Game Mode state is incomplete'
|
|
# An integrated GPU reporting no video memory must not be described as discrete.
|
|
jq -e '[.gpus[] | select(.discrete) | .vramTotalBytes > 0] | all' <<<"$state" >/dev/null \
|
|
|| fail 'a card with no video memory is reported as the discrete one'
|
|
|
|
[[ -n "$("$helper" set-overlay-preset nonsense 2>/dev/null | jq -r '.error // ""')" ]] \
|
|
|| fail 'an unknown overlay preset was accepted'
|
|
[[ -n "$("$helper" hook nonsense 2>/dev/null | jq -r '.error // ""')" ]] \
|
|
|| fail 'an unknown hook phase was accepted'
|
|
|
|
printf 'gaming contract: PASS (%s GPU(s), %s games, hooks %s)\n' \
|
|
"$(jq '.gpus | length' <<<"$state")" \
|
|
"$(jq -r '.library.games' <<<"$state")" \
|
|
"$(jq -r 'if .gameMode.hooksInstalled then "installed" else "not installed" end' <<<"$state")"
|