platform.rs

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