SessiondBridge: handle pushed {"directive":"lock"} on the heartbeat line and
raise the lock surface. Unknown directives are console.error — a silent drop
leaves the daemon blanking unlocked when its budget expires.
SessiondPolicy: new singleton, reads/writes DeviceStatePolicy over its own
short-lived connection (not the bridge socket — that EOF is shell-death
detection).
IdleConfig: new "Lock screen (device authority)" section bound to it. SetPolicy
had zero callers, so the old spinboxes moved a JSON file the daemon never read.
Says so in the error colour when sessiond is unreachable.
New file needs a deploy.sh manifest line or the services qmldir fails whole.
248 lines
10 KiB
QML
248 lines
10 KiB
QML
import QtQuick
|
|
import QtQuick.Layouts
|
|
import qs.services
|
|
import qs.modules.common
|
|
import qs.modules.common.widgets
|
|
|
|
// Idle & sleep — the staged idle projection, exposed honestly.
|
|
//
|
|
// The governing idea is REFERENCE-EXTRACTION.md's "idle is a transition graph,
|
|
// not a timer": the page shows the live stage the shell is actually in, not
|
|
// just three timeout knobs pretending idle is linear.
|
|
//
|
|
// 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.
|
|
//
|
|
// 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`
|
|
// children, which can become accidental shell instances when display
|
|
// selection is ambiguous.
|
|
ContentPage {
|
|
id: page
|
|
forceWidth: true
|
|
|
|
// --- Live stage readout ----------------------------------------------
|
|
// 0 Active · 1 Dimmed · 2 Lock requested · 3 Lock secure ·
|
|
// 4 Suspending · 5 Asleep · 6 Waking — the IdleCoordinator.State
|
|
// enum, read directly from the shell-owned coordinator.
|
|
readonly property int liveStage: IdleCoordinator.state
|
|
readonly property bool liveNative: IdleCoordinator.nativeEnabled
|
|
readonly property bool probeOk: true
|
|
readonly property bool sleepInhibitorHeld: SessionEvents.sleepInhibitorHeld
|
|
readonly property bool stepUpEnabled: StepUpAuth.grantTtlMs > 0
|
|
|
|
readonly property var stageNames: [
|
|
Translation.tr("Active"),
|
|
Translation.tr("Dimmed"),
|
|
Translation.tr("Lock requested"),
|
|
Translation.tr("Lock secure"),
|
|
Translation.tr("Suspending"),
|
|
Translation.tr("Asleep"),
|
|
Translation.tr("Waking")
|
|
]
|
|
function stageLabel(s) {
|
|
return (s >= 0 && s < stageNames.length) ? stageNames[s] : Translation.tr("unknown");
|
|
}
|
|
|
|
ContentSection {
|
|
icon: "motion_sensor_active"
|
|
title: Translation.tr("Current state")
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 12
|
|
|
|
StyledText {
|
|
text: page.probeOk
|
|
? Translation.tr("Idle stage: %1").arg(page.stageLabel(page.liveStage))
|
|
: Translation.tr("Idle stage: (shell not reachable)")
|
|
font.pixelSize: Appearance.font.pixelSize.normal
|
|
}
|
|
}
|
|
|
|
StyledText {
|
|
Layout.fillWidth: true
|
|
wrapMode: Text.WordWrap
|
|
color: Appearance.colors.colSubtext
|
|
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.")
|
|
}
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 12
|
|
|
|
StyledText {
|
|
text: page.sleepInhibitorHeld
|
|
? Translation.tr("Sleep inhibitor: held (suspend blocked until lock is secure)")
|
|
: Translation.tr("Sleep inhibitor: released (suspend may proceed)")
|
|
font.pixelSize: Appearance.font.pixelSize.normal
|
|
color: page.sleepInhibitorHeld
|
|
? Appearance.colors.colOnLayer1
|
|
: Appearance.colors.colSubtext
|
|
}
|
|
}
|
|
}
|
|
|
|
ContentSection {
|
|
icon: "experiment"
|
|
title: Translation.tr("Native idle coordinator")
|
|
|
|
ConfigSwitch {
|
|
buttonIcon: "science"
|
|
text: Translation.tr("Enable native coordinator (experimental)")
|
|
checked: Config.options.lock.idle.nativeCoordinatorEnabled
|
|
onCheckedChanged: {
|
|
Config.options.lock.idle.nativeCoordinatorEnabled = checked;
|
|
}
|
|
StyledToolTip {
|
|
text: Translation.tr("Use Wayland idle-notify to drive the dim/lock timers. Unverified on the Pixel 3 compositor build — leave off unless you are testing it. When off, hypridle handles idle and screen-off.")
|
|
}
|
|
}
|
|
}
|
|
|
|
ContentSection {
|
|
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.")
|
|
}
|
|
}
|
|
|
|
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
|
|
|
|
StyledToolTip {
|
|
text: Translation.tr("Seconds of inactivity before the session locks. Only active while the native coordinator is enabled.")
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Lock screen timers, owned by sessiond ----------------------------
|
|
// A separate section because these are a different authority. The two
|
|
// above are the shell's own IdleMonitors deciding when to dim and lock a
|
|
// session in use; these are the device state machine deciding how long a
|
|
// LOCKED, lit panel may burn before it goes dark. That machine has its own
|
|
// clock and its own actuators, so its numbers must come from it — which is
|
|
// what SessiondPolicy is for, and what this page did not do until
|
|
// 2026-07-25 (SetPolicy had zero callers; the daemon ran on its built-in
|
|
// 15 s while this page showed whatever was in the JSON file).
|
|
Component.onCompleted: SessiondPolicy.refresh()
|
|
|
|
ContentSection {
|
|
icon: "phonelink_lock"
|
|
title: Translation.tr("Lock screen (device authority)")
|
|
|
|
StyledText {
|
|
Layout.fillWidth: true
|
|
wrapMode: Text.WordWrap
|
|
color: SessiondPolicy.available
|
|
? Appearance.colors.colSubtext
|
|
: Appearance.colors.colError
|
|
font.pixelSize: Appearance.font.pixelSize.smaller
|
|
text: SessiondPolicy.available
|
|
? Translation.tr("Read live from souveraine-sessiond. Changes here go straight to the daemon that blanks the panel.")
|
|
: 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.")
|
|
}
|
|
}
|
|
|
|
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.")
|
|
}
|
|
}
|
|
|
|
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.")
|
|
}
|
|
}
|
|
|
|
ConfigSwitch {
|
|
buttonIcon: "brightness_low"
|
|
text: Translation.tr("Show the dim warning")
|
|
checked: SessiondPolicy.dimWarning
|
|
enabled: SessiondPolicy.available
|
|
onCheckedChanged: {
|
|
if (SessiondPolicy.available && checked !== SessiondPolicy.dimWarning)
|
|
SessiondPolicy.apply({ dim_warning: checked });
|
|
}
|
|
}
|
|
|
|
StyledText {
|
|
Layout.fillWidth: true
|
|
wrapMode: Text.WordWrap
|
|
color: Appearance.colors.colSubtext
|
|
font.pixelSize: Appearance.font.pixelSize.smaller
|
|
text: Translation.tr("The screen never goes dark on an unlocked session. sessiond locks first and waits %1s for the compositor to acknowledge; if it cannot, it blanks anyway and records a security error rather than pretending the session locked.")
|
|
.arg(SessiondPolicy.lockAckBudgetSecs)
|
|
}
|
|
}
|
|
}
|