diff --git a/Cargo.lock b/Cargo.lock index 8385e0adb2..6fcac40d20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -341,9 +341,8 @@ dependencies = [ [[package]] name = "ashpd" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd884d7c72877a94102c3715f3b1cd09ff4fac28221add3e57cfbe25c236d093" +version = "0.9.0" +source = "git+https://github.com/bilelmoussaoui/ashpd?rev=29f2e1a#29f2e1a6f4b0911f504658f5f4630c02e01b13f2" dependencies = [ "async-fs 2.1.1", "async-net 2.0.0", @@ -4893,7 +4892,6 @@ dependencies = [ "num_cpus", "objc", "oo7", - "open", "parking", "parking_lot", "pathfinder_geometry", @@ -5705,25 +5703,6 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" -[[package]] -name = "is-docker" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" -dependencies = [ - "once_cell", -] - -[[package]] -name = "is-wsl" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" -dependencies = [ - "is-docker", - "once_cell", -] - [[package]] name = "isahc" version = "1.7.2" @@ -7225,17 +7204,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" -[[package]] -name = "open" -version = "5.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "449f0ff855d85ddbf1edd5b646d65249ead3f5e422aaa86b7d2d0b049b103e32" -dependencies = [ - "is-wsl", - "libc", - "pathdiff", -] - [[package]] name = "open_ai" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 3561c95f7a..c4386f0303 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -272,9 +272,9 @@ zed_actions = { path = "crates/zed_actions" } alacritty_terminal = "0.23" any_vec = "0.13" anyhow = "1.0.57" -ashpd = "0.8.0" +ashpd = { git = "https://github.com/bilelmoussaoui/ashpd", rev = "29f2e1a" } async-compression = { version = "0.4", features = ["gzip", "futures-io"] } -async-dispatcher = { version = "0.1"} +async-dispatcher = { version = "0.1" } async-fs = "1.6" async-recursion = "1.0.0" async-tar = "0.4.2" @@ -315,7 +315,9 @@ image = "0.25.1" indexmap = { version = "1.6.2", features = ["serde"] } indoc = "1" # We explicitly disable http2 support in isahc. -isahc = { version = "1.7.2", default-features = false, features = [ "text-decoding" ] } +isahc = { version = "1.7.2", default-features = false, features = [ + "text-decoding", +] } itertools = "0.11.0" lazy_static = "1.4.0" libc = "0.2" @@ -341,7 +343,9 @@ rand = "0.8.5" refineable = { path = "./crates/refineable" } regex = "1.5" repair_json = "0.1.0" -runtimelib = { version="0.12", default-features = false, features = ["async-dispatcher-runtime"] } +runtimelib = { version = "0.12", default-features = false, features = [ + "async-dispatcher-runtime", +] } rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] } rust-embed = { version = "8.4", features = ["include-exclude"] } schemars = "0.8" diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 04f62f28e7..b71a10c07f 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -124,7 +124,6 @@ wayland-protocols = { version = "0.31.2", features = [ ] } wayland-protocols-plasma = { version = "0.2.0", features = ["client"] } oo7 = "0.3.0" -open = "5.1.2" filedescriptor = "0.8.2" x11rb = { version = "0.13.0", features = [ "allow-unsafe-code", diff --git a/crates/gpui/src/platform/linux/headless/client.rs b/crates/gpui/src/platform/linux/headless/client.rs index 7f9a623a6e..7abc19cc4c 100644 --- a/crates/gpui/src/platform/linux/headless/client.rs +++ b/crates/gpui/src/platform/linux/headless/client.rs @@ -81,6 +81,8 @@ impl LinuxClient for HeadlessClient { fn open_uri(&self, _uri: &str) {} + fn reveal_path(&self, _path: std::path::PathBuf) {} + fn write_to_primary(&self, _item: crate::ClipboardItem) {} fn write_to_clipboard(&self, _item: crate::ClipboardItem) {} diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index 0e46419b39..ad50290864 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -7,7 +7,7 @@ use std::ffi::OsString; use std::fs::File; use std::io::Read; use std::ops::{Deref, DerefMut}; -use std::os::fd::{AsRawFd, FromRawFd}; +use std::os::fd::{AsFd, AsRawFd, FromRawFd}; use std::panic::Location; use std::rc::Weak; use std::{ @@ -20,6 +20,8 @@ use std::{ use anyhow::anyhow; use ashpd::desktop::file_chooser::{OpenFileRequest, SaveFileRequest}; +use ashpd::desktop::open_uri::{OpenDirectoryRequest, OpenFileRequest as OpenUriRequest}; +use ashpd::{url, ActivationToken}; use async_task::Runnable; use calloop::channel::Channel; use calloop::{EventLoop, LoopHandle, LoopSignal}; @@ -67,6 +69,7 @@ pub trait LinuxClient { ) -> anyhow::Result>; fn set_cursor_style(&self, style: CursorStyle); fn open_uri(&self, uri: &str); + fn reveal_path(&self, path: PathBuf); fn write_to_primary(&self, item: ClipboardItem); fn write_to_clipboard(&self, item: ClipboardItem); fn read_from_primary(&self) -> Option; @@ -344,13 +347,7 @@ impl Platform for P { } fn reveal_path(&self, path: &Path) { - if path.is_dir() { - open::that_detached(path); - return; - } - // If `path` is a file, the system may try to open it in a text editor - let dir = path.parent().unwrap_or(Path::new("")); - open::that_detached(dir); + self.reveal_path(path.to_owned()); } fn on_quit(&self, callback: Box) { @@ -511,18 +508,40 @@ impl Platform for P { fn add_recent_document(&self, _path: &Path) {} } -pub(super) fn open_uri_internal(uri: &str, activation_token: Option<&str>) { - let mut last_err = None; - for mut command in open::commands(uri) { - if let Some(token) = activation_token { - command.env("XDG_ACTIVATION_TOKEN", token); - } - match command.spawn() { - Ok(_) => return, - Err(err) => last_err = Some(err), - } +pub(super) fn open_uri_internal( + executor: BackgroundExecutor, + uri: &str, + activation_token: Option, +) { + if let Some(uri) = url::Url::parse(uri).log_err() { + executor + .spawn(async move { + OpenUriRequest::default() + .activation_token(activation_token.map(ActivationToken::from)) + .send_uri(&uri) + .await + .log_err(); + }) + .detach(); } - log::error!("failed to open uri: {uri:?}, last error: {last_err:?}"); +} + +pub(super) fn reveal_path_internal( + executor: BackgroundExecutor, + path: PathBuf, + activation_token: Option, +) { + executor + .spawn(async move { + if let Some(dir) = File::open(path).log_err() { + OpenDirectoryRequest::default() + .activation_token(activation_token.map(ActivationToken::from)) + .send(&dir.as_fd()) + .await + .log_err(); + } + }) + .detach(); } pub(super) fn is_within_click_distance(a: Point, b: Point) -> bool { diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index ab3573540a..00d2cd540f 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -61,7 +61,6 @@ use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blu use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1; use xkbcommon::xkb::{self, Keycode, KEYMAP_COMPILE_NO_FLAGS}; -use super::super::{open_uri_internal, read_fd, DOUBLE_CLICK_INTERVAL}; use super::display::WaylandDisplay; use super::window::{ImeInput, WaylandWindowStatePtr}; use crate::platform::linux::wayland::clipboard::{ @@ -72,11 +71,14 @@ use crate::platform::linux::wayland::serial::{SerialKind, SerialTracker}; use crate::platform::linux::wayland::window::WaylandWindow; use crate::platform::linux::xdg_desktop_portal::{Event as XDPEvent, XDPEventSource}; use crate::platform::linux::LinuxClient; -use crate::platform::linux::{get_xkb_compose_state, is_within_click_distance}; +use crate::platform::linux::{ + get_xkb_compose_state, is_within_click_distance, open_uri_internal, read_fd, + reveal_path_internal, +}; use crate::platform::PlatformWindow; use crate::{ point, px, size, Bounds, DevicePixels, FileDropEvent, ForegroundExecutor, MouseExitEvent, Size, - SCROLL_LINES, + DOUBLE_CLICK_INTERVAL, SCROLL_LINES, }; use crate::{ AnyWindowHandle, CursorStyle, DisplayId, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, @@ -220,7 +222,7 @@ pub(crate) struct WaylandClientState { data_offers: Vec>, primary_data_offer: Option>, cursor: Cursor, - pending_open_uri: Option, + pending_activation: Option, event_loop: Option>, common: LinuxCommon, } @@ -244,6 +246,15 @@ pub(crate) struct KeyRepeat { current_keycode: Option, } +pub(crate) enum PendingActivation { + /// URI to open in the web browser. + Uri(String), + /// Path to open in the file explorer. + Path(PathBuf), + /// A window from ourselves to raise. + Window(ObjectId), +} + /// This struct is required to conform to Rust's orphan rules, so we can dispatch on the state but hand the /// window to GPUI. #[derive(Clone)] @@ -260,6 +271,11 @@ impl WaylandClientStatePtr { self.0.upgrade().unwrap().borrow().serial_tracker.get(kind) } + pub fn set_pending_activation(&self, window: ObjectId) { + self.0.upgrade().unwrap().borrow_mut().pending_activation = + Some(PendingActivation::Window(window)); + } + pub fn enable_ime(&self) { let client = self.get_client(); let mut state = client.borrow_mut(); @@ -530,7 +546,7 @@ impl WaylandClient { data_offers: Vec::new(), primary_data_offer: None, cursor, - pending_open_uri: None, + pending_activation: None, event_loop: Some(event_loop), })); @@ -629,14 +645,33 @@ impl LinuxClient for WaylandClient { state.globals.activation.clone(), state.mouse_focused_window.clone(), ) { - state.pending_open_uri = Some(uri.to_owned()); + state.pending_activation = Some(PendingActivation::Uri(uri.to_string())); let token = activation.get_activation_token(&state.globals.qh, ()); let serial = state.serial_tracker.get(SerialKind::MousePress); token.set_serial(serial, &state.wl_seat); token.set_surface(&window.surface()); token.commit(); } else { - open_uri_internal(uri, None); + let executor = state.common.background_executor.clone(); + open_uri_internal(executor, uri, None); + } + } + + fn reveal_path(&self, path: PathBuf) { + let mut state = self.0.borrow_mut(); + if let (Some(activation), Some(window)) = ( + state.globals.activation.clone(), + state.mouse_focused_window.clone(), + ) { + state.pending_activation = Some(PendingActivation::Path(path)); + let token = activation.get_activation_token(&state.globals.qh, ()); + let serial = state.serial_tracker.get(SerialKind::MousePress); + token.set_serial(serial, &state.wl_seat); + token.set_surface(&window.surface()); + token.commit(); + } else { + let executor = state.common.background_executor.clone(); + reveal_path_internal(executor, path, None); } } @@ -954,13 +989,25 @@ impl Dispatch for WaylandClie ) { let client = this.get_client(); let mut state = client.borrow_mut(); + if let xdg_activation_token_v1::Event::Done { token } = event { - if let Some(uri) = state.pending_open_uri.take() { - open_uri_internal(&uri, Some(&token)); - } else { - log::error!("called while pending_open_uri is None"); + let executor = state.common.background_executor.clone(); + match state.pending_activation.take() { + Some(PendingActivation::Uri(uri)) => open_uri_internal(executor, &uri, Some(token)), + Some(PendingActivation::Path(path)) => { + reveal_path_internal(executor, path, Some(token)) + } + Some(PendingActivation::Window(window)) => { + let Some(window) = get_window(&mut state, &window) else { + return; + }; + let activation = state.globals.activation.as_ref().unwrap(); + activation.activate(token, &window.surface()); + } + None => log::error!("activation token received with no pending activation"), } } + token.destroy(); } } diff --git a/crates/gpui/src/platform/linux/wayland/window.rs b/crates/gpui/src/platform/linux/wayland/window.rs index 70d6ecec1b..6acb63815a 100644 --- a/crates/gpui/src/platform/linux/wayland/window.rs +++ b/crates/gpui/src/platform/linux/wayland/window.rs @@ -76,6 +76,7 @@ pub struct WaylandWindowState { acknowledged_first_configure: bool, pub surface: wl_surface::WlSurface, decoration: Option, + app_id: Option, appearance: WindowAppearance, blur: Option, toplevel: xdg_toplevel::XdgToplevel, @@ -158,6 +159,7 @@ impl WaylandWindowState { acknowledged_first_configure: false, surface, decoration, + app_id: None, blur: None, toplevel, viewport, @@ -823,7 +825,20 @@ impl PlatformWindow for WaylandWindow { } fn activate(&self) { - log::info!("Wayland does not support this API"); + // Try to request an activation token. Even though the activation is likely going to be rejected, + // KWin and Mutter can use the app_id to visually indicate we're requesting attention. + let state = self.borrow(); + if let (Some(activation), Some(app_id)) = (&state.globals.activation, state.app_id.clone()) + { + state.client.set_pending_activation(state.surface.id()); + let token = activation.get_activation_token(&state.globals.qh, ()); + // The serial isn't exactly important here, since the activation is probably going to be rejected anyway. + let serial = state.client.get_serial(SerialKind::MousePress); + token.set_app_id(app_id); + token.set_serial(serial, &state.globals.seat); + token.set_surface(&state.surface); + token.commit(); + } } fn is_active(&self) -> bool { @@ -835,7 +850,9 @@ impl PlatformWindow for WaylandWindow { } fn set_app_id(&mut self, app_id: &str) { - self.borrow().toplevel.set_app_id(app_id.to_owned()); + let mut state = self.borrow_mut(); + state.toplevel.set_app_id(app_id.to_owned()); + state.app_id = Some(app_id.to_owned()); } fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) { diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index 60f9b2202f..e867ebeef3 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -2,6 +2,7 @@ use std::cell::RefCell; use std::collections::HashSet; use std::ops::Deref; use std::os::fd::AsRawFd; +use std::path::PathBuf; use std::rc::{Rc, Weak}; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -33,19 +34,18 @@ use crate::platform::linux::LinuxClient; use crate::platform::{LinuxCommon, PlatformWindow}; use crate::{ modifiers_from_xinput_info, point, px, AnyWindowHandle, Bounds, ClipboardItem, CursorStyle, - DisplayId, Keystroke, Modifiers, ModifiersChangedEvent, Pixels, PlatformDisplay, PlatformInput, - Point, QuitSignal, ScrollDelta, Size, TouchPhase, WindowParams, X11Window, + DisplayId, Keystroke, Modifiers, ModifiersChangedEvent, Pixels, Platform, PlatformDisplay, + PlatformInput, Point, QuitSignal, ScrollDelta, Size, TouchPhase, WindowParams, X11Window, }; -use super::{ - super::{get_xkb_compose_state, open_uri_internal, SCROLL_LINES}, - X11Display, X11WindowStatePtr, XcbAtoms, -}; use super::{button_of_key, modifiers_from_state, pressed_button_from_mask}; +use super::{X11Display, X11WindowStatePtr, XcbAtoms}; use super::{XimCallbackEvent, XimHandler}; -use crate::platform::linux::is_within_click_distance; -use crate::platform::linux::platform::DOUBLE_CLICK_INTERVAL; +use crate::platform::linux::platform::{DOUBLE_CLICK_INTERVAL, SCROLL_LINES}; use crate::platform::linux::xdg_desktop_portal::{Event as XDPEvent, XDPEventSource}; +use crate::platform::linux::{ + get_xkb_compose_state, is_within_click_distance, open_uri_internal, reveal_path_internal, +}; pub(super) const XINPUT_MASTER_DEVICE: u16 = 1; @@ -1100,7 +1100,11 @@ impl LinuxClient for X11Client { } fn open_uri(&self, uri: &str) { - open_uri_internal(uri, None); + open_uri_internal(self.background_executor(), uri, None); + } + + fn reveal_path(&self, path: PathBuf) { + reveal_path_internal(self.background_executor(), path, None); } fn write_to_primary(&self, item: crate::ClipboardItem) {