Skip to main content

agentmux_launcher/splash_linux/
wayland.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Wayland splash backend — a software-drawn (`wl_shm`) `xdg_toplevel` showing
5//! the pulsing brain. This is the **fallback** path, used only when no X server
6//! is reachable (`super::detect` prefers the self-centering X11/XWayland backend
7//! whenever XWayland is up — see that module's doc for why).
8//!
9//! Wayland deliberately denies clients window positioning and "always on top",
10//! and GNOME/Mutter does not implement `wlr-layer-shell`, so this splash *cannot
11//! self-center*: it's a borderless `xdg_toplevel` the compositor places — Mutter
12//! drops small toplevels at the top-left, not centered — which we dismiss the
13//! instant the host paints. No GPU/EGL — a shared-memory buffer we blit each
14//! frame. See docs/specs/SPEC_LINUX_SPLASH_SESSION_AWARE_2026_06_20.md §3, §6.
15
16use std::error::Error;
17use std::path::{Path, PathBuf};
18use std::time::{Duration, Instant};
19
20use smithay_client_toolkit::{
21    compositor::{CompositorHandler, CompositorState},
22    delegate_compositor, delegate_output, delegate_registry, delegate_shm, delegate_xdg_shell,
23    delegate_xdg_window,
24    output::{OutputHandler, OutputState},
25    registry::{ProvidesRegistryState, RegistryState},
26    registry_handlers,
27    shell::{
28        xdg::{
29            window::{Window, WindowConfigure, WindowDecorations, WindowHandler},
30            XdgShell,
31        },
32        WaylandSurface,
33    },
34    shm::{slot::SlotPool, Shm, ShmHandler},
35};
36use smithay_client_toolkit::reexports::client::{
37    globals::registry_queue_init,
38    protocol::{wl_output, wl_shm, wl_surface},
39    Connection, QueueHandle,
40};
41
42use super::{
43    fade_alpha, min_hold, pulse_alpha, render_frame, StageList, CARD_H, CARD_W, CORNER_RADIUS_PX,
44    DISMISS_TIMEOUT,
45};
46
47pub(super) fn run(
48    ready_file: &Path,
49    footer: Vec<String>,
50    startup_rx: std::sync::mpsc::Receiver<crate::startup_events::StartupEvent>,
51) -> Result<(), Box<dyn Error>> {
52    let conn = Connection::connect_to_env()?;
53    let (globals, mut event_queue) = registry_queue_init::<SplashState>(&conn)?;
54    let qh = event_queue.handle();
55
56    let compositor = CompositorState::bind(&globals, &qh)?;
57    let shm = Shm::bind(&globals, &qh)?;
58    let xdg_shell = XdgShell::bind(&globals, &qh)?;
59
60    let w = CARD_W as u32;
61    let h = CARD_H as u32;
62
63    let surface = compositor.create_surface(&qh);
64    let window = xdg_shell.create_window(surface, WindowDecorations::None, &qh);
65    window.set_title("AgentMux");
66    // Match the host's app_id so the compositor associates the two.
67    window.set_app_id("ai.agentmux.AgentMux");
68    window.set_min_size(Some((w, h)));
69    window.set_max_size(Some((w, h)));
70    window.commit();
71
72    let pool = SlotPool::new((w * h * 4) as usize, &shm)?;
73
74    let mut state = SplashState {
75        registry_state: RegistryState::new(&globals),
76        output_state: OutputState::new(&globals, &qh),
77        shm,
78        pool,
79        window,
80        width: w,
81        height: h,
82        start: Instant::now(),
83        min_hold: min_hold(),
84        ready_file: ready_file.to_path_buf(),
85        fade_start: None,
86        exit: false,
87        footer,
88        startup_rx,
89        stage_list: StageList::new(),
90    };
91
92    while !state.exit {
93        event_queue.blocking_dispatch(&mut state)?;
94    }
95    Ok(())
96}
97
98struct SplashState {
99    registry_state: RegistryState,
100    output_state: OutputState,
101    shm: Shm,
102    pool: SlotPool,
103    window: Window,
104    width: u32,
105    height: u32,
106    start: Instant,
107    min_hold: Duration,
108    ready_file: PathBuf,
109    fade_start: Option<Instant>,
110    exit: bool,
111    footer: Vec<String>,
112    startup_rx: std::sync::mpsc::Receiver<crate::startup_events::StartupEvent>,
113    stage_list: StageList,
114}
115
116impl SplashState {
117    fn should_dismiss(&self) -> bool {
118        let elapsed = self.start.elapsed();
119        (self.ready_file.exists() && elapsed >= self.min_hold) || elapsed >= DISMISS_TIMEOUT
120    }
121
122    /// Paint one frame and request the next frame callback, or set `exit` once
123    /// the dismiss condition is met. Driven by the compositor's frame callbacks
124    /// (which also pace the ~60 fps pulse).
125    fn draw(&mut self, qh: &QueueHandle<Self>) {
126        // Drain all pending startup events before rendering (non-blocking),
127        // as one batch — `StageList::apply_tick` defers a same-tick
128        // Begin+End pair to the next call so every row gets at least one
129        // "running" frame before it can complete, instead of snapping
130        // straight to its final value.
131        let mut fresh = Vec::new();
132        while let Ok(event) = self.startup_rx.try_recv() {
133            fresh.push(event);
134        }
135        self.stage_list.apply_tick(fresh);
136
137        let now = Instant::now();
138        // Begin the fade-out the first time the dismiss condition holds; tear
139        // down once it completes.
140        if self.fade_start.is_none() && self.should_dismiss() {
141            // Any row still open at this instant (no matching End ever
142            // arrived) gets a definite outcome now instead of staying
143            // visually "running" for the rest of the fade — see
144            // splash_core::StageTimeline::finalize_running.
145            self.stage_list.finalize_running(now);
146            self.fade_start = Some(now);
147        }
148        let window_alpha = fade_alpha(self.fade_start, now);
149        if self.fade_start.is_some() && window_alpha <= 0.0 {
150            self.exit = true;
151            return;
152        }
153
154        let (w, h) = (self.width as i32, self.height as i32);
155        let stride = w * 4;
156        let (buffer, canvas) = match self
157            .pool
158            .create_buffer(w, h, stride, wl_shm::Format::Argb8888)
159        {
160            Ok(b) => b,
161            Err(_) => {
162                self.exit = true;
163                return;
164            }
165        };
166
167        let brain_alpha = pulse_alpha(self.start.elapsed().as_secs_f32());
168        let stage_lines = self.stage_list.lines();
169        // wl_shm ARGB8888 is native-endian 0xAARRGGBB → pre-multiplied B,G,R,A on LE.
170        render_frame(
171            canvas,
172            w,
173            h,
174            brain_alpha,
175            window_alpha,
176            CORNER_RADIUS_PX,
177            /* bgr = */ true,
178            &self.footer,
179            &stage_lines,
180        );
181
182        let surface = self.window.wl_surface();
183        surface.damage_buffer(0, 0, w, h);
184        surface.frame(qh, surface.clone());
185        let _ = buffer.attach_to(surface);
186        self.window.commit();
187    }
188}
189
190impl CompositorHandler for SplashState {
191    fn scale_factor_changed(
192        &mut self,
193        _: &Connection,
194        _: &QueueHandle<Self>,
195        _: &wl_surface::WlSurface,
196        _: i32,
197    ) {
198    }
199    fn transform_changed(
200        &mut self,
201        _: &Connection,
202        _: &QueueHandle<Self>,
203        _: &wl_surface::WlSurface,
204        _: wl_output::Transform,
205    ) {
206    }
207    fn frame(
208        &mut self,
209        _: &Connection,
210        qh: &QueueHandle<Self>,
211        _: &wl_surface::WlSurface,
212        _: u32,
213    ) {
214        self.draw(qh);
215    }
216    fn surface_enter(
217        &mut self,
218        _: &Connection,
219        _: &QueueHandle<Self>,
220        _: &wl_surface::WlSurface,
221        _: &wl_output::WlOutput,
222    ) {
223    }
224    fn surface_leave(
225        &mut self,
226        _: &Connection,
227        _: &QueueHandle<Self>,
228        _: &wl_surface::WlSurface,
229        _: &wl_output::WlOutput,
230    ) {
231    }
232}
233
234impl WindowHandler for SplashState {
235    fn request_close(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &Window) {
236        self.exit = true;
237    }
238    fn configure(
239        &mut self,
240        _: &Connection,
241        qh: &QueueHandle<Self>,
242        _: &Window,
243        _: WindowConfigure,
244        _: u32,
245    ) {
246        // First (and every) configure: (re)paint, which kicks the frame loop.
247        self.draw(qh);
248    }
249}
250
251impl OutputHandler for SplashState {
252    fn output_state(&mut self) -> &mut OutputState {
253        &mut self.output_state
254    }
255    fn new_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
256    fn update_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
257    fn output_destroyed(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
258}
259
260impl ShmHandler for SplashState {
261    fn shm_state(&mut self) -> &mut Shm {
262        &mut self.shm
263    }
264}
265
266impl ProvidesRegistryState for SplashState {
267    fn registry(&mut self) -> &mut RegistryState {
268        &mut self.registry_state
269    }
270    registry_handlers![OutputState];
271}
272
273delegate_compositor!(SplashState);
274delegate_output!(SplashState);
275delegate_xdg_shell!(SplashState);
276delegate_xdg_window!(SplashState);
277delegate_shm!(SplashState);
278delegate_registry!(SplashState);