From c91c3a779653b20e4d3bd9bfc5c924088742c387 Mon Sep 17 00:00:00 2001 From: Fimeg Date: Fri, 17 Jul 2026 10:48:06 -0400 Subject: [PATCH] sessiond: quiet fallback until first input, dismissnotify on handoff fallback surface renders only the dark field until touched, so a normal boot no longer flashes a second PIN style under the shell lockscreen. hyprctl dismissnotify fires once the shell confirms its lock, clearing the bogus lockscreen-crashed banner from the handoff connection drop. --- src/sessiond/draw.rs | 22 +++++++++++++++++++++- src/sessiond/lock.rs | 33 ++++++++++++++++++++++++++++++++- src/sessiond/server.rs | 4 ++++ 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/sessiond/draw.rs b/src/sessiond/draw.rs index 05adc3f..0244c8b 100644 --- a/src/sessiond/draw.rs +++ b/src/sessiond/draw.rs @@ -76,6 +76,10 @@ pub struct Scene { pub pin_len: usize, pub mood: Mood, pub pressed: Option, + /// Boot-handoff face: render nothing but the dark field. The keypad + /// appears on first input, so a normal boot (shell takes over in + /// seconds) never flashes a second PIN style at the user. + pub quiet: bool, } /// The keypad geometry for a surface of w x h. Pure function of size so @@ -186,6 +190,9 @@ fn draw_glyph(buf: &mut [u32], w: i32, h: i32, key: Key, cell: Rect, color: u32) pub fn render(buf: &mut [u32], w: i32, h: i32, scene: &Scene) { debug_assert_eq!(buf.len(), (w * h) as usize); buf.fill(BG); + if scene.quiet { + return; + } // PIN dots, centered in the upper region above the keypad. let dot_color = match scene.mood { @@ -244,10 +251,23 @@ mod tests { &mut buf, w, h, - &Scene { pin_len: 3, mood: Mood::Entering, pressed: Some(Key::Digit(5)) }, + &Scene { pin_len: 3, mood: Mood::Entering, pressed: Some(Key::Digit(5)), quiet: false }, ); assert!(buf.iter().any(|&p| p == BG)); assert!(buf.iter().any(|&p| p == KEY_FILL)); assert!(buf.iter().any(|&p| p == KEY_PRESSED)); } + + #[test] + fn quiet_scene_is_bare_field() { + let (w, h) = (400, 800); + let mut buf = vec![0u32; (w * h) as usize]; + render( + &mut buf, + w, + h, + &Scene { pin_len: 0, mood: Mood::Entering, pressed: None, quiet: true }, + ); + assert!(buf.iter().all(|&p| p == BG)); + } } diff --git a/src/sessiond/lock.rs b/src/sessiond/lock.rs index da4f3a9..9da913a 100644 --- a/src/sessiond/lock.rs +++ b/src/sessiond/lock.rs @@ -137,6 +137,11 @@ struct LockState { pointer_pos: (f64, f64), pointer_surface: Option, // protocol id of the entered wl_surface splash_poked: bool, + /// True until the user touches the surface. While quiet we render only + /// the dark field, so the boot handoff to the shell never flashes the + /// fallback keypad. The first input reveals the keypad (and is + /// swallowed — it's a "show me" gesture, not a digit). + quiet: bool, } impl LockState { @@ -160,9 +165,21 @@ impl LockState { pointer_pos: (0.0, 0.0), pointer_surface: None, splash_poked: false, + quiet: true, } } + /// First input while quiet reveals the keypad instead of registering. + /// Returns true when the input was consumed by the reveal. + fn reveal(&mut self) -> bool { + if self.quiet { + self.quiet = false; + self.dirty = true; + return true; + } + false + } + fn press(&mut self, key: Key) { if self.authing { return; @@ -410,7 +427,12 @@ fn ensure_surfaces(state: &mut LockState, qh: &QueueHandle) { } fn redraw_all(state: &mut LockState, qh: &QueueHandle) -> Result<()> { - let scene = Scene { pin_len: state.pin.len(), mood: state.mood, pressed: state.pressed }; + let scene = Scene { + pin_len: state.pin.len(), + mood: state.mood, + pressed: state.pressed, + quiet: state.quiet, + }; let Some(shm) = state.shm.clone() else { return Ok(()) }; let mut drew = false; for ctx in &mut state.surfaces { @@ -603,6 +625,9 @@ impl Dispatch for LockState { ) { match event { wl_touch::Event::Down { surface, x, y, .. } => { + if state.reveal() { + return; + } let pid = surface.id().protocol_id(); if let Some((w, h)) = state.surface_size_by_proto_id(pid) { if let Some(key) = hit_test(w, h, x, y) { @@ -642,6 +667,9 @@ impl Dispatch for LockState { } wl_pointer::Event::Button { state: WEnum::Value(st), .. } => match st { wl_pointer::ButtonState::Pressed => { + if state.reveal() { + return; + } if let Some(pid) = state.pointer_surface { if let Some((w, h)) = state.surface_size_by_proto_id(pid) { let (px, py) = state.pointer_pos; @@ -676,6 +704,9 @@ impl Dispatch for LockState { wl_keyboard::Event::Key { key, state: WEnum::Value(st), .. } => { match st { wl_keyboard::KeyState::Pressed => { + if state.reveal() { + return; + } if let Some(k) = LockState::key_from_evdev(key) { state.press(k); } diff --git a/src/sessiond/server.rs b/src/sessiond/server.rs index 380e41e..af24673 100644 --- a/src/sessiond/server.rs +++ b/src/sessiond/server.rs @@ -294,6 +294,10 @@ fn handle_request( } d.phase = Phase::Released; info!("shell lock confirmed"); + // The handoff is a deliberate connection drop, but Hyprland + // announces any dying lock client as a crashed lockscreen. + // The shell's lock is confirmed, so clear that banner. + let _ = std::process::Command::new("hyprctl").arg("dismissnotify").spawn(); serde_json::json!({ "ok": true }) } Request::Lock => {