use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use tauri::{AppHandle, Emitter, State}; use x11rb::atom_manager; use x11rb::connection::Connection; use x11rb::protocol::xproto::{ AtomEnum, ChangeWindowAttributesAux, ClientMessageEvent, ConfigureWindowAux, ConnectionExt as XprotoConnectionExt, EventMask, StackMode, }; use x11rb::protocol::Event; use x11rb::rust_connection::RustConnection; atom_manager! { pub Atoms: AtomsCookie { _NET_CLIENT_LIST, _NET_WM_PID, _NET_WM_NAME, _XEMBED, _XEMBED_INFO, WM_PROTOCOLS, WM_DELETE_WINDOW, UTF8_STRING, } } // XEmbed protocol message opcodes (subset). const XEMBED_EMBEDDED_NOTIFY: u32 = 0; #[derive(Clone)] struct X11Conn { conn: Arc, atoms: Atoms, root: u32, } #[allow(dead_code)] #[derive(Debug, Clone)] pub struct AppInfo { pub xid: u32, pub pid: u32, pub command: String, } pub struct X11State { conn: Mutex>, tauri_xid: Mutex>, windows: Arc>>, } impl X11State { pub fn new() -> Self { Self { conn: Mutex::new(None), tauri_xid: Mutex::new(None), windows: Arc::new(Mutex::new(HashMap::new())), } } fn ensure_conn(&self, app: &AppHandle) -> Result { let mut guard = self.conn.lock().unwrap(); if let Some(c) = guard.as_ref() { return Ok(c.clone()); } let (raw, screen_num) = RustConnection::connect(None).map_err(|e| e.to_string())?; let conn = Arc::new(raw); let atoms = Atoms::new(&*conn) .map_err(|e| e.to_string())? .reply() .map_err(|e| e.to_string())?; let root = conn.setup().roots[screen_num].root; let conn_t = conn.clone(); let windows_t = self.windows.clone(); let app_t = app.clone(); std::thread::spawn(move || event_loop(conn_t, windows_t, app_t)); let c = X11Conn { conn, atoms, root }; *guard = Some(c.clone()); Ok(c) } fn ensure_xid( &self, app: &AppHandle, window: &tauri::WebviewWindow, ) -> Result { let mut guard = self.tauri_xid.lock().unwrap(); if let Some(xid) = *guard { return Ok(xid); } let xid = fetch_tauri_xid(app, window.clone())?; *guard = Some(xid); Ok(xid) } } fn fetch_tauri_xid(app: &AppHandle, window: tauri::WebviewWindow) -> Result { use std::sync::mpsc; let (tx, rx) = mpsc::sync_channel(1); app.run_on_main_thread(move || { let result: Result = (|| { use gtk::prelude::*; let gtk_win = window.gtk_window().map_err(|e| e.to_string())?; let gdk_win = gtk_win.window().ok_or_else(|| "no GDK window yet".to_string())?; let x11: gdkx11::X11Window = gdk_win.downcast().map_err(|_| { "Tauri's window is not an X11 window — likely running on Wayland. \ GDK_BACKEND=x11 should force X11; check that XWayland is installed \ (apt install xwayland) and restart the app." .to_string() })?; Ok(x11.xid() as u32) })(); let _ = tx.send(result); }) .map_err(|e| e.to_string())?; rx.recv().map_err(|e| e.to_string())? } fn event_loop( conn: Arc, windows: Arc>>, app: AppHandle, ) { loop { match conn.wait_for_event() { Ok(Event::DestroyNotify(e)) => { let removed = windows.lock().unwrap().remove(&e.window); if removed.is_some() { let _ = app.emit("app:destroyed", e.window); } } Ok(_) => {} Err(_) => break, } } } #[derive(Deserialize)] pub struct LaunchOpts { pub command: String, } #[derive(Serialize, Clone)] pub struct LaunchedApp { pub xid: u32, pub title: String, } #[tauri::command] pub async fn app_launch( app: AppHandle, state: State<'_, X11State>, window: tauri::WebviewWindow, opts: LaunchOpts, ) -> Result { let c = state.ensure_conn(&app)?; let parent_xid = state.ensure_xid(&app, &window)?; let parts = shell_words::split(&opts.command).map_err(|e| e.to_string())?; if parts.is_empty() { return Err("empty command".into()); } let (program, args) = parts.split_first().unwrap(); let child = std::process::Command::new(program) .args(args) .stdin(std::process::Stdio::null()) .spawn() .map_err(|e| format!("spawn failed: {}", e))?; let root_pid = child.id(); // Detach: we don't wait() — Tauri exiting will SIGHUP it via the X11 parent. std::mem::forget(child); let client_xid = wait_for_window_by_pid(&c, root_pid, Duration::from_secs(20)) .ok_or_else(|| "could not find window for launched process".to_string())?; embed_window(&c, client_xid, parent_xid)?; let title = read_window_title(&c, client_xid).unwrap_or_else(|| opts.command.clone()); state.windows.lock().unwrap().insert( client_xid, AppInfo { xid: client_xid, pid: root_pid, command: opts.command.clone(), }, ); Ok(LaunchedApp { xid: client_xid, title }) } #[tauri::command] pub fn app_set_geometry( app: AppHandle, state: State<'_, X11State>, window: tauri::WebviewWindow, xid: u32, x: f64, y: f64, width: f64, height: f64, ) -> Result<(), String> { let c = state.ensure_conn(&app)?; let sf = window.scale_factor().map_err(|e| e.to_string())?; let outer = window.outer_position().map_err(|e| e.to_string())?; let inner = window.inner_position().map_err(|e| e.to_string())?; let off_x = inner.x - outer.x; let off_y = inner.y - outer.y; let phys_x = (x * sf).round() as i32 + off_x; let phys_y = (y * sf).round() as i32 + off_y; let phys_w = ((width * sf).round() as i32).max(1) as u32; let phys_h = ((height * sf).round() as i32).max(1) as u32; c.conn .configure_window( xid, &ConfigureWindowAux::new() .x(phys_x) .y(phys_y) .width(phys_w) .height(phys_h) .stack_mode(StackMode::ABOVE), ) .map_err(|e| e.to_string())?; c.conn.flush().map_err(|e| e.to_string())?; Ok(()) } #[tauri::command] pub fn app_set_visible( app: AppHandle, state: State<'_, X11State>, xid: u32, visible: bool, ) -> Result<(), String> { let c = state.ensure_conn(&app)?; if visible { c.conn.map_window(xid).map_err(|e| e.to_string())?; } else { c.conn.unmap_window(xid).map_err(|e| e.to_string())?; } c.conn.flush().map_err(|e| e.to_string())?; Ok(()) } #[tauri::command] pub fn app_set_all_visible( app: AppHandle, state: State<'_, X11State>, visible: bool, ) -> Result<(), String> { let c = state.ensure_conn(&app)?; let xids: Vec = state.windows.lock().unwrap().keys().copied().collect(); for xid in xids { if visible { let _ = c.conn.map_window(xid); } else { let _ = c.conn.unmap_window(xid); } } let _ = c.conn.flush(); Ok(()) } #[tauri::command] pub fn app_close(app: AppHandle, state: State<'_, X11State>, xid: u32) -> Result<(), String> { let c = state.ensure_conn(&app)?; let event = ClientMessageEvent::new( 32, xid, c.atoms.WM_PROTOCOLS, [c.atoms.WM_DELETE_WINDOW, 0, 0, 0, 0], ); let _ = c.conn.send_event(false, xid, EventMask::NO_EVENT, event); c.conn.flush().map_err(|e| e.to_string())?; state.windows.lock().unwrap().remove(&xid); Ok(()) } fn embed_window(c: &X11Conn, client_xid: u32, parent_xid: u32) -> Result<(), String> { // Listen for destroy / property changes on the embedded window. c.conn .change_window_attributes( client_xid, &ChangeWindowAttributesAux::new() .event_mask(EventMask::STRUCTURE_NOTIFY | EventMask::PROPERTY_CHANGE), ) .map_err(|e| e.to_string())?; // Reparent into Tauri's X11 toplevel. c.conn .reparent_window(client_xid, parent_xid, 0, 0) .map_err(|e| e.to_string())?; c.conn.map_window(client_xid).map_err(|e| e.to_string())?; // Stack above the webview's GDK X11 surface (siblings under Tauri toplevel). c.conn .configure_window( client_xid, &ConfigureWindowAux::new().stack_mode(StackMode::ABOVE), ) .map_err(|e| e.to_string())?; c.conn.flush().map_err(|e| e.to_string())?; // Inform the client it is now embedded (XEmbed protocol). let event = ClientMessageEvent::new( 32, client_xid, c.atoms._XEMBED, [x11rb::CURRENT_TIME, XEMBED_EMBEDDED_NOTIFY, 0, parent_xid, 0], ); let _ = c.conn.send_event(false, client_xid, EventMask::NO_EVENT, event); c.conn.flush().map_err(|e| e.to_string())?; Ok(()) } fn wait_for_window_by_pid(c: &X11Conn, root_pid: u32, timeout: Duration) -> Option { let deadline = Instant::now() + timeout; while Instant::now() < deadline { let pids = collect_descendant_pids(root_pid); if let Some(xid) = find_window_for_pids(c, &pids) { return Some(xid); } std::thread::sleep(Duration::from_millis(150)); } None } fn collect_descendant_pids(root: u32) -> Vec { let mut parents: HashMap = HashMap::new(); let Ok(entries) = std::fs::read_dir("/proc") else { return vec![root]; }; for entry in entries.flatten() { let name = entry.file_name(); let Some(name_str) = name.to_str() else { continue; }; let Ok(pid) = name_str.parse::() else { continue; }; let stat_path = entry.path().join("stat"); let Ok(stat) = std::fs::read_to_string(&stat_path) else { continue; }; if let Some(rparen) = stat.rfind(')') { let rest = &stat[rparen + 1..]; let fields: Vec<&str> = rest.split_whitespace().collect(); if let Some(ppid_str) = fields.get(1) { if let Ok(ppid) = ppid_str.parse::() { parents.insert(pid, ppid); } } } } let mut result = vec![root]; let mut grew = true; while grew { grew = false; for (&pid, &ppid) in &parents { if result.contains(&ppid) && !result.contains(&pid) { result.push(pid); grew = true; } } } result } fn find_window_for_pids(c: &X11Conn, pids: &[u32]) -> Option { let reply = c .conn .get_property(false, c.root, c.atoms._NET_CLIENT_LIST, AtomEnum::WINDOW, 0, 1024) .ok()? .reply() .ok()?; let windows: Vec = reply.value32()?.collect(); for xid in windows { if let Some(wpid) = read_window_pid(c, xid) { if pids.contains(&wpid) { return Some(xid); } } } None } fn read_window_pid(c: &X11Conn, xid: u32) -> Option { let reply = c .conn .get_property(false, xid, c.atoms._NET_WM_PID, AtomEnum::CARDINAL, 0, 1) .ok()? .reply() .ok()?; reply.value32().and_then(|mut it| it.next()) } fn read_window_title(c: &X11Conn, xid: u32) -> Option { let reply = c .conn .get_property(false, xid, c.atoms._NET_WM_NAME, c.atoms.UTF8_STRING, 0, 1024) .ok()? .reply() .ok()?; let s = String::from_utf8_lossy(&reply.value).to_string(); if s.is_empty() { let r2 = c .conn .get_property(false, xid, AtomEnum::WM_NAME, AtomEnum::STRING, 0, 1024) .ok()? .reply() .ok()?; Some(String::from_utf8_lossy(&r2.value).to_string()) } else { Some(s) } }