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}