From e3cf42c2eaaf12b7763418edcf18d7c76caf6d45 Mon Sep 17 00:00:00 2001 From: Baptiste Parmantier Date: Sat, 22 Aug 2026 10:50:14 +0200 Subject: [PATCH] fix(app): raise the window a repository already has, instead of a second one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The handoff routed the second invocation's path to the live process correctly and that process opened another window for it every time. `add_or_activate` dedupes the persisted project list, which has nothing to do with what is on screen. A map from canonical root to `WindowHandle`, consulted before opening. Both sides already agree on the key: `resolve_repository_root` canonicalises before the path goes into the socket, which is what makes `/tmp/x` and `/private/tmp/x` one entry rather than two. No window-close hook. `WindowHandle::update` fails once its window is gone, so raising doubles as the liveness check and a closed window's entry is dropped the next time that repository is asked for. Watching for closes to prune eagerly would buy back one map entry per repository opened in this process — a leak worth less than the code that plugs it, and a close hook that missed a case would be the worse failure: an entry that outlives its window and stops the repository ever opening again. Verified by driving the real handoff: same repository raises, a different one opens, the first raises again. The stale-handle path is not exercised, since closing a window from the command line is not something this can do. --- crates/gitr/src/main.rs | 50 +++++++++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/crates/gitr/src/main.rs b/crates/gitr/src/main.rs index 79810a3..f385ec6 100644 --- a/crates/gitr/src/main.rs +++ b/crates/gitr/src/main.rs @@ -51,6 +51,7 @@ mod instance; +use std::collections::HashMap; use std::env; use std::io; use std::os::unix::process::CommandExt as _; @@ -58,7 +59,7 @@ use std::path::{Path, PathBuf}; use std::process::{Command, ExitCode, Stdio}; use domain::RepositoryError; -use gpui::{App, AppContext, AsyncApp, KeyBinding, QuitMode}; +use gpui::{App, AppContext, AsyncApp, KeyBinding, QuitMode, WindowHandle}; use gpui_component::{Root, TitleBar}; use ui::Workspace; use ui::actions::{CloseWindow, Quit}; @@ -137,19 +138,27 @@ fn main() -> ExitCode { let projects = projects.clone(); let requests = requests.clone(); + let first_root = opened_root.clone(); cx.spawn(async move |cx| { - open_window(projects, cx).expect("gitr cannot run without a window"); + let mut windows: HashMap> = HashMap::new(); + let window = open_window(projects, cx).expect("gitr cannot run without a window"); + if !first_root.as_os_str().is_empty() { + windows.insert(first_root, window); + } cx.update(|cx| cx.activate(true)); while let Ok(root) = requests.recv().await { - if !root.as_os_str().is_empty() { + if !root.as_os_str().is_empty() && !raise(&mut windows, &root, cx) { let mut projects = project_list_at_startup(); - projects.add_or_activate(Project::local(root)); + projects.add_or_activate(Project::local(root.clone())); if let Err(error) = persistence::save_project_list(&projects) { eprintln!("gitr: failed to save project list: {error:#}"); } - if open_window(projects, cx).is_err() { - continue; + match open_window(projects, cx) { + Ok(window) => { + windows.insert(root, window); + } + Err(_) => continue, } } cx.update(|cx| cx.activate(true)); @@ -193,13 +202,36 @@ fn set_dock_icon() { unsafe { NSApplication::sharedApplication(main_thread).setApplicationIconImage(Some(&image)) }; } -fn open_window(projects: ProjectList, cx: &mut AsyncApp) -> anyhow::Result<()> { - cx.open_window(TitleBar::window_options(), |window, cx| { +fn open_window(projects: ProjectList, cx: &mut AsyncApp) -> anyhow::Result> { + let window = cx.open_window(TitleBar::window_options(), |window, cx| { let workspace = cx.new(|cx| Workspace::new(projects, window, cx)); Workspace::register_menu_actions(&workspace, window, cx); cx.new(|cx| Root::new(workspace, window, cx)) })?; - Ok(()) + Ok(window) +} + +/// Brings the window already showing `root` to the front, reporting whether there was one. +/// +/// A handle whose window has since closed is dropped rather than trusted: the map is only +/// ever read here, so an entry that has gone stale costs nothing until the same repository +/// is asked for again, and that request repairs it. Watching for window closes to prune +/// eagerly would buy one `HashMap` entry per repository opened in this process. +fn raise( + windows: &mut HashMap>, + root: &Path, + cx: &mut AsyncApp, +) -> bool { + let Some(window) = windows.get(root) else { + return false; + }; + let raised = window + .update(cx, |_, window, _| window.activate_window()) + .is_ok(); + if !raised { + windows.remove(root); + } + raised } /// Set on the process that opens the window, so it runs the event loop instead of