1use std::{
2 collections::HashMap,
3 env,
4 path::{Path, PathBuf},
5 process::Command,
6 rc::Rc,
7 sync::Arc,
8};
9#[cfg(any(feature = "wayland", feature = "x11"))]
10use std::{
11 ffi::OsString,
12 fs::File,
13 io::Read as _,
14 os::fd::{AsFd, AsRawFd, FromRawFd},
15 time::Duration,
16};
17
18use anyhow::{Context as _, anyhow};
19use async_task::Runnable;
20use calloop::{LoopSignal, channel::Channel};
21use futures::channel::oneshot;
22use util::ResultExt as _;
23#[cfg(any(feature = "wayland", feature = "x11"))]
24use xkbcommon::xkb::{self, Keycode, Keysym, State};
25
26use crate::{
27 Action, AnyWindowHandle, AttentionType, BackgroundExecutor, BiometricStatus, ClipboardItem,
28 CursorStyle, DialogOptions, DisplayId, FocusedWindowInfo, ForegroundExecutor, Keymap, Keystroke,
29 LinuxDispatcher, MediaKeyEvent, Menu, MenuItem, NetworkStatus, OsInfo, OwnedMenu,
30 PathPromptOptions, Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout,
31 PlatformKeyboardMapper, PlatformTextSystem, PlatformWindow, Point, PowerSaveBlockerKind,
32 Result, SharedString, SystemPowerEvent, Task, TrayIconEvent, TrayMenuItem, WindowAppearance,
33 WindowParams, px,
34};
35
36#[cfg(any(feature = "wayland", feature = "x11"))]
37pub(crate) const SCROLL_LINES: f32 = 3.0;
38
39#[cfg(any(feature = "wayland", feature = "x11"))]
42pub(crate) const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(400);
43pub(crate) const DOUBLE_CLICK_DISTANCE: Pixels = px(5.0);
44pub(crate) const KEYRING_LABEL: &str = "zed-github-account";
45
46#[cfg(any(feature = "wayland", feature = "x11"))]
47const FILE_PICKER_PORTAL_MISSING: &str =
48 "Couldn't open file picker due to missing xdg-desktop-portal implementation.";
49
50pub trait LinuxClient {
51 fn compositor_name(&self) -> &'static str;
52 fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R;
53 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
54 fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
55 #[allow(unused)]
56 fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>>;
57 fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
58 #[cfg(feature = "screen-capture")]
59 fn is_screen_capture_supported(&self) -> bool;
60 #[cfg(feature = "screen-capture")]
61 fn screen_capture_sources(
62 &self,
63 ) -> oneshot::Receiver<Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>>;
64
65 fn open_window(
66 &self,
67 handle: AnyWindowHandle,
68 options: WindowParams,
69 ) -> anyhow::Result<Box<dyn PlatformWindow>>;
70 fn set_cursor_style(&self, style: CursorStyle);
71 fn open_uri(&self, uri: &str);
72 fn reveal_path(&self, path: PathBuf);
73 fn write_to_primary(&self, item: ClipboardItem);
74 fn write_to_clipboard(&self, item: ClipboardItem);
75 fn read_from_primary(&self) -> Option<ClipboardItem>;
76 fn read_from_clipboard(&self) -> Option<ClipboardItem>;
77 fn active_window(&self) -> Option<AnyWindowHandle>;
78 fn window_stack(&self) -> Option<Vec<AnyWindowHandle>>;
79 fn run(&self);
80
81 fn focused_window_info(&self) -> Option<FocusedWindowInfo> {
82 None
83 }
84
85 fn set_tray_icon(&self, _icon: Option<&[u8]>) {}
86 fn set_tray_menu(&self, _menu: Vec<TrayMenuItem>) {}
87 fn set_tray_tooltip(&self, _tooltip: &str) {}
88 fn register_global_hotkey(&self, _id: u32, _keystroke: &Keystroke) -> Result<()> {
89 Err(anyhow::anyhow!(
90 "Global hotkeys not supported on this platform"
91 ))
92 }
93 fn unregister_global_hotkey(&self, _id: u32) {}
94
95 fn system_idle_time(&self) -> Option<Duration> {
96 None
97 }
98
99 fn request_user_attention(&self, _level: AttentionType, _handle: Option<AnyWindowHandle>) {}
100
101 fn cancel_user_attention(&self, _handle: Option<AnyWindowHandle>) {}
102
103 #[cfg(any(feature = "wayland", feature = "x11"))]
104 fn window_identifier(
105 &self,
106 ) -> impl Future<Output = Option<ashpd::WindowIdentifier>> + Send + 'static {
107 std::future::ready::<Option<ashpd::WindowIdentifier>>(None)
108 }
109}
110
111#[derive(Default)]
112pub(crate) struct PlatformHandlers {
113 pub(crate) open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
114 pub(crate) quit: Option<Box<dyn FnMut()>>,
115 pub(crate) reopen: Option<Box<dyn FnMut()>>,
116 pub(crate) app_menu_action: Option<Box<dyn FnMut(&dyn Action)>>,
117 pub(crate) will_open_app_menu: Option<Box<dyn FnMut()>>,
118 pub(crate) validate_app_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
119 pub(crate) keyboard_layout_change: Option<Box<dyn FnMut()>>,
120 pub(crate) tray_icon_event: Option<Box<dyn FnMut(TrayIconEvent)>>,
121 pub(crate) tray_menu_action: Option<Box<dyn FnMut(SharedString)>>,
122 pub(crate) global_hotkey: Option<Box<dyn FnMut(u32)>>,
123 pub(crate) system_power: Option<Box<dyn FnMut(SystemPowerEvent)>>,
124 pub(crate) network_status_change: Option<Box<dyn FnMut(NetworkStatus)>>,
125 pub(crate) media_key: Option<Box<dyn FnMut(MediaKeyEvent)>>,
126 pub(crate) context_menu: Option<Box<dyn FnMut(SharedString)>>,
127}
128
129pub(crate) enum PowerSaveHandle {
130 ScreenSaverCookie(u32),
131 ChildProcess(std::process::Child),
132}
133
134pub(crate) struct LinuxCommon {
135 pub(crate) background_executor: BackgroundExecutor,
136 pub(crate) foreground_executor: ForegroundExecutor,
137 pub(crate) text_system: Arc<dyn PlatformTextSystem>,
138 pub(crate) appearance: WindowAppearance,
139 pub(crate) auto_hide_scrollbars: bool,
140 pub(crate) callbacks: PlatformHandlers,
141 pub(crate) signal: LoopSignal,
142 pub(crate) menus: Vec<OwnedMenu>,
143 pub(crate) keep_alive_without_windows: bool,
144 pub(crate) power_save_blockers: HashMap<u32, PowerSaveHandle>,
145 pub(crate) next_blocker_id: u32,
146 pub(crate) last_network_status: NetworkStatus,
147 pub(crate) attention_window: Option<AnyWindowHandle>,
148}
149
150impl LinuxCommon {
151 pub fn new(signal: LoopSignal) -> (Self, Channel<Runnable>) {
152 let (main_sender, main_receiver) = calloop::channel::channel::<Runnable>();
153
154 #[cfg(any(feature = "wayland", feature = "x11"))]
155 let text_system = Arc::new(crate::CosmicTextSystem::new());
156 #[cfg(not(any(feature = "wayland", feature = "x11")))]
157 let text_system = Arc::new(crate::NoopTextSystem::new());
158
159 let callbacks = PlatformHandlers::default();
160
161 let dispatcher = Arc::new(LinuxDispatcher::new(main_sender));
162
163 let background_executor = BackgroundExecutor::new(dispatcher.clone());
164
165 let common = LinuxCommon {
166 background_executor,
167 foreground_executor: ForegroundExecutor::new(dispatcher),
168 text_system,
169 appearance: WindowAppearance::Light,
170 auto_hide_scrollbars: false,
171 callbacks,
172 signal,
173 menus: Vec::new(),
174 keep_alive_without_windows: false,
175 power_save_blockers: HashMap::new(),
176 next_blocker_id: 0,
177 last_network_status: NetworkStatus::Online,
178 attention_window: None,
179 };
180
181 (common, main_receiver)
182 }
183}
184
185impl Drop for LinuxCommon {
186 fn drop(&mut self) {
187 for (_, handle) in self.power_save_blockers.drain() {
188 crate::platform::linux::power::release_blocker(handle);
189 }
190 }
191}
192
193impl<P: LinuxClient + 'static> Platform for P {
194 fn background_executor(&self) -> BackgroundExecutor {
195 self.with_common(|common| common.background_executor.clone())
196 }
197
198 fn foreground_executor(&self) -> ForegroundExecutor {
199 self.with_common(|common| common.foreground_executor.clone())
200 }
201
202 fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
203 self.with_common(|common| common.text_system.clone())
204 }
205
206 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
207 self.keyboard_layout()
208 }
209
210 fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
211 Rc::new(crate::DummyKeyboardMapper)
212 }
213
214 fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
215 self.with_common(|common| common.callbacks.keyboard_layout_change = Some(callback));
216 }
217
218 fn run(&self, on_finish_launching: Box<dyn FnOnce()>) {
219 on_finish_launching();
220
221 LinuxClient::run(self);
222
223 let quit = self.with_common(|common| common.callbacks.quit.take());
224 if let Some(mut fun) = quit {
225 fun();
226 }
227 }
228
229 fn quit(&self) {
230 self.with_common(|common| common.signal.stop());
231 }
232
233 fn compositor_name(&self) -> &'static str {
234 self.compositor_name()
235 }
236
237 fn restart(&self, binary_path: Option<PathBuf>) {
238 use std::os::unix::process::CommandExt as _;
239
240 let app_pid = std::process::id().to_string();
242 let app_path = if let Some(path) = binary_path {
244 path
245 } else {
246 match self.app_path() {
247 Ok(path) => path,
248 Err(err) => {
249 log::error!("Failed to get app path: {:?}", err);
250 return;
251 }
252 }
253 };
254
255 log::info!("Restarting process, using app path: {:?}", app_path);
256
257 let script = format!(
259 r#"
260 while kill -0 {pid} 2>/dev/null; do
261 sleep 0.1
262 done
263
264 {app_path}
265 "#,
266 pid = app_pid,
267 app_path = app_path.display()
268 );
269
270 #[allow(
271 clippy::disallowed_methods,
272 reason = "We are restarting ourselves, using std command thus is fine"
273 )]
274 let restart_process = Command::new("/usr/bin/env")
275 .arg("bash")
276 .arg("-c")
277 .arg(script)
278 .process_group(0)
279 .spawn();
280
281 match restart_process {
282 Ok(_) => self.quit(),
283 Err(e) => log::error!("failed to spawn restart script: {:?}", e),
284 }
285 }
286
287 fn activate(&self, _ignoring_other_apps: bool) {
288 log::info!("activate is not implemented on Linux, ignoring the call")
289 }
290
291 fn hide(&self) {
292 log::info!("hide is not implemented on Linux, ignoring the call")
293 }
294
295 fn hide_other_apps(&self) {
296 log::info!("hide_other_apps is not implemented on Linux, ignoring the call")
297 }
298
299 fn unhide_other_apps(&self) {
300 log::info!("unhide_other_apps is not implemented on Linux, ignoring the call")
301 }
302
303 fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
304 self.primary_display()
305 }
306
307 fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
308 self.displays()
309 }
310
311 #[cfg(feature = "screen-capture")]
312 fn is_screen_capture_supported(&self) -> bool {
313 self.is_screen_capture_supported()
314 }
315
316 #[cfg(feature = "screen-capture")]
317 fn screen_capture_sources(
318 &self,
319 ) -> oneshot::Receiver<Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>> {
320 self.screen_capture_sources()
321 }
322
323 fn active_window(&self) -> Option<AnyWindowHandle> {
324 self.active_window()
325 }
326
327 fn window_stack(&self) -> Option<Vec<AnyWindowHandle>> {
328 self.window_stack()
329 }
330
331 fn open_window(
332 &self,
333 handle: AnyWindowHandle,
334 options: WindowParams,
335 ) -> anyhow::Result<Box<dyn PlatformWindow>> {
336 self.open_window(handle, options)
337 }
338
339 fn open_url(&self, url: &str) {
340 self.open_uri(url);
341 }
342
343 fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
344 self.with_common(|common| common.callbacks.open_urls = Some(callback));
345 }
346
347 fn prompt_for_paths(
348 &self,
349 options: PathPromptOptions,
350 ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
351 let (done_tx, done_rx) = oneshot::channel();
352
353 #[cfg(not(any(feature = "wayland", feature = "x11")))]
354 let _ = (done_tx.send(Ok(None)), options);
355
356 #[cfg(any(feature = "wayland", feature = "x11"))]
357 let identifier = self.window_identifier();
358
359 #[cfg(any(feature = "wayland", feature = "x11"))]
360 self.foreground_executor()
361 .spawn(async move {
362 let title = if options.directories {
363 "Open Folder"
364 } else {
365 "Open File"
366 };
367
368 let request = match ashpd::desktop::file_chooser::OpenFileRequest::default()
369 .identifier(identifier.await)
370 .modal(true)
371 .title(title)
372 .accept_label(options.prompt.as_ref().map(crate::SharedString::as_str))
373 .multiple(options.multiple)
374 .directory(options.directories)
375 .send()
376 .await
377 {
378 Ok(request) => request,
379 Err(err) => {
380 let result = match err {
381 ashpd::Error::PortalNotFound(_) => anyhow!(FILE_PICKER_PORTAL_MISSING),
382 err => err.into(),
383 };
384 let _ = done_tx.send(Err(result));
385 return;
386 }
387 };
388
389 let result = match request.response() {
390 Ok(response) => Ok(Some(
391 response
392 .uris()
393 .iter()
394 .filter_map(|uri| uri.to_file_path().ok())
395 .collect::<Vec<_>>(),
396 )),
397 Err(ashpd::Error::Response(_)) => Ok(None),
398 Err(e) => Err(e.into()),
399 };
400 let _ = done_tx.send(result);
401 })
402 .detach();
403 done_rx
404 }
405
406 fn prompt_for_new_path(
407 &self,
408 directory: &Path,
409 suggested_name: Option<&str>,
410 ) -> oneshot::Receiver<Result<Option<PathBuf>>> {
411 let (done_tx, done_rx) = oneshot::channel();
412
413 #[cfg(not(any(feature = "wayland", feature = "x11")))]
414 let _ = (done_tx.send(Ok(None)), directory, suggested_name);
415
416 #[cfg(any(feature = "wayland", feature = "x11"))]
417 let identifier = self.window_identifier();
418
419 #[cfg(any(feature = "wayland", feature = "x11"))]
420 self.foreground_executor()
421 .spawn({
422 let directory = directory.to_owned();
423 let suggested_name = suggested_name.map(|s| s.to_owned());
424
425 async move {
426 let mut request_builder =
427 ashpd::desktop::file_chooser::SaveFileRequest::default()
428 .identifier(identifier.await)
429 .modal(true)
430 .title("Save File")
431 .current_folder(directory)
432 .expect("pathbuf should not be nul terminated");
433
434 if let Some(suggested_name) = suggested_name {
435 request_builder = request_builder.current_name(suggested_name.as_str());
436 }
437
438 let request = match request_builder.send().await {
439 Ok(request) => request,
440 Err(err) => {
441 let result = match err {
442 ashpd::Error::PortalNotFound(_) => {
443 anyhow!(FILE_PICKER_PORTAL_MISSING)
444 }
445 err => err.into(),
446 };
447 let _ = done_tx.send(Err(result));
448 return;
449 }
450 };
451
452 let result = match request.response() {
453 Ok(response) => Ok(response
454 .uris()
455 .first()
456 .and_then(|uri| uri.to_file_path().ok())),
457 Err(ashpd::Error::Response(_)) => Ok(None),
458 Err(e) => Err(e.into()),
459 };
460 let _ = done_tx.send(result);
461 }
462 })
463 .detach();
464
465 done_rx
466 }
467
468 fn can_select_mixed_files_and_dirs(&self) -> bool {
469 false
471 }
472
473 fn reveal_path(&self, path: &Path) {
474 self.reveal_path(path.to_owned());
475 }
476
477 fn open_with_system(&self, path: &Path) {
478 let path = path.to_owned();
479 self.background_executor()
480 .spawn(async move {
481 let _ = smol::process::Command::new("xdg-open")
482 .arg(path)
483 .spawn()
484 .context("invoking xdg-open")
485 .log_err()?
486 .status()
487 .await
488 .log_err()?;
489 Some(())
490 })
491 .detach();
492 }
493
494 fn on_quit(&self, callback: Box<dyn FnMut()>) {
495 self.with_common(|common| {
496 common.callbacks.quit = Some(callback);
497 });
498 }
499
500 fn on_reopen(&self, callback: Box<dyn FnMut()>) {
501 self.with_common(|common| {
502 common.callbacks.reopen = Some(callback);
503 });
504 }
505
506 fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
507 self.with_common(|common| {
508 common.callbacks.app_menu_action = Some(callback);
509 });
510 }
511
512 fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>) {
513 self.with_common(|common| {
514 common.callbacks.will_open_app_menu = Some(callback);
515 });
516 }
517
518 fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
519 self.with_common(|common| {
520 common.callbacks.validate_app_menu_command = Some(callback);
521 });
522 }
523
524 fn app_path(&self) -> Result<PathBuf> {
525 let app_path = env::current_exe()?;
527 Ok(app_path)
528 }
529
530 fn set_menus(&self, menus: Vec<Menu>, _keymap: &Keymap) {
531 self.with_common(|common| {
532 common.menus = menus.into_iter().map(|menu| menu.owned()).collect();
533 })
534 }
535
536 fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
537 self.with_common(|common| Some(common.menus.clone()))
538 }
539
540 fn set_dock_menu(&self, _menu: Vec<MenuItem>, _keymap: &Keymap) {
541 }
543
544 fn path_for_auxiliary_executable(&self, _name: &str) -> Result<PathBuf> {
545 Err(anyhow::Error::msg(
546 "Platform<LinuxPlatform>::path_for_auxiliary_executable is not implemented yet",
547 ))
548 }
549
550 fn set_cursor_style(&self, style: CursorStyle) {
551 self.set_cursor_style(style)
552 }
553
554 fn should_auto_hide_scrollbars(&self) -> bool {
555 self.with_common(|common| common.auto_hide_scrollbars)
556 }
557
558 fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>> {
559 let url = url.to_string();
560 let username = username.to_string();
561 let password = password.to_vec();
562 self.background_executor().spawn(async move {
563 let keyring = oo7::Keyring::new().await?;
564 keyring.unlock().await?;
565 keyring
566 .create_item(
567 KEYRING_LABEL,
568 &vec![("url", &url), ("username", &username)],
569 password,
570 true,
571 )
572 .await?;
573 Ok(())
574 })
575 }
576
577 fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
578 let url = url.to_string();
579 self.background_executor().spawn(async move {
580 let keyring = oo7::Keyring::new().await?;
581 keyring.unlock().await?;
582
583 let items = keyring.search_items(&vec![("url", &url)]).await?;
584
585 for item in items.into_iter() {
586 if item.label().await.is_ok_and(|label| label == KEYRING_LABEL) {
587 let attributes = item.attributes().await?;
588 let username = attributes
589 .get("username")
590 .context("Cannot find username in stored credentials")?;
591 item.unlock().await?;
592 let secret = item.secret().await?;
593
594 return Ok(Some((username.to_string(), secret.to_vec())));
597 } else {
598 continue;
599 }
600 }
601 Ok(None)
602 })
603 }
604
605 fn delete_credentials(&self, url: &str) -> Task<Result<()>> {
606 let url = url.to_string();
607 self.background_executor().spawn(async move {
608 let keyring = oo7::Keyring::new().await?;
609 keyring.unlock().await?;
610
611 let items = keyring.search_items(&vec![("url", &url)]).await?;
612
613 for item in items.into_iter() {
614 if item.label().await.is_ok_and(|label| label == KEYRING_LABEL) {
615 item.delete().await?;
616 return Ok(());
617 }
618 }
619
620 Ok(())
621 })
622 }
623
624 fn window_appearance(&self) -> WindowAppearance {
625 self.with_common(|common| common.appearance)
626 }
627
628 fn register_url_scheme(&self, _: &str) -> Task<anyhow::Result<()>> {
629 Task::ready(Err(anyhow!("register_url_scheme unimplemented")))
630 }
631
632 fn write_to_primary(&self, item: ClipboardItem) {
633 self.write_to_primary(item)
634 }
635
636 fn write_to_clipboard(&self, item: ClipboardItem) {
637 self.write_to_clipboard(item)
638 }
639
640 fn read_from_primary(&self) -> Option<ClipboardItem> {
641 self.read_from_primary()
642 }
643
644 fn read_from_clipboard(&self) -> Option<ClipboardItem> {
645 self.read_from_clipboard()
646 }
647
648 fn add_recent_document(&self, _path: &Path) {}
649
650 fn set_keep_alive_without_windows(&self, keep_alive: bool) {
651 self.with_common(|common| common.keep_alive_without_windows = keep_alive);
652 }
653
654 fn set_tray_icon(&self, icon: Option<&[u8]>) {
655 LinuxClient::set_tray_icon(self, icon);
656 }
657
658 fn set_tray_menu(&self, menu: Vec<TrayMenuItem>) {
659 LinuxClient::set_tray_menu(self, menu);
660 }
661
662 fn set_tray_tooltip(&self, tooltip: &str) {
663 LinuxClient::set_tray_tooltip(self, tooltip);
664 }
665
666 fn on_tray_icon_event(&self, callback: Box<dyn FnMut(TrayIconEvent)>) {
667 self.with_common(|common| common.callbacks.tray_icon_event = Some(callback));
668 }
669
670 fn on_tray_menu_action(&self, callback: Box<dyn FnMut(SharedString)>) {
671 self.with_common(|common| common.callbacks.tray_menu_action = Some(callback));
672 }
673
674 fn register_global_hotkey(&self, id: u32, keystroke: &Keystroke) -> Result<()> {
675 LinuxClient::register_global_hotkey(self, id, keystroke)
676 }
677
678 fn unregister_global_hotkey(&self, id: u32) {
679 LinuxClient::unregister_global_hotkey(self, id);
680 }
681
682 fn on_global_hotkey(&self, callback: Box<dyn FnMut(u32)>) {
683 self.with_common(|common| common.callbacks.global_hotkey = Some(callback));
684 }
685
686 fn focused_window_info(&self) -> Option<FocusedWindowInfo> {
687 LinuxClient::focused_window_info(self)
688 }
689
690 fn set_auto_launch(&self, app_id: &str, enabled: bool) -> Result<()> {
691 crate::platform::linux::auto_launch::set_auto_launch(app_id, enabled)
692 }
693
694 fn is_auto_launch_enabled(&self, app_id: &str) -> bool {
695 crate::platform::linux::auto_launch::is_auto_launch_enabled(app_id)
696 }
697
698 fn show_notification(&self, title: &str, body: &str) -> Result<()> {
699 crate::platform::linux::notifications::show_notification(title, body)
700 }
701
702 fn os_info(&self) -> OsInfo {
703 crate::platform::linux::os_info::get_os_info()
704 }
705
706 fn network_status(&self) -> NetworkStatus {
707 network_status_from_sysfs()
708 }
709
710 fn on_network_status_change(&self, callback: Box<dyn FnMut(NetworkStatus)>) {
711 self.with_common(|common| common.callbacks.network_status_change = Some(callback));
712 log::warn!("Network change monitoring requires D-Bus integration — not yet implemented on Linux");
713 }
714
715 fn start_power_save_blocker(&self, kind: PowerSaveBlockerKind) -> Option<u32> {
716 self.with_common(|common| {
717 let handle = match kind {
718 PowerSaveBlockerKind::PreventDisplaySleep => {
719 crate::platform::linux::power::inhibit_screensaver(
720 "gpui",
721 "Power save blocker",
722 )?
723 }
724 PowerSaveBlockerKind::PreventAppSuspension => {
725 crate::platform::linux::power::inhibit_suspend(
726 "gpui",
727 "Power save blocker",
728 )?
729 }
730 };
731 let id = common.next_blocker_id;
732 common.next_blocker_id += 1;
733 common.power_save_blockers.insert(id, handle);
734 Some(id)
735 })
736 }
737
738 fn stop_power_save_blocker(&self, id: u32) {
739 self.with_common(|common| {
740 if let Some(handle) = common.power_save_blockers.remove(&id) {
741 crate::platform::linux::power::release_blocker(handle);
742 }
743 });
744 }
745
746 fn system_idle_time(&self) -> Option<Duration> {
747 LinuxClient::system_idle_time(self)
748 }
749
750 fn on_system_power_event(&self, callback: Box<dyn FnMut(SystemPowerEvent)>) {
751 self.with_common(|common| common.callbacks.system_power = Some(callback));
752 log::warn!("System power events require D-Bus logind integration — not yet implemented on Linux");
753 }
754
755 fn on_media_key_event(&self, callback: Box<dyn FnMut(MediaKeyEvent)>) {
756 self.with_common(|common| common.callbacks.media_key = Some(callback));
757 }
758
759 fn request_user_attention(&self, level: AttentionType) {
760 let handle = self.active_window();
761 self.with_common(|common| common.attention_window = handle);
762 LinuxClient::request_user_attention(self, level, handle);
763 }
764
765 fn cancel_user_attention(&self) {
766 let handle = self.with_common(|common| common.attention_window.take());
767 LinuxClient::cancel_user_attention(self, handle);
768 }
769
770 fn show_context_menu(
771 &self,
772 _position: Point<Pixels>,
773 _items: Vec<TrayMenuItem>,
774 _callback: Box<dyn FnMut(SharedString)>,
775 ) {
776 log::warn!("Context menus not yet implemented on Linux");
777 }
778
779 fn show_dialog(&self, options: DialogOptions) -> oneshot::Receiver<usize> {
780 let (tx, rx) = oneshot::channel();
781 self.background_executor()
782 .spawn(async move {
783 let result = crate::platform::linux::dialog::show_dialog(&options);
784 let _ = tx.send(result);
785 })
786 .detach();
787 rx
788 }
789
790 fn biometric_status(&self) -> BiometricStatus {
791 BiometricStatus::Unavailable
792 }
793
794 fn authenticate_biometric(&self, _reason: &str, callback: Box<dyn FnOnce(bool) + Send>) {
795 callback(false);
796 }
797}
798
799fn network_status_from_sysfs() -> NetworkStatus {
800 if let Ok(entries) = std::fs::read_dir("/sys/class/net") {
801 for entry in entries.flatten() {
802 let name = entry.file_name();
803 if name == "lo" {
804 continue;
805 }
806 if let Ok(state) = std::fs::read_to_string(entry.path().join("operstate")) {
807 if state.trim() == "up" {
808 return NetworkStatus::Online;
809 }
810 }
811 }
812 }
813 NetworkStatus::Offline
814}
815
816#[cfg(any(feature = "wayland", feature = "x11"))]
817pub(crate) fn keysym_to_media_key(keysym: xkbcommon::xkb::Keysym) -> Option<MediaKeyEvent> {
818 use xkbcommon::xkb::Keysym;
819 match keysym {
820 Keysym::XF86_AudioPlay => Some(MediaKeyEvent::PlayPause),
821 Keysym::XF86_AudioPause => Some(MediaKeyEvent::Pause),
822 Keysym::XF86_AudioStop => Some(MediaKeyEvent::Stop),
823 Keysym::XF86_AudioNext => Some(MediaKeyEvent::NextTrack),
824 Keysym::XF86_AudioPrev => Some(MediaKeyEvent::PreviousTrack),
825 _ => None,
826 }
827}
828
829#[cfg(any(feature = "wayland", feature = "x11"))]
830pub(super) fn open_uri_internal(
831 executor: BackgroundExecutor,
832 uri: &str,
833 activation_token: Option<String>,
834) {
835 if let Some(uri) = ashpd::url::Url::parse(uri).log_err() {
836 executor
837 .spawn(async move {
838 match ashpd::desktop::open_uri::OpenFileRequest::default()
839 .activation_token(activation_token.clone().map(ashpd::ActivationToken::from))
840 .send_uri(&uri)
841 .await
842 {
843 Ok(_) => return,
844 Err(e) => log::error!("Failed to open with dbus: {}", e),
845 }
846
847 for mut command in open::commands(uri.to_string()) {
848 if let Some(token) = activation_token.as_ref() {
849 command.env("XDG_ACTIVATION_TOKEN", token);
850 }
851 let program = format!("{:?}", command.get_program());
852 match smol::process::Command::from(command).spawn() {
853 Ok(mut cmd) => {
854 cmd.status().await.log_err();
855 return;
856 }
857 Err(e) => {
858 log::error!("Failed to open with {}: {}", program, e)
859 }
860 }
861 }
862 })
863 .detach();
864 }
865}
866
867#[cfg(any(feature = "x11", feature = "wayland"))]
868pub(super) fn reveal_path_internal(
869 executor: BackgroundExecutor,
870 path: PathBuf,
871 activation_token: Option<String>,
872) {
873 executor
874 .spawn(async move {
875 if let Some(dir) = File::open(path.clone()).log_err() {
876 match ashpd::desktop::open_uri::OpenDirectoryRequest::default()
877 .activation_token(activation_token.map(ashpd::ActivationToken::from))
878 .send(&dir.as_fd())
879 .await
880 {
881 Ok(_) => return,
882 Err(e) => log::error!("Failed to open with dbus: {}", e),
883 }
884 if path.is_dir() {
885 open::that_detached(path).log_err();
886 } else {
887 open::that_detached(path.parent().unwrap_or(Path::new(""))).log_err();
888 }
889 }
890 })
891 .detach();
892}
893
894#[allow(unused)]
895pub(super) fn is_within_click_distance(a: Point<Pixels>, b: Point<Pixels>) -> bool {
896 let diff = a - b;
897 diff.x.abs() <= DOUBLE_CLICK_DISTANCE && diff.y.abs() <= DOUBLE_CLICK_DISTANCE
898}
899
900#[cfg(any(feature = "wayland", feature = "x11"))]
901pub(super) fn get_xkb_compose_state(cx: &xkb::Context) -> Option<xkb::compose::State> {
902 let mut locales = Vec::default();
903 if let Some(locale) = env::var_os("LC_CTYPE") {
904 locales.push(locale);
905 }
906 locales.push(OsString::from("C"));
907 let mut state: Option<xkb::compose::State> = None;
908 for locale in locales {
909 if let Ok(table) =
910 xkb::compose::Table::new_from_locale(cx, &locale, xkb::compose::COMPILE_NO_FLAGS)
911 {
912 state = Some(xkb::compose::State::new(
913 &table,
914 xkb::compose::STATE_NO_FLAGS,
915 ));
916 break;
917 }
918 }
919 state
920}
921
922#[cfg(any(feature = "wayland", feature = "x11"))]
923pub(super) unsafe fn read_fd(mut fd: filedescriptor::FileDescriptor) -> Result<Vec<u8>> {
924 let mut file = unsafe { File::from_raw_fd(fd.as_raw_fd()) };
925 let mut buffer = Vec::new();
926 file.read_to_end(&mut buffer)?;
927 Ok(buffer)
928}
929
930#[cfg(any(feature = "wayland", feature = "x11"))]
931pub(super) const DEFAULT_CURSOR_ICON_NAME: &str = "left_ptr";
932
933impl CursorStyle {
934 #[cfg(any(feature = "wayland", feature = "x11"))]
935 pub(super) fn to_icon_names(self) -> &'static [&'static str] {
936 match self {
939 CursorStyle::Arrow => &[DEFAULT_CURSOR_ICON_NAME],
940 CursorStyle::IBeam => &["text", "xterm"],
941 CursorStyle::Crosshair => &["crosshair", "cross"],
942 CursorStyle::ClosedHand => &["closedhand", "grabbing", "hand2"],
943 CursorStyle::OpenHand => &["openhand", "grab", "hand1"],
944 CursorStyle::PointingHand => &["pointer", "hand", "hand2"],
945 CursorStyle::ResizeLeft => &["w-resize", "left_side"],
946 CursorStyle::ResizeRight => &["e-resize", "right_side"],
947 CursorStyle::ResizeLeftRight => &["ew-resize", "sb_h_double_arrow"],
948 CursorStyle::ResizeUp => &["n-resize", "top_side"],
949 CursorStyle::ResizeDown => &["s-resize", "bottom_side"],
950 CursorStyle::ResizeUpDown => &["sb_v_double_arrow", "ns-resize"],
951 CursorStyle::ResizeUpLeftDownRight => &["size_fdiag", "bd_double_arrow", "nwse-resize"],
952 CursorStyle::ResizeUpRightDownLeft => &["size_bdiag", "nesw-resize", "fd_double_arrow"],
953 CursorStyle::ResizeColumn => &["col-resize", "sb_h_double_arrow"],
954 CursorStyle::ResizeRow => &["row-resize", "sb_v_double_arrow"],
955 CursorStyle::IBeamCursorForVerticalLayout => &["vertical-text"],
956 CursorStyle::OperationNotAllowed => &["not-allowed", "crossed_circle"],
957 CursorStyle::DragLink => &["alias"],
958 CursorStyle::DragCopy => &["copy"],
959 CursorStyle::ContextualMenu => &["context-menu"],
960 CursorStyle::None => {
961 #[cfg(debug_assertions)]
962 panic!("CursorStyle::None should be handled separately in the client");
963 #[cfg(not(debug_assertions))]
964 &[DEFAULT_CURSOR_ICON_NAME]
965 }
966 }
967 }
968}
969
970#[cfg(any(feature = "wayland", feature = "x11"))]
971pub(super) fn log_cursor_icon_warning(message: impl std::fmt::Display) {
972 if let Ok(xcursor_path) = env::var("XCURSOR_PATH") {
973 log::warn!(
974 "{:#}\ncursor icon loading may be failing if XCURSOR_PATH environment variable is invalid. \
975 XCURSOR_PATH overrides the default icon search. Its current value is '{}'",
976 message,
977 xcursor_path
978 );
979 } else {
980 log::warn!("{:#}", message);
981 }
982}
983
984#[cfg(any(feature = "wayland", feature = "x11"))]
985fn guess_ascii(keycode: Keycode, shift: bool) -> Option<char> {
986 let c = match (keycode.raw(), shift) {
987 (24, _) => 'q',
988 (25, _) => 'w',
989 (26, _) => 'e',
990 (27, _) => 'r',
991 (28, _) => 't',
992 (29, _) => 'y',
993 (30, _) => 'u',
994 (31, _) => 'i',
995 (32, _) => 'o',
996 (33, _) => 'p',
997 (34, false) => '[',
998 (34, true) => '{',
999 (35, false) => ']',
1000 (35, true) => '}',
1001 (38, _) => 'a',
1002 (39, _) => 's',
1003 (40, _) => 'd',
1004 (41, _) => 'f',
1005 (42, _) => 'g',
1006 (43, _) => 'h',
1007 (44, _) => 'j',
1008 (45, _) => 'k',
1009 (46, _) => 'l',
1010 (47, false) => ';',
1011 (47, true) => ':',
1012 (48, false) => '\'',
1013 (48, true) => '"',
1014 (49, false) => '`',
1015 (49, true) => '~',
1016 (51, false) => '\\',
1017 (51, true) => '|',
1018 (52, _) => 'z',
1019 (53, _) => 'x',
1020 (54, _) => 'c',
1021 (55, _) => 'v',
1022 (56, _) => 'b',
1023 (57, _) => 'n',
1024 (58, _) => 'm',
1025 (59, false) => ',',
1026 (59, true) => '>',
1027 (60, false) => '.',
1028 (60, true) => '<',
1029 (61, false) => '/',
1030 (61, true) => '?',
1031
1032 _ => return None,
1033 };
1034
1035 Some(c)
1036}
1037
1038#[cfg(any(feature = "wayland", feature = "x11"))]
1039impl crate::Keystroke {
1040 pub(super) fn from_xkb(
1041 state: &State,
1042 mut modifiers: crate::Modifiers,
1043 keycode: Keycode,
1044 ) -> Self {
1045 let key_utf32 = state.key_get_utf32(keycode);
1046 let key_utf8 = state.key_get_utf8(keycode);
1047 let key_sym = state.key_get_one_sym(keycode);
1048
1049 let key = match key_sym {
1050 Keysym::Return => "enter".to_owned(),
1051 Keysym::Prior => "pageup".to_owned(),
1052 Keysym::Next => "pagedown".to_owned(),
1053 Keysym::ISO_Left_Tab => "tab".to_owned(),
1054 Keysym::KP_Prior => "pageup".to_owned(),
1055 Keysym::KP_Next => "pagedown".to_owned(),
1056 Keysym::XF86_Back => "back".to_owned(),
1057 Keysym::XF86_Forward => "forward".to_owned(),
1058 Keysym::XF86_Cut => "cut".to_owned(),
1059 Keysym::XF86_Copy => "copy".to_owned(),
1060 Keysym::XF86_Paste => "paste".to_owned(),
1061 Keysym::XF86_New => "new".to_owned(),
1062 Keysym::XF86_Open => "open".to_owned(),
1063 Keysym::XF86_Save => "save".to_owned(),
1064
1065 Keysym::comma => ",".to_owned(),
1066 Keysym::period => ".".to_owned(),
1067 Keysym::less => "<".to_owned(),
1068 Keysym::greater => ">".to_owned(),
1069 Keysym::slash => "/".to_owned(),
1070 Keysym::question => "?".to_owned(),
1071
1072 Keysym::semicolon => ";".to_owned(),
1073 Keysym::colon => ":".to_owned(),
1074 Keysym::apostrophe => "'".to_owned(),
1075 Keysym::quotedbl => "\"".to_owned(),
1076
1077 Keysym::bracketleft => "[".to_owned(),
1078 Keysym::braceleft => "{".to_owned(),
1079 Keysym::bracketright => "]".to_owned(),
1080 Keysym::braceright => "}".to_owned(),
1081 Keysym::backslash => "\\".to_owned(),
1082 Keysym::bar => "|".to_owned(),
1083
1084 Keysym::grave => "`".to_owned(),
1085 Keysym::asciitilde => "~".to_owned(),
1086 Keysym::exclam => "!".to_owned(),
1087 Keysym::at => "@".to_owned(),
1088 Keysym::numbersign => "#".to_owned(),
1089 Keysym::dollar => "$".to_owned(),
1090 Keysym::percent => "%".to_owned(),
1091 Keysym::asciicircum => "^".to_owned(),
1092 Keysym::ampersand => "&".to_owned(),
1093 Keysym::asterisk => "*".to_owned(),
1094 Keysym::parenleft => "(".to_owned(),
1095 Keysym::parenright => ")".to_owned(),
1096 Keysym::minus => "-".to_owned(),
1097 Keysym::underscore => "_".to_owned(),
1098 Keysym::equal => "=".to_owned(),
1099 Keysym::plus => "+".to_owned(),
1100 Keysym::space => "space".to_owned(),
1101 Keysym::BackSpace => "backspace".to_owned(),
1102 Keysym::Tab => "tab".to_owned(),
1103 Keysym::Delete => "delete".to_owned(),
1104 Keysym::Escape => "escape".to_owned(),
1105
1106 Keysym::Left => "left".to_owned(),
1107 Keysym::Right => "right".to_owned(),
1108 Keysym::Up => "up".to_owned(),
1109 Keysym::Down => "down".to_owned(),
1110 Keysym::Home => "home".to_owned(),
1111 Keysym::End => "end".to_owned(),
1112 Keysym::Insert => "insert".to_owned(),
1113
1114 _ => {
1115 let name = xkb::keysym_get_name(key_sym).to_lowercase();
1116 if key_sym.is_keypad_key() {
1117 name.replace("kp_", "")
1118 } else if let Some(key) = key_utf8.chars().next()
1119 && key_utf8.len() == 1
1120 && key.is_ascii()
1121 {
1122 if key.is_ascii_graphic() {
1123 key_utf8.to_lowercase()
1124 } else if key_utf32 <= 0x1f
1128 && !name.chars().next().is_some_and(|c| c.is_ascii_digit())
1129 {
1130 ((key_utf32 as u8 + 0x40) as char)
1131 .to_ascii_lowercase()
1132 .to_string()
1133 } else {
1134 name
1135 }
1136 } else if let Some(key_en) = guess_ascii(keycode, modifiers.shift) {
1137 String::from(key_en)
1138 } else {
1139 name
1140 }
1141 }
1142 };
1143
1144 if modifiers.shift {
1145 if key.chars().count() == 1 && key.to_lowercase() == key.to_uppercase() {
1149 modifiers.shift = false;
1150 }
1151 }
1152
1153 let key_char =
1155 (key_utf32 >= 32 && key_utf32 != 127 && !key_utf8.is_empty()).then_some(key_utf8);
1156
1157 Self {
1158 modifiers,
1159 key,
1160 key_char,
1161 }
1162 }
1163
1164 pub fn underlying_dead_key(keysym: Keysym) -> Option<String> {
1169 match keysym {
1170 Keysym::dead_grave => Some("`".to_owned()),
1171 Keysym::dead_acute => Some("´".to_owned()),
1172 Keysym::dead_circumflex => Some("^".to_owned()),
1173 Keysym::dead_tilde => Some("~".to_owned()),
1174 Keysym::dead_macron => Some("¯".to_owned()),
1175 Keysym::dead_breve => Some("˘".to_owned()),
1176 Keysym::dead_abovedot => Some("˙".to_owned()),
1177 Keysym::dead_diaeresis => Some("¨".to_owned()),
1178 Keysym::dead_abovering => Some("˚".to_owned()),
1179 Keysym::dead_doubleacute => Some("˝".to_owned()),
1180 Keysym::dead_caron => Some("ˇ".to_owned()),
1181 Keysym::dead_cedilla => Some("¸".to_owned()),
1182 Keysym::dead_ogonek => Some("˛".to_owned()),
1183 Keysym::dead_iota => Some("ͅ".to_owned()),
1184 Keysym::dead_voiced_sound => Some("゙".to_owned()),
1185 Keysym::dead_semivoiced_sound => Some("゚".to_owned()),
1186 Keysym::dead_belowdot => Some("̣̣".to_owned()),
1187 Keysym::dead_hook => Some("̡".to_owned()),
1188 Keysym::dead_horn => Some("̛".to_owned()),
1189 Keysym::dead_stroke => Some("̶̶".to_owned()),
1190 Keysym::dead_abovecomma => Some("̓̓".to_owned()),
1191 Keysym::dead_abovereversedcomma => Some("ʽ".to_owned()),
1192 Keysym::dead_doublegrave => Some("̏".to_owned()),
1193 Keysym::dead_belowring => Some("˳".to_owned()),
1194 Keysym::dead_belowmacron => Some("̱".to_owned()),
1195 Keysym::dead_belowcircumflex => Some("ꞈ".to_owned()),
1196 Keysym::dead_belowtilde => Some("̰".to_owned()),
1197 Keysym::dead_belowbreve => Some("̮".to_owned()),
1198 Keysym::dead_belowdiaeresis => Some("̤".to_owned()),
1199 Keysym::dead_invertedbreve => Some("̯".to_owned()),
1200 Keysym::dead_belowcomma => Some("̦".to_owned()),
1201 Keysym::dead_currency => None,
1202 Keysym::dead_lowline => None,
1203 Keysym::dead_aboveverticalline => None,
1204 Keysym::dead_belowverticalline => None,
1205 Keysym::dead_longsolidusoverlay => None,
1206 Keysym::dead_a => None,
1207 Keysym::dead_A => None,
1208 Keysym::dead_e => None,
1209 Keysym::dead_E => None,
1210 Keysym::dead_i => None,
1211 Keysym::dead_I => None,
1212 Keysym::dead_o => None,
1213 Keysym::dead_O => None,
1214 Keysym::dead_u => None,
1215 Keysym::dead_U => None,
1216 Keysym::dead_small_schwa => Some("ə".to_owned()),
1217 Keysym::dead_capital_schwa => Some("Ə".to_owned()),
1218 Keysym::dead_greek => None,
1219 _ => None,
1220 }
1221 }
1222}
1223
1224#[cfg(any(feature = "wayland", feature = "x11"))]
1225impl crate::Modifiers {
1226 pub(super) fn from_xkb(keymap_state: &State) -> Self {
1227 let shift = keymap_state.mod_name_is_active(xkb::MOD_NAME_SHIFT, xkb::STATE_MODS_EFFECTIVE);
1228 let alt = keymap_state.mod_name_is_active(xkb::MOD_NAME_ALT, xkb::STATE_MODS_EFFECTIVE);
1229 let control =
1230 keymap_state.mod_name_is_active(xkb::MOD_NAME_CTRL, xkb::STATE_MODS_EFFECTIVE);
1231 let platform =
1232 keymap_state.mod_name_is_active(xkb::MOD_NAME_LOGO, xkb::STATE_MODS_EFFECTIVE);
1233 Self {
1234 shift,
1235 alt,
1236 control,
1237 platform,
1238 function: false,
1239 }
1240 }
1241}
1242
1243#[cfg(any(feature = "wayland", feature = "x11"))]
1244impl crate::Capslock {
1245 pub(super) fn from_xkb(keymap_state: &State) -> Self {
1246 let on = keymap_state.mod_name_is_active(xkb::MOD_NAME_CAPS, xkb::STATE_MODS_EFFECTIVE);
1247 Self { on }
1248 }
1249}
1250
1251#[cfg(test)]
1252mod tests {
1253 use super::*;
1254 use crate::{Point, px};
1255
1256 #[test]
1257 fn test_is_within_click_distance() {
1258 let zero = Point::new(px(0.0), px(0.0));
1259 assert!(is_within_click_distance(zero, Point::new(px(5.0), px(5.0))));
1260 assert!(is_within_click_distance(
1261 zero,
1262 Point::new(px(-4.9), px(5.0))
1263 ));
1264 assert!(is_within_click_distance(
1265 Point::new(px(3.0), px(2.0)),
1266 Point::new(px(-2.0), px(-2.0))
1267 ));
1268 assert!(!is_within_click_distance(
1269 zero,
1270 Point::new(px(5.0), px(5.1))
1271 ),);
1272 }
1273}