Rust · 14836 bytes Raw Blame History
1 //! X11 window management for the greeter
2 //!
3 //! Creates a fullscreen window for displaying the login interface.
4
5 use anyhow::{Context, Result};
6 use x11rb::connection::Connection;
7 use x11rb::protocol::xkb::{self, ConnectionExt as XkbConnectionExt};
8 use x11rb::protocol::xproto::*;
9 use x11rb::rust_connection::RustConnection;
10 use x11rb::wrapper::ConnectionExt as _;
11
12 /// Standard X11 cursor font glyphs
13 mod cursor_glyphs {
14 pub const XC_LEFT_PTR: u16 = 68; // Default arrow cursor
15 pub const XC_XTERM: u16 = 152; // I-beam text cursor
16 pub const XC_HAND2: u16 = 60; // Pointing hand cursor
17 }
18
19 /// Cursor types available for the greeter
20 #[derive(Clone, Copy, PartialEq, Eq, Debug)]
21 pub enum CursorType {
22 Default,
23 Text,
24 Pointer,
25 }
26
27 /// Greeter window wrapping X11 connection and window handle
28 pub struct GreeterWindow {
29 conn: RustConnection,
30 screen_num: usize,
31 window: Window,
32 gc: Gcontext,
33 width: u16,
34 height: u16,
35 depth: u8,
36 // Cursors
37 cursor_default: Cursor,
38 cursor_text: Cursor,
39 cursor_pointer: Cursor,
40 current_cursor: CursorType,
41 }
42
43 impl GreeterWindow {
44 /// Create a new fullscreen greeter window
45 pub fn new() -> Result<Self> {
46 let (conn, screen_num) = x11rb::connect(None).context("Failed to connect to X server")?;
47
48 let screen = &conn.setup().roots[screen_num];
49 let width = screen.width_in_pixels;
50 let height = screen.height_in_pixels;
51 let root = screen.root;
52 let depth = screen.root_depth;
53 let visual = screen.root_visual;
54
55 // Clear the root window to black to hide any leftover content from previous session
56 // garbg sets the root background using a pixmap AND stores the pixmap ID in
57 // _XROOTPMAP_ID and ESETROOT_PMAP_ID atoms. We must clear all of these.
58
59 // First, clear the root pixmap atoms that garbg sets
60 // This prevents compositors from reading stale pixmap references
61 let xrootpmap_atom = conn
62 .intern_atom(false, b"_XROOTPMAP_ID")?
63 .reply()
64 .map(|r| r.atom)
65 .unwrap_or(x11rb::NONE);
66 let esetroot_atom = conn
67 .intern_atom(false, b"ESETROOT_PMAP_ID")?
68 .reply()
69 .map(|r| r.atom)
70 .unwrap_or(x11rb::NONE);
71
72 // Try to free the old pixmap if it exists (to avoid memory leak)
73 if xrootpmap_atom != x11rb::NONE {
74 if let Ok(reply) = conn.get_property(false, root, xrootpmap_atom, AtomEnum::PIXMAP, 0, 1)?.reply() {
75 if reply.format == 32 && !reply.value.is_empty() {
76 let pixmap_id = u32::from_ne_bytes([
77 reply.value[0], reply.value[1], reply.value[2], reply.value[3]
78 ]);
79 if pixmap_id != 0 {
80 let _ = conn.free_pixmap(pixmap_id);
81 }
82 }
83 }
84 // Delete the property
85 let _ = conn.delete_property(root, xrootpmap_atom);
86 }
87 if esetroot_atom != x11rb::NONE {
88 let _ = conn.delete_property(root, esetroot_atom);
89 }
90
91 // Now clear the window's background pixmap attribute and set solid color
92 conn.change_window_attributes(
93 root,
94 &ChangeWindowAttributesAux::new()
95 .background_pixmap(x11rb::NONE) // Remove any pixmap (e.g., from garbg)
96 .background_pixel(screen.black_pixel),
97 )?;
98 conn.clear_area(false, root, 0, 0, width, height)?;
99 conn.flush()?;
100
101 // Create window
102 let window = conn.generate_id().context("Failed to generate window ID")?;
103 conn.create_window(
104 depth,
105 window,
106 root,
107 0,
108 0,
109 width,
110 height,
111 0,
112 WindowClass::INPUT_OUTPUT,
113 visual,
114 &CreateWindowAux::new()
115 .background_pixel(screen.black_pixel)
116 .event_mask(
117 EventMask::EXPOSURE
118 | EventMask::KEY_PRESS
119 | EventMask::KEY_RELEASE
120 | EventMask::BUTTON_PRESS
121 | EventMask::BUTTON_RELEASE
122 | EventMask::POINTER_MOTION
123 | EventMask::STRUCTURE_NOTIFY
124 | EventMask::FOCUS_CHANGE,
125 ),
126 )
127 .context("Failed to create window")?;
128
129 // Set fullscreen hint
130 let net_wm_state = conn
131 .intern_atom(false, b"_NET_WM_STATE")
132 .context("Failed to intern _NET_WM_STATE")?
133 .reply()
134 .context("Failed to get _NET_WM_STATE reply")?
135 .atom;
136
137 let fullscreen = conn
138 .intern_atom(false, b"_NET_WM_STATE_FULLSCREEN")
139 .context("Failed to intern fullscreen atom")?
140 .reply()
141 .context("Failed to get fullscreen atom reply")?
142 .atom;
143
144 conn.change_property32(PropMode::REPLACE, window, net_wm_state, AtomEnum::ATOM, &[
145 fullscreen,
146 ])
147 .context("Failed to set fullscreen property")?;
148
149 // Override redirect - no window manager decorations
150 conn.change_window_attributes(
151 window,
152 &ChangeWindowAttributesAux::new().override_redirect(1),
153 )
154 .context("Failed to set override redirect")?;
155
156 // Create graphics context for image rendering
157 let gc = conn.generate_id().context("Failed to generate GC ID")?;
158 conn.create_gc(gc, window, &CreateGCAux::new())
159 .context("Failed to create GC")?;
160
161 // Load cursor font and create cursors
162 let cursor_font = conn.generate_id().context("Failed to generate font ID")?;
163 conn.open_font(cursor_font, b"cursor")
164 .context("Failed to open cursor font")?;
165
166 let cursor_default = conn.generate_id().context("Failed to generate cursor ID")?;
167 conn.create_glyph_cursor(
168 cursor_default,
169 cursor_font,
170 cursor_font,
171 cursor_glyphs::XC_LEFT_PTR,
172 cursor_glyphs::XC_LEFT_PTR + 1,
173 0xFFFF, 0xFFFF, 0xFFFF, // White foreground
174 0, 0, 0, // Black background
175 )
176 .context("Failed to create default cursor")?;
177
178 let cursor_text = conn.generate_id().context("Failed to generate cursor ID")?;
179 conn.create_glyph_cursor(
180 cursor_text,
181 cursor_font,
182 cursor_font,
183 cursor_glyphs::XC_XTERM,
184 cursor_glyphs::XC_XTERM + 1,
185 0xFFFF, 0xFFFF, 0xFFFF,
186 0, 0, 0,
187 )
188 .context("Failed to create text cursor")?;
189
190 let cursor_pointer = conn.generate_id().context("Failed to generate cursor ID")?;
191 conn.create_glyph_cursor(
192 cursor_pointer,
193 cursor_font,
194 cursor_font,
195 cursor_glyphs::XC_HAND2,
196 cursor_glyphs::XC_HAND2 + 1,
197 0xFFFF, 0xFFFF, 0xFFFF,
198 0, 0, 0,
199 )
200 .context("Failed to create pointer cursor")?;
201
202 conn.close_font(cursor_font).context("Failed to close cursor font")?;
203
204 // Set initial cursor
205 conn.change_window_attributes(window, &ChangeWindowAttributesAux::new().cursor(cursor_default))
206 .context("Failed to set initial cursor")?;
207
208 // Map and flush
209 conn.map_window(window).context("Failed to map window")?;
210 conn.flush().context("Failed to flush X connection")?;
211
212 // Grab keyboard focus
213 conn.set_input_focus(InputFocus::POINTER_ROOT, window, x11rb::CURRENT_TIME)
214 .context("Failed to set input focus")?;
215 conn.flush()?;
216
217 // Sync X11 keyboard lock state with kernel state
218 // X11 doesn't query kernel state at startup, so we must do it manually
219 sync_keyboard_locks(&conn);
220
221 tracing::info!(width, height, "Created greeter window");
222
223 Ok(Self {
224 conn,
225 screen_num,
226 window,
227 gc,
228 width,
229 height,
230 depth,
231 cursor_default,
232 cursor_text,
233 cursor_pointer,
234 current_cursor: CursorType::Default,
235 })
236 }
237
238 /// Get window width
239 pub fn width(&self) -> u16 {
240 self.width
241 }
242
243 /// Get window height
244 pub fn height(&self) -> u16 {
245 self.height
246 }
247
248 /// Get the X11 connection
249 pub fn conn(&self) -> &RustConnection {
250 &self.conn
251 }
252
253 /// Get the window ID
254 pub fn window(&self) -> Window {
255 self.window
256 }
257
258 /// Get the root window ID
259 pub fn root(&self) -> Window {
260 self.conn.setup().roots[self.screen_num].root
261 }
262
263 /// Get the screen number
264 pub fn screen_num(&self) -> usize {
265 self.screen_num
266 }
267
268 /// Put an ARGB image to the window
269 /// Splits large images into chunks to avoid exceeding X11 request limits
270 pub fn put_image(&self, data: &[u8]) -> Result<()> {
271 let bytes_per_row = self.width as usize * 4;
272 let total_rows = self.height as usize;
273
274 // X11 max request is typically 4MB, use 1MB chunks to be safe
275 const MAX_CHUNK_BYTES: usize = 1024 * 1024;
276 let rows_per_chunk = (MAX_CHUNK_BYTES / bytes_per_row).max(1);
277
278 let mut y_offset: i16 = 0;
279 let mut remaining_rows = total_rows;
280 let mut data_offset = 0;
281
282 while remaining_rows > 0 {
283 let chunk_rows = remaining_rows.min(rows_per_chunk);
284 let chunk_bytes = chunk_rows * bytes_per_row;
285 let chunk_data = &data[data_offset..data_offset + chunk_bytes];
286
287 self.conn
288 .put_image(
289 ImageFormat::Z_PIXMAP,
290 self.window,
291 self.gc,
292 self.width,
293 chunk_rows as u16,
294 0,
295 y_offset,
296 0,
297 self.depth,
298 chunk_data,
299 )
300 .context("Failed to put image chunk")?;
301
302 y_offset += chunk_rows as i16;
303 remaining_rows -= chunk_rows;
304 data_offset += chunk_bytes;
305 }
306
307 self.conn.flush().context("Failed to flush after put_image")?;
308 Ok(())
309 }
310
311 /// Wait for and return the next X11 event
312 pub fn wait_for_event(&self) -> Result<x11rb::protocol::Event> {
313 self.conn
314 .wait_for_event()
315 .context("Failed to wait for X11 event")
316 }
317
318 /// Poll for event without blocking
319 pub fn poll_for_event(&self) -> Result<Option<x11rb::protocol::Event>> {
320 self.conn
321 .poll_for_event()
322 .context("Failed to poll for X11 event")
323 }
324
325 /// Set the cursor type (only updates if different from current)
326 pub fn set_cursor(&mut self, cursor_type: CursorType) {
327 if self.current_cursor == cursor_type {
328 return;
329 }
330
331 let cursor = match cursor_type {
332 CursorType::Default => self.cursor_default,
333 CursorType::Text => self.cursor_text,
334 CursorType::Pointer => self.cursor_pointer,
335 };
336
337 let _ = self.conn.change_window_attributes(
338 self.window,
339 &ChangeWindowAttributesAux::new().cursor(cursor),
340 );
341 let _ = self.conn.flush();
342 self.current_cursor = cursor_type;
343 }
344 }
345
346 impl Drop for GreeterWindow {
347 fn drop(&mut self) {
348 let _ = self.conn.destroy_window(self.window);
349 let _ = self.conn.flush();
350 }
351 }
352
353 /// Sync X11 keyboard lock state with kernel state
354 ///
355 /// X11/Xorg doesn't query the kernel's keyboard lock state at startup - it only
356 /// tracks state changes from key events. When returning from a Wayland session,
357 /// X11 starts fresh and doesn't know that caps/num lock was already active.
358 ///
359 /// This function queries the kernel's lock state via evdev and sets X11's XKB
360 /// state to match, ensuring the greeter's keyboard behavior matches the
361 /// physical keyboard state.
362 fn sync_keyboard_locks(conn: &RustConnection) {
363
364 // Initialize XKB extension
365 if let Err(e) = conn.xkb_use_extension(1, 0) {
366 tracing::warn!("Failed to initialize XKB: {}", e);
367 return;
368 }
369
370 // Find a keyboard device and query its lock state
371 let (caps_on, num_on) = match query_kernel_lock_state() {
372 Some(state) => state,
373 None => {
374 tracing::debug!("Could not query kernel keyboard state");
375 return;
376 }
377 };
378
379 tracing::info!("Kernel keyboard state: caps_lock={}, num_lock={}", caps_on, num_on);
380
381 // Build modifier mask to match kernel state
382 let mut mod_locks = ModMask::from(0u16);
383 if caps_on {
384 mod_locks |= ModMask::LOCK;
385 }
386 if num_on {
387 mod_locks |= ModMask::M2;
388 }
389
390 // Set X11 XKB state to match kernel
391 let affect_locks = ModMask::LOCK | ModMask::M2;
392 match conn.xkb_latch_lock_state(
393 xkb::ID::USE_CORE_KBD.into(),
394 affect_locks,
395 mod_locks,
396 false,
397 xkb::Group::M1,
398 ModMask::from(0u16),
399 false,
400 0u16,
401 ) {
402 Ok(_) => {
403 let _ = conn.flush();
404 tracing::debug!("Synced X11 lock state with kernel");
405 }
406 Err(e) => {
407 tracing::warn!("Failed to sync keyboard locks: {}", e);
408 }
409 }
410 }
411
412 /// Query kernel keyboard lock state via evdev EVIOCGLED ioctl
413 fn query_kernel_lock_state() -> Option<(bool, bool)> {
414 use std::fs::{self, File};
415 use std::os::unix::io::AsRawFd;
416
417 // evdev LED indices
418 const LED_NUML: u8 = 0;
419 const LED_CAPSL: u8 = 1;
420
421 // EVIOCGLED ioctl - read LED state bitmap
422 // _IOR('E', 0x19, len) where len is LED_MAX/8+1 bytes
423 // For typical keyboards, we just need 1 byte
424 const EVIOCGLED_1: libc::c_ulong = 0x80014519;
425
426 // Find keyboard devices
427 let input_dir = fs::read_dir("/dev/input").ok()?;
428
429 for entry in input_dir.flatten() {
430 let path = entry.path();
431 let name = path.file_name()?.to_str()?;
432
433 // Only check event devices
434 if !name.starts_with("event") {
435 continue;
436 }
437
438 // Try to open and query
439 if let Ok(file) = File::open(&path) {
440 let mut leds: u8 = 0;
441 let fd = file.as_raw_fd();
442
443 // Query LED state
444 let ret = unsafe {
445 libc::ioctl(fd, EVIOCGLED_1, &mut leds as *mut u8)
446 };
447
448 if ret >= 0 {
449 let caps_on = (leds & (1 << LED_CAPSL)) != 0;
450 let num_on = (leds & (1 << LED_NUML)) != 0;
451 return Some((caps_on, num_on));
452 }
453 }
454 }
455
456 None
457 }
458