Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:47:26.186Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

platform.rs

1361 lines · 48.6 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.