platform.rs

  1#![allow(unused)]
  2
  3use std::any::{type_name, Any};
  4use std::cell::{self, RefCell};
  5use std::env;
  6use std::ffi::OsString;
  7use std::fs::File;
  8use std::io::Read;
  9use std::ops::{Deref, DerefMut};
 10use std::os::fd::{AsFd, AsRawFd, FromRawFd};
 11use std::panic::Location;
 12use std::rc::Weak;
 13use std::{
 14    path::{Path, PathBuf},
 15    process::Command,
 16    rc::Rc,
 17    sync::Arc,
 18    time::Duration,
 19};
 20
 21use anyhow::anyhow;
 22use ashpd::desktop::file_chooser::{OpenFileRequest, SaveFileRequest};
 23use ashpd::desktop::open_uri::{OpenDirectoryRequest, OpenFileRequest as OpenUriRequest};
 24use ashpd::desktop::ResponseError;
 25use ashpd::{url, ActivationToken};
 26use async_task::Runnable;
 27use calloop::channel::Channel;
 28use calloop::{EventLoop, LoopHandle, LoopSignal};
 29use filedescriptor::FileDescriptor;
 30use flume::{Receiver, Sender};
 31use futures::channel::oneshot;
 32use parking_lot::Mutex;
 33use time::UtcOffset;
 34use util::ResultExt;
 35use wayland_client::Connection;
 36use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape;
 37use xkbcommon::xkb::{self, Keycode, Keysym, State};
 38
 39use crate::platform::linux::wayland::WaylandClient;
 40use crate::{
 41    px, Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CosmicTextSystem, CursorStyle,
 42    DisplayId, ForegroundExecutor, Keymap, Keystroke, LinuxDispatcher, Menu, MenuItem, Modifiers,
 43    OwnedMenu, PathPromptOptions, Pixels, Platform, PlatformDisplay, PlatformInputHandler,
 44    PlatformTextSystem, PlatformWindow, Point, PromptLevel, Result, SemanticVersion, SharedString,
 45    Size, Task, WindowAppearance, WindowOptions, WindowParams,
 46};
 47
 48use super::x11::X11Client;
 49
 50pub(crate) const SCROLL_LINES: f64 = 3.0;
 51
 52// Values match the defaults on GTK.
 53// Taken from https://github.com/GNOME/gtk/blob/main/gtk/gtksettings.c#L320
 54pub(crate) const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(400);
 55pub(crate) const DOUBLE_CLICK_DISTANCE: Pixels = px(5.0);
 56pub(crate) const KEYRING_LABEL: &str = "zed-github-account";
 57
 58const FILE_PICKER_PORTAL_MISSING: &str =
 59    "Couldn't open file picker due to missing xdg-desktop-portal implementation.";
 60
 61pub trait LinuxClient {
 62    fn compositor_name(&self) -> &'static str;
 63    fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R;
 64    fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
 65    fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
 66    fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>>;
 67
 68    fn open_window(
 69        &self,
 70        handle: AnyWindowHandle,
 71        options: WindowParams,
 72    ) -> anyhow::Result<Box<dyn PlatformWindow>>;
 73    fn set_cursor_style(&self, style: CursorStyle);
 74    fn open_uri(&self, uri: &str);
 75    fn reveal_path(&self, path: PathBuf);
 76    fn write_to_primary(&self, item: ClipboardItem);
 77    fn write_to_clipboard(&self, item: ClipboardItem);
 78    fn read_from_primary(&self) -> Option<ClipboardItem>;
 79    fn read_from_clipboard(&self) -> Option<ClipboardItem>;
 80    fn active_window(&self) -> Option<AnyWindowHandle>;
 81    fn run(&self);
 82}
 83
 84#[derive(Default)]
 85pub(crate) struct PlatformHandlers {
 86    pub(crate) open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
 87    pub(crate) quit: Option<Box<dyn FnMut()>>,
 88    pub(crate) reopen: Option<Box<dyn FnMut()>>,
 89    pub(crate) app_menu_action: Option<Box<dyn FnMut(&dyn Action)>>,
 90    pub(crate) will_open_app_menu: Option<Box<dyn FnMut()>>,
 91    pub(crate) validate_app_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
 92}
 93
 94pub(crate) struct LinuxCommon {
 95    pub(crate) background_executor: BackgroundExecutor,
 96    pub(crate) foreground_executor: ForegroundExecutor,
 97    pub(crate) text_system: Arc<CosmicTextSystem>,
 98    pub(crate) appearance: WindowAppearance,
 99    pub(crate) auto_hide_scrollbars: bool,
100    pub(crate) callbacks: PlatformHandlers,
101    pub(crate) signal: LoopSignal,
102    pub(crate) menus: Vec<OwnedMenu>,
103}
104
105impl LinuxCommon {
106    pub fn new(signal: LoopSignal) -> (Self, Channel<Runnable>) {
107        let (main_sender, main_receiver) = calloop::channel::channel::<Runnable>();
108        let text_system = Arc::new(CosmicTextSystem::new());
109        let callbacks = PlatformHandlers::default();
110
111        let dispatcher = Arc::new(LinuxDispatcher::new(main_sender.clone()));
112
113        let background_executor = BackgroundExecutor::new(dispatcher.clone());
114
115        let common = LinuxCommon {
116            background_executor,
117            foreground_executor: ForegroundExecutor::new(dispatcher.clone()),
118            text_system,
119            appearance: WindowAppearance::Light,
120            auto_hide_scrollbars: false,
121            callbacks,
122            signal,
123            menus: Vec::new(),
124        };
125
126        (common, main_receiver)
127    }
128}
129
130impl<P: LinuxClient + 'static> Platform for P {
131    fn background_executor(&self) -> BackgroundExecutor {
132        self.with_common(|common| common.background_executor.clone())
133    }
134
135    fn foreground_executor(&self) -> ForegroundExecutor {
136        self.with_common(|common| common.foreground_executor.clone())
137    }
138
139    fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
140        self.with_common(|common| common.text_system.clone())
141    }
142
143    fn run(&self, on_finish_launching: Box<dyn FnOnce()>) {
144        on_finish_launching();
145
146        LinuxClient::run(self);
147
148        self.with_common(|common| {
149            if let Some(mut fun) = common.callbacks.quit.take() {
150                fun();
151            }
152        });
153    }
154
155    fn quit(&self) {
156        self.with_common(|common| common.signal.stop());
157    }
158
159    fn compositor_name(&self) -> &'static str {
160        self.compositor_name()
161    }
162
163    fn restart(&self, binary_path: Option<PathBuf>) {
164        use std::os::unix::process::CommandExt as _;
165
166        // get the process id of the current process
167        let app_pid = std::process::id().to_string();
168        // get the path to the executable
169        let app_path = if let Some(path) = binary_path {
170            path
171        } else {
172            match self.app_path() {
173                Ok(path) => path,
174                Err(err) => {
175                    log::error!("Failed to get app path: {:?}", err);
176                    return;
177                }
178            }
179        };
180
181        log::info!("Restarting process, using app path: {:?}", app_path);
182
183        // Script to wait for the current process to exit and then restart the app.
184        // We also wait for possibly open TCP sockets by the process to be closed,
185        // since on Linux it's not guaranteed that a process' resources have been
186        // cleaned up when `kill -0` returns.
187        let script = format!(
188            r#"
189            while kill -O {pid} 2>/dev/null; do
190                sleep 0.1
191            done
192
193            while lsof -nP -iTCP -a -p {pid} 2>/dev/null; do
194                sleep 0.1
195            done
196
197            {app_path}
198            "#,
199            pid = app_pid,
200            app_path = app_path.display()
201        );
202
203        // execute the script using /bin/bash
204        let restart_process = Command::new("/bin/bash")
205            .arg("-c")
206            .arg(script)
207            .process_group(0)
208            .spawn();
209
210        match restart_process {
211            Ok(_) => self.quit(),
212            Err(e) => log::error!("failed to spawn restart script: {:?}", e),
213        }
214    }
215
216    fn activate(&self, ignoring_other_apps: bool) {
217        log::info!("activate is not implemented on Linux, ignoring the call")
218    }
219
220    fn hide(&self) {
221        log::info!("hide is not implemented on Linux, ignoring the call")
222    }
223
224    fn hide_other_apps(&self) {
225        log::info!("hide_other_apps is not implemented on Linux, ignoring the call")
226    }
227
228    fn unhide_other_apps(&self) {
229        log::info!("unhide_other_apps is not implemented on Linux, ignoring the call")
230    }
231
232    fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
233        self.primary_display()
234    }
235
236    fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
237        self.displays()
238    }
239
240    fn active_window(&self) -> Option<AnyWindowHandle> {
241        self.active_window()
242    }
243
244    fn open_window(
245        &self,
246        handle: AnyWindowHandle,
247        options: WindowParams,
248    ) -> anyhow::Result<Box<dyn PlatformWindow>> {
249        self.open_window(handle, options)
250    }
251
252    fn open_url(&self, url: &str) {
253        self.open_uri(url);
254    }
255
256    fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
257        self.with_common(|common| common.callbacks.open_urls = Some(callback));
258    }
259
260    fn prompt_for_paths(
261        &self,
262        options: PathPromptOptions,
263    ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
264        let (done_tx, done_rx) = oneshot::channel();
265        self.foreground_executor()
266            .spawn(async move {
267                let title = if options.multiple {
268                    if !options.files {
269                        "Open folders"
270                    } else {
271                        "Open files"
272                    }
273                } else {
274                    if !options.files {
275                        "Open folder"
276                    } else {
277                        "Open file"
278                    }
279                };
280
281                let request = match OpenFileRequest::default()
282                    .modal(true)
283                    .title(title)
284                    .accept_label("Select")
285                    .multiple(options.multiple)
286                    .directory(options.directories)
287                    .send()
288                    .await
289                {
290                    Ok(request) => request,
291                    Err(err) => {
292                        let result = match err {
293                            ashpd::Error::PortalNotFound(_) => anyhow!(FILE_PICKER_PORTAL_MISSING),
294                            err => err.into(),
295                        };
296                        done_tx.send(Err(result));
297                        return;
298                    }
299                };
300
301                let result = match request.response() {
302                    Ok(response) => Ok(Some(
303                        response
304                            .uris()
305                            .iter()
306                            .filter_map(|uri| uri.to_file_path().ok())
307                            .collect::<Vec<_>>(),
308                    )),
309                    Err(ashpd::Error::Response(ResponseError::Cancelled)) => Ok(None),
310                    Err(e) => Err(e.into()),
311                };
312                done_tx.send(result);
313            })
314            .detach();
315        done_rx
316    }
317
318    fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Result<Option<PathBuf>>> {
319        let (done_tx, done_rx) = oneshot::channel();
320        let directory = directory.to_owned();
321        self.foreground_executor()
322            .spawn(async move {
323                let request = match SaveFileRequest::default()
324                    .modal(true)
325                    .title("Select new path")
326                    .accept_label("Accept")
327                    .current_folder(directory)
328                    .expect("pathbuf should not be nul terminated")
329                    .send()
330                    .await
331                {
332                    Ok(request) => request,
333                    Err(err) => {
334                        let result = match err {
335                            ashpd::Error::PortalNotFound(_) => anyhow!(FILE_PICKER_PORTAL_MISSING),
336                            err => err.into(),
337                        };
338                        done_tx.send(Err(result));
339                        return;
340                    }
341                };
342
343                let result = match request.response() {
344                    Ok(response) => Ok(response
345                        .uris()
346                        .first()
347                        .and_then(|uri| uri.to_file_path().ok())),
348                    Err(ashpd::Error::Response(ResponseError::Cancelled)) => Ok(None),
349                    Err(e) => Err(e.into()),
350                };
351                done_tx.send(result);
352            })
353            .detach();
354
355        done_rx
356    }
357
358    fn reveal_path(&self, path: &Path) {
359        self.reveal_path(path.to_owned());
360    }
361
362    fn on_quit(&self, callback: Box<dyn FnMut()>) {
363        self.with_common(|common| {
364            common.callbacks.quit = Some(callback);
365        });
366    }
367
368    fn on_reopen(&self, callback: Box<dyn FnMut()>) {
369        self.with_common(|common| {
370            common.callbacks.reopen = Some(callback);
371        });
372    }
373
374    fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
375        self.with_common(|common| {
376            common.callbacks.app_menu_action = Some(callback);
377        });
378    }
379
380    fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>) {
381        self.with_common(|common| {
382            common.callbacks.will_open_app_menu = Some(callback);
383        });
384    }
385
386    fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
387        self.with_common(|common| {
388            common.callbacks.validate_app_menu_command = Some(callback);
389        });
390    }
391
392    fn app_path(&self) -> Result<PathBuf> {
393        // get the path of the executable of the current process
394        let exe_path = std::env::current_exe()?;
395        Ok(exe_path)
396    }
397
398    fn set_menus(&self, menus: Vec<Menu>, _keymap: &Keymap) {
399        self.with_common(|common| {
400            common.menus = menus.into_iter().map(|menu| menu.owned()).collect();
401        })
402    }
403
404    fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
405        self.with_common(|common| Some(common.menus.clone()))
406    }
407
408    fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap) {}
409
410    fn local_timezone(&self) -> UtcOffset {
411        UtcOffset::UTC
412    }
413
414    fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
415        Err(anyhow::Error::msg(
416            "Platform<LinuxPlatform>::path_for_auxiliary_executable is not implemented yet",
417        ))
418    }
419
420    fn set_cursor_style(&self, style: CursorStyle) {
421        self.set_cursor_style(style)
422    }
423
424    fn should_auto_hide_scrollbars(&self) -> bool {
425        self.with_common(|common| common.auto_hide_scrollbars)
426    }
427
428    fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>> {
429        let url = url.to_string();
430        let username = username.to_string();
431        let password = password.to_vec();
432        self.background_executor().spawn(async move {
433            let keyring = oo7::Keyring::new().await?;
434            keyring.unlock().await?;
435            keyring
436                .create_item(
437                    KEYRING_LABEL,
438                    &vec![("url", &url), ("username", &username)],
439                    password,
440                    true,
441                )
442                .await?;
443            Ok(())
444        })
445    }
446
447    fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
448        let url = url.to_string();
449        self.background_executor().spawn(async move {
450            let keyring = oo7::Keyring::new().await?;
451            keyring.unlock().await?;
452
453            let items = keyring.search_items(&vec![("url", &url)]).await?;
454
455            for item in items.into_iter() {
456                if item.label().await.is_ok_and(|label| label == KEYRING_LABEL) {
457                    let attributes = item.attributes().await?;
458                    let username = attributes
459                        .get("username")
460                        .ok_or_else(|| anyhow!("Cannot find username in stored credentials"))?;
461                    let secret = item.secret().await?;
462
463                    // we lose the zeroizing capabilities at this boundary,
464                    // a current limitation GPUI's credentials api
465                    return Ok(Some((username.to_string(), secret.to_vec())));
466                } else {
467                    continue;
468                }
469            }
470            Ok(None)
471        })
472    }
473
474    fn delete_credentials(&self, url: &str) -> Task<Result<()>> {
475        let url = url.to_string();
476        self.background_executor().spawn(async move {
477            let keyring = oo7::Keyring::new().await?;
478            keyring.unlock().await?;
479
480            let items = keyring.search_items(&vec![("url", &url)]).await?;
481
482            for item in items.into_iter() {
483                if item.label().await.is_ok_and(|label| label == KEYRING_LABEL) {
484                    item.delete().await?;
485                    return Ok(());
486                }
487            }
488
489            Ok(())
490        })
491    }
492
493    fn window_appearance(&self) -> WindowAppearance {
494        self.with_common(|common| common.appearance)
495    }
496
497    fn register_url_scheme(&self, _: &str) -> Task<anyhow::Result<()>> {
498        Task::ready(Err(anyhow!("register_url_scheme unimplemented")))
499    }
500
501    fn write_to_primary(&self, item: ClipboardItem) {
502        self.write_to_primary(item)
503    }
504
505    fn write_to_clipboard(&self, item: ClipboardItem) {
506        self.write_to_clipboard(item)
507    }
508
509    fn read_from_primary(&self) -> Option<ClipboardItem> {
510        self.read_from_primary()
511    }
512
513    fn read_from_clipboard(&self) -> Option<ClipboardItem> {
514        self.read_from_clipboard()
515    }
516
517    fn add_recent_document(&self, _path: &Path) {}
518}
519
520pub(super) fn open_uri_internal(
521    executor: BackgroundExecutor,
522    uri: &str,
523    activation_token: Option<String>,
524) {
525    if let Some(uri) = url::Url::parse(uri).log_err() {
526        executor
527            .spawn(async move {
528                OpenUriRequest::default()
529                    .activation_token(activation_token.map(ActivationToken::from))
530                    .send_uri(&uri)
531                    .await
532                    .log_err();
533            })
534            .detach();
535    }
536}
537
538pub(super) fn reveal_path_internal(
539    executor: BackgroundExecutor,
540    path: PathBuf,
541    activation_token: Option<String>,
542) {
543    executor
544        .spawn(async move {
545            if let Some(dir) = File::open(path).log_err() {
546                OpenDirectoryRequest::default()
547                    .activation_token(activation_token.map(ActivationToken::from))
548                    .send(&dir.as_fd())
549                    .await
550                    .log_err();
551            }
552        })
553        .detach();
554}
555
556pub(super) fn is_within_click_distance(a: Point<Pixels>, b: Point<Pixels>) -> bool {
557    let diff = a - b;
558    diff.x.abs() <= DOUBLE_CLICK_DISTANCE && diff.y.abs() <= DOUBLE_CLICK_DISTANCE
559}
560
561pub(super) fn get_xkb_compose_state(cx: &xkb::Context) -> Option<xkb::compose::State> {
562    let mut locales = Vec::default();
563    if let Some(locale) = std::env::var_os("LC_CTYPE") {
564        locales.push(locale);
565    }
566    locales.push(OsString::from("C"));
567    let mut state: Option<xkb::compose::State> = None;
568    for locale in locales {
569        if let Ok(table) =
570            xkb::compose::Table::new_from_locale(&cx, &locale, xkb::compose::COMPILE_NO_FLAGS)
571        {
572            state = Some(xkb::compose::State::new(
573                &table,
574                xkb::compose::STATE_NO_FLAGS,
575            ));
576            break;
577        }
578    }
579    state
580}
581
582pub(super) unsafe fn read_fd(mut fd: FileDescriptor) -> Result<String> {
583    let mut file = File::from_raw_fd(fd.as_raw_fd());
584
585    let mut buffer = String::new();
586    file.read_to_string(&mut buffer)?;
587
588    // Normalize the text to unix line endings, otherwise
589    // copying from eg: firefox inserts a lot of blank
590    // lines, and that is super annoying.
591    let result = buffer.replace("\r\n", "\n");
592    Ok(result)
593}
594
595impl CursorStyle {
596    pub(super) fn to_shape(&self) -> Shape {
597        match self {
598            CursorStyle::Arrow => Shape::Default,
599            CursorStyle::IBeam => Shape::Text,
600            CursorStyle::Crosshair => Shape::Crosshair,
601            CursorStyle::ClosedHand => Shape::Grabbing,
602            CursorStyle::OpenHand => Shape::Grab,
603            CursorStyle::PointingHand => Shape::Pointer,
604            CursorStyle::ResizeLeft => Shape::WResize,
605            CursorStyle::ResizeRight => Shape::EResize,
606            CursorStyle::ResizeLeftRight => Shape::EwResize,
607            CursorStyle::ResizeUp => Shape::NResize,
608            CursorStyle::ResizeDown => Shape::SResize,
609            CursorStyle::ResizeUpDown => Shape::NsResize,
610            CursorStyle::ResizeUpLeftDownRight => Shape::NwseResize,
611            CursorStyle::ResizeUpRightDownLeft => Shape::NeswResize,
612            CursorStyle::ResizeColumn => Shape::ColResize,
613            CursorStyle::ResizeRow => Shape::RowResize,
614            CursorStyle::IBeamCursorForVerticalLayout => Shape::VerticalText,
615            CursorStyle::OperationNotAllowed => Shape::NotAllowed,
616            CursorStyle::DragLink => Shape::Alias,
617            CursorStyle::DragCopy => Shape::Copy,
618            CursorStyle::ContextualMenu => Shape::ContextMenu,
619        }
620    }
621
622    pub(super) fn to_icon_name(&self) -> String {
623        // Based on cursor names from https://gitlab.gnome.org/GNOME/adwaita-icon-theme (GNOME)
624        // and https://github.com/KDE/breeze (KDE). Both of them seem to be also derived from
625        // Web CSS cursor names: https://developer.mozilla.org/en-US/docs/Web/CSS/cursor#values
626        match self {
627            CursorStyle::Arrow => "arrow",
628            CursorStyle::IBeam => "text",
629            CursorStyle::Crosshair => "crosshair",
630            CursorStyle::ClosedHand => "grabbing",
631            CursorStyle::OpenHand => "grab",
632            CursorStyle::PointingHand => "pointer",
633            CursorStyle::ResizeLeft => "w-resize",
634            CursorStyle::ResizeRight => "e-resize",
635            CursorStyle::ResizeLeftRight => "ew-resize",
636            CursorStyle::ResizeUp => "n-resize",
637            CursorStyle::ResizeDown => "s-resize",
638            CursorStyle::ResizeUpDown => "ns-resize",
639            CursorStyle::ResizeUpLeftDownRight => "nwse-resize",
640            CursorStyle::ResizeUpRightDownLeft => "nesw-resize",
641            CursorStyle::ResizeColumn => "col-resize",
642            CursorStyle::ResizeRow => "row-resize",
643            CursorStyle::IBeamCursorForVerticalLayout => "vertical-text",
644            CursorStyle::OperationNotAllowed => "not-allowed",
645            CursorStyle::DragLink => "alias",
646            CursorStyle::DragCopy => "copy",
647            CursorStyle::ContextualMenu => "context-menu",
648        }
649        .to_string()
650    }
651}
652
653impl Keystroke {
654    pub(super) fn from_xkb(state: &State, modifiers: Modifiers, keycode: Keycode) -> Self {
655        let mut modifiers = modifiers;
656
657        let key_utf32 = state.key_get_utf32(keycode);
658        let key_utf8 = state.key_get_utf8(keycode);
659        let key_sym = state.key_get_one_sym(keycode);
660
661        let key = match key_sym {
662            Keysym::Return => "enter".to_owned(),
663            Keysym::Prior => "pageup".to_owned(),
664            Keysym::Next => "pagedown".to_owned(),
665            Keysym::ISO_Left_Tab => "tab".to_owned(),
666            Keysym::KP_Prior => "pageup".to_owned(),
667            Keysym::KP_Next => "pagedown".to_owned(),
668
669            Keysym::comma => ",".to_owned(),
670            Keysym::period => ".".to_owned(),
671            Keysym::less => "<".to_owned(),
672            Keysym::greater => ">".to_owned(),
673            Keysym::slash => "/".to_owned(),
674            Keysym::question => "?".to_owned(),
675
676            Keysym::semicolon => ";".to_owned(),
677            Keysym::colon => ":".to_owned(),
678            Keysym::apostrophe => "'".to_owned(),
679            Keysym::quotedbl => "\"".to_owned(),
680
681            Keysym::bracketleft => "[".to_owned(),
682            Keysym::braceleft => "{".to_owned(),
683            Keysym::bracketright => "]".to_owned(),
684            Keysym::braceright => "}".to_owned(),
685            Keysym::backslash => "\\".to_owned(),
686            Keysym::bar => "|".to_owned(),
687
688            Keysym::grave => "`".to_owned(),
689            Keysym::asciitilde => "~".to_owned(),
690            Keysym::exclam => "!".to_owned(),
691            Keysym::at => "@".to_owned(),
692            Keysym::numbersign => "#".to_owned(),
693            Keysym::dollar => "$".to_owned(),
694            Keysym::percent => "%".to_owned(),
695            Keysym::asciicircum => "^".to_owned(),
696            Keysym::ampersand => "&".to_owned(),
697            Keysym::asterisk => "*".to_owned(),
698            Keysym::parenleft => "(".to_owned(),
699            Keysym::parenright => ")".to_owned(),
700            Keysym::minus => "-".to_owned(),
701            Keysym::underscore => "_".to_owned(),
702            Keysym::equal => "=".to_owned(),
703            Keysym::plus => "+".to_owned(),
704
705            _ => {
706                let name = xkb::keysym_get_name(key_sym).to_lowercase();
707                if key_sym.is_keypad_key() {
708                    name.replace("kp_", "")
709                } else {
710                    name
711                }
712            }
713        };
714
715        if modifiers.shift {
716            // we only include the shift for upper-case letters by convention,
717            // so don't include for numbers and symbols, but do include for
718            // tab/enter, etc.
719            if key.chars().count() == 1 && key_utf8 == key {
720                modifiers.shift = false;
721            }
722        }
723
724        // Ignore control characters (and DEL) for the purposes of ime_key
725        let ime_key =
726            (key_utf32 >= 32 && key_utf32 != 127 && !key_utf8.is_empty()).then_some(key_utf8);
727
728        Keystroke {
729            modifiers,
730            key,
731            ime_key,
732        }
733    }
734
735    /**
736     * Returns which symbol the dead key represents
737     * https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values#dead_keycodes_for_linux
738     */
739    pub fn underlying_dead_key(keysym: Keysym) -> Option<String> {
740        match keysym {
741            Keysym::dead_grave => Some("`".to_owned()),
742            Keysym::dead_acute => Some("´".to_owned()),
743            Keysym::dead_circumflex => Some("^".to_owned()),
744            Keysym::dead_tilde => Some("~".to_owned()),
745            Keysym::dead_perispomeni => Some("͂".to_owned()),
746            Keysym::dead_macron => Some("¯".to_owned()),
747            Keysym::dead_breve => Some("˘".to_owned()),
748            Keysym::dead_abovedot => Some("˙".to_owned()),
749            Keysym::dead_diaeresis => Some("¨".to_owned()),
750            Keysym::dead_abovering => Some("˚".to_owned()),
751            Keysym::dead_doubleacute => Some("˝".to_owned()),
752            Keysym::dead_caron => Some("ˇ".to_owned()),
753            Keysym::dead_cedilla => Some("¸".to_owned()),
754            Keysym::dead_ogonek => Some("˛".to_owned()),
755            Keysym::dead_iota => Some("ͅ".to_owned()),
756            Keysym::dead_voiced_sound => Some("".to_owned()),
757            Keysym::dead_semivoiced_sound => Some("".to_owned()),
758            Keysym::dead_belowdot => Some("̣̣".to_owned()),
759            Keysym::dead_hook => Some("̡".to_owned()),
760            Keysym::dead_horn => Some("̛".to_owned()),
761            Keysym::dead_stroke => Some("̶̶".to_owned()),
762            Keysym::dead_abovecomma => Some("̓̓".to_owned()),
763            Keysym::dead_psili => Some("᾿".to_owned()),
764            Keysym::dead_abovereversedcomma => Some("ʽ".to_owned()),
765            Keysym::dead_dasia => Some("".to_owned()),
766            Keysym::dead_doublegrave => Some("̏".to_owned()),
767            Keysym::dead_belowring => Some("˳".to_owned()),
768            Keysym::dead_belowmacron => Some("̱".to_owned()),
769            Keysym::dead_belowcircumflex => Some("".to_owned()),
770            Keysym::dead_belowtilde => Some("̰".to_owned()),
771            Keysym::dead_belowbreve => Some("̮".to_owned()),
772            Keysym::dead_belowdiaeresis => Some("̤".to_owned()),
773            Keysym::dead_invertedbreve => Some("̯".to_owned()),
774            Keysym::dead_belowcomma => Some("̦".to_owned()),
775            Keysym::dead_currency => None,
776            Keysym::dead_lowline => None,
777            Keysym::dead_aboveverticalline => None,
778            Keysym::dead_belowverticalline => None,
779            Keysym::dead_longsolidusoverlay => None,
780            Keysym::dead_a => None,
781            Keysym::dead_A => None,
782            Keysym::dead_e => None,
783            Keysym::dead_E => None,
784            Keysym::dead_i => None,
785            Keysym::dead_I => None,
786            Keysym::dead_o => None,
787            Keysym::dead_O => None,
788            Keysym::dead_u => None,
789            Keysym::dead_U => None,
790            Keysym::dead_small_schwa => Some("ə".to_owned()),
791            Keysym::dead_capital_schwa => Some("Ə".to_owned()),
792            Keysym::dead_greek => None,
793            _ => None,
794        }
795    }
796}
797
798impl Modifiers {
799    pub(super) fn from_xkb(keymap_state: &State) -> Self {
800        let shift = keymap_state.mod_name_is_active(xkb::MOD_NAME_SHIFT, xkb::STATE_MODS_EFFECTIVE);
801        let alt = keymap_state.mod_name_is_active(xkb::MOD_NAME_ALT, xkb::STATE_MODS_EFFECTIVE);
802        let control =
803            keymap_state.mod_name_is_active(xkb::MOD_NAME_CTRL, xkb::STATE_MODS_EFFECTIVE);
804        let platform =
805            keymap_state.mod_name_is_active(xkb::MOD_NAME_LOGO, xkb::STATE_MODS_EFFECTIVE);
806        Modifiers {
807            shift,
808            alt,
809            control,
810            platform,
811            function: false,
812        }
813    }
814}
815
816#[cfg(test)]
817mod tests {
818    use super::*;
819    use crate::{px, Point};
820
821    #[test]
822    fn test_is_within_click_distance() {
823        let zero = Point::new(px(0.0), px(0.0));
824        assert_eq!(
825            is_within_click_distance(zero, Point::new(px(5.0), px(5.0))),
826            true
827        );
828        assert_eq!(
829            is_within_click_distance(zero, Point::new(px(-4.9), px(5.0))),
830            true
831        );
832        assert_eq!(
833            is_within_click_distance(Point::new(px(3.0), px(2.0)), Point::new(px(-2.0), px(-2.0))),
834            true
835        );
836        assert_eq!(
837            is_within_click_distance(zero, Point::new(px(5.0), px(5.1))),
838            false
839        );
840    }
841}