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 {
357 major: 1,
358 minor: 0,
359 patch: 0,
360 })
361 }
362
363 fn app_version(&self) -> Result<SemanticVersion> {
364 Ok(SemanticVersion {
365 major: 1,
366 minor: 0,
367 patch: 0,
368 })
369 }
370
371 fn app_path(&self) -> Result<PathBuf> {
372 // get the path of the executable of the current process
373 let exe_path = std::env::current_exe()?;
374 Ok(exe_path)
375 }
376
377 // todo(linux)
378 fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap) {}
379
380 fn local_timezone(&self) -> UtcOffset {
381 UtcOffset::UTC
382 }
383
384 //todo(linux)
385 fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
386 Err(anyhow::Error::msg(
387 "Platform<LinuxPlatform>::path_for_auxiliary_executable is not implemented yet",
388 ))
389 }
390
391 fn set_cursor_style(&self, style: CursorStyle) {
392 self.client.set_cursor_style(style)
393 }
394
395 // todo(linux)
396 fn should_auto_hide_scrollbars(&self) -> bool {
397 false
398 }
399
400 fn write_to_clipboard(&self, item: ClipboardItem) {
401 let clipboard = self.client.get_clipboard();
402 clipboard.borrow_mut().set_contents(item.text);
403 }
404
405 fn read_from_clipboard(&self) -> Option<ClipboardItem> {
406 let clipboard = self.client.get_clipboard();
407 let contents = clipboard.borrow_mut().get_contents();
408 match contents {
409 Ok(text) => Some(ClipboardItem {
410 metadata: None,
411 text,
412 }),
413 _ => None,
414 }
415 }
416
417 fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>> {
418 let url = url.to_string();
419 let username = username.to_string();
420 let password = password.to_vec();
421 self.background_executor().spawn(async move {
422 let keyring = oo7::Keyring::new().await?;
423 keyring.unlock().await?;
424 keyring
425 .create_item(
426 KEYRING_LABEL,
427 &vec![("url", &url), ("username", &username)],
428 password,
429 true,
430 )
431 .await?;
432 Ok(())
433 })
434 }
435
436 //todo(linux): add trait methods for accessing the primary selection
437 fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>> {
438 let url = url.to_string();
439 self.background_executor().spawn(async move {
440 let keyring = oo7::Keyring::new().await?;
441 keyring.unlock().await?;
442
443 let items = keyring.search_items(&vec![("url", &url)]).await?;
444
445 for item in items.into_iter() {
446 if item.label().await.is_ok_and(|label| label == KEYRING_LABEL) {
447 let attributes = item.attributes().await?;
448 let username = attributes
449 .get("username")
450 .ok_or_else(|| anyhow!("Cannot find username in stored credentials"))?;
451 let secret = item.secret().await?;
452
453 // we lose the zeroizing capabilities at this boundary,
454 // a current limitation GPUI's credentials api
455 return Ok(Some((username.to_string(), secret.to_vec())));
456 } else {
457 continue;
458 }
459 }
460 Ok(None)
461 })
462 }
463
464 fn delete_credentials(&self, url: &str) -> Task<Result<()>> {
465 let url = url.to_string();
466 self.background_executor().spawn(async move {
467 let keyring = oo7::Keyring::new().await?;
468 keyring.unlock().await?;
469
470 let items = keyring.search_items(&vec![("url", &url)]).await?;
471
472 for item in items.into_iter() {
473 if item.label().await.is_ok_and(|label| label == KEYRING_LABEL) {
474 item.delete().await?;
475 return Ok(());
476 }
477 }
478
479 Ok(())
480 })
481 }
482
483 fn window_appearance(&self) -> crate::WindowAppearance {
484 crate::WindowAppearance::Light
485 }
486
487 fn register_url_scheme(&self, _: &str) -> Task<anyhow::Result<()>> {
488 Task::ready(Err(anyhow!("register_url_scheme unimplemented")))
489 }
490}
491
492#[cfg(test)]
493mod tests {
494 use super::*;
495
496 fn build_platform() -> LinuxPlatform {
497 let platform = LinuxPlatform::new();
498 platform
499 }
500}