787 lines
33 KiB
QML
787 lines
33 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.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 <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(); }
|
||
}
|
||
|
||
// 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, "'\\''") + "'";
|
||
}
|
||
}
|