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"))]
713impl crate::Keystroke {
714    pub(super) fn from_xkb(
715        state: &State,
716        keyboard_mapper: &LinuxKeyboardMapper,
717        mut modifiers: crate::Modifiers,
718        keycode: Keycode,
719    ) -> Self {
720        let key_utf32 = state.key_get_utf32(keycode);
721        let key_utf8 = state.key_get_utf8(keycode);
722        let key_sym = state.key_get_one_sym(keycode);
723
724        let key = match key_sym {
725            Keysym::space => "space".to_owned(),
726            Keysym::BackSpace => "backspace".to_owned(),
727            Keysym::Return => "enter".to_owned(),
728            // Keysym::Tab => "tab".to_owned(),
729            Keysym::ISO_Left_Tab => "tab".to_owned(),
730            Keysym::uparrow => "up".to_owned(),
731            Keysym::downarrow => "down".to_owned(),
732            Keysym::leftarrow => "left".to_owned(),
733            Keysym::rightarrow => "right".to_owned(),
734            Keysym::Home | Keysym::KP_Home => "home".to_owned(),
735            Keysym::End | Keysym::KP_End => "end".to_owned(),
736            Keysym::Prior | Keysym::KP_Prior => "pageup".to_owned(),
737            Keysym::Next | Keysym::KP_Next => "pagedown".to_owned(),
738            Keysym::XF86_Back => "back".to_owned(),
739            Keysym::XF86_Forward => "forward".to_owned(),
740            Keysym::Escape => "escape".to_owned(),
741            Keysym::Insert | Keysym::KP_Insert => "insert".to_owned(),
742            Keysym::Delete | Keysym::KP_Delete => "delete".to_owned(),
743            Keysym::Menu => "menu".to_owned(),
744            Keysym::XF86_Cut => "cut".to_owned(),
745            Keysym::XF86_Copy => "copy".to_owned(),
746            Keysym::XF86_Paste => "paste".to_owned(),
747            Keysym::XF86_New => "new".to_owned(),
748            Keysym::XF86_Open => "open".to_owned(),
749            Keysym::XF86_Save => "save".to_owned(),
750            Keysym::F1 => "f1".to_owned(),
751            Keysym::F2 => "f2".to_owned(),
752            Keysym::F3 => "f3".to_owned(),
753            Keysym::F4 => "f4".to_owned(),
754            Keysym::F5 => "f5".to_owned(),
755            Keysym::F6 => "f6".to_owned(),
756            Keysym::F7 => "f7".to_owned(),
757            Keysym::F8 => "f8".to_owned(),
758            Keysym::F9 => "f9".to_owned(),
759            Keysym::F10 => "f10".to_owned(),
760            Keysym::F11 => "f11".to_owned(),
761            Keysym::F12 => "f12".to_owned(),
762            Keysym::F13 => "f13".to_owned(),
763            Keysym::F14 => "f14".to_owned(),
764            Keysym::F15 => "f15".to_owned(),
765            Keysym::F16 => "f16".to_owned(),
766            Keysym::F17 => "f17".to_owned(),
767            Keysym::F18 => "f18".to_owned(),
768            Keysym::F19 => "f19".to_owned(),
769            Keysym::F20 => "f20".to_owned(),
770            Keysym::F21 => "f21".to_owned(),
771            Keysym::F22 => "f22".to_owned(),
772            Keysym::F23 => "f23".to_owned(),
773            Keysym::F24 => "f24".to_owned(),
774            _ => keyboard_mapper
775                .get_key(keycode, &mut modifiers)
776                .unwrap_or_else(|| {
777                    let name = xkb::keysym_get_name(key_sym).to_lowercase();
778                    if key_sym.is_keypad_key() {
779                        name.replace("kp_", "")
780                    } else {
781                        name
782                    }
783                }),
784        };
785
786        // Ignore control characters (and DEL) for the purposes of key_char
787        let key_char =
788            (key_utf32 >= 32 && key_utf32 != 127 && !key_utf8.is_empty()).then_some(key_utf8);
789
790        Self {
791            modifiers,
792            key,
793            key_char,
794        }
795    }
796}
797
798#[cfg(any(feature = "wayland", feature = "x11"))]
799impl crate::Modifiers {
800    pub(super) fn from_xkb(keymap_state: &State) -> Self {
801        let shift = keymap_state.mod_name_is_active(xkb::MOD_NAME_SHIFT, xkb::STATE_MODS_EFFECTIVE);
802        let alt = keymap_state.mod_name_is_active(xkb::MOD_NAME_ALT, xkb::STATE_MODS_EFFECTIVE);
803        let control =
804            keymap_state.mod_name_is_active(xkb::MOD_NAME_CTRL, xkb::STATE_MODS_EFFECTIVE);
805        let platform =
806            keymap_state.mod_name_is_active(xkb::MOD_NAME_LOGO, xkb::STATE_MODS_EFFECTIVE);
807        Self {
808            shift,
809            alt,
810            control,
811            platform,
812            function: false,
813        }
814    }
815}
816
817#[cfg(any(feature = "wayland", feature = "x11"))]
818impl crate::Capslock {
819    pub(super) fn from_xkb(keymap_state: &State) -> Self {
820        let on = keymap_state.mod_name_is_active(xkb::MOD_NAME_CAPS, xkb::STATE_MODS_EFFECTIVE);
821        Self { on }
822    }
823}
824
825#[cfg(test)]
826mod tests {
827    use super::*;
828    use crate::{Point, px};
829
830    #[test]
831    fn test_is_within_click_distance() {
832        let zero = Point::new(px(0.0), px(0.0));
833        assert_eq!(
834            is_within_click_distance(zero, Point::new(px(5.0), px(5.0))),
835            true
836        );
837        assert_eq!(
838            is_within_click_distance(zero, Point::new(px(-4.9), px(5.0))),
839            true
840        );
841        assert_eq!(
842            is_within_click_distance(Point::new(px(3.0), px(2.0)), Point::new(px(-2.0), px(-2.0))),
843            true
844        );
845        assert_eq!(
846            is_within_click_distance(zero, Point::new(px(5.0), px(5.1))),
847            false
848        );
849    }
850}