Files
Panama/config/dot/quickshell/services/DeviceEvents.qml
T
Gabriel Brown 05fd5346db Turn a website into an application, and notice the charger
Two of Section E's small wins.

panama-webapp gives a site its own icon, its own window with no browser
chrome, and its own launcher entry, which is most of what "installed"
means in practice and what both macOS and Windows now ship. It scrapes
the site's apple-touch-icon, falls back twice, and never fails an
install over a favicon: a web app with a generic icon still works.
Names are slugged, so "../../../../tmp/pwn" lands inside the
applications directory as tmp-pwn rather than anywhere else, and
remove refuses anything without the marker it writes -- sharing a name
with a real application must not delete that application. A browser
that cannot do app mode is told so rather than handed something that
opens an ordinary window and pretends.

The charger now announces itself through StatusEvents, which was
already the right layer and only wanted a producer. Ambient priority,
so Do Not Disturb quiets it, because a charger is exactly what DND is
for. A critically low battery is published at a priority DND does not
silence, because the one message you must not miss is the one saying
the machine is about to stop. Both join the existing silent-startup
window rather than announcing the state they found.

The keyboard-layout toast the plan also listed is deliberately not
built. Hyprland exposes the active keymap but not a change event
Quickshell already consumes, so it would need either polling or new
event plumbing, and this machine has one layout and could not test it.
2026-08-22 06:33:19 -04:00

174 lines
6.2 KiB
QML

pragma Singleton
// Transition-only device monitoring. Resting state is intentionally silent.
import Quickshell
import Quickshell.Bluetooth
import Quickshell.Io
import Quickshell.Services.Pipewire
import QtQuick
Singleton {
id: root
property bool outputInitialized: false
property bool bluetoothInitialized: false
property bool kdeInitialized: false
property bool powerInitialized: false
property bool previousAcOnline: true
property string previousOutput: ""
property string previousBluetooth: ""
property bool previousKdeReachable: false
property string previousKdeName: ""
PwObjectTracker { objects: [Pipewire.defaultAudioSink] }
readonly property string outputName: {
const sink = Pipewire.defaultAudioSink;
return sink?.description || sink?.nickname || sink?.name || "";
}
readonly property string bluetoothNames: {
const devices = Bluetooth.devices?.values ?? [];
return devices.filter(device => device.connected)
.map(device => device.name || device.address)
.sort()
.join("\u001f");
}
readonly property string kdeName: KdeConnect.preferredPhone?.name ?? ""
onOutputNameChanged: {
if (!root.outputInitialized) {
root.previousOutput = root.outputName;
return;
}
if (root.outputInitialized && root.outputName && root.outputName !== root.previousOutput) {
StatusEvents.publish({
key: "device-output",
glyph: "\u{F07E7}",
title: root.outputName,
detail: "Audio output selected",
priority: StatusEvents.ambientPriority
});
}
root.previousOutput = root.outputName;
}
onBluetoothNamesChanged: {
if (!root.bluetoothInitialized) {
root.previousBluetooth = root.bluetoothNames;
return;
}
if (root.bluetoothInitialized && root.bluetoothNames !== root.previousBluetooth) {
const current = root.bluetoothNames ? root.bluetoothNames.split("\u001f") : [];
const previous = root.previousBluetooth ? root.previousBluetooth.split("\u001f") : [];
const connected = current.find(name => !previous.includes(name));
const disconnected = previous.find(name => !current.includes(name));
StatusEvents.publish({
key: "device-bluetooth",
glyph: "\u{F00B1}",
title: connected || disconnected || "Bluetooth device",
detail: connected ? "Connected" : "Disconnected",
tone: connected ? "accent" : "warn",
priority: StatusEvents.ambientPriority
});
}
root.previousBluetooth = root.bluetoothNames;
}
Timer {
id: discoverySettle
interval: 1800
onTriggered: {
root.previousOutput = root.outputName;
root.previousBluetooth = root.bluetoothNames;
root.previousKdeReachable = KdeConnect.phoneReachable;
root.previousKdeName = root.kdeName;
root.previousAcOnline = Battery.acOnline;
root.outputInitialized = true;
root.bluetoothInitialized = true;
root.kdeInitialized = true;
root.powerInitialized = true;
}
}
// The charger. Announced the way every other device transition here is:
// once, on the change, and never as a description of the resting state.
// Ambient priority, so Do Not Disturb quiets it -- a charger is exactly
// the kind of thing DND is for.
//
// Absent entirely on a desktop, because Battery.acChanged only fires where
// there is a battery to be on.
Connections {
target: Battery
function onAcChanged(online: bool): void {
if (!root.powerInitialized) {
root.previousAcOnline = online;
return;
}
if (online === root.previousAcOnline)
return;
root.previousAcOnline = online;
StatusEvents.publish({
key: "device-power",
glyph: online ? "\u{F06A5}" : "\u{F0084}",
title: online ? "Charging" : "On battery",
detail: Battery.available ? Math.round(Battery.percent) + "%" : "",
priority: StatusEvents.ambientPriority
});
}
}
// Running out is not ambient. Published at a priority Do Not Disturb does
// not silence, because the one notification you must not miss is the one
// saying the machine is about to stop.
Connections {
target: Battery
function onCriticalChanged(): void {
if (!root.powerInitialized || !Battery.critical)
return;
StatusEvents.publish({
key: "battery-critical",
glyph: "\u{F0083}",
title: "Battery critically low",
detail: Math.round(Battery.percent) + "% remaining",
tone: "danger",
priority: StatusEvents.criticalPriority,
actionId: "open-settings",
actionData: "power"
});
}
}
Connections {
target: KdeConnect
function onPhoneReachableChanged(): void {
if (!root.kdeInitialized)
return;
const reachable = KdeConnect.phoneReachable;
if (reachable === root.previousKdeReachable)
return;
StatusEvents.publish({
key: "device-kdeconnect",
glyph: "\u{F03F2}",
title: root.kdeName || root.previousKdeName || "Phone",
detail: reachable ? "KDE Connect available" : "KDE Connect disconnected",
tone: reachable ? "accent" : "warn",
priority: StatusEvents.ambientPriority
});
root.previousKdeReachable = reachable;
root.previousKdeName = root.kdeName || root.previousKdeName;
}
}
Component.onCompleted: {
root.previousOutput = root.outputName;
root.previousBluetooth = root.bluetoothNames;
root.previousKdeReachable = KdeConnect.phoneReachable;
root.previousKdeName = root.kdeName;
discoverySettle.start();
}
}