From 35537b32c870a56238317ac98e8f1ede46b1a1a2 Mon Sep 17 00:00:00 2001 From: Samuel Onoja Date: Wed, 30 Sep 2026 05:10:04 +0100 Subject: [PATCH] Dim and sleep locked displays after inactivity Add adjustable lock-only inactivity delays, a smooth dimming fade and DRM display sleep without changing output layout. Input, session resume and policy changes wake displays. Pending flips retire before power-off, and protected fallback frames preserve the policy after an early locker crash while unlock ownership checks remain strict. Reduce the password field and avatar, expose delays in Settings, document the practical swaylock comparison and remove the completed backlog. Validation: independent review cleared; 255 compositor tests, 7 locker tests and 15 Settings tests pass; affected debug binaries rebuilt. Isolated visual smoke remains blocked by an unlocked nested screenshot timeout, matching the previously established baseline presentation limitation. Real DRM display sleep requires a live session check. --- compositor/src/backends/direct.rs | 55 ++++++ compositor/src/config.rs | 3 + compositor/src/input.rs | 8 + compositor/src/session_lock.rs | 288 +++++++++++++++++++++++++++- compositor/src/state.rs | 9 + compositor/src/winit.rs | 24 +++ docs/backlog.md | 3 - docs/configuration.md | 5 + docs/locking.md | 45 ++++- scripts/tests/test_lock_isolated.py | 9 +- shell/ferese-lock/src/main.rs | 8 +- shell/ferese-settings/src/schema.rs | 22 +++ 12 files changed, 466 insertions(+), 13 deletions(-) delete mode 100644 docs/backlog.md diff --git a/compositor/src/backends/direct.rs b/compositor/src/backends/direct.rs index 69e2433..5e40e77 100644 --- a/compositor/src/backends/direct.rs +++ b/compositor/src/backends/direct.rs @@ -184,6 +184,7 @@ struct DirectOutput { damage_tracker: OutputDamageTracker, render_metrics: RenderMetrics, frame_pending: bool, + power_off: bool, lock_frame_pending: bool, } @@ -268,6 +269,7 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() output.damage_tracker = OutputDamageTracker::from_output(&output.output); output.frame_pending = false; + output.power_off = false; }); }); } @@ -287,6 +289,7 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() } } tracing::info!("direct session activated"); + state.lock_input_activity(); render_all(state); } })?; @@ -561,6 +564,45 @@ fn open_primary_device( Ok(()) } +pub(crate) fn sleep_locked_outputs(state: &mut Ferese) { + if !state.session_lock.active || !state.session_lock.sleeping { + return; + } + let Some(backend) = state.direct_backend.as_mut() else { + return; + }; + for device in backend + .devices + .values_mut() + .filter(|device| device.drm.is_active()) + { + for output in device.outputs.values_mut() { + if output.power_off || output.frame_pending { + continue; + } + match output.surface.surface().clear() { + Ok(()) => output.power_off = true, + Err(error) => { + tracing::warn!(%error, output = %output.output.name(), "could not sleep locked display") + } + } + } + } +} + +pub(crate) fn wake_locked_outputs(state: &mut Ferese) { + if let Some(backend) = state.direct_backend.as_mut() { + for device in backend.devices.values_mut() { + for output in device.outputs.values_mut() { + if std::mem::take(&mut output.power_off) { + output.surface.reset_buffer_ages(); + } + } + } + } + render_all(state); +} + pub fn render_all(state: &mut Ferese) { let outputs = state .direct_backend @@ -652,6 +694,18 @@ fn lid_hides_panel(closed: bool, external_available: bool) -> bool { } fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle) { + if state.session_lock.active && state.session_lock.sleeping { + sleep_locked_outputs(state); + let asleep = state + .direct_backend + .as_ref() + .and_then(|backend| backend.devices.get(&node)) + .and_then(|device| device.outputs.get(&crtc)) + .is_none_or(|output| output.power_off || output.frame_pending); + if asleep { + return; + } + } let missed_deadlines = state .direct_backend .as_ref() @@ -1318,6 +1372,7 @@ fn create_direct_output( damage_tracker, render_metrics, frame_pending: false, + power_off: false, lock_frame_pending: false, }) } diff --git a/compositor/src/config.rs b/compositor/src/config.rs index 5a9ad18..73800e0 100644 --- a/compositor/src/config.rs +++ b/compositor/src/config.rs @@ -18,6 +18,8 @@ pub struct Config { #[serde(default)] notifications: ferese_core::notifications::NotificationConfig, #[serde(default)] + lock_screen: crate::session_lock::IdleSettings, + #[serde(default)] desktop_widgets: ferese_core::desktop::DesktopWidgets, #[serde(default)] pub(crate) autostart: Vec, @@ -292,6 +294,7 @@ impl Config { let input_settings = self.input_settings()?; let bindings = self.bindings(&input_settings)?; Ok(crate::RuntimeConfig { + lock_idle: self.lock_screen.validate()?, autostart: self.autostart.clone(), layout_mode: self.layout_mode(), gap_config: self.gap_config()?, diff --git a/compositor/src/input.rs b/compositor/src/input.rs index 14b9f89..99358a4 100644 --- a/compositor/src/input.rs +++ b/compositor/src/input.rs @@ -43,6 +43,14 @@ impl Ferese { pub fn process_input_event(&mut self, event: InputEvent) { let seat = self.seat.clone(); self.idle_notifier_state.notify_activity(&seat); + if !matches!( + &event, + InputEvent::DeviceAdded { .. } + | InputEvent::DeviceRemoved { .. } + | InputEvent::SwitchToggle { .. } + ) { + self.lock_input_activity(); + } if self.session_lock.active { // An already-bound IME or drag client may install a grab after the diff --git a/compositor/src/session_lock.rs b/compositor/src/session_lock.rs index 463530d..42e1ab1 100644 --- a/compositor/src/session_lock.rs +++ b/compositor/src/session_lock.rs @@ -1,4 +1,8 @@ use crate::Ferese; +use smithay::reexports::calloop::{ + RegistrationToken, + timer::{TimeoutAction, Timer}, +}; use smithay::{ output::Output, reexports::{ @@ -16,7 +20,72 @@ use smithay::{ LockSurface, SessionLockHandler, SessionLockManagerState, SessionLockState, SessionLocker, }, }; -use std::collections::{HashMap, HashSet}; +use std::{ + collections::{HashMap, HashSet}, + time::{Duration, Instant}, +}; + +#[derive(Clone, Copy, Debug, serde::Deserialize)] +#[serde(default)] +pub(crate) struct IdleSettings { + pub dim_after_seconds: u64, + pub sleep_after_seconds: u64, +} + +impl Default for IdleSettings { + fn default() -> Self { + Self { + dim_after_seconds: 30, + sleep_after_seconds: 120, + } + } +} + +impl IdleSettings { + pub(crate) fn validate(self) -> Result { + if self.dim_after_seconds > 86400 || self.sleep_after_seconds > 86400 { + return Err(crate::config::ConfigError::InvalidInputValue { + field: "lock_screen.idle_delay", + value: "must be between 0 and 86400 seconds".into(), + }); + } + Ok(self) + } + + fn next_poll(self, elapsed: Duration, animate: bool) -> Duration { + let dim_at = Duration::from_secs(self.dim_after_seconds); + if animate + && self.dim_after_seconds != 0 + && elapsed >= dim_at + && elapsed < dim_at + Duration::from_millis(500) + { + return Duration::from_millis(16); + } + [self.dim_after_seconds, self.sleep_after_seconds] + .into_iter() + .filter(|delay| *delay != 0) + .filter_map(|delay| Duration::from_secs(delay).checked_sub(elapsed)) + .filter(|delay| !delay.is_zero()) + .min() + .unwrap_or(Duration::from_secs(1)) + .min(Duration::from_secs(1)) + } + + fn appearance(self, elapsed: Duration) -> (f32, bool) { + let sleeping = self.sleep_after_seconds != 0 + && elapsed >= Duration::from_secs(self.sleep_after_seconds); + let dim = if self.dim_after_seconds == 0 { + 0.0 + } else { + elapsed + .saturating_sub(Duration::from_secs(self.dim_after_seconds)) + .as_secs_f32() + .min(0.5) + * 1.3 + }; + (if sleeping { 1.0 } else { dim }, sleeping) + } +} #[derive(Default)] pub(crate) struct Lock { @@ -27,9 +96,79 @@ pub(crate) struct Lock { pub backgrounds: HashMap, presented: HashSet, confirmed: bool, + idle_since: Option, + pub(crate) idle_opacity: f32, + pub(crate) sleeping: bool, + pub(crate) idle_overlays: + HashMap, + idle_timer: Option, +} + +impl Lock { + fn ready_for_idle<'a>(&self, outputs: impl Iterator) -> bool { + self.active + && outputs + .into_iter() + .all(|output| self.presented.contains(output)) + } + + pub(crate) fn output_added(&mut self, output: &Output) { + self.presented.remove(output); + } + + pub(crate) fn output_removed(&mut self, output: &Output) { + self.presented.remove(output); + self.idle_overlays.remove(output); + } + + fn activity(&mut self, now: Instant) -> bool { + if !self.active { + return false; + } + let changed = self.idle_opacity != 0.0 || self.sleeping; + self.idle_since = Some(now); + self.idle_opacity = 0.0; + self.sleeping = false; + changed + } } impl Ferese { + pub(crate) fn lock_input_activity(&mut self) { + if self.session_lock.activity(Instant::now()) { + crate::backends::direct::wake_locked_outputs(self); + self.cursor_redraw_pending = true; + } + } + + fn update_lock_idle(&mut self, now: Instant) { + if !self.session_lock.ready_for_idle(self.space.outputs()) { + return; + } + let since = self.session_lock.idle_since.get_or_insert(now); + let (mut opacity, sleeping) = self + .lock_idle + .appearance(now.saturating_duration_since(*since)); + if !self.animations_enabled() && opacity > 0.0 && !sleeping { + opacity = 0.65; + } + let changed = + opacity != self.session_lock.idle_opacity || sleeping != self.session_lock.sleeping; + let was_sleeping = self.session_lock.sleeping; + self.session_lock.idle_opacity = opacity; + self.session_lock.sleeping = sleeping; + if changed { + if was_sleeping && !sleeping { + crate::backends::direct::wake_locked_outputs(self); + } else if sleeping { + crate::backends::direct::sleep_locked_outputs(self); + } else { + crate::backends::direct::render_all(self); + } + self.cursor_redraw_pending = true; + } + } + pub(crate) fn lock_surface_under( &self, position: Point, @@ -123,6 +262,28 @@ impl SessionLockHandler for Ferese { } self.cancel_logout_confirmation(); self.session_lock.active = true; + self.session_lock.idle_since = Some(Instant::now()); + match self.loop_handle.insert_source( + Timer::from_duration(Duration::from_secs(1)), + |_, _, state| { + if !state.session_lock.active { + return TimeoutAction::Drop; + } + state.update_lock_idle(Instant::now()); + let elapsed = state + .session_lock + .idle_since + .map_or(Duration::ZERO, |since| since.elapsed()); + TimeoutAction::ToDuration( + state + .lock_idle + .next_poll(elapsed, state.animations_enabled()), + ) + }, + ) { + Ok(token) => self.session_lock.idle_timer = Some(token), + Err(error) => tracing::warn!(%error, "could not schedule lock screen inactivity"), + } self.input_capture.disable_all(); self.portal_session.set_locked(true); self.session_lock.owner = Some(confirmation.ext_session_lock().clone()); @@ -166,7 +327,14 @@ impl SessionLockHandler for Ferese { } fn unlock(&mut self) { + if let Some(timer) = self.session_lock.idle_timer.take() { + self.loop_handle.remove(timer); + } + let sleeping = self.session_lock.sleeping; self.session_lock = Lock::default(); + if sleeping { + crate::backends::direct::wake_locked_outputs(self); + } self.portal_session.set_locked(false); self.restore_keyboard_focus(); crate::backends::direct::render_all(self); @@ -223,3 +391,121 @@ impl Dispatch for Ferese { smithay::reexports::wayland_server::delegate_global_dispatch!(Ferese: [ExtSessionLockManagerV1: smithay::wayland::session_lock::SessionLockManagerGlobalData] => SessionLockManagerState); smithay::reexports::wayland_server::delegate_dispatch!(Ferese: [ExtSessionLockManagerV1: ()] => SessionLockManagerState); smithay::reexports::wayland_server::delegate_dispatch!(Ferese: [ExtSessionLockSurfaceV1: smithay::wayland::session_lock::ExtLockSurfaceUserData] => SessionLockManagerState); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn idle_policy_fades_then_sleeps_and_activity_resets_it() { + let policy = IdleSettings::default(); + assert_eq!(policy.appearance(Duration::from_secs(29)), (0.0, false)); + let halfway = policy.appearance(Duration::from_millis(30_250)); + assert!((halfway.0 - 0.325).abs() < 0.001 && !halfway.1); + assert_eq!(policy.appearance(Duration::from_secs(31)), (0.65, false)); + assert_eq!(policy.appearance(Duration::from_secs(120)), (1.0, true)); + assert_eq!(policy.appearance(Duration::ZERO), (0.0, false)); + assert_eq!( + policy.next_poll(Duration::from_millis(29_998), true), + Duration::from_millis(2) + ); + assert_eq!( + policy.next_poll(Duration::from_secs(30), true), + Duration::from_millis(16) + ); + assert_eq!( + policy.next_poll(Duration::from_secs(30), false), + Duration::from_secs(1) + ); + } + + #[test] + fn protected_fallback_frames_allow_idle_after_early_locker_crash() { + let output = Output::new( + "test".into(), + smithay::output::PhysicalProperties { + size: (0, 0).into(), + subpixel: smithay::output::Subpixel::Unknown, + make: "test".into(), + model: "test".into(), + }, + ); + let mut lock = Lock { + active: true, + ..Lock::default() + }; + assert!(!lock.ready_for_idle(std::iter::once(&output))); + lock.presented.insert(output.clone()); + assert!(lock.ready_for_idle(std::iter::once(&output))); + assert!(!lock.confirmed); + lock.output_added(&output); + assert!(!lock.ready_for_idle(std::iter::once(&output))); + assert!(!lock.confirmed); + } + + #[test] + fn input_wakes_a_sleeping_lock_without_unlocking_or_losing_confirmation() { + let mut lock = Lock { + active: true, + confirmed: true, + sleeping: true, + idle_opacity: 1.0, + ..Lock::default() + }; + let now = Instant::now(); + assert!(lock.activity(now)); + assert!(lock.active && lock.confirmed); + assert!(!lock.sleeping); + assert_eq!(lock.idle_opacity, 0.0); + assert_eq!(lock.idle_since, Some(now)); + assert!(!lock.activity(now)); + let mut unlocked = Lock::default(); + assert!(!unlocked.activity(now)); + assert!(unlocked.idle_since.is_none()); + } + + #[test] + fn idle_disabled_options_are_independent() { + assert_eq!( + IdleSettings { + dim_after_seconds: 0, + sleep_after_seconds: 0 + } + .appearance(Duration::from_secs(9999)), + (0.0, false) + ); + assert_eq!( + IdleSettings { + dim_after_seconds: 0, + sleep_after_seconds: 10 + } + .appearance(Duration::from_secs(10)), + (1.0, true) + ); + assert_eq!( + IdleSettings { + dim_after_seconds: 1, + sleep_after_seconds: 0 + } + .appearance(Duration::from_secs(9999)), + (0.65, false) + ); + assert!( + IdleSettings { + dim_after_seconds: 86401, + sleep_after_seconds: 0 + } + .validate() + .is_err() + ); + assert!( + crate::config::Config::parse_source("lock-screen { dim-after-seconds -1; }").is_err() + ); + assert!( + crate::config::Config::parse_source("lock-screen { sleep-after-seconds 90000; }") + .unwrap() + .runtime_config() + .is_err() + ); + } +} diff --git a/compositor/src/state.rs b/compositor/src/state.rs index 22f663d..60697bc 100644 --- a/compositor/src/state.rs +++ b/compositor/src/state.rs @@ -226,6 +226,7 @@ impl ClosingAnimation { pub struct Ferese { pub(crate) session_lock_state: smithay::wayland::session_lock::SessionLockManagerState, pub(crate) session_lock: crate::session_lock::Lock, + pub(crate) lock_idle: crate::session_lock::IdleSettings, pub(crate) config_source: Option, pub start_time: Instant, pub socket_name: OsString, @@ -353,6 +354,7 @@ pub struct Ferese { } pub struct RuntimeConfig { + pub(crate) lock_idle: crate::session_lock::IdleSettings, pub(crate) autostart: Vec, pub(crate) overview_font_family: String, pub(crate) wallpaper: crate::wallpaper::WallpaperConfig, @@ -447,6 +449,7 @@ impl Ferese { let mut state = Self { session_lock_state, session_lock: crate::session_lock::Lock::default(), + lock_idle: config.lock_idle, config_source: None, start_time, socket_name, @@ -609,6 +612,7 @@ impl Ferese { ); } + self.lock_idle = config.lock_idle; self.advance_animations(Instant::now()); let touchpad_changed = self.input_settings.touchpad != config.input_settings.touchpad; @@ -677,6 +681,10 @@ impl Ferese { } pub fn register_output(&mut self, output: &Output, identity: String) { + if self.session_lock.active { + self.session_lock.output_added(output); + self.lock_input_activity(); + } let Some(geometry) = self.space.output_geometry(output) else { tracing::error!(output = %output.name(), "cannot register an unmapped output"); return; @@ -738,6 +746,7 @@ impl Ferese { self.space.unmap_output(output); self.session_lock.surfaces.remove(output); self.session_lock.backgrounds.remove(output); + self.session_lock.output_removed(output); match self.output_workspaces.disconnect(output_id) { Ok(Some(target)) => { diff --git a/compositor/src/winit.rs b/compositor/src/winit.rs index cc93bbb..78958ed 100644 --- a/compositor/src/winit.rs +++ b/compositor/src/winit.rs @@ -1148,6 +1148,30 @@ fn output_elements( }); background.resize(geometry.size); let mut elements = Vec::new(); + let opacity = state.session_lock.idle_opacity; + if opacity > 0.0 { + let overlay = state + .session_lock + .idle_overlays + .entry(output.clone()) + .or_insert_with(|| { + smithay::backend::renderer::element::solid::SolidColorBuffer::new( + geometry.size, + [0.0, 0.0, 0.0, 1.0], + ) + }); + overlay.resize(geometry.size); + elements.push( + SolidColorRenderElement::from_buffer( + overlay, + (0, 0), + scale, + opacity, + RenderElementKind::Unspecified, + ) + .into(), + ); + } if let Some(surface) = state .session_lock .surfaces diff --git a/docs/backlog.md b/docs/backlog.md deleted file mode 100644 index 796eb90..0000000 --- a/docs/backlog.md +++ /dev/null @@ -1,3 +0,0 @@ -# Backlog - -- [ ] Review the lock screen against swaylock for practical feature parity: dim and sleep after inactivity, and reduce the oversized password input. Keep the implementation simple. diff --git a/docs/configuration.md b/docs/configuration.md index f08a79e..faa3c2c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -413,6 +413,11 @@ autostart { } ``` +The locked display dims after 30 seconds and sleeps after 120 seconds of +inactivity. Settings → Lock screen changes `lock-screen.dim-after-seconds` and +`lock-screen.sleep-after-seconds`; zero disables each action. Input wakes the +display without unlocking. Display sleep does not suspend the computer. + `ferese-lock` is the native PAM-authenticated locker. It follows your wallpaper, colors, font and shell corner radius. Its command returns successfully only after the compositor confirms the lock; the UI keeps running until authentication diff --git a/docs/locking.md b/docs/locking.md index 2fbe6f0..1f7e2b4 100644 --- a/docs/locking.md +++ b/docs/locking.md @@ -15,7 +15,7 @@ This is an ordinary, closable window. Submitting the field does not call PAM, lock, or unlock anything. Do not type your actual password into a visual preview. Settings → Lock screen provides clock visibility, date visibility, time format, -wallpaper dimming and softness, and a safe preview button. Your account picture is loaded from +wallpaper dimming and softness, inactivity delays, and a safe preview button. Your account picture is loaded from AccountsService or `~/.face`, with a user silhouette as a fallback. The locker reads Ferese's wallpaper, theme colors, typography, and shell radius @@ -64,3 +64,46 @@ never reports success merely because the window appeared. Validation references: [session-lock protocol](https://wayland.app/protocols/ext-session-lock-v1), [PAM authentication](https://man7.org/linux/man-pages/man3/pam_authenticate.3.html), and [PAM account checks](https://man7.org/linux/man-pages/man3/pam_acct_mgmt.3.html). + +## Inactivity and display sleep + +A locked display dims after 30 seconds without input and sleeps after 120 seconds. +Dimming fades over half a second. Any keyboard, pointer or touch activity restores +brightness and wakes the display while keeping the session locked. The compositor +owns this policy, so it remains effective if the locker crashes. Sleep starts only +after the compositor has confirmed that every display is protected. + +Settings → Lock screen adjusts both delays. Zero disables that action: + +```kdl +lock-screen { + dim-after-seconds 30 + sleep-after-seconds 120 +} +``` + +These delays are independent of automatic locking. Existing idle inhibitors from +apps cannot keep an already locked display awake. In a direct session, display +sleep uses DRM DPMS without removing outputs or rearranging workspaces; nested +previews show black instead of powering off the host display. This puts displays +to sleep, not the computer. Suspend remains a separate system power action. + +## Practical swaylock comparison + +| Behavior | Ferese Lock | +| --- | --- | +| Secure Wayland locking and protection after a locker crash | Session-lock protocol, compositor-owned fallback | +| All displays, including hotplug | Separate lock surface for each display | +| Readiness before an idle manager permits suspend | Default command waits for compositor confirmation | +| Password verification | PAM authentication and account checks on a worker | +| Password clearing and Caps Lock feedback | Escape clears; Caps Lock indicator | +| Authentication failure feedback and retry delay | Error message and bounded retry delay | +| Wallpaper, font and color customization | Shared Ferese theme and Settings | +| Inactivity and display power | Compositor dimming and DPMS; input restores brightness | + +[Swaylock](https://github.com/swaywm/swaylock/blob/master/swaylock.1.scd) provides +additional appearance flags, layout indicators and readiness-FD CLI options. +Ferese uses its shared configuration and existing startup acknowledgement instead. +Like the [swayidle example](https://github.com/swaywm/swayidle/blob/master/swayidle.1.scd), +locking and display sleep are separate from system suspend. Password changes and +interactive multi-factor prompts remain outside this password-only interface. diff --git a/scripts/tests/test_lock_isolated.py b/scripts/tests/test_lock_isolated.py index 6381598..0888251 100644 --- a/scripts/tests/test_lock_isolated.py +++ b/scripts/tests/test_lock_isolated.py @@ -1,6 +1,6 @@ """Opt-in lock protocol smoke test. Opens a temporary nested compositor. Run: FERESE_TEST_LOCK=1 python3 scripts/tests/test_lock_isolated.py -Requires release binaries and bwrap. Host PAM is never modified. +Requires built binaries and bwrap. FERESE_TEST_BIN_DIR selects debug/release. Host PAM is never modified. """ import os import unittest @@ -21,9 +21,10 @@ class NativeLockTest(unittest.TestCase): host_display=os.environ.get('WAYLAND_DISPLAY','wayland-1') if not host_display.startswith('/'):host_display=str(pathlib.Path(os.environ['XDG_RUNTIME_DIR'])/host_display) env=dict(os.environ,XDG_RUNTIME_DIR=str(runtime),XDG_CONFIG_HOME=str(root/'config'),WAYLAND_DISPLAY=host_display,FERESE_ENABLE_SCREENCOPY='1') + binary_dir = Path(os.environ.get('FERESE_TEST_BIN_DIR', 'target/release')) log=(root/'compositor.log').open('w') - compositor=subprocess.Popen(['target/release/ferese','--backend','nested'],env=env,stdout=log,stderr=subprocess.STDOUT) - ctl=pathlib.Path('target/release/feresectl') + compositor=subprocess.Popen([str(binary_dir / 'ferese'),'--backend','nested'],env=env,stdout=log,stderr=subprocess.STDOUT) + ctl=binary_dir / 'feresectl' assert ctl.is_file(), f'missing {ctl}; build it with cargo build --release -p feresectl' locker=None locker_log=(root/'locker.log').open('w') @@ -42,7 +43,7 @@ class NativeLockTest(unittest.TestCase): assert result.returncode == 0, f'Capture failed while unlocked: {result.stderr.decode()}' assert (root/'unlocked.png').stat().st_size > 0, 'Capture produced an empty file' print('Native capture succeeded while unlocked',flush=True) - locker=subprocess.Popen(['bwrap','--bind','/','/','--dev-bind','/dev','/dev','--ro-bind',str(pam),'/etc/pam.d','--unshare-user','--','target/release/ferese-lock'],env=childenv,stdout=subprocess.DEVNULL,stderr=locker_log,start_new_session=True) + locker=subprocess.Popen(['bwrap','--bind','/','/','--dev-bind','/dev','/dev','--ro-bind',str(pam),'/etc/pam.d','--unshare-user','--',str(binary_dir / 'ferese-lock')],env=childenv,stdout=subprocess.DEVNULL,stderr=locker_log,start_new_session=True) assert locker.wait(timeout=20) == 0, (root/'locker.log').read_text() print('Native locker received compositor confirmation',flush=True) result=capture(root/'locked.png') diff --git a/shell/ferese-lock/src/main.rs b/shell/ferese-lock/src/main.rs index 2ec00c4..3fba417 100644 --- a/shell/ferese-lock/src/main.rs +++ b/shell/ferese-lock/src/main.rs @@ -351,7 +351,7 @@ impl Locker { } fn screen_content(&self, size: iced::Size) -> Element<'_, Message> { - let avatar_size = (size.height * 0.09).clamp(64., 112.); + let avatar_size = (size.height * 0.08).clamp(56., 88.); let clock_size = (size.height * 0.165) .min( size.width @@ -367,7 +367,7 @@ impl Locker { let a = &self.appearance; let mut input = widget::text_input("Password", self.password.as_str()) .password() - .padding([8, 18]) + .padding([6, 12]) .style(ferese_theme::controls::lock_input( a.radius, a.accent, a.panel, )) @@ -435,8 +435,8 @@ impl Locker { .spacing(12) .align_x(iced::Alignment::Center) .width( - (size.height * 0.37) - .clamp(260., 420.) + (size.height * 0.31) + .clamp(240., 320.) .min((size.width - margin * 2.).max(160.)), ); let mut clock = column([]).spacing(0).align_x(iced::Alignment::Center); diff --git a/shell/ferese-settings/src/schema.rs b/shell/ferese-settings/src/schema.rs index b7605e7..01c2774 100644 --- a/shell/ferese-settings/src/schema.rs +++ b/shell/ferese-settings/src/schema.rs @@ -550,6 +550,28 @@ pub fn note_fields(index: usize) -> Vec { pub fn fields(page: Page) -> Vec { match page { Page::LockScreen => vec![ + range( + "lock_screen.dim_after_seconds", + "Dim after", + "Dim the locked display after inactivity. Zero disables dimming.", + 30.0, + 0.0, + 600.0, + 5.0, + "s", + true, + ), + range( + "lock_screen.sleep_after_seconds", + "Display sleep after", + "Turn off locked displays after inactivity. Input wakes them; zero disables display sleep.", + 120.0, + 0.0, + 1800.0, + 15.0, + "s", + true, + ), toggle( "lock_screen.show_clock", "Show clock",