diff --git a/gnome-shell-extension/ai-control-popup@local/extension.js b/gnome-shell-extension/ai-control-popup@local/extension.js index 43413c0..295e18a 100644 --- a/gnome-shell-extension/ai-control-popup@local/extension.js +++ b/gnome-shell-extension/ai-control-popup@local/extension.js @@ -18,7 +18,6 @@ const DBUS_NAME = 'com.aicontrol.Popup'; const DBUS_PATH = '/com/aicontrol/Popup'; const DBUS_IFACE = 'com.aicontrol.Popup1'; const WIN_TITLE = 'ai-control-popup'; -const ICON_PATH = '/home/marcuh/projects/ai-control/src-tauri/icons/trayLinux.png'; export default class AiControlPopupExtension extends Extension { enable() { @@ -59,7 +58,8 @@ export default class AiControlPopupExtension extends Extension { // dontCreateMenu=true: wir wollen kein Shell-Menü, nur den Klick relayen. this._button = new PanelMenu.Button(0.0, 'ai-control', true); const icon = new St.Icon({ - gicon: Gio.icon_new_for_string(ICON_PATH), + // Icon liegt neben der Extension (vom Paket mitgeliefert). + gicon: Gio.icon_new_for_string(`${this.path}/trayLinux.png`), icon_size: 36, }); this._button.add_child(icon); diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 29fe745..dbc23a7 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -40,6 +40,7 @@ dependencies = [ "objc2-app-kit", "objc2-foundation", "portable-pty", + "rfd 0.15.4", "serde", "serde_json", "sha2", @@ -3118,6 +3119,30 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfd" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.59.0", +] + [[package]] name = "rfd" version = "0.16.0" @@ -3976,7 +4001,7 @@ checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884" dependencies = [ "log", "raw-window-handle", - "rfd", + "rfd 0.16.0", "serde", "serde_json", "tauri", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 2418c95..4c42f64 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -14,6 +14,10 @@ rust-version = "1.77.2" name = "app_lib" crate-type = ["staticlib", "cdylib", "rlib"] +# Zeilennummern im Release-Backtrace (sonst zeigt der Panic-Tracer nur Adressen). +[profile.release] +debug = 1 + [build-dependencies] tauri-build = { version = "2.6.3", features = [] } @@ -34,6 +38,7 @@ keyring = { version = "3", features = ["apple-native", "sync-secret-service", "w [target.'cfg(target_os = "linux")'.dependencies] zbus = "5" glib = "0.18" +rfd = { version = "0.15", default-features = false, features = ["gtk3"] } [target.'cfg(target_os = "macos")'.dependencies] objc2 = "0.6" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 93404ad..3ca0b51 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -111,7 +111,7 @@ pub(crate) fn set_project_archive_dir(project: &str, dir: &str) -> Result<(), St fn add_archive_permission(paths: &Paths, project: &str, dir: &str) -> Result<(), String> { let sp = settings_path(&project_dir(paths, project)?); let mut v: serde_json::Value = if sp.is_file() { - serde_json::from_str(&fs::read_to_string(&sp).map_err(|e| e.to_string())?) + serde_json::from_str(&fs::read_to_string(&sp).map_err(|e| format!("{}: {e}", sp.display()))?) .map_err(|e| format!("{}: {e}", sp.display()))? } else { serde_json::json!({}) @@ -140,7 +140,7 @@ fn add_archive_permission(paths: &Paths, project: &str, dir: &str) -> Result<(), allow.push(serde_json::json!(edit)); } let parent = sp.parent().ok_or("settings.json ohne Elternordner")?; - fs::create_dir_all(parent).map_err(|e| e.to_string())?; + fs::create_dir_all(parent).map_err(|e| format!("{}: {e}", parent.display()))?; let raw = serde_json::to_string_pretty(&v).map_err(|e| e.to_string())?; fs::write(&sp, raw + "\n").map_err(|e| format!("{}: {e}", sp.display())) } @@ -275,7 +275,8 @@ fn save_registry( let contracted: std::collections::BTreeMap<&String, String> = reg.iter().map(|(name, dir)| (name, contract_home(paths, dir))).collect(); let raw = serde_json::to_string_pretty(&contracted).map_err(|e| e.to_string())?; - fs::create_dir_all(paths.config_dir()).map_err(|e| e.to_string())?; + fs::create_dir_all(paths.config_dir()) + .map_err(|e| format!("{}: {e}", paths.config_dir().display()))?; let file = paths.projects_file(); fs::write(&file, raw + "\n").map_err(|e| format!("{}: {e}", file.display())) } @@ -423,7 +424,8 @@ fn set_sync_on_session_end_in(paths: &Paths, enabled: bool) -> Result<(), String .and_then(|s| serde_json::from_str(&s).ok()) .unwrap_or_else(|| serde_json::json!({})); v["syncOnSessionEnd"] = serde_json::json!(enabled); - fs::create_dir_all(paths.config_dir()).map_err(|e| e.to_string())?; + fs::create_dir_all(paths.config_dir()) + .map_err(|e| format!("{}: {e}", paths.config_dir().display()))?; 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())) } @@ -702,9 +704,12 @@ fn create_project_full_in( } } - fs::create_dir_all(dir.join(".claude")).map_err(|e| e.to_string())?; - fs::create_dir_all(dir.join("memory")).map_err(|e| e.to_string())?; - fs::write(dir.join(".gitignore"), ".ai-control-running\n").map_err(|e| e.to_string())?; + fs::create_dir_all(dir.join(".claude")) + .map_err(|e| format!("{}: {e}", dir.join(".claude").display()))?; + fs::create_dir_all(dir.join("memory")) + .map_err(|e| format!("{}: {e}", dir.join("memory").display()))?; + fs::write(dir.join(".gitignore"), ".ai-control-running\n") + .map_err(|e| format!("{}: {e}", dir.join(".gitignore").display()))?; let contracted = contract_home(paths, &dir); let mut allow = vec![format!("Edit({contracted}/**)")]; @@ -721,7 +726,8 @@ fn create_project_full_in( }, }); let raw = serde_json::to_string_pretty(&settings).map_err(|e| e.to_string())?; - fs::write(dir.join(".claude").join("settings.json"), raw + "\n").map_err(|e| e.to_string())?; + fs::write(dir.join(".claude").join("settings.json"), raw + "\n") + .map_err(|e| format!("{}: {e}", dir.join(".claude").join("settings.json").display()))?; reg.insert(name.to_string(), dir.clone()); save_registry(paths, ®)?; @@ -814,7 +820,8 @@ fn create_project_in(paths: &Paths, name: &str) -> Result<(), String> { if dir.exists() { return Err(format!("Projekt existiert bereits: {name}")); } - fs::create_dir_all(dir.join(".claude")).map_err(|e| e.to_string())?; + fs::create_dir_all(dir.join(".claude")) + .map_err(|e| format!("{}: {e}", dir.join(".claude").display()))?; register_project(paths, name, &dir) } @@ -924,7 +931,7 @@ fn delete_project_in(paths: &Paths, name: &str, delete_work_dirs: bool) -> Resul fs::remove_dir_all(&wd_path).map_err(|e| format!("{}: {e}", wd_path.display()))?; } } - fs::remove_dir_all(&dir).map_err(|e| e.to_string())?; + fs::remove_dir_all(&dir).map_err(|e| format!("{}: {e}", dir.display()))?; unregister_project(paths, name)?; #[cfg(target_os = "linux")] remove_terminal_desktop(paths, name); @@ -947,7 +954,8 @@ fn write_project_config_in( cfg: &ProjectConfig, ) -> Result<(), String> { let raw = serde_json::to_string_pretty(cfg).map_err(|e| e.to_string())?; - fs::write(project_config_path(paths, project)?, raw + "\n").map_err(|e| e.to_string()) + let path = project_config_path(paths, project)?; + fs::write(&path, raw + "\n").map_err(|e| format!("{}: {e}", path.display())) } fn assign_pool_in(paths: &Paths, project: &str, pool: &str) -> Result<(), String> { @@ -1116,7 +1124,7 @@ fn projects_using_pool(paths: &Paths, pool: &str) -> Result, String> if !cfg_path.is_file() { continue; } - let raw = fs::read_to_string(&cfg_path).map_err(|e| e.to_string())?; + let raw = fs::read_to_string(&cfg_path).map_err(|e| format!("{}: {e}", cfg_path.display()))?; let cfg: ProjectConfig = serde_json::from_str(&raw).map_err(|e| e.to_string())?; if cfg.pool.as_deref() == Some(pool) { users.push(name); @@ -1260,14 +1268,18 @@ struct PoolInfo { fn list_pools_in(paths: &Paths, store: &dyn ApikeyStore) -> Result, String> { let mut pools = Vec::new(); - let entries = fs::read_dir(paths.pools_dir()).map_err(|e| e.to_string())?; + if !paths.pools_dir().is_dir() { + return Ok(pools); + } + let entries = + fs::read_dir(paths.pools_dir()).map_err(|e| format!("{}: {e}", paths.pools_dir().display()))?; for entry in entries { - let entry = entry.map_err(|e| e.to_string())?; + let entry = entry.map_err(|e| format!("{}: {e}", paths.pools_dir().display()))?; let cfg_path = entry.path().join(POOL_FILE); if !cfg_path.is_file() { continue; } - let raw = fs::read_to_string(&cfg_path).map_err(|e| e.to_string())?; + let raw = fs::read_to_string(&cfg_path).map_err(|e| format!("{}: {e}", cfg_path.display()))?; let pool: Pool = serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", cfg_path.display()))?; let id = entry.file_name().to_string_lossy().into_owned(); @@ -1305,13 +1317,15 @@ fn pool_names(paths: &Paths) -> Result, String> { if !paths.pools_dir().is_dir() { return Ok(out); } - for entry in fs::read_dir(paths.pools_dir()).map_err(|e| e.to_string())? { - let entry = entry.map_err(|e| e.to_string())?; + for entry in + fs::read_dir(paths.pools_dir()).map_err(|e| format!("{}: {e}", paths.pools_dir().display()))? + { + let entry = entry.map_err(|e| format!("{}: {e}", paths.pools_dir().display()))?; let cfg_path = entry.path().join(POOL_FILE); if !cfg_path.is_file() { continue; } - let raw = fs::read_to_string(&cfg_path).map_err(|e| e.to_string())?; + let raw = fs::read_to_string(&cfg_path).map_err(|e| format!("{}: {e}", cfg_path.display()))?; let pool: Pool = serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", cfg_path.display()))?; out.push((entry.file_name().to_string_lossy().into_owned(), pool.name)); @@ -1330,21 +1344,23 @@ fn check_new_pool(paths: &Paths, name: &str) -> Result { } fn write_pool_json(dir: &PathBuf, name: &str, credential_type: &str) -> Result<(), String> { - fs::create_dir_all(dir).map_err(|e| e.to_string())?; + fs::create_dir_all(dir).map_err(|e| format!("{}: {e}", dir.display()))?; let pool = Pool { name: name.to_string(), credential_type: credential_type.to_string(), }; let raw = serde_json::to_string_pretty(&pool).map_err(|e| e.to_string())?; - fs::write(dir.join(POOL_FILE), raw + "\n").map_err(|e| e.to_string()) + fs::write(dir.join(POOL_FILE), raw + "\n") + .map_err(|e| format!("{}: {e}", dir.join(POOL_FILE).display())) } fn write_secret_file(path: &PathBuf, content: &str) -> Result<(), String> { if let Some(parent) = path.parent() { - fs::create_dir_all(parent).map_err(|e| e.to_string())?; + fs::create_dir_all(parent).map_err(|e| format!("{}: {e}", parent.display()))?; } - fs::write(path, content).map_err(|e| e.to_string())?; - fs::set_permissions(path, fs::Permissions::from_mode(0o600)).map_err(|e| e.to_string()) + fs::write(path, content).map_err(|e| format!("{}: {e}", path.display()))?; + fs::set_permissions(path, fs::Permissions::from_mode(0o600)) + .map_err(|e| format!("{}: {e}", path.display())) } /// Grundausstattung eines Pool-Ordners (= CLAUDE_CONFIG_DIR): settings.json @@ -1367,12 +1383,13 @@ fn init_pool_config( base.insert(k.clone(), v.clone()); } } - fs::create_dir_all(dir).map_err(|e| e.to_string())?; + fs::create_dir_all(dir).map_err(|e| format!("{}: {e}", dir.display()))?; let raw = serde_json::to_string_pretty(&settings).map_err(|e| e.to_string())?; - fs::write(dir.join("settings.json"), raw + "\n").map_err(|e| e.to_string())?; + fs::write(dir.join("settings.json"), raw + "\n") + .map_err(|e| format!("{}: {e}", dir.join("settings.json").display()))?; let claude_md = dir.join("CLAUDE.md"); if !claude_md.is_file() { - fs::write(&claude_md, "").map_err(|e| e.to_string())?; + fs::write(&claude_md, "").map_err(|e| format!("{}: {e}", claude_md.display()))?; } install_panel_skill(dir); register_mcp_server(dir); @@ -1533,20 +1550,21 @@ fn link_pool_runtime_in(paths: &Paths, pool: &str) -> Result<(), String> { for (name, is_dir) in SYNCED_RUNTIME { let target = data.join(name); if is_dir { - fs::create_dir_all(&target).map_err(|e| e.to_string())?; + fs::create_dir_all(&target).map_err(|e| format!("{}: {e}", target.display()))?; } else { - fs::create_dir_all(&data).map_err(|e| e.to_string())?; + fs::create_dir_all(&data).map_err(|e| format!("{}: {e}", data.display()))?; if !target.exists() { - fs::write(&target, "").map_err(|e| e.to_string())?; + fs::write(&target, "").map_err(|e| format!("{}: {e}", target.display()))?; } } let link = src.join(name); if link.is_dir() && !link.is_symlink() { - fs::remove_dir_all(&link).map_err(|e| e.to_string())?; + fs::remove_dir_all(&link).map_err(|e| format!("{}: {e}", link.display()))?; } else if link.is_symlink() || link.exists() { - fs::remove_file(&link).map_err(|e| e.to_string())?; + fs::remove_file(&link).map_err(|e| format!("{}: {e}", link.display()))?; } - std::os::unix::fs::symlink(&target, &link).map_err(|e| e.to_string())?; + std::os::unix::fs::symlink(&target, &link) + .map_err(|e| format!("{}: {e}", link.display()))?; } Ok(()) } @@ -1626,7 +1644,7 @@ fn delete_pool_in(paths: &Paths, store: &dyn ApikeyStore, name: &str) -> Result< for project in projects_using_pool(paths, name)? { unassign_pool_in(paths, &project)?; } - fs::remove_dir_all(&dir).map_err(|e| e.to_string()) + fs::remove_dir_all(&dir).map_err(|e| format!("{}: {e}", dir.display())) } /// Schreibt den API-Key eines apikey-Pools neu: in den Keychain/Keyring, die @@ -1652,7 +1670,7 @@ fn set_apikey_in( let key_path = dir.join(APIKEY_FILE); if store.set(pool, key).is_ok() { if key_path.is_file() { - fs::remove_file(&key_path).map_err(|e| e.to_string())?; + fs::remove_file(&key_path).map_err(|e| format!("{}: {e}", key_path.display()))?; } } else { if !allow_file { @@ -1667,7 +1685,7 @@ fn set_apikey_in( .map_err(|e| format!("{}: {e}", settings_path.display()))?; settings["apiKeyHelper"] = serde_json::json!(apikey_helper_command(&dir, pool)); let raw = serde_json::to_string_pretty(&settings).map_err(|e| e.to_string())?; - fs::write(&settings_path, raw + "\n").map_err(|e| e.to_string()) + fs::write(&settings_path, raw + "\n").map_err(|e| format!("{}: {e}", settings_path.display())) } fn ensure_oauth_pool(paths: &Paths, pool: &str) -> Result<(), String> { @@ -1814,8 +1832,13 @@ fn usage_stats_in(paths: &Paths, days: u32) -> Result, String> { let mut seen = HashSet::new(); let mut rows: HashMap<(String, String), UsageTotals> = HashMap::new(); - for pool_entry in fs::read_dir(paths.pools_dir()).map_err(|e| e.to_string())? { - let pool_entry = pool_entry.map_err(|e| e.to_string())?; + if !paths.pools_dir().is_dir() { + return Ok(Vec::new()); + } + for pool_entry in + fs::read_dir(paths.pools_dir()).map_err(|e| format!("{}: {e}", paths.pools_dir().display()))? + { + let pool_entry = pool_entry.map_err(|e| format!("{}: {e}", paths.pools_dir().display()))?; let id = pool_entry.file_name().to_string_lossy().into_owned(); // Anzeigename aus pool.json; Ordner ohne pool.json unter der ID ausweisen. let pool = if pool_entry.path().join(POOL_FILE).is_file() { @@ -1827,8 +1850,10 @@ fn usage_stats_in(paths: &Paths, days: u32) -> Result, String> { if !projects_root.is_dir() { continue; } - for proj_entry in fs::read_dir(&projects_root).map_err(|e| e.to_string())? { - let proj_entry = proj_entry.map_err(|e| e.to_string())?; + for proj_entry in + fs::read_dir(&projects_root).map_err(|e| format!("{}: {e}", projects_root.display()))? + { + let proj_entry = proj_entry.map_err(|e| format!("{}: {e}", projects_root.display()))?; if !proj_entry.path().is_dir() { continue; } @@ -1837,8 +1862,10 @@ fn usage_stats_in(paths: &Paths, days: u32) -> Result, String> { // kodierten Namen ausweisen statt verschlucken. let project = names.get(&encoded).cloned().unwrap_or(encoded); let totals = rows.entry((pool.clone(), project)).or_default(); - for file in fs::read_dir(proj_entry.path()).map_err(|e| e.to_string())? { - let file = file.map_err(|e| e.to_string())?; + for file in fs::read_dir(proj_entry.path()) + .map_err(|e| format!("{}: {e}", proj_entry.path().display()))? + { + let file = file.map_err(|e| format!("{}: {e}", proj_entry.path().display()))?; let path = file.path(); if path.extension().and_then(|e| e.to_str()) != Some("jsonl") { continue; @@ -2152,7 +2179,8 @@ fn set_terminal_font_size(size: u32) -> Result<(), String> { .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())?; + fs::create_dir_all(paths.config_dir()) + .map_err(|e| format!("{}: {e}", paths.config_dir().display()))?; 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())) } @@ -2169,7 +2197,29 @@ fn link_pool_runtime(pool: String) -> Result<(), String> { } #[cfg_attr(mobile, tauri::mobile_entry_point)] +/// Panic-Tracer: erzwingt bei jedem Panic einen vollständigen Backtrace +/// (unabhängig von RUST_BACKTRACE), gibt ihn klar abgegrenzt auf stderr aus und +/// schreibt ihn zusätzlich in eine kopierbare Datei (Pfad pro Prozess, damit +/// Haupt- und Terminal-Prozesse sich nicht überschreiben). +fn install_panic_tracer() { + std::panic::set_hook(Box::new(|info| { + let bt = std::backtrace::Backtrace::force_capture(); + let dump = format!( + "\n========== ai-control PANIC (pid {}) ==========\n{info}\n\n{bt}\n========== END ==========\n", + std::process::id() + ); + eprintln!("{dump}"); + let path = + std::env::temp_dir().join(format!("ai-control-panic-{}.log", std::process::id())); + if std::fs::write(&path, &dump).is_ok() { + eprintln!("Backtrace kopierbar in: {}", path.display()); + } + })); +} + pub fn run() { + install_panic_tracer(); + let mut args = std::env::args().skip(1); let first = args.next(); @@ -2180,6 +2230,24 @@ pub fn run() { return; } + // AppImage ist installationsfrei und kann die GNOME-Popup-Extension nicht + // ausrollen/aktivieren. Unter GNOME daher auf das deb-/rpm-Paket verweisen und + // beenden. KDE/XFCE zeigen das Tray ohne Extension — dort läuft die AppImage. + #[cfg(target_os = "linux")] + if std::env::var_os("APPIMAGE").is_some() && is_gnome() { + rfd::MessageDialog::new() + .set_level(rfd::MessageLevel::Warning) + .set_title("ai-control") + .set_description( + "Die AppImage-Fassung wird unter GNOME nicht unterstützt: Das Tray-Icon \ + braucht eine GNOME-Shell-Extension, die nur das deb-/rpm-Paket \ + installiert. Bitte das deb- bzw. rpm-Paket verwenden.", + ) + .set_buttons(rfd::MessageButtons::Ok) + .show(); + std::process::exit(1); + } + // generate_context! darf pro Crate nur einmal expandieren (_EMBED_INFO_PLIST). let context = tauri::generate_context!(); match first.as_deref() { @@ -2331,6 +2399,15 @@ impl PopupService { } } +/// GNOME-Session? (XDG_CURRENT_DESKTOP enthält „GNOME"). Steuert AppImage-Sperre +/// und Extension-Aktivierung. +#[cfg(target_os = "linux")] +fn is_gnome() -> bool { + std::env::var("XDG_CURRENT_DESKTOP") + .map(|d| d.to_uppercase().contains("GNOME")) + .unwrap_or(false) +} + #[cfg(target_os = "linux")] fn serve_popup_dbus(app: tauri::AppHandle) -> zbus::Result<()> { let _conn = zbus::blocking::connection::Builder::session()? @@ -2455,17 +2532,51 @@ fn main_builder() -> tauri::Builder { .build(app)?; } - // Linux/GNOME: 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. + // Linux: GNOME kann kein SNI-Tray, daher dort der Extension/D-Bus-Relay; + // alle anderen Desktops (KDE, Cinnamon, LXQt, …) bekommen ein natives + // Tray-Icon. libappindicator liefert keine Klick-Events, nur ein Menü — + // deshalb Menü statt Klick-Popup wie auf macOS/Windows. #[cfg(target_os = "linux")] { - 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}"); - } - }); + use tauri::Manager; + if is_gnome() { + // Die Extension liegt systemweit (aus dem Paket), aktiviert wird sie + // pro Benutzer in dconf — das erledigt die im Benutzerkontext + // laufende App. Show() der Extension schiebt das Popup unter den + // Panel-Button. + let _ = Command::new("gnome-extensions") + .args(["enable", "ai-control-popup@local"]) + .status(); + 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}"); + } + }); + } else { + use tauri::menu::MenuItemBuilder; + use tauri::menu::MenuBuilder; + use tauri::tray::TrayIconBuilder; + let open = MenuItemBuilder::with_id("open", "ai-control öffnen").build(app)?; + let quit = MenuItemBuilder::with_id("quit", "Beenden").build(app)?; + let menu = MenuBuilder::new(app).items(&[&open, &quit]).build()?; + let icon = tauri::image::Image::from_bytes(include_bytes!("../icons/trayLinux.png"))?; + TrayIconBuilder::with_id("main") + .icon(icon) + .menu(&menu) + .on_menu_event(|app, event| match event.id().as_ref() { + "open" => { + if let Some(w) = app.get_webview_window("popup") { + let _ = w.center(); + let _ = w.show(); + let _ = w.set_focus(); + } + } + "quit" => app.exit(0), + _ => {} + }) + .build(app)?; + } } Ok(()) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index c69a043..63adb06 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -18,7 +18,7 @@ }, "bundle": { "active": true, - "targets": ["deb", "rpm", "app", "dmg", "nsis", "msi"], + "targets": ["deb", "rpm", "appimage", "app", "dmg", "nsis", "msi"], "icon": [ "icons/32x32.png", "icons/128x128.png", @@ -26,6 +26,24 @@ "icons/icon.icns", "icons/icon.ico" ], + "linux": { + "deb": { + "depends": ["libayatana-appindicator3-1"], + "files": { + "/usr/share/gnome-shell/extensions/ai-control-popup@local/extension.js": "../gnome-shell-extension/ai-control-popup@local/extension.js", + "/usr/share/gnome-shell/extensions/ai-control-popup@local/metadata.json": "../gnome-shell-extension/ai-control-popup@local/metadata.json", + "/usr/share/gnome-shell/extensions/ai-control-popup@local/trayLinux.png": "icons/trayLinux.png" + } + }, + "rpm": { + "depends": ["libappindicator-gtk3"], + "files": { + "/usr/share/gnome-shell/extensions/ai-control-popup@local/extension.js": "../gnome-shell-extension/ai-control-popup@local/extension.js", + "/usr/share/gnome-shell/extensions/ai-control-popup@local/metadata.json": "../gnome-shell-extension/ai-control-popup@local/metadata.json", + "/usr/share/gnome-shell/extensions/ai-control-popup@local/trayLinux.png": "icons/trayLinux.png" + } + } + }, "android": { "debugApplicationIdSuffix": ".debug" }