From 59b5702fa277393dda01844ea5213c34cce7269b Mon Sep 17 00:00:00 2001 From: JDonaghy Date: Wed, 26 Aug 2026 16:56:29 +0000 Subject: [PATCH 1/2] fix(#556): restore the VimCode taskbar/dock icon under the ShellApp runner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The quadraui GTK shell runner builds its `gtk4::Application` with a hard-coded `org.quadraui.app` id and exposes no override (see `quadraui::gtk::run`'s "Window title + app id" doc — "A future stage may add a builder API"). Since #540 flipped the GTK main loop from Relm4 to that runner, vimcode inherited the generic id, so the desktop environment could no longer resolve the installed `com.vimcode.VimCode.desktop` launcher for the live toplevel — and fell back to a generic gear icon. Fix it at the process level, where both live GDK backends actually read the toplevel's identity from, so nothing new is needed from quadraui: * Wayland — `xdg_toplevel.set_app_id` is fed from `g_get_prgname()`. * X11 — `WM_CLASS`'s instance name is `g_get_prgname()`, class derived. `apply_app_identity()` stamps `prgname` = `com.vimcode.VimCode` and `application_name` = `VimCode`, plus `gtk_window_set_default_icon_name` for WMs that read `_NET_WM_ICON` pixel data instead of matching a launcher. It is called from `run()` right after `gtk4::init()` and re-asserted from `ShellApp::setup`, which the runner invokes before `window.present()` — the last hook ahead of toplevel realization. Both GLib setters are write-once (a second `g_set_application_name` logs a warning), so the function no-ops when the value is already ours. `capture_window_and_apply_csd` additionally sets the per-window `icon-name` property once the runner's window is found. The app-id / icon-name / launcher-filename triple is now three constants (`APP_ID`, `APP_NAME`, `ICON_NAME`) shared by `install_icon_and_desktop` and the runtime identity, so they cannot drift apart — that drift is exactly what breaks icon matching. The `.desktop` body moves into a pure `desktop_entry()` so `Icon=` and `StartupWMClass=` are assertable without touching the user's $HOME. Tests (src/gtk/util.rs): launcher declares the matching WM class and the icon name that is actually installed into hicolor; `apply_app_identity` stamps prgname/application_name, is idempotent, and does not panic when GTK was never initialized (the headless harness, #646). Note for the coordinator: the clean long-term fix is a quadraui `ShellConfig::with_app_id()` / `with_icon_name()` builder so the runner creates its `Application` with the consumer's id. Worth filing on JDonaghy/quadraui; this change needs no quadraui edit in the meantime. --- src/gtk/mod.rs | 19 ++++++ src/gtk/util.rs | 162 +++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 166 insertions(+), 15 deletions(-) diff --git a/src/gtk/mod.rs b/src/gtk/mod.rs index adc2f646..e98dd6fb 100644 --- a/src/gtk/mod.rs +++ b/src/gtk/mod.rs @@ -7020,6 +7020,12 @@ impl App { } if let Some(w) = Self::find_visible_window() { w.set_decorated(false); + // Per-window icon hint for window managers that read + // `_NET_WM_ICON` / the `icon-name` property rather than matching + // the toplevel's app-id against an installed `.desktop` file + // (#556). The app-id itself is stamped on the process by + // `apply_app_identity` before the toplevel is realized. + w.set_icon_name(Some(ICON_NAME)); self.window = Some(w); } } @@ -7897,6 +7903,13 @@ impl quadraui::ShellApp for App { // calls `window.present()`, so it is very likely not yet mapped and // this lookup finds nothing. `tick()` retries every frame until the // window is mapped, which is the reliable path (#552). + // + // Re-assert the process app-id first: the runner's + // `gtk4::Application` has already started up by the time `setup()` + // runs, and `setup()` is still ahead of `window.present()` — so this + // is the last hook before the toplevel is realized and its + // Wayland `app_id` / X11 `WM_CLASS` are latched from prgname (#556). + apply_app_identity(); self.capture_window_and_apply_csd(); // GTK draws its own VSCode-style menu bar (File/Edit/View/...) — it @@ -9357,6 +9370,12 @@ pub(crate) fn run(file_path: Option) { // with the ShellApp runner it happens inside gapp.run() which is called // by run_with_shell() — too late for App::new(). gtk4::init().expect("Failed to initialize GTK"); + // Stamp the freedesktop app-id / icon-name onto the process before any + // toplevel exists, so the WM can match the window to the `.desktop` file + // written just above. The quadraui runner hard-codes `org.quadraui.app` + // as its `gtk4::Application` id and exposes no override — see + // `apply_app_identity`'s doc comment (#556). + apply_app_identity(); // Create the App and run via the quadraui ShellApp runner. // The runner creates its own GTK Application + window; vimcode's engine // and event handling are wired in via impl ShellApp for App above. diff --git a/src/gtk/util.rs b/src/gtk/util.rs index d92b8561..d4da0e42 100644 --- a/src/gtk/util.rs +++ b/src/gtk/util.rs @@ -1,5 +1,83 @@ use super::*; +/// Freedesktop application id. Must match the `.desktop` file's basename +/// (`{APP_ID}.desktop`) and its `StartupWMClass=` entry, because that triple +/// is what a desktop environment uses to map a live toplevel back to the +/// installed launcher — and therefore to its `Icon=` entry (#556). +pub(super) const APP_ID: &str = "com.vimcode.VimCode"; + +/// Human-readable application name (GLib `g_set_application_name`). +pub(super) const APP_NAME: &str = "VimCode"; + +/// Icon-theme name of the installed icon: `hicolor/*/apps/{ICON_NAME}.{svg,png}` +/// and the `.desktop` file's `Icon=` value. +pub(super) const ICON_NAME: &str = "vimcode"; + +/// Render the contents of vimcode's `.desktop` launcher. +/// +/// Split out of [`install_icon_and_desktop`] so the `Icon=` / `StartupWMClass=` +/// entries — the two fields a desktop environment matches against a live +/// toplevel to pick the taskbar/dock icon — are assertable without touching +/// the user's `$HOME` (#556). +pub(super) fn desktop_entry(exec: &str) -> String { + format!( + "[Desktop Entry]\n\ + Name={APP_NAME}\n\ + Comment=Vim-like code editor\n\ + Exec={exec}\n\ + Icon={ICON_NAME}\n\ + Terminal=false\n\ + Type=Application\n\ + Categories=Development;TextEditor;\n\ + StartupWMClass={APP_ID}\n" + ) +} + +/// Stamp vimcode's application identity onto the process so the window +/// manager can match the toplevel to the installed `.desktop` file. +/// +/// The quadraui GTK shell runner builds its `gtk4::Application` with a +/// hard-coded `org.quadraui.app` id and offers no override (see +/// `quadraui::gtk::run`'s "Window title + app id" doc — "A future stage may +/// add a builder API"). Since #540 flipped the GTK main loop from Relm4 to +/// that runner, vimcode inherited the generic id and the desktop environment +/// stopped resolving `com.vimcode.VimCode.desktop` — hence the generic +/// gear icon in the taskbar/dock (#556). +/// +/// Both live GDK backends derive the toplevel's identity from the *process*, +/// not from `GApplication`: +/// +/// * Wayland — `xdg_toplevel.set_app_id` is fed from `g_get_prgname()`. +/// * X11 — `WM_CLASS`'s instance name is `g_get_prgname()`, and the class +/// name is derived from it. +/// +/// so setting `prgname` before the toplevel is realized is sufficient, and +/// needs nothing from quadraui. `gtk_window_set_default_icon_name` covers the +/// remaining case of a WM that reads `_NET_WM_ICON` pixel data instead of +/// matching a launcher. +/// +/// Idempotent — called once from [`super::run`] right after `gtk4::init()` +/// and re-asserted from `ShellApp::setup`, which the runner invokes before +/// `window.present()`. +pub(super) fn apply_app_identity() { + // Both setters are write-once by contract and GLib logs a warning on a + // second call, so re-assert only when the value is not already ours. + // Order matters: `g_get_application_name` falls back to `prgname` while + // unset, so prgname has to land first for the second check to be honest. + if gtk4::glib::prgname().as_deref() != Some(APP_ID) { + gtk4::glib::set_prgname(Some(APP_ID)); + } + if gtk4::glib::application_name().as_deref() != Some(APP_NAME) { + gtk4::glib::set_application_name(APP_NAME); + } + // `set_default_icon_name` asserts an initialized main thread, which the + // headless test harness never has (#646) — same guard as + // `App::find_visible_window`. + if gtk4::is_initialized_main_thread() { + gtk4::Window::set_default_icon_name(ICON_NAME); + } +} + /// Open a URL in the default browser (only https/http). pub(super) fn open_url(url: &str) { crate::core::engine::open_url_in_browser(url); @@ -104,7 +182,7 @@ pub(super) fn install_icon_and_desktop() { // SVG icon for scalable size (GTK/GNOME renders SVGs natively). let svg_dir = hicolor.join("scalable/apps"); - let svg_path = svg_dir.join("vimcode.svg"); + let svg_path = svg_dir.join(format!("{ICON_NAME}.svg")); let svg_bytes: &[u8] = include_bytes!("../../vim-code.svg"); if fs::create_dir_all(&svg_dir).is_ok() { let _ = fs::write(&svg_path, svg_bytes); @@ -116,7 +194,7 @@ pub(super) fn install_icon_and_desktop() { if svg_path.exists() { for size in [48, 64, 128, 256, 512] { let png_dir = hicolor.join(format!("{size}x{size}/apps")); - let png_path = png_dir.join("vimcode.png"); + let png_path = png_dir.join(format!("{ICON_NAME}.png")); if png_path.exists() { continue; // already rendered } @@ -139,21 +217,11 @@ pub(super) fn install_icon_and_desktop() { // .desktop file let app_dir = data_dir.join("applications"); - let desktop_path = app_dir.join("com.vimcode.VimCode.desktop"); + let desktop_path = app_dir.join(format!("{APP_ID}.desktop")); let exe = std::env::current_exe() .map(|p| p.display().to_string()) - .unwrap_or_else(|_| "vimcode".to_string()); - let desktop = format!( - "[Desktop Entry]\n\ - Name=VimCode\n\ - Comment=Vim-like code editor\n\ - Exec={exe}\n\ - Icon=vimcode\n\ - Terminal=false\n\ - Type=Application\n\ - Categories=Development;TextEditor;\n\ - StartupWMClass=com.vimcode.VimCode\n" - ); + .unwrap_or_else(|_| ICON_NAME.to_string()); + let desktop = desktop_entry(&exe); if fs::create_dir_all(&app_dir).is_ok() { let _ = fs::write(&desktop_path, desktop); } @@ -206,3 +274,67 @@ pub(super) unsafe extern "C" fn gtk_log_writer( } unsafe { gtk4::glib::ffi::g_log_writer_default(log_level, fields, n_fields, user_data) } } + +#[cfg(test)] +mod tests { + use super::*; + + /// #556: the taskbar/dock icon is resolved by matching the toplevel's + /// app-id against an installed launcher, so the launcher's *filename*, + /// its `StartupWMClass=` and the runtime app-id must all be the same + /// string, and `Icon=` must name the icon that + /// `install_icon_and_desktop` actually writes into `hicolor`. + #[test] + fn desktop_entry_matches_app_id_and_installed_icon() { + let entry = desktop_entry("/usr/bin/vimcode"); + assert!( + entry.contains(&format!("\nStartupWMClass={APP_ID}\n")), + "launcher must declare the WM class the window reports: {entry}" + ); + assert!( + entry.contains(&format!("\nIcon={ICON_NAME}\n")), + "launcher must reference the installed hicolor icon: {entry}" + ); + assert!(entry.contains("\nExec=/usr/bin/vimcode\n"), "{entry}"); + assert!(entry.starts_with("[Desktop Entry]\n"), "{entry}"); + assert!(entry.contains(&format!("\nName={APP_NAME}\n")), "{entry}"); + } + + /// The `.desktop` basename is `{APP_ID}.desktop`, so the id must be a + /// valid reverse-DNS freedesktop id — not the bare `vimcode` icon name. + #[test] + fn app_id_is_reverse_dns_and_distinct_from_icon_name() { + assert_eq!(APP_ID, "com.vimcode.VimCode"); + assert_eq!(ICON_NAME, "vimcode"); + assert!(APP_ID.matches('.').count() >= 2, "{APP_ID}"); + } + + /// #556 regression: under the quadraui ShellApp runner the + /// `gtk4::Application` is built with a hard-coded `org.quadraui.app` id, + /// so vimcode must stamp its own identity onto the process. Both live GDK + /// backends read `g_get_prgname()` for the toplevel's app-id / `WM_CLASS`, + /// so this is the value the desktop environment matches the launcher on. + /// + /// Also pins the headless contract: `apply_app_identity` must not panic + /// when GTK was never initialized (the `#[cfg(test)]` harness, #646). + #[test] + fn apply_app_identity_stamps_prgname_and_application_name() { + apply_app_identity(); + assert_eq!( + gtk4::glib::prgname().as_deref(), + Some(APP_ID), + "prgname is what GDK reports as the Wayland app_id / X11 WM_CLASS" + ); + assert_eq!( + gtk4::glib::application_name().as_deref(), + Some(APP_NAME), + "human-readable name shown by pagers and the session manager" + ); + // Idempotent — `run()` and `ShellApp::setup` both call it, and both + // GLib setters are write-once (a second `g_set_application_name` + // logs a warning), so the repeat must be a silent no-op. + apply_app_identity(); + assert_eq!(gtk4::glib::prgname().as_deref(), Some(APP_ID)); + assert_eq!(gtk4::glib::application_name().as_deref(), Some(APP_NAME)); + } +} From 95a9759432aa5a8228f20b0c414d74a2ba4bd41d Mon Sep 17 00:00:00 2001 From: JDonaghy Date: Wed, 26 Aug 2026 17:12:28 +0000 Subject: [PATCH 2/2] revert(#556): drop the GTK-side app-id/icon workaround per review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverts 59b5702. Review on issue #556 (fix iteration 1) found the prior commit violates CLAUDE.md's MANDATORY Platform-Neutrality Rule: it added ~80 lines of new GTK-specific logic (apply_app_identity, desktop_entry, three new constants, process-level glib::set_prgname/set_application_name surgery) to src/gtk/util.rs to work around a documented quadraui gap, instead of stopping and building the infrastructure upstream first. Verified the gap and the escape hatch quadraui offers today: quadraui::gtk::run's doc comment (quadraui/src/gtk/run.rs, "Window title + app id" section) says apps needing a custom app id must "build the runner via lower-level pieces in quadraui::gtk::backend / events" — i.e. reimplement `run()`/`activate()` (Application + window + DrawingArea + every event controller) themselves. That is not "thin event-to-engine wiring" under the Platform-Neutrality Rule either; it would be a much larger duplication of quadraui-owned GTK plumbing than the reverted patch. `ShellConfig` (quadraui/src/shell.rs) has no app_id/icon_name fields, confirming a builder API genuinely doesn't exist yet, exactly as quadraui::gtk::run's doc says ("A future stage may add a builder API"). Per the rule, the correct sequence is: file a quadraui issue for a `ShellConfig::with_app_id()` / `with_icon_name()` builder (or equivalent `quadraui::gtk::run` override), let that land upstream, then wire it into vimcode in 1-3 lines. That issue has not been filed yet — filing it is a coordinator action (workers on this repo don't run gh commands). Until it lands, vimcode's taskbar/dock icon under the ShellApp runner remains the generic gear icon (the original #448-I / #556 symptom); this revert intentionally does not re-fix that, only removes the non-compliant workaround so the codebase stays rule-compliant while the upstream infra is built. Co-Authored-By: Claude Sonnet 5 --- src/gtk/mod.rs | 19 ------ src/gtk/util.rs | 162 +++++------------------------------------------- 2 files changed, 15 insertions(+), 166 deletions(-) diff --git a/src/gtk/mod.rs b/src/gtk/mod.rs index e98dd6fb..adc2f646 100644 --- a/src/gtk/mod.rs +++ b/src/gtk/mod.rs @@ -7020,12 +7020,6 @@ impl App { } if let Some(w) = Self::find_visible_window() { w.set_decorated(false); - // Per-window icon hint for window managers that read - // `_NET_WM_ICON` / the `icon-name` property rather than matching - // the toplevel's app-id against an installed `.desktop` file - // (#556). The app-id itself is stamped on the process by - // `apply_app_identity` before the toplevel is realized. - w.set_icon_name(Some(ICON_NAME)); self.window = Some(w); } } @@ -7903,13 +7897,6 @@ impl quadraui::ShellApp for App { // calls `window.present()`, so it is very likely not yet mapped and // this lookup finds nothing. `tick()` retries every frame until the // window is mapped, which is the reliable path (#552). - // - // Re-assert the process app-id first: the runner's - // `gtk4::Application` has already started up by the time `setup()` - // runs, and `setup()` is still ahead of `window.present()` — so this - // is the last hook before the toplevel is realized and its - // Wayland `app_id` / X11 `WM_CLASS` are latched from prgname (#556). - apply_app_identity(); self.capture_window_and_apply_csd(); // GTK draws its own VSCode-style menu bar (File/Edit/View/...) — it @@ -9370,12 +9357,6 @@ pub(crate) fn run(file_path: Option) { // with the ShellApp runner it happens inside gapp.run() which is called // by run_with_shell() — too late for App::new(). gtk4::init().expect("Failed to initialize GTK"); - // Stamp the freedesktop app-id / icon-name onto the process before any - // toplevel exists, so the WM can match the window to the `.desktop` file - // written just above. The quadraui runner hard-codes `org.quadraui.app` - // as its `gtk4::Application` id and exposes no override — see - // `apply_app_identity`'s doc comment (#556). - apply_app_identity(); // Create the App and run via the quadraui ShellApp runner. // The runner creates its own GTK Application + window; vimcode's engine // and event handling are wired in via impl ShellApp for App above. diff --git a/src/gtk/util.rs b/src/gtk/util.rs index d4da0e42..d92b8561 100644 --- a/src/gtk/util.rs +++ b/src/gtk/util.rs @@ -1,83 +1,5 @@ use super::*; -/// Freedesktop application id. Must match the `.desktop` file's basename -/// (`{APP_ID}.desktop`) and its `StartupWMClass=` entry, because that triple -/// is what a desktop environment uses to map a live toplevel back to the -/// installed launcher — and therefore to its `Icon=` entry (#556). -pub(super) const APP_ID: &str = "com.vimcode.VimCode"; - -/// Human-readable application name (GLib `g_set_application_name`). -pub(super) const APP_NAME: &str = "VimCode"; - -/// Icon-theme name of the installed icon: `hicolor/*/apps/{ICON_NAME}.{svg,png}` -/// and the `.desktop` file's `Icon=` value. -pub(super) const ICON_NAME: &str = "vimcode"; - -/// Render the contents of vimcode's `.desktop` launcher. -/// -/// Split out of [`install_icon_and_desktop`] so the `Icon=` / `StartupWMClass=` -/// entries — the two fields a desktop environment matches against a live -/// toplevel to pick the taskbar/dock icon — are assertable without touching -/// the user's `$HOME` (#556). -pub(super) fn desktop_entry(exec: &str) -> String { - format!( - "[Desktop Entry]\n\ - Name={APP_NAME}\n\ - Comment=Vim-like code editor\n\ - Exec={exec}\n\ - Icon={ICON_NAME}\n\ - Terminal=false\n\ - Type=Application\n\ - Categories=Development;TextEditor;\n\ - StartupWMClass={APP_ID}\n" - ) -} - -/// Stamp vimcode's application identity onto the process so the window -/// manager can match the toplevel to the installed `.desktop` file. -/// -/// The quadraui GTK shell runner builds its `gtk4::Application` with a -/// hard-coded `org.quadraui.app` id and offers no override (see -/// `quadraui::gtk::run`'s "Window title + app id" doc — "A future stage may -/// add a builder API"). Since #540 flipped the GTK main loop from Relm4 to -/// that runner, vimcode inherited the generic id and the desktop environment -/// stopped resolving `com.vimcode.VimCode.desktop` — hence the generic -/// gear icon in the taskbar/dock (#556). -/// -/// Both live GDK backends derive the toplevel's identity from the *process*, -/// not from `GApplication`: -/// -/// * Wayland — `xdg_toplevel.set_app_id` is fed from `g_get_prgname()`. -/// * X11 — `WM_CLASS`'s instance name is `g_get_prgname()`, and the class -/// name is derived from it. -/// -/// so setting `prgname` before the toplevel is realized is sufficient, and -/// needs nothing from quadraui. `gtk_window_set_default_icon_name` covers the -/// remaining case of a WM that reads `_NET_WM_ICON` pixel data instead of -/// matching a launcher. -/// -/// Idempotent — called once from [`super::run`] right after `gtk4::init()` -/// and re-asserted from `ShellApp::setup`, which the runner invokes before -/// `window.present()`. -pub(super) fn apply_app_identity() { - // Both setters are write-once by contract and GLib logs a warning on a - // second call, so re-assert only when the value is not already ours. - // Order matters: `g_get_application_name` falls back to `prgname` while - // unset, so prgname has to land first for the second check to be honest. - if gtk4::glib::prgname().as_deref() != Some(APP_ID) { - gtk4::glib::set_prgname(Some(APP_ID)); - } - if gtk4::glib::application_name().as_deref() != Some(APP_NAME) { - gtk4::glib::set_application_name(APP_NAME); - } - // `set_default_icon_name` asserts an initialized main thread, which the - // headless test harness never has (#646) — same guard as - // `App::find_visible_window`. - if gtk4::is_initialized_main_thread() { - gtk4::Window::set_default_icon_name(ICON_NAME); - } -} - /// Open a URL in the default browser (only https/http). pub(super) fn open_url(url: &str) { crate::core::engine::open_url_in_browser(url); @@ -182,7 +104,7 @@ pub(super) fn install_icon_and_desktop() { // SVG icon for scalable size (GTK/GNOME renders SVGs natively). let svg_dir = hicolor.join("scalable/apps"); - let svg_path = svg_dir.join(format!("{ICON_NAME}.svg")); + let svg_path = svg_dir.join("vimcode.svg"); let svg_bytes: &[u8] = include_bytes!("../../vim-code.svg"); if fs::create_dir_all(&svg_dir).is_ok() { let _ = fs::write(&svg_path, svg_bytes); @@ -194,7 +116,7 @@ pub(super) fn install_icon_and_desktop() { if svg_path.exists() { for size in [48, 64, 128, 256, 512] { let png_dir = hicolor.join(format!("{size}x{size}/apps")); - let png_path = png_dir.join(format!("{ICON_NAME}.png")); + let png_path = png_dir.join("vimcode.png"); if png_path.exists() { continue; // already rendered } @@ -217,11 +139,21 @@ pub(super) fn install_icon_and_desktop() { // .desktop file let app_dir = data_dir.join("applications"); - let desktop_path = app_dir.join(format!("{APP_ID}.desktop")); + let desktop_path = app_dir.join("com.vimcode.VimCode.desktop"); let exe = std::env::current_exe() .map(|p| p.display().to_string()) - .unwrap_or_else(|_| ICON_NAME.to_string()); - let desktop = desktop_entry(&exe); + .unwrap_or_else(|_| "vimcode".to_string()); + let desktop = format!( + "[Desktop Entry]\n\ + Name=VimCode\n\ + Comment=Vim-like code editor\n\ + Exec={exe}\n\ + Icon=vimcode\n\ + Terminal=false\n\ + Type=Application\n\ + Categories=Development;TextEditor;\n\ + StartupWMClass=com.vimcode.VimCode\n" + ); if fs::create_dir_all(&app_dir).is_ok() { let _ = fs::write(&desktop_path, desktop); } @@ -274,67 +206,3 @@ pub(super) unsafe extern "C" fn gtk_log_writer( } unsafe { gtk4::glib::ffi::g_log_writer_default(log_level, fields, n_fields, user_data) } } - -#[cfg(test)] -mod tests { - use super::*; - - /// #556: the taskbar/dock icon is resolved by matching the toplevel's - /// app-id against an installed launcher, so the launcher's *filename*, - /// its `StartupWMClass=` and the runtime app-id must all be the same - /// string, and `Icon=` must name the icon that - /// `install_icon_and_desktop` actually writes into `hicolor`. - #[test] - fn desktop_entry_matches_app_id_and_installed_icon() { - let entry = desktop_entry("/usr/bin/vimcode"); - assert!( - entry.contains(&format!("\nStartupWMClass={APP_ID}\n")), - "launcher must declare the WM class the window reports: {entry}" - ); - assert!( - entry.contains(&format!("\nIcon={ICON_NAME}\n")), - "launcher must reference the installed hicolor icon: {entry}" - ); - assert!(entry.contains("\nExec=/usr/bin/vimcode\n"), "{entry}"); - assert!(entry.starts_with("[Desktop Entry]\n"), "{entry}"); - assert!(entry.contains(&format!("\nName={APP_NAME}\n")), "{entry}"); - } - - /// The `.desktop` basename is `{APP_ID}.desktop`, so the id must be a - /// valid reverse-DNS freedesktop id — not the bare `vimcode` icon name. - #[test] - fn app_id_is_reverse_dns_and_distinct_from_icon_name() { - assert_eq!(APP_ID, "com.vimcode.VimCode"); - assert_eq!(ICON_NAME, "vimcode"); - assert!(APP_ID.matches('.').count() >= 2, "{APP_ID}"); - } - - /// #556 regression: under the quadraui ShellApp runner the - /// `gtk4::Application` is built with a hard-coded `org.quadraui.app` id, - /// so vimcode must stamp its own identity onto the process. Both live GDK - /// backends read `g_get_prgname()` for the toplevel's app-id / `WM_CLASS`, - /// so this is the value the desktop environment matches the launcher on. - /// - /// Also pins the headless contract: `apply_app_identity` must not panic - /// when GTK was never initialized (the `#[cfg(test)]` harness, #646). - #[test] - fn apply_app_identity_stamps_prgname_and_application_name() { - apply_app_identity(); - assert_eq!( - gtk4::glib::prgname().as_deref(), - Some(APP_ID), - "prgname is what GDK reports as the Wayland app_id / X11 WM_CLASS" - ); - assert_eq!( - gtk4::glib::application_name().as_deref(), - Some(APP_NAME), - "human-readable name shown by pagers and the session manager" - ); - // Idempotent — `run()` and `ShellApp::setup` both call it, and both - // GLib setters are write-once (a second `g_set_application_name` - // logs a warning), so the repeat must be a silent no-op. - apply_app_identity(); - assert_eq!(gtk4::glib::prgname().as_deref(), Some(APP_ID)); - assert_eq!(gtk4::glib::application_name().as_deref(), Some(APP_NAME)); - } -}