Modul-Split: Fachlogik nach domain/, OS-Code nach platform/, Tauri-Verdrahtung in app.rs/commands.rs
- lib.rs nur noch Prozessrollen-Dispatch + Panic-Tracer - KWin-Script als KDE-Pendant zur GNOME-Extension (Popup ans Zeiger- Rechteck), wird von deb/rpm mit ausgeliefert - SNI-Tray auf ksni (pure Rust), appindicator-Depends entfallen
This commit is contained in:
+17
-99
@@ -1,10 +1,15 @@
|
||||
use base64::{engine::general_purpose::STANDARD, Engine};
|
||||
use portable_pty::{native_pty_system, ChildKiller, CommandBuilder, MasterPty, PtySize};
|
||||
use portable_pty::{native_pty_system, ChildKiller, MasterPty, PtySize};
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Read, Write};
|
||||
use std::sync::Mutex;
|
||||
use tauri::{AppHandle, Emitter, Manager, State, WebviewUrl, WebviewWindowBuilder};
|
||||
|
||||
use crate::domain::paths::{panel_file, Paths};
|
||||
use crate::domain::project::{project_pool_dir, terminal_config, TerminalConfig};
|
||||
use crate::domain::registry::project_dir;
|
||||
use crate::domain::settings::claude_command;
|
||||
|
||||
/// Eine laufende PTY-Session, gekoppelt an ein Terminal-Fenster (Key = Fenster-Label).
|
||||
pub struct Session {
|
||||
writer: Box<dyn Write + Send>,
|
||||
@@ -22,13 +27,13 @@ pub struct Terminals(pub Mutex<HashMap<String, Session>>);
|
||||
/// das Terminal-Fenster hinter der aktiven App.
|
||||
#[tauri::command]
|
||||
pub fn open_terminal(app: AppHandle, project: String) -> Result<(), String> {
|
||||
let dir = crate::project_dir(&crate::Paths::real(), &project)?;
|
||||
let dir = project_dir(&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))
|
||||
.run_on_main_thread(move || crate::platform::yield_activation(&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)
|
||||
@@ -38,44 +43,6 @@ pub fn open_terminal(app: AppHandle, 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).
|
||||
#[cfg(target_os = "macos")]
|
||||
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));
|
||||
}
|
||||
|
||||
/// Linux kennt keine kooperative Aktivierungsabtretung.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn yield_activation_to_bundle(_bundle_id: &str) {}
|
||||
|
||||
/// Selbst-Aktivierung des frisch gestarteten Terminal-Prozesses (Ready-Event);
|
||||
/// die Gegenseite hat vorher per yield abgetreten.
|
||||
#[cfg(target_os = "macos")]
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
/// Linux: Fenster ohne NSApplication über die Tauri-API fokussieren.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn activate_self(app: &AppHandle) {
|
||||
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) {
|
||||
@@ -88,60 +55,13 @@ fn theme_background(theme: &str) -> (u8, u8, u8) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Setzt das Dock-Icon dieses Terminal-Prozesses aus einer PNG/ICNS-Datei.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn set_dock_icon(path: &str) {
|
||||
use objc2::{AnyThread, MainThreadMarker};
|
||||
use objc2_app_kit::{NSApplication, NSImage};
|
||||
use objc2_foundation::NSString;
|
||||
|
||||
let mtm = MainThreadMarker::new().expect("set_dock_icon läuft nicht auf dem Main-Thread");
|
||||
// Nicht ladbar (Datei weg, TCC-geschützter Ordner wie ~/Downloads): Terminal
|
||||
// ohne eigenes Dock-Icon starten statt den Prozess zu beenden.
|
||||
let Some(img) = NSImage::initWithContentsOfFile(NSImage::alloc(), &NSString::from_str(path))
|
||||
else {
|
||||
eprintln!("Dock-Icon nicht ladbar, Terminal startet ohne: {path}");
|
||||
return;
|
||||
};
|
||||
// unsafe laut objc2-Signatur; das NSImage stammt aus einer Datei und ist gültig.
|
||||
unsafe {
|
||||
NSApplication::sharedApplication(mtm).setApplicationIconImage(Some(&img));
|
||||
}
|
||||
}
|
||||
|
||||
/// Linux kennt kein Dock-Icon pro Prozess.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn set_dock_icon(_path: &str) {}
|
||||
|
||||
/// Holt das Terminal-Fenster eines laufenden Projekts in den Vordergrund:
|
||||
/// aktiviert den Terminal-Prozess über seine PID. Aktivierung ist seit
|
||||
/// macOS 14 kooperativ — die Tray-App tritt sie vorher ab.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn focus_terminal(pid: u32) {
|
||||
use objc2::MainThreadMarker;
|
||||
use objc2_app_kit::{NSApplication, NSApplicationActivationOptions, NSRunningApplication};
|
||||
let Some(term) = NSRunningApplication::runningApplicationWithProcessIdentifier(pid as i32)
|
||||
else {
|
||||
eprintln!("Terminal-Prozess {pid} nicht gefunden");
|
||||
return;
|
||||
};
|
||||
let mtm = MainThreadMarker::new().expect("focus_terminal läuft nicht auf dem Main-Thread");
|
||||
NSApplication::sharedApplication(mtm).yieldActivationToApplication(&term);
|
||||
term.activateWithOptions(NSApplicationActivationOptions::ActivateAllWindows);
|
||||
}
|
||||
|
||||
/// Linux: Fremdprozess-Fokus über die PID steht ohne NSRunningApplication
|
||||
/// nicht zur Verfügung.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn focus_terminal(_pid: u32) {}
|
||||
|
||||
/// Baut das Terminal-Fenster des Terminal-Prozesses. Die PTY entsteht erst,
|
||||
/// wenn das Fenster geladen ist und `term_start` ruft — so gehen keine
|
||||
/// Ausgaben verloren, bevor der Event-Listener steht.
|
||||
pub fn build_window(
|
||||
app: &AppHandle,
|
||||
project: &str,
|
||||
cfg: &crate::TerminalConfig,
|
||||
cfg: &TerminalConfig,
|
||||
) -> tauri::Result<()> {
|
||||
let (r, g, b) = theme_background(cfg.theme.as_deref().unwrap_or_default());
|
||||
let title = cfg.title.as_deref().unwrap_or(project);
|
||||
@@ -180,8 +100,8 @@ pub fn term_start(
|
||||
rows: u16,
|
||||
cols: u16,
|
||||
) -> Result<(), String> {
|
||||
let paths = crate::Paths::real();
|
||||
let cwd = crate::project_dir(&paths, &project)?;
|
||||
let paths = Paths::real();
|
||||
let cwd = project_dir(&paths, &project)?;
|
||||
|
||||
let pty = native_pty_system()
|
||||
.openpty(PtySize { rows, cols, pixel_width: 0, pixel_height: 0 })
|
||||
@@ -190,20 +110,18 @@ pub fn term_start(
|
||||
// Panel-Kanal: leere Datei anlegen (definierter Startzustand für den
|
||||
// Watcher) und ihren Pfad als AI_CONTROL_PANEL in die PTY geben — der Skill
|
||||
// schreibt seinen Entwurf dorthin.
|
||||
let panel_path = crate::panel_file(&project);
|
||||
let panel_path = panel_file(&project);
|
||||
if let Some(parent) = panel_path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
let _ = std::fs::write(&panel_path, "");
|
||||
|
||||
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".into());
|
||||
let mut cmd = CommandBuilder::new(&shell);
|
||||
cmd.args(["-lc", &crate::claude_command(&paths)]);
|
||||
let mut cmd = crate::platform::shell_command(&claude_command(&paths));
|
||||
cmd.cwd(&cwd);
|
||||
cmd.env("TERM", "xterm-256color");
|
||||
cmd.env("AI_CONTROL_PANEL", &panel_path);
|
||||
cmd.env("AI_CONTROL_PROJECT", &project);
|
||||
if let Some(pool_dir) = crate::project_pool_dir(&project)? {
|
||||
if let Some(pool_dir) = project_pool_dir(&project)? {
|
||||
cmd.env("CLAUDE_CONFIG_DIR", pool_dir);
|
||||
}
|
||||
|
||||
@@ -293,14 +211,14 @@ fn spawn_panel_watcher(app: AppHandle, path: std::path::PathBuf) {
|
||||
/// Aktueller Panel-Inhalt (Erstbefüllung eines gerade geöffneten Panel-Fensters).
|
||||
#[tauri::command]
|
||||
pub fn panel_read(project: String) -> String {
|
||||
std::fs::read_to_string(crate::panel_file(&project)).unwrap_or_default()
|
||||
std::fs::read_to_string(panel_file(&project)).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Schreibt den Panel-Inhalt (Titel-Edit im Panel). Der Watcher meldet die
|
||||
/// Änderung als `panel-update` an alle Fenster.
|
||||
#[tauri::command]
|
||||
pub fn panel_set(project: String, text: String) -> Result<(), String> {
|
||||
std::fs::write(crate::panel_file(&project), text).map_err(|e| e.to_string())
|
||||
std::fs::write(panel_file(&project), text).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Löst das Panel in ein eigenes Fenster ab. Existiert es schon, kommt es nach
|
||||
@@ -312,7 +230,7 @@ pub fn open_panel_window(app: AppHandle, project: String) -> Result<(), String>
|
||||
let _ = w.set_focus();
|
||||
return Ok(());
|
||||
}
|
||||
let cfg = crate::terminal_config(&project)?;
|
||||
let cfg = terminal_config(&project)?;
|
||||
let (r, g, b) = theme_background(cfg.theme.as_deref().unwrap_or_default());
|
||||
let title = cfg.title.as_deref().unwrap_or(project.as_str());
|
||||
WebviewWindowBuilder::new(
|
||||
|
||||
Reference in New Issue
Block a user