530 lines
21 KiB
QML
530 lines
21 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.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.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 {}
|
|
}
|
|
|
|
Variants {
|
|
model: Quickshell.screens
|
|
SignalGlass {}
|
|
}
|
|
|
|
Variants {
|
|
model: Quickshell.screens
|
|
Osd {}
|
|
}
|
|
|
|
// ── 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.
|
|
|
|
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;
|
|
}
|
|
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,
|
|
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
|
|
});
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
}
|