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"]);
|
||||
|
||||
@@ -34,19 +34,52 @@ pub fn open_terminal(project: String) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fenster-Hintergrund je Theme — muss zu den Theme-Definitionen in
|
||||
/// terminal.ts passen, sonst blitzt beim Öffnen die falsche Farbe auf.
|
||||
fn theme_background(theme: &str) -> (u8, u8, u8) {
|
||||
match theme {
|
||||
"dracula" => (0x28, 0x2a, 0x36),
|
||||
"solarized-dark" => (0x00, 0x2b, 0x36),
|
||||
"gruvbox" => (0x28, 0x28, 0x28),
|
||||
"one-dark" => (0x28, 0x2c, 0x34),
|
||||
_ => (0x1e, 0x1e, 0x2e), // Catppuccin Mocha
|
||||
}
|
||||
}
|
||||
|
||||
/// Setzt das Dock-Icon dieses Terminal-Prozesses aus einer PNG/ICNS-Datei.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn set_dock_icon(path: &str) {
|
||||
use objc2::{AnyThread, MainThreadMarker};
|
||||
use objc2_app_kit::{NSApplication, NSImage};
|
||||
use objc2_foundation::NSString;
|
||||
|
||||
let mtm = MainThreadMarker::new().expect("set_dock_icon läuft nicht auf dem Main-Thread");
|
||||
let img = NSImage::initWithContentsOfFile(NSImage::alloc(), &NSString::from_str(path))
|
||||
.unwrap_or_else(|| panic!("Dock-Icon nicht ladbar: {path}"));
|
||||
// unsafe laut objc2-Signatur; das NSImage stammt aus einer Datei und ist gültig.
|
||||
unsafe {
|
||||
NSApplication::sharedApplication(mtm).setApplicationIconImage(Some(&img));
|
||||
}
|
||||
}
|
||||
|
||||
/// Baut das Terminal-Fenster des Terminal-Prozesses. Die PTY entsteht erst,
|
||||
/// wenn das Fenster geladen ist und `term_start` ruft — so gehen keine
|
||||
/// Ausgaben verloren, bevor der Event-Listener steht.
|
||||
pub fn build_window(app: &AppHandle, project: &str) -> tauri::Result<()> {
|
||||
pub fn build_window(
|
||||
app: &AppHandle,
|
||||
project: &str,
|
||||
cfg: &crate::TerminalConfig,
|
||||
) -> tauri::Result<()> {
|
||||
let (r, g, b) = theme_background(cfg.theme.as_deref().unwrap_or_default());
|
||||
let title = cfg.title.as_deref().unwrap_or(project);
|
||||
let builder = WebviewWindowBuilder::new(
|
||||
app,
|
||||
format!("term-{project}"),
|
||||
WebviewUrl::App(format!("terminal.html?project={project}").into()),
|
||||
)
|
||||
.title(format!("{project} — Session"))
|
||||
.title(format!("{title} — Session"))
|
||||
.inner_size(980.0, 640.0)
|
||||
// Theme-Hintergrund (Catppuccin Mocha), sonst blitzt beim Öffnen Weiß auf.
|
||||
.background_color(tauri::window::Color(0x1e, 0x1e, 0x2e, 0xff));
|
||||
.background_color(tauri::window::Color(r, g, b, 0xff));
|
||||
|
||||
// Titelbar liegt über dem Inhalt; der Header in terminal.html ist Drag-Region.
|
||||
#[cfg(target_os = "macos")]
|
||||
|
||||
Reference in New Issue
Block a user