Skip to content

Commit 022b9b8

Browse files
author
Lukas Essmann
committed
feat(idle): add per-behavior locked screen timeout
Idle behaviors get an optional locked_timeout (seconds). While the session is locked a behavior with locked_timeout > 0 arms at that value instead of its normal timeout, and reverts on unlock. Set timeout = 0 with locked_timeout > 0 to run a behavior only while locked. The main use is screen_off: turn the display off quickly on the lock screen (and again after each wake, since the behavior re-arms) without making the display sleep aggressively during normal use. Re-arming a behavior that had already fired now runs its resume action first, so unlocking while the screen is off turns it back on instead of leaving it black. The Settings field is shown for screen_off only.
1 parent ed3cb98 commit 022b9b8

9 files changed

Lines changed: 89 additions & 16 deletions

File tree

assets/translations/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1060,6 +1060,7 @@
10601060
"suspend": "Suspend"
10611061
},
10621062
"kind-section-label": "Action",
1063+
"locked-timeout-label": "Timeout while locked (seconds, 0 = off)",
10631064
"name-label": "Name",
10641065
"name-placeholder": "idle-behavior",
10651066
"resume-command-label": "Resume command",

src/app/application_ui.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,11 +317,13 @@ void Application::initLockScreenAndSession() {
317317
[this]() {
318318
m_idleGraceOverlay.hide();
319319
m_lockscreenWidgetsController.onLockStateChanged();
320+
m_idleManager.setSessionLocked(true);
320321
m_hookManager.fire(HookKind::SessionLocked);
321322
},
322323
[this]() {
323324
m_idleGraceOverlay.hide();
324325
m_lockscreenWidgetsController.onLockStateChanged();
326+
m_idleManager.setSessionLocked(false);
325327
m_hookManager.fire(HookKind::SessionUnlocked);
326328
requestAllSurfacesRedraw();
327329
if (m_logindService != nullptr) {

src/config/config_overrides.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,9 @@ namespace {
519519
toml::table row;
520520
row.insert_or_assign("enabled", item.enabled);
521521
row.insert_or_assign("timeout", item.timeoutSeconds);
522+
if (item.lockedTimeoutSeconds > 0.0) {
523+
row.insert_or_assign("locked_timeout", item.lockedTimeoutSeconds);
524+
}
522525
if (!item.action.empty()) {
523526
row.insert_or_assign("action", item.action);
524527
}

src/config/config_types.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,8 @@ struct IdleBehaviorConfig {
251251
std::string resumeCommand;
252252
/// When `action` is `suspend`, lock the session before running suspend so lock surfaces are ready (recommended).
253253
bool lockBeforeSuspend = true;
254+
/// Shorter timeout (seconds) applied only while the session is locked; 0 = always use timeoutSeconds.
255+
double lockedTimeoutSeconds = 0.0;
254256

255257
bool operator==(const IdleBehaviorConfig&) const = default;
256258
};

src/config/schema/config_schema.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -841,6 +841,7 @@ namespace noctalia::config::schema {
841841
static const Schema<IdleBehaviorConfig> s = {
842842
field(&IdleBehaviorConfig::enabled, "enabled"),
843843
field(&IdleBehaviorConfig::timeoutSeconds, "timeout"),
844+
field(&IdleBehaviorConfig::lockedTimeoutSeconds, "locked_timeout"),
844845
// action is trimmed on read.
845846
custom<IdleBehaviorConfig>(
846847
"action",

src/idle/idle_manager.cpp

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,21 @@ void IdleManager::setScreenSaverInhibitLocks(std::int64_t locks) {
8383
}
8484
}
8585

86+
void IdleManager::setSessionLocked(bool locked) {
87+
if (m_sessionLocked == locked) {
88+
return;
89+
}
90+
m_sessionLocked = locked;
91+
recreateBehaviorNotifications();
92+
}
93+
94+
double IdleManager::effectiveTimeoutSeconds(const IdleBehaviorConfig& config) const {
95+
if (m_sessionLocked && std::isfinite(config.lockedTimeoutSeconds) && config.lockedTimeoutSeconds > 0.0) {
96+
return config.lockedTimeoutSeconds;
97+
}
98+
return config.timeoutSeconds;
99+
}
100+
86101
void IdleManager::reload(const IdleConfig& config) {
87102
clearBehaviors();
88103
m_idleConfig = config;
@@ -157,19 +172,24 @@ void IdleManager::recreateBehaviorNotification(BehaviorState& behavior) {
157172
return;
158173
}
159174

175+
// Re-arming an idled behavior (e.g. on unlock) has no natural resume event, so run it here —
176+
// otherwise screen_off's display-off is never undone and the screen stays black.
177+
if (behavior.phase == BehaviorPhase::Idled) {
178+
runResumeBehavior(behavior);
179+
}
180+
160181
if (behavior.notification != nullptr) {
161182
ext_idle_notification_v1_destroy(behavior.notification);
162183
behavior.notification = nullptr;
163184
}
164185
behavior.phase = BehaviorPhase::Waiting;
165186

166-
if (!behavior.config.enabled
167-
|| !std::isfinite(behavior.config.timeoutSeconds)
168-
|| behavior.config.timeoutSeconds <= 0.0) {
187+
const double timeout = effectiveTimeoutSeconds(behavior.config);
188+
if (!behavior.config.enabled || !std::isfinite(timeout) || timeout <= 0.0) {
169189
return;
170190
}
171191

172-
const auto timeoutMs = timeoutSecondsToMilliseconds(behavior.config.timeoutSeconds);
192+
const auto timeoutMs = timeoutSecondsToMilliseconds(timeout);
173193
behavior.notification = m_wayland->createIdleNotification(timeoutMs);
174194
if (behavior.notification == nullptr) {
175195
kLog.warn("failed to re-register idle behavior '{}'", behavior.config.name);
@@ -187,7 +207,7 @@ void IdleManager::recreateBehaviorNotifications() {
187207
for (auto& behavior : m_behaviors) {
188208
recreateBehaviorNotification(*behavior);
189209
}
190-
kLog.info("idle behavior notifications reset after screensaver inhibit released");
210+
kLog.debug("idle behavior notifications re-armed");
191211
}
192212

193213
void IdleManager::createBehavior(const IdleBehaviorConfig& config) {
@@ -198,7 +218,11 @@ void IdleManager::createBehavior(const IdleBehaviorConfig& config) {
198218
kLog.warn("idle behavior '{}' ignored: timeout must be >= 0 seconds", config.name);
199219
return;
200220
}
201-
if (config.timeoutSeconds == 0.0) {
221+
if (!std::isfinite(config.lockedTimeoutSeconds) || config.lockedTimeoutSeconds < 0.0) {
222+
kLog.warn("idle behavior '{}' ignored: locked timeout must be >= 0 seconds", config.name);
223+
return;
224+
}
225+
if (config.timeoutSeconds == 0.0 && config.lockedTimeoutSeconds == 0.0) {
202226
kLog.debug("idle behavior '{}' disabled by zero timeout", config.name);
203227
return;
204228
}
@@ -211,15 +235,11 @@ void IdleManager::createBehavior(const IdleBehaviorConfig& config) {
211235
auto behavior = std::make_unique<BehaviorState>();
212236
behavior->owner = this;
213237
behavior->config = config;
214-
const auto timeoutMs = timeoutSecondsToMilliseconds(config.timeoutSeconds);
215-
behavior->notification = m_wayland->createIdleNotification(timeoutMs);
216-
if (behavior->notification == nullptr) {
217-
kLog.warn("failed to register idle behavior '{}'", config.name);
218-
return;
219-
}
220-
221-
ext_idle_notification_v1_add_listener(behavior->notification, &kIdleNotificationListener, behavior.get());
222-
kLog.info("registered idle behavior '{}' timeout={}s", config.name, config.timeoutSeconds);
238+
recreateBehaviorNotification(*behavior);
239+
kLog.info(
240+
"registered idle behavior '{}' timeout={}s locked_timeout={}s", config.name, config.timeoutSeconds,
241+
config.lockedTimeoutSeconds
242+
);
223243
m_behaviors.push_back(std::move(behavior));
224244
}
225245

src/idle/idle_manager.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ class IdleManager {
4141
void reload(const IdleConfig& config);
4242
/// D-Bus screensaver inhibits (e.g. Chrome video playback). Suppresses idle actions while > 0.
4343
void setScreenSaverInhibitLocks(std::int64_t locks);
44+
/// While locked, behaviors with a positive lockedTimeoutSeconds re-arm at that shorter timeout.
45+
void setSessionLocked(bool locked);
4446
/// Seconds the compositor has reported session-idle (1s heartbeat notification); 0 when active.
4547
[[nodiscard]] std::int64_t liveIdleSeconds() const noexcept { return m_liveIdleSeconds; }
4648
void onSecondTick();
@@ -63,6 +65,7 @@ class IdleManager {
6365
BehaviorPhase phase = BehaviorPhase::Waiting;
6466
};
6567

68+
[[nodiscard]] double effectiveTimeoutSeconds(const IdleBehaviorConfig& config) const;
6669
void clearBehaviors();
6770
void syncHeartbeat();
6871
void destroyHeartbeat();
@@ -94,4 +97,5 @@ class IdleManager {
9497
std::int64_t m_liveIdleSeconds = 0;
9598
std::int64_t m_screenSaverInhibitLocks = 0;
9699
bool m_idledWhileScreenSaverInhibited = false;
100+
bool m_sessionLocked = false;
97101
};

src/shell/settings/settings_content_idle_behavior.cpp

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ namespace settings {
3737
IdleBehaviorConfig norm = row;
3838
normalizeIdleBehaviorAction(norm);
3939
const bool showCustomCommands = (norm.action == "command");
40+
const bool showLockedTimeout = (norm.action == "screen_off");
4041

4142
auto body = ui::column({
4243
.align = FlexAlign::Stretch,
@@ -205,6 +206,45 @@ namespace settings {
205206
timeoutBlock->addChild(std::move(timeoutIn));
206207
body->addChild(std::move(timeoutBlock));
207208

209+
if (showLockedTimeout) {
210+
auto lockedTimeoutBlock = ui::column(
211+
{.align = FlexAlign::Stretch, .gap = Style::spaceXs * scale},
212+
makeLabel(
213+
i18n::tr("settings.idle.behavior.locked-timeout-label"), Style::fontSizeCaption * scale,
214+
colorSpecFromRole(ColorRole::OnSurfaceVariant), FontWeight::Normal
215+
)
216+
);
217+
Input* lockedTimeoutPtr = nullptr;
218+
auto lockedTimeoutIn = ui::input({
219+
.out = &lockedTimeoutPtr,
220+
.value = StringUtils::formatDotDecimal(row.lockedTimeoutSeconds),
221+
.placeholder = "0",
222+
.fontSize = Style::fontSizeBody * scale,
223+
.controlHeight = Style::controlHeight * scale,
224+
.horizontalPadding = Style::spaceSm * scale,
225+
});
226+
const auto commitLockedTimeout = [&row, persist, lockedTimeoutPtr]() {
227+
const auto parsed = parseDoubleInput(lockedTimeoutPtr->value());
228+
constexpr double kMaxIdleTimeoutSeconds =
229+
static_cast<double>(std::numeric_limits<std::uint32_t>::max()) / 1000.0;
230+
if (!parsed.has_value() || *parsed < 0.0 || *parsed > kMaxIdleTimeoutSeconds) {
231+
lockedTimeoutPtr->setInvalid(true);
232+
return;
233+
}
234+
row.lockedTimeoutSeconds = *parsed;
235+
lockedTimeoutPtr->setInvalid(false);
236+
lockedTimeoutPtr->setValue(StringUtils::formatDotDecimal(row.lockedTimeoutSeconds));
237+
persist();
238+
};
239+
lockedTimeoutIn->setOnChange([lockedTimeoutPtr](const std::string& /*t*/) {
240+
lockedTimeoutPtr->setInvalid(false);
241+
});
242+
lockedTimeoutIn->setOnSubmit([commitLockedTimeout](const std::string& /*text*/) { commitLockedTimeout(); });
243+
lockedTimeoutIn->setOnFocusLoss(commitLockedTimeout);
244+
lockedTimeoutBlock->addChild(std::move(lockedTimeoutIn));
245+
body->addChild(std::move(lockedTimeoutBlock));
246+
}
247+
208248
body->addChild(std::move(customCommandsGrp));
209249
body->addChild(std::move(resumeCommandGrp));
210250

tests/config_schema_roundtrip_test.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@ location = "https://example.invalid/bad"
369369
// Explicit normalized actions so normalizeIdleBehaviorAction is a no-op on read.
370370
c.idle.behaviors = {
371371
{"dim", true, 60, "lock", "", "", true},
372-
{"off", false, 300, "screen_off", "", "", true},
372+
{"off", false, 300, "screen_off", "", "", true, 30},
373373
};
374374
c.wallpaper.enabled = false;
375375
c.wallpaper.fillColor = colorSpecFromConfigString("#ff8800");

0 commit comments

Comments
 (0)