1use std::path::PathBuf;
  2
  3use itertools::Itertools;
  4use smallvec::SmallVec;
  5use windows::{
  6    Win32::{
  7        Foundation::PROPERTYKEY,
  8        Globalization::u_strlen,
  9        System::Com::{CLSCTX_INPROC_SERVER, CoCreateInstance, StructuredStorage::PROPVARIANT},
 10        UI::{
 11            Controls::INFOTIPSIZE,
 12            Shell::{
 13                Common::{IObjectArray, IObjectCollection},
 14                DestinationList, EnumerableObjectCollection, ICustomDestinationList, IShellLinkW,
 15                PropertiesSystem::IPropertyStore,
 16                ShellLink,
 17            },
 18        },
 19    },
 20    core::{GUID, HSTRING, Interface},
 21};
 22
 23use crate::{Action, MenuItem};
 24
 25pub(crate) struct JumpList {
 26    pub(crate) dock_menus: Vec<DockMenuItem>,
 27    pub(crate) recent_workspaces: Vec<SmallVec<[PathBuf; 2]>>,
 28}
 29
 30impl JumpList {
 31    pub(crate) fn new() -> Self {
 32        Self {
 33            dock_menus: Vec::new(),
 34            recent_workspaces: Vec::new(),
 35        }
 36    }
 37}
 38
 39pub(crate) struct DockMenuItem {
 40    pub(crate) name: String,
 41    pub(crate) description: String,
 42    pub(crate) action: Box<dyn Action>,
 43}
 44
 45impl DockMenuItem {
 46    pub(crate) fn new(item: MenuItem) -> anyhow::Result<Self> {
 47        match item {
 48            MenuItem::Action { name, action, .. } => Ok(Self {
 49                name: name.clone().into(),
 50                description: if name == "New Window" {
 51                    "Opens a new window".to_string()
 52                } else {
 53                    name.into()
 54                },
 55                action,
 56            }),
 57            _ => anyhow::bail!("Only `MenuItem::Action` is supported for dock menu on Windows."),
 58        }
 59    }
 60}
 61
 62// This code is based on the example from Microsoft:
 63// https://github.com/microsoft/Windows-classic-samples/blob/main/Samples/Win7Samples/winui/shell/appshellintegration/RecipePropertyHandler/RecipePropertyHandler.cpp
 64pub(crate) fn update_jump_list(
 65    jump_list: &JumpList,
 66) -> anyhow::Result<Vec<SmallVec<[PathBuf; 2]>>> {
 67    let (list, removed) = create_destination_list()?;
 68    add_recent_folders(&list, &jump_list.recent_workspaces, removed.as_ref())?;
 69    add_dock_menu(&list, &jump_list.dock_menus)?;
 70    unsafe { list.CommitList() }?;
 71    Ok(removed)
 72}
 73
 74// Copied from:
 75// https://github.com/microsoft/windows-rs/blob/0fc3c2e5a13d4316d242bdeb0a52af611eba8bd4/crates/libs/windows/src/Windows/Win32/Storage/EnhancedStorage/mod.rs#L1881
 76const PKEY_TITLE: PROPERTYKEY = PROPERTYKEY {
 77    fmtid: GUID::from_u128(0xf29f85e0_4ff9_1068_ab91_08002b27b3d9),
 78    pid: 2,
 79};
 80
 81fn create_destination_list() -> anyhow::Result<(ICustomDestinationList, Vec<SmallVec<[PathBuf; 2]>>)>
 82{
 83    let list: ICustomDestinationList =
 84        unsafe { CoCreateInstance(&DestinationList, None, CLSCTX_INPROC_SERVER) }?;
 85
 86    let mut slots = 0;
 87    let user_removed: IObjectArray = unsafe { list.BeginList(&mut slots) }?;
 88
 89    let count = unsafe { user_removed.GetCount() }?;
 90    if count == 0 {
 91        return Ok((list, Vec::new()));
 92    }
 93
 94    let mut removed = Vec::with_capacity(count as usize);
 95    for i in 0..count {
 96        let shell_link: IShellLinkW = unsafe { user_removed.GetAt(i)? };
 97        let description = {
 98            // INFOTIPSIZE is the maximum size of the buffer
 99            // see https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-ishelllinkw-getdescription
100            let mut buffer = [0u16; INFOTIPSIZE as usize];
101            unsafe { shell_link.GetDescription(&mut buffer)? };
102            let len = unsafe { u_strlen(buffer.as_ptr()) };
103            String::from_utf16_lossy(&buffer[..len as usize])
104        };
105        let args = description.split('\n').map(PathBuf::from).collect();
106
107        removed.push(args);
108    }
109
110    Ok((list, removed))
111}
112
113fn add_dock_menu(list: &ICustomDestinationList, dock_menus: &[DockMenuItem]) -> anyhow::Result<()> {
114    unsafe {
115        let tasks: IObjectCollection =
116            CoCreateInstance(&EnumerableObjectCollection, None, CLSCTX_INPROC_SERVER)?;
117        for (idx, dock_menu) in dock_menus.iter().enumerate() {
118            let argument = HSTRING::from(format!("--dock-action {}", idx));
119            let description = HSTRING::from(dock_menu.description.as_str());
120            let display = dock_menu.name.as_str();
121            let task = create_shell_link(argument, description, None, display)?;
122            tasks.AddObject(&task)?;
123        }
124        list.AddUserTasks(&tasks)?;
125        Ok(())
126    }
127}
128
129fn add_recent_folders(
130    list: &ICustomDestinationList,
131    entries: &[SmallVec<[PathBuf; 2]>],
132    removed: &Vec<SmallVec<[PathBuf; 2]>>,
133) -> anyhow::Result<()> {
134    unsafe {
135        let tasks: IObjectCollection =
136            CoCreateInstance(&EnumerableObjectCollection, None, CLSCTX_INPROC_SERVER)?;
137
138        for folder_path in entries.iter().filter(|path| !removed.contains(path)) {
139            let argument = HSTRING::from(
140                folder_path
141                    .iter()
142                    .map(|path| format!("\"{}\"", path.display()))
143                    .join(" "),
144            );
145
146            let description = HSTRING::from(
147                folder_path
148                    .iter()
149                    .map(|path| path.to_string_lossy())
150                    .collect::<Vec<_>>()
151                    .join("\n"),
152            );
153            // simulate folder icon
154            // https://github.com/microsoft/vscode/blob/7a5dc239516a8953105da34f84bae152421a8886/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts#L380
155            let icon = HSTRING::from("explorer.exe");
156
157            let display = folder_path
158                .iter()
159                .map(|p| {
160                    p.file_name()
161                        .map(|name| name.to_string_lossy())
162                        .unwrap_or_else(|| p.to_string_lossy())
163                })
164                .join(", ");
165
166            tasks.AddObject(&create_shell_link(
167                argument,
168                description,
169                Some(icon),
170                &display,
171            )?)?;
172        }
173
174        list.AppendCategory(&HSTRING::from("Recent Folders"), &tasks)?;
175        Ok(())
176    }
177}
178
179fn create_shell_link(
180    argument: HSTRING,
181    description: HSTRING,
182    icon: Option<HSTRING>,
183    display: &str,
184) -> anyhow::Result<IShellLinkW> {
185    unsafe {
186        let link: IShellLinkW = CoCreateInstance(&ShellLink, None, CLSCTX_INPROC_SERVER)?;
187        let exe_path = HSTRING::from(std::env::current_exe()?.as_os_str());
188        link.SetPath(&exe_path)?;
189        link.SetArguments(&argument)?;
190        link.SetDescription(&description)?;
191        if let Some(icon) = icon {
192            link.SetIconLocation(&icon, 0)?;
193        }
194        let store: IPropertyStore = link.cast()?;
195        let title = PROPVARIANT::from(display);
196        store.SetValue(&PKEY_TITLE, &title)?;
197        store.Commit()?;
198
199        Ok(link)
200    }
201}