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:
@@ -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);
|
||||
|
||||
Generated
+26
-1
@@ -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",
|
||||
|
||||
@@ -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
@@ -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<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(())
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user