fix: keyboard focus on built-in display to avoid evdi phantom grab (v0.8.4)
All checks were successful
Update PKGBUILD version / update-pkgver (push) Successful in 3s

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.
This commit is contained in:
nevaforget 2026-04-23 11:02:24 +02:00
parent 48d363bb18
commit 97165d94f8
5 changed files with 94 additions and 9 deletions

View File

@ -45,7 +45,7 @@ cd pkg && makepkg -sf && sudo pacman -U moongreet-git-<version>-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 0200)
- `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

2
Cargo.lock generated
View File

@ -575,7 +575,7 @@ dependencies = [
[[package]]
name = "moongreet"
version = "0.8.3"
version = "0.8.4"
dependencies = [
"gdk-pixbuf",
"gdk4",

View File

@ -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"

View File

@ -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

View File

@ -40,6 +40,26 @@ fn setup_layer_shell(window: &gtk::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<Item = Option<&'a str>>,
{
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: &gtk::Application) {
let display = match gdk::Display::default() {
Some(d) => d,
@ -66,20 +86,37 @@ fn activate(app: &gtk::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<Option<String>> = (0..count)
.map(|i| {
monitors
.item(i)
.and_then(|obj| obj.downcast::<gdk::Monitor>().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::<gdk::Monitor>().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::<Option<&str>>()), 0);
}
}
fn main() {
setup_logging();
log::info!("Moongreet starting");