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