Skip to content

Commit eabcdd7

Browse files
Merge pull request #1 from Legend-Master/multi-roots-windows
Support multiple roots on Windows
2 parents cbd918e + d0f8846 commit eabcdd7

File tree

4 files changed

+83
-68
lines changed

4 files changed

+83
-68
lines changed

plugins/opener/README.md

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22

33
<!-- description -->
44

5-
| Platform | Supported | Notes |
6-
|----------|-----------|---------------------------------------------------------------------------|
7-
| Linux || |
8-
| Windows || Revealing multiple files placed in different directories is not supported |
9-
| macOS || |
10-
| Android | ? | |
11-
| iOS | ? | |
5+
| Platform | Supported | Notes |
6+
| -------- | --------- | ----- |
7+
| Linux || |
8+
| Windows || |
9+
| macOS || |
10+
| Android | ? | |
11+
| iOS | ? | |
1212

1313
## Install
1414

@@ -77,8 +77,7 @@ await openPath('/path/to/file', 'firefox')
7777
await revealItemInDir('/path/to/file')
7878

7979
// Reveal multiple paths with the system's default explorer
80-
// Note: on Windows, files have to be in the same directory
81-
await revealItemsInDir([ '/path/to/file', '/path/to/another/file' ])
80+
await revealItemsInDir(['/path/to/file', '/path/to/another/file'])
8281
```
8382

8483
### Usage from Rust
@@ -108,8 +107,7 @@ fn main() {
108107
opener.reveal_item_in_dir("/path/to/file")?;
109108

110109
// Reveal multiple paths with the system's default explorer
111-
// Note: on Windows, files have to be in the same directory
112-
opener.reveal_items_in_dir(&["/path/to/file"])?;
110+
opener.reveal_items_in_dir(["/path/to/file"])?;
113111
Ok(())
114112
})
115113
.run(tauri::generate_context!())

plugins/opener/guest-js/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,6 @@ export async function revealItemInDir(path: string) {
102102
* #### Platform-specific:
103103
*
104104
* - **Android / iOS:** Unsupported.
105-
* - **Windows:** Only supports revealing items in the same directory.
106105
*
107106
* @example
108107
* ```typescript

