From 38df3d3845ca80d7c307e82b26a6c6d3d957b5b7 Mon Sep 17 00:00:00 2001 From: harald Date: Tue, 8 Sep 2026 10:04:24 +0200 Subject: [PATCH] feat(windows): implement explorer shell integration, auto drive allocation, auto-open, and system tray icon --- Cargo.lock | 259 ++++++++++++++++++++++++++++++++++++++++++++++--- Cargo.toml | 1 + src/lib.rs | 1 + src/main.rs | 57 ++++++++++- src/mount.rs | 65 +++++++++++-- src/windows.rs | 201 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 561 insertions(+), 23 deletions(-) create mode 100644 src/windows.rs diff --git a/Cargo.lock b/Cargo.lock index 293ad36..02fa595 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -104,7 +104,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -115,7 +115,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -187,6 +187,12 @@ dependencies = [ "hex-conservative", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.13.1" @@ -202,6 +208,12 @@ dependencies = [ "digest", ] +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + [[package]] name = "block-buffer" version = "0.10.4" @@ -300,18 +312,82 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation", + "core-graphics", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation", + "core-graphics-types", + "libc", + "objc", +] + [[package]] name = "colorchoice" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -421,7 +497,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -448,6 +524,33 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -947,6 +1050,15 @@ dependencies = [ "twox-hash", ] +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + [[package]] name = "matchers" version = "0.2.0" @@ -986,7 +1098,7 @@ checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -995,7 +1107,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1007,6 +1119,35 @@ dependencies = [ "autocfg", ] +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -1025,6 +1166,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "padlock" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c10569378a1dacd9f30dbe7ae49e054d2c45dc2f8ee49899903e09c3924e8b6f" + [[package]] name = "parking_lot" version = "0.12.5" @@ -1167,7 +1314,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.13.1", ] [[package]] @@ -1195,7 +1342,7 @@ checksum = "2da316a15f47e3d053de9cb2c439650bd8fa4aaeb9365f2e5f27f492ff73c196" dependencies = [ "libc", "rtoolbox", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1205,7 +1352,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a1efe12a1469752d0e6ff5ebec0b6ef4924cc5c4c71046b0ec730040535819d" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1214,7 +1361,7 @@ version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" dependencies = [ - "bitflags", + "bitflags 2.13.1", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -1255,6 +1402,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", + "tray-item", "zeroize", ] @@ -1362,7 +1510,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1484,7 +1632,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1573,6 +1721,22 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "tray-item" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59d4bd406170690dc30eabb3badc67a085beaf9b2c3b1923afcc9c26a2191353" +dependencies = [ + "cocoa", + "core-graphics", + "libc", + "objc", + "objc-foundation", + "objc_id", + "padlock", + "windows-sys 0.52.0", +] + [[package]] name = "twox-hash" version = "2.1.4" @@ -1779,6 +1943,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -1788,6 +1961,70 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "writeable" version = "0.6.4" diff --git a/Cargo.toml b/Cargo.toml index b4ac2b9..4e3c93a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ dyn-clone = "1.0" lz4_flex = "0.11" bip39 = { version = "2.2", features = ["zeroize"] } hex = "0.4" +tray-item = "0.10" [profile.release] opt-level = 3 diff --git a/src/lib.rs b/src/lib.rs index 710f405..14852c2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,3 +5,4 @@ pub mod storage; pub mod ui; pub mod verify; pub mod vfs; +pub mod windows; diff --git a/src/main.rs b/src/main.rs index 1e07b75..4d19936 100644 --- a/src/main.rs +++ b/src/main.rs @@ -41,9 +41,9 @@ enum Commands { #[arg(short, long)] path: PathBuf, - /// Laufwerksbuchstabe (z. B. 'S' oder 'S:') + /// Laufwerksbuchstabe (z. B. 'S' oder 'S:', optional; wählt standardmäßig automatisch das nächste freie Laufwerk) #[arg(short, long)] - drive: String, + drive: Option, /// Optionaler TCP-Port für den lokalen WebDAV-Server (Standard: 8443) #[arg(long)] @@ -52,6 +52,14 @@ enum Commands { /// Optionaler 24-Wort Notfall-Wiederherstellungsschlüssel (umgeht Passwortabfrage) #[arg(long)] recovery_key: Option, + + /// Öffnet das Netzlaufwerk nach dem Mounten nicht automatisch im Windows Explorer + #[arg(long, default_value_t = false)] + no_open: bool, + + /// Deaktiviert das Windows System-Tray Icon während des Mounts + #[arg(long, default_value_t = false)] + no_tray: bool, }, /// Trennt ein eingebundenes Netzlaufwerk manuell @@ -115,6 +123,12 @@ enum Commands { #[arg(long, default_value_t = true)] full: bool, }, + + /// Registriert .sanctum Containerdateien im Windows Explorer (Doppelklick & Kontextmenü, keine Adminrechte) + Register, + + /// Entfernt die .sanctum Explorer-Integration aus der Benutzer-Registry + Unregister, } fn parse_drive_letter(s: &str) -> Result { @@ -465,8 +479,21 @@ async fn run() -> Result<()> { drive, port, recovery_key, + no_open, + no_tray, } => { - let drive_char = parse_drive_letter(&drive)?; + let drive_char = match drive { + Some(ref d) => parse_drive_letter(d)?, + None => { + let auto_drive = sanctum::windows::find_next_available_drive()?; + println!( + " {} Kein Laufwerksbuchstabe angegeben. Wähle automatisch freien Buchstaben {}:", + ui::cyan("ℹ"), + auto_drive + ); + auto_drive + } + }; let auth = if let Some(key) = recovery_key { ContainerAuth::RecoveryKey(key) } else { @@ -479,7 +506,9 @@ async fn run() -> Result<()> { ContainerAuth::Password(password) }; - mount_container(&path, drive_char, port, auth).await?; + let open_explorer = !no_open; + let enable_tray = !no_tray; + mount_container(&path, drive_char, port, auth, open_explorer, enable_tray).await?; } Commands::Unmount { drive } => { let drive_char = parse_drive_letter(&drive)?; @@ -509,6 +538,26 @@ async fn run() -> Result<()> { Commands::Verify { path, full } => { handle_verify(&path, full)?; } + Commands::Register => { + println!("┌─────────────────────────────────────────────────────────────┐"); + println!("│ Sanctum — Windows Explorer Integration einrichten │"); + println!("└─────────────────────────────────────────────────────────────┘"); + println!(); + sanctum::windows::register_explorer_integration()?; + println!(" {} .sanctum-Dateien wurden erfolgreich im Windows Explorer verknüpft!", ui::green("✔")); + println!(" • Doppelklick: Öffnet und bindet den Container direkt als Laufwerk ein"); + println!(" • Rechtsklick: Bietet Optionen für Integritätsprüfung (FSCK) und Header-Backup"); + println!(); + } + Commands::Unregister => { + println!("┌─────────────────────────────────────────────────────────────┐"); + println!("│ Sanctum — Windows Explorer Integration entfernen │"); + println!("└─────────────────────────────────────────────────────────────┘"); + println!(); + sanctum::windows::unregister_explorer_integration()?; + println!(" {} .sanctum Dateiverknüpfungen wurden aus der Registry entfernt.", ui::green("✔")); + println!(); + } } Ok(()) diff --git a/src/mount.rs b/src/mount.rs index 695b41d..0038bf3 100644 --- a/src/mount.rs +++ b/src/mount.rs @@ -81,6 +81,8 @@ pub async fn mount_container( drive_letter: char, requested_port: Option, auth: ContainerAuth, + open_explorer: bool, + enable_tray: bool, ) -> Result<()> { let drive_str = format_drive(drive_letter); @@ -207,6 +209,45 @@ pub async fn mount_container( return Err(e); } + // Optional automatisch im Windows Explorer öffnen + if open_explorer { + let _ = crate::windows::open_in_explorer(drive_letter); + } + + // System-Tray Initialisierung + let (tray_shutdown_tx, mut tray_shutdown_rx) = tokio::sync::mpsc::channel::<()>(1); + #[cfg(windows)] + let _tray = if enable_tray { + let icon_source = crate::windows::get_default_system_icon() + .unwrap_or(tray_item::IconSource::Resource("")); + let title = format!("Sanctum ({drive_str})"); + match tray_item::TrayItem::new(&title, icon_source) { + Ok(mut tray) => { + let container_name = container_path + .file_name() + .unwrap_or_default() + .to_string_lossy() + .to_string(); + let _ = tray.add_label(&format!("Sanctum: {drive_str} ({container_name})")); + let dl = drive_letter; + let _ = tray.add_menu_item("Im Explorer öffnen", move || { + let _ = crate::windows::open_in_explorer(dl); + }); + let s_tx = tray_shutdown_tx.clone(); + let _ = tray.add_menu_item("Trennen & Beenden", move || { + let _ = s_tx.blocking_send(()); + }); + Some(tray) + } + Err(e) => { + debug!("System-Tray Icon konnte nicht erstellt werden: {e}"); + None + } + } + } else { + None + }; + println!(); println!("┌─────────────────────────────────────────────────────────────┐"); println!("│ ✔ Sanctum Container erfolgreich gemountet │"); @@ -215,17 +256,25 @@ pub async fn mount_container( println!(" • Container: {}", container_path.display()); println!(" • Netzlaufwerk: {} (im Windows Explorer bereit)", ui::cyan(&drive_str)); println!(" • WebDAV-URL: http://127.0.0.1:{}/", bound_port); + if enable_tray { + println!(" • System-Tray: Icon aktiv (Rechtsklick für Explorer/Trennen)"); + } println!(); - println!(" [{}] Drücke [Ctrl+C] zum sicheren Trennen und Schließen.", ui::yellow("Tipp")); + println!(" [{}] Drücke [Ctrl+C] oder nutze das Tray-Icon zum Beenden.", ui::yellow("Tipp")); println!(); - // Warten auf Strg+C - tokio::signal::ctrl_c() - .await - .context("Fehler beim Registrieren des Ctrl+C Signalhandlers")?; - - println!(); - println!(" {} Beendigungssignal (Ctrl+C) empfangen.", ui::yellow("[!]")); + // Warten auf Strg+C ODER Signal aus dem System-Tray + tokio::select! { + res = tokio::signal::ctrl_c() => { + let _ = res; + println!(); + println!(" {} Beendigungssignal (Ctrl+C) empfangen.", ui::yellow("[!]")); + } + _ = tray_shutdown_rx.recv() => { + println!(); + println!(" {} Beendigungssignal aus System-Tray empfangen.", ui::yellow("[!]")); + } + } print!(" {} Trenne Windows-Netzlaufwerk {} ... ", ui::dim("[-]"), drive_str); let _ = std::io::Write::flush(&mut std::io::stdout()); diff --git a/src/windows.rs b/src/windows.rs new file mode 100644 index 0000000..e79d9a6 --- /dev/null +++ b/src/windows.rs @@ -0,0 +1,201 @@ +use std::process::Command; + +use anyhow::{bail, Context, Result}; + +/// Ermittelt den nächsten verfügbaren Windows-Laufwerksbuchstaben (von 'Z' rückwärts bis 'D'). +pub fn find_next_available_drive() -> Result { + #[cfg(windows)] + { + extern "system" { + fn GetLogicalDrives() -> u32; + } + + let mask = unsafe { GetLogicalDrives() }; + if mask == 0 { + bail!("Fehler beim Abfragen der logischen Laufwerke via Win32 API"); + } + + // Suche von 'Z' abwärts bis 'D' (A, B für Diskettenlaufwerke, C für System reservieren) + for ch in ('D'..='Z').rev() { + let bit_index = (ch as u8) - b'A'; + if (mask & (1 << bit_index)) == 0 { + return Ok(ch); + } + } + + bail!("Kein freier Windows-Laufwerksbuchstabe (D: bis Z:) verfügbar!"); + } + + #[cfg(not(windows))] + { + Ok('S') + } +} + +/// Öffnet das eingebundene Netzlaufwerk direkt im Windows Explorer. +pub fn open_in_explorer(drive_char: char) -> Result<()> { + let drive_path = format!("{}:\\", drive_char.to_ascii_uppercase()); + Command::new("explorer.exe") + .arg(&drive_path) + .spawn() + .with_context(|| format!("Konnte Windows Explorer für '{}' nicht öffnen", drive_path))?; + Ok(()) +} + +/// Benachrichtigt die Windows-Shell (Explorer) über geänderte Dateiverknüpfungen (SHCNE_ASSOCCHANGED). +pub fn notify_shell_associations_changed() { + #[cfg(windows)] + { + extern "system" { + fn SHChangeNotify( + w_event_id: i32, + u_flags: u32, + dw_item1: *const std::ffi::c_void, + dw_item2: *const std::ffi::c_void, + ); + } + + const SHCNE_ASSOCCHANGED: i32 = 0x0800_0000; + const SHCNF_IDLIST: u32 = 0x0000; + + unsafe { + SHChangeNotify( + SHCNE_ASSOCCHANGED, + SHCNF_IDLIST, + std::ptr::null(), + std::ptr::null(), + ); + } + } +} + +/// Registriert `.sanctum`-Containerdateien im Windows Explorer für den aktuellen Benutzer (HKCU, 100% Userland, keine Adminrechte). +pub fn register_explorer_integration() -> Result<()> { + let current_exe = std::env::current_exe() + .context("Konnte den Pfad zur aktuellen sanctum.exe nicht ermitteln")?; + let exe_str = current_exe.display().to_string(); + + let reg_commands = [ + // 1. .sanctum Erweiterung mit ProgID verknüpfen + ( + r"HKCU\Software\Classes\.sanctum", + "", + "Sanctum.Container", + ), + // 2. ProgID Metadaten & Beschreibung + ( + r"HKCU\Software\Classes\Sanctum.Container", + "", + "Sanctum Verschlüsselter Container", + ), + // 3. Icon + ( + r"HKCU\Software\Classes\Sanctum.Container\DefaultIcon", + "", + &format!("\"{exe_str}\",0"), + ), + // 4. Standard-Doppelklick-Aktion: Mount + ( + r"HKCU\Software\Classes\Sanctum.Container\shell\open", + "", + "Als Laufwerk einbinden", + ), + ( + r"HKCU\Software\Classes\Sanctum.Container\shell\open\command", + "", + &format!("\"{exe_str}\" mount --path \"%1\""), + ), + // 5. Kontextmenü-Aktion: Integritätsprüfung + ( + r"HKCU\Software\Classes\Sanctum.Container\shell\verify", + "", + "Integrität prüfen (FSCK)", + ), + ( + r"HKCU\Software\Classes\Sanctum.Container\shell\verify\command", + "", + &format!("\"{exe_str}\" verify --path \"%1\""), + ), + // 6. Kontextmenü-Aktion: Header sichern + ( + r"HKCU\Software\Classes\Sanctum.Container\shell\backup_header", + "", + "Header sichern", + ), + ( + r"HKCU\Software\Classes\Sanctum.Container\shell\backup_header\command", + "", + &format!("\"{exe_str}\" backup-header --path \"%1\""), + ), + ]; + + for (key, val_name, val_data) in reg_commands { + let mut cmd = Command::new("reg"); + cmd.arg("add").arg(key); + if val_name.is_empty() { + cmd.arg("/ve"); + } else { + cmd.arg("/v").arg(val_name); + } + cmd.arg("/d").arg(val_data).arg("/f"); + + let output = cmd.output().with_context(|| format!("Fehler beim Ausführen von 'reg add {key}'"))?; + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + bail!("Registry-Fehler beim Anlegen von {key}: {stderr}"); + } + } + + notify_shell_associations_changed(); + Ok(()) +} + +/// Entfernt die Windows-Explorer-Verknüpfungen aus der Benutzer-Registry (HKCU). +pub fn unregister_explorer_integration() -> Result<()> { + let keys_to_delete = [ + r"HKCU\Software\Classes\.sanctum", + r"HKCU\Software\Classes\Sanctum.Container", + ]; + + for key in keys_to_delete { + let _ = Command::new("reg") + .args(["delete", key, "/f"]) + .output(); + } + + notify_shell_associations_changed(); + Ok(()) +} + +/// Lädt das Windows-Sicherheitsschild-Icon (IDI_SHIELD) oder Anwendungs-Icon für den System-Tray. +#[cfg(windows)] +pub fn get_default_system_icon() -> Option { + extern "system" { + fn LoadIconW(instance: isize, icon_name: *const u16) -> isize; + } + + // IDI_SHIELD = 32518, IDI_APPLICATION = 32512 + let icon = unsafe { LoadIconW(0, 32518 as *const u16) }; + if icon != 0 { + Some(tray_item::IconSource::RawIcon(icon)) + } else { + let icon_app = unsafe { LoadIconW(0, 32512 as *const u16) }; + if icon_app != 0 { + Some(tray_item::IconSource::RawIcon(icon_app)) + } else { + None + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_find_next_available_drive() { + let drive = find_next_available_drive().expect("Find next drive"); + assert!(drive.is_ascii_alphabetic()); + assert!(drive >= 'D' && drive <= 'Z'); + } +}