Linux: nativer SNI-Tray für Nicht-GNOME-Desktops, pfadtragende fs-Fehler, Panic-Tracer

- Tray verzweigt: GNOME -> Extension/D-Bus, KDE/Cinnamon/LXQt -> natives
  SNI-Tray-Icon mit Menue (libappindicator liefert keine Klick-Events)
- fs-Aufrufe nennen bei Fehlern den Pfad statt bloss 'os error 2';
  is_dir-Guard in list_pools_in und Usage-Auswertung
- Panic-Hook schreibt kopierbaren Backtrace (profile.release debug = 1)
- AppImage unter GNOME gesperrt (rfd-Dialog), Verweis auf deb/rpm
- deb/rpm liefern die GNOME-Extension mit; Appindicator als Depends
- Bundle-Targets um deb/rpm/appimage erweitert
This commit is contained in:
marcus hinz
2026-07-12 18:06:35 +02:00
parent b1a2291aef
commit cf844c45da
5 changed files with 214 additions and 55 deletions
@@ -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);
+26 -1
View File
@@ -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",
+5
View File
@@ -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"
+162 -51
View File
@@ -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, &reg)?;
@@ -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<Vec<String>, 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<Vec<PoolInfo>, 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<Vec<(String, String)>, 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<PathBuf, String> {
}
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<Vec<UsageRow>, 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<Vec<UsageRow>, 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<Vec<UsageRow>, 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<tauri::Wry> {
.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(())
+19 -1
View File
@@ -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"
}