Eingebautes Terminal (xterm.js + portable-pty) und Start/Beenden-Button
- Terminal-Fenster als eigener Prozess (--terminal <projekt>) mit eigener PTY, startet claude-sync im Projektordner - Projektliste: ein Button pro Zeile — grün Starten (open_terminal) bzw. rot Beenden (stop_project, SIGTERM auf exakte PID / Ghostty-Quit) - is_running erfasst Ghostty oder laufenden Terminal-Prozess - Status-Kreis ohne Prozess unsichtbar
This commit is contained in:
+104
-4
@@ -1,3 +1,5 @@
|
||||
mod terminal;
|
||||
|
||||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
@@ -75,6 +77,10 @@ fn bundle_id(project: &str) -> String {
|
||||
}
|
||||
|
||||
fn is_running(project: &str) -> bool {
|
||||
ghostty_running(project) || !terminal_pids(project).is_empty()
|
||||
}
|
||||
|
||||
fn ghostty_running(project: &str) -> bool {
|
||||
let out = Command::new("lsappinfo")
|
||||
.args(["find", &format!("bundleid={}", bundle_id(project))])
|
||||
.output();
|
||||
@@ -84,6 +90,20 @@ fn is_running(project: &str) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// PIDs der eingebauten Terminal-Prozesse (`app --terminal <projekt>`).
|
||||
fn terminal_pids(project: &str) -> Vec<u32> {
|
||||
let out = Command::new("pgrep")
|
||||
.args(["-f", "--", &format!("--terminal {project}$")])
|
||||
.output();
|
||||
match out {
|
||||
Ok(o) => String::from_utf8_lossy(&o.stdout)
|
||||
.lines()
|
||||
.filter_map(|l| l.trim().parse().ok())
|
||||
.collect(),
|
||||
Err(_) => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn check_name(name: &str) -> Result<(), String> {
|
||||
if name.trim().is_empty() || name.contains('/') || name.contains("..") {
|
||||
return Err(format!("ungültiger Name: {name}"));
|
||||
@@ -402,6 +422,37 @@ fn start_project(project: String) -> Result<(), String> {
|
||||
open_project(&project)
|
||||
}
|
||||
|
||||
/// Beendet den laufenden Prozess eines Projekts: das eingebaute Terminal per
|
||||
/// SIGTERM auf die exakte PID (nie über die Bundle-ID — die würde alle
|
||||
/// Instanzen inkl. der Haupt-App treffen), sonst die Ghostty-App per Quit.
|
||||
#[tauri::command]
|
||||
fn stop_project(project: String) -> Result<(), String> {
|
||||
let pids = terminal_pids(&project);
|
||||
if !pids.is_empty() {
|
||||
for pid in pids {
|
||||
let out = Command::new("kill")
|
||||
.arg(pid.to_string())
|
||||
.output()
|
||||
.map_err(|e| e.to_string())?;
|
||||
if !out.status.success() {
|
||||
return Err(String::from_utf8_lossy(&out.stderr).trim().to_string());
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
if ghostty_running(&project) {
|
||||
let out = Command::new("osascript")
|
||||
.args(["-e", &format!("quit app id \"{}\"", bundle_id(&project))])
|
||||
.output()
|
||||
.map_err(|e| e.to_string())?;
|
||||
if !out.status.success() {
|
||||
return Err(String::from_utf8_lossy(&out.stderr).trim().to_string());
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
Err(format!("{project} läuft nicht"))
|
||||
}
|
||||
|
||||
/// Beendet die Ghostty-App des Projekts (regulärer Quit, Ghostty darf noch
|
||||
/// nachfragen), wartet auf das Prozessende und startet sie neu.
|
||||
#[tauri::command]
|
||||
@@ -624,6 +675,22 @@ fn pct_decode(s: &str) -> String {
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
let mut args = std::env::args().skip(1);
|
||||
let builder = match args.next().as_deref() {
|
||||
// `app --terminal <projekt>`: eigener Prozess pro Terminal-Fenster,
|
||||
// damit jedes Terminal ein eigenes Dock-Icon bekommt.
|
||||
Some("--terminal") => {
|
||||
terminal_builder(args.next().expect("--terminal braucht einen Projektnamen"))
|
||||
}
|
||||
_ => main_builder(),
|
||||
};
|
||||
builder
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
/// Haupt-App: reine Tray-App ohne Dock-Eintrag.
|
||||
fn main_builder() -> tauri::Builder<tauri::Wry> {
|
||||
use tauri::Manager;
|
||||
|
||||
tauri::Builder::default()
|
||||
@@ -644,6 +711,14 @@ pub fn run() {
|
||||
#[cfg(target_os = "macos")]
|
||||
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||
|
||||
// Fenster im Code statt in tauri.conf.json, damit der Terminal-Prozess
|
||||
// (gleiches Binary, gleiche Config) kein main-Fenster anlegt.
|
||||
tauri::WebviewWindowBuilder::new(app, "main", tauri::WebviewUrl::default())
|
||||
.title("AI Control")
|
||||
.inner_size(800.0, 600.0)
|
||||
.visible(false)
|
||||
.build()?;
|
||||
|
||||
let open = tauri::menu::MenuItem::with_id(app, "open", "Öffnen", true, None::<&str>)?;
|
||||
let quit = tauri::menu::MenuItem::with_id(app, "quit", "Beenden", true, None::<&str>)?;
|
||||
let menu = tauri::menu::Menu::with_items(app, &[&open, &quit])?;
|
||||
@@ -663,7 +738,7 @@ pub fn run() {
|
||||
|
||||
Ok(())
|
||||
})
|
||||
// Fenster schließen versteckt nur; Beenden geht übers Tray-Menü.
|
||||
// Hauptfenster schließen versteckt nur; Beenden geht übers Tray-Menü.
|
||||
.on_window_event(|window, event| {
|
||||
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
|
||||
api.prevent_close();
|
||||
@@ -681,13 +756,38 @@ pub fn run() {
|
||||
assign_pool,
|
||||
unassign_pool,
|
||||
start_project,
|
||||
stop_project,
|
||||
restart_project,
|
||||
oauth_login,
|
||||
oauth_refresh,
|
||||
set_apikey
|
||||
set_apikey,
|
||||
terminal::open_terminal
|
||||
])
|
||||
}
|
||||
|
||||
/// Terminal-Prozess: ein Fenster mit eigener PTY; Activation-Policy bleibt
|
||||
/// Regular, dadurch Dock-Icon und Cmd-Tab-Eintrag pro Terminal.
|
||||
fn terminal_builder(project: String) -> tauri::Builder<tauri::Wry> {
|
||||
tauri::Builder::default()
|
||||
.manage(terminal::Terminals::default())
|
||||
.setup(move |app| {
|
||||
terminal::build_window(app.handle(), &project)?;
|
||||
Ok(())
|
||||
})
|
||||
// Fenster zu → PTY-Session abräumen; danach endet der Prozess.
|
||||
.on_window_event(|window, event| {
|
||||
if let tauri::WindowEvent::Destroyed = event {
|
||||
terminal::close(window);
|
||||
}
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
terminal::term_start,
|
||||
terminal::term_log,
|
||||
terminal::term_write,
|
||||
terminal::term_resize,
|
||||
// Pool-Chip im Terminal-Header braucht die Projektliste auch hier.
|
||||
list_projects
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
// ---------- Tests ----------
|
||||
|
||||
Reference in New Issue
Block a user