platform.rs

  1#![allow(unused)]
  2
  3use std::cell::RefCell;
  4use std::env;
  5use std::{
  6    path::{Path, PathBuf},
  7    process::Command,
  8    rc::Rc,
  9    sync::Arc,
 10    time::Duration,
 11};
 12
 13use anyhow::anyhow;
 14use ashpd::desktop::file_chooser::{OpenFileRequest, SaveFileRequest};
 15use async_task::Runnable;
 16use calloop::{EventLoop, LoopHandle, LoopSignal};
 17use flume::{Receiver, Sender};
 18use futures::channel::oneshot;
 19use parking_lot::Mutex;
 20use time::UtcOffset;
 21use wayland_client::Connection;
 22
 23use crate::platform::linux::client::Client;
 24use crate::platform::linux::wayland::WaylandClient;
 25use crate::{
 26    px, Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
 27    ForegroundExecutor, Keymap, LinuxDispatcher, LinuxTextSystem, Menu, PathPromptOptions, Pixels,
 28    Platform, PlatformDisplay, PlatformInput, PlatformTextSystem, PlatformWindow, Result,
 29    SemanticVersion, Task, WindowOptions, WindowParams,
 30};
 31
 32use super::x11::X11Client;
 33
 34pub(super) const SCROLL_LINES: f64 = 3.0;
 35
 36// Values match the defaults on GTK.
 37// Taken from https://github.com/GNOME/gtk/blob/main/gtk/gtksettings.c#L320
 38pub(super) const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(400);
 39pub(super) const DOUBLE_CLICK_DISTANCE: Pixels = px(5.0);
 40
 41#[derive(Default)]
 42pub(crate) struct Callbacks {
 43    open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
 44    become_active: Option<Box<dyn FnMut()>>,
 45    resign_active: Option<Box<dyn FnMut()>>,
 46    quit: Option<Box<dyn FnMut()>>,
 47    reopen: Option<Box<dyn FnMut()>>,
 48    event: Option<Box<dyn FnMut(PlatformInput) -> bool>>,
 49    app_menu_action: Option<Box<dyn FnMut(&dyn Action)>>,
 50    will_open_app_menu: Option<Box<dyn FnMut()>>,
 51    validate_app_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
 52}
 53
 54pub(crate) struct LinuxPlatformInner {
 55    pub(crate) event_loop: RefCell<EventLoop<'static, ()>>,
 56    pub(crate) loop_handle: Rc<LoopHandle<'static, ()>>,
 57    pub(crate) loop_signal: LoopSignal,
 58    pub(crate) background_executor: BackgroundExecutor,
 59    pub(crate) foreground_executor: ForegroundExecutor,
 60    pub(crate) text_system: Arc<LinuxTextSystem>,
 61    pub(crate) callbacks: RefCell<Callbacks>,
 62}
 63
 64pub(crate) struct LinuxPlatform {
 65    client: Rc<dyn Client>,
 66    inner: Rc<LinuxPlatformInner>,
 67}
 68
 69impl Default for LinuxPlatform {
 70    fn default() -> Self {
 71        Self::new()
 72    }
 73}
 74
 75impl LinuxPlatform {
 76    pub(crate) fn new() -> Self {
 77        let wayland_display = env::var_os("WAYLAND_DISPLAY");
 78        let use_wayland = wayland_display.is_some_and(|display| !display.is_empty());
 79
 80        let (main_sender, main_receiver) = calloop::channel::channel::<Runnable>();
 81        let text_system = Arc::new(LinuxTextSystem::new());
 82        let callbacks = RefCell::new(Callbacks::default());
 83
 84        let event_loop = EventLoop::try_new().unwrap();
 85        event_loop
 86            .handle()
 87            .insert_source(main_receiver, |event, _, _| {
 88                if let calloop::channel::Event::Msg(runnable) = event {
 89                    runnable.run();
 90                }
 91            });
 92
 93        let dispatcher = Arc::new(LinuxDispatcher::new(main_sender));
 94
 95        let inner = Rc::new(LinuxPlatformInner {
 96            loop_handle: Rc::new(event_loop.handle()),
 97            loop_signal: event_loop.get_signal(),
 98            event_loop: RefCell::new(event_loop),
 99            background_executor: BackgroundExecutor::new(dispatcher.clone()),
100            foreground_executor: ForegroundExecutor::new(dispatcher.clone()),
101            text_system,
102            callbacks,
103        });
104
105        if use_wayland {
106            Self {
107                client: Rc::new(WaylandClient::new(Rc::clone(&inner))),
108                inner,
109            }
110        } else {
111            Self {
112                client: X11Client::new(Rc::clone(&inner)),
113                inner,
114            }
115        }
116    }
117}
118
119const KEYRING_LABEL: &str = "zed-github-account";
120
121impl Platform for LinuxPlatform {
122    fn background_executor(&self) -> BackgroundExecutor {
123        self.inner.background_executor.clone()
124    }
125
126    fn foreground_executor(&self) -> ForegroundExecutor {
127        self.inner.foreground_executor.clone()
128    }
129
130    fn text_system(&self) -> Arc<dyn PlatformTextSystem> {
131        self.inner.text_system.clone()
132    }
133
134    fn run(&self, on_finish_launching: Box<dyn FnOnce()>) {
135        on_finish_launching();
136
137        self.inner
138            .event_loop
139            .borrow_mut()
140            .run(None, &mut (), |&mut ()| {})
141            .expect("Run loop failed");
142
143        if let Some(mut fun) = self.inner.callbacks.borrow_mut().quit.take() {
144            fun();
145        }
146    }
147
148    fn quit(&self) {
149        self.inner.loop_signal.stop();
150    }
151
152    fn restart(&self) {
153        use std::os::unix::process::CommandExt as _;
154
155        // get the process id of the current process
156        let app_pid = std::process::id().to_string();
157        // get the path to the executable
158        let app_path = match self.app_path() {
159            Ok(path) => path,
160            Err(err) => {
161                log::error!("Failed to get app path: {:?}", err);
162                return;
163            }
164        };
165
166        // script to wait for the current process to exit  and then restart the app
167        let script = format!(
168            r#"
169            while kill -O {pid} 2>/dev/null; do
170                sleep 0.1
171            done
172            {app_path}
173            "#,
174            pid = app_pid,
175            app_path = app_path.display()
176        );
177
178        // execute the script using /bin/bash
179        let restart_process = Command::new("/bin/bash")
180            .arg("-c")
181            .arg(script)
182            .process_group(0)
183            .spawn();
184
185        match restart_process {
186            Ok(_) => self.quit(),
187            Err(e) => log::error!("failed to spawn restart script: {:?}", e),
188        }
189    }
190
191    // todo(linux)
192    fn activate(&self, ignoring_other_apps: bool) {}
193
194    // todo(linux)
195    fn hide(&self) {}
196
197    // todo(linux)
198    fn hide_other_apps(&self) {}
199
200    // todo(linux)
201    fn unhide_other_apps(&self) {}
202
203    fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
204        self.client.primary_display()
205    }
206
207    fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
208        self.client.displays()
209    }
210
211    fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
212        self.client.display(id)
213    }
214
215    // todo(linux)
216    fn active_window(&self) -> Option<AnyWindowHandle> {
217        None
218    }
219
220    fn open_window(
221        &self,
222        handle: AnyWindowHandle,
223        options: WindowParams,
224    ) -> Box<dyn PlatformWindow> {
225        self.client.open_window(handle, options)
226    }
227
228    fn open_url(&self, url: &str) {
229        open::that(url);
230    }
231
232    fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
233        self.inner.callbacks.borrow_mut().open_urls = Some(callback);
234    }
235
236    fn prompt_for_paths(
237        &self,
238        options: PathPromptOptions,
239    ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
240        let (done_tx, done_rx) = oneshot::channel();
241        self.inner
242            .foreground_executor
243            .spawn(async move {
244                let title = if options.multiple {
245                    if !options.files {
246                        "Open folders"
247                    } else {
248                        "Open files"
249                    }
250                } else {
251                    if !options.files {
252                        "Open folder"
253                    } else {
254                        "Open file"
255                    }
256                };
257
258                let result = OpenFileRequest::default()
259                    .modal(true)
260                    .title(title)
261                    .accept_label("Select")
262                    .multiple(options.multiple)
263                    .directory(options.directories)
264                    .send()
265                    .await
266                    .ok()
267                    .and_then(|request| request.response().ok())
268                    .and_then(|response| {
269                        response
270                            .uris()
271                            .iter()
272                            .map(|uri| uri.to_file_path().ok())
273                            .collect()
274                    });
275
276                done_tx.send(result);
277            })
278            .detach();
279        done_rx
280    }
281
282    fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>> {
283        let (done_tx, done_rx) = oneshot::channel();
284        let directory = directory.to_owned();
285        self.inner
286            .foreground_executor
287            .spawn(async move {
288                let result = SaveFileRequest::default()
289                    .modal(true)
290                    .title("Select new path")
291                    .accept_label("Accept")
292                    .send()
293                    .await
294                    .ok()
295                    .and_then(|request| request.response().ok())
296                    .and_then(|response| {
297                        response
298                            .uris()
299                            .first()
300                            .and_then(|uri| uri.to_file_path().ok())
301                    });
302
303                done_tx.send(result);
304            })
305            .detach();
306        done_rx
307    }
308
309    fn reveal_path(&self, path: &Path) {
310        if path.is_dir() {
311            open::that(path);
312            return;
313        }
314        // If `path` is a file, the system may try to open it in a text editor
315        let dir = path.parent().unwrap_or(Path::new(""));
316        open::that(dir);
317    }
318
319    fn on_become_active(&self, callback: Box<dyn FnMut()>) {
320        self.inner.callbacks.borrow_mut().become_active = Some(callback);
321    }
322
323    fn on_resign_active(&self, callback: Box<dyn FnMut()>) {
324        self.inner.callbacks.borrow_mut().resign_active = Some(callback);
325    }
326
327    fn on_quit(&self, callback: Box<dyn FnMut()>) {
328        self.inner.callbacks.borrow_mut().quit = Some(callback);
329    }
330
331    fn on_reopen(&self, callback: Box<dyn FnMut()>) {
332        self.inner.callbacks.borrow_mut().reopen = Some(callback);
333    }
334
335    fn on_event(&self, callback: Box<dyn FnMut(PlatformInput) -> bool>) {
336        self.inner.callbacks.borrow_mut().event = Some(callback);
337    }
338
339    fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
340        self.inner.callbacks.borrow_mut().app_menu_action = Some(callback);
341    }
342
343    fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>) {
344        self.inner.callbacks.borrow_mut().will_open_app_menu = Some(callback);
345    }
346
347    fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
348        self.inner.callbacks.borrow_mut().validate_app_menu_command = Some(callback);
349    }
350
351    fn os_name(&self) -> &'static str {
352        "Linux"
353    }
354
355    fn os_version(&self) -> Result<SemanticVersion> {
356        Ok(SemanticVersion::new(1, 0, 0))
357    }
358
359    fn app_version(&self) -> Result<SemanticVersion> {
360        Ok(SemanticVersion::new(1, 0, 0))
361    }
362
363    fn app_path(&self) -> Result<PathBuf> {
364        // get the path of the executable of the current process
365        let exe_path = std::env::current_exe()?;
366        Ok(exe_path)
367    }
368
369    // todo(linux)
370    fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap) {}
371
372    fn local_timezone(&self) -> UtcOffset {
373        UtcOffset::UTC
374    }
375
376    //todo(linux)
377    fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
378        Err(anyhow::Error::msg(
379            "Platform<LinuxPlatform>::path_for_auxiliary_executable is not implemented yet",
380        ))
381    }
382
383    fn set_cursor_style(&self, style: CursorStyle) {
384        self.client.set_cursor_style(style)
385    }
386
387    // todo(linux)
388    fn should_auto_hide_scrollbars(&self) -> bool {
389        false
390    }
391
392    fn write_to_clipboard(&self, item: ClipboardItem) {
393        let clipboard = self.client.get_clipboard();
394        clipboard.borrow_mut().set_contents(item.text);
395    }
396
397    fn read_from_clipboard(&self) -> Option<ClipboardItem> {
398        let clipboard = self.client.get_clipboard();
399        let contents = clipboard.borrow_mut().get_contents();
400        match contents {
401            Ok(text) => Some(ClipboardItem {
402                metadata: None,
403                text,
404            }),
405            _ => None,
406        }
407    }
408
409    fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>> {
410        let url = url.to_string();
411        let username = username.to_string();
412        let password = password.to_vec();
413        self.background_executor().spawn(async move {
414            let keyring = oo7::Keyring::new().await?;
415            keyring.unlock().await?;
416            keyring
417                .create_item(
418                    KEYRING_LABEL,
419                    &vec![("url", &url), ("username", &username)],
420                    password,
421                    true,
422                )
423                .await?;
424            Ok(())
425        })
426    }
427
428    //todo(linux): add trait methods for accessing the primary selection
429    fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
430        let url = url.to_string();
431        self.background_executor().spawn(async move {
432            let keyring = oo7::Keyring::new().await?;
433            keyring.unlock().await?;
434
435            let items = keyring.search_items(&vec![("url", &url)]).await?;
436
437            for item in items.into_iter() {
438                if item.label().await.is_ok_and(|label| label == KEYRING_LABEL) {
439                    let attributes = item.attributes().await?;
440                    let username = attributes
441                        .get("username")
442                        .ok_or_else(|| anyhow!("Cannot find username in stored credentials"))?;
443                    let secret = item.secret().await?;
444
445                    // we lose the zeroizing capabilities at this boundary,
446                    // a current limitation GPUI's credentials api
447                    return Ok(Some((username.to_string(), secret.to_vec())));
448                } else {
449                    continue;
450                }
451            }
452            Ok(None)
453        })
454    }
455
456    fn delete_credentials(&self, url: &str) -> Task<Result<()>> {
457        let url = url.to_string();
458        self.background_executor().spawn(async move {
459            let keyring = oo7::Keyring::new().await?;
460            keyring.unlock().await?;
461
462            let items = keyring.search_items(&vec![("url", &url)]).await?;
463
464            for item in items.into_iter() {
465                if item.label().await.is_ok_and(|label| label == KEYRING_LABEL) {
466                    item.delete().await?;
467                    return Ok(());
468                }
469            }
470
471            Ok(())
472        })
473    }
474
475    fn window_appearance(&self) -> crate::WindowAppearance {
476        crate::WindowAppearance::Light
477    }
478
479    fn register_url_scheme(&self, _: &str) -> Task<anyhow::Result<()>> {
480        Task::ready(Err(anyhow!("register_url_scheme unimplemented")))
481    }
482}
483
484#[cfg(test)]
485mod tests {
486    use super::*;
487
488    fn build_platform() -> LinuxPlatform {
489        let platform = LinuxPlatform::new();
490        platform
491    }
492}