//@ 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.cheatsheet import qs.modules.welcome 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 {} } // The visual bell, on every screen at once -- someone watching the other // monitor is exactly who this is for. Each one stays unmapped until a // bell-eligible notification arrives, and does nothing at all while the // Visual alerts setting is off. Variants { model: Quickshell.screens VisualBell {} } 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 {} Cheatsheet {} Welcome {} DockPickOverlay { id: dockPicker } CaptureOverlay {} IntelligenceResult {} ActivityPanel {} PowerMenu { id: 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 `. // // 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(); } } // Every shortcut on one keypress. Toggle so the same chord closes it. IpcHandler { target: "cheatsheet" function toggle(): void { ShellState.toggle("cheatsheet"); } function open(): void { ShellState.open("cheatsheet"); } function close(): void { ShellState.close(); } function status(): string { return JSON.stringify({ open: ShellState.cheatsheetOpen, groups: Keybinds.grouped().map(group => group.name), binds: Keybinds.binds.length, uncategorised: Keybinds.binds .filter(bind => !Keybinds.categoryManifest[bind.luaChord]) .map(bind => bind.chord) }); } } // Shown once on a fresh machine, and reachable afterwards -- the moment // somebody wants it again is exactly when a one-shot has thrown it away. IpcHandler { target: "welcome" function open(): void { ShellState.open("welcome"); } function close(): void { ShellState.close(); } function status(): string { return JSON.stringify({ open: ShellState.welcomeOpen, seen: DesktopPreferences.get("welcomeSeen") === true }); } } 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 }); } } // The magnifier keys: SUPER+ALT+= steps in, SUPER+ALT+- steps out, // SUPER+ALT+0 goes back to 1.00 ×. // // They come through the shell rather than setting cursor:zoom_factor on the // compositor directly, so the stored preference, the Magnifier slider and // what is actually on screen are always the same number -- and so there is // an OSD saying what the magnification now is, which matters when the thing // you are looking at is somewhere else entirely. // // A dead shell means dead zoom keys. That is honest: with the shell down // there is no bar, no dock and no OSD either. IpcHandler { target: "accessibility" function zoom(direction: string): string { return Accessibility.stepZoom(direction); } // Re-probe the screen reader and the accessibility bus. The settings // page calls Accessibility.refreshScreenReader() directly when it // opens; this is the same probe from outside, so `status` below can be // asked for something other than "not looked yet". function refresh(): void { Accessibility.refreshScreenReader(); } function status(): string { return JSON.stringify({ magnifierFactor: Accessibility.magnifierFactor, visualAlerts: Settings.visualAlerts, orcaRunning: Accessibility.orcaRunning, accessibilityBusRunning: Accessibility.accessibilityBusRunning }); } } 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 } // Same reason: the lid policy has to be watching display topology from the // start, not from the first time somebody opens the Power page. On a // machine with no lid it costs one process that exits immediately. Connections { target: LidPolicy } 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(); } // panama-sudo's side channel: the reason a privileged command is about // to run, shown labeled on the prompt beside polkitd's own message. function reason(text: string): void { Polkit.stateReason(text); } function status(): string { return JSON.stringify({ active: Polkit.active, action: Polkit.actionId, statedReason: Polkit.statedReason }); } } // Video wallpapers: what the bar pill and Settings do, reachable from a // terminal and from test harnesses. start() routes through Wallpaper so a // video rides wallpaperPath exactly like a click in the picker. IpcHandler { target: "video-wallpaper" function start(path: string): void { Wallpaper.setSingle(path); } function stop(): void { VideoWallpaper.stop(); } function pause(): void { VideoWallpaper.manuallyPaused = true; } function resume(): void { VideoWallpaper.manuallyPaused = false; } function rescan(): void { VideoWallpaper.rescan(); } function status(): string { return JSON.stringify({ available: VideoWallpaper.available, active: VideoWallpaper.active, path: VideoWallpaper.path, paused: VideoWallpaper.paused, manuallyPaused: VideoWallpaper.manuallyPaused, gamePaused: VideoWallpaper.gamePaused, batteryPaused: VideoWallpaper.batteryPaused, candidates: VideoWallpaper.candidates.length, lastError: VideoWallpaper.lastError }); } } 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, myHome: settingsWindow.myHomeDiagnostics }); } } 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 }); } } // Adding to the dock from outside the shell -- the launcher's "Add App to // Dock" command opens the picker, and `pin` is the scripted path for // anything that already knows the desktop id. IpcHandler { target: "dock" function pickApp(): void { ShellState.open("dock-picker"); } // false for an id nothing installs: a pin that does not resolve is // simply absent from the dock, so a silent success would be a lie. function pin(id: string): bool { return dockPicker.pin(id); } } IpcHandler { target: "clipboard" function toggle(): void { ShellState.toggle("clipboard"); } function open(): void { ShellState.open("clipboard"); } } IpcHandler { target: "powermenu" function toggle(): void { ShellState.toggle("powermenu"); } // Open with one entry pre-armed, by id: lock, logout, suspend, // hibernate, restart, poweroff. The power-button bind uses this for // its "Powers off" setting -- the first press opens the menu with // Power Off armed and the second press is the menu's own confirm, so // nothing here is a shortcut past it. An id this machine has no entry // for just opens the menu. function open(entry: string): void { powerMenu.preselect(entry); } } 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(); root.offerReloadDiagnosis(error); } } // ── The reload-failure rung of the escalation ladder ───────────────────── // // This is the failure most likely to happen while somebody is customizing // the desktop, and the one they are least equipped to read: a QML parse // error in the journal, and a shell that silently keeps running the old // configuration. That "keeps running" is what makes the rung possible -- // the shell that failed to load the change is not the shell answering // here, so it can still say so and still offer the log to an agent. // // Sent through notify-send rather than constructed in-process on purpose: // it takes the same delivery path, the same per-application rules and the // same `panama-exec` click as every other rung, so there is one mechanism // to keep working rather than two. // // Everything below is guarded. A handler that throws on a failed reload // turns one bad save into a broken desktop, so a fault in the offer must // cost nothing beyond the offer. function offerReloadDiagnosis(error: string): void { try { if (DesktopPreferences.get("reloadFailureOffer") !== true) return; // No agent chosen is the shipped default, and it means exactly // what it says: the desktop stays quiet rather than volunteering a // tool nobody asked for. The old actionless behaviour, verbatim. const agent = String(DesktopPreferences.get("preferredAgent") ?? "none"); if (agent === "" || agent === "none") return; const spec = PreferenceSchema.spec("preferredAgent"); const option = (spec?.options ?? []).find(entry => entry.value === agent); const label = option ? option.label : agent; // The failing message goes to the launcher as ONE single-quoted // argument, so nothing a QML parse error can contain -- and they // contain plenty of punctuation -- ends the quoting and starts a // command of its own. `panama-agent-reload` reads the rest of the // context out of the journal itself. const summary = String(error ?? "").replace(/\s+/g, " ").trim().slice(0, 500); // The launcher is reached by path rather than by name: the shell // is started by systemd, whose environment does not carry the // repo's bin directory on PATH. Same expansion the // panama-crash-watch unit uses. const command = '"${PANAMA_PATH:-$HOME/.local/share/Panama}/bin/panama-agent-reload" ' + root.shellQuote(summary); // Ordinary urgency, the same as the crash rung's. A critical // notification never expires and can break through Do Not Disturb, // which is a louder desktop than anybody asked for in exchange for // an offer that keeps in history until it is read anyway. Quickshell.execDetached([ "notify-send", "--app-name=Panama", "--icon=dialog-error-symbolic", "--hint=string:panama-exec:" + command, "The shell could not reload your change", "The previous configuration is still running. Click to hand the failure to " + label + "." ]); } catch (problem) { console.warn("Panama shell reload failure offer skipped:", problem); } } // POSIX single-quoting: everything between the quotes is literal, and the // only character that needs care is the quote itself. function shellQuote(text: string): string { return "'" + String(text).replace(/'/g, "'\\''") + "'"; } }