Files
ObsiGate/desktop/src/jumplist.rs
T
bruno bab272bb5c
CI / lint (push) Successful in 2m45s
CI / security (push) Successful in 1m59s
CI / test (push) Successful in 4m44s
CI / build (push) Successful in 1m52s
CI / e2e (push) Successful in 16m16s
fix: desktop — commandes Tauri bloquées par l'ACL et crash heap à l'ouverture (BUG-104, BUG-105)
2026-10-02 20:17:20 -04:00

155 lines
5.7 KiB
Rust

//! Windows Taskbar Jump List — recent vaults.
//!
//! Populates the taskbar jump list with the configured vaults so the user can
//! jump straight into a vault from the Windows taskbar / Start menu.
//!
//! Best-effort: every step is fallible and logged, never panics, and a failure
//! simply leaves the jump list at its OS-managed default state.
use log::{info, warn};
#[cfg(target_os = "windows")]
mod imp {
use super::*;
use std::os::windows::ffi::OsStrExt;
use windows::core::{Interface, HSTRING, PCWSTR};
use windows::Win32::System::Com::{
CoCreateInstance, CoInitializeEx, CLSCTX_INPROC_SERVER, COINIT_APARTMENTTHREADED,
};
use windows::Win32::UI::Shell::{
ICustomDestinationList, IShellLinkW, SetCurrentProcessExplicitAppUserModelID,
DestinationList, EnumerableObjectCollection, ShellLink,
};
use windows::Win32::UI::Shell::Common::IObjectCollection;
const APP_ID: &str = "com.obsigate.desktop";
fn wide_null_terminated(s: &str) -> Vec<u16> {
std::ffi::OsStr::new(s).encode_wide().chain(std::iter::once(0)).collect()
}
/// Build one shell link for a vault. `arg` is the command-line argument the
/// app expects to open that vault (see file associations / single-instance).
fn build_shell_link(name: &str, arg: &str) -> windows::core::Result<IShellLinkW> {
let link: IShellLinkW =
unsafe { CoCreateInstance(&ShellLink, None, CLSCTX_INPROC_SERVER)? };
let exe = std::env::current_exe()
.map_err(|e| windows::core::Error::from(std::io::Error::other(e)))?;
let exe_wide = exe.as_os_str().encode_wide().chain(std::iter::once(0)).collect::<Vec<u16>>();
unsafe {
link.SetPath(PCWSTR(exe_wide.as_ptr()))?;
if !arg.is_empty() {
link.SetArguments(&HSTRING::from(arg))?;
}
link.SetDescription(&HSTRING::from(name))?;
}
// Title (display name): the raw PROPVARIANT setter that used to live
// here corrupted the process heap (STATUS_HEAP_CORRUPTION c0000374 —
// the shell property store freed our Rust-allocated VT_LPWSTR buffer).
// SetDescription below already gives jump-list entries their label.
Ok(link)
}
/// Populate the jump list with the given (name, arg) vault entries.
pub fn update_jumplist(vaults: &[(String, String)]) {
// COM must be initialised on this thread.
let _ = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) };
let result = (|| -> windows::core::Result<()> {
unsafe {
SetCurrentProcessExplicitAppUserModelID(PCWSTR(wide_null_terminated(APP_ID).as_ptr()))?;
}
let list: ICustomDestinationList =
unsafe { CoCreateInstance(&DestinationList, None, CLSCTX_INPROC_SERVER)? };
let mut min_slots = 0u32;
unsafe {
list.BeginList::<windows::core::IUnknown>(&mut min_slots)?;
}
// "Vaults" category
let collection: IObjectCollection = unsafe {
CoCreateInstance(&EnumerableObjectCollection, None, CLSCTX_INPROC_SERVER)?
};
for (name, arg) in vaults {
match build_shell_link(name, arg) {
Ok(link) => unsafe {
let _ = collection.AddObject(&link);
},
Err(e) => warn!("jumplist: failed to build link for {}: {}", name, e),
}
}
unsafe {
let array: windows::Win32::UI::Shell::Common::IObjectArray = collection.cast()?;
list.AppendCategory(&HSTRING::from("Vaults"), &array)?;
list.CommitList()?;
}
Ok(())
})();
match result {
Ok(()) => info!("jumplist updated with {} vaults", vaults.len()),
Err(e) => warn!("jumplist update failed (non-fatal): {}", e),
}
}
}
#[cfg(not(target_os = "windows"))]
mod imp {
use super::*;
/// No-op on non-Windows platforms.
pub fn update_jumplist(_vaults: &[(String, String)]) {
info!("jumplist is Windows-only — skipped");
}
}
/// Public entry point: update the Windows jump list from the configured vaults.
/// Each entry maps a vault name to the CLI argument the app accepts to open it.
pub fn update_jumplist(vaults: &[(String, String)]) {
imp::update_jumplist(vaults);
}
/// Build the (name, arg) pairs for the jump list from vault paths.
/// Pure helper so the mapping is unit-testable on every platform.
pub fn build_vault_args(vaults: &[crate::VaultConfig]) -> Vec<(String, String)> {
vaults
.iter()
.map(|v| (v.name.clone(), format!("--vault={}", v.path)))
.collect()
}
#[cfg(test)]
mod tests {
use super::build_vault_args;
use crate::VaultConfig;
fn v(name: &str, path: &str) -> VaultConfig {
VaultConfig { name: name.to_string(), path: path.to_string() }
}
#[test]
fn test_build_vault_args_empty() {
assert!(build_vault_args(&[]).is_empty());
}
#[test]
fn test_build_vault_args_maps_name_and_path() {
let vaults = vec![v("Perso", "C:\\vaults\\perso"), v("IT", "/home/bruno/IT")];
let args = build_vault_args(&vaults);
assert_eq!(args.len(), 2);
assert_eq!(args[0], ("Perso".to_string(), "--vault=C:\\vaults\\perso".to_string()));
assert_eq!(args[1], ("IT".to_string(), "--vault=/home/bruno/IT".to_string()));
}
#[test]
fn test_build_vault_args_preserves_order() {
let vaults = vec![v("a", "/a"), v("b", "/b"), v("c", "/c")];
let args = build_vault_args(&vaults);
assert_eq!(args.iter().map(|(n, _)| n.as_str()).collect::<Vec<_>>(), vec!["a", "b", "c"]);
}
}