git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commitcdaa5239ccf4853385c854b054d686ab1f70a25f
parent084deb4d8a
authorLucas Galante <[email protected]>
date2026-07-28 14:34
fix: autostarts can't steal focus from the restored session before input

Logging back in with (e.g.) claude focused ended with keepassxc focused
and centered: the map-time focus gate only blocked RESTORED windows, so
an autostarted window mapping late (keepassxc's main window, which
wasn't in the saved session because it lives in the tray) took focus
and dragged the focus-follow pan over to itself. The gate now also
blocks unrestored windows that map after the restored-focused window
while the session is still settling — "settling" ends at the first
deliberate input (key or pointer-button press, wm.startup_input_seen),
so anything the user actually spawns focuses normally.

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/server/cursor.rs         |  6 ++++++
 src/server/keyboard.rs       |  5 +++++
 src/server/window.rs         | 28 +++++++++++++++++++---------
 src/server/window_manager.rs |  6 ++++++
 4 files changed, 36 insertions(+), 9 deletions(-)

diff --git a/src/server/cursor.rs b/src/server/cursor.rs
index 316a60c..ab57ada 100644
--- a/src/server/cursor.rs
+++ b/src/server/cursor.rs
@@ -756,6 +756,12 @@ unsafe extern "C" fn handle_button(listener: *mut ffi::wl_listener, data: *mut s
     let ly = cursor.y();
     let server = seat.server;
 
+    // First deliberate input ends the session-restore settling phase (see
+    // the focus gate in Window::map).
+    if (*event).state == ffi::wl_pointer_button_state_WL_POINTER_BUTTON_STATE_PRESSED {
+        (*server).wm.startup_input_seen = true;
+    }
+
     let mut is_app_surface = false;
     let mut is_overlay_window = false;
     if let Some(result) = (*server).scene.at(lx, ly) {
diff --git a/src/server/keyboard.rs b/src/server/keyboard.rs
index 025132c..55ec155 100644
--- a/src/server/keyboard.rs
+++ b/src/server/keyboard.rs
@@ -234,6 +234,11 @@ unsafe extern "C" fn handle_key(listener: *mut ffi::wl_listener, data: *mut std:
         if keyboard.pressed.len() < 32 {
             keyboard.pressed.insert((*event).keycode);
         }
+        // First deliberate input ends the session-restore settling phase
+        // (see the focus gate in Window::map).
+        if !keyboard.group.is_null() && !(*keyboard.group).seat.is_null() {
+            (*(*(*keyboard.group).seat).server).wm.startup_input_seen = true;
+        }
     }
 
     if !keyboard.group.is_null() {
diff --git a/src/server/window.rs b/src/server/window.rs
index 3b6f31e..175193b 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -1019,15 +1019,25 @@ impl Window {
             }
         } else {
             let mut should_focus = true;
-            if self.restored {
-                if self.restored_focused {
-                    (*self.server).wm.restored_focused_window_mapped = true;
-                    log::info!("[FocusRestore] Restored focused window mapped: {:?}", self.get_title());
-                } else {
-                    if (*self.server).wm.has_restored_focused_window && (*self.server).wm.restored_focused_window_mapped {
-                        log::info!("[FocusRestore] Blocking focus to non-focused restored window {:?} because restored focused window is already mapped", self.get_title());
-                        should_focus = false;
-                    }
+            if self.restored && self.restored_focused {
+                (*self.server).wm.restored_focused_window_mapped = true;
+                log::info!("[FocusRestore] Restored focused window mapped: {:?}", self.get_title());
+            } else if (*self.server).wm.has_restored_focused_window
+                && (*self.server).wm.restored_focused_window_mapped
+            {
+                if self.restored {
+                    // A restored sibling mapping after the session's focused
+                    // window: never steal back.
+                    log::info!("[FocusRestore] Blocking focus to non-focused restored window {:?} because restored focused window is already mapped", self.get_title());
+                    should_focus = false;
+                } else if !(*self.server).wm.startup_input_seen {
+                    // A window mapping unbidden while the session is still
+                    // settling (no key/button pressed yet) — an autostart
+                    // like keepassxc popping up after the restored windows.
+                    // It must not steal focus (or drag the focus-follow pan
+                    // over to itself) from the session's focused window.
+                    log::info!("[FocusRestore] Blocking focus steal by unrestored window {:?} mapping before first input", self.get_title());
+                    should_focus = false;
                 }
             }
 
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index 664f38a..3ab35d8 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -124,6 +124,11 @@ pub struct WindowManager {
     pub edge_pan_timer: *mut ffi::wl_event_source,
     pub has_restored_focused_window: bool,
     pub restored_focused_window_mapped: bool,
+    /// True after the first deliberate input (key or button press) of the
+    /// session. Until then the session is still "settling" from restore:
+    /// windows that map unbidden (autostarts like keepassxc) must not steal
+    /// focus from the restored session's focused window.
+    pub startup_input_seen: bool,
     pub last_viewport_zoom: f64,
     pub last_viewport_pan_x: f64,
     pub last_viewport_pan_y: f64,
@@ -208,6 +213,7 @@ impl WindowManager {
         self.display = std::collections::HashMap::new();
         self.has_restored_focused_window = false;
         self.restored_focused_window_mapped = false;
+        self.startup_input_seen = false;
         self.mode_rules = Vec::new();
         self.keybinds = Vec::new();
         self.pointer_binds = Vec::new();