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