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