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:
@@ -0,0 +1,301 @@
|
||||
//! 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
|
||||
])
|
||||
}
|
||||
Reference in New Issue
Block a user