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