Linux-Port: GNOME-Popup + Tray, Terminal-Schriftgröße global
- Popup als rahmenloses Fenster mit D-Bus-Relay (popup.html/ts, Popup.vue); GNOME-Extension schiebt es unter den Panel-Button - Terminal: globale Schriftgröße via settings.json (terminalFontSize), Zoom mit Ctrl/Cmd +/-/0 im Terminal - trayLinux-Icon, Capabilities, App.vue/terminal.html angepasst
This commit is contained in:
+310
-53
@@ -340,12 +340,12 @@ fn spawn_session_watcher(app: tauri::AppHandle) {
|
||||
let handle = app.clone();
|
||||
app
|
||||
.run_on_main_thread(move || {
|
||||
let menu = tray_menu(&handle).expect("Tray-Menü nicht aufbaubar");
|
||||
handle
|
||||
.tray_by_id("main")
|
||||
.expect("Tray-Icon fehlt")
|
||||
.set_menu(Some(menu))
|
||||
.expect("Tray-Menü nicht setzbar");
|
||||
// Nur wenn ein Tray existiert (macOS). Unter Linux pollt das Popup selbst.
|
||||
if let Some(tray) = handle.tray_by_id("main") {
|
||||
if let Ok(menu) = tray_menu(&handle) {
|
||||
let _ = tray.set_menu(Some(menu));
|
||||
}
|
||||
}
|
||||
})
|
||||
.expect("Main-Thread nicht erreichbar");
|
||||
state = current;
|
||||
@@ -556,6 +556,8 @@ fn create_project_full_in(
|
||||
if todo {
|
||||
set_todo_in(paths, name, true)?;
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
write_terminal_desktop(paths, name, &cfg.terminal);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -618,7 +620,15 @@ fn add_project_in(paths: &Paths, path: &str) -> Result<(), String> {
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
check_name(&name)?;
|
||||
register_project(paths, &name, &dir)
|
||||
register_project(paths, &name, &dir)?;
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let cfg = read_project_config_in(paths, &name)
|
||||
.map(|c| c.terminal)
|
||||
.unwrap_or_default();
|
||||
write_terminal_desktop(paths, &name, &cfg);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -739,7 +749,10 @@ fn delete_project_in(paths: &Paths, name: &str, delete_work_dirs: bool) -> Resul
|
||||
}
|
||||
}
|
||||
fs::remove_dir_all(&dir).map_err(|e| e.to_string())?;
|
||||
unregister_project(paths, name)
|
||||
unregister_project(paths, name)?;
|
||||
#[cfg(target_os = "linux")]
|
||||
remove_terminal_desktop(paths, name);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_project_config_in(paths: &Paths, project: &str) -> Result<ProjectConfig, String> {
|
||||
@@ -809,7 +822,10 @@ fn set_terminal_config_in(
|
||||
}
|
||||
}
|
||||
cfg.terminal = terminal;
|
||||
write_project_config_in(paths, project, &cfg)
|
||||
write_project_config_in(paths, project, &cfg)?;
|
||||
#[cfg(target_os = "linux")]
|
||||
write_terminal_desktop(paths, project, &cfg.terminal);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Icon-Pfad einer Projekt-Config auflösen: relative Namen liegen im
|
||||
@@ -822,6 +838,78 @@ fn resolve_icon_path(paths: &Paths, icon: &str) -> PathBuf {
|
||||
}
|
||||
}
|
||||
|
||||
/// ~/.local/share/applications — Ziel der pro-Terminal-.desktop-Dateien.
|
||||
/// paths-abhängig (nicht $HOME direkt), damit Tests in ihr tmp-home schreiben.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn applications_dir(paths: &Paths) -> PathBuf {
|
||||
paths.home.join(".local/share/applications")
|
||||
}
|
||||
|
||||
/// Schreibt/aktualisiert die .desktop eines Projekts: dash-to-dock ordnet dem
|
||||
/// Fenster (app_id `aicontrol-<projekt>`, via set_prgname) darüber das Projekt-
|
||||
/// Icon zu. NoDisplay=true hält den App-Starter sauber.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) fn write_terminal_desktop(paths: &Paths, project: &str, cfg: &TerminalConfig) {
|
||||
let dir = applications_dir(paths);
|
||||
if fs::create_dir_all(&dir).is_err() {
|
||||
return;
|
||||
}
|
||||
let exec = std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|p| p.to_str().map(str::to_string))
|
||||
.unwrap_or_else(|| "ai-control".into());
|
||||
let icon_line = cfg
|
||||
.icon
|
||||
.as_deref()
|
||||
.map(|i| resolve_icon_path(paths, i))
|
||||
.filter(|p| p.exists())
|
||||
.and_then(|p| p.to_str().map(str::to_string))
|
||||
.map(|p| format!("Icon={p}\n"))
|
||||
.unwrap_or_default();
|
||||
let content = format!(
|
||||
"[Desktop Entry]\nType=Application\nName={project}\nExec={exec} --terminal {project}\n\
|
||||
{icon_line}StartupWMClass=aicontrol-{project}\nNoDisplay=true\n"
|
||||
);
|
||||
let _ = fs::write(dir.join(format!("aicontrol-{project}.desktop")), content);
|
||||
}
|
||||
|
||||
/// Entfernt die .desktop eines Projekts (beim Löschen).
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) fn remove_terminal_desktop(paths: &Paths, project: &str) {
|
||||
let _ = fs::remove_file(applications_dir(paths).join(format!("aicontrol-{project}.desktop")));
|
||||
}
|
||||
|
||||
/// Beim App-Start: für jedes registrierte Projekt die .desktop neu schreiben und
|
||||
/// verwaiste (kein registriertes Projekt mehr) entfernen. Dadurch sind sie immer
|
||||
/// vorhanden und aktuell, bevor überhaupt ein Terminal startet.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn sync_all_desktops(paths: &Paths) {
|
||||
let Ok(reg) = load_registry(paths) else {
|
||||
return;
|
||||
};
|
||||
for project in reg.keys() {
|
||||
let cfg = read_project_config_in(paths, project)
|
||||
.map(|c| c.terminal)
|
||||
.unwrap_or_default();
|
||||
write_terminal_desktop(paths, project, &cfg);
|
||||
}
|
||||
let dir = applications_dir(paths);
|
||||
if let Ok(entries) = fs::read_dir(&dir) {
|
||||
for e in entries.flatten() {
|
||||
let name = e.file_name();
|
||||
let Some(name) = name.to_str() else { continue };
|
||||
if let Some(project) = name
|
||||
.strip_prefix("aicontrol-")
|
||||
.and_then(|n| n.strip_suffix(".desktop"))
|
||||
{
|
||||
if !reg.contains_key(project) {
|
||||
let _ = fs::remove_file(e.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Icon eines Projekts als PNG-data-URL für die Übersicht; ICNS wird per
|
||||
/// sips nach PNG konvertiert, weil der Browser ICNS nicht rendert.
|
||||
#[tauri::command]
|
||||
@@ -837,20 +925,7 @@ fn project_icon(project: String) -> Result<Option<String>, String> {
|
||||
.and_then(|e| e.to_str())
|
||||
.is_some_and(|e| e.eq_ignore_ascii_case("icns"));
|
||||
let png = if is_icns {
|
||||
let tmp = std::env::temp_dir().join(format!("ai-control-icon-{project}.png"));
|
||||
let out = std::process::Command::new("sips")
|
||||
.args(["-s", "format", "png"])
|
||||
.arg(&path)
|
||||
.arg("--out")
|
||||
.arg(&tmp)
|
||||
.output()
|
||||
.map_err(|e| format!("sips: {e}"))?;
|
||||
if !out.status.success() {
|
||||
return Err(format!("sips: {}", String::from_utf8_lossy(&out.stderr)));
|
||||
}
|
||||
let bytes = fs::read(&tmp).map_err(|e| format!("{}: {e}", tmp.display()))?;
|
||||
let _ = fs::remove_file(&tmp);
|
||||
bytes
|
||||
icns_to_png_bytes(&path, &project)?
|
||||
} else {
|
||||
fs::read(&path).map_err(|e| format!("{}: {e}", path.display()))?
|
||||
};
|
||||
@@ -904,17 +979,49 @@ fn menu_icon(
|
||||
Ok(tauri::image::Image::new_owned(canvas, W as u32, H as u32))
|
||||
}
|
||||
|
||||
/// Projekt-Icon als 36×36-RGBA (sips konvertiert, auch ICNS).
|
||||
/// Projekt-Icon als 36×36-RGBA. PNG wird von tauri geladen und skaliert;
|
||||
/// ICNS läuft über sips (nur macOS).
|
||||
fn project_icon_rgba_36(
|
||||
paths: &Paths,
|
||||
project: &str,
|
||||
icon: &str,
|
||||
) -> Result<tauri::image::Image<'static>, String> {
|
||||
let src = resolve_icon_path(paths, icon);
|
||||
let is_icns = src
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.is_some_and(|e| e.eq_ignore_ascii_case("icns"));
|
||||
let img = if is_icns {
|
||||
tauri::image::Image::from_bytes(&icns_to_png_bytes(&src, project)?).map_err(|e| e.to_string())?
|
||||
} else {
|
||||
tauri::image::Image::from_path(&src).map_err(|e| format!("{}: {e}", src.display()))?
|
||||
};
|
||||
Ok(scale_rgba(&img, 36))
|
||||
}
|
||||
|
||||
/// Nearest-Neighbor-Skalierung eines RGBA-Bildes auf ein Quadrat der Kantenlänge `size`.
|
||||
fn scale_rgba(img: &tauri::image::Image, size: usize) -> tauri::image::Image<'static> {
|
||||
let (sw, sh) = (img.width() as usize, img.height() as usize);
|
||||
let src = img.rgba();
|
||||
let mut out = vec![0u8; size * size * 4];
|
||||
for y in 0..size {
|
||||
for x in 0..size {
|
||||
let sx = x * sw / size;
|
||||
let sy = y * sh / size;
|
||||
let si = (sy * sw + sx) * 4;
|
||||
let di = (y * size + x) * 4;
|
||||
out[di..di + 4].copy_from_slice(&src[si..si + 4]);
|
||||
}
|
||||
}
|
||||
tauri::image::Image::new_owned(out, size as u32, size as u32)
|
||||
}
|
||||
|
||||
/// ICNS → PNG-Bytes via sips (nur macOS; Linux hat kein ICNS).
|
||||
fn icns_to_png_bytes(src: &std::path::Path, project: &str) -> Result<Vec<u8>, String> {
|
||||
let tmp = std::env::temp_dir().join(format!("ai-control-tray-icon-{project}.png"));
|
||||
let out = Command::new("sips")
|
||||
.args(["-s", "format", "png", "-z", "36", "36"])
|
||||
.arg(&src)
|
||||
.args(["-s", "format", "png"])
|
||||
.arg(src)
|
||||
.arg("--out")
|
||||
.arg(&tmp)
|
||||
.output()
|
||||
@@ -924,7 +1031,7 @@ fn project_icon_rgba_36(
|
||||
}
|
||||
let bytes = fs::read(&tmp).map_err(|e| format!("{}: {e}", tmp.display()))?;
|
||||
let _ = fs::remove_file(&tmp);
|
||||
tauri::image::Image::from_bytes(&bytes).map_err(|e| e.to_string())
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
/// Terminal-Einstellungen eines Projekts, für den Terminal-Prozess.
|
||||
@@ -1818,6 +1925,33 @@ fn set_sync_setting(enabled: bool) -> Result<(), String> {
|
||||
set_sync_on_session_end_in(&Paths::real(), enabled)
|
||||
}
|
||||
|
||||
/// Terminal-Schriftgröße (settings.json: terminalFontSize), ein Wert für alle
|
||||
/// Terminals. Default 13.
|
||||
#[tauri::command]
|
||||
fn terminal_font_size() -> u32 {
|
||||
fs::read_to_string(Paths::real().config_dir().join(APP_SETTINGS_FILE))
|
||||
.ok()
|
||||
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
|
||||
.and_then(|v| v["terminalFontSize"].as_u64())
|
||||
.map(|n| n as u32)
|
||||
.unwrap_or(13)
|
||||
}
|
||||
|
||||
/// Setzt die Schriftgröße; erhält übrige App-settings.
|
||||
#[tauri::command]
|
||||
fn set_terminal_font_size(size: u32) -> Result<(), String> {
|
||||
let paths = Paths::real();
|
||||
let path = paths.config_dir().join(APP_SETTINGS_FILE);
|
||||
let mut v: serde_json::Value = fs::read_to_string(&path)
|
||||
.ok()
|
||||
.and_then(|s| serde_json::from_str(&s).ok())
|
||||
.unwrap_or_else(|| serde_json::json!({}));
|
||||
v["terminalFontSize"] = serde_json::json!(size);
|
||||
fs::create_dir_all(paths.config_dir()).map_err(|e| e.to_string())?;
|
||||
let raw = serde_json::to_string_pretty(&v).map_err(|e| e.to_string())?;
|
||||
fs::write(&path, raw + "\n").map_err(|e| format!("{}: {e}", path.display()))
|
||||
}
|
||||
|
||||
/// Verlinkt die synced Runtime eines bestehenden Pools. Nur bei idlem Pool —
|
||||
/// sonst würde das Transkript der laufenden Session ersetzt.
|
||||
#[tauri::command]
|
||||
@@ -1839,6 +1973,10 @@ pub fn run() {
|
||||
// damit jedes Terminal ein eigenes Dock-Icon bekommt.
|
||||
Some("--terminal") => {
|
||||
let project = args.next().expect("--terminal braucht einen Projektnamen");
|
||||
// Linux: eigene Wayland-app_id pro Terminal-Prozess -> eigener Dock-Eintrag
|
||||
// (muss vor dem GTK-Init stehen). Test, ob set_prgname die app_id setzt.
|
||||
#[cfg(target_os = "linux")]
|
||||
glib::set_prgname(Some(format!("aicontrol-{project}").as_str()));
|
||||
let icon = terminal_config(&project)
|
||||
.expect("Projekt-Config nicht lesbar")
|
||||
.icon
|
||||
@@ -1908,6 +2046,72 @@ fn start_or_focus(app: &tauri::AppHandle, project: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Tray-/Popup-Klick: Projekt starten oder das laufende Terminal fokussieren.
|
||||
#[tauri::command]
|
||||
fn start_or_focus_cmd(app: tauri::AppHandle, project: String) {
|
||||
start_or_focus(&app, &project);
|
||||
}
|
||||
|
||||
/// Popup-Fußzeile „Öffnen": das Hauptfenster zeigen.
|
||||
#[tauri::command]
|
||||
fn open_main_window(app: tauri::AppHandle) {
|
||||
use tauri::Manager;
|
||||
if let Some(w) = app.get_webview_window("main") {
|
||||
let _ = w.show();
|
||||
let _ = w.set_focus();
|
||||
}
|
||||
}
|
||||
|
||||
/// Popup-Fußzeile „Beenden": App verlassen.
|
||||
#[tauri::command]
|
||||
fn quit_app(app: tauri::AppHandle) {
|
||||
app.exit(0);
|
||||
}
|
||||
|
||||
/// D-Bus-Relay für die GNOME-Extension: Show()/Hide() steuern das Popup-Fenster.
|
||||
/// Die Extension ruft Show() beim Panel-Klick und positioniert das Fenster selbst.
|
||||
#[cfg(target_os = "linux")]
|
||||
struct PopupService {
|
||||
app: tauri::AppHandle,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[zbus::interface(name = "com.aicontrol.Popup1")]
|
||||
impl PopupService {
|
||||
fn show(&self) {
|
||||
use tauri::Manager;
|
||||
let app = self.app.clone();
|
||||
let win = app.clone();
|
||||
let _ = app.run_on_main_thread(move || {
|
||||
if let Some(w) = win.get_webview_window("popup") {
|
||||
let _ = w.show();
|
||||
let _ = w.set_focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
fn hide(&self) {
|
||||
use tauri::Manager;
|
||||
let app = self.app.clone();
|
||||
let win = app.clone();
|
||||
let _ = app.run_on_main_thread(move || {
|
||||
if let Some(w) = win.get_webview_window("popup") {
|
||||
let _ = w.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn serve_popup_dbus(app: tauri::AppHandle) -> zbus::Result<()> {
|
||||
let _conn = zbus::blocking::connection::Builder::session()?
|
||||
.name("com.aicontrol.Popup")?
|
||||
.serve_at("/com/aicontrol/Popup", PopupService { app })?
|
||||
.build()?;
|
||||
loop {
|
||||
std::thread::park();
|
||||
}
|
||||
}
|
||||
|
||||
/// Haupt-App: reine Tray-App ohne Dock-Eintrag.
|
||||
fn main_builder() -> tauri::Builder<tauri::Wry> {
|
||||
use tauri::Manager;
|
||||
@@ -1935,38 +2139,77 @@ fn main_builder() -> tauri::Builder<tauri::Wry> {
|
||||
// hält das Tray-Menü aktuell.
|
||||
spawn_session_watcher(app.handle().clone());
|
||||
|
||||
// Alle pro-Terminal-.desktop-Dateien neu schreiben + verwaiste entfernen,
|
||||
// damit sie da sind, bevor ein Terminal startet.
|
||||
#[cfg(target_os = "linux")]
|
||||
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.
|
||||
tauri::WebviewWindowBuilder::new(app, "main", tauri::WebviewUrl::default())
|
||||
let main_win = tauri::WebviewWindowBuilder::new(app, "main", tauri::WebviewUrl::default())
|
||||
.title("ai-control")
|
||||
.inner_size(800.0, 600.0)
|
||||
.visible(false)
|
||||
.build()?;
|
||||
.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()?;
|
||||
|
||||
let menu = tray_menu(app.handle())?;
|
||||
tauri::tray::TrayIconBuilder::with_id("main")
|
||||
// include_bytes! statt default_window_icon: cargo trackt die Datei,
|
||||
// neu generierte Icons landen damit sicher im nächsten Build.
|
||||
.icon(tauri::image::Image::from_bytes(include_bytes!(
|
||||
"../icons/trayTemplate.png"
|
||||
))?)
|
||||
// Template-Icon: macOS färbt es passend zur Menüleiste ein.
|
||||
.icon_as_template(true)
|
||||
.menu(&menu)
|
||||
.on_menu_event(|app, event| match event.id.as_ref() {
|
||||
"open" => {
|
||||
let w = app.get_webview_window("main").unwrap();
|
||||
w.show().unwrap();
|
||||
w.set_focus().unwrap();
|
||||
}
|
||||
"quit" => app.exit(0),
|
||||
id => {
|
||||
if let Some(project) = id.strip_prefix("project:") {
|
||||
start_or_focus(app, project);
|
||||
// macOS: natives Tray-Menü (rendert Icons pro Eintrag). Unter Linux/GNOME
|
||||
// übernimmt die Shell-Extension den Panel-Button + Popup — kein Tray hier,
|
||||
// sonst erschiene das aIC-Icon doppelt.
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let menu = tray_menu(app.handle())?;
|
||||
tauri::tray::TrayIconBuilder::with_id("main")
|
||||
.icon(tauri::image::Image::from_bytes(include_bytes!(
|
||||
"../icons/trayTemplate.png"
|
||||
))?)
|
||||
.icon_as_template(true)
|
||||
.menu(&menu)
|
||||
.on_menu_event(|app, event| match event.id.as_ref() {
|
||||
"open" => {
|
||||
let w = app.get_webview_window("main").unwrap();
|
||||
w.show().unwrap();
|
||||
w.set_focus().unwrap();
|
||||
}
|
||||
"quit" => app.exit(0),
|
||||
id => {
|
||||
if let Some(project) = id.strip_prefix("project:") {
|
||||
start_or_focus(app, project);
|
||||
}
|
||||
}
|
||||
})
|
||||
.build(app)?;
|
||||
}
|
||||
|
||||
// Linux/GNOME: rahmenloses Popup-Fenster + D-Bus-Relay. Die Shell-Extension
|
||||
// (ai-control-popup@local) ruft Show() und schiebt das Fenster unter ihren
|
||||
// Panel-Button — GNOME liefert der App selbst keinen Tray-Klick.
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
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()?;
|
||||
let app_dbus = app.handle().clone();
|
||||
std::thread::spawn(move || {
|
||||
if let Err(e) = serve_popup_dbus(app_dbus) {
|
||||
eprintln!("D-Bus-Popup-Dienst: {e}");
|
||||
}
|
||||
})
|
||||
.build(app)?;
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
@@ -1976,6 +2219,13 @@ fn main_builder() -> tauri::Builder<tauri::Wry> {
|
||||
api.prevent_close();
|
||||
window.hide().unwrap();
|
||||
}
|
||||
// Popup schließt bei Fokusverlust.
|
||||
#[cfg(target_os = "linux")]
|
||||
if let tauri::WindowEvent::Focused(false) = event {
|
||||
if window.label() == "popup" {
|
||||
let _ = window.hide();
|
||||
}
|
||||
}
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
list_projects,
|
||||
@@ -2002,8 +2252,13 @@ fn main_builder() -> tauri::Builder<tauri::Wry> {
|
||||
usage_stats,
|
||||
stop_project,
|
||||
restart_project,
|
||||
start_or_focus_cmd,
|
||||
open_main_window,
|
||||
quit_app,
|
||||
sync_setting,
|
||||
set_sync_setting,
|
||||
terminal_font_size,
|
||||
set_terminal_font_size,
|
||||
link_pool_runtime,
|
||||
oauth_login,
|
||||
keychain_status,
|
||||
@@ -2036,7 +2291,9 @@ fn terminal_builder(project: String) -> tauri::Builder<tauri::Wry> {
|
||||
// Header im Terminal-Prozess: Projektliste, Icon und Pool-Name.
|
||||
list_projects,
|
||||
project_icon,
|
||||
pool_label
|
||||
pool_label,
|
||||
terminal_font_size,
|
||||
set_terminal_font_size
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ pub fn open_terminal(app: AppHandle, project: String) -> Result<(), String> {
|
||||
|
||||
/// 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;
|
||||
@@ -50,8 +51,13 @@ fn yield_activation_to_bundle(bundle_id: &str) {
|
||||
.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;
|
||||
@@ -62,6 +68,14 @@ pub fn activate_self(app: &AppHandle) {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
@@ -95,9 +109,14 @@ pub fn set_dock_icon(path: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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};
|
||||
@@ -111,6 +130,11 @@ pub fn focus_terminal(pid: u32) {
|
||||
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.
|
||||
@@ -136,6 +160,10 @@ pub fn build_window(
|
||||
.title_bar_style(tauri::TitleBarStyle::Overlay)
|
||||
.hidden_title(true);
|
||||
|
||||
// Linux/GNOME: keine GTK-Deko — eigene Titelleiste im Header (terminal.html).
|
||||
#[cfg(target_os = "linux")]
|
||||
let builder = builder.decorations(false);
|
||||
|
||||
builder.build()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user