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:
+125
-22
@@ -1,12 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref } from "vue";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
|
||||
interface Project {
|
||||
name: string;
|
||||
path: string;
|
||||
pool: string | null;
|
||||
running: boolean;
|
||||
terminal: { theme: string | null; icon: string | null; title: string | null };
|
||||
}
|
||||
|
||||
interface Pool {
|
||||
@@ -84,6 +86,58 @@ async function restartNow() {
|
||||
restarting.value = false;
|
||||
}
|
||||
|
||||
const THEME_NAMES: [string, string][] = [
|
||||
["mocha", "Catppuccin Mocha"],
|
||||
["dracula", "Dracula"],
|
||||
["solarized-dark", "Solarized Dark"],
|
||||
["gruvbox", "Gruvbox"],
|
||||
["one-dark", "One Dark"],
|
||||
];
|
||||
|
||||
interface TerminalSettings {
|
||||
name: string;
|
||||
theme: string;
|
||||
icon: string | null;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const settings = ref<TerminalSettings | null>(null);
|
||||
|
||||
function openSettings(p: Project) {
|
||||
settings.value = {
|
||||
name: p.name,
|
||||
theme: p.terminal.theme ?? "mocha",
|
||||
icon: p.terminal.icon,
|
||||
title: p.terminal.title ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
async function pickIcon() {
|
||||
const file = await open({
|
||||
multiple: false,
|
||||
directory: false,
|
||||
filters: [{ name: "Icon", extensions: ["png", "icns"] }],
|
||||
});
|
||||
if (typeof file === "string") settings.value!.icon = file;
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
const s = settings.value!;
|
||||
try {
|
||||
const title = s.title.trim();
|
||||
await invoke("set_terminal_config", {
|
||||
project: s.name,
|
||||
theme: s.theme === "mocha" ? null : s.theme,
|
||||
icon: s.icon,
|
||||
title: title === "" ? null : title,
|
||||
});
|
||||
settings.value = null;
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
error.value = String(e);
|
||||
}
|
||||
}
|
||||
|
||||
let timer: number;
|
||||
onMounted(() => {
|
||||
refresh();
|
||||
@@ -94,61 +148,110 @@ onUnmounted(() => window.clearInterval(timer));
|
||||
|
||||
<template>
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<table>
|
||||
<table class="grid">
|
||||
<colgroup>
|
||||
<col class="col-dot" />
|
||||
<col />
|
||||
<col class="col-pool" />
|
||||
<col class="col-actions" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Projekt</th>
|
||||
<th>Pool</th>
|
||||
<th>{{ $t("projects.project") }}</th>
|
||||
<th>{{ $t("projects.pool") }}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="p in projects" :key="p.name">
|
||||
<td>
|
||||
<td class="cell-dot">
|
||||
<span class="dot" :class="{ on: p.running }"></span>
|
||||
</td>
|
||||
<td>
|
||||
<td class="cell-name">
|
||||
<strong>{{ p.name }}</strong>
|
||||
<small>{{ p.path }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<select :value="p.pool ?? ''" @change="assign(p, $event)">
|
||||
<option value="" disabled>– kein Pool –</option>
|
||||
<option value="" disabled>{{ $t("projects.noPool") }}</option>
|
||||
<option v-for="pool in pools" :key="pool.name" :value="pool.name">
|
||||
{{ pool.name }}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<button v-if="p.running" class="stop" @click="stop(p)">Beenden</button>
|
||||
<button v-else class="start" @click="openTerminal(p)">Starten</button>
|
||||
<td class="cell-actions">
|
||||
<span class="row-actions">
|
||||
<button
|
||||
class="gear"
|
||||
:title="$t('projects.terminalSettings')"
|
||||
@click="openSettings(p)"
|
||||
>
|
||||
⚙︎
|
||||
</button>
|
||||
<button v-if="p.running" class="stop" @click="stop(p)">
|
||||
{{ $t("projects.stop") }}
|
||||
</button>
|
||||
<button v-else class="start" @click="openTerminal(p)">
|
||||
{{ $t("projects.start") }}
|
||||
</button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div v-if="settings" class="overlay">
|
||||
<div class="dialog">
|
||||
<h3>{{ $t("projects.terminal", { name: settings.name }) }}</h3>
|
||||
<label class="field">
|
||||
{{ $t("projects.title") }}
|
||||
<input v-model="settings.title" :placeholder="settings.name" />
|
||||
</label>
|
||||
<label class="field">
|
||||
{{ $t("projects.theme") }}
|
||||
<select v-model="settings.theme">
|
||||
<option v-for="[value, label] in THEME_NAMES" :key="value" :value="value">
|
||||
{{ label }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field">
|
||||
{{ $t("projects.dockIcon") }}
|
||||
<span class="icon-row">
|
||||
<button @click="pickIcon">{{ $t("projects.chooseFile") }}</button>
|
||||
<button v-if="settings.icon" @click="settings.icon = null">
|
||||
{{ $t("projects.remove") }}
|
||||
</button>
|
||||
<span class="icon-path">
|
||||
{{ settings.icon ?? $t("projects.defaultIcon") }}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<p class="hint">{{ $t("projects.appliesNextStart") }}</p>
|
||||
<div class="actions">
|
||||
<button @click="settings = null">{{ $t("projects.cancel") }}</button>
|
||||
<button class="primary" @click="saveSettings">
|
||||
{{ $t("projects.save") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="pending" class="overlay">
|
||||
<div class="dialog">
|
||||
<h3>{{ pending.name }} läuft noch</h3>
|
||||
<p>
|
||||
Der Pool-Wechsel ist gespeichert. Die laufende Session arbeitet aber
|
||||
weiter im alten Pool — der neue gilt erst ab dem nächsten Start.
|
||||
</p>
|
||||
<h3>{{ $t("projects.stillRunning", { name: pending.name }) }}</h3>
|
||||
<p>{{ $t("projects.poolChangeSaved") }}</p>
|
||||
<div class="pools">
|
||||
<span class="chip">{{ pending.from ?? "kein Pool" }}</span>
|
||||
<span class="chip">{{ pending.from ?? $t("projects.noPoolAssigned") }}</span>
|
||||
<span class="arrow">→</span>
|
||||
<span class="chip new">{{ pending.to }}</span>
|
||||
</div>
|
||||
<p class="hint">
|
||||
Der Neustart beendet die laufende Session. Der automatische
|
||||
commit+push von claude-sync nach Sessionende entfällt dabei —
|
||||
Ghostty fragt bei laufendem Prozess vor dem Beenden noch einmal nach.
|
||||
</p>
|
||||
<p class="hint">{{ $t("projects.restartHint") }}</p>
|
||||
<div class="actions">
|
||||
<button @click="pending = null">Weiterlaufen lassen</button>
|
||||
<button @click="pending = null">{{ $t("projects.keepRunning") }}</button>
|
||||
<button class="primary" :disabled="restarting" @click="restartNow">
|
||||
{{ restarting ? "Starte neu …" : "Jetzt neu starten" }}
|
||||
{{ restarting ? $t("projects.restarting") : $t("projects.restartNow") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user