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:
parent
df654e6bee
commit
70a05819e8
3 changed files with 111 additions and 79 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue