Build the Panama Hyprland desktop

This commit is contained in:
Gabriel Brown
2026-08-17 10:32:55 -04:00
parent 67033f2a31
commit 5248883e4b
190 changed files with 18554 additions and 34 deletions
@@ -0,0 +1,151 @@
pragma Singleton
// Curated shell feedback. This is deliberately not a notification store: one
// event is visible, at most four wait behind it, and expired entries vanish.
import Quickshell
import Quickshell.Hyprland
import QtQuick
Singleton {
id: root
readonly property int ambientPriority: 10
readonly property int importantPriority: 50
readonly property int criticalPriority: 90
property var activeEvent: null
property var pendingEvents: []
property int sequence: 0
readonly property bool active: root.activeEvent !== null
readonly property int queueLength: root.pendingEvents.length
signal eventPublished(var event)
signal eventDismissed(string key)
Timer {
id: expiryTimer
repeat: false
onTriggered: root.dismiss()
}
function publish(candidate: var): bool {
const event = root.normalize(candidate);
if (!event)
return false;
// DND is allowed to quiet device ambience, but it must never hide a
// privacy transition or the result of something the user initiated.
if (Notifs.doNotDisturb && event.priority < root.importantPriority)
return false;
if (root.activeEvent && root.activeEvent.key === event.key) {
root.activeEvent = event;
root.armExpiry();
root.eventPublished(event);
return true;
}
const withoutEquivalent = root.pendingEvents.filter(item => item.key !== event.key);
if (!root.activeEvent || event.priority > root.activeEvent.priority) {
if (root.activeEvent)
withoutEquivalent.push(root.activeEvent);
root.pendingEvents = root.trimQueue(withoutEquivalent);
root.activeEvent = event;
root.armExpiry();
} else {
withoutEquivalent.push(event);
root.pendingEvents = root.trimQueue(withoutEquivalent);
}
root.eventPublished(event);
return true;
}
function dismiss(): void {
if (!root.activeEvent)
return;
const dismissedKey = root.activeEvent.key;
expiryTimer.stop();
root.activeEvent = null;
root.eventDismissed(dismissedKey);
root.advance();
}
function invoke(): void {
if (!root.activeEvent)
return;
const action = root.activeEvent.actionId;
const data = root.activeEvent.actionData;
root.dismiss();
if (action === "focus-workspace") {
FocusSession.activateWorkspace();
} else if (action === "open-workspace") {
const workspaceId = Number(data);
if (Number.isInteger(workspaceId) && workspaceId > 0)
Hyprland.dispatch(`hl.dsp.focus({ workspace = ${workspaceId} })`);
} else if (action === "open-activity") {
ShellState.open("activity");
} else if (action === "open-path" && data) {
Quickshell.execDetached(["xdg-open", data]);
}
}
function reset(): void {
expiryTimer.stop();
root.activeEvent = null;
root.pendingEvents = [];
}
function normalize(candidate: var): var {
if (!candidate || !candidate.key || !candidate.title)
return null;
root.sequence++;
const priority = Number(candidate.priority ?? root.ambientPriority);
const duration = Number(candidate.durationMs ?? 3200);
return {
key: String(candidate.key),
icon: String(candidate.icon ?? "dialog-information-symbolic"),
glyph: String(candidate.glyph ?? ""),
title: String(candidate.title),
detail: String(candidate.detail ?? ""),
tone: String(candidate.tone ?? "accent"),
priority: Number.isFinite(priority) ? priority : root.ambientPriority,
durationMs: Math.max(1200, Number.isFinite(duration) ? duration : 3200),
actionId: String(candidate.actionId ?? ""),
actionData: String(candidate.actionData ?? ""),
monitorName: String(candidate.monitorName ?? ""),
sequence: root.sequence
};
}
function trimQueue(events: var): var {
const sorted = events.slice().sort((a, b) => {
if (a.priority !== b.priority)
return b.priority - a.priority;
return a.sequence - b.sequence;
});
return sorted.slice(0, 4);
}
function advance(): void {
if (root.pendingEvents.length === 0)
return;
const next = root.pendingEvents[0];
root.pendingEvents = root.pendingEvents.slice(1);
root.activeEvent = next;
root.armExpiry();
}
function armExpiry(): void {
expiryTimer.stop();
if (!root.activeEvent)
return;
expiryTimer.interval = root.activeEvent.durationMs;
expiryTimer.start();
}
}