agentmux_launcher/splash_linux/
x11.rs1use std::error::Error;
18use std::path::Path;
19use std::time::{Duration, Instant};
20
21use x11rb::connection::Connection;
22use x11rb::protocol::randr::ConnectionExt as _;
23use x11rb::protocol::xproto::ConnectionExt as _;
24use x11rb::protocol::xproto::{
25 AtomEnum, ColormapAlloc, CreateGCAux, CreateWindowAux, EventMask, ImageFormat, PropMode, Screen,
26 VisualClass, Window, WindowClass,
27};
28use x11rb::wrapper::ConnectionExt as _;
29use x11rb::NONE;
30
31use super::{
32 fade_alpha, min_hold, pulse_alpha, render_frame, StageList, CARD_H, CARD_W, CORNER_RADIUS_PX,
33 DISMISS_TIMEOUT, FRAME_MS,
34};
35
36pub(super) fn server_reachable() -> bool {
42 x11rb::connect(None).is_ok()
43}
44
45pub(super) fn run(
46 ready_file: &Path,
47 footer: Vec<String>,
48 startup_rx: std::sync::mpsc::Receiver<crate::startup_events::StartupEvent>,
49) -> Result<(), Box<dyn Error>> {
50 let (conn, screen_num) = x11rb::connect(None)?;
51 let screen = conn.setup().roots[screen_num].clone();
52 let root = screen.root;
53
54 let w = CARD_W as u16;
55 let h = CARD_H as u16;
56
57 let argb_visual = if has_compositor(&conn, screen_num).unwrap_or(false) {
60 find_argb_visual(&screen)
61 } else {
62 None
63 };
64
65 let (depth, visual, colormap) = match argb_visual {
66 Some(vid) => {
67 let cmap = conn.generate_id()?;
68 conn.create_colormap(ColormapAlloc::NONE, cmap, root, vid)?;
69 (32u8, vid, Some(cmap))
70 }
71 None => (screen.root_depth, screen.root_visual, None),
72 };
73 let radius = if colormap.is_some() { CORNER_RADIUS_PX } else { 0.0 };
74
75 let (mx, my, mw, mh) = primary_monitor(&conn, root, &screen);
77 let x = (mx + (mw - w as i32) / 2).max(0) as i16;
78 let y = (my + (mh - h as i32) / 2).max(0) as i16;
79
80 let win = conn.generate_id()?;
81 let mut win_aux = CreateWindowAux::new()
82 .override_redirect(1)
83 .event_mask(EventMask::EXPOSURE);
84 win_aux = if let Some(cmap) = colormap {
85 win_aux.colormap(cmap).border_pixel(0).background_pixel(0)
88 } else {
89 win_aux.background_pixel(screen.black_pixel)
90 };
91 conn.create_window(
92 depth,
93 win,
94 root,
95 x,
96 y,
97 w,
98 h,
99 0,
100 WindowClass::INPUT_OUTPUT,
101 visual,
102 &win_aux,
103 )?;
104
105 set_ewmh_hints(&conn, win)?;
106
107 let gc = conn.generate_id()?;
108 conn.create_gc(gc, win, &CreateGCAux::new())?;
109 let pixmap = conn.generate_id()?;
110 conn.create_pixmap(depth, pixmap, win, w, h)?;
111
112 conn.map_window(win)?;
113 conn.flush()?;
114
115 let start = Instant::now();
116 let hold = min_hold();
117 let mut fade_start: Option<Instant> = None;
118 let mut stage_list = StageList::new();
119 let mut buf = vec![0u8; w as usize * h as usize * 4];
121
122 loop {
123 let mut fresh = Vec::new();
129 while let Ok(event) = startup_rx.try_recv() {
130 fresh.push(event);
131 }
132 stage_list.apply_tick(fresh);
133
134 let now = Instant::now();
135 let elapsed = now.duration_since(start);
136 let dismiss =
137 (ready_file.exists() && elapsed >= hold) || elapsed >= DISMISS_TIMEOUT;
138 if dismiss {
139 if colormap.is_none() {
140 break; }
142 if fade_start.is_none() {
143 stage_list.finalize_running(now);
148 fade_start = Some(now);
149 }
150 }
151 let window_alpha = fade_alpha(fade_start, now);
152 if fade_start.is_some() && window_alpha <= 0.0 {
153 break;
154 }
155
156 let brain_alpha = pulse_alpha(elapsed.as_secs_f32());
157 render_frame(
158 &mut buf,
159 w as i32,
160 h as i32,
161 brain_alpha,
162 window_alpha,
163 radius,
164 true,
165 &footer,
166 &stage_list.lines(),
167 );
168
169 let row_bytes = w as usize * 4;
172 let strip_rows = (200_000 / row_bytes).max(1) as u16;
173 let mut row = 0u16;
174 while row < h {
175 let rows = strip_rows.min(h - row);
176 let off = row as usize * row_bytes;
177 let len = rows as usize * row_bytes;
178 conn.put_image(
179 ImageFormat::Z_PIXMAP,
180 pixmap,
181 gc,
182 w,
183 rows,
184 0,
185 row as i16,
186 0,
187 depth,
188 &buf[off..off + len],
189 )?;
190 row += rows;
191 }
192 conn.copy_area(pixmap, win, gc, 0, 0, 0, 0, w, h)?;
193 conn.flush()?;
194
195 while let Ok(Some(_)) = conn.poll_for_event() {}
196 std::thread::sleep(Duration::from_millis(FRAME_MS));
197 }
198
199 let _ = conn.destroy_window(win);
200 let _ = conn.free_pixmap(pixmap);
201 let _ = conn.free_gc(gc);
202 if let Some(cmap) = colormap {
203 let _ = conn.free_colormap(cmap);
204 }
205 let _ = conn.flush();
206 Ok(())
207}
208
209fn has_compositor<C: Connection>(conn: &C, screen_num: usize) -> Result<bool, Box<dyn Error>> {
212 let name = format!("_NET_WM_CM_S{screen_num}");
213 let atom = conn.intern_atom(false, name.as_bytes())?.reply()?.atom;
214 let owner = conn.get_selection_owner(atom)?.reply()?.owner;
215 Ok(owner != NONE)
216}
217
218fn find_argb_visual(screen: &Screen) -> Option<u32> {
220 screen
221 .allowed_depths
222 .iter()
223 .filter(|d| d.depth == 32)
224 .flat_map(|d| d.visuals.iter())
225 .find(|v| v.class == VisualClass::TRUE_COLOR)
226 .map(|v| v.visual_id)
227}
228
229fn primary_monitor<C: Connection>(conn: &C, root: Window, screen: &Screen) -> (i32, i32, i32, i32) {
232 let fallback = (
233 0,
234 0,
235 screen.width_in_pixels as i32,
236 screen.height_in_pixels as i32,
237 );
238 let prim = match conn.randr_get_output_primary(root).ok().and_then(|c| c.reply().ok()) {
239 Some(p) if p.output != NONE => p.output,
240 _ => return fallback,
241 };
242 let info = match conn.randr_get_output_info(prim, 0).ok().and_then(|c| c.reply().ok()) {
243 Some(i) if i.crtc != NONE => i,
244 _ => return fallback,
245 };
246 match conn.randr_get_crtc_info(info.crtc, 0).ok().and_then(|c| c.reply().ok()) {
247 Some(c) => (c.x as i32, c.y as i32, c.width as i32, c.height as i32),
248 None => fallback,
249 }
250}
251
252fn set_ewmh_hints<C: Connection>(conn: &C, win: Window) -> Result<(), Box<dyn Error>> {
255 let atom = |name: &[u8]| -> Result<u32, Box<dyn Error>> {
256 Ok(conn.intern_atom(false, name)?.reply()?.atom)
257 };
258
259 let wm_type = atom(b"_NET_WM_WINDOW_TYPE")?;
260 let wm_type_splash = atom(b"_NET_WM_WINDOW_TYPE_SPLASH")?;
261 conn.change_property32(PropMode::REPLACE, win, wm_type, AtomEnum::ATOM, &[wm_type_splash])?;
262
263 let wm_state = atom(b"_NET_WM_STATE")?;
264 let above = atom(b"_NET_WM_STATE_ABOVE")?;
265 let skip_taskbar = atom(b"_NET_WM_STATE_SKIP_TASKBAR")?;
266 let skip_pager = atom(b"_NET_WM_STATE_SKIP_PAGER")?;
267 conn.change_property32(
268 PropMode::REPLACE,
269 win,
270 wm_state,
271 AtomEnum::ATOM,
272 &[above, skip_taskbar, skip_pager],
273 )?;
274
275 Ok(())
276}