Skip to main content

gpui_linux/linux/
platform.rs

1use std::{
2    env,
3    path::{Path, PathBuf},
4    rc::Rc,
5    sync::Arc,
6};
7#[cfg(any(feature = "wayland", feature = "x11"))]
8use std::{
9    ffi::OsString,
10    fs::File,
11    io::Read as _,
12    os::fd::{AsFd, AsRawFd},
13    time::Duration,
14};
15
16#[cfg(any(feature = "wayland", feature = "x11"))]
17use anyhow::ensure;
18use anyhow::{Context as _, Result, anyhow};
19#[cfg(any(feature = "wayland", feature = "x11"))]
20use ashpd::{
21    desktop::{
22        Request,
23        inhibit::{InhibitFlags, InhibitOptions, InhibitProxy},
24    },
25    enumflags2::BitFlags,
26};
27use calloop::{LoopSignal, channel::Sender};
28use futures::channel::oneshot;
29use gpui_util::{ResultExt as _, new_std_command};
30#[cfg(any(feature = "wayland", feature = "x11"))]
31use xkbcommon::xkb::{self, Keycode, Keysym, State};
32
33use crate::linux::{LinuxDispatcher, PriorityQueueCalloopReceiver};
34use gpui_engine::{DmaBufCapabilities, PlatformTextSystem};
35use gpui_platform::{
36    ActivityGuard, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor,
37    GpuAdapterInfo, GpuId, GpuPreference, MenuCommandId, PathPromptOptions, Platform,
38    PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformMenu,
39    PlatformMenuItem, PlatformWindow, RunnableVariant, Task, ThermalState, WindowAppearance,
40    WindowButtonLayout, WindowId, WindowParams,
41};
42#[cfg(any(feature = "wayland", feature = "x11"))]
43use gpui_types::{Pixels, Point, px};
44
45#[cfg(any(feature = "wayland", feature = "x11"))]
46pub(crate) const SCROLL_LINES: f32 = 3.0;
47
48// Values match the defaults on GTK.
49// Taken from https://github.com/GNOME/gtk/blob/main/gtk/gtksettings.c#L320
50#[cfg(any(feature = "wayland", feature = "x11"))]
51pub(crate) const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(400);
52#[cfg(any(feature = "wayland", feature = "x11"))]
53pub(crate) const DOUBLE_CLICK_DISTANCE: Pixels = px(5.0);
54pub(crate) const KEYRING_LABEL: &str = "zed-github-account";
55
56#[cfg(any(feature = "wayland", feature = "x11"))]
57const FILE_PICKER_PORTAL_MISSING: &str =
58    "Couldn't open file picker due to missing xdg-desktop-portal implementation.";
59
60pub(crate) trait LinuxClient {
61    fn compositor_name(&self) -> &'static str;
62    fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R;
63    fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
64    fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
65    #[allow(unused)]
66    fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>>;
67    fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
68
69    #[cfg(feature = "screen-capture")]
70    fn is_screen_capture_supported(&self) -> bool {
71        true
72    }
73
74    #[cfg(feature = "screen-capture")]
75    fn screen_capture_sources(
76        &self,
77    ) -> oneshot::Receiver<Result<Vec<Rc<dyn gpui_platform::ScreenCaptureSource>>>> {
78        let (sources_tx, sources_rx) = oneshot::channel();
79        sources_tx
80            .send(Err(anyhow::anyhow!(
81                "gpui_linux was compiled without the screen-capture feature"
82            )))
83            .ok();
84        sources_rx
85    }
86
87    fn open_window(
88        &self,
89        handle: WindowId,
90        options: WindowParams,
91    ) -> anyhow::Result<Box<dyn PlatformWindow>>;
92    fn set_cursor_style(&self, style: CursorStyle);
93    fn hide_cursor_until_mouse_moves(&self) {}
94    fn is_cursor_visible(&self) -> bool {
95        true
96    }
97    fn open_uri(&self, uri: &str);
98    fn reveal_path(&self, path: PathBuf);
99    fn write_to_primary(&self, item: ClipboardItem);
100    fn write_to_clipboard(&self, item: ClipboardItem);
101    fn read_from_primary(&self) -> Option<ClipboardItem>;
102    fn read_from_clipboard(&self) -> Option<ClipboardItem>;
103    fn active_window(&self) -> Option<WindowId>;
104    fn window_stack(&self) -> Option<Vec<WindowId>>;
105    fn run(&self);
106
107    /// The adapters this client's renderer could draw on. The default is for a backend that cannot
108    /// enumerate, such as headless mode.
109    fn enumerate_gpus(&self) -> Vec<GpuAdapterInfo> {
110        Vec::new()
111    }
112
113    /// The adapter the shared renderer context chose, once a window has created it.
114    fn active_gpu(&self) -> Option<GpuAdapterInfo> {
115        None
116    }
117
118    /// What that adapter's device can import as a dma-buf surface, once a window has created it.
119    fn dmabuf_capabilities(&self) -> Option<DmaBufCapabilities> {
120        None
121    }
122
123    #[cfg(any(feature = "wayland", feature = "x11"))]
124    fn window_identifier(
125        &self,
126    ) -> impl Future<Output = Option<ashpd::WindowIdentifier>> + Send + 'static {
127        std::future::ready::<Option<ashpd::WindowIdentifier>>(None)
128    }
129}
130
131#[derive(Default)]
132pub(crate) struct PlatformHandlers {
133    pub(crate) open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
134    pub(crate) quit: Option<Box<dyn FnMut() -> bool>>,
135    pub(crate) reopen: Option<Box<dyn FnMut()>>,
136    pub(crate) app_menu_action: Option<Box<dyn FnMut(MenuCommandId)>>,
137    pub(crate) will_open_app_menu: Option<Box<dyn FnMut()>>,
138    pub(crate) validate_app_menu_command: Option<Box<dyn FnMut(MenuCommandId) -> bool>>,
139    pub(crate) keyboard_layout_change: Option<Box<dyn FnMut()>>,
140    pub(crate) system_sleep: Option<Box<dyn FnMut()>>,
141    pub(crate) system_wake: Option<Box<dyn FnMut()>>,
142}
143
144/// A logind `PrepareForSleep` signal, forwarded from the D-Bus listener to
145/// the client's event loop.
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147#[cfg_attr(
148    not(all(target_os = "linux", any(feature = "wayland", feature = "x11"))),
149    allow(dead_code)
150)]
151pub(crate) enum SystemPowerEvent {
152    Sleep,
153    Wake,
154}
155
156/// The debug override, lowered to a [`GpuPreference::Device`].
157///
158/// It lives here, not in the renderer or the shared platform crate, because a debug variable is an
159/// OS-integration detail: the one path the renderer sees is `set_gpu_preference`, which an
160/// application's stated preference also takes. The headless entry point has no platform instance to
161/// ask, so it calls this directly. A malformed value is logged and ignored rather than silently
162/// falling through to a different GPU.
163pub fn gpu_preference_from_env() -> Option<GpuPreference> {
164    match std::env::var("ZED_DEVICE_ID") {
165        Ok(value) => match GpuId::parse_pci_id(&value) {
166            Some(device_id) => Some(GpuPreference::Device(GpuId::by_device(device_id))),
167            None => {
168                log::error!(
169                    "ZED_DEVICE_ID: expected a four-digit hexadecimal PCI id, got {value:?}"
170                );
171                None
172            }
173        },
174        Err(std::env::VarError::NotPresent) => None,
175        Err(error) => {
176            log::error!("ZED_DEVICE_ID: could not be read: {error}");
177            None
178        }
179    }
180}
181
182pub(crate) struct LinuxCommon {
183    pub(crate) background_executor: BackgroundExecutor,
184    pub(crate) foreground_executor: ForegroundExecutor,
185    pub(crate) text_system: Arc<dyn PlatformTextSystem>,
186    pub(crate) appearance: WindowAppearance,
187    pub(crate) auto_hide_scrollbars: bool,
188    pub(crate) button_layout: WindowButtonLayout,
189    /// Which GPU the renderer should choose. Set on the application before it runs; the choice is
190    /// process-wide.
191    pub(crate) gpu_preference: GpuPreference,
192    pub(crate) callbacks: PlatformHandlers,
193    pub(crate) signal: LoopSignal,
194    app_name: Option<String>,
195    system_notifications: crate::linux::system_notifications::SystemNotificationState,
196    #[cfg_attr(
197        not(all(target_os = "linux", any(feature = "wayland", feature = "x11"))),
198        allow(dead_code)
199    )]
200    power_sender: Sender<SystemPowerEvent>,
201    power_listener_started: bool,
202}
203
204impl LinuxCommon {
205    pub fn new(
206        signal: LoopSignal,
207    ) -> (
208        Self,
209        PriorityQueueCalloopReceiver<RunnableVariant>,
210        calloop::channel::Channel<SystemPowerEvent>,
211    ) {
212        let (main_sender, main_receiver) = PriorityQueueCalloopReceiver::new();
213        let (power_sender, power_receiver) = calloop::channel::channel();
214
215        #[cfg(any(feature = "wayland", feature = "x11"))]
216        let text_system = Arc::new(crate::linux::CosmicTextSystem::new("IBM Plex Sans"));
217        #[cfg(not(any(feature = "wayland", feature = "x11")))]
218        let text_system = Arc::new(gpui_engine::NoopTextSystem::new());
219
220        let callbacks = PlatformHandlers::default();
221
222        let dispatcher = Arc::new(LinuxDispatcher::new(main_sender));
223
224        let background_executor = BackgroundExecutor::new(dispatcher.clone());
225
226        let common = LinuxCommon {
227            background_executor,
228            foreground_executor: ForegroundExecutor::new(dispatcher),
229            text_system,
230            appearance: WindowAppearance::Light,
231            auto_hide_scrollbars: false,
232            button_layout: WindowButtonLayout::linux_default(),
233            gpu_preference: gpu_preference_from_env().unwrap_or_default(),
234            callbacks,
235            signal,
236            app_name: None,
237            system_notifications: crate::linux::system_notifications::SystemNotificationState::new(
238            ),
239            power_sender,
240            power_listener_started: false,
241        };
242
243        (common, main_receiver, power_receiver)
244    }
245
246    pub(crate) fn start_power_listener(&mut self) {
247        if !self.power_listener_started {
248            #[cfg(all(target_os = "linux", any(feature = "wayland", feature = "x11")))]
249            smol::spawn({
250                let power_sender = self.power_sender.clone();
251                async move {
252                    if let Err(error) = listen_for_system_power_events(power_sender).await {
253                        log::debug!("failed to listen for system sleep/wake events: {error:?}");
254                    }
255                }
256            })
257            .detach();
258
259            self.power_listener_started = true;
260        }
261    }
262
263    pub(crate) fn handle_system_power_event(&mut self, event: SystemPowerEvent) {
264        let callback = match event {
265            SystemPowerEvent::Sleep => &mut self.callbacks.system_sleep,
266            SystemPowerEvent::Wake => &mut self.callbacks.system_wake,
267        };
268        if let Some(callback) = callback.as_mut() {
269            callback();
270        }
271    }
272}
273
274#[cfg(all(target_os = "linux", any(feature = "wayland", feature = "x11")))]
275async fn listen_for_system_power_events(
276    power_sender: Sender<SystemPowerEvent>,
277) -> anyhow::Result<()> {
278    use futures::StreamExt as _;
279
280    let connection = ashpd::zbus::Connection::system().await?;
281    let proxy = ashpd::zbus::Proxy::new(
282        &connection,
283        "org.freedesktop.login1",
284        "/org/freedesktop/login1",
285        "org.freedesktop.login1.Manager",
286    )
287    .await?;
288    let mut sleep_events = proxy.receive_signal("PrepareForSleep").await?;
289
290    while let Some(message) = sleep_events.next().await {
291        let event = if message.body().deserialize::<bool>()? {
292            SystemPowerEvent::Sleep
293        } else {
294            SystemPowerEvent::Wake
295        };
296        power_sender.send(event).ok();
297    }
298
299    Ok(())
300}
301
302pub(crate) struct LinuxPlatform<P> {
303    pub(crate) inner: P,
304}
305
306/// The adapters the renderer could choose between.
307///
308/// The context's own instance is used once one exists, so the list is the one the renderer can
309/// actually pick from. Before that a fresh instance enumerates; it creates no device, which is the
310/// cost this accessor exists to avoid. `try_borrow` because this can be asked during the window
311/// construction that is holding the slot mutably — returning nothing is better than panicking.
312#[cfg(any(feature = "wayland", feature = "x11"))]
313pub(crate) fn enumerate_gpus(gpu_context: &gpui_wgpu::WgpuContextSlot) -> Vec<GpuAdapterInfo> {
314    if let Ok(context) = gpu_context.try_borrow()
315        && let Some(context) = context.as_ref()
316    {
317        return gpui_wgpu::enumerate_adapters_from(&context.instance);
318    }
319    gpui_wgpu::enumerate_adapters()
320}
321
322/// The adapter the shared context renders on, once it has one.
323#[cfg(any(feature = "wayland", feature = "x11"))]
324pub(crate) fn active_gpu(gpu_context: &gpui_wgpu::WgpuContextSlot) -> Option<GpuAdapterInfo> {
325    let context = gpu_context.try_borrow().ok()?;
326    context.as_ref().map(|context| context.adapter_info())
327}
328
329/// What the renderer's device can import as a dma-buf surface.
330#[cfg(any(feature = "wayland", feature = "x11"))]
331pub(crate) fn dmabuf_capabilities(
332    gpu_context: &gpui_wgpu::WgpuContextSlot,
333) -> Option<DmaBufCapabilities> {
334    let context = gpu_context.try_borrow().ok()?;
335    context
336        .as_ref()
337        .and_then(|context| context.dmabuf_capabilities())
338}
339
340impl<P: LinuxClient + 'static> Platform for LinuxPlatform<P> {
341    fn background_executor(&self) -> BackgroundExecutor {
342        self.inner
343            .with_common(|common| common.background_executor.clone())
344    }
345
346    fn foreground_executor(&self) -> ForegroundExecutor {
347        self.inner
348            .with_common(|common| common.foreground_executor.clone())
349    }
350
351    fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
352        self.inner.with_common(|common| common.text_system.clone())
353    }
354
355    fn set_gpu_preference(&self, preference: GpuPreference) {
356        self.inner
357            .with_common(|common| common.gpu_preference = preference);
358    }
359
360    fn enumerate_gpus(&self) -> Vec<GpuAdapterInfo> {
361        self.inner.enumerate_gpus()
362    }
363
364    fn active_gpu(&self) -> Option<GpuAdapterInfo> {
365        self.inner.active_gpu()
366    }
367
368    fn dmabuf_capabilities(&self) -> Option<DmaBufCapabilities> {
369        self.inner.dmabuf_capabilities()
370    }
371
372    fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
373        self.inner.keyboard_layout()
374    }
375
376    fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
377        Rc::new(gpui_platform::DummyKeyboardMapper)
378    }
379
380    fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
381        self.inner
382            .with_common(|common| common.callbacks.keyboard_layout_change = Some(callback));
383    }
384
385    fn on_thermal_state_change(&self, _callback: Box<dyn FnMut()>) {}
386
387    fn thermal_state(&self) -> ThermalState {
388        ThermalState::Nominal
389    }
390
391    #[cfg(not(any(feature = "wayland", feature = "x11")))]
392    fn prevent_idle_sleep(&self, reason: &str) -> Task<Result<ActivityGuard>> {
393        Task::ready(Err(anyhow!(
394            "Idle sleep prevention for {reason:?} requires a Linux windowing backend"
395        )))
396    }
397
398    #[cfg(any(feature = "wayland", feature = "x11"))]
399    fn prevent_idle_sleep(&self, reason: &str) -> Task<Result<ActivityGuard>> {
400        let executor = self.background_executor();
401        let (guard_tx, guard_rx) = oneshot::channel();
402        executor
403            .spawn({
404                let executor = executor.clone();
405                let reason = reason.to_owned();
406                async move {
407                    guard_tx
408                        .send(inhibit_idle_sleep(reason, executor).await)
409                        .ok();
410                }
411            })
412            .detach();
413        executor
414            .clone()
415            .spawn(async move { await_idle_sleep_prevention(guard_rx, &executor).await })
416    }
417
418    fn run(&self, on_finish_launching: Box<dyn FnOnce()>) {
419        on_finish_launching();
420
421        LinuxClient::run(&self.inner);
422
423        let quit = self
424            .inner
425            .with_common(|common| common.callbacks.quit.take());
426        if let Some(mut fun) = quit {
427            fun();
428        }
429    }
430
431    fn quit(&self) {
432        self.inner.with_common(|common| common.signal.stop());
433    }
434
435    fn compositor_name(&self) -> &'static str {
436        self.inner.compositor_name()
437    }
438
439    fn restart(&self, binary_path: Option<PathBuf>, arguments: Vec<std::ffi::OsString>) {
440        use std::os::unix::process::CommandExt as _;
441
442        // get the process id of the current process
443        let app_pid = std::process::id().to_string();
444        // get the path to the executable
445        let app_path = if let Some(path) = binary_path {
446            path
447        } else {
448            match self.app_path() {
449                Ok(path) => path,
450                Err(err) => {
451                    log::error!("Failed to get app path: {:?}", err);
452                    return;
453                }
454            }
455        };
456
457        log::info!("Restarting process, using app path: {:?}", app_path);
458
459        // Script to wait for the current process to exit and then restart the app.
460        // Pass dynamic values as positional parameters to avoid shell interpolation issues.
461        let script = r#"
462            while kill -0 "$0" 2>/dev/null; do
463                sleep 0.1
464            done
465
466            app_path="$1"
467            shift
468            "$app_path" "$@"
469            "#;
470
471        #[allow(
472            clippy::disallowed_methods,
473            reason = "We are restarting ourselves, using std command thus is fine"
474        )]
475        let restart_process = new_std_command("/usr/bin/env")
476            .arg("bash")
477            .arg("-c")
478            .arg(script)
479            .arg(&app_pid)
480            .arg(&app_path)
481            .args(arguments)
482            .process_group(0)
483            .spawn();
484
485        match restart_process {
486            Ok(_) => self.quit(),
487            Err(e) => log::error!("failed to spawn restart script: {:?}", e),
488        }
489    }
490
491    fn activate(&self, _ignoring_other_apps: bool) {
492        log::info!("activate is not implemented on Linux, ignoring the call")
493    }
494
495    fn hide(&self) {
496        log::info!("hide is not implemented on Linux, ignoring the call")
497    }
498
499    fn hide_other_apps(&self) {
500        log::info!("hide_other_apps is not implemented on Linux, ignoring the call")
501    }
502
503    fn unhide_other_apps(&self) {
504        log::info!("unhide_other_apps is not implemented on Linux, ignoring the call")
505    }
506
507    fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
508        self.inner.primary_display()
509    }
510
511    fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
512        self.inner.displays()
513    }
514
515    #[cfg(feature = "screen-capture")]
516    fn is_screen_capture_supported(&self) -> bool {
517        self.inner.is_screen_capture_supported()
518    }
519
520    #[cfg(feature = "screen-capture")]
521    fn screen_capture_sources(
522        &self,
523    ) -> oneshot::Receiver<Result<Vec<Rc<dyn gpui_platform::ScreenCaptureSource>>>> {
524        self.inner.screen_capture_sources()
525    }
526
527    fn active_window(&self) -> Option<WindowId> {
528        self.inner.active_window()
529    }
530
531    fn window_stack(&self) -> Option<Vec<WindowId>> {
532        self.inner.window_stack()
533    }
534
535    fn open_window(
536        &self,
537        handle: WindowId,
538        options: WindowParams,
539    ) -> anyhow::Result<Box<dyn PlatformWindow>> {
540        self.inner.open_window(handle, options)
541    }
542
543    fn open_url(&self, url: &str) {
544        self.inner.open_uri(url);
545    }
546
547    fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
548        self.inner
549            .with_common(|common| common.callbacks.open_urls = Some(callback));
550    }
551
552    fn prompt_for_paths(
553        &self,
554        options: PathPromptOptions,
555    ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
556        let (done_tx, done_rx) = oneshot::channel();
557
558        #[cfg(not(any(feature = "wayland", feature = "x11")))]
559        let _ = (done_tx.send(Ok(None)), options);
560
561        #[cfg(any(feature = "wayland", feature = "x11"))]
562        let identifier = self.inner.window_identifier();
563
564        #[cfg(any(feature = "wayland", feature = "x11"))]
565        self.foreground_executor()
566            .spawn(async move {
567                let title = if options.directories {
568                    "Open Folder"
569                } else {
570                    "Open File"
571                };
572
573                let request = match ashpd::desktop::file_chooser::OpenFileRequest::default()
574                    .identifier(identifier.await)
575                    .modal(true)
576                    .title(title)
577                    .accept_label(
578                        options
579                            .prompt
580                            .as_ref()
581                            .map(gpui_platform::SharedString::as_str),
582                    )
583                    .multiple(options.multiple)
584                    .directory(options.directories)
585                    .send()
586                    .await
587                {
588                    Ok(request) => request,
589                    Err(err) => {
590                        let result = match err {
591                            ashpd::Error::PortalNotFound(_) => anyhow!(FILE_PICKER_PORTAL_MISSING),
592                            err => err.into(),
593                        };
594                        let _ = done_tx.send(Err(result));
595                        return;
596                    }
597                };
598
599                let result = match request.response() {
600                    Ok(response) => Ok(Some(
601                        response
602                            .uris()
603                            .iter()
604                            .filter_map(|uri: &ashpd::Uri| url::Url::parse(uri.as_str()).ok())
605                            .filter_map(|uri: url::Url| uri.to_file_path().ok())
606                            .collect::<Vec<_>>(),
607                    )),
608                    Err(ashpd::Error::Response(_)) => Ok(None),
609                    Err(e) => Err(e.into()),
610                };
611                let _ = done_tx.send(result);
612            })
613            .detach();
614        done_rx
615    }
616
617    fn prompt_for_new_path(
618        &self,
619        directory: &Path,
620        suggested_name: Option<&str>,
621    ) -> oneshot::Receiver<Result<Option<PathBuf>>> {
622        let (done_tx, done_rx) = oneshot::channel();
623
624        #[cfg(not(any(feature = "wayland", feature = "x11")))]
625        let _ = (done_tx.send(Ok(None)), directory, suggested_name);
626
627        #[cfg(any(feature = "wayland", feature = "x11"))]
628        let identifier = self.inner.window_identifier();
629
630        #[cfg(any(feature = "wayland", feature = "x11"))]
631        self.foreground_executor()
632            .spawn({
633                let directory = directory.to_owned();
634                let suggested_name = suggested_name.map(|s| s.to_owned());
635
636                async move {
637                    let mut request_builder =
638                        ashpd::desktop::file_chooser::SaveFileRequest::default()
639                            .identifier(identifier.await)
640                            .modal(true)
641                            .title("Save File")
642                            .current_folder(directory)
643                            .expect("pathbuf should not be nul terminated");
644
645                    if let Some(suggested_name) = suggested_name {
646                        request_builder = request_builder.current_name(suggested_name.as_str());
647                    }
648
649                    let request = match request_builder.send().await {
650                        Ok(request) => request,
651                        Err(err) => {
652                            let result = match err {
653                                ashpd::Error::PortalNotFound(_) => {
654                                    anyhow!(FILE_PICKER_PORTAL_MISSING)
655                                }
656                                err => err.into(),
657                            };
658                            let _ = done_tx.send(Err(result));
659                            return;
660                        }
661                    };
662
663                    let result = match request.response() {
664                        Ok(response) => Ok(response
665                            .uris()
666                            .first()
667                            .and_then(|uri: &ashpd::Uri| url::Url::parse(uri.as_str()).ok())
668                            .and_then(|uri: url::Url| uri.to_file_path().ok())),
669                        Err(ashpd::Error::Response(_)) => Ok(None),
670                        Err(e) => Err(e.into()),
671                    };
672                    let _ = done_tx.send(result);
673                }
674            })
675            .detach();
676
677        done_rx
678    }
679
680    fn can_select_mixed_files_and_dirs(&self) -> bool {
681        // org.freedesktop.portal.FileChooser only supports "pick files" and "pick directories".
682        false
683    }
684
685    fn reveal_path(&self, path: &Path) {
686        self.inner.reveal_path(path.to_owned());
687    }
688
689    fn open_with_system(&self, path: &Path) {
690        let path = path.to_owned();
691        self.background_executor()
692            .spawn(async move {
693                #[allow(
694                    clippy::disallowed_methods,
695                    reason = "running on a background thread, so blocking is fine"
696                )]
697                new_std_command("xdg-open")
698                    .arg(path)
699                    .status()
700                    .context("invoking xdg-open")
701                    .log_err();
702            })
703            .detach();
704    }
705
706    fn on_quit(&self, callback: Box<dyn FnMut() -> bool>) {
707        self.inner.with_common(|common| {
708            common.callbacks.quit = Some(callback);
709        });
710    }
711
712    fn on_reopen(&self, callback: Box<dyn FnMut()>) {
713        self.inner.with_common(|common| {
714            common.callbacks.reopen = Some(callback);
715        });
716    }
717
718    fn on_system_sleep(&self, callback: Box<dyn FnMut()>) {
719        self.inner.with_common(|common| {
720            common.callbacks.system_sleep = Some(callback);
721            common.start_power_listener();
722        });
723    }
724
725    fn on_system_wake(&self, callback: Box<dyn FnMut()>) {
726        self.inner.with_common(|common| {
727            common.callbacks.system_wake = Some(callback);
728            common.start_power_listener();
729        });
730    }
731
732    fn set_app_identity(&self, _identifier: &str, name: &str) {
733        self.inner
734            .with_common(|common| common.app_name = Some(name.to_string()));
735    }
736
737    fn show_system_notification(&self, notification: gpui_platform::SystemNotification) {
738        self.inner.with_common(|common| {
739            common
740                .system_notifications
741                .show(common.app_name.as_deref(), notification)
742        });
743    }
744
745    fn dismiss_system_notification(&self, tag: &str) {
746        self.inner
747            .with_common(|common| common.system_notifications.dismiss(tag));
748    }
749
750    fn on_system_notification_response(
751        &self,
752        callback: Box<dyn FnMut(gpui_platform::SystemNotificationResponse)>,
753    ) {
754        self.inner.with_common(|common| {
755            let executor = common.foreground_executor.clone();
756            common.system_notifications.on_response(&executor, callback)
757        });
758    }
759
760    fn on_app_menu_action(&self, callback: Box<dyn FnMut(MenuCommandId)>) {
761        self.inner.with_common(|common| {
762            common.callbacks.app_menu_action = Some(callback);
763        });
764    }
765
766    fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>) {
767        self.inner.with_common(|common| {
768            common.callbacks.will_open_app_menu = Some(callback);
769        });
770    }
771
772    fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(MenuCommandId) -> bool>) {
773        self.inner.with_common(|common| {
774            common.callbacks.validate_app_menu_command = Some(callback);
775        });
776    }
777
778    fn app_path(&self) -> Result<PathBuf> {
779        // get the path of the executable of the current process
780        let app_path = env::current_exe()?;
781        Ok(app_path)
782    }
783
784    fn set_menus(&self, _menus: Vec<PlatformMenu>) {
785        // todo(linux)
786    }
787
788    fn set_dock_menu(&self, _menu: Vec<PlatformMenuItem>) {
789        // todo(linux)
790    }
791
792    fn path_for_auxiliary_executable(&self, _name: &str) -> Result<PathBuf> {
793        Err(anyhow::Error::msg(
794            "Platform<LinuxPlatform>::path_for_auxiliary_executable is not implemented yet",
795        ))
796    }
797
798    fn set_cursor_style(&self, style: CursorStyle) {
799        self.inner.set_cursor_style(style)
800    }
801
802    fn hide_cursor_until_mouse_moves(&self) {
803        self.inner.hide_cursor_until_mouse_moves()
804    }
805
806    fn is_cursor_visible(&self) -> bool {
807        self.inner.is_cursor_visible()
808    }
809
810    fn should_auto_hide_scrollbars(&self) -> bool {
811        self.inner.with_common(|common| common.auto_hide_scrollbars)
812    }
813
814    fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>> {
815        let url = url.to_string();
816        let username = username.to_string();
817        let password = password.to_vec();
818        self.background_executor().spawn(async move {
819            let keyring = oo7::Keyring::new().await?;
820            keyring.unlock().await?;
821            keyring
822                .create_item(
823                    KEYRING_LABEL,
824                    &vec![("url", &url), ("username", &username)],
825                    password,
826                    true,
827                )
828                .await?;
829            Ok(())
830        })
831    }
832
833    fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
834        let url = url.to_string();
835        self.background_executor().spawn(async move {
836            let keyring = oo7::Keyring::new().await?;
837            keyring.unlock().await?;
838
839            let items = keyring.search_items(&vec![("url", &url)]).await?;
840
841            for item in items.into_iter() {
842                if item.label().await.is_ok_and(|label| label == KEYRING_LABEL) {
843                    let attributes = item.attributes().await?;
844                    let username = attributes
845                        .get("username")
846                        .context("Cannot find username in stored credentials")?;
847                    item.unlock().await?;
848                    let secret = item.secret().await?;
849
850                    // we lose the zeroizing capabilities at this boundary,
851                    // a current limitation GPUI's credentials api
852                    return Ok(Some((username.to_string(), secret.to_vec())));
853                } else {
854                    continue;
855                }
856            }
857            Ok(None)
858        })
859    }
860
861    fn delete_credentials(&self, url: &str) -> Task<Result<()>> {
862        let url = url.to_string();
863        self.background_executor().spawn(async move {
864            let keyring = oo7::Keyring::new().await?;
865            keyring.unlock().await?;
866
867            let items = keyring.search_items(&vec![("url", &url)]).await?;
868
869            for item in items.into_iter() {
870                if item.label().await.is_ok_and(|label| label == KEYRING_LABEL) {
871                    item.delete().await?;
872                    return Ok(());
873                }
874            }
875
876            Ok(())
877        })
878    }
879
880    fn window_appearance(&self) -> WindowAppearance {
881        self.inner.with_common(|common| common.appearance)
882    }
883
884    fn button_layout(&self) -> Option<WindowButtonLayout> {
885        Some(self.inner.with_common(|common| common.button_layout))
886    }
887
888    fn register_url_scheme(&self, _: &str) -> Task<anyhow::Result<()>> {
889        Task::ready(Err(anyhow!("register_url_scheme unimplemented")))
890    }
891
892    fn write_to_primary(&self, item: ClipboardItem) {
893        self.inner.write_to_primary(item)
894    }
895
896    fn write_to_clipboard(&self, item: ClipboardItem) {
897        self.inner.write_to_clipboard(item)
898    }
899
900    fn read_from_primary(&self) -> Option<ClipboardItem> {
901        self.inner.read_from_primary()
902    }
903
904    fn read_from_clipboard(&self) -> Option<ClipboardItem> {
905        self.inner.read_from_clipboard()
906    }
907
908    fn add_recent_document(&self, _path: &Path) {}
909}
910
911#[cfg(any(feature = "wayland", feature = "x11"))]
912pub(super) fn open_uri_internal(
913    executor: BackgroundExecutor,
914    uri: &str,
915    activation_token: Option<String>,
916) {
917    if let Some(uri) = ashpd::Uri::parse(uri).log_err() {
918        executor
919            .spawn(async move {
920                let mut xdg_open_failed = false;
921                for mut command in open::commands(uri.to_string()) {
922                    if let Some(token) = activation_token.as_ref() {
923                        command.env("XDG_ACTIVATION_TOKEN", token);
924                    }
925                    let program = format!("{:?}", command.get_program());
926                    match smol::process::Command::from(command).spawn() {
927                        Ok(mut cmd) => match cmd.status().await {
928                            Ok(status) if status.success() => return,
929                            Ok(status) => {
930                                log::error!("Command {} exited with status: {}", program, status);
931                                xdg_open_failed = true;
932                            }
933                            Err(e) => {
934                                log::error!("Failed to get status from {}: {}", program, e);
935                                xdg_open_failed = true;
936                            }
937                        },
938                        Err(e) => {
939                            log::error!("Failed to open with {}: {}", program, e);
940                            xdg_open_failed = true;
941                        }
942                    }
943                }
944
945                if xdg_open_failed {
946                    match ashpd::desktop::open_uri::OpenFileRequest::default()
947                        .activation_token(activation_token.map(ashpd::ActivationToken::from))
948                        .send_uri(&uri)
949                        .await
950                        .and_then(|e| e.response())
951                    {
952                        Ok(()) => {}
953                        Err(ashpd::Error::Response(ashpd::desktop::ResponseError::Cancelled)) => {}
954                        Err(e) => {
955                            log::error!("Failed to open with dbus: {}", e);
956                        }
957                    }
958                }
959            })
960            .detach();
961    }
962}
963
964#[cfg(any(feature = "x11", feature = "wayland"))]
965pub(super) fn reveal_path_internal(
966    executor: BackgroundExecutor,
967    path: PathBuf,
968    activation_token: Option<String>,
969) {
970    executor
971        .spawn(async move {
972            if let Some(dir) = File::open(path.clone()).log_err() {
973                match ashpd::desktop::open_uri::OpenDirectoryRequest::default()
974                    .activation_token(activation_token.map(ashpd::ActivationToken::from))
975                    .send(&dir.as_fd())
976                    .await
977                {
978                    Ok(_) => return,
979                    Err(e) => log::error!("Failed to open with dbus: {}", e),
980                }
981                if path.is_dir() {
982                    open::that_detached(path).log_err();
983                } else {
984                    open::that_detached(path.parent().unwrap_or(Path::new(""))).log_err();
985                }
986            }
987        })
988        .detach();
989}
990
991#[cfg(any(feature = "wayland", feature = "x11"))]
992pub(super) fn is_within_click_distance(a: Point<Pixels>, b: Point<Pixels>) -> bool {
993    let diff = a - b;
994    diff.x.abs() <= DOUBLE_CLICK_DISTANCE && diff.y.abs() <= DOUBLE_CLICK_DISTANCE
995}
996
997/// Creates an XKB context for keymaps supplied by Wayland or X11.
998///
999/// Server keymaps are already resolved and need no local keyboard definitions.
1000/// Loading default include paths can fail on systems without those files.
1001#[cfg(any(feature = "wayland", feature = "x11"))]
1002pub(super) fn new_xkb_context() -> anyhow::Result<xkb::Context> {
1003    validate_xkb_context(xkb::Context::new(xkb::CONTEXT_NO_DEFAULT_INCLUDES))
1004}
1005
1006#[cfg(any(feature = "wayland", feature = "x11"))]
1007fn validate_xkb_context(context: xkb::Context) -> anyhow::Result<xkb::Context> {
1008    ensure!(
1009        !context.get_raw_ptr().is_null(),
1010        "libxkbcommon failed to create an XKB context"
1011    );
1012    Ok(context)
1013}
1014
1015#[cfg(any(feature = "wayland", feature = "x11"))]
1016pub(super) fn get_xkb_compose_state(cx: &xkb::Context) -> Option<xkb::compose::State> {
1017    let mut locales = Vec::default();
1018    if let Some(locale) = env::var_os("LC_CTYPE") {
1019        locales.push(locale);
1020    }
1021    locales.push(OsString::from("C"));
1022    let mut state: Option<xkb::compose::State> = None;
1023    for locale in locales {
1024        if let Ok(table) =
1025            xkb::compose::Table::new_from_locale(cx, &locale, xkb::compose::COMPILE_NO_FLAGS)
1026        {
1027            state = Some(xkb::compose::State::new(
1028                &table,
1029                xkb::compose::STATE_NO_FLAGS,
1030            ));
1031            break;
1032        }
1033    }
1034    state
1035}
1036
1037#[cfg(any(feature = "wayland", feature = "x11"))]
1038pub(super) const PIPE_READ_TIMEOUT: Duration = Duration::from_secs(4);
1039
1040#[cfg(any(feature = "wayland", feature = "x11"))]
1041pub(super) fn read_fd_with_timeout(
1042    mut fd: filedescriptor::FileDescriptor,
1043    timeout: Duration,
1044) -> Result<Vec<u8>> {
1045    fd.set_non_blocking(true)?;
1046    let mut buffer = Vec::new();
1047    let mut chunk = [0u8; 8192];
1048    loop {
1049        let mut poll_fds = [filedescriptor::pollfd {
1050            fd: fd.as_raw_fd(),
1051            events: filedescriptor::POLLIN,
1052            revents: 0,
1053        }];
1054        let ready = match filedescriptor::poll(&mut poll_fds, Some(timeout)) {
1055            Ok(ready) => ready,
1056            Err(filedescriptor::Error::Poll(err))
1057                if err.kind() == std::io::ErrorKind::Interrupted =>
1058            {
1059                continue;
1060            }
1061            Err(err) => return Err(err.into()),
1062        };
1063        if ready == 0 {
1064            anyhow::bail!("timed out waiting for data on pipe after {timeout:?}");
1065        }
1066        match fd.read(&mut chunk) {
1067            Ok(0) => return Ok(buffer),
1068            Ok(len) => buffer.extend_from_slice(&chunk[..len]),
1069            Err(err)
1070                if err.kind() == std::io::ErrorKind::WouldBlock
1071                    || err.kind() == std::io::ErrorKind::Interrupted => {}
1072            Err(err) => return Err(err.into()),
1073        }
1074    }
1075}
1076
1077#[cfg(any(feature = "wayland", feature = "x11"))]
1078pub(super) const DEFAULT_CURSOR_ICON_NAME: &str = "left_ptr";
1079
1080#[cfg(any(feature = "wayland", feature = "x11"))]
1081pub(super) fn cursor_style_to_icon_names(style: CursorStyle) -> &'static [&'static str] {
1082    // Based on cursor names from chromium:
1083    // https://github.com/chromium/chromium/blob/d3069cf9c973dc3627fa75f64085c6a86c8f41bf/ui/base/cursor/cursor_factory.cc#L113
1084    match style {
1085        CursorStyle::Arrow => &[DEFAULT_CURSOR_ICON_NAME],
1086        CursorStyle::IBeam => &["text", "xterm"],
1087        CursorStyle::Crosshair => &["crosshair", "cross"],
1088        CursorStyle::ClosedHand => &["closedhand", "grabbing", "hand2"],
1089        CursorStyle::OpenHand => &["openhand", "grab", "hand1"],
1090        CursorStyle::PointingHand => &["pointer", "hand", "hand2"],
1091        CursorStyle::ResizeLeft => &["w-resize", "left_side"],
1092        CursorStyle::ResizeRight => &["e-resize", "right_side"],
1093        CursorStyle::ResizeLeftRight => &["ew-resize", "sb_h_double_arrow"],
1094        CursorStyle::ResizeUp => &["n-resize", "top_side"],
1095        CursorStyle::ResizeDown => &["s-resize", "bottom_side"],
1096        CursorStyle::ResizeUpDown => &["sb_v_double_arrow", "ns-resize"],
1097        CursorStyle::ResizeUpLeftDownRight => &["size_fdiag", "bd_double_arrow", "nwse-resize"],
1098        CursorStyle::ResizeUpRightDownLeft => &["size_bdiag", "nesw-resize", "fd_double_arrow"],
1099        CursorStyle::ResizeColumn => &["col-resize", "sb_h_double_arrow"],
1100        CursorStyle::ResizeRow => &["row-resize", "sb_v_double_arrow"],
1101        CursorStyle::IBeamCursorForVerticalLayout => &["vertical-text"],
1102        CursorStyle::OperationNotAllowed => &["not-allowed", "crossed_circle"],
1103        CursorStyle::DragLink => &["alias"],
1104        CursorStyle::DragCopy => &["copy"],
1105        CursorStyle::ContextualMenu => &["context-menu"],
1106    }
1107}
1108
1109#[cfg(any(feature = "wayland", feature = "x11"))]
1110pub(super) fn log_cursor_icon_warning(message: impl std::fmt::Display) {
1111    if let Ok(xcursor_path) = env::var("XCURSOR_PATH") {
1112        log::warn!(
1113            "{:#}\ncursor icon loading may be failing if XCURSOR_PATH environment variable is invalid. \
1114                    XCURSOR_PATH overrides the default icon search. Its current value is '{}'",
1115            message,
1116            xcursor_path
1117        );
1118    } else {
1119        log::warn!("{:#}", message);
1120    }
1121}
1122
1123#[cfg(any(feature = "wayland", feature = "x11"))]
1124fn guess_ascii(keycode: Keycode, shift: bool) -> Option<char> {
1125    let c = match (keycode.raw(), shift) {
1126        (24, _) => 'q',
1127        (25, _) => 'w',
1128        (26, _) => 'e',
1129        (27, _) => 'r',
1130        (28, _) => 't',
1131        (29, _) => 'y',
1132        (30, _) => 'u',
1133        (31, _) => 'i',
1134        (32, _) => 'o',
1135        (33, _) => 'p',
1136        (34, false) => '[',
1137        (34, true) => '{',
1138        (35, false) => ']',
1139        (35, true) => '}',
1140        (38, _) => 'a',
1141        (39, _) => 's',
1142        (40, _) => 'd',
1143        (41, _) => 'f',
1144        (42, _) => 'g',
1145        (43, _) => 'h',
1146        (44, _) => 'j',
1147        (45, _) => 'k',
1148        (46, _) => 'l',
1149        (47, false) => ';',
1150        (47, true) => ':',
1151        (48, false) => '\'',
1152        (48, true) => '"',
1153        (49, false) => '`',
1154        (49, true) => '~',
1155        (51, false) => '\\',
1156        (51, true) => '|',
1157        (52, _) => 'z',
1158        (53, _) => 'x',
1159        (54, _) => 'c',
1160        (55, _) => 'v',
1161        (56, _) => 'b',
1162        (57, _) => 'n',
1163        (58, _) => 'm',
1164        (59, false) => ',',
1165        (59, true) => '>',
1166        (60, false) => '.',
1167        (60, true) => '<',
1168        (61, false) => '/',
1169        (61, true) => '?',
1170
1171        _ => return None,
1172    };
1173
1174    Some(c)
1175}
1176
1177#[cfg(any(feature = "wayland", feature = "x11"))]
1178pub(super) fn keystroke_from_xkb(
1179    state: &State,
1180    mut modifiers: gpui_platform::Modifiers,
1181    keycode: Keycode,
1182) -> gpui_platform::Keystroke {
1183    let key_utf32 = state.key_get_utf32(keycode);
1184    let key_utf8 = state.key_get_utf8(keycode);
1185    let key_sym = state.key_get_one_sym(keycode);
1186
1187    let key = match key_sym {
1188        Keysym::Return => "enter".to_owned(),
1189        Keysym::Prior => "pageup".to_owned(),
1190        Keysym::Next => "pagedown".to_owned(),
1191        Keysym::ISO_Left_Tab => "tab".to_owned(),
1192        Keysym::KP_Prior => "pageup".to_owned(),
1193        Keysym::KP_Next => "pagedown".to_owned(),
1194        Keysym::XF86_Back => "back".to_owned(),
1195        Keysym::XF86_Forward => "forward".to_owned(),
1196        Keysym::XF86_Cut => "cut".to_owned(),
1197        Keysym::XF86_Copy => "copy".to_owned(),
1198        Keysym::XF86_Paste => "paste".to_owned(),
1199        Keysym::XF86_New => "new".to_owned(),
1200        Keysym::XF86_Open => "open".to_owned(),
1201        Keysym::XF86_Save => "save".to_owned(),
1202
1203        Keysym::comma => ",".to_owned(),
1204        Keysym::period => ".".to_owned(),
1205        Keysym::less => "<".to_owned(),
1206        Keysym::greater => ">".to_owned(),
1207        Keysym::slash => "/".to_owned(),
1208        Keysym::question => "?".to_owned(),
1209
1210        Keysym::semicolon => ";".to_owned(),
1211        Keysym::colon => ":".to_owned(),
1212        Keysym::apostrophe => "'".to_owned(),
1213        Keysym::quotedbl => "\"".to_owned(),
1214
1215        Keysym::bracketleft => "[".to_owned(),
1216        Keysym::braceleft => "{".to_owned(),
1217        Keysym::bracketright => "]".to_owned(),
1218        Keysym::braceright => "}".to_owned(),
1219        Keysym::backslash => "\\".to_owned(),
1220        Keysym::bar => "|".to_owned(),
1221
1222        Keysym::grave => "`".to_owned(),
1223        Keysym::asciitilde => "~".to_owned(),
1224        Keysym::exclam => "!".to_owned(),
1225        Keysym::at => "@".to_owned(),
1226        Keysym::numbersign => "#".to_owned(),
1227        Keysym::dollar => "$".to_owned(),
1228        Keysym::percent => "%".to_owned(),
1229        Keysym::asciicircum => "^".to_owned(),
1230        Keysym::ampersand => "&".to_owned(),
1231        Keysym::asterisk => "*".to_owned(),
1232        Keysym::parenleft => "(".to_owned(),
1233        Keysym::parenright => ")".to_owned(),
1234        Keysym::minus => "-".to_owned(),
1235        Keysym::underscore => "_".to_owned(),
1236        Keysym::equal => "=".to_owned(),
1237        Keysym::plus => "+".to_owned(),
1238        Keysym::space => "space".to_owned(),
1239        Keysym::BackSpace => "backspace".to_owned(),
1240        Keysym::Tab => "tab".to_owned(),
1241        Keysym::Delete => "delete".to_owned(),
1242        Keysym::Escape => "escape".to_owned(),
1243
1244        Keysym::Left => "left".to_owned(),
1245        Keysym::Right => "right".to_owned(),
1246        Keysym::Up => "up".to_owned(),
1247        Keysym::Down => "down".to_owned(),
1248        Keysym::Home => "home".to_owned(),
1249        Keysym::End => "end".to_owned(),
1250        Keysym::Insert => "insert".to_owned(),
1251
1252        _ => {
1253            let name = xkb::keysym_get_name(key_sym).to_lowercase();
1254            if key_sym.is_keypad_key() {
1255                name.replace("kp_", "")
1256            } else if let Some(key) = key_utf8.chars().next()
1257                && key_utf8.len() == 1
1258                && key.is_ascii()
1259            {
1260                if key.is_ascii_graphic() {
1261                    key_utf8.to_lowercase()
1262                // map ctrl-a to `a`
1263                // ctrl-0..9 may emit control codes like ctrl-[, but
1264                // we don't want to map them to `[`
1265                } else if key_utf32 <= 0x1f
1266                    && !name.chars().next().is_some_and(|c| c.is_ascii_digit())
1267                {
1268                    ((key_utf32 as u8 + 0x40) as char)
1269                        .to_ascii_lowercase()
1270                        .to_string()
1271                } else {
1272                    name
1273                }
1274            } else if let Some(key_en) = guess_ascii(keycode, modifiers.shift) {
1275                String::from(key_en)
1276            } else {
1277                name
1278            }
1279        }
1280    };
1281
1282    if modifiers.shift {
1283        // we only include the shift for upper-case letters by convention,
1284        // so don't include for numbers and symbols, but do include for
1285        // tab/enter, etc.
1286        if key.chars().count() == 1 && key.to_lowercase() == key.to_uppercase() {
1287            modifiers.shift = false;
1288        }
1289    }
1290
1291    // Ignore control characters (and DEL) for the purposes of key_char
1292    let key_char =
1293        (key_utf32 >= 32 && key_utf32 != 127 && !key_utf8.is_empty()).then_some(key_utf8);
1294
1295    gpui_platform::Keystroke {
1296        modifiers,
1297        key,
1298        key_char,
1299    }
1300}
1301
1302/**
1303 * Returns which symbol the dead key represents
1304 * <https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values#dead_keycodes_for_linux>
1305 */
1306#[cfg(any(feature = "wayland", feature = "x11"))]
1307pub fn keystroke_underlying_dead_key(keysym: Keysym) -> Option<String> {
1308    match keysym {
1309        Keysym::dead_grave => Some("`".to_owned()),
1310        Keysym::dead_acute => Some("´".to_owned()),
1311        Keysym::dead_circumflex => Some("^".to_owned()),
1312        Keysym::dead_tilde => Some("~".to_owned()),
1313        Keysym::dead_macron => Some("¯".to_owned()),
1314        Keysym::dead_breve => Some("˘".to_owned()),
1315        Keysym::dead_abovedot => Some("˙".to_owned()),
1316        Keysym::dead_diaeresis => Some("¨".to_owned()),
1317        Keysym::dead_abovering => Some("˚".to_owned()),
1318        Keysym::dead_doubleacute => Some("˝".to_owned()),
1319        Keysym::dead_caron => Some("ˇ".to_owned()),
1320        Keysym::dead_cedilla => Some("¸".to_owned()),
1321        Keysym::dead_ogonek => Some("˛".to_owned()),
1322        Keysym::dead_iota => Some("ͅ".to_owned()),
1323        Keysym::dead_voiced_sound => Some("゙".to_owned()),
1324        Keysym::dead_semivoiced_sound => Some("゚".to_owned()),
1325        Keysym::dead_belowdot => Some("̣̣".to_owned()),
1326        Keysym::dead_hook => Some("̡".to_owned()),
1327        Keysym::dead_horn => Some("̛".to_owned()),
1328        Keysym::dead_stroke => Some("̶̶".to_owned()),
1329        Keysym::dead_abovecomma => Some("̓̓".to_owned()),
1330        Keysym::dead_abovereversedcomma => Some("ʽ".to_owned()),
1331        Keysym::dead_doublegrave => Some("̏".to_owned()),
1332        Keysym::dead_belowring => Some("˳".to_owned()),
1333        Keysym::dead_belowmacron => Some("̱".to_owned()),
1334        Keysym::dead_belowcircumflex => Some("ꞈ".to_owned()),
1335        Keysym::dead_belowtilde => Some("̰".to_owned()),
1336        Keysym::dead_belowbreve => Some("̮".to_owned()),
1337        Keysym::dead_belowdiaeresis => Some("̤".to_owned()),
1338        Keysym::dead_invertedbreve => Some("̯".to_owned()),
1339        Keysym::dead_belowcomma => Some("̦".to_owned()),
1340        Keysym::dead_currency => None,
1341        Keysym::dead_lowline => None,
1342        Keysym::dead_aboveverticalline => None,
1343        Keysym::dead_belowverticalline => None,
1344        Keysym::dead_longsolidusoverlay => None,
1345        Keysym::dead_a => None,
1346        Keysym::dead_A => None,
1347        Keysym::dead_e => None,
1348        Keysym::dead_E => None,
1349        Keysym::dead_i => None,
1350        Keysym::dead_I => None,
1351        Keysym::dead_o => None,
1352        Keysym::dead_O => None,
1353        Keysym::dead_u => None,
1354        Keysym::dead_U => None,
1355        Keysym::dead_small_schwa => Some("ə".to_owned()),
1356        Keysym::dead_capital_schwa => Some("Ə".to_owned()),
1357        Keysym::dead_greek => None,
1358        _ => None,
1359    }
1360}
1361#[cfg(any(feature = "wayland", feature = "x11"))]
1362pub(super) fn modifiers_from_xkb(keymap_state: &State) -> gpui_platform::Modifiers {
1363    let shift = keymap_state.mod_name_is_active(xkb::MOD_NAME_SHIFT, xkb::STATE_MODS_EFFECTIVE);
1364    let alt = keymap_state.mod_name_is_active(xkb::MOD_NAME_ALT, xkb::STATE_MODS_EFFECTIVE);
1365    let control = keymap_state.mod_name_is_active(xkb::MOD_NAME_CTRL, xkb::STATE_MODS_EFFECTIVE);
1366    let platform = keymap_state.mod_name_is_active(xkb::MOD_NAME_LOGO, xkb::STATE_MODS_EFFECTIVE);
1367    gpui_platform::Modifiers {
1368        shift,
1369        alt,
1370        control,
1371        platform,
1372        function: false,
1373    }
1374}
1375
1376#[cfg(any(feature = "wayland", feature = "x11"))]
1377pub(super) fn capslock_from_xkb(keymap_state: &State) -> gpui_platform::Capslock {
1378    let on = keymap_state.mod_name_is_active(xkb::MOD_NAME_CAPS, xkb::STATE_MODS_EFFECTIVE);
1379    gpui_platform::Capslock { on }
1380}
1381
1382/// Resolve a Linux `dev_t` to PCI vendor/device IDs via sysfs, returning a
1383/// [`CompositorGpuHint`] that the GPU adapter selection code can use to
1384/// prioritize the compositor's rendering device.
1385#[cfg(any(feature = "wayland", feature = "x11"))]
1386pub(super) fn compositor_gpu_hint_from_dev_t(dev: u64) -> Option<gpui_wgpu::CompositorGpuHint> {
1387    fn dev_major(dev: u64) -> u32 {
1388        ((dev >> 8) & 0xfff) as u32 | (((dev >> 32) & !0xfff) as u32)
1389    }
1390
1391    fn dev_minor(dev: u64) -> u32 {
1392        (dev & 0xff) as u32 | (((dev >> 12) & !0xff) as u32)
1393    }
1394
1395    fn read_sysfs_hex_id(path: &str) -> Option<u32> {
1396        let content = std::fs::read_to_string(path).ok()?;
1397        let trimmed = content.trim().strip_prefix("0x").unwrap_or(content.trim());
1398        u32::from_str_radix(trimmed, 16).ok()
1399    }
1400
1401    let major = dev_major(dev);
1402    let minor = dev_minor(dev);
1403
1404    let vendor_path = format!("/sys/dev/char/{major}:{minor}/device/vendor");
1405    let device_path = format!("/sys/dev/char/{major}:{minor}/device/device");
1406
1407    let vendor_id = read_sysfs_hex_id(&vendor_path)?;
1408    let device_id = read_sysfs_hex_id(&device_path)?;
1409
1410    log::info!(
1411        "Compositor GPU hint: vendor={:#06x}, device={:#06x} (from dev {major}:{minor})",
1412        vendor_id,
1413        device_id,
1414    );
1415
1416    Some(gpui_wgpu::CompositorGpuHint {
1417        vendor_id,
1418        device_id,
1419    })
1420}
1421
1422#[cfg(any(feature = "wayland", feature = "x11"))]
1423async fn inhibit_idle_sleep(reason: String, executor: BackgroundExecutor) -> Result<ActivityGuard> {
1424    let proxy = InhibitProxy::new()
1425        .await
1426        .context("Idle sleep prevention portal is unavailable")?;
1427    let request = proxy
1428        .inhibit(
1429            None,
1430            BitFlags::from(InhibitFlags::Suspend),
1431            InhibitOptions::default().set_reason(reason.as_str()),
1432        )
1433        .await
1434        .context("Failed to request idle sleep prevention")?;
1435    request
1436        .response()
1437        .context("Idle sleep prevention request was rejected")?;
1438    Ok(release_on_drop(request, executor))
1439}
1440
1441#[cfg(any(feature = "wayland", feature = "x11"))]
1442fn release_on_drop(request: Request<()>, executor: BackgroundExecutor) -> ActivityGuard {
1443    ActivityGuard::new(move || {
1444        executor
1445            .spawn(async move {
1446                request
1447                    .close()
1448                    .await
1449                    .context("Failed to release idle sleep prevention")
1450                    .log_err();
1451            })
1452            .detach();
1453    })
1454}
1455
1456#[cfg(any(feature = "wayland", feature = "x11"))]
1457async fn await_idle_sleep_prevention(
1458    guard_rx: oneshot::Receiver<Result<ActivityGuard>>,
1459    executor: &BackgroundExecutor,
1460) -> Result<ActivityGuard> {
1461    match futures::future::select(guard_rx, executor.timer(Duration::from_secs(10))).await {
1462        futures::future::Either::Left((Ok(result), _)) => result,
1463        futures::future::Either::Left((Err(_), _)) => {
1464            Err(anyhow!("Idle sleep prevention request was abandoned"))
1465        }
1466        futures::future::Either::Right(_) => Err(anyhow!(
1467            "Idle sleep prevention acquisition timed out after 10 seconds"
1468        )),
1469    }
1470}
1471
1472#[cfg(test)]
1473mod tests {
1474    use super::*;
1475    use gpui_platform::{Point, px};
1476
1477    #[cfg(any(feature = "wayland", feature = "x11"))]
1478    #[test]
1479    fn rejects_null_xkb_context() {
1480        let context = unsafe {
1481            // libxkbcommon permits unref on null, matching the value returned by Context::new on failure.
1482            xkb::Context::from_raw_ptr(std::ptr::null_mut())
1483        };
1484        let error = validate_xkb_context(context)
1485            .err()
1486            .expect("null XKB context should be rejected");
1487
1488        assert_eq!(
1489            error.to_string(),
1490            "libxkbcommon failed to create an XKB context"
1491        );
1492    }
1493
1494    #[test]
1495    fn test_is_within_click_distance() {
1496        let zero = Point::new(px(0.0), px(0.0));
1497        assert!(is_within_click_distance(zero, Point::new(px(5.0), px(5.0))));
1498        assert!(is_within_click_distance(
1499            zero,
1500            Point::new(px(-4.9), px(5.0))
1501        ));
1502        assert!(is_within_click_distance(
1503            Point::new(px(3.0), px(2.0)),
1504            Point::new(px(-2.0), px(-2.0))
1505        ));
1506        assert!(!is_within_click_distance(
1507            zero,
1508            Point::new(px(5.0), px(5.1))
1509        ),);
1510    }
1511
1512    #[cfg(any(feature = "wayland", feature = "x11"))]
1513    mod idle_sleep_prevention {
1514        use super::super::await_idle_sleep_prevention;
1515        use anyhow::{Result, anyhow};
1516        use futures::channel::oneshot;
1517        use gpui::{ActivityGuard, TestAppContext};
1518        use std::{
1519            sync::{
1520                Arc,
1521                atomic::{AtomicUsize, Ordering::SeqCst},
1522            },
1523            time::Duration,
1524        };
1525
1526        #[gpui::test]
1527        async fn guard_passes_through_and_releases_on_drop(cx: &mut TestAppContext) {
1528            let released = Arc::new(AtomicUsize::new(0));
1529            let (guard_tx, guard_rx) = oneshot::channel();
1530            assert!(guard_tx.send(Ok(release_guard(released.clone()))).is_ok());
1531            let guard = await_idle_sleep_prevention(guard_rx, &cx.background_executor)
1532                .await
1533                .expect("acquisition should succeed");
1534
1535            assert_eq!(released.load(SeqCst), 0);
1536            drop(guard);
1537            assert_eq!(released.load(SeqCst), 1);
1538        }
1539
1540        #[gpui::test]
1541        async fn acquisition_error_passes_through(cx: &mut TestAppContext) {
1542            let (guard_tx, guard_rx) = oneshot::channel::<Result<ActivityGuard>>();
1543            assert!(guard_tx.send(Err(anyhow!("inhibition rejected"))).is_ok());
1544            assert_eq!(
1545                await_idle_sleep_prevention(guard_rx, &cx.background_executor)
1546                    .await
1547                    .err()
1548                    .expect("acquisition should fail")
1549                    .to_string(),
1550                "inhibition rejected"
1551            );
1552        }
1553
1554        #[gpui::test]
1555        async fn abandoned_acquisition_is_an_error(cx: &mut TestAppContext) {
1556            let (guard_tx, guard_rx) = oneshot::channel::<Result<ActivityGuard>>();
1557            drop(guard_tx);
1558            assert_eq!(
1559                await_idle_sleep_prevention(guard_rx, &cx.background_executor)
1560                    .await
1561                    .err()
1562                    .expect("acquisition should fail")
1563                    .to_string(),
1564                "Idle sleep prevention request was abandoned"
1565            );
1566        }
1567
1568        #[gpui::test]
1569        async fn late_guard_after_timeout_is_released(cx: &mut TestAppContext) {
1570            let released = Arc::new(AtomicUsize::new(0));
1571            let (guard_tx, guard_rx) = oneshot::channel();
1572            let task = cx.background_executor.spawn({
1573                let executor = cx.background_executor.clone();
1574                async move { await_idle_sleep_prevention(guard_rx, &executor).await }
1575            });
1576            cx.run_until_parked();
1577            cx.executor().advance_clock(Duration::from_secs(9));
1578            cx.run_until_parked();
1579            assert!(!guard_tx.is_canceled());
1580            cx.executor().advance_clock(Duration::from_secs(1));
1581            assert_eq!(
1582                task.await
1583                    .err()
1584                    .expect("acquisition should time out")
1585                    .to_string(),
1586                "Idle sleep prevention acquisition timed out after 10 seconds"
1587            );
1588
1589            assert!(guard_tx.is_canceled());
1590            if let Err(Ok(guard)) = guard_tx.send(Ok(release_guard(released.clone()))) {
1591                assert_eq!(released.load(SeqCst), 0);
1592                drop(guard);
1593            }
1594            assert_eq!(released.load(SeqCst), 1);
1595        }
1596
1597        fn release_guard(released: Arc<AtomicUsize>) -> ActivityGuard {
1598            ActivityGuard::new(move || {
1599                released.fetch_add(1, SeqCst);
1600            })
1601        }
1602    }
1603
1604    #[cfg(any(feature = "wayland", feature = "x11"))]
1605    mod read_fd_with_timeout {
1606        use super::super::{PIPE_READ_TIMEOUT, read_fd_with_timeout};
1607        use std::io::Write as _;
1608        use std::time::{Duration, Instant};
1609
1610        #[test]
1611        fn reads_data_written_before_close() {
1612            let mut pipe = filedescriptor::Pipe::new().unwrap();
1613            pipe.write.write_all(b"hello clipboard").unwrap();
1614            drop(pipe.write);
1615
1616            let bytes = read_fd_with_timeout(pipe.read, PIPE_READ_TIMEOUT).unwrap();
1617            assert_eq!(bytes, b"hello clipboard");
1618        }
1619
1620        #[test]
1621        fn returns_empty_when_writer_closes_without_writing() {
1622            let pipe = filedescriptor::Pipe::new().unwrap();
1623            drop(pipe.write);
1624
1625            let bytes = read_fd_with_timeout(pipe.read, PIPE_READ_TIMEOUT).unwrap();
1626            assert!(bytes.is_empty());
1627        }
1628
1629        #[test]
1630        fn times_out_when_writer_never_writes() {
1631            let pipe = filedescriptor::Pipe::new().unwrap();
1632            let _open_writer = pipe.write;
1633
1634            let timeout = Duration::from_millis(50);
1635            let started = Instant::now();
1636            let result = read_fd_with_timeout(pipe.read, timeout);
1637            let elapsed = started.elapsed();
1638
1639            let err = result.unwrap_err();
1640            assert!(
1641                err.to_string().contains("timed out"),
1642                "unexpected error: {err}"
1643            );
1644            assert!(elapsed >= timeout, "returned before the timeout elapsed");
1645        }
1646
1647        #[test]
1648        fn times_out_when_writer_stalls_after_partial_write() {
1649            let mut pipe = filedescriptor::Pipe::new().unwrap();
1650            pipe.write.write_all(b"partial").unwrap();
1651            let _open_writer = pipe.write;
1652
1653            let err = read_fd_with_timeout(pipe.read, Duration::from_millis(50)).unwrap_err();
1654            assert!(
1655                err.to_string().contains("timed out"),
1656                "unexpected error: {err}"
1657            );
1658        }
1659
1660        #[test]
1661        fn slow_writer_resets_deadline_between_chunks() {
1662            let pipe = filedescriptor::Pipe::new().unwrap();
1663            let chunks = 12;
1664            let gap = Duration::from_millis(40);
1665            let timeout = Duration::from_millis(400);
1666
1667            let writer = std::thread::spawn({
1668                let mut write = pipe.write;
1669                move || {
1670                    for _ in 0..chunks {
1671                        std::thread::sleep(gap);
1672                        write.write_all(&[b'x'; 1000]).unwrap();
1673                    }
1674                }
1675            });
1676            // The total transfer (~480ms) exceeds the timeout; this only
1677            // passes because the timeout is re-armed per chunk.
1678            let bytes = read_fd_with_timeout(pipe.read, timeout).unwrap();
1679            writer.join().unwrap();
1680            assert_eq!(bytes, vec![b'x'; 1000 * chunks]);
1681        }
1682
1683        #[test]
1684        fn reads_payload_larger_than_pipe_capacity() {
1685            let pipe = filedescriptor::Pipe::new().unwrap();
1686            // Exceeds the 64 KiB pipe capacity, forcing the writer to block.
1687            let payload = vec![b'z'; 1024 * 1024];
1688
1689            let writer = std::thread::spawn({
1690                let mut write = pipe.write;
1691                let payload = payload.clone();
1692                move || write.write_all(&payload).unwrap()
1693            });
1694            let bytes = read_fd_with_timeout(pipe.read, PIPE_READ_TIMEOUT).unwrap();
1695            writer.join().unwrap();
1696            assert_eq!(bytes, payload);
1697        }
1698    }
1699}