Skip to main content

gpui_linux/linux/
platform.rs

1use std::{
2    cell::RefCell,
3    env,
4    path::{Path, PathBuf},
5    rc::Rc,
6    sync::Arc,
7};
8#[cfg(any(feature = "wayland", feature = "x11"))]
9use std::{
10    ffi::OsString,
11    fs::File,
12    io::Read as _,
13    os::fd::{AsFd, AsRawFd},
14    time::Duration,
15};
16
17#[cfg(any(feature = "wayland", feature = "x11"))]
18use anyhow::ensure;
19use anyhow::{Context as _, Result, anyhow};
20#[cfg(any(feature = "wayland", feature = "x11"))]
21use ashpd::{
22    desktop::{
23        Request,
24        inhibit::{InhibitFlags, InhibitOptions, InhibitProxy},
25    },
26    enumflags2::BitFlags,
27};
28use calloop::{EventLoop, LoopHandle, LoopSignal, channel::Sender};
29use futures::channel::oneshot;
30use gpui_util::{ResultExt as _, new_std_command};
31#[cfg(any(feature = "wayland", feature = "x11"))]
32use xkbcommon::xkb::{self, Keycode, Keysym, State};
33
34use crate::linux::{
35    Backend, DisplayConnection, HeadlessConnection, LinuxDispatcher, PriorityQueueCalloopReceiver,
36    select_backend,
37};
38use gpui_engine::{DmaBufCapabilities, PlatformTextSystem};
39use gpui_platform::{
40    ActivityGuard, BackgroundExecutor, ClipboardItem, CursorStyle, ForegroundExecutor,
41    GpuAdapterInfo, GpuId, GpuPreference, GraphicalEnvironment, MenuCommandId, PathPromptOptions,
42    Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformMenu,
43    PlatformMenuItem, PlatformWindow, RunnableVariant, Task, ThermalState, WindowAppearance,
44    WindowButtonLayout, WindowId, WindowParams, WindowingModes, WindowingRequest,
45};
46#[cfg(any(feature = "wayland", feature = "x11"))]
47use gpui_types::{Pixels, Point, px};
48
49#[cfg(any(feature = "wayland", feature = "x11"))]
50pub(crate) const SCROLL_LINES: f32 = 3.0;
51
52// Values match the defaults on GTK.
53// Taken from https://github.com/GNOME/gtk/blob/main/gtk/gtksettings.c#L320
54#[cfg(any(feature = "wayland", feature = "x11"))]
55pub(crate) const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(400);
56#[cfg(any(feature = "wayland", feature = "x11"))]
57pub(crate) const DOUBLE_CLICK_DISTANCE: Pixels = px(5.0);
58pub(crate) const KEYRING_LABEL: &str = "zed-github-account";
59
60#[cfg(any(feature = "wayland", feature = "x11"))]
61const FILE_PICKER_PORTAL_MISSING: &str =
62    "Couldn't open file picker due to missing xdg-desktop-portal implementation.";
63
64#[derive(Default)]
65pub(crate) struct PlatformHandlers {
66    pub(crate) open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
67    pub(crate) quit: Option<Box<dyn FnMut() -> bool>>,
68    pub(crate) reopen: Option<Box<dyn FnMut()>>,
69    pub(crate) app_menu_action: Option<Box<dyn FnMut(MenuCommandId)>>,
70    pub(crate) will_open_app_menu: Option<Box<dyn FnMut()>>,
71    pub(crate) validate_app_menu_command: Option<Box<dyn FnMut(MenuCommandId) -> bool>>,
72    pub(crate) keyboard_layout_change: Option<Box<dyn FnMut()>>,
73    pub(crate) system_sleep: Option<Box<dyn FnMut()>>,
74    pub(crate) system_wake: Option<Box<dyn FnMut()>>,
75}
76
77/// A logind `PrepareForSleep` signal, forwarded from the D-Bus listener to
78/// the client's event loop.
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80#[cfg_attr(
81    not(all(target_os = "linux", any(feature = "wayland", feature = "x11"))),
82    allow(dead_code)
83)]
84pub(crate) enum SystemPowerEvent {
85    Sleep,
86    Wake,
87}
88
89/// The debug override, lowered to a [`GpuPreference::Device`].
90///
91/// It lives here, not in the renderer or the shared platform crate, because a debug variable is an
92/// OS-integration detail: the one path the renderer sees is `set_gpu_preference`, which an
93/// application's stated preference also takes. The headless entry point has no platform instance to
94/// ask, so it calls this directly. A malformed value is logged and ignored rather than silently
95/// falling through to a different GPU.
96pub fn gpu_preference_from_env() -> Option<GpuPreference> {
97    match std::env::var("ZED_DEVICE_ID") {
98        Ok(value) => match GpuId::parse_pci_id(&value) {
99            Some(device_id) => Some(GpuPreference::Device(GpuId::by_device(device_id))),
100            None => {
101                log::error!(
102                    "ZED_DEVICE_ID: expected a four-digit hexadecimal PCI id, got {value:?}"
103                );
104                None
105            }
106        },
107        Err(std::env::VarError::NotPresent) => None,
108        Err(error) => {
109            log::error!("ZED_DEVICE_ID: could not be read: {error}");
110            None
111        }
112    }
113}
114
115pub(crate) struct LinuxCommon {
116    pub(crate) background_executor: BackgroundExecutor,
117    pub(crate) foreground_executor: ForegroundExecutor,
118    pub(crate) text_system: Arc<dyn PlatformTextSystem>,
119    pub(crate) appearance: WindowAppearance,
120    pub(crate) auto_hide_scrollbars: bool,
121    pub(crate) button_layout: WindowButtonLayout,
122    /// Which GPU the renderer should choose. Set on the application before it runs; the choice is
123    /// process-wide.
124    pub(crate) gpu_preference: GpuPreference,
125    pub(crate) callbacks: PlatformHandlers,
126    pub(crate) signal: LoopSignal,
127    app_name: Option<String>,
128    system_notifications: crate::linux::system_notifications::SystemNotificationState,
129    #[cfg_attr(
130        not(all(target_os = "linux", any(feature = "wayland", feature = "x11"))),
131        allow(dead_code)
132    )]
133    power_sender: Sender<SystemPowerEvent>,
134    power_listener_started: bool,
135    /// Runs after each foreground runnable registered by [`LinuxCommon::register_sources`].
136    ///
137    /// X11 uses this to handle events that a runnable's requests read into xcb's queue, where
138    /// they would not wake the event loop.
139    pub(crate) after_runnable: Option<Rc<dyn Fn()>>,
140}
141
142impl LinuxCommon {
143    pub fn new(
144        signal: LoopSignal,
145    ) -> (
146        Self,
147        PriorityQueueCalloopReceiver<RunnableVariant>,
148        calloop::channel::Channel<SystemPowerEvent>,
149    ) {
150        let (main_sender, main_receiver) = PriorityQueueCalloopReceiver::new();
151        let (power_sender, power_receiver) = calloop::channel::channel();
152
153        #[cfg(any(feature = "wayland", feature = "x11"))]
154        let text_system = Arc::new(crate::linux::CosmicTextSystem::new("IBM Plex Sans"));
155        #[cfg(not(any(feature = "wayland", feature = "x11")))]
156        let text_system = Arc::new(gpui_engine::NoopTextSystem::new());
157
158        let callbacks = PlatformHandlers::default();
159
160        let dispatcher = Arc::new(LinuxDispatcher::new(main_sender));
161
162        let background_executor = BackgroundExecutor::new(dispatcher.clone());
163
164        let common = LinuxCommon {
165            background_executor,
166            foreground_executor: ForegroundExecutor::new(dispatcher),
167            text_system,
168            appearance: WindowAppearance::Light,
169            auto_hide_scrollbars: false,
170            button_layout: WindowButtonLayout::linux_default(),
171            gpu_preference: gpu_preference_from_env().unwrap_or_default(),
172            callbacks,
173            signal,
174            app_name: None,
175            system_notifications: crate::linux::system_notifications::SystemNotificationState::new(
176            ),
177            power_sender,
178            power_listener_started: false,
179            after_runnable: None,
180        };
181
182        (common, main_receiver, power_receiver)
183    }
184
185    pub(crate) fn start_power_listener(&mut self) {
186        if !self.power_listener_started {
187            #[cfg(all(target_os = "linux", any(feature = "wayland", feature = "x11")))]
188            smol::spawn({
189                let power_sender = self.power_sender.clone();
190                async move {
191                    if let Err(error) = listen_for_system_power_events(power_sender).await {
192                        log::debug!("failed to listen for system sleep/wake events: {error:?}");
193                    }
194                }
195            })
196            .detach();
197
198            self.power_listener_started = true;
199        }
200    }
201
202    /// Registers the foreground executor and power listener on a loop shared with display clients.
203    ///
204    /// Foreground runnables run as idle callbacks, after the loop has dispatched all pending events.
205    pub(crate) fn register_sources(
206        common: &Rc<RefCell<Self>>,
207        loop_handle: &calloop::LoopHandle<'static, ()>,
208        main_receiver: PriorityQueueCalloopReceiver<RunnableVariant>,
209        power_receiver: calloop::channel::Channel<SystemPowerEvent>,
210    ) -> anyhow::Result<()> {
211        loop_handle
212            .insert_source(main_receiver, {
213                let loop_handle = loop_handle.clone();
214                let common = common.clone();
215                move |event, _, _| {
216                    if let calloop::channel::Event::Msg(runnable) = event {
217                        let common = common.clone();
218                        loop_handle.insert_idle(move |_| {
219                            let location = runnable.metadata().location;
220                            let spawned = runnable.metadata().spawned;
221                            gpui_platform::profiler::update_running_task(spawned, location);
222                            runnable.run();
223                            gpui_platform::profiler::save_task_timing();
224
225                            let after_runnable = common.borrow().after_runnable.clone();
226                            if let Some(after_runnable) = after_runnable {
227                                after_runnable();
228                            }
229                        });
230                    }
231                }
232            })
233            .map_err(|error| anyhow!("failed to register foreground executor: {error}"))?;
234        loop_handle
235            .insert_source(power_receiver, {
236                let common = common.clone();
237                move |event, _, _| {
238                    if let calloop::channel::Event::Msg(event) = event {
239                        LinuxCommon::handle_system_power_event(&common, event);
240                    }
241                }
242            })
243            .map_err(|error| anyhow!("failed to register power listener: {error}"))?;
244        Ok(())
245    }
246
247    /// Runs the app's sleep or wake callback, without `common` borrowed, since the callback
248    /// can call back into the platform.
249    fn handle_system_power_event(common: &RefCell<Self>, event: SystemPowerEvent) {
250        fn slot(
251            callbacks: &mut PlatformHandlers,
252            event: SystemPowerEvent,
253        ) -> &mut Option<Box<dyn FnMut()>> {
254            match event {
255                SystemPowerEvent::Sleep => &mut callbacks.system_sleep,
256                SystemPowerEvent::Wake => &mut callbacks.system_wake,
257            }
258        }
259        let callback = slot(&mut common.borrow_mut().callbacks, event).take();
260        if let Some(mut callback) = callback {
261            callback();
262            let mut common = common.borrow_mut();
263            let slot = slot(&mut common.callbacks, event);
264            // The callback may have replaced itself.
265            if slot.is_none() {
266                *slot = Some(callback);
267            }
268        }
269    }
270}
271
272#[cfg(all(target_os = "linux", any(feature = "wayland", feature = "x11")))]
273async fn listen_for_system_power_events(
274    power_sender: Sender<SystemPowerEvent>,
275) -> anyhow::Result<()> {
276    use futures::StreamExt as _;
277
278    let connection = ashpd::zbus::Connection::system().await?;
279    let proxy = ashpd::zbus::Proxy::new(
280        &connection,
281        "org.freedesktop.login1",
282        "/org/freedesktop/login1",
283        "org.freedesktop.login1.Manager",
284    )
285    .await?;
286    let mut sleep_events = proxy.receive_signal("PrepareForSleep").await?;
287
288    while let Some(message) = sleep_events.next().await {
289        let event = if message.body().deserialize::<bool>()? {
290            SystemPowerEvent::Sleep
291        } else {
292            SystemPowerEvent::Wake
293        };
294        power_sender.send(event).ok();
295    }
296
297    Ok(())
298}
299
300/// GPUI's platform for Linux and FreeBSD.
301///
302/// One event loop serves the whole process. The foreground executor and power listener are
303/// registered on it once, and the current [`DisplayConnection`] adds and removes its own sources
304/// as the app switches between the display modes it allows.
305pub(crate) struct LinuxPlatform {
306    common: Rc<RefCell<LinuxCommon>>,
307    /// The event loop, until `run` takes it.
308    event_loop: RefCell<Option<EventLoop<'static, ()>>>,
309    loop_handle: LoopHandle<'static, ()>,
310    connection: RefCell<DisplayConnection>,
311    allowed_modes: WindowingModes,
312    /// The mode to start in, applied when `run` starts. Windowed in the process's own
313    /// environment when unset.
314    initial_windowing: RefCell<Option<WindowingRequest>>,
315    /// Taken by the connection made when `run` starts.
316    startup_environment: RefCell<Option<StartupEnvironment>>,
317    /// A requested switch waiting for the event loop to apply it.
318    pending_mode: RefCell<Option<WindowingRequest>>,
319    transition_waiter: RefCell<Option<oneshot::Sender<anyhow::Result<()>>>>,
320}
321
322/// What a Wayland connection made at startup may use from this process's environment.
323///
324/// Both are taken from the environment when the platform is created, before it starts any
325/// threads, and are only meaningful for the connection made then.
326#[derive(Default)]
327struct StartupEnvironment {
328    #[cfg(feature = "wayland")]
329    activation_token: Option<String>,
330    /// The socket `WAYLAND_SOCKET` names: a connection to the compositor that the process which
331    /// started us already opened.
332    #[cfg(feature = "wayland")]
333    wayland_socket: Option<std::os::unix::net::UnixStream>,
334}
335
336impl LinuxPlatform {
337    /// Creates the platform, headless until `run` connects it in its initial mode.
338    pub(crate) fn new(allowed_modes: WindowingModes) -> Self {
339        let startup = StartupEnvironment {
340            #[cfg(feature = "wayland")]
341            activation_token: crate::linux::take_startup_activation_token_from_environment(),
342            #[cfg(feature = "wayland")]
343            wayland_socket: crate::linux::take_wayland_socket_from_environment(),
344        };
345        let event_loop = EventLoop::try_new().expect("failed to create Linux event loop");
346        let (common, main_receiver, power_receiver) = LinuxCommon::new(event_loop.get_signal());
347        let common = Rc::new(RefCell::new(common));
348        let loop_handle = event_loop.handle();
349        LinuxCommon::register_sources(&common, &loop_handle, main_receiver, power_receiver)
350            .expect("failed to register Linux event sources");
351        Self {
352            common,
353            event_loop: RefCell::new(Some(event_loop)),
354            loop_handle,
355            connection: RefCell::new(DisplayConnection::Headless(HeadlessConnection::new())),
356            allowed_modes,
357            initial_windowing: RefCell::new(None),
358            startup_environment: RefCell::new(Some(startup)),
359            pending_mode: RefCell::new(None),
360            transition_waiter: RefCell::new(None),
361        }
362    }
363
364    /// Connects in the initial mode, before the app finishes launching.
365    ///
366    /// # Panics
367    ///
368    /// Panics if the allowed modes don't allow the initial mode, or if its display server can't
369    /// be reached, since the app can't start. A windowed initial mode whose environment names no
370    /// allowed display server starts headless when that's allowed.
371    fn connect_initially(&self) {
372        let request = self
373            .initial_windowing
374            .borrow_mut()
375            .take()
376            .unwrap_or_else(|| WindowingRequest::Windowed(GraphicalEnvironment::detect()));
377        // Dropped unused when starting headless, closing any inherited `WAYLAND_SOCKET`.
378        let startup = self
379            .startup_environment
380            .borrow_mut()
381            .take()
382            .unwrap_or_default();
383        let backend = match &request {
384            WindowingRequest::Headless => None,
385            WindowingRequest::Windowed(environment) => {
386                select_backend(self.allowed_modes, environment)
387                    .map(|backend| (backend, environment))
388            }
389        };
390        match backend {
391            Some((backend, environment)) => {
392                let connection = self
393                    .connect(backend, environment, startup)
394                    .unwrap_or_else(|error| panic!("{error:#}"));
395                self.replace_connection(connection);
396            }
397            None => assert!(
398                self.allowed_modes.contains(WindowingModes::HEADLESS),
399                "{:?} does not allow starting headless, and no allowed display server was given",
400                self.allowed_modes
401            ),
402        }
403    }
404
405    fn with_common<R>(&self, function: impl FnOnce(&mut LinuxCommon) -> R) -> R {
406        function(&mut self.common.borrow_mut())
407    }
408
409    /// Connects to the display server `backend` in `environment`.
410    ///
411    /// `startup` is only given for the connection made when the platform is created.
412    #[cfg_attr(
413        not(feature = "wayland"),
414        allow(
415            unused_variables,
416            reason = "only Wayland uses the startup environment, and without a display backend \
417                      `Backend` has no variants"
418        )
419    )]
420    fn connect(
421        &self,
422        backend: Backend,
423        environment: &GraphicalEnvironment,
424        startup: StartupEnvironment,
425    ) -> anyhow::Result<DisplayConnection> {
426        let loop_handle = self.loop_handle.clone();
427        let common = self.common.clone();
428        match backend {
429            #[cfg(feature = "wayland")]
430            Backend::Wayland => {
431                let activation_token = environment
432                    .activation_token
433                    .clone()
434                    .or(startup.activation_token);
435                Ok(DisplayConnection::Wayland(
436                    crate::linux::WaylandConnection::attach(
437                        loop_handle,
438                        common,
439                        environment,
440                        startup.wayland_socket,
441                        activation_token,
442                    )?,
443                ))
444            }
445            #[cfg(feature = "x11")]
446            Backend::X11 => Ok(DisplayConnection::X11(crate::linux::X11Connection::attach(
447                loop_handle,
448                common,
449                environment,
450            )?)),
451        }
452    }
453
454    /// Installs `connection`, then drops the previous one outside the borrow, since dropping a
455    /// connection can call back into the platform.
456    fn replace_connection(&self, connection: DisplayConnection) {
457        let previous = std::mem::replace(&mut *self.connection.borrow_mut(), connection);
458        drop(previous);
459        // The app caches the keyboard layout, which belongs to the connection.
460        let callback = self
461            .common
462            .borrow_mut()
463            .callbacks
464            .keyboard_layout_change
465            .take();
466        if let Some(mut callback) = callback {
467            callback();
468            let mut common = self.common.borrow_mut();
469            // The callback may have replaced itself.
470            if common.callbacks.keyboard_layout_change.is_none() {
471                common.callbacks.keyboard_layout_change = Some(callback);
472            }
473        }
474    }
475
476    /// Applies a pending switch.
477    ///
478    /// Runs between event-loop iterations, so no event source is mid-dispatch when the
479    /// connection's sources are added or removed.
480    fn apply_pending_mode(&self) {
481        let Some(mode) = self.pending_mode.borrow().clone() else {
482            return;
483        };
484        // `request_mode` rejects open windows, but one can be opened before this runs.
485        let result = if self.connection.borrow().has_windows() {
486            Err(anyhow!("a window was opened while switching display modes"))
487        } else {
488            match mode {
489                WindowingRequest::Headless => {
490                    self.replace_connection(DisplayConnection::Headless(HeadlessConnection::new()));
491                    Ok(())
492                }
493                WindowingRequest::Windowed(environment) => {
494                    select_backend(self.allowed_modes, &environment)
495                        .context("the environment names no allowed Wayland or X11 display server")
496                        .and_then(|backend| {
497                            self.connect(backend, &environment, StartupEnvironment::default())
498                        })
499                        .map(|connection| self.replace_connection(connection))
500                }
501            }
502        };
503        self.pending_mode.borrow_mut().take();
504        if let Some(waiter) = self.transition_waiter.borrow_mut().take() {
505            waiter.send(result).ok();
506        }
507    }
508
509    /// Validates a requested switch, then leaves it for the event loop to apply.
510    fn request_mode(&self, mode: WindowingRequest) -> Task<anyhow::Result<()>> {
511        if self.pending_mode.borrow().is_some() {
512            return Task::ready(Err(anyhow!(
513                "a display mode transition is already in progress"
514            )));
515        }
516        let is_headless = self.connection.borrow().is_headless();
517        match &mode {
518            WindowingRequest::Headless
519                if !self.allowed_modes.contains(WindowingModes::HEADLESS) =>
520            {
521                return Task::ready(Err(anyhow!(
522                    "{:?} does not allow headless mode",
523                    self.allowed_modes
524                )));
525            }
526            WindowingRequest::Headless if is_headless => {
527                return Task::ready(Err(anyhow!("already headless")));
528            }
529            WindowingRequest::Headless => {}
530            WindowingRequest::Windowed(_) if !is_headless => {
531                return Task::ready(Err(anyhow!(
532                    "already windowed ({}); switch to headless mode first",
533                    self.connection.borrow().compositor_name()
534                )));
535            }
536            WindowingRequest::Windowed(environment) => {
537                if select_backend(self.allowed_modes, environment).is_none() {
538                    return Task::ready(Err(anyhow!(
539                        "the environment names no display server that {:?} allows",
540                        self.allowed_modes
541                    )));
542                }
543            }
544        }
545        let (sender, receiver) = oneshot::channel();
546        self.transition_waiter.borrow_mut().replace(sender);
547        *self.pending_mode.borrow_mut() = Some(mode);
548        // The wakeup persists until the loop next polls, so a request made before `run` starts is
549        // applied after its first iteration.
550        self.common.borrow().signal.wakeup();
551        self.common.borrow().foreground_executor.spawn(async move {
552            receiver
553                .await
554                .map_err(|_| anyhow!("display mode transition was canceled"))
555                .and_then(|result| result)
556        })
557    }
558}
559
560impl Drop for LinuxPlatform {
561    fn drop(&mut self) {
562        self.replace_connection(DisplayConnection::Headless(HeadlessConnection::new()));
563        #[cfg(feature = "x11")]
564        crate::linux::wait_for_clipboard_handovers();
565    }
566}
567
568/// The adapters the renderer could choose between.
569///
570/// The context's own instance is used once one exists, so the list is the one the renderer can
571/// actually pick from. Before that a fresh instance enumerates; it creates no device, which is the
572/// cost this accessor exists to avoid. `try_borrow` because this can be asked during the window
573/// construction that is holding the slot mutably — returning nothing is better than panicking.
574#[cfg(any(feature = "wayland", feature = "x11"))]
575pub(crate) fn enumerate_gpus(gpu_context: &gpui_wgpu::WgpuContextSlot) -> Vec<GpuAdapterInfo> {
576    if let Ok(context) = gpu_context.try_borrow()
577        && let Some(context) = context.as_ref()
578    {
579        return gpui_wgpu::enumerate_adapters_from(&context.instance);
580    }
581    gpui_wgpu::enumerate_adapters()
582}
583
584/// The adapter the shared context renders on, once it has one.
585#[cfg(any(feature = "wayland", feature = "x11"))]
586pub(crate) fn active_gpu(gpu_context: &gpui_wgpu::WgpuContextSlot) -> Option<GpuAdapterInfo> {
587    let context = gpu_context.try_borrow().ok()?;
588    context.as_ref().map(|context| context.adapter_info())
589}
590
591/// What the renderer's device can import as a dma-buf surface.
592#[cfg(any(feature = "wayland", feature = "x11"))]
593pub(crate) fn dmabuf_capabilities(
594    gpu_context: &gpui_wgpu::WgpuContextSlot,
595) -> Option<DmaBufCapabilities> {
596    let context = gpu_context.try_borrow().ok()?;
597    context
598        .as_ref()
599        .and_then(|context| context.dmabuf_capabilities())
600}
601
602impl Platform for LinuxPlatform {
603    fn background_executor(&self) -> BackgroundExecutor {
604        self.with_common(|common| common.background_executor.clone())
605    }
606
607    fn foreground_executor(&self) -> ForegroundExecutor {
608        self.with_common(|common| common.foreground_executor.clone())
609    }
610
611    fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
612        self.with_common(|common| common.text_system.clone())
613    }
614
615    fn set_gpu_preference(&self, preference: GpuPreference) {
616        self.common.borrow_mut().gpu_preference = preference;
617    }
618
619    fn enumerate_gpus(&self) -> Vec<GpuAdapterInfo> {
620        self.connection.borrow().enumerate_gpus()
621    }
622
623    fn active_gpu(&self) -> Option<GpuAdapterInfo> {
624        self.connection.borrow().active_gpu()
625    }
626
627    fn dmabuf_capabilities(&self) -> Option<DmaBufCapabilities> {
628        self.connection.borrow().dmabuf_capabilities()
629    }
630
631    fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
632        self.connection.borrow().keyboard_layout()
633    }
634
635    fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
636        Rc::new(gpui_platform::DummyKeyboardMapper)
637    }
638
639    fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
640        self.with_common(|common| common.callbacks.keyboard_layout_change = Some(callback));
641    }
642
643    fn on_thermal_state_change(&self, _callback: Box<dyn FnMut()>) {}
644
645    fn thermal_state(&self) -> ThermalState {
646        ThermalState::Nominal
647    }
648
649    #[cfg(not(any(feature = "wayland", feature = "x11")))]
650    fn prevent_idle_sleep(&self, reason: &str) -> Task<Result<ActivityGuard>> {
651        Task::ready(Err(anyhow!(
652            "Idle sleep prevention for {reason:?} requires a Linux windowing backend"
653        )))
654    }
655
656    #[cfg(any(feature = "wayland", feature = "x11"))]
657    fn prevent_idle_sleep(&self, reason: &str) -> Task<Result<ActivityGuard>> {
658        let executor = self.background_executor();
659        let (guard_tx, guard_rx) = oneshot::channel();
660        executor
661            .spawn({
662                let executor = executor.clone();
663                let reason = reason.to_owned();
664                async move {
665                    guard_tx
666                        .send(inhibit_idle_sleep(reason, executor).await)
667                        .ok();
668                }
669            })
670            .detach();
671        executor
672            .clone()
673            .spawn(async move { await_idle_sleep_prevention(guard_rx, &executor).await })
674    }
675
676    fn run(&self, on_finish_launching: Box<dyn FnOnce()>) {
677        self.connect_initially();
678        on_finish_launching();
679
680        let mut event_loop = self
681            .event_loop
682            .borrow_mut()
683            .take()
684            .expect("App is already running");
685        event_loop
686            .run(None, &mut (), |_| self.apply_pending_mode())
687            .log_err();
688
689        let quit = self.with_common(|common| common.callbacks.quit.take());
690        if let Some(mut fun) = quit {
691            fun();
692        }
693        #[cfg(feature = "x11")]
694        crate::linux::wait_for_clipboard_handovers();
695    }
696
697    fn quit(&self) {
698        self.with_common(|common| common.signal.stop());
699    }
700
701    fn set_initial_windowing(&self, request: WindowingRequest) {
702        *self.initial_windowing.borrow_mut() = Some(request);
703    }
704
705    fn request_windowing(&self, request: WindowingRequest) -> Task<anyhow::Result<()>> {
706        self.request_mode(request)
707    }
708
709    fn compositor_name(&self) -> &'static str {
710        self.connection.borrow().compositor_name()
711    }
712
713    fn graphical_environment(&self) -> Option<GraphicalEnvironment> {
714        self.connection.borrow().graphical_environment()
715    }
716
717    fn restart(&self, binary_path: Option<PathBuf>, arguments: Vec<std::ffi::OsString>) {
718        use std::os::unix::process::CommandExt as _;
719
720        // get the process id of the current process
721        let app_pid = std::process::id().to_string();
722        // get the path to the executable
723        let app_path = if let Some(path) = binary_path {
724            path
725        } else {
726            match self.app_path() {
727                Ok(path) => path,
728                Err(err) => {
729                    log::error!("Failed to get app path: {:?}", err);
730                    return;
731                }
732            }
733        };
734
735        log::info!("Restarting process, using app path: {:?}", app_path);
736
737        // Script to wait for the current process to exit and then restart the app.
738        // Pass dynamic values as positional parameters to avoid shell interpolation issues.
739        let script = r#"
740            while kill -0 "$0" 2>/dev/null; do
741                sleep 0.1
742            done
743
744            app_path="$1"
745            shift
746            "$app_path" "$@"
747            "#;
748
749        #[allow(
750            clippy::disallowed_methods,
751            reason = "We are restarting ourselves, using std command thus is fine"
752        )]
753        let restart_process = new_std_command("/usr/bin/env")
754            .arg("bash")
755            .arg("-c")
756            .arg(script)
757            .arg(&app_pid)
758            .arg(&app_path)
759            .args(arguments)
760            .process_group(0)
761            .spawn();
762
763        match restart_process {
764            Ok(_) => self.quit(),
765            Err(e) => log::error!("failed to spawn restart script: {:?}", e),
766        }
767    }
768
769    fn activate(&self, _ignoring_other_apps: bool) {
770        log::info!("activate is not implemented on Linux, ignoring the call")
771    }
772
773    fn hide(&self) {
774        log::info!("hide is not implemented on Linux, ignoring the call")
775    }
776
777    fn hide_other_apps(&self) {
778        log::info!("hide_other_apps is not implemented on Linux, ignoring the call")
779    }
780
781    fn unhide_other_apps(&self) {
782        log::info!("unhide_other_apps is not implemented on Linux, ignoring the call")
783    }
784
785    fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
786        self.connection.borrow().primary_display()
787    }
788
789    fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
790        self.connection.borrow().displays()
791    }
792
793    #[cfg(feature = "screen-capture")]
794    fn is_screen_capture_supported(&self) -> bool {
795        self.connection.borrow().is_screen_capture_supported()
796    }
797
798    #[cfg(feature = "screen-capture")]
799    fn screen_capture_sources(
800        &self,
801    ) -> oneshot::Receiver<Result<Vec<Rc<dyn gpui_platform::ScreenCaptureSource>>>> {
802        self.connection.borrow().screen_capture_sources()
803    }
804
805    fn active_window(&self) -> Option<WindowId> {
806        self.connection.borrow().active_window()
807    }
808
809    fn window_stack(&self) -> Option<Vec<WindowId>> {
810        self.connection.borrow().window_stack()
811    }
812
813    fn open_window(
814        &self,
815        handle: WindowId,
816        options: WindowParams,
817    ) -> anyhow::Result<Box<dyn PlatformWindow>> {
818        self.connection.borrow().open_window(handle, options)
819    }
820
821    fn open_url(&self, url: &str) {
822        self.connection.borrow().open_uri(url);
823    }
824
825    fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
826        self.with_common(|common| common.callbacks.open_urls = Some(callback));
827    }
828
829    fn prompt_for_paths(
830        &self,
831        options: PathPromptOptions,
832    ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
833        let (done_tx, done_rx) = oneshot::channel();
834
835        #[cfg(not(any(feature = "wayland", feature = "x11")))]
836        let _ = (done_tx.send(Ok(None)), options);
837
838        #[cfg(any(feature = "wayland", feature = "x11"))]
839        let identifier = self.connection.borrow().window_identifier();
840
841        #[cfg(any(feature = "wayland", feature = "x11"))]
842        self.foreground_executor()
843            .spawn(async move {
844                let title = if options.directories {
845                    "Open Folder"
846                } else {
847                    "Open File"
848                };
849
850                let request = match ashpd::desktop::file_chooser::OpenFileRequest::default()
851                    .identifier(identifier.await)
852                    .modal(true)
853                    .title(title)
854                    .accept_label(
855                        options
856                            .prompt
857                            .as_ref()
858                            .map(gpui_platform::SharedString::as_str),
859                    )
860                    .multiple(options.multiple)
861                    .directory(options.directories)
862                    .send()
863                    .await
864                {
865                    Ok(request) => request,
866                    Err(err) => {
867                        let result = match err {
868                            ashpd::Error::PortalNotFound(_) => anyhow!(FILE_PICKER_PORTAL_MISSING),
869                            err => err.into(),
870                        };
871                        let _ = done_tx.send(Err(result));
872                        return;
873                    }
874                };
875
876                let result = match request.response() {
877                    Ok(response) => Ok(Some(
878                        response
879                            .uris()
880                            .iter()
881                            .filter_map(|uri: &ashpd::Uri| url::Url::parse(uri.as_str()).ok())
882                            .filter_map(|uri: url::Url| uri.to_file_path().ok())
883                            .collect::<Vec<_>>(),
884                    )),
885                    Err(ashpd::Error::Response(_)) => Ok(None),
886                    Err(e) => Err(e.into()),
887                };
888                let _ = done_tx.send(result);
889            })
890            .detach();
891        done_rx
892    }
893
894    fn prompt_for_new_path(
895        &self,
896        directory: &Path,
897        suggested_name: Option<&str>,
898    ) -> oneshot::Receiver<Result<Option<PathBuf>>> {
899        let (done_tx, done_rx) = oneshot::channel();
900
901        #[cfg(not(any(feature = "wayland", feature = "x11")))]
902        let _ = (done_tx.send(Ok(None)), directory, suggested_name);
903
904        #[cfg(any(feature = "wayland", feature = "x11"))]
905        let identifier = self.connection.borrow().window_identifier();
906
907        #[cfg(any(feature = "wayland", feature = "x11"))]
908        self.foreground_executor()
909            .spawn({
910                let directory = directory.to_owned();
911                let suggested_name = suggested_name.map(|s| s.to_owned());
912
913                async move {
914                    let mut request_builder =
915                        ashpd::desktop::file_chooser::SaveFileRequest::default()
916                            .identifier(identifier.await)
917                            .modal(true)
918                            .title("Save File")
919                            .current_folder(directory)
920                            .expect("pathbuf should not be nul terminated");
921
922                    if let Some(suggested_name) = suggested_name {
923                        request_builder = request_builder.current_name(suggested_name.as_str());
924                    }
925
926                    let request = match request_builder.send().await {
927                        Ok(request) => request,
928                        Err(err) => {
929                            let result = match err {
930                                ashpd::Error::PortalNotFound(_) => {
931                                    anyhow!(FILE_PICKER_PORTAL_MISSING)
932                                }
933                                err => err.into(),
934                            };
935                            let _ = done_tx.send(Err(result));
936                            return;
937                        }
938                    };
939
940                    let result = match request.response() {
941                        Ok(response) => Ok(response
942                            .uris()
943                            .first()
944                            .and_then(|uri: &ashpd::Uri| url::Url::parse(uri.as_str()).ok())
945                            .and_then(|uri: url::Url| uri.to_file_path().ok())),
946                        Err(ashpd::Error::Response(_)) => Ok(None),
947                        Err(e) => Err(e.into()),
948                    };
949                    let _ = done_tx.send(result);
950                }
951            })
952            .detach();
953
954        done_rx
955    }
956
957    fn can_select_mixed_files_and_dirs(&self) -> bool {
958        // org.freedesktop.portal.FileChooser only supports "pick files" and "pick directories".
959        false
960    }
961
962    fn reveal_path(&self, path: &Path) {
963        self.connection.borrow().reveal_path(path.to_owned());
964    }
965
966    fn open_with_system(&self, path: &Path) {
967        let path = path.to_owned();
968        let environment = self.connection.borrow().graphical_environment();
969        self.background_executor()
970            .spawn(async move {
971                let mut command = new_std_command("xdg-open");
972                command.arg(path);
973                if let Some(environment) = environment {
974                    environment.apply_to(&mut command);
975                }
976                #[allow(
977                    clippy::disallowed_methods,
978                    reason = "running on a background thread, so blocking is fine"
979                )]
980                command.status().context("invoking xdg-open").log_err();
981            })
982            .detach();
983    }
984
985    fn on_quit(&self, callback: Box<dyn FnMut() -> bool>) {
986        self.with_common(|common| {
987            common.callbacks.quit = Some(callback);
988        });
989    }
990
991    fn on_reopen(&self, callback: Box<dyn FnMut()>) {
992        self.with_common(|common| {
993            common.callbacks.reopen = Some(callback);
994        });
995    }
996
997    fn on_system_sleep(&self, callback: Box<dyn FnMut()>) {
998        self.with_common(|common| {
999            common.callbacks.system_sleep = Some(callback);
1000            common.start_power_listener();
1001        });
1002    }
1003
1004    fn on_system_wake(&self, callback: Box<dyn FnMut()>) {
1005        self.with_common(|common| {
1006            common.callbacks.system_wake = Some(callback);
1007            common.start_power_listener();
1008        });
1009    }
1010
1011    fn set_app_identity(&self, _identifier: &str, name: &str) {
1012        self.with_common(|common| common.app_name = Some(name.to_string()));
1013    }
1014
1015    fn show_system_notification(&self, notification: gpui_platform::SystemNotification) {
1016        self.with_common(|common| {
1017            common
1018                .system_notifications
1019                .show(common.app_name.as_deref(), notification)
1020        });
1021    }
1022
1023    fn dismiss_system_notification(&self, tag: &str) {
1024        self.with_common(|common| common.system_notifications.dismiss(tag));
1025    }
1026
1027    fn on_system_notification_response(
1028        &self,
1029        callback: Box<dyn FnMut(gpui_platform::SystemNotificationResponse)>,
1030    ) {
1031        self.with_common(|common| {
1032            let executor = common.foreground_executor.clone();
1033            common.system_notifications.on_response(&executor, callback)
1034        });
1035    }
1036
1037    fn on_app_menu_action(&self, callback: Box<dyn FnMut(MenuCommandId)>) {
1038        self.with_common(|common| {
1039            common.callbacks.app_menu_action = Some(callback);
1040        });
1041    }
1042
1043    fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>) {
1044        self.with_common(|common| {
1045            common.callbacks.will_open_app_menu = Some(callback);
1046        });
1047    }
1048
1049    fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(MenuCommandId) -> bool>) {
1050        self.with_common(|common| {
1051            common.callbacks.validate_app_menu_command = Some(callback);
1052        });
1053    }
1054
1055    fn app_path(&self) -> Result<PathBuf> {
1056        // get the path of the executable of the current process
1057        let app_path = env::current_exe()?;
1058        Ok(app_path)
1059    }
1060
1061    fn set_menus(&self, _menus: Vec<PlatformMenu>) {
1062        // todo(linux)
1063    }
1064
1065    fn set_dock_menu(&self, _menu: Vec<PlatformMenuItem>) {
1066        // todo(linux)
1067    }
1068
1069    fn path_for_auxiliary_executable(&self, _name: &str) -> Result<PathBuf> {
1070        Err(anyhow::Error::msg(
1071            "Platform<LinuxPlatform>::path_for_auxiliary_executable is not implemented yet",
1072        ))
1073    }
1074
1075    fn set_cursor_style(&self, style: CursorStyle) {
1076        self.connection.borrow().set_cursor_style(style)
1077    }
1078
1079    fn hide_cursor_until_mouse_moves(&self) {
1080        self.connection.borrow().hide_cursor_until_mouse_moves()
1081    }
1082
1083    fn is_cursor_visible(&self) -> bool {
1084        self.connection.borrow().is_cursor_visible()
1085    }
1086
1087    fn should_auto_hide_scrollbars(&self) -> bool {
1088        self.with_common(|common| common.auto_hide_scrollbars)
1089    }
1090
1091    fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>> {
1092        let url = url.to_string();
1093        let username = username.to_string();
1094        let password = password.to_vec();
1095        self.background_executor().spawn(async move {
1096            let keyring = oo7::Keyring::new().await?;
1097            keyring.unlock().await?;
1098            keyring
1099                .create_item(
1100                    KEYRING_LABEL,
1101                    &vec![("url", &url), ("username", &username)],
1102                    password,
1103                    true,
1104                )
1105                .await?;
1106            Ok(())
1107        })
1108    }
1109
1110    fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
1111        let url = url.to_string();
1112        self.background_executor().spawn(async move {
1113            let keyring = oo7::Keyring::new().await?;
1114            keyring.unlock().await?;
1115
1116            let items = keyring.search_items(&vec![("url", &url)]).await?;
1117
1118            for item in items.into_iter() {
1119                if item.label().await.is_ok_and(|label| label == KEYRING_LABEL) {
1120                    let attributes = item.attributes().await?;
1121                    let username = attributes
1122                        .get("username")
1123                        .context("Cannot find username in stored credentials")?;
1124                    item.unlock().await?;
1125                    let secret = item.secret().await?;
1126
1127                    // we lose the zeroizing capabilities at this boundary,
1128                    // a current limitation GPUI's credentials api
1129                    return Ok(Some((username.to_string(), secret.to_vec())));
1130                } else {
1131                    continue;
1132                }
1133            }
1134            Ok(None)
1135        })
1136    }
1137
1138    fn delete_credentials(&self, url: &str) -> Task<Result<()>> {
1139        let url = url.to_string();
1140        self.background_executor().spawn(async move {
1141            let keyring = oo7::Keyring::new().await?;
1142            keyring.unlock().await?;
1143
1144            let items = keyring.search_items(&vec![("url", &url)]).await?;
1145
1146            for item in items.into_iter() {
1147                if item.label().await.is_ok_and(|label| label == KEYRING_LABEL) {
1148                    item.delete().await?;
1149                    return Ok(());
1150                }
1151            }
1152
1153            Ok(())
1154        })
1155    }
1156
1157    fn window_appearance(&self) -> WindowAppearance {
1158        self.with_common(|common| common.appearance)
1159    }
1160
1161    fn button_layout(&self) -> Option<WindowButtonLayout> {
1162        Some(self.with_common(|common| common.button_layout))
1163    }
1164
1165    fn register_url_scheme(&self, _: &str) -> Task<anyhow::Result<()>> {
1166        Task::ready(Err(anyhow!("register_url_scheme unimplemented")))
1167    }
1168
1169    fn write_to_primary(&self, item: ClipboardItem) {
1170        self.connection.borrow().write_to_primary(item)
1171    }
1172
1173    fn write_to_clipboard(&self, item: ClipboardItem) {
1174        self.connection.borrow().write_to_clipboard(item)
1175    }
1176
1177    fn read_from_primary(&self) -> Option<ClipboardItem> {
1178        self.connection.borrow().read_from_primary()
1179    }
1180
1181    fn read_from_clipboard(&self) -> Option<ClipboardItem> {
1182        self.connection.borrow().read_from_clipboard()
1183    }
1184
1185    fn add_recent_document(&self, _path: &Path) {}
1186}
1187
1188/// Opens `target` with the first of the desktop's launchers that succeeds. Returns whether one
1189/// did.
1190#[cfg(any(feature = "wayland", feature = "x11"))]
1191async fn run_open_commands(
1192    target: &std::ffi::OsStr,
1193    activation_token: Option<&str>,
1194    environment: &GraphicalEnvironment,
1195) -> bool {
1196    for mut command in open::commands(target) {
1197        environment.apply_to(&mut command);
1198        if let Some(token) = activation_token {
1199            command.env("XDG_ACTIVATION_TOKEN", token);
1200        }
1201        let program = format!("{:?}", command.get_program());
1202        match smol::process::Command::from(command).spawn() {
1203            Ok(mut child) => match child.status().await {
1204                Ok(status) if status.success() => return true,
1205                Ok(status) => log::error!("Command {} exited with status: {}", program, status),
1206                Err(error) => log::error!("Failed to get status from {}: {}", program, error),
1207            },
1208            Err(error) => log::error!("Failed to open with {}: {}", program, error),
1209        }
1210    }
1211    false
1212}
1213
1214#[cfg(any(feature = "wayland", feature = "x11"))]
1215pub(super) fn open_uri_internal(
1216    executor: BackgroundExecutor,
1217    uri: &str,
1218    activation_token: Option<String>,
1219    environment: GraphicalEnvironment,
1220) {
1221    if let Some(uri) = ashpd::Uri::parse(uri).log_err() {
1222        executor
1223            .spawn(async move {
1224                let opened = run_open_commands(
1225                    uri.as_str().as_ref(),
1226                    activation_token.as_deref(),
1227                    &environment,
1228                )
1229                .await;
1230                if !opened {
1231                    match ashpd::desktop::open_uri::OpenFileRequest::default()
1232                        .activation_token(activation_token.map(ashpd::ActivationToken::from))
1233                        .send_uri(&uri)
1234                        .await
1235                        .and_then(|e| e.response())
1236                    {
1237                        Ok(()) => {}
1238                        Err(ashpd::Error::Response(ashpd::desktop::ResponseError::Cancelled)) => {}
1239                        Err(e) => {
1240                            log::error!("Failed to open with dbus: {}", e);
1241                        }
1242                    }
1243                }
1244            })
1245            .detach();
1246    }
1247}
1248
1249#[cfg(any(feature = "x11", feature = "wayland"))]
1250pub(super) fn reveal_path_internal(
1251    executor: BackgroundExecutor,
1252    path: PathBuf,
1253    activation_token: Option<String>,
1254    environment: GraphicalEnvironment,
1255) {
1256    executor
1257        .spawn(async move {
1258            if let Some(dir) = File::open(path.clone()).log_err() {
1259                match ashpd::desktop::open_uri::OpenDirectoryRequest::default()
1260                    .activation_token(activation_token.clone().map(ashpd::ActivationToken::from))
1261                    .send(&dir.as_fd())
1262                    .await
1263                {
1264                    Ok(_) => return,
1265                    Err(e) => log::error!("Failed to open with dbus: {}", e),
1266                }
1267                let directory = if path.is_dir() {
1268                    path.as_path()
1269                } else {
1270                    path.parent().unwrap_or(Path::new(""))
1271                };
1272                run_open_commands(
1273                    directory.as_os_str(),
1274                    activation_token.as_deref(),
1275                    &environment,
1276                )
1277                .await;
1278            }
1279        })
1280        .detach();
1281}
1282
1283#[cfg(any(feature = "wayland", feature = "x11"))]
1284pub(super) fn is_within_click_distance(a: Point<Pixels>, b: Point<Pixels>) -> bool {
1285    let diff = a - b;
1286    diff.x.abs() <= DOUBLE_CLICK_DISTANCE && diff.y.abs() <= DOUBLE_CLICK_DISTANCE
1287}
1288
1289/// Creates an XKB context for keymaps supplied by Wayland or X11.
1290///
1291/// Server keymaps are already resolved and need no local keyboard definitions.
1292/// Loading default include paths can fail on systems without those files.
1293#[cfg(any(feature = "wayland", feature = "x11"))]
1294pub(super) fn new_xkb_context() -> anyhow::Result<xkb::Context> {
1295    validate_xkb_context(xkb::Context::new(xkb::CONTEXT_NO_DEFAULT_INCLUDES))
1296}
1297
1298#[cfg(any(feature = "wayland", feature = "x11"))]
1299fn validate_xkb_context(context: xkb::Context) -> anyhow::Result<xkb::Context> {
1300    ensure!(
1301        !context.get_raw_ptr().is_null(),
1302        "libxkbcommon failed to create an XKB context"
1303    );
1304    Ok(context)
1305}
1306
1307#[cfg(any(feature = "wayland", feature = "x11"))]
1308pub(super) fn get_xkb_compose_state(cx: &xkb::Context) -> Option<xkb::compose::State> {
1309    let mut locales = Vec::default();
1310    if let Some(locale) = env::var_os("LC_CTYPE") {
1311        locales.push(locale);
1312    }
1313    locales.push(OsString::from("C"));
1314    let mut state: Option<xkb::compose::State> = None;
1315    for locale in locales {
1316        if let Ok(table) =
1317            xkb::compose::Table::new_from_locale(cx, &locale, xkb::compose::COMPILE_NO_FLAGS)
1318        {
1319            state = Some(xkb::compose::State::new(
1320                &table,
1321                xkb::compose::STATE_NO_FLAGS,
1322            ));
1323            break;
1324        }
1325    }
1326    state
1327}
1328
1329#[cfg(any(feature = "wayland", feature = "x11"))]
1330pub(super) const PIPE_READ_TIMEOUT: Duration = Duration::from_secs(4);
1331
1332#[cfg(any(feature = "wayland", feature = "x11"))]
1333pub(super) fn read_fd_with_timeout(
1334    mut fd: filedescriptor::FileDescriptor,
1335    timeout: Duration,
1336) -> Result<Vec<u8>> {
1337    fd.set_non_blocking(true)?;
1338    let mut buffer = Vec::new();
1339    let mut chunk = [0u8; 8192];
1340    loop {
1341        let mut poll_fds = [filedescriptor::pollfd {
1342            fd: fd.as_raw_fd(),
1343            events: filedescriptor::POLLIN,
1344            revents: 0,
1345        }];
1346        let ready = match filedescriptor::poll(&mut poll_fds, Some(timeout)) {
1347            Ok(ready) => ready,
1348            Err(filedescriptor::Error::Poll(err))
1349                if err.kind() == std::io::ErrorKind::Interrupted =>
1350            {
1351                continue;
1352            }
1353            Err(err) => return Err(err.into()),
1354        };
1355        if ready == 0 {
1356            anyhow::bail!("timed out waiting for data on pipe after {timeout:?}");
1357        }
1358        match fd.read(&mut chunk) {
1359            Ok(0) => return Ok(buffer),
1360            Ok(len) => buffer.extend_from_slice(&chunk[..len]),
1361            Err(err)
1362                if err.kind() == std::io::ErrorKind::WouldBlock
1363                    || err.kind() == std::io::ErrorKind::Interrupted => {}
1364            Err(err) => return Err(err.into()),
1365        }
1366    }
1367}
1368
1369#[cfg(any(feature = "wayland", feature = "x11"))]
1370pub(super) const DEFAULT_CURSOR_ICON_NAME: &str = "left_ptr";
1371
1372#[cfg(any(feature = "wayland", feature = "x11"))]
1373pub(super) fn cursor_style_to_icon_names(style: CursorStyle) -> &'static [&'static str] {
1374    // Based on cursor names from chromium:
1375    // https://github.com/chromium/chromium/blob/d3069cf9c973dc3627fa75f64085c6a86c8f41bf/ui/base/cursor/cursor_factory.cc#L113
1376    match style {
1377        CursorStyle::Arrow => &[DEFAULT_CURSOR_ICON_NAME],
1378        CursorStyle::IBeam => &["text", "xterm"],
1379        CursorStyle::Crosshair => &["crosshair", "cross"],
1380        CursorStyle::ClosedHand => &["closedhand", "grabbing", "hand2"],
1381        CursorStyle::OpenHand => &["openhand", "grab", "hand1"],
1382        CursorStyle::PointingHand => &["pointer", "hand", "hand2"],
1383        CursorStyle::ResizeLeft => &["w-resize", "left_side"],
1384        CursorStyle::ResizeRight => &["e-resize", "right_side"],
1385        CursorStyle::ResizeLeftRight => &["ew-resize", "sb_h_double_arrow"],
1386        CursorStyle::ResizeUp => &["n-resize", "top_side"],
1387        CursorStyle::ResizeDown => &["s-resize", "bottom_side"],
1388        CursorStyle::ResizeUpDown => &["sb_v_double_arrow", "ns-resize"],
1389        CursorStyle::ResizeUpLeftDownRight => &["size_fdiag", "bd_double_arrow", "nwse-resize"],
1390        CursorStyle::ResizeUpRightDownLeft => &["size_bdiag", "nesw-resize", "fd_double_arrow"],
1391        CursorStyle::ResizeColumn => &["col-resize", "sb_h_double_arrow"],
1392        CursorStyle::ResizeRow => &["row-resize", "sb_v_double_arrow"],
1393        CursorStyle::IBeamCursorForVerticalLayout => &["vertical-text"],
1394        CursorStyle::OperationNotAllowed => &["not-allowed", "crossed_circle"],
1395        CursorStyle::DragLink => &["alias"],
1396        CursorStyle::DragCopy => &["copy"],
1397        CursorStyle::ContextualMenu => &["context-menu"],
1398    }
1399}
1400
1401#[cfg(any(feature = "wayland", feature = "x11"))]
1402pub(super) fn log_cursor_icon_warning(message: impl std::fmt::Display) {
1403    if let Ok(xcursor_path) = env::var("XCURSOR_PATH") {
1404        log::warn!(
1405            "{:#}\ncursor icon loading may be failing if XCURSOR_PATH environment variable is invalid. \
1406                    XCURSOR_PATH overrides the default icon search. Its current value is '{}'",
1407            message,
1408            xcursor_path
1409        );
1410    } else {
1411        log::warn!("{:#}", message);
1412    }
1413}
1414
1415#[cfg(any(feature = "wayland", feature = "x11"))]
1416fn guess_ascii(keycode: Keycode, shift: bool) -> Option<char> {
1417    let c = match (keycode.raw(), shift) {
1418        (24, _) => 'q',
1419        (25, _) => 'w',
1420        (26, _) => 'e',
1421        (27, _) => 'r',
1422        (28, _) => 't',
1423        (29, _) => 'y',
1424        (30, _) => 'u',
1425        (31, _) => 'i',
1426        (32, _) => 'o',
1427        (33, _) => 'p',
1428        (34, false) => '[',
1429        (34, true) => '{',
1430        (35, false) => ']',
1431        (35, true) => '}',
1432        (38, _) => 'a',
1433        (39, _) => 's',
1434        (40, _) => 'd',
1435        (41, _) => 'f',
1436        (42, _) => 'g',
1437        (43, _) => 'h',
1438        (44, _) => 'j',
1439        (45, _) => 'k',
1440        (46, _) => 'l',
1441        (47, false) => ';',
1442        (47, true) => ':',
1443        (48, false) => '\'',
1444        (48, true) => '"',
1445        (49, false) => '`',
1446        (49, true) => '~',
1447        (51, false) => '\\',
1448        (51, true) => '|',
1449        (52, _) => 'z',
1450        (53, _) => 'x',
1451        (54, _) => 'c',
1452        (55, _) => 'v',
1453        (56, _) => 'b',
1454        (57, _) => 'n',
1455        (58, _) => 'm',
1456        (59, false) => ',',
1457        (59, true) => '>',
1458        (60, false) => '.',
1459        (60, true) => '<',
1460        (61, false) => '/',
1461        (61, true) => '?',
1462
1463        _ => return None,
1464    };
1465
1466    Some(c)
1467}
1468
1469#[cfg(any(feature = "wayland", feature = "x11"))]
1470pub(super) fn keystroke_from_xkb(
1471    state: &State,
1472    mut modifiers: gpui_platform::Modifiers,
1473    keycode: Keycode,
1474) -> gpui_platform::Keystroke {
1475    let key_utf32 = state.key_get_utf32(keycode);
1476    let key_utf8 = state.key_get_utf8(keycode);
1477    let key_sym = state.key_get_one_sym(keycode);
1478
1479    let key = match key_sym {
1480        Keysym::Return => "enter".to_owned(),
1481        Keysym::Prior => "pageup".to_owned(),
1482        Keysym::Next => "pagedown".to_owned(),
1483        Keysym::ISO_Left_Tab => "tab".to_owned(),
1484        Keysym::KP_Prior => "pageup".to_owned(),
1485        Keysym::KP_Next => "pagedown".to_owned(),
1486        Keysym::XF86_Back => "back".to_owned(),
1487        Keysym::XF86_Forward => "forward".to_owned(),
1488        Keysym::XF86_Cut => "cut".to_owned(),
1489        Keysym::XF86_Copy => "copy".to_owned(),
1490        Keysym::XF86_Paste => "paste".to_owned(),
1491        Keysym::XF86_New => "new".to_owned(),
1492        Keysym::XF86_Open => "open".to_owned(),
1493        Keysym::XF86_Save => "save".to_owned(),
1494
1495        Keysym::comma => ",".to_owned(),
1496        Keysym::period => ".".to_owned(),
1497        Keysym::less => "<".to_owned(),
1498        Keysym::greater => ">".to_owned(),
1499        Keysym::slash => "/".to_owned(),
1500        Keysym::question => "?".to_owned(),
1501
1502        Keysym::semicolon => ";".to_owned(),
1503        Keysym::colon => ":".to_owned(),
1504        Keysym::apostrophe => "'".to_owned(),
1505        Keysym::quotedbl => "\"".to_owned(),
1506
1507        Keysym::bracketleft => "[".to_owned(),
1508        Keysym::braceleft => "{".to_owned(),
1509        Keysym::bracketright => "]".to_owned(),
1510        Keysym::braceright => "}".to_owned(),
1511        Keysym::backslash => "\\".to_owned(),
1512        Keysym::bar => "|".to_owned(),
1513
1514        Keysym::grave => "`".to_owned(),
1515        Keysym::asciitilde => "~".to_owned(),
1516        Keysym::exclam => "!".to_owned(),
1517        Keysym::at => "@".to_owned(),
1518        Keysym::numbersign => "#".to_owned(),
1519        Keysym::dollar => "$".to_owned(),
1520        Keysym::percent => "%".to_owned(),
1521        Keysym::asciicircum => "^".to_owned(),
1522        Keysym::ampersand => "&".to_owned(),
1523        Keysym::asterisk => "*".to_owned(),
1524        Keysym::parenleft => "(".to_owned(),
1525        Keysym::parenright => ")".to_owned(),
1526        Keysym::minus => "-".to_owned(),
1527        Keysym::underscore => "_".to_owned(),
1528        Keysym::equal => "=".to_owned(),
1529        Keysym::plus => "+".to_owned(),
1530        Keysym::space => "space".to_owned(),
1531        Keysym::BackSpace => "backspace".to_owned(),
1532        Keysym::Tab => "tab".to_owned(),
1533        Keysym::Delete => "delete".to_owned(),
1534        Keysym::Escape => "escape".to_owned(),
1535
1536        Keysym::Left => "left".to_owned(),
1537        Keysym::Right => "right".to_owned(),
1538        Keysym::Up => "up".to_owned(),
1539        Keysym::Down => "down".to_owned(),
1540        Keysym::Home => "home".to_owned(),
1541        Keysym::End => "end".to_owned(),
1542        Keysym::Insert => "insert".to_owned(),
1543
1544        _ => {
1545            let name = xkb::keysym_get_name(key_sym).to_lowercase();
1546            if key_sym.is_keypad_key() {
1547                name.replace("kp_", "")
1548            } else if let Some(key) = key_utf8.chars().next()
1549                && key_utf8.len() == 1
1550                && key.is_ascii()
1551            {
1552                if key.is_ascii_graphic() {
1553                    key_utf8.to_lowercase()
1554                // map ctrl-a to `a`
1555                // ctrl-0..9 may emit control codes like ctrl-[, but
1556                // we don't want to map them to `[`
1557                } else if key_utf32 <= 0x1f
1558                    && !name.chars().next().is_some_and(|c| c.is_ascii_digit())
1559                {
1560                    ((key_utf32 as u8 + 0x40) as char)
1561                        .to_ascii_lowercase()
1562                        .to_string()
1563                } else {
1564                    name
1565                }
1566            } else if let Some(key_en) = guess_ascii(keycode, modifiers.shift) {
1567                String::from(key_en)
1568            } else {
1569                name
1570            }
1571        }
1572    };
1573
1574    if modifiers.shift {
1575        // we only include the shift for upper-case letters by convention,
1576        // so don't include for numbers and symbols, but do include for
1577        // tab/enter, etc.
1578        if key.chars().count() == 1 && key.to_lowercase() == key.to_uppercase() {
1579            modifiers.shift = false;
1580        }
1581    }
1582
1583    // Ignore control characters (and DEL) for the purposes of key_char
1584    let key_char =
1585        (key_utf32 >= 32 && key_utf32 != 127 && !key_utf8.is_empty()).then_some(key_utf8);
1586
1587    gpui_platform::Keystroke {
1588        modifiers,
1589        key,
1590        key_char,
1591    }
1592}
1593
1594/**
1595 * Returns which symbol the dead key represents
1596 * <https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values#dead_keycodes_for_linux>
1597 */
1598#[cfg(any(feature = "wayland", feature = "x11"))]
1599pub fn keystroke_underlying_dead_key(keysym: Keysym) -> Option<String> {
1600    match keysym {
1601        Keysym::dead_grave => Some("`".to_owned()),
1602        Keysym::dead_acute => Some("´".to_owned()),
1603        Keysym::dead_circumflex => Some("^".to_owned()),
1604        Keysym::dead_tilde => Some("~".to_owned()),
1605        Keysym::dead_macron => Some("¯".to_owned()),
1606        Keysym::dead_breve => Some("˘".to_owned()),
1607        Keysym::dead_abovedot => Some("˙".to_owned()),
1608        Keysym::dead_diaeresis => Some("¨".to_owned()),
1609        Keysym::dead_abovering => Some("˚".to_owned()),
1610        Keysym::dead_doubleacute => Some("˝".to_owned()),
1611        Keysym::dead_caron => Some("ˇ".to_owned()),
1612        Keysym::dead_cedilla => Some("¸".to_owned()),
1613        Keysym::dead_ogonek => Some("˛".to_owned()),
1614        Keysym::dead_iota => Some("ͅ".to_owned()),
1615        Keysym::dead_voiced_sound => Some("゙".to_owned()),
1616        Keysym::dead_semivoiced_sound => Some("゚".to_owned()),
1617        Keysym::dead_belowdot => Some("̣̣".to_owned()),
1618        Keysym::dead_hook => Some("̡".to_owned()),
1619        Keysym::dead_horn => Some("̛".to_owned()),
1620        Keysym::dead_stroke => Some("̶̶".to_owned()),
1621        Keysym::dead_abovecomma => Some("̓̓".to_owned()),
1622        Keysym::dead_abovereversedcomma => Some("ʽ".to_owned()),
1623        Keysym::dead_doublegrave => Some("̏".to_owned()),
1624        Keysym::dead_belowring => Some("˳".to_owned()),
1625        Keysym::dead_belowmacron => Some("̱".to_owned()),
1626        Keysym::dead_belowcircumflex => Some("ꞈ".to_owned()),
1627        Keysym::dead_belowtilde => Some("̰".to_owned()),
1628        Keysym::dead_belowbreve => Some("̮".to_owned()),
1629        Keysym::dead_belowdiaeresis => Some("̤".to_owned()),
1630        Keysym::dead_invertedbreve => Some("̯".to_owned()),
1631        Keysym::dead_belowcomma => Some("̦".to_owned()),
1632        Keysym::dead_currency => None,
1633        Keysym::dead_lowline => None,
1634        Keysym::dead_aboveverticalline => None,
1635        Keysym::dead_belowverticalline => None,
1636        Keysym::dead_longsolidusoverlay => None,
1637        Keysym::dead_a => None,
1638        Keysym::dead_A => None,
1639        Keysym::dead_e => None,
1640        Keysym::dead_E => None,
1641        Keysym::dead_i => None,
1642        Keysym::dead_I => None,
1643        Keysym::dead_o => None,
1644        Keysym::dead_O => None,
1645        Keysym::dead_u => None,
1646        Keysym::dead_U => None,
1647        Keysym::dead_small_schwa => Some("ə".to_owned()),
1648        Keysym::dead_capital_schwa => Some("Ə".to_owned()),
1649        Keysym::dead_greek => None,
1650        _ => None,
1651    }
1652}
1653#[cfg(any(feature = "wayland", feature = "x11"))]
1654pub(super) fn modifiers_from_xkb(keymap_state: &State) -> gpui_platform::Modifiers {
1655    let shift = keymap_state.mod_name_is_active(xkb::MOD_NAME_SHIFT, xkb::STATE_MODS_EFFECTIVE);
1656    let alt = keymap_state.mod_name_is_active(xkb::MOD_NAME_ALT, xkb::STATE_MODS_EFFECTIVE);
1657    let control = keymap_state.mod_name_is_active(xkb::MOD_NAME_CTRL, xkb::STATE_MODS_EFFECTIVE);
1658    let platform = keymap_state.mod_name_is_active(xkb::MOD_NAME_LOGO, xkb::STATE_MODS_EFFECTIVE);
1659    gpui_platform::Modifiers {
1660        shift,
1661        alt,
1662        control,
1663        platform,
1664        function: false,
1665    }
1666}
1667
1668#[cfg(any(feature = "wayland", feature = "x11"))]
1669pub(super) fn capslock_from_xkb(keymap_state: &State) -> gpui_platform::Capslock {
1670    let on = keymap_state.mod_name_is_active(xkb::MOD_NAME_CAPS, xkb::STATE_MODS_EFFECTIVE);
1671    gpui_platform::Capslock { on }
1672}
1673
1674/// Resolve a Linux `dev_t` to PCI vendor/device IDs via sysfs, returning a
1675/// [`CompositorGpuHint`] that the GPU adapter selection code can use to
1676/// prioritize the compositor's rendering device.
1677#[cfg(any(feature = "wayland", feature = "x11"))]
1678pub(super) fn compositor_gpu_hint_from_dev_t(dev: u64) -> Option<gpui_wgpu::CompositorGpuHint> {
1679    fn dev_major(dev: u64) -> u32 {
1680        ((dev >> 8) & 0xfff) as u32 | (((dev >> 32) & !0xfff) as u32)
1681    }
1682
1683    fn dev_minor(dev: u64) -> u32 {
1684        (dev & 0xff) as u32 | (((dev >> 12) & !0xff) as u32)
1685    }
1686
1687    fn read_sysfs_hex_id(path: &str) -> Option<u32> {
1688        let content = std::fs::read_to_string(path).ok()?;
1689        let trimmed = content.trim().strip_prefix("0x").unwrap_or(content.trim());
1690        u32::from_str_radix(trimmed, 16).ok()
1691    }
1692
1693    let major = dev_major(dev);
1694    let minor = dev_minor(dev);
1695
1696    let vendor_path = format!("/sys/dev/char/{major}:{minor}/device/vendor");
1697    let device_path = format!("/sys/dev/char/{major}:{minor}/device/device");
1698
1699    let vendor_id = read_sysfs_hex_id(&vendor_path)?;
1700    let device_id = read_sysfs_hex_id(&device_path)?;
1701
1702    log::info!(
1703        "Compositor GPU hint: vendor={:#06x}, device={:#06x} (from dev {major}:{minor})",
1704        vendor_id,
1705        device_id,
1706    );
1707
1708    Some(gpui_wgpu::CompositorGpuHint {
1709        vendor_id,
1710        device_id,
1711    })
1712}
1713
1714#[cfg(any(feature = "wayland", feature = "x11"))]
1715async fn inhibit_idle_sleep(reason: String, executor: BackgroundExecutor) -> Result<ActivityGuard> {
1716    let proxy = InhibitProxy::new()
1717        .await
1718        .context("Idle sleep prevention portal is unavailable")?;
1719    let request = proxy
1720        .inhibit(
1721            None,
1722            BitFlags::from(InhibitFlags::Suspend),
1723            InhibitOptions::default().set_reason(reason.as_str()),
1724        )
1725        .await
1726        .context("Failed to request idle sleep prevention")?;
1727    request
1728        .response()
1729        .context("Idle sleep prevention request was rejected")?;
1730    Ok(release_on_drop(request, executor))
1731}
1732
1733#[cfg(any(feature = "wayland", feature = "x11"))]
1734fn release_on_drop(request: Request<()>, executor: BackgroundExecutor) -> ActivityGuard {
1735    ActivityGuard::new(move || {
1736        executor
1737            .spawn(async move {
1738                request
1739                    .close()
1740                    .await
1741                    .context("Failed to release idle sleep prevention")
1742                    .log_err();
1743            })
1744            .detach();
1745    })
1746}
1747
1748#[cfg(any(feature = "wayland", feature = "x11"))]
1749async fn await_idle_sleep_prevention(
1750    guard_rx: oneshot::Receiver<Result<ActivityGuard>>,
1751    executor: &BackgroundExecutor,
1752) -> Result<ActivityGuard> {
1753    match futures::future::select(guard_rx, executor.timer(Duration::from_secs(10))).await {
1754        futures::future::Either::Left((Ok(result), _)) => result,
1755        futures::future::Either::Left((Err(_), _)) => {
1756            Err(anyhow!("Idle sleep prevention request was abandoned"))
1757        }
1758        futures::future::Either::Right(_) => Err(anyhow!(
1759            "Idle sleep prevention acquisition timed out after 10 seconds"
1760        )),
1761    }
1762}
1763
1764#[cfg(test)]
1765mod tests {
1766    use super::*;
1767    use gpui_platform::{Point, px};
1768
1769    #[cfg(any(feature = "wayland", feature = "x11"))]
1770    #[test]
1771    fn rejects_null_xkb_context() {
1772        let context = unsafe {
1773            // libxkbcommon permits unref on null, matching the value returned by Context::new on failure.
1774            xkb::Context::from_raw_ptr(std::ptr::null_mut())
1775        };
1776        let error = validate_xkb_context(context)
1777            .err()
1778            .expect("null XKB context should be rejected");
1779
1780        assert_eq!(
1781            error.to_string(),
1782            "libxkbcommon failed to create an XKB context"
1783        );
1784    }
1785
1786    #[test]
1787    fn test_is_within_click_distance() {
1788        let zero = Point::new(px(0.0), px(0.0));
1789        assert!(is_within_click_distance(zero, Point::new(px(5.0), px(5.0))));
1790        assert!(is_within_click_distance(
1791            zero,
1792            Point::new(px(-4.9), px(5.0))
1793        ));
1794        assert!(is_within_click_distance(
1795            Point::new(px(3.0), px(2.0)),
1796            Point::new(px(-2.0), px(-2.0))
1797        ));
1798        assert!(!is_within_click_distance(
1799            zero,
1800            Point::new(px(5.0), px(5.1))
1801        ),);
1802    }
1803
1804    #[cfg(any(feature = "wayland", feature = "x11"))]
1805    mod idle_sleep_prevention {
1806        use super::super::await_idle_sleep_prevention;
1807        use anyhow::{Result, anyhow};
1808        use futures::channel::oneshot;
1809        use gpui::{ActivityGuard, TestAppContext};
1810        use std::{
1811            sync::{
1812                Arc,
1813                atomic::{AtomicUsize, Ordering::SeqCst},
1814            },
1815            time::Duration,
1816        };
1817
1818        #[gpui::test]
1819        async fn guard_passes_through_and_releases_on_drop(cx: &mut TestAppContext) {
1820            let released = Arc::new(AtomicUsize::new(0));
1821            let (guard_tx, guard_rx) = oneshot::channel();
1822            assert!(guard_tx.send(Ok(release_guard(released.clone()))).is_ok());
1823            let guard = await_idle_sleep_prevention(guard_rx, &cx.background_executor)
1824                .await
1825                .expect("acquisition should succeed");
1826
1827            assert_eq!(released.load(SeqCst), 0);
1828            drop(guard);
1829            assert_eq!(released.load(SeqCst), 1);
1830        }
1831
1832        #[gpui::test]
1833        async fn acquisition_error_passes_through(cx: &mut TestAppContext) {
1834            let (guard_tx, guard_rx) = oneshot::channel::<Result<ActivityGuard>>();
1835            assert!(guard_tx.send(Err(anyhow!("inhibition rejected"))).is_ok());
1836            assert_eq!(
1837                await_idle_sleep_prevention(guard_rx, &cx.background_executor)
1838                    .await
1839                    .err()
1840                    .expect("acquisition should fail")
1841                    .to_string(),
1842                "inhibition rejected"
1843            );
1844        }
1845
1846        #[gpui::test]
1847        async fn abandoned_acquisition_is_an_error(cx: &mut TestAppContext) {
1848            let (guard_tx, guard_rx) = oneshot::channel::<Result<ActivityGuard>>();
1849            drop(guard_tx);
1850            assert_eq!(
1851                await_idle_sleep_prevention(guard_rx, &cx.background_executor)
1852                    .await
1853                    .err()
1854                    .expect("acquisition should fail")
1855                    .to_string(),
1856                "Idle sleep prevention request was abandoned"
1857            );
1858        }
1859
1860        #[gpui::test]
1861        async fn late_guard_after_timeout_is_released(cx: &mut TestAppContext) {
1862            let released = Arc::new(AtomicUsize::new(0));
1863            let (guard_tx, guard_rx) = oneshot::channel();
1864            let task = cx.background_executor.spawn({
1865                let executor = cx.background_executor.clone();
1866                async move { await_idle_sleep_prevention(guard_rx, &executor).await }
1867            });
1868            cx.run_until_parked();
1869            cx.executor().advance_clock(Duration::from_secs(9));
1870            cx.run_until_parked();
1871            assert!(!guard_tx.is_canceled());
1872            cx.executor().advance_clock(Duration::from_secs(1));
1873            assert_eq!(
1874                task.await
1875                    .err()
1876                    .expect("acquisition should time out")
1877                    .to_string(),
1878                "Idle sleep prevention acquisition timed out after 10 seconds"
1879            );
1880
1881            assert!(guard_tx.is_canceled());
1882            if let Err(Ok(guard)) = guard_tx.send(Ok(release_guard(released.clone()))) {
1883                assert_eq!(released.load(SeqCst), 0);
1884                drop(guard);
1885            }
1886            assert_eq!(released.load(SeqCst), 1);
1887        }
1888
1889        fn release_guard(released: Arc<AtomicUsize>) -> ActivityGuard {
1890            ActivityGuard::new(move || {
1891                released.fetch_add(1, SeqCst);
1892            })
1893        }
1894    }
1895
1896    #[cfg(any(feature = "wayland", feature = "x11"))]
1897    mod read_fd_with_timeout {
1898        use super::super::{PIPE_READ_TIMEOUT, read_fd_with_timeout};
1899        use std::io::Write as _;
1900        use std::time::{Duration, Instant};
1901
1902        #[test]
1903        fn reads_data_written_before_close() {
1904            let mut pipe = filedescriptor::Pipe::new().unwrap();
1905            pipe.write.write_all(b"hello clipboard").unwrap();
1906            drop(pipe.write);
1907
1908            let bytes = read_fd_with_timeout(pipe.read, PIPE_READ_TIMEOUT).unwrap();
1909            assert_eq!(bytes, b"hello clipboard");
1910        }
1911
1912        #[test]
1913        fn returns_empty_when_writer_closes_without_writing() {
1914            let pipe = filedescriptor::Pipe::new().unwrap();
1915            drop(pipe.write);
1916
1917            let bytes = read_fd_with_timeout(pipe.read, PIPE_READ_TIMEOUT).unwrap();
1918            assert!(bytes.is_empty());
1919        }
1920
1921        #[test]
1922        fn times_out_when_writer_never_writes() {
1923            let pipe = filedescriptor::Pipe::new().unwrap();
1924            let _open_writer = pipe.write;
1925
1926            let timeout = Duration::from_millis(50);
1927            let started = Instant::now();
1928            let result = read_fd_with_timeout(pipe.read, timeout);
1929            let elapsed = started.elapsed();
1930
1931            let err = result.unwrap_err();
1932            assert!(
1933                err.to_string().contains("timed out"),
1934                "unexpected error: {err}"
1935            );
1936            assert!(elapsed >= timeout, "returned before the timeout elapsed");
1937        }
1938
1939        #[test]
1940        fn times_out_when_writer_stalls_after_partial_write() {
1941            let mut pipe = filedescriptor::Pipe::new().unwrap();
1942            pipe.write.write_all(b"partial").unwrap();
1943            let _open_writer = pipe.write;
1944
1945            let err = read_fd_with_timeout(pipe.read, Duration::from_millis(50)).unwrap_err();
1946            assert!(
1947                err.to_string().contains("timed out"),
1948                "unexpected error: {err}"
1949            );
1950        }
1951
1952        #[test]
1953        fn slow_writer_resets_deadline_between_chunks() {
1954            let pipe = filedescriptor::Pipe::new().unwrap();
1955            let chunks = 12;
1956            let gap = Duration::from_millis(40);
1957            let timeout = Duration::from_millis(400);
1958
1959            let writer = std::thread::spawn({
1960                let mut write = pipe.write;
1961                move || {
1962                    for _ in 0..chunks {
1963                        std::thread::sleep(gap);
1964                        write.write_all(&[b'x'; 1000]).unwrap();
1965                    }
1966                }
1967            });
1968            // The total transfer (~480ms) exceeds the timeout; this only
1969            // passes because the timeout is re-armed per chunk.
1970            let bytes = read_fd_with_timeout(pipe.read, timeout).unwrap();
1971            writer.join().unwrap();
1972            assert_eq!(bytes, vec![b'x'; 1000 * chunks]);
1973        }
1974
1975        #[test]
1976        fn reads_payload_larger_than_pipe_capacity() {
1977            let pipe = filedescriptor::Pipe::new().unwrap();
1978            // Exceeds the 64 KiB pipe capacity, forcing the writer to block.
1979            let payload = vec![b'z'; 1024 * 1024];
1980
1981            let writer = std::thread::spawn({
1982                let mut write = pipe.write;
1983                let payload = payload.clone();
1984                move || write.write_all(&payload).unwrap()
1985            });
1986            let bytes = read_fd_with_timeout(pipe.read, PIPE_READ_TIMEOUT).unwrap();
1987            writer.join().unwrap();
1988            assert_eq!(bytes, payload);
1989        }
1990    }
1991}
1992
1993#[cfg(test)]
1994mod display_mode_tests {
1995    use std::{cell::Cell, process::Command};
1996
1997    #[cfg(feature = "wayland")]
1998    use gpui::{AppContext as _, Context, IntoElement, Render, Window, WindowOptions};
1999    use gpui::{Application, AsyncApp, QuitMode};
2000
2001    use super::*;
2002
2003    /// Set in the child process that runs a scenario, naming the scenario to run.
2004    const SCENARIO_ENV: &str = "GPUI_LINUX_DISPLAY_MODE_SCENARIO";
2005    const MISSING_WAYLAND_DISPLAY: &str = "/gpui-test/wayland-display-does-not-exist";
2006
2007    #[cfg(feature = "wayland")]
2008    #[test]
2009    fn failed_wayland_attach_preserves_headless_app_and_allows_retry() {
2010        run_scenario(
2011            "failed_wayland_attach_preserves_headless_app_and_allows_retry",
2012            WindowingModes::all(),
2013            |cx| {
2014                Box::pin(async move {
2015                    let entity = cx.new(|_| 1usize);
2016                    let (release_task, wait_for_release) = oneshot::channel();
2017                    let queued_task_ran = Rc::new(Cell::new(false));
2018                    let queued_task = cx.spawn({
2019                        let queued_task_ran = queued_task_ran.clone();
2020                        async move |_| {
2021                            wait_for_release
2022                                .await
2023                                .expect("queued task release sender remains alive");
2024                            queued_task_ran.set(true);
2025                        }
2026                    });
2027
2028                    let first_attach = cx.update(|cx| {
2029                        cx.request_windowing(WindowingRequest::Windowed(
2030                            GraphicalEnvironment::detect(),
2031                        ))
2032                    });
2033                    assert_error_contains(
2034                        first_attach.await,
2035                        "failed to connect to Wayland compositor",
2036                    );
2037                    entity.update(cx, |value, _| *value += 1);
2038                    release_task
2039                        .send(())
2040                        .expect("queued task remains attached to the executor");
2041                    queued_task.await;
2042                    assert!(queued_task_ran.get());
2043
2044                    let second_attach = cx.update(|cx| {
2045                        cx.request_windowing(WindowingRequest::Windowed(
2046                            GraphicalEnvironment::detect(),
2047                        ))
2048                    });
2049                    assert_error_contains(
2050                        second_attach.await,
2051                        "failed to connect to Wayland compositor",
2052                    );
2053                    entity.update(cx, |value, _| *value += 1);
2054                    assert_eq!(entity.read_with(cx, |value, _| *value), 3);
2055                })
2056            },
2057        );
2058    }
2059
2060    #[cfg(feature = "wayland")]
2061    #[test]
2062    fn headless_windows_block_switching_to_a_display_server() {
2063        run_scenario(
2064            "headless_windows_block_switching_to_a_display_server",
2065            WindowingModes::all(),
2066            |cx| {
2067                Box::pin(async move {
2068                    let window = cx
2069                        .update(|cx| {
2070                            cx.open_window(WindowOptions::default(), |_, cx| cx.new(|_| Blank))
2071                        })
2072                        .expect("headless mode opens headless windows");
2073                    let attach = cx.update(|cx| {
2074                        cx.request_windowing(WindowingRequest::Windowed(
2075                            GraphicalEnvironment::detect(),
2076                        ))
2077                    });
2078                    assert_error_contains(attach.await, "while windows are open");
2079
2080                    cx.update(|cx| window.update(cx, |_, window, _| window.remove_window()))
2081                        .expect("close the headless window");
2082                    let attach = cx.update(|cx| {
2083                        cx.request_windowing(WindowingRequest::Windowed(
2084                            GraphicalEnvironment::detect(),
2085                        ))
2086                    });
2087                    assert_error_contains(attach.await, "failed to connect to Wayland compositor");
2088                })
2089            },
2090        );
2091    }
2092
2093    #[test]
2094    fn display_modes_limit_switching() {
2095        run_scenario(
2096            "display_modes_limit_switching",
2097            WindowingModes::HEADLESS,
2098            |cx| {
2099                Box::pin(async move {
2100                    let attach = cx.update(|cx| {
2101                        cx.request_windowing(WindowingRequest::Windowed(
2102                            GraphicalEnvironment::detect(),
2103                        ))
2104                    });
2105                    assert_error_contains(attach.await, "names no display server that");
2106                    assert_eq!(cx.update(|cx| cx.compositor_name()), "headless");
2107                    assert!(cx.update(|cx| cx.graphical_environment()).is_none());
2108
2109                    let detach = cx.update(|cx| cx.request_windowing(WindowingRequest::Headless));
2110                    assert_error_contains(detach.await, "already headless");
2111                })
2112            },
2113        );
2114    }
2115
2116    #[cfg(feature = "wayland")]
2117    struct Blank;
2118
2119    #[cfg(feature = "wayland")]
2120    impl Render for Blank {
2121        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2122            gpui::Empty
2123        }
2124    }
2125
2126    type Scenario = fn(&mut AsyncApp) -> futures::future::LocalBoxFuture<'_, ()>;
2127
2128    /// Runs `scenario` on a headless platform in a child process, where `WAYLAND_DISPLAY` names a
2129    /// socket that doesn't exist.
2130    fn run_scenario(name: &str, modes: WindowingModes, scenario: Scenario) {
2131        if std::env::var(SCENARIO_ENV).as_deref() == Ok(name) {
2132            let platform = Rc::new(LinuxPlatform::new(modes));
2133            platform.set_initial_windowing(WindowingRequest::Headless);
2134            let completed = Rc::new(Cell::new(false));
2135            Application::with_platform(platform)
2136                .with_quit_mode(QuitMode::Explicit)
2137                .run({
2138                    let completed = completed.clone();
2139                    move |cx| {
2140                        cx.spawn(async move |cx| {
2141                            scenario(cx).await;
2142                            completed.set(true);
2143                            cx.update(|cx| cx.quit());
2144                        })
2145                        .detach();
2146                    }
2147                });
2148            assert!(completed.get(), "scenario did not complete");
2149            return;
2150        }
2151
2152        // Test names don't include the crate's name.
2153        let module = module_path!()
2154            .split_once("::")
2155            .map_or(module_path!(), |(_, module)| module);
2156        let test_name = format!("{module}::{name}");
2157        #[allow(
2158            clippy::disallowed_methods,
2159            reason = "the test thread has nothing else to do while the child runs"
2160        )]
2161        let output = Command::new(std::env::current_exe().expect("current test executable"))
2162            .args(["--exact", &test_name, "--nocapture"])
2163            .env(SCENARIO_ENV, name)
2164            .env("WAYLAND_DISPLAY", MISSING_WAYLAND_DISPLAY)
2165            .env_remove("DISPLAY")
2166            .output()
2167            .expect("run the scenario in a child process");
2168        let stdout = String::from_utf8_lossy(&output.stdout);
2169        assert!(
2170            output.status.success() && stdout.contains("1 passed"),
2171            "scenario failed\nstdout:\n{stdout}\nstderr:\n{}",
2172            String::from_utf8_lossy(&output.stderr),
2173        );
2174    }
2175
2176    fn assert_error_contains(result: anyhow::Result<()>, expected: &str) {
2177        let error = result.expect_err("the transition fails");
2178        assert!(
2179            format!("{error:#}").contains(expected),
2180            "expected an error containing {expected:?}, got: {error:#}"
2181        );
2182    }
2183}