1use std::{
2 cell::RefCell,
3 env,
4 path::{Path, PathBuf},
5 rc::Rc,
6 sync::Arc,
7};
8#[cfg(any(feature = "wayland", feature = "x11"))]
9use std::{
10 ffi::OsString,
11 fs::File,
12 io::Read as _,
13 os::fd::{AsFd, AsRawFd},
14 time::Duration,
15};
16
17#[cfg(any(feature = "wayland", feature = "x11"))]
18use anyhow::ensure;
19use anyhow::{Context as _, Result, anyhow};
20#[cfg(any(feature = "wayland", feature = "x11"))]
21use ashpd::{
22 desktop::{
23 Request,
24 inhibit::{InhibitFlags, InhibitOptions, InhibitProxy},
25 },
26 enumflags2::BitFlags,
27};
28use calloop::{EventLoop, LoopHandle, LoopSignal, channel::Sender};
29use futures::channel::oneshot;
30use gpui_util::{ResultExt as _, new_std_command};
31#[cfg(any(feature = "wayland", feature = "x11"))]
32use xkbcommon::xkb::{self, Keycode, Keysym, State};
33
34use crate::linux::{
35 Backend, DisplayConnection, HeadlessConnection, LinuxDispatcher, PriorityQueueCalloopReceiver,
36 select_backend,
37};
38use gpui_engine::{DmaBufCapabilities, PlatformTextSystem};
39use gpui_platform::{
40 ActivityGuard, BackgroundExecutor, ClipboardItem, CursorStyle, ForegroundExecutor,
41 GpuAdapterInfo, GpuId, GpuPreference, GraphicalEnvironment, MenuCommandId, PathPromptOptions,
42 Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformMenu,
43 PlatformMenuItem, PlatformWindow, RunnableVariant, Task, ThermalState, WindowAppearance,
44 WindowButtonLayout, WindowId, WindowParams, WindowingModes, WindowingRequest,
45};
46#[cfg(any(feature = "wayland", feature = "x11"))]
47use gpui_types::{Pixels, Point, px};
48
49#[cfg(any(feature = "wayland", feature = "x11"))]
50pub(crate) const SCROLL_LINES: f32 = 3.0;
51
52#[cfg(any(feature = "wayland", feature = "x11"))]
55pub(crate) const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(400);
56#[cfg(any(feature = "wayland", feature = "x11"))]
57pub(crate) const DOUBLE_CLICK_DISTANCE: Pixels = px(5.0);
58pub(crate) const KEYRING_LABEL: &str = "zed-github-account";
59
60#[cfg(any(feature = "wayland", feature = "x11"))]
61const FILE_PICKER_PORTAL_MISSING: &str =
62 "Couldn't open file picker due to missing xdg-desktop-portal implementation.";
63
64#[derive(Default)]
65pub(crate) struct PlatformHandlers {
66 pub(crate) open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
67 pub(crate) quit: Option<Box<dyn FnMut() -> bool>>,
68 pub(crate) reopen: Option<Box<dyn FnMut()>>,
69 pub(crate) app_menu_action: Option<Box<dyn FnMut(MenuCommandId)>>,
70 pub(crate) will_open_app_menu: Option<Box<dyn FnMut()>>,
71 pub(crate) validate_app_menu_command: Option<Box<dyn FnMut(MenuCommandId) -> bool>>,
72 pub(crate) keyboard_layout_change: Option<Box<dyn FnMut()>>,
73 pub(crate) system_sleep: Option<Box<dyn FnMut()>>,
74 pub(crate) system_wake: Option<Box<dyn FnMut()>>,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80#[cfg_attr(
81 not(all(target_os = "linux", any(feature = "wayland", feature = "x11"))),
82 allow(dead_code)
83)]
84pub(crate) enum SystemPowerEvent {
85 Sleep,
86 Wake,
87}
88
89pub fn gpu_preference_from_env() -> Option<GpuPreference> {
97 match std::env::var("ZED_DEVICE_ID") {
98 Ok(value) => match GpuId::parse_pci_id(&value) {
99 Some(device_id) => Some(GpuPreference::Device(GpuId::by_device(device_id))),
100 None => {
101 log::error!(
102 "ZED_DEVICE_ID: expected a four-digit hexadecimal PCI id, got {value:?}"
103 );
104 None
105 }
106 },
107 Err(std::env::VarError::NotPresent) => None,
108 Err(error) => {
109 log::error!("ZED_DEVICE_ID: could not be read: {error}");
110 None
111 }
112 }
113}
114
115pub(crate) struct LinuxCommon {
116 pub(crate) background_executor: BackgroundExecutor,
117 pub(crate) foreground_executor: ForegroundExecutor,
118 pub(crate) text_system: Arc<dyn PlatformTextSystem>,
119 pub(crate) appearance: WindowAppearance,
120 pub(crate) auto_hide_scrollbars: bool,
121 pub(crate) button_layout: WindowButtonLayout,
122 pub(crate) gpu_preference: GpuPreference,
125 pub(crate) callbacks: PlatformHandlers,
126 pub(crate) signal: LoopSignal,
127 app_name: Option<String>,
128 system_notifications: crate::linux::system_notifications::SystemNotificationState,
129 #[cfg_attr(
130 not(all(target_os = "linux", any(feature = "wayland", feature = "x11"))),
131 allow(dead_code)
132 )]
133 power_sender: Sender<SystemPowerEvent>,
134 power_listener_started: bool,
135 pub(crate) after_runnable: Option<Rc<dyn Fn()>>,
140}
141
142impl LinuxCommon {
143 pub fn new(
144 signal: LoopSignal,
145 ) -> (
146 Self,
147 PriorityQueueCalloopReceiver<RunnableVariant>,
148 calloop::channel::Channel<SystemPowerEvent>,
149 ) {
150 let (main_sender, main_receiver) = PriorityQueueCalloopReceiver::new();
151 let (power_sender, power_receiver) = calloop::channel::channel();
152
153 #[cfg(any(feature = "wayland", feature = "x11"))]
154 let text_system = Arc::new(crate::linux::CosmicTextSystem::new("IBM Plex Sans"));
155 #[cfg(not(any(feature = "wayland", feature = "x11")))]
156 let text_system = Arc::new(gpui_engine::NoopTextSystem::new());
157
158 let callbacks = PlatformHandlers::default();
159
160 let dispatcher = Arc::new(LinuxDispatcher::new(main_sender));
161
162 let background_executor = BackgroundExecutor::new(dispatcher.clone());
163
164 let common = LinuxCommon {
165 background_executor,
166 foreground_executor: ForegroundExecutor::new(dispatcher),
167 text_system,
168 appearance: WindowAppearance::Light,
169 auto_hide_scrollbars: false,
170 button_layout: WindowButtonLayout::linux_default(),
171 gpu_preference: gpu_preference_from_env().unwrap_or_default(),
172 callbacks,
173 signal,
174 app_name: None,
175 system_notifications: crate::linux::system_notifications::SystemNotificationState::new(
176 ),
177 power_sender,
178 power_listener_started: false,
179 after_runnable: None,
180 };
181
182 (common, main_receiver, power_receiver)
183 }
184
185 pub(crate) fn start_power_listener(&mut self) {
186 if !self.power_listener_started {
187 #[cfg(all(target_os = "linux", any(feature = "wayland", feature = "x11")))]
188 smol::spawn({
189 let power_sender = self.power_sender.clone();
190 async move {
191 if let Err(error) = listen_for_system_power_events(power_sender).await {
192 log::debug!("failed to listen for system sleep/wake events: {error:?}");
193 }
194 }
195 })
196 .detach();
197
198 self.power_listener_started = true;
199 }
200 }
201
202 pub(crate) fn register_sources(
206 common: &Rc<RefCell<Self>>,
207 loop_handle: &calloop::LoopHandle<'static, ()>,
208 main_receiver: PriorityQueueCalloopReceiver<RunnableVariant>,
209 power_receiver: calloop::channel::Channel<SystemPowerEvent>,
210 ) -> anyhow::Result<()> {
211 loop_handle
212 .insert_source(main_receiver, {
213 let loop_handle = loop_handle.clone();
214 let common = common.clone();
215 move |event, _, _| {
216 if let calloop::channel::Event::Msg(runnable) = event {
217 let common = common.clone();
218 loop_handle.insert_idle(move |_| {
219 let location = runnable.metadata().location;
220 let spawned = runnable.metadata().spawned;
221 gpui_platform::profiler::update_running_task(spawned, location);
222 runnable.run();
223 gpui_platform::profiler::save_task_timing();
224
225 let after_runnable = common.borrow().after_runnable.clone();
226 if let Some(after_runnable) = after_runnable {
227 after_runnable();
228 }
229 });
230 }
231 }
232 })
233 .map_err(|error| anyhow!("failed to register foreground executor: {error}"))?;
234 loop_handle
235 .insert_source(power_receiver, {
236 let common = common.clone();
237 move |event, _, _| {
238 if let calloop::channel::Event::Msg(event) = event {
239 LinuxCommon::handle_system_power_event(&common, event);
240 }
241 }
242 })
243 .map_err(|error| anyhow!("failed to register power listener: {error}"))?;
244 Ok(())
245 }
246
247 fn handle_system_power_event(common: &RefCell<Self>, event: SystemPowerEvent) {
250 fn slot(
251 callbacks: &mut PlatformHandlers,
252 event: SystemPowerEvent,
253 ) -> &mut Option<Box<dyn FnMut()>> {
254 match event {
255 SystemPowerEvent::Sleep => &mut callbacks.system_sleep,
256 SystemPowerEvent::Wake => &mut callbacks.system_wake,
257 }
258 }
259 let callback = slot(&mut common.borrow_mut().callbacks, event).take();
260 if let Some(mut callback) = callback {
261 callback();
262 let mut common = common.borrow_mut();
263 let slot = slot(&mut common.callbacks, event);
264 if slot.is_none() {
266 *slot = Some(callback);
267 }
268 }
269 }
270}
271
272#[cfg(all(target_os = "linux", any(feature = "wayland", feature = "x11")))]
273async fn listen_for_system_power_events(
274 power_sender: Sender<SystemPowerEvent>,
275) -> anyhow::Result<()> {
276 use futures::StreamExt as _;
277
278 let connection = ashpd::zbus::Connection::system().await?;
279 let proxy = ashpd::zbus::Proxy::new(
280 &connection,
281 "org.freedesktop.login1",
282 "/org/freedesktop/login1",
283 "org.freedesktop.login1.Manager",
284 )
285 .await?;
286 let mut sleep_events = proxy.receive_signal("PrepareForSleep").await?;
287
288 while let Some(message) = sleep_events.next().await {
289 let event = if message.body().deserialize::<bool>()? {
290 SystemPowerEvent::Sleep
291 } else {
292 SystemPowerEvent::Wake
293 };
294 power_sender.send(event).ok();
295 }
296
297 Ok(())
298}
299
300pub(crate) struct LinuxPlatform {
306 common: Rc<RefCell<LinuxCommon>>,
307 event_loop: RefCell<Option<EventLoop<'static, ()>>>,
309 loop_handle: LoopHandle<'static, ()>,
310 connection: RefCell<DisplayConnection>,
311 allowed_modes: WindowingModes,
312 initial_windowing: RefCell<Option<WindowingRequest>>,
315 startup_environment: RefCell<Option<StartupEnvironment>>,
317 pending_mode: RefCell<Option<WindowingRequest>>,
319 transition_waiter: RefCell<Option<oneshot::Sender<anyhow::Result<()>>>>,
320}
321
322#[derive(Default)]
327struct StartupEnvironment {
328 #[cfg(feature = "wayland")]
329 activation_token: Option<String>,
330 #[cfg(feature = "wayland")]
333 wayland_socket: Option<std::os::unix::net::UnixStream>,
334}
335
336impl LinuxPlatform {
337 pub(crate) fn new(allowed_modes: WindowingModes) -> Self {
339 let startup = StartupEnvironment {
340 #[cfg(feature = "wayland")]
341 activation_token: crate::linux::take_startup_activation_token_from_environment(),
342 #[cfg(feature = "wayland")]
343 wayland_socket: crate::linux::take_wayland_socket_from_environment(),
344 };
345 let event_loop = EventLoop::try_new().expect("failed to create Linux event loop");
346 let (common, main_receiver, power_receiver) = LinuxCommon::new(event_loop.get_signal());
347 let common = Rc::new(RefCell::new(common));
348 let loop_handle = event_loop.handle();
349 LinuxCommon::register_sources(&common, &loop_handle, main_receiver, power_receiver)
350 .expect("failed to register Linux event sources");
351 Self {
352 common,
353 event_loop: RefCell::new(Some(event_loop)),
354 loop_handle,
355 connection: RefCell::new(DisplayConnection::Headless(HeadlessConnection::new())),
356 allowed_modes,
357 initial_windowing: RefCell::new(None),
358 startup_environment: RefCell::new(Some(startup)),
359 pending_mode: RefCell::new(None),
360 transition_waiter: RefCell::new(None),
361 }
362 }
363
364 fn connect_initially(&self) {
372 let request = self
373 .initial_windowing
374 .borrow_mut()
375 .take()
376 .unwrap_or_else(|| WindowingRequest::Windowed(GraphicalEnvironment::detect()));
377 let startup = self
379 .startup_environment
380 .borrow_mut()
381 .take()
382 .unwrap_or_default();
383 let backend = match &request {
384 WindowingRequest::Headless => None,
385 WindowingRequest::Windowed(environment) => {
386 select_backend(self.allowed_modes, environment)
387 .map(|backend| (backend, environment))
388 }
389 };
390 match backend {
391 Some((backend, environment)) => {
392 let connection = self
393 .connect(backend, environment, startup)
394 .unwrap_or_else(|error| panic!("{error:#}"));
395 self.replace_connection(connection);
396 }
397 None => assert!(
398 self.allowed_modes.contains(WindowingModes::HEADLESS),
399 "{:?} does not allow starting headless, and no allowed display server was given",
400 self.allowed_modes
401 ),
402 }
403 }
404
405 fn with_common<R>(&self, function: impl FnOnce(&mut LinuxCommon) -> R) -> R {
406 function(&mut self.common.borrow_mut())
407 }
408
409 #[cfg_attr(
413 not(feature = "wayland"),
414 allow(
415 unused_variables,
416 reason = "only Wayland uses the startup environment, and without a display backend \
417 `Backend` has no variants"
418 )
419 )]
420 fn connect(
421 &self,
422 backend: Backend,
423 environment: &GraphicalEnvironment,
424 startup: StartupEnvironment,
425 ) -> anyhow::Result<DisplayConnection> {
426 let loop_handle = self.loop_handle.clone();
427 let common = self.common.clone();
428 match backend {
429 #[cfg(feature = "wayland")]
430 Backend::Wayland => {
431 let activation_token = environment
432 .activation_token
433 .clone()
434 .or(startup.activation_token);
435 Ok(DisplayConnection::Wayland(
436 crate::linux::WaylandConnection::attach(
437 loop_handle,
438 common,
439 environment,
440 startup.wayland_socket,
441 activation_token,
442 )?,
443 ))
444 }
445 #[cfg(feature = "x11")]
446 Backend::X11 => Ok(DisplayConnection::X11(crate::linux::X11Connection::attach(
447 loop_handle,
448 common,
449 environment,
450 )?)),
451 }
452 }
453
454 fn replace_connection(&self, connection: DisplayConnection) {
457 let previous = std::mem::replace(&mut *self.connection.borrow_mut(), connection);
458 drop(previous);
459 let callback = self
461 .common
462 .borrow_mut()
463 .callbacks
464 .keyboard_layout_change
465 .take();
466 if let Some(mut callback) = callback {
467 callback();
468 let mut common = self.common.borrow_mut();
469 if common.callbacks.keyboard_layout_change.is_none() {
471 common.callbacks.keyboard_layout_change = Some(callback);
472 }
473 }
474 }
475
476 fn apply_pending_mode(&self) {
481 let Some(mode) = self.pending_mode.borrow().clone() else {
482 return;
483 };
484 let result = if self.connection.borrow().has_windows() {
486 Err(anyhow!("a window was opened while switching display modes"))
487 } else {
488 match mode {
489 WindowingRequest::Headless => {
490 self.replace_connection(DisplayConnection::Headless(HeadlessConnection::new()));
491 Ok(())
492 }
493 WindowingRequest::Windowed(environment) => {
494 select_backend(self.allowed_modes, &environment)
495 .context("the environment names no allowed Wayland or X11 display server")
496 .and_then(|backend| {
497 self.connect(backend, &environment, StartupEnvironment::default())
498 })
499 .map(|connection| self.replace_connection(connection))
500 }
501 }
502 };
503 self.pending_mode.borrow_mut().take();
504 if let Some(waiter) = self.transition_waiter.borrow_mut().take() {
505 waiter.send(result).ok();
506 }
507 }
508
509 fn request_mode(&self, mode: WindowingRequest) -> Task<anyhow::Result<()>> {
511 if self.pending_mode.borrow().is_some() {
512 return Task::ready(Err(anyhow!(
513 "a display mode transition is already in progress"
514 )));
515 }
516 let is_headless = self.connection.borrow().is_headless();
517 match &mode {
518 WindowingRequest::Headless
519 if !self.allowed_modes.contains(WindowingModes::HEADLESS) =>
520 {
521 return Task::ready(Err(anyhow!(
522 "{:?} does not allow headless mode",
523 self.allowed_modes
524 )));
525 }
526 WindowingRequest::Headless if is_headless => {
527 return Task::ready(Err(anyhow!("already headless")));
528 }
529 WindowingRequest::Headless => {}
530 WindowingRequest::Windowed(_) if !is_headless => {
531 return Task::ready(Err(anyhow!(
532 "already windowed ({}); switch to headless mode first",
533 self.connection.borrow().compositor_name()
534 )));
535 }
536 WindowingRequest::Windowed(environment) => {
537 if select_backend(self.allowed_modes, environment).is_none() {
538 return Task::ready(Err(anyhow!(
539 "the environment names no display server that {:?} allows",
540 self.allowed_modes
541 )));
542 }
543 }
544 }
545 let (sender, receiver) = oneshot::channel();
546 self.transition_waiter.borrow_mut().replace(sender);
547 *self.pending_mode.borrow_mut() = Some(mode);
548 self.common.borrow().signal.wakeup();
551 self.common.borrow().foreground_executor.spawn(async move {
552 receiver
553 .await
554 .map_err(|_| anyhow!("display mode transition was canceled"))
555 .and_then(|result| result)
556 })
557 }
558}
559
560impl Drop for LinuxPlatform {
561 fn drop(&mut self) {
562 self.replace_connection(DisplayConnection::Headless(HeadlessConnection::new()));
563 #[cfg(feature = "x11")]
564 crate::linux::wait_for_clipboard_handovers();
565 }
566}
567
568#[cfg(any(feature = "wayland", feature = "x11"))]
575pub(crate) fn enumerate_gpus(gpu_context: &gpui_wgpu::WgpuContextSlot) -> Vec<GpuAdapterInfo> {
576 if let Ok(context) = gpu_context.try_borrow()
577 && let Some(context) = context.as_ref()
578 {
579 return gpui_wgpu::enumerate_adapters_from(&context.instance);
580 }
581 gpui_wgpu::enumerate_adapters()
582}
583
584#[cfg(any(feature = "wayland", feature = "x11"))]
586pub(crate) fn active_gpu(gpu_context: &gpui_wgpu::WgpuContextSlot) -> Option<GpuAdapterInfo> {
587 let context = gpu_context.try_borrow().ok()?;
588 context.as_ref().map(|context| context.adapter_info())
589}
590
591#[cfg(any(feature = "wayland", feature = "x11"))]
593pub(crate) fn dmabuf_capabilities(
594 gpu_context: &gpui_wgpu::WgpuContextSlot,
595) -> Option<DmaBufCapabilities> {
596 let context = gpu_context.try_borrow().ok()?;
597 context
598 .as_ref()
599 .and_then(|context| context.dmabuf_capabilities())
600}
601
602impl Platform for LinuxPlatform {
603 fn background_executor(&self) -> BackgroundExecutor {
604 self.with_common(|common| common.background_executor.clone())
605 }
606
607 fn foreground_executor(&self) -> ForegroundExecutor {
608 self.with_common(|common| common.foreground_executor.clone())
609 }
610
611 fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
612 self.with_common(|common| common.text_system.clone())
613 }
614
615 fn set_gpu_preference(&self, preference: GpuPreference) {
616 self.common.borrow_mut().gpu_preference = preference;
617 }
618
619 fn enumerate_gpus(&self) -> Vec<GpuAdapterInfo> {
620 self.connection.borrow().enumerate_gpus()
621 }
622
623 fn active_gpu(&self) -> Option<GpuAdapterInfo> {
624 self.connection.borrow().active_gpu()
625 }
626
627 fn dmabuf_capabilities(&self) -> Option<DmaBufCapabilities> {
628 self.connection.borrow().dmabuf_capabilities()
629 }
630
631 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
632 self.connection.borrow().keyboard_layout()
633 }
634
635 fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
636 Rc::new(gpui_platform::DummyKeyboardMapper)
637 }
638
639 fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
640 self.with_common(|common| common.callbacks.keyboard_layout_change = Some(callback));
641 }
642
643 fn on_thermal_state_change(&self, _callback: Box<dyn FnMut()>) {}
644
645 fn thermal_state(&self) -> ThermalState {
646 ThermalState::Nominal
647 }
648
649 #[cfg(not(any(feature = "wayland", feature = "x11")))]
650 fn prevent_idle_sleep(&self, reason: &str) -> Task<Result<ActivityGuard>> {
651 Task::ready(Err(anyhow!(
652 "Idle sleep prevention for {reason:?} requires a Linux windowing backend"
653 )))
654 }
655
656 #[cfg(any(feature = "wayland", feature = "x11"))]
657 fn prevent_idle_sleep(&self, reason: &str) -> Task<Result<ActivityGuard>> {
658 let executor = self.background_executor();
659 let (guard_tx, guard_rx) = oneshot::channel();
660 executor
661 .spawn({
662 let executor = executor.clone();
663 let reason = reason.to_owned();
664 async move {
665 guard_tx
666 .send(inhibit_idle_sleep(reason, executor).await)
667 .ok();
668 }
669 })
670 .detach();
671 executor
672 .clone()
673 .spawn(async move { await_idle_sleep_prevention(guard_rx, &executor).await })
674 }
675
676 fn run(&self, on_finish_launching: Box<dyn FnOnce()>) {
677 self.connect_initially();
678 on_finish_launching();
679
680 let mut event_loop = self
681 .event_loop
682 .borrow_mut()
683 .take()
684 .expect("App is already running");
685 event_loop
686 .run(None, &mut (), |_| self.apply_pending_mode())
687 .log_err();
688
689 let quit = self.with_common(|common| common.callbacks.quit.take());
690 if let Some(mut fun) = quit {
691 fun();
692 }
693 #[cfg(feature = "x11")]
694 crate::linux::wait_for_clipboard_handovers();
695 }
696
697 fn quit(&self) {
698 self.with_common(|common| common.signal.stop());
699 }
700
701 fn set_initial_windowing(&self, request: WindowingRequest) {
702 *self.initial_windowing.borrow_mut() = Some(request);
703 }
704
705 fn request_windowing(&self, request: WindowingRequest) -> Task<anyhow::Result<()>> {
706 self.request_mode(request)
707 }
708
709 fn compositor_name(&self) -> &'static str {
710 self.connection.borrow().compositor_name()
711 }
712
713 fn graphical_environment(&self) -> Option<GraphicalEnvironment> {
714 self.connection.borrow().graphical_environment()
715 }
716
717 fn restart(&self, binary_path: Option<PathBuf>, arguments: Vec<std::ffi::OsString>) {
718 use std::os::unix::process::CommandExt as _;
719
720 let app_pid = std::process::id().to_string();
722 let app_path = if let Some(path) = binary_path {
724 path
725 } else {
726 match self.app_path() {
727 Ok(path) => path,
728 Err(err) => {
729 log::error!("Failed to get app path: {:?}", err);
730 return;
731 }
732 }
733 };
734
735 log::info!("Restarting process, using app path: {:?}", app_path);
736
737 let script = r#"
740 while kill -0 "$0" 2>/dev/null; do
741 sleep 0.1
742 done
743
744 app_path="$1"
745 shift
746 "$app_path" "$@"
747 "#;
748
749 #[allow(
750 clippy::disallowed_methods,
751 reason = "We are restarting ourselves, using std command thus is fine"
752 )]
753 let restart_process = new_std_command("/usr/bin/env")
754 .arg("bash")
755 .arg("-c")
756 .arg(script)
757 .arg(&app_pid)
758 .arg(&app_path)
759 .args(arguments)
760 .process_group(0)
761 .spawn();
762
763 match restart_process {
764 Ok(_) => self.quit(),
765 Err(e) => log::error!("failed to spawn restart script: {:?}", e),
766 }
767 }
768
769 fn activate(&self, _ignoring_other_apps: bool) {
770 log::info!("activate is not implemented on Linux, ignoring the call")
771 }
772
773 fn hide(&self) {
774 log::info!("hide is not implemented on Linux, ignoring the call")
775 }
776
777 fn hide_other_apps(&self) {
778 log::info!("hide_other_apps is not implemented on Linux, ignoring the call")
779 }
780
781 fn unhide_other_apps(&self) {
782 log::info!("unhide_other_apps is not implemented on Linux, ignoring the call")
783 }
784
785 fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
786 self.connection.borrow().primary_display()
787 }
788
789 fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
790 self.connection.borrow().displays()
791 }
792
793 #[cfg(feature = "screen-capture")]
794 fn is_screen_capture_supported(&self) -> bool {
795 self.connection.borrow().is_screen_capture_supported()
796 }
797
798 #[cfg(feature = "screen-capture")]
799 fn screen_capture_sources(
800 &self,
801 ) -> oneshot::Receiver<Result<Vec<Rc<dyn gpui_platform::ScreenCaptureSource>>>> {
802 self.connection.borrow().screen_capture_sources()
803 }
804
805 fn active_window(&self) -> Option<WindowId> {
806 self.connection.borrow().active_window()
807 }
808
809 fn window_stack(&self) -> Option<Vec<WindowId>> {
810 self.connection.borrow().window_stack()
811 }
812
813 fn open_window(
814 &self,
815 handle: WindowId,
816 options: WindowParams,
817 ) -> anyhow::Result<Box<dyn PlatformWindow>> {
818 self.connection.borrow().open_window(handle, options)
819 }
820
821 fn open_url(&self, url: &str) {
822 self.connection.borrow().open_uri(url);
823 }
824
825 fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
826 self.with_common(|common| common.callbacks.open_urls = Some(callback));
827 }
828
829 fn prompt_for_paths(
830 &self,
831 options: PathPromptOptions,
832 ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
833 let (done_tx, done_rx) = oneshot::channel();
834
835 #[cfg(not(any(feature = "wayland", feature = "x11")))]
836 let _ = (done_tx.send(Ok(None)), options);
837
838 #[cfg(any(feature = "wayland", feature = "x11"))]
839 let identifier = self.connection.borrow().window_identifier();
840
841 #[cfg(any(feature = "wayland", feature = "x11"))]
842 self.foreground_executor()
843 .spawn(async move {
844 let title = if options.directories {
845 "Open Folder"
846 } else {
847 "Open File"
848 };
849
850 let request = match ashpd::desktop::file_chooser::OpenFileRequest::default()
851 .identifier(identifier.await)
852 .modal(true)
853 .title(title)
854 .accept_label(
855 options
856 .prompt
857 .as_ref()
858 .map(gpui_platform::SharedString::as_str),
859 )
860 .multiple(options.multiple)
861 .directory(options.directories)
862 .send()
863 .await
864 {
865 Ok(request) => request,
866 Err(err) => {
867 let result = match err {
868 ashpd::Error::PortalNotFound(_) => anyhow!(FILE_PICKER_PORTAL_MISSING),
869 err => err.into(),
870 };
871 let _ = done_tx.send(Err(result));
872 return;
873 }
874 };
875
876 let result = match request.response() {
877 Ok(response) => Ok(Some(
878 response
879 .uris()
880 .iter()
881 .filter_map(|uri: &ashpd::Uri| url::Url::parse(uri.as_str()).ok())
882 .filter_map(|uri: url::Url| uri.to_file_path().ok())
883 .collect::<Vec<_>>(),
884 )),
885 Err(ashpd::Error::Response(_)) => Ok(None),
886 Err(e) => Err(e.into()),
887 };
888 let _ = done_tx.send(result);
889 })
890 .detach();
891 done_rx
892 }
893
894 fn prompt_for_new_path(
895 &self,
896 directory: &Path,
897 suggested_name: Option<&str>,
898 ) -> oneshot::Receiver<Result<Option<PathBuf>>> {
899 let (done_tx, done_rx) = oneshot::channel();
900
901 #[cfg(not(any(feature = "wayland", feature = "x11")))]
902 let _ = (done_tx.send(Ok(None)), directory, suggested_name);
903
904 #[cfg(any(feature = "wayland", feature = "x11"))]
905 let identifier = self.connection.borrow().window_identifier();
906
907 #[cfg(any(feature = "wayland", feature = "x11"))]
908 self.foreground_executor()
909 .spawn({
910 let directory = directory.to_owned();
911 let suggested_name = suggested_name.map(|s| s.to_owned());
912
913 async move {
914 let mut request_builder =
915 ashpd::desktop::file_chooser::SaveFileRequest::default()
916 .identifier(identifier.await)
917 .modal(true)
918 .title("Save File")
919 .current_folder(directory)
920 .expect("pathbuf should not be nul terminated");
921
922 if let Some(suggested_name) = suggested_name {
923 request_builder = request_builder.current_name(suggested_name.as_str());
924 }
925
926 let request = match request_builder.send().await {
927 Ok(request) => request,
928 Err(err) => {
929 let result = match err {
930 ashpd::Error::PortalNotFound(_) => {
931 anyhow!(FILE_PICKER_PORTAL_MISSING)
932 }
933 err => err.into(),
934 };
935 let _ = done_tx.send(Err(result));
936 return;
937 }
938 };
939
940 let result = match request.response() {
941 Ok(response) => Ok(response
942 .uris()
943 .first()
944 .and_then(|uri: &ashpd::Uri| url::Url::parse(uri.as_str()).ok())
945 .and_then(|uri: url::Url| uri.to_file_path().ok())),
946 Err(ashpd::Error::Response(_)) => Ok(None),
947 Err(e) => Err(e.into()),
948 };
949 let _ = done_tx.send(result);
950 }
951 })
952 .detach();
953
954 done_rx
955 }
956
957 fn can_select_mixed_files_and_dirs(&self) -> bool {
958 false
960 }
961
962 fn reveal_path(&self, path: &Path) {
963 self.connection.borrow().reveal_path(path.to_owned());
964 }
965
966 fn open_with_system(&self, path: &Path) {
967 let path = path.to_owned();
968 let environment = self.connection.borrow().graphical_environment();
969 self.background_executor()
970 .spawn(async move {
971 let mut command = new_std_command("xdg-open");
972 command.arg(path);
973 if let Some(environment) = environment {
974 environment.apply_to(&mut command);
975 }
976 #[allow(
977 clippy::disallowed_methods,
978 reason = "running on a background thread, so blocking is fine"
979 )]
980 command.status().context("invoking xdg-open").log_err();
981 })
982 .detach();
983 }
984
985 fn on_quit(&self, callback: Box<dyn FnMut() -> bool>) {
986 self.with_common(|common| {
987 common.callbacks.quit = Some(callback);
988 });
989 }
990
991 fn on_reopen(&self, callback: Box<dyn FnMut()>) {
992 self.with_common(|common| {
993 common.callbacks.reopen = Some(callback);
994 });
995 }
996
997 fn on_system_sleep(&self, callback: Box<dyn FnMut()>) {
998 self.with_common(|common| {
999 common.callbacks.system_sleep = Some(callback);
1000 common.start_power_listener();
1001 });
1002 }
1003
1004 fn on_system_wake(&self, callback: Box<dyn FnMut()>) {
1005 self.with_common(|common| {
1006 common.callbacks.system_wake = Some(callback);
1007 common.start_power_listener();
1008 });
1009 }
1010
1011 fn set_app_identity(&self, _identifier: &str, name: &str) {
1012 self.with_common(|common| common.app_name = Some(name.to_string()));
1013 }
1014
1015 fn show_system_notification(&self, notification: gpui_platform::SystemNotification) {
1016 self.with_common(|common| {
1017 common
1018 .system_notifications
1019 .show(common.app_name.as_deref(), notification)
1020 });
1021 }
1022
1023 fn dismiss_system_notification(&self, tag: &str) {
1024 self.with_common(|common| common.system_notifications.dismiss(tag));
1025 }
1026
1027 fn on_system_notification_response(
1028 &self,
1029 callback: Box<dyn FnMut(gpui_platform::SystemNotificationResponse)>,
1030 ) {
1031 self.with_common(|common| {
1032 let executor = common.foreground_executor.clone();
1033 common.system_notifications.on_response(&executor, callback)
1034 });
1035 }
1036
1037 fn on_app_menu_action(&self, callback: Box<dyn FnMut(MenuCommandId)>) {
1038 self.with_common(|common| {
1039 common.callbacks.app_menu_action = Some(callback);
1040 });
1041 }
1042
1043 fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>) {
1044 self.with_common(|common| {
1045 common.callbacks.will_open_app_menu = Some(callback);
1046 });
1047 }
1048
1049 fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(MenuCommandId) -> bool>) {
1050 self.with_common(|common| {
1051 common.callbacks.validate_app_menu_command = Some(callback);
1052 });
1053 }
1054
1055 fn app_path(&self) -> Result<PathBuf> {
1056 let app_path = env::current_exe()?;
1058 Ok(app_path)
1059 }
1060
1061 fn set_menus(&self, _menus: Vec<PlatformMenu>) {
1062 }
1064
1065 fn set_dock_menu(&self, _menu: Vec<PlatformMenuItem>) {
1066 }
1068
1069 fn path_for_auxiliary_executable(&self, _name: &str) -> Result<PathBuf> {
1070 Err(anyhow::Error::msg(
1071 "Platform<LinuxPlatform>::path_for_auxiliary_executable is not implemented yet",
1072 ))
1073 }
1074
1075 fn set_cursor_style(&self, style: CursorStyle) {
1076 self.connection.borrow().set_cursor_style(style)
1077 }
1078
1079 fn hide_cursor_until_mouse_moves(&self) {
1080 self.connection.borrow().hide_cursor_until_mouse_moves()
1081 }
1082
1083 fn is_cursor_visible(&self) -> bool {
1084 self.connection.borrow().is_cursor_visible()
1085 }
1086
1087 fn should_auto_hide_scrollbars(&self) -> bool {
1088 self.with_common(|common| common.auto_hide_scrollbars)
1089 }
1090
1091 fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>> {
1092 let url = url.to_string();
1093 let username = username.to_string();
1094 let password = password.to_vec();
1095 self.background_executor().spawn(async move {
1096 let keyring = oo7::Keyring::new().await?;
1097 keyring.unlock().await?;
1098 keyring
1099 .create_item(
1100 KEYRING_LABEL,
1101 &vec![("url", &url), ("username", &username)],
1102 password,
1103 true,
1104 )
1105 .await?;
1106 Ok(())
1107 })
1108 }
1109
1110 fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
1111 let url = url.to_string();
1112 self.background_executor().spawn(async move {
1113 let keyring = oo7::Keyring::new().await?;
1114 keyring.unlock().await?;
1115
1116 let items = keyring.search_items(&vec![("url", &url)]).await?;
1117
1118 for item in items.into_iter() {
1119 if item.label().await.is_ok_and(|label| label == KEYRING_LABEL) {
1120 let attributes = item.attributes().await?;
1121 let username = attributes
1122 .get("username")
1123 .context("Cannot find username in stored credentials")?;
1124 item.unlock().await?;
1125 let secret = item.secret().await?;
1126
1127 return Ok(Some((username.to_string(), secret.to_vec())));
1130 } else {
1131 continue;
1132 }
1133 }
1134 Ok(None)
1135 })
1136 }
1137
1138 fn delete_credentials(&self, url: &str) -> Task<Result<()>> {
1139 let url = url.to_string();
1140 self.background_executor().spawn(async move {
1141 let keyring = oo7::Keyring::new().await?;
1142 keyring.unlock().await?;
1143
1144 let items = keyring.search_items(&vec![("url", &url)]).await?;
1145
1146 for item in items.into_iter() {
1147 if item.label().await.is_ok_and(|label| label == KEYRING_LABEL) {
1148 item.delete().await?;
1149 return Ok(());
1150 }
1151 }
1152
1153 Ok(())
1154 })
1155 }
1156
1157 fn window_appearance(&self) -> WindowAppearance {
1158 self.with_common(|common| common.appearance)
1159 }
1160
1161 fn button_layout(&self) -> Option<WindowButtonLayout> {
1162 Some(self.with_common(|common| common.button_layout))
1163 }
1164
1165 fn register_url_scheme(&self, _: &str) -> Task<anyhow::Result<()>> {
1166 Task::ready(Err(anyhow!("register_url_scheme unimplemented")))
1167 }
1168
1169 fn write_to_primary(&self, item: ClipboardItem) {
1170 self.connection.borrow().write_to_primary(item)
1171 }
1172
1173 fn write_to_clipboard(&self, item: ClipboardItem) {
1174 self.connection.borrow().write_to_clipboard(item)
1175 }
1176
1177 fn read_from_primary(&self) -> Option<ClipboardItem> {
1178 self.connection.borrow().read_from_primary()
1179 }
1180
1181 fn read_from_clipboard(&self) -> Option<ClipboardItem> {
1182 self.connection.borrow().read_from_clipboard()
1183 }
1184
1185 fn add_recent_document(&self, _path: &Path) {}
1186}
1187
1188#[cfg(any(feature = "wayland", feature = "x11"))]
1191async fn run_open_commands(
1192 target: &std::ffi::OsStr,
1193 activation_token: Option<&str>,
1194 environment: &GraphicalEnvironment,
1195) -> bool {
1196 for mut command in open::commands(target) {
1197 environment.apply_to(&mut command);
1198 if let Some(token) = activation_token {
1199 command.env("XDG_ACTIVATION_TOKEN", token);
1200 }
1201 let program = format!("{:?}", command.get_program());
1202 match smol::process::Command::from(command).spawn() {
1203 Ok(mut child) => match child.status().await {
1204 Ok(status) if status.success() => return true,
1205 Ok(status) => log::error!("Command {} exited with status: {}", program, status),
1206 Err(error) => log::error!("Failed to get status from {}: {}", program, error),
1207 },
1208 Err(error) => log::error!("Failed to open with {}: {}", program, error),
1209 }
1210 }
1211 false
1212}
1213
1214#[cfg(any(feature = "wayland", feature = "x11"))]
1215pub(super) fn open_uri_internal(
1216 executor: BackgroundExecutor,
1217 uri: &str,
1218 activation_token: Option<String>,
1219 environment: GraphicalEnvironment,
1220) {
1221 if let Some(uri) = ashpd::Uri::parse(uri).log_err() {
1222 executor
1223 .spawn(async move {
1224 let opened = run_open_commands(
1225 uri.as_str().as_ref(),
1226 activation_token.as_deref(),
1227 &environment,
1228 )
1229 .await;
1230 if !opened {
1231 match ashpd::desktop::open_uri::OpenFileRequest::default()
1232 .activation_token(activation_token.map(ashpd::ActivationToken::from))
1233 .send_uri(&uri)
1234 .await
1235 .and_then(|e| e.response())
1236 {
1237 Ok(()) => {}
1238 Err(ashpd::Error::Response(ashpd::desktop::ResponseError::Cancelled)) => {}
1239 Err(e) => {
1240 log::error!("Failed to open with dbus: {}", e);
1241 }
1242 }
1243 }
1244 })
1245 .detach();
1246 }
1247}
1248
1249#[cfg(any(feature = "x11", feature = "wayland"))]
1250pub(super) fn reveal_path_internal(
1251 executor: BackgroundExecutor,
1252 path: PathBuf,
1253 activation_token: Option<String>,
1254 environment: GraphicalEnvironment,
1255) {
1256 executor
1257 .spawn(async move {
1258 if let Some(dir) = File::open(path.clone()).log_err() {
1259 match ashpd::desktop::open_uri::OpenDirectoryRequest::default()
1260 .activation_token(activation_token.clone().map(ashpd::ActivationToken::from))
1261 .send(&dir.as_fd())
1262 .await
1263 {
1264 Ok(_) => return,
1265 Err(e) => log::error!("Failed to open with dbus: {}", e),
1266 }
1267 let directory = if path.is_dir() {
1268 path.as_path()
1269 } else {
1270 path.parent().unwrap_or(Path::new(""))
1271 };
1272 run_open_commands(
1273 directory.as_os_str(),
1274 activation_token.as_deref(),
1275 &environment,
1276 )
1277 .await;
1278 }
1279 })
1280 .detach();
1281}
1282
1283#[cfg(any(feature = "wayland", feature = "x11"))]
1284pub(super) fn is_within_click_distance(a: Point<Pixels>, b: Point<Pixels>) -> bool {
1285 let diff = a - b;
1286 diff.x.abs() <= DOUBLE_CLICK_DISTANCE && diff.y.abs() <= DOUBLE_CLICK_DISTANCE
1287}
1288
1289#[cfg(any(feature = "wayland", feature = "x11"))]
1294pub(super) fn new_xkb_context() -> anyhow::Result<xkb::Context> {
1295 validate_xkb_context(xkb::Context::new(xkb::CONTEXT_NO_DEFAULT_INCLUDES))
1296}
1297
1298#[cfg(any(feature = "wayland", feature = "x11"))]
1299fn validate_xkb_context(context: xkb::Context) -> anyhow::Result<xkb::Context> {
1300 ensure!(
1301 !context.get_raw_ptr().is_null(),
1302 "libxkbcommon failed to create an XKB context"
1303 );
1304 Ok(context)
1305}
1306
1307#[cfg(any(feature = "wayland", feature = "x11"))]
1308pub(super) fn get_xkb_compose_state(cx: &xkb::Context) -> Option<xkb::compose::State> {
1309 let mut locales = Vec::default();
1310 if let Some(locale) = env::var_os("LC_CTYPE") {
1311 locales.push(locale);
1312 }
1313 locales.push(OsString::from("C"));
1314 let mut state: Option<xkb::compose::State> = None;
1315 for locale in locales {
1316 if let Ok(table) =
1317 xkb::compose::Table::new_from_locale(cx, &locale, xkb::compose::COMPILE_NO_FLAGS)
1318 {
1319 state = Some(xkb::compose::State::new(
1320 &table,
1321 xkb::compose::STATE_NO_FLAGS,
1322 ));
1323 break;
1324 }
1325 }
1326 state
1327}
1328
1329#[cfg(any(feature = "wayland", feature = "x11"))]
1330pub(super) const PIPE_READ_TIMEOUT: Duration = Duration::from_secs(4);
1331
1332#[cfg(any(feature = "wayland", feature = "x11"))]
1333pub(super) fn read_fd_with_timeout(
1334 mut fd: filedescriptor::FileDescriptor,
1335 timeout: Duration,
1336) -> Result<Vec<u8>> {
1337 fd.set_non_blocking(true)?;
1338 let mut buffer = Vec::new();
1339 let mut chunk = [0u8; 8192];
1340 loop {
1341 let mut poll_fds = [filedescriptor::pollfd {
1342 fd: fd.as_raw_fd(),
1343 events: filedescriptor::POLLIN,
1344 revents: 0,
1345 }];
1346 let ready = match filedescriptor::poll(&mut poll_fds, Some(timeout)) {
1347 Ok(ready) => ready,
1348 Err(filedescriptor::Error::Poll(err))
1349 if err.kind() == std::io::ErrorKind::Interrupted =>
1350 {
1351 continue;
1352 }
1353 Err(err) => return Err(err.into()),
1354 };
1355 if ready == 0 {
1356 anyhow::bail!("timed out waiting for data on pipe after {timeout:?}");
1357 }
1358 match fd.read(&mut chunk) {
1359 Ok(0) => return Ok(buffer),
1360 Ok(len) => buffer.extend_from_slice(&chunk[..len]),
1361 Err(err)
1362 if err.kind() == std::io::ErrorKind::WouldBlock
1363 || err.kind() == std::io::ErrorKind::Interrupted => {}
1364 Err(err) => return Err(err.into()),
1365 }
1366 }
1367}
1368
1369#[cfg(any(feature = "wayland", feature = "x11"))]
1370pub(super) const DEFAULT_CURSOR_ICON_NAME: &str = "left_ptr";
1371
1372#[cfg(any(feature = "wayland", feature = "x11"))]
1373pub(super) fn cursor_style_to_icon_names(style: CursorStyle) -> &'static [&'static str] {
1374 match style {
1377 CursorStyle::Arrow => &[DEFAULT_CURSOR_ICON_NAME],
1378 CursorStyle::IBeam => &["text", "xterm"],
1379 CursorStyle::Crosshair => &["crosshair", "cross"],
1380 CursorStyle::ClosedHand => &["closedhand", "grabbing", "hand2"],
1381 CursorStyle::OpenHand => &["openhand", "grab", "hand1"],
1382 CursorStyle::PointingHand => &["pointer", "hand", "hand2"],
1383 CursorStyle::ResizeLeft => &["w-resize", "left_side"],
1384 CursorStyle::ResizeRight => &["e-resize", "right_side"],
1385 CursorStyle::ResizeLeftRight => &["ew-resize", "sb_h_double_arrow"],
1386 CursorStyle::ResizeUp => &["n-resize", "top_side"],
1387 CursorStyle::ResizeDown => &["s-resize", "bottom_side"],
1388 CursorStyle::ResizeUpDown => &["sb_v_double_arrow", "ns-resize"],
1389 CursorStyle::ResizeUpLeftDownRight => &["size_fdiag", "bd_double_arrow", "nwse-resize"],
1390 CursorStyle::ResizeUpRightDownLeft => &["size_bdiag", "nesw-resize", "fd_double_arrow"],
1391 CursorStyle::ResizeColumn => &["col-resize", "sb_h_double_arrow"],
1392 CursorStyle::ResizeRow => &["row-resize", "sb_v_double_arrow"],
1393 CursorStyle::IBeamCursorForVerticalLayout => &["vertical-text"],
1394 CursorStyle::OperationNotAllowed => &["not-allowed", "crossed_circle"],
1395 CursorStyle::DragLink => &["alias"],
1396 CursorStyle::DragCopy => &["copy"],
1397 CursorStyle::ContextualMenu => &["context-menu"],
1398 }
1399}
1400
1401#[cfg(any(feature = "wayland", feature = "x11"))]
1402pub(super) fn log_cursor_icon_warning(message: impl std::fmt::Display) {
1403 if let Ok(xcursor_path) = env::var("XCURSOR_PATH") {
1404 log::warn!(
1405 "{:#}\ncursor icon loading may be failing if XCURSOR_PATH environment variable is invalid. \
1406 XCURSOR_PATH overrides the default icon search. Its current value is '{}'",
1407 message,
1408 xcursor_path
1409 );
1410 } else {
1411 log::warn!("{:#}", message);
1412 }
1413}
1414
1415#[cfg(any(feature = "wayland", feature = "x11"))]
1416fn guess_ascii(keycode: Keycode, shift: bool) -> Option<char> {
1417 let c = match (keycode.raw(), shift) {
1418 (24, _) => 'q',
1419 (25, _) => 'w',
1420 (26, _) => 'e',
1421 (27, _) => 'r',
1422 (28, _) => 't',
1423 (29, _) => 'y',
1424 (30, _) => 'u',
1425 (31, _) => 'i',
1426 (32, _) => 'o',
1427 (33, _) => 'p',
1428 (34, false) => '[',
1429 (34, true) => '{',
1430 (35, false) => ']',
1431 (35, true) => '}',
1432 (38, _) => 'a',
1433 (39, _) => 's',
1434 (40, _) => 'd',
1435 (41, _) => 'f',
1436 (42, _) => 'g',
1437 (43, _) => 'h',
1438 (44, _) => 'j',
1439 (45, _) => 'k',
1440 (46, _) => 'l',
1441 (47, false) => ';',
1442 (47, true) => ':',
1443 (48, false) => '\'',
1444 (48, true) => '"',
1445 (49, false) => '`',
1446 (49, true) => '~',
1447 (51, false) => '\\',
1448 (51, true) => '|',
1449 (52, _) => 'z',
1450 (53, _) => 'x',
1451 (54, _) => 'c',
1452 (55, _) => 'v',
1453 (56, _) => 'b',
1454 (57, _) => 'n',
1455 (58, _) => 'm',
1456 (59, false) => ',',
1457 (59, true) => '>',
1458 (60, false) => '.',
1459 (60, true) => '<',
1460 (61, false) => '/',
1461 (61, true) => '?',
1462
1463 _ => return None,
1464 };
1465
1466 Some(c)
1467}
1468
1469#[cfg(any(feature = "wayland", feature = "x11"))]
1470pub(super) fn keystroke_from_xkb(
1471 state: &State,
1472 mut modifiers: gpui_platform::Modifiers,
1473 keycode: Keycode,
1474) -> gpui_platform::Keystroke {
1475 let key_utf32 = state.key_get_utf32(keycode);
1476 let key_utf8 = state.key_get_utf8(keycode);
1477 let key_sym = state.key_get_one_sym(keycode);
1478
1479 let key = match key_sym {
1480 Keysym::Return => "enter".to_owned(),
1481 Keysym::Prior => "pageup".to_owned(),
1482 Keysym::Next => "pagedown".to_owned(),
1483 Keysym::ISO_Left_Tab => "tab".to_owned(),
1484 Keysym::KP_Prior => "pageup".to_owned(),
1485 Keysym::KP_Next => "pagedown".to_owned(),
1486 Keysym::XF86_Back => "back".to_owned(),
1487 Keysym::XF86_Forward => "forward".to_owned(),
1488 Keysym::XF86_Cut => "cut".to_owned(),
1489 Keysym::XF86_Copy => "copy".to_owned(),
1490 Keysym::XF86_Paste => "paste".to_owned(),
1491 Keysym::XF86_New => "new".to_owned(),
1492 Keysym::XF86_Open => "open".to_owned(),
1493 Keysym::XF86_Save => "save".to_owned(),
1494
1495 Keysym::comma => ",".to_owned(),
1496 Keysym::period => ".".to_owned(),
1497 Keysym::less => "<".to_owned(),
1498 Keysym::greater => ">".to_owned(),
1499 Keysym::slash => "/".to_owned(),
1500 Keysym::question => "?".to_owned(),
1501
1502 Keysym::semicolon => ";".to_owned(),
1503 Keysym::colon => ":".to_owned(),
1504 Keysym::apostrophe => "'".to_owned(),
1505 Keysym::quotedbl => "\"".to_owned(),
1506
1507 Keysym::bracketleft => "[".to_owned(),
1508 Keysym::braceleft => "{".to_owned(),
1509 Keysym::bracketright => "]".to_owned(),
1510 Keysym::braceright => "}".to_owned(),
1511 Keysym::backslash => "\\".to_owned(),
1512 Keysym::bar => "|".to_owned(),
1513
1514 Keysym::grave => "`".to_owned(),
1515 Keysym::asciitilde => "~".to_owned(),
1516 Keysym::exclam => "!".to_owned(),
1517 Keysym::at => "@".to_owned(),
1518 Keysym::numbersign => "#".to_owned(),
1519 Keysym::dollar => "$".to_owned(),
1520 Keysym::percent => "%".to_owned(),
1521 Keysym::asciicircum => "^".to_owned(),
1522 Keysym::ampersand => "&".to_owned(),
1523 Keysym::asterisk => "*".to_owned(),
1524 Keysym::parenleft => "(".to_owned(),
1525 Keysym::parenright => ")".to_owned(),
1526 Keysym::minus => "-".to_owned(),
1527 Keysym::underscore => "_".to_owned(),
1528 Keysym::equal => "=".to_owned(),
1529 Keysym::plus => "+".to_owned(),
1530 Keysym::space => "space".to_owned(),
1531 Keysym::BackSpace => "backspace".to_owned(),
1532 Keysym::Tab => "tab".to_owned(),
1533 Keysym::Delete => "delete".to_owned(),
1534 Keysym::Escape => "escape".to_owned(),
1535
1536 Keysym::Left => "left".to_owned(),
1537 Keysym::Right => "right".to_owned(),
1538 Keysym::Up => "up".to_owned(),
1539 Keysym::Down => "down".to_owned(),
1540 Keysym::Home => "home".to_owned(),
1541 Keysym::End => "end".to_owned(),
1542 Keysym::Insert => "insert".to_owned(),
1543
1544 _ => {
1545 let name = xkb::keysym_get_name(key_sym).to_lowercase();
1546 if key_sym.is_keypad_key() {
1547 name.replace("kp_", "")
1548 } else if let Some(key) = key_utf8.chars().next()
1549 && key_utf8.len() == 1
1550 && key.is_ascii()
1551 {
1552 if key.is_ascii_graphic() {
1553 key_utf8.to_lowercase()
1554 } else if key_utf32 <= 0x1f
1558 && !name.chars().next().is_some_and(|c| c.is_ascii_digit())
1559 {
1560 ((key_utf32 as u8 + 0x40) as char)
1561 .to_ascii_lowercase()
1562 .to_string()
1563 } else {
1564 name
1565 }
1566 } else if let Some(key_en) = guess_ascii(keycode, modifiers.shift) {
1567 String::from(key_en)
1568 } else {
1569 name
1570 }
1571 }
1572 };
1573
1574 if modifiers.shift {
1575 if key.chars().count() == 1 && key.to_lowercase() == key.to_uppercase() {
1579 modifiers.shift = false;
1580 }
1581 }
1582
1583 let key_char =
1585 (key_utf32 >= 32 && key_utf32 != 127 && !key_utf8.is_empty()).then_some(key_utf8);
1586
1587 gpui_platform::Keystroke {
1588 modifiers,
1589 key,
1590 key_char,
1591 }
1592}
1593
1594#[cfg(any(feature = "wayland", feature = "x11"))]
1599pub fn keystroke_underlying_dead_key(keysym: Keysym) -> Option<String> {
1600 match keysym {
1601 Keysym::dead_grave => Some("`".to_owned()),
1602 Keysym::dead_acute => Some("´".to_owned()),
1603 Keysym::dead_circumflex => Some("^".to_owned()),
1604 Keysym::dead_tilde => Some("~".to_owned()),
1605 Keysym::dead_macron => Some("¯".to_owned()),
1606 Keysym::dead_breve => Some("˘".to_owned()),
1607 Keysym::dead_abovedot => Some("˙".to_owned()),
1608 Keysym::dead_diaeresis => Some("¨".to_owned()),
1609 Keysym::dead_abovering => Some("˚".to_owned()),
1610 Keysym::dead_doubleacute => Some("˝".to_owned()),
1611 Keysym::dead_caron => Some("ˇ".to_owned()),
1612 Keysym::dead_cedilla => Some("¸".to_owned()),
1613 Keysym::dead_ogonek => Some("˛".to_owned()),
1614 Keysym::dead_iota => Some("ͅ".to_owned()),
1615 Keysym::dead_voiced_sound => Some("゙".to_owned()),
1616 Keysym::dead_semivoiced_sound => Some("゚".to_owned()),
1617 Keysym::dead_belowdot => Some("̣̣".to_owned()),
1618 Keysym::dead_hook => Some("̡".to_owned()),
1619 Keysym::dead_horn => Some("̛".to_owned()),
1620 Keysym::dead_stroke => Some("̶̶".to_owned()),
1621 Keysym::dead_abovecomma => Some("̓̓".to_owned()),
1622 Keysym::dead_abovereversedcomma => Some("ʽ".to_owned()),
1623 Keysym::dead_doublegrave => Some("̏".to_owned()),
1624 Keysym::dead_belowring => Some("˳".to_owned()),
1625 Keysym::dead_belowmacron => Some("̱".to_owned()),
1626 Keysym::dead_belowcircumflex => Some("ꞈ".to_owned()),
1627 Keysym::dead_belowtilde => Some("̰".to_owned()),
1628 Keysym::dead_belowbreve => Some("̮".to_owned()),
1629 Keysym::dead_belowdiaeresis => Some("̤".to_owned()),
1630 Keysym::dead_invertedbreve => Some("̯".to_owned()),
1631 Keysym::dead_belowcomma => Some("̦".to_owned()),
1632 Keysym::dead_currency => None,
1633 Keysym::dead_lowline => None,
1634 Keysym::dead_aboveverticalline => None,
1635 Keysym::dead_belowverticalline => None,
1636 Keysym::dead_longsolidusoverlay => None,
1637 Keysym::dead_a => None,
1638 Keysym::dead_A => None,
1639 Keysym::dead_e => None,
1640 Keysym::dead_E => None,
1641 Keysym::dead_i => None,
1642 Keysym::dead_I => None,
1643 Keysym::dead_o => None,
1644 Keysym::dead_O => None,
1645 Keysym::dead_u => None,
1646 Keysym::dead_U => None,
1647 Keysym::dead_small_schwa => Some("ə".to_owned()),
1648 Keysym::dead_capital_schwa => Some("Ə".to_owned()),
1649 Keysym::dead_greek => None,
1650 _ => None,
1651 }
1652}
1653#[cfg(any(feature = "wayland", feature = "x11"))]
1654pub(super) fn modifiers_from_xkb(keymap_state: &State) -> gpui_platform::Modifiers {
1655 let shift = keymap_state.mod_name_is_active(xkb::MOD_NAME_SHIFT, xkb::STATE_MODS_EFFECTIVE);
1656 let alt = keymap_state.mod_name_is_active(xkb::MOD_NAME_ALT, xkb::STATE_MODS_EFFECTIVE);
1657 let control = keymap_state.mod_name_is_active(xkb::MOD_NAME_CTRL, xkb::STATE_MODS_EFFECTIVE);
1658 let platform = keymap_state.mod_name_is_active(xkb::MOD_NAME_LOGO, xkb::STATE_MODS_EFFECTIVE);
1659 gpui_platform::Modifiers {
1660 shift,
1661 alt,
1662 control,
1663 platform,
1664 function: false,
1665 }
1666}
1667
1668#[cfg(any(feature = "wayland", feature = "x11"))]
1669pub(super) fn capslock_from_xkb(keymap_state: &State) -> gpui_platform::Capslock {
1670 let on = keymap_state.mod_name_is_active(xkb::MOD_NAME_CAPS, xkb::STATE_MODS_EFFECTIVE);
1671 gpui_platform::Capslock { on }
1672}
1673
1674#[cfg(any(feature = "wayland", feature = "x11"))]
1678pub(super) fn compositor_gpu_hint_from_dev_t(dev: u64) -> Option<gpui_wgpu::CompositorGpuHint> {
1679 fn dev_major(dev: u64) -> u32 {
1680 ((dev >> 8) & 0xfff) as u32 | (((dev >> 32) & !0xfff) as u32)
1681 }
1682
1683 fn dev_minor(dev: u64) -> u32 {
1684 (dev & 0xff) as u32 | (((dev >> 12) & !0xff) as u32)
1685 }
1686
1687 fn read_sysfs_hex_id(path: &str) -> Option<u32> {
1688 let content = std::fs::read_to_string(path).ok()?;
1689 let trimmed = content.trim().strip_prefix("0x").unwrap_or(content.trim());
1690 u32::from_str_radix(trimmed, 16).ok()
1691 }
1692
1693 let major = dev_major(dev);
1694 let minor = dev_minor(dev);
1695
1696 let vendor_path = format!("/sys/dev/char/{major}:{minor}/device/vendor");
1697 let device_path = format!("/sys/dev/char/{major}:{minor}/device/device");
1698
1699 let vendor_id = read_sysfs_hex_id(&vendor_path)?;
1700 let device_id = read_sysfs_hex_id(&device_path)?;
1701
1702 log::info!(
1703 "Compositor GPU hint: vendor={:#06x}, device={:#06x} (from dev {major}:{minor})",
1704 vendor_id,
1705 device_id,
1706 );
1707
1708 Some(gpui_wgpu::CompositorGpuHint {
1709 vendor_id,
1710 device_id,
1711 })
1712}
1713
1714#[cfg(any(feature = "wayland", feature = "x11"))]
1715async fn inhibit_idle_sleep(reason: String, executor: BackgroundExecutor) -> Result<ActivityGuard> {
1716 let proxy = InhibitProxy::new()
1717 .await
1718 .context("Idle sleep prevention portal is unavailable")?;
1719 let request = proxy
1720 .inhibit(
1721 None,
1722 BitFlags::from(InhibitFlags::Suspend),
1723 InhibitOptions::default().set_reason(reason.as_str()),
1724 )
1725 .await
1726 .context("Failed to request idle sleep prevention")?;
1727 request
1728 .response()
1729 .context("Idle sleep prevention request was rejected")?;
1730 Ok(release_on_drop(request, executor))
1731}
1732
1733#[cfg(any(feature = "wayland", feature = "x11"))]
1734fn release_on_drop(request: Request<()>, executor: BackgroundExecutor) -> ActivityGuard {
1735 ActivityGuard::new(move || {
1736 executor
1737 .spawn(async move {
1738 request
1739 .close()
1740 .await
1741 .context("Failed to release idle sleep prevention")
1742 .log_err();
1743 })
1744 .detach();
1745 })
1746}
1747
1748#[cfg(any(feature = "wayland", feature = "x11"))]
1749async fn await_idle_sleep_prevention(
1750 guard_rx: oneshot::Receiver<Result<ActivityGuard>>,
1751 executor: &BackgroundExecutor,
1752) -> Result<ActivityGuard> {
1753 match futures::future::select(guard_rx, executor.timer(Duration::from_secs(10))).await {
1754 futures::future::Either::Left((Ok(result), _)) => result,
1755 futures::future::Either::Left((Err(_), _)) => {
1756 Err(anyhow!("Idle sleep prevention request was abandoned"))
1757 }
1758 futures::future::Either::Right(_) => Err(anyhow!(
1759 "Idle sleep prevention acquisition timed out after 10 seconds"
1760 )),
1761 }
1762}
1763
1764#[cfg(test)]
1765mod tests {
1766 use super::*;
1767 use gpui_platform::{Point, px};
1768
1769 #[cfg(any(feature = "wayland", feature = "x11"))]
1770 #[test]
1771 fn rejects_null_xkb_context() {
1772 let context = unsafe {
1773 xkb::Context::from_raw_ptr(std::ptr::null_mut())
1775 };
1776 let error = validate_xkb_context(context)
1777 .err()
1778 .expect("null XKB context should be rejected");
1779
1780 assert_eq!(
1781 error.to_string(),
1782 "libxkbcommon failed to create an XKB context"
1783 );
1784 }
1785
1786 #[test]
1787 fn test_is_within_click_distance() {
1788 let zero = Point::new(px(0.0), px(0.0));
1789 assert!(is_within_click_distance(zero, Point::new(px(5.0), px(5.0))));
1790 assert!(is_within_click_distance(
1791 zero,
1792 Point::new(px(-4.9), px(5.0))
1793 ));
1794 assert!(is_within_click_distance(
1795 Point::new(px(3.0), px(2.0)),
1796 Point::new(px(-2.0), px(-2.0))
1797 ));
1798 assert!(!is_within_click_distance(
1799 zero,
1800 Point::new(px(5.0), px(5.1))
1801 ),);
1802 }
1803
1804 #[cfg(any(feature = "wayland", feature = "x11"))]
1805 mod idle_sleep_prevention {
1806 use super::super::await_idle_sleep_prevention;
1807 use anyhow::{Result, anyhow};
1808 use futures::channel::oneshot;
1809 use gpui::{ActivityGuard, TestAppContext};
1810 use std::{
1811 sync::{
1812 Arc,
1813 atomic::{AtomicUsize, Ordering::SeqCst},
1814 },
1815 time::Duration,
1816 };
1817
1818 #[gpui::test]
1819 async fn guard_passes_through_and_releases_on_drop(cx: &mut TestAppContext) {
1820 let released = Arc::new(AtomicUsize::new(0));
1821 let (guard_tx, guard_rx) = oneshot::channel();
1822 assert!(guard_tx.send(Ok(release_guard(released.clone()))).is_ok());
1823 let guard = await_idle_sleep_prevention(guard_rx, &cx.background_executor)
1824 .await
1825 .expect("acquisition should succeed");
1826
1827 assert_eq!(released.load(SeqCst), 0);
1828 drop(guard);
1829 assert_eq!(released.load(SeqCst), 1);
1830 }
1831
1832 #[gpui::test]
1833 async fn acquisition_error_passes_through(cx: &mut TestAppContext) {
1834 let (guard_tx, guard_rx) = oneshot::channel::<Result<ActivityGuard>>();
1835 assert!(guard_tx.send(Err(anyhow!("inhibition rejected"))).is_ok());
1836 assert_eq!(
1837 await_idle_sleep_prevention(guard_rx, &cx.background_executor)
1838 .await
1839 .err()
1840 .expect("acquisition should fail")
1841 .to_string(),
1842 "inhibition rejected"
1843 );
1844 }
1845
1846 #[gpui::test]
1847 async fn abandoned_acquisition_is_an_error(cx: &mut TestAppContext) {
1848 let (guard_tx, guard_rx) = oneshot::channel::<Result<ActivityGuard>>();
1849 drop(guard_tx);
1850 assert_eq!(
1851 await_idle_sleep_prevention(guard_rx, &cx.background_executor)
1852 .await
1853 .err()
1854 .expect("acquisition should fail")
1855 .to_string(),
1856 "Idle sleep prevention request was abandoned"
1857 );
1858 }
1859
1860 #[gpui::test]
1861 async fn late_guard_after_timeout_is_released(cx: &mut TestAppContext) {
1862 let released = Arc::new(AtomicUsize::new(0));
1863 let (guard_tx, guard_rx) = oneshot::channel();
1864 let task = cx.background_executor.spawn({
1865 let executor = cx.background_executor.clone();
1866 async move { await_idle_sleep_prevention(guard_rx, &executor).await }
1867 });
1868 cx.run_until_parked();
1869 cx.executor().advance_clock(Duration::from_secs(9));
1870 cx.run_until_parked();
1871 assert!(!guard_tx.is_canceled());
1872 cx.executor().advance_clock(Duration::from_secs(1));
1873 assert_eq!(
1874 task.await
1875 .err()
1876 .expect("acquisition should time out")
1877 .to_string(),
1878 "Idle sleep prevention acquisition timed out after 10 seconds"
1879 );
1880
1881 assert!(guard_tx.is_canceled());
1882 if let Err(Ok(guard)) = guard_tx.send(Ok(release_guard(released.clone()))) {
1883 assert_eq!(released.load(SeqCst), 0);
1884 drop(guard);
1885 }
1886 assert_eq!(released.load(SeqCst), 1);
1887 }
1888
1889 fn release_guard(released: Arc<AtomicUsize>) -> ActivityGuard {
1890 ActivityGuard::new(move || {
1891 released.fetch_add(1, SeqCst);
1892 })
1893 }
1894 }
1895
1896 #[cfg(any(feature = "wayland", feature = "x11"))]
1897 mod read_fd_with_timeout {
1898 use super::super::{PIPE_READ_TIMEOUT, read_fd_with_timeout};
1899 use std::io::Write as _;
1900 use std::time::{Duration, Instant};
1901
1902 #[test]
1903 fn reads_data_written_before_close() {
1904 let mut pipe = filedescriptor::Pipe::new().unwrap();
1905 pipe.write.write_all(b"hello clipboard").unwrap();
1906 drop(pipe.write);
1907
1908 let bytes = read_fd_with_timeout(pipe.read, PIPE_READ_TIMEOUT).unwrap();
1909 assert_eq!(bytes, b"hello clipboard");
1910 }
1911
1912 #[test]
1913 fn returns_empty_when_writer_closes_without_writing() {
1914 let pipe = filedescriptor::Pipe::new().unwrap();
1915 drop(pipe.write);
1916
1917 let bytes = read_fd_with_timeout(pipe.read, PIPE_READ_TIMEOUT).unwrap();
1918 assert!(bytes.is_empty());
1919 }
1920
1921 #[test]
1922 fn times_out_when_writer_never_writes() {
1923 let pipe = filedescriptor::Pipe::new().unwrap();
1924 let _open_writer = pipe.write;
1925
1926 let timeout = Duration::from_millis(50);
1927 let started = Instant::now();
1928 let result = read_fd_with_timeout(pipe.read, timeout);
1929 let elapsed = started.elapsed();
1930
1931 let err = result.unwrap_err();
1932 assert!(
1933 err.to_string().contains("timed out"),
1934 "unexpected error: {err}"
1935 );
1936 assert!(elapsed >= timeout, "returned before the timeout elapsed");
1937 }
1938
1939 #[test]
1940 fn times_out_when_writer_stalls_after_partial_write() {
1941 let mut pipe = filedescriptor::Pipe::new().unwrap();
1942 pipe.write.write_all(b"partial").unwrap();
1943 let _open_writer = pipe.write;
1944
1945 let err = read_fd_with_timeout(pipe.read, Duration::from_millis(50)).unwrap_err();
1946 assert!(
1947 err.to_string().contains("timed out"),
1948 "unexpected error: {err}"
1949 );
1950 }
1951
1952 #[test]
1953 fn slow_writer_resets_deadline_between_chunks() {
1954 let pipe = filedescriptor::Pipe::new().unwrap();
1955 let chunks = 12;
1956 let gap = Duration::from_millis(40);
1957 let timeout = Duration::from_millis(400);
1958
1959 let writer = std::thread::spawn({
1960 let mut write = pipe.write;
1961 move || {
1962 for _ in 0..chunks {
1963 std::thread::sleep(gap);
1964 write.write_all(&[b'x'; 1000]).unwrap();
1965 }
1966 }
1967 });
1968 let bytes = read_fd_with_timeout(pipe.read, timeout).unwrap();
1971 writer.join().unwrap();
1972 assert_eq!(bytes, vec![b'x'; 1000 * chunks]);
1973 }
1974
1975 #[test]
1976 fn reads_payload_larger_than_pipe_capacity() {
1977 let pipe = filedescriptor::Pipe::new().unwrap();
1978 let payload = vec![b'z'; 1024 * 1024];
1980
1981 let writer = std::thread::spawn({
1982 let mut write = pipe.write;
1983 let payload = payload.clone();
1984 move || write.write_all(&payload).unwrap()
1985 });
1986 let bytes = read_fd_with_timeout(pipe.read, PIPE_READ_TIMEOUT).unwrap();
1987 writer.join().unwrap();
1988 assert_eq!(bytes, payload);
1989 }
1990 }
1991}
1992
1993#[cfg(test)]
1994mod display_mode_tests {
1995 use std::{cell::Cell, process::Command};
1996
1997 #[cfg(feature = "wayland")]
1998 use gpui::{AppContext as _, Context, IntoElement, Render, Window, WindowOptions};
1999 use gpui::{Application, AsyncApp, QuitMode};
2000
2001 use super::*;
2002
2003 const SCENARIO_ENV: &str = "GPUI_LINUX_DISPLAY_MODE_SCENARIO";
2005 const MISSING_WAYLAND_DISPLAY: &str = "/gpui-test/wayland-display-does-not-exist";
2006
2007 #[cfg(feature = "wayland")]
2008 #[test]
2009 fn failed_wayland_attach_preserves_headless_app_and_allows_retry() {
2010 run_scenario(
2011 "failed_wayland_attach_preserves_headless_app_and_allows_retry",
2012 WindowingModes::all(),
2013 |cx| {
2014 Box::pin(async move {
2015 let entity = cx.new(|_| 1usize);
2016 let (release_task, wait_for_release) = oneshot::channel();
2017 let queued_task_ran = Rc::new(Cell::new(false));
2018 let queued_task = cx.spawn({
2019 let queued_task_ran = queued_task_ran.clone();
2020 async move |_| {
2021 wait_for_release
2022 .await
2023 .expect("queued task release sender remains alive");
2024 queued_task_ran.set(true);
2025 }
2026 });
2027
2028 let first_attach = cx.update(|cx| {
2029 cx.request_windowing(WindowingRequest::Windowed(
2030 GraphicalEnvironment::detect(),
2031 ))
2032 });
2033 assert_error_contains(
2034 first_attach.await,
2035 "failed to connect to Wayland compositor",
2036 );
2037 entity.update(cx, |value, _| *value += 1);
2038 release_task
2039 .send(())
2040 .expect("queued task remains attached to the executor");
2041 queued_task.await;
2042 assert!(queued_task_ran.get());
2043
2044 let second_attach = cx.update(|cx| {
2045 cx.request_windowing(WindowingRequest::Windowed(
2046 GraphicalEnvironment::detect(),
2047 ))
2048 });
2049 assert_error_contains(
2050 second_attach.await,
2051 "failed to connect to Wayland compositor",
2052 );
2053 entity.update(cx, |value, _| *value += 1);
2054 assert_eq!(entity.read_with(cx, |value, _| *value), 3);
2055 })
2056 },
2057 );
2058 }
2059
2060 #[cfg(feature = "wayland")]
2061 #[test]
2062 fn headless_windows_block_switching_to_a_display_server() {
2063 run_scenario(
2064 "headless_windows_block_switching_to_a_display_server",
2065 WindowingModes::all(),
2066 |cx| {
2067 Box::pin(async move {
2068 let window = cx
2069 .update(|cx| {
2070 cx.open_window(WindowOptions::default(), |_, cx| cx.new(|_| Blank))
2071 })
2072 .expect("headless mode opens headless windows");
2073 let attach = cx.update(|cx| {
2074 cx.request_windowing(WindowingRequest::Windowed(
2075 GraphicalEnvironment::detect(),
2076 ))
2077 });
2078 assert_error_contains(attach.await, "while windows are open");
2079
2080 cx.update(|cx| window.update(cx, |_, window, _| window.remove_window()))
2081 .expect("close the headless window");
2082 let attach = cx.update(|cx| {
2083 cx.request_windowing(WindowingRequest::Windowed(
2084 GraphicalEnvironment::detect(),
2085 ))
2086 });
2087 assert_error_contains(attach.await, "failed to connect to Wayland compositor");
2088 })
2089 },
2090 );
2091 }
2092
2093 #[test]
2094 fn display_modes_limit_switching() {
2095 run_scenario(
2096 "display_modes_limit_switching",
2097 WindowingModes::HEADLESS,
2098 |cx| {
2099 Box::pin(async move {
2100 let attach = cx.update(|cx| {
2101 cx.request_windowing(WindowingRequest::Windowed(
2102 GraphicalEnvironment::detect(),
2103 ))
2104 });
2105 assert_error_contains(attach.await, "names no display server that");
2106 assert_eq!(cx.update(|cx| cx.compositor_name()), "headless");
2107 assert!(cx.update(|cx| cx.graphical_environment()).is_none());
2108
2109 let detach = cx.update(|cx| cx.request_windowing(WindowingRequest::Headless));
2110 assert_error_contains(detach.await, "already headless");
2111 })
2112 },
2113 );
2114 }
2115
2116 #[cfg(feature = "wayland")]
2117 struct Blank;
2118
2119 #[cfg(feature = "wayland")]
2120 impl Render for Blank {
2121 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
2122 gpui::Empty
2123 }
2124 }
2125
2126 type Scenario = fn(&mut AsyncApp) -> futures::future::LocalBoxFuture<'_, ()>;
2127
2128 fn run_scenario(name: &str, modes: WindowingModes, scenario: Scenario) {
2131 if std::env::var(SCENARIO_ENV).as_deref() == Ok(name) {
2132 let platform = Rc::new(LinuxPlatform::new(modes));
2133 platform.set_initial_windowing(WindowingRequest::Headless);
2134 let completed = Rc::new(Cell::new(false));
2135 Application::with_platform(platform)
2136 .with_quit_mode(QuitMode::Explicit)
2137 .run({
2138 let completed = completed.clone();
2139 move |cx| {
2140 cx.spawn(async move |cx| {
2141 scenario(cx).await;
2142 completed.set(true);
2143 cx.update(|cx| cx.quit());
2144 })
2145 .detach();
2146 }
2147 });
2148 assert!(completed.get(), "scenario did not complete");
2149 return;
2150 }
2151
2152 let module = module_path!()
2154 .split_once("::")
2155 .map_or(module_path!(), |(_, module)| module);
2156 let test_name = format!("{module}::{name}");
2157 #[allow(
2158 clippy::disallowed_methods,
2159 reason = "the test thread has nothing else to do while the child runs"
2160 )]
2161 let output = Command::new(std::env::current_exe().expect("current test executable"))
2162 .args(["--exact", &test_name, "--nocapture"])
2163 .env(SCENARIO_ENV, name)
2164 .env("WAYLAND_DISPLAY", MISSING_WAYLAND_DISPLAY)
2165 .env_remove("DISPLAY")
2166 .output()
2167 .expect("run the scenario in a child process");
2168 let stdout = String::from_utf8_lossy(&output.stdout);
2169 assert!(
2170 output.status.success() && stdout.contains("1 passed"),
2171 "scenario failed\nstdout:\n{stdout}\nstderr:\n{}",
2172 String::from_utf8_lossy(&output.stderr),
2173 );
2174 }
2175
2176 fn assert_error_contains(result: anyhow::Result<()>, expected: &str) {
2177 let error = result.expect_err("the transition fails");
2178 assert!(
2179 format!("{error:#}").contains(expected),
2180 "expected an error containing {expected:?}, got: {error:#}"
2181 );
2182 }
2183}