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
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
// (see PowerButton); Lock and Suspend are one press because neither loses work.
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import QtQuick
|
||||
import qs.config
|
||||
@@ -31,6 +32,23 @@ PanelWindow {
|
||||
|
||||
property int currentIndex: 0
|
||||
|
||||
// Hibernate appears only where it can deliver: logind answers "yes" only
|
||||
// with a resume-capable swap configured, and a menu entry that fails
|
||||
// silently is worse than none. Checked once per shell run -- swap does
|
||||
// not come and go.
|
||||
property bool canHibernate: false
|
||||
|
||||
Process {
|
||||
id: hibernateProbe
|
||||
command: ["busctl", "call", "org.freedesktop.login1",
|
||||
"/org/freedesktop/login1", "org.freedesktop.login1.Manager",
|
||||
"CanHibernate"]
|
||||
running: true
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: win.canHibernate = this.text.includes('"yes"')
|
||||
}
|
||||
}
|
||||
|
||||
// A uwsm-managed session logs out by stopping the uwsm unit; a plain
|
||||
// "Hyprland" session must ask the compositor to exit instead. The branch
|
||||
// used to test merely that the uwsm BINARY existed -- but uwsm is always
|
||||
@@ -41,7 +59,11 @@ PanelWindow {
|
||||
// compositor unit, wayland-wm@*.service, so that is what decides.
|
||||
readonly property string logoutScript: 'if systemctl --user list-units --no-legend --state=active "wayland-wm@*.service" 2>/dev/null | grep -q .; then exec uwsm stop; else exec hyprctl dispatch "hl.dsp.exit()"; fi'
|
||||
|
||||
readonly property var entries: [
|
||||
// The rows actually shown: an entry may declare `available: false` to
|
||||
// withdraw itself (hibernate on a machine with no resume swap).
|
||||
readonly property var entries: allEntries.filter(entry => entry.available !== false)
|
||||
|
||||
readonly property var allEntries: [
|
||||
{
|
||||
glyph: "",
|
||||
label: "Lock",
|
||||
@@ -60,6 +82,13 @@ PanelWindow {
|
||||
destructive: false,
|
||||
cmd: ["systemctl", "suspend"]
|
||||
},
|
||||
{
|
||||
glyph: "",
|
||||
label: "Hibernate",
|
||||
destructive: false,
|
||||
available: win.canHibernate,
|
||||
cmd: ["systemctl", "hibernate"]
|
||||
},
|
||||
{
|
||||
glyph: "",
|
||||
label: "Restart",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import QtQuick
|
||||
import qs.config
|
||||
import qs.services
|
||||
import qs.modules.quicksettings
|
||||
import qs.widgets
|
||||
|
||||
SettingsPage {
|
||||
@@ -289,35 +290,21 @@ SettingsPage {
|
||||
// writing it into settings.json would mean restoring a value the panel had
|
||||
// already moved on from. So there is no schema key here and no SliderRow --
|
||||
// the rows read and write the display directly.
|
||||
// One brightness surface for both kinds this machine may have, shared
|
||||
// with the quick-settings panel so the two can never disagree. This card
|
||||
// was DDC/CI-only for a while, which on a laptop meant Settings showed
|
||||
// an error about external-monitor brightness while the panel's backlight
|
||||
// worked fine one panel over.
|
||||
SettingsCard {
|
||||
visible: Brightness.available || Brightness.lastError !== ""
|
||||
visible: pageBrightness.visible || Brightness.lastError !== ""
|
||||
title: "Brightness"
|
||||
subtitle: Brightness.available
|
||||
? "Sent to the monitor over DDC/CI, the same channel its buttons use."
|
||||
subtitle: pageBrightness.visible
|
||||
? "The built-in panel through its backlight; external monitors over DDC/CI, the same channel their buttons use."
|
||||
: Brightness.lastError
|
||||
|
||||
Repeater {
|
||||
model: Brightness.displays
|
||||
|
||||
SettingRow {
|
||||
id: brightnessRow
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
label: Displays.monitorNamed(modelData.connector)?.description || modelData.connector
|
||||
detail: modelData.connector ? modelData.connector + " · " + modelData.value + "%"
|
||||
: modelData.value + "%"
|
||||
divider: brightnessRow.index < Brightness.displays.length - 1
|
||||
controlWidth: 190
|
||||
|
||||
ValueSlider {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
width: parent.width
|
||||
value: brightnessRow.modelData.value / 100
|
||||
onMoved: v => Brightness.set(brightnessRow.modelData.bus, Math.round(v * 100))
|
||||
}
|
||||
}
|
||||
BrightnessControl {
|
||||
id: pageBrightness
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,8 +53,9 @@ SettingsPage {
|
||||
// page rendered them -- search delivered people to this card and the
|
||||
// controls were not here.
|
||||
SliderRow { setting: "batteryLowPercent" }
|
||||
SliderRow {
|
||||
setting: "batteryCriticalPercent"
|
||||
SliderRow { setting: "batteryCriticalPercent" }
|
||||
ChoiceRow {
|
||||
setting: "batteryCriticalAction"
|
||||
divider: Battery.chargeLimitSupported
|
||||
}
|
||||
|
||||
@@ -149,6 +150,15 @@ SettingsPage {
|
||||
}
|
||||
}
|
||||
|
||||
// Informational like the lid card above it: the behavior is a logind
|
||||
// drop-in plus a compositor bind (see keybinds.lua), not a preference,
|
||||
// and saying what a physical button does beats leaving it to be
|
||||
// discovered by pressing it.
|
||||
SettingsCard {
|
||||
title: "The power button"
|
||||
subtitle: "A press opens the power menu instead of powering off immediately. Holding it still forces power off through the firmware, as ever."
|
||||
}
|
||||
|
||||
// Only shown when the numbers are actually contradictory, rather than as a
|
||||
// permanent warning nobody reads.
|
||||
SettingsCard {
|
||||
|
||||
Reference in New Issue
Block a user