Files
Panama/config/dot/quickshell/shell.qml
T
Gabriel Brown 6dc606b872 Give focus modes conditions rather than alarms, and let Gaming hand over
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
2026-08-20 02:06:50 -04:00

583 lines
23 KiB
QML

//@ pragma IconTheme Adwaita
//@ pragma DropExpensiveFonts
// ─────────────────────────────────────────────────────────────────────────────
// Panama :: Quickshell
//
// The desktop shell — top bar, dock, overview, quick settings, notifications,
// screenshot/record UI and power menu. Replaces GNOME Shell plus the Forge,
// Dash-to-Dock, Openbar, Vitals, AppIndicator, Caffeine and clipboard
// extensions it was carrying.
//
// This file is wiring only. It owns two things and nothing else:
// 1. Which windows exist, and on which screens.
// 2. The IPC surface that hypr/keybinds.lua calls into.
//
// All shared state lives in services/ShellState.qml, so no module needs a
// reference to any other module.
//
// Layer namespaces set by the modules below are matched by layer rules in
// hypr/rules.lua (blur, dim, no-screen-share). Keep the two in sync.
// ─────────────────────────────────────────────────────────────────────────────
import Quickshell
import Quickshell.Io
import QtQuick
import qs.config
import qs.services
import qs.modules.bar
import qs.modules.dock
import qs.modules.switcher
import qs.modules.overview
import qs.modules.quicksettings
import qs.modules.notifications
import qs.modules.capture
import qs.modules.powermenu
import qs.modules.clipboard
import qs.modules.datemenu
import qs.modules.focus
import qs.modules.signals
import qs.modules.polkit
import qs.modules.settings
import qs.modules.osd
// Clipboard and datemenu ship explicit qmldir manifests because they were
// added while this daily-driver shell was already running; that also keeps
// future hot reloads deterministic instead of relying on module synthesis.
ShellRoot {
id: root
// ── Per-screen surfaces ─────────────────────────────────────────────────
// Variants rebuilds these when monitors are added or removed, so plugging
// in a second display Just Works without a reload.
//
// Do NOT redeclare `required property var modelData` on the delegate here:
// Bar and Dock already declare a plain `modelData` and bind `screen` to it.
// Redeclaring it shadows theirs, `screen` never resolves, and the windows
// are constructed but silently never map — with no error logged.
Variants {
model: Quickshell.screens
Bar {
idleInhibited: Caffeine.enabled
onRequestQuickSettings: ShellState.toggle("quicksettings")
}
}
Variants {
model: Quickshell.screens
Dock {}
}
// The Alt-Tab overlay. Present only while a switch is in progress; the
// Loader inside it keeps the window unbuilt the rest of the time.
Variants {
model: Quickshell.screens
WindowSwitcher {}
}
Variants {
model: Quickshell.screens
SignalGlass {}
}
Variants {
model: Quickshell.screens
Osd {}
}
DisplayIdentify {}
// ── Single-instance overlays ────────────────────────────────────────────
// These are always constructed but only *visible* when ShellState says so.
// They're cheap while hidden, and keeping them alive means opening the
// overview or quick settings is instant rather than paying a load.
Overview {}
// QuickSettings is the window; QuickSettingsPanel is its content.
QuickSettings { id: quickSettings }
DateMenu { id: dateMenu }
ClipboardPanel {}
CaptureOverlay {}
IntelligenceResult {}
ActivityPanel {}
PowerMenu {}
SettingsWindow { id: settingsWindow }
// Toasts are their own always-on layer; they must be able to appear
// without any overlay being open.
Toasts {}
// ── IPC ─────────────────────────────────────────────────────────────────
// Called from hypr/keybinds.lua as `qs ipc call <target> <function>`.
//
// Every parameter AND the return type must be annotated, or Quickshell
// silently declines to register the function — it will not warn you.
// Driven entirely from keybinds: Super+Tab steps, and a bind on Super
// RELEASE commits. `commit` therefore runs on every Super release in the
// session, so it returns immediately when no switch is open.
IpcHandler {
target: "switcher"
function next(): void { WindowSwitcherState.step(true); }
function previous(): void { WindowSwitcherState.step(false); }
function commit(): void { WindowSwitcherState.commit(); }
function cancel(): void { WindowSwitcherState.cancel(); }
}
IpcHandler {
target: "overview"
function toggle(): void { ShellState.toggle("overview"); }
function open(): void { ShellState.openOverview(0); }
function search(query: string): void { ShellState.searchOverview(query); }
function close(): void { ShellState.close(); }
}
IpcHandler {
target: "focus"
function start(): bool {
FocusSession.startDefault();
return FocusSession.active;
}
// Told by the gamemode hooks, which are the only thing that reliably
// knows a game started. The hook used to set Do Not Disturb itself;
// now it reports the fact and the mode decides what that means.
function gameStarted(): void { FocusModes.gameRunning = true; }
function gameEnded(): void { FocusModes.gameRunning = false; }
function reveal(): void { FocusSession.reveal(); }
function dismiss(): void { FocusSession.dismiss(); }
function pause(): void { FocusSession.pauseOrResume(); }
function workspace(): void { FocusSession.activateWorkspace(); }
function overview(): void {
ShellState.openOverview(FocusSession.workspaceId);
FocusSession.dismiss();
}
function end(): bool {
if (!FocusSession.active)
return false;
FocusSession.end(false);
return !FocusSession.active;
}
function status(): string {
return JSON.stringify({
active: FocusSession.active,
paused: FocusSession.paused,
capsuleVisible: FocusSession.capsuleVisible,
workspaceId: FocusSession.workspaceId,
remaining: FocusSession.remainingText,
caffeine: Caffeine.enabled,
doNotDisturb: Notifs.doNotDisturb
});
}
}
IpcHandler {
target: "health"
function refresh(): bool { return Health.refresh(); }
function status(): string {
return JSON.stringify({
summary: Health.summary,
busy: Health.busy,
generation: Health.generation,
acceptedGeneration: Health.acceptedGeneration,
checks: Health.checks.map(check => ({ id: check.id, status: check.status }))
});
}
function open(): void {
ShellState.openSettings("services");
Health.refresh();
}
function repair(id: string): bool { return Health.repair(id, true); }
}
// Health checks the wallpaper service through the same typed IPC boundary
// as capture and clipboard. This is deliberately read-only: choosing an
// image remains an explicit Settings action.
IpcHandler {
target: "wallpaper"
function refresh(): void { Wallpaper.refreshActive(); }
function status(): string {
return JSON.stringify({
active: Wallpaper.active,
activeByOutput: Wallpaper.activeByOutput,
configured: Wallpaper.configured,
availableCount: Wallpaper.available.length,
lastError: Wallpaper.lastError
});
}
}
// A small diagnostics surface doubles as a deterministic contract harness.
// Real producers call StatusEvents.publish() directly; fixtures never run
// unless explicitly requested over IPC by the test suite.
IpcHandler {
target: "status-events"
function screenshot(path: string): void {
StatusEvents.publish({
key: "capture-screenshot",
glyph: "\u{F0100}",
title: "Screenshot captured",
detail: path.split("/").pop(),
tone: "ok",
priority: StatusEvents.importantPriority,
actionId: "open-path",
actionData: path
});
}
function fixture(name: string): void {
if (name === "ambient") {
StatusEvents.publish({
key: "device-output",
glyph: "\u{F07E7}",
title: "USB Audio",
detail: "Audio output connected",
priority: StatusEvents.ambientPriority
});
} else if (name === "ambient-replacement") {
StatusEvents.publish({
key: "device-output",
glyph: "\u{F07E7}",
title: "Studio Display",
detail: "Audio output connected",
priority: StatusEvents.ambientPriority
});
} else if (name === "critical") {
StatusEvents.publish({
key: "privacy-microphone",
glyph: "\u{F036C}",
title: "Microphone in use",
detail: "An application is capturing audio",
tone: "danger",
priority: StatusEvents.criticalPriority,
actionId: "open-activity"
});
}
}
function dismiss(): void { StatusEvents.dismiss(); }
function reset(): void { StatusEvents.reset(); }
function status(): string {
const event = StatusEvents.activeEvent;
return JSON.stringify({
active: StatusEvents.active,
activeKey: event?.key ?? "",
title: event?.title ?? "",
priority: event?.priority ?? 0,
queueLength: StatusEvents.queueLength
});
}
}
IpcHandler {
target: "osd"
function progress(kind: string, value: int, maximum: int, label: string): void {
OsdState.progress(kind, value, maximum, label);
}
function message(kind: string, label: string): void {
OsdState.message(kind, label);
}
function hide(): void { OsdState.hide(); }
function status(): string {
return JSON.stringify({
active: OsdState.active,
monitorName: OsdState.monitorName,
state: OsdState.state
});
}
}
IpcHandler {
target: "activity"
function fixture(name: string): void { PrivacyState.applyFixture(name); }
function clear(): void { PrivacyState.clearFixture(); }
function open(): void { ShellState.open("activity"); }
function close(): void { ShellState.close(); }
function toggle(): void { ShellState.toggle("activity"); }
function stopRecording(): void { Capture.stopRecording(); }
function status(): string {
return JSON.stringify({
microphoneActive: PrivacyState.microphoneActive,
cameraActive: PrivacyState.cameraActive,
screenSharingActive: PrivacyState.screenSharingActive,
recordingActive: PrivacyState.recordingActive,
activeKinds: PrivacyState.activeKinds
});
}
}
// Referencing the transition monitor here ensures the singleton is alive
// even before any quick-settings device list has been opened.
Connections { target: DeviceEvents }
IpcHandler {
target: "quicksettings"
function toggle(): void { ShellState.toggle("quicksettings"); }
function open(): void { ShellState.open("quicksettings"); }
function close(): void { ShellState.close(); }
function section(name: string): void { quickSettings.expand(name); }
function status(): string {
return JSON.stringify({
open: ShellState.quickSettingsOpen,
expandedSection: quickSettings.expandedSection
});
}
}
// Small stateful controls used by launcher commands. Returning the state
// after mutation lets callers give accurate OSD feedback without keeping a
// second copy of service state in a shell script.
IpcHandler {
target: "caffeine"
function toggle(): bool {
Caffeine.toggle();
return Caffeine.enabled;
}
function status(): bool { return Caffeine.enabled; }
}
IpcHandler {
target: "night-light"
function toggle(): bool {
NightLight.toggleQuietly();
return NightLight.active;
}
function status(): bool { return NightLight.active; }
function settings(): string {
return JSON.stringify({
enabled: NightLight.enabled,
automatic: NightLight.automatic,
active: NightLight.active
});
}
function restore(enabled: bool, automatic: bool): bool {
NightLight.restore(enabled, automatic);
return NightLight.active;
}
}
IpcHandler {
target: "kdeconnect"
function fixture(name: string): void { KdeConnect.applyFixture(name); }
function reset(): void { KdeConnect.clearFixture(); }
function refresh(): void { KdeConnect.refresh(); }
function cancel(): void { KdeConnect.cancelTransfer(); }
function status(): string {
return JSON.stringify({
fixture: KdeConnect.fixtureMode,
available: KdeConnect.available,
reachable: KdeConnect.phoneReachable,
pairedCount: KdeConnect.pairedCount,
actionCount: KdeConnect.phoneActions.length,
transferActive: KdeConnect.transferActive,
transferFileName: KdeConnect.transferFileName,
ongoingCount: Ongoing.activities.filter(item => item.kind === "phone-transfer").length,
lastError: KdeConnect.lastError
});
}
}
IpcHandler {
target: "home-assistant"
function fixture(name: string): void { HomeAssistant.applyFixture(name); }
function reset(): void { HomeAssistant.clearFixture(); }
function refresh(): void { HomeAssistant.refresh(); }
function brightness(id: string, percent: int): void { HomeAssistant.setBrightness(id, percent); }
function toggle(id: string): void { HomeAssistant.toggleEntity(id); }
function status(): string {
return JSON.stringify({
fixture: HomeAssistant.fixtureMode,
phase: HomeAssistant.phase,
discoveredCount: HomeAssistant.discoveredCount,
configuredCount: HomeAssistant.configuredCount,
visibleCount: HomeAssistant.visibleEntities.length,
selectedIds: HomeAssistant.selectedEntities.map(entity => entity.id),
entities: HomeAssistant.selectedEntities,
stale: HomeAssistant.stale,
busy: HomeAssistant.busyEntityIds.length > 0,
busyEntityIds: HomeAssistant.busyEntityIds,
pendingBrightness: HomeAssistant.pendingBrightness,
entityErrors: HomeAssistant.entityErrors,
actionProcessRunning: HomeAssistant.actionProcessRunning,
actionStreamFinished: HomeAssistant.actionStreamFinished,
fixtureTransitionDraining: HomeAssistant.fixtureTransitionDraining,
queuedActionCount: HomeAssistant.actionQueue.length,
actionActive: HomeAssistant.activeAction !== null,
lastError: HomeAssistant.lastError
});
}
}
// Panama's authentication prompt. The agent process calls in here with a
// path to a request file; no password ever travels the other way.
PolkitPrompt {}
IpcHandler {
target: "polkit"
function begin(path: string): void { Polkit.begin(path); }
function cancel(): void { Polkit.cancel(); }
function status(): string {
return JSON.stringify({ active: Polkit.active, action: Polkit.actionId });
}
}
IpcHandler {
target: "settings"
function open(): void { ShellState.openSettings(DesktopPreferences.get("lastPage") || "home"); }
function toggle(): void { ShellState.toggleSettings(); }
function close(): void { ShellState.closeSettings(); }
function page(name: string): void { ShellState.openSettings(name); }
function status(): string {
return JSON.stringify({
open: ShellState.settingsOpen,
page: ShellState.settingsPage,
discoveredCount: HomeAssistant.discoveredCount,
selectedCount: HomeAssistant.configuredCount,
homePhone: settingsWindow.homePhoneDiagnostics
});
}
}
IpcHandler {
target: "settings-system"
function refresh(): void { SystemSettings.refresh(); }
function status(): string {
return JSON.stringify({
monitor: SystemSettings.monitorName,
width: SystemSettings.monitorWidth,
height: SystemSettings.monitorHeight,
autoHdr: SystemSettings.autoHdr,
vrrPolicy: SystemSettings.vrrPolicy,
directScanoutPolicy: SystemSettings.directScanoutPolicy,
bluebubblesAvailable: SystemSettings.bluebubblesAvailable,
busy: SystemSettings.busy,
lastError: SystemSettings.lastError
});
}
}
IpcHandler {
target: "notifications"
function toggle(): void { ShellState.toggleDateMenu("notifications"); }
function open(): void { ShellState.openDateMenu("notifications"); }
function clear(): void { Notifs.dismissAll(); }
function dnd(): bool {
Notifs.doNotDisturb = !Notifs.doNotDisturb;
return Notifs.doNotDisturb;
}
// Explicit set and read, which a script needs. A toggle is the wrong
// primitive for "silence while a game runs": if notifications were
// already silenced, toggling at game start would UNsilence them.
function setDnd(enabled: bool): bool {
Notifs.doNotDisturb = enabled;
return Notifs.doNotDisturb;
}
function dndState(): bool { return Notifs.doNotDisturb; }
}
IpcHandler {
target: "calendar-agenda"
function fixture(name: string): void { CalendarAgenda.applyFixture(name); }
function reset(): void { CalendarAgenda.clearFixture(); }
function refresh(): void { CalendarAgenda.refresh(); }
function open(): void { ShellState.openDateMenu("agenda"); }
function page(name: string): void { ShellState.openDateMenu(name); }
function close(): void { ShellState.close(); }
function select(date: string): void { CalendarAgenda.selectIsoDate(date); }
function status(): string {
return JSON.stringify({
phase: CalendarAgenda.phase,
fixture: CalendarAgenda.fixtureMode,
open: ShellState.notificationsOpen,
page: ShellState.dateMenuPage,
sourceCount: CalendarAgenda.sources.length,
eventCount: CalendarAgenda.events.length,
selectedDate: CalendarAgenda.selectedDateKey,
selectedEventCount: CalendarAgenda.selectedEvents.length,
capsuleVisible: CalendarAgenda.capsuleVisible,
capsuleText: CalendarAgenda.capsuleText,
ongoingCount: Ongoing.count,
ongoingFits: dateMenu.ongoingContentFits
});
}
}
IpcHandler {
target: "clipboard"
function toggle(): void { ShellState.toggle("clipboard"); }
function open(): void { ShellState.open("clipboard"); }
}
IpcHandler {
target: "powermenu"
function toggle(): void { ShellState.toggle("powermenu"); }
}
IpcHandler {
target: "capture"
// Pressing Print while recording stops the recording instead of
// opening the picker again — otherwise there's no way to stop it
// without the indicator.
function open(): void {
if (Capture.recording)
Capture.stopRecording();
else
Capture.open();
}
function screenNow(): void { Capture.screenNow(); }
function windowNow(): void { Capture.windowNow(); }
function stop(): void { Capture.stopRecording(); }
function close(): void { Capture.close(); }
function status(): string {
return JSON.stringify({
open: ShellState.captureOpen,
mode: Capture.mode,
recordMode: Capture.recordMode,
intelligenceMode: Capture.intelligenceMode
});
}
}
IpcHandler {
target: "screen-intelligence"
function open(): void { Capture.openIntelligence(); }
function close(): void { ScreenIntelligence.close(); }
function analyzeFile(path: string): void { ScreenIntelligence.analyzeFile(path); }
function copy(): void { ScreenIntelligence.copyText(); }
function status(): string {
return JSON.stringify({
phase: ScreenIntelligence.phase,
ocrReady: ScreenIntelligence.ocrReady,
codeReady: ScreenIntelligence.codeReady,
englishReady: ScreenIntelligence.englishReady,
text: ScreenIntelligence.text,
codeType: ScreenIntelligence.codeType,
codeValue: ScreenIntelligence.codeValue,
error: ScreenIntelligence.error
});
}
}
// ── Diagnostics ─────────────────────────────────────────────────────────
// Quickshell pops a reload notice on every hot reload, which is noise when
// you're editing. Failures still surface in the log.
Connections {
target: Quickshell
function onReloadCompleted(): void { Quickshell.inhibitReloadPopup(); }
function onReloadFailed(error: string): void {
console.warn("Panama shell reload failed:", error);
Quickshell.inhibitReloadPopup();
}
}
}