destination_list.rs

  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}