The audit's first tier, in one change: every case found where the interface
asserted something the system did not do.
Twenty-one compositor-owned preferences -- the whole Mouse & Touchpad page,
plus window layout, snapping, dim-inactive and the magnifier -- had the live
half (hyprctl eval) and not the config-time half, so they quietly reverted on
every hyprctl reload. All 70 hypr-backed keys now have a prefs.get() in the
Lua, and hypr-prefs-contract pins both the presence and that the Lua fallback
equals the schema default, which is how the touchpad page misreported natural
scrolling on first boot.
The idle generator fell back from unwritten battery keys to the AC values
while the Power page displayed the schema defaults: a fresh laptop showed
"suspend at 20 minutes" and generated no suspend listener, then discharged to
zero in a bag. Unwritten keys now use the defaults the page shows
(idle-defaults-contract pins generator to schema; idle-config-contract
re-pinned to the new rule with the tradeoff recorded), and change-settings
enables managed idle on any machine with a battery -- without starting
hypridle in whatever session the installer runs under.
The per-app lock-screen notification switches wrote fields nothing read:
hyprlock cannot render notifications. Removed, with the rule model shrunk to
{enabled}, stale stored fields dropped at normalization, and the contract now
forbidding the page from growing lock-screen switches it cannot honor.
The battery warning thresholds were searchable, documented as "Found on
Power & Lock", and rendered nowhere -- and crossing the low threshold changed
only a glyph's color. Both sliders now exist where search was already sending
people, and low battery publishes a real notification at important priority.
Three handoffs opened GNOME panels that are inert in a Hyprland session. The
keyboard handoff is gone (that panel writes gsettings nothing here reads, and
the working controls sat on the same page); Connectivity gains a Wi-Fi row
that opens GNOME's actual Wi-Fi panel -- hidden SSIDs and 802.1X finally have
a road -- beside the network row that legitimately drives NetworkManager; the
universal-access handoff is gone, its few working toggles being controls this
app already owns. And the accessibility page now gives the true reason sticky
keys are missing: each Wayland compositor implements its own and Hyprland
does not yet -- not "an X11 feature with no Wayland equivalent," which sent
people to the wrong conclusion about the platform.
Claude-Session: https://claude.ai/code/session_01Epx9ZC1gwm81K3jm9x9CKh
197 lines
7.2 KiB
QML
197 lines
7.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
|
|
});
|
|
}
|
|
}
|
|
|
|
// The "Warn at" threshold from the Power page. A quiet mention, not an
|
|
// interruption: Do Not Disturb may still silence it, and the bar glyph
|
|
// turns with it. For a long time this threshold changed only the glyph's
|
|
// color, which nobody watching their work ever saw -- the first actual
|
|
// message arrived at the critical level.
|
|
Connections {
|
|
target: Battery
|
|
function onLowChanged(): void {
|
|
if (!root.powerInitialized || !Battery.low || Battery.critical)
|
|
return;
|
|
StatusEvents.publish({
|
|
key: "battery-low",
|
|
glyph: "\u{F007A}",
|
|
title: "Battery low",
|
|
detail: Math.round(Battery.percent) + "% remaining",
|
|
tone: "warning",
|
|
priority: StatusEvents.importantPriority,
|
|
actionId: "open-settings",
|
|
actionData: "power"
|
|
});
|
|
}
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
}
|