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, AsRawFd},
13 time::Duration,
14};
15
16#[cfg(any(feature = "wayland", feature = "x11"))]
17use anyhow::ensure;
18use anyhow::{Context as _, Result, anyhow};
19#[cfg(any(feature = "wayland", feature = "x11"))]
20use ashpd::{
21 desktop::{
22 Request,
23 inhibit::{InhibitFlags, InhibitOptions, InhibitProxy},
24 },
25 enumflags2::BitFlags,
26};
27use calloop::{LoopSignal, channel::Sender};
28use futures::channel::oneshot;
29use gpui_util::{ResultExt as _, new_std_command};
30#[cfg(any(feature = "wayland", feature = "x11"))]
31use xkbcommon::xkb::{self, Keycode, Keysym, State};
32
33use crate::linux::{LinuxDispatcher, PriorityQueueCalloopReceiver};
34use gpui_engine::{DmaBufCapabilities, PlatformTextSystem};
35use gpui_platform::{
36 ActivityGuard, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor,
37 GpuAdapterInfo, GpuId, GpuPreference, MenuCommandId, PathPromptOptions, Platform,
38 PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformMenu,
39 PlatformMenuItem, PlatformWindow, RunnableVariant, Task, ThermalState, WindowAppearance,
40 WindowButtonLayout, WindowId, WindowParams,
41};
42#[cfg(any(feature = "wayland", feature = "x11"))]
43use gpui_types::{Pixels, Point, px};
44
45#[cfg(any(feature = "wayland", feature = "x11"))]
46pub(crate) const SCROLL_LINES: f32 = 3.0;
47
48#[cfg(any(feature = "wayland", feature = "x11"))]
51pub(crate) const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(400);
52#[cfg(any(feature = "wayland", feature = "x11"))]
53pub(crate) const DOUBLE_CLICK_DISTANCE: Pixels = px(5.0);
54pub(crate) const KEYRING_LABEL: &str = "zed-github-account";
55
56#[cfg(any(feature = "wayland", feature = "x11"))]
57const FILE_PICKER_PORTAL_MISSING: &str =
58 "Couldn't open file picker due to missing xdg-desktop-portal implementation.";
59
60pub(crate) trait LinuxClient {
61 fn compositor_name(&self) -> &'static str;
62 fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R;
63 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
64 fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
65 #[allow(unused)]
66 fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>>;
67 fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
68
69 #[cfg(feature = "screen-capture")]
70 fn is_screen_capture_supported(&self) -> bool {
71 true
72 }
73
74 #[cfg(feature = "screen-capture")]
75 fn screen_capture_sources(
76 &self,
77 ) -> oneshot::Receiver<Result<Vec<Rc<dyn gpui_platform::ScreenCaptureSource>>>> {
78 let (sources_tx, sources_rx) = oneshot::channel();
79 sources_tx
80 .send(Err(anyhow::anyhow!(
81 "gpui_linux was compiled without the screen-capture feature"
82 )))
83 .ok();
84 sources_rx
85 }
86
87 fn open_window(
88 &self,
89 handle: WindowId,
90 options: WindowParams,
91 ) -> anyhow::Result<Box<dyn PlatformWindow>>;
92 fn set_cursor_style(&self, style: CursorStyle);
93 fn hide_cursor_until_mouse_moves(&self) {}
94 fn is_cursor_visible(&self) -> bool {
95 true
96 }
97 fn open_uri(&self, uri: &str);
98 fn reveal_path(&self, path: PathBuf);
99 fn write_to_primary(&self, item: ClipboardItem);
100 fn write_to_clipboard(&self, item: ClipboardItem);
101 fn read_from_primary(&self) -> Option<ClipboardItem>;
102 fn read_from_clipboard(&self) -> Option<ClipboardItem>;
103 fn active_window(&self) -> Option<WindowId>;
104 fn window_stack(&self) -> Option<Vec<WindowId>>;
105 fn run(&self);
106
107 fn enumerate_gpus(&self) -> Vec<GpuAdapterInfo> {
110 Vec::new()
111 }
112
113 fn active_gpu(&self) -> Option<GpuAdapterInfo> {
115 None
116 }
117
118 fn dmabuf_capabilities(&self) -> Option<DmaBufCapabilities> {
120 None
121 }
122
123 #[cfg(any(feature = "wayland", feature = "x11"))]
124 fn window_identifier(
125 &self,
126 ) -> impl Future<Output = Option<ashpd::WindowIdentifier>> + Send + 'static {
127 std::future::ready::<Option<ashpd::WindowIdentifier>>(None)
128 }
129}
130
131#[derive(Default)]
132pub(crate) struct PlatformHandlers {
133 pub(crate) open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
134 pub(crate) quit: Option<Box<dyn FnMut() -> bool>>,
135 pub(crate) reopen: Option<Box<dyn FnMut()>>,
136 pub(crate) app_menu_action: Option<Box<dyn FnMut(MenuCommandId)>>,
137 pub(crate) will_open_app_menu: Option<Box<dyn FnMut()>>,
138 pub(crate) validate_app_menu_command: Option<Box<dyn FnMut(MenuCommandId) -> bool>>,
139 pub(crate) keyboard_layout_change: Option<Box<dyn FnMut()>>,
140 pub(crate) system_sleep: Option<Box<dyn FnMut()>>,
141 pub(crate) system_wake: Option<Box<dyn FnMut()>>,
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147#[cfg_attr(
148 not(all(target_os = "linux", any(feature = "wayland", feature = "x11"))),
149 allow(dead_code)
150)]
151pub(crate) enum SystemPowerEvent {
152 Sleep,
153 Wake,
154}
155
156pub fn gpu_preference_from_env() -> Option<GpuPreference> {
164 match std::env::var("ZED_DEVICE_ID") {
165 Ok(value) => match GpuId::parse_pci_id(&value) {
166 Some(device_id) => Some(GpuPreference::Device(GpuId::by_device(device_id))),
167 None => {
168 log::error!(
169 "ZED_DEVICE_ID: expected a four-digit hexadecimal PCI id, got {value:?}"
170 );
171 None
172 }
173 },
174 Err(std::env::VarError::NotPresent) => None,
175 Err(error) => {
176 log::error!("ZED_DEVICE_ID: could not be read: {error}");
177 None
178 }
179 }
180}
181
182pub(crate) struct LinuxCommon {
183 pub(crate) background_executor: BackgroundExecutor,
184 pub(crate) foreground_executor: ForegroundExecutor,
185 pub(crate) text_system: Arc<dyn PlatformTextSystem>,
186 pub(crate) appearance: WindowAppearance,
187 pub(crate) auto_hide_scrollbars: bool,
188 pub(crate) button_layout: WindowButtonLayout,
189 pub(crate) gpu_preference: GpuPreference,
192 pub(crate) callbacks: PlatformHandlers,
193 pub(crate) signal: LoopSignal,
194 app_name: Option<String>,
195 system_notifications: crate::linux::system_notifications::SystemNotificationState,
196 #[cfg_attr(
197 not(all(target_os = "linux", any(feature = "wayland", feature = "x11"))),
198 allow(dead_code)
199 )]
200 power_sender: Sender<SystemPowerEvent>,
201 power_listener_started: bool,
202}
203
204impl LinuxCommon {
205 pub fn new(
206 signal: LoopSignal,
207 ) -> (
208 Self,
209 PriorityQueueCalloopReceiver<RunnableVariant>,
210 calloop::channel::Channel<SystemPowerEvent>,
211 ) {
212 let (main_sender, main_receiver) = PriorityQueueCalloopReceiver::new();
213 let (power_sender, power_receiver) = calloop::channel::channel();
214
215 #[cfg(any(feature = "wayland", feature = "x11"))]
216 let text_system = Arc::new(crate::linux::CosmicTextSystem::new("IBM Plex Sans"));
217 #[cfg(not(any(feature = "wayland", feature = "x11")))]
218 let text_system = Arc::new(gpui_engine::NoopTextSystem::new());
219
220 let callbacks = PlatformHandlers::default();
221
222 let dispatcher = Arc::new(LinuxDispatcher::new(main_sender));
223
224 let background_executor = BackgroundExecutor::new(dispatcher.clone());
225
226 let common = LinuxCommon {
227 background_executor,
228 foreground_executor: ForegroundExecutor::new(dispatcher),
229 text_system,
230 appearance: WindowAppearance::Light,
231 auto_hide_scrollbars: false,
232 button_layout: WindowButtonLayout::linux_default(),
233 gpu_preference: gpu_preference_from_env().unwrap_or_default(),
234 callbacks,
235 signal,
236 app_name: None,
237 system_notifications: crate::linux::system_notifications::SystemNotificationState::new(
238 ),
239 power_sender,
240 power_listener_started: false,
241 };
242
243 (common, main_receiver, power_receiver)
244 }
245
246 pub(crate) fn start_power_listener(&mut self) {
247 if !self.power_listener_started {
248 #[cfg(all(target_os = "linux", any(feature = "wayland", feature = "x11")))]
249 smol::spawn({
250 let power_sender = self.power_sender.clone();
251 async move {
252 if let Err(error) = listen_for_system_power_events(power_sender).await {
253 log::debug!("failed to listen for system sleep/wake events: {error:?}");
254 }
255 }
256 })
257 .detach();
258
259 self.power_listener_started = true;
260 }
261 }
262
263 pub(crate) fn handle_system_power_event(&mut self, event: SystemPowerEvent) {
264 let callback = match event {
265 SystemPowerEvent::Sleep => &mut self.callbacks.system_sleep,
266 SystemPowerEvent::Wake => &mut self.callbacks.system_wake,
267 };
268 if let Some(callback) = callback.as_mut() {
269 callback();
270 }
271 }
272}
273
274#[cfg(all(target_os = "linux", any(feature = "wayland", feature = "x11")))]
275async fn listen_for_system_power_events(
276 power_sender: Sender<SystemPowerEvent>,
277) -> anyhow::Result<()> {
278 use futures::StreamExt as _;
279
280 let connection = ashpd::zbus::Connection::system().await?;
281 let proxy = ashpd::zbus::Proxy::new(
282 &connection,
283 "org.freedesktop.login1",
284 "/org/freedesktop/login1",
285 "org.freedesktop.login1.Manager",
286 )
287 .await?;
288 let mut sleep_events = proxy.receive_signal("PrepareForSleep").await?;
289
290 while let Some(message) = sleep_events.next().await {
291 let event = if message.body().deserialize::<bool>()? {
292 SystemPowerEvent::Sleep
293 } else {
294 SystemPowerEvent::Wake
295 };
296 power_sender.send(event).ok();
297 }
298
299 Ok(())
300}
301
302pub(crate) struct LinuxPlatform<P> {
303 pub(crate) inner: P,
304}
305
306#[cfg(any(feature = "wayland", feature = "x11"))]
313pub(crate) fn enumerate_gpus(gpu_context: &gpui_wgpu::WgpuContextSlot) -> Vec<GpuAdapterInfo> {
314 if let Ok(context) = gpu_context.try_borrow()
315 && let Some(context) = context.as_ref()
316 {
317 return gpui_wgpu::enumerate_adapters_from(&context.instance);
318 }
319 gpui_wgpu::enumerate_adapters()
320}
321
322#[cfg(any(feature = "wayland", feature = "x11"))]
324pub(crate) fn active_gpu(gpu_context: &gpui_wgpu::WgpuContextSlot) -> Option<GpuAdapterInfo> {
325 let context = gpu_context.try_borrow().ok()?;
326 context.as_ref().map(|context| context.adapter_info())
327}
328
329#[cfg(any(feature = "wayland", feature = "x11"))]
331pub(crate) fn dmabuf_capabilities(
332 gpu_context: &gpui_wgpu::WgpuContextSlot,
333) -> Option<DmaBufCapabilities> {
334 let context = gpu_context.try_borrow().ok()?;
335 context
336 .as_ref()
337 .and_then(|context| context.dmabuf_capabilities())
338}
339
340impl<P: LinuxClient + 'static> Platform for LinuxPlatform<P> {
341 fn background_executor(&self) -> BackgroundExecutor {
342 self.inner
343 .with_common(|common| common.background_executor.clone())
344 }
345
346 fn foreground_executor(&self) -> ForegroundExecutor {
347 self.inner
348 .with_common(|common| common.foreground_executor.clone())
349 }
350
351 fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
352 self.inner.with_common(|common| common.text_system.clone())
353 }
354
355 fn set_gpu_preference(&self, preference: GpuPreference) {
356 self.inner
357 .with_common(|common| common.gpu_preference = preference);
358 }
359
360 fn enumerate_gpus(&self) -> Vec<GpuAdapterInfo> {
361 self.inner.enumerate_gpus()
362 }
363
364 fn active_gpu(&self) -> Option<GpuAdapterInfo> {
365 self.inner.active_gpu()
366 }
367
368 fn dmabuf_capabilities(&self) -> Option<DmaBufCapabilities> {
369 self.inner.dmabuf_capabilities()
370 }
371
372 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
373 self.inner.keyboard_layout()
374 }
375
376 fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
377 Rc::new(gpui_platform::DummyKeyboardMapper)
378 }
379
380 fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
381 self.inner
382 .with_common(|common| common.callbacks.keyboard_layout_change = Some(callback));
383 }
384
385 fn on_thermal_state_change(&self, _callback: Box<dyn FnMut()>) {}
386
387 fn thermal_state(&self) -> ThermalState {
388 ThermalState::Nominal
389 }
390
391 #[cfg(not(any(feature = "wayland", feature = "x11")))]
392 fn prevent_idle_sleep(&self, reason: &str) -> Task<Result<ActivityGuard>> {
393 Task::ready(Err(anyhow!(
394 "Idle sleep prevention for {reason:?} requires a Linux windowing backend"
395 )))
396 }
397
398 #[cfg(any(feature = "wayland", feature = "x11"))]
399 fn prevent_idle_sleep(&self, reason: &str) -> Task<Result<ActivityGuard>> {
400 let executor = self.background_executor();
401 let (guard_tx, guard_rx) = oneshot::channel();
402 executor
403 .spawn({
404 let executor = executor.clone();
405 let reason = reason.to_owned();
406 async move {
407 guard_tx
408 .send(inhibit_idle_sleep(reason, executor).await)
409 .ok();
410 }
411 })
412 .detach();
413 executor
414 .clone()
415 .spawn(async move { await_idle_sleep_prevention(guard_rx, &executor).await })
416 }
417
418 fn run(&self, on_finish_launching: Box<dyn FnOnce()>) {
419 on_finish_launching();
420
421 LinuxClient::run(&self.inner);
422
423 let quit = self
424 .inner
425 .with_common(|common| common.callbacks.quit.take());
426 if let Some(mut fun) = quit {
427 fun();
428 }
429 }
430
431 fn quit(&self) {
432 self.inner.with_common(|common| common.signal.stop());
433 }
434
435 fn compositor_name(&self) -> &'static str {
436 self.inner.compositor_name()
437 }
438
439 fn restart(&self, binary_path: Option<PathBuf>, arguments: Vec<std::ffi::OsString>) {
440 use std::os::unix::process::CommandExt as _;
441
442 let app_pid = std::process::id().to_string();
444 let app_path = if let Some(path) = binary_path {
446 path
447 } else {
448 match self.app_path() {
449 Ok(path) => path,
450 Err(err) => {
451 log::error!("Failed to get app path: {:?}", err);
452 return;
453 }
454 }
455 };
456
457 log::info!("Restarting process, using app path: {:?}", app_path);
458
459 let script = r#"
462 while kill -0 "$0" 2>/dev/null; do
463 sleep 0.1
464 done
465
466 app_path="$1"
467 shift
468 "$app_path" "$@"
469 "#;
470
471 #[allow(
472 clippy::disallowed_methods,
473 reason = "We are restarting ourselves, using std command thus is fine"
474 )]
475 let restart_process = new_std_command("/usr/bin/env")
476 .arg("bash")
477 .arg("-c")
478 .arg(script)
479 .arg(&app_pid)
480 .arg(&app_path)
481 .args(arguments)
482 .process_group(0)
483 .spawn();
484
485 match restart_process {
486 Ok(_) => self.quit(),
487 Err(e) => log::error!("failed to spawn restart script: {:?}", e),
488 }
489 }
490
491 fn activate(&self, _ignoring_other_apps: bool) {
492 log::info!("activate is not implemented on Linux, ignoring the call")
493 }
494
495 fn hide(&self) {
496 log::info!("hide is not implemented on Linux, ignoring the call")
497 }
498
499 fn hide_other_apps(&self) {
500 log::info!("hide_other_apps is not implemented on Linux, ignoring the call")
501 }
502
503 fn unhide_other_apps(&self) {
504 log::info!("unhide_other_apps is not implemented on Linux, ignoring the call")
505 }
506
507 fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
508 self.inner.primary_display()
509 }
510
511 fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
512 self.inner.displays()
513 }
514
515 #[cfg(feature = "screen-capture")]
516 fn is_screen_capture_supported(&self) -> bool {
517 self.inner.is_screen_capture_supported()
518 }
519
520 #[cfg(feature = "screen-capture")]
521 fn screen_capture_sources(
522 &self,
523 ) -> oneshot::Receiver<Result<Vec<Rc<dyn gpui_platform::ScreenCaptureSource>>>> {
524 self.inner.screen_capture_sources()
525 }
526
527 fn active_window(&self) -> Option<WindowId> {
528 self.inner.active_window()
529 }
530
531 fn window_stack(&self) -> Option<Vec<WindowId>> {
532 self.inner.window_stack()
533 }
534
535 fn open_window(
536 &self,
537 handle: WindowId,
538 options: WindowParams,
539 ) -> anyhow::Result<Box<dyn PlatformWindow>> {
540 self.inner.open_window(handle, options)
541 }
542
543 fn open_url(&self, url: &str) {
544 self.inner.open_uri(url);
545 }
546
547 fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
548 self.inner
549 .with_common(|common| common.callbacks.open_urls = Some(callback));
550 }
551
552 fn prompt_for_paths(
553 &self,
554 options: PathPromptOptions,
555 ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
556 let (done_tx, done_rx) = oneshot::channel();
557
558 #[cfg(not(any(feature = "wayland", feature = "x11")))]
559 let _ = (done_tx.send(Ok(None)), options);
560
561 #[cfg(any(feature = "wayland", feature = "x11"))]
562 let identifier = self.inner.window_identifier();
563
564 #[cfg(any(feature = "wayland", feature = "x11"))]
565 self.foreground_executor()
566 .spawn(async move {
567 let title = if options.directories {
568 "Open Folder"
569 } else {
570 "Open File"
571 };
572
573 let request = match ashpd::desktop::file_chooser::OpenFileRequest::default()
574 .identifier(identifier.await)
575 .modal(true)
576 .title(title)
577 .accept_label(
578 options
579 .prompt
580 .as_ref()
581 .map(gpui_platform::SharedString::as_str),
582 )
583 .multiple(options.multiple)
584 .directory(options.directories)
585 .send()
586 .await
587 {
588 Ok(request) => request,
589 Err(err) => {
590 let result = match err {
591 ashpd::Error::PortalNotFound(_) => anyhow!(FILE_PICKER_PORTAL_MISSING),
592 err => err.into(),
593 };
594 let _ = done_tx.send(Err(result));
595 return;
596 }
597 };
598
599 let result = match request.response() {
600 Ok(response) => Ok(Some(
601 response
602 .uris()
603 .iter()
604 .filter_map(|uri: &ashpd::Uri| url::Url::parse(uri.as_str()).ok())
605 .filter_map(|uri: url::Url| uri.to_file_path().ok())
606 .collect::<Vec<_>>(),
607 )),
608 Err(ashpd::Error::Response(_)) => Ok(None),
609 Err(e) => Err(e.into()),
610 };
611 let _ = done_tx.send(result);
612 })
613 .detach();
614 done_rx
615 }
616
617 fn prompt_for_new_path(
618 &self,
619 directory: &Path,
620 suggested_name: Option<&str>,
621 ) -> oneshot::Receiver<Result<Option<PathBuf>>> {
622 let (done_tx, done_rx) = oneshot::channel();
623
624 #[cfg(not(any(feature = "wayland", feature = "x11")))]
625 let _ = (done_tx.send(Ok(None)), directory, suggested_name);
626
627 #[cfg(any(feature = "wayland", feature = "x11"))]
628 let identifier = self.inner.window_identifier();
629
630 #[cfg(any(feature = "wayland", feature = "x11"))]
631 self.foreground_executor()
632 .spawn({
633 let directory = directory.to_owned();
634 let suggested_name = suggested_name.map(|s| s.to_owned());
635
636 async move {
637 let mut request_builder =
638 ashpd::desktop::file_chooser::SaveFileRequest::default()
639 .identifier(identifier.await)
640 .modal(true)
641 .title("Save File")
642 .current_folder(directory)
643 .expect("pathbuf should not be nul terminated");
644
645 if let Some(suggested_name) = suggested_name {
646 request_builder = request_builder.current_name(suggested_name.as_str());
647 }
648
649 let request = match request_builder.send().await {
650 Ok(request) => request,
651 Err(err) => {
652 let result = match err {
653 ashpd::Error::PortalNotFound(_) => {
654 anyhow!(FILE_PICKER_PORTAL_MISSING)
655 }
656 err => err.into(),
657 };
658 let _ = done_tx.send(Err(result));
659 return;
660 }
661 };
662
663 let result = match request.response() {
664 Ok(response) => Ok(response
665 .uris()
666 .first()
667 .and_then(|uri: &ashpd::Uri| url::Url::parse(uri.as_str()).ok())
668 .and_then(|uri: url::Url| uri.to_file_path().ok())),
669 Err(ashpd::Error::Response(_)) => Ok(None),
670 Err(e) => Err(e.into()),
671 };
672 let _ = done_tx.send(result);
673 }
674 })
675 .detach();
676
677 done_rx
678 }
679
680 fn can_select_mixed_files_and_dirs(&self) -> bool {
681 false
683 }
684
685 fn reveal_path(&self, path: &Path) {
686 self.inner.reveal_path(path.to_owned());
687 }
688
689 fn open_with_system(&self, path: &Path) {
690 let path = path.to_owned();
691 self.background_executor()
692 .spawn(async move {
693 #[allow(
694 clippy::disallowed_methods,
695 reason = "running on a background thread, so blocking is fine"
696 )]
697 new_std_command("xdg-open")
698 .arg(path)
699 .status()
700 .context("invoking xdg-open")
701 .log_err();
702 })
703 .detach();
704 }
705
706 fn on_quit(&self, callback: Box<dyn FnMut() -> bool>) {
707 self.inner.with_common(|common| {
708 common.callbacks.quit = Some(callback);
709 });
710 }
711
712 fn on_reopen(&self, callback: Box<dyn FnMut()>) {
713 self.inner.with_common(|common| {
714 common.callbacks.reopen = Some(callback);
715 });
716 }
717
718 fn on_system_sleep(&self, callback: Box<dyn FnMut()>) {
719 self.inner.with_common(|common| {
720 common.callbacks.system_sleep = Some(callback);
721 common.start_power_listener();
722 });
723 }
724
725 fn on_system_wake(&self, callback: Box<dyn FnMut()>) {
726 self.inner.with_common(|common| {
727 common.callbacks.system_wake = Some(callback);
728 common.start_power_listener();
729 });
730 }
731
732 fn set_app_identity(&self, _identifier: &str, name: &str) {
733 self.inner
734 .with_common(|common| common.app_name = Some(name.to_string()));
735 }
736
737 fn show_system_notification(&self, notification: gpui_platform::SystemNotification) {
738 self.inner.with_common(|common| {
739 common
740 .system_notifications
741 .show(common.app_name.as_deref(), notification)
742 });
743 }
744
745 fn dismiss_system_notification(&self, tag: &str) {
746 self.inner
747 .with_common(|common| common.system_notifications.dismiss(tag));
748 }
749
750 fn on_system_notification_response(
751 &self,
752 callback: Box<dyn FnMut(gpui_platform::SystemNotificationResponse)>,
753 ) {
754 self.inner.with_common(|common| {
755 let executor = common.foreground_executor.clone();
756 common.system_notifications.on_response(&executor, callback)
757 });
758 }
759
760 fn on_app_menu_action(&self, callback: Box<dyn FnMut(MenuCommandId)>) {
761 self.inner.with_common(|common| {
762 common.callbacks.app_menu_action = Some(callback);
763 });
764 }
765
766 fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>) {
767 self.inner.with_common(|common| {
768 common.callbacks.will_open_app_menu = Some(callback);
769 });
770 }
771
772 fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(MenuCommandId) -> bool>) {
773 self.inner.with_common(|common| {
774 common.callbacks.validate_app_menu_command = Some(callback);
775 });
776 }
777
778 fn app_path(&self) -> Result<PathBuf> {
779 let app_path = env::current_exe()?;
781 Ok(app_path)
782 }
783
784 fn set_menus(&self, _menus: Vec<PlatformMenu>) {
785 }
787
788 fn set_dock_menu(&self, _menu: Vec<PlatformMenuItem>) {
789 }
791
792 fn path_for_auxiliary_executable(&self, _name: &str) -> Result<PathBuf> {
793 Err(anyhow::Error::msg(
794 "Platform<LinuxPlatform>::path_for_auxiliary_executable is not implemented yet",
795 ))
796 }
797
798 fn set_cursor_style(&self, style: CursorStyle) {
799 self.inner.set_cursor_style(style)
800 }
801
802 fn hide_cursor_until_mouse_moves(&self) {
803 self.inner.hide_cursor_until_mouse_moves()
804 }
805
806 fn is_cursor_visible(&self) -> bool {
807 self.inner.is_cursor_visible()
808 }
809
810 fn should_auto_hide_scrollbars(&self) -> bool {
811 self.inner.with_common(|common| common.auto_hide_scrollbars)
812 }
813
814 fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>> {
815 let url = url.to_string();
816 let username = username.to_string();
817 let password = password.to_vec();
818 self.background_executor().spawn(async move {
819 let keyring = oo7::Keyring::new().await?;
820 keyring.unlock().await?;
821 keyring
822 .create_item(
823 KEYRING_LABEL,
824 &vec![("url", &url), ("username", &username)],
825 password,
826 true,
827 )
828 .await?;
829 Ok(())
830 })
831 }
832
833 fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
834 let url = url.to_string();
835 self.background_executor().spawn(async move {
836 let keyring = oo7::Keyring::new().await?;
837 keyring.unlock().await?;
838
839 let items = keyring.search_items(&vec![("url", &url)]).await?;
840
841 for item in items.into_iter() {
842 if item.label().await.is_ok_and(|label| label == KEYRING_LABEL) {
843 let attributes = item.attributes().await?;
844 let username = attributes
845 .get("username")
846 .context("Cannot find username in stored credentials")?;
847 item.unlock().await?;
848 let secret = item.secret().await?;
849
850 return Ok(Some((username.to_string(), secret.to_vec())));
853 } else {
854 continue;
855 }
856 }
857 Ok(None)
858 })
859 }
860
861 fn delete_credentials(&self, url: &str) -> Task<Result<()>> {
862 let url = url.to_string();
863 self.background_executor().spawn(async move {
864 let keyring = oo7::Keyring::new().await?;
865 keyring.unlock().await?;
866
867 let items = keyring.search_items(&vec![("url", &url)]).await?;
868
869 for item in items.into_iter() {
870 if item.label().await.is_ok_and(|label| label == KEYRING_LABEL) {
871 item.delete().await?;
872 return Ok(());
873 }
874 }
875
876 Ok(())
877 })
878 }
879
880 fn window_appearance(&self) -> WindowAppearance {
881 self.inner.with_common(|common| common.appearance)
882 }
883
884 fn button_layout(&self) -> Option<WindowButtonLayout> {
885 Some(self.inner.with_common(|common| common.button_layout))
886 }
887
888 fn register_url_scheme(&self, _: &str) -> Task<anyhow::Result<()>> {
889 Task::ready(Err(anyhow!("register_url_scheme unimplemented")))
890 }
891
892 fn write_to_primary(&self, item: ClipboardItem) {
893 self.inner.write_to_primary(item)
894 }
895
896 fn write_to_clipboard(&self, item: ClipboardItem) {
897 self.inner.write_to_clipboard(item)
898 }
899
900 fn read_from_primary(&self) -> Option<ClipboardItem> {
901 self.inner.read_from_primary()
902 }
903
904 fn read_from_clipboard(&self) -> Option<ClipboardItem> {
905 self.inner.read_from_clipboard()
906 }
907
908 fn add_recent_document(&self, _path: &Path) {}
909}
910
911#[cfg(any(feature = "wayland", feature = "x11"))]
912pub(super) fn open_uri_internal(
913 executor: BackgroundExecutor,
914 uri: &str,
915 activation_token: Option<String>,
916) {
917 if let Some(uri) = ashpd::Uri::parse(uri).log_err() {
918 executor
919 .spawn(async move {
920 let mut xdg_open_failed = false;
921 for mut command in open::commands(uri.to_string()) {
922 if let Some(token) = activation_token.as_ref() {
923 command.env("XDG_ACTIVATION_TOKEN", token);
924 }
925 let program = format!("{:?}", command.get_program());
926 match smol::process::Command::from(command).spawn() {
927 Ok(mut cmd) => match cmd.status().await {
928 Ok(status) if status.success() => return,
929 Ok(status) => {
930 log::error!("Command {} exited with status: {}", program, status);
931 xdg_open_failed = true;
932 }
933 Err(e) => {
934 log::error!("Failed to get status from {}: {}", program, e);
935 xdg_open_failed = true;
936 }
937 },
938 Err(e) => {
939 log::error!("Failed to open with {}: {}", program, e);
940 xdg_open_failed = true;
941 }
942 }
943 }
944
945 if xdg_open_failed {
946 match ashpd::desktop::open_uri::OpenFileRequest::default()
947 .activation_token(activation_token.map(ashpd::ActivationToken::from))
948 .send_uri(&uri)
949 .await
950 .and_then(|e| e.response())
951 {
952 Ok(()) => {}
953 Err(ashpd::Error::Response(ashpd::desktop::ResponseError::Cancelled)) => {}
954 Err(e) => {
955 log::error!("Failed to open with dbus: {}", e);
956 }
957 }
958 }
959 })
960 .detach();
961 }
962}
963
964#[cfg(any(feature = "x11", feature = "wayland"))]
965pub(super) fn reveal_path_internal(
966 executor: BackgroundExecutor,
967 path: PathBuf,
968 activation_token: Option<String>,
969) {
970 executor
971 .spawn(async move {
972 if let Some(dir) = File::open(path.clone()).log_err() {
973 match ashpd::desktop::open_uri::OpenDirectoryRequest::default()
974 .activation_token(activation_token.map(ashpd::ActivationToken::from))
975 .send(&dir.as_fd())
976 .await
977 {
978 Ok(_) => return,
979 Err(e) => log::error!("Failed to open with dbus: {}", e),
980 }
981 if path.is_dir() {
982 open::that_detached(path).log_err();
983 } else {
984 open::that_detached(path.parent().unwrap_or(Path::new(""))).log_err();
985 }
986 }
987 })
988 .detach();
989}
990
991#[cfg(any(feature = "wayland", feature = "x11"))]
992pub(super) fn is_within_click_distance(a: Point<Pixels>, b: Point<Pixels>) -> bool {
993 let diff = a - b;
994 diff.x.abs() <= DOUBLE_CLICK_DISTANCE && diff.y.abs() <= DOUBLE_CLICK_DISTANCE
995}
996
997#[cfg(any(feature = "wayland", feature = "x11"))]
1002pub(super) fn new_xkb_context() -> anyhow::Result<xkb::Context> {
1003 validate_xkb_context(xkb::Context::new(xkb::CONTEXT_NO_DEFAULT_INCLUDES))
1004}
1005
1006#[cfg(any(feature = "wayland", feature = "x11"))]
1007fn validate_xkb_context(context: xkb::Context) -> anyhow::Result<xkb::Context> {
1008 ensure!(
1009 !context.get_raw_ptr().is_null(),
1010 "libxkbcommon failed to create an XKB context"
1011 );
1012 Ok(context)
1013}
1014
1015#[cfg(any(feature = "wayland", feature = "x11"))]
1016pub(super) fn get_xkb_compose_state(cx: &xkb::Context) -> Option<xkb::compose::State> {
1017 let mut locales = Vec::default();
1018 if let Some(locale) = env::var_os("LC_CTYPE") {
1019 locales.push(locale);
1020 }
1021 locales.push(OsString::from("C"));
1022 let mut state: Option<xkb::compose::State> = None;
1023 for locale in locales {
1024 if let Ok(table) =
1025 xkb::compose::Table::new_from_locale(cx, &locale, xkb::compose::COMPILE_NO_FLAGS)
1026 {
1027 state = Some(xkb::compose::State::new(
1028 &table,
1029 xkb::compose::STATE_NO_FLAGS,
1030 ));
1031 break;
1032 }
1033 }
1034 state
1035}
1036
1037#[cfg(any(feature = "wayland", feature = "x11"))]
1038pub(super) const PIPE_READ_TIMEOUT: Duration = Duration::from_secs(4);
1039
1040#[cfg(any(feature = "wayland", feature = "x11"))]
1041pub(super) fn read_fd_with_timeout(
1042 mut fd: filedescriptor::FileDescriptor,
1043 timeout: Duration,
1044) -> Result<Vec<u8>> {
1045 fd.set_non_blocking(true)?;
1046 let mut buffer = Vec::new();
1047 let mut chunk = [0u8; 8192];
1048 loop {
1049 let mut poll_fds = [filedescriptor::pollfd {
1050 fd: fd.as_raw_fd(),
1051 events: filedescriptor::POLLIN,
1052 revents: 0,
1053 }];
1054 let ready = match filedescriptor::poll(&mut poll_fds, Some(timeout)) {
1055 Ok(ready) => ready,
1056 Err(filedescriptor::Error::Poll(err))
1057 if err.kind() == std::io::ErrorKind::Interrupted =>
1058 {
1059 continue;
1060 }
1061 Err(err) => return Err(err.into()),
1062 };
1063 if ready == 0 {
1064 anyhow::bail!("timed out waiting for data on pipe after {timeout:?}");
1065 }
1066 match fd.read(&mut chunk) {
1067 Ok(0) => return Ok(buffer),
1068 Ok(len) => buffer.extend_from_slice(&chunk[..len]),
1069 Err(err)
1070 if err.kind() == std::io::ErrorKind::WouldBlock
1071 || err.kind() == std::io::ErrorKind::Interrupted => {}
1072 Err(err) => return Err(err.into()),
1073 }
1074 }
1075}
1076
1077#[cfg(any(feature = "wayland", feature = "x11"))]
1078pub(super) const DEFAULT_CURSOR_ICON_NAME: &str = "left_ptr";
1079
1080#[cfg(any(feature = "wayland", feature = "x11"))]
1081pub(super) fn cursor_style_to_icon_names(style: CursorStyle) -> &'static [&'static str] {
1082 match style {
1085 CursorStyle::Arrow => &[DEFAULT_CURSOR_ICON_NAME],
1086 CursorStyle::IBeam => &["text", "xterm"],
1087 CursorStyle::Crosshair => &["crosshair", "cross"],
1088 CursorStyle::ClosedHand => &["closedhand", "grabbing", "hand2"],
1089 CursorStyle::OpenHand => &["openhand", "grab", "hand1"],
1090 CursorStyle::PointingHand => &["pointer", "hand", "hand2"],
1091 CursorStyle::ResizeLeft => &["w-resize", "left_side"],
1092 CursorStyle::ResizeRight => &["e-resize", "right_side"],
1093 CursorStyle::ResizeLeftRight => &["ew-resize", "sb_h_double_arrow"],
1094 CursorStyle::ResizeUp => &["n-resize", "top_side"],
1095 CursorStyle::ResizeDown => &["s-resize", "bottom_side"],
1096 CursorStyle::ResizeUpDown => &["sb_v_double_arrow", "ns-resize"],
1097 CursorStyle::ResizeUpLeftDownRight => &["size_fdiag", "bd_double_arrow", "nwse-resize"],
1098 CursorStyle::ResizeUpRightDownLeft => &["size_bdiag", "nesw-resize", "fd_double_arrow"],
1099 CursorStyle::ResizeColumn => &["col-resize", "sb_h_double_arrow"],
1100 CursorStyle::ResizeRow => &["row-resize", "sb_v_double_arrow"],
1101 CursorStyle::IBeamCursorForVerticalLayout => &["vertical-text"],
1102 CursorStyle::OperationNotAllowed => &["not-allowed", "crossed_circle"],
1103 CursorStyle::DragLink => &["alias"],
1104 CursorStyle::DragCopy => &["copy"],
1105 CursorStyle::ContextualMenu => &["context-menu"],
1106 }
1107}
1108
1109#[cfg(any(feature = "wayland", feature = "x11"))]
1110pub(super) fn log_cursor_icon_warning(message: impl std::fmt::Display) {
1111 if let Ok(xcursor_path) = env::var("XCURSOR_PATH") {
1112 log::warn!(
1113 "{:#}\ncursor icon loading may be failing if XCURSOR_PATH environment variable is invalid. \
1114 XCURSOR_PATH overrides the default icon search. Its current value is '{}'",
1115 message,
1116 xcursor_path
1117 );
1118 } else {
1119 log::warn!("{:#}", message);
1120 }
1121}
1122
1123#[cfg(any(feature = "wayland", feature = "x11"))]
1124fn guess_ascii(keycode: Keycode, shift: bool) -> Option<char> {
1125 let c = match (keycode.raw(), shift) {
1126 (24, _) => 'q',
1127 (25, _) => 'w',
1128 (26, _) => 'e',
1129 (27, _) => 'r',
1130 (28, _) => 't',
1131 (29, _) => 'y',
1132 (30, _) => 'u',
1133 (31, _) => 'i',
1134 (32, _) => 'o',
1135 (33, _) => 'p',
1136 (34, false) => '[',
1137 (34, true) => '{',
1138 (35, false) => ']',
1139 (35, true) => '}',
1140 (38, _) => 'a',
1141 (39, _) => 's',
1142 (40, _) => 'd',
1143 (41, _) => 'f',
1144 (42, _) => 'g',
1145 (43, _) => 'h',
1146 (44, _) => 'j',
1147 (45, _) => 'k',
1148 (46, _) => 'l',
1149 (47, false) => ';',
1150 (47, true) => ':',
1151 (48, false) => '\'',
1152 (48, true) => '"',
1153 (49, false) => '`',
1154 (49, true) => '~',
1155 (51, false) => '\\',
1156 (51, true) => '|',
1157 (52, _) => 'z',
1158 (53, _) => 'x',
1159 (54, _) => 'c',
1160 (55, _) => 'v',
1161 (56, _) => 'b',
1162 (57, _) => 'n',
1163 (58, _) => 'm',
1164 (59, false) => ',',
1165 (59, true) => '>',
1166 (60, false) => '.',
1167 (60, true) => '<',
1168 (61, false) => '/',
1169 (61, true) => '?',
1170
1171 _ => return None,
1172 };
1173
1174 Some(c)
1175}
1176
1177#[cfg(any(feature = "wayland", feature = "x11"))]
1178pub(super) fn keystroke_from_xkb(
1179 state: &State,
1180 mut modifiers: gpui_platform::Modifiers,
1181 keycode: Keycode,
1182) -> gpui_platform::Keystroke {
1183 let key_utf32 = state.key_get_utf32(keycode);
1184 let key_utf8 = state.key_get_utf8(keycode);
1185 let key_sym = state.key_get_one_sym(keycode);
1186
1187 let key = match key_sym {
1188 Keysym::Return => "enter".to_owned(),
1189 Keysym::Prior => "pageup".to_owned(),
1190 Keysym::Next => "pagedown".to_owned(),
1191 Keysym::ISO_Left_Tab => "tab".to_owned(),
1192 Keysym::KP_Prior => "pageup".to_owned(),
1193 Keysym::KP_Next => "pagedown".to_owned(),
1194 Keysym::XF86_Back => "back".to_owned(),
1195 Keysym::XF86_Forward => "forward".to_owned(),
1196 Keysym::XF86_Cut => "cut".to_owned(),
1197 Keysym::XF86_Copy => "copy".to_owned(),
1198 Keysym::XF86_Paste => "paste".to_owned(),
1199 Keysym::XF86_New => "new".to_owned(),
1200 Keysym::XF86_Open => "open".to_owned(),
1201 Keysym::XF86_Save => "save".to_owned(),
1202
1203 Keysym::comma => ",".to_owned(),
1204 Keysym::period => ".".to_owned(),
1205 Keysym::less => "<".to_owned(),
1206 Keysym::greater => ">".to_owned(),
1207 Keysym::slash => "/".to_owned(),
1208 Keysym::question => "?".to_owned(),
1209
1210 Keysym::semicolon => ";".to_owned(),
1211 Keysym::colon => ":".to_owned(),
1212 Keysym::apostrophe => "'".to_owned(),
1213 Keysym::quotedbl => "\"".to_owned(),
1214
1215 Keysym::bracketleft => "[".to_owned(),
1216 Keysym::braceleft => "{".to_owned(),
1217 Keysym::bracketright => "]".to_owned(),
1218 Keysym::braceright => "}".to_owned(),
1219 Keysym::backslash => "\\".to_owned(),
1220 Keysym::bar => "|".to_owned(),
1221
1222 Keysym::grave => "`".to_owned(),
1223 Keysym::asciitilde => "~".to_owned(),
1224 Keysym::exclam => "!".to_owned(),
1225 Keysym::at => "@".to_owned(),
1226 Keysym::numbersign => "#".to_owned(),
1227 Keysym::dollar => "$".to_owned(),
1228 Keysym::percent => "%".to_owned(),
1229 Keysym::asciicircum => "^".to_owned(),
1230 Keysym::ampersand => "&".to_owned(),
1231 Keysym::asterisk => "*".to_owned(),
1232 Keysym::parenleft => "(".to_owned(),
1233 Keysym::parenright => ")".to_owned(),
1234 Keysym::minus => "-".to_owned(),
1235 Keysym::underscore => "_".to_owned(),
1236 Keysym::equal => "=".to_owned(),
1237 Keysym::plus => "+".to_owned(),
1238 Keysym::space => "space".to_owned(),
1239 Keysym::BackSpace => "backspace".to_owned(),
1240 Keysym::Tab => "tab".to_owned(),
1241 Keysym::Delete => "delete".to_owned(),
1242 Keysym::Escape => "escape".to_owned(),
1243
1244 Keysym::Left => "left".to_owned(),
1245 Keysym::Right => "right".to_owned(),
1246 Keysym::Up => "up".to_owned(),
1247 Keysym::Down => "down".to_owned(),
1248 Keysym::Home => "home".to_owned(),
1249 Keysym::End => "end".to_owned(),
1250 Keysym::Insert => "insert".to_owned(),
1251
1252 _ => {
1253 let name = xkb::keysym_get_name(key_sym).to_lowercase();
1254 if key_sym.is_keypad_key() {
1255 name.replace("kp_", "")
1256 } else if let Some(key) = key_utf8.chars().next()
1257 && key_utf8.len() == 1
1258 && key.is_ascii()
1259 {
1260 if key.is_ascii_graphic() {
1261 key_utf8.to_lowercase()
1262 } else if key_utf32 <= 0x1f
1266 && !name.chars().next().is_some_and(|c| c.is_ascii_digit())
1267 {
1268 ((key_utf32 as u8 + 0x40) as char)
1269 .to_ascii_lowercase()
1270 .to_string()
1271 } else {
1272 name
1273 }
1274 } else if let Some(key_en) = guess_ascii(keycode, modifiers.shift) {
1275 String::from(key_en)
1276 } else {
1277 name
1278 }
1279 }
1280 };
1281
1282 if modifiers.shift {
1283 if key.chars().count() == 1 && key.to_lowercase() == key.to_uppercase() {
1287 modifiers.shift = false;
1288 }
1289 }
1290
1291 let key_char =
1293 (key_utf32 >= 32 && key_utf32 != 127 && !key_utf8.is_empty()).then_some(key_utf8);
1294
1295 gpui_platform::Keystroke {
1296 modifiers,
1297 key,
1298 key_char,
1299 }
1300}
1301
1302#[cfg(any(feature = "wayland", feature = "x11"))]
1307pub fn keystroke_underlying_dead_key(keysym: Keysym) -> Option<String> {
1308 match keysym {
1309 Keysym::dead_grave => Some("`".to_owned()),
1310 Keysym::dead_acute => Some("´".to_owned()),
1311 Keysym::dead_circumflex => Some("^".to_owned()),
1312 Keysym::dead_tilde => Some("~".to_owned()),
1313 Keysym::dead_macron => Some("¯".to_owned()),
1314 Keysym::dead_breve => Some("˘".to_owned()),
1315 Keysym::dead_abovedot => Some("˙".to_owned()),
1316 Keysym::dead_diaeresis => Some("¨".to_owned()),
1317 Keysym::dead_abovering => Some("˚".to_owned()),
1318 Keysym::dead_doubleacute => Some("˝".to_owned()),
1319 Keysym::dead_caron => Some("ˇ".to_owned()),
1320 Keysym::dead_cedilla => Some("¸".to_owned()),
1321 Keysym::dead_ogonek => Some("˛".to_owned()),
1322 Keysym::dead_iota => Some("ͅ".to_owned()),
1323 Keysym::dead_voiced_sound => Some("゙".to_owned()),
1324 Keysym::dead_semivoiced_sound => Some("゚".to_owned()),
1325 Keysym::dead_belowdot => Some("̣̣".to_owned()),
1326 Keysym::dead_hook => Some("̡".to_owned()),
1327 Keysym::dead_horn => Some("̛".to_owned()),
1328 Keysym::dead_stroke => Some("̶̶".to_owned()),
1329 Keysym::dead_abovecomma => Some("̓̓".to_owned()),
1330 Keysym::dead_abovereversedcomma => Some("ʽ".to_owned()),
1331 Keysym::dead_doublegrave => Some("̏".to_owned()),
1332 Keysym::dead_belowring => Some("˳".to_owned()),
1333 Keysym::dead_belowmacron => Some("̱".to_owned()),
1334 Keysym::dead_belowcircumflex => Some("ꞈ".to_owned()),
1335 Keysym::dead_belowtilde => Some("̰".to_owned()),
1336 Keysym::dead_belowbreve => Some("̮".to_owned()),
1337 Keysym::dead_belowdiaeresis => Some("̤".to_owned()),
1338 Keysym::dead_invertedbreve => Some("̯".to_owned()),
1339 Keysym::dead_belowcomma => Some("̦".to_owned()),
1340 Keysym::dead_currency => None,
1341 Keysym::dead_lowline => None,
1342 Keysym::dead_aboveverticalline => None,
1343 Keysym::dead_belowverticalline => None,
1344 Keysym::dead_longsolidusoverlay => None,
1345 Keysym::dead_a => None,
1346 Keysym::dead_A => None,
1347 Keysym::dead_e => None,
1348 Keysym::dead_E => None,
1349 Keysym::dead_i => None,
1350 Keysym::dead_I => None,
1351 Keysym::dead_o => None,
1352 Keysym::dead_O => None,
1353 Keysym::dead_u => None,
1354 Keysym::dead_U => None,
1355 Keysym::dead_small_schwa => Some("ə".to_owned()),
1356 Keysym::dead_capital_schwa => Some("Ə".to_owned()),
1357 Keysym::dead_greek => None,
1358 _ => None,
1359 }
1360}
1361#[cfg(any(feature = "wayland", feature = "x11"))]
1362pub(super) fn modifiers_from_xkb(keymap_state: &State) -> gpui_platform::Modifiers {
1363 let shift = keymap_state.mod_name_is_active(xkb::MOD_NAME_SHIFT, xkb::STATE_MODS_EFFECTIVE);
1364 let alt = keymap_state.mod_name_is_active(xkb::MOD_NAME_ALT, xkb::STATE_MODS_EFFECTIVE);
1365 let control = keymap_state.mod_name_is_active(xkb::MOD_NAME_CTRL, xkb::STATE_MODS_EFFECTIVE);
1366 let platform = keymap_state.mod_name_is_active(xkb::MOD_NAME_LOGO, xkb::STATE_MODS_EFFECTIVE);
1367 gpui_platform::Modifiers {
1368 shift,
1369 alt,
1370 control,
1371 platform,
1372 function: false,
1373 }
1374}
1375
1376#[cfg(any(feature = "wayland", feature = "x11"))]
1377pub(super) fn capslock_from_xkb(keymap_state: &State) -> gpui_platform::Capslock {
1378 let on = keymap_state.mod_name_is_active(xkb::MOD_NAME_CAPS, xkb::STATE_MODS_EFFECTIVE);
1379 gpui_platform::Capslock { on }
1380}
1381
1382#[cfg(any(feature = "wayland", feature = "x11"))]
1386pub(super) fn compositor_gpu_hint_from_dev_t(dev: u64) -> Option<gpui_wgpu::CompositorGpuHint> {
1387 fn dev_major(dev: u64) -> u32 {
1388 ((dev >> 8) & 0xfff) as u32 | (((dev >> 32) & !0xfff) as u32)
1389 }
1390
1391 fn dev_minor(dev: u64) -> u32 {
1392 (dev & 0xff) as u32 | (((dev >> 12) & !0xff) as u32)
1393 }
1394
1395 fn read_sysfs_hex_id(path: &str) -> Option<u32> {
1396 let content = std::fs::read_to_string(path).ok()?;
1397 let trimmed = content.trim().strip_prefix("0x").unwrap_or(content.trim());
1398 u32::from_str_radix(trimmed, 16).ok()
1399 }
1400
1401 let major = dev_major(dev);
1402 let minor = dev_minor(dev);
1403
1404 let vendor_path = format!("/sys/dev/char/{major}:{minor}/device/vendor");
1405 let device_path = format!("/sys/dev/char/{major}:{minor}/device/device");
1406
1407 let vendor_id = read_sysfs_hex_id(&vendor_path)?;
1408 let device_id = read_sysfs_hex_id(&device_path)?;
1409
1410 log::info!(
1411 "Compositor GPU hint: vendor={:#06x}, device={:#06x} (from dev {major}:{minor})",
1412 vendor_id,
1413 device_id,
1414 );
1415
1416 Some(gpui_wgpu::CompositorGpuHint {
1417 vendor_id,
1418 device_id,
1419 })
1420}
1421
1422#[cfg(any(feature = "wayland", feature = "x11"))]
1423async fn inhibit_idle_sleep(reason: String, executor: BackgroundExecutor) -> Result<ActivityGuard> {
1424 let proxy = InhibitProxy::new()
1425 .await
1426 .context("Idle sleep prevention portal is unavailable")?;
1427 let request = proxy
1428 .inhibit(
1429 None,
1430 BitFlags::from(InhibitFlags::Suspend),
1431 InhibitOptions::default().set_reason(reason.as_str()),
1432 )
1433 .await
1434 .context("Failed to request idle sleep prevention")?;
1435 request
1436 .response()
1437 .context("Idle sleep prevention request was rejected")?;
1438 Ok(release_on_drop(request, executor))
1439}
1440
1441#[cfg(any(feature = "wayland", feature = "x11"))]
1442fn release_on_drop(request: Request<()>, executor: BackgroundExecutor) -> ActivityGuard {
1443 ActivityGuard::new(move || {
1444 executor
1445 .spawn(async move {
1446 request
1447 .close()
1448 .await
1449 .context("Failed to release idle sleep prevention")
1450 .log_err();
1451 })
1452 .detach();
1453 })
1454}
1455
1456#[cfg(any(feature = "wayland", feature = "x11"))]
1457async fn await_idle_sleep_prevention(
1458 guard_rx: oneshot::Receiver<Result<ActivityGuard>>,
1459 executor: &BackgroundExecutor,
1460) -> Result<ActivityGuard> {
1461 match futures::future::select(guard_rx, executor.timer(Duration::from_secs(10))).await {
1462 futures::future::Either::Left((Ok(result), _)) => result,
1463 futures::future::Either::Left((Err(_), _)) => {
1464 Err(anyhow!("Idle sleep prevention request was abandoned"))
1465 }
1466 futures::future::Either::Right(_) => Err(anyhow!(
1467 "Idle sleep prevention acquisition timed out after 10 seconds"
1468 )),
1469 }
1470}
1471
1472#[cfg(test)]
1473mod tests {
1474 use super::*;
1475 use gpui_platform::{Point, px};
1476
1477 #[cfg(any(feature = "wayland", feature = "x11"))]
1478 #[test]
1479 fn rejects_null_xkb_context() {
1480 let context = unsafe {
1481 xkb::Context::from_raw_ptr(std::ptr::null_mut())
1483 };
1484 let error = validate_xkb_context(context)
1485 .err()
1486 .expect("null XKB context should be rejected");
1487
1488 assert_eq!(
1489 error.to_string(),
1490 "libxkbcommon failed to create an XKB context"
1491 );
1492 }
1493
1494 #[test]
1495 fn test_is_within_click_distance() {
1496 let zero = Point::new(px(0.0), px(0.0));
1497 assert!(is_within_click_distance(zero, Point::new(px(5.0), px(5.0))));
1498 assert!(is_within_click_distance(
1499 zero,
1500 Point::new(px(-4.9), px(5.0))
1501 ));
1502 assert!(is_within_click_distance(
1503 Point::new(px(3.0), px(2.0)),
1504 Point::new(px(-2.0), px(-2.0))
1505 ));
1506 assert!(!is_within_click_distance(
1507 zero,
1508 Point::new(px(5.0), px(5.1))
1509 ),);
1510 }
1511
1512 #[cfg(any(feature = "wayland", feature = "x11"))]
1513 mod idle_sleep_prevention {
1514 use super::super::await_idle_sleep_prevention;
1515 use anyhow::{Result, anyhow};
1516 use futures::channel::oneshot;
1517 use gpui::{ActivityGuard, TestAppContext};
1518 use std::{
1519 sync::{
1520 Arc,
1521 atomic::{AtomicUsize, Ordering::SeqCst},
1522 },
1523 time::Duration,
1524 };
1525
1526 #[gpui::test]
1527 async fn guard_passes_through_and_releases_on_drop(cx: &mut TestAppContext) {
1528 let released = Arc::new(AtomicUsize::new(0));
1529 let (guard_tx, guard_rx) = oneshot::channel();
1530 assert!(guard_tx.send(Ok(release_guard(released.clone()))).is_ok());
1531 let guard = await_idle_sleep_prevention(guard_rx, &cx.background_executor)
1532 .await
1533 .expect("acquisition should succeed");
1534
1535 assert_eq!(released.load(SeqCst), 0);
1536 drop(guard);
1537 assert_eq!(released.load(SeqCst), 1);
1538 }
1539
1540 #[gpui::test]
1541 async fn acquisition_error_passes_through(cx: &mut TestAppContext) {
1542 let (guard_tx, guard_rx) = oneshot::channel::<Result<ActivityGuard>>();
1543 assert!(guard_tx.send(Err(anyhow!("inhibition rejected"))).is_ok());
1544 assert_eq!(
1545 await_idle_sleep_prevention(guard_rx, &cx.background_executor)
1546 .await
1547 .err()
1548 .expect("acquisition should fail")
1549 .to_string(),
1550 "inhibition rejected"
1551 );
1552 }
1553
1554 #[gpui::test]
1555 async fn abandoned_acquisition_is_an_error(cx: &mut TestAppContext) {
1556 let (guard_tx, guard_rx) = oneshot::channel::<Result<ActivityGuard>>();
1557 drop(guard_tx);
1558 assert_eq!(
1559 await_idle_sleep_prevention(guard_rx, &cx.background_executor)
1560 .await
1561 .err()
1562 .expect("acquisition should fail")
1563 .to_string(),
1564 "Idle sleep prevention request was abandoned"
1565 );
1566 }
1567
1568 #[gpui::test]
1569 async fn late_guard_after_timeout_is_released(cx: &mut TestAppContext) {
1570 let released = Arc::new(AtomicUsize::new(0));
1571 let (guard_tx, guard_rx) = oneshot::channel();
1572 let task = cx.background_executor.spawn({
1573 let executor = cx.background_executor.clone();
1574 async move { await_idle_sleep_prevention(guard_rx, &executor).await }
1575 });
1576 cx.run_until_parked();
1577 cx.executor().advance_clock(Duration::from_secs(9));
1578 cx.run_until_parked();
1579 assert!(!guard_tx.is_canceled());
1580 cx.executor().advance_clock(Duration::from_secs(1));
1581 assert_eq!(
1582 task.await
1583 .err()
1584 .expect("acquisition should time out")
1585 .to_string(),
1586 "Idle sleep prevention acquisition timed out after 10 seconds"
1587 );
1588
1589 assert!(guard_tx.is_canceled());
1590 if let Err(Ok(guard)) = guard_tx.send(Ok(release_guard(released.clone()))) {
1591 assert_eq!(released.load(SeqCst), 0);
1592 drop(guard);
1593 }
1594 assert_eq!(released.load(SeqCst), 1);
1595 }
1596
1597 fn release_guard(released: Arc<AtomicUsize>) -> ActivityGuard {
1598 ActivityGuard::new(move || {
1599 released.fetch_add(1, SeqCst);
1600 })
1601 }
1602 }
1603
1604 #[cfg(any(feature = "wayland", feature = "x11"))]
1605 mod read_fd_with_timeout {
1606 use super::super::{PIPE_READ_TIMEOUT, read_fd_with_timeout};
1607 use std::io::Write as _;
1608 use std::time::{Duration, Instant};
1609
1610 #[test]
1611 fn reads_data_written_before_close() {
1612 let mut pipe = filedescriptor::Pipe::new().unwrap();
1613 pipe.write.write_all(b"hello clipboard").unwrap();
1614 drop(pipe.write);
1615
1616 let bytes = read_fd_with_timeout(pipe.read, PIPE_READ_TIMEOUT).unwrap();
1617 assert_eq!(bytes, b"hello clipboard");
1618 }
1619
1620 #[test]
1621 fn returns_empty_when_writer_closes_without_writing() {
1622 let pipe = filedescriptor::Pipe::new().unwrap();
1623 drop(pipe.write);
1624
1625 let bytes = read_fd_with_timeout(pipe.read, PIPE_READ_TIMEOUT).unwrap();
1626 assert!(bytes.is_empty());
1627 }
1628
1629 #[test]
1630 fn times_out_when_writer_never_writes() {
1631 let pipe = filedescriptor::Pipe::new().unwrap();
1632 let _open_writer = pipe.write;
1633
1634 let timeout = Duration::from_millis(50);
1635 let started = Instant::now();
1636 let result = read_fd_with_timeout(pipe.read, timeout);
1637 let elapsed = started.elapsed();
1638
1639 let err = result.unwrap_err();
1640 assert!(
1641 err.to_string().contains("timed out"),
1642 "unexpected error: {err}"
1643 );
1644 assert!(elapsed >= timeout, "returned before the timeout elapsed");
1645 }
1646
1647 #[test]
1648 fn times_out_when_writer_stalls_after_partial_write() {
1649 let mut pipe = filedescriptor::Pipe::new().unwrap();
1650 pipe.write.write_all(b"partial").unwrap();
1651 let _open_writer = pipe.write;
1652
1653 let err = read_fd_with_timeout(pipe.read, Duration::from_millis(50)).unwrap_err();
1654 assert!(
1655 err.to_string().contains("timed out"),
1656 "unexpected error: {err}"
1657 );
1658 }
1659
1660 #[test]
1661 fn slow_writer_resets_deadline_between_chunks() {
1662 let pipe = filedescriptor::Pipe::new().unwrap();
1663 let chunks = 12;
1664 let gap = Duration::from_millis(40);
1665 let timeout = Duration::from_millis(400);
1666
1667 let writer = std::thread::spawn({
1668 let mut write = pipe.write;
1669 move || {
1670 for _ in 0..chunks {
1671 std::thread::sleep(gap);
1672 write.write_all(&[b'x'; 1000]).unwrap();
1673 }
1674 }
1675 });
1676 let bytes = read_fd_with_timeout(pipe.read, timeout).unwrap();
1679 writer.join().unwrap();
1680 assert_eq!(bytes, vec![b'x'; 1000 * chunks]);
1681 }
1682
1683 #[test]
1684 fn reads_payload_larger_than_pipe_capacity() {
1685 let pipe = filedescriptor::Pipe::new().unwrap();
1686 let payload = vec![b'z'; 1024 * 1024];
1688
1689 let writer = std::thread::spawn({
1690 let mut write = pipe.write;
1691 let payload = payload.clone();
1692 move || write.write_all(&payload).unwrap()
1693 });
1694 let bytes = read_fd_with_timeout(pipe.read, PIPE_READ_TIMEOUT).unwrap();
1695 writer.join().unwrap();
1696 assert_eq!(bytes, payload);
1697 }
1698 }
1699}