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:
Generated
+100
-1
@@ -11,12 +11,14 @@
|
|||||||
"@fontsource/jetbrains-mono": "^5.2.8",
|
"@fontsource/jetbrains-mono": "^5.2.8",
|
||||||
"@tauri-apps/api": "^2.11.1",
|
"@tauri-apps/api": "^2.11.1",
|
||||||
"@tauri-apps/plugin-autostart": "^2.5.1",
|
"@tauri-apps/plugin-autostart": "^2.5.1",
|
||||||
|
"@tauri-apps/plugin-dialog": "^2.7.1",
|
||||||
"@tauri-apps/plugin-opener": "^2.5.4",
|
"@tauri-apps/plugin-opener": "^2.5.4",
|
||||||
"@xterm/addon-fit": "^0.11.0",
|
"@xterm/addon-fit": "^0.11.0",
|
||||||
"@xterm/addon-ligatures": "^0.10.0",
|
"@xterm/addon-ligatures": "^0.10.0",
|
||||||
"@xterm/addon-webgl": "^0.19.0",
|
"@xterm/addon-webgl": "^0.19.0",
|
||||||
"@xterm/xterm": "^6.0.0",
|
"@xterm/xterm": "^6.0.0",
|
||||||
"vue": "^3.5.39"
|
"vue": "^3.5.39",
|
||||||
|
"vue-i18n": "^11.4.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2.11.4",
|
"@tauri-apps/cli": "^2.11.4",
|
||||||
@@ -117,6 +119,67 @@
|
|||||||
"url": "https://github.com/sponsors/ayuhito"
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@intlify/core-base": {
|
||||||
|
"version": "11.4.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.4.6.tgz",
|
||||||
|
"integrity": "sha512-EOeHO95XESK9IFHgHeZXunsM/WBAoCA0DlaWODvx14vKmetAuS97t+l6Xe9hTUqntPpF93vtVSjjUDafw3wXMw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@intlify/devtools-types": "11.4.6",
|
||||||
|
"@intlify/message-compiler": "11.4.6",
|
||||||
|
"@intlify/shared": "11.4.6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/kazupon"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@intlify/devtools-types": {
|
||||||
|
"version": "11.4.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.4.6.tgz",
|
||||||
|
"integrity": "sha512-wowQPpNem56b2d43IJmqbrzG2FeBKe5f/kUGlpNuBmXs6OSqncF8m1+1lxHuW8ISZJF0ma2RkW3iLkw0g0G4VA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@intlify/core-base": "11.4.6",
|
||||||
|
"@intlify/shared": "11.4.6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/kazupon"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@intlify/message-compiler": {
|
||||||
|
"version": "11.4.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.4.6.tgz",
|
||||||
|
"integrity": "sha512-5nj3jULqeTAC1WovwMs1LQWgatTa2pM/rXN9T3XW8rdOtXW9ZF6/GLSNFTKDQmPLwclhPdgUWLJ/4w3fMeeC/Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@intlify/shared": "11.4.6",
|
||||||
|
"source-map-js": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/kazupon"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@intlify/shared": {
|
||||||
|
"version": "11.4.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.4.6.tgz",
|
||||||
|
"integrity": "sha512-m1p1HHAMLhqSpTRH7VnXdrN0CQ4y+9vunFkpLkbD8soIuBsnQdawZXqMCgvwI2UVF9Ww7sVaw7g9tV2VO7shoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/kazupon"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@jridgewell/sourcemap-codec": {
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.5.5",
|
"version": "1.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||||
@@ -685,6 +748,15 @@
|
|||||||
"@tauri-apps/api": "^2.8.0"
|
"@tauri-apps/api": "^2.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tauri-apps/plugin-dialog": {
|
||||||
|
"version": "2.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.7.1.tgz",
|
||||||
|
"integrity": "sha512-OK1UBXYt+ojcmxMktzzuyonYIFta8CmAASpX+CA+DTGK24KlHjhYI6x2iOJ/TjZF4N7/ACK1oFmEOjIY9IhzOQ==",
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2.11.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tauri-apps/plugin-opener": {
|
"node_modules/@tauri-apps/plugin-opener": {
|
||||||
"version": "2.5.4",
|
"version": "2.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.4.tgz",
|
||||||
@@ -811,6 +883,12 @@
|
|||||||
"@vue/shared": "3.5.39"
|
"@vue/shared": "3.5.39"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vue/devtools-api": {
|
||||||
|
"version": "6.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
|
||||||
|
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@vue/language-core": {
|
"node_modules/@vue/language-core": {
|
||||||
"version": "3.3.6",
|
"version": "3.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.3.6.tgz",
|
||||||
@@ -1635,6 +1713,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-i18n": {
|
||||||
|
"version": "11.4.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.6.tgz",
|
||||||
|
"integrity": "sha512-l0gE7Rfy0phCa5ChKYkOq543Wgd39BCK6hkktfr1Ed4D99oRkgPK9ffShASZdeC8OJxGfdWmpYoAaAH6iLEuIg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@intlify/core-base": "11.4.6",
|
||||||
|
"@intlify/devtools-types": "11.4.6",
|
||||||
|
"@intlify/shared": "11.4.6",
|
||||||
|
"@vue/devtools-api": "^6.5.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/kazupon"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vue-tsc": {
|
"node_modules/vue-tsc": {
|
||||||
"version": "3.3.6",
|
"version": "3.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.3.6.tgz",
|
||||||
|
|||||||
+3
-1
@@ -13,12 +13,14 @@
|
|||||||
"@fontsource/jetbrains-mono": "^5.2.8",
|
"@fontsource/jetbrains-mono": "^5.2.8",
|
||||||
"@tauri-apps/api": "^2.11.1",
|
"@tauri-apps/api": "^2.11.1",
|
||||||
"@tauri-apps/plugin-autostart": "^2.5.1",
|
"@tauri-apps/plugin-autostart": "^2.5.1",
|
||||||
|
"@tauri-apps/plugin-dialog": "^2.7.1",
|
||||||
"@tauri-apps/plugin-opener": "^2.5.4",
|
"@tauri-apps/plugin-opener": "^2.5.4",
|
||||||
"@xterm/addon-fit": "^0.11.0",
|
"@xterm/addon-fit": "^0.11.0",
|
||||||
"@xterm/addon-ligatures": "^0.10.0",
|
"@xterm/addon-ligatures": "^0.10.0",
|
||||||
"@xterm/addon-webgl": "^0.19.0",
|
"@xterm/addon-webgl": "^0.19.0",
|
||||||
"@xterm/xterm": "^6.0.0",
|
"@xterm/xterm": "^6.0.0",
|
||||||
"vue": "^3.5.39"
|
"vue": "^3.5.39",
|
||||||
|
"vue-i18n": "^11.4.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2.11.4",
|
"@tauri-apps/cli": "^2.11.4",
|
||||||
|
|||||||
Generated
+168
-1
@@ -82,6 +82,9 @@ dependencies = [
|
|||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"getrandom 0.2.17",
|
"getrandom 0.2.17",
|
||||||
"log",
|
"log",
|
||||||
|
"objc2",
|
||||||
|
"objc2-app-kit",
|
||||||
|
"objc2-foundation",
|
||||||
"portable-pty",
|
"portable-pty",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -89,6 +92,7 @@ dependencies = [
|
|||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-autostart",
|
"tauri-plugin-autostart",
|
||||||
|
"tauri-plugin-dialog",
|
||||||
"tauri-plugin-log",
|
"tauri-plugin-log",
|
||||||
"ureq",
|
"ureq",
|
||||||
]
|
]
|
||||||
@@ -2067,9 +2071,17 @@ checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.0",
|
"bitflags 2.13.0",
|
||||||
"block2",
|
"block2",
|
||||||
|
"libc",
|
||||||
"objc2",
|
"objc2",
|
||||||
|
"objc2-cloud-kit",
|
||||||
|
"objc2-core-data",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
|
"objc2-core-graphics",
|
||||||
|
"objc2-core-image",
|
||||||
|
"objc2-core-text",
|
||||||
|
"objc2-core-video",
|
||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
|
"objc2-quartz-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2089,6 +2101,7 @@ version = "0.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
|
checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bitflags 2.13.0",
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
]
|
]
|
||||||
@@ -2149,6 +2162,19 @@ dependencies = [
|
|||||||
"objc2-core-graphics",
|
"objc2-core-graphics",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-core-video"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.0",
|
||||||
|
"objc2",
|
||||||
|
"objc2-core-foundation",
|
||||||
|
"objc2-core-graphics",
|
||||||
|
"objc2-io-surface",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2-encode"
|
name = "objc2-encode"
|
||||||
version = "4.1.0"
|
version = "4.1.0"
|
||||||
@@ -2172,6 +2198,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.0",
|
"bitflags 2.13.0",
|
||||||
"block2",
|
"block2",
|
||||||
|
"libc",
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
]
|
]
|
||||||
@@ -2742,6 +2769,30 @@ dependencies = [
|
|||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rfd"
|
||||||
|
version = "0.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
|
||||||
|
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.60.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.17.14"
|
version = "0.17.14"
|
||||||
@@ -3563,6 +3614,48 @@ dependencies = [
|
|||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-dialog"
|
||||||
|
version = "2.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"raw-window-handle",
|
||||||
|
"rfd",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"tauri-plugin-fs",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-fs"
|
||||||
|
version = "2.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"dunce",
|
||||||
|
"glob",
|
||||||
|
"log",
|
||||||
|
"objc2-foundation",
|
||||||
|
"percent-encoding",
|
||||||
|
"schemars 0.8.22",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_repr",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"tauri-utils",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"toml 1.1.2+spec-1.1.0",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-log"
|
name = "tauri-plugin-log"
|
||||||
version = "2.8.0"
|
version = "2.8.0"
|
||||||
@@ -4647,6 +4740,15 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.60.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.53.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.61.2"
|
version = "0.61.2"
|
||||||
@@ -4680,13 +4782,30 @@ dependencies = [
|
|||||||
"windows_aarch64_gnullvm 0.52.6",
|
"windows_aarch64_gnullvm 0.52.6",
|
||||||
"windows_aarch64_msvc 0.52.6",
|
"windows_aarch64_msvc 0.52.6",
|
||||||
"windows_i686_gnu 0.52.6",
|
"windows_i686_gnu 0.52.6",
|
||||||
"windows_i686_gnullvm",
|
"windows_i686_gnullvm 0.52.6",
|
||||||
"windows_i686_msvc 0.52.6",
|
"windows_i686_msvc 0.52.6",
|
||||||
"windows_x86_64_gnu 0.52.6",
|
"windows_x86_64_gnu 0.52.6",
|
||||||
"windows_x86_64_gnullvm 0.52.6",
|
"windows_x86_64_gnullvm 0.52.6",
|
||||||
"windows_x86_64_msvc 0.52.6",
|
"windows_x86_64_msvc 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-targets"
|
||||||
|
version = "0.53.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link 0.2.1",
|
||||||
|
"windows_aarch64_gnullvm 0.53.1",
|
||||||
|
"windows_aarch64_msvc 0.53.1",
|
||||||
|
"windows_i686_gnu 0.53.1",
|
||||||
|
"windows_i686_gnullvm 0.53.1",
|
||||||
|
"windows_i686_msvc 0.53.1",
|
||||||
|
"windows_x86_64_gnu 0.53.1",
|
||||||
|
"windows_x86_64_gnullvm 0.53.1",
|
||||||
|
"windows_x86_64_msvc 0.53.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-threading"
|
name = "windows-threading"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -4717,6 +4836,12 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_gnullvm"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_msvc"
|
name = "windows_aarch64_msvc"
|
||||||
version = "0.42.2"
|
version = "0.42.2"
|
||||||
@@ -4729,6 +4854,12 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_msvc"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_gnu"
|
name = "windows_i686_gnu"
|
||||||
version = "0.42.2"
|
version = "0.42.2"
|
||||||
@@ -4741,12 +4872,24 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnu"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_gnullvm"
|
name = "windows_i686_gnullvm"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnullvm"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_msvc"
|
name = "windows_i686_msvc"
|
||||||
version = "0.42.2"
|
version = "0.42.2"
|
||||||
@@ -4759,6 +4902,12 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_msvc"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnu"
|
name = "windows_x86_64_gnu"
|
||||||
version = "0.42.2"
|
version = "0.42.2"
|
||||||
@@ -4771,6 +4920,12 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnu"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnullvm"
|
name = "windows_x86_64_gnullvm"
|
||||||
version = "0.42.2"
|
version = "0.42.2"
|
||||||
@@ -4783,6 +4938,12 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnullvm"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_msvc"
|
name = "windows_x86_64_msvc"
|
||||||
version = "0.42.2"
|
version = "0.42.2"
|
||||||
@@ -4795,6 +4956,12 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_msvc"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "0.5.40"
|
version = "0.5.40"
|
||||||
|
|||||||
@@ -24,8 +24,14 @@ log = "0.4"
|
|||||||
tauri = { version = "2.11.3", features = ["tray-icon"] }
|
tauri = { version = "2.11.3", features = ["tray-icon"] }
|
||||||
tauri-plugin-log = "2"
|
tauri-plugin-log = "2"
|
||||||
tauri-plugin-autostart = "2"
|
tauri-plugin-autostart = "2"
|
||||||
|
tauri-plugin-dialog = "2"
|
||||||
ureq = { version = "2", features = ["json"] }
|
ureq = { version = "2", features = ["json"] }
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
getrandom = "0.2"
|
getrandom = "0.2"
|
||||||
portable-pty = "0.9"
|
portable-pty = "0.9"
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
|
objc2 = "0.6"
|
||||||
|
objc2-app-kit = { version = "0.3", features = ["NSApplication", "NSImage", "NSResponder"] }
|
||||||
|
objc2-foundation = { version = "0.3", features = ["NSString"] }
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
"core:window:allow-start-dragging",
|
"core:window:allow-start-dragging",
|
||||||
"autostart:default"
|
"autostart:default",
|
||||||
|
"dialog:default"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+110
-27
@@ -58,6 +58,7 @@ struct Project {
|
|||||||
path: String,
|
path: String,
|
||||||
pool: Option<String>,
|
pool: Option<String>,
|
||||||
running: bool,
|
running: bool,
|
||||||
|
terminal: TerminalConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
@@ -67,9 +68,28 @@ struct Pool {
|
|||||||
credential_type: String,
|
credential_type: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize, Default)]
|
||||||
struct ProjectConfig {
|
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 {
|
fn bundle_id(project: &str) -> String {
|
||||||
@@ -130,20 +150,13 @@ fn list_projects_in(paths: &Paths) -> Result<Vec<Project>, String> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let name = entry.file_name().to_string_lossy().into_owned();
|
let name = entry.file_name().to_string_lossy().into_owned();
|
||||||
let cfg_path = path.join(PROJECT_FILE);
|
let cfg = read_project_config_in(paths, &name)?;
|
||||||
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
|
|
||||||
};
|
|
||||||
projects.push(Project {
|
projects.push(Project {
|
||||||
running: is_running(&name),
|
running: is_running(&name),
|
||||||
path: path.to_string_lossy().into_owned(),
|
path: path.to_string_lossy().into_owned(),
|
||||||
|
pool: cfg.pool,
|
||||||
|
terminal: cfg.terminal,
|
||||||
name,
|
name,
|
||||||
pool,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
projects.sort_by(|a, b| a.name.cmp(&b.name));
|
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())
|
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> {
|
fn assign_pool_in(paths: &Paths, project: &str, pool: &str) -> Result<(), String> {
|
||||||
if !paths.pool_dir(pool).join(POOL_FILE).is_file() {
|
if !paths.pool_dir(pool).join(POOL_FILE).is_file() {
|
||||||
return Err(format!("Pool existiert nicht: {pool}"));
|
return Err(format!("Pool existiert nicht: {pool}"));
|
||||||
}
|
}
|
||||||
let cfg = ProjectConfig { pool: pool.to_string() };
|
let mut cfg = read_project_config_in(paths, project)?;
|
||||||
let raw = serde_json::to_string_pretty(&cfg).map_err(|e| e.to_string())?;
|
cfg.pool = Some(pool.to_string());
|
||||||
fs::write(paths.project_config(project), raw + "\n").map_err(|e| e.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> {
|
fn unassign_pool_in(paths: &Paths, project: &str) -> Result<(), String> {
|
||||||
let cfg_path = paths.project_config(project);
|
let mut cfg = read_project_config_in(paths, project)?;
|
||||||
fs::remove_file(&cfg_path).map_err(|e| format!("{}: {e}", cfg_path.display()))
|
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.
|
/// 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 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())?;
|
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());
|
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)
|
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]
|
#[tauri::command]
|
||||||
fn set_apikey(pool: String, key: String) -> Result<(), String> {
|
fn set_apikey(pool: String, key: String) -> Result<(), String> {
|
||||||
set_apikey_in(&Paths::real(), &pool, &key)
|
set_apikey_in(&Paths::real(), &pool, &key)
|
||||||
@@ -675,18 +739,34 @@ fn pct_decode(s: &str) -> String {
|
|||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
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 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,
|
// `app --terminal <projekt>`: eigener Prozess pro Terminal-Fenster,
|
||||||
// damit jedes Terminal ein eigenes Dock-Icon bekommt.
|
// damit jedes Terminal ein eigenes Dock-Icon bekommt.
|
||||||
Some("--terminal") => {
|
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(),
|
_ => main_builder()
|
||||||
};
|
.run(context)
|
||||||
builder
|
.expect("error while running tauri application"),
|
||||||
.run(tauri::generate_context!())
|
}
|
||||||
.expect("error while running tauri application");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Haupt-App: reine Tray-App ohne Dock-Eintrag.
|
/// Haupt-App: reine Tray-App ohne Dock-Eintrag.
|
||||||
@@ -698,6 +778,7 @@ fn main_builder() -> tauri::Builder<tauri::Wry> {
|
|||||||
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
|
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
|
||||||
None,
|
None,
|
||||||
))
|
))
|
||||||
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
if cfg!(debug_assertions) {
|
if cfg!(debug_assertions) {
|
||||||
app.handle().plugin(
|
app.handle().plugin(
|
||||||
@@ -755,6 +836,7 @@ fn main_builder() -> tauri::Builder<tauri::Wry> {
|
|||||||
delete_pool,
|
delete_pool,
|
||||||
assign_pool,
|
assign_pool,
|
||||||
unassign_pool,
|
unassign_pool,
|
||||||
|
set_terminal_config,
|
||||||
start_project,
|
start_project,
|
||||||
stop_project,
|
stop_project,
|
||||||
restart_project,
|
restart_project,
|
||||||
@@ -771,7 +853,8 @@ fn terminal_builder(project: String) -> tauri::Builder<tauri::Wry> {
|
|||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.manage(terminal::Terminals::default())
|
.manage(terminal::Terminals::default())
|
||||||
.setup(move |app| {
|
.setup(move |app| {
|
||||||
terminal::build_window(app.handle(), &project)?;
|
let cfg = terminal_config(&project)?;
|
||||||
|
terminal::build_window(app.handle(), &project, &cfg)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
// Fenster zu → PTY-Session abräumen; danach endet der Prozess.
|
// Fenster zu → PTY-Session abräumen; danach endet der Prozess.
|
||||||
@@ -982,7 +1065,7 @@ mod tests {
|
|||||||
let cfg: ProjectConfig =
|
let cfg: ProjectConfig =
|
||||||
serde_json::from_str(&fs::read_to_string(p.project_config("proj")).unwrap())
|
serde_json::from_str(&fs::read_to_string(p.project_config("proj")).unwrap())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(cfg.pool, "kunde");
|
assert_eq!(cfg.pool.as_deref(), Some("kunde"));
|
||||||
|
|
||||||
let pools = list_pools_in(&p).unwrap();
|
let pools = list_pools_in(&p).unwrap();
|
||||||
assert_eq!(pools[0].projects, vec!["proj"]);
|
assert_eq!(pools[0].projects, vec!["proj"]);
|
||||||
|
|||||||
@@ -34,19 +34,52 @@ pub fn open_terminal(project: String) -> Result<(), String> {
|
|||||||
Ok(())
|
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,
|
/// Baut das Terminal-Fenster des Terminal-Prozesses. Die PTY entsteht erst,
|
||||||
/// wenn das Fenster geladen ist und `term_start` ruft — so gehen keine
|
/// wenn das Fenster geladen ist und `term_start` ruft — so gehen keine
|
||||||
/// Ausgaben verloren, bevor der Event-Listener steht.
|
/// 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(
|
let builder = WebviewWindowBuilder::new(
|
||||||
app,
|
app,
|
||||||
format!("term-{project}"),
|
format!("term-{project}"),
|
||||||
WebviewUrl::App(format!("terminal.html?project={project}").into()),
|
WebviewUrl::App(format!("terminal.html?project={project}").into()),
|
||||||
)
|
)
|
||||||
.title(format!("{project} — Session"))
|
.title(format!("{title} — Session"))
|
||||||
.inner_size(980.0, 640.0)
|
.inner_size(980.0, 640.0)
|
||||||
// Theme-Hintergrund (Catppuccin Mocha), sonst blitzt beim Öffnen Weiß auf.
|
.background_color(tauri::window::Color(r, g, b, 0xff));
|
||||||
.background_color(tauri::window::Color(0x1e, 0x1e, 0x2e, 0xff));
|
|
||||||
|
|
||||||
// Titelbar liegt über dem Inhalt; der Header in terminal.html ist Drag-Region.
|
// Titelbar liegt über dem Inhalt; der Header in terminal.html ist Drag-Region.
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
|
|||||||
+16
-7
@@ -1,11 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from "vue";
|
import { onMounted, ref } from "vue";
|
||||||
import { enable, disable, isEnabled } from "@tauri-apps/plugin-autostart";
|
import { enable, disable, isEnabled } from "@tauri-apps/plugin-autostart";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
import ProjectList from "./components/ProjectList.vue";
|
import ProjectList from "./components/ProjectList.vue";
|
||||||
import PoolList from "./components/PoolList.vue";
|
import PoolList from "./components/PoolList.vue";
|
||||||
|
import { setLocale } from "./i18n";
|
||||||
|
|
||||||
const tab = ref<"projects" | "pools">("projects");
|
const tab = ref<"projects" | "pools">("projects");
|
||||||
const autostart = ref(false);
|
const autostart = ref(false);
|
||||||
|
const { locale } = useI18n();
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
autostart.value = await isEnabled();
|
autostart.value = await isEnabled();
|
||||||
@@ -23,19 +26,25 @@ async function toggleAutostart() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header>
|
<header>
|
||||||
<h1>AI Control</h1>
|
<h1>ai-control</h1>
|
||||||
<nav>
|
<nav>
|
||||||
<button :class="{ active: tab === 'projects' }" @click="tab = 'projects'">
|
<button :class="{ active: tab === 'projects' }" @click="tab = 'projects'">
|
||||||
Projekte
|
{{ $t("app.projects") }}
|
||||||
</button>
|
</button>
|
||||||
<button :class="{ active: tab === 'pools' }" @click="tab = 'pools'">
|
<button :class="{ active: tab === 'pools' }" @click="tab = 'pools'">
|
||||||
Pools
|
{{ $t("app.pools") }}
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
<label class="autostart">
|
<div class="header-right">
|
||||||
<input type="checkbox" :checked="autostart" @change="toggleAutostart" />
|
<div class="lang">
|
||||||
Autostart
|
<button :class="{ active: locale === 'de' }" @click="setLocale('de')">DE</button>
|
||||||
</label>
|
<button :class="{ active: locale === 'en' }" @click="setLocale('en')">EN</button>
|
||||||
|
</div>
|
||||||
|
<label class="autostart">
|
||||||
|
<input type="checkbox" :checked="autostart" @change="toggleAutostart" />
|
||||||
|
{{ $t("app.autostart") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<ProjectList v-if="tab === 'projects'" />
|
<ProjectList v-if="tab === 'projects'" />
|
||||||
|
|||||||
+78
-164
@@ -1,6 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from "vue";
|
import { computed, onMounted, ref } from "vue";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
interface Pool {
|
interface Pool {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -19,9 +22,9 @@ const dKey = ref("");
|
|||||||
|
|
||||||
const dialogTitle = computed(() => {
|
const dialogTitle = computed(() => {
|
||||||
if (!dialog.value) return "";
|
if (!dialog.value) return "";
|
||||||
if (dialog.value.mode === "oauth") return "Neuen oAuth-Pool anlegen";
|
if (dialog.value.mode === "oauth") return t("pools.newOauthPool");
|
||||||
if (dialog.value.editing) return `API-Key ändern – ${dName.value}`;
|
if (dialog.value.editing) return t("pools.editKey", { name: dName.value });
|
||||||
return "Neuen API-Key-Pool anlegen";
|
return t("pools.newApikeyPool");
|
||||||
});
|
});
|
||||||
|
|
||||||
function openNew(mode: Mode) {
|
function openNew(mode: Mode) {
|
||||||
@@ -128,65 +131,89 @@ onMounted(refresh);
|
|||||||
<template>
|
<template>
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<button class="primary" :disabled="busy" @click="openNew('oauth')">
|
<button class="primary" :disabled="busy" @click="openNew('oauth')">
|
||||||
+ oAuth
|
{{ $t("pools.newOauth") }}
|
||||||
</button>
|
</button>
|
||||||
<button class="primary" :disabled="busy" @click="openNew('apikey')">
|
<button class="primary" :disabled="busy" @click="openNew('apikey')">
|
||||||
+ apiKey
|
{{ $t("pools.newApikey") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p v-if="error" class="error">{{ error }}</p>
|
<p v-if="error" class="error">{{ error }}</p>
|
||||||
|
|
||||||
<table v-if="pools.length">
|
<table v-if="pools.length" class="grid">
|
||||||
|
<colgroup>
|
||||||
|
<col />
|
||||||
|
<col class="col-type" />
|
||||||
|
<col class="col-projects" />
|
||||||
|
<col class="col-actions-wide" />
|
||||||
|
</colgroup>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Pool</th>
|
<th>{{ $t("pools.pool") }}</th>
|
||||||
<th>Typ</th>
|
<th>{{ $t("pools.type") }}</th>
|
||||||
<th class="actions-col">Aktionen</th>
|
<th></th>
|
||||||
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="p in pools" :key="p.name">
|
<tr v-for="p in pools" :key="p.name">
|
||||||
<td><strong>{{ p.name }}</strong></td>
|
<td class="cell-name">
|
||||||
|
<strong>{{ p.name }}</strong>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge" :class="p.credentialType">{{ p.credentialType }}</span>
|
<span class="badge" :class="p.credentialType">{{ p.credentialType }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="actions-col">
|
<td>
|
||||||
<template v-if="p.credentialType === 'oauth'">
|
<span v-if="p.projects.length" class="assigned">
|
||||||
<button :disabled="busy" @click="relogin(p.name)">Neu anmelden</button>
|
{{ $t("pools.assigned", p.projects.length) }}
|
||||||
<button :disabled="busy" @click="renew(p.name)">Token erneuern</button>
|
<span class="pop">
|
||||||
</template>
|
<span v-for="name in p.projects" :key="name" class="pop-item">
|
||||||
<template v-else>
|
{{ name }}
|
||||||
<button :disabled="busy" @click="openEditKey(p.name)">Key ändern</button>
|
</span>
|
||||||
</template>
|
</span>
|
||||||
<button
|
</span>
|
||||||
v-if="!p.projects.length"
|
</td>
|
||||||
class="danger"
|
<td class="cell-actions">
|
||||||
:disabled="busy"
|
<span class="row-actions">
|
||||||
@click="askDelete(p.name)"
|
<template v-if="p.credentialType === 'oauth'">
|
||||||
>
|
<button :disabled="busy" @click="relogin(p.name)">
|
||||||
Löschen
|
{{ $t("pools.relogin") }}
|
||||||
</button>
|
</button>
|
||||||
<span v-else class="assigned" :title="p.projects.join(', ')">
|
<button :disabled="busy" @click="renew(p.name)">
|
||||||
zugeordnet: {{ p.projects.join(", ") }}
|
{{ $t("pools.renewToken") }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<button :disabled="busy" @click="openEditKey(p.name)">
|
||||||
|
{{ $t("pools.changeKey") }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<button
|
||||||
|
v-if="!p.projects.length"
|
||||||
|
class="danger"
|
||||||
|
:disabled="busy"
|
||||||
|
@click="askDelete(p.name)"
|
||||||
|
>
|
||||||
|
{{ $t("pools.delete") }}
|
||||||
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<p v-else class="empty">Noch keine Pools angelegt.</p>
|
<p v-else class="empty">{{ $t("pools.empty") }}</p>
|
||||||
|
|
||||||
<div v-if="dialog" class="modal-backdrop" @click.self="closeDialog">
|
<div v-if="dialog" class="overlay" @click.self="closeDialog">
|
||||||
<form class="modal" @submit.prevent="submit">
|
<form class="dialog" @submit.prevent="submit">
|
||||||
<h2>{{ dialogTitle }}</h2>
|
<h3>{{ dialogTitle }}</h3>
|
||||||
|
|
||||||
<label v-if="!dialog.editing">
|
<label v-if="!dialog.editing" class="field">
|
||||||
<span>Name</span>
|
{{ $t("pools.name") }}
|
||||||
<input v-model="dName" placeholder="z. B. Mx9Privat" autofocus required />
|
<input v-model="dName" placeholder="z. B. privateDefault" autofocus required />
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label v-if="dialog.mode === 'apikey'">
|
<label v-if="dialog.mode === 'apikey'" class="field">
|
||||||
<span>API-Key</span>
|
{{ $t("pools.apiKey") }}
|
||||||
<input
|
<input
|
||||||
v-model="dKey"
|
v-model="dKey"
|
||||||
type="password"
|
type="password"
|
||||||
@@ -197,148 +224,35 @@ onMounted(refresh);
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<p v-if="dialog.mode === 'oauth'" class="hint">
|
<p v-if="dialog.mode === 'oauth'" class="hint">
|
||||||
Nach „Login starten“ öffnet sich der Browser zur Anmeldung gegen dein Abo.
|
{{ $t("pools.oauthHint") }}
|
||||||
</p>
|
</p>
|
||||||
<p v-if="busy && dialog.mode === 'oauth'" class="hint busy">
|
<p v-if="busy && dialog.mode === 'oauth'" class="hint busy">
|
||||||
Warte auf die Anmeldung im Browser…
|
{{ $t("pools.oauthWaiting") }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="modal-actions">
|
<div class="actions">
|
||||||
<button type="button" :disabled="busy" @click="closeDialog">Abbrechen</button>
|
<button type="button" :disabled="busy" @click="closeDialog">
|
||||||
|
{{ $t("pools.cancel") }}
|
||||||
|
</button>
|
||||||
<button type="submit" class="primary" :disabled="busy">
|
<button type="submit" class="primary" :disabled="busy">
|
||||||
{{ dialog.mode === "oauth" ? "Login starten" : "Speichern" }}
|
{{ dialog.mode === "oauth" ? $t("pools.startLogin") : $t("pools.save") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="deleteName" class="modal-backdrop" @click.self="deleteName = null">
|
<div v-if="deleteName" class="overlay" @click.self="deleteName = null">
|
||||||
<div class="modal">
|
<div class="dialog">
|
||||||
<h2>Pool löschen</h2>
|
<h3>{{ $t("pools.deletePool") }}</h3>
|
||||||
<p class="hint">
|
<p class="hint">{{ $t("pools.deleteWarning", { name: deleteName }) }}</p>
|
||||||
Pool <strong>{{ deleteName }}</strong> und seine Credential-Datei werden
|
<div class="actions">
|
||||||
gelöscht. Das lässt sich nicht rückgängig machen.
|
|
||||||
</p>
|
|
||||||
<div class="modal-actions">
|
|
||||||
<button type="button" :disabled="busy" @click="deleteName = null">
|
<button type="button" :disabled="busy" @click="deleteName = null">
|
||||||
Abbrechen
|
{{ $t("pools.cancel") }}
|
||||||
</button>
|
</button>
|
||||||
<button class="danger" :disabled="busy" @click="confirmDelete">
|
<button class="danger" :disabled="busy" @click="confirmDelete">
|
||||||
Löschen
|
{{ $t("pools.delete") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.toolbar {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.primary {
|
|
||||||
background: #4a4af0;
|
|
||||||
border-color: #4a4af0;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.primary:hover:not(:disabled) {
|
|
||||||
background: #5b5bf5;
|
|
||||||
}
|
|
||||||
button:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions-col {
|
|
||||||
text-align: right;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.actions-col button {
|
|
||||||
margin-left: 0.4rem;
|
|
||||||
}
|
|
||||||
.danger {
|
|
||||||
color: #ff6b6b;
|
|
||||||
border-color: #ff6b6b55;
|
|
||||||
}
|
|
||||||
.danger:hover:not(:disabled) {
|
|
||||||
background: #ff6b6b22;
|
|
||||||
}
|
|
||||||
.assigned {
|
|
||||||
color: #888;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.1rem 0.5rem;
|
|
||||||
border-radius: 999px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
border: 1px solid #444;
|
|
||||||
color: #cfcfd6;
|
|
||||||
}
|
|
||||||
.badge.oauth {
|
|
||||||
border-color: #3ecf6a55;
|
|
||||||
color: #3ecf6a;
|
|
||||||
}
|
|
||||||
.badge.apikey {
|
|
||||||
border-color: #f0a84a55;
|
|
||||||
color: #f0a84a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty {
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-backdrop {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.55);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
.modal {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.9rem;
|
|
||||||
background: #23232a;
|
|
||||||
border: 1px solid #3a3a44;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 1.5rem;
|
|
||||||
min-width: 24rem;
|
|
||||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
.modal h2 {
|
|
||||||
font-size: 1rem;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.modal label {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.3rem;
|
|
||||||
}
|
|
||||||
.modal label span {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
.modal input {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.hint {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
.hint.busy {
|
|
||||||
color: #4a9af5;
|
|
||||||
}
|
|
||||||
.modal-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-top: 0.25rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
+125
-22
@@ -1,12 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, onUnmounted, ref } from "vue";
|
import { onMounted, onUnmounted, ref } from "vue";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { open } from "@tauri-apps/plugin-dialog";
|
||||||
|
|
||||||
interface Project {
|
interface Project {
|
||||||
name: string;
|
name: string;
|
||||||
path: string;
|
path: string;
|
||||||
pool: string | null;
|
pool: string | null;
|
||||||
running: boolean;
|
running: boolean;
|
||||||
|
terminal: { theme: string | null; icon: string | null; title: string | null };
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Pool {
|
interface Pool {
|
||||||
@@ -84,6 +86,58 @@ async function restartNow() {
|
|||||||
restarting.value = false;
|
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;
|
let timer: number;
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
refresh();
|
refresh();
|
||||||
@@ -94,61 +148,110 @@ onUnmounted(() => window.clearInterval(timer));
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<p v-if="error" class="error">{{ error }}</p>
|
<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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th>Projekt</th>
|
<th>{{ $t("projects.project") }}</th>
|
||||||
<th>Pool</th>
|
<th>{{ $t("projects.pool") }}</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="p in projects" :key="p.name">
|
<tr v-for="p in projects" :key="p.name">
|
||||||
<td>
|
<td class="cell-dot">
|
||||||
<span class="dot" :class="{ on: p.running }"></span>
|
<span class="dot" :class="{ on: p.running }"></span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td class="cell-name">
|
||||||
<strong>{{ p.name }}</strong>
|
<strong>{{ p.name }}</strong>
|
||||||
<small>{{ p.path }}</small>
|
<small>{{ p.path }}</small>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<select :value="p.pool ?? ''" @change="assign(p, $event)">
|
<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">
|
<option v-for="pool in pools" :key="pool.name" :value="pool.name">
|
||||||
{{ pool.name }}
|
{{ pool.name }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td class="cell-actions">
|
||||||
<button v-if="p.running" class="stop" @click="stop(p)">Beenden</button>
|
<span class="row-actions">
|
||||||
<button v-else class="start" @click="openTerminal(p)">Starten</button>
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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 v-if="pending" class="overlay">
|
||||||
<div class="dialog">
|
<div class="dialog">
|
||||||
<h3>{{ pending.name }} läuft noch</h3>
|
<h3>{{ $t("projects.stillRunning", { name: pending.name }) }}</h3>
|
||||||
<p>
|
<p>{{ $t("projects.poolChangeSaved") }}</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>
|
|
||||||
<div class="pools">
|
<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="arrow">→</span>
|
||||||
<span class="chip new">{{ pending.to }}</span>
|
<span class="chip new">{{ pending.to }}</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="hint">
|
<p class="hint">{{ $t("projects.restartHint") }}</p>
|
||||||
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>
|
|
||||||
<div class="actions">
|
<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">
|
<button class="primary" :disabled="restarting" @click="restartNow">
|
||||||
{{ restarting ? "Starte neu …" : "Jetzt neu starten" }}
|
{{ restarting ? $t("projects.restarting") : $t("projects.restartNow") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+146
@@ -0,0 +1,146 @@
|
|||||||
|
import { createI18n } from "vue-i18n";
|
||||||
|
|
||||||
|
const de = {
|
||||||
|
app: {
|
||||||
|
projects: "Projekte",
|
||||||
|
pools: "Pools",
|
||||||
|
autostart: "Autostart",
|
||||||
|
},
|
||||||
|
projects: {
|
||||||
|
project: "Projekt",
|
||||||
|
pool: "Pool",
|
||||||
|
noPool: "– kein Pool –",
|
||||||
|
start: "Starten",
|
||||||
|
stop: "Beenden",
|
||||||
|
terminalSettings: "Terminal-Einstellungen",
|
||||||
|
stillRunning: "{name} läuft noch",
|
||||||
|
poolChangeSaved:
|
||||||
|
"Der Pool-Wechsel ist gespeichert. Die laufende Session arbeitet aber weiter im alten Pool — der neue gilt erst ab dem nächsten Start.",
|
||||||
|
restartHint:
|
||||||
|
"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.",
|
||||||
|
keepRunning: "Weiterlaufen lassen",
|
||||||
|
restartNow: "Jetzt neu starten",
|
||||||
|
restarting: "Starte neu …",
|
||||||
|
terminal: "Terminal — {name}",
|
||||||
|
title: "Titel",
|
||||||
|
theme: "Theme",
|
||||||
|
dockIcon: "Dock-Icon",
|
||||||
|
chooseFile: "Datei wählen …",
|
||||||
|
remove: "Entfernen",
|
||||||
|
defaultIcon: "Standard-Icon",
|
||||||
|
appliesNextStart: "Gilt ab dem nächsten Terminal-Start.",
|
||||||
|
cancel: "Abbrechen",
|
||||||
|
save: "Speichern",
|
||||||
|
noPoolAssigned: "kein Pool",
|
||||||
|
},
|
||||||
|
pools: {
|
||||||
|
pool: "Pool",
|
||||||
|
type: "Typ",
|
||||||
|
actions: "Aktionen",
|
||||||
|
newOauth: "+ oAuth",
|
||||||
|
newApikey: "+ apiKey",
|
||||||
|
relogin: "Neu anmelden",
|
||||||
|
renewToken: "Token erneuern",
|
||||||
|
changeKey: "Key ändern",
|
||||||
|
delete: "Löschen",
|
||||||
|
assigned: "kein Projekt | 1 Projekt | {count} Projekte",
|
||||||
|
empty: "Noch keine Pools angelegt.",
|
||||||
|
newOauthPool: "Neuen oAuth-Pool anlegen",
|
||||||
|
newApikeyPool: "Neuen API-Key-Pool anlegen",
|
||||||
|
editKey: "API-Key ändern – {name}",
|
||||||
|
name: "Name",
|
||||||
|
apiKey: "API-Key",
|
||||||
|
oauthHint:
|
||||||
|
"Nach „Login starten“ öffnet sich der Browser zur Anmeldung gegen dein Abo.",
|
||||||
|
oauthWaiting: "Warte auf die Anmeldung im Browser…",
|
||||||
|
cancel: "Abbrechen",
|
||||||
|
startLogin: "Login starten",
|
||||||
|
save: "Speichern",
|
||||||
|
deletePool: "Pool löschen",
|
||||||
|
deleteWarning:
|
||||||
|
"Pool {name} und seine Credential-Datei werden gelöscht. Das lässt sich nicht rückgängig machen.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const en: typeof de = {
|
||||||
|
app: {
|
||||||
|
projects: "Projects",
|
||||||
|
pools: "Pools",
|
||||||
|
autostart: "Autostart",
|
||||||
|
},
|
||||||
|
projects: {
|
||||||
|
project: "Project",
|
||||||
|
pool: "Pool",
|
||||||
|
noPool: "– no pool –",
|
||||||
|
start: "Start",
|
||||||
|
stop: "Stop",
|
||||||
|
terminalSettings: "Terminal settings",
|
||||||
|
stillRunning: "{name} is still running",
|
||||||
|
poolChangeSaved:
|
||||||
|
"The pool change is saved. The running session keeps working in the old pool — the new one applies from the next start.",
|
||||||
|
restartHint:
|
||||||
|
"Restarting ends the running session. The automatic commit+push by claude-sync after the session is skipped — Ghostty asks once more before quitting a running process.",
|
||||||
|
keepRunning: "Keep running",
|
||||||
|
restartNow: "Restart now",
|
||||||
|
restarting: "Restarting …",
|
||||||
|
terminal: "Terminal — {name}",
|
||||||
|
title: "Title",
|
||||||
|
theme: "Theme",
|
||||||
|
dockIcon: "Dock icon",
|
||||||
|
chooseFile: "Choose file …",
|
||||||
|
remove: "Remove",
|
||||||
|
defaultIcon: "Default icon",
|
||||||
|
appliesNextStart: "Applies from the next terminal start.",
|
||||||
|
cancel: "Cancel",
|
||||||
|
save: "Save",
|
||||||
|
noPoolAssigned: "no pool",
|
||||||
|
},
|
||||||
|
pools: {
|
||||||
|
pool: "Pool",
|
||||||
|
type: "Type",
|
||||||
|
actions: "Actions",
|
||||||
|
newOauth: "+ oAuth",
|
||||||
|
newApikey: "+ apiKey",
|
||||||
|
relogin: "Sign in again",
|
||||||
|
renewToken: "Renew token",
|
||||||
|
changeKey: "Change key",
|
||||||
|
delete: "Delete",
|
||||||
|
assigned: "no projects | 1 project | {count} projects",
|
||||||
|
empty: "No pools yet.",
|
||||||
|
newOauthPool: "Create oAuth pool",
|
||||||
|
newApikeyPool: "Create API key pool",
|
||||||
|
editKey: "Change API key – {name}",
|
||||||
|
name: "Name",
|
||||||
|
apiKey: "API key",
|
||||||
|
oauthHint:
|
||||||
|
"After “Start login” the browser opens to sign in against your subscription.",
|
||||||
|
oauthWaiting: "Waiting for the sign-in in the browser…",
|
||||||
|
cancel: "Cancel",
|
||||||
|
startLogin: "Start login",
|
||||||
|
save: "Save",
|
||||||
|
deletePool: "Delete pool",
|
||||||
|
deleteWarning:
|
||||||
|
"Pool {name} and its credential file will be deleted. This cannot be undone.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const stored = localStorage.getItem("lang");
|
||||||
|
const initial =
|
||||||
|
stored === "de" || stored === "en"
|
||||||
|
? stored
|
||||||
|
: navigator.language.startsWith("de")
|
||||||
|
? "de"
|
||||||
|
: "en";
|
||||||
|
|
||||||
|
export const i18n = createI18n({
|
||||||
|
legacy: false,
|
||||||
|
globalInjection: true,
|
||||||
|
locale: initial,
|
||||||
|
fallbackLocale: "en",
|
||||||
|
messages: { de, en },
|
||||||
|
});
|
||||||
|
|
||||||
|
export function setLocale(lang: "de" | "en") {
|
||||||
|
i18n.global.locale.value = lang;
|
||||||
|
localStorage.setItem("lang", lang);
|
||||||
|
}
|
||||||
+7
-4
@@ -1,5 +1,8 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from "vue";
|
||||||
import './style.css'
|
import "@fontsource/jetbrains-mono/400.css";
|
||||||
import App from './App.vue'
|
import "@fontsource/jetbrains-mono/500.css";
|
||||||
|
import "./style.css";
|
||||||
|
import App from "./App.vue";
|
||||||
|
import { i18n } from "./i18n";
|
||||||
|
|
||||||
createApp(App).mount('#app')
|
createApp(App).use(i18n).mount("#app");
|
||||||
|
|||||||
+348
-99
@@ -1,8 +1,27 @@
|
|||||||
|
/* Catppuccin Mocha — dieselbe Palette wie das eingebaute Terminal.
|
||||||
|
Die App verwaltet Terminals; sie spricht deren visuelle Sprache. */
|
||||||
:root {
|
:root {
|
||||||
|
--base: #1e1e2e;
|
||||||
|
--mantle: #181825;
|
||||||
|
--crust: #11111b;
|
||||||
|
--surface0: #313244;
|
||||||
|
--surface1: #45475a;
|
||||||
|
--surface2: #585b70;
|
||||||
|
--text: #cdd6f4;
|
||||||
|
--subtext: #a6adc8;
|
||||||
|
--overlay: #6c7086;
|
||||||
|
--green: #a6e3a1;
|
||||||
|
--red: #f38ba8;
|
||||||
|
--blue: #89b4fa;
|
||||||
|
--peach: #fab387;
|
||||||
|
--lavender: #b4befe;
|
||||||
|
--mauve: #cba6f7;
|
||||||
|
--mono: "JetBrains Mono", Menlo, monospace;
|
||||||
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #e6e6e6;
|
color: var(--text);
|
||||||
background: #1b1b1f;
|
background: var(--base);
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14,131 +33,351 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- Header ---------- */
|
||||||
|
|
||||||
header {
|
header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
padding: 0.75rem 1.25rem;
|
padding: 0.85rem 1.5rem;
|
||||||
border-bottom: 1px solid #333;
|
background: var(--mantle);
|
||||||
|
border-bottom: 1px solid var(--surface0);
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 1rem;
|
font-family: var(--mono);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
color: var(--lavender);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
nav,
|
||||||
font-size: 0.9rem;
|
.lang {
|
||||||
margin: 0 0 0.5rem;
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 2px;
|
||||||
|
background: var(--crust);
|
||||||
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav {
|
nav button,
|
||||||
|
.lang button {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--subtext);
|
||||||
|
padding: 0.3rem 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang button {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 0.3rem 0.55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav button:hover,
|
||||||
|
.lang button:hover {
|
||||||
|
background: var(--surface0);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
nav button.active,
|
||||||
|
.lang button.active {
|
||||||
|
background: var(--surface0);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
margin-left: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
align-items: center;
|
||||||
|
gap: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
header .autostart {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
color: var(--subtext);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"] {
|
||||||
|
padding: 0;
|
||||||
|
accent-color: var(--mauve);
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
padding: 1.25rem;
|
padding: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- Controls ---------- */
|
||||||
|
|
||||||
button,
|
button,
|
||||||
select,
|
select,
|
||||||
input {
|
input {
|
||||||
font: inherit;
|
font: inherit;
|
||||||
color: inherit;
|
color: var(--text);
|
||||||
background: #2a2a30;
|
background: var(--surface0);
|
||||||
border: 1px solid #444;
|
border: 1px solid var(--surface1);
|
||||||
border-radius: 6px;
|
border-radius: 7px;
|
||||||
padding: 0.35rem 0.75rem;
|
padding: 0.4rem 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--subtext);
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
button:hover:not(:disabled) {
|
||||||
background: #35353d;
|
background: var(--surface0);
|
||||||
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
nav button.active {
|
button:disabled {
|
||||||
background: #4a4af0;
|
opacity: 0.5;
|
||||||
border-color: #4a4af0;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
button:focus-visible,
|
||||||
width: 100%;
|
select:focus-visible,
|
||||||
border-collapse: collapse;
|
input:focus-visible {
|
||||||
|
outline: 2px solid var(--lavender);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.primary {
|
||||||
|
background: var(--blue);
|
||||||
|
border-color: var(--blue);
|
||||||
|
color: var(--crust);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.primary:hover:not(:disabled) {
|
||||||
|
background: var(--lavender);
|
||||||
|
border-color: var(--lavender);
|
||||||
|
color: var(--crust);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.start {
|
||||||
|
background: var(--green);
|
||||||
|
border-color: var(--green);
|
||||||
|
color: var(--crust);
|
||||||
|
font-weight: 500;
|
||||||
|
width: 6.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.start:hover:not(:disabled) {
|
||||||
|
background: #b8ecb3;
|
||||||
|
color: var(--crust);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.stop {
|
||||||
|
background: var(--red);
|
||||||
|
border-color: var(--red);
|
||||||
|
color: var(--crust);
|
||||||
|
font-weight: 500;
|
||||||
|
width: 6.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.stop:hover:not(:disabled) {
|
||||||
|
background: #f6a3bb;
|
||||||
|
color: var(--crust);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.danger {
|
||||||
|
color: var(--red);
|
||||||
|
border-color: color-mix(in srgb, var(--red) 40%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.danger:hover:not(:disabled) {
|
||||||
|
background: color-mix(in srgb, var(--red) 12%, transparent);
|
||||||
|
color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.gear {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 2.3rem;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.6rem;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- Tabellen ---------- */
|
||||||
|
|
||||||
|
table.grid {
|
||||||
|
width: 100%;
|
||||||
|
table-layout: fixed;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
col.col-dot {
|
||||||
|
width: 2.25rem;
|
||||||
|
}
|
||||||
|
col.col-pool {
|
||||||
|
width: 12rem;
|
||||||
|
}
|
||||||
|
col.col-actions {
|
||||||
|
width: 15rem;
|
||||||
|
}
|
||||||
|
col.col-actions-wide {
|
||||||
|
width: 19rem;
|
||||||
|
}
|
||||||
|
col.col-type {
|
||||||
|
width: 7rem;
|
||||||
|
}
|
||||||
|
col.col-projects {
|
||||||
|
width: 10rem;
|
||||||
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
font-size: 0.7rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #999;
|
text-transform: uppercase;
|
||||||
padding: 0.4rem 0.6rem;
|
letter-spacing: 0.08em;
|
||||||
border-bottom: 1px solid #333;
|
color: var(--overlay);
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-bottom: 1px solid var(--surface0);
|
||||||
}
|
}
|
||||||
|
|
||||||
td {
|
td {
|
||||||
padding: 0.5rem 0.6rem;
|
padding: 0.8rem 0.75rem;
|
||||||
border-bottom: 1px solid #26262b;
|
border-bottom: 1px solid color-mix(in srgb, var(--surface0) 45%, transparent);
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
td small {
|
tbody tr:hover {
|
||||||
display: block;
|
background: color-mix(in srgb, var(--mantle) 60%, transparent);
|
||||||
color: #888;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cell-name strong {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-name small {
|
||||||
|
display: block;
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--overlay);
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-actions {
|
||||||
|
text-align: right;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
td select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status-Punkt: leuchtet nur, wenn eine Session läuft. */
|
||||||
.dot {
|
.dot {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 10px;
|
width: 9px;
|
||||||
height: 10px;
|
height: 9px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dot.on {
|
.dot.on {
|
||||||
background: #3ecf6a;
|
background: var(--green);
|
||||||
|
box-shadow: 0 0 8px color-mix(in srgb, var(--green) 70%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
button.start {
|
/* ---------- Pools ---------- */
|
||||||
background: #2f9e4f;
|
|
||||||
border-color: #2f9e4f;
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 0.15rem 0.6rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid var(--surface1);
|
||||||
|
color: var(--subtext);
|
||||||
}
|
}
|
||||||
|
|
||||||
button.start:hover {
|
.badge.oauth {
|
||||||
background: #37b85c;
|
border-color: color-mix(in srgb, var(--green) 40%, transparent);
|
||||||
|
color: var(--green);
|
||||||
}
|
}
|
||||||
|
|
||||||
button.stop {
|
.badge.apikey {
|
||||||
background: #c0392b;
|
border-color: color-mix(in srgb, var(--peach) 40%, transparent);
|
||||||
border-color: #c0392b;
|
color: var(--peach);
|
||||||
}
|
}
|
||||||
|
|
||||||
button.stop:hover {
|
/* Zugeordnete Projekte: Anzahl in der Zeile, Liste im Hover. */
|
||||||
background: #d64434;
|
.assigned {
|
||||||
|
position: relative;
|
||||||
|
color: var(--subtext);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
cursor: default;
|
||||||
|
border-bottom: 1px dotted var(--surface2);
|
||||||
|
padding-bottom: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assigned .pop {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 8px);
|
||||||
|
left: 0;
|
||||||
|
z-index: 20;
|
||||||
|
min-width: 13rem;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.4rem;
|
||||||
|
background: var(--crust);
|
||||||
|
border: 1px solid var(--surface1);
|
||||||
|
border-radius: 9px;
|
||||||
|
padding: 0.7rem 0.9rem;
|
||||||
|
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.assigned:hover .pop {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pop-item {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: var(--text);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
color: var(--overlay);
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
color: #ff6b6b;
|
color: var(--red);
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
form {
|
/* ---------- Dialoge ---------- */
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.5rem;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
form h2 {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
form input {
|
|
||||||
min-width: 16rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overlay {
|
.overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -152,84 +391,94 @@ form input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dialog {
|
.dialog {
|
||||||
background: #222228;
|
display: flex;
|
||||||
border: 1px solid #3a3a42;
|
flex-direction: column;
|
||||||
|
gap: 0.9rem;
|
||||||
|
background: var(--mantle);
|
||||||
|
border: 1px solid var(--surface0);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 1.75rem 2rem;
|
padding: 1.75rem 2rem;
|
||||||
|
min-width: 26rem;
|
||||||
max-width: 34rem;
|
max-width: 34rem;
|
||||||
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog h3 {
|
.dialog h3 {
|
||||||
margin: 0 0 0.75rem;
|
margin: 0;
|
||||||
font-size: 1.05rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog p {
|
.dialog p {
|
||||||
margin: 0 0 0.75rem;
|
margin: 0;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
color: #cfcfd6;
|
color: var(--subtext);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog .hint {
|
.dialog .hint {
|
||||||
color: #9a9aa4;
|
color: var(--overlay);
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hint.busy {
|
||||||
|
color: var(--blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog .field {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 6rem 1fr;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
color: var(--subtext);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-path {
|
||||||
|
font-family: var(--mono);
|
||||||
|
color: var(--overlay);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
.dialog .pools {
|
.dialog .pools {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.6rem;
|
gap: 0.6rem;
|
||||||
margin: 1rem 0 1.25rem;
|
margin: 0.25rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chip {
|
.chip {
|
||||||
background: #2a2a30;
|
font-family: var(--mono);
|
||||||
border: 1px solid #444;
|
font-size: 0.8rem;
|
||||||
|
background: var(--surface0);
|
||||||
|
border: 1px solid var(--surface1);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
padding: 0.2rem 0.7rem;
|
padding: 0.2rem 0.7rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chip.new {
|
.chip.new {
|
||||||
border-color: #4a4af0;
|
border-color: var(--blue);
|
||||||
color: #b9b9ff;
|
color: var(--blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow {
|
.arrow {
|
||||||
color: #777;
|
color: var(--overlay);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog .actions {
|
.dialog .actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 0.6rem;
|
gap: 0.6rem;
|
||||||
margin-top: 1.5rem;
|
margin-top: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.primary {
|
/* ---------- Formulare (Pools-Dialoge) ---------- */
|
||||||
background: #4a4af0;
|
|
||||||
border-color: #4a4af0;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.primary:hover {
|
form.dialog input {
|
||||||
background: #5c5cf2;
|
width: 100%;
|
||||||
}
|
|
||||||
|
|
||||||
button:disabled {
|
|
||||||
opacity: 0.55;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
header .autostart {
|
|
||||||
margin-left: auto;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.4rem;
|
|
||||||
color: #999;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"] {
|
|
||||||
padding: 0;
|
|
||||||
accent-color: #4a4af0;
|
|
||||||
}
|
}
|
||||||
|
|||||||
+163
-34
@@ -28,33 +28,171 @@ window.addEventListener("unhandledrejection", (e) =>
|
|||||||
const project = new URLSearchParams(location.search).get("project")!;
|
const project = new URLSearchParams(location.search).get("project")!;
|
||||||
const win = getCurrentWebviewWindow();
|
const win = getCurrentWebviewWindow();
|
||||||
|
|
||||||
document.getElementById("project-name")!.textContent = project;
|
// Fensterhintergründe je Theme stehen zusätzlich in terminal.rs
|
||||||
|
// (theme_background) — beide Stellen müssen zusammenpassen.
|
||||||
// Catppuccin Mocha
|
const THEMES: Record<
|
||||||
const theme = {
|
string,
|
||||||
background: "#1e1e2e",
|
{ header: string; border: string; xterm: Record<string, string> }
|
||||||
foreground: "#cdd6f4",
|
> = {
|
||||||
cursor: "#f5e0dc",
|
mocha: {
|
||||||
cursorAccent: "#1e1e2e",
|
header: "#181825",
|
||||||
selectionBackground: "#585b7080",
|
border: "#313244",
|
||||||
black: "#45475a",
|
xterm: {
|
||||||
red: "#f38ba8",
|
background: "#1e1e2e",
|
||||||
green: "#a6e3a1",
|
foreground: "#cdd6f4",
|
||||||
yellow: "#f9e2af",
|
cursor: "#f5e0dc",
|
||||||
blue: "#89b4fa",
|
cursorAccent: "#1e1e2e",
|
||||||
magenta: "#f5c2e7",
|
selectionBackground: "#585b7080",
|
||||||
cyan: "#94e2d5",
|
black: "#45475a",
|
||||||
white: "#bac2de",
|
red: "#f38ba8",
|
||||||
brightBlack: "#585b70",
|
green: "#a6e3a1",
|
||||||
brightRed: "#f38ba8",
|
yellow: "#f9e2af",
|
||||||
brightGreen: "#a6e3a1",
|
blue: "#89b4fa",
|
||||||
brightYellow: "#f9e2af",
|
magenta: "#f5c2e7",
|
||||||
brightBlue: "#89b4fa",
|
cyan: "#94e2d5",
|
||||||
brightMagenta: "#f5c2e7",
|
white: "#bac2de",
|
||||||
brightCyan: "#94e2d5",
|
brightBlack: "#585b70",
|
||||||
brightWhite: "#a6adc8",
|
brightRed: "#f38ba8",
|
||||||
|
brightGreen: "#a6e3a1",
|
||||||
|
brightYellow: "#f9e2af",
|
||||||
|
brightBlue: "#89b4fa",
|
||||||
|
brightMagenta: "#f5c2e7",
|
||||||
|
brightCyan: "#94e2d5",
|
||||||
|
brightWhite: "#a6adc8",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dracula: {
|
||||||
|
header: "#21222c",
|
||||||
|
border: "#44475a",
|
||||||
|
xterm: {
|
||||||
|
background: "#282a36",
|
||||||
|
foreground: "#f8f8f2",
|
||||||
|
cursor: "#f8f8f2",
|
||||||
|
cursorAccent: "#282a36",
|
||||||
|
selectionBackground: "#44475a80",
|
||||||
|
black: "#21222c",
|
||||||
|
red: "#ff5555",
|
||||||
|
green: "#50fa7b",
|
||||||
|
yellow: "#f1fa8c",
|
||||||
|
blue: "#bd93f9",
|
||||||
|
magenta: "#ff79c6",
|
||||||
|
cyan: "#8be9fd",
|
||||||
|
white: "#f8f8f2",
|
||||||
|
brightBlack: "#6272a4",
|
||||||
|
brightRed: "#ff6e6e",
|
||||||
|
brightGreen: "#69ff94",
|
||||||
|
brightYellow: "#ffffa5",
|
||||||
|
brightBlue: "#d6acff",
|
||||||
|
brightMagenta: "#ff92df",
|
||||||
|
brightCyan: "#a4ffff",
|
||||||
|
brightWhite: "#ffffff",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"solarized-dark": {
|
||||||
|
header: "#073642",
|
||||||
|
border: "#586e75",
|
||||||
|
xterm: {
|
||||||
|
background: "#002b36",
|
||||||
|
foreground: "#839496",
|
||||||
|
cursor: "#839496",
|
||||||
|
cursorAccent: "#002b36",
|
||||||
|
selectionBackground: "#07364280",
|
||||||
|
black: "#073642",
|
||||||
|
red: "#dc322f",
|
||||||
|
green: "#859900",
|
||||||
|
yellow: "#b58900",
|
||||||
|
blue: "#268bd2",
|
||||||
|
magenta: "#d33682",
|
||||||
|
cyan: "#2aa198",
|
||||||
|
white: "#eee8d5",
|
||||||
|
brightBlack: "#586e75",
|
||||||
|
brightRed: "#cb4b16",
|
||||||
|
brightGreen: "#859900",
|
||||||
|
brightYellow: "#657b83",
|
||||||
|
brightBlue: "#839496",
|
||||||
|
brightMagenta: "#6c71c4",
|
||||||
|
brightCyan: "#93a1a1",
|
||||||
|
brightWhite: "#fdf6e3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
gruvbox: {
|
||||||
|
header: "#1d2021",
|
||||||
|
border: "#3c3836",
|
||||||
|
xterm: {
|
||||||
|
background: "#282828",
|
||||||
|
foreground: "#ebdbb2",
|
||||||
|
cursor: "#ebdbb2",
|
||||||
|
cursorAccent: "#282828",
|
||||||
|
selectionBackground: "#3c383680",
|
||||||
|
black: "#282828",
|
||||||
|
red: "#cc241d",
|
||||||
|
green: "#98971a",
|
||||||
|
yellow: "#d79921",
|
||||||
|
blue: "#458588",
|
||||||
|
magenta: "#b16286",
|
||||||
|
cyan: "#689d6a",
|
||||||
|
white: "#a89984",
|
||||||
|
brightBlack: "#928374",
|
||||||
|
brightRed: "#fb4934",
|
||||||
|
brightGreen: "#b8bb26",
|
||||||
|
brightYellow: "#fabd2f",
|
||||||
|
brightBlue: "#83a598",
|
||||||
|
brightMagenta: "#d3869b",
|
||||||
|
brightCyan: "#8ec07c",
|
||||||
|
brightWhite: "#ebdbb2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"one-dark": {
|
||||||
|
header: "#21252b",
|
||||||
|
border: "#3e4451",
|
||||||
|
xterm: {
|
||||||
|
background: "#282c34",
|
||||||
|
foreground: "#abb2bf",
|
||||||
|
cursor: "#abb2bf",
|
||||||
|
cursorAccent: "#282c34",
|
||||||
|
selectionBackground: "#3e445180",
|
||||||
|
black: "#282c34",
|
||||||
|
red: "#e06c75",
|
||||||
|
green: "#98c379",
|
||||||
|
yellow: "#e5c07b",
|
||||||
|
blue: "#61afef",
|
||||||
|
magenta: "#c678dd",
|
||||||
|
cyan: "#56b6c2",
|
||||||
|
white: "#abb2bf",
|
||||||
|
brightBlack: "#5c6370",
|
||||||
|
brightRed: "#e06c75",
|
||||||
|
brightGreen: "#98c379",
|
||||||
|
brightYellow: "#e5c07b",
|
||||||
|
brightBlue: "#61afef",
|
||||||
|
brightMagenta: "#c678dd",
|
||||||
|
brightCyan: "#56b6c2",
|
||||||
|
brightWhite: "#ffffff",
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface Project {
|
||||||
|
name: string;
|
||||||
|
pool: string | null;
|
||||||
|
terminal: { theme: string | null; icon: string | null; title: string | null };
|
||||||
|
}
|
||||||
|
const projects = await invoke<Project[]>("list_projects");
|
||||||
|
const cfg = projects.find((p) => p.name === project);
|
||||||
|
if (cfg?.pool) document.getElementById("pool")!.textContent = cfg.pool;
|
||||||
|
document.getElementById("project-name")!.textContent =
|
||||||
|
cfg?.terminal.title ?? project;
|
||||||
|
|
||||||
|
const picked = THEMES[cfg?.terminal.theme ?? "mocha"];
|
||||||
|
const theme = picked.xterm;
|
||||||
|
|
||||||
|
// Seitenfarben ans Theme anpassen (Defaults in terminal.html sind Mocha).
|
||||||
|
document.documentElement.style.background = theme.background;
|
||||||
|
document.body.style.background = theme.background;
|
||||||
|
const header = document.querySelector("header") as HTMLElement;
|
||||||
|
header.style.background = picked.header;
|
||||||
|
header.style.borderBottomColor = picked.border;
|
||||||
|
header.style.color = theme.foreground;
|
||||||
|
|
||||||
const term = new Terminal({
|
const term = new Terminal({
|
||||||
// Ligatur-Addon nutzt die Character-Joiner-API (Proposed API)
|
// Ligatur-Addon nutzt die Character-Joiner-API (Proposed API)
|
||||||
allowProposedApi: true,
|
allowProposedApi: true,
|
||||||
@@ -106,12 +244,3 @@ window.addEventListener("resize", () => fit.fit());
|
|||||||
|
|
||||||
await invoke("term_start", { project, rows: term.rows, cols: term.cols });
|
await invoke("term_start", { project, rows: term.rows, cols: term.cols });
|
||||||
term.focus();
|
term.focus();
|
||||||
|
|
||||||
// Pool-Chip im Titelbalken
|
|
||||||
interface Project {
|
|
||||||
name: string;
|
|
||||||
pool: string | null;
|
|
||||||
}
|
|
||||||
const projects = await invoke<Project[]>("list_projects");
|
|
||||||
const pool = projects.find((p) => p.name === project)?.pool;
|
|
||||||
if (pool) document.getElementById("pool")!.textContent = pool;
|
|
||||||
|
|||||||
Reference in New Issue
Block a user