From cc0032d3261a7e60232908a2eac6530965125d89 Mon Sep 17 00:00:00 2001 From: Nihal <121309701+nihalxkumar@users.noreply.github.com> Date: Wed, 11 Mar 2026 23:34:32 +0530 Subject: [PATCH 01/16] gpui_linux: Implement D-Bus menu integration for Wayland --- Cargo.lock | 19 +- Cargo.toml | 4 - crates/gpui_linux/Cargo.toml | 5 + crates/gpui_linux/src/linux.rs | 2 + crates/gpui_linux/src/linux/dbusmenu.rs | 509 ++++++++++++++++++ crates/gpui_linux/src/linux/platform.rs | 8 + crates/gpui_linux/src/linux/wayland.rs | 2 + .../gpui_linux/src/linux/wayland/appmenu.rs | 21 + .../gpui_linux/src/linux/wayland/appmenu.xml | 40 ++ crates/gpui_linux/src/linux/wayland/client.rs | 136 ++++- 10 files changed, 734 insertions(+), 12 deletions(-) create mode 100644 crates/gpui_linux/src/linux/dbusmenu.rs create mode 100644 crates/gpui_linux/src/linux/wayland/appmenu.rs create mode 100644 crates/gpui_linux/src/linux/wayland/appmenu.xml diff --git a/Cargo.lock b/Cargo.lock index 6570398f5b2..e8008f5363f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7682,6 +7682,7 @@ dependencies = [ "smol", "strum 0.27.2", "swash", + "tokio", "url", "util", "uuid", @@ -7691,9 +7692,11 @@ dependencies = [ "wayland-protocols", "wayland-protocols-plasma", "wayland-protocols-wlr", + "wayland-scanner", "x11-clipboard", "x11rb", "xkbcommon", + "zbus", "zed-scap", "zed-xim", ] @@ -13578,18 +13581,18 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.37.5" +version = "0.38.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" dependencies = [ "memchr", ] [[package]] name = "quick-xml" -version = "0.38.3" +version = "0.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" +checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" dependencies = [ "memchr", ] @@ -17692,6 +17695,7 @@ dependencies = [ "signal-hook-registry", "socket2 0.6.1", "tokio-macros", + "tracing", "windows-sys 0.61.2", ] @@ -19811,12 +19815,12 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.7" +version = "0.31.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +checksum = "c86287151a309799b821ca709b7345a048a2956af05957c89cb824ab919fa4e3" dependencies = [ "proc-macro2", - "quick-xml 0.37.5", + "quick-xml 0.39.2", "quote", ] @@ -21718,6 +21722,7 @@ dependencies = [ "rustix 1.1.2", "serde", "serde_repr", + "tokio", "tracing", "uds_windows", "uuid", diff --git a/Cargo.toml b/Cargo.toml index 36e7ca8cc71..b9af56e1488 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -459,7 +459,6 @@ web_search_providers = { path = "crates/web_search_providers" } workspace = { path = "crates/workspace" } worktree = { path = "crates/worktree" } x_ai = { path = "crates/x_ai" } -zed = { path = "crates/zed" } zed_actions = { path = "crates/zed_actions" } zed_env_vars = { path = "crates/zed_env_vars" } edit_prediction = { path = "crates/edit_prediction" } @@ -915,9 +914,6 @@ debug = "limited" lto = "thin" codegen-units = 1 -[profile.release.package] -zed = { codegen-units = 16 } - [profile.release-fast] inherits = "release" debug = "full" diff --git a/crates/gpui_linux/Cargo.toml b/crates/gpui_linux/Cargo.toml index 9078fa82c28..89985e33109 100644 --- a/crates/gpui_linux/Cargo.toml +++ b/crates/gpui_linux/Cargo.toml @@ -129,3 +129,8 @@ xim = { git = "https://github.com/zed-industries/xim-rs.git", rev = "16f35a2c881 "x11rb-client", ], package = "zed-xim", version = "0.4.0-zed", optional = true } x11-clipboard = { version = "0.9.3", optional = true } + +[dependencies] +tokio = { workspace = true, features = ["rt"] } +wayland-scanner = "0.31.9" +zbus = { version = "5", features = ["tokio"] } diff --git a/crates/gpui_linux/src/linux.rs b/crates/gpui_linux/src/linux.rs index bafdc2e5241..eaec270a6db 100644 --- a/crates/gpui_linux/src/linux.rs +++ b/crates/gpui_linux/src/linux.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "wayland")] +pub mod dbusmenu; mod dispatcher; mod headless; mod keyboard; diff --git a/crates/gpui_linux/src/linux/dbusmenu.rs b/crates/gpui_linux/src/linux/dbusmenu.rs new file mode 100644 index 00000000000..1f8e2127553 --- /dev/null +++ b/crates/gpui_linux/src/linux/dbusmenu.rs @@ -0,0 +1,509 @@ +use std::collections::HashMap; +use std::env; +use std::sync::{Arc, Mutex}; + +use gpui::{Action, OwnedMenu, OwnedMenuItem}; +use zbus::Connection; +use zbus::object_server::SignalEmitter; +use zbus::zvariant::{OwnedValue, Value}; + +pub const DBUSMENU_OBJECT_PATH: &str = "/MenuBar"; + +type ActionCallback = dyn Fn(Box) + Send + Sync; +type WillOpenCallback = dyn Fn() + Send + Sync; + +struct MenuItemEntry { + action: Option>, + properties: HashMap, + children: Vec, +} + +struct MenuState { + items: HashMap, + revision: u32, +} + +/// The DBusMenu server that exposes the application's menus over DBus. +/// +/// Implements the `com.canonical.dbusmenu` interface so that desktop environments +/// (KDE Plasma, etc.) can render the app's menus in their global menu bar. +#[derive(Clone)] +pub struct DBusMenuServer { + state: Arc>, + action_callback: Arc>>>, + will_open_callback: Arc>>>, + connection: Arc>>, + runtime_handle: Arc>>, +} + +impl DBusMenuServer { + pub fn new() -> Self { + let mut items = HashMap::new(); + items.insert( + 0, + MenuItemEntry { + action: None, + properties: root_properties(), + children: Vec::new(), + }, + ); + Self { + state: Arc::new(Mutex::new(MenuState { items, revision: 1 })), + action_callback: Arc::new(Mutex::new(None)), + will_open_callback: Arc::new(Mutex::new(None)), + connection: Arc::new(Mutex::new(None)), + runtime_handle: Arc::new(Mutex::new(None)), + } + } + + pub fn set_connection(&self, connection: Connection) { + match self.connection.lock() { + Ok(mut slot) => { + *slot = Some(connection); + } + Err(error) => { + log::error!("Failed to store DBus connection for DBusMenu: {error}"); + return; + } + } + + let revision = match self.state.lock() { + Ok(state) => state.revision, + Err(error) => { + log::error!("Failed to read DBusMenu revision for layout update: {error}"); + return; + } + }; + self.emit_layout_updated(revision); + } + + pub fn set_runtime_handle(&self, runtime_handle: tokio::runtime::Handle) { + if let Ok(mut slot) = self.runtime_handle.lock() { + *slot = Some(runtime_handle); + } else { + log::error!("Failed to store DBusMenu runtime handle due to lock poisoning"); + } + } + + pub fn set_action_callback(&self, callback: Box) { + if let Ok(mut slot) = self.action_callback.lock() { + *slot = Some(callback); + } else { + log::error!("Failed to store DBusMenu action callback due to lock poisoning"); + } + } + + pub fn set_will_open_callback(&self, callback: Arc) { + if let Ok(mut slot) = self.will_open_callback.lock() { + *slot = Some(callback); + } else { + log::error!("Failed to store DBusMenu will-open callback due to lock poisoning"); + } + } + + pub fn set_menus(&self, menus: Vec) { + let mut state = match self.state.lock() { + Ok(state) => state, + Err(error) => { + log::error!("Failed to update DBusMenu state: {error}"); + return; + } + }; + state.items.clear(); + + let mut next_id: i32 = 1; + let mut root_children = Vec::new(); + + for menu in &menus { + let submenu_id = next_id; + next_id = next_id.wrapping_add(1); + build_menu_tree(&mut state.items, &mut next_id, submenu_id, menu); + root_children.push(submenu_id); + } + + state.items.insert( + 0, + MenuItemEntry { + action: None, + properties: root_properties(), + children: root_children, + }, + ); + + state.revision = state.revision.wrapping_add(1); + let revision = state.revision; + drop(state); + self.emit_layout_updated(revision); + } + + fn get_layout_node( + &self, + id: i32, + remaining_depth: i32, + ) -> Option<(i32, HashMap, Vec)> { + let state = match self.state.lock() { + Ok(state) => state, + Err(error) => { + log::error!("Failed to read DBusMenu state: {error}"); + return None; + } + }; + let entry = state.items.get(&id)?; + + let properties = entry.properties.clone(); + let children = if remaining_depth == 0 { + Vec::new() + } else { + let child_ids = entry.children.clone(); + drop(state); + let mut result = Vec::new(); + for child_id in child_ids { + if let Some(child_node) = self.get_layout_node(child_id, remaining_depth - 1) { + let variant = + Value::from(zbus::zvariant::Structure::from(child_node)).try_into(); + if let Ok(v) = variant { + result.push(v); + } + } + } + result + }; + + Some((id, properties, children)) + } + + fn emit_layout_updated(&self, revision: u32) { + let runtime_handle = match self.runtime_handle.lock() { + Ok(handle) => handle.clone(), + Err(error) => { + log::error!("Failed to read DBusMenu runtime handle: {error}"); + return; + } + }; + let connection = match self.connection.lock() { + Ok(connection) => connection.clone(), + Err(error) => { + log::error!("Failed to read DBusMenu connection: {error}"); + return; + } + }; + + let (Some(runtime_handle), Some(connection)) = (runtime_handle, connection) else { + return; + }; + + runtime_handle.spawn(async move { + let emitter = match SignalEmitter::new(&connection, DBUSMENU_OBJECT_PATH) { + Ok(emitter) => emitter, + Err(error) => { + log::error!("Failed to build DBusMenu signal emitter: {error}"); + return; + } + }; + if let Err(error) = DBusMenuServer::layout_updated(&emitter, revision, 0).await { + log::error!("Failed to emit DBusMenu LayoutUpdated signal: {error}"); + } + }); + } +} + +#[zbus::interface(name = "com.canonical.dbusmenu")] +impl DBusMenuServer { + async fn about_to_show(&self, _id: i32) -> zbus::fdo::Result { + let callback = match self.will_open_callback.lock() { + Ok(callback) => callback.clone(), + Err(_) => { + return Err(zbus::fdo::Error::Failed( + "Failed to access will-open callback".to_string(), + )); + } + }; + if let Some(callback) = callback { + callback(); + } + Ok(false) + } + + async fn about_to_show_group(&self, _ids: Vec) -> zbus::fdo::Result<(Vec, Vec)> { + let callback = match self.will_open_callback.lock() { + Ok(callback) => callback.clone(), + Err(_) => { + return Err(zbus::fdo::Error::Failed( + "Failed to access will-open callback".to_string(), + )); + } + }; + if let Some(callback) = callback { + callback(); + } + Ok((Vec::new(), Vec::new())) + } + + async fn event( + &self, + id: i32, + event_id: &str, + _data: Value<'_>, + _timestamp: u32, + ) -> zbus::fdo::Result<()> { + if event_id != "clicked" { + return Ok(()); + } + + let action = { + let state = self.state.lock().map_err(|_| { + zbus::fdo::Error::Failed("Failed to access DBusMenu state".to_string()) + })?; + state + .items + .get(&id) + .and_then(|entry| entry.action.as_ref().map(|a| a.boxed_clone())) + }; + + if let Some(action) = action { + let callback = self.action_callback.lock().map_err(|_| { + zbus::fdo::Error::Failed("Failed to access DBusMenu action callback".to_string()) + })?; + if let Some(callback) = callback.as_ref() { + callback(action); + } + } + + Ok(()) + } + + async fn get_group_properties( + &self, + ids: Vec, + _property_names: Vec, + ) -> zbus::fdo::Result)>> { + let state = self + .state + .lock() + .map_err(|_| zbus::fdo::Error::Failed("Failed to access DBusMenu state".to_string()))?; + let result = ids + .into_iter() + .filter_map(|id| { + state + .items + .get(&id) + .map(|entry| (id, entry.properties.clone())) + }) + .collect(); + Ok(result) + } + + async fn get_layout( + &self, + parent_id: i32, + recursion_depth: i32, + _property_names: Vec, + ) -> zbus::fdo::Result<(u32, (i32, HashMap, Vec))> { + let revision = self + .state + .lock() + .map_err(|_| { + zbus::fdo::Error::Failed("Failed to access DBusMenu revision".to_string()) + })? + .revision; + let depth = if recursion_depth < 0 { + i32::MAX + } else { + recursion_depth + }; + + let layout = self.get_layout_node(parent_id, depth).ok_or_else(|| { + zbus::fdo::Error::InvalidArgs(format!("Unknown menu item id: {parent_id}")) + })?; + + Ok((revision, layout)) + } + + async fn get_property(&self, id: i32, name: &str) -> zbus::fdo::Result { + let state = self + .state + .lock() + .map_err(|_| zbus::fdo::Error::Failed("Failed to access DBusMenu state".to_string()))?; + state + .items + .get(&id) + .and_then(|entry| entry.properties.get(name).cloned()) + .ok_or_else(|| zbus::fdo::Error::UnknownProperty(name.to_string())) + } + + #[zbus(property)] + fn status(&self) -> &str { + "normal" + } + + #[zbus(property)] + fn version(&self) -> u32 { + 3 + } + + #[zbus(property)] + fn text_direction(&self) -> &str { + if is_rtl_locale() { "rtl" } else { "ltr" } + } + + #[zbus(property)] + fn icon_theme_path(&self) -> Vec { + Vec::new() + } + + #[zbus(signal)] + async fn layout_updated( + ctxt: &SignalEmitter<'_>, + revision: u32, + parent: i32, + ) -> zbus::Result<()>; + + #[zbus(signal)] + async fn items_properties_updated( + ctxt: &SignalEmitter<'_>, + updated_props: Vec<(i32, HashMap)>, + removed_props: Vec<(i32, Vec)>, + ) -> zbus::Result<()>; + + #[zbus(signal)] + async fn item_activation_requested( + ctxt: &SignalEmitter<'_>, + id: i32, + timestamp: u32, + ) -> zbus::Result<()>; +} + +fn root_properties() -> HashMap { + let mut props = HashMap::new(); + let value = Value::Str("submenu".into()); + if let Ok(value) = value.try_into() { + props.insert("children-display".to_string(), value); + } else { + log::error!("Failed to build DBusMenu root properties"); + } + props +} + +fn build_menu_tree( + items: &mut HashMap, + next_id: &mut i32, + this_id: i32, + menu: &OwnedMenu, +) { + let mut properties = HashMap::new(); + let label_value = Value::Str(menu.name.to_string().into()); + if let Ok(value) = label_value.try_into() { + properties.insert("label".to_string(), value); + } else { + log::error!("Failed to encode DBusMenu label for menu {}", menu.name); + } + let children_value = Value::Str("submenu".into()); + if let Ok(value) = children_value.try_into() { + properties.insert("children-display".to_string(), value); + } else { + log::error!("Failed to encode DBusMenu children-display property"); + } + + let mut child_ids = Vec::new(); + + for item in &menu.items { + let child_id = *next_id; + *next_id = next_id.wrapping_add(1); + + match item { + OwnedMenuItem::Separator => { + let mut props = HashMap::new(); + let value = Value::Str("separator".into()); + if let Ok(value) = value.try_into() { + props.insert("type".to_string(), value); + } else { + log::error!("Failed to encode DBusMenu separator type"); + } + items.insert( + child_id, + MenuItemEntry { + action: None, + properties: props, + children: Vec::new(), + }, + ); + child_ids.push(child_id); + } + OwnedMenuItem::Action { + name, + action, + checked, + .. + } => { + let mut props = HashMap::new(); + let label_value = Value::Str(name.clone().into()); + if let Ok(value) = label_value.try_into() { + props.insert("label".to_string(), value); + } else { + log::error!("Failed to encode DBusMenu label for menu item {}", name); + } + if *checked { + let toggle_type = Value::Str("checkmark".into()); + if let Ok(value) = toggle_type.try_into() { + props.insert("toggle-type".to_string(), value); + } else { + log::error!("Failed to encode DBusMenu toggle-type"); + } + let toggle_state = Value::I32(1); + if let Ok(value) = toggle_state.try_into() { + props.insert("toggle-state".to_string(), value); + } else { + log::error!("Failed to encode DBusMenu toggle-state"); + } + } + items.insert( + child_id, + MenuItemEntry { + action: Some(action.boxed_clone()), + properties: props, + children: Vec::new(), + }, + ); + child_ids.push(child_id); + } + OwnedMenuItem::Submenu(submenu) => { + build_menu_tree(items, next_id, child_id, submenu); + child_ids.push(child_id); + } + OwnedMenuItem::SystemMenu(_) => { + // System menus (e.g., macOS Services) are not meaningful on Linux + } + } + } + + items.insert( + this_id, + MenuItemEntry { + action: None, + properties, + children: child_ids, + }, + ); +} + +fn is_rtl_locale() -> bool { + let locale = ["LC_ALL", "LC_MESSAGES", "LANG"] + .iter() + .find_map(|key| env::var(key).ok()) + .unwrap_or_default(); + + let language = locale + .split('.') + .next() + .and_then(|value| value.split('@').next()) + .and_then(|value| value.split('_').next()) + .and_then(|value| value.split('-').next()) + .unwrap_or_default() + .trim() + .to_ascii_lowercase(); + + matches!( + language.as_str(), + "ar" | "he" | "iw" | "fa" | "ur" | "ps" | "dv" | "ku" | "sd" | "ug" | "yi" + ) +} diff --git a/crates/gpui_linux/src/linux/platform.rs b/crates/gpui_linux/src/linux/platform.rs index 4cd89f35d1e..ea99692b687 100644 --- a/crates/gpui_linux/src/linux/platform.rs +++ b/crates/gpui_linux/src/linux/platform.rs @@ -117,6 +117,8 @@ pub(crate) struct LinuxCommon { pub(crate) callbacks: PlatformHandlers, pub(crate) signal: LoopSignal, pub(crate) menus: Vec, + #[cfg(feature = "wayland")] + pub(crate) dbus_menu_server: Option, } impl LinuxCommon { @@ -143,6 +145,8 @@ impl LinuxCommon { callbacks, signal, menus: Vec::new(), + #[cfg(feature = "wayland")] + dbus_menu_server: None, }; (common, main_receiver) @@ -506,6 +510,10 @@ impl Platform for LinuxPlatform

{ fn set_menus(&self, menus: Vec

, _keymap: &Keymap) { self.inner.with_common(|common| { common.menus = menus.into_iter().map(|menu| menu.owned()).collect(); + #[cfg(feature = "wayland")] + if let Some(server) = &common.dbus_menu_server { + server.set_menus(common.menus.clone()); + } }) } diff --git a/crates/gpui_linux/src/linux/wayland.rs b/crates/gpui_linux/src/linux/wayland.rs index aa1e7974043..9048652eaf5 100644 --- a/crates/gpui_linux/src/linux/wayland.rs +++ b/crates/gpui_linux/src/linux/wayland.rs @@ -5,6 +5,8 @@ mod display; mod serial; mod window; +pub mod appmenu; + /// Contains Types for configuring layer_shell surfaces. pub mod layer_shell; diff --git a/crates/gpui_linux/src/linux/wayland/appmenu.rs b/crates/gpui_linux/src/linux/wayland/appmenu.rs new file mode 100644 index 00000000000..7f0aea7e782 --- /dev/null +++ b/crates/gpui_linux/src/linux/wayland/appmenu.rs @@ -0,0 +1,21 @@ +#![allow( + unused_imports, + non_camel_case_types, + non_snake_case, + dead_code, + unused_mut, + unused_variables +)] + +pub mod client { + use wayland_client; + use wayland_client::protocol::*; + + pub mod __interfaces { + use wayland_client::protocol::__interfaces::*; + wayland_scanner::generate_interfaces!("src/linux/wayland/appmenu.xml"); + } + use self::__interfaces::*; + + wayland_scanner::generate_client_code!("src/linux/wayland/appmenu.xml"); +} diff --git a/crates/gpui_linux/src/linux/wayland/appmenu.xml b/crates/gpui_linux/src/linux/wayland/appmenu.xml new file mode 100644 index 00000000000..02a72f7ef4c --- /dev/null +++ b/crates/gpui_linux/src/linux/wayland/appmenu.xml @@ -0,0 +1,40 @@ + + + + + + This interface allows a client to link a window (or wl_surface) to an com.canonical.dbusmenu + interface registered on DBus. + + + + + + + + + + + + + The DBus service name and object path where the appmenu interface is present + The object should be registered on the session bus before sending this request. + If not applicable, clients should remove this object. + + + + Set or update the service name and object path. + Strings should be formatted in Latin-1 matching the relevant DBus specifications. + + + + + + + + + diff --git a/crates/gpui_linux/src/linux/wayland/client.rs b/crates/gpui_linux/src/linux/wayland/client.rs index ce49fca3723..1b9b1ff3b63 100644 --- a/crates/gpui_linux/src/linux/wayland/client.rs +++ b/crates/gpui_linux/src/linux/wayland/client.rs @@ -4,6 +4,7 @@ use std::{ os::fd::{AsRawFd, BorrowedFd}, path::PathBuf, rc::{Rc, Weak}, + sync::Arc, time::{Duration, Instant}, }; @@ -67,6 +68,8 @@ use wayland_protocols::{ xdg::dialog::v1::client::xdg_dialog_v1::XdgDialogV1, }; use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blur_manager}; + +use super::appmenu::client::{org_kde_kwin_appmenu, org_kde_kwin_appmenu_manager}; use wayland_protocols_wlr::layer_shell::v1::client::{zwlr_layer_shell_v1, zwlr_layer_surface_v1}; use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1; use xkbcommon::xkb::{self, KEYMAP_COMPILE_NO_FLAGS, Keycode}; @@ -91,7 +94,7 @@ use crate::linux::{ xdg_desktop_portal::{Event as XDPEvent, XDPEventSource}, }; use gpui::{ - AnyWindowHandle, Bounds, Capslock, CursorStyle, DevicePixels, DisplayId, FileDropEvent, + Action, AnyWindowHandle, Bounds, Capslock, CursorStyle, DevicePixels, DisplayId, FileDropEvent, ForegroundExecutor, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels, PlatformDisplay, PlatformInput, PlatformKeyboardLayout, PlatformWindow, Point, @@ -126,6 +129,7 @@ pub struct Globals { pub decoration_manager: Option, pub layer_shell: Option, pub blur_manager: Option, + pub appmenu_manager: Option, pub text_input_manager: Option, pub gesture_manager: Option, pub dialog: Option, @@ -167,6 +171,7 @@ impl Globals { decoration_manager: globals.bind(&qh, 1..=1, ()).ok(), layer_shell: globals.bind(&qh, 1..=5, ()).ok(), blur_manager: globals.bind(&qh, 1..=1, ()).ok(), + appmenu_manager: globals.bind(&qh, 1..=2, ()).ok(), text_input_manager: globals.bind(&qh, 1..=1, ()).ok(), gesture_manager: globals.bind(&qh, 1..=3, ()).ok(), dialog: globals.bind(&qh, dialog_v..=dialog_v, ()).ok(), @@ -256,6 +261,7 @@ pub(crate) struct WaylandClientState { cursor: Cursor, pending_activation: Option, event_loop: Option>, + dbus_service_name: Option, pub common: LinuxCommon, } @@ -652,12 +658,126 @@ impl WaylandClient { cursor, pending_activation: None, event_loop: Some(event_loop), + dbus_service_name: None, })); WaylandSource::new(conn, event_queue) .insert(handle) .unwrap(); + // Start the DBusMenu server if the compositor supports global menus. + { + let has_appmenu = state.borrow().globals.appmenu_manager.is_some(); + if has_appmenu { + let dbus_menu_server = crate::linux::dbusmenu::DBusMenuServer::new(); + let service_name = format!("com.zed.dbusmenu.pid{}", std::process::id()); + + // Channel for sending menu actions from the DBus thread to the main thread. + let (action_tx, action_rx) = calloop::channel::channel::>(); + let (will_open_tx, will_open_rx) = calloop::channel::channel::<()>(); + + dbus_menu_server.set_action_callback({ + let action_tx = action_tx.clone(); + Box::new(move |action| { + action_tx.send(action).ok(); + }) + }); + dbus_menu_server.set_will_open_callback({ + let will_open_tx = will_open_tx.clone(); + Arc::new(move || { + will_open_tx.send(()).ok(); + }) + }); + + state + .borrow() + .loop_handle + .insert_source(action_rx, { + let client = Rc::downgrade(&state); + move |event, _, _| { + if let calloop::channel::Event::Msg(action) = event { + if let Some(client) = client.upgrade() { + let mut state = client.borrow_mut(); + if let Some(callback) = + state.common.callbacks.app_menu_action.as_mut() + { + callback(action.as_ref()); + } + } + } + } + }) + .log_err(); + + state + .borrow() + .loop_handle + .insert_source(will_open_rx, { + let client = Rc::downgrade(&state); + move |event, _, _| { + if let calloop::channel::Event::Msg(()) = event { + if let Some(client) = client.upgrade() { + let mut state = client.borrow_mut(); + if let Some(callback) = + state.common.callbacks.will_open_app_menu.as_mut() + { + callback(); + } + } + } + } + }) + .log_err(); + + state.borrow_mut().common.dbus_menu_server = Some(dbus_menu_server.clone()); + state.borrow_mut().dbus_service_name = Some(service_name.clone()); + + let object_path = crate::linux::dbusmenu::DBUSMENU_OBJECT_PATH.to_string(); + std::thread::Builder::new() + .name("dbus-menu".into()) + .spawn(move || { + let rt = match tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + { + Ok(rt) => rt, + Err(error) => { + log::error!("Failed to create tokio runtime for DBusMenu: {error}"); + return; + } + }; + dbus_menu_server.set_runtime_handle(rt.handle().clone()); + + rt.block_on(async move { + let dbus_menu_server_for_service = dbus_menu_server.clone(); + match zbus::connection::Builder::session() + .and_then(|builder| builder.name(service_name.as_str())) + .and_then(|builder| { + builder.serve_at( + object_path.as_str(), + dbus_menu_server_for_service, + ) + }) { + Ok(builder) => match builder.build().await { + Ok(connection) => { + dbus_menu_server.set_connection(connection.clone()); + log::info!("DBusMenu server started on {service_name}"); + std::future::pending::<()>().await; + } + Err(error) => { + log::error!("Failed to build DBus connection: {error}"); + } + }, + Err(error) => { + log::error!("Failed to configure DBus connection: {error}"); + } + } + }); + }) + .log_err(); + } + } + Self(state) } } @@ -758,6 +878,18 @@ impl LinuxClient for WaylandClient { )?; state.windows.insert(surface_id, window.0.clone()); + if let (Some(appmenu_manager), Some(service_name)) = ( + state.globals.appmenu_manager.as_ref(), + state.dbus_service_name.as_ref(), + ) { + let surface = window.0.surface(); + let appmenu = appmenu_manager.create(&surface, &state.globals.qh, ()); + appmenu.set_address( + service_name.clone(), + crate::linux::dbusmenu::DBUSMENU_OBJECT_PATH.to_string(), + ); + } + Ok(Box::new(window)) } @@ -1070,6 +1202,8 @@ delegate_noop!(WaylandClientStatePtr: ignore wp_fractional_scale_manager_v1::WpF delegate_noop!(WaylandClientStatePtr: ignore zxdg_decoration_manager_v1::ZxdgDecorationManagerV1); delegate_noop!(WaylandClientStatePtr: ignore zwlr_layer_shell_v1::ZwlrLayerShellV1); delegate_noop!(WaylandClientStatePtr: ignore org_kde_kwin_blur_manager::OrgKdeKwinBlurManager); +delegate_noop!(WaylandClientStatePtr: ignore org_kde_kwin_appmenu_manager::OrgKdeKwinAppmenuManager); +delegate_noop!(WaylandClientStatePtr: ignore org_kde_kwin_appmenu::OrgKdeKwinAppmenu); delegate_noop!(WaylandClientStatePtr: ignore zwp_text_input_manager_v3::ZwpTextInputManagerV3); delegate_noop!(WaylandClientStatePtr: ignore org_kde_kwin_blur::OrgKdeKwinBlur); delegate_noop!(WaylandClientStatePtr: ignore wp_viewporter::WpViewporter); From 26fdead13aa41b76d0d487418023a597770b2a9d Mon Sep 17 00:00:00 2001 From: Nihal <121309701+nihalxkumar@users.noreply.github.com> Date: Thu, 12 Mar 2026 14:09:18 +0530 Subject: [PATCH 02/16] gpui_linux: Add opt-in Wayland global menu --- Cargo.lock | 4 +- crates/gpui/src/app.rs | 5 + crates/gpui/src/platform.rs | 8 + crates/gpui_linux/Cargo.toml | 9 +- crates/gpui_linux/src/linux.rs | 2 +- crates/gpui_linux/src/linux/dbusmenu.rs | 403 +++++++++++++++--- crates/gpui_linux/src/linux/platform.rs | 26 +- crates/gpui_linux/src/linux/wayland.rs | 1 + crates/gpui_linux/src/linux/wayland/client.rs | 213 +++++++-- crates/gpui_platform/Cargo.toml | 1 + crates/title_bar/src/application_menu.rs | 1 + crates/zed/Cargo.toml | 1 + 12 files changed, 554 insertions(+), 120 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e8008f5363f..84bdde57dc5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7658,6 +7658,7 @@ dependencies = [ "anyhow", "as-raw-xcb-connection", "ashpd", + "async-channel 2.5.0", "bitflags 2.10.0", "bytemuck", "calloop", @@ -7682,7 +7683,6 @@ dependencies = [ "smol", "strum 0.27.2", "swash", - "tokio", "url", "util", "uuid", @@ -17695,7 +17695,6 @@ dependencies = [ "signal-hook-registry", "socket2 0.6.1", "tokio-macros", - "tracing", "windows-sys 0.61.2", ] @@ -21722,7 +21721,6 @@ dependencies = [ "rustix 1.1.2", "serde", "serde_repr", - "tokio", "tracing", "uds_windows", "uuid", diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 8af0a8923b3..cb8413ba663 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1249,6 +1249,11 @@ impl App { self.platform.compositor_name() } + /// Returns true when the application is exporting its menus to a system-managed global menu. + pub fn is_global_menu_active(&self) -> bool { + self.platform.is_global_menu_active() + } + /// Returns the file URL of the executable with the specified name in the application bundle pub fn path_for_auxiliary_executable(&self, name: &str) -> Result { self.platform.path_for_auxiliary_executable(name) diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 061a055e7ef..2422b006c6f 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -200,6 +200,14 @@ pub trait Platform: 'static { fn compositor_name(&self) -> &'static str { "" } + + /// Returns true when the application is exporting its menus to a system-managed global menu. + /// + /// This is used by cross-platform UI code to hide the in-window menu bar when the desktop + /// environment provides an app menu (e.g. KDE Plasma's global menu). + fn is_global_menu_active(&self) -> bool { + false + } fn app_path(&self) -> Result; fn path_for_auxiliary_executable(&self, name: &str) -> Result; diff --git a/crates/gpui_linux/Cargo.toml b/crates/gpui_linux/Cargo.toml index 89985e33109..0c30c5ab700 100644 --- a/crates/gpui_linux/Cargo.toml +++ b/crates/gpui_linux/Cargo.toml @@ -14,6 +14,7 @@ path = "src/gpui_linux.rs" [features] default = ["wayland", "x11"] test-support = ["gpui/test-support"] +global-menu = ["dep:async-channel", "dep:zbus", "dep:wayland-scanner"] wayland = [ "bitflags", "gpui_wgpu", @@ -71,6 +72,9 @@ strum.workspace = true url.workspace = true util.workspace = true uuid.workspace = true +async-channel = { workspace = true, optional = true } +zbus = { version = "5", optional = true } +wayland-scanner = { version = "0.31.9", optional = true } # Always used oo7 = { version = "0.6", default-features = false, features = [ @@ -129,8 +133,3 @@ xim = { git = "https://github.com/zed-industries/xim-rs.git", rev = "16f35a2c881 "x11rb-client", ], package = "zed-xim", version = "0.4.0-zed", optional = true } x11-clipboard = { version = "0.9.3", optional = true } - -[dependencies] -tokio = { workspace = true, features = ["rt"] } -wayland-scanner = "0.31.9" -zbus = { version = "5", features = ["tokio"] } diff --git a/crates/gpui_linux/src/linux.rs b/crates/gpui_linux/src/linux.rs index eaec270a6db..fa5788bc178 100644 --- a/crates/gpui_linux/src/linux.rs +++ b/crates/gpui_linux/src/linux.rs @@ -1,4 +1,4 @@ -#[cfg(feature = "wayland")] +#[cfg(all(feature = "wayland", feature = "global-menu"))] pub mod dbusmenu; mod dispatcher; mod headless; diff --git a/crates/gpui_linux/src/linux/dbusmenu.rs b/crates/gpui_linux/src/linux/dbusmenu.rs index 1f8e2127553..f0a5f0e82b6 100644 --- a/crates/gpui_linux/src/linux/dbusmenu.rs +++ b/crates/gpui_linux/src/linux/dbusmenu.rs @@ -1,10 +1,13 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::env; -use std::sync::{Arc, Mutex}; +use std::sync::OnceLock; +use std::sync::{ + Arc, Mutex, + atomic::{AtomicBool, Ordering}, +}; +use std::time::Duration; -use gpui::{Action, OwnedMenu, OwnedMenuItem}; -use zbus::Connection; -use zbus::object_server::SignalEmitter; +use gpui::{Action, KeyContext, Keymap, KeybindingKeystroke, OwnedMenu, OwnedMenuItem}; use zbus::zvariant::{OwnedValue, Value}; pub const DBUSMENU_OBJECT_PATH: &str = "/MenuBar"; @@ -12,8 +15,27 @@ pub const DBUSMENU_OBJECT_PATH: &str = "/MenuBar"; type ActionCallback = dyn Fn(Box) + Send + Sync; type WillOpenCallback = dyn Fn() + Send + Sync; +#[derive(Clone)] +pub enum DBusMenuCommand { + EnsureExported { + object_path: String, + responded: std::sync::mpsc::Sender, + }, + LayoutUpdated { + revision: u32, + parent: i32, + object_paths: Vec, + }, + ItemsPropertiesUpdated { + updated_props: Vec<(i32, HashMap)>, + removed_props: Vec<(i32, Vec)>, + object_paths: Vec, + }, +} + struct MenuItemEntry { action: Option>, + enabled: Option, properties: HashMap, children: Vec, } @@ -23,17 +45,15 @@ struct MenuState { revision: u32, } -/// The DBusMenu server that exposes the application's menus over DBus. -/// -/// Implements the `com.canonical.dbusmenu` interface so that desktop environments -/// (KDE Plasma, etc.) can render the app's menus in their global menu bar. #[derive(Clone)] pub struct DBusMenuServer { state: Arc>, action_callback: Arc>>>, will_open_callback: Arc>>>, - connection: Arc>>, - runtime_handle: Arc>>, + command_sender: Arc>>>, + connected: Arc, + object_paths: Arc>>, + exported_paths: Arc>>, } impl DBusMenuServer { @@ -43,45 +63,58 @@ impl DBusMenuServer { 0, MenuItemEntry { action: None, + enabled: None, properties: root_properties(), children: Vec::new(), }, ); + + let mut object_paths = HashSet::new(); + object_paths.insert(DBUSMENU_OBJECT_PATH.to_string()); + + let mut exported_paths = HashSet::new(); + exported_paths.insert(DBUSMENU_OBJECT_PATH.to_string()); + Self { state: Arc::new(Mutex::new(MenuState { items, revision: 1 })), action_callback: Arc::new(Mutex::new(None)), will_open_callback: Arc::new(Mutex::new(None)), - connection: Arc::new(Mutex::new(None)), - runtime_handle: Arc::new(Mutex::new(None)), + command_sender: Arc::new(Mutex::new(None)), + connected: Arc::new(AtomicBool::new(false)), + object_paths: Arc::new(Mutex::new(object_paths)), + exported_paths: Arc::new(Mutex::new(exported_paths)), } } - pub fn set_connection(&self, connection: Connection) { - match self.connection.lock() { - Ok(mut slot) => { - *slot = Some(connection); - } + pub fn set_command_sender(&self, sender: async_channel::Sender) { + match self.command_sender.lock() { + Ok(mut slot) => *slot = Some(sender), Err(error) => { - log::error!("Failed to store DBus connection for DBusMenu: {error}"); - return; + log::error!("Failed to store DBusMenu command sender: {error}"); } } + } + + pub fn mark_connected(&self) { + self.connected.store(true, Ordering::SeqCst); let revision = match self.state.lock() { Ok(state) => state.revision, Err(error) => { - log::error!("Failed to read DBusMenu revision for layout update: {error}"); + log::error!("Failed to read DBusMenu revision on connect: {error}"); return; } }; - self.emit_layout_updated(revision); + self.request_layout_updated(revision); } - pub fn set_runtime_handle(&self, runtime_handle: tokio::runtime::Handle) { - if let Ok(mut slot) = self.runtime_handle.lock() { - *slot = Some(runtime_handle); - } else { - log::error!("Failed to store DBusMenu runtime handle due to lock poisoning"); + pub fn is_connected(&self) -> bool { + self.connected.load(Ordering::SeqCst) + } + + pub fn note_exported(&self, object_path: String) { + if let Ok(mut exported_paths) = self.exported_paths.lock() { + exported_paths.insert(object_path); } } @@ -101,7 +134,7 @@ impl DBusMenuServer { } } - pub fn set_menus(&self, menus: Vec) { + pub fn set_menus(&self, menus: Vec, keymap: &Keymap) { let mut state = match self.state.lock() { Ok(state) => state, Err(error) => { @@ -117,7 +150,7 @@ impl DBusMenuServer { for menu in &menus { let submenu_id = next_id; next_id = next_id.wrapping_add(1); - build_menu_tree(&mut state.items, &mut next_id, submenu_id, menu); + build_menu_tree(&mut state.items, &mut next_id, submenu_id, menu, keymap); root_children.push(submenu_id); } @@ -125,6 +158,7 @@ impl DBusMenuServer { 0, MenuItemEntry { action: None, + enabled: None, properties: root_properties(), children: root_children, }, @@ -133,7 +167,117 @@ impl DBusMenuServer { state.revision = state.revision.wrapping_add(1); let revision = state.revision; drop(state); - self.emit_layout_updated(revision); + self.request_layout_updated(revision); + } + + pub fn ensure_exported_blocking(&self, object_path: String, timeout: Duration) -> bool { + let should_export = match self.object_paths.lock() { + Ok(mut paths) => paths.insert(object_path.clone()), + Err(error) => { + log::error!("Failed to update DBusMenu object paths: {error}"); + return false; + } + }; + if !should_export { + return true; + } + + if match self.exported_paths.lock() { + Ok(exported_paths) => exported_paths.contains(&object_path), + Err(error) => { + log::error!("Failed to read exported DBusMenu paths: {error}"); + false + } + } { + return true; + } + + if !self.is_connected() { + return false; + } + + let sender = match self.command_sender.lock() { + Ok(sender) => sender.clone(), + Err(error) => { + log::error!("Failed to read DBusMenu command sender: {error}"); + return false; + } + }; + let Some(sender) = sender else { + return false; + }; + + let (responded_tx, responded_rx) = std::sync::mpsc::channel(); + if let Err(error) = sender.try_send(DBusMenuCommand::EnsureExported { + object_path: object_path.clone(), + responded: responded_tx, + }) { + log::error!("Failed to send DBusMenu export request: {error}"); + return false; + } + + let exported = match responded_rx.recv_timeout(timeout) { + Ok(ok) => ok, + Err(error) => { + log::error!("Timed out exporting DBusMenu object at {object_path}: {error}"); + false + } + }; + + if exported { + let revision = match self.state.lock() { + Ok(state) => state.revision, + Err(error) => { + log::error!("Failed to read DBusMenu revision after export: {error}"); + return true; + } + }; + self.request_layout_updated_for_paths(revision, vec![object_path]); + } + + exported + } + + pub fn refresh_enabled_states(&self, validate: &mut dyn FnMut(&dyn Action) -> bool) { + let mut updated_props: Vec<(i32, HashMap)> = Vec::new(); + + { + let mut state = match self.state.lock() { + Ok(state) => state, + Err(error) => { + log::error!("Failed to access DBusMenu state for enable refresh: {error}"); + return; + } + }; + + for (id, entry) in state.items.iter_mut() { + let Some(previous_enabled) = entry.enabled else { + continue; + }; + let Some(action) = entry.action.as_ref() else { + continue; + }; + + let enabled = validate(action.as_ref()); + if enabled == previous_enabled { + continue; + } + + entry.enabled = Some(enabled); + if let Some(value) = owned_bool(enabled) { + entry.properties.insert("enabled".to_string(), value.clone()); + let mut props = HashMap::new(); + props.insert("enabled".to_string(), value); + updated_props.push((*id, props)); + } + } + } + + if updated_props.is_empty() { + return; + } + + self.request_items_properties_updated(updated_props, Vec::new()); } fn get_layout_node( @@ -161,8 +305,8 @@ impl DBusMenuServer { if let Some(child_node) = self.get_layout_node(child_id, remaining_depth - 1) { let variant = Value::from(zbus::zvariant::Structure::from(child_node)).try_into(); - if let Ok(v) = variant { - result.push(v); + if let Ok(value) = variant { + result.push(value); } } } @@ -172,38 +316,83 @@ impl DBusMenuServer { Some((id, properties, children)) } - fn emit_layout_updated(&self, revision: u32) { - let runtime_handle = match self.runtime_handle.lock() { - Ok(handle) => handle.clone(), - Err(error) => { - log::error!("Failed to read DBusMenu runtime handle: {error}"); - return; - } - }; - let connection = match self.connection.lock() { - Ok(connection) => connection.clone(), - Err(error) => { - log::error!("Failed to read DBusMenu connection: {error}"); - return; - } - }; + fn request_layout_updated(&self, revision: u32) { + let object_paths = self.object_paths(); + self.request_layout_updated_for_paths(revision, object_paths); + } - let (Some(runtime_handle), Some(connection)) = (runtime_handle, connection) else { + fn request_layout_updated_for_paths(&self, revision: u32, object_paths: Vec) { + if !self.is_connected() { + return; + } + + let sender = match self.command_sender.lock() { + Ok(sender) => sender.clone(), + Err(error) => { + log::error!("Failed to read DBusMenu command sender: {error}"); + return; + } + }; + let Some(sender) = sender else { return; }; - runtime_handle.spawn(async move { - let emitter = match SignalEmitter::new(&connection, DBUSMENU_OBJECT_PATH) { - Ok(emitter) => emitter, - Err(error) => { - log::error!("Failed to build DBusMenu signal emitter: {error}"); - return; - } - }; - if let Err(error) = DBusMenuServer::layout_updated(&emitter, revision, 0).await { - log::error!("Failed to emit DBusMenu LayoutUpdated signal: {error}"); + if let Err(error) = sender.try_send(DBusMenuCommand::LayoutUpdated { + revision, + parent: 0, + object_paths, + }) { + log::error!("Failed to queue DBusMenu LayoutUpdated signal: {error}"); + } + } + + fn request_items_properties_updated( + &self, + updated_props: Vec<(i32, HashMap)>, + removed_props: Vec<(i32, Vec)>, + ) { + let object_paths = self.object_paths(); + self.request_items_properties_updated_for_paths(updated_props, removed_props, object_paths); + } + + fn request_items_properties_updated_for_paths( + &self, + updated_props: Vec<(i32, HashMap)>, + removed_props: Vec<(i32, Vec)>, + object_paths: Vec, + ) { + if !self.is_connected() { + return; + } + + let sender = match self.command_sender.lock() { + Ok(sender) => sender.clone(), + Err(error) => { + log::error!("Failed to read DBusMenu command sender: {error}"); + return; } - }); + }; + let Some(sender) = sender else { + return; + }; + + if let Err(error) = sender.try_send(DBusMenuCommand::ItemsPropertiesUpdated { + updated_props, + removed_props, + object_paths, + }) { + log::error!("Failed to queue DBusMenu ItemsPropertiesUpdated signal: {error}"); + } + } + + fn object_paths(&self) -> Vec { + match self.object_paths.lock() { + Ok(paths) => paths.iter().cloned().collect(), + Err(error) => { + log::error!("Failed to read DBusMenu object paths: {error}"); + vec![DBUSMENU_OBJECT_PATH.to_string()] + } + } } } @@ -352,22 +541,22 @@ impl DBusMenuServer { } #[zbus(signal)] - async fn layout_updated( - ctxt: &SignalEmitter<'_>, + pub async fn layout_updated( + ctxt: &zbus::object_server::SignalEmitter<'_>, revision: u32, parent: i32, ) -> zbus::Result<()>; #[zbus(signal)] - async fn items_properties_updated( - ctxt: &SignalEmitter<'_>, + pub async fn items_properties_updated( + ctxt: &zbus::object_server::SignalEmitter<'_>, updated_props: Vec<(i32, HashMap)>, removed_props: Vec<(i32, Vec)>, ) -> zbus::Result<()>; #[zbus(signal)] - async fn item_activation_requested( - ctxt: &SignalEmitter<'_>, + pub async fn item_activation_requested( + ctxt: &zbus::object_server::SignalEmitter<'_>, id: i32, timestamp: u32, ) -> zbus::Result<()>; @@ -389,6 +578,7 @@ fn build_menu_tree( next_id: &mut i32, this_id: i32, menu: &OwnedMenu, + keymap: &Keymap, ) { let mut properties = HashMap::new(); let label_value = Value::Str(menu.name.to_string().into()); @@ -423,6 +613,7 @@ fn build_menu_tree( child_id, MenuItemEntry { action: None, + enabled: None, properties: props, children: Vec::new(), }, @@ -442,6 +633,17 @@ fn build_menu_tree( } else { log::error!("Failed to encode DBusMenu label for menu item {}", name); } + + if let Some(value) = owned_bool(true) { + props.insert("enabled".to_string(), value); + } else { + log::error!("Failed to encode DBusMenu enabled state for menu item {}", name); + } + + if let Some(shortcut) = dbus_shortcut_for_action(action.as_ref(), keymap) { + props.insert("shortcut".to_string(), shortcut); + } + if *checked { let toggle_type = Value::Str("checkmark".into()); if let Ok(value) = toggle_type.try_into() { @@ -460,6 +662,7 @@ fn build_menu_tree( child_id, MenuItemEntry { action: Some(action.boxed_clone()), + enabled: Some(true), properties: props, children: Vec::new(), }, @@ -467,12 +670,10 @@ fn build_menu_tree( child_ids.push(child_id); } OwnedMenuItem::Submenu(submenu) => { - build_menu_tree(items, next_id, child_id, submenu); + build_menu_tree(items, next_id, child_id, submenu, keymap); child_ids.push(child_id); } - OwnedMenuItem::SystemMenu(_) => { - // System menus (e.g., macOS Services) are not meaningful on Linux - } + OwnedMenuItem::SystemMenu(_) => {} } } @@ -480,12 +681,76 @@ fn build_menu_tree( this_id, MenuItemEntry { action: None, + enabled: None, properties, children: child_ids, }, ); } +pub fn object_path_for_window(window_id: u32) -> String { + format!("{DBUSMENU_OBJECT_PATH}/window_{window_id}") +} + +fn owned_bool(value: bool) -> Option { + Value::Bool(value).try_into().ok() +} + +fn dbus_shortcut_for_action(action: &dyn Action, keymap: &Keymap) -> Option { + static DEFAULT_CONTEXT: OnceLock> = OnceLock::new(); + + let contexts = DEFAULT_CONTEXT.get_or_init(|| { + let mut workspace_context = KeyContext::new_with_defaults(); + workspace_context.add("Workspace"); + let mut pane_context = KeyContext::new_with_defaults(); + pane_context.add("Pane"); + let mut editor_context = KeyContext::new_with_defaults(); + editor_context.add("Editor"); + + pane_context.extend(&editor_context); + workspace_context.extend(&pane_context); + vec![workspace_context] + }); + + let binding = keymap + .bindings_for_action(action) + .find(|binding| binding.predicate().is_none_or(|predicate| predicate.eval(contexts))) + .or_else(|| keymap.bindings_for_action(action).next()); + + let keystrokes = binding?.keystrokes(); + if keystrokes.len() != 1 { + return None; + } + + dbus_shortcut_for_keystroke(&keystrokes[0]) +} + +fn dbus_shortcut_for_keystroke(keystroke: &KeybindingKeystroke) -> Option { + let mut keys: Vec = Vec::new(); + + let modifiers = keystroke.modifiers(); + if modifiers.control { + keys.push("Control".to_string()); + } + if modifiers.alt { + keys.push("Alt".to_string()); + } + if modifiers.shift { + keys.push("Shift".to_string()); + } + if modifiers.platform { + keys.push("Super".to_string()); + } + if modifiers.function { + keys.push("Fn".to_string()); + } + + keys.push(keystroke.key().to_string()); + + let shortcut: Vec> = vec![keys]; + Value::from(shortcut).try_into().ok() +} + fn is_rtl_locale() -> bool { let locale = ["LC_ALL", "LC_MESSAGES", "LANG"] .iter() diff --git a/crates/gpui_linux/src/linux/platform.rs b/crates/gpui_linux/src/linux/platform.rs index ea99692b687..5b8ab3052df 100644 --- a/crates/gpui_linux/src/linux/platform.rs +++ b/crates/gpui_linux/src/linux/platform.rs @@ -117,7 +117,7 @@ pub(crate) struct LinuxCommon { pub(crate) callbacks: PlatformHandlers, pub(crate) signal: LoopSignal, pub(crate) menus: Vec, - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", feature = "global-menu"))] pub(crate) dbus_menu_server: Option, } @@ -145,7 +145,7 @@ impl LinuxCommon { callbacks, signal, menus: Vec::new(), - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", feature = "global-menu"))] dbus_menu_server: None, }; @@ -212,6 +212,22 @@ impl Platform for LinuxPlatform

{ self.inner.compositor_name() } + fn is_global_menu_active(&self) -> bool { + #[cfg(all(feature = "wayland", feature = "global-menu"))] + { + self.inner.with_common(|common| { + common + .dbus_menu_server + .as_ref() + .is_some_and(|server| server.is_connected()) + }) + } + #[cfg(not(all(feature = "wayland", feature = "global-menu")))] + { + false + } + } + fn restart(&self, binary_path: Option) { use std::os::unix::process::CommandExt as _; @@ -507,12 +523,12 @@ impl Platform for LinuxPlatform

{ Ok(app_path) } - fn set_menus(&self, menus: Vec

, _keymap: &Keymap) { + fn set_menus(&self, menus: Vec, keymap: &Keymap) { self.inner.with_common(|common| { common.menus = menus.into_iter().map(|menu| menu.owned()).collect(); - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", feature = "global-menu"))] if let Some(server) = &common.dbus_menu_server { - server.set_menus(common.menus.clone()); + server.set_menus(common.menus.clone(), keymap); } }) } diff --git a/crates/gpui_linux/src/linux/wayland.rs b/crates/gpui_linux/src/linux/wayland.rs index 9048652eaf5..6fbce26b549 100644 --- a/crates/gpui_linux/src/linux/wayland.rs +++ b/crates/gpui_linux/src/linux/wayland.rs @@ -5,6 +5,7 @@ mod display; mod serial; mod window; +#[cfg(feature = "global-menu")] pub mod appmenu; /// Contains Types for configuring layer_shell surfaces. diff --git a/crates/gpui_linux/src/linux/wayland/client.rs b/crates/gpui_linux/src/linux/wayland/client.rs index 1b9b1ff3b63..a9529a9f17a 100644 --- a/crates/gpui_linux/src/linux/wayland/client.rs +++ b/crates/gpui_linux/src/linux/wayland/client.rs @@ -69,6 +69,7 @@ use wayland_protocols::{ }; use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blur_manager}; +#[cfg(feature = "global-menu")] use super::appmenu::client::{org_kde_kwin_appmenu, org_kde_kwin_appmenu_manager}; use wayland_protocols_wlr::layer_shell::v1::client::{zwlr_layer_shell_v1, zwlr_layer_surface_v1}; use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1; @@ -106,6 +107,21 @@ use wayland_protocols::wp::linux_dmabuf::zv1::client::{ zwp_linux_dmabuf_feedback_v1, zwp_linux_dmabuf_v1, }; +#[cfg(feature = "global-menu")] +fn global_menu_env_override() -> Option { + match std::env::var("ZED_GLOBAL_MENU").ok().as_deref() { + None => None, + Some("1" | "true" | "TRUE" | "yes" | "YES" | "on" | "ON") => Some(true), + Some("0" | "false" | "FALSE" | "no" | "NO" | "off" | "OFF") => Some(false), + Some(value) => { + log::warn!( + "Ignoring invalid ZED_GLOBAL_MENU value {value:?}. Expected 0/1, true/false, yes/no, or on/off." + ); + None + } + } +} + /// Used to convert evdev scancode to xkb scancode const MIN_KEYCODE: u32 = 8; @@ -129,6 +145,7 @@ pub struct Globals { pub decoration_manager: Option, pub layer_shell: Option, pub blur_manager: Option, + #[cfg(feature = "global-menu")] pub appmenu_manager: Option, pub text_input_manager: Option, pub gesture_manager: Option, @@ -171,6 +188,7 @@ impl Globals { decoration_manager: globals.bind(&qh, 1..=1, ()).ok(), layer_shell: globals.bind(&qh, 1..=5, ()).ok(), blur_manager: globals.bind(&qh, 1..=1, ()).ok(), + #[cfg(feature = "global-menu")] appmenu_manager: globals.bind(&qh, 1..=2, ()).ok(), text_input_manager: globals.bind(&qh, 1..=1, ()).ok(), gesture_manager: globals.bind(&qh, 1..=3, ()).ok(), @@ -261,6 +279,7 @@ pub(crate) struct WaylandClientState { cursor: Cursor, pending_activation: Option, event_loop: Option>, + #[cfg(feature = "global-menu")] dbus_service_name: Option, pub common: LinuxCommon, } @@ -658,6 +677,7 @@ impl WaylandClient { cursor, pending_activation: None, event_loop: Some(event_loop), + #[cfg(feature = "global-menu")] dbus_service_name: None, })); @@ -666,9 +686,16 @@ impl WaylandClient { .unwrap(); // Start the DBusMenu server if the compositor supports global menus. + #[cfg(feature = "global-menu")] { let has_appmenu = state.borrow().globals.appmenu_manager.is_some(); - if has_appmenu { + let enabled = match global_menu_env_override() { + Some(true) => has_appmenu, + Some(false) => false, + None => has_appmenu, + }; + + if enabled { let dbus_menu_server = crate::linux::dbusmenu::DBusMenuServer::new(); let service_name = format!("com.zed.dbusmenu.pid{}", std::process::id()); @@ -679,13 +706,21 @@ impl WaylandClient { dbus_menu_server.set_action_callback({ let action_tx = action_tx.clone(); Box::new(move |action| { - action_tx.send(action).ok(); + if let Err(error) = action_tx.send(action) { + log::error!( + "Failed to send DBus menu action to the Wayland event loop: {error}" + ); + } }) }); dbus_menu_server.set_will_open_callback({ let will_open_tx = will_open_tx.clone(); Arc::new(move || { - will_open_tx.send(()).ok(); + if let Err(error) = will_open_tx.send(()) { + log::error!( + "Failed to send DBus menu open notification to the Wayland event loop: {error}" + ); + } }) }); @@ -718,11 +753,18 @@ impl WaylandClient { if let calloop::channel::Event::Msg(()) = event { if let Some(client) = client.upgrade() { let mut state = client.borrow_mut(); + let dbus_menu_server = state.common.dbus_menu_server.clone(); if let Some(callback) = state.common.callbacks.will_open_app_menu.as_mut() { callback(); } + if let (Some(dbus_menu_server), Some(validate_callback)) = ( + dbus_menu_server.as_ref(), + state.common.callbacks.validate_app_menu_command.as_mut(), + ) { + dbus_menu_server.refresh_enabled_states(validate_callback); + } } } } @@ -733,24 +775,14 @@ impl WaylandClient { state.borrow_mut().dbus_service_name = Some(service_name.clone()); let object_path = crate::linux::dbusmenu::DBUSMENU_OBJECT_PATH.to_string(); + let (dbus_command_tx, dbus_command_rx) = async_channel::unbounded(); + dbus_menu_server.set_command_sender(dbus_command_tx); std::thread::Builder::new() .name("dbus-menu".into()) .spawn(move || { - let rt = match tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - { - Ok(rt) => rt, - Err(error) => { - log::error!("Failed to create tokio runtime for DBusMenu: {error}"); - return; - } - }; - dbus_menu_server.set_runtime_handle(rt.handle().clone()); - - rt.block_on(async move { + smol::block_on(async move { let dbus_menu_server_for_service = dbus_menu_server.clone(); - match zbus::connection::Builder::session() + let builder = match zbus::connection::Builder::session() .and_then(|builder| builder.name(service_name.as_str())) .and_then(|builder| { builder.serve_at( @@ -758,18 +790,108 @@ impl WaylandClient { dbus_menu_server_for_service, ) }) { - Ok(builder) => match builder.build().await { - Ok(connection) => { - dbus_menu_server.set_connection(connection.clone()); - log::info!("DBusMenu server started on {service_name}"); - std::future::pending::<()>().await; - } - Err(error) => { - log::error!("Failed to build DBus connection: {error}"); - } - }, + Ok(builder) => builder, Err(error) => { log::error!("Failed to configure DBus connection: {error}"); + return; + } + }; + + let connection = match builder.build().await { + Ok(connection) => connection, + Err(error) => { + log::error!("Failed to build DBus connection: {error}"); + return; + } + }; + + dbus_menu_server.mark_connected(); + log::info!("DBusMenu server started on {service_name}"); + + while let Ok(command) = dbus_command_rx.recv().await { + match command { + crate::linux::dbusmenu::DBusMenuCommand::EnsureExported { + object_path, + responded, + } => { + let result = connection + .object_server() + .at(object_path.as_str(), dbus_menu_server.clone()) + .await; + let ok = result.is_ok(); + if ok { + dbus_menu_server.note_exported(object_path); + } + if let Err(error) = responded.send(ok) { + log::error!( + "Failed to send DBusMenu export response: {error}" + ); + } + } + crate::linux::dbusmenu::DBusMenuCommand::LayoutUpdated { + revision, + parent, + object_paths, + } => { + for object_path in object_paths { + let emitter = match zbus::object_server::SignalEmitter::new( + &connection, + object_path.as_str(), + ) { + Ok(emitter) => emitter, + Err(error) => { + log::error!( + "Failed to build DBusMenu signal emitter for {object_path}: {error}" + ); + continue; + } + }; + if let Err(error) = + crate::linux::dbusmenu::DBusMenuServer::layout_updated( + &emitter, + revision, + parent, + ) + .await + { + log::error!( + "Failed to emit DBusMenu LayoutUpdated signal for {object_path}: {error}" + ); + } + } + } + crate::linux::dbusmenu::DBusMenuCommand::ItemsPropertiesUpdated { + updated_props, + removed_props, + object_paths, + } => { + for object_path in object_paths { + let emitter = match zbus::object_server::SignalEmitter::new( + &connection, + object_path.as_str(), + ) { + Ok(emitter) => emitter, + Err(error) => { + log::error!( + "Failed to build DBusMenu signal emitter for {object_path}: {error}" + ); + continue; + } + }; + if let Err(error) = + crate::linux::dbusmenu::DBusMenuServer::items_properties_updated( + &emitter, + updated_props.clone(), + removed_props.clone(), + ) + .await + { + log::error!( + "Failed to emit DBusMenu ItemsPropertiesUpdated for {object_path}: {error}" + ); + } + } + } } } }); @@ -876,18 +998,29 @@ impl LinuxClient for WaylandClient { parent, target_output, )?; - state.windows.insert(surface_id, window.0.clone()); + state.windows.insert(surface_id.clone(), window.0.clone()); + #[cfg(feature = "global-menu")] if let (Some(appmenu_manager), Some(service_name)) = ( state.globals.appmenu_manager.as_ref(), state.dbus_service_name.as_ref(), ) { + let dbus_menu_server = state.common.dbus_menu_server.clone(); + let mut object_path = + crate::linux::dbusmenu::object_path_for_window(surface_id.protocol_id()); + if let Some(dbus_menu_server) = dbus_menu_server.as_ref() { + if !dbus_menu_server + .ensure_exported_blocking(object_path.clone(), Duration::from_millis(250)) + { + object_path = crate::linux::dbusmenu::DBUSMENU_OBJECT_PATH.to_string(); + } + } else { + object_path = crate::linux::dbusmenu::DBUSMENU_OBJECT_PATH.to_string(); + } + let surface = window.0.surface(); let appmenu = appmenu_manager.create(&surface, &state.globals.qh, ()); - appmenu.set_address( - service_name.clone(), - crate::linux::dbusmenu::DBUSMENU_OBJECT_PATH.to_string(), - ); + appmenu.set_address(service_name.clone(), object_path); } Ok(Box::new(window)) @@ -1030,16 +1163,18 @@ impl LinuxClient for WaylandClient { } fn read_from_primary(&self) -> Option { - self.0.borrow_mut().clipboard.read_primary() + let mut state = self.0.try_borrow_mut().ok()?; + state.clipboard.read_primary() } fn read_from_clipboard(&self) -> Option { - self.0.borrow_mut().clipboard.read() + let mut state = self.0.try_borrow_mut().ok()?; + state.clipboard.read() } fn active_window(&self) -> Option { - self.0 - .borrow_mut() + let state = self.0.try_borrow().ok()?; + state .keyboard_focused_window .as_ref() .map(|window| window.handle()) @@ -1202,7 +1337,11 @@ delegate_noop!(WaylandClientStatePtr: ignore wp_fractional_scale_manager_v1::WpF delegate_noop!(WaylandClientStatePtr: ignore zxdg_decoration_manager_v1::ZxdgDecorationManagerV1); delegate_noop!(WaylandClientStatePtr: ignore zwlr_layer_shell_v1::ZwlrLayerShellV1); delegate_noop!(WaylandClientStatePtr: ignore org_kde_kwin_blur_manager::OrgKdeKwinBlurManager); -delegate_noop!(WaylandClientStatePtr: ignore org_kde_kwin_appmenu_manager::OrgKdeKwinAppmenuManager); +#[cfg(feature = "global-menu")] +delegate_noop!( + WaylandClientStatePtr: ignore org_kde_kwin_appmenu_manager::OrgKdeKwinAppmenuManager +); +#[cfg(feature = "global-menu")] delegate_noop!(WaylandClientStatePtr: ignore org_kde_kwin_appmenu::OrgKdeKwinAppmenu); delegate_noop!(WaylandClientStatePtr: ignore zwp_text_input_manager_v3::ZwpTextInputManagerV3); delegate_noop!(WaylandClientStatePtr: ignore org_kde_kwin_blur::OrgKdeKwinBlur); diff --git a/crates/gpui_platform/Cargo.toml b/crates/gpui_platform/Cargo.toml index cfb47b1851b..952589cc654 100644 --- a/crates/gpui_platform/Cargo.toml +++ b/crates/gpui_platform/Cargo.toml @@ -19,6 +19,7 @@ screen-capture = ["gpui/screen-capture", "gpui_macos/screen-capture", "gpui_wind runtime_shaders = ["gpui_macos/runtime_shaders"] wayland = ["gpui_linux/wayland"] x11 = ["gpui_linux/x11"] +global-menu = ["gpui_linux/global-menu"] [dependencies] gpui.workspace = true diff --git a/crates/title_bar/src/application_menu.rs b/crates/title_bar/src/application_menu.rs index 579e4dadbd5..eec27070696 100644 --- a/crates/title_bar/src/application_menu.rs +++ b/crates/title_bar/src/application_menu.rs @@ -270,6 +270,7 @@ impl ApplicationMenu { pub(crate) fn show_menus(cx: &mut App) -> bool { TitleBarSettings::get_global(cx).show_menus + && !cx.is_global_menu_active() && (cfg!(not(target_os = "macos")) || option_env!("ZED_USE_CROSS_PLATFORM_MENU").is_some()) } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index b38e5a774d7..2668c595977 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -13,6 +13,7 @@ workspace = true [features] tracy = ["ztracing/tracy"] +global-menu = ["gpui_platform/global-menu"] test-support = [ "gpui/test-support", "gpui_platform/screen-capture", From 1d4e61e9a9cbaeaa4ad72c332652013227d13c83 Mon Sep 17 00:00:00 2001 From: Nihal <121309701+nihalxkumar@users.noreply.github.com> Date: Thu, 12 Mar 2026 16:08:36 +0530 Subject: [PATCH 03/16] gpui_linux: Clean up global menu exports and validate enabled state on demand --- crates/gpui_linux/src/linux/dbusmenu.rs | 163 ++++++++++++++++-- crates/gpui_linux/src/linux/wayland/client.rs | 61 +++++++ 2 files changed, 207 insertions(+), 17 deletions(-) diff --git a/crates/gpui_linux/src/linux/dbusmenu.rs b/crates/gpui_linux/src/linux/dbusmenu.rs index f0a5f0e82b6..b184c1ebfdd 100644 --- a/crates/gpui_linux/src/linux/dbusmenu.rs +++ b/crates/gpui_linux/src/linux/dbusmenu.rs @@ -7,6 +7,7 @@ use std::sync::{ }; use std::time::Duration; +use calloop::channel::Sender as CalloopSender; use gpui::{Action, KeyContext, Keymap, KeybindingKeystroke, OwnedMenu, OwnedMenuItem}; use zbus::zvariant::{OwnedValue, Value}; @@ -31,6 +32,14 @@ pub enum DBusMenuCommand { removed_props: Vec<(i32, Vec)>, object_paths: Vec, }, + Unexport { + object_path: String, + }, +} + +pub struct ValidateRequest { + pub action: Box, + pub responded: std::sync::mpsc::Sender, } struct MenuItemEntry { @@ -51,6 +60,7 @@ pub struct DBusMenuServer { action_callback: Arc>>>, will_open_callback: Arc>>>, command_sender: Arc>>>, + validate_sender: Arc>>>, connected: Arc, object_paths: Arc>>, exported_paths: Arc>>, @@ -80,6 +90,7 @@ impl DBusMenuServer { action_callback: Arc::new(Mutex::new(None)), will_open_callback: Arc::new(Mutex::new(None)), command_sender: Arc::new(Mutex::new(None)), + validate_sender: Arc::new(Mutex::new(None)), connected: Arc::new(AtomicBool::new(false)), object_paths: Arc::new(Mutex::new(object_paths)), exported_paths: Arc::new(Mutex::new(exported_paths)), @@ -95,6 +106,15 @@ impl DBusMenuServer { } } + pub fn set_validate_sender(&self, sender: CalloopSender) { + match self.validate_sender.lock() { + Ok(mut slot) => *slot = Some(sender), + Err(error) => { + log::error!("Failed to store DBusMenu validate sender: {error}"); + } + } + } + pub fn mark_connected(&self) { self.connected.store(true, Ordering::SeqCst); @@ -238,6 +258,34 @@ impl DBusMenuServer { exported } + pub fn unexport_object_path(&self, object_path: String) { + if object_path == DBUSMENU_OBJECT_PATH { + return; + } + + if let Ok(mut object_paths) = self.object_paths.lock() { + object_paths.remove(&object_path); + } + if let Ok(mut exported_paths) = self.exported_paths.lock() { + exported_paths.remove(&object_path); + } + + let sender = match self.command_sender.lock() { + Ok(sender) => sender.clone(), + Err(error) => { + log::error!("Failed to read DBusMenu command sender: {error}"); + return; + } + }; + let Some(sender) = sender else { + return; + }; + + if let Err(error) = sender.try_send(DBusMenuCommand::Unexport { object_path }) { + log::error!("Failed to queue DBusMenu unexport request: {error}"); + } + } + pub fn refresh_enabled_states(&self, validate: &mut dyn FnMut(&dyn Action) -> bool) { let mut updated_props: Vec<(i32, HashMap)> = Vec::new(); @@ -280,6 +328,32 @@ impl DBusMenuServer { self.request_items_properties_updated(updated_props, Vec::new()); } + fn validate_enabled(&self, action: &dyn Action) -> Option { + let sender = match self.validate_sender.lock() { + Ok(sender) => sender, + Err(error) => { + log::error!("Failed to read DBusMenu validate sender: {error}"); + return None; + } + }; + let Some(sender) = sender.as_ref() else { + return None; + }; + + let (responded_tx, responded_rx) = std::sync::mpsc::channel(); + let request = ValidateRequest { + action: action.boxed_clone(), + responded: responded_tx, + }; + + if let Err(error) = sender.send(request) { + log::error!("Failed to send DBusMenu validate request: {error}"); + return None; + } + + responded_rx.recv_timeout(Duration::from_millis(20)).ok() + } + fn get_layout_node( &self, id: i32, @@ -466,19 +540,49 @@ impl DBusMenuServer { ids: Vec, _property_names: Vec, ) -> zbus::fdo::Result)>> { - let state = self - .state - .lock() - .map_err(|_| zbus::fdo::Error::Failed("Failed to access DBusMenu state".to_string()))?; - let result = ids - .into_iter() - .filter_map(|id| { - state - .items - .get(&id) - .map(|entry| (id, entry.properties.clone())) - }) - .collect(); + let entries = { + let state = self.state.lock().map_err(|_| { + zbus::fdo::Error::Failed("Failed to access DBusMenu state".to_string()) + })?; + ids.into_iter() + .filter_map(|id| { + state.items.get(&id).map(|entry| { + ( + id, + entry.properties.clone(), + entry.action.as_ref().map(|action| action.boxed_clone()), + ) + }) + }) + .collect::>() + }; + + let mut updated: Vec<(i32, bool, OwnedValue)> = Vec::new(); + let mut result = Vec::with_capacity(entries.len()); + + for (id, mut properties, action) in entries { + if let Some(action) = action { + if let Some(enabled) = self.validate_enabled(action.as_ref()) { + if let Some(value) = owned_bool(enabled) { + properties.insert("enabled".to_string(), value.clone()); + updated.push((id, enabled, value)); + } + } + } + result.push((id, properties)); + } + + if !updated.is_empty() { + if let Ok(mut state) = self.state.lock() { + for (id, enabled, value) in updated { + if let Some(entry) = state.items.get_mut(&id) { + entry.enabled = Some(enabled); + entry.properties.insert("enabled".to_string(), value); + } + } + } + } + Ok(result) } @@ -509,10 +613,35 @@ impl DBusMenuServer { } async fn get_property(&self, id: i32, name: &str) -> zbus::fdo::Result { - let state = self - .state - .lock() - .map_err(|_| zbus::fdo::Error::Failed("Failed to access DBusMenu state".to_string()))?; + if name == "enabled" { + let action = { + let state = self.state.lock().map_err(|_| { + zbus::fdo::Error::Failed("Failed to access DBusMenu state".to_string()) + })?; + state + .items + .get(&id) + .and_then(|entry| entry.action.as_ref().map(|action| action.boxed_clone())) + }; + + if let Some(action) = action { + if let Some(enabled) = self.validate_enabled(action.as_ref()) + && let Some(value) = owned_bool(enabled) + { + if let Ok(mut state) = self.state.lock() { + if let Some(entry) = state.items.get_mut(&id) { + entry.enabled = Some(enabled); + entry.properties.insert("enabled".to_string(), value.clone()); + } + } + return Ok(value); + } + } + } + + let state = self.state.lock().map_err(|_| { + zbus::fdo::Error::Failed("Failed to access DBusMenu state".to_string()) + })?; state .items .get(&id) diff --git a/crates/gpui_linux/src/linux/wayland/client.rs b/crates/gpui_linux/src/linux/wayland/client.rs index a9529a9f17a..9a46b3af0ff 100644 --- a/crates/gpui_linux/src/linux/wayland/client.rs +++ b/crates/gpui_linux/src/linux/wayland/client.rs @@ -418,6 +418,14 @@ impl WaylandClientStatePtr { pub fn drop_window(&self, surface_id: &ObjectId) { let client = self.get_client(); let mut state = client.borrow_mut(); + + #[cfg(feature = "global-menu")] + if let Some(dbus_menu_server) = state.common.dbus_menu_server.as_ref() { + let object_path = + crate::linux::dbusmenu::object_path_for_window(surface_id.protocol_id()); + dbus_menu_server.unexport_object_path(object_path); + } + let closed_window = state.windows.remove(surface_id).unwrap(); if let Some(window) = state.mouse_focused_window.take() && !window.ptr_eq(&closed_window) @@ -702,6 +710,8 @@ impl WaylandClient { // Channel for sending menu actions from the DBus thread to the main thread. let (action_tx, action_rx) = calloop::channel::channel::>(); let (will_open_tx, will_open_rx) = calloop::channel::channel::<()>(); + let (validate_tx, validate_rx) = + calloop::channel::channel::(); dbus_menu_server.set_action_callback({ let action_tx = action_tx.clone(); @@ -723,6 +733,7 @@ impl WaylandClient { } }) }); + dbus_menu_server.set_validate_sender(validate_tx); state .borrow() @@ -771,6 +782,38 @@ impl WaylandClient { }) .log_err(); + state + .borrow() + .loop_handle + .insert_source(validate_rx, { + let client = Rc::downgrade(&state); + move |event, _, _| { + if let calloop::channel::Event::Msg(request) = event { + if let Some(client) = client.upgrade() { + let enabled = { + let mut state = client.borrow_mut(); + match state + .common + .callbacks + .validate_app_menu_command + .as_mut() + { + Some(validate) => validate(request.action.as_ref()), + None => true, + } + }; + + if let Err(error) = request.responded.send(enabled) { + log::error!( + "Failed to send DBusMenu validate response: {error}" + ); + } + } + } + } + }) + .log_err(); + state.borrow_mut().common.dbus_menu_server = Some(dbus_menu_server.clone()); state.borrow_mut().dbus_service_name = Some(service_name.clone()); @@ -892,6 +935,24 @@ impl WaylandClient { } } } + crate::linux::dbusmenu::DBusMenuCommand::Unexport { + object_path, + } => { + let result = connection + .object_server() + .remove::( + object_path.as_str(), + ) + .await; + match result { + Ok(_) => {} + Err(error) => { + log::error!( + "Failed to unexport DBusMenu object at {object_path}: {error}" + ); + } + } + } } } }); From 1d6c89a76c6a66b1f39252b282fe2279bea4c3b5 Mon Sep 17 00:00:00 2001 From: Nihal <121309701+nihalxkumar@users.noreply.github.com> Date: Thu, 12 Mar 2026 22:06:49 +0530 Subject: [PATCH 04/16] gpui_linux: Add X11 export and some DBusMenu updates --- crates/gpui_linux/src/linux.rs | 2 +- crates/gpui_linux/src/linux/dbusmenu.rs | 99 +++- crates/gpui_linux/src/linux/platform.rs | 10 +- crates/gpui_linux/src/linux/wayland/client.rs | 118 +++- crates/gpui_linux/src/linux/x11/client.rs | 523 +++++++++++++++++- crates/gpui_linux/src/linux/x11/window.rs | 2 + 6 files changed, 713 insertions(+), 41 deletions(-) diff --git a/crates/gpui_linux/src/linux.rs b/crates/gpui_linux/src/linux.rs index fa5788bc178..a2e4c0166a5 100644 --- a/crates/gpui_linux/src/linux.rs +++ b/crates/gpui_linux/src/linux.rs @@ -1,4 +1,4 @@ -#[cfg(all(feature = "wayland", feature = "global-menu"))] +#[cfg(feature = "global-menu")] pub mod dbusmenu; mod dispatcher; mod headless; diff --git a/crates/gpui_linux/src/linux/dbusmenu.rs b/crates/gpui_linux/src/linux/dbusmenu.rs index b184c1ebfdd..fe5c2c80e80 100644 --- a/crates/gpui_linux/src/linux/dbusmenu.rs +++ b/crates/gpui_linux/src/linux/dbusmenu.rs @@ -8,7 +8,7 @@ use std::sync::{ use std::time::Duration; use calloop::channel::Sender as CalloopSender; -use gpui::{Action, KeyContext, Keymap, KeybindingKeystroke, OwnedMenu, OwnedMenuItem}; +use gpui::{Action, KeyContext, KeybindingKeystroke, Keymap, OsAction, OwnedMenu, OwnedMenuItem}; use zbus::zvariant::{OwnedValue, Value}; pub const DBUSMENU_OBJECT_PATH: &str = "/MenuBar"; @@ -35,6 +35,7 @@ pub enum DBusMenuCommand { Unexport { object_path: String, }, + Shutdown, } pub struct ValidateRequest { @@ -286,6 +287,23 @@ impl DBusMenuServer { } } + pub fn shutdown(&self) { + let sender = match self.command_sender.lock() { + Ok(sender) => sender.clone(), + Err(error) => { + log::error!("Failed to read DBusMenu command sender: {error}"); + return; + } + }; + let Some(sender) = sender else { + return; + }; + + if let Err(error) = sender.try_send(DBusMenuCommand::Shutdown) { + log::error!("Failed to queue DBusMenu shutdown request: {error}"); + } + } + pub fn refresh_enabled_states(&self, validate: &mut dyn FnMut(&dyn Action) -> bool) { let mut updated_props: Vec<(i32, HashMap)> = Vec::new(); @@ -313,7 +331,9 @@ impl DBusMenuServer { entry.enabled = Some(enabled); if let Some(value) = owned_bool(enabled) { - entry.properties.insert("enabled".to_string(), value.clone()); + entry + .properties + .insert("enabled".to_string(), value.clone()); let mut props = HashMap::new(); props.insert("enabled".to_string(), value); updated_props.push((*id, props)); @@ -631,7 +651,9 @@ impl DBusMenuServer { if let Ok(mut state) = self.state.lock() { if let Some(entry) = state.items.get_mut(&id) { entry.enabled = Some(enabled); - entry.properties.insert("enabled".to_string(), value.clone()); + entry + .properties + .insert("enabled".to_string(), value.clone()); } } return Ok(value); @@ -639,9 +661,10 @@ impl DBusMenuServer { } } - let state = self.state.lock().map_err(|_| { - zbus::fdo::Error::Failed("Failed to access DBusMenu state".to_string()) - })?; + let state = self + .state + .lock() + .map_err(|_| zbus::fdo::Error::Failed("Failed to access DBusMenu state".to_string()))?; state .items .get(&id) @@ -699,6 +722,7 @@ fn root_properties() -> HashMap { } else { log::error!("Failed to build DBusMenu root properties"); } + insert_visible_property(&mut props); props } @@ -722,6 +746,7 @@ fn build_menu_tree( } else { log::error!("Failed to encode DBusMenu children-display property"); } + insert_visible_property(&mut properties); let mut child_ids = Vec::new(); @@ -738,6 +763,7 @@ fn build_menu_tree( } else { log::error!("Failed to encode DBusMenu separator type"); } + insert_visible_property(&mut props); items.insert( child_id, MenuItemEntry { @@ -753,6 +779,7 @@ fn build_menu_tree( name, action, checked, + os_action, .. } => { let mut props = HashMap::new(); @@ -766,13 +793,31 @@ fn build_menu_tree( if let Some(value) = owned_bool(true) { props.insert("enabled".to_string(), value); } else { - log::error!("Failed to encode DBusMenu enabled state for menu item {}", name); + log::error!( + "Failed to encode DBusMenu enabled state for menu item {}", + name + ); } if let Some(shortcut) = dbus_shortcut_for_action(action.as_ref(), keymap) { props.insert("shortcut".to_string(), shortcut); } + if let Some(os_action) = os_action { + if let Some(icon_name) = icon_name_for_os_action(*os_action) { + match Value::Str(icon_name.into()).try_into() { + Ok(value) => { + props.insert("icon-name".to_string(), value); + } + Err(error) => { + log::error!( + "Failed to encode DBusMenu icon-name for {icon_name}: {error}" + ); + } + } + } + } + if *checked { let toggle_type = Value::Str("checkmark".into()); if let Ok(value) = toggle_type.try_into() { @@ -787,6 +832,7 @@ fn build_menu_tree( log::error!("Failed to encode DBusMenu toggle-state"); } } + insert_visible_property(&mut props); items.insert( child_id, MenuItemEntry { @@ -821,10 +867,43 @@ pub fn object_path_for_window(window_id: u32) -> String { format!("{DBUSMENU_OBJECT_PATH}/window_{window_id}") } +pub fn global_menu_env_override() -> Option { + match std::env::var("ZED_GLOBAL_MENU").ok().as_deref() { + None => None, + Some("1" | "true" | "TRUE" | "yes" | "YES" | "on" | "ON") => Some(true), + Some("0" | "false" | "FALSE" | "no" | "NO" | "off" | "OFF") => Some(false), + Some(value) => { + log::warn!( + "Ignoring invalid ZED_GLOBAL_MENU value {value:?}. Expected 0/1, true/false, yes/no, or on/off." + ); + None + } + } +} + fn owned_bool(value: bool) -> Option { Value::Bool(value).try_into().ok() } +fn insert_visible_property(props: &mut HashMap) { + if let Some(value) = owned_bool(true) { + props.insert("visible".to_string(), value); + } else { + log::error!("Failed to encode DBusMenu visible property"); + } +} + +fn icon_name_for_os_action(action: OsAction) -> Option<&'static str> { + match action { + OsAction::Cut => Some("edit-cut"), + OsAction::Copy => Some("edit-copy"), + OsAction::Paste => Some("edit-paste"), + OsAction::SelectAll => Some("edit-select-all"), + OsAction::Undo => Some("edit-undo"), + OsAction::Redo => Some("edit-redo"), + } +} + fn dbus_shortcut_for_action(action: &dyn Action, keymap: &Keymap) -> Option { static DEFAULT_CONTEXT: OnceLock> = OnceLock::new(); @@ -843,7 +922,11 @@ fn dbus_shortcut_for_action(action: &dyn Action, keymap: &Keymap) -> Option, - #[cfg(all(feature = "wayland", feature = "global-menu"))] + #[cfg(all(any(feature = "wayland", feature = "x11"), feature = "global-menu"))] pub(crate) dbus_menu_server: Option, } @@ -145,7 +145,7 @@ impl LinuxCommon { callbacks, signal, menus: Vec::new(), - #[cfg(all(feature = "wayland", feature = "global-menu"))] + #[cfg(all(any(feature = "wayland", feature = "x11"), feature = "global-menu"))] dbus_menu_server: None, }; @@ -213,7 +213,7 @@ impl Platform for LinuxPlatform

{ } fn is_global_menu_active(&self) -> bool { - #[cfg(all(feature = "wayland", feature = "global-menu"))] + #[cfg(all(any(feature = "wayland", feature = "x11"), feature = "global-menu"))] { self.inner.with_common(|common| { common @@ -222,7 +222,7 @@ impl Platform for LinuxPlatform

{ .is_some_and(|server| server.is_connected()) }) } - #[cfg(not(all(feature = "wayland", feature = "global-menu")))] + #[cfg(not(all(any(feature = "wayland", feature = "x11"), feature = "global-menu")))] { false } @@ -526,7 +526,7 @@ impl Platform for LinuxPlatform

{ fn set_menus(&self, menus: Vec

, keymap: &Keymap) { self.inner.with_common(|common| { common.menus = menus.into_iter().map(|menu| menu.owned()).collect(); - #[cfg(all(feature = "wayland", feature = "global-menu"))] + #[cfg(all(any(feature = "wayland", feature = "x11"), feature = "global-menu"))] if let Some(server) = &common.dbus_menu_server { server.set_menus(common.menus.clone(), keymap); } diff --git a/crates/gpui_linux/src/linux/wayland/client.rs b/crates/gpui_linux/src/linux/wayland/client.rs index 9a46b3af0ff..446e1113de4 100644 --- a/crates/gpui_linux/src/linux/wayland/client.rs +++ b/crates/gpui_linux/src/linux/wayland/client.rs @@ -107,21 +107,6 @@ use wayland_protocols::wp::linux_dmabuf::zv1::client::{ zwp_linux_dmabuf_feedback_v1, zwp_linux_dmabuf_v1, }; -#[cfg(feature = "global-menu")] -fn global_menu_env_override() -> Option { - match std::env::var("ZED_GLOBAL_MENU").ok().as_deref() { - None => None, - Some("1" | "true" | "TRUE" | "yes" | "YES" | "on" | "ON") => Some(true), - Some("0" | "false" | "FALSE" | "no" | "NO" | "off" | "OFF") => Some(false), - Some(value) => { - log::warn!( - "Ignoring invalid ZED_GLOBAL_MENU value {value:?}. Expected 0/1, true/false, yes/no, or on/off." - ); - None - } - } -} - /// Used to convert evdev scancode to xkb scancode const MIN_KEYCODE: u32 = 8; @@ -281,6 +266,8 @@ pub(crate) struct WaylandClientState { event_loop: Option>, #[cfg(feature = "global-menu")] dbus_service_name: Option, + #[cfg(feature = "global-menu")] + dbus_menu_thread: Option>, pub common: LinuxCommon, } @@ -445,6 +432,26 @@ pub struct WaylandClient(Rc>); impl Drop for WaylandClient { fn drop(&mut self) { + #[cfg(feature = "global-menu")] + { + let (dbus_menu_server, dbus_menu_thread) = match self.0.try_borrow_mut() { + Ok(mut state) => ( + state.common.dbus_menu_server.clone(), + state.dbus_menu_thread.take(), + ), + Err(_) => (None, None), + }; + + if let Some(dbus_menu_server) = dbus_menu_server { + dbus_menu_server.shutdown(); + } + if let Some(thread) = dbus_menu_thread { + if let Err(error) = thread.join() { + log::error!("Failed to join DBusMenu thread: {error:?}"); + } + } + } + let mut state = self.0.borrow_mut(); state.windows.clear(); @@ -687,6 +694,8 @@ impl WaylandClient { event_loop: Some(event_loop), #[cfg(feature = "global-menu")] dbus_service_name: None, + #[cfg(feature = "global-menu")] + dbus_menu_thread: None, })); WaylandSource::new(conn, event_queue) @@ -697,8 +706,8 @@ impl WaylandClient { #[cfg(feature = "global-menu")] { let has_appmenu = state.borrow().globals.appmenu_manager.is_some(); - let enabled = match global_menu_env_override() { - Some(true) => has_appmenu, + let enabled = match crate::linux::dbusmenu::global_menu_env_override() { + Some(true) => true, Some(false) => false, None => has_appmenu, }; @@ -763,19 +772,38 @@ impl WaylandClient { move |event, _, _| { if let calloop::channel::Event::Msg(()) = event { if let Some(client) = client.upgrade() { - let mut state = client.borrow_mut(); - let dbus_menu_server = state.common.dbus_menu_server.clone(); - if let Some(callback) = - state.common.callbacks.will_open_app_menu.as_mut() - { + let ( + dbus_menu_server, + mut validate_app_menu_command, + mut will_open_app_menu, + ) = { + let mut state = client.borrow_mut(); + ( + state.common.dbus_menu_server.clone(), + state.common.callbacks.validate_app_menu_command.take(), + state.common.callbacks.will_open_app_menu.take(), + ) + }; + + if let Some(callback) = will_open_app_menu.as_mut() { callback(); } if let (Some(dbus_menu_server), Some(validate_callback)) = ( dbus_menu_server.as_ref(), - state.common.callbacks.validate_app_menu_command.as_mut(), + validate_app_menu_command.as_mut(), ) { dbus_menu_server.refresh_enabled_states(validate_callback); } + + let mut state = client.borrow_mut(); + if state.common.callbacks.validate_app_menu_command.is_none() { + state.common.callbacks.validate_app_menu_command = + validate_app_menu_command; + } + if state.common.callbacks.will_open_app_menu.is_none() { + state.common.callbacks.will_open_app_menu = + will_open_app_menu; + } } } } @@ -814,13 +842,48 @@ impl WaylandClient { }) .log_err(); + let refresh_interval = Duration::from_millis(250); + state + .borrow() + .loop_handle + .insert_source(Timer::from_duration(refresh_interval), { + let client = Rc::downgrade(&state); + move |event_timestamp, _, _| { + let Some(client) = client.upgrade() else { + return TimeoutAction::Drop; + }; + let (dbus_menu_server, mut validate_app_menu_command) = { + let mut state = client.borrow_mut(); + ( + state.common.dbus_menu_server.clone(), + state.common.callbacks.validate_app_menu_command.take(), + ) + }; + + if let (Some(dbus_menu_server), Some(validate_callback)) = ( + dbus_menu_server.as_ref(), + validate_app_menu_command.as_mut(), + ) { + dbus_menu_server.refresh_enabled_states(validate_callback); + } + + let mut state = client.borrow_mut(); + if state.common.callbacks.validate_app_menu_command.is_none() { + state.common.callbacks.validate_app_menu_command = + validate_app_menu_command; + } + TimeoutAction::ToInstant(event_timestamp + refresh_interval) + } + }) + .log_err(); + state.borrow_mut().common.dbus_menu_server = Some(dbus_menu_server.clone()); state.borrow_mut().dbus_service_name = Some(service_name.clone()); let object_path = crate::linux::dbusmenu::DBUSMENU_OBJECT_PATH.to_string(); let (dbus_command_tx, dbus_command_rx) = async_channel::unbounded(); dbus_menu_server.set_command_sender(dbus_command_tx); - std::thread::Builder::new() + let dbus_menu_thread = std::thread::Builder::new() .name("dbus-menu".into()) .spawn(move || { smol::block_on(async move { @@ -953,11 +1016,18 @@ impl WaylandClient { } } } + crate::linux::dbusmenu::DBusMenuCommand::Shutdown => { + break; + } } } }); }) .log_err(); + + if let Some(thread) = dbus_menu_thread { + state.borrow_mut().dbus_menu_thread = Some(thread); + } } } diff --git a/crates/gpui_linux/src/linux/x11/client.rs b/crates/gpui_linux/src/linux/x11/client.rs index 1f8db390029..061295978ef 100644 --- a/crates/gpui_linux/src/linux/x11/client.rs +++ b/crates/gpui_linux/src/linux/x11/client.rs @@ -1,5 +1,7 @@ use anyhow::{Context as _, anyhow}; use ashpd::WindowIdentifier; +#[cfg(feature = "global-menu")] +use calloop::timer::{TimeoutAction, Timer}; use calloop::{ EventLoop, LoopHandle, RegistrationToken, generic::{FdWrapper, Generic}, @@ -10,6 +12,8 @@ use gpui::{Capslock, TaskTiming, profiler}; use http_client::Url; use log::Level; use smallvec::SmallVec; +#[cfg(feature = "global-menu")] +use std::sync::Arc; use std::{ cell::RefCell, collections::{BTreeMap, HashSet}, @@ -58,6 +62,8 @@ use crate::linux::{ }; use crate::linux::{LinuxCommon, LinuxKeyboardLayout, X11Window, modifiers_from_xinput_info}; +#[cfg(feature = "global-menu")] +use gpui::Action; use gpui::{ AnyWindowHandle, Bounds, ClipboardItem, CursorStyle, DisplayId, FileDropEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, Pixels, PlatformDisplay, PlatformInput, @@ -213,6 +219,13 @@ pub struct X11ClientState { pointer_device_states: BTreeMap, + #[cfg(feature = "global-menu")] + pub(crate) dbus_service_name: Option, + #[cfg(feature = "global-menu")] + pub(crate) dbus_unique_name: Option, + #[cfg(feature = "global-menu")] + pub(crate) dbus_menu_thread: Option>, + pub(crate) common: LinuxCommon, pub(crate) clipboard: Clipboard, pub(crate) clipboard_item: Option, @@ -233,6 +246,12 @@ impl X11ClientStatePtr { }; let mut state = client.0.borrow_mut(); + #[cfg(feature = "global-menu")] + if let Some(dbus_menu_server) = state.common.dbus_menu_server.as_ref() { + let object_path = crate::linux::dbusmenu::object_path_for_window(x_window); + dbus_menu_server.unexport_object_path(object_path); + } + if let Some(window_ref) = state.windows.remove(&x_window) && let Some(RefreshState::PeriodicRefresh { event_loop_token, .. @@ -297,6 +316,32 @@ impl X11ClientStatePtr { #[derive(Clone)] pub(crate) struct X11Client(pub(crate) Rc>); +impl Drop for X11Client { + fn drop(&mut self) { + #[cfg(feature = "global-menu")] + { + let (dbus_menu_server, dbus_menu_thread) = match self.0.try_borrow_mut() { + Ok(mut state) => ( + state.common.dbus_menu_server.clone(), + state.dbus_menu_thread.take(), + ), + Err(_) => { + return; + } + }; + + if let Some(dbus_menu_server) = dbus_menu_server { + dbus_menu_server.shutdown(); + } + if let Some(thread) = dbus_menu_thread { + if let Err(error) = thread.join() { + log::error!("Failed to join DBusMenu thread: {error:?}"); + } + } + } + } +} + impl X11Client { pub(crate) fn new() -> anyhow::Result { let event_loop = EventLoop::try_new()?; @@ -481,7 +526,7 @@ impl X11Client { xcb_flush(&xcb_connection); - Ok(X11Client(Rc::new(RefCell::new(X11ClientState { + let state = Rc::new(RefCell::new(X11ClientState { modifiers: Modifiers::default(), capslock: Capslock::default(), last_modifiers_changed_event: Modifiers::default(), @@ -498,7 +543,7 @@ impl X11Client { scale_factor, xkb_context, - xcb_connection, + xcb_connection: xcb_connection.clone(), xkb_device_id, client_side_decorations_supported, x_root_index, @@ -523,10 +568,387 @@ impl X11Client { pointer_device_states, + #[cfg(feature = "global-menu")] + dbus_service_name: None, + #[cfg(feature = "global-menu")] + dbus_unique_name: None, + #[cfg(feature = "global-menu")] + dbus_menu_thread: None, + clipboard, clipboard_item: None, xdnd_state: Xdnd::default(), - })))) + })); + + #[cfg(feature = "global-menu")] + { + let root = xcb_connection.setup().roots[x_root_index].root; + let has_appmenu = x11_global_menu_supported(&xcb_connection, &atoms, root); + let enabled = match crate::linux::dbusmenu::global_menu_env_override() { + Some(true) => true, + Some(false) => false, + None => has_appmenu, + }; + + if enabled { + let dbus_menu_server = crate::linux::dbusmenu::DBusMenuServer::new(); + let service_name = format!("com.zed.dbusmenu.pid{}", std::process::id()); + + let (action_tx, action_rx) = calloop::channel::channel::>(); + let (will_open_tx, will_open_rx) = calloop::channel::channel::<()>(); + let (validate_tx, validate_rx) = + calloop::channel::channel::(); + let (unique_name_tx, unique_name_rx) = calloop::channel::channel::(); + + dbus_menu_server.set_action_callback({ + let action_tx = action_tx.clone(); + Box::new(move |action| { + if let Err(error) = action_tx.send(action) { + log::error!( + "Failed to send DBus menu action to the X11 event loop: {error}" + ); + } + }) + }); + dbus_menu_server.set_will_open_callback({ + let will_open_tx = will_open_tx.clone(); + Arc::new(move || { + if let Err(error) = will_open_tx.send(()) { + log::error!( + "Failed to send DBus menu open notification to the X11 event loop: {error}" + ); + } + }) + }); + dbus_menu_server.set_validate_sender(validate_tx); + + state + .borrow() + .loop_handle + .insert_source(action_rx, { + let client = Rc::downgrade(&state); + move |event, _, _| { + if let calloop::channel::Event::Msg(action) = event { + if let Some(client) = client.upgrade() { + let mut state = client.borrow_mut(); + if let Some(callback) = + state.common.callbacks.app_menu_action.as_mut() + { + callback(action.as_ref()); + } + } + } + } + }) + .log_err(); + + state + .borrow() + .loop_handle + .insert_source(will_open_rx, { + let client = Rc::downgrade(&state); + move |event, _, _| { + if let calloop::channel::Event::Msg(()) = event { + if let Some(client) = client.upgrade() { + let ( + dbus_menu_server, + mut validate_app_menu_command, + mut will_open_app_menu, + ) = { + let mut state = client.borrow_mut(); + ( + state.common.dbus_menu_server.clone(), + state.common.callbacks.validate_app_menu_command.take(), + state.common.callbacks.will_open_app_menu.take(), + ) + }; + + if let Some(callback) = will_open_app_menu.as_mut() { + callback(); + } + if let (Some(dbus_menu_server), Some(validate_callback)) = ( + dbus_menu_server.as_ref(), + validate_app_menu_command.as_mut(), + ) { + dbus_menu_server.refresh_enabled_states(validate_callback); + } + + let mut state = client.borrow_mut(); + if state.common.callbacks.validate_app_menu_command.is_none() { + state.common.callbacks.validate_app_menu_command = + validate_app_menu_command; + } + if state.common.callbacks.will_open_app_menu.is_none() { + state.common.callbacks.will_open_app_menu = + will_open_app_menu; + } + } + } + } + }) + .log_err(); + + state + .borrow() + .loop_handle + .insert_source(validate_rx, { + let client = Rc::downgrade(&state); + move |event, _, _| { + if let calloop::channel::Event::Msg(request) = event { + if let Some(client) = client.upgrade() { + let enabled = { + let mut state = client.borrow_mut(); + match state + .common + .callbacks + .validate_app_menu_command + .as_mut() + { + Some(validate) => validate(request.action.as_ref()), + None => true, + } + }; + + if let Err(error) = request.responded.send(enabled) { + log::error!( + "Failed to send DBusMenu validate response: {error}" + ); + } + } + } + } + }) + .log_err(); + + state + .borrow() + .loop_handle + .insert_source(unique_name_rx, { + let client = Rc::downgrade(&state); + move |event, _, _| { + if let calloop::channel::Event::Msg(unique_name) = event { + if let Some(client) = client.upgrade() { + let mut state = client.borrow_mut(); + state.dbus_unique_name = Some(unique_name.clone()); + let window_ids: Vec = + state.windows.keys().copied().collect(); + for x_window in window_ids { + let object_path = + crate::linux::dbusmenu::object_path_for_window( + x_window, + ); + set_x11_appmenu_properties( + &state.xcb_connection, + &state.atoms, + x_window, + &unique_name, + &object_path, + ); + } + xcb_flush(&state.xcb_connection); + } + } + } + }) + .log_err(); + + let refresh_interval = Duration::from_millis(250); + state + .borrow() + .loop_handle + .insert_source(Timer::from_duration(refresh_interval), { + let client = Rc::downgrade(&state); + move |event_timestamp, _, _| { + let Some(client) = client.upgrade() else { + return TimeoutAction::Drop; + }; + let (dbus_menu_server, mut validate_app_menu_command) = { + let mut state = client.borrow_mut(); + ( + state.common.dbus_menu_server.clone(), + state.common.callbacks.validate_app_menu_command.take(), + ) + }; + + if let (Some(dbus_menu_server), Some(validate_callback)) = ( + dbus_menu_server.as_ref(), + validate_app_menu_command.as_mut(), + ) { + dbus_menu_server.refresh_enabled_states(validate_callback); + } + + let mut state = client.borrow_mut(); + if state.common.callbacks.validate_app_menu_command.is_none() { + state.common.callbacks.validate_app_menu_command = + validate_app_menu_command; + } + TimeoutAction::ToInstant(event_timestamp + refresh_interval) + } + }) + .log_err(); + + state.borrow_mut().common.dbus_menu_server = Some(dbus_menu_server.clone()); + state.borrow_mut().dbus_service_name = Some(service_name.clone()); + + let object_path = crate::linux::dbusmenu::DBUSMENU_OBJECT_PATH.to_string(); + let (dbus_command_tx, dbus_command_rx) = async_channel::unbounded(); + dbus_menu_server.set_command_sender(dbus_command_tx); + let dbus_menu_thread = std::thread::Builder::new() + .name("dbus-menu".into()) + .spawn(move || { + smol::block_on(async move { + let dbus_menu_server_for_service = dbus_menu_server.clone(); + let builder = match zbus::connection::Builder::session() + .and_then(|builder| builder.name(service_name.as_str())) + .and_then(|builder| { + builder.serve_at( + object_path.as_str(), + dbus_menu_server_for_service, + ) + }) { + Ok(builder) => builder, + Err(error) => { + log::error!("Failed to configure DBus connection: {error}"); + return; + } + }; + + let connection = match builder.build().await { + Ok(connection) => connection, + Err(error) => { + log::error!("Failed to build DBus connection: {error}"); + return; + } + }; + + dbus_menu_server.mark_connected(); + log::info!("DBusMenu server started on {service_name}"); + if let Some(unique_name) = connection.unique_name() { + if let Err(error) = unique_name_tx.send(unique_name.to_string()) { + log::error!( + "Failed to send DBusMenu unique name: {error}" + ); + } + } + + while let Ok(command) = dbus_command_rx.recv().await { + match command { + crate::linux::dbusmenu::DBusMenuCommand::EnsureExported { + object_path, + responded, + } => { + let result = connection + .object_server() + .at(object_path.as_str(), dbus_menu_server.clone()) + .await; + let ok = result.is_ok(); + if ok { + dbus_menu_server.note_exported(object_path); + } + if let Err(error) = responded.send(ok) { + log::error!( + "Failed to send DBusMenu export response: {error}" + ); + } + } + crate::linux::dbusmenu::DBusMenuCommand::LayoutUpdated { + revision, + parent, + object_paths, + } => { + for object_path in object_paths { + let emitter = + match zbus::object_server::SignalEmitter::new( + &connection, + object_path.as_str(), + ) { + Ok(emitter) => emitter, + Err(error) => { + log::error!( + "Failed to build DBusMenu signal emitter for {object_path}: {error}" + ); + continue; + } + }; + if let Err(error) = crate::linux::dbusmenu::DBusMenuServer::layout_updated( + &emitter, + revision, + parent, + ) + .await + { + log::error!( + "Failed to emit DBusMenu LayoutUpdated signal for {object_path}: {error}" + ); + } + } + } + crate::linux::dbusmenu::DBusMenuCommand::ItemsPropertiesUpdated { + updated_props, + removed_props, + object_paths, + } => { + for object_path in object_paths { + let emitter = + match zbus::object_server::SignalEmitter::new( + &connection, + object_path.as_str(), + ) { + Ok(emitter) => emitter, + Err(error) => { + log::error!( + "Failed to build DBusMenu signal emitter for {object_path}: {error}" + ); + continue; + } + }; + if let Err(error) = crate::linux::dbusmenu::DBusMenuServer::items_properties_updated( + &emitter, + updated_props.clone(), + removed_props.clone(), + ) + .await + { + log::error!( + "Failed to emit DBusMenu ItemsPropertiesUpdated for {object_path}: {error}" + ); + } + } + } + crate::linux::dbusmenu::DBusMenuCommand::Unexport { + object_path, + } => { + let result = connection + .object_server() + .remove::( + object_path.as_str(), + ) + .await; + match result { + Ok(_) => {} + Err(error) => { + log::error!( + "Failed to unexport DBusMenu object at {object_path}: {error}" + ); + } + } + } + crate::linux::dbusmenu::DBusMenuCommand::Shutdown => { + break; + } + } + } + }); + }) + .log_err(); + + if let Some(thread) = dbus_menu_thread { + state.borrow_mut().dbus_menu_thread = Some(thread); + } + } + } + + Ok(X11Client(state)) } pub fn process_x11_events( @@ -1547,6 +1969,29 @@ impl LinuxClient for X11Client { ), ) .log_err(); + + #[cfg(feature = "global-menu")] + if let Some(service_name) = x11_appmenu_service_name(&state) { + let dbus_menu_server = state.common.dbus_menu_server.clone(); + let mut object_path = crate::linux::dbusmenu::object_path_for_window(x_window); + if let Some(dbus_menu_server) = dbus_menu_server.as_ref() { + if !dbus_menu_server + .ensure_exported_blocking(object_path.clone(), Duration::from_millis(250)) + { + object_path = crate::linux::dbusmenu::DBUSMENU_OBJECT_PATH.to_string(); + } + } else { + object_path = crate::linux::dbusmenu::DBUSMENU_OBJECT_PATH.to_string(); + } + + set_x11_appmenu_properties( + &state.xcb_connection, + &state.atoms, + x_window, + &service_name, + &object_path, + ); + } xcb_flush(&state.xcb_connection); let window_ref = WindowRef { @@ -2104,6 +2549,78 @@ fn check_gtk_frame_extents_supported( supported_atom_ids.contains(&atoms._GTK_FRAME_EXTENTS) } +#[cfg(feature = "global-menu")] +fn x11_global_menu_supported( + xcb_connection: &XCBConnection, + atoms: &XcbAtoms, + root: xproto::Window, +) -> bool { + let Some(supported_atoms) = get_reply( + || "Failed to get _NET_SUPPORTED", + xcb_connection.get_property( + false, + root, + atoms._NET_SUPPORTED, + xproto::AtomEnum::ATOM, + 0, + 1024, + ), + ) + .log_with_level(Level::Debug) else { + return false; + }; + + let supported_atom_ids: Vec = supported_atoms + .value + .chunks_exact(4) + .filter_map(|chunk| chunk.try_into().ok().map(u32::from_ne_bytes)) + .collect(); + + supported_atom_ids.contains(&atoms._KDE_NET_WM_APPMENU_SERVICE_NAME) + || supported_atom_ids.contains(&atoms._KDE_NET_WM_APPMENU_OBJECT_PATH) +} + +#[cfg(feature = "global-menu")] +fn x11_appmenu_service_name(state: &X11ClientState) -> Option { + state + .dbus_unique_name + .clone() + .or_else(|| state.dbus_service_name.clone()) +} + +#[cfg(feature = "global-menu")] +fn set_x11_appmenu_properties( + xcb_connection: &XCBConnection, + atoms: &XcbAtoms, + x_window: xproto::Window, + service_name: &str, + object_path: &str, +) { + check_reply( + || "X11 ChangeProperty for _KDE_NET_WM_APPMENU_SERVICE_NAME failed.", + xcb_connection.change_property8( + xproto::PropMode::REPLACE, + x_window, + atoms._KDE_NET_WM_APPMENU_SERVICE_NAME, + xproto::AtomEnum::STRING, + service_name.as_bytes(), + ), + ) + .log_err(); + + check_reply( + || "X11 ChangeProperty for _KDE_NET_WM_APPMENU_OBJECT_PATH failed.", + xcb_connection.change_property8( + xproto::PropMode::REPLACE, + x_window, + atoms._KDE_NET_WM_APPMENU_OBJECT_PATH, + xproto::AtomEnum::STRING, + object_path.as_bytes(), + ), + ) + .log_err(); +} + fn xdnd_is_atom_supported(atom: u32, atoms: &XcbAtoms) -> bool { atom == atoms.TEXT || atom == atoms.STRING diff --git a/crates/gpui_linux/src/linux/x11/window.rs b/crates/gpui_linux/src/linux/x11/window.rs index 57600103ce9..27356f926ea 100644 --- a/crates/gpui_linux/src/linux/x11/window.rs +++ b/crates/gpui_linux/src/linux/x11/window.rs @@ -82,6 +82,8 @@ x11rb::atom_manager! { _GTK_FRAME_EXTENTS, _GTK_EDGE_CONSTRAINTS, _NET_CLIENT_LIST_STACKING, + _KDE_NET_WM_APPMENU_SERVICE_NAME, + _KDE_NET_WM_APPMENU_OBJECT_PATH, } } From 55392c316aee4aeb0b1d41939c992ee1ec58fc9f Mon Sep 17 00:00:00 2001 From: Nihal <121309701+nihalxkumar@users.noreply.github.com> Date: Fri, 13 Mar 2026 13:45:58 +0530 Subject: [PATCH 05/16] gpui_linux: Simplify global_menu architecture and fix bugs - Fix about_to_show to synchronously refresh state and return accurate results - Fix Wayland appmenu object lifecycle (store per-window, release on close) - Fix refresh_enabled_states lock hygiene (validate outside mutex) - Fix event() to skip dispatch for disabled items - Fix get_layout snapshot consistency (single lock hold) - Add checkable field to MenuItem for correct toggle-state encoding - Respect propertyNames filter in GetLayout/GetGroupProperties - Normalize shortcut keys to X11 keysym names, support multi-keystroke - Simplify to single /MenuBar object (remove per-window export complexity) - Extract shared setup into setup_global_menu_sources/spawn_dbus_menu_thread - Remove 250ms polling timer and cross-thread validate_sender - Add connected callback to handle DBus startup race --- crates/gpui/src/platform/app_menu.rs | 13 + crates/gpui_linux/src/linux/dbusmenu.rs | 906 +++++++++++------- crates/gpui_linux/src/linux/platform.rs | 4 +- crates/gpui_linux/src/linux/wayland/client.rs | 373 ++----- crates/gpui_linux/src/linux/x11/client.rs | 360 +------ crates/gpui_macos/src/platform.rs | 1 + crates/livekit_client/examples/test_app.rs | 1 + 7 files changed, 652 insertions(+), 1006 deletions(-) diff --git a/crates/gpui/src/platform/app_menu.rs b/crates/gpui/src/platform/app_menu.rs index b1e0d82bb9f..06393f4fa5b 100644 --- a/crates/gpui/src/platform/app_menu.rs +++ b/crates/gpui/src/platform/app_menu.rs @@ -70,6 +70,9 @@ pub enum MenuItem { /// See [`OsAction`] for more information os_action: Option, + /// Whether this action is checkable + checkable: bool, + /// Whether this action is checked checked: bool, }, @@ -100,6 +103,7 @@ impl MenuItem { name: name.into(), action: Box::new(action), os_action: None, + checkable: false, checked: false, } } @@ -114,6 +118,7 @@ impl MenuItem { name: name.into(), action: Box::new(action), os_action: Some(os_action), + checkable: false, checked: false, } } @@ -127,11 +132,13 @@ impl MenuItem { name, action, os_action, + checkable, checked, } => OwnedMenuItem::Action { name: name.into(), action, os_action, + checkable, checked, }, MenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.owned()), @@ -152,6 +159,7 @@ impl MenuItem { name, action, os_action, + checkable: true, checked, }, _ => self, @@ -204,6 +212,9 @@ pub enum OwnedMenuItem { /// See [`OsAction`] for more information os_action: Option, + /// Whether this action is checkable + checkable: bool, + /// Whether this action is checked checked: bool, }, @@ -218,11 +229,13 @@ impl Clone for OwnedMenuItem { name, action, os_action, + checkable, checked, } => OwnedMenuItem::Action { name: name.clone(), action: action.boxed_clone(), os_action: *os_action, + checkable: *checkable, checked: *checked, }, OwnedMenuItem::SystemMenu(os_menu) => OwnedMenuItem::SystemMenu(os_menu.clone()), diff --git a/crates/gpui_linux/src/linux/dbusmenu.rs b/crates/gpui_linux/src/linux/dbusmenu.rs index fe5c2c80e80..3794c48b948 100644 --- a/crates/gpui_linux/src/linux/dbusmenu.rs +++ b/crates/gpui_linux/src/linux/dbusmenu.rs @@ -7,21 +7,116 @@ use std::sync::{ }; use std::time::Duration; -use calloop::channel::Sender as CalloopSender; +use calloop::{LoopHandle, channel::Sender as CalloopSender}; use gpui::{Action, KeyContext, KeybindingKeystroke, Keymap, OsAction, OwnedMenu, OwnedMenuItem}; +use util::ResultExt as _; use zbus::zvariant::{OwnedValue, Value}; +use std::rc::Weak; +use std::cell::RefCell; + +pub trait GlobalMenuState { + fn linux_common(&mut self) -> &mut crate::linux::LinuxCommon; +} + +pub fn setup_global_menu_sources( + dbus_menu_server: &DBusMenuServer, + loop_handle: &LoopHandle, + client: Weak>, + mut on_connected: impl FnMut(&mut T) + 'static, +) { + dbus_menu_server.install_global_menu_sources( + loop_handle, + { + let client = client.clone(); + move |action| { + if let Some(client) = client.upgrade() { + let mut state = client.borrow_mut(); + if let Some(callback) = state.linux_common().callbacks.app_menu_action.as_mut() { + callback(action.as_ref()); + } + } + } + }, + { + let client = client.clone(); + move |request| { + if let Some(client) = client.upgrade() { + let (dbus_menu_server, mut validate_app_menu_command, mut will_open_app_menu) = { + let mut state = client.borrow_mut(); + let common = state.linux_common(); + ( + common.dbus_menu_server.clone(), + common.callbacks.validate_app_menu_command.take(), + common.callbacks.will_open_app_menu.take(), + ) + }; + + let request_ids = request.ids; + if let Some(callback) = will_open_app_menu.as_mut() { + callback(); + } + + let (valid_ids, id_errors) = dbus_menu_server + .as_ref() + .map(|dbus_menu_server| { + dbus_menu_server.classify_ids(&request_ids) + }) + .unwrap_or_else(|| (Vec::new(), request_ids.clone())); + + let refreshed_ids = match ( + dbus_menu_server.as_ref(), + validate_app_menu_command.as_mut(), + ) { + (Some(dbus_menu_server), Some(validate_callback)) => { + dbus_menu_server.refresh_enabled_states_inner(validate_callback) + } + _ => Vec::new(), + }; + + let updated_ids = if refreshed_ids.is_empty() { + Vec::new() + } else { + valid_ids + }; + + let mut state = client.borrow_mut(); + let common = state.linux_common(); + if common.callbacks.validate_app_menu_command.is_none() { + common.callbacks.validate_app_menu_command = validate_app_menu_command; + } + if common.callbacks.will_open_app_menu.is_none() { + common.callbacks.will_open_app_menu = will_open_app_menu; + } + + let _ = request.responded.send( + AboutToShowResponse { + updated_ids, + id_errors, + }, + ); + } + } + }, + { + let client = client.clone(); + move || { + if let Some(client) = client.upgrade() { + let mut state = client.borrow_mut(); + on_connected(&mut state); + } + } + }, + ); +} + pub const DBUSMENU_OBJECT_PATH: &str = "/MenuBar"; type ActionCallback = dyn Fn(Box) + Send + Sync; -type WillOpenCallback = dyn Fn() + Send + Sync; +type ConnectedCallback = dyn Fn() + Send + Sync; #[derive(Clone)] pub enum DBusMenuCommand { - EnsureExported { - object_path: String, - responded: std::sync::mpsc::Sender, - }, LayoutUpdated { revision: u32, parent: i32, @@ -32,15 +127,17 @@ pub enum DBusMenuCommand { removed_props: Vec<(i32, Vec)>, object_paths: Vec, }, - Unexport { - object_path: String, - }, Shutdown, } -pub struct ValidateRequest { - pub action: Box, - pub responded: std::sync::mpsc::Sender, +pub struct AboutToShowRequest { + pub ids: Vec, + pub responded: std::sync::mpsc::Sender, +} + +pub struct AboutToShowResponse { + pub updated_ids: Vec, + pub id_errors: Vec, } struct MenuItemEntry { @@ -59,12 +156,10 @@ struct MenuState { pub struct DBusMenuServer { state: Arc>, action_callback: Arc>>>, - will_open_callback: Arc>>>, + connected_callback: Arc>>>, command_sender: Arc>>>, - validate_sender: Arc>>>, + about_to_show_sender: Arc>>>, connected: Arc, - object_paths: Arc>>, - exported_paths: Arc>>, } impl DBusMenuServer { @@ -80,21 +175,13 @@ impl DBusMenuServer { }, ); - let mut object_paths = HashSet::new(); - object_paths.insert(DBUSMENU_OBJECT_PATH.to_string()); - - let mut exported_paths = HashSet::new(); - exported_paths.insert(DBUSMENU_OBJECT_PATH.to_string()); - Self { state: Arc::new(Mutex::new(MenuState { items, revision: 1 })), action_callback: Arc::new(Mutex::new(None)), - will_open_callback: Arc::new(Mutex::new(None)), + connected_callback: Arc::new(Mutex::new(None)), command_sender: Arc::new(Mutex::new(None)), - validate_sender: Arc::new(Mutex::new(None)), + about_to_show_sender: Arc::new(Mutex::new(None)), connected: Arc::new(AtomicBool::new(false)), - object_paths: Arc::new(Mutex::new(object_paths)), - exported_paths: Arc::new(Mutex::new(exported_paths)), } } @@ -107,11 +194,181 @@ impl DBusMenuServer { } } - pub fn set_validate_sender(&self, sender: CalloopSender) { - match self.validate_sender.lock() { + pub fn spawn_dbus_menu_thread( + &self, + service_name: String, + object_path: String, + unique_name_sender: Option>, + ) -> Option> { + let (dbus_command_tx, dbus_command_rx) = async_channel::unbounded(); + self.set_command_sender(dbus_command_tx); + + let dbus_menu_server = self.clone(); + std::thread::Builder::new() + .name("dbus-menu".into()) + .spawn(move || { + smol::block_on(async move { + let builder = match zbus::connection::Builder::session() + .and_then(|builder| builder.name(service_name.as_str())) + .and_then(|builder| { + builder.serve_at(object_path.as_str(), dbus_menu_server.clone()) + }) { + Ok(builder) => builder, + Err(error) => { + log::error!("Failed to configure DBus connection: {error}"); + return; + } + }; + + let connection = match builder.build().await { + Ok(connection) => connection, + Err(error) => { + log::error!("Failed to build DBus connection: {error}"); + return; + } + }; + + dbus_menu_server.mark_connected(); + log::info!("DBusMenu server started on {service_name}"); + + if let Some(unique_name_sender) = unique_name_sender { + if let Some(unique_name) = connection.unique_name() { + if let Err(error) = unique_name_sender.send(unique_name.to_string()) { + log::error!("Failed to send DBusMenu unique name: {error}"); + } + } + } + + while let Ok(command) = dbus_command_rx.recv().await { + match command { + DBusMenuCommand::LayoutUpdated { + revision, + parent, + object_paths, + } => { + for object_path in object_paths { + let emitter = + match zbus::object_server::SignalEmitter::new( + &connection, + object_path.as_str(), + ) { + Ok(emitter) => emitter, + Err(error) => { + log::error!( + "Failed to build DBusMenu signal emitter for {object_path}: {error}" + ); + continue; + } + }; + if let Err(error) = DBusMenuServer::layout_updated( + &emitter, + revision, + parent, + ) + .await + { + log::error!( + "Failed to emit DBusMenu LayoutUpdated signal for {object_path}: {error}" + ); + } + } + } + DBusMenuCommand::ItemsPropertiesUpdated { + updated_props, + removed_props, + object_paths, + } => { + for object_path in object_paths { + let emitter = + match zbus::object_server::SignalEmitter::new( + &connection, + object_path.as_str(), + ) { + Ok(emitter) => emitter, + Err(error) => { + log::error!( + "Failed to build DBusMenu signal emitter for {object_path}: {error}" + ); + continue; + } + }; + if let Err(error) = DBusMenuServer::items_properties_updated( + &emitter, + updated_props.clone(), + removed_props.clone(), + ) + .await + { + log::error!( + "Failed to emit DBusMenu ItemsPropertiesUpdated for {object_path}: {error}" + ); + } + } + } + DBusMenuCommand::Shutdown => { + break; + } + } + } + }); + }) + .log_err() + } + + pub fn install_global_menu_sources( + &self, + loop_handle: &LoopHandle, + mut action_handler: impl FnMut(Box) + 'static, + mut about_to_show_handler: impl FnMut(AboutToShowRequest) + 'static, + mut connected_handler: impl FnMut() + 'static, + ) { + let (action_tx, action_rx) = calloop::channel::channel::>(); + let (about_to_show_tx, about_to_show_rx) = + calloop::channel::channel::(); + let (connected_tx, connected_rx) = calloop::channel::channel::<()>(); + + self.set_action_callback(Box::new(move |action| { + if let Err(error) = action_tx.send(action) { + log::error!("Failed to send DBus menu action: {error}"); + } + })); + self.set_about_to_show_sender(about_to_show_tx); + self.set_connected_callback(Box::new(move || { + if let Err(error) = connected_tx.send(()) { + log::error!("Failed to send DBusMenu connected event: {error}"); + } + })); + + loop_handle + .insert_source(action_rx, move |event, _, _| { + if let calloop::channel::Event::Msg(action) = event { + action_handler(action); + } + }) + .log_err(); + + loop_handle + .insert_source(about_to_show_rx, move |event, _, _| { + if let calloop::channel::Event::Msg(request) = event { + about_to_show_handler(request); + } + }) + .log_err(); + + loop_handle + .insert_source(connected_rx, move |event, _, _| { + if let calloop::channel::Event::Msg(()) = event { + connected_handler(); + } + }) + .log_err(); + } + + pub fn set_about_to_show_sender(&self, sender: CalloopSender) { + match self.about_to_show_sender.lock() { Ok(mut slot) => *slot = Some(sender), Err(error) => { - log::error!("Failed to store DBusMenu validate sender: {error}"); + log::error!("Failed to store DBusMenu about-to-show sender: {error}"); } } } @@ -119,6 +376,12 @@ impl DBusMenuServer { pub fn mark_connected(&self) { self.connected.store(true, Ordering::SeqCst); + if let Ok(callback) = self.connected_callback.lock() { + if let Some(callback) = callback.as_ref() { + callback(); + } + } + let revision = match self.state.lock() { Ok(state) => state.revision, Err(error) => { @@ -133,12 +396,6 @@ impl DBusMenuServer { self.connected.load(Ordering::SeqCst) } - pub fn note_exported(&self, object_path: String) { - if let Ok(mut exported_paths) = self.exported_paths.lock() { - exported_paths.insert(object_path); - } - } - pub fn set_action_callback(&self, callback: Box) { if let Ok(mut slot) = self.action_callback.lock() { *slot = Some(callback); @@ -147,11 +404,11 @@ impl DBusMenuServer { } } - pub fn set_will_open_callback(&self, callback: Arc) { - if let Ok(mut slot) = self.will_open_callback.lock() { + pub fn set_connected_callback(&self, callback: Box) { + if let Ok(mut slot) = self.connected_callback.lock() { *slot = Some(callback); } else { - log::error!("Failed to store DBusMenu will-open callback due to lock poisoning"); + log::error!("Failed to store DBusMenu connected callback due to lock poisoning"); } } @@ -191,102 +448,6 @@ impl DBusMenuServer { self.request_layout_updated(revision); } - pub fn ensure_exported_blocking(&self, object_path: String, timeout: Duration) -> bool { - let should_export = match self.object_paths.lock() { - Ok(mut paths) => paths.insert(object_path.clone()), - Err(error) => { - log::error!("Failed to update DBusMenu object paths: {error}"); - return false; - } - }; - if !should_export { - return true; - } - - if match self.exported_paths.lock() { - Ok(exported_paths) => exported_paths.contains(&object_path), - Err(error) => { - log::error!("Failed to read exported DBusMenu paths: {error}"); - false - } - } { - return true; - } - - if !self.is_connected() { - return false; - } - - let sender = match self.command_sender.lock() { - Ok(sender) => sender.clone(), - Err(error) => { - log::error!("Failed to read DBusMenu command sender: {error}"); - return false; - } - }; - let Some(sender) = sender else { - return false; - }; - - let (responded_tx, responded_rx) = std::sync::mpsc::channel(); - if let Err(error) = sender.try_send(DBusMenuCommand::EnsureExported { - object_path: object_path.clone(), - responded: responded_tx, - }) { - log::error!("Failed to send DBusMenu export request: {error}"); - return false; - } - - let exported = match responded_rx.recv_timeout(timeout) { - Ok(ok) => ok, - Err(error) => { - log::error!("Timed out exporting DBusMenu object at {object_path}: {error}"); - false - } - }; - - if exported { - let revision = match self.state.lock() { - Ok(state) => state.revision, - Err(error) => { - log::error!("Failed to read DBusMenu revision after export: {error}"); - return true; - } - }; - self.request_layout_updated_for_paths(revision, vec![object_path]); - } - - exported - } - - pub fn unexport_object_path(&self, object_path: String) { - if object_path == DBUSMENU_OBJECT_PATH { - return; - } - - if let Ok(mut object_paths) = self.object_paths.lock() { - object_paths.remove(&object_path); - } - if let Ok(mut exported_paths) = self.exported_paths.lock() { - exported_paths.remove(&object_path); - } - - let sender = match self.command_sender.lock() { - Ok(sender) => sender.clone(), - Err(error) => { - log::error!("Failed to read DBusMenu command sender: {error}"); - return; - } - }; - let Some(sender) = sender else { - return; - }; - - if let Err(error) = sender.try_send(DBusMenuCommand::Unexport { object_path }) { - log::error!("Failed to queue DBusMenu unexport request: {error}"); - } - } - pub fn shutdown(&self) { let sender = match self.command_sender.lock() { Ok(sender) => sender.clone(), @@ -304,115 +465,76 @@ impl DBusMenuServer { } } - pub fn refresh_enabled_states(&self, validate: &mut dyn FnMut(&dyn Action) -> bool) { + pub fn refresh_enabled_states(&self, validate: &mut dyn FnMut(&dyn Action) -> bool) -> bool { + let updated_ids = self.refresh_enabled_states_inner(validate); + !updated_ids.is_empty() + } + + pub fn refresh_enabled_states_inner( + &self, + validate: &mut dyn FnMut(&dyn Action) -> bool, + ) -> Vec { + let items_to_check: Vec<(i32, Box, bool)> = { + let state = match self.state.lock() { + Ok(state) => state, + Err(error) => { + log::error!("Failed to access DBusMenu state for enable refresh: {error}"); + return Vec::new(); + } + }; + state + .items + .iter() + .filter_map(|(id, entry)| { + let previous_enabled = entry.enabled?; + let action = entry.action.as_ref()?.boxed_clone(); + Some((*id, action, previous_enabled)) + }) + .collect() + }; + let mut updated_props: Vec<(i32, HashMap)> = Vec::new(); + let mut updates: Vec<(i32, bool, OwnedValue)> = Vec::new(); + + for (id, action, previous_enabled) in items_to_check { + let enabled = validate(action.as_ref()); + if enabled == previous_enabled { + continue; + } + if let Some(value) = owned_bool(enabled) { + let mut props = HashMap::new(); + props.insert("enabled".to_string(), value.clone()); + updated_props.push((id, props)); + updates.push((id, enabled, value)); + } + } + + if updates.is_empty() { + return Vec::new(); + } { let mut state = match self.state.lock() { Ok(state) => state, Err(error) => { - log::error!("Failed to access DBusMenu state for enable refresh: {error}"); - return; + log::error!("Failed to update DBusMenu state after refresh: {error}"); + return Vec::new(); } }; - - for (id, entry) in state.items.iter_mut() { - let Some(previous_enabled) = entry.enabled else { - continue; - }; - let Some(action) = entry.action.as_ref() else { - continue; - }; - - let enabled = validate(action.as_ref()); - if enabled == previous_enabled { - continue; - } - - entry.enabled = Some(enabled); - if let Some(value) = owned_bool(enabled) { - entry - .properties - .insert("enabled".to_string(), value.clone()); - let mut props = HashMap::new(); - props.insert("enabled".to_string(), value); - updated_props.push((*id, props)); + for (id, enabled, value) in &updates { + if let Some(entry) = state.items.get_mut(id) { + entry.enabled = Some(*enabled); + entry.properties.insert("enabled".to_string(), value.clone()); } } } - if updated_props.is_empty() { - return; - } - self.request_items_properties_updated(updated_props, Vec::new()); - } - - fn validate_enabled(&self, action: &dyn Action) -> Option { - let sender = match self.validate_sender.lock() { - Ok(sender) => sender, - Err(error) => { - log::error!("Failed to read DBusMenu validate sender: {error}"); - return None; - } - }; - let Some(sender) = sender.as_ref() else { - return None; - }; - - let (responded_tx, responded_rx) = std::sync::mpsc::channel(); - let request = ValidateRequest { - action: action.boxed_clone(), - responded: responded_tx, - }; - - if let Err(error) = sender.send(request) { - log::error!("Failed to send DBusMenu validate request: {error}"); - return None; - } - - responded_rx.recv_timeout(Duration::from_millis(20)).ok() - } - - fn get_layout_node( - &self, - id: i32, - remaining_depth: i32, - ) -> Option<(i32, HashMap, Vec)> { - let state = match self.state.lock() { - Ok(state) => state, - Err(error) => { - log::error!("Failed to read DBusMenu state: {error}"); - return None; - } - }; - let entry = state.items.get(&id)?; - - let properties = entry.properties.clone(); - let children = if remaining_depth == 0 { - Vec::new() - } else { - let child_ids = entry.children.clone(); - drop(state); - let mut result = Vec::new(); - for child_id in child_ids { - if let Some(child_node) = self.get_layout_node(child_id, remaining_depth - 1) { - let variant = - Value::from(zbus::zvariant::Structure::from(child_node)).try_into(); - if let Ok(value) = variant { - result.push(value); - } - } - } - result - }; - - Some((id, properties, children)) + updates.iter().map(|(id, _, _)| *id).collect() } fn request_layout_updated(&self, revision: u32) { - let object_paths = self.object_paths(); - self.request_layout_updated_for_paths(revision, object_paths); + self.request_layout_updated_for_paths(revision, vec![DBUSMENU_OBJECT_PATH.to_string()]); } fn request_layout_updated_for_paths(&self, revision: u32, object_paths: Vec) { @@ -445,8 +567,11 @@ impl DBusMenuServer { updated_props: Vec<(i32, HashMap)>, removed_props: Vec<(i32, Vec)>, ) { - let object_paths = self.object_paths(); - self.request_items_properties_updated_for_paths(updated_props, removed_props, object_paths); + self.request_items_properties_updated_for_paths( + updated_props, + removed_props, + vec![DBUSMENU_OBJECT_PATH.to_string()] + ); } fn request_items_properties_updated_for_paths( @@ -479,47 +604,89 @@ impl DBusMenuServer { } } - fn object_paths(&self) -> Vec { - match self.object_paths.lock() { - Ok(paths) => paths.iter().cloned().collect(), + pub fn classify_ids(&self, ids: &[i32]) -> (Vec, Vec) { + let state = match self.state.lock() { + Ok(state) => state, Err(error) => { - log::error!("Failed to read DBusMenu object paths: {error}"); - vec![DBUSMENU_OBJECT_PATH.to_string()] + log::error!("Failed to read DBusMenu state for id lookup: {error}"); + return (Vec::new(), ids.to_vec()); + } + }; + + let mut valid = Vec::new(); + let mut errors = Vec::new(); + + for id in ids { + if state.items.contains_key(id) { + valid.push(*id); + } else { + errors.push(*id); } } + + (valid, errors) } } #[zbus::interface(name = "com.canonical.dbusmenu")] impl DBusMenuServer { - async fn about_to_show(&self, _id: i32) -> zbus::fdo::Result { - let callback = match self.will_open_callback.lock() { - Ok(callback) => callback.clone(), - Err(_) => { - return Err(zbus::fdo::Error::Failed( - "Failed to access will-open callback".to_string(), - )); - } + async fn about_to_show(&self, id: i32) -> zbus::fdo::Result { + let sender = match self.about_to_show_sender.lock() { + Ok(sender) => sender.clone(), + Err(_) => return Ok(false), }; - if let Some(callback) = callback { - callback(); + let Some(sender) = sender else { + return Ok(false); + }; + + let (responded_tx, responded_rx) = std::sync::mpsc::channel(); + if sender + .send(AboutToShowRequest { + ids: vec![id], + responded: responded_tx, + }) + .is_err() + { + return Ok(false); + } + + match responded_rx.recv_timeout(Duration::from_millis(50)) { + Ok(response) => Ok(!response.updated_ids.is_empty()), + Err(_) => Ok(false), } - Ok(false) } - async fn about_to_show_group(&self, _ids: Vec) -> zbus::fdo::Result<(Vec, Vec)> { - let callback = match self.will_open_callback.lock() { - Ok(callback) => callback.clone(), - Err(_) => { - return Err(zbus::fdo::Error::Failed( - "Failed to access will-open callback".to_string(), - )); - } + async fn about_to_show_group( + &self, + ids: Vec, + ) -> zbus::fdo::Result<(Vec, Vec)> { + let sender = match self.about_to_show_sender.lock() { + Ok(sender) => sender.clone(), + Err(_) => return Ok((Vec::new(), Vec::new())), }; - if let Some(callback) = callback { - callback(); + let Some(sender) = sender else { + return Ok((Vec::new(), Vec::new())); + }; + + let (responded_tx, responded_rx) = std::sync::mpsc::channel(); + if sender + .send(AboutToShowRequest { + ids, + responded: responded_tx, + }) + .is_err() + { + return Ok((Vec::new(), Vec::new())); } - Ok((Vec::new(), Vec::new())) + + let response = responded_rx + .recv_timeout(Duration::from_millis(50)) + .unwrap_or(AboutToShowResponse { + updated_ids: Vec::new(), + id_errors: Vec::new(), + }); + + Ok((response.updated_ids, response.id_errors)) } async fn event( @@ -537,10 +704,12 @@ impl DBusMenuServer { let state = self.state.lock().map_err(|_| { zbus::fdo::Error::Failed("Failed to access DBusMenu state".to_string()) })?; - state - .items - .get(&id) - .and_then(|entry| entry.action.as_ref().map(|a| a.boxed_clone())) + let entry = state.items.get(&id); + let enabled = entry.and_then(|e| e.enabled).unwrap_or(true); + if !enabled { + return Ok(()); + } + entry.and_then(|entry| entry.action.as_ref().map(|a| a.boxed_clone())) }; if let Some(action) = action { @@ -558,8 +727,9 @@ impl DBusMenuServer { async fn get_group_properties( &self, ids: Vec, - _property_names: Vec, + property_names: Vec, ) -> zbus::fdo::Result)>> { + let property_filter = build_property_filter(&property_names); let entries = { let state = self.state.lock().map_err(|_| { zbus::fdo::Error::Failed("Failed to access DBusMenu state".to_string()) @@ -567,65 +737,33 @@ impl DBusMenuServer { ids.into_iter() .filter_map(|id| { state.items.get(&id).map(|entry| { - ( - id, - entry.properties.clone(), - entry.action.as_ref().map(|action| action.boxed_clone()), - ) + (id, filter_properties(entry.properties.clone(), &property_filter)) }) }) .collect::>() }; - let mut updated: Vec<(i32, bool, OwnedValue)> = Vec::new(); - let mut result = Vec::with_capacity(entries.len()); - - for (id, mut properties, action) in entries { - if let Some(action) = action { - if let Some(enabled) = self.validate_enabled(action.as_ref()) { - if let Some(value) = owned_bool(enabled) { - properties.insert("enabled".to_string(), value.clone()); - updated.push((id, enabled, value)); - } - } - } - result.push((id, properties)); - } - - if !updated.is_empty() { - if let Ok(mut state) = self.state.lock() { - for (id, enabled, value) in updated { - if let Some(entry) = state.items.get_mut(&id) { - entry.enabled = Some(enabled); - entry.properties.insert("enabled".to_string(), value); - } - } - } - } - - Ok(result) + Ok(entries) } async fn get_layout( &self, parent_id: i32, recursion_depth: i32, - _property_names: Vec, + property_names: Vec, ) -> zbus::fdo::Result<(u32, (i32, HashMap, Vec))> { - let revision = self - .state - .lock() - .map_err(|_| { - zbus::fdo::Error::Failed("Failed to access DBusMenu revision".to_string()) - })? - .revision; + let property_filter = build_property_filter(&property_names); + let state = self.state.lock().map_err(|_| { + zbus::fdo::Error::Failed("Failed to access DBusMenu state".to_string()) + })?; + let revision = state.revision; let depth = if recursion_depth < 0 { i32::MAX } else { recursion_depth }; - - let layout = self.get_layout_node(parent_id, depth).ok_or_else(|| { + let layout = collect_layout_node_filtered(&state, parent_id, depth, &property_filter) + .ok_or_else(|| { zbus::fdo::Error::InvalidArgs(format!("Unknown menu item id: {parent_id}")) })?; @@ -633,34 +771,6 @@ impl DBusMenuServer { } async fn get_property(&self, id: i32, name: &str) -> zbus::fdo::Result { - if name == "enabled" { - let action = { - let state = self.state.lock().map_err(|_| { - zbus::fdo::Error::Failed("Failed to access DBusMenu state".to_string()) - })?; - state - .items - .get(&id) - .and_then(|entry| entry.action.as_ref().map(|action| action.boxed_clone())) - }; - - if let Some(action) = action { - if let Some(enabled) = self.validate_enabled(action.as_ref()) - && let Some(value) = owned_bool(enabled) - { - if let Ok(mut state) = self.state.lock() { - if let Some(entry) = state.items.get_mut(&id) { - entry.enabled = Some(enabled); - entry - .properties - .insert("enabled".to_string(), value.clone()); - } - } - return Ok(value); - } - } - } - let state = self .state .lock() @@ -726,6 +836,56 @@ fn root_properties() -> HashMap { props } +fn build_property_filter(property_names: &[String]) -> Option> { + if property_names.is_empty() { + None + } else { + Some(property_names.iter().cloned().collect()) + } +} + +fn filter_properties( + properties: HashMap, + filter: &Option>, +) -> HashMap { + match filter { + Some(filter) => properties + .into_iter() + .filter(|(name, _)| filter.contains(name)) + .collect(), + None => properties, + } +} + +fn collect_layout_node_filtered( + state: &MenuState, + id: i32, + remaining_depth: i32, + filter: &Option>, +) -> Option<(i32, HashMap, Vec)> { + let entry = state.items.get(&id)?; + let properties = filter_properties(entry.properties.clone(), filter); + + let children = if remaining_depth == 0 { + Vec::new() + } else { + let mut result = Vec::new(); + for &child_id in &entry.children { + if let Some(child_node) = + collect_layout_node_filtered(state, child_id, remaining_depth - 1, filter) + { + let variant = Value::from(zbus::zvariant::Structure::from(child_node)).try_into(); + if let Ok(value) = variant { + result.push(value); + } + } + } + result + }; + + Some((id, properties, children)) +} + fn build_menu_tree( items: &mut HashMap, next_id: &mut i32, @@ -780,6 +940,7 @@ fn build_menu_tree( action, checked, os_action, + checkable, .. } => { let mut props = HashMap::new(); @@ -818,14 +979,14 @@ fn build_menu_tree( } } - if *checked { + if *checkable { let toggle_type = Value::Str("checkmark".into()); if let Ok(value) = toggle_type.try_into() { props.insert("toggle-type".to_string(), value); } else { log::error!("Failed to encode DBusMenu toggle-type"); } - let toggle_state = Value::I32(1); + let toggle_state = if *checked { Value::I32(1) } else { Value::I32(0) }; if let Ok(value) = toggle_state.try_into() { props.insert("toggle-state".to_string(), value); } else { @@ -863,9 +1024,6 @@ fn build_menu_tree( ); } -pub fn object_path_for_window(window_id: u32) -> String { - format!("{DBUSMENU_OBJECT_PATH}/window_{window_id}") -} pub fn global_menu_env_override() -> Option { match std::env::var("ZED_GLOBAL_MENU").ok().as_deref() { @@ -930,14 +1088,21 @@ fn dbus_shortcut_for_action(action: &dyn Action, keymap: &Keymap) -> Option Option { +fn dbus_shortcut_for_keystrokes(keystrokes: &[KeybindingKeystroke]) -> Option { + if keystrokes.is_empty() { + return None; + } + let mut shortcut: Vec> = Vec::with_capacity(keystrokes.len()); + for keystroke in keystrokes { + shortcut.push(dbus_keys_for_keystroke(keystroke)?); + } + Value::from(shortcut).try_into().ok() +} + +fn dbus_keys_for_keystroke(keystroke: &KeybindingKeystroke) -> Option> { let mut keys: Vec = Vec::new(); let modifiers = keystroke.modifiers(); @@ -953,14 +1118,57 @@ fn dbus_shortcut_for_keystroke(keystroke: &KeybindingKeystroke) -> Option Option { + let normalized = match key { + "enter" | "return" => "Return".to_string(), + "escape" => "Escape".to_string(), + "tab" => "Tab".to_string(), + "backspace" => "BackSpace".to_string(), + "delete" => "Delete".to_string(), + "insert" => "Insert".to_string(), + "home" => "Home".to_string(), + "end" => "End".to_string(), + "pageup" => "PageUp".to_string(), + "pagedown" => "PageDown".to_string(), + "left" => "Left".to_string(), + "right" => "Right".to_string(), + "up" => "Up".to_string(), + "down" => "Down".to_string(), + "space" => "space".to_string(), + _ => { + if let Some(suffix) = key.strip_prefix('f') { + if !suffix.is_empty() && suffix.chars().all(|c| c.is_ascii_digit()) { + format!("F{suffix}") + } else { + key.to_string() + } + } else if key.len() == 1 { + let mut chars = key.chars(); + let ch = chars.next()?; + if chars.next().is_some() { + key.to_string() + } else if ch.is_ascii_alphabetic() { + ch.to_ascii_uppercase().to_string() + } else { + key.to_string() + } + } else { + key.to_string() + } + } + }; + + if normalized.is_empty() { + None + } else { + Some(normalized) } - - keys.push(keystroke.key().to_string()); - - let shortcut: Vec> = vec![keys]; - Value::from(shortcut).try_into().ok() } fn is_rtl_locale() -> bool { diff --git a/crates/gpui_linux/src/linux/platform.rs b/crates/gpui_linux/src/linux/platform.rs index 1c0545aa825..16762ccedc1 100644 --- a/crates/gpui_linux/src/linux/platform.rs +++ b/crates/gpui_linux/src/linux/platform.rs @@ -523,12 +523,12 @@ impl Platform for LinuxPlatform

{ Ok(app_path) } - fn set_menus(&self, menus: Vec

, keymap: &Keymap) { + fn set_menus(&self, menus: Vec, _keymap: &Keymap) { self.inner.with_common(|common| { common.menus = menus.into_iter().map(|menu| menu.owned()).collect(); #[cfg(all(any(feature = "wayland", feature = "x11"), feature = "global-menu"))] if let Some(server) = &common.dbus_menu_server { - server.set_menus(common.menus.clone(), keymap); + server.set_menus(common.menus.clone(), _keymap); } }) } diff --git a/crates/gpui_linux/src/linux/wayland/client.rs b/crates/gpui_linux/src/linux/wayland/client.rs index 446e1113de4..20c002c7d32 100644 --- a/crates/gpui_linux/src/linux/wayland/client.rs +++ b/crates/gpui_linux/src/linux/wayland/client.rs @@ -4,7 +4,6 @@ use std::{ os::fd::{AsRawFd, BorrowedFd}, path::PathBuf, rc::{Rc, Weak}, - sync::Arc, time::{Duration, Instant}, }; @@ -95,7 +94,7 @@ use crate::linux::{ xdg_desktop_portal::{Event as XDPEvent, XDPEventSource}, }; use gpui::{ - Action, AnyWindowHandle, Bounds, Capslock, CursorStyle, DevicePixels, DisplayId, FileDropEvent, + AnyWindowHandle, Bounds, Capslock, CursorStyle, DevicePixels, DisplayId, FileDropEvent, ForegroundExecutor, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels, PlatformDisplay, PlatformInput, PlatformKeyboardLayout, PlatformWindow, Point, @@ -268,6 +267,8 @@ pub(crate) struct WaylandClientState { dbus_service_name: Option, #[cfg(feature = "global-menu")] dbus_menu_thread: Option>, + #[cfg(feature = "global-menu")] + appmenu_objects: HashMap, pub common: LinuxCommon, } @@ -407,10 +408,10 @@ impl WaylandClientStatePtr { let mut state = client.borrow_mut(); #[cfg(feature = "global-menu")] - if let Some(dbus_menu_server) = state.common.dbus_menu_server.as_ref() { - let object_path = - crate::linux::dbusmenu::object_path_for_window(surface_id.protocol_id()); - dbus_menu_server.unexport_object_path(object_path); + { + if let Some(appmenu) = state.appmenu_objects.remove(surface_id) { + appmenu.release(); + } } let closed_window = state.windows.remove(surface_id).unwrap(); @@ -453,6 +454,12 @@ impl Drop for WaylandClient { } let mut state = self.0.borrow_mut(); + + #[cfg(feature = "global-menu")] + for (_, appmenu) in state.appmenu_objects.drain() { + appmenu.release(); + } + state.windows.clear(); if let Some(wl_pointer) = &state.wl_pointer { @@ -470,6 +477,13 @@ impl Drop for WaylandClient { } } +#[cfg(feature = "global-menu")] +impl crate::linux::dbusmenu::GlobalMenuState for WaylandClientState { + fn linux_common(&mut self) -> &mut crate::linux::LinuxCommon { + &mut self.common + } +} + const WL_DATA_DEVICE_MANAGER_VERSION: u32 = 3; fn wl_seat_version(version: u32) -> u32 { @@ -696,6 +710,8 @@ impl WaylandClient { dbus_service_name: None, #[cfg(feature = "global-menu")] dbus_menu_thread: None, + #[cfg(feature = "global-menu")] + appmenu_objects: HashMap::default(), })); WaylandSource::new(conn, event_queue) @@ -716,314 +732,41 @@ impl WaylandClient { let dbus_menu_server = crate::linux::dbusmenu::DBusMenuServer::new(); let service_name = format!("com.zed.dbusmenu.pid{}", std::process::id()); - // Channel for sending menu actions from the DBus thread to the main thread. - let (action_tx, action_rx) = calloop::channel::channel::>(); - let (will_open_tx, will_open_rx) = calloop::channel::channel::<()>(); - let (validate_tx, validate_rx) = - calloop::channel::channel::(); - - dbus_menu_server.set_action_callback({ - let action_tx = action_tx.clone(); - Box::new(move |action| { - if let Err(error) = action_tx.send(action) { - log::error!( - "Failed to send DBus menu action to the Wayland event loop: {error}" - ); - } - }) - }); - dbus_menu_server.set_will_open_callback({ - let will_open_tx = will_open_tx.clone(); - Arc::new(move || { - if let Err(error) = will_open_tx.send(()) { - log::error!( - "Failed to send DBus menu open notification to the Wayland event loop: {error}" - ); - } - }) - }); - dbus_menu_server.set_validate_sender(validate_tx); - - state - .borrow() - .loop_handle - .insert_source(action_rx, { - let client = Rc::downgrade(&state); - move |event, _, _| { - if let calloop::channel::Event::Msg(action) = event { - if let Some(client) = client.upgrade() { - let mut state = client.borrow_mut(); - if let Some(callback) = - state.common.callbacks.app_menu_action.as_mut() - { - callback(action.as_ref()); - } - } - } - } - }) - .log_err(); - - state - .borrow() - .loop_handle - .insert_source(will_open_rx, { - let client = Rc::downgrade(&state); - move |event, _, _| { - if let calloop::channel::Event::Msg(()) = event { - if let Some(client) = client.upgrade() { - let ( - dbus_menu_server, - mut validate_app_menu_command, - mut will_open_app_menu, - ) = { - let mut state = client.borrow_mut(); - ( - state.common.dbus_menu_server.clone(), - state.common.callbacks.validate_app_menu_command.take(), - state.common.callbacks.will_open_app_menu.take(), - ) - }; - - if let Some(callback) = will_open_app_menu.as_mut() { - callback(); - } - if let (Some(dbus_menu_server), Some(validate_callback)) = ( - dbus_menu_server.as_ref(), - validate_app_menu_command.as_mut(), - ) { - dbus_menu_server.refresh_enabled_states(validate_callback); - } - - let mut state = client.borrow_mut(); - if state.common.callbacks.validate_app_menu_command.is_none() { - state.common.callbacks.validate_app_menu_command = - validate_app_menu_command; - } - if state.common.callbacks.will_open_app_menu.is_none() { - state.common.callbacks.will_open_app_menu = - will_open_app_menu; - } - } - } - } - }) - .log_err(); - - state - .borrow() - .loop_handle - .insert_source(validate_rx, { - let client = Rc::downgrade(&state); - move |event, _, _| { - if let calloop::channel::Event::Msg(request) = event { - if let Some(client) = client.upgrade() { - let enabled = { - let mut state = client.borrow_mut(); - match state - .common - .callbacks - .validate_app_menu_command - .as_mut() - { - Some(validate) => validate(request.action.as_ref()), - None => true, - } - }; - - if let Err(error) = request.responded.send(enabled) { - log::error!( - "Failed to send DBusMenu validate response: {error}" - ); - } - } - } - } - }) - .log_err(); - - let refresh_interval = Duration::from_millis(250); - state - .borrow() - .loop_handle - .insert_source(Timer::from_duration(refresh_interval), { - let client = Rc::downgrade(&state); - move |event_timestamp, _, _| { - let Some(client) = client.upgrade() else { - return TimeoutAction::Drop; + crate::linux::dbusmenu::setup_global_menu_sources( + &dbus_menu_server, + &state.borrow().loop_handle, + Rc::downgrade(&state), + move |state| { + let (service_name, dbus_menu_server, appmenus) = { + let Some(service_name) = state.dbus_service_name.as_ref().cloned() else { + return; }; - let (dbus_menu_server, mut validate_app_menu_command) = { - let mut state = client.borrow_mut(); - ( - state.common.dbus_menu_server.clone(), - state.common.callbacks.validate_app_menu_command.take(), - ) + let Some(dbus_menu_server) = state.common.dbus_menu_server.clone() else { + return; }; + let appmenus = state + .appmenu_objects + .iter() + .map(|(surface_id, appmenu)| (surface_id.clone(), appmenu.clone())) + .collect::>(); + (service_name, dbus_menu_server, appmenus) + }; - if let (Some(dbus_menu_server), Some(validate_callback)) = ( - dbus_menu_server.as_ref(), - validate_app_menu_command.as_mut(), - ) { - dbus_menu_server.refresh_enabled_states(validate_callback); - } - - let mut state = client.borrow_mut(); - if state.common.callbacks.validate_app_menu_command.is_none() { - state.common.callbacks.validate_app_menu_command = - validate_app_menu_command; - } - TimeoutAction::ToInstant(event_timestamp + refresh_interval) + for (_, appmenu) in appmenus { + appmenu.set_address(service_name.clone(), crate::linux::dbusmenu::DBUSMENU_OBJECT_PATH.to_string()); } - }) - .log_err(); + }, + ); state.borrow_mut().common.dbus_menu_server = Some(dbus_menu_server.clone()); state.borrow_mut().dbus_service_name = Some(service_name.clone()); let object_path = crate::linux::dbusmenu::DBUSMENU_OBJECT_PATH.to_string(); - let (dbus_command_tx, dbus_command_rx) = async_channel::unbounded(); - dbus_menu_server.set_command_sender(dbus_command_tx); - let dbus_menu_thread = std::thread::Builder::new() - .name("dbus-menu".into()) - .spawn(move || { - smol::block_on(async move { - let dbus_menu_server_for_service = dbus_menu_server.clone(); - let builder = match zbus::connection::Builder::session() - .and_then(|builder| builder.name(service_name.as_str())) - .and_then(|builder| { - builder.serve_at( - object_path.as_str(), - dbus_menu_server_for_service, - ) - }) { - Ok(builder) => builder, - Err(error) => { - log::error!("Failed to configure DBus connection: {error}"); - return; - } - }; - - let connection = match builder.build().await { - Ok(connection) => connection, - Err(error) => { - log::error!("Failed to build DBus connection: {error}"); - return; - } - }; - - dbus_menu_server.mark_connected(); - log::info!("DBusMenu server started on {service_name}"); - - while let Ok(command) = dbus_command_rx.recv().await { - match command { - crate::linux::dbusmenu::DBusMenuCommand::EnsureExported { - object_path, - responded, - } => { - let result = connection - .object_server() - .at(object_path.as_str(), dbus_menu_server.clone()) - .await; - let ok = result.is_ok(); - if ok { - dbus_menu_server.note_exported(object_path); - } - if let Err(error) = responded.send(ok) { - log::error!( - "Failed to send DBusMenu export response: {error}" - ); - } - } - crate::linux::dbusmenu::DBusMenuCommand::LayoutUpdated { - revision, - parent, - object_paths, - } => { - for object_path in object_paths { - let emitter = match zbus::object_server::SignalEmitter::new( - &connection, - object_path.as_str(), - ) { - Ok(emitter) => emitter, - Err(error) => { - log::error!( - "Failed to build DBusMenu signal emitter for {object_path}: {error}" - ); - continue; - } - }; - if let Err(error) = - crate::linux::dbusmenu::DBusMenuServer::layout_updated( - &emitter, - revision, - parent, - ) - .await - { - log::error!( - "Failed to emit DBusMenu LayoutUpdated signal for {object_path}: {error}" - ); - } - } - } - crate::linux::dbusmenu::DBusMenuCommand::ItemsPropertiesUpdated { - updated_props, - removed_props, - object_paths, - } => { - for object_path in object_paths { - let emitter = match zbus::object_server::SignalEmitter::new( - &connection, - object_path.as_str(), - ) { - Ok(emitter) => emitter, - Err(error) => { - log::error!( - "Failed to build DBusMenu signal emitter for {object_path}: {error}" - ); - continue; - } - }; - if let Err(error) = - crate::linux::dbusmenu::DBusMenuServer::items_properties_updated( - &emitter, - updated_props.clone(), - removed_props.clone(), - ) - .await - { - log::error!( - "Failed to emit DBusMenu ItemsPropertiesUpdated for {object_path}: {error}" - ); - } - } - } - crate::linux::dbusmenu::DBusMenuCommand::Unexport { - object_path, - } => { - let result = connection - .object_server() - .remove::( - object_path.as_str(), - ) - .await; - match result { - Ok(_) => {} - Err(error) => { - log::error!( - "Failed to unexport DBusMenu object at {object_path}: {error}" - ); - } - } - } - crate::linux::dbusmenu::DBusMenuCommand::Shutdown => { - break; - } - } - } - }); - }) - .log_err(); + let dbus_menu_thread = dbus_menu_server.spawn_dbus_menu_thread( + service_name.clone(), + object_path, + None, + ); if let Some(thread) = dbus_menu_thread { state.borrow_mut().dbus_menu_thread = Some(thread); @@ -1137,21 +880,15 @@ impl LinuxClient for WaylandClient { state.dbus_service_name.as_ref(), ) { let dbus_menu_server = state.common.dbus_menu_server.clone(); - let mut object_path = - crate::linux::dbusmenu::object_path_for_window(surface_id.protocol_id()); - if let Some(dbus_menu_server) = dbus_menu_server.as_ref() { - if !dbus_menu_server - .ensure_exported_blocking(object_path.clone(), Duration::from_millis(250)) - { - object_path = crate::linux::dbusmenu::DBUSMENU_OBJECT_PATH.to_string(); - } - } else { - object_path = crate::linux::dbusmenu::DBUSMENU_OBJECT_PATH.to_string(); - } - let surface = window.0.surface(); let appmenu = appmenu_manager.create(&surface, &state.globals.qh, ()); - appmenu.set_address(service_name.clone(), object_path); + if let Some(_dbus_menu_server) = + dbus_menu_server.as_ref().filter(|server| server.is_connected()) + { + let object_path = crate::linux::dbusmenu::DBUSMENU_OBJECT_PATH.to_string(); + appmenu.set_address(service_name.clone(), object_path); + } + state.appmenu_objects.insert(surface_id.clone(), appmenu); } Ok(Box::new(window)) diff --git a/crates/gpui_linux/src/linux/x11/client.rs b/crates/gpui_linux/src/linux/x11/client.rs index 061295978ef..de813a81663 100644 --- a/crates/gpui_linux/src/linux/x11/client.rs +++ b/crates/gpui_linux/src/linux/x11/client.rs @@ -1,7 +1,5 @@ use anyhow::{Context as _, anyhow}; use ashpd::WindowIdentifier; -#[cfg(feature = "global-menu")] -use calloop::timer::{TimeoutAction, Timer}; use calloop::{ EventLoop, LoopHandle, RegistrationToken, generic::{FdWrapper, Generic}, @@ -12,8 +10,6 @@ use gpui::{Capslock, TaskTiming, profiler}; use http_client::Url; use log::Level; use smallvec::SmallVec; -#[cfg(feature = "global-menu")] -use std::sync::Arc; use std::{ cell::RefCell, collections::{BTreeMap, HashSet}, @@ -62,8 +58,7 @@ use crate::linux::{ }; use crate::linux::{LinuxCommon, LinuxKeyboardLayout, X11Window, modifiers_from_xinput_info}; -#[cfg(feature = "global-menu")] -use gpui::Action; + use gpui::{ AnyWindowHandle, Bounds, ClipboardItem, CursorStyle, DisplayId, FileDropEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, Pixels, PlatformDisplay, PlatformInput, @@ -246,12 +241,6 @@ impl X11ClientStatePtr { }; let mut state = client.0.borrow_mut(); - #[cfg(feature = "global-menu")] - if let Some(dbus_menu_server) = state.common.dbus_menu_server.as_ref() { - let object_path = crate::linux::dbusmenu::object_path_for_window(x_window); - dbus_menu_server.unexport_object_path(object_path); - } - if let Some(window_ref) = state.windows.remove(&x_window) && let Some(RefreshState::PeriodicRefresh { event_loop_token, .. @@ -313,6 +302,13 @@ impl X11ClientStatePtr { } } +#[cfg(feature = "global-menu")] +impl crate::linux::dbusmenu::GlobalMenuState for X11ClientState { + fn linux_common(&mut self) -> &mut crate::linux::LinuxCommon { + &mut self.common + } +} + #[derive(Clone)] pub(crate) struct X11Client(pub(crate) Rc>); @@ -594,131 +590,13 @@ impl X11Client { let dbus_menu_server = crate::linux::dbusmenu::DBusMenuServer::new(); let service_name = format!("com.zed.dbusmenu.pid{}", std::process::id()); - let (action_tx, action_rx) = calloop::channel::channel::>(); - let (will_open_tx, will_open_rx) = calloop::channel::channel::<()>(); - let (validate_tx, validate_rx) = - calloop::channel::channel::(); let (unique_name_tx, unique_name_rx) = calloop::channel::channel::(); - - dbus_menu_server.set_action_callback({ - let action_tx = action_tx.clone(); - Box::new(move |action| { - if let Err(error) = action_tx.send(action) { - log::error!( - "Failed to send DBus menu action to the X11 event loop: {error}" - ); - } - }) - }); - dbus_menu_server.set_will_open_callback({ - let will_open_tx = will_open_tx.clone(); - Arc::new(move || { - if let Err(error) = will_open_tx.send(()) { - log::error!( - "Failed to send DBus menu open notification to the X11 event loop: {error}" - ); - } - }) - }); - dbus_menu_server.set_validate_sender(validate_tx); - - state - .borrow() - .loop_handle - .insert_source(action_rx, { - let client = Rc::downgrade(&state); - move |event, _, _| { - if let calloop::channel::Event::Msg(action) = event { - if let Some(client) = client.upgrade() { - let mut state = client.borrow_mut(); - if let Some(callback) = - state.common.callbacks.app_menu_action.as_mut() - { - callback(action.as_ref()); - } - } - } - } - }) - .log_err(); - - state - .borrow() - .loop_handle - .insert_source(will_open_rx, { - let client = Rc::downgrade(&state); - move |event, _, _| { - if let calloop::channel::Event::Msg(()) = event { - if let Some(client) = client.upgrade() { - let ( - dbus_menu_server, - mut validate_app_menu_command, - mut will_open_app_menu, - ) = { - let mut state = client.borrow_mut(); - ( - state.common.dbus_menu_server.clone(), - state.common.callbacks.validate_app_menu_command.take(), - state.common.callbacks.will_open_app_menu.take(), - ) - }; - - if let Some(callback) = will_open_app_menu.as_mut() { - callback(); - } - if let (Some(dbus_menu_server), Some(validate_callback)) = ( - dbus_menu_server.as_ref(), - validate_app_menu_command.as_mut(), - ) { - dbus_menu_server.refresh_enabled_states(validate_callback); - } - - let mut state = client.borrow_mut(); - if state.common.callbacks.validate_app_menu_command.is_none() { - state.common.callbacks.validate_app_menu_command = - validate_app_menu_command; - } - if state.common.callbacks.will_open_app_menu.is_none() { - state.common.callbacks.will_open_app_menu = - will_open_app_menu; - } - } - } - } - }) - .log_err(); - - state - .borrow() - .loop_handle - .insert_source(validate_rx, { - let client = Rc::downgrade(&state); - move |event, _, _| { - if let calloop::channel::Event::Msg(request) = event { - if let Some(client) = client.upgrade() { - let enabled = { - let mut state = client.borrow_mut(); - match state - .common - .callbacks - .validate_app_menu_command - .as_mut() - { - Some(validate) => validate(request.action.as_ref()), - None => true, - } - }; - - if let Err(error) = request.responded.send(enabled) { - log::error!( - "Failed to send DBusMenu validate response: {error}" - ); - } - } - } - } - }) - .log_err(); + crate::linux::dbusmenu::setup_global_menu_sources( + &dbus_menu_server, + &state.borrow().loop_handle, + Rc::downgrade(&state), + |_| {}, + ); state .borrow() @@ -730,19 +608,17 @@ impl X11Client { if let Some(client) = client.upgrade() { let mut state = client.borrow_mut(); state.dbus_unique_name = Some(unique_name.clone()); + let window_ids: Vec = state.windows.keys().copied().collect(); for x_window in window_ids { - let object_path = - crate::linux::dbusmenu::object_path_for_window( - x_window, - ); + let final_path = crate::linux::dbusmenu::DBUSMENU_OBJECT_PATH.to_string(); set_x11_appmenu_properties( &state.xcb_connection, &state.atoms, x_window, &unique_name, - &object_path, + &final_path, ); } xcb_flush(&state.xcb_connection); @@ -752,195 +628,15 @@ impl X11Client { }) .log_err(); - let refresh_interval = Duration::from_millis(250); - state - .borrow() - .loop_handle - .insert_source(Timer::from_duration(refresh_interval), { - let client = Rc::downgrade(&state); - move |event_timestamp, _, _| { - let Some(client) = client.upgrade() else { - return TimeoutAction::Drop; - }; - let (dbus_menu_server, mut validate_app_menu_command) = { - let mut state = client.borrow_mut(); - ( - state.common.dbus_menu_server.clone(), - state.common.callbacks.validate_app_menu_command.take(), - ) - }; - - if let (Some(dbus_menu_server), Some(validate_callback)) = ( - dbus_menu_server.as_ref(), - validate_app_menu_command.as_mut(), - ) { - dbus_menu_server.refresh_enabled_states(validate_callback); - } - - let mut state = client.borrow_mut(); - if state.common.callbacks.validate_app_menu_command.is_none() { - state.common.callbacks.validate_app_menu_command = - validate_app_menu_command; - } - TimeoutAction::ToInstant(event_timestamp + refresh_interval) - } - }) - .log_err(); - state.borrow_mut().common.dbus_menu_server = Some(dbus_menu_server.clone()); state.borrow_mut().dbus_service_name = Some(service_name.clone()); let object_path = crate::linux::dbusmenu::DBUSMENU_OBJECT_PATH.to_string(); - let (dbus_command_tx, dbus_command_rx) = async_channel::unbounded(); - dbus_menu_server.set_command_sender(dbus_command_tx); - let dbus_menu_thread = std::thread::Builder::new() - .name("dbus-menu".into()) - .spawn(move || { - smol::block_on(async move { - let dbus_menu_server_for_service = dbus_menu_server.clone(); - let builder = match zbus::connection::Builder::session() - .and_then(|builder| builder.name(service_name.as_str())) - .and_then(|builder| { - builder.serve_at( - object_path.as_str(), - dbus_menu_server_for_service, - ) - }) { - Ok(builder) => builder, - Err(error) => { - log::error!("Failed to configure DBus connection: {error}"); - return; - } - }; - - let connection = match builder.build().await { - Ok(connection) => connection, - Err(error) => { - log::error!("Failed to build DBus connection: {error}"); - return; - } - }; - - dbus_menu_server.mark_connected(); - log::info!("DBusMenu server started on {service_name}"); - if let Some(unique_name) = connection.unique_name() { - if let Err(error) = unique_name_tx.send(unique_name.to_string()) { - log::error!( - "Failed to send DBusMenu unique name: {error}" - ); - } - } - - while let Ok(command) = dbus_command_rx.recv().await { - match command { - crate::linux::dbusmenu::DBusMenuCommand::EnsureExported { - object_path, - responded, - } => { - let result = connection - .object_server() - .at(object_path.as_str(), dbus_menu_server.clone()) - .await; - let ok = result.is_ok(); - if ok { - dbus_menu_server.note_exported(object_path); - } - if let Err(error) = responded.send(ok) { - log::error!( - "Failed to send DBusMenu export response: {error}" - ); - } - } - crate::linux::dbusmenu::DBusMenuCommand::LayoutUpdated { - revision, - parent, - object_paths, - } => { - for object_path in object_paths { - let emitter = - match zbus::object_server::SignalEmitter::new( - &connection, - object_path.as_str(), - ) { - Ok(emitter) => emitter, - Err(error) => { - log::error!( - "Failed to build DBusMenu signal emitter for {object_path}: {error}" - ); - continue; - } - }; - if let Err(error) = crate::linux::dbusmenu::DBusMenuServer::layout_updated( - &emitter, - revision, - parent, - ) - .await - { - log::error!( - "Failed to emit DBusMenu LayoutUpdated signal for {object_path}: {error}" - ); - } - } - } - crate::linux::dbusmenu::DBusMenuCommand::ItemsPropertiesUpdated { - updated_props, - removed_props, - object_paths, - } => { - for object_path in object_paths { - let emitter = - match zbus::object_server::SignalEmitter::new( - &connection, - object_path.as_str(), - ) { - Ok(emitter) => emitter, - Err(error) => { - log::error!( - "Failed to build DBusMenu signal emitter for {object_path}: {error}" - ); - continue; - } - }; - if let Err(error) = crate::linux::dbusmenu::DBusMenuServer::items_properties_updated( - &emitter, - updated_props.clone(), - removed_props.clone(), - ) - .await - { - log::error!( - "Failed to emit DBusMenu ItemsPropertiesUpdated for {object_path}: {error}" - ); - } - } - } - crate::linux::dbusmenu::DBusMenuCommand::Unexport { - object_path, - } => { - let result = connection - .object_server() - .remove::( - object_path.as_str(), - ) - .await; - match result { - Ok(_) => {} - Err(error) => { - log::error!( - "Failed to unexport DBusMenu object at {object_path}: {error}" - ); - } - } - } - crate::linux::dbusmenu::DBusMenuCommand::Shutdown => { - break; - } - } - } - }); - }) - .log_err(); + let dbus_menu_thread = dbus_menu_server.spawn_dbus_menu_thread( + service_name.clone(), + object_path, + Some(unique_name_tx.clone()), + ); if let Some(thread) = dbus_menu_thread { state.borrow_mut().dbus_menu_thread = Some(thread); @@ -1972,17 +1668,7 @@ impl LinuxClient for X11Client { #[cfg(feature = "global-menu")] if let Some(service_name) = x11_appmenu_service_name(&state) { - let dbus_menu_server = state.common.dbus_menu_server.clone(); - let mut object_path = crate::linux::dbusmenu::object_path_for_window(x_window); - if let Some(dbus_menu_server) = dbus_menu_server.as_ref() { - if !dbus_menu_server - .ensure_exported_blocking(object_path.clone(), Duration::from_millis(250)) - { - object_path = crate::linux::dbusmenu::DBUSMENU_OBJECT_PATH.to_string(); - } - } else { - object_path = crate::linux::dbusmenu::DBUSMENU_OBJECT_PATH.to_string(); - } + let object_path = crate::linux::dbusmenu::DBUSMENU_OBJECT_PATH.to_string(); set_x11_appmenu_properties( &state.xcb_connection, diff --git a/crates/gpui_macos/src/platform.rs b/crates/gpui_macos/src/platform.rs index d9c22cbea03..d29fdd1cced 100644 --- a/crates/gpui_macos/src/platform.rs +++ b/crates/gpui_macos/src/platform.rs @@ -297,6 +297,7 @@ impl MacPlatform { action, os_action, checked, + .. } => { // Note that this is intentionally using earlier bindings, whereas typically // later ones take display precedence. See the discussion on diff --git a/crates/livekit_client/examples/test_app.rs b/crates/livekit_client/examples/test_app.rs index 06b9a1402a5..ce1969d50ab 100644 --- a/crates/livekit_client/examples/test_app.rs +++ b/crates/livekit_client/examples/test_app.rs @@ -41,6 +41,7 @@ fn main() { name: "Quit".into(), action: Box::new(Quit), os_action: None, + checkable: false, checked: false, }], }]); From 4b252a34935020ffbc91a2d37cb3b21bc2e8f277 Mon Sep 17 00:00:00 2001 From: Nihal <121309701+nihalxkumar@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:40:21 +0530 Subject: [PATCH 06/16] gpui_linux: Fix potential DBusMenu leak and cleanup global menu code This commit addresses several minor issues in the Linux global menu implementation: - In X11, added a warning log and fallback to immutably borrow and shutdown the DBusMenu server if `try_borrow_mut` fails during X11Client drop, preventing silent resource leaks. - In Wayland, avoided unnecessary cloning of `surface_id` in the `on_connected` callback by iterating over `.values()`. - Renamed `_keymap` back to `keymap` in set_menus commit addresses several minor issues in the Linux global menu implementation: - In X11, added a warning log and fallback to immutably borrow and shutdown the DBusMenu server if `try_borrow_mut` fails during X11Client drop, preventing silent resource leaks. - In Wayland, avoided unnecessary cloning of `surface_id` in the `on_connected` callback by iterating over `.values()`. - Renamed `_keymap` back to `keymap` in set_menus and used `let _ = keymap;` for conventional unused variable suppression when the `global-menu` feature is disabled. --- crates/gpui_linux/src/linux/platform.rs | 7 +++++-- crates/gpui_linux/src/linux/wayland/client.rs | 6 +++--- crates/gpui_linux/src/linux/x11/client.rs | 4 +++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/crates/gpui_linux/src/linux/platform.rs b/crates/gpui_linux/src/linux/platform.rs index 16762ccedc1..ffd9c405098 100644 --- a/crates/gpui_linux/src/linux/platform.rs +++ b/crates/gpui_linux/src/linux/platform.rs @@ -523,12 +523,15 @@ impl Platform for LinuxPlatform

{ Ok(app_path) } - fn set_menus(&self, menus: Vec

, _keymap: &Keymap) { + fn set_menus(&self, menus: Vec, keymap: &Keymap) { + #[cfg(not(all(any(feature = "wayland", feature = "x11"), feature = "global-menu")))] + let _ = keymap; + self.inner.with_common(|common| { common.menus = menus.into_iter().map(|menu| menu.owned()).collect(); #[cfg(all(any(feature = "wayland", feature = "x11"), feature = "global-menu"))] if let Some(server) = &common.dbus_menu_server { - server.set_menus(common.menus.clone(), _keymap); + server.set_menus(common.menus.clone(), keymap); } }) } diff --git a/crates/gpui_linux/src/linux/wayland/client.rs b/crates/gpui_linux/src/linux/wayland/client.rs index 20c002c7d32..d9c2c4b0f5e 100644 --- a/crates/gpui_linux/src/linux/wayland/client.rs +++ b/crates/gpui_linux/src/linux/wayland/client.rs @@ -746,13 +746,13 @@ impl WaylandClient { }; let appmenus = state .appmenu_objects - .iter() - .map(|(surface_id, appmenu)| (surface_id.clone(), appmenu.clone())) + .values() + .map(|appmenu| appmenu.clone()) .collect::>(); (service_name, dbus_menu_server, appmenus) }; - for (_, appmenu) in appmenus { + for appmenu in appmenus { appmenu.set_address(service_name.clone(), crate::linux::dbusmenu::DBUSMENU_OBJECT_PATH.to_string()); } }, diff --git a/crates/gpui_linux/src/linux/x11/client.rs b/crates/gpui_linux/src/linux/x11/client.rs index de813a81663..1012eb83f48 100644 --- a/crates/gpui_linux/src/linux/x11/client.rs +++ b/crates/gpui_linux/src/linux/x11/client.rs @@ -322,7 +322,9 @@ impl Drop for X11Client { state.dbus_menu_thread.take(), ), Err(_) => { - return; + log::warn!("Failed to borrow X11Client inner in Drop; DBusMenu resources may leak"); + let server = self.0.try_borrow().ok().and_then(|state| state.common.dbus_menu_server.clone()); + (server, None) } }; From 2e8fbda6ba7e99f8c2c38560223a97261d792799 Mon Sep 17 00:00:00 2001 From: Nihal <121309701+nihalxkumar@users.noreply.github.com> Date: Sun, 22 Mar 2026 14:26:47 +0530 Subject: [PATCH 07/16] cargo fmt --- crates/gpui_linux/src/linux/dbusmenu.rs | 42 +++++++++++-------- crates/gpui_linux/src/linux/wayland/client.rs | 16 ++++--- crates/gpui_linux/src/linux/x11/client.rs | 15 +++++-- 3 files changed, 46 insertions(+), 27 deletions(-) diff --git a/crates/gpui_linux/src/linux/dbusmenu.rs b/crates/gpui_linux/src/linux/dbusmenu.rs index 3794c48b948..75094ddb440 100644 --- a/crates/gpui_linux/src/linux/dbusmenu.rs +++ b/crates/gpui_linux/src/linux/dbusmenu.rs @@ -12,8 +12,8 @@ use gpui::{Action, KeyContext, KeybindingKeystroke, Keymap, OsAction, OwnedMenu, use util::ResultExt as _; use zbus::zvariant::{OwnedValue, Value}; -use std::rc::Weak; use std::cell::RefCell; +use std::rc::Weak; pub trait GlobalMenuState { fn linux_common(&mut self) -> &mut crate::linux::LinuxCommon; @@ -51,7 +51,7 @@ pub fn setup_global_menu_sources( common.callbacks.will_open_app_menu.take(), ) }; - + let request_ids = request.ids; if let Some(callback) = will_open_app_menu.as_mut() { callback(); @@ -524,7 +524,9 @@ impl DBusMenuServer { for (id, enabled, value) in &updates { if let Some(entry) = state.items.get_mut(id) { entry.enabled = Some(*enabled); - entry.properties.insert("enabled".to_string(), value.clone()); + entry + .properties + .insert("enabled".to_string(), value.clone()); } } } @@ -568,9 +570,9 @@ impl DBusMenuServer { removed_props: Vec<(i32, Vec)>, ) { self.request_items_properties_updated_for_paths( - updated_props, - removed_props, - vec![DBUSMENU_OBJECT_PATH.to_string()] + updated_props, + removed_props, + vec![DBUSMENU_OBJECT_PATH.to_string()], ); } @@ -656,10 +658,7 @@ impl DBusMenuServer { } } - async fn about_to_show_group( - &self, - ids: Vec, - ) -> zbus::fdo::Result<(Vec, Vec)> { + async fn about_to_show_group(&self, ids: Vec) -> zbus::fdo::Result<(Vec, Vec)> { let sender = match self.about_to_show_sender.lock() { Ok(sender) => sender.clone(), Err(_) => return Ok((Vec::new(), Vec::new())), @@ -737,7 +736,10 @@ impl DBusMenuServer { ids.into_iter() .filter_map(|id| { state.items.get(&id).map(|entry| { - (id, filter_properties(entry.properties.clone(), &property_filter)) + ( + id, + filter_properties(entry.properties.clone(), &property_filter), + ) }) }) .collect::>() @@ -753,9 +755,10 @@ impl DBusMenuServer { property_names: Vec, ) -> zbus::fdo::Result<(u32, (i32, HashMap, Vec))> { let property_filter = build_property_filter(&property_names); - let state = self.state.lock().map_err(|_| { - zbus::fdo::Error::Failed("Failed to access DBusMenu state".to_string()) - })?; + let state = self + .state + .lock() + .map_err(|_| zbus::fdo::Error::Failed("Failed to access DBusMenu state".to_string()))?; let revision = state.revision; let depth = if recursion_depth < 0 { i32::MAX @@ -764,8 +767,8 @@ impl DBusMenuServer { }; let layout = collect_layout_node_filtered(&state, parent_id, depth, &property_filter) .ok_or_else(|| { - zbus::fdo::Error::InvalidArgs(format!("Unknown menu item id: {parent_id}")) - })?; + zbus::fdo::Error::InvalidArgs(format!("Unknown menu item id: {parent_id}")) + })?; Ok((revision, layout)) } @@ -986,7 +989,11 @@ fn build_menu_tree( } else { log::error!("Failed to encode DBusMenu toggle-type"); } - let toggle_state = if *checked { Value::I32(1) } else { Value::I32(0) }; + let toggle_state = if *checked { + Value::I32(1) + } else { + Value::I32(0) + }; if let Ok(value) = toggle_state.try_into() { props.insert("toggle-state".to_string(), value); } else { @@ -1024,7 +1031,6 @@ fn build_menu_tree( ); } - pub fn global_menu_env_override() -> Option { match std::env::var("ZED_GLOBAL_MENU").ok().as_deref() { None => None, diff --git a/crates/gpui_linux/src/linux/wayland/client.rs b/crates/gpui_linux/src/linux/wayland/client.rs index d9c2c4b0f5e..7b47016bf52 100644 --- a/crates/gpui_linux/src/linux/wayland/client.rs +++ b/crates/gpui_linux/src/linux/wayland/client.rs @@ -738,10 +738,12 @@ impl WaylandClient { Rc::downgrade(&state), move |state| { let (service_name, dbus_menu_server, appmenus) = { - let Some(service_name) = state.dbus_service_name.as_ref().cloned() else { + let Some(service_name) = state.dbus_service_name.as_ref().cloned() + else { return; }; - let Some(dbus_menu_server) = state.common.dbus_menu_server.clone() else { + let Some(dbus_menu_server) = state.common.dbus_menu_server.clone() + else { return; }; let appmenus = state @@ -753,7 +755,10 @@ impl WaylandClient { }; for appmenu in appmenus { - appmenu.set_address(service_name.clone(), crate::linux::dbusmenu::DBUSMENU_OBJECT_PATH.to_string()); + appmenu.set_address( + service_name.clone(), + crate::linux::dbusmenu::DBUSMENU_OBJECT_PATH.to_string(), + ); } }, ); @@ -882,8 +887,9 @@ impl LinuxClient for WaylandClient { let dbus_menu_server = state.common.dbus_menu_server.clone(); let surface = window.0.surface(); let appmenu = appmenu_manager.create(&surface, &state.globals.qh, ()); - if let Some(_dbus_menu_server) = - dbus_menu_server.as_ref().filter(|server| server.is_connected()) + if let Some(_dbus_menu_server) = dbus_menu_server + .as_ref() + .filter(|server| server.is_connected()) { let object_path = crate::linux::dbusmenu::DBUSMENU_OBJECT_PATH.to_string(); appmenu.set_address(service_name.clone(), object_path); diff --git a/crates/gpui_linux/src/linux/x11/client.rs b/crates/gpui_linux/src/linux/x11/client.rs index 1012eb83f48..275d4929dd2 100644 --- a/crates/gpui_linux/src/linux/x11/client.rs +++ b/crates/gpui_linux/src/linux/x11/client.rs @@ -58,7 +58,6 @@ use crate::linux::{ }; use crate::linux::{LinuxCommon, LinuxKeyboardLayout, X11Window, modifiers_from_xinput_info}; - use gpui::{ AnyWindowHandle, Bounds, ClipboardItem, CursorStyle, DisplayId, FileDropEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, Pixels, PlatformDisplay, PlatformInput, @@ -322,8 +321,14 @@ impl Drop for X11Client { state.dbus_menu_thread.take(), ), Err(_) => { - log::warn!("Failed to borrow X11Client inner in Drop; DBusMenu resources may leak"); - let server = self.0.try_borrow().ok().and_then(|state| state.common.dbus_menu_server.clone()); + log::warn!( + "Failed to borrow X11Client inner in Drop; DBusMenu resources may leak" + ); + let server = self + .0 + .try_borrow() + .ok() + .and_then(|state| state.common.dbus_menu_server.clone()); (server, None) } }; @@ -614,7 +619,9 @@ impl X11Client { let window_ids: Vec = state.windows.keys().copied().collect(); for x_window in window_ids { - let final_path = crate::linux::dbusmenu::DBUSMENU_OBJECT_PATH.to_string(); + let final_path = + crate::linux::dbusmenu::DBUSMENU_OBJECT_PATH + .to_string(); set_x11_appmenu_properties( &state.xcb_connection, &state.atoms, From 569771c92964cba04e82c5ed6513f7fa7fd97544 Mon Sep 17 00:00:00 2001 From: Nihal <121309701+nihalxkumar@users.noreply.github.com> Date: Sun, 22 Mar 2026 15:50:14 +0530 Subject: [PATCH 08/16] gpui_linux: Fix action callback reentrancy and interface correctness bugs The action callback was invoked while borrowing the client mutably. If dispatch_action() synchronously re-entered the client (e.g. to update menu state), this would panic with a RefCell reborrow. Take the callback out, drop the borrow, invoke the callback, then restore it. This is the same pattern already used by the about_to_show handler. AboutToShowGroup: return only the intersection of actually-changed IDs with requested valid IDs, instead of returning all valid IDs whenever anything changed anywhere. GetGroupProperties([]): when ids is empty, return all menu items per the DBusMenu spec. Previously returned an empty vec. GetProperty: return InvalidArgs for unknown item ID and UnknownProperty for missing property name. Previously conflated both cases into UnknownProperty. --- crates/gpui_linux/src/linux/dbusmenu.rs | 47 ++++++++++++++++++++----- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/crates/gpui_linux/src/linux/dbusmenu.rs b/crates/gpui_linux/src/linux/dbusmenu.rs index 75094ddb440..35e6958b1d0 100644 --- a/crates/gpui_linux/src/linux/dbusmenu.rs +++ b/crates/gpui_linux/src/linux/dbusmenu.rs @@ -31,10 +31,19 @@ pub fn setup_global_menu_sources( let client = client.clone(); move |action| { if let Some(client) = client.upgrade() { - let mut state = client.borrow_mut(); - if let Some(callback) = state.linux_common().callbacks.app_menu_action.as_mut() { + let mut callback = { + let mut state = client.borrow_mut(); + state.linux_common().callbacks.app_menu_action.take() + }; + + if let Some(callback) = callback.as_mut() { callback(action.as_ref()); } + + let mut state = client.borrow_mut(); + if state.linux_common().callbacks.app_menu_action.is_none() { + state.linux_common().callbacks.app_menu_action = callback; + } } } }, @@ -78,6 +87,9 @@ pub fn setup_global_menu_sources( Vec::new() } else { valid_ids + .into_iter() + .filter(|id| refreshed_ids.contains(id)) + .collect() }; let mut state = client.borrow_mut(); @@ -733,16 +745,29 @@ impl DBusMenuServer { let state = self.state.lock().map_err(|_| { zbus::fdo::Error::Failed("Failed to access DBusMenu state".to_string()) })?; - ids.into_iter() - .filter_map(|id| { - state.items.get(&id).map(|entry| { + if ids.is_empty() { + state + .items + .iter() + .map(|(&id, entry)| { ( id, filter_properties(entry.properties.clone(), &property_filter), ) }) - }) - .collect::>() + .collect::>() + } else { + ids.into_iter() + .filter_map(|id| { + state.items.get(&id).map(|entry| { + ( + id, + filter_properties(entry.properties.clone(), &property_filter), + ) + }) + }) + .collect::>() + } }; Ok(entries) @@ -778,10 +803,14 @@ impl DBusMenuServer { .state .lock() .map_err(|_| zbus::fdo::Error::Failed("Failed to access DBusMenu state".to_string()))?; - state + let entry = state .items .get(&id) - .and_then(|entry| entry.properties.get(name).cloned()) + .ok_or_else(|| zbus::fdo::Error::InvalidArgs(format!("Unknown menu item id: {id}")))?; + entry + .properties + .get(name) + .cloned() .ok_or_else(|| zbus::fdo::Error::UnknownProperty(name.to_string())) } From caaafc048e471904d96f1163a83e07d12415c70e Mon Sep 17 00:00:00 2001 From: Nihal <121309701+nihalxkumar@users.noreply.github.com> Date: Sun, 22 Mar 2026 15:58:26 +0530 Subject: [PATCH 09/16] gpui_linux: Fix connection lifecycle in global menu - Add mark_disconnected() to reset the connected flag and clear command/about_to_show senders. - shutdown() now takes the command sender before clearing state, sends the Shutdown command, then marks disconnected. This ensures is_global_menu_active() returns false after shutdown and no new signals are queued to a dead channel. - DBus thread calls mark_disconnected() on exit, covering cases where the thread exits without an explicit shutdown() call. - Wayland Drop now warns on borrow failure, matching X11's existing behavior. Previously it silently gave up, risking DBus resource leaks. --- crates/gpui_linux/src/linux/dbusmenu.rs | 31 ++++++++++++++----- crates/gpui_linux/src/linux/wayland/client.rs | 7 ++++- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/crates/gpui_linux/src/linux/dbusmenu.rs b/crates/gpui_linux/src/linux/dbusmenu.rs index 35e6958b1d0..f3daeed3ee0 100644 --- a/crates/gpui_linux/src/linux/dbusmenu.rs +++ b/crates/gpui_linux/src/linux/dbusmenu.rs @@ -322,6 +322,9 @@ impl DBusMenuServer { } } } + + dbus_menu_server.mark_disconnected(); + log::info!("DBusMenu server stopped"); }); }) .log_err() @@ -408,6 +411,16 @@ impl DBusMenuServer { self.connected.load(Ordering::SeqCst) } + pub fn mark_disconnected(&self) { + self.connected.store(false, Ordering::SeqCst); + if let Ok(mut sender) = self.command_sender.lock() { + *sender = None; + } + if let Ok(mut sender) = self.about_to_show_sender.lock() { + *sender = None; + } + } + pub fn set_action_callback(&self, callback: Box) { if let Ok(mut slot) = self.action_callback.lock() { *slot = Some(callback); @@ -462,18 +475,22 @@ impl DBusMenuServer { pub fn shutdown(&self) { let sender = match self.command_sender.lock() { - Ok(sender) => sender.clone(), + Ok(mut sender) => sender.take(), Err(error) => { log::error!("Failed to read DBusMenu command sender: {error}"); - return; + None } }; - let Some(sender) = sender else { - return; - }; - if let Err(error) = sender.try_send(DBusMenuCommand::Shutdown) { - log::error!("Failed to queue DBusMenu shutdown request: {error}"); + self.connected.store(false, Ordering::SeqCst); + if let Ok(mut slot) = self.about_to_show_sender.lock() { + *slot = None; + } + + if let Some(sender) = sender { + if let Err(error) = sender.try_send(DBusMenuCommand::Shutdown) { + log::error!("Failed to queue DBusMenu shutdown request: {error}"); + } } } diff --git a/crates/gpui_linux/src/linux/wayland/client.rs b/crates/gpui_linux/src/linux/wayland/client.rs index 7b47016bf52..5d63f04e103 100644 --- a/crates/gpui_linux/src/linux/wayland/client.rs +++ b/crates/gpui_linux/src/linux/wayland/client.rs @@ -440,7 +440,12 @@ impl Drop for WaylandClient { state.common.dbus_menu_server.clone(), state.dbus_menu_thread.take(), ), - Err(_) => (None, None), + Err(_) => { + log::warn!( + "Failed to borrow WaylandClient inner in Drop; DBusMenu resources may leak" + ); + (None, None) + } }; if let Some(dbus_menu_server) = dbus_menu_server { From 65c91a4e54e1805829bae2598e66a5719333ba9e Mon Sep 17 00:00:00 2001 From: Nihal <121309701+nihalxkumar@users.noreply.github.com> Date: Sun, 22 Mar 2026 16:01:19 +0530 Subject: [PATCH 10/16] gpui_linux: Improve error handling in global menu DBus methods Add debug logging to all silent failure paths in about_to_show and about_to_show_group: lock poisoning, channel closure, and response timeouts. These previously returned false/empty with no diagnostic information, making timeout-related menu bugs difficult to investigate. Also log variant conversion failures in collect_layout_node_filtered instead of silently dropping children. --- crates/gpui_linux/src/linux/dbusmenu.rs | 42 ++++++++++++++++++------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/crates/gpui_linux/src/linux/dbusmenu.rs b/crates/gpui_linux/src/linux/dbusmenu.rs index f3daeed3ee0..da51b05bc92 100644 --- a/crates/gpui_linux/src/linux/dbusmenu.rs +++ b/crates/gpui_linux/src/linux/dbusmenu.rs @@ -664,7 +664,10 @@ impl DBusMenuServer { async fn about_to_show(&self, id: i32) -> zbus::fdo::Result { let sender = match self.about_to_show_sender.lock() { Ok(sender) => sender.clone(), - Err(_) => return Ok(false), + Err(_) => { + log::debug!("about_to_show({id}): sender lock poisoned"); + return Ok(false); + } }; let Some(sender) = sender else { return Ok(false); @@ -678,19 +681,26 @@ impl DBusMenuServer { }) .is_err() { + log::debug!("about_to_show({id}): main thread channel closed"); return Ok(false); } match responded_rx.recv_timeout(Duration::from_millis(50)) { Ok(response) => Ok(!response.updated_ids.is_empty()), - Err(_) => Ok(false), + Err(_) => { + log::debug!("about_to_show({id}): timed out waiting for main thread"); + Ok(false) + } } } async fn about_to_show_group(&self, ids: Vec) -> zbus::fdo::Result<(Vec, Vec)> { let sender = match self.about_to_show_sender.lock() { Ok(sender) => sender.clone(), - Err(_) => return Ok((Vec::new(), Vec::new())), + Err(_) => { + log::debug!("about_to_show_group({ids:?}): sender lock poisoned"); + return Ok((Vec::new(), Vec::new())); + } }; let Some(sender) = sender else { return Ok((Vec::new(), Vec::new())); @@ -704,15 +714,20 @@ impl DBusMenuServer { }) .is_err() { + log::debug!("about_to_show_group: main thread channel closed"); return Ok((Vec::new(), Vec::new())); } - let response = responded_rx - .recv_timeout(Duration::from_millis(50)) - .unwrap_or(AboutToShowResponse { - updated_ids: Vec::new(), - id_errors: Vec::new(), - }); + let response = match responded_rx.recv_timeout(Duration::from_millis(50)) { + Ok(response) => response, + Err(_) => { + log::debug!("about_to_show_group: timed out waiting for main thread"); + AboutToShowResponse { + updated_ids: Vec::new(), + id_errors: Vec::new(), + } + } + }; Ok((response.updated_ids, response.id_errors)) } @@ -924,8 +939,13 @@ fn collect_layout_node_filtered( collect_layout_node_filtered(state, child_id, remaining_depth - 1, filter) { let variant = Value::from(zbus::zvariant::Structure::from(child_node)).try_into(); - if let Ok(value) = variant { - result.push(value); + match variant { + Ok(value) => result.push(value), + Err(error) => { + log::error!( + "Failed to convert layout node for child {child_id} to OwnedValue: {error}" + ); + } } } } From e5e1441c895a6c97bed672b2afcebb06fc729869 Mon Sep 17 00:00:00 2001 From: Nihal <121309701+nihalxkumar@users.noreply.github.com> Date: Sun, 22 Mar 2026 16:16:52 +0530 Subject: [PATCH 11/16] gpui_linux: Remove dead code and clean up DBusMenu properties - Remove unused public wrapper `refresh_enabled_states()` - Simplify DBusMenu object path plumbing; since the server only binds to a single object path per session, don't pass around a list of paths in commands. - Remove redundant default properties (visible=true, enabled=true). DBusMenu defaults to true for these if they are absent, and setting them manually bloated the dictionary. - Remove unused `item_activation_requested` signal definition, as activation is handled entirely via the event() method. - Fix unreachable string iteration code in `normalize_dbus_key`. --- crates/gpui_linux/src/linux/dbusmenu.rs | 149 +++++------------- crates/gpui_linux/src/linux/wayland/client.rs | 2 +- 2 files changed, 41 insertions(+), 110 deletions(-) diff --git a/crates/gpui_linux/src/linux/dbusmenu.rs b/crates/gpui_linux/src/linux/dbusmenu.rs index da51b05bc92..a65478af853 100644 --- a/crates/gpui_linux/src/linux/dbusmenu.rs +++ b/crates/gpui_linux/src/linux/dbusmenu.rs @@ -132,12 +132,10 @@ pub enum DBusMenuCommand { LayoutUpdated { revision: u32, parent: i32, - object_paths: Vec, }, ItemsPropertiesUpdated { updated_props: Vec<(i32, HashMap)>, removed_props: Vec<(i32, Vec)>, - object_paths: Vec, }, Shutdown, } @@ -256,65 +254,53 @@ impl DBusMenuServer { DBusMenuCommand::LayoutUpdated { revision, parent, - object_paths, } => { - for object_path in object_paths { - let emitter = - match zbus::object_server::SignalEmitter::new( - &connection, - object_path.as_str(), - ) { - Ok(emitter) => emitter, - Err(error) => { - log::error!( - "Failed to build DBusMenu signal emitter for {object_path}: {error}" - ); - continue; - } - }; - if let Err(error) = DBusMenuServer::layout_updated( - &emitter, - revision, - parent, - ) - .await - { - log::error!( - "Failed to emit DBusMenu LayoutUpdated signal for {object_path}: {error}" - ); + let emitter = match zbus::object_server::SignalEmitter::new( + &connection, + object_path.as_str(), + ) { + Ok(emitter) => emitter, + Err(error) => { + log::error!("Failed to build DBusMenu signal emitter for {object_path}: {error}"); + continue; } + }; + if let Err(error) = DBusMenuServer::layout_updated( + &emitter, + revision, + parent, + ) + .await + { + log::error!( + "Failed to emit DBusMenu LayoutUpdated signal for {object_path}: {error}" + ); } } DBusMenuCommand::ItemsPropertiesUpdated { updated_props, removed_props, - object_paths, } => { - for object_path in object_paths { - let emitter = - match zbus::object_server::SignalEmitter::new( - &connection, - object_path.as_str(), - ) { - Ok(emitter) => emitter, - Err(error) => { - log::error!( - "Failed to build DBusMenu signal emitter for {object_path}: {error}" - ); - continue; - } - }; - if let Err(error) = DBusMenuServer::items_properties_updated( - &emitter, - updated_props.clone(), - removed_props.clone(), - ) - .await - { - log::error!( - "Failed to emit DBusMenu ItemsPropertiesUpdated for {object_path}: {error}" - ); + let emitter = match zbus::object_server::SignalEmitter::new( + &connection, + object_path.as_str(), + ) { + Ok(emitter) => emitter, + Err(error) => { + log::error!("Failed to build DBusMenu signal emitter for {object_path}: {error}"); + continue; } + }; + if let Err(error) = DBusMenuServer::items_properties_updated( + &emitter, + updated_props.clone(), + removed_props.clone(), + ) + .await + { + log::error!( + "Failed to emit DBusMenu ItemsPropertiesUpdated for {object_path}: {error}" + ); } } DBusMenuCommand::Shutdown => { @@ -494,11 +480,6 @@ impl DBusMenuServer { } } - pub fn refresh_enabled_states(&self, validate: &mut dyn FnMut(&dyn Action) -> bool) -> bool { - let updated_ids = self.refresh_enabled_states_inner(validate); - !updated_ids.is_empty() - } - pub fn refresh_enabled_states_inner( &self, validate: &mut dyn FnMut(&dyn Action) -> bool, @@ -565,10 +546,6 @@ impl DBusMenuServer { } fn request_layout_updated(&self, revision: u32) { - self.request_layout_updated_for_paths(revision, vec![DBUSMENU_OBJECT_PATH.to_string()]); - } - - fn request_layout_updated_for_paths(&self, revision: u32, object_paths: Vec) { if !self.is_connected() { return; } @@ -587,7 +564,6 @@ impl DBusMenuServer { if let Err(error) = sender.try_send(DBusMenuCommand::LayoutUpdated { revision, parent: 0, - object_paths, }) { log::error!("Failed to queue DBusMenu LayoutUpdated signal: {error}"); } @@ -597,19 +573,6 @@ impl DBusMenuServer { &self, updated_props: Vec<(i32, HashMap)>, removed_props: Vec<(i32, Vec)>, - ) { - self.request_items_properties_updated_for_paths( - updated_props, - removed_props, - vec![DBUSMENU_OBJECT_PATH.to_string()], - ); - } - - fn request_items_properties_updated_for_paths( - &self, - updated_props: Vec<(i32, HashMap)>, - removed_props: Vec<(i32, Vec)>, - object_paths: Vec, ) { if !self.is_connected() { return; @@ -629,7 +592,6 @@ impl DBusMenuServer { if let Err(error) = sender.try_send(DBusMenuCommand::ItemsPropertiesUpdated { updated_props, removed_props, - object_paths, }) { log::error!("Failed to queue DBusMenu ItemsPropertiesUpdated signal: {error}"); } @@ -879,13 +841,6 @@ impl DBusMenuServer { updated_props: Vec<(i32, HashMap)>, removed_props: Vec<(i32, Vec)>, ) -> zbus::Result<()>; - - #[zbus(signal)] - pub async fn item_activation_requested( - ctxt: &zbus::object_server::SignalEmitter<'_>, - id: i32, - timestamp: u32, - ) -> zbus::Result<()>; } fn root_properties() -> HashMap { @@ -896,7 +851,6 @@ fn root_properties() -> HashMap { } else { log::error!("Failed to build DBusMenu root properties"); } - insert_visible_property(&mut props); props } @@ -975,7 +929,6 @@ fn build_menu_tree( } else { log::error!("Failed to encode DBusMenu children-display property"); } - insert_visible_property(&mut properties); let mut child_ids = Vec::new(); @@ -992,7 +945,6 @@ fn build_menu_tree( } else { log::error!("Failed to encode DBusMenu separator type"); } - insert_visible_property(&mut props); items.insert( child_id, MenuItemEntry { @@ -1020,15 +972,6 @@ fn build_menu_tree( log::error!("Failed to encode DBusMenu label for menu item {}", name); } - if let Some(value) = owned_bool(true) { - props.insert("enabled".to_string(), value); - } else { - log::error!( - "Failed to encode DBusMenu enabled state for menu item {}", - name - ); - } - if let Some(shortcut) = dbus_shortcut_for_action(action.as_ref(), keymap) { props.insert("shortcut".to_string(), shortcut); } @@ -1066,7 +1009,6 @@ fn build_menu_tree( log::error!("Failed to encode DBusMenu toggle-state"); } } - insert_visible_property(&mut props); items.insert( child_id, MenuItemEntry { @@ -1115,14 +1057,6 @@ fn owned_bool(value: bool) -> Option { Value::Bool(value).try_into().ok() } -fn insert_visible_property(props: &mut HashMap) { - if let Some(value) = owned_bool(true) { - props.insert("visible".to_string(), value); - } else { - log::error!("Failed to encode DBusMenu visible property"); - } -} - fn icon_name_for_os_action(action: OsAction) -> Option<&'static str> { match action { OsAction::Cut => Some("edit-cut"), @@ -1221,11 +1155,8 @@ fn normalize_dbus_key(key: &str) -> Option { key.to_string() } } else if key.len() == 1 { - let mut chars = key.chars(); - let ch = chars.next()?; - if chars.next().is_some() { - key.to_string() - } else if ch.is_ascii_alphabetic() { + let ch = key.chars().next()?; + if ch.is_ascii_alphabetic() { ch.to_ascii_uppercase().to_string() } else { key.to_string() diff --git a/crates/gpui_linux/src/linux/wayland/client.rs b/crates/gpui_linux/src/linux/wayland/client.rs index 5d63f04e103..074c7c336d7 100644 --- a/crates/gpui_linux/src/linux/wayland/client.rs +++ b/crates/gpui_linux/src/linux/wayland/client.rs @@ -742,7 +742,7 @@ impl WaylandClient { &state.borrow().loop_handle, Rc::downgrade(&state), move |state| { - let (service_name, dbus_menu_server, appmenus) = { + let (service_name, _dbus_menu_server, appmenus) = { let Some(service_name) = state.dbus_service_name.as_ref().cloned() else { return; From d056bb5331389c5e44326ea64a2bd099abe89936 Mon Sep 17 00:00:00 2001 From: Nihal <121309701+nihalxkumar@users.noreply.github.com> Date: Thu, 26 Mar 2026 14:17:31 +0530 Subject: [PATCH 12/16] gpui_linux: Prevent DBus menu shutdown on cloned client drop --- crates/gpui_linux/src/linux/wayland/client.rs | 4 ++++ crates/gpui_linux/src/linux/x11/client.rs | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/crates/gpui_linux/src/linux/wayland/client.rs b/crates/gpui_linux/src/linux/wayland/client.rs index 074c7c336d7..b4a7062d500 100644 --- a/crates/gpui_linux/src/linux/wayland/client.rs +++ b/crates/gpui_linux/src/linux/wayland/client.rs @@ -433,6 +433,10 @@ pub struct WaylandClient(Rc>); impl Drop for WaylandClient { fn drop(&mut self) { + if Rc::strong_count(&self.0) > 1 { + return; + } + #[cfg(feature = "global-menu")] { let (dbus_menu_server, dbus_menu_thread) = match self.0.try_borrow_mut() { diff --git a/crates/gpui_linux/src/linux/x11/client.rs b/crates/gpui_linux/src/linux/x11/client.rs index 275d4929dd2..bb1748bc1c3 100644 --- a/crates/gpui_linux/src/linux/x11/client.rs +++ b/crates/gpui_linux/src/linux/x11/client.rs @@ -313,6 +313,10 @@ pub(crate) struct X11Client(pub(crate) Rc>); impl Drop for X11Client { fn drop(&mut self) { + if Rc::strong_count(&self.0) > 1 { + return; + } + #[cfg(feature = "global-menu")] { let (dbus_menu_server, dbus_menu_thread) = match self.0.try_borrow_mut() { From 2a7f1f4ebe29fde67340092a4ea87a7d28471b03 Mon Sep 17 00:00:00 2001 From: Nihal <121309701+nihalxkumar@users.noreply.github.com> Date: Sun, 5 Apr 2026 01:22:39 +0530 Subject: [PATCH 13/16] gpui_linux: Add some comments to non-obvious patterns --- crates/gpui_linux/src/linux/dbusmenu.rs | 10 ++++++++++ crates/gpui_linux/src/linux/wayland/client.rs | 3 +++ crates/gpui_linux/src/linux/x11/client.rs | 3 +++ 3 files changed, 16 insertions(+) diff --git a/crates/gpui_linux/src/linux/dbusmenu.rs b/crates/gpui_linux/src/linux/dbusmenu.rs index a65478af853..fc4e35ea31f 100644 --- a/crates/gpui_linux/src/linux/dbusmenu.rs +++ b/crates/gpui_linux/src/linux/dbusmenu.rs @@ -31,6 +31,9 @@ pub fn setup_global_menu_sources( let client = client.clone(); move |action| { if let Some(client) = client.upgrade() { + // Take the callback out before invoking it to avoid holding a + // borrow_mut across the call, which would panic if the callback + // re-enters client state (e.g. dispatching actions that read it). let mut callback = { let mut state = client.borrow_mut(); state.linux_common().callbacks.app_menu_action.take() @@ -51,6 +54,8 @@ pub fn setup_global_menu_sources( let client = client.clone(); move |request| { if let Some(client) = client.upgrade() { + // Same take-and-restore pattern as the action handler above: + // callbacks are extracted before use to avoid reentrancy panics. let (dbus_menu_server, mut validate_app_menu_command, mut will_open_app_menu) = { let mut state = client.borrow_mut(); let common = state.linux_common(); @@ -647,6 +652,8 @@ impl DBusMenuServer { return Ok(false); } + // D-Bus about_to_show is synchronous — the menu host blocks until we reply. + // 50ms keeps the menu responsive; on timeout we return false (no updates). match responded_rx.recv_timeout(Duration::from_millis(50)) { Ok(response) => Ok(!response.updated_ids.is_empty()), Err(_) => { @@ -1071,6 +1078,9 @@ fn icon_name_for_os_action(action: OsAction) -> Option<&'static str> { fn dbus_shortcut_for_action(action: &dyn Action, keymap: &Keymap) -> Option { static DEFAULT_CONTEXT: OnceLock> = OnceLock::new(); + // Build a synthetic context matching the typical focus stack + // (Workspace > Pane > Editor) so keybinding lookups resolve the same + // shortcuts a user would see in the editor. let contexts = DEFAULT_CONTEXT.get_or_init(|| { let mut workspace_context = KeyContext::new_with_defaults(); workspace_context.add("Workspace"); diff --git a/crates/gpui_linux/src/linux/wayland/client.rs b/crates/gpui_linux/src/linux/wayland/client.rs index b4a7062d500..1ec42cb45dc 100644 --- a/crates/gpui_linux/src/linux/wayland/client.rs +++ b/crates/gpui_linux/src/linux/wayland/client.rs @@ -433,6 +433,9 @@ pub struct WaylandClient(Rc>); impl Drop for WaylandClient { fn drop(&mut self) { + // Only shut down the D-Bus menu server when the last clone drops, + // because `WaylandClient` is cheaply cloned via `Rc` and earlier drops + // must not tear down the shared server. if Rc::strong_count(&self.0) > 1 { return; } diff --git a/crates/gpui_linux/src/linux/x11/client.rs b/crates/gpui_linux/src/linux/x11/client.rs index bb1748bc1c3..9f9c532c58b 100644 --- a/crates/gpui_linux/src/linux/x11/client.rs +++ b/crates/gpui_linux/src/linux/x11/client.rs @@ -313,6 +313,9 @@ pub(crate) struct X11Client(pub(crate) Rc>); impl Drop for X11Client { fn drop(&mut self) { + // Only shut down the D-Bus menu server when the last clone drops, + // because `X11Client` is cheaply cloned via `Rc` and earlier drops + // must not tear down the shared server. if Rc::strong_count(&self.0) > 1 { return; } From e0253de3f380283a2f911d3246fcfce19120d68d Mon Sep 17 00:00:00 2001 From: Nihal <121309701+nihalxkumar@users.noreply.github.com> Date: Tue, 28 Apr 2026 00:21:02 +0530 Subject: [PATCH 14/16] gpui_linux: Restore removal of zed workspace member and release codegen profile --- Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index b9af56e1488..36e7ca8cc71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -459,6 +459,7 @@ web_search_providers = { path = "crates/web_search_providers" } workspace = { path = "crates/workspace" } worktree = { path = "crates/worktree" } x_ai = { path = "crates/x_ai" } +zed = { path = "crates/zed" } zed_actions = { path = "crates/zed_actions" } zed_env_vars = { path = "crates/zed_env_vars" } edit_prediction = { path = "crates/edit_prediction" } @@ -914,6 +915,9 @@ debug = "limited" lto = "thin" codegen-units = 1 +[profile.release.package] +zed = { codegen-units = 16 } + [profile.release-fast] inherits = "release" debug = "full" From 29b9265f29bc7ce5de8e783dccc139faa95b2b32 Mon Sep 17 00:00:00 2001 From: Nihal <121309701+nihalxkumar@users.noreply.github.com> Date: Tue, 28 Apr 2026 11:07:08 +0530 Subject: [PATCH 15/16] gpui_linux: Remove gated flag and use wayland_protocols_plasma instead of wayland_scanner + xml files --- Cargo.lock | 1 - crates/gpui_linux/Cargo.toml | 6 +-- crates/gpui_linux/src/linux.rs | 1 - crates/gpui_linux/src/linux/dbusmenu.rs | 1 - crates/gpui_linux/src/linux/platform.rs | 25 +++--------- crates/gpui_linux/src/linux/wayland.rs | 3 -- .../gpui_linux/src/linux/wayland/appmenu.rs | 21 ---------- .../gpui_linux/src/linux/wayland/appmenu.xml | 40 ------------------- crates/gpui_linux/src/linux/wayland/client.rs | 31 +++----------- crates/gpui_linux/src/linux/x11/client.rs | 17 +------- crates/gpui_platform/Cargo.toml | 1 - crates/zed/Cargo.toml | 1 - 12 files changed, 16 insertions(+), 132 deletions(-) delete mode 100644 crates/gpui_linux/src/linux/wayland/appmenu.rs delete mode 100644 crates/gpui_linux/src/linux/wayland/appmenu.xml diff --git a/Cargo.lock b/Cargo.lock index 84bdde57dc5..07207d27423 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7692,7 +7692,6 @@ dependencies = [ "wayland-protocols", "wayland-protocols-plasma", "wayland-protocols-wlr", - "wayland-scanner", "x11-clipboard", "x11rb", "xkbcommon", diff --git a/crates/gpui_linux/Cargo.toml b/crates/gpui_linux/Cargo.toml index 0c30c5ab700..910e1960706 100644 --- a/crates/gpui_linux/Cargo.toml +++ b/crates/gpui_linux/Cargo.toml @@ -14,7 +14,6 @@ path = "src/gpui_linux.rs" [features] default = ["wayland", "x11"] test-support = ["gpui/test-support"] -global-menu = ["dep:async-channel", "dep:zbus", "dep:wayland-scanner"] wayland = [ "bitflags", "gpui_wgpu", @@ -72,9 +71,8 @@ strum.workspace = true url.workspace = true util.workspace = true uuid.workspace = true -async-channel = { workspace = true, optional = true } -zbus = { version = "5", optional = true } -wayland-scanner = { version = "0.31.9", optional = true } +async-channel.workspace = true +zbus = "5" # Always used oo7 = { version = "0.6", default-features = false, features = [ diff --git a/crates/gpui_linux/src/linux.rs b/crates/gpui_linux/src/linux.rs index a2e4c0166a5..36282c18b24 100644 --- a/crates/gpui_linux/src/linux.rs +++ b/crates/gpui_linux/src/linux.rs @@ -1,4 +1,3 @@ -#[cfg(feature = "global-menu")] pub mod dbusmenu; mod dispatcher; mod headless; diff --git a/crates/gpui_linux/src/linux/dbusmenu.rs b/crates/gpui_linux/src/linux/dbusmenu.rs index fc4e35ea31f..526fbf16f9a 100644 --- a/crates/gpui_linux/src/linux/dbusmenu.rs +++ b/crates/gpui_linux/src/linux/dbusmenu.rs @@ -116,7 +116,6 @@ pub fn setup_global_menu_sources( } }, { - let client = client.clone(); move || { if let Some(client) = client.upgrade() { let mut state = client.borrow_mut(); diff --git a/crates/gpui_linux/src/linux/platform.rs b/crates/gpui_linux/src/linux/platform.rs index ffd9c405098..fb4224bc13d 100644 --- a/crates/gpui_linux/src/linux/platform.rs +++ b/crates/gpui_linux/src/linux/platform.rs @@ -117,7 +117,6 @@ pub(crate) struct LinuxCommon { pub(crate) callbacks: PlatformHandlers, pub(crate) signal: LoopSignal, pub(crate) menus: Vec, - #[cfg(all(any(feature = "wayland", feature = "x11"), feature = "global-menu"))] pub(crate) dbus_menu_server: Option, } @@ -145,7 +144,6 @@ impl LinuxCommon { callbacks, signal, menus: Vec::new(), - #[cfg(all(any(feature = "wayland", feature = "x11"), feature = "global-menu"))] dbus_menu_server: None, }; @@ -213,19 +211,12 @@ impl Platform for LinuxPlatform

{ } fn is_global_menu_active(&self) -> bool { - #[cfg(all(any(feature = "wayland", feature = "x11"), feature = "global-menu"))] - { - self.inner.with_common(|common| { - common - .dbus_menu_server - .as_ref() - .is_some_and(|server| server.is_connected()) - }) - } - #[cfg(not(all(any(feature = "wayland", feature = "x11"), feature = "global-menu")))] - { - false - } + self.inner.with_common(|common| { + common + .dbus_menu_server + .as_ref() + .is_some_and(|server| server.is_connected()) + }) } fn restart(&self, binary_path: Option) { @@ -524,12 +515,8 @@ impl Platform for LinuxPlatform

{ } fn set_menus(&self, menus: Vec

, keymap: &Keymap) { - #[cfg(not(all(any(feature = "wayland", feature = "x11"), feature = "global-menu")))] - let _ = keymap; - self.inner.with_common(|common| { common.menus = menus.into_iter().map(|menu| menu.owned()).collect(); - #[cfg(all(any(feature = "wayland", feature = "x11"), feature = "global-menu"))] if let Some(server) = &common.dbus_menu_server { server.set_menus(common.menus.clone(), keymap); } diff --git a/crates/gpui_linux/src/linux/wayland.rs b/crates/gpui_linux/src/linux/wayland.rs index 6fbce26b549..aa1e7974043 100644 --- a/crates/gpui_linux/src/linux/wayland.rs +++ b/crates/gpui_linux/src/linux/wayland.rs @@ -5,9 +5,6 @@ mod display; mod serial; mod window; -#[cfg(feature = "global-menu")] -pub mod appmenu; - /// Contains Types for configuring layer_shell surfaces. pub mod layer_shell; diff --git a/crates/gpui_linux/src/linux/wayland/appmenu.rs b/crates/gpui_linux/src/linux/wayland/appmenu.rs deleted file mode 100644 index 7f0aea7e782..00000000000 --- a/crates/gpui_linux/src/linux/wayland/appmenu.rs +++ /dev/null @@ -1,21 +0,0 @@ -#![allow( - unused_imports, - non_camel_case_types, - non_snake_case, - dead_code, - unused_mut, - unused_variables -)] - -pub mod client { - use wayland_client; - use wayland_client::protocol::*; - - pub mod __interfaces { - use wayland_client::protocol::__interfaces::*; - wayland_scanner::generate_interfaces!("src/linux/wayland/appmenu.xml"); - } - use self::__interfaces::*; - - wayland_scanner::generate_client_code!("src/linux/wayland/appmenu.xml"); -} diff --git a/crates/gpui_linux/src/linux/wayland/appmenu.xml b/crates/gpui_linux/src/linux/wayland/appmenu.xml deleted file mode 100644 index 02a72f7ef4c..00000000000 --- a/crates/gpui_linux/src/linux/wayland/appmenu.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - This interface allows a client to link a window (or wl_surface) to an com.canonical.dbusmenu - interface registered on DBus. - - - - - - - - - - - - - The DBus service name and object path where the appmenu interface is present - The object should be registered on the session bus before sending this request. - If not applicable, clients should remove this object. - - - - Set or update the service name and object path. - Strings should be formatted in Latin-1 matching the relevant DBus specifications. - - - - - - - - - diff --git a/crates/gpui_linux/src/linux/wayland/client.rs b/crates/gpui_linux/src/linux/wayland/client.rs index 1ec42cb45dc..cc8e51027c6 100644 --- a/crates/gpui_linux/src/linux/wayland/client.rs +++ b/crates/gpui_linux/src/linux/wayland/client.rs @@ -66,10 +66,10 @@ use wayland_protocols::{ wp::fractional_scale::v1::client::{wp_fractional_scale_manager_v1, wp_fractional_scale_v1}, xdg::dialog::v1::client::xdg_dialog_v1::XdgDialogV1, }; +use wayland_protocols_plasma::appmenu::client::{ + org_kde_kwin_appmenu, org_kde_kwin_appmenu_manager, +}; use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blur_manager}; - -#[cfg(feature = "global-menu")] -use super::appmenu::client::{org_kde_kwin_appmenu, org_kde_kwin_appmenu_manager}; use wayland_protocols_wlr::layer_shell::v1::client::{zwlr_layer_shell_v1, zwlr_layer_surface_v1}; use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1; use xkbcommon::xkb::{self, KEYMAP_COMPILE_NO_FLAGS, Keycode}; @@ -129,7 +129,6 @@ pub struct Globals { pub decoration_manager: Option, pub layer_shell: Option, pub blur_manager: Option, - #[cfg(feature = "global-menu")] pub appmenu_manager: Option, pub text_input_manager: Option, pub gesture_manager: Option, @@ -172,7 +171,6 @@ impl Globals { decoration_manager: globals.bind(&qh, 1..=1, ()).ok(), layer_shell: globals.bind(&qh, 1..=5, ()).ok(), blur_manager: globals.bind(&qh, 1..=1, ()).ok(), - #[cfg(feature = "global-menu")] appmenu_manager: globals.bind(&qh, 1..=2, ()).ok(), text_input_manager: globals.bind(&qh, 1..=1, ()).ok(), gesture_manager: globals.bind(&qh, 1..=3, ()).ok(), @@ -263,11 +261,8 @@ pub(crate) struct WaylandClientState { cursor: Cursor, pending_activation: Option, event_loop: Option>, - #[cfg(feature = "global-menu")] dbus_service_name: Option, - #[cfg(feature = "global-menu")] dbus_menu_thread: Option>, - #[cfg(feature = "global-menu")] appmenu_objects: HashMap, pub common: LinuxCommon, } @@ -407,7 +402,6 @@ impl WaylandClientStatePtr { let client = self.get_client(); let mut state = client.borrow_mut(); - #[cfg(feature = "global-menu")] { if let Some(appmenu) = state.appmenu_objects.remove(surface_id) { appmenu.release(); @@ -440,7 +434,6 @@ impl Drop for WaylandClient { return; } - #[cfg(feature = "global-menu")] { let (dbus_menu_server, dbus_menu_thread) = match self.0.try_borrow_mut() { Ok(mut state) => ( @@ -467,7 +460,6 @@ impl Drop for WaylandClient { let mut state = self.0.borrow_mut(); - #[cfg(feature = "global-menu")] for (_, appmenu) in state.appmenu_objects.drain() { appmenu.release(); } @@ -489,7 +481,6 @@ impl Drop for WaylandClient { } } -#[cfg(feature = "global-menu")] impl crate::linux::dbusmenu::GlobalMenuState for WaylandClientState { fn linux_common(&mut self) -> &mut crate::linux::LinuxCommon { &mut self.common @@ -718,11 +709,8 @@ impl WaylandClient { cursor, pending_activation: None, event_loop: Some(event_loop), - #[cfg(feature = "global-menu")] dbus_service_name: None, - #[cfg(feature = "global-menu")] dbus_menu_thread: None, - #[cfg(feature = "global-menu")] appmenu_objects: HashMap::default(), })); @@ -731,7 +719,6 @@ impl WaylandClient { .unwrap(); // Start the DBusMenu server if the compositor supports global menus. - #[cfg(feature = "global-menu")] { let has_appmenu = state.borrow().globals.appmenu_manager.is_some(); let enabled = match crate::linux::dbusmenu::global_menu_env_override() { @@ -779,11 +766,8 @@ impl WaylandClient { state.borrow_mut().dbus_service_name = Some(service_name.clone()); let object_path = crate::linux::dbusmenu::DBUSMENU_OBJECT_PATH.to_string(); - let dbus_menu_thread = dbus_menu_server.spawn_dbus_menu_thread( - service_name.clone(), - object_path, - None, - ); + let dbus_menu_thread = + dbus_menu_server.spawn_dbus_menu_thread(service_name, object_path, None); if let Some(thread) = dbus_menu_thread { state.borrow_mut().dbus_menu_thread = Some(thread); @@ -891,7 +875,6 @@ impl LinuxClient for WaylandClient { )?; state.windows.insert(surface_id.clone(), window.0.clone()); - #[cfg(feature = "global-menu")] if let (Some(appmenu_manager), Some(service_name)) = ( state.globals.appmenu_manager.as_ref(), state.dbus_service_name.as_ref(), @@ -906,7 +889,7 @@ impl LinuxClient for WaylandClient { let object_path = crate::linux::dbusmenu::DBUSMENU_OBJECT_PATH.to_string(); appmenu.set_address(service_name.clone(), object_path); } - state.appmenu_objects.insert(surface_id.clone(), appmenu); + state.appmenu_objects.insert(surface_id, appmenu); } Ok(Box::new(window)) @@ -1223,11 +1206,9 @@ delegate_noop!(WaylandClientStatePtr: ignore wp_fractional_scale_manager_v1::WpF delegate_noop!(WaylandClientStatePtr: ignore zxdg_decoration_manager_v1::ZxdgDecorationManagerV1); delegate_noop!(WaylandClientStatePtr: ignore zwlr_layer_shell_v1::ZwlrLayerShellV1); delegate_noop!(WaylandClientStatePtr: ignore org_kde_kwin_blur_manager::OrgKdeKwinBlurManager); -#[cfg(feature = "global-menu")] delegate_noop!( WaylandClientStatePtr: ignore org_kde_kwin_appmenu_manager::OrgKdeKwinAppmenuManager ); -#[cfg(feature = "global-menu")] delegate_noop!(WaylandClientStatePtr: ignore org_kde_kwin_appmenu::OrgKdeKwinAppmenu); delegate_noop!(WaylandClientStatePtr: ignore zwp_text_input_manager_v3::ZwpTextInputManagerV3); delegate_noop!(WaylandClientStatePtr: ignore org_kde_kwin_blur::OrgKdeKwinBlur); diff --git a/crates/gpui_linux/src/linux/x11/client.rs b/crates/gpui_linux/src/linux/x11/client.rs index 9f9c532c58b..019be4919f4 100644 --- a/crates/gpui_linux/src/linux/x11/client.rs +++ b/crates/gpui_linux/src/linux/x11/client.rs @@ -213,11 +213,8 @@ pub struct X11ClientState { pointer_device_states: BTreeMap, - #[cfg(feature = "global-menu")] pub(crate) dbus_service_name: Option, - #[cfg(feature = "global-menu")] pub(crate) dbus_unique_name: Option, - #[cfg(feature = "global-menu")] pub(crate) dbus_menu_thread: Option>, pub(crate) common: LinuxCommon, @@ -301,7 +298,6 @@ impl X11ClientStatePtr { } } -#[cfg(feature = "global-menu")] impl crate::linux::dbusmenu::GlobalMenuState for X11ClientState { fn linux_common(&mut self) -> &mut crate::linux::LinuxCommon { &mut self.common @@ -320,7 +316,6 @@ impl Drop for X11Client { return; } - #[cfg(feature = "global-menu")] { let (dbus_menu_server, dbus_menu_thread) = match self.0.try_borrow_mut() { Ok(mut state) => ( @@ -578,11 +573,8 @@ impl X11Client { pointer_device_states, - #[cfg(feature = "global-menu")] dbus_service_name: None, - #[cfg(feature = "global-menu")] dbus_unique_name: None, - #[cfg(feature = "global-menu")] dbus_menu_thread: None, clipboard, @@ -590,7 +582,6 @@ impl X11Client { xdnd_state: Xdnd::default(), })); - #[cfg(feature = "global-menu")] { let root = xcb_connection.setup().roots[x_root_index].root; let has_appmenu = x11_global_menu_supported(&xcb_connection, &atoms, root); @@ -649,9 +640,9 @@ impl X11Client { let object_path = crate::linux::dbusmenu::DBUSMENU_OBJECT_PATH.to_string(); let dbus_menu_thread = dbus_menu_server.spawn_dbus_menu_thread( - service_name.clone(), + service_name, object_path, - Some(unique_name_tx.clone()), + Some(unique_name_tx), ); if let Some(thread) = dbus_menu_thread { @@ -1682,7 +1673,6 @@ impl LinuxClient for X11Client { ) .log_err(); - #[cfg(feature = "global-menu")] if let Some(service_name) = x11_appmenu_service_name(&state) { let object_path = crate::linux::dbusmenu::DBUSMENU_OBJECT_PATH.to_string(); @@ -2251,7 +2241,6 @@ fn check_gtk_frame_extents_supported( supported_atom_ids.contains(&atoms._GTK_FRAME_EXTENTS) } -#[cfg(feature = "global-menu")] fn x11_global_menu_supported( xcb_connection: &XCBConnection, atoms: &XcbAtoms, @@ -2282,7 +2271,6 @@ fn x11_global_menu_supported( || supported_atom_ids.contains(&atoms._KDE_NET_WM_APPMENU_OBJECT_PATH) } -#[cfg(feature = "global-menu")] fn x11_appmenu_service_name(state: &X11ClientState) -> Option { state .dbus_unique_name @@ -2290,7 +2278,6 @@ fn x11_appmenu_service_name(state: &X11ClientState) -> Option { .or_else(|| state.dbus_service_name.clone()) } -#[cfg(feature = "global-menu")] fn set_x11_appmenu_properties( xcb_connection: &XCBConnection, atoms: &XcbAtoms, diff --git a/crates/gpui_platform/Cargo.toml b/crates/gpui_platform/Cargo.toml index 952589cc654..cfb47b1851b 100644 --- a/crates/gpui_platform/Cargo.toml +++ b/crates/gpui_platform/Cargo.toml @@ -19,7 +19,6 @@ screen-capture = ["gpui/screen-capture", "gpui_macos/screen-capture", "gpui_wind runtime_shaders = ["gpui_macos/runtime_shaders"] wayland = ["gpui_linux/wayland"] x11 = ["gpui_linux/x11"] -global-menu = ["gpui_linux/global-menu"] [dependencies] gpui.workspace = true diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 2668c595977..b38e5a774d7 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -13,7 +13,6 @@ workspace = true [features] tracy = ["ztracing/tracy"] -global-menu = ["gpui_platform/global-menu"] test-support = [ "gpui/test-support", "gpui_platform/screen-capture", From 21f443959fad31b2969c44fbc7609a085f6e993f Mon Sep 17 00:00:00 2001 From: Nihal <121309701+nihalxkumar@users.noreply.github.com> Date: Wed, 29 Apr 2026 16:13:54 +0530 Subject: [PATCH 16/16] gpui_linux: Probe AppMenu registrar for X11 global menu detection KDE/X11 setups don't always advertise the appmenu atoms in `_NET_SUPPORTED` even when the registrar daemon is running. --- crates/gpui_linux/src/linux/dbusmenu.rs | 37 +++++++++++++++++++++++ crates/gpui_linux/src/linux/x11/client.rs | 35 ++------------------- 2 files changed, 39 insertions(+), 33 deletions(-) diff --git a/crates/gpui_linux/src/linux/dbusmenu.rs b/crates/gpui_linux/src/linux/dbusmenu.rs index 981698d9487..cf26f0db126 100644 --- a/crates/gpui_linux/src/linux/dbusmenu.rs +++ b/crates/gpui_linux/src/linux/dbusmenu.rs @@ -1065,6 +1065,43 @@ pub fn global_menu_env_override() -> Option { } } +/// Probe the session bus for the global menu registrar. +/// +/// Used by X11 (which has no compositor-level appmenu protocol) to decide whether +/// to publish the `_KDE_NET_WM_APPMENU_*` window properties. Most KDE/X11 setups +/// do not advertise the appmenu atoms in `_NET_SUPPORTED` even when the registrar +/// is running, so checking the registrar's bus name is more reliable. +pub fn appmenu_registrar_present() -> bool { + use zbus::blocking; + use zbus::names::BusName; + + let connection = match blocking::Connection::session() { + Ok(connection) => connection, + Err(error) => { + log::debug!("Failed to open session bus while probing appmenu registrar: {error}"); + return false; + } + }; + + let proxy = match blocking::fdo::DBusProxy::new(&connection) { + Ok(proxy) => proxy, + Err(error) => { + log::debug!("Failed to build DBus proxy while probing appmenu registrar: {error}"); + return false; + } + }; + + let name = match BusName::try_from("com.canonical.AppMenu.Registrar") { + Ok(name) => name, + Err(error) => { + log::debug!("Failed to build BusName for appmenu registrar: {error}"); + return false; + } + }; + + proxy.name_has_owner(name).unwrap_or(false) +} + fn owned_bool(value: bool) -> Option { Value::Bool(value).try_into().ok() } diff --git a/crates/gpui_linux/src/linux/x11/client.rs b/crates/gpui_linux/src/linux/x11/client.rs index a0b5bca5fb6..76d666c730d 100644 --- a/crates/gpui_linux/src/linux/x11/client.rs +++ b/crates/gpui_linux/src/linux/x11/client.rs @@ -571,7 +571,7 @@ impl X11Client { scale_factor, xkb_context, - xcb_connection: xcb_connection.clone(), + xcb_connection: xcb_connection, xkb_device_id, client_side_decorations_supported, x_root_index, @@ -608,8 +608,7 @@ impl X11Client { })); { - let root = xcb_connection.setup().roots[x_root_index].root; - let has_appmenu = x11_global_menu_supported(&xcb_connection, &atoms, root); + let has_appmenu = crate::linux::dbusmenu::appmenu_registrar_present(); let enabled = match crate::linux::dbusmenu::global_menu_env_override() { Some(true) => true, Some(false) => false, @@ -2324,36 +2323,6 @@ fn check_gtk_frame_extents_supported( supported_atom_ids.contains(&atoms._GTK_FRAME_EXTENTS) } -fn x11_global_menu_supported( - xcb_connection: &XCBConnection, - atoms: &XcbAtoms, - root: xproto::Window, -) -> bool { - let Some(supported_atoms) = get_reply( - || "Failed to get _NET_SUPPORTED", - xcb_connection.get_property( - false, - root, - atoms._NET_SUPPORTED, - xproto::AtomEnum::ATOM, - 0, - 1024, - ), - ) - .log_with_level(Level::Debug) else { - return false; - }; - - let supported_atom_ids: Vec = supported_atoms - .value - .chunks_exact(4) - .filter_map(|chunk| chunk.try_into().ok().map(u32::from_ne_bytes)) - .collect(); - - supported_atom_ids.contains(&atoms._KDE_NET_WM_APPMENU_SERVICE_NAME) - || supported_atom_ids.contains(&atoms._KDE_NET_WM_APPMENU_OBJECT_PATH) -} - fn x11_appmenu_service_name(state: &X11ClientState) -> Option { state .dbus_unique_name