platform.rs

  1use std::{
  2    env,
  3    path::{Path, PathBuf},
  4    process::Command,
  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, FromRawFd},
 14    time::Duration,
 15};
 16
 17use anyhow::{Context as _, anyhow};
 18use async_task::Runnable;
 19use calloop::{LoopSignal, channel::Channel};
 20use futures::channel::oneshot;
 21use util::ResultExt as _;
 22#[cfg(any(feature = "wayland", feature = "x11"))]
 23use xkbcommon::xkb::{self, Keycode, Keysym, State};
 24
 25use crate::{
 26    Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
 27    ForegroundExecutor, Keymap, LinuxDispatcher, Menu, MenuItem, OwnedMenu, PathPromptOptions,
 28    Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow,
 29    Point, Result, Task, WindowAppearance, WindowParams, px,
 30};
 31
 32#[cfg(any(feature = "wayland", feature = "x11"))]
 33use super::LinuxKeyboardMapper;
 34
 35#[cfg(any(feature = "wayland", feature = "x11"))]
 36pub(crate) const SCROLL_LINES: f32 = 3.0;
 37
 38// Values match the defaults on GTK.
 39// Taken from https://github.com/GNOME/gtk/blob/main/gtk/gtksettings.c#L320
 40#[cfg(any(feature = "wayland", feature = "x11"))]
 41pub(crate) const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(400);
 42pub(crate) const DOUBLE_CLICK_DISTANCE: Pixels = px(5.0);
 43pub(crate) const KEYRING_LABEL: &str = "zed-github-account";
 44
 45#[cfg(any(feature = "wayland", feature = "x11"))]
 46const FILE_PICKER_PORTAL_MISSING: &str =
 47    "Couldn't open file picker due to missing xdg-desktop-portal implementation.";
 48
 49pub trait LinuxClient {
 50    fn compositor_name(&self) -> &'static str;
 51    fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R;
 52    fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
 53    fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
 54    #[allow(unused)]
 55    fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>>;
 56    fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
 57    #[cfg(feature = "screen-capture")]
 58    fn is_screen_capture_supported(&self) -> bool;
 59    #[cfg(feature = "screen-capture")]
 60    fn screen_capture_sources(
 61        &self,
 62    ) -> oneshot::Receiver<Result<Vec<Box<dyn crate::ScreenCaptureSource>>>>;
 63
 64    fn open_window(
 65        &self,
 66        handle: AnyWindowHandle,
 67        options: WindowParams,
 68    ) -> anyhow::Result<Box<dyn PlatformWindow>>;
 69    fn set_cursor_style(&self, style: CursorStyle);
 70    fn open_uri(&self, uri: &str);
 71    fn reveal_path(&self, path: PathBuf);
 72    fn write_to_primary(&self, item: ClipboardItem);
 73    fn write_to_clipboard(&self, item: ClipboardItem);
 74    fn read_from_primary(&self) -> Option<ClipboardItem>;
 75    fn read_from_clipboard(&self) -> Option<ClipboardItem>;
 76    fn active_window(&self) -> Option<AnyWindowHandle>;
 77    fn window_stack(&self) -> Option<Vec<AnyWindowHandle>>;
 78    fn run(&self);
 79}
 80
 81#[derive(Default)]
 82pub(crate) struct PlatformHandlers {
 83    pub(crate) open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
 84    pub(crate) quit: Option<Box<dyn FnMut()>>,
 85    pub(crate) reopen: Option<Box<dyn FnMut()>>,
 86    pub(crate) app_menu_action: Option<Box<dyn FnMut(&dyn Action)>>,
 87    pub(crate) will_open_app_menu: Option<Box<dyn FnMut()>>,
 88    pub(crate) validate_app_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
 89    pub(crate) keyboard_layout_change: Option<Box<dyn FnMut()>>,
 90}
 91
 92pub(crate) struct LinuxCommon {
 93    pub(crate) background_executor: BackgroundExecutor,
 94    pub(crate) foreground_executor: ForegroundExecutor,
 95    pub(crate) text_system: Arc<dyn PlatformTextSystem>,
 96    pub(crate) appearance: WindowAppearance,
 97    pub(crate) auto_hide_scrollbars: bool,
 98    pub(crate) callbacks: PlatformHandlers,
 99    pub(crate) signal: LoopSignal,
100    pub(crate) menus: Vec<OwnedMenu>,
101}
102
103impl LinuxCommon {
104    pub fn new(signal: LoopSignal) -> (Self, Channel<Runnable>) {
105        let (main_sender, main_receiver) = calloop::channel::channel::<Runnable>();
106
107        #[cfg(any(feature = "wayland", feature = "x11"))]
108        let text_system = Arc::new(crate::CosmicTextSystem::new());
109        #[cfg(not(any(feature = "wayland", feature = "x11")))]
110        let text_system = Arc::new(crate::NoopTextSystem::new());
111
112        let callbacks = PlatformHandlers::default();
113
114        let dispatcher = Arc::new(LinuxDispatcher::new(main_sender.clone()));
115
116        let background_executor = BackgroundExecutor::new(dispatcher.clone());
117
118        let common = LinuxCommon {
119            background_executor,
120            foreground_executor: ForegroundExecutor::new(dispatcher.clone()),
121            text_system,
122            appearance: WindowAppearance::Light,
123            auto_hide_scrollbars: false,
124            callbacks,
125            signal,
126            menus: Vec::new(),
127        };
128
129        (common, main_receiver)
130    }
131}
132
133impl<P: LinuxClient + 'static> Platform for P {
134    fn background_executor(&self) -> BackgroundExecutor {
135        self.with_common(|common| common.background_executor.clone())
136    }
137
138    fn foreground_executor(&self) -> ForegroundExecutor {
139        self.with_common(|common| common.foreground_executor.clone())
140    }
141
142    fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
143        self.with_common(|common| common.text_system.clone())
144    }
145
146    fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
147        self.keyboard_layout()
148    }
149
150    fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
151        self.with_common(|common| common.callbacks.keyboard_layout_change = Some(callback));
152    }
153
154    fn run(&self, on_finish_launching: Box<dyn FnOnce()>) {
155        on_finish_launching();
156
157        LinuxClient::run(self);
158
159        let quit = self.with_common(|common| common.callbacks.quit.take());
160        if let Some(mut fun) = quit {
161            fun();
162        }
163    }
164
165    fn quit(&self) {
166        self.with_common(|common| common.signal.stop());
167    }
168
169    fn compositor_name(&self) -> &'static str {
170        self.compositor_name()
171    }
172
173    fn restart(&self, binary_path: Option<PathBuf>) {
174        use std::os::unix::process::CommandExt as _;
175
176        // get the process id of the current process
177        let app_pid = std::process::id().to_string();
178        // get the path to the executable
179        let app_path = if let Some(path) = binary_path {
180            path
181        } else {
182            match self.app_path() {
183                Ok(path) => path,
184                Err(err) => {
185                    log::error!("Failed to get app path: {:?}", err);
186                    return;
187                }
188            }
189        };
190
191        log::info!("Restarting process, using app path: {:?}", app_path);
192
193        // Script to wait for the current process to exit and then restart the app.
194        let script = format!(
195            r#"
196            while kill -0 {pid} 2>/dev/null; do
197                sleep 0.1
198            done
199
200            {app_path}
201            "#,
202            pid = app_pid,
203            app_path = app_path.display()
204        );
205
206        let restart_process = Command::new("/usr/bin/env")
207            .arg("bash")
208            .arg("-c")
209            .arg(script)
210            .process_group(0)
211            .spawn();
212
213        match restart_process {
214            Ok(_) => self.quit(),
215            Err(e) => log::error!("failed to spawn restart script: {:?}", e),
216        }
217    }
218
219    fn activate(&self, _ignoring_other_apps: bool) {
220        log::info!("activate is not implemented on Linux, ignoring the call")
221    }
222
223    fn hide(&self) {
224        log::info!("hide is not implemented on Linux, ignoring the call")
225    }
226
227    fn hide_other_apps(&self) {
228        log::info!("hide_other_apps is not implemented on Linux, ignoring the call")
229    }
230
231    fn unhide_other_apps(&self) {
232        log::info!("unhide_other_apps is not implemented on Linux, ignoring the call")
233    }
234
235    fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
236        self.primary_display()
237    }
238
239    fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
240        self.displays()
241    }
242
243    #[cfg(feature = "screen-capture")]
244    fn is_screen_capture_supported(&self) -> bool {
245        self.is_screen_capture_supported()
246    }
247
248    #[cfg(feature = "screen-capture")]
249    fn screen_capture_sources(
250        &self,
251    ) -> oneshot::Receiver<Result<Vec<Box<dyn crate::ScreenCaptureSource>>>> {
252        self.screen_capture_sources()
253    }
254
255    fn active_window(&self) -> Option<AnyWindowHandle> {
256        self.active_window()
257    }
258
259    fn window_stack(&self) -> Option<Vec<AnyWindowHandle>> {
260        self.window_stack()
261    }
262
263    fn open_window(
264        &self,
265        handle: AnyWindowHandle,
266        options: WindowParams,
267    ) -> anyhow::Result<Box<dyn PlatformWindow>> {
268        self.open_window(handle, options)
269    }
270
271    fn open_url(&self, url: &str) {
272        self.open_uri(url);
273    }
274
275    fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
276        self.with_common(|common| common.callbacks.open_urls = Some(callback));
277    }
278
279    fn prompt_for_paths(
280        &self,
281        options: PathPromptOptions,
282    ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
283        let (done_tx, done_rx) = oneshot::channel();
284
285        #[cfg(not(any(feature = "wayland", feature = "x11")))]
286        let _ = (done_tx.send(Ok(None)), options);
287
288        #[cfg(any(feature = "wayland", feature = "x11"))]
289        self.foreground_executor()
290            .spawn(async move {
291                let title = if options.directories {
292                    "Open Folder"
293                } else {
294                    "Open File"
295                };
296
297                let request = match ashpd::desktop::file_chooser::OpenFileRequest::default()
298                    .modal(true)
299                    .title(title)
300                    .multiple(options.multiple)
301                    .directory(options.directories)
302                    .send()
303                    .await
304                {
305                    Ok(request) => request,
306                    Err(err) => {
307                        let result = match err {
308                            ashpd::Error::PortalNotFound(_) => anyhow!(FILE_PICKER_PORTAL_MISSING),
309                            err => err.into(),
310                        };
311                        let _ = done_tx.send(Err(result));
312                        return;
313                    }
314                };
315
316                let result = match request.response() {
317                    Ok(response) => Ok(Some(
318                        response
319                            .uris()
320                            .iter()
321                            .filter_map(|uri| uri.to_file_path().ok())
322                            .collect::<Vec<_>>(),
323                    )),
324                    Err(ashpd::Error::Response(_)) => Ok(None),
325                    Err(e) => Err(e.into()),
326                };
327                let _ = done_tx.send(result);
328            })
329            .detach();
330        done_rx
331    }
332
333    fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Result<Option<PathBuf>>> {
334        let (done_tx, done_rx) = oneshot::channel();
335
336        #[cfg(not(any(feature = "wayland", feature = "x11")))]
337        let _ = (done_tx.send(Ok(None)), directory);
338
339        #[cfg(any(feature = "wayland", feature = "x11"))]
340        self.foreground_executor()
341            .spawn({
342                let directory = directory.to_owned();
343
344                async move {
345                    let request = match ashpd::desktop::file_chooser::SaveFileRequest::default()
346                        .modal(true)
347                        .title("Save File")
348                        .current_folder(directory)
349                        .expect("pathbuf should not be nul terminated")
350                        .send()
351                        .await
352                    {
353                        Ok(request) => request,
354                        Err(err) => {
355                            let result = match err {
356                                ashpd::Error::PortalNotFound(_) => {
357                                    anyhow!(FILE_PICKER_PORTAL_MISSING)
358                                }
359                                err => err.into(),
360                            };
361                            let _ = done_tx.send(Err(result));
362                            return;
363                        }
364                    };
365
366                    let result = match request.response() {
367                        Ok(response) => Ok(response
368                            .uris()
369                            .first()
370                            .and_then(|uri| uri.to_file_path().ok())),
371                        Err(ashpd::Error::Response(_)) => Ok(None),
372                        Err(e) => Err(e.into()),
373                    };
374                    let _ = done_tx.send(result);
375                }
376            })
377            .detach();
378
379        done_rx
380    }
381
382    fn can_select_mixed_files_and_dirs(&self) -> bool {
383        // org.freedesktop.portal.FileChooser only supports "pick files" and "pick directories".
384        false
385    }
386
387    fn reveal_path(&self, path: &Path) {
388        self.reveal_path(path.to_owned());
389    }
390
391    fn open_with_system(&self, path: &Path) {
392        let path = path.to_owned();
393        self.background_executor()
394            .spawn(async move {
395                let _ = std::process::Command::new("xdg-open")
396                    .arg(path)
397                    .spawn()
398                    .context("invoking xdg-open")
399                    .log_err();
400            })
401            .detach();
402    }
403
404    fn on_quit(&self, callback: Box<dyn FnMut()>) {
405        self.with_common(|common| {
406            common.callbacks.quit = Some(callback);
407        });
408    }
409
410    fn on_reopen(&self, callback: Box<dyn FnMut()>) {
411        self.with_common(|common| {
412            common.callbacks.reopen = Some(callback);
413        });
414    }
415
416    fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
417        self.with_common(|common| {
418            common.callbacks.app_menu_action = Some(callback);
419        });
420    }
421
422    fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>) {
423        self.with_common(|common| {
424            common.callbacks.will_open_app_menu = Some(callback);
425        });
426    }
427
428    fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
429        self.with_common(|common| {
430            common.callbacks.validate_app_menu_command = Some(callback);
431        });
432    }
433
434    fn app_path(&self) -> Result<PathBuf> {
435        // get the path of the executable of the current process
436        let app_path = env::current_exe()?;
437        return Ok(app_path);
438    }
439
440    fn set_menus(&self, menus: Vec<Menu>, _keymap: &Keymap) {
441        self.with_common(|common| {
442            common.menus = menus.into_iter().map(|menu| menu.owned()).collect();
443        })
444    }
445
446    fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
447        self.with_common(|common| Some(common.menus.clone()))
448    }
449
450    fn set_dock_menu(&self, _menu: Vec<MenuItem>, _keymap: &Keymap) {
451        // todo(linux)
452    }
453
454    fn path_for_auxiliary_executable(&self, _name: &str) -> Result<PathBuf> {
455        Err(anyhow::Error::msg(
456            "Platform<LinuxPlatform>::path_for_auxiliary_executable is not implemented yet",
457        ))
458    }
459
460    fn set_cursor_style(&self, style: CursorStyle) {
461        self.set_cursor_style(style)
462    }
463
464    fn should_auto_hide_scrollbars(&self) -> bool {
465        self.with_common(|common| common.auto_hide_scrollbars)
466    }
467
468    fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>> {
469        let url = url.to_string();
470        let username = username.to_string();
471        let password = password.to_vec();
472        self.background_executor().spawn(async move {
473            let keyring = oo7::Keyring::new().await?;
474            keyring.unlock().await?;
475            keyring
476                .create_item(
477                    KEYRING_LABEL,
478                    &vec![("url", &url), ("username", &username)],
479                    password,
480                    true,
481                )
482                .await?;
483            Ok(())
484        })
485    }
486
487    fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
488        let url = url.to_string();
489        self.background_executor().spawn(async move {
490            let keyring = oo7::Keyring::new().await?;
491            keyring.unlock().await?;
492
493            let items = keyring.search_items(&vec![("url", &url)]).await?;
494
495            for item in items.into_iter() {
496                if item.label().await.is_ok_and(|label| label == KEYRING_LABEL) {
497                    let attributes = item.attributes().await?;
498                    let username = attributes
499                        .get("username")
500                        .context("Cannot find username in stored credentials")?;
501                    item.unlock().await?;
502                    let secret = item.secret().await?;
503
504                    // we lose the zeroizing capabilities at this boundary,
505                    // a current limitation GPUI's credentials api
506                    return Ok(Some((username.to_string(), secret.to_vec())));
507                } else {
508                    continue;
509                }
510            }
511            Ok(None)
512        })
513    }
514
515    fn delete_credentials(&self, url: &str) -> Task<Result<()>> {
516        let url = url.to_string();
517        self.background_executor().spawn(async move {
518            let keyring = oo7::Keyring::new().await?;
519            keyring.unlock().await?;
520
521            let items = keyring.search_items(&vec![("url", &url)]).await?;
522
523            for item in items.into_iter() {
524                if item.label().await.is_ok_and(|label| label == KEYRING_LABEL) {
525                    item.delete().await?;
526                    return Ok(());
527                }
528            }
529
530            Ok(())
531        })
532    }
533
534    fn window_appearance(&self) -> WindowAppearance {
535        self.with_common(|common| common.appearance)
536    }
537
538    fn register_url_scheme(&self, _: &str) -> Task<anyhow::Result<()>> {
539        Task::ready(Err(anyhow!("register_url_scheme unimplemented")))
540    }
541
542    fn write_to_primary(&self, item: ClipboardItem) {
543        self.write_to_primary(item)
544    }
545
546    fn write_to_clipboard(&self, item: ClipboardItem) {
547        self.write_to_clipboard(item)
548    }
549
550    fn read_from_primary(&self) -> Option<ClipboardItem> {
551        self.read_from_primary()
552    }
553
554    fn read_from_clipboard(&self) -> Option<ClipboardItem> {
555        self.read_from_clipboard()
556    }
557
558    fn add_recent_document(&self, _path: &Path) {}
559}
560
561#[cfg(any(feature = "wayland", feature = "x11"))]
562pub(super) fn open_uri_internal(
563    executor: BackgroundExecutor,
564    uri: &str,
565    activation_token: Option<String>,
566) {
567    if let Some(uri) = ashpd::url::Url::parse(uri).log_err() {
568        executor
569            .spawn(async move {
570                match ashpd::desktop::open_uri::OpenFileRequest::default()
571                    .activation_token(activation_token.clone().map(ashpd::ActivationToken::from))
572                    .send_uri(&uri)
573                    .await
574                {
575                    Ok(_) => return,
576                    Err(e) => log::error!("Failed to open with dbus: {}", e),
577                }
578
579                for mut command in open::commands(uri.to_string()) {
580                    if let Some(token) = activation_token.as_ref() {
581                        command.env("XDG_ACTIVATION_TOKEN", token);
582                    }
583                    match command.spawn() {
584                        Ok(_) => return,
585                        Err(e) => {
586                            log::error!("Failed to open with {:?}: {}", command.get_program(), e)
587                        }
588                    }
589                }
590            })
591            .detach();
592    }
593}
594
595#[cfg(any(feature = "x11", feature = "wayland"))]
596pub(super) fn reveal_path_internal(
597    executor: BackgroundExecutor,
598    path: PathBuf,
599    activation_token: Option<String>,
600) {
601    executor
602        .spawn(async move {
603            if let Some(dir) = File::open(path.clone()).log_err() {
604                match ashpd::desktop::open_uri::OpenDirectoryRequest::default()
605                    .activation_token(activation_token.map(ashpd::ActivationToken::from))
606                    .send(&dir.as_fd())
607                    .await
608                {
609                    Ok(_) => return,
610                    Err(e) => log::error!("Failed to open with dbus: {}", e),
611                }
612                if path.is_dir() {
613                    open::that_detached(path).log_err();
614                } else {
615                    open::that_detached(path.parent().unwrap_or(Path::new(""))).log_err();
616                }
617            }
618        })
619        .detach();
620}
621
622#[allow(unused)]
623pub(super) fn is_within_click_distance(a: Point<Pixels>, b: Point<Pixels>) -> bool {
624    let diff = a - b;
625    diff.x.abs() <= DOUBLE_CLICK_DISTANCE && diff.y.abs() <= DOUBLE_CLICK_DISTANCE
626}
627
628#[cfg(any(feature = "wayland", feature = "x11"))]
629pub(super) fn get_xkb_compose_state(cx: &xkb::Context) -> Option<xkb::compose::State> {
630    let mut locales = Vec::default();
631    if let Some(locale) = env::var_os("LC_CTYPE") {
632        locales.push(locale);
633    }
634    locales.push(OsString::from("C"));
635    let mut state: Option<xkb::compose::State> = None;
636    for locale in locales {
637        if let Ok(table) =
638            xkb::compose::Table::new_from_locale(&cx, &locale, xkb::compose::COMPILE_NO_FLAGS)
639        {
640            state = Some(xkb::compose::State::new(
641                &table,
642                xkb::compose::STATE_NO_FLAGS,
643            ));
644            break;
645        }
646    }
647    state
648}
649
650#[cfg(any(feature = "wayland", feature = "x11"))]
651pub(super) unsafe fn read_fd(mut fd: filedescriptor::FileDescriptor) -> Result<Vec<u8>> {
652    let mut file = unsafe { File::from_raw_fd(fd.as_raw_fd()) };
653    let mut buffer = Vec::new();
654    file.read_to_end(&mut buffer)?;
655    Ok(buffer)
656}
657
658#[cfg(any(feature = "wayland", feature = "x11"))]
659pub(super) const DEFAULT_CURSOR_ICON_NAME: &str = "left_ptr";
660
661impl CursorStyle {
662    #[cfg(any(feature = "wayland", feature = "x11"))]
663    pub(super) fn to_icon_names(&self) -> &'static [&'static str] {
664        // Based on cursor names from chromium:
665        // https://github.com/chromium/chromium/blob/d3069cf9c973dc3627fa75f64085c6a86c8f41bf/ui/base/cursor/cursor_factory.cc#L113
666        match self {
667            CursorStyle::Arrow => &[DEFAULT_CURSOR_ICON_NAME],
668            CursorStyle::IBeam => &["text", "xterm"],
669            CursorStyle::Crosshair => &["crosshair", "cross"],
670            CursorStyle::ClosedHand => &["closedhand", "grabbing", "hand2"],
671            CursorStyle::OpenHand => &["openhand", "grab", "hand1"],
672            CursorStyle::PointingHand => &["pointer", "hand", "hand2"],
673            CursorStyle::ResizeLeft => &["w-resize", "left_side"],
674            CursorStyle::ResizeRight => &["e-resize", "right_side"],
675            CursorStyle::ResizeLeftRight => &["ew-resize", "sb_h_double_arrow"],
676            CursorStyle::ResizeUp => &["n-resize", "top_side"],
677            CursorStyle::ResizeDown => &["s-resize", "bottom_side"],
678            CursorStyle::ResizeUpDown => &["sb_v_double_arrow", "ns-resize"],
679            CursorStyle::ResizeUpLeftDownRight => &["size_fdiag", "bd_double_arrow", "nwse-resize"],
680            CursorStyle::ResizeUpRightDownLeft => &["size_bdiag", "nesw-resize", "fd_double_arrow"],
681            CursorStyle::ResizeColumn => &["col-resize", "sb_h_double_arrow"],
682            CursorStyle::ResizeRow => &["row-resize", "sb_v_double_arrow"],
683            CursorStyle::IBeamCursorForVerticalLayout => &["vertical-text"],
684            CursorStyle::OperationNotAllowed => &["not-allowed", "crossed_circle"],
685            CursorStyle::DragLink => &["alias"],
686            CursorStyle::DragCopy => &["copy"],
687            CursorStyle::ContextualMenu => &["context-menu"],
688            CursorStyle::None => {
689                #[cfg(debug_assertions)]
690                panic!("CursorStyle::None should be handled separately in the client");
691                #[cfg(not(debug_assertions))]
692                &[DEFAULT_CURSOR_ICON_NAME]
693            }
694        }
695    }
696}
697
698#[cfg(any(feature = "wayland", feature = "x11"))]
699pub(super) fn log_cursor_icon_warning(message: impl std::fmt::Display) {
700    if let Ok(xcursor_path) = env::var("XCURSOR_PATH") {
701        log::warn!(
702            "{:#}\ncursor icon loading may be failing if XCURSOR_PATH environment variable is invalid. \
703                    XCURSOR_PATH overrides the default icon search. Its current value is '{}'",
704            message,
705            xcursor_path
706        );
707    } else {
708        log::warn!("{:#}", message);
709    }
710}
711
712#[cfg(any(feature = "wayland", feature = "x11"))]
713struct KeyboardState {
714    state: xkb::State,
715    mapper: LinuxKeyboardMapper,
716}
717
718#[cfg(any(feature = "wayland", feature = "x11"))]
719impl KeyboardState {
720    fn new(state: xkb::State) -> Self {
721        let mapper = LinuxKeyboardMapper::new(&state);
722        Self { state, mapper }
723    }
724}
725
726#[cfg(any(feature = "wayland", feature = "x11"))]
727impl crate::Keystroke {
728    pub(super) fn from_xkb(
729        state: &State,
730        mut modifiers: crate::Modifiers,
731        keycode: Keycode,
732    ) -> Self {
733        let key_utf32 = state.key_get_utf32(keycode);
734        let key_utf8 = state.key_get_utf8(keycode);
735        let key_sym = state.key_get_one_sym(keycode);
736
737        let key = match key_sym {
738            Keysym::space => "space".to_owned(),
739            Keysym::BackSpace => "backspace".to_owned(),
740            Keysym::Return => "enter".to_owned(),
741            // Keysym::Tab => "tab".to_owned(),
742            Keysym::ISO_Left_Tab => "tab".to_owned(),
743            Keysym::uparrow => "up".to_owned(),
744            Keysym::downarrow => "down".to_owned(),
745            Keysym::leftarrow => "left".to_owned(),
746            Keysym::rightarrow => "right".to_owned(),
747            Keysym::Home | Keysym::KP_Home => "home".to_owned(),
748            Keysym::End | Keysym::KP_End => "end".to_owned(),
749            Keysym::Prior | Keysym::KP_Prior => "pageup".to_owned(),
750            Keysym::Next | Keysym::KP_Next => "pagedown".to_owned(),
751            Keysym::XF86_Back => "back".to_owned(),
752            Keysym::XF86_Forward => "forward".to_owned(),
753            Keysym::Escape => "escape".to_owned(),
754            Keysym::Insert | Keysym::KP_Insert => "insert".to_owned(),
755            Keysym::Delete | Keysym::KP_Delete => "delete".to_owned(),
756            Keysym::Menu => "menu".to_owned(),
757            Keysym::XF86_Cut => "cut".to_owned(),
758            Keysym::XF86_Copy => "copy".to_owned(),
759            Keysym::XF86_Paste => "paste".to_owned(),
760            Keysym::XF86_New => "new".to_owned(),
761            Keysym::XF86_Open => "open".to_owned(),
762            Keysym::XF86_Save => "save".to_owned(),
763            _ => {
764                let name = xkb::keysym_get_name(key_sym).to_lowercase();
765                if key_sym.is_keypad_key() {
766                    name.replace("kp_", "")
767                } else {
768                    name
769                }
770            }
771        };
772
773        if modifiers.shift {
774            // we only include the shift for upper-case letters by convention,
775            // so don't include for numbers and symbols, but do include for
776            // tab/enter, etc.
777            if key.chars().count() == 1 && key.to_lowercase() == key.to_uppercase() {
778                modifiers.shift = false;
779            }
780        }
781
782        // Ignore control characters (and DEL) for the purposes of key_char
783        let key_char =
784            (key_utf32 >= 32 && key_utf32 != 127 && !key_utf8.is_empty()).then_some(key_utf8);
785
786        Self {
787            modifiers,
788            key,
789            key_char,
790        }
791    }
792
793    /**
794     * Returns which symbol the dead key represents
795     * <https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values#dead_keycodes_for_linux>
796     */
797    pub fn underlying_dead_key(keysym: Keysym) -> Option<String> {
798        match keysym {
799            Keysym::dead_grave => Some("`".to_owned()),
800            Keysym::dead_acute => Some("´".to_owned()),
801            Keysym::dead_circumflex => Some("^".to_owned()),
802            Keysym::dead_tilde => Some("~".to_owned()),
803            Keysym::dead_macron => Some("¯".to_owned()),
804            Keysym::dead_breve => Some("˘".to_owned()),
805            Keysym::dead_abovedot => Some("˙".to_owned()),
806            Keysym::dead_diaeresis => Some("¨".to_owned()),
807            Keysym::dead_abovering => Some("˚".to_owned()),
808            Keysym::dead_doubleacute => Some("˝".to_owned()),
809            Keysym::dead_caron => Some("ˇ".to_owned()),
810            Keysym::dead_cedilla => Some("¸".to_owned()),
811            Keysym::dead_ogonek => Some("˛".to_owned()),
812            Keysym::dead_iota => Some("ͅ".to_owned()),
813            Keysym::dead_voiced_sound => Some("".to_owned()),
814            Keysym::dead_semivoiced_sound => Some("".to_owned()),
815            Keysym::dead_belowdot => Some("̣̣".to_owned()),
816            Keysym::dead_hook => Some("̡".to_owned()),
817            Keysym::dead_horn => Some("̛".to_owned()),
818            Keysym::dead_stroke => Some("̶̶".to_owned()),
819            Keysym::dead_abovecomma => Some("̓̓".to_owned()),
820            Keysym::dead_abovereversedcomma => Some("ʽ".to_owned()),
821            Keysym::dead_doublegrave => Some("̏".to_owned()),
822            Keysym::dead_belowring => Some("˳".to_owned()),
823            Keysym::dead_belowmacron => Some("̱".to_owned()),
824            Keysym::dead_belowcircumflex => Some("".to_owned()),
825            Keysym::dead_belowtilde => Some("̰".to_owned()),
826            Keysym::dead_belowbreve => Some("̮".to_owned()),
827            Keysym::dead_belowdiaeresis => Some("̤".to_owned()),
828            Keysym::dead_invertedbreve => Some("̯".to_owned()),
829            Keysym::dead_belowcomma => Some("̦".to_owned()),
830            Keysym::dead_currency => None,
831            Keysym::dead_lowline => None,
832            Keysym::dead_aboveverticalline => None,
833            Keysym::dead_belowverticalline => None,
834            Keysym::dead_longsolidusoverlay => None,
835            Keysym::dead_a => None,
836            Keysym::dead_A => None,
837            Keysym::dead_e => None,
838            Keysym::dead_E => None,
839            Keysym::dead_i => None,
840            Keysym::dead_I => None,
841            Keysym::dead_o => None,
842            Keysym::dead_O => None,
843            Keysym::dead_u => None,
844            Keysym::dead_U => None,
845            Keysym::dead_small_schwa => Some("ə".to_owned()),
846            Keysym::dead_capital_schwa => Some("Ə".to_owned()),
847            Keysym::dead_greek => None,
848            _ => None,
849        }
850    }
851}
852
853#[cfg(any(feature = "wayland", feature = "x11"))]
854impl crate::Modifiers {
855    pub(super) fn from_xkb(keymap_state: &State) -> Self {
856        let shift = keymap_state.mod_name_is_active(xkb::MOD_NAME_SHIFT, xkb::STATE_MODS_EFFECTIVE);
857        let alt = keymap_state.mod_name_is_active(xkb::MOD_NAME_ALT, xkb::STATE_MODS_EFFECTIVE);
858        let control =
859            keymap_state.mod_name_is_active(xkb::MOD_NAME_CTRL, xkb::STATE_MODS_EFFECTIVE);
860        let platform =
861            keymap_state.mod_name_is_active(xkb::MOD_NAME_LOGO, xkb::STATE_MODS_EFFECTIVE);
862        Self {
863            shift,
864            alt,
865            control,
866            platform,
867            function: false,
868        }
869    }
870}
871
872#[cfg(any(feature = "wayland", feature = "x11"))]
873impl crate::Capslock {
874    pub(super) fn from_xkb(keymap_state: &State) -> Self {
875        let on = keymap_state.mod_name_is_active(xkb::MOD_NAME_CAPS, xkb::STATE_MODS_EFFECTIVE);
876        Self { on }
877    }
878}
879
880#[cfg(test)]
881mod tests {
882    use super::*;
883    use crate::{Point, px};
884
885    #[test]
886    fn test_is_within_click_distance() {
887        let zero = Point::new(px(0.0), px(0.0));
888        assert_eq!(
889            is_within_click_distance(zero, Point::new(px(5.0), px(5.0))),
890            true
891        );
892        assert_eq!(
893            is_within_click_distance(zero, Point::new(px(-4.9), px(5.0))),
894            true
895        );
896        assert_eq!(
897            is_within_click_distance(Point::new(px(3.0), px(2.0)), Point::new(px(-2.0), px(-2.0))),
898            true
899        );
900        assert_eq!(
901            is_within_click_distance(zero, Point::new(px(5.0), px(5.1))),
902            false
903        );
904    }
905}