//! 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::Foundation::PROPERTYKEY; use windows::Win32::System::Com::{ CoCreateInstance, CoInitializeEx, CLSCTX_INPROC_SERVER, COINIT_APARTMENTTHREADED, }; use windows::Win32::System::Com::StructuredStorage::PROPVARIANT; use windows::Win32::System::Variant::VT_LPWSTR; use windows::Win32::UI::Shell::{ ICustomDestinationList, IShellLinkW, SetCurrentProcessExplicitAppUserModelID, DestinationList, EnumerableObjectCollection, ShellLink, }; use windows::Win32::UI::Shell::Common::IObjectCollection; use windows::Win32::UI::Shell::PropertiesSystem::IPropertyStore; const APP_ID: &str = "com.obsigate.desktop"; // PKEY_Title (System.Title): {F29F85E0-4FF9-1068-AB91-08002B27B3D9}, pid 2 const PKEY_TITLE: PROPERTYKEY = PROPERTYKEY { fmtid: windows::core::GUID::from_u128(0xF29F85E0_4FF9_1068_AB91_08002B27B3D9), pid: 2, }; fn wide_null_terminated(s: &str) -> Vec { std::ffi::OsStr::new(s).encode_wide().chain(std::iter::once(0)).collect() } /// Set the jump-list display title via the shell link's property store. fn set_link_title(link: &IShellLinkW, title: &str) -> windows::core::Result<()> { let store: IPropertyStore = link.cast()?; let mut wide = wide_null_terminated(title); let mut pv: PROPVARIANT = unsafe { std::mem::zeroed() }; unsafe { let inner = &mut *pv.Anonymous.Anonymous; inner.vt = VT_LPWSTR; inner.Anonymous.pwszVal = windows::core::PWSTR(wide.as_mut_ptr()); store.SetValue(&PKEY_TITLE, &pv)?; store.Commit()?; } Ok(()) } /// 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 { 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::>(); 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) is best-effort — a missing title only means the // jump-list entry falls back to the executable name. let _ = set_link_title(&link, name); 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::(&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!["a", "b", "c"]); } }