Watch
1
0
Fork
You've already forked souveraine
0

settings: presets instead of seconds; two ordering fixes

IdleConfig: ConfigSelectionArray presets (30s/1/2/5/10 min for the shell's
timers, 15s/30s/1/5 min/Never for the lock screen) instead of seconds
spinboxes. Nobody reasons about a lock screen in 5s increments; the policy
struct already said "iOS Auto-Lock shape". Wire values stay seconds.
Header comment corrected — hypridle no longer owns screen-off.

set_panel(false) clears pending_blank. A panel darkened by any other route
left it armed, so the next tick emitted a second Blank or hit the
blank-without-lock error path for a blank nobody was waiting on.

SetPolicy refuses dim_grace >= lock_blank_after. dim_at saturates to zero, so
the panel dimmed the instant it went idle with no lit period.

Verified by rendering the page, not by parse alone.
This commit is contained in:
Fimeg 2026-07-26 07:24:28 -04:00
commit 70a05819e8
3 changed files with 111 additions and 79 deletions

View file

@ -792,6 +792,14 @@ impl DeviceStateMachine {
self.panel_on = on;
info!("[device-state] panel {}", if on { "on" } else { "off" });
self.blank_requested = false;
// A blank waiting on a lock ack is moot once the panel is dark by any
// route — the power button, the proximity service, a wake that never
// came. Leaving it armed makes the next tick emit a second `Blank` at
// an already-dark panel, or worse, fire the `blank-without-lock` error
// path for a blank nobody is waiting on.
if !on {
self.pending_blank = None;
}
if on {
self.idle_since = None;
// A wake must never come up dim. If the blank landed while the

View file

@ -707,6 +707,18 @@ fn handle_request(
if let Some(v) = unlocked_blank_after_secs {
p.unlocked_blank_after = as_budget(v);
}
// A dim warning longer than the budget it warns about is not a
// warning — `dim_at` saturates to zero and the panel dims the
// instant it goes idle, with no lit period at all. Refuse rather
// than accept a setting whose behaviour contradicts its label.
if let Some(budget) = p.lock_blank_after {
if p.dim_warning && p.dim_grace >= budget {
drop(d);
return refusal(
"dim_grace_secs must be shorter than lock_blank_after_secs",
);
}
}
let applied = serde_json::json!({
"lock_blank_after_secs": p.lock_blank_after.map(|v| v.as_secs()).unwrap_or(0),
"lock_blank_after_held_secs":

View file

@ -13,11 +13,16 @@ import qs.modules.common.widgets
// Two honesty constraints drive the layout, and both come straight from the
// extraction's build order (truth before visuals):
//
// 1. The native idle coordinator is OFF by default and stays experimental
// until the Wayland idle-notify is verified on the Pixel compositor.
// While it is off, the dim/lock TIMERS DO NOT RUN — hypridle owns real
// screen-off, and the only transitions that fire are lock-request and
// lock-secure. The page says so instead of implying the timers are live.
// 1. Two authorities, two sections, never blended. The shell's own
// IdleMonitors decide when a session IN USE dims and locks; sessiond's
// device state machine decides how long a LOCKED panel may burn. The
// second set is read live from the daemon (SessiondPolicy) rather than
// from a config file, because the daemon is what actuates them —
// TASK-19's "no success-shaped switches".
//
// hypridle no longer owns screen-off: its idle listeners were deleted
// 2026-07-25 once sessiond got real actuators, because "lock, then off"
// held only while its 300s lock happened to precede its 600s blank.
//
// 2. Settings is a window in the authoritative shell process, so this page
// reads the idle/session singletons directly. It must not spawn `qs ipc`
@ -37,6 +42,28 @@ ContentPage {
readonly property bool sleepInhibitorHeld: SessionEvents.sleepInhibitorHeld
readonly property bool stepUpEnabled: StepUpAuth.grantTtlMs > 0
// Seconds on the wire, human words on screen. "Never" is 0, which is what
// the daemon already means by it.
readonly property var blankPresets: [
{ displayName: Translation.tr("15s"), icon: "timer", value: 15 },
{ displayName: Translation.tr("30s"), icon: "timer", value: 30 },
{ displayName: Translation.tr("1 min"), icon: "timer", value: 60 },
{ displayName: Translation.tr("5 min"), icon: "timer", value: 300 },
{ displayName: Translation.tr("Never"), icon: "timer_off", value: 0 }
]
readonly property var idlePresets: [
{ displayName: Translation.tr("30s"), icon: "timer", value: 30 },
{ displayName: Translation.tr("1 min"), icon: "timer", value: 60 },
{ displayName: Translation.tr("2 min"), icon: "timer", value: 120 },
{ displayName: Translation.tr("5 min"), icon: "timer", value: 300 },
{ displayName: Translation.tr("10 min"), icon: "timer", value: 600 }
]
readonly property var gracePresets: [
{ displayName: Translation.tr("5s"), icon: "brightness_medium", value: 5 },
{ displayName: Translation.tr("10s"), icon: "brightness_medium", value: 10 },
{ displayName: Translation.tr("20s"), icon: "brightness_medium", value: 20 }
]
readonly property var stageNames: [
Translation.tr("Active"),
Translation.tr("Dimmed"),
@ -73,7 +100,7 @@ ContentPage {
font.pixelSize: Appearance.font.pixelSize.smaller
text: page.liveNative
? Translation.tr("The native coordinator is driving idle transitions. Dim and lock fire on the timers below.")
: Translation.tr("The native coordinator is off, so the timers below do not run — hypridle owns screen-off. Only lock-requested and lock-secure transitions are reported here.")
: Translation.tr("The native coordinator is off, so the session timers below do not run. Nothing else locks on idle now that hypridle's listeners are gone — turn it on, or the phone only locks when you lock it.")
}
RowLayout {
@ -113,37 +140,33 @@ ContentPage {
icon: "timer"
title: Translation.tr("Session timers")
// These write real Config keys, but they only take EFFECT when the
// native coordinator is on. Disabled (greyed) otherwise so the page
// never implies a knob is doing something it isn't.
ConfigSpinBox {
icon: "brightness_low"
text: Translation.tr("Dim after (seconds)")
value: Config.options.lock.idle.dimAfterSeconds
from: 5
to: 600
stepSize: 5
enabled: Config.options.lock.idle.nativeCoordinatorEnabled
onValueChanged: Config.options.lock.idle.dimAfterSeconds = value
StyledToolTip {
text: Translation.tr("Seconds of inactivity before the screen dims. Only active while the native coordinator is enabled.")
}
// Config keys, effective only while the native coordinator is on.
// Presets rather than a seconds spinner, same reasoning as below.
ContentSubsectionLabel {
text: Translation.tr("Dim the screen after")
}
ConfigSelectionArray {
currentValue: Config.options.lock.idle.dimAfterSeconds
onSelected: v => Config.options.lock.idle.dimAfterSeconds = v
options: page.idlePresets
}
ConfigSpinBox {
icon: "lock_clock"
text: Translation.tr("Lock after (seconds)")
value: Config.options.lock.idle.lockAfterSeconds
from: 10
to: 1800
stepSize: 10
enabled: Config.options.lock.idle.nativeCoordinatorEnabled
onValueChanged: Config.options.lock.idle.lockAfterSeconds = value
ContentSubsectionLabel {
text: Translation.tr("Lock the session after")
}
ConfigSelectionArray {
currentValue: Config.options.lock.idle.lockAfterSeconds
onSelected: v => Config.options.lock.idle.lockAfterSeconds = v
options: page.idlePresets
}
StyledToolTip {
text: Translation.tr("Seconds of inactivity before the session locks. Only active while the native coordinator is enabled.")
}
StyledText {
Layout.fillWidth: true
visible: !Config.options.lock.idle.nativeCoordinatorEnabled
wrapMode: Text.WordWrap
color: Appearance.colors.colError
font.pixelSize: Appearance.font.pixelSize.smaller
text: Translation.tr("The native coordinator is off, so these do not run.")
}
}
@ -174,55 +197,44 @@ ContentPage {
: Translation.tr("sessiond is not answering — these values are NOT authoritative. %1").arg(SessiondPolicy.lastError)
}
ConfigSpinBox {
icon: "timer_off"
text: Translation.tr("Blank the lock screen after (seconds, 0 = never)")
value: SessiondPolicy.lockBlankAfterSecs
from: 0
to: 600
stepSize: 5
enabled: SessiondPolicy.available
onValueChanged: {
if (SessiondPolicy.available && value !== SessiondPolicy.lockBlankAfterSecs)
SessiondPolicy.apply({ lock_blank_after_secs: value });
}
StyledToolTip {
text: Translation.tr("How long a locked, lit screen waits before going dark. 0 keeps it lit forever — the desk-clock case.")
}
// Presets, not a seconds spinner. The policy struct's own comment
// names the shape — "iOS's Auto-Lock: a user-chosen timeout with a
// visible dim shortly before it, and a 'never' option for the
// desk-clock case" — and nobody reasons about a lock screen in
// 5-second increments. Values are still seconds on the wire; the
// daemon's vocabulary does not change because the UI got legible.
ContentSubsectionLabel {
text: Translation.tr("Blank after — on a table")
}
ConfigSelectionArray {
currentValue: SessiondPolicy.lockBlankAfterSecs
onSelected: v => SessiondPolicy.apply({ lock_blank_after_secs: v })
options: page.blankPresets
}
ConfigSpinBox {
icon: "back_hand"
text: Translation.tr("…when the device is being held (seconds)")
value: SessiondPolicy.lockBlankAfterHeldSecs
from: 0
to: 600
stepSize: 5
enabled: SessiondPolicy.available
onValueChanged: {
if (SessiondPolicy.available && value !== SessiondPolicy.lockBlankAfterHeldSecs)
SessiondPolicy.apply({ lock_blank_after_held_secs: value });
}
StyledToolTip {
text: Translation.tr("The same budget when the accelerometer says the device is in a hand rather than on a table.")
}
ContentSubsectionLabel {
text: Translation.tr("Blank after — in your hand")
}
StyledText {
Layout.fillWidth: true
wrapMode: Text.WordWrap
color: Appearance.colors.colSubtext
font.pixelSize: Appearance.font.pixelSize.smaller
text: Translation.tr("Used when the accelerometer says the device is being held rather than resting.")
}
ConfigSelectionArray {
currentValue: SessiondPolicy.lockBlankAfterHeldSecs
onSelected: v => SessiondPolicy.apply({ lock_blank_after_held_secs: v })
options: page.blankPresets
}
ConfigSpinBox {
icon: "brightness_medium"
text: Translation.tr("Dim warning before blanking (seconds)")
value: SessiondPolicy.dimGraceSecs
from: 1
to: 120
stepSize: 1
enabled: SessiondPolicy.available && SessiondPolicy.dimWarning
onValueChanged: {
if (SessiondPolicy.available && value !== SessiondPolicy.dimGraceSecs)
SessiondPolicy.apply({ dim_grace_secs: value });
}
StyledToolTip {
text: Translation.tr("The panel visibly fades this long before it goes dark. A tap inside the window cancels the blank.")
}
ContentSubsectionLabel {
text: Translation.tr("Dim warning before blanking")
}
ConfigSelectionArray {
currentValue: SessiondPolicy.dimGraceSecs
onSelected: v => SessiondPolicy.apply({ dim_grace_secs: v })
options: page.gracePresets
}
ConfigSwitch {