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 _ => Err(anyhow::anyhow!(
58 "Only `MenuItem::Action` is supported for dock menu on Windows."
59 )),
60 }
61 }
62}
63
64// This code is based on the example from Microsoft:
65// https://github.com/microsoft/Windows-classic-samples/blob/main/Samples/Win7Samples/winui/shell/appshellintegration/RecipePropertyHandler/RecipePropertyHandler.cpp
66pub(crate) fn update_jump_list(
67 jump_list: &JumpList,
68) -> anyhow::Result<Vec<SmallVec<[PathBuf; 2]>>> {
69 let (list, removed) = create_destination_list()?;
70 add_recent_folders(&list, &jump_list.recent_workspaces, removed.as_ref())?;
71 add_dock_menu(&list, &jump_list.dock_menus)?;
72 unsafe { list.CommitList() }?;
73 Ok(removed)
74}
75
76// Copied from:
77// https://github.com/microsoft/windows-rs/blob/0fc3c2e5a13d4316d242bdeb0a52af611eba8bd4/crates/libs/windows/src/Windows/Win32/Storage/EnhancedStorage/mod.rs#L1881
78const PKEY_TITLE: PROPERTYKEY = PROPERTYKEY {
79 fmtid: GUID::from_u128(0xf29f85e0_4ff9_1068_ab91_08002b27b3d9),
80 pid: 2,
81};
82
83fn create_destination_list() -> anyhow::Result<(ICustomDestinationList, Vec<SmallVec<[PathBuf; 2]>>)>
84{
85 let list: ICustomDestinationList =
86 unsafe { CoCreateInstance(&DestinationList, None, CLSCTX_INPROC_SERVER) }?;
87
88 let mut slots = 0;
89 let user_removed: IObjectArray = unsafe { list.BeginList(&mut slots) }?;
90
91 let count = unsafe { user_removed.GetCount() }?;
92 if count == 0 {
93 return Ok((list, Vec::new()));
94 }
95
96 let mut removed = Vec::with_capacity(count as usize);
97 for i in 0..count {
98 let shell_link: IShellLinkW = unsafe { user_removed.GetAt(i)? };
99 let description = {
100 // INFOTIPSIZE is the maximum size of the buffer
101 // see https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-ishelllinkw-getdescription
102 let mut buffer = [0u16; INFOTIPSIZE as usize];
103 unsafe { shell_link.GetDescription(&mut buffer)? };
104 let len = unsafe { u_strlen(buffer.as_ptr()) };
105 String::from_utf16_lossy(&buffer[..len as usize])
106 };
107 let args = description.split('\n').map(PathBuf::from).collect();
108
109 removed.push(args);
110 }
111
112 Ok((list, removed))
113}
114
115fn add_dock_menu(list: &ICustomDestinationList, dock_menus: &[DockMenuItem]) -> anyhow::Result<()> {
116 unsafe {
117 let tasks: IObjectCollection =
118 CoCreateInstance(&EnumerableObjectCollection, None, CLSCTX_INPROC_SERVER)?;
119 for (idx, dock_menu) in dock_menus.iter().enumerate() {
120 let argument = HSTRING::from(format!("--dock-action {}", idx));
121 let description = HSTRING::from(dock_menu.description.as_str());
122 let display = dock_menu.name.as_str();
123 let task = create_shell_link(argument, description, None, display)?;
124 tasks.AddObject(&task)?;
125 }
126 list.AddUserTasks(&tasks)?;
127 Ok(())
128 }
129}
130
131fn add_recent_folders(
132 list: &ICustomDestinationList,
133 entries: &[SmallVec<[PathBuf; 2]>],
134 removed: &Vec<SmallVec<[PathBuf; 2]>>,
135) -> anyhow::Result<()> {
136 unsafe {
137 let tasks: IObjectCollection =
138 CoCreateInstance(&EnumerableObjectCollection, None, CLSCTX_INPROC_SERVER)?;
139
140 for folder_path in entries.iter().filter(|path| !removed.contains(path)) {
141 let argument = HSTRING::from(
142 folder_path
143 .iter()
144 .map(|path| format!("\"{}\"", path.display()))
145 .join(" "),
146 );
147
148 let description = HSTRING::from(
149 folder_path
150 .iter()
151 .map(|path| path.to_string_lossy())
152 .collect::<Vec<_>>()
153 .join("\n"),
154 );
155 // simulate folder icon
156 // https://github.com/microsoft/vscode/blob/7a5dc239516a8953105da34f84bae152421a8886/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts#L380
157 let icon = HSTRING::from("explorer.exe");
158
159 let display = folder_path
160 .iter()
161 .map(|p| {
162 p.file_name()
163 .map(|name| name.to_string_lossy().to_string())
164 .unwrap_or_else(|| p.to_string_lossy().to_string())
165 })
166 .join(", ");
167
168 tasks.AddObject(&create_shell_link(
169 argument,
170 description,
171 Some(icon),
172 &display,
173 )?)?;
174 }
175
176 list.AppendCategory(&HSTRING::from("Recent Folders"), &tasks)?;
177 Ok(())
178 }
179}
180
181fn create_shell_link(
182 argument: HSTRING,
183 description: HSTRING,
184 icon: Option<HSTRING>,
185 display: &str,
186) -> anyhow::Result<IShellLinkW> {
187 unsafe {
188 let link: IShellLinkW = CoCreateInstance(&ShellLink, None, CLSCTX_INPROC_SERVER)?;
189 let exe_path = HSTRING::from(std::env::current_exe()?.as_os_str());
190 link.SetPath(&exe_path)?;
191 link.SetArguments(&argument)?;
192 link.SetDescription(&description)?;
193 if let Some(icon) = icon {
194 link.SetIconLocation(&icon, 0)?;
195 }
196 let store: IPropertyStore = link.cast()?;
197 let title = PROPVARIANT::from(display);
198 store.SetValue(&PKEY_TITLE, &title)?;
199 store.Commit()?;
200
201 Ok(link)
202 }
203}