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_PATH = '/com/aicontrol/Popup';
|
||||||
const DBUS_IFACE = 'com.aicontrol.Popup1';
|
const DBUS_IFACE = 'com.aicontrol.Popup1';
|
||||||
const WIN_TITLE = 'ai-control-popup';
|
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 {
|
export default class AiControlPopupExtension extends Extension {
|
||||||
enable() {
|
enable() {
|
||||||
@@ -59,7 +58,8 @@ export default class AiControlPopupExtension extends Extension {
|
|||||||
// dontCreateMenu=true: wir wollen kein Shell-Menü, nur den Klick relayen.
|
// dontCreateMenu=true: wir wollen kein Shell-Menü, nur den Klick relayen.
|
||||||
this._button = new PanelMenu.Button(0.0, 'ai-control', true);
|
this._button = new PanelMenu.Button(0.0, 'ai-control', true);
|
||||||
const icon = new St.Icon({
|
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,
|
icon_size: 36,
|
||||||
});
|
});
|
||||||
this._button.add_child(icon);
|
this._button.add_child(icon);
|
||||||
|
|||||||
Generated
+26
-1
@@ -40,6 +40,7 @@ dependencies = [
|
|||||||
"objc2-app-kit",
|
"objc2-app-kit",
|
||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
"portable-pty",
|
"portable-pty",
|
||||||
|
"rfd 0.15.4",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
@@ -3118,6 +3119,30 @@ dependencies = [
|
|||||||
"web-sys",
|
"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]]
|
[[package]]
|
||||||
name = "rfd"
|
name = "rfd"
|
||||||
version = "0.16.0"
|
version = "0.16.0"
|
||||||
@@ -3976,7 +4001,7 @@ checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
"rfd",
|
"rfd 0.16.0",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ rust-version = "1.77.2"
|
|||||||
name = "app_lib"
|
name = "app_lib"
|
||||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
|
# Zeilennummern im Release-Backtrace (sonst zeigt der Panic-Tracer nur Adressen).
|
||||||
|
[profile.release]
|
||||||
|
debug = 1
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2.6.3", features = [] }
|
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]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
zbus = "5"
|
zbus = "5"
|
||||||
glib = "0.18"
|
glib = "0.18"
|
||||||
|
rfd = { version = "0.15", default-features = false, features = ["gtk3"] }
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
objc2 = "0.6"
|
objc2 = "0.6"
|
||||||
|
|||||||
+156
-45
@@ -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> {
|
fn add_archive_permission(paths: &Paths, project: &str, dir: &str) -> Result<(), String> {
|
||||||
let sp = settings_path(&project_dir(paths, project)?);
|
let sp = settings_path(&project_dir(paths, project)?);
|
||||||
let mut v: serde_json::Value = if sp.is_file() {
|
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()))?
|
.map_err(|e| format!("{}: {e}", sp.display()))?
|
||||||
} else {
|
} else {
|
||||||
serde_json::json!({})
|
serde_json::json!({})
|
||||||
@@ -140,7 +140,7 @@ fn add_archive_permission(paths: &Paths, project: &str, dir: &str) -> Result<(),
|
|||||||
allow.push(serde_json::json!(edit));
|
allow.push(serde_json::json!(edit));
|
||||||
}
|
}
|
||||||
let parent = sp.parent().ok_or("settings.json ohne Elternordner")?;
|
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())?;
|
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()))
|
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> =
|
let contracted: std::collections::BTreeMap<&String, String> =
|
||||||
reg.iter().map(|(name, dir)| (name, contract_home(paths, dir))).collect();
|
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())?;
|
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();
|
let file = paths.projects_file();
|
||||||
fs::write(&file, raw + "\n").map_err(|e| format!("{}: {e}", file.display()))
|
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())
|
.and_then(|s| serde_json::from_str(&s).ok())
|
||||||
.unwrap_or_else(|| serde_json::json!({}));
|
.unwrap_or_else(|| serde_json::json!({}));
|
||||||
v["syncOnSessionEnd"] = serde_json::json!(enabled);
|
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())?;
|
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()))
|
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(".claude"))
|
||||||
fs::create_dir_all(dir.join("memory")).map_err(|e| e.to_string())?;
|
.map_err(|e| format!("{}: {e}", dir.join(".claude").display()))?;
|
||||||
fs::write(dir.join(".gitignore"), ".ai-control-running\n").map_err(|e| e.to_string())?;
|
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 contracted = contract_home(paths, &dir);
|
||||||
let mut allow = vec![format!("Edit({contracted}/**)")];
|
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())?;
|
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());
|
reg.insert(name.to_string(), dir.clone());
|
||||||
save_registry(paths, ®)?;
|
save_registry(paths, ®)?;
|
||||||
@@ -814,7 +820,8 @@ fn create_project_in(paths: &Paths, name: &str) -> Result<(), String> {
|
|||||||
if dir.exists() {
|
if dir.exists() {
|
||||||
return Err(format!("Projekt existiert bereits: {name}"));
|
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)
|
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(&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)?;
|
unregister_project(paths, name)?;
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
remove_terminal_desktop(paths, name);
|
remove_terminal_desktop(paths, name);
|
||||||
@@ -947,7 +954,8 @@ fn write_project_config_in(
|
|||||||
cfg: &ProjectConfig,
|
cfg: &ProjectConfig,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let raw = serde_json::to_string_pretty(cfg).map_err(|e| e.to_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> {
|
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() {
|
if !cfg_path.is_file() {
|
||||||
continue;
|
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())?;
|
let cfg: ProjectConfig = serde_json::from_str(&raw).map_err(|e| e.to_string())?;
|
||||||
if cfg.pool.as_deref() == Some(pool) {
|
if cfg.pool.as_deref() == Some(pool) {
|
||||||
users.push(name);
|
users.push(name);
|
||||||
@@ -1260,14 +1268,18 @@ struct PoolInfo {
|
|||||||
|
|
||||||
fn list_pools_in(paths: &Paths, store: &dyn ApikeyStore) -> Result<Vec<PoolInfo>, String> {
|
fn list_pools_in(paths: &Paths, store: &dyn ApikeyStore) -> Result<Vec<PoolInfo>, String> {
|
||||||
let mut pools = Vec::new();
|
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 {
|
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);
|
let cfg_path = entry.path().join(POOL_FILE);
|
||||||
if !cfg_path.is_file() {
|
if !cfg_path.is_file() {
|
||||||
continue;
|
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 =
|
let pool: Pool =
|
||||||
serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", cfg_path.display()))?;
|
serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", cfg_path.display()))?;
|
||||||
let id = entry.file_name().to_string_lossy().into_owned();
|
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() {
|
if !paths.pools_dir().is_dir() {
|
||||||
return Ok(out);
|
return Ok(out);
|
||||||
}
|
}
|
||||||
for entry in fs::read_dir(paths.pools_dir()).map_err(|e| e.to_string())? {
|
for entry in
|
||||||
let entry = entry.map_err(|e| e.to_string())?;
|
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);
|
let cfg_path = entry.path().join(POOL_FILE);
|
||||||
if !cfg_path.is_file() {
|
if !cfg_path.is_file() {
|
||||||
continue;
|
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 =
|
let pool: Pool =
|
||||||
serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", cfg_path.display()))?;
|
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));
|
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> {
|
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 {
|
let pool = Pool {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
credential_type: credential_type.to_string(),
|
credential_type: credential_type.to_string(),
|
||||||
};
|
};
|
||||||
let raw = serde_json::to_string_pretty(&pool).map_err(|e| e.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> {
|
fn write_secret_file(path: &PathBuf, content: &str) -> Result<(), String> {
|
||||||
if let Some(parent) = path.parent() {
|
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::write(path, content).map_err(|e| format!("{}: {e}", path.display()))?;
|
||||||
fs::set_permissions(path, fs::Permissions::from_mode(0o600)).map_err(|e| e.to_string())
|
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
|
/// Grundausstattung eines Pool-Ordners (= CLAUDE_CONFIG_DIR): settings.json
|
||||||
@@ -1367,12 +1383,13 @@ fn init_pool_config(
|
|||||||
base.insert(k.clone(), v.clone());
|
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())?;
|
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");
|
let claude_md = dir.join("CLAUDE.md");
|
||||||
if !claude_md.is_file() {
|
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);
|
install_panel_skill(dir);
|
||||||
register_mcp_server(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 {
|
for (name, is_dir) in SYNCED_RUNTIME {
|
||||||
let target = data.join(name);
|
let target = data.join(name);
|
||||||
if is_dir {
|
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 {
|
} 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() {
|
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);
|
let link = src.join(name);
|
||||||
if link.is_dir() && !link.is_symlink() {
|
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() {
|
} 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(())
|
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)? {
|
for project in projects_using_pool(paths, name)? {
|
||||||
unassign_pool_in(paths, &project)?;
|
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
|
/// 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);
|
let key_path = dir.join(APIKEY_FILE);
|
||||||
if store.set(pool, key).is_ok() {
|
if store.set(pool, key).is_ok() {
|
||||||
if key_path.is_file() {
|
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 {
|
} else {
|
||||||
if !allow_file {
|
if !allow_file {
|
||||||
@@ -1667,7 +1685,7 @@ fn set_apikey_in(
|
|||||||
.map_err(|e| format!("{}: {e}", settings_path.display()))?;
|
.map_err(|e| format!("{}: {e}", settings_path.display()))?;
|
||||||
settings["apiKeyHelper"] = serde_json::json!(apikey_helper_command(&dir, pool));
|
settings["apiKeyHelper"] = serde_json::json!(apikey_helper_command(&dir, pool));
|
||||||
let raw = serde_json::to_string_pretty(&settings).map_err(|e| e.to_string())?;
|
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> {
|
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 seen = HashSet::new();
|
||||||
let mut rows: HashMap<(String, String), UsageTotals> = HashMap::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())? {
|
if !paths.pools_dir().is_dir() {
|
||||||
let pool_entry = pool_entry.map_err(|e| e.to_string())?;
|
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();
|
let id = pool_entry.file_name().to_string_lossy().into_owned();
|
||||||
// Anzeigename aus pool.json; Ordner ohne pool.json unter der ID ausweisen.
|
// Anzeigename aus pool.json; Ordner ohne pool.json unter der ID ausweisen.
|
||||||
let pool = if pool_entry.path().join(POOL_FILE).is_file() {
|
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() {
|
if !projects_root.is_dir() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for proj_entry in fs::read_dir(&projects_root).map_err(|e| e.to_string())? {
|
for proj_entry in
|
||||||
let proj_entry = proj_entry.map_err(|e| e.to_string())?;
|
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() {
|
if !proj_entry.path().is_dir() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1837,8 +1862,10 @@ fn usage_stats_in(paths: &Paths, days: u32) -> Result<Vec<UsageRow>, String> {
|
|||||||
// kodierten Namen ausweisen statt verschlucken.
|
// kodierten Namen ausweisen statt verschlucken.
|
||||||
let project = names.get(&encoded).cloned().unwrap_or(encoded);
|
let project = names.get(&encoded).cloned().unwrap_or(encoded);
|
||||||
let totals = rows.entry((pool.clone(), project)).or_default();
|
let totals = rows.entry((pool.clone(), project)).or_default();
|
||||||
for file in fs::read_dir(proj_entry.path()).map_err(|e| e.to_string())? {
|
for file in fs::read_dir(proj_entry.path())
|
||||||
let file = file.map_err(|e| e.to_string())?;
|
.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();
|
let path = file.path();
|
||||||
if path.extension().and_then(|e| e.to_str()) != Some("jsonl") {
|
if path.extension().and_then(|e| e.to_str()) != Some("jsonl") {
|
||||||
continue;
|
continue;
|
||||||
@@ -2152,7 +2179,8 @@ fn set_terminal_font_size(size: u32) -> Result<(), String> {
|
|||||||
.and_then(|s| serde_json::from_str(&s).ok())
|
.and_then(|s| serde_json::from_str(&s).ok())
|
||||||
.unwrap_or_else(|| serde_json::json!({}));
|
.unwrap_or_else(|| serde_json::json!({}));
|
||||||
v["terminalFontSize"] = serde_json::json!(size);
|
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())?;
|
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()))
|
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)]
|
#[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() {
|
pub fn run() {
|
||||||
|
install_panic_tracer();
|
||||||
|
|
||||||
let mut args = std::env::args().skip(1);
|
let mut args = std::env::args().skip(1);
|
||||||
let first = args.next();
|
let first = args.next();
|
||||||
|
|
||||||
@@ -2180,6 +2230,24 @@ pub fn run() {
|
|||||||
return;
|
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).
|
// generate_context! darf pro Crate nur einmal expandieren (_EMBED_INFO_PLIST).
|
||||||
let context = tauri::generate_context!();
|
let context = tauri::generate_context!();
|
||||||
match first.as_deref() {
|
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")]
|
#[cfg(target_os = "linux")]
|
||||||
fn serve_popup_dbus(app: tauri::AppHandle) -> zbus::Result<()> {
|
fn serve_popup_dbus(app: tauri::AppHandle) -> zbus::Result<()> {
|
||||||
let _conn = zbus::blocking::connection::Builder::session()?
|
let _conn = zbus::blocking::connection::Builder::session()?
|
||||||
@@ -2455,17 +2532,51 @@ fn main_builder() -> tauri::Builder<tauri::Wry> {
|
|||||||
.build(app)?;
|
.build(app)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Linux/GNOME: D-Bus-Relay. Die Shell-Extension (ai-control-popup@local)
|
// Linux: GNOME kann kein SNI-Tray, daher dort der Extension/D-Bus-Relay;
|
||||||
// ruft Show() und schiebt das Fenster unter ihren Panel-Button — GNOME
|
// alle anderen Desktops (KDE, Cinnamon, LXQt, …) bekommen ein natives
|
||||||
// liefert der App selbst keinen Tray-Klick.
|
// Tray-Icon. libappindicator liefert keine Klick-Events, nur ein Menü —
|
||||||
|
// deshalb Menü statt Klick-Popup wie auf macOS/Windows.
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
|
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();
|
let app_dbus = app.handle().clone();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
if let Err(e) = serve_popup_dbus(app_dbus) {
|
if let Err(e) = serve_popup_dbus(app_dbus) {
|
||||||
eprintln!("D-Bus-Popup-Dienst: {e}");
|
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(())
|
Ok(())
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
"targets": ["deb", "rpm", "app", "dmg", "nsis", "msi"],
|
"targets": ["deb", "rpm", "appimage", "app", "dmg", "nsis", "msi"],
|
||||||
"icon": [
|
"icon": [
|
||||||
"icons/32x32.png",
|
"icons/32x32.png",
|
||||||
"icons/128x128.png",
|
"icons/128x128.png",
|
||||||
@@ -26,6 +26,24 @@
|
|||||||
"icons/icon.icns",
|
"icons/icon.icns",
|
||||||
"icons/icon.ico"
|
"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": {
|
"android": {
|
||||||
"debugApplicationIdSuffix": ".debug"
|
"debugApplicationIdSuffix": ".debug"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user