Bound the notification app list, and give Focus a real editor
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
@@ -31,13 +31,14 @@ service="$repo_dir/config/dot/quickshell/services/FocusModes.qml"
|
||||
schema="$repo_dir/config/dot/quickshell/config/PreferenceSchema.qml"
|
||||
hook="$repo_dir/config/dot/quickshell/scripts/panama-gaming"
|
||||
gaming_page="$repo_dir/config/dot/quickshell/modules/settings/GamingPage.qml"
|
||||
focus_page="$repo_dir/config/dot/quickshell/modules/settings/FocusPage.qml"
|
||||
|
||||
fail() {
|
||||
printf 'focus modes contract: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
for path in "$service" "$schema" "$hook" "$gaming_page"; do
|
||||
for path in "$service" "$schema" "$hook" "$gaming_page" "$focus_page"; do
|
||||
[[ -r "$path" ]] || fail "missing $path"
|
||||
done
|
||||
|
||||
@@ -158,7 +159,134 @@ grep -q 'function allows' "$service" \
|
||||
grep -q 'if (!mode || mode.silence !== true)' "$service" \
|
||||
|| fail 'exceptions are not scoped to an active silencing mode, so a manual Do Not Disturb could leak'
|
||||
|
||||
grep -q 'function toggleAllowed' "$page" \
|
||||
# The editor moved from the Notifications page to the Focus tab when the
|
||||
# category split in two, and the accordion row and its chips became their own
|
||||
# components. What must hold is unchanged: somewhere a person can reach, the
|
||||
# list the gate above consults can be edited. Asserted over the whole Focus
|
||||
# surface rather than the page file, so pulling a row into a component does
|
||||
# not read as the feature being deleted.
|
||||
focus_surface=("$focus_page" "$repo_dir"/config/dot/quickshell/modules/settings/Focus*.qml)
|
||||
|
||||
focus_surface_has() {
|
||||
grep -qF "$1" "${focus_surface[@]}"
|
||||
}
|
||||
|
||||
focus_surface_has 'FocusAllowChips' \
|
||||
|| fail 'nothing on the Focus page renders the exception chips, so the list is unreachable'
|
||||
focus_surface_has 'FocusModes.update(' \
|
||||
|| fail 'the Focus page never writes a mode back, so nothing it shows can be edited'
|
||||
focus_surface_has 'allow:' \
|
||||
|| fail 'the exception list cannot be edited, so the summary could claim something unreachable'
|
||||
# The chips are only honest if removing one writes the shorter list back.
|
||||
focus_surface_has 'toggled(' \
|
||||
|| fail 'the chips report nothing, so removing an application from a mode would change only the chip'
|
||||
|
||||
# ── 7. A mode you can make, name, order and retrigger ───────────────────────
|
||||
#
|
||||
# The five modes used to be whatever the schema shipped: the page could toggle
|
||||
# them and edit a schedule, and nothing else. Everything below is new surface
|
||||
# on a service whose entire job is to silence a machine, so each entry point is
|
||||
# pinned by name and the two that compute something are replayed.
|
||||
|
||||
for required in createMode removeMode renameMode moveMode setTriggerKind seedTrigger uniqueId hasMode; do
|
||||
grep -q "function $required" "$service" \
|
||||
|| fail "there is no $required(), so the editor would be calling something that does not exist"
|
||||
done
|
||||
|
||||
# Manual modes did not gain an activation path when the editor did. A manual
|
||||
# mode is never `activeMode`, because triggerActive() answers false for it and
|
||||
# `automatic` filters it out -- FocusSession owns the by-hand session, which is
|
||||
# rule 4 above. An editor that quietly made "manual" mean "on" would be a
|
||||
# second owner for Do Not Disturb wearing a new name.
|
||||
grep -q 'trigger.kind !== "manual"' "$service" \
|
||||
|| fail 'manual modes are no longer excluded from the automatic list, so switching one on would silence the machine behind FocusSession'
|
||||
grep -q '"manual" included: FocusSession owns those' "$service" \
|
||||
|| fail 'the manual fall-through lost the note explaining why it is not an activation path'
|
||||
|
||||
# Order is priority -- activeMode takes the first matching mode -- so reordering
|
||||
# is a behaviour change and the page has to say so rather than looking like a
|
||||
# cosmetic sort.
|
||||
grep -qi 'priority' "$focus_page" \
|
||||
|| fail 'the Focus page never says the order is the priority, so dragging a row reads as decoration'
|
||||
|
||||
PANAMA_FOCUS_SERVICE="$service" bun -e '
|
||||
const source = await Bun.file(process.env.PANAMA_FOCUS_SERVICE).text();
|
||||
|
||||
function fail(message) {
|
||||
console.error(`focus modes contract: ${message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Same extraction the schedule arithmetic uses above, so these replay the
|
||||
// shipped function rather than a copy that can drift.
|
||||
function functionBody(name) {
|
||||
const start = source.indexOf(`function ${name}(`);
|
||||
if (start === -1)
|
||||
fail(`missing ${name}()`);
|
||||
const open = source.indexOf("{", start);
|
||||
let depth = 0;
|
||||
for (let index = open; index < source.length; index++) {
|
||||
if (source[index] === "{") depth++;
|
||||
if (source[index] === "}" && --depth === 0)
|
||||
return source.slice(open + 1, index);
|
||||
}
|
||||
fail(`${name}() is unterminated`);
|
||||
}
|
||||
|
||||
const seedTrigger = Function("kind", "fields", functionBody("seedTrigger"));
|
||||
const uniqueIdBody = Function("root", "name", functionBody("uniqueId"));
|
||||
const stubRoot = modes => ({
|
||||
modes: modes,
|
||||
hasMode: id => modes.some(mode => mode.id === id)
|
||||
});
|
||||
|
||||
function same(actual, expected, why) {
|
||||
if (JSON.stringify(actual) !== JSON.stringify(expected))
|
||||
fail(`${why}: got ${JSON.stringify(actual)}, expected ${JSON.stringify(expected)}`);
|
||||
}
|
||||
|
||||
// A trigger is seeded so that a mode is never saved meaning nothing. A
|
||||
// schedule with no days, or a workspace trigger with no workspace, is a mode
|
||||
// that silently never turns on -- the failure that is hardest to notice,
|
||||
// because nothing happens.
|
||||
same(seedTrigger("schedule", undefined),
|
||||
{ kind: "schedule", start: "22:00", end: "07:00", days: [0, 1, 2, 3, 4, 5, 6] },
|
||||
"a fresh schedule trigger");
|
||||
same(seedTrigger("schedule", { start: "23:15", days: [5] }),
|
||||
{ kind: "schedule", start: "23:15", end: "07:00", days: [5] },
|
||||
"a schedule keeps what it was handed and defaults the rest");
|
||||
same(seedTrigger("workspace", undefined), { kind: "workspace", id: 1 },
|
||||
"a fresh workspace trigger");
|
||||
same(seedTrigger("workspace", { id: "3" }), { kind: "workspace", id: 3 },
|
||||
"a workspace id arriving as text");
|
||||
same(seedTrigger("fullscreen", undefined), { kind: "fullscreen" },
|
||||
"a fullscreen trigger carries no fields");
|
||||
same(seedTrigger("game", undefined), { kind: "game" }, "a game trigger carries no fields");
|
||||
same(seedTrigger("manual", undefined), { kind: "manual" }, "a manual trigger carries no fields");
|
||||
|
||||
// An unknown kind is refused rather than stored. triggerActive() reads an
|
||||
// unknown kind as off, so storing one would be a mode that can never turn on.
|
||||
for (const kind of ["", "sometimes", undefined, null]) {
|
||||
if (seedTrigger(kind, undefined) !== null)
|
||||
fail(`an unknown trigger kind ${JSON.stringify(kind)} was seeded instead of refused`);
|
||||
}
|
||||
|
||||
// Ids are derived from the name and made unique by suffix. A duplicate id
|
||||
// would make removeMode and update() act on whichever came first.
|
||||
const existing = [{ id: "deep-work" }, { id: "deep-work-2" }, { id: "sleep" }];
|
||||
for (const [name, expected, why] of [
|
||||
["Reading", "reading", "a plain name"],
|
||||
[" Sleep ", "sleep-2", "a name whose id is taken"],
|
||||
["Deep work", "deep-work-3", "a name whose first two ids are taken"],
|
||||
["!!!", "mode", "a name with nothing usable in it"],
|
||||
["", "mode", "an empty name"]
|
||||
]) {
|
||||
const actual = uniqueIdBody(stubRoot(existing), name);
|
||||
if (actual !== expected)
|
||||
fail(`${why}: uniqueId gave ${JSON.stringify(actual)}, expected ${JSON.stringify(expected)}`);
|
||||
}
|
||||
|
||||
console.log("focus modes API: ok");
|
||||
' >/dev/null || fail 'the mode editor API does not behave as the Focus page assumes'
|
||||
|
||||
printf 'focus modes contract: ok\n'
|
||||
|
||||
Reference in New Issue
Block a user