9a99bca466
- 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
302 lines
11 KiB
Rust
302 lines
11 KiB
Rust
//! Tauri-Verdrahtung: Haupt-App (Tray, main-/popup-Fenster, Watcher) und
|
|
//! Terminal-Prozess. Das Popup ist auf allen OS dasselbe HTML-Fenster; nur
|
|
//! der Klick-Trigger kommt aus platform::init_tray (Anchor-Kontrakt).
|
|
|
|
use crate::domain::paths::Paths;
|
|
use crate::domain::pool::provision_pools_for_panel;
|
|
use crate::domain::project::terminal_config;
|
|
use crate::domain::watcher::spawn_session_watcher;
|
|
use crate::platform::Anchor;
|
|
use crate::{commands, terminal};
|
|
|
|
/// Sperrt das Auto-Hide des Popups bei Fokusverlust für einen Moment nach dem
|
|
/// Anzeigen (KDE-Erststart-Race). Als Tauri-State geführt.
|
|
#[cfg(target_os = "linux")]
|
|
pub(crate) struct PopupBlurGuard(pub(crate) std::sync::Arc<std::sync::atomic::AtomicBool>);
|
|
|
|
/// Popup an den Klick-Koordinaten platzieren, in den Monitor geklemmt.
|
|
#[cfg(target_os = "linux")]
|
|
fn position_popup(w: &tauri::WebviewWindow, x: i32, y: i32) {
|
|
use tauri::{PhysicalPosition, PhysicalSize};
|
|
let size = w.outer_size().unwrap_or_else(|_| PhysicalSize::new(320, 300));
|
|
let (mon_pos, mon_size) = w
|
|
.current_monitor()
|
|
.ok()
|
|
.flatten()
|
|
.map(|m| (*m.position(), *m.size()))
|
|
.unwrap_or((PhysicalPosition::new(0, 0), PhysicalSize::new(1920, 1080)));
|
|
// Rechte Kante an den Klick; oben/unten ergibt sich aus dem Klemmen an die
|
|
// Monitorkante (Panel oben -> unter dem Icon, Panel unten -> darüber).
|
|
let max_x = mon_pos.x + mon_size.width as i32 - size.width as i32;
|
|
let max_y = mon_pos.y + mon_size.height as i32 - size.height as i32;
|
|
let px = (x - size.width as i32).clamp(mon_pos.x, max_x.max(mon_pos.x));
|
|
let py = y.clamp(mon_pos.y, max_y.max(mon_pos.y));
|
|
let _ = w.set_position(PhysicalPosition::new(px, py));
|
|
}
|
|
|
|
/// Popup zeigen — ein Codepfad für alle OS, nur die Platzierung folgt dem
|
|
/// Anchor aus dem Tray-Trigger.
|
|
pub(crate) fn show_popup(app: &tauri::AppHandle, anchor: Anchor) {
|
|
use tauri::Manager;
|
|
let win = app.clone();
|
|
let _ = app.run_on_main_thread(move || {
|
|
let Some(w) = win.get_webview_window("popup") else {
|
|
return;
|
|
};
|
|
match anchor {
|
|
// Nativer Tray (macOS/Windows): rechte Popup-Kante an der rechten
|
|
// Icon-Kante; macOS unter dem Menüleisten-Icon, Windows über der Taskbar.
|
|
Anchor::IconRect { rect, popup_below } => {
|
|
let scale = w.scale_factor().unwrap_or(1.0);
|
|
let pos = rect.position.to_physical::<f64>(scale);
|
|
let size = rect.size.to_physical::<f64>(scale);
|
|
let win_w = w.outer_size().map(|s| s.width as f64).unwrap_or(320.0);
|
|
let x = (pos.x + size.width - win_w).max(0.0);
|
|
let y = if popup_below {
|
|
pos.y + size.height
|
|
} else {
|
|
let win_h = w.outer_size().map(|s| s.height as f64).unwrap_or(300.0);
|
|
(pos.y - win_h).max(0.0)
|
|
};
|
|
let _ = w.set_position(tauri::PhysicalPosition::new(x, y));
|
|
}
|
|
// SNI (KDE/XFCE/Cinnamon): Activate liefert keine brauchbaren
|
|
// Koordinaten, aber der Zeiger sitzt beim Klick auf dem Icon.
|
|
Anchor::Cursor => {
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
if let Some(g) = win.try_state::<PopupBlurGuard>() {
|
|
g.0.store(true, std::sync::atomic::Ordering::SeqCst);
|
|
}
|
|
match win.cursor_position() {
|
|
Ok(p) => position_popup(&w, p.x as i32, p.y as i32),
|
|
Err(_) => {
|
|
let _ = w.center();
|
|
}
|
|
}
|
|
// Guard nach kurzer Zeit lösen — danach schließt Fokusverlust wieder
|
|
// normal.
|
|
let g2 = win.clone();
|
|
std::thread::spawn(move || {
|
|
std::thread::sleep(std::time::Duration::from_millis(350));
|
|
if let Some(g) = g2.try_state::<PopupBlurGuard>() {
|
|
g.0.store(false, std::sync::atomic::Ordering::SeqCst);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
// GNOME-Extension/KWin: der Compositor positioniert selbst.
|
|
Anchor::Managed => {}
|
|
}
|
|
let _ = w.show();
|
|
let _ = w.set_focus();
|
|
});
|
|
}
|
|
|
|
/// Popup verstecken (GNOME-Toggle über D-Bus Hide()).
|
|
pub(crate) fn hide_popup(app: &tauri::AppHandle) {
|
|
use tauri::Manager;
|
|
let win = app.clone();
|
|
let _ = app.run_on_main_thread(move || {
|
|
if let Some(w) = win.get_webview_window("popup") {
|
|
let _ = w.hide();
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Haupt-App: reine Tray-App ohne Dock-Eintrag.
|
|
pub(crate) fn main_builder() -> tauri::Builder<tauri::Wry> {
|
|
tauri::Builder::default()
|
|
.plugin(tauri_plugin_autostart::init(
|
|
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
|
|
None,
|
|
))
|
|
.plugin(tauri_plugin_dialog::init())
|
|
.setup(|app| {
|
|
if cfg!(debug_assertions) {
|
|
app.handle().plugin(
|
|
tauri_plugin_log::Builder::default()
|
|
.level(log::LevelFilter::Info)
|
|
.build(),
|
|
)?;
|
|
}
|
|
|
|
// Nur Tray-Icon in der Menüleiste, kein Dock-Eintrag.
|
|
#[cfg(target_os = "macos")]
|
|
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
|
|
|
// Session-Watcher: synct bei Session-Ende (Prozess verschwindet).
|
|
spawn_session_watcher();
|
|
|
|
// Panel-MCP-Server + Tool-Freigabe in alle Pools legen (auch bestehende),
|
|
// damit write_panel in jedem CLAUDE_CONFIG_DIR ohne Rückfrage bereitsteht.
|
|
provision_pools_for_panel(&Paths::real());
|
|
|
|
// Alle pro-Terminal-.desktop-Dateien neu schreiben + verwaiste entfernen,
|
|
// damit sie da sind, bevor ein Terminal startet (No-op außerhalb Linux).
|
|
crate::platform::sync_all_desktops(&Paths::real());
|
|
|
|
// Fenster im Code statt in tauri.conf.json, damit der Terminal-Prozess
|
|
// (gleiches Binary, gleiche Config) kein main-Fenster anlegt.
|
|
let main_win = tauri::WebviewWindowBuilder::new(app, "main", tauri::WebviewUrl::default())
|
|
.title("ai-control")
|
|
.inner_size(800.0, 600.0)
|
|
// Fenster-Icon (_NET_WM_ICON) — deckt X11-DEs wie XFCE direkt ab,
|
|
// unabhängig von der .desktop-Zuordnung; auf Windows das Titel-/Taskbar-Icon.
|
|
.icon(tauri::image::Image::from_bytes(include_bytes!(
|
|
"../icons/128x128.png"
|
|
))?)?
|
|
.visible(false);
|
|
// Wie die Terminal-Fenster: eigener Header als Titelleiste. macOS behält die
|
|
// Ampel (Overlay), Linux ist dekorationslos (eigene Fensterknöpfe im Header).
|
|
#[cfg(target_os = "macos")]
|
|
let main_win = main_win
|
|
.title_bar_style(tauri::TitleBarStyle::Overlay)
|
|
.hidden_title(true);
|
|
#[cfg(target_os = "linux")]
|
|
let main_win = main_win.decorations(false);
|
|
main_win.build()?;
|
|
|
|
// Rahmenloses Popup-Fenster — dieselbe Optik auf allen Plattformen. Nur
|
|
// der Weg zum Klick unterscheidet sich (platform::init_tray).
|
|
tauri::WebviewWindowBuilder::new(app, "popup", tauri::WebviewUrl::App("popup.html".into()))
|
|
.title("ai-control-popup")
|
|
.inner_size(320.0, 300.0)
|
|
.decorations(false)
|
|
.visible(false)
|
|
.transparent(true)
|
|
.skip_taskbar(true)
|
|
.always_on_top(true)
|
|
.resizable(false)
|
|
.build()?;
|
|
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
use tauri::Manager;
|
|
app.manage(PopupBlurGuard(std::sync::Arc::new(
|
|
std::sync::atomic::AtomicBool::new(false),
|
|
)));
|
|
}
|
|
|
|
// Der gesamte Tray-Kontrakt: Icon zeigen, Klick als Anchor melden —
|
|
// was dann passiert, entscheidet ausschließlich dieser Code.
|
|
crate::platform::init_tray(
|
|
app.handle(),
|
|
crate::platform::TrayCallbacks {
|
|
show: Box::new(|app, anchor| show_popup(app, anchor)),
|
|
hide: Box::new(hide_popup),
|
|
},
|
|
)?;
|
|
|
|
Ok(())
|
|
})
|
|
// 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();
|
|
window.hide().unwrap();
|
|
}
|
|
// Popup schließt bei Fokusverlust.
|
|
if let tauri::WindowEvent::Focused(false) = event {
|
|
if window.label() == "popup" {
|
|
// KDE feuert direkt nach show/set_focus ein Focused(false) — ohne
|
|
// diese kurze Sperre verschwände das Popup sofort wieder (erst der
|
|
// zweite Klick hielte). GNOME/macOS setzen die Sperre nie.
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
use tauri::Manager;
|
|
if let Some(g) = window.try_state::<PopupBlurGuard>() {
|
|
if g.0.load(std::sync::atomic::Ordering::SeqCst) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
let _ = window.hide();
|
|
}
|
|
}
|
|
})
|
|
.invoke_handler(tauri::generate_handler![
|
|
commands::list_projects,
|
|
commands::create_project_full,
|
|
commands::add_project,
|
|
commands::remove_project,
|
|
commands::delete_project,
|
|
commands::project_work_dirs,
|
|
commands::set_project_dir,
|
|
commands::add_work_dir,
|
|
commands::remove_work_dir,
|
|
commands::list_pools,
|
|
commands::create_oauth_pool,
|
|
commands::create_apikey_pool,
|
|
commands::rename_pool,
|
|
commands::delete_pool,
|
|
commands::assign_pool,
|
|
commands::unassign_pool,
|
|
commands::set_terminal_config,
|
|
commands::project_icon,
|
|
commands::pool_label,
|
|
commands::todo_state,
|
|
commands::set_todo,
|
|
commands::usage_stats,
|
|
commands::stop_project,
|
|
commands::restart_project,
|
|
commands::start_or_focus_cmd,
|
|
commands::open_main_window,
|
|
commands::quit_app,
|
|
commands::sync_setting,
|
|
commands::set_sync_setting,
|
|
commands::terminal_font_size,
|
|
commands::set_terminal_font_size,
|
|
commands::link_pool_runtime,
|
|
commands::oauth_login,
|
|
commands::keychain_status,
|
|
commands::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.
|
|
pub(crate) fn terminal_builder(project: String) -> tauri::Builder<tauri::Wry> {
|
|
tauri::Builder::default()
|
|
.plugin(tauri_plugin_dialog::init())
|
|
.manage(terminal::Terminals::default())
|
|
.setup(move |app| {
|
|
let cfg = terminal_config(&project)?;
|
|
terminal::build_window(app.handle(), &project, &cfg)?;
|
|
Ok(())
|
|
})
|
|
// Fenster zu → PTY-Session abräumen; danach endet der Prozess. Das
|
|
// abgelöste Panel-Fenster hat keine PTY: sein Schließen dockt das Panel
|
|
// wieder an (panel-attached), der Prozess läuft weiter.
|
|
.on_window_event(|window, event| {
|
|
if let tauri::WindowEvent::Destroyed = event {
|
|
if window.label().starts_with("panel-") {
|
|
use tauri::{Emitter, Manager};
|
|
let _ = window.app_handle().emit("panel-window-closed", ());
|
|
} else {
|
|
terminal::close(window);
|
|
}
|
|
}
|
|
})
|
|
.invoke_handler(tauri::generate_handler![
|
|
terminal::term_start,
|
|
terminal::term_log,
|
|
terminal::term_write,
|
|
terminal::term_resize,
|
|
terminal::panel_read,
|
|
terminal::panel_set,
|
|
terminal::open_panel_window,
|
|
commands::panel_archive_dir_cmd,
|
|
commands::panel_archive_cmd,
|
|
commands::reveal_path_cmd,
|
|
// Header im Terminal-Prozess: Projektliste, Icon und Pool-Name.
|
|
commands::list_projects,
|
|
commands::project_icon,
|
|
commands::pool_label,
|
|
commands::terminal_font_size,
|
|
commands::set_terminal_font_size,
|
|
commands::spellcheck_lang
|
|
])
|
|
}
|