Files
Gabriel Brown 9530e32326 Give a laptop its first week: the keys, the lid, the battery's endgame
The audit's fourth tier -- what a Framework owner reaches for in the first
week and found missing.

The power button stops being an instant, unconfirmed poweroff: a shipped
logind drop-in tells the daemon to stand down and the compositor binds the
key to the power menu, the way GNOME turns it into a question. Holding it
still hard-cuts through firmware. change-settings restarts logind so the
change applies without waiting for a boot, and the Power page says what the
button does now.

The function row fills in: F10 (XF86RFKill) toggles airplane mode through a
new panama-osd verb that blocks or unblocks every radio and says which way it
went; F9 (XF86Display) opens the Displays page, the honest action until
mirroring exists. And the lid becomes a switch bind: closing a docked lid
turns the internal panel off so nothing renders inside a closed shell and no
workspace strands on an invisible output, and opening it restores the panel
with the mode and scale chosen in Settings. panama-lid owns both decisions;
undocked machines suspend via logind before any of it matters.

The battery gets an endgame. On battery the screen dims to 30% two-thirds of
the way to blanking -- GNOME's single largest idle battery saver -- and
restores exactly the level it saved. At the urgent threshold the machine
suspends after a fifteen-second grace, cancelled by plugging in, because a
suspend preserves the session for days and a hard cut at 0% preserves
nothing; "Only warn" remains a choice on the Power page. Hibernate joins the
power menu, but only where logind answers CanHibernate with yes -- an entry
that fails silently is worse than none.

And brightness stops being two code paths: the Displays page now embeds the
same control the quick-settings panel uses, so the built-in backlight and
DDC/CI monitors share one surface that withdraws itself where neither
exists. The lid contract narrows to what its principle protects -- a
HandleLidSwitch drop-in -- so deliberate policy for other keys can ship.

Claude-Session: https://claude.ai/code/session_01Epx9ZC1gwm81K3jm9x9CKh
2026-08-23 12:21:08 -04:00

225 lines
8.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
import qs.config
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.
//
// And then, unless told otherwise, the machine is put to sleep: a suspend
// at the urgent threshold preserves the session for days, a hard cut at
// 0% preserves nothing. The grace timer is what makes the warning above
// readable and the action cancelable -- plugging in during it stands
// everything down.
Timer {
id: criticalSuspend
interval: 15000
onTriggered: {
if (Battery.critical && !Battery.acOnline
&& DesktopPreferences.get("batteryCriticalAction") === "suspend")
Quickshell.execDetached(["systemctl", "suspend"]);
}
}
Connections {
target: Battery
function onAcOnlineChanged(): void {
if (Battery.acOnline)
criticalSuspend.stop();
}
}
Connections {
target: Battery
function onCriticalChanged(): void {
if (!root.powerInitialized || !Battery.critical)
return;
if (!Battery.acOnline
&& DesktopPreferences.get("batteryCriticalAction") === "suspend")
criticalSuspend.restart();
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();
}
}