diff --git a/package-lock.json b/package-lock.json index 249d990..462e818 100644 --- a/package-lock.json +++ b/package-lock.json @@ -309,9 +309,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -329,9 +326,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -349,9 +343,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -369,9 +360,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -389,9 +377,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -409,9 +394,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -597,9 +579,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0 OR MIT", "optional": true, "os": [ @@ -617,9 +596,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "Apache-2.0 OR MIT", "optional": true, "os": [ @@ -637,9 +613,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0 OR MIT", "optional": true, "os": [ @@ -657,9 +630,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0 OR MIT", "optional": true, "os": [ @@ -677,9 +647,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "Apache-2.0 OR MIT", "optional": true, "os": [ @@ -1262,9 +1229,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1286,9 +1250,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1310,9 +1271,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1334,9 +1292,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ diff --git a/popup.html b/popup.html new file mode 100644 index 0000000..ea253d6 --- /dev/null +++ b/popup.html @@ -0,0 +1,18 @@ + + + + + Projekte + + + +
+ + + diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 8253903..85462bf 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -33,6 +33,7 @@ name = "ai-control" version = "0.1.1" dependencies = [ "base64 0.22.1", + "glib", "keyring", "log", "objc2", @@ -48,6 +49,7 @@ dependencies = [ "tauri-plugin-dialog", "tauri-plugin-log", "uuid", + "zbus", ] [[package]] @@ -103,6 +105,137 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "atk" version = "0.18.2" @@ -221,6 +354,19 @@ dependencies = [ "objc2", ] +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "borsh" version = "1.7.0" @@ -486,6 +632,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "cookie" version = "0.18.1" @@ -902,6 +1057,33 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "env_filter" version = "0.1.4" @@ -929,6 +1111,37 @@ dependencies = [ "typeid", ] +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "fastrand" version = "2.4.1" @@ -1076,6 +1289,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.32" @@ -1433,6 +1659,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -1921,6 +2153,12 @@ dependencies = [ "libc", ] +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.8.2" @@ -2347,6 +2585,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "pango" version = "0.18.3" @@ -2372,6 +2620,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -2460,6 +2714,17 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkg-config" version = "0.3.33" @@ -2505,6 +2770,20 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "portable-pty" version = "0.9.0" @@ -2924,6 +3203,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -3269,6 +3561,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "simd-adler32" version = "0.3.9" @@ -3830,6 +4132,19 @@ dependencies = [ "toml 1.1.2+spec-1.1.0", ] +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "tendril" version = "0.5.0" @@ -4136,9 +4451,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "tracing-core" version = "0.1.36" @@ -4188,6 +4515,17 @@ version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + [[package]] name = "unic-char-property" version = "0.9.0" @@ -5115,6 +5453,67 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zbus" +version = "5.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.3", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" +dependencies = [ + "serde", + "winnow 1.0.3", + "zvariant", +] + [[package]] name = "zerocopy" version = "0.8.52" @@ -5214,3 +5613,43 @@ name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zvariant" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 1.0.3", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.118", + "winnow 1.0.3", +] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d1825ec..5d52386 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -31,6 +31,10 @@ portable-pty = "0.9" uuid = { version = "1", features = ["v4"] } keyring = { version = "3", features = ["apple-native", "sync-secret-service", "windows-native"] } +[target.'cfg(target_os = "linux")'.dependencies] +zbus = "5" +glib = "0.18" + [target.'cfg(target_os = "macos")'.dependencies] objc2 = "0.6" objc2-app-kit = { version = "0.3", features = ["NSApplication", "NSImage", "NSResponder", "NSRunningApplication"] } diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 2868d06..6a6513d 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -4,11 +4,18 @@ "description": "enables the default permissions", "windows": [ "main", + "popup", "term-*" ], "permissions": [ "core:default", "core:window:allow-start-dragging", + "core:window:allow-set-size", + "core:window:allow-minimize", + "core:window:allow-toggle-maximize", + "core:window:allow-close", + "core:window:allow-hide", + "core:window:allow-start-resize-dragging", "autostart:default", "dialog:default" ] diff --git a/src-tauri/icons/trayLinux.png b/src-tauri/icons/trayLinux.png new file mode 100644 index 0000000..b305de0 Binary files /dev/null and b/src-tauri/icons/trayLinux.png differ diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5e09721..945c767 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -340,12 +340,12 @@ fn spawn_session_watcher(app: tauri::AppHandle) { let handle = app.clone(); app .run_on_main_thread(move || { - let menu = tray_menu(&handle).expect("Tray-Menü nicht aufbaubar"); - handle - .tray_by_id("main") - .expect("Tray-Icon fehlt") - .set_menu(Some(menu)) - .expect("Tray-Menü nicht setzbar"); + // Nur wenn ein Tray existiert (macOS). Unter Linux pollt das Popup selbst. + if let Some(tray) = handle.tray_by_id("main") { + if let Ok(menu) = tray_menu(&handle) { + let _ = tray.set_menu(Some(menu)); + } + } }) .expect("Main-Thread nicht erreichbar"); state = current; @@ -556,6 +556,8 @@ fn create_project_full_in( if todo { set_todo_in(paths, name, true)?; } + #[cfg(target_os = "linux")] + write_terminal_desktop(paths, name, &cfg.terminal); Ok(()) } @@ -618,7 +620,15 @@ fn add_project_in(paths: &Paths, path: &str) -> Result<(), String> { .to_string_lossy() .into_owned(); check_name(&name)?; - register_project(paths, &name, &dir) + register_project(paths, &name, &dir)?; + #[cfg(target_os = "linux")] + { + let cfg = read_project_config_in(paths, &name) + .map(|c| c.terminal) + .unwrap_or_default(); + write_terminal_desktop(paths, &name, &cfg); + } + Ok(()) } #[cfg(test)] @@ -739,7 +749,10 @@ fn delete_project_in(paths: &Paths, name: &str, delete_work_dirs: bool) -> Resul } } fs::remove_dir_all(&dir).map_err(|e| e.to_string())?; - unregister_project(paths, name) + unregister_project(paths, name)?; + #[cfg(target_os = "linux")] + remove_terminal_desktop(paths, name); + Ok(()) } fn read_project_config_in(paths: &Paths, project: &str) -> Result { @@ -809,7 +822,10 @@ fn set_terminal_config_in( } } cfg.terminal = terminal; - write_project_config_in(paths, project, &cfg) + write_project_config_in(paths, project, &cfg)?; + #[cfg(target_os = "linux")] + write_terminal_desktop(paths, project, &cfg.terminal); + Ok(()) } /// Icon-Pfad einer Projekt-Config auflösen: relative Namen liegen im @@ -822,6 +838,78 @@ fn resolve_icon_path(paths: &Paths, icon: &str) -> PathBuf { } } +/// ~/.local/share/applications — Ziel der pro-Terminal-.desktop-Dateien. +/// paths-abhängig (nicht $HOME direkt), damit Tests in ihr tmp-home schreiben. +#[cfg(target_os = "linux")] +fn applications_dir(paths: &Paths) -> PathBuf { + paths.home.join(".local/share/applications") +} + +/// Schreibt/aktualisiert die .desktop eines Projekts: dash-to-dock ordnet dem +/// Fenster (app_id `aicontrol-`, via set_prgname) darüber das Projekt- +/// Icon zu. NoDisplay=true hält den App-Starter sauber. +#[cfg(target_os = "linux")] +pub(crate) fn write_terminal_desktop(paths: &Paths, project: &str, cfg: &TerminalConfig) { + let dir = applications_dir(paths); + if fs::create_dir_all(&dir).is_err() { + return; + } + let exec = std::env::current_exe() + .ok() + .and_then(|p| p.to_str().map(str::to_string)) + .unwrap_or_else(|| "ai-control".into()); + let icon_line = cfg + .icon + .as_deref() + .map(|i| resolve_icon_path(paths, i)) + .filter(|p| p.exists()) + .and_then(|p| p.to_str().map(str::to_string)) + .map(|p| format!("Icon={p}\n")) + .unwrap_or_default(); + let content = format!( + "[Desktop Entry]\nType=Application\nName={project}\nExec={exec} --terminal {project}\n\ + {icon_line}StartupWMClass=aicontrol-{project}\nNoDisplay=true\n" + ); + let _ = fs::write(dir.join(format!("aicontrol-{project}.desktop")), content); +} + +/// Entfernt die .desktop eines Projekts (beim Löschen). +#[cfg(target_os = "linux")] +pub(crate) fn remove_terminal_desktop(paths: &Paths, project: &str) { + let _ = fs::remove_file(applications_dir(paths).join(format!("aicontrol-{project}.desktop"))); +} + +/// Beim App-Start: für jedes registrierte Projekt die .desktop neu schreiben und +/// verwaiste (kein registriertes Projekt mehr) entfernen. Dadurch sind sie immer +/// vorhanden und aktuell, bevor überhaupt ein Terminal startet. +#[cfg(target_os = "linux")] +fn sync_all_desktops(paths: &Paths) { + let Ok(reg) = load_registry(paths) else { + return; + }; + for project in reg.keys() { + let cfg = read_project_config_in(paths, project) + .map(|c| c.terminal) + .unwrap_or_default(); + write_terminal_desktop(paths, project, &cfg); + } + let dir = applications_dir(paths); + if let Ok(entries) = fs::read_dir(&dir) { + for e in entries.flatten() { + let name = e.file_name(); + let Some(name) = name.to_str() else { continue }; + if let Some(project) = name + .strip_prefix("aicontrol-") + .and_then(|n| n.strip_suffix(".desktop")) + { + if !reg.contains_key(project) { + let _ = fs::remove_file(e.path()); + } + } + } + } +} + /// Icon eines Projekts als PNG-data-URL für die Übersicht; ICNS wird per /// sips nach PNG konvertiert, weil der Browser ICNS nicht rendert. #[tauri::command] @@ -837,20 +925,7 @@ fn project_icon(project: String) -> Result, String> { .and_then(|e| e.to_str()) .is_some_and(|e| e.eq_ignore_ascii_case("icns")); let png = if is_icns { - let tmp = std::env::temp_dir().join(format!("ai-control-icon-{project}.png")); - let out = std::process::Command::new("sips") - .args(["-s", "format", "png"]) - .arg(&path) - .arg("--out") - .arg(&tmp) - .output() - .map_err(|e| format!("sips: {e}"))?; - if !out.status.success() { - return Err(format!("sips: {}", String::from_utf8_lossy(&out.stderr))); - } - let bytes = fs::read(&tmp).map_err(|e| format!("{}: {e}", tmp.display()))?; - let _ = fs::remove_file(&tmp); - bytes + icns_to_png_bytes(&path, &project)? } else { fs::read(&path).map_err(|e| format!("{}: {e}", path.display()))? }; @@ -904,17 +979,49 @@ fn menu_icon( Ok(tauri::image::Image::new_owned(canvas, W as u32, H as u32)) } -/// Projekt-Icon als 36×36-RGBA (sips konvertiert, auch ICNS). +/// Projekt-Icon als 36×36-RGBA. PNG wird von tauri geladen und skaliert; +/// ICNS läuft über sips (nur macOS). fn project_icon_rgba_36( paths: &Paths, project: &str, icon: &str, ) -> Result, String> { let src = resolve_icon_path(paths, icon); + let is_icns = src + .extension() + .and_then(|e| e.to_str()) + .is_some_and(|e| e.eq_ignore_ascii_case("icns")); + let img = if is_icns { + tauri::image::Image::from_bytes(&icns_to_png_bytes(&src, project)?).map_err(|e| e.to_string())? + } else { + tauri::image::Image::from_path(&src).map_err(|e| format!("{}: {e}", src.display()))? + }; + Ok(scale_rgba(&img, 36)) +} + +/// Nearest-Neighbor-Skalierung eines RGBA-Bildes auf ein Quadrat der Kantenlänge `size`. +fn scale_rgba(img: &tauri::image::Image, size: usize) -> tauri::image::Image<'static> { + let (sw, sh) = (img.width() as usize, img.height() as usize); + let src = img.rgba(); + let mut out = vec![0u8; size * size * 4]; + for y in 0..size { + for x in 0..size { + let sx = x * sw / size; + let sy = y * sh / size; + let si = (sy * sw + sx) * 4; + let di = (y * size + x) * 4; + out[di..di + 4].copy_from_slice(&src[si..si + 4]); + } + } + tauri::image::Image::new_owned(out, size as u32, size as u32) +} + +/// ICNS → PNG-Bytes via sips (nur macOS; Linux hat kein ICNS). +fn icns_to_png_bytes(src: &std::path::Path, project: &str) -> Result, String> { let tmp = std::env::temp_dir().join(format!("ai-control-tray-icon-{project}.png")); let out = Command::new("sips") - .args(["-s", "format", "png", "-z", "36", "36"]) - .arg(&src) + .args(["-s", "format", "png"]) + .arg(src) .arg("--out") .arg(&tmp) .output() @@ -924,7 +1031,7 @@ fn project_icon_rgba_36( } let bytes = fs::read(&tmp).map_err(|e| format!("{}: {e}", tmp.display()))?; let _ = fs::remove_file(&tmp); - tauri::image::Image::from_bytes(&bytes).map_err(|e| e.to_string()) + Ok(bytes) } /// Terminal-Einstellungen eines Projekts, für den Terminal-Prozess. @@ -1818,6 +1925,33 @@ fn set_sync_setting(enabled: bool) -> Result<(), String> { set_sync_on_session_end_in(&Paths::real(), enabled) } +/// Terminal-Schriftgröße (settings.json: terminalFontSize), ein Wert für alle +/// Terminals. Default 13. +#[tauri::command] +fn terminal_font_size() -> u32 { + fs::read_to_string(Paths::real().config_dir().join(APP_SETTINGS_FILE)) + .ok() + .and_then(|s| serde_json::from_str::(&s).ok()) + .and_then(|v| v["terminalFontSize"].as_u64()) + .map(|n| n as u32) + .unwrap_or(13) +} + +/// Setzt die Schriftgröße; erhält übrige App-settings. +#[tauri::command] +fn set_terminal_font_size(size: u32) -> Result<(), String> { + let paths = Paths::real(); + let path = paths.config_dir().join(APP_SETTINGS_FILE); + let mut v: serde_json::Value = fs::read_to_string(&path) + .ok() + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or_else(|| serde_json::json!({})); + v["terminalFontSize"] = serde_json::json!(size); + fs::create_dir_all(paths.config_dir()).map_err(|e| e.to_string())?; + let raw = serde_json::to_string_pretty(&v).map_err(|e| e.to_string())?; + fs::write(&path, raw + "\n").map_err(|e| format!("{}: {e}", path.display())) +} + /// Verlinkt die synced Runtime eines bestehenden Pools. Nur bei idlem Pool — /// sonst würde das Transkript der laufenden Session ersetzt. #[tauri::command] @@ -1839,6 +1973,10 @@ pub fn run() { // damit jedes Terminal ein eigenes Dock-Icon bekommt. Some("--terminal") => { let project = args.next().expect("--terminal braucht einen Projektnamen"); + // Linux: eigene Wayland-app_id pro Terminal-Prozess -> eigener Dock-Eintrag + // (muss vor dem GTK-Init stehen). Test, ob set_prgname die app_id setzt. + #[cfg(target_os = "linux")] + glib::set_prgname(Some(format!("aicontrol-{project}").as_str())); let icon = terminal_config(&project) .expect("Projekt-Config nicht lesbar") .icon @@ -1908,6 +2046,72 @@ fn start_or_focus(app: &tauri::AppHandle, project: &str) { } } +/// Tray-/Popup-Klick: Projekt starten oder das laufende Terminal fokussieren. +#[tauri::command] +fn start_or_focus_cmd(app: tauri::AppHandle, project: String) { + start_or_focus(&app, &project); +} + +/// Popup-Fußzeile „Öffnen": das Hauptfenster zeigen. +#[tauri::command] +fn open_main_window(app: tauri::AppHandle) { + use tauri::Manager; + if let Some(w) = app.get_webview_window("main") { + let _ = w.show(); + let _ = w.set_focus(); + } +} + +/// Popup-Fußzeile „Beenden": App verlassen. +#[tauri::command] +fn quit_app(app: tauri::AppHandle) { + app.exit(0); +} + +/// D-Bus-Relay für die GNOME-Extension: Show()/Hide() steuern das Popup-Fenster. +/// Die Extension ruft Show() beim Panel-Klick und positioniert das Fenster selbst. +#[cfg(target_os = "linux")] +struct PopupService { + app: tauri::AppHandle, +} + +#[cfg(target_os = "linux")] +#[zbus::interface(name = "com.aicontrol.Popup1")] +impl PopupService { + fn show(&self) { + use tauri::Manager; + let app = self.app.clone(); + let win = app.clone(); + let _ = app.run_on_main_thread(move || { + if let Some(w) = win.get_webview_window("popup") { + let _ = w.show(); + let _ = w.set_focus(); + } + }); + } + fn hide(&self) { + use tauri::Manager; + let app = self.app.clone(); + let win = app.clone(); + let _ = app.run_on_main_thread(move || { + if let Some(w) = win.get_webview_window("popup") { + let _ = w.hide(); + } + }); + } +} + +#[cfg(target_os = "linux")] +fn serve_popup_dbus(app: tauri::AppHandle) -> zbus::Result<()> { + let _conn = zbus::blocking::connection::Builder::session()? + .name("com.aicontrol.Popup")? + .serve_at("/com/aicontrol/Popup", PopupService { app })? + .build()?; + loop { + std::thread::park(); + } +} + /// Haupt-App: reine Tray-App ohne Dock-Eintrag. fn main_builder() -> tauri::Builder { use tauri::Manager; @@ -1935,38 +2139,77 @@ fn main_builder() -> tauri::Builder { // hält das Tray-Menü aktuell. spawn_session_watcher(app.handle().clone()); + // Alle pro-Terminal-.desktop-Dateien neu schreiben + verwaiste entfernen, + // damit sie da sind, bevor ein Terminal startet. + #[cfg(target_os = "linux")] + sync_all_desktops(&Paths::real()); + // Fenster im Code statt in tauri.conf.json, damit der Terminal-Prozess // (gleiches Binary, gleiche Config) kein main-Fenster anlegt. - tauri::WebviewWindowBuilder::new(app, "main", tauri::WebviewUrl::default()) + let main_win = tauri::WebviewWindowBuilder::new(app, "main", tauri::WebviewUrl::default()) .title("ai-control") .inner_size(800.0, 600.0) - .visible(false) - .build()?; + .visible(false); + // Wie die Terminal-Fenster: eigener Header als Titelleiste. macOS behält die + // Ampel (Overlay), Linux ist dekorationslos (eigene Fensterknöpfe im Header). + #[cfg(target_os = "macos")] + let main_win = main_win + .title_bar_style(tauri::TitleBarStyle::Overlay) + .hidden_title(true); + #[cfg(target_os = "linux")] + let main_win = main_win.decorations(false); + main_win.build()?; - let menu = tray_menu(app.handle())?; - tauri::tray::TrayIconBuilder::with_id("main") - // include_bytes! statt default_window_icon: cargo trackt die Datei, - // neu generierte Icons landen damit sicher im nächsten Build. - .icon(tauri::image::Image::from_bytes(include_bytes!( - "../icons/trayTemplate.png" - ))?) - // Template-Icon: macOS färbt es passend zur Menüleiste ein. - .icon_as_template(true) - .menu(&menu) - .on_menu_event(|app, event| match event.id.as_ref() { - "open" => { - let w = app.get_webview_window("main").unwrap(); - w.show().unwrap(); - w.set_focus().unwrap(); - } - "quit" => app.exit(0), - id => { - if let Some(project) = id.strip_prefix("project:") { - start_or_focus(app, project); + // macOS: natives Tray-Menü (rendert Icons pro Eintrag). Unter Linux/GNOME + // übernimmt die Shell-Extension den Panel-Button + Popup — kein Tray hier, + // sonst erschiene das aIC-Icon doppelt. + #[cfg(target_os = "macos")] + { + let menu = tray_menu(app.handle())?; + tauri::tray::TrayIconBuilder::with_id("main") + .icon(tauri::image::Image::from_bytes(include_bytes!( + "../icons/trayTemplate.png" + ))?) + .icon_as_template(true) + .menu(&menu) + .on_menu_event(|app, event| match event.id.as_ref() { + "open" => { + let w = app.get_webview_window("main").unwrap(); + w.show().unwrap(); + w.set_focus().unwrap(); } + "quit" => app.exit(0), + id => { + if let Some(project) = id.strip_prefix("project:") { + start_or_focus(app, project); + } + } + }) + .build(app)?; + } + + // Linux/GNOME: rahmenloses Popup-Fenster + D-Bus-Relay. Die Shell-Extension + // (ai-control-popup@local) ruft Show() und schiebt das Fenster unter ihren + // Panel-Button — GNOME liefert der App selbst keinen Tray-Klick. + #[cfg(target_os = "linux")] + { + tauri::WebviewWindowBuilder::new(app, "popup", tauri::WebviewUrl::App("popup.html".into())) + .title("ai-control-popup") + .inner_size(320.0, 300.0) + .decorations(false) + .visible(false) + .transparent(true) + .skip_taskbar(true) + .always_on_top(true) + .resizable(false) + .build()?; + let app_dbus = app.handle().clone(); + std::thread::spawn(move || { + if let Err(e) = serve_popup_dbus(app_dbus) { + eprintln!("D-Bus-Popup-Dienst: {e}"); } - }) - .build(app)?; + }); + } Ok(()) }) @@ -1976,6 +2219,13 @@ fn main_builder() -> tauri::Builder { api.prevent_close(); window.hide().unwrap(); } + // Popup schließt bei Fokusverlust. + #[cfg(target_os = "linux")] + if let tauri::WindowEvent::Focused(false) = event { + if window.label() == "popup" { + let _ = window.hide(); + } + } }) .invoke_handler(tauri::generate_handler![ list_projects, @@ -2002,8 +2252,13 @@ fn main_builder() -> tauri::Builder { usage_stats, stop_project, restart_project, + start_or_focus_cmd, + open_main_window, + quit_app, sync_setting, set_sync_setting, + terminal_font_size, + set_terminal_font_size, link_pool_runtime, oauth_login, keychain_status, @@ -2036,7 +2291,9 @@ fn terminal_builder(project: String) -> tauri::Builder { // Header im Terminal-Prozess: Projektliste, Icon und Pool-Name. list_projects, project_icon, - pool_label + pool_label, + terminal_font_size, + set_terminal_font_size ]) } diff --git a/src-tauri/src/terminal.rs b/src-tauri/src/terminal.rs index 45f1f07..e81570c 100644 --- a/src-tauri/src/terminal.rs +++ b/src-tauri/src/terminal.rs @@ -40,6 +40,7 @@ pub fn open_terminal(app: AppHandle, project: String) -> Result<(), String> { /// Tritt die Aktivierung an den nächsten startenden Prozess mit dieser /// Bundle-ID ab (der Terminal-Prozess läuft unter derselben). +#[cfg(target_os = "macos")] fn yield_activation_to_bundle(bundle_id: &str) { use objc2::MainThreadMarker; use objc2_app_kit::NSApplication; @@ -50,8 +51,13 @@ fn yield_activation_to_bundle(bundle_id: &str) { .yieldActivationToApplicationWithBundleIdentifier(&NSString::from_str(bundle_id)); } +/// Linux kennt keine kooperative Aktivierungsabtretung. +#[cfg(target_os = "linux")] +fn yield_activation_to_bundle(_bundle_id: &str) {} + /// Selbst-Aktivierung des frisch gestarteten Terminal-Prozesses (Ready-Event); /// die Gegenseite hat vorher per yield abgetreten. +#[cfg(target_os = "macos")] pub fn activate_self(app: &AppHandle) { use objc2::MainThreadMarker; use objc2_app_kit::NSApplication; @@ -62,6 +68,14 @@ pub fn activate_self(app: &AppHandle) { } } +/// Linux: Fenster ohne NSApplication über die Tauri-API fokussieren. +#[cfg(target_os = "linux")] +pub fn activate_self(app: &AppHandle) { + if let Some(window) = app.webview_windows().values().next() { + window.set_focus().expect("Terminal-Fenster nicht fokussierbar"); + } +} + /// 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) { @@ -95,9 +109,14 @@ pub fn set_dock_icon(path: &str) { } } +/// Linux kennt kein Dock-Icon pro Prozess. +#[cfg(target_os = "linux")] +pub fn set_dock_icon(_path: &str) {} + /// Holt das Terminal-Fenster eines laufenden Projekts in den Vordergrund: /// aktiviert den Terminal-Prozess über seine PID. Aktivierung ist seit /// macOS 14 kooperativ — die Tray-App tritt sie vorher ab. +#[cfg(target_os = "macos")] pub fn focus_terminal(pid: u32) { use objc2::MainThreadMarker; use objc2_app_kit::{NSApplication, NSApplicationActivationOptions, NSRunningApplication}; @@ -111,6 +130,11 @@ pub fn focus_terminal(pid: u32) { term.activateWithOptions(NSApplicationActivationOptions::ActivateAllWindows); } +/// Linux: Fremdprozess-Fokus über die PID steht ohne NSRunningApplication +/// nicht zur Verfügung. +#[cfg(target_os = "linux")] +pub fn focus_terminal(_pid: u32) {} + /// Baut das Terminal-Fenster des Terminal-Prozesses. Die PTY entsteht erst, /// wenn das Fenster geladen ist und `term_start` ruft — so gehen keine /// Ausgaben verloren, bevor der Event-Listener steht. @@ -136,6 +160,10 @@ pub fn build_window( .title_bar_style(tauri::TitleBarStyle::Overlay) .hidden_title(true); + // Linux/GNOME: keine GTK-Deko — eigene Titelleiste im Header (terminal.html). + #[cfg(target_os = "linux")] + let builder = builder.decorations(false); + builder.build()?; Ok(()) } diff --git a/src/App.vue b/src/App.vue index df15058..5ea462d 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,6 +1,7 @@ + + diff --git a/src/components/Popup.vue b/src/components/Popup.vue new file mode 100644 index 0000000..6d317cd --- /dev/null +++ b/src/components/Popup.vue @@ -0,0 +1,182 @@ + + + + + diff --git a/src/popup.ts b/src/popup.ts new file mode 100644 index 0000000..0f64bfc --- /dev/null +++ b/src/popup.ts @@ -0,0 +1,4 @@ +import { createApp } from "vue"; +import Popup from "./components/Popup.vue"; + +createApp(Popup).mount("#app"); diff --git a/src/terminal.ts b/src/terminal.ts index bfda1fe..e0d33ae 100644 --- a/src/terminal.ts +++ b/src/terminal.ts @@ -29,6 +29,29 @@ window.addEventListener("unhandledrejection", (e) => const project = new URLSearchParams(location.search).get("project")!; const win = getCurrentWebviewWindow(); +// macOS nutzt die native Ampel (Overlay-Titelbar); sonst eigene Fensterknöpfe +// im Header (Linux ist dekorationslos, siehe terminal.rs). +const isMac = /Mac|Macintosh/.test(navigator.userAgent); +document.documentElement.dataset.platform = isMac ? "mac" : "other"; +if (!isMac) { + document + .getElementById("win-min") + ?.addEventListener("click", () => win.minimize()); + document + .getElementById("win-max") + ?.addEventListener("click", () => win.toggleMaximize()); + document + .getElementById("win-close") + ?.addEventListener("click", () => win.close()); + // Resize-Zonen: dekorationslose Fenster haben keinen nativen Rand. + for (const g of document.querySelectorAll(".grip")) { + g.addEventListener("mousedown", (e) => { + e.preventDefault(); + win.startResizeDragging(g.dataset.dir as never); + }); + } +} + // Fensterhintergrund beim Öffnen kommt aus terminal.rs (theme_background); // dort nicht gepflegte Themes fallen auf Mocha zurück (nur kurzer Moment // bis die Webview lädt). @@ -427,11 +450,15 @@ header.style.background = picked.header; header.style.borderBottomColor = picked.border; header.style.color = theme.foreground; +// Globale Schriftgröße (settings.json: terminalFontSize), ein Wert für alle. +const DEFAULT_FONT_SIZE = 13; +let fontSize = await invoke("terminal_font_size"); + const term = new Terminal({ // Ligatur-Addon nutzt die Character-Joiner-API (Proposed API) allowProposedApi: true, fontFamily: '"JetBrains Mono", Menlo, monospace', - fontSize: 13, + fontSize, fontWeightBold: "600", lineHeight: 1.0, letterSpacing: 0, @@ -443,6 +470,14 @@ const term = new Terminal({ const fit = new FitAddon(); term.loadAddon(fit); +// Ctrl/Cmd + +/-/0: Schriftgröße global anpassen, sofort im aktuellen Fenster. +function applyFontSize(px: number) { + fontSize = Math.max(8, Math.min(24, px)); + term.options.fontSize = fontSize; + fit.fit(); + invoke("set_terminal_font_size", { size: fontSize }); +} + // Shift+Enter als CSI-u-Sequenz senden (Zeilenumbruch in Claude Code, // auch bei nicht-leerer Zeile). Neben keydown muss auch keypress unter- // drückt werden, sonst sendet xterm.js zusätzlich \r und submittet damit. @@ -451,13 +486,27 @@ term.attachCustomKeyEventHandler((e) => { if (e.type === "keydown") invoke("term_write", { data: "\x1b[13;2u" }); return false; } + if ((e.ctrlKey || e.metaKey) && !e.altKey) { + if (e.key === "+" || e.key === "=") { + if (e.type === "keydown") applyFontSize(fontSize + 1); + return false; + } + if (e.key === "-" || e.key === "_") { + if (e.type === "keydown") applyFontSize(fontSize - 1); + return false; + } + if (e.key === "0") { + if (e.type === "keydown") applyFontSize(DEFAULT_FONT_SIZE); + return false; + } + } return true; }); // Font muss geladen sein, bevor WebGL den Glyphen-Atlas aufbaut — // beide verwendeten Gewichte (400 normal, 600 bold). -await document.fonts.load('13px "JetBrains Mono"'); -await document.fonts.load('600 13px "JetBrains Mono"'); +await document.fonts.load(`${fontSize}px "JetBrains Mono"`); +await document.fonts.load(`600 ${fontSize}px "JetBrains Mono"`); term.open(document.getElementById("term")!); // Reihenfolge laut Addon-Doku: Ligaturen vor WebGL aktivieren. term.loadAddon(new LigaturesAddon()); diff --git a/terminal.html b/terminal.html index 1e41ba6..32711d5 100644 --- a/terminal.html +++ b/terminal.html @@ -16,13 +16,13 @@ flex-direction: column; } header { - height: 44px; + height: 40px; flex: none; display: flex; align-items: center; gap: 10px; - /* links Platz für die macOS-Ampel (Overlay-Titelbar) */ - padding: 0 16px 0 84px; + /* Linux: schmaler Rand links; macOS: Ampel-Platz (Override unten) */ + padding: 0 0 0 16px; background: #181825; border-bottom: 1px solid #313244; color: #cdd6f4; @@ -34,6 +34,14 @@ -webkit-user-select: none; cursor: default; } + /* macOS: Platz links für die Ampel, eigene Fensterknöpfe aus. */ + :root[data-platform="mac"] header { + padding-left: 84px; + padding-right: 16px; + } + :root[data-platform="mac"] #winbtns { + display: none; + } #project-icon { width: 18px; height: 18px; @@ -53,6 +61,113 @@ #pool:empty { display: none; } + /* Eigene Fensterknöpfe (Linux): rechtsbündig, volle Header-Höhe. */ + #winbtns { + margin-left: 8px; + display: flex; + align-self: stretch; + } + #pool:empty + #winbtns { + margin-left: auto; + } + .winbtn { + width: 44px; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + border: none; + background: transparent; + color: inherit; + cursor: default; + transition: background 0.12s ease; + } + .winbtn svg { + stroke: currentColor; + stroke-width: 1.2; + opacity: 0.8; + } + .winbtn:hover { + background: #313244; + } + .winbtn:hover svg { + opacity: 1; + } + .winbtn.close:hover { + background: #f38ba8; + color: #11111b; + } + /* Resize-Zonen für das dekorationslose Fenster (nur Linux). */ + #resize-grips { + display: none; + } + :root[data-platform="other"] #resize-grips { + display: block; + } + .grip { + position: fixed; + z-index: 100; + } + .grip.n { + top: 0; + left: 0; + right: 0; + height: 4px; + cursor: ns-resize; + } + .grip.s { + bottom: 0; + left: 0; + right: 0; + height: 4px; + cursor: ns-resize; + } + .grip.w { + top: 0; + bottom: 0; + left: 0; + width: 4px; + cursor: ew-resize; + } + .grip.e { + top: 0; + bottom: 0; + right: 0; + width: 4px; + cursor: ew-resize; + } + .grip.nw { + top: 0; + left: 0; + width: 9px; + height: 9px; + cursor: nwse-resize; + z-index: 101; + } + .grip.ne { + top: 0; + right: 0; + width: 9px; + height: 9px; + cursor: nesw-resize; + z-index: 101; + } + .grip.sw { + bottom: 0; + left: 0; + width: 9px; + height: 9px; + cursor: nesw-resize; + z-index: 101; + } + .grip.se { + bottom: 0; + right: 0; + width: 9px; + height: 9px; + cursor: nwse-resize; + z-index: 101; + } #term { flex: 1; min-height: 0; @@ -75,8 +190,29 @@ +
+ + + +
+ diff --git a/vite.config.ts b/vite.config.ts index ddb8191..6fbc1f6 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -17,6 +17,7 @@ export default defineConfig(async () => ({ input: { main: new URL("./index.html", import.meta.url).pathname, terminal: new URL("./terminal.html", import.meta.url).pathname, + popup: new URL("./popup.html", import.meta.url).pathname, }, }, },