Set appid/wmclass for zed window (#10909)

Jakob Hellermann created

fixes https://github.com/zed-industries/zed/issues/9132

By setting the app id, window managers like `sway` can apply custom
configuration like `for_window [app_id="zed"] floating enable`.
Tested using `wlprop`/`hyprctl activewindow` for wayland, `xprop` for
x11.


Release Notes:

- Zed now sets the window app id / class, which can be used e.g. in
window managers like `sway`/`i3` to define custom rules

Change summary

crates/collab_ui/src/collab_ui.rs                |  1 +
crates/gpui/examples/window_positioning.rs       |  1 +
crates/gpui/src/platform.rs                      |  5 +++++
crates/gpui/src/platform/linux/wayland/window.rs |  4 ++++
crates/gpui/src/platform/linux/x11/window.rs     | 15 +++++++++++++++
crates/gpui/src/platform/mac/window.rs           |  2 ++
crates/gpui/src/platform/test/window.rs          |  2 ++
crates/gpui/src/platform/windows/window.rs       |  2 ++
crates/gpui/src/window.rs                        | 12 +++++++++++-
crates/semantic_index/src/chunking.rs            |  3 ++-
crates/zed/src/zed.rs                            |  1 +
11 files changed, 46 insertions(+), 2 deletions(-)

Detailed changes

crates/collab_ui/src/collab_ui.rs 🔗

@@ -122,5 +122,6 @@ fn notification_window_options(
         display_id: Some(screen.id()),
         fullscreen: false,
         window_background: WindowBackgroundAppearance::default(),
+        app_id: Some("dev.zed.Zed".to_owned()),
     }
 }

crates/gpui/src/platform.rs 🔗

@@ -209,6 +209,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
     fn activate(&self);
     fn is_active(&self) -> bool;
     fn set_title(&mut self, title: &str);
+    fn set_app_id(&mut self, app_id: &str);
     fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance);
     fn set_edited(&mut self, edited: bool);
     fn show_character_palette(&self);
@@ -557,6 +558,9 @@ pub struct WindowOptions {
 
     /// The appearance of the window background.
     pub window_background: WindowBackgroundAppearance,
+
+    /// Application identifier of the window. Can by used by desktop environments to group applications together.
+    pub app_id: Option<String>,
 }
 
 /// The variables that can be configured when creating a new window
@@ -599,6 +603,7 @@ impl Default for WindowOptions {
             display_id: None,
             fullscreen: false,
             window_background: WindowBackgroundAppearance::default(),
+            app_id: None,
         }
     }
 }

crates/gpui/src/platform/linux/wayland/window.rs 🔗

@@ -609,6 +609,10 @@ impl PlatformWindow for WaylandWindow {
         self.borrow_mut().toplevel.set_title(title.to_string());
     }
 
+    fn set_app_id(&mut self, app_id: &str) {
+        self.borrow_mut().toplevel.set_app_id(app_id.to_owned());
+    }
+
     fn set_background_appearance(&mut self, _background_appearance: WindowBackgroundAppearance) {
         // todo(linux)
     }

crates/gpui/src/platform/linux/x11/window.rs 🔗

@@ -494,6 +494,21 @@ impl PlatformWindow for X11Window {
             .unwrap();
     }
 
+    fn set_app_id(&mut self, app_id: &str) {
+        let mut data = Vec::with_capacity(app_id.len() * 2 + 1);
+        data.extend(app_id.bytes()); // instance https://unix.stackexchange.com/a/494170
+        data.push(b'\0');
+        data.extend(app_id.bytes()); // class
+
+        self.0.xcb_connection.change_property8(
+            xproto::PropMode::REPLACE,
+            self.0.x_window,
+            xproto::AtomEnum::WM_CLASS,
+            xproto::AtomEnum::STRING,
+            &data,
+        );
+    }
+
     // todo(linux)
     fn set_edited(&mut self, edited: bool) {}
 

crates/gpui/src/platform/mac/window.rs 🔗

@@ -982,6 +982,8 @@ impl PlatformWindow for MacWindow {
         }
     }
 
+    fn set_app_id(&mut self, _app_id: &str) {}
+
     fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) {
         let this = self.0.as_ref().lock();
         let blur_radius = if background_appearance == WindowBackgroundAppearance::Blurred {

crates/gpui/src/platform/test/window.rs 🔗

@@ -190,6 +190,8 @@ impl PlatformWindow for TestWindow {
         self.0.lock().title = Some(title.to_owned());
     }
 
+    fn set_app_id(&mut self, _app_id: &str) {}
+
     fn set_background_appearance(&mut self, _background: WindowBackgroundAppearance) {
         unimplemented!()
     }

crates/gpui/src/platform/windows/window.rs 🔗

@@ -1514,6 +1514,8 @@ impl PlatformWindow for WindowsWindow {
             .ok();
     }
 
+    fn set_app_id(&mut self, _app_id: &str) {}
+
     fn set_background_appearance(&mut self, _background_appearance: WindowBackgroundAppearance) {
         // todo(windows)
     }

crates/gpui/src/window.rs 🔗

@@ -600,10 +600,11 @@ impl Window {
             display_id,
             fullscreen,
             window_background,
+            app_id,
         } = options;
 
         let bounds = bounds.unwrap_or_else(|| default_bounds(display_id, cx));
-        let platform_window = cx.platform.open_window(
+        let mut platform_window = cx.platform.open_window(
             handle,
             WindowParams {
                 bounds,
@@ -735,6 +736,10 @@ impl Window {
             })
         });
 
+        if let Some(app_id) = app_id {
+            platform_window.set_app_id(&app_id);
+        }
+
         Window {
             handle,
             removed: false,
@@ -1125,6 +1130,11 @@ impl<'a> WindowContext<'a> {
         self.window.platform_window.set_title(title);
     }
 
+    /// Sets the application identifier.
+    pub fn set_app_id(&mut self, app_id: &str) {
+        self.window.platform_window.set_app_id(app_id);
+    }
+
     /// Sets the window background appearance.
     pub fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) {
         self.window

crates/semantic_index/src/chunking.rs 🔗

@@ -188,6 +188,7 @@ mod tests {
                         kind: WindowKind::PopUp,
                         is_movable: false,
                         fullscreen: false,
+                        app_id: None,
                     }
                 };
 
@@ -239,7 +240,7 @@ mod tests {
         // The break between chunks is right before the "Specify the display_id" comment
 
         assert_eq!(chunks[1].range.start, 1498);
-        assert_eq!(chunks[1].range.end, 2396);
+        assert_eq!(chunks[1].range.end, 2434);
     }
 
     #[test]

crates/zed/src/zed.rs 🔗

@@ -102,6 +102,7 @@ pub fn build_window_options(display_uuid: Option<Uuid>, cx: &mut AppContext) ->
         display_id: display.map(|display| display.id()),
         fullscreen: false,
         window_background: cx.theme().window_background_appearance(),
+        app_id: Some("dev.zed.Zed".to_owned()),
     }
 }