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