gardesk/gardm / 20fb5ee

Browse files

Sync X11 keyboard lock state with kernel via evdev ioctl

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
20fb5eed452b832637be7bd430f9380ae1cd98a0
Parents
7b0230a
Tree
6af9cd9

4 changed files

StatusFile+-
M Cargo.lock 1 0
M Cargo.toml 2 1
M gardm-greeter/Cargo.toml 1 0
M gardm-greeter/src/window.rs 111 0
Cargo.lockmodified
@@ -731,6 +731,7 @@ dependencies = [
731731
  "cairo-rs",
732732
  "gardm-ipc",
733733
  "image",
734
+ "libc",
734735
  "nix 0.27.1",
735736
  "pango",
736737
  "pangocairo",
Cargo.tomlmodified
@@ -28,9 +28,10 @@ thiserror = "1.0"
2828
 # Unix/Linux
2929
 nix = { version = "0.27", features = ["user", "process", "signal"] }
3030
 pam-client = "0.5"
31
+libc = "0.2"
3132
 
3233
 # X11
33
-x11rb = { version = "0.13", features = ["allow-unsafe-code", "randr"] }
34
+x11rb = { version = "0.13", features = ["allow-unsafe-code", "randr", "xkb"] }
3435
 
3536
 # Graphics
3637
 cairo-rs = { version = "0.18", features = ["png"] }
gardm-greeter/Cargo.tomlmodified
@@ -33,3 +33,4 @@ cairo-rs = { workspace = true }
3333
 pango = { workspace = true }
3434
 pangocairo = { workspace = true }
3535
 image = { workspace = true }
36
+libc = { workspace = true }
gardm-greeter/src/window.rsmodified
@@ -4,6 +4,7 @@
44
 
55
 use anyhow::{Context, Result};
66
 use x11rb::connection::Connection;
7
+use x11rb::protocol::xkb::{self, ConnectionExt as XkbConnectionExt};
78
 use x11rb::protocol::xproto::*;
89
 use x11rb::rust_connection::RustConnection;
910
 use x11rb::wrapper::ConnectionExt as _;
@@ -179,6 +180,10 @@ impl GreeterWindow {
179180
             .context("Failed to set input focus")?;
180181
         conn.flush()?;
181182
 
183
+        // Sync X11 keyboard lock state with kernel state
184
+        // X11 doesn't query kernel state at startup, so we must do it manually
185
+        sync_keyboard_locks(&conn);
186
+
182187
         tracing::info!(width, height, "Created greeter window");
183188
 
184189
         Ok(Self {
@@ -310,3 +315,109 @@ impl Drop for GreeterWindow {
310315
         let _ = self.conn.flush();
311316
     }
312317
 }
318
+
319
+/// Sync X11 keyboard lock state with kernel state
320
+///
321
+/// X11/Xorg doesn't query the kernel's keyboard lock state at startup - it only
322
+/// tracks state changes from key events. When returning from a Wayland session,
323
+/// X11 starts fresh and doesn't know that caps/num lock was already active.
324
+///
325
+/// This function queries the kernel's lock state via evdev and sets X11's XKB
326
+/// state to match, ensuring the greeter's keyboard behavior matches the
327
+/// physical keyboard state.
328
+fn sync_keyboard_locks(conn: &RustConnection) {
329
+
330
+    // Initialize XKB extension
331
+    if let Err(e) = conn.xkb_use_extension(1, 0) {
332
+        tracing::warn!("Failed to initialize XKB: {}", e);
333
+        return;
334
+    }
335
+
336
+    // Find a keyboard device and query its lock state
337
+    let (caps_on, num_on) = match query_kernel_lock_state() {
338
+        Some(state) => state,
339
+        None => {
340
+            tracing::debug!("Could not query kernel keyboard state");
341
+            return;
342
+        }
343
+    };
344
+
345
+    tracing::info!("Kernel keyboard state: caps_lock={}, num_lock={}", caps_on, num_on);
346
+
347
+    // Build modifier mask to match kernel state
348
+    let mut mod_locks = ModMask::from(0u16);
349
+    if caps_on {
350
+        mod_locks |= ModMask::LOCK;
351
+    }
352
+    if num_on {
353
+        mod_locks |= ModMask::M2;
354
+    }
355
+
356
+    // Set X11 XKB state to match kernel
357
+    let affect_locks = ModMask::LOCK | ModMask::M2;
358
+    match conn.xkb_latch_lock_state(
359
+        xkb::ID::USE_CORE_KBD.into(),
360
+        affect_locks,
361
+        mod_locks,
362
+        false,
363
+        xkb::Group::M1,
364
+        ModMask::from(0u16),
365
+        false,
366
+        0u16,
367
+    ) {
368
+        Ok(_) => {
369
+            let _ = conn.flush();
370
+            tracing::debug!("Synced X11 lock state with kernel");
371
+        }
372
+        Err(e) => {
373
+            tracing::warn!("Failed to sync keyboard locks: {}", e);
374
+        }
375
+    }
376
+}
377
+
378
+/// Query kernel keyboard lock state via evdev EVIOCGLED ioctl
379
+fn query_kernel_lock_state() -> Option<(bool, bool)> {
380
+    use std::fs::{self, File};
381
+    use std::os::unix::io::AsRawFd;
382
+
383
+    // evdev LED indices
384
+    const LED_NUML: u8 = 0;
385
+    const LED_CAPSL: u8 = 1;
386
+
387
+    // EVIOCGLED ioctl - read LED state bitmap
388
+    // _IOR('E', 0x19, len) where len is LED_MAX/8+1 bytes
389
+    // For typical keyboards, we just need 1 byte
390
+    const EVIOCGLED_1: libc::c_ulong = 0x80014519;
391
+
392
+    // Find keyboard devices
393
+    let input_dir = fs::read_dir("/dev/input").ok()?;
394
+
395
+    for entry in input_dir.flatten() {
396
+        let path = entry.path();
397
+        let name = path.file_name()?.to_str()?;
398
+
399
+        // Only check event devices
400
+        if !name.starts_with("event") {
401
+            continue;
402
+        }
403
+
404
+        // Try to open and query
405
+        if let Ok(file) = File::open(&path) {
406
+            let mut leds: u8 = 0;
407
+            let fd = file.as_raw_fd();
408
+
409
+            // Query LED state
410
+            let ret = unsafe {
411
+                libc::ioctl(fd, EVIOCGLED_1, &mut leds as *mut u8)
412
+            };
413
+
414
+            if ret >= 0 {
415
+                let caps_on = (leds & (1 << LED_CAPSL)) != 0;
416
+                let num_on = (leds & (1 << LED_NUML)) != 0;
417
+                return Some((caps_on, num_on));
418
+            }
419
+        }
420
+    }
421
+
422
+    None
423
+}