plugins/opener/src/error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ pub enum Error {
3131
Win32Error(#[from] windows::core::Error),
3232
#[error("Path doesn't have a parent: {0}")]
3333
NoParent(PathBuf),
34+
#[error("Path is invalid: {0}")]
35+
InvalidPath(PathBuf),
3436
#[error("Failed to convert path to file:// url")]
3537
FailedToConvertPathToFileUrl,
3638
#[error(transparent)]

plugins/opener/src/reveal_item_in_dir.rs

Lines changed: 72 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use std::path::Path;
1010
///
1111
/// - **Android / iOS:** Unsupported.
1212
pub fn reveal_item_in_dir<P: AsRef<Path>>(path: P) -> crate::Result<()> {
13-
let path = path.as_ref().canonicalize()?;
13+
let path = dunce::canonicalize(path.as_ref())?;
1414

1515
#[cfg(any(
1616
windows,
@@ -40,7 +40,6 @@ pub fn reveal_item_in_dir<P: AsRef<Path>>(path: P) -> crate::Result<()> {
4040
/// ## Platform-specific:
4141
///
4242
/// - **Android / iOS:** Unsupported.
43-
/// - **Windows:** Only supports revealing items in the same directory.
4443
pub fn reveal_items_in_dir<I, P>(paths: I) -> crate::Result<()>
4544
where
4645
I: IntoIterator<Item = P>,
@@ -49,7 +48,7 @@ where
4948
let mut canonicalized = vec![];
5049

5150
for path in paths {
52-
let path = path.as_ref().canonicalize()?;
51+
let path = dunce::canonicalize(path.as_ref())?;
5352
canonicalized.push(path);
5453
}
5554

@@ -78,7 +77,8 @@ where
7877

7978
#[cfg(windows)]
8079
mod imp {
81-
use std::path::PathBuf;
80+
use std::collections::HashMap;
81+
use std::path::{Path, PathBuf};
8282

8383
use windows::Win32::UI::Shell::Common::ITEMIDLIST;
8484
use windows::{
@@ -101,74 +101,90 @@ mod imp {
101101
return Ok(());
102102
}
103103

104-
let first_path = dunce::simplified(&paths[0]);
105-
let parent_dir = first_path
106-
.parent()
107-
.ok_or_else(|| crate::Error::NoParent(first_path.to_path_buf()))?;
108-
109-
// On Windows, SHOpenFolderAndSelectItems requires all items to be in the same directory.
110-
// We filter the paths to ensure they all share the same parent as the first path.
111-
let files_in_same_dir: Vec<_> = paths
112-
.iter()
113-
.map(|p| dunce::simplified(p))
114-
.filter(|p| p.parent() == Some(parent_dir))
115-
.collect();
116-
117-
if files_in_same_dir.is_empty() {
118-
// This case can happen if the original list had paths from different directories.
119-
// We can't open multiple directories, so we do nothing.
120-
return Ok(());
104+
let mut grouped_paths: HashMap<&Path, Vec<&Path>> = HashMap::new();
105+
for path in paths {
106+
let parent = path
107+
.parent()
108+
.ok_or_else(|| crate::Error::NoParent(path.to_path_buf()))?;
109+
grouped_paths.entry(parent).or_default().push(path);
121110
}
122111

123112
let _ = unsafe { CoInitialize(None) };
124113

125-
let dir_hstring = HSTRING::from(parent_dir);
126-
let dir_item = unsafe { ILCreateFromPathW(&dir_hstring) };
127-
128-
// Ensure dir_item is freed even if subsequent operations fail.
129-
let mut created_file_items = Vec::new();
130-
131-
for path in &files_in_same_dir {
132-
let file_hstring = HSTRING::from(path.as_os_str());
133-
let file_item = unsafe { ILCreateFromPathW(&file_hstring) };
134-
if !file_item.is_null() {
135-
created_file_items.push(file_item);
136-
}
137-
}
138-
139-
// The function expects a slice of *const ITEMIDLIST, so we must cast our *mut pointers.
140-
let item_id_lists_const: Vec<*const ITEMIDLIST> =
141-
created_file_items.iter().map(|&p| p as *const _).collect();
142-
143-
let result = unsafe {
144-
if let Err(e) = SHOpenFolderAndSelectItems(dir_item, Some(&item_id_lists_const), 0) {
145-
// Fallback logic from the original function.
114+
for (parent, to_reveals) in grouped_paths {
115+
let parent_item_id_list = OwnedItemIdList::new(parent)?;
116+
let to_reveals_item_id_list = to_reveals
117+
.iter()
118+
.map(|to_reveal| OwnedItemIdList::new(*to_reveal))
119+
.collect::<crate::Result<Vec<_>>>()?;
120+
if let Err(e) = unsafe {
121+
SHOpenFolderAndSelectItems(
122+
parent_item_id_list.item,
123+
Some(
124+
&to_reveals_item_id_list
125+
.iter()
126+
.map(|item| item.item)
127+
.collect::<Vec<_>>(),
128+
),
129+
0,
130+
)
131+
} {
132+
// from https://github.com/electron/electron/blob/10d967028af2e72382d16b7e2025d243b9e204ae/shell/common/platform_util_win.cc#L302
133+
// On some systems, the above call mysteriously fails with "file not
134+
// found" even though the file is there. In these cases, ShellExecute()
135+
// seems to work as a fallback (although it won't select the file).
136+
//
137+
// Note: we only handle the first file here if multiple of are present
146138
if e.code().0 == ERROR_FILE_NOT_FOUND.0 as i32 {
139+
let first_path = to_reveals[0];
140+
let is_dir = first_path.is_dir();
147141
let mut info = SHELLEXECUTEINFOW {
148142
cbSize: std::mem::size_of::<SHELLEXECUTEINFOW>() as _,
149143
nShow: SW_SHOWNORMAL.0,
150-
lpFile: PCWSTR(dir_hstring.as_ptr()),
151-
lpVerb: w!("explore"),
144+
lpFile: PCWSTR(parent_item_id_list.hstring.as_ptr()),
145+
lpClass: if is_dir { w!("folder") } else { PCWSTR::null() },
146+
lpVerb: if is_dir {
147+
w!("explore")
148+
} else {
149+
PCWSTR::null()
150+
},
152151
..Default::default()
153152
};
154-
ShellExecuteExW(&mut info).map(|_| ()).map_err(Into::into)
155-
} else {
156-
Err(e.into())
153+
154+
unsafe { ShellExecuteExW(&mut info) }?;
157155
}
158-
} else {
159-
Ok(())
160156
}
161-
};
157+
}
162158

163-
// Free all allocated ITEMIDLISTs
164-
unsafe {
165-
for item in created_file_items {
166-
ILFree(Some(item));
159+
Ok(())
160+
}
161+
162+
struct OwnedItemIdList {
163+
hstring: HSTRING,
164+
item: *const ITEMIDLIST,
165+
}
166+
167+
impl OwnedItemIdList {
168+
fn new(path: &Path) -> crate::Result<Self> {
169+
let path_hstring = HSTRING::from(path);
170+
let item_id_list = unsafe { ILCreateFromPathW(&path_hstring) };
171+
if item_id_list.is_null() {
172+
Err(crate::Error::InvalidPath(path.to_owned()))
173+
} else {
174+
Ok(Self {
175+
hstring: path_hstring,
176+
item: item_id_list,
177+
})
167178
}
168-
ILFree(Some(dir_item));
169179
}
180+
}
170181

171-
result
182+
impl Drop for OwnedItemIdList {
183+
fn drop(&mut self) {
184+
if !self.item.is_null() {
185+
unsafe { ILFree(Some(self.item)) };
186+
}
187+
}
172188
}
173189
}
174190

0 commit comments

Comments
 (0)