Bound the notification app list, and give Focus a real editor
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
@@ -30,6 +30,16 @@ Singleton {
|
||||
}
|
||||
|
||||
// Modes something could switch on without being asked.
|
||||
//
|
||||
// A mode whose only trigger is "manual" is deliberately not here, and that
|
||||
// is its whole activation story: triggerActive() answers false for
|
||||
// "manual", so such a mode is never `activeMode` and never silences
|
||||
// anything by itself. Switching it on means exactly one thing -- its
|
||||
// `enabled` flag is true -- and the timed session that people actually
|
||||
// start by hand is FocusSession, which owns Do Not Disturb itself and does
|
||||
// not read this list. That split is the single-owner rule from the
|
||||
// contract, so a manual mode is a saved intent rather than a second
|
||||
// silencer, and the editor says so rather than pretending otherwise.
|
||||
readonly property var automatic: root.modes.filter(mode =>
|
||||
mode.enabled === true && (mode.triggers ?? []).some(trigger => trigger.kind !== "manual"))
|
||||
|
||||
@@ -213,6 +223,130 @@ Singleton {
|
||||
mode.id === id ? Object.assign({}, mode, changes) : mode));
|
||||
}
|
||||
|
||||
// ── Editing the list ────────────────────────────────────────────────────
|
||||
//
|
||||
// The list is the priority order (first match wins), so creating appends
|
||||
// and moving is a real edit rather than a view preference. Every function
|
||||
// here answers false when it was asked about a mode that is not there,
|
||||
// instead of silently writing the list back unchanged.
|
||||
|
||||
function hasMode(id: string): bool {
|
||||
return root.modes.some(mode => mode.id === id);
|
||||
}
|
||||
|
||||
// A readable id derived from the name, made unique by suffix. Ids are
|
||||
// referenced by nothing outside this list -- GamingPage looks up "gaming"
|
||||
// and that mode ships with the schema -- so this only has to stay stable
|
||||
// for itself.
|
||||
function uniqueId(name: string): string {
|
||||
const base = String(name ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "") || "mode";
|
||||
let candidate = base;
|
||||
let suffix = 2;
|
||||
while (root.hasMode(candidate)) {
|
||||
candidate = base + "-" + suffix;
|
||||
suffix++;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
// Returns the new mode's id, so the page can open the row it just made.
|
||||
// A new mode is manual and silencing: it does nothing until somebody picks
|
||||
// what turns it on, which is the safe direction for a thing whose job is
|
||||
// to quiet the machine.
|
||||
function createMode(name: string): string {
|
||||
const label = String(name ?? "").trim() || "New mode";
|
||||
const id = root.uniqueId(label);
|
||||
root.save(root.modes.concat([{
|
||||
id: id,
|
||||
name: label,
|
||||
enabled: true,
|
||||
triggers: [{ kind: "manual" }],
|
||||
silence: true,
|
||||
keepAwake: false,
|
||||
allow: []
|
||||
}]));
|
||||
return id;
|
||||
}
|
||||
|
||||
// Deleting the mode in force needs no special handling: activeMode is a
|
||||
// binding over this list, so it recomputes to null and the release path
|
||||
// below puts Do Not Disturb and Caffeine back.
|
||||
function removeMode(id: string): bool {
|
||||
if (!root.hasMode(id))
|
||||
return false;
|
||||
root.save(root.modes.filter(mode => mode.id !== id));
|
||||
return true;
|
||||
}
|
||||
|
||||
function renameMode(id: string, name: string): bool {
|
||||
const label = String(name ?? "").trim();
|
||||
if (!label || !root.hasMode(id))
|
||||
return false;
|
||||
root.update(id, { name: label });
|
||||
return true;
|
||||
}
|
||||
|
||||
// Order is priority, so this is what "make Gaming beat Sleep" is. A move
|
||||
// off either end is a no-op rather than a wrap, because the buttons that
|
||||
// call this are disabled at the ends and a keyboard repeat should stop
|
||||
// there rather than teleport the row.
|
||||
function moveMode(id: string, delta: int): bool {
|
||||
const next = root.modes.slice();
|
||||
const from = next.findIndex(mode => mode.id === id);
|
||||
const to = from + Math.round(delta);
|
||||
if (from < 0 || to < 0 || to >= next.length || to === from)
|
||||
return false;
|
||||
next.splice(to, 0, next.splice(from, 1)[0]);
|
||||
root.save(next);
|
||||
return true;
|
||||
}
|
||||
|
||||
// One trigger of the given kind, seeded so a mode is never saved in a
|
||||
// state that means nothing. Unknown kinds are refused rather than stored:
|
||||
// triggerActive() would read them as off, which is a mode that silently
|
||||
// never turns on.
|
||||
function seedTrigger(kind: string, fields: var): var {
|
||||
const extra = fields && typeof fields === "object" ? fields : {};
|
||||
switch (String(kind ?? "")) {
|
||||
case "schedule":
|
||||
return {
|
||||
kind: "schedule",
|
||||
start: String(extra.start ?? "22:00"),
|
||||
end: String(extra.end ?? "07:00"),
|
||||
days: Array.isArray(extra.days) ? extra.days.map(Number) : [0, 1, 2, 3, 4, 5, 6]
|
||||
};
|
||||
case "workspace":
|
||||
return { kind: "workspace", id: Number(extra.id ?? 1) };
|
||||
case "fullscreen":
|
||||
// `monitor` is optional and empty means any display. It is only
|
||||
// carried when handed in, so the editor's plain "a window is
|
||||
// fullscreen" stays plain.
|
||||
return extra.monitor === undefined
|
||||
? { kind: "fullscreen" }
|
||||
: { kind: "fullscreen", monitor: String(extra.monitor) };
|
||||
case "game":
|
||||
return { kind: "game" };
|
||||
case "manual":
|
||||
return { kind: "manual" };
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Replaces the mode's triggers with exactly one. Every shipped mode has a
|
||||
// single trigger and the editor only edits `triggers[0]`, so a list
|
||||
// hand-edited to hold several collapses to one the first time its kind is
|
||||
// changed here. The alternative -- editing one of several through a UI
|
||||
// that shows one -- is the more surprising of the two.
|
||||
function setTriggerKind(id: string, kind: string, fields: var): bool {
|
||||
const trigger = root.seedTrigger(kind, fields);
|
||||
if (!trigger || !root.hasMode(id))
|
||||
return false;
|
||||
root.update(id, { triggers: [trigger] });
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── applying ────────────────────────────────────────────────────────────
|
||||
//
|
||||
// What was true before a mode took over, so it can be put back. Recorded at
|
||||
|
||||
Reference in New Issue
Block a user