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
214 lines
8.6 KiB
QML
214 lines
8.6 KiB
QML
// Power & Lock.
|
|
//
|
|
// hypridle has no IPC for reconfiguration, so these values reach it by
|
|
// regenerating its config and restarting the daemon (services/IdleLock.qml).
|
|
// That only happens when Panama manages the daemon, and the card below says
|
|
// plainly which state you are in rather than presenting sliders that silently
|
|
// do nothing.
|
|
|
|
import QtQuick
|
|
import Quickshell
|
|
import qs.config
|
|
import qs.services
|
|
|
|
SettingsPage {
|
|
id: root
|
|
|
|
// Probing the power daemon is a D-Bus round trip, so it happens when the
|
|
// page opens rather than at shell startup.
|
|
Component.onCompleted: if (!PowerProfiles.scanned) PowerProfiles.refresh()
|
|
|
|
title: "Power & Lock"
|
|
lede: "When the screen turns off, when the session locks, and whether it ever sleeps."
|
|
|
|
// Absent on a desktop, in full. `available` is false until a battery has
|
|
// actually been read, so this is not an empty card claiming 0%.
|
|
SettingsCard {
|
|
visible: Battery.available
|
|
title: "Battery"
|
|
subtitle: Battery.acOnline
|
|
? "On wall power."
|
|
: "On battery. The timings below switch to their battery values automatically."
|
|
|
|
TextRow {
|
|
label: "Charge"
|
|
value: Math.round(Battery.percent) + "%"
|
|
}
|
|
|
|
TextRow {
|
|
label: "State"
|
|
value: {
|
|
if (Battery.charging)
|
|
return "Charging";
|
|
if (Battery.status === "Full")
|
|
return "Full";
|
|
if (Battery.acOnline)
|
|
return "Plugged in, not charging";
|
|
return "On battery";
|
|
}
|
|
}
|
|
|
|
// The two points at which the desktop starts telling you. These were
|
|
// in the schema and reachable from settings search long before any
|
|
// page rendered them -- search delivered people to this card and the
|
|
// controls were not here.
|
|
SliderRow { setting: "batteryLowPercent" }
|
|
SliderRow { setting: "batteryCriticalPercent" }
|
|
ChoiceRow {
|
|
setting: "batteryCriticalAction"
|
|
divider: Battery.chargeLimitSupported
|
|
}
|
|
|
|
// Only where the firmware actually has a ceiling. A machine whose
|
|
// kernel exposes nothing gets no control at all, rather than one that
|
|
// would accept a value and change nothing.
|
|
SliderRow {
|
|
visible: Battery.chargeLimitSupported
|
|
setting: "batteryChargeLimit"
|
|
divider: false
|
|
}
|
|
}
|
|
|
|
// The same profiles GNOME's Power panel offers. Not a stored preference --
|
|
// the daemon owns it, it survives Panama restarts, and anything else on the
|
|
// system can change it, so a copy here would drift.
|
|
SettingsCard {
|
|
visible: PowerProfiles.available || PowerProfiles.lastError !== ""
|
|
title: "Power profile"
|
|
subtitle: PowerProfiles.degraded !== ""
|
|
? "Performance is limited right now: " + PowerProfiles.degraded
|
|
: (PowerProfiles.available
|
|
? "Applies to the whole system and persists across sessions."
|
|
: PowerProfiles.lastError)
|
|
|
|
Repeater {
|
|
model: PowerProfiles.profiles
|
|
|
|
SettingRow {
|
|
id: profileRow
|
|
required property var modelData
|
|
required property int index
|
|
|
|
label: PowerProfiles.label(profileRow.modelData)
|
|
detail: PowerProfiles.detail(profileRow.modelData)
|
|
value: profileRow.modelData === PowerProfiles.active ? "Active" : ""
|
|
divider: profileRow.index < PowerProfiles.profiles.length - 1
|
|
activatable: profileRow.modelData !== PowerProfiles.active && !PowerProfiles.busy
|
|
onActivated: PowerProfiles.set(profileRow.modelData)
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
// Named for the power source only on a machine that has two of them.
|
|
// On a desktop this is simply "Idle behavior", as it always was.
|
|
title: Battery.available ? "Idle behavior on wall power" : "Idle behavior"
|
|
subtitle: IdleLock.managed
|
|
? (Battery.available && !Battery.acOnline
|
|
? "Managed here. These apply when the charger is connected; the battery timings below are what is in effect right now."
|
|
: "Idle timings are managed here. Changes take effect immediately.")
|
|
: "hypridle is running its shipped configuration. Turn on management below to make these adjustable."
|
|
|
|
SliderRow { setting: "screenBlankMinutes"; zeroLabel: "Never" }
|
|
SliderRow { setting: "lockMinutes"; zeroLabel: "Never" }
|
|
SliderRow { setting: "suspendMinutes"; zeroLabel: "Never" }
|
|
ToggleRow { setting: "lockOnSleep"; divider: false }
|
|
}
|
|
|
|
// Absent on a desktop. hypridle holds one set of timeouts at a time, so
|
|
// these do not layer on top of the card above -- they replace it whenever
|
|
// the charger is unplugged, and panama-idle rebuilds the config at that
|
|
// moment.
|
|
SettingsCard {
|
|
visible: Battery.available
|
|
title: "Idle behavior on battery"
|
|
subtitle: Battery.acOnline
|
|
? "What will apply once the charger is unplugged."
|
|
: "In effect right now."
|
|
|
|
SliderRow { setting: "screenBlankMinutesBattery"; zeroLabel: "Never" }
|
|
SliderRow { setting: "lockMinutesBattery"; zeroLabel: "Never" }
|
|
SliderRow { setting: "suspendMinutesBattery"; zeroLabel: "Never"; divider: false }
|
|
}
|
|
|
|
// What the lid does. Informational by design: the decision follows what
|
|
// is connected (see services/LidPolicy.qml), and the deliberate absence
|
|
// of an override is part of the design -- a lid switch set to "never
|
|
// suspend" is a laptop that cooks in a bag. Saying so here beats leaving
|
|
// the behavior undiscoverable.
|
|
SettingsCard {
|
|
visible: Battery.available
|
|
title: "When the lid closes"
|
|
subtitle: "Decided by what is connected rather than by a setting: with an external display attached the machine is docked and keeps running; on its own it suspends, locking on the way down."
|
|
|
|
TextRow {
|
|
label: "Right now"
|
|
value: LidPolicy.inhibited
|
|
? "Stays awake — an external display is connected"
|
|
: "Suspends"
|
|
divider: false
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
visible: IdleLock.lockBeforeBlank
|
|
title: "Lock happens before the screen turns off"
|
|
subtitle: "The session will lock at " + IdleLock.lockMinutes
|
|
+ " minutes and the display will not blank until " + IdleLock.blankMinutes
|
|
+ ". That works, but the screen stays lit on the lock screen for the difference."
|
|
}
|
|
|
|
SettingsCard {
|
|
title: "Management"
|
|
subtitle: "hypridle's configuration is generated into your state directory and the service is pointed at it with a systemd drop-in. ~/.config/hypr is a symlink into the configuration repository, so the shipped file cannot be rewritten in place."
|
|
|
|
SettingRow {
|
|
label: "Manage idle timings here"
|
|
detail: IdleLock.serviceState === "active"
|
|
? "hypridle is running"
|
|
: "hypridle is " + IdleLock.serviceState
|
|
controlWidth: 48
|
|
|
|
SettingsToggle {
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
checked: IdleLock.managed
|
|
enabled: !IdleLock.busy
|
|
onToggled: value => IdleLock.setManaged(value)
|
|
}
|
|
}
|
|
|
|
ActionRow {
|
|
label: "Lock the screen now"
|
|
detail: "Same as the Super+L shortcut"
|
|
action: "Lock"
|
|
divider: false
|
|
onTriggered: Quickshell.execDetached(["loginctl", "lock-session"])
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
visible: IdleLock.lastError !== ""
|
|
title: "Idle configuration problem"
|
|
subtitle: IdleLock.lastError
|
|
|
|
ActionRow {
|
|
label: "Read the idle configuration again"
|
|
action: "Retry"
|
|
divider: false
|
|
onTriggered: IdleLock.refresh()
|
|
}
|
|
}
|
|
}
|