veridian_kernel/desktop/xwayland.rs
1//! XWayland Compatibility Layer
2//!
3//! Provides the socket infrastructure and window mapping for running X11
4//! applications via XWayland on VeridianOS. Full X11 server implementation
5//! is deferred to Phase 8; this module establishes the correct architecture
6//! and provides the bridge between X11 window IDs and Wayland surface IDs.
7//!
8//! ## Architecture
9//!
10//! XWayland runs as a child process of the compositor, speaking the X11
11//! protocol over a Unix socket pair. It creates Wayland surfaces for each
12//! X11 window and forwards input events.
13//!
14//! ```text
15//! X11 Client -> XWayland Process -> Wayland Compositor
16//! | |
17//! X11 protocol Wayland protocol
18//! (/tmp/.X11-unix/X0) (kernel syscalls)
19//! ```
20//!
21//! ## Current Status
22//!
23//! This is a stub implementation that:
24//! - Creates the X11 socket directory structure
25//! - Manages X11-to-Wayland window ID mappings
26//! - Provides the XWayland server lifecycle (start/stop)
27//! - Logs operations for debugging
28//!
29//! Actual X11 protocol handling requires Phase 8.
30
31#![allow(dead_code)]
32
33use alloc::{format, string::String, vec::Vec};
34
35use crate::error::KernelError;
36
37// ---------------------------------------------------------------------------
38// Constants
39// ---------------------------------------------------------------------------
40
41/// Default X11 display number
42pub const DEFAULT_DISPLAY: u32 = 0;
43
44/// Maximum display number we will try
45pub const MAX_DISPLAY: u32 = 32;
46
47/// X11 socket directory
48pub const X11_SOCKET_DIR: &str = "/tmp/.X11-unix";
49
50/// X11 lock file directory
51pub const X11_LOCK_DIR: &str = "/tmp";
52
53/// Maximum number of X11 windows we track
54const MAX_WINDOWS: usize = 256;
55
56// ---------------------------------------------------------------------------
57// XWayland server state
58// ---------------------------------------------------------------------------
59
60/// State of the XWayland server process.
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum XWaylandState {
63 /// Server has not been started
64 NotStarted,
65 /// Server is in the process of starting (socket created, waiting for ready)
66 Starting,
67 /// Server is running and accepting X11 connections
68 Running,
69 /// Server failed to start
70 Failed,
71 /// Server has been stopped
72 Stopped,
73}
74
75// ---------------------------------------------------------------------------
76// X11 window mapping
77// ---------------------------------------------------------------------------
78
79/// Maps an X11 window to a Wayland compositor surface.
80///
81/// When XWayland creates a window, it also creates a corresponding Wayland
82/// surface. This mapping lets the compositor route events and manage the
83/// window's lifecycle across both protocols.
84#[derive(Debug, Clone)]
85pub struct X11WindowMapping {
86 /// X11 window ID (XID)
87 pub x11_window_id: u32,
88 /// Corresponding Wayland surface ID in the compositor
89 pub wayland_surface_id: u32,
90 /// Whether this is an override-redirect window (popup/tooltip)
91 pub override_redirect: bool,
92 /// Window position in X11 coordinate space
93 pub x: i32,
94 pub y: i32,
95 /// Window dimensions
96 pub width: u32,
97 pub height: u32,
98 /// Whether the window is currently mapped (visible)
99 pub mapped: bool,
100 /// Window title (from _NET_WM_NAME or WM_NAME property)
101 pub title: String,
102 /// Window class (from WM_CLASS property)
103 pub window_class: String,
104 /// Whether this window wants input focus
105 pub accepts_focus: bool,
106}
107
108impl X11WindowMapping {
109 /// Create a new window mapping.
110 fn new(x11_id: u32, wayland_id: u32) -> Self {
111 Self {
112 x11_window_id: x11_id,
113 wayland_surface_id: wayland_id,
114 override_redirect: false,
115 x: 0,
116 y: 0,
117 width: 0,
118 height: 0,
119 mapped: false,
120 title: String::new(),
121 window_class: String::new(),
122 accepts_focus: true,
123 }
124 }
125}
126
127// ---------------------------------------------------------------------------
128// X11 event types (stub)
129// ---------------------------------------------------------------------------
130
131/// Simplified X11 event for the compatibility layer.
132///
133/// These are the events that XWayland would forward from X11 clients
134/// to the Wayland compositor.
135#[derive(Debug, Clone)]
136pub enum X11Event {
137 /// Window creation (CreateNotify equivalent)
138 WindowCreated {
139 window_id: u32,
140 x: i32,
141 y: i32,
142 width: u32,
143 height: u32,
144 override_redirect: bool,
145 },
146 /// Window destruction (DestroyNotify equivalent)
147 WindowDestroyed { window_id: u32 },
148 /// Window mapped (visible)
149 WindowMapped { window_id: u32 },
150 /// Window unmapped (hidden)
151 WindowUnmapped { window_id: u32 },
152 /// Window reconfigured (position/size change)
153 WindowConfigured {
154 window_id: u32,
155 x: i32,
156 y: i32,
157 width: u32,
158 height: u32,
159 },
160 /// Window title changed
161 TitleChanged { window_id: u32, title: String },
162 /// Focus request
163 FocusRequest { window_id: u32 },
164}
165
166// ---------------------------------------------------------------------------
167// XWayland server
168// ---------------------------------------------------------------------------
169
170/// XWayland compatibility server.
171///
172/// Manages the lifecycle of the XWayland process and maintains the mapping
173/// between X11 window IDs and Wayland surface IDs.
174pub struct XWaylandServer {
175 /// Current server state
176 state: XWaylandState,
177 /// X11 display number (typically 0)
178 display_number: u32,
179 /// Path to the X11 socket (e.g., /tmp/.X11-unix/X0)
180 socket_path: String,
181 /// Path to the X11 lock file (e.g., /tmp/.X0-lock)
182 lock_path: String,
183 /// Active window mappings
184 window_mappings: Vec<X11WindowMapping>,
185 /// Next X11 window ID to assign (for stub purposes)
186 next_window_id: u32,
187 /// Window manager socket file descriptor (for WM <-> XWayland comms)
188 wm_fd: Option<i32>,
189 /// PID of the XWayland process (0 = not running)
190 xwayland_pid: u64,
191}
192
193impl XWaylandServer {
194 /// Create a new XWayland server (not yet started).
195 pub fn new() -> Self {
196 Self::with_display(DEFAULT_DISPLAY)
197 }
198
199 /// Create a new XWayland server with a specific display number.
200 pub fn with_display(display: u32) -> Self {
201 Self {
202 state: XWaylandState::NotStarted,
203 display_number: display,
204 socket_path: format!("{}/X{}", X11_SOCKET_DIR, display),
205 lock_path: format!("{}/.X{}-lock", X11_LOCK_DIR, display),
206 window_mappings: Vec::new(),
207 next_window_id: 1,
208 wm_fd: None,
209 xwayland_pid: 0,
210 }
211 }
212
213 /// Start the XWayland server.
214 ///
215 /// In this stub implementation, this creates the socket directory
216 /// structure and transitions to the Running state. Actual XWayland
217 /// process spawning is deferred to Phase 8.
218 pub fn start(&mut self) -> Result<(), KernelError> {
219 if self.state == XWaylandState::Running {
220 return Err(KernelError::InvalidState {
221 expected: "not running",
222 actual: "already running",
223 });
224 }
225
226 self.state = XWaylandState::Starting;
227
228 // Create socket directory (in real implementation, this would use
229 // the VFS to create /tmp/.X11-unix/)
230 crate::println!(
231 "[XWAYLAND] Stub: creating X11 socket directory at {}",
232 X11_SOCKET_DIR
233 );
234 crate::println!(
235 "[XWAYLAND] Stub: display :{} socket at {}",
236 self.display_number,
237 self.socket_path
238 );
239 crate::println!("[XWAYLAND] Stub: lock file at {}", self.lock_path);
240
241 // In Phase 8, this would:
242 // 1. Create a socketpair for WM communication
243 // 2. Fork and exec the XWayland binary
244 // 3. Wait for the SIGUSR1 ready signal
245 // 4. Set up the X11 window manager (reparenting, decorations)
246
247 self.state = XWaylandState::Running;
248 crate::println!(
249 "[XWAYLAND] Stub server running on display :{}",
250 self.display_number
251 );
252 Ok(())
253 }
254
255 /// Stop the XWayland server.
256 pub fn stop(&mut self) -> Result<(), KernelError> {
257 if self.state != XWaylandState::Running && self.state != XWaylandState::Starting {
258 return Err(KernelError::InvalidState {
259 expected: "running or starting",
260 actual: "not running",
261 });
262 }
263
264 // In Phase 8, this would:
265 // 1. Send SIGTERM to the XWayland process
266 // 2. Wait for exit
267 // 3. Clean up sockets and lock file
268
269 // Destroy all window mappings
270 self.window_mappings.clear();
271
272 self.wm_fd = None;
273 self.xwayland_pid = 0;
274 self.state = XWaylandState::Stopped;
275
276 crate::println!(
277 "[XWAYLAND] Server stopped on display :{}",
278 self.display_number
279 );
280 Ok(())
281 }
282
283 /// Get the current server state.
284 pub fn state(&self) -> XWaylandState {
285 self.state
286 }
287
288 /// Get the display number.
289 pub fn display_number(&self) -> u32 {
290 self.display_number
291 }
292
293 /// Get the socket path.
294 pub fn socket_path(&self) -> &str {
295 &self.socket_path
296 }
297
298 /// Check if the server is running.
299 pub fn is_running(&self) -> bool {
300 self.state == XWaylandState::Running
301 }
302
303 /// Create a new X11-to-Wayland window mapping.
304 ///
305 /// Called when XWayland creates a new X11 window and its corresponding
306 /// Wayland surface.
307 pub fn create_window_mapping(&mut self, wayland_surface_id: u32) -> Result<u32, KernelError> {
308 if self.window_mappings.len() >= MAX_WINDOWS {
309 return Err(KernelError::ResourceExhausted {
310 resource: "x11_windows",
311 });
312 }
313
314 let x11_id = self.next_window_id;
315 self.next_window_id += 1;
316
317 let mapping = X11WindowMapping::new(x11_id, wayland_surface_id);
318 self.window_mappings.push(mapping);
319
320 Ok(x11_id)
321 }
322
323 /// Destroy a window mapping by X11 window ID.
324 pub fn destroy_window_mapping(&mut self, x11_window_id: u32) -> Result<(), KernelError> {
325 let idx = self
326 .window_mappings
327 .iter()
328 .position(|m| m.x11_window_id == x11_window_id)
329 .ok_or(KernelError::NotFound {
330 resource: "x11_window",
331 id: x11_window_id as u64,
332 })?;
333
334 self.window_mappings.remove(idx);
335 Ok(())
336 }
337
338 /// Get the Wayland surface ID for an X11 window.
339 pub fn get_surface_for_x11_window(&self, x11_window_id: u32) -> Option<u32> {
340 self.window_mappings
341 .iter()
342 .find(|m| m.x11_window_id == x11_window_id)
343 .map(|m| m.wayland_surface_id)
344 }
345
346 /// Get the X11 window ID for a Wayland surface.
347 pub fn get_x11_window_for_surface(&self, wayland_surface_id: u32) -> Option<u32> {
348 self.window_mappings
349 .iter()
350 .find(|m| m.wayland_surface_id == wayland_surface_id)
351 .map(|m| m.x11_window_id)
352 }
353
354 /// Get a reference to a window mapping.
355 pub fn get_window_mapping(&self, x11_window_id: u32) -> Option<&X11WindowMapping> {
356 self.window_mappings
357 .iter()
358 .find(|m| m.x11_window_id == x11_window_id)
359 }
360
361 /// Get a mutable reference to a window mapping.
362 pub fn get_window_mapping_mut(&mut self, x11_window_id: u32) -> Option<&mut X11WindowMapping> {
363 self.window_mappings
364 .iter_mut()
365 .find(|m| m.x11_window_id == x11_window_id)
366 }
367
368 /// Get all active window mappings.
369 pub fn get_all_mappings(&self) -> &[X11WindowMapping] {
370 &self.window_mappings
371 }
372
373 /// Get the number of active window mappings.
374 pub fn window_count(&self) -> usize {
375 self.window_mappings.len()
376 }
377
378 /// Handle an X11 event from the XWayland process.
379 ///
380 /// In the stub implementation, this updates internal mappings.
381 /// In Phase 8, this would translate X11 events to Wayland compositor
382 /// actions.
383 pub fn handle_x11_event(&mut self, event: &X11Event) -> Result<(), KernelError> {
384 match event {
385 X11Event::WindowCreated {
386 window_id,
387 x,
388 y,
389 width,
390 height,
391 override_redirect,
392 } => {
393 if let Some(mapping) = self.get_window_mapping_mut(*window_id) {
394 mapping.x = *x;
395 mapping.y = *y;
396 mapping.width = *width;
397 mapping.height = *height;
398 mapping.override_redirect = *override_redirect;
399 }
400 Ok(())
401 }
402
403 X11Event::WindowDestroyed { window_id } => {
404 let _ = self.destroy_window_mapping(*window_id);
405 Ok(())
406 }
407
408 X11Event::WindowMapped { window_id } => {
409 if let Some(mapping) = self.get_window_mapping_mut(*window_id) {
410 mapping.mapped = true;
411 }
412 Ok(())
413 }
414
415 X11Event::WindowUnmapped { window_id } => {
416 if let Some(mapping) = self.get_window_mapping_mut(*window_id) {
417 mapping.mapped = false;
418 }
419 Ok(())
420 }
421
422 X11Event::WindowConfigured {
423 window_id,
424 x,
425 y,
426 width,
427 height,
428 } => {
429 if let Some(mapping) = self.get_window_mapping_mut(*window_id) {
430 mapping.x = *x;
431 mapping.y = *y;
432 mapping.width = *width;
433 mapping.height = *height;
434 }
435 Ok(())
436 }
437
438 X11Event::TitleChanged { window_id, title } => {
439 if let Some(mapping) = self.get_window_mapping_mut(*window_id) {
440 mapping.title = title.clone();
441 }
442 Ok(())
443 }
444
445 X11Event::FocusRequest { window_id: _ } => {
446 // In Phase 8, this would request focus through the Wayland
447 // compositor's window manager
448 Ok(())
449 }
450 }
451 }
452
453 /// Find a free display number by checking for existing lock files.
454 ///
455 /// Returns the first available display number, or an error if all
456 /// display numbers up to MAX_DISPLAY are taken.
457 pub fn find_free_display() -> Result<u32, KernelError> {
458 // In the stub, we always return 0 since there is no actual
459 // lock file checking.
460 Ok(DEFAULT_DISPLAY)
461 }
462
463 /// Get the DISPLAY environment variable string for this server.
464 ///
465 /// Returns a string like ":0" that X11 clients use to connect.
466 pub fn display_string(&self) -> String {
467 format!(":{}", self.display_number)
468 }
469}
470
471impl Default for XWaylandServer {
472 fn default() -> Self {
473 Self::new()
474 }
475}
476
477// ---------------------------------------------------------------------------
478// Tests
479// ---------------------------------------------------------------------------
480
481#[cfg(test)]
482mod tests {
483 use super::*;
484
485 #[test]
486 fn test_xwayland_lifecycle() {
487 let mut server = XWaylandServer::new();
488 assert_eq!(server.state(), XWaylandState::NotStarted);
489 assert!(!server.is_running());
490
491 server.start().unwrap();
492 assert_eq!(server.state(), XWaylandState::Running);
493 assert!(server.is_running());
494
495 server.stop().unwrap();
496 assert_eq!(server.state(), XWaylandState::Stopped);
497 assert!(!server.is_running());
498 }
499
500 #[test]
501 fn test_window_mapping() {
502 let mut server = XWaylandServer::new();
503 server.start().unwrap();
504
505 let x11_id = server.create_window_mapping(2001).unwrap();
506 assert_eq!(server.window_count(), 1);
507 assert_eq!(server.get_surface_for_x11_window(x11_id), Some(2001));
508 assert_eq!(server.get_x11_window_for_surface(2001), Some(x11_id));
509
510 server.destroy_window_mapping(x11_id).unwrap();
511 assert_eq!(server.window_count(), 0);
512 }
513
514 #[test]
515 fn test_x11_events() {
516 let mut server = XWaylandServer::new();
517 server.start().unwrap();
518
519 let x11_id = server.create_window_mapping(2002).unwrap();
520
521 server
522 .handle_x11_event(&X11Event::WindowCreated {
523 window_id: x11_id,
524 x: 100,
525 y: 200,
526 width: 640,
527 height: 480,
528 override_redirect: false,
529 })
530 .unwrap();
531
532 let mapping = server.get_window_mapping(x11_id).unwrap();
533 assert_eq!(mapping.x, 100);
534 assert_eq!(mapping.y, 200);
535 assert_eq!(mapping.width, 640);
536 assert_eq!(mapping.height, 480);
537 assert!(!mapping.override_redirect);
538
539 server
540 .handle_x11_event(&X11Event::WindowMapped { window_id: x11_id })
541 .unwrap();
542 assert!(server.get_window_mapping(x11_id).unwrap().mapped);
543
544 server
545 .handle_x11_event(&X11Event::TitleChanged {
546 window_id: x11_id,
547 title: String::from("Test Window"),
548 })
549 .unwrap();
550 assert_eq!(
551 server.get_window_mapping(x11_id).unwrap().title,
552 "Test Window"
553 );
554 }
555
556 #[test]
557 fn test_display_string() {
558 let server = XWaylandServer::with_display(2);
559 assert_eq!(server.display_string(), ":2");
560 assert_eq!(server.socket_path(), "/tmp/.X11-unix/X2");
561 }
562}