From 97165d94f8347fc3881cf5d094254508faf085b2 Mon Sep 17 00:00:00 2001 From: nevaforget Date: Thu, 23 Apr 2026 11:02:24 +0200 Subject: [PATCH] fix: keyboard focus on built-in display to avoid evdi phantom grab (v0.8.4) DisplayLink/evdi virtual displays enumerate as DVI-I-* before eDP-1 and were stealing the KeyboardMode::Exclusive grab on the first enumerated monitor, leaving the visible greeter surfaces without keyboard input. Introduce pick_primary_monitor_index() that prefers eDP/LVDS/DSI connectors for the keyboard grab and falls back to index 0 when no built-in panel is present. Pure, unit-tested; hotplug path unchanged. --- CLAUDE.md | 2 +- Cargo.lock | 2 +- Cargo.toml | 2 +- DECISIONS.md | 7 ++++ src/main.rs | 90 ++++++++++++++++++++++++++++++++++++++++++++++++---- 5 files changed, 94 insertions(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b693b5d..9bebad4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -45,7 +45,7 @@ cd pkg && makepkg -sf && sudo pacman -U moongreet-git--x86_64.pkg.tar.z - `fingerprint.rs` — fprintd D-Bus Probe (gio::DBusProxy) — Geräteerkennung und Enrollment-Check für UI-Feedback - `config.rs` — TOML-Config ([appearance] background, gtk-theme, fingerprint-enabled) + Wallpaper-Fallback + Blur-Validierung (finite, clamp 0–200) - `greeter.rs` — GTK4 UI (Overlay-Layout), Login-Flow via greetd IPC (Multi-Stage-Auth für fprintd), Faillock-Warnung, Avatar-Cache, Last-User/Last-Session Persistence (0o700 Dirs, 0o600 Files) -- `main.rs` — Entry Point, GTK App, Layer Shell Setup, Multi-Monitor mit Hotplug via `items-changed` auf Monitor-ListModel (one greeter window per monitor, first gets keyboard), systemd-journal-logger +- `main.rs` — Entry Point, GTK App, Layer Shell Setup, Multi-Monitor mit Hotplug via `items-changed` auf Monitor-ListModel (one greeter window per monitor; keyboard goes to the built-in display via `pick_primary_monitor_index`, falls back to first), systemd-journal-logger - `resources/style.css` — Catppuccin-inspiriertes Theme ## Design Decisions diff --git a/Cargo.lock b/Cargo.lock index c5445b8..586ac30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -575,7 +575,7 @@ dependencies = [ [[package]] name = "moongreet" -version = "0.8.3" +version = "0.8.4" dependencies = [ "gdk-pixbuf", "gdk4", diff --git a/Cargo.toml b/Cargo.toml index b5e5548..738c21b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "moongreet" -version = "0.8.3" +version = "0.8.4" edition = "2024" description = "A greetd greeter for Wayland with GTK4 and Layer Shell" license = "MIT" diff --git a/DECISIONS.md b/DECISIONS.md index 94e9f8e..6176764 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1,5 +1,12 @@ # Decisions +## 2026-04-23 – Keyboard focus on built-in display, not first enumerated monitor (v0.8.4) + +- **Who**: ClaudeCode, Dom +- **Why**: With a DisplayLink dock attached, the greeter showed its UI on all monitors but the password entry accepted no input. `display.monitors()` enumerated evdi phantom connectors (`DVI-I-*`) before the laptop panel (`eDP-1`); the v0.8.0 logic gave `KeyboardMode::Exclusive` to index 0, so the keyboard grab landed on an invisible surface. Symptom showed up on 2026-04-23 after kernel 6.19.11 → 6.19.12 + moongreet 0.8.0 → 0.8.2 changed evdi enumeration timing — previous Thursdays with the same dock worked. +- **Tradeoffs**: Prefers built-in displays by connector-name pattern (`eDP*`/`LVDS*`/`DSI*`) rather than a generic "primary monitor" concept — Wayland has no portable primary signal, and gdk4's `primary_monitor()` was removed. Pattern-matching covers every current Linux laptop, at the cost of a tiny list to maintain if a new form factor ships a new connector type. Fallback is still index 0, so behavior on desktops without a built-in panel is unchanged. +- **How**: New pure function `pick_primary_monitor_index()` in `main.rs` scans connector names and returns the built-in index (or 0). Used during initial enumeration to decide which window gets `KeyboardMode::Exclusive`. Hotplug branch unchanged — new monitors still get keyboard=false so focus never migrates off the panel. Unit-tested against evdi/eDP/LVDS/DSI/HDMI/DP mixes. + ## 2026-04-21 – Ship polkit rule in moongreet instead of moonarch (v0.8.3) - **Who**: ClaudeCode, Dom diff --git a/src/main.rs b/src/main.rs index d225afd..a59ddba 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,6 +40,26 @@ fn setup_layer_shell(window: >k::ApplicationWindow, keyboard: bool, layer: gtk window.set_anchor(gtk4_layer_shell::Edge::Right, true); } +/// Pick the index of the built-in display among the given connector names. +/// +/// Prefers `eDP*` / `LVDS*` / `DSI*` over anything else — otherwise keyboard +/// focus can land on DisplayLink/evdi phantom displays (connector `DVI-I-*`) +/// that are enumerated before the laptop panel. Falls back to index 0 when +/// no built-in connector is present. +fn pick_primary_monitor_index<'a, I>(connectors: I) -> usize +where + I: IntoIterator>, +{ + for (i, conn) in connectors.into_iter().enumerate() { + if let Some(c) = conn + && (c.starts_with("eDP") || c.starts_with("LVDS") || c.starts_with("DSI")) + { + return i; + } + } + 0 +} + fn activate(app: >k::Application) { let display = match gdk::Display::default() { Some(d) => d, @@ -66,20 +86,37 @@ fn activate(app: >k::Application) { log::debug!("Layer shell: {use_layer_shell}"); if use_layer_shell { - // One greeter window per monitor — only the first gets keyboard input + // One greeter window per monitor — keyboard focus goes to the + // built-in display so DisplayLink/evdi phantoms don't steal it. let monitors = display.monitors(); - log::debug!("Monitor count: {}", monitors.n_items()); - let mut first = true; - for i in 0..monitors.n_items() { + let count = monitors.n_items(); + log::debug!("Monitor count: {count}"); + + let connectors: Vec> = (0..count) + .map(|i| { + monitors + .item(i) + .and_then(|obj| obj.downcast::().ok()) + .and_then(|m| m.connector()) + .map(|gs| gs.to_string()) + }) + .collect(); + + let primary_idx = pick_primary_monitor_index(connectors.iter().map(|o| o.as_deref())); + log::debug!( + "Primary monitor: idx={primary_idx} connector={:?}", + connectors.get(primary_idx).and_then(|o| o.as_deref()) + ); + + for i in 0..count { if let Some(monitor) = monitors .item(i) .and_then(|obj| obj.downcast::().ok()) { let window = greeter::create_greeter_window(bg_texture.as_ref(), &config, &blur_cache, app); - setup_layer_shell(&window, first, gtk4_layer_shell::Layer::Top); + setup_layer_shell(&window, i as usize == primary_idx, gtk4_layer_shell::Layer::Top); window.set_monitor(Some(&monitor)); window.present(); - first = false; } } @@ -135,6 +172,47 @@ fn setup_logging() { log::set_max_level(level); } +#[cfg(test)] +mod tests { + use super::pick_primary_monitor_index; + + #[test] + fn prefers_edp_over_phantom_dvi() { + assert_eq!( + pick_primary_monitor_index([Some("DVI-I-1"), Some("eDP-1"), Some("DVI-I-2")]), + 1, + ); + } + + #[test] + fn prefers_lvds() { + assert_eq!(pick_primary_monitor_index([Some("HDMI-A-1"), Some("LVDS-1")]), 1); + } + + #[test] + fn prefers_dsi() { + assert_eq!(pick_primary_monitor_index([Some("DP-1"), Some("DSI-1")]), 1); + } + + #[test] + fn falls_back_to_first_without_builtin() { + assert_eq!( + pick_primary_monitor_index([Some("DVI-I-1"), Some("HDMI-A-1")]), + 0, + ); + } + + #[test] + fn skips_missing_connector() { + assert_eq!(pick_primary_monitor_index([None, Some("eDP-1")]), 1); + } + + #[test] + fn empty_returns_zero() { + assert_eq!(pick_primary_monitor_index(std::iter::empty::>()), 0); + } +} + fn main() { setup_logging(); log::info!("Moongreet starting");