feat(windows): implement explorer shell integration, auto drive allocation, auto-open, and system tray icon

This commit is contained in:
2026-09-08 10:04:24 +02:00
parent 1c8a860184
commit 38df3d3845
6 changed files with 561 additions and 23 deletions
+201
View File
@@ -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<char> {
#[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<tray_item::IconSource> {
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');
}
}