Files
Panama/config/dot/quickshell/modules/settings/PowerPage.qml
T
Gabriel Brown bc6d63b70f Let the idle timings know whether you are plugged in
An idle screen costs a screen on wall power and the rest of your
afternoon on battery, so they should not be the same number. hypridle
has no concept of a power source -- one config, one set of timeouts --
so rather than maintaining two configs and swapping them, panama-idle
builds the single config from whichever key set applies, and IdleLock
rebuilds it when the charger comes or goes. That runs through the same
400ms debounce a settings change uses, so a loose charger cannot
restart hypridle in a loop.

The battery keys fall back to their AC counterparts rather than to the
schema defaults. Without that, unplugging would silently override a
deliberately long timing with a shipped short one, which is the kind
of thing you would notice only by losing work.

A machine with no battery reads none of it and generates exactly what
it generated before. The contract pins that alongside the two obvious
directions, and was checked by sabotaging the detection to confirm it
fails rather than passing vacuously.
2026-08-21 22:18:29 -04:00

176 lines
6.8 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";
}
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 }
}
// 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()
}
}
}