From 65e9df131fd0f53d662b3f98fc8c1a5f0a7193d7 Mon Sep 17 00:00:00 2001 From: "marcus.hinz" Date: Sun, 5 Jul 2026 14:09:30 +0200 Subject: [PATCH] =?UTF-8?q?Terminal=20=C3=B6ffnet=20als=20aktives=20Fenste?= =?UTF-8?q?r:=20Starter=20tritt=20Aktivierung=20per=20yieldActivationToApp?= =?UTF-8?q?licationWithBundleIdentifier=20ab,=20Terminal-Prozess=20aktivie?= =?UTF-8?q?rt=20sich=20beim=20Ready-Event=20(kooperative=20Aktivierung=20s?= =?UTF-8?q?eit=20macOS=2014)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/lib.rs | 82 +++++++++++++-------------------------- src-tauri/src/terminal.rs | 34 +++++++++++++++- 2 files changed, 59 insertions(+), 57 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 86bf857..67872a4 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -64,35 +64,24 @@ fn contract_home(paths: &Paths, p: &std::path::Path) -> String { } } -/// Registry Name → Projektordner. Ohne projects.json gilt das Alt-Layout: -/// Unterordner von ~/claude-projects mit .claude-Ordner. +/// Registry Name → Projektordner; ohne projects.json gibt es keine Projekte. fn load_registry(paths: &Paths) -> Result, String> { let file = paths.projects_file(); - if file.is_file() { - let raw = fs::read_to_string(&file).map_err(|e| format!("{}: {e}", file.display()))?; - let map: std::collections::BTreeMap = - serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", file.display()))?; - return Ok( - map - .into_iter() - .map(|(name, p)| { - let dir = expand_home(paths, &p); - (name, dir) - }) - .collect(), - ); + if !file.is_file() { + return Ok(std::collections::BTreeMap::new()); } - let mut map = std::collections::BTreeMap::new(); - let Ok(entries) = fs::read_dir(paths.projects_dir()) else { - return Ok(map); // kein Alt-Layout → keine Projekte - }; - for entry in entries { - let entry = entry.map_err(|e| e.to_string())?; - if entry.path().join(".claude").is_dir() { - map.insert(entry.file_name().to_string_lossy().into_owned(), entry.path()); - } - } - Ok(map) + let raw = fs::read_to_string(&file).map_err(|e| format!("{}: {e}", file.display()))?; + let map: std::collections::BTreeMap = + serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", file.display()))?; + Ok( + map + .into_iter() + .map(|(name, p)| { + let dir = expand_home(paths, &p); + (name, dir) + }) + .collect(), + ) } fn save_registry( @@ -114,8 +103,7 @@ pub(crate) fn project_dir(paths: &Paths, name: &str) -> Result .ok_or_else(|| format!("Projekt nicht registriert: {name}")) } -/// Nimmt ein Projekt in die Registry auf; materialisiert dabei auch die -/// per Alt-Layout gefundenen Einträge in projects.json. +/// Nimmt ein Projekt in die Registry auf. fn register_project(paths: &Paths, name: &str, dir: &std::path::Path) -> Result<(), String> { let mut reg = load_registry(paths)?; if reg.contains_key(name) { @@ -493,8 +481,6 @@ fn create_project_full_in( Some(d) => expand_home(paths, d), None => paths.projects_dir().join(name), }; - // Registry-Snapshot vor dem Anlegen — der Alt-Layout-Scan würde den frisch - // angelegten Ordner sonst schon als bestehendes Projekt sehen. let mut reg = load_registry(paths)?; if reg.contains_key(name) { return Err(format!("Projekt existiert bereits: {name}")); @@ -574,12 +560,7 @@ fn create_project_in(paths: &Paths, name: &str) -> Result<(), String> { return Err(format!("Projekt existiert bereits: {name}")); } fs::create_dir_all(dir.join(".claude")).map_err(|e| e.to_string())?; - // Registrieren nur, wenn die Registry schon materialisiert ist — sonst - // greift der Alt-Layout-Scan. - if paths.projects_file().is_file() { - register_project(paths, name, &dir)?; - } - Ok(()) + register_project(paths, name, &dir) } /// Arbeitsordner des Projekts: additionalDirectories aus der Projekt-settings.json. @@ -619,11 +600,7 @@ fn delete_project_in(paths: &Paths, name: &str, delete_work_dirs: bool) -> Resul } } fs::remove_dir_all(&dir).map_err(|e| e.to_string())?; - // Vor der Materialisierung gab es keinen Eintrag zu entfernen. - if paths.projects_file().is_file() { - unregister_project(paths, name)?; - } - Ok(()) + unregister_project(paths, name) } fn read_project_config_in(paths: &Paths, project: &str) -> Result { @@ -1454,13 +1431,7 @@ fn remove_project(name: String) -> Result<(), String> { if is_running(&name) { return Err(format!("{name} läuft noch — erst beenden")); } - let paths = Paths::real(); - // Alt-Layout erst materialisieren, sonst taucht das Projekt beim - // nächsten Scan wieder auf. - if !paths.projects_file().is_file() { - save_registry(&paths, &load_registry(&paths)?)?; - } - unregister_project(&paths, &name) + unregister_project(&Paths::real(), &name) } #[tauri::command] @@ -1627,7 +1598,7 @@ fn stop_project(project: String) -> Result<(), String> { /// Beendet die laufenden Terminal-Prozesse, wartet auf ihr Ende und öffnet das /// interne Terminal neu. #[tauri::command] -fn restart_project(project: String) -> Result<(), String> { +fn restart_project(app: tauri::AppHandle, project: String) -> Result<(), String> { kill_terminals(&project)?; let deadline = Instant::now() + Duration::from_secs(30); while is_running(&project) { @@ -1636,7 +1607,7 @@ fn restart_project(project: String) -> Result<(), String> { } std::thread::sleep(Duration::from_millis(250)); } - terminal::open_terminal(project) + terminal::open_terminal(app, project) } #[tauri::command] @@ -1684,11 +1655,12 @@ pub fn run() { .expect("error while building tauri application") // Das Dock-Icon erst nach dem App-Start setzen: in setup() gesetzt // überschreibt macOS es beim Anlegen des Dock-Tiles wieder. - .run(move |_app, event| { + .run(move |app, event| { if let tauri::RunEvent::Ready = event { if let Some(icon) = icon.as_deref() { terminal::set_dock_icon(icon); } + terminal::activate_self(app); } }); } @@ -1727,12 +1699,12 @@ fn tray_menu( } /// Tray-Klick auf ein Projekt: läuft es, kommt das Terminal-Fenster nach vorn, -/// sonst startet es. Das Label wechselt, sobald der Watcher den neuen Status sieht. -fn start_or_focus(project: &str) { +/// sonst startet es. +fn start_or_focus(app: &tauri::AppHandle, project: &str) { match terminal_pids(project).first() { Some(pid) => terminal::focus_terminal(*pid), None => { - if let Err(e) = terminal::open_terminal(project.to_string()) { + if let Err(e) = terminal::open_terminal(app.clone(), project.to_string()) { eprintln!("{project} starten: {e}"); } } @@ -1793,7 +1765,7 @@ fn main_builder() -> tauri::Builder { "quit" => app.exit(0), id => { if let Some(project) = id.strip_prefix("project:") { - start_or_focus(project); + start_or_focus(app, project); } } }) diff --git a/src-tauri/src/terminal.rs b/src-tauri/src/terminal.rs index a267b52..28478b9 100644 --- a/src-tauri/src/terminal.rs +++ b/src-tauri/src/terminal.rs @@ -17,13 +17,19 @@ pub struct Terminals(pub Mutex>); /// Öffnet ein Terminal für ein Projekt: startet das eigene Binary als /// Terminal-Prozess (`--terminal `), damit jedes Terminal ein -/// eigenes Dock-Icon bekommt. +/// eigenes Dock-Icon bekommt. Vorher tritt der rufende Prozess die +/// Aktivierung ab — Aktivierung ist seit macOS 14 kooperativ, sonst öffnet +/// das Terminal-Fenster hinter der aktiven App. #[tauri::command] -pub fn open_terminal(project: String) -> Result<(), String> { +pub fn open_terminal(app: AppHandle, project: String) -> Result<(), String> { let dir = crate::project_dir(&crate::Paths::real(), &project)?; if !dir.is_dir() { return Err(format!("Projektordner fehlt: {}", dir.display())); } + let bundle_id = app.config().identifier.clone(); + app + .run_on_main_thread(move || yield_activation_to_bundle(&bundle_id)) + .map_err(|e| e.to_string())?; let exe = std::env::current_exe().map_err(|e| e.to_string())?; std::process::Command::new(exe) .args(["--terminal", &project]) @@ -32,6 +38,30 @@ pub fn open_terminal(project: String) -> Result<(), String> { Ok(()) } +/// Tritt die Aktivierung an den nächsten startenden Prozess mit dieser +/// Bundle-ID ab (der Terminal-Prozess läuft unter derselben). +fn yield_activation_to_bundle(bundle_id: &str) { + use objc2::MainThreadMarker; + use objc2_app_kit::NSApplication; + use objc2_foundation::NSString; + let mtm = + MainThreadMarker::new().expect("yield_activation_to_bundle läuft nicht auf dem Main-Thread"); + NSApplication::sharedApplication(mtm) + .yieldActivationToApplicationWithBundleIdentifier(&NSString::from_str(bundle_id)); +} + +/// Selbst-Aktivierung des frisch gestarteten Terminal-Prozesses (Ready-Event); +/// die Gegenseite hat vorher per yield abgetreten. +pub fn activate_self(app: &AppHandle) { + use objc2::MainThreadMarker; + use objc2_app_kit::NSApplication; + let mtm = MainThreadMarker::new().expect("activate_self läuft nicht auf dem Main-Thread"); + NSApplication::sharedApplication(mtm).activate(); + if let Some(window) = app.webview_windows().values().next() { + window.set_focus().expect("Terminal-Fenster nicht fokussierbar"); + } +} + /// Fenster-Hintergrund je Theme — muss zu den Theme-Definitionen in /// terminal.ts passen, sonst blitzt beim Öffnen die falsche Farbe auf. fn theme_background(theme: &str) -> (u8, u8, u8) {