Skip to repository content1361 lines · 48.6 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T03:53:58.050Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
platform.rs
1use std::{
2 env,
3 path::{Path, PathBuf},
4 rc::Rc,
5 sync::Arc,
6};
7#[cfg(any(feature = "wayland", feature = "x11"))]
8use std::{
9 ffi::OsString,
10 fs::File,
11 io::Read as _,
12 os::fd::{AsFd, AsRawFd},
13 time::Duration,
14};
15
16use anyhow::{Context as _, anyhow};
17use calloop::{LoopSignal, channel::Sender};
18use futures::channel::oneshot;
19use gpui_util::{ResultExt as _, new_std_command};
20#[cfg(any(feature = "wayland", feature = "x11"))]
21use xkbcommon::xkb::{self, Keycode, Keysym, State};
22
23use crate::linux::{LinuxDispatcher, PriorityQueueCalloopReceiver};
24use gpui::{
25 Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
26 ForegroundExecutor, Keymap, Menu, MenuItem, OwnedMenu, PathPromptOptions, Platform,
27 PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem,
28 PlatformWindow, Result, RunnableVariant, Task, ThermalState, WindowAppearance,
29 WindowButtonLayout, WindowParams,
30};
31#[cfg(any(feature = "wayland", feature = "x11"))]
32use gpui::{Pixels, Point, px};
33
34#[cfg(any(feature = "wayland", feature = "x11"))]
35pub(crate) const SCROLL_LINES: f32 = 3.0;
36
37// Values match the defaults on GTK.
38// Taken from https://github.com/GNOME/gtk/blob/main/gtk/gtksettings.c#L320
39#[cfg(any(feature = "wayland", feature = "x11"))]
40pub(crate) const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(400);
41#[cfg(any(feature = "wayland", feature = "x11"))]
42pub(crate) const DOUBLE_CLICK_DISTANCE: Pixels = px(5.0);
43pub(crate) const KEYRING_LABEL: &str = "zed-github-account";
44
45#[cfg(any(feature = "wayland", feature = "x11"))]
46const FILE_PICKER_PORTAL_MISSING: &str =
47 "Couldn't open file picker due to missing xdg-desktop-portal implementation.";
48
49pub(crate) trait LinuxClient {
50 fn compositor_name(&self) -> &'static str;
51 fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R;
52 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
53 fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
54 #[allow(unused)]
55 fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>>;
56 fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
57
58 #[cfg(feature = "screen-capture")]
59 fn is_screen_capture_supported(&self) -> bool {
60 true
61 }
62
63 #[cfg(feature = "screen-capture")]
64 fn screen_capture_sources(
65 &self,
66 ) -> oneshot::Receiver<Result<Vec<Rc<dyn gpui::ScreenCaptureSource>>>> {
67 let (sources_tx, sources_rx) = oneshot::channel();
68 sources_tx
69 .send(Err(anyhow::anyhow!(
70 "gpui_linux was compiled without the screen-capture feature"
71 )))
72 .ok();
73 sources_rx
74 }
75
76 fn open_window(
77 &self,
78 handle: AnyWindowHandle,
79 options: WindowParams,
80 ) -> anyhow::Result<Box<dyn PlatformWindow>>;
81 fn set_cursor_style(&self, style: CursorStyle);
82 fn hide_cursor_until_mouse_moves(&self) {}
83 fn is_cursor_visible(&self) -> bool {
84 true
85 }
86 fn open_uri(&self, uri: &str);
87 fn reveal_path(&self, path: PathBuf);
88 fn write_to_primary(&self, item: ClipboardItem);
89 fn write_to_clipboard(&self, item: ClipboardItem);
90 fn read_from_primary(&self) -> Option<ClipboardItem>;
91 fn read_from_clipboard(&self) -> Option<ClipboardItem>;
92 fn active_window(&self) -> Option<AnyWindowHandle>;
93 fn window_stack(&self) -> Option<Vec<AnyWindowHandle>>;
94 fn run(&self);
95
96 #[cfg(any(feature = "wayland", feature = "x11"))]
97 fn window_identifier(
98 &self,
99 ) -> impl Future<Output = Option<ashpd::WindowIdentifier>> + Send + 'static {
100 std::future::ready::<Option<ashpd::WindowIdentifier>>(None)
101 }
102}
103
104#[derive(Default)]
105pub(crate) struct PlatformHandlers {
106 pub(crate) open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
107 pub(crate) quit: Option<Box<dyn FnMut()>>,
108 pub(crate) reopen: Option<Box<dyn FnMut()>>,
109 pub(crate) app_menu_action: Option<Box<dyn FnMut(&dyn Action)>>,
110 pub(crate) will_open_app_menu: Option<Box<dyn FnMut()>>,
111 pub(crate) validate_app_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
112 pub(crate) keyboard_layout_change: Option<Box<dyn FnMut()>>,
113 pub(crate) system_wake: Option<Box<dyn FnMut()>>,
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) callbacks: PlatformHandlers,
124 pub(crate) signal: LoopSignal,
125 pub(crate) menus: Vec<OwnedMenu>,
126 app_name: Option<String>,
127 system_notifications: crate::linux::system_notifications::SystemNotificationState,
128 #[cfg_attr(
129 not(all(target_os = "linux", any(feature = "wayland", feature = "x11"))),
130 allow(dead_code)
131 )]
132 wake_sender: Sender<()>,
133 wake_listener_started: bool,
134}
135
136impl LinuxCommon {
137 pub fn new(
138 signal: LoopSignal,
139 ) -> (
140 Self,
141 PriorityQueueCalloopReceiver<RunnableVariant>,
142 calloop::channel::Channel<()>,
143 ) {
144 let (main_sender, main_receiver) = PriorityQueueCalloopReceiver::new();
145 let (wake_sender, wake_receiver) = calloop::channel::channel();
146
147 #[cfg(any(feature = "wayland", feature = "x11"))]
148 let text_system = Arc::new(crate::linux::CosmicTextSystem::new("IBM Plex Sans"));
149 #[cfg(not(any(feature = "wayland", feature = "x11")))]
150 let text_system = Arc::new(gpui::NoopTextSystem::new());
151
152 let callbacks = PlatformHandlers::default();
153
154 let dispatcher = Arc::new(LinuxDispatcher::new(main_sender));
155
156 let background_executor = BackgroundExecutor::new(dispatcher.clone());
157
158 let common = LinuxCommon {
159 background_executor,
160 foreground_executor: ForegroundExecutor::new(dispatcher),
161 text_system,
162 appearance: WindowAppearance::Light,
163 auto_hide_scrollbars: false,
164 button_layout: WindowButtonLayout::linux_default(),
165 callbacks,
166 signal,
167 menus: Vec::new(),
168 app_name: None,
169 system_notifications: crate::linux::system_notifications::SystemNotificationState::new(
170 ),
171 wake_sender,
172 wake_listener_started: false,
173 };
174
175 (common, main_receiver, wake_receiver)
176 }
177
178 pub(crate) fn start_wake_listener(&mut self) {
179 if !self.wake_listener_started {
180 #[cfg(all(target_os = "linux", any(feature = "wayland", feature = "x11")))]
181 smol::spawn({
182 let wake_sender = self.wake_sender.clone();
183 async move {
184 if let Err(error) = listen_for_system_wake(wake_sender).await {
185 log::debug!("failed to listen for system wake events: {error:?}");
186 }
187 }
188 })
189 .detach();
190
191 self.wake_listener_started = true;
192 }
193 }
194
195 pub(crate) fn handle_system_wake(&mut self) {
196 if let Some(mut callback) = self.callbacks.system_wake.take() {
197 callback();
198 self.callbacks.system_wake = Some(callback);
199 }
200 }
201}
202
203#[cfg(all(target_os = "linux", any(feature = "wayland", feature = "x11")))]
204async fn listen_for_system_wake(wake_sender: Sender<()>) -> anyhow::Result<()> {
205 use futures::StreamExt as _;
206
207 let connection = ashpd::zbus::Connection::system().await?;
208 let proxy = ashpd::zbus::Proxy::new(
209 &connection,
210 "org.freedesktop.login1",
211 "/org/freedesktop/login1",
212 "org.freedesktop.login1.Manager",
213 )
214 .await?;
215 let mut sleep_events = proxy.receive_signal("PrepareForSleep").await?;
216
217 while let Some(message) = sleep_events.next().await {
218 let sleeping = message.body().deserialize::<bool>()?;
219 if !sleeping {
220 wake_sender.send(()).ok();
221 }
222 }
223
224 Ok(())
225}
226
227pub(crate) struct LinuxPlatform<P> {
228 pub(crate) inner: P,
229}
230
231impl<P: LinuxClient + 'static> Platform for LinuxPlatform<P> {
232 fn background_executor(&self) -> BackgroundExecutor {
233 self.inner
234 .with_common(|common| common.background_executor.clone())
235 }
236
237 fn foreground_executor(&self) -> ForegroundExecutor {
238 self.inner
239 .with_common(|common| common.foreground_executor.clone())
240 }
241
242 fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
243 self.inner.with_common(|common| common.text_system.clone())
244 }
245
246 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
247 self.inner.keyboard_layout()
248 }
249
250 fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
251 Rc::new(gpui::DummyKeyboardMapper)
252 }
253
254 fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
255 self.inner
256 .with_common(|common| common.callbacks.keyboard_layout_change = Some(callback));
257 }
258
259 fn on_thermal_state_change(&self, _callback: Box<dyn FnMut()>) {}
260
261 fn thermal_state(&self) -> ThermalState {
262 ThermalState::Nominal
263 }
264
265 fn run(&self, on_finish_launching: Box<dyn FnOnce()>) {
266 on_finish_launching();
267
268 LinuxClient::run(&self.inner);
269
270 let quit = self
271 .inner
272 .with_common(|common| common.callbacks.quit.take());
273 if let Some(mut fun) = quit {
274 fun();
275 }
276 }
277
278 fn quit(&self) {
279 self.inner.with_common(|common| common.signal.stop());
280 }
281
282 fn compositor_name(&self) -> &'static str {
283 self.inner.compositor_name()
284 }
285
286 fn restart(&self, binary_path: Option<PathBuf>) {
287 use std::os::unix::process::CommandExt as _;
288
289 // get the process id of the current process
290 let app_pid = std::process::id().to_string();
291 // get the path to the executable
292 let app_path = if let Some(path) = binary_path {
293 path
294 } else {
295 match self.app_path() {
296 Ok(path) => path,
297 Err(err) => {
298 log::error!("Failed to get app path: {:?}", err);
299 return;
300 }
301 }
302 };
303
304 log::info!("Restarting process, using app path: {:?}", app_path);
305
306 // Script to wait for the current process to exit and then restart the app.
307 // Pass dynamic values as positional parameters to avoid shell interpolation issues.
308 let script = r#"
309 while kill -0 "$0" 2>/dev/null; do
310 sleep 0.1
311 done
312
313 "$1"
314 "#;
315
316 #[allow(
317 clippy::disallowed_methods,
318 reason = "We are restarting ourselves, using std command thus is fine"
319 )]
320 let restart_process = new_std_command("/usr/bin/env")
321 .arg("bash")
322 .arg("-c")
323 .arg(script)
324 .arg(&app_pid)
325 .arg(&app_path)
326 .process_group(0)
327 .spawn();
328
329 match restart_process {
330 Ok(_) => self.quit(),
331 Err(e) => log::error!("failed to spawn restart script: {:?}", e),
332 }
333 }
334
335 fn activate(&self, _ignoring_other_apps: bool) {
336 log::info!("activate is not implemented on Linux, ignoring the call")
337 }
338
339 fn hide(&self) {
340 log::info!("hide is not implemented on Linux, ignoring the call")
341 }
342
343 fn hide_other_apps(&self) {
344 log::info!("hide_other_apps is not implemented on Linux, ignoring the call")
345 }
346
347 fn unhide_other_apps(&self) {
348 log::info!("unhide_other_apps is not implemented on Linux, ignoring the call")
349 }
350
351 fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
352 self.inner.primary_display()
353 }
354
355 fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
356 self.inner.displays()
357 }
358
359 #[cfg(feature = "screen-capture")]
360 fn is_screen_capture_supported(&self) -> bool {
361 self.inner.is_screen_capture_supported()
362 }
363
364 #[cfg(feature = "screen-capture")]
365 fn screen_capture_sources(
366 &self,
367 ) -> oneshot::Receiver<Result<Vec<Rc<dyn gpui::ScreenCaptureSource>>>> {
368 self.inner.screen_capture_sources()
369 }
370
371 fn active_window(&self) -> Option<AnyWindowHandle> {
372 self.inner.active_window()
373 }
374
375 fn window_stack(&self) -> Option<Vec<AnyWindowHandle>> {
376 self.inner.window_stack()
377 }
378
379 fn open_window(
380 &self,
381 handle: AnyWindowHandle,
382 options: WindowParams,
383 ) -> anyhow::Result<Box<dyn PlatformWindow>> {
384 self.inner.open_window(handle, options)
385 }
386
387 fn open_url(&self, url: &str) {
388 self.inner.open_uri(url);
389 }
390
391 fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
392 self.inner
393 .with_common(|common| common.callbacks.open_urls = Some(callback));
394 }
395
396 fn prompt_for_paths(
397 &self,
398 options: PathPromptOptions,
399 ) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
400 let (done_tx, done_rx) = oneshot::channel();
401
402 #[cfg(not(any(feature = "wayland", feature = "x11")))]
403 let _ = (done_tx.send(Ok(None)), options);
404
405 #[cfg(any(feature = "wayland", feature = "x11"))]
406 let identifier = self.inner.window_identifier();
407
408 #[cfg(any(feature = "wayland", feature = "x11"))]
409 self.foreground_executor()
410 .spawn(async move {
411 let title = if options.directories {
412 "Open Folder"
413 } else {
414 "Open File"
415 };
416
417 let request = match ashpd::desktop::file_chooser::OpenFileRequest::default()
418 .identifier(identifier.await)
419 .modal(true)
420 .title(title)
421 .accept_label(options.prompt.as_ref().map(gpui::SharedString::as_str))
422 .multiple(options.multiple)
423 .directory(options.directories)
424 .send()
425 .await
426 {
427 Ok(request) => request,
428 Err(err) => {
429 let result = match err {
430 ashpd::Error::PortalNotFound(_) => anyhow!(FILE_PICKER_PORTAL_MISSING),
431 err => err.into(),
432 };
433 let _ = done_tx.send(Err(result));
434 return;
435 }
436 };
437
438 let result = match request.response() {
439 Ok(response) => Ok(Some(
440 response
441 .uris()
442 .iter()
443 .filter_map(|uri: &ashpd::Uri| url::Url::parse(uri.as_str()).ok())
444 .filter_map(|uri: url::Url| uri.to_file_path().ok())
445 .collect::<Vec<_>>(),
446 )),
447 Err(ashpd::Error::Response(_)) => Ok(None),
448 Err(e) => Err(e.into()),
449 };
450 let _ = done_tx.send(result);
451 })
452 .detach();
453 done_rx
454 }
455
456 fn prompt_for_new_path(
457 &self,
458 directory: &Path,
459 suggested_name: Option<&str>,
460 ) -> oneshot::Receiver<Result<Option<PathBuf>>> {
461 let (done_tx, done_rx) = oneshot::channel();
462
463 #[cfg(not(any(feature = "wayland", feature = "x11")))]
464 let _ = (done_tx.send(Ok(None)), directory, suggested_name);
465
466 #[cfg(any(feature = "wayland", feature = "x11"))]
467 let identifier = self.inner.window_identifier();
468
469 #[cfg(any(feature = "wayland", feature = "x11"))]
470 self.foreground_executor()
471 .spawn({
472 let directory = directory.to_owned();
473 let suggested_name = suggested_name.map(|s| s.to_owned());
474
475 async move {
476 let mut request_builder =
477 ashpd::desktop::file_chooser::SaveFileRequest::default()
478 .identifier(identifier.await)
479 .modal(true)
480 .title("Save File")
481 .current_folder(directory)
482 .expect("pathbuf should not be nul terminated");
483
484 if let Some(suggested_name) = suggested_name {
485 request_builder = request_builder.current_name(suggested_name.as_str());
486 }
487
488 let request = match request_builder.send().await {
489 Ok(request) => request,
490 Err(err) => {
491 let result = match err {
492 ashpd::Error::PortalNotFound(_) => {
493 anyhow!(FILE_PICKER_PORTAL_MISSING)
494 }
495 err => err.into(),
496 };
497 let _ = done_tx.send(Err(result));
498 return;
499 }
500 };
501
502 let result = match request.response() {
503 Ok(response) => Ok(response
504 .uris()
505 .first()
506 .and_then(|uri: &ashpd::Uri| url::Url::parse(uri.as_str()).ok())
507 .and_then(|uri: url::Url| uri.to_file_path().ok())),
508 Err(ashpd::Error::Response(_)) => Ok(None),
509 Err(e) => Err(e.into()),
510 };
511 let _ = done_tx.send(result);
512 }
513 })
514 .detach();
515
516 done_rx
517 }
518
519 fn can_select_mixed_files_and_dirs(&self) -> bool {
520 // org.freedesktop.portal.FileChooser only supports "pick files" and "pick directories".
521 false
522 }
523
524 fn reveal_path(&self, path: &Path) {
525 self.inner.reveal_path(path.to_owned());
526 }
527
528 fn open_with_system(&self, path: &Path) {
529 let path = path.to_owned();
530 self.background_executor()
531 .spawn(async move {
532 #[allow(
533 clippy::disallowed_methods,
534 reason = "running on a background thread, so blocking is fine"
535 )]
536 new_std_command("xdg-open")
537 .arg(path)
538 .status()
539 .context("invoking xdg-open")
540 .log_err();
541 })
542 .detach();
543 }
544
545 fn on_quit(&self, callback: Box<dyn FnMut()>) {
546 self.inner.with_common(|common| {
547 common.callbacks.quit = Some(callback);
548 });
549 }
550
551 fn on_reopen(&self, callback: Box<dyn FnMut()>) {
552 self.inner.with_common(|common| {
553 common.callbacks.reopen = Some(callback);
554 });
555 }
556
557 fn on_system_wake(&self, callback: Box<dyn FnMut()>) {
558 self.inner.with_common(|common| {
559 common.callbacks.system_wake = Some(callback);
560 common.start_wake_listener();
561 });
562 }
563
564 fn set_app_identity(&self, _identifier: &str, name: &str) {
565 self.inner
566 .with_common(|common| common.app_name = Some(name.to_string()));
567 }
568
569 fn show_system_notification(&self, notification: gpui::SystemNotification) {
570 self.inner.with_common(|common| {
571 common
572 .system_notifications
573 .show(common.app_name.as_deref(), notification)
574 });
575 }
576
577 fn dismiss_system_notification(&self, tag: &str) {
578 self.inner
579 .with_common(|common| common.system_notifications.dismiss(tag));
580 }
581
582 fn on_system_notification_response(
583 &self,
584 callback: Box<dyn FnMut(gpui::SystemNotificationResponse)>,
585 ) {
586 self.inner.with_common(|common| {
587 let executor = common.foreground_executor.clone();
588 common.system_notifications.on_response(&executor, callback)
589 });
590 }
591
592 fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
593 self.inner.with_common(|common| {
594 common.callbacks.app_menu_action = Some(callback);
595 });
596 }
597
598 fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>) {
599 self.inner.with_common(|common| {
600 common.callbacks.will_open_app_menu = Some(callback);
601 });
602 }
603
604 fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
605 self.inner.with_common(|common| {
606 common.callbacks.validate_app_menu_command = Some(callback);
607 });
608 }
609
610 fn app_path(&self) -> Result<PathBuf> {
611 // get the path of the executable of the current process
612 let app_path = env::current_exe()?;
613 Ok(app_path)
614 }
615
616 fn set_menus(&self, menus: Vec<Menu>, _keymap: &Keymap) {
617 self.inner.with_common(|common| {
618 common.menus = menus.into_iter().map(|menu| menu.owned()).collect();
619 })
620 }
621
622 fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
623 self.inner.with_common(|common| Some(common.menus.clone()))
624 }
625
626 fn set_dock_menu(&self, _menu: Vec<MenuItem>, _keymap: &Keymap) {
627 // todo(linux)
628 }
629
630 fn path_for_auxiliary_executable(&self, _name: &str) -> Result<PathBuf> {
631 Err(anyhow::Error::msg(
632 "Platform<LinuxPlatform>::path_for_auxiliary_executable is not implemented yet",
633 ))
634 }
635
636 fn set_cursor_style(&self, style: CursorStyle) {
637 self.inner.set_cursor_style(style)
638 }
639
640 fn hide_cursor_until_mouse_moves(&self) {
641 self.inner.hide_cursor_until_mouse_moves()
642 }
643
644 fn is_cursor_visible(&self) -> bool {
645 self.inner.is_cursor_visible()
646 }
647
648 fn should_auto_hide_scrollbars(&self) -> bool {
649 self.inner.with_common(|common| common.auto_hide_scrollbars)
650 }
651
652 fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>> {
653 let url = url.to_string();
654 let username = username.to_string();
655 let password = password.to_vec();
656 self.background_executor().spawn(async move {
657 let keyring = oo7::Keyring::new().await?;
658 keyring.unlock().await?;
659 keyring
660 .create_item(
661 KEYRING_LABEL,
662 &vec![("url", &url), ("username", &username)],
663 password,
664 true,
665 )
666 .await?;
667 Ok(())
668 })
669 }
670
671 fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
672 let url = url.to_string();
673 self.background_executor().spawn(async move {
674 let keyring = oo7::Keyring::new().await?;
675 keyring.unlock().await?;
676
677 let items = keyring.search_items(&vec![("url", &url)]).await?;
678
679 for item in items.into_iter() {
680 if item.label().await.is_ok_and(|label| label == KEYRING_LABEL) {
681 let attributes = item.attributes().await?;
682 let username = attributes
683 .get("username")
684 .context("Cannot find username in stored credentials")?;
685 item.unlock().await?;
686 let secret = item.secret().await?;
687
688 // we lose the zeroizing capabilities at this boundary,
689 // a current limitation GPUI's credentials api
690 return Ok(Some((username.to_string(), secret.to_vec())));
691 } else {
692 continue;
693 }
694 }
695 Ok(None)
696 })
697 }
698
699 fn delete_credentials(&self, url: &str) -> Task<Result<()>> {
700 let url = url.to_string();
701 self.background_executor().spawn(async move {
702 let keyring = oo7::Keyring::new().await?;
703 keyring.unlock().await?;
704
705 let items = keyring.search_items(&vec![("url", &url)]).await?;
706
707 for item in items.into_iter() {
708 if item.label().await.is_ok_and(|label| label == KEYRING_LABEL) {
709 item.delete().await?;
710 return Ok(());
711 }
712 }
713
714 Ok(())
715 })
716 }
717
718 fn window_appearance(&self) -> WindowAppearance {
719 self.inner.with_common(|common| common.appearance)
720 }
721
722 fn button_layout(&self) -> Option<WindowButtonLayout> {
723 Some(self.inner.with_common(|common| common.button_layout))
724 }
725
726 fn register_url_scheme(&self, _: &str) -> Task<anyhow::Result<()>> {
727 Task::ready(Err(anyhow!("register_url_scheme unimplemented")))
728 }
729
730 fn write_to_primary(&self, item: ClipboardItem) {
731 self.inner.write_to_primary(item)
732 }
733
734 fn write_to_clipboard(&self, item: ClipboardItem) {
735 self.inner.write_to_clipboard(item)
736 }
737
738 fn read_from_primary(&self) -> Option<ClipboardItem> {
739 self.inner.read_from_primary()
740 }
741
742 fn read_from_clipboard(&self) -> Option<ClipboardItem> {
743 self.inner.read_from_clipboard()
744 }
745
746 fn add_recent_document(&self, _path: &Path) {}
747}
748
749#[cfg(any(feature = "wayland", feature = "x11"))]
750pub(super) fn open_uri_internal(
751 executor: BackgroundExecutor,
752 uri: &str,
753 activation_token: Option<String>,
754) {
755 if let Some(uri) = ashpd::Uri::parse(uri).log_err() {
756 executor
757 .spawn(async move {
758 let mut xdg_open_failed = false;
759 for mut command in open::commands(uri.to_string()) {
760 if let Some(token) = activation_token.as_ref() {
761 command.env("XDG_ACTIVATION_TOKEN", token);
762 }
763 let program = format!("{:?}", command.get_program());
764 match smol::process::Command::from(command).spawn() {
765 Ok(mut cmd) => match cmd.status().await {
766 Ok(status) if status.success() => return,
767 Ok(status) => {
768 log::error!("Command {} exited with status: {}", program, status);
769 xdg_open_failed = true;
770 }
771 Err(e) => {
772 log::error!("Failed to get status from {}: {}", program, e);
773 xdg_open_failed = true;
774 }
775 },
776 Err(e) => {
777 log::error!("Failed to open with {}: {}", program, e);
778 xdg_open_failed = true;
779 }
780 }
781 }
782
783 if xdg_open_failed {
784 match ashpd::desktop::open_uri::OpenFileRequest::default()
785 .activation_token(activation_token.map(ashpd::ActivationToken::from))
786 .send_uri(&uri)
787 .await
788 .and_then(|e| e.response())
789 {
790 Ok(()) => {}
791 Err(ashpd::Error::Response(ashpd::desktop::ResponseError::Cancelled)) => {}
792 Err(e) => {
793 log::error!("Failed to open with dbus: {}", e);
794 }
795 }
796 }
797 })
798 .detach();
799 }
800}
801
802#[cfg(any(feature = "x11", feature = "wayland"))]
803pub(super) fn reveal_path_internal(
804 executor: BackgroundExecutor,
805 path: PathBuf,
806 activation_token: Option<String>,
807) {
808 executor
809 .spawn(async move {
810 if let Some(dir) = File::open(path.clone()).log_err() {
811 match ashpd::desktop::open_uri::OpenDirectoryRequest::default()
812 .activation_token(activation_token.map(ashpd::ActivationToken::from))
813 .send(&dir.as_fd())
814 .await
815 {
816 Ok(_) => return,
817 Err(e) => log::error!("Failed to open with dbus: {}", e),
818 }
819 if path.is_dir() {
820 open::that_detached(path).log_err();
821 } else {
822 open::that_detached(path.parent().unwrap_or(Path::new(""))).log_err();
823 }
824 }
825 })
826 .detach();
827}
828
829#[cfg(any(feature = "wayland", feature = "x11"))]
830pub(super) fn is_within_click_distance(a: Point<Pixels>, b: Point<Pixels>) -> bool {
831 let diff = a - b;
832 diff.x.abs() <= DOUBLE_CLICK_DISTANCE && diff.y.abs() <= DOUBLE_CLICK_DISTANCE
833}
834
835#[cfg(any(feature = "wayland", feature = "x11"))]
836pub(super) fn get_xkb_compose_state(cx: &xkb::Context) -> Option<xkb::compose::State> {
837 let mut locales = Vec::default();
838 if let Some(locale) = env::var_os("LC_CTYPE") {
839 locales.push(locale);
840 }
841 locales.push(OsString::from("C"));
842 let mut state: Option<xkb::compose::State> = None;
843 for locale in locales {
844 if let Ok(table) =
845 xkb::compose::Table::new_from_locale(cx, &locale, xkb::compose::COMPILE_NO_FLAGS)
846 {
847 state = Some(xkb::compose::State::new(
848 &table,
849 xkb::compose::STATE_NO_FLAGS,
850 ));
851 break;
852 }
853 }
854 state
855}
856
857#[cfg(any(feature = "wayland", feature = "x11"))]
858pub(super) const PIPE_READ_TIMEOUT: Duration = Duration::from_secs(4);
859
860#[cfg(any(feature = "wayland", feature = "x11"))]
861pub(super) fn read_fd_with_timeout(
862 mut fd: filedescriptor::FileDescriptor,
863 timeout: Duration,
864) -> Result<Vec<u8>> {
865 fd.set_non_blocking(true)?;
866 let mut buffer = Vec::new();
867 let mut chunk = [0u8; 8192];
868 loop {
869 let mut poll_fds = [filedescriptor::pollfd {
870 fd: fd.as_raw_fd(),
871 events: filedescriptor::POLLIN,
872 revents: 0,
873 }];
874 let ready = match filedescriptor::poll(&mut poll_fds, Some(timeout)) {
875 Ok(ready) => ready,
876 Err(filedescriptor::Error::Poll(err))
877 if err.kind() == std::io::ErrorKind::Interrupted =>
878 {
879 continue;
880 }
881 Err(err) => return Err(err.into()),
882 };
883 if ready == 0 {
884 anyhow::bail!("timed out waiting for data on pipe after {timeout:?}");
885 }
886 match fd.read(&mut chunk) {
887 Ok(0) => return Ok(buffer),
888 Ok(len) => buffer.extend_from_slice(&chunk[..len]),
889 Err(err)
890 if err.kind() == std::io::ErrorKind::WouldBlock
891 || err.kind() == std::io::ErrorKind::Interrupted => {}
892 Err(err) => return Err(err.into()),
893 }
894 }
895}
896
897#[cfg(any(feature = "wayland", feature = "x11"))]
898pub(super) const DEFAULT_CURSOR_ICON_NAME: &str = "left_ptr";
899
900#[cfg(any(feature = "wayland", feature = "x11"))]
901pub(super) fn cursor_style_to_icon_names(style: CursorStyle) -> &'static [&'static str] {
902 // Based on cursor names from chromium:
903 // https://github.com/chromium/chromium/blob/d3069cf9c973dc3627fa75f64085c6a86c8f41bf/ui/base/cursor/cursor_factory.cc#L113
904 match style {
905 CursorStyle::Arrow => &[DEFAULT_CURSOR_ICON_NAME],
906 CursorStyle::IBeam => &["text", "xterm"],
907 CursorStyle::Crosshair => &["crosshair", "cross"],
908 CursorStyle::ClosedHand => &["closedhand", "grabbing", "hand2"],
909 CursorStyle::OpenHand => &["openhand", "grab", "hand1"],
910 CursorStyle::PointingHand => &["pointer", "hand", "hand2"],
911 CursorStyle::ResizeLeft => &["w-resize", "left_side"],
912 CursorStyle::ResizeRight => &["e-resize", "right_side"],
913 CursorStyle::ResizeLeftRight => &["ew-resize", "sb_h_double_arrow"],
914 CursorStyle::ResizeUp => &["n-resize", "top_side"],
915 CursorStyle::ResizeDown => &["s-resize", "bottom_side"],
916 CursorStyle::ResizeUpDown => &["sb_v_double_arrow", "ns-resize"],
917 CursorStyle::ResizeUpLeftDownRight => &["size_fdiag", "bd_double_arrow", "nwse-resize"],
918 CursorStyle::ResizeUpRightDownLeft => &["size_bdiag", "nesw-resize", "fd_double_arrow"],
919 CursorStyle::ResizeColumn => &["col-resize", "sb_h_double_arrow"],
920 CursorStyle::ResizeRow => &["row-resize", "sb_v_double_arrow"],
921 CursorStyle::IBeamCursorForVerticalLayout => &["vertical-text"],
922 CursorStyle::OperationNotAllowed => &["not-allowed", "crossed_circle"],
923 CursorStyle::DragLink => &["alias"],
924 CursorStyle::DragCopy => &["copy"],
925 CursorStyle::ContextualMenu => &["context-menu"],
926 }
927}
928
929#[cfg(any(feature = "wayland", feature = "x11"))]
930pub(super) fn log_cursor_icon_warning(message: impl std::fmt::Display) {
931 if let Ok(xcursor_path) = env::var("XCURSOR_PATH") {
932 log::warn!(
933 "{:#}\ncursor icon loading may be failing if XCURSOR_PATH environment variable is invalid. \
934 XCURSOR_PATH overrides the default icon search. Its current value is '{}'",
935 message,
936 xcursor_path
937 );
938 } else {
939 log::warn!("{:#}", message);
940 }
941}
942
943#[cfg(any(feature = "wayland", feature = "x11"))]
944fn guess_ascii(keycode: Keycode, shift: bool) -> Option<char> {
945 let c = match (keycode.raw(), shift) {
946 (24, _) => 'q',
947 (25, _) => 'w',
948 (26, _) => 'e',
949 (27, _) => 'r',
950 (28, _) => 't',
951 (29, _) => 'y',
952 (30, _) => 'u',
953 (31, _) => 'i',
954 (32, _) => 'o',
955 (33, _) => 'p',
956 (34, false) => '[',
957 (34, true) => '{',
958 (35, false) => ']',
959 (35, true) => '}',
960 (38, _) => 'a',
961 (39, _) => 's',
962 (40, _) => 'd',
963 (41, _) => 'f',
964 (42, _) => 'g',
965 (43, _) => 'h',
966 (44, _) => 'j',
967 (45, _) => 'k',
968 (46, _) => 'l',
969 (47, false) => ';',
970 (47, true) => ':',
971 (48, false) => '\'',
972 (48, true) => '"',
973 (49, false) => '`',
974 (49, true) => '~',
975 (51, false) => '\\',
976 (51, true) => '|',
977 (52, _) => 'z',
978 (53, _) => 'x',
979 (54, _) => 'c',
980 (55, _) => 'v',
981 (56, _) => 'b',
982 (57, _) => 'n',
983 (58, _) => 'm',
984 (59, false) => ',',
985 (59, true) => '>',
986 (60, false) => '.',
987 (60, true) => '<',
988 (61, false) => '/',
989 (61, true) => '?',
990
991 _ => return None,
992 };
993
994 Some(c)
995}
996
997#[cfg(any(feature = "wayland", feature = "x11"))]
998pub(super) fn keystroke_from_xkb(
999 state: &State,
1000 mut modifiers: gpui::Modifiers,
1001 keycode: Keycode,
1002) -> gpui::Keystroke {
1003 let key_utf32 = state.key_get_utf32(keycode);
1004 let key_utf8 = state.key_get_utf8(keycode);
1005 let key_sym = state.key_get_one_sym(keycode);
1006
1007 let key = match key_sym {
1008 Keysym::Return => "enter".to_owned(),
1009 Keysym::Prior => "pageup".to_owned(),
1010 Keysym::Next => "pagedown".to_owned(),
1011 Keysym::ISO_Left_Tab => "tab".to_owned(),
1012 Keysym::KP_Prior => "pageup".to_owned(),
1013 Keysym::KP_Next => "pagedown".to_owned(),
1014 Keysym::XF86_Back => "back".to_owned(),
1015 Keysym::XF86_Forward => "forward".to_owned(),
1016 Keysym::XF86_Cut => "cut".to_owned(),
1017 Keysym::XF86_Copy => "copy".to_owned(),
1018 Keysym::XF86_Paste => "paste".to_owned(),
1019 Keysym::XF86_New => "new".to_owned(),
1020 Keysym::XF86_Open => "open".to_owned(),
1021 Keysym::XF86_Save => "save".to_owned(),
1022
1023 Keysym::comma => ",".to_owned(),
1024 Keysym::period => ".".to_owned(),
1025 Keysym::less => "<".to_owned(),
1026 Keysym::greater => ">".to_owned(),
1027 Keysym::slash => "/".to_owned(),
1028 Keysym::question => "?".to_owned(),
1029
1030 Keysym::semicolon => ";".to_owned(),
1031 Keysym::colon => ":".to_owned(),
1032 Keysym::apostrophe => "'".to_owned(),
1033 Keysym::quotedbl => "\"".to_owned(),
1034
1035 Keysym::bracketleft => "[".to_owned(),
1036 Keysym::braceleft => "{".to_owned(),
1037 Keysym::bracketright => "]".to_owned(),
1038 Keysym::braceright => "}".to_owned(),
1039 Keysym::backslash => "\\".to_owned(),
1040 Keysym::bar => "|".to_owned(),
1041
1042 Keysym::grave => "`".to_owned(),
1043 Keysym::asciitilde => "~".to_owned(),
1044 Keysym::exclam => "!".to_owned(),
1045 Keysym::at => "@".to_owned(),
1046 Keysym::numbersign => "#".to_owned(),
1047 Keysym::dollar => "$".to_owned(),
1048 Keysym::percent => "%".to_owned(),
1049 Keysym::asciicircum => "^".to_owned(),
1050 Keysym::ampersand => "&".to_owned(),
1051 Keysym::asterisk => "*".to_owned(),
1052 Keysym::parenleft => "(".to_owned(),
1053 Keysym::parenright => ")".to_owned(),
1054 Keysym::minus => "-".to_owned(),
1055 Keysym::underscore => "_".to_owned(),
1056 Keysym::equal => "=".to_owned(),
1057 Keysym::plus => "+".to_owned(),
1058 Keysym::space => "space".to_owned(),
1059 Keysym::BackSpace => "backspace".to_owned(),
1060 Keysym::Tab => "tab".to_owned(),
1061 Keysym::Delete => "delete".to_owned(),
1062 Keysym::Escape => "escape".to_owned(),
1063
1064 Keysym::Left => "left".to_owned(),
1065 Keysym::Right => "right".to_owned(),
1066 Keysym::Up => "up".to_owned(),
1067 Keysym::Down => "down".to_owned(),
1068 Keysym::Home => "home".to_owned(),
1069 Keysym::End => "end".to_owned(),
1070 Keysym::Insert => "insert".to_owned(),
1071
1072 _ => {
1073 let name = xkb::keysym_get_name(key_sym).to_lowercase();
1074 if key_sym.is_keypad_key() {
1075 name.replace("kp_", "")
1076 } else if let Some(key) = key_utf8.chars().next()
1077 && key_utf8.len() == 1
1078 && key.is_ascii()
1079 {
1080 if key.is_ascii_graphic() {
1081 key_utf8.to_lowercase()
1082 // map ctrl-a to `a`
1083 // ctrl-0..9 may emit control codes like ctrl-[, but
1084 // we don't want to map them to `[`
1085 } else if key_utf32 <= 0x1f
1086 && !name.chars().next().is_some_and(|c| c.is_ascii_digit())
1087 {
1088 ((key_utf32 as u8 + 0x40) as char)
1089 .to_ascii_lowercase()
1090 .to_string()
1091 } else {
1092 name
1093 }
1094 } else if let Some(key_en) = guess_ascii(keycode, modifiers.shift) {
1095 String::from(key_en)
1096 } else {
1097 name
1098 }
1099 }
1100 };
1101
1102 if modifiers.shift {
1103 // we only include the shift for upper-case letters by convention,
1104 // so don't include for numbers and symbols, but do include for
1105 // tab/enter, etc.
1106 if key.chars().count() == 1 && key.to_lowercase() == key.to_uppercase() {
1107 modifiers.shift = false;
1108 }
1109 }
1110
1111 // Ignore control characters (and DEL) for the purposes of key_char
1112 let key_char =
1113 (key_utf32 >= 32 && key_utf32 != 127 && !key_utf8.is_empty()).then_some(key_utf8);
1114
1115 gpui::Keystroke {
1116 modifiers,
1117 key,
1118 key_char,
1119 }
1120}
1121
1122/**
1123 * Returns which symbol the dead key represents
1124 * <https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values#dead_keycodes_for_linux>
1125 */
1126#[cfg(any(feature = "wayland", feature = "x11"))]
1127pub fn keystroke_underlying_dead_key(keysym: Keysym) -> Option<String> {
1128 match keysym {
1129 Keysym::dead_grave => Some("`".to_owned()),
1130 Keysym::dead_acute => Some("´".to_owned()),
1131 Keysym::dead_circumflex => Some("^".to_owned()),
1132 Keysym::dead_tilde => Some("~".to_owned()),
1133 Keysym::dead_macron => Some("¯".to_owned()),
1134 Keysym::dead_breve => Some("˘".to_owned()),
1135 Keysym::dead_abovedot => Some("˙".to_owned()),
1136 Keysym::dead_diaeresis => Some("¨".to_owned()),
1137 Keysym::dead_abovering => Some("˚".to_owned()),
1138 Keysym::dead_doubleacute => Some("˝".to_owned()),
1139 Keysym::dead_caron => Some("ˇ".to_owned()),
1140 Keysym::dead_cedilla => Some("¸".to_owned()),
1141 Keysym::dead_ogonek => Some("˛".to_owned()),
1142 Keysym::dead_iota => Some("ͅ".to_owned()),
1143 Keysym::dead_voiced_sound => Some("゙".to_owned()),
1144 Keysym::dead_semivoiced_sound => Some("゚".to_owned()),
1145 Keysym::dead_belowdot => Some("̣̣".to_owned()),
1146 Keysym::dead_hook => Some("̡".to_owned()),
1147 Keysym::dead_horn => Some("̛".to_owned()),
1148 Keysym::dead_stroke => Some("̶̶".to_owned()),
1149 Keysym::dead_abovecomma => Some("̓̓".to_owned()),
1150 Keysym::dead_abovereversedcomma => Some("ʽ".to_owned()),
1151 Keysym::dead_doublegrave => Some("̏".to_owned()),
1152 Keysym::dead_belowring => Some("˳".to_owned()),
1153 Keysym::dead_belowmacron => Some("̱".to_owned()),
1154 Keysym::dead_belowcircumflex => Some("ꞈ".to_owned()),
1155 Keysym::dead_belowtilde => Some("̰".to_owned()),
1156 Keysym::dead_belowbreve => Some("̮".to_owned()),
1157 Keysym::dead_belowdiaeresis => Some("̤".to_owned()),
1158 Keysym::dead_invertedbreve => Some("̯".to_owned()),
1159 Keysym::dead_belowcomma => Some("̦".to_owned()),
1160 Keysym::dead_currency => None,
1161 Keysym::dead_lowline => None,
1162 Keysym::dead_aboveverticalline => None,
1163 Keysym::dead_belowverticalline => None,
1164 Keysym::dead_longsolidusoverlay => None,
1165 Keysym::dead_a => None,
1166 Keysym::dead_A => None,
1167 Keysym::dead_e => None,
1168 Keysym::dead_E => None,
1169 Keysym::dead_i => None,
1170 Keysym::dead_I => None,
1171 Keysym::dead_o => None,
1172 Keysym::dead_O => None,
1173 Keysym::dead_u => None,
1174 Keysym::dead_U => None,
1175 Keysym::dead_small_schwa => Some("ə".to_owned()),
1176 Keysym::dead_capital_schwa => Some("Ə".to_owned()),
1177 Keysym::dead_greek => None,
1178 _ => None,
1179 }
1180}
1181#[cfg(any(feature = "wayland", feature = "x11"))]
1182pub(super) fn modifiers_from_xkb(keymap_state: &State) -> gpui::Modifiers {
1183 let shift = keymap_state.mod_name_is_active(xkb::MOD_NAME_SHIFT, xkb::STATE_MODS_EFFECTIVE);
1184 let alt = keymap_state.mod_name_is_active(xkb::MOD_NAME_ALT, xkb::STATE_MODS_EFFECTIVE);
1185 let control = keymap_state.mod_name_is_active(xkb::MOD_NAME_CTRL, xkb::STATE_MODS_EFFECTIVE);
1186 let platform = keymap_state.mod_name_is_active(xkb::MOD_NAME_LOGO, xkb::STATE_MODS_EFFECTIVE);
1187 gpui::Modifiers {
1188 shift,
1189 alt,
1190 control,
1191 platform,
1192 function: false,
1193 }
1194}
1195
1196#[cfg(any(feature = "wayland", feature = "x11"))]
1197pub(super) fn capslock_from_xkb(keymap_state: &State) -> gpui::Capslock {
1198 let on = keymap_state.mod_name_is_active(xkb::MOD_NAME_CAPS, xkb::STATE_MODS_EFFECTIVE);
1199 gpui::Capslock { on }
1200}
1201
1202/// Resolve a Linux `dev_t` to PCI vendor/device IDs via sysfs, returning a
1203/// [`CompositorGpuHint`] that the GPU adapter selection code can use to
1204/// prioritize the compositor's rendering device.
1205#[cfg(any(feature = "wayland", feature = "x11"))]
1206pub(super) fn compositor_gpu_hint_from_dev_t(dev: u64) -> Option<gpui_wgpu::CompositorGpuHint> {
1207 fn dev_major(dev: u64) -> u32 {
1208 ((dev >> 8) & 0xfff) as u32 | (((dev >> 32) & !0xfff) as u32)
1209 }
1210
1211 fn dev_minor(dev: u64) -> u32 {
1212 (dev & 0xff) as u32 | (((dev >> 12) & !0xff) as u32)
1213 }
1214
1215 fn read_sysfs_hex_id(path: &str) -> Option<u32> {
1216 let content = std::fs::read_to_string(path).ok()?;
1217 let trimmed = content.trim().strip_prefix("0x").unwrap_or(content.trim());
1218 u32::from_str_radix(trimmed, 16).ok()
1219 }
1220
1221 let major = dev_major(dev);
1222 let minor = dev_minor(dev);
1223
1224 let vendor_path = format!("/sys/dev/char/{major}:{minor}/device/vendor");
1225 let device_path = format!("/sys/dev/char/{major}:{minor}/device/device");
1226
1227 let vendor_id = read_sysfs_hex_id(&vendor_path)?;
1228 let device_id = read_sysfs_hex_id(&device_path)?;
1229
1230 log::info!(
1231 "Compositor GPU hint: vendor={:#06x}, device={:#06x} (from dev {major}:{minor})",
1232 vendor_id,
1233 device_id,
1234 );
1235
1236 Some(gpui_wgpu::CompositorGpuHint {
1237 vendor_id,
1238 device_id,
1239 })
1240}
1241
1242#[cfg(test)]
1243mod tests {
1244 use super::*;
1245 use gpui::{Point, px};
1246
1247 #[test]
1248 fn test_is_within_click_distance() {
1249 let zero = Point::new(px(0.0), px(0.0));
1250 assert!(is_within_click_distance(zero, Point::new(px(5.0), px(5.0))));
1251 assert!(is_within_click_distance(
1252 zero,
1253 Point::new(px(-4.9), px(5.0))
1254 ));
1255 assert!(is_within_click_distance(
1256 Point::new(px(3.0), px(2.0)),
1257 Point::new(px(-2.0), px(-2.0))
1258 ));
1259 assert!(!is_within_click_distance(
1260 zero,
1261 Point::new(px(5.0), px(5.1))
1262 ),);
1263 }
1264
1265 #[cfg(any(feature = "wayland", feature = "x11"))]
1266 mod read_fd_with_timeout {
1267 use super::super::{PIPE_READ_TIMEOUT, read_fd_with_timeout};
1268 use std::io::Write as _;
1269 use std::time::{Duration, Instant};
1270
1271 #[test]
1272 fn reads_data_written_before_close() {
1273 let mut pipe = filedescriptor::Pipe::new().unwrap();
1274 pipe.write.write_all(b"hello clipboard").unwrap();
1275 drop(pipe.write);
1276
1277 let bytes = read_fd_with_timeout(pipe.read, PIPE_READ_TIMEOUT).unwrap();
1278 assert_eq!(bytes, b"hello clipboard");
1279 }
1280
1281 #[test]
1282 fn returns_empty_when_writer_closes_without_writing() {
1283 let pipe = filedescriptor::Pipe::new().unwrap();
1284 drop(pipe.write);
1285
1286 let bytes = read_fd_with_timeout(pipe.read, PIPE_READ_TIMEOUT).unwrap();
1287 assert!(bytes.is_empty());
1288 }
1289
1290 #[test]
1291 fn times_out_when_writer_never_writes() {
1292 let pipe = filedescriptor::Pipe::new().unwrap();
1293 let _open_writer = pipe.write;
1294
1295 let timeout = Duration::from_millis(50);
1296 let started = Instant::now();
1297 let result = read_fd_with_timeout(pipe.read, timeout);
1298 let elapsed = started.elapsed();
1299
1300 let err = result.unwrap_err();
1301 assert!(
1302 err.to_string().contains("timed out"),
1303 "unexpected error: {err}"
1304 );
1305 assert!(elapsed >= timeout, "returned before the timeout elapsed");
1306 }
1307
1308 #[test]
1309 fn times_out_when_writer_stalls_after_partial_write() {
1310 let mut pipe = filedescriptor::Pipe::new().unwrap();
1311 pipe.write.write_all(b"partial").unwrap();
1312 let _open_writer = pipe.write;
1313
1314 let err = read_fd_with_timeout(pipe.read, Duration::from_millis(50)).unwrap_err();
1315 assert!(
1316 err.to_string().contains("timed out"),
1317 "unexpected error: {err}"
1318 );
1319 }
1320
1321 #[test]
1322 fn slow_writer_resets_deadline_between_chunks() {
1323 let pipe = filedescriptor::Pipe::new().unwrap();
1324 let chunks = 12;
1325 let gap = Duration::from_millis(40);
1326 let timeout = Duration::from_millis(400);
1327
1328 let writer = std::thread::spawn({
1329 let mut write = pipe.write;
1330 move || {
1331 for _ in 0..chunks {
1332 std::thread::sleep(gap);
1333 write.write_all(&[b'x'; 1000]).unwrap();
1334 }
1335 }
1336 });
1337 // The total transfer (~480ms) exceeds the timeout; this only
1338 // passes because the timeout is re-armed per chunk.
1339 let bytes = read_fd_with_timeout(pipe.read, timeout).unwrap();
1340 writer.join().unwrap();
1341 assert_eq!(bytes, vec![b'x'; 1000 * chunks]);
1342 }
1343
1344 #[test]
1345 fn reads_payload_larger_than_pipe_capacity() {
1346 let pipe = filedescriptor::Pipe::new().unwrap();
1347 // Exceeds the 64 KiB pipe capacity, forcing the writer to block.
1348 let payload = vec![b'z'; 1024 * 1024];
1349
1350 let writer = std::thread::spawn({
1351 let mut write = pipe.write;
1352 let payload = payload.clone();
1353 move || write.write_all(&payload).unwrap()
1354 });
1355 let bytes = read_fd_with_timeout(pipe.read, PIPE_READ_TIMEOUT).unwrap();
1356 writer.join().unwrap();
1357 assert_eq!(bytes, payload);
1358 }
1359 }
1360}
1361