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