Terminal-Config (Theme/Icon/Titel), UI-Überarbeitung, i18n de/en
- Terminal-Einstellungen pro Projekt in ai-control.json: 5 Themes (Paletten in terminal.ts, Fenster-BG gespiegelt in terminal.rs), Dock-Icon (PNG/ICNS via NSApplication in RunEvent::Ready), Fenstertitel mit Fallback Projektname; Zahnrad-Dialog mit Datei-Picker (tauri-plugin-dialog) - UI: Catppuccin-Mocha-Token, JetBrains Mono für Namen/Pfade/Chips, feste Tabellenspalten, Status-Punkt mit Glow, Pool-Projekte als Hover-Popover, Buttons ausgerichtet (Zahnrad links von Starten/Beenden) - i18n: vue-i18n, Deutsch/Englisch, Umschalter im Header (localStorage) - generate_context nur noch einmal expandiert (Release-Build-Fehler)
This commit is contained in:
+110
-27
@@ -58,6 +58,7 @@ struct Project {
|
||||
path: String,
|
||||
pool: Option<String>,
|
||||
running: bool,
|
||||
terminal: TerminalConfig,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -67,9 +68,28 @@ struct Pool {
|
||||
credential_type: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
struct ProjectConfig {
|
||||
pool: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pool: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "TerminalConfig::is_empty")]
|
||||
terminal: TerminalConfig,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Clone)]
|
||||
pub struct TerminalConfig {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub theme: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub icon: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
impl TerminalConfig {
|
||||
fn is_empty(&self) -> bool {
|
||||
self.theme.is_none() && self.icon.is_none() && self.title.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
fn bundle_id(project: &str) -> String {
|
||||
@@ -130,20 +150,13 @@ fn list_projects_in(paths: &Paths) -> Result<Vec<Project>, String> {
|
||||
continue;
|
||||
}
|
||||
let name = entry.file_name().to_string_lossy().into_owned();
|
||||
let cfg_path = path.join(PROJECT_FILE);
|
||||
let pool = if cfg_path.is_file() {
|
||||
let raw = fs::read_to_string(&cfg_path).map_err(|e| e.to_string())?;
|
||||
let cfg: ProjectConfig = serde_json::from_str(&raw)
|
||||
.map_err(|e| format!("{}: {e}", cfg_path.display()))?;
|
||||
Some(cfg.pool)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let cfg = read_project_config_in(paths, &name)?;
|
||||
projects.push(Project {
|
||||
running: is_running(&name),
|
||||
path: path.to_string_lossy().into_owned(),
|
||||
pool: cfg.pool,
|
||||
terminal: cfg.terminal,
|
||||
name,
|
||||
pool,
|
||||
});
|
||||
}
|
||||
projects.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
@@ -168,18 +181,59 @@ fn delete_project_in(paths: &Paths, name: &str) -> Result<(), String> {
|
||||
fs::remove_dir_all(&dir).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn read_project_config_in(paths: &Paths, project: &str) -> Result<ProjectConfig, String> {
|
||||
let cfg_path = paths.project_config(project);
|
||||
if !cfg_path.is_file() {
|
||||
return Ok(ProjectConfig::default());
|
||||
}
|
||||
let raw =
|
||||
fs::read_to_string(&cfg_path).map_err(|e| format!("{}: {e}", cfg_path.display()))?;
|
||||
serde_json::from_str(&raw).map_err(|e| format!("{}: {e}", cfg_path.display()))
|
||||
}
|
||||
|
||||
fn write_project_config_in(
|
||||
paths: &Paths,
|
||||
project: &str,
|
||||
cfg: &ProjectConfig,
|
||||
) -> Result<(), String> {
|
||||
let raw = serde_json::to_string_pretty(cfg).map_err(|e| e.to_string())?;
|
||||
fs::write(paths.project_config(project), raw + "\n").map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn assign_pool_in(paths: &Paths, project: &str, pool: &str) -> Result<(), String> {
|
||||
if !paths.pool_dir(pool).join(POOL_FILE).is_file() {
|
||||
return Err(format!("Pool existiert nicht: {pool}"));
|
||||
}
|
||||
let cfg = ProjectConfig { pool: pool.to_string() };
|
||||
let raw = serde_json::to_string_pretty(&cfg).map_err(|e| e.to_string())?;
|
||||
fs::write(paths.project_config(project), raw + "\n").map_err(|e| e.to_string())
|
||||
let mut cfg = read_project_config_in(paths, project)?;
|
||||
cfg.pool = Some(pool.to_string());
|
||||
write_project_config_in(paths, project, &cfg)
|
||||
}
|
||||
|
||||
/// Nimmt den Pool raus; die Config-Datei bleibt nur, wenn sie noch
|
||||
/// Terminal-Einstellungen trägt.
|
||||
fn unassign_pool_in(paths: &Paths, project: &str) -> Result<(), String> {
|
||||
let cfg_path = paths.project_config(project);
|
||||
fs::remove_file(&cfg_path).map_err(|e| format!("{}: {e}", cfg_path.display()))
|
||||
let mut cfg = read_project_config_in(paths, project)?;
|
||||
cfg.pool = None;
|
||||
if cfg.terminal.is_empty() {
|
||||
let cfg_path = paths.project_config(project);
|
||||
return fs::remove_file(&cfg_path).map_err(|e| format!("{}: {e}", cfg_path.display()));
|
||||
}
|
||||
write_project_config_in(paths, project, &cfg)
|
||||
}
|
||||
|
||||
fn set_terminal_config_in(
|
||||
paths: &Paths,
|
||||
project: &str,
|
||||
terminal: TerminalConfig,
|
||||
) -> Result<(), String> {
|
||||
let mut cfg = read_project_config_in(paths, project)?;
|
||||
cfg.terminal = terminal;
|
||||
write_project_config_in(paths, project, &cfg)
|
||||
}
|
||||
|
||||
/// Terminal-Einstellungen eines Projekts, für den Terminal-Prozess.
|
||||
pub(crate) fn terminal_config(project: &str) -> Result<TerminalConfig, String> {
|
||||
Ok(read_project_config_in(&Paths::real(), project)?.terminal)
|
||||
}
|
||||
|
||||
/// Namen aller Projekte, die `pool` zugeordnet haben.
|
||||
@@ -194,7 +248,7 @@ fn projects_using_pool(paths: &Paths, pool: &str) -> Result<Vec<String>, String>
|
||||
}
|
||||
let raw = fs::read_to_string(&cfg_path).map_err(|e| e.to_string())?;
|
||||
let cfg: ProjectConfig = serde_json::from_str(&raw).map_err(|e| e.to_string())?;
|
||||
if cfg.pool == pool {
|
||||
if cfg.pool.as_deref() == Some(pool) {
|
||||
users.push(entry.file_name().to_string_lossy().into_owned());
|
||||
}
|
||||
}
|
||||
@@ -372,6 +426,16 @@ fn unassign_pool(project: String) -> Result<(), String> {
|
||||
unassign_pool_in(&Paths::real(), &project)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn set_terminal_config(
|
||||
project: String,
|
||||
theme: Option<String>,
|
||||
icon: Option<String>,
|
||||
title: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
set_terminal_config_in(&Paths::real(), &project, TerminalConfig { theme, icon, title })
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn set_apikey(pool: String, key: String) -> Result<(), String> {
|
||||
set_apikey_in(&Paths::real(), &pool, &key)
|
||||
@@ -675,18 +739,34 @@ fn pct_decode(s: &str) -> String {
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
// generate_context! darf pro Crate nur einmal expandieren (_EMBED_INFO_PLIST).
|
||||
let context = tauri::generate_context!();
|
||||
let mut args = std::env::args().skip(1);
|
||||
let builder = match args.next().as_deref() {
|
||||
match args.next().as_deref() {
|
||||
// `app --terminal <projekt>`: eigener Prozess pro Terminal-Fenster,
|
||||
// damit jedes Terminal ein eigenes Dock-Icon bekommt.
|
||||
Some("--terminal") => {
|
||||
terminal_builder(args.next().expect("--terminal braucht einen Projektnamen"))
|
||||
let project = args.next().expect("--terminal braucht einen Projektnamen");
|
||||
let icon = terminal_config(&project)
|
||||
.expect("Projekt-Config nicht lesbar")
|
||||
.icon;
|
||||
terminal_builder(project)
|
||||
.build(context)
|
||||
.expect("error while building tauri application")
|
||||
// Das Dock-Icon erst nach dem App-Start setzen: in setup() gesetzt
|
||||
// überschreibt macOS es beim Anlegen des Dock-Tiles wieder.
|
||||
.run(move |_app, event| {
|
||||
if let tauri::RunEvent::Ready = event {
|
||||
if let Some(icon) = icon.as_deref() {
|
||||
terminal::set_dock_icon(icon);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
_ => main_builder(),
|
||||
};
|
||||
builder
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
_ => main_builder()
|
||||
.run(context)
|
||||
.expect("error while running tauri application"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Haupt-App: reine Tray-App ohne Dock-Eintrag.
|
||||
@@ -698,6 +778,7 @@ fn main_builder() -> tauri::Builder<tauri::Wry> {
|
||||
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
|
||||
None,
|
||||
))
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.setup(|app| {
|
||||
if cfg!(debug_assertions) {
|
||||
app.handle().plugin(
|
||||
@@ -755,6 +836,7 @@ fn main_builder() -> tauri::Builder<tauri::Wry> {
|
||||
delete_pool,
|
||||
assign_pool,
|
||||
unassign_pool,
|
||||
set_terminal_config,
|
||||
start_project,
|
||||
stop_project,
|
||||
restart_project,
|
||||
@@ -771,7 +853,8 @@ fn terminal_builder(project: String) -> tauri::Builder<tauri::Wry> {
|
||||
tauri::Builder::default()
|
||||
.manage(terminal::Terminals::default())
|
||||
.setup(move |app| {
|
||||
terminal::build_window(app.handle(), &project)?;
|
||||
let cfg = terminal_config(&project)?;
|
||||
terminal::build_window(app.handle(), &project, &cfg)?;
|
||||
Ok(())
|
||||
})
|
||||
// Fenster zu → PTY-Session abräumen; danach endet der Prozess.
|
||||
@@ -982,7 +1065,7 @@ mod tests {
|
||||
let cfg: ProjectConfig =
|
||||
serde_json::from_str(&fs::read_to_string(p.project_config("proj")).unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(cfg.pool, "kunde");
|
||||
assert_eq!(cfg.pool.as_deref(), Some("kunde"));
|
||||
|
||||
let pools = list_pools_in(&p).unwrap();
|
||||
assert_eq!(pools[0].projects, vec!["proj"]);
|
||||
|
||||
Reference in New Issue
Block a user