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
204 lines
8.1 KiB
QML
204 lines
8.1 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"
|
|
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
|
|
}
|
|
}
|
|
|
|
// 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()
|
|
}
|
|
}
|
|
}
|