use crate::libs::toast_action::{ToastAction, shortcut_dir, validate_issue_key};
use anyhow::{Context, Result, bail};
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
use std::path::Path;
use tracing::debug;
use winapi::Interface;
use winapi::shared::guiddef::CLSID;
use winapi::shared::wtypesbase::CLSCTX_INPROC_SERVER;
use winapi::um::combaseapi::{CoCreateInstance, CoInitializeEx, CoUninitialize};
use winapi::um::objbase::COINIT_APARTMENTTHREADED;
use winapi::um::objidl::IPersistFile;
use winapi::um::shobjidl_core::IShellLinkW;
use winapi::um::winuser::SW_SHOWMINNOACTIVE;
const CLSID_SHELL_LINK: CLSID = CLSID {
Data1: 0x0002_1401,
Data2: 0x0000,
Data3: 0x0000,
Data4: [0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46],
};
pub fn ensure(action: ToastAction, issue_key: &str) -> Result<String> {
validate_issue_key(issue_key)?;
let exe = std::env::current_exe().context("cannot find the running kasl executable")?;
let dir = shortcut_dir()?;
let path = dir.join(format!("{}-{}.lnk", action.as_str(), issue_key));
let arguments = format!("toast-action {} {}", action.as_str(), issue_key);
write_shortcut(&path, &exe, &arguments).with_context(|| format!("cannot write the toast shortcut {}", path.display()))?;
debug!("Toast shortcut ready: {}", path.display());
Ok(file_uri(&path))
}
pub fn forget(issue_key: &str) {
let Ok(dir) = shortcut_dir() else { return };
for action in ToastAction::ALL {
let path = dir.join(format!("{}-{}.lnk", action.as_str(), issue_key));
let _ = std::fs::remove_file(path);
}
}
fn file_uri(path: &Path) -> String {
format!("file:///{}", path.display().to_string().replace('\\', "/"))
}
fn wide(value: &str) -> Vec<u16> {
OsStr::new(value).encode_wide().chain(std::iter::once(0)).collect()
}
fn write_shortcut(path: &Path, target: &Path, arguments: &str) -> Result<()> {
let init = unsafe { CoInitializeEx(std::ptr::null_mut(), COINIT_APARTMENTTHREADED) };
let ours = init == 0;
let result = write_link(path, target, arguments);
if ours {
unsafe { CoUninitialize() };
}
result
}
fn write_link(path: &Path, target: &Path, arguments: &str) -> Result<()> {
let mut raw: *mut IShellLinkW = std::ptr::null_mut();
let hr = unsafe {
CoCreateInstance(
&CLSID_SHELL_LINK,
std::ptr::null_mut(),
CLSCTX_INPROC_SERVER,
&IShellLinkW::uuidof(),
&mut raw as *mut *mut IShellLinkW as *mut *mut _,
)
};
if hr < 0 || raw.is_null() {
bail!("CoCreateInstance for ShellLink failed: 0x{hr:08X}");
}
let link = ComPtr(raw);
fill_and_save(link.get(), path, target, arguments)
}
struct ComPtr(*mut IShellLinkW);
impl ComPtr {
fn get(&self) -> &IShellLinkW {
unsafe { &*self.0 }
}
}
impl Drop for ComPtr {
fn drop(&mut self) {
unsafe { (*self.0).Release() };
}
}
fn fill_and_save(link: &IShellLinkW, path: &Path, target: &Path, arguments: &str) -> Result<()> {
let target_w = wide(&target.display().to_string());
let hr = unsafe { link.SetPath(target_w.as_ptr()) };
if hr < 0 {
bail!("IShellLink::SetPath failed: 0x{hr:08X}");
}
let args_w = wide(arguments);
let hr = unsafe { link.SetArguments(args_w.as_ptr()) };
if hr < 0 {
bail!("IShellLink::SetArguments failed: 0x{hr:08X}");
}
let hr = unsafe { link.SetShowCmd(SW_SHOWMINNOACTIVE) };
if hr < 0 {
bail!("IShellLink::SetShowCmd failed: 0x{hr:08X}");
}
let mut persist: *mut IPersistFile = std::ptr::null_mut();
let hr = unsafe { link.QueryInterface(&IPersistFile::uuidof(), &mut persist as *mut *mut IPersistFile as *mut *mut _) };
if hr < 0 || persist.is_null() {
bail!("QueryInterface for IPersistFile failed: 0x{hr:08X}");
}
let path_w = wide(&path.display().to_string());
let hr = unsafe {
let saved = (*persist).Save(path_w.as_ptr(), 1);
(*persist).Release();
saved
};
if hr < 0 {
bail!("IPersistFile::Save failed: 0x{hr:08X}");
}
Ok(())
}