mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-01-31 00:45:24 +01:00
feat(opener): reveal multiple items in dir (#2897)
* feature: reveal multiple items in dir * feature: reveal multiple items in dir * feature: reveal multiple items in dir * feature: reveal multiple items in dir * feature: reveal multiple items in dir * feature: reveal multiple items in dir * feature: reveal multiple items in dir * Support multiple roots on Windows * feature: reveal multiple items in dir * feature: reveal multiple items in dir * feature: reveal multiple items in dir * feature: reveal multiple items in dir --------- Co-authored-by: Tony <legendmastertony@gmail.com>
This commit is contained in:
6
.changes/opener-reveal-multiple-items.md
Normal file
6
.changes/opener-reveal-multiple-items.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"opener": 'minor:enhance'
|
||||
"opener-js": 'minor:enhance'
|
||||
---
|
||||
|
||||
Allow reveal multiple items in the file explorer.
|
||||
@@ -35,8 +35,6 @@ tauri = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
open = { version = "5", features = ["shellexecute-on-windows"] }
|
||||
glob = { workspace = true }
|
||||
|
||||
[target."cfg(windows)".dependencies]
|
||||
dunce = { workspace = true }
|
||||
|
||||
[target."cfg(windows)".dependencies.windows]
|
||||
|
||||
@@ -75,6 +75,10 @@ await openPath('/path/to/file', 'firefox')
|
||||
|
||||
// Reveal a path with the system's default explorer
|
||||
await revealItemInDir('/path/to/file')
|
||||
|
||||
// Reveal multiple paths with the system's default explorer
|
||||
// Note: will be renamed to `revealItemsInDir` in the next major version
|
||||
await revealItemInDir(['/path/to/file', '/path/to/another/file'])
|
||||
```
|
||||
|
||||
### Usage from Rust
|
||||
@@ -102,6 +106,9 @@ fn main() {
|
||||
|
||||
// Reveal a path with the system's default explorer
|
||||
opener.reveal_item_in_dir("/path/to/file")?;
|
||||
|
||||
// Reveal multiple paths with the system's default explorer
|
||||
opener.reveal_items_in_dir(["/path/to/file"])?;
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
|
||||
@@ -1 +1 @@
|
||||
if("__TAURI__"in window){var __TAURI_PLUGIN_OPENER__=function(n){"use strict";async function e(n,e={},_){return window.__TAURI_INTERNALS__.invoke(n,e,_)}return"function"==typeof SuppressedError&&SuppressedError,n.openPath=async function(n,_){await e("plugin:opener|open_path",{path:n,with:_})},n.openUrl=async function(n,_){await e("plugin:opener|open_url",{url:n,with:_})},n.revealItemInDir=async function(n){return e("plugin:opener|reveal_item_in_dir",{path:n})},n}({});Object.defineProperty(window.__TAURI__,"opener",{value:__TAURI_PLUGIN_OPENER__})}
|
||||
if("__TAURI__"in window){var __TAURI_PLUGIN_OPENER__=function(n){"use strict";async function e(n,e={},r){return window.__TAURI_INTERNALS__.invoke(n,e,r)}return"function"==typeof SuppressedError&&SuppressedError,n.openPath=async function(n,r){await e("plugin:opener|open_path",{path:n,with:r})},n.openUrl=async function(n,r){await e("plugin:opener|open_url",{url:n,with:r})},n.revealItemInDir=async function(n){return e("plugin:opener|reveal_item_in_dir",{paths:"string"==typeof n?[n]:n})},n}({});Object.defineProperty(window.__TAURI__,"opener",{value:__TAURI_PLUGIN_OPENER__})}
|
||||
|
||||
@@ -86,12 +86,14 @@ export async function openPath(path: string, openWith?: string): Promise<void> {
|
||||
* ```typescript
|
||||
* import { revealItemInDir } from '@tauri-apps/plugin-opener';
|
||||
* await revealItemInDir('/path/to/file');
|
||||
* await revealItemInDir([ '/path/to/file', '/path/to/another/file' ]);
|
||||
* ```
|
||||
*
|
||||
* @param path The path to reveal.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
export async function revealItemInDir(path: string) {
|
||||
return invoke('plugin:opener|reveal_item_in_dir', { path })
|
||||
export async function revealItemInDir(path: string | string[]): Promise<void> {
|
||||
const paths = typeof path === 'string' ? [path] : path
|
||||
return invoke('plugin:opener|reveal_item_in_dir', { paths })
|
||||
}
|
||||
|
||||
@@ -69,7 +69,8 @@ pub async fn open_path<R: Runtime>(
|
||||
}
|
||||
}
|
||||
|
||||
/// TODO: in the next major version, rename to `reveal_items_in_dir`
|
||||
#[tauri::command]
|
||||
pub async fn reveal_item_in_dir(path: PathBuf) -> crate::Result<()> {
|
||||
crate::reveal_item_in_dir(path)
|
||||
pub async fn reveal_item_in_dir(paths: Vec<PathBuf>) -> crate::Result<()> {
|
||||
crate::reveal_items_in_dir(&paths)
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ pub enum Error {
|
||||
Win32Error(#[from] windows::core::Error),
|
||||
#[error("Path doesn't have a parent: {0}")]
|
||||
NoParent(PathBuf),
|
||||
#[error("Path is invalid: {0}")]
|
||||
InvalidPath(PathBuf),
|
||||
#[error("Failed to convert path to file:// url")]
|
||||
FailedToConvertPathToFileUrl,
|
||||
#[error(transparent)]
|
||||
|
||||
@@ -25,7 +25,7 @@ pub use error::Error;
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
pub use open::{open_path, open_url};
|
||||
pub use reveal_item_in_dir::reveal_item_in_dir;
|
||||
pub use reveal_item_in_dir::{reveal_item_in_dir, reveal_items_in_dir};
|
||||
|
||||
pub struct Opener<R: Runtime> {
|
||||
// we use `fn() -> R` to silence the unused generic error
|
||||
@@ -146,7 +146,15 @@ impl<R: Runtime> Opener<R> {
|
||||
}
|
||||
|
||||
pub fn reveal_item_in_dir<P: AsRef<Path>>(&self, p: P) -> Result<()> {
|
||||
crate::reveal_item_in_dir::reveal_item_in_dir(p)
|
||||
reveal_item_in_dir(p)
|
||||
}
|
||||
|
||||
pub fn reveal_items_in_dir<I, P>(&self, paths: I) -> Result<()>
|
||||
where
|
||||
I: IntoIterator<Item = P>,
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
reveal_items_in_dir(paths)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,7 +221,7 @@ impl Builder {
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::open_url,
|
||||
commands::open_path,
|
||||
commands::reveal_item_in_dir
|
||||
commands::reveal_item_in_dir,
|
||||
]);
|
||||
|
||||
if self.open_js_links_on_click {
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::path::Path;
|
||||
///
|
||||
/// - **Android / iOS:** Unsupported.
|
||||
pub fn reveal_item_in_dir<P: AsRef<Path>>(path: P) -> crate::Result<()> {
|
||||
let path = path.as_ref().canonicalize()?;
|
||||
let path = dunce::canonicalize(path.as_ref())?;
|
||||
|
||||
#[cfg(any(
|
||||
windows,
|
||||
@@ -21,7 +21,47 @@ pub fn reveal_item_in_dir<P: AsRef<Path>>(path: P) -> crate::Result<()> {
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
))]
|
||||
return imp::reveal_item_in_dir(&path);
|
||||
return imp::reveal_items_in_dir(&[path]);
|
||||
|
||||
#[cfg(not(any(
|
||||
windows,
|
||||
target_os = "macos",
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
)))]
|
||||
Err(crate::Error::UnsupportedPlatform)
|
||||
}
|
||||
|
||||
/// Reveal the paths the system's default explorer.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Android / iOS:** Unsupported.
|
||||
pub fn reveal_items_in_dir<I, P>(paths: I) -> crate::Result<()>
|
||||
where
|
||||
I: IntoIterator<Item = P>,
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let mut canonicalized = vec![];
|
||||
|
||||
for path in paths {
|
||||
let path = dunce::canonicalize(path.as_ref())?;
|
||||
canonicalized.push(path);
|
||||
}
|
||||
|
||||
#[cfg(any(
|
||||
windows,
|
||||
target_os = "macos",
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
))]
|
||||
return imp::reveal_items_in_dir(&canonicalized);
|
||||
|
||||
#[cfg(not(any(
|
||||
windows,
|
||||
@@ -37,8 +77,10 @@ pub fn reveal_item_in_dir<P: AsRef<Path>>(path: P) -> crate::Result<()> {
|
||||
|
||||
#[cfg(windows)]
|
||||
mod imp {
|
||||
use super::*;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use windows::Win32::UI::Shell::Common::ITEMIDLIST;
|
||||
use windows::{
|
||||
core::{w, HSTRING, PCWSTR},
|
||||
Win32::{
|
||||
@@ -54,57 +96,96 @@ mod imp {
|
||||
},
|
||||
};
|
||||
|
||||
pub fn reveal_item_in_dir(path: &Path) -> crate::Result<()> {
|
||||
let file = dunce::simplified(path);
|
||||
pub fn reveal_items_in_dir(paths: &[PathBuf]) -> crate::Result<()> {
|
||||
if paths.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut grouped_paths: HashMap<&Path, Vec<&Path>> = HashMap::new();
|
||||
for path in paths {
|
||||
let parent = path
|
||||
.parent()
|
||||
.ok_or_else(|| crate::Error::NoParent(path.to_path_buf()))?;
|
||||
grouped_paths.entry(parent).or_default().push(path);
|
||||
}
|
||||
|
||||
let _ = unsafe { CoInitialize(None) };
|
||||
|
||||
let dir = file
|
||||
.parent()
|
||||
.ok_or_else(|| crate::Error::NoParent(file.to_path_buf()))?;
|
||||
|
||||
let dir = HSTRING::from(dir);
|
||||
let dir_item = unsafe { ILCreateFromPathW(&dir) };
|
||||
|
||||
let file_h = HSTRING::from(file);
|
||||
let file_item = unsafe { ILCreateFromPathW(&file_h) };
|
||||
|
||||
unsafe {
|
||||
if let Err(e) = SHOpenFolderAndSelectItems(dir_item, Some(&[file_item]), 0) {
|
||||
for (parent, to_reveals) in grouped_paths {
|
||||
let parent_item_id_list = OwnedItemIdList::new(parent)?;
|
||||
let to_reveals_item_id_list = to_reveals
|
||||
.iter()
|
||||
.map(|to_reveal| OwnedItemIdList::new(*to_reveal))
|
||||
.collect::<crate::Result<Vec<_>>>()?;
|
||||
if let Err(e) = unsafe {
|
||||
SHOpenFolderAndSelectItems(
|
||||
parent_item_id_list.item,
|
||||
Some(
|
||||
&to_reveals_item_id_list
|
||||
.iter()
|
||||
.map(|item| item.item)
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
0,
|
||||
)
|
||||
} {
|
||||
// from https://github.com/electron/electron/blob/10d967028af2e72382d16b7e2025d243b9e204ae/shell/common/platform_util_win.cc#L302
|
||||
// On some systems, the above call mysteriously fails with "file not
|
||||
// found" even though the file is there. In these cases, ShellExecute()
|
||||
// seems to work as a fallback (although it won't select the file).
|
||||
//
|
||||
// Note: we only handle the first file here if multiple of are present
|
||||
if e.code().0 == ERROR_FILE_NOT_FOUND.0 as i32 {
|
||||
let is_dir = file.is_dir();
|
||||
let first_path = to_reveals[0];
|
||||
let is_dir = first_path.is_dir();
|
||||
let mut info = SHELLEXECUTEINFOW {
|
||||
cbSize: std::mem::size_of::<SHELLEXECUTEINFOW>() as _,
|
||||
nShow: SW_SHOWNORMAL.0,
|
||||
lpFile: PCWSTR(dir.as_ptr()),
|
||||
lpFile: PCWSTR(parent_item_id_list.hstring.as_ptr()),
|
||||
lpClass: if is_dir { w!("folder") } else { PCWSTR::null() },
|
||||
lpVerb: if is_dir {
|
||||
w!("explore")
|
||||
} else {
|
||||
PCWSTR::null()
|
||||
},
|
||||
..std::mem::zeroed()
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
ShellExecuteExW(&mut info).inspect_err(|_| {
|
||||
ILFree(Some(dir_item));
|
||||
ILFree(Some(file_item));
|
||||
})?;
|
||||
unsafe { ShellExecuteExW(&mut info) }?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
ILFree(Some(dir_item));
|
||||
ILFree(Some(file_item));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct OwnedItemIdList {
|
||||
hstring: HSTRING,
|
||||
item: *const ITEMIDLIST,
|
||||
}
|
||||
|
||||
impl OwnedItemIdList {
|
||||
fn new(path: &Path) -> crate::Result<Self> {
|
||||
let path_hstring = HSTRING::from(path);
|
||||
let item_id_list = unsafe { ILCreateFromPathW(&path_hstring) };
|
||||
if item_id_list.is_null() {
|
||||
Err(crate::Error::InvalidPath(path.to_owned()))
|
||||
} else {
|
||||
Ok(Self {
|
||||
hstring: path_hstring,
|
||||
item: item_id_list,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for OwnedItemIdList {
|
||||
fn drop(&mut self) {
|
||||
if !self.item.is_null() {
|
||||
unsafe { ILFree(Some(self.item)) };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(
|
||||
@@ -115,24 +196,36 @@ mod imp {
|
||||
target_os = "openbsd"
|
||||
))]
|
||||
mod imp {
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::*;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub fn reveal_item_in_dir(path: &Path) -> crate::Result<()> {
|
||||
pub fn reveal_items_in_dir(paths: &[PathBuf]) -> crate::Result<()> {
|
||||
let connection = zbus::blocking::Connection::session()?;
|
||||
|
||||
reveal_with_filemanager1(path, &connection)
|
||||
.or_else(|_| reveal_with_open_uri_portal(path, &connection))
|
||||
reveal_with_filemanager1(paths, &connection).or_else(|e| {
|
||||
// Fallback to opening the directory of the first item if revealing multiple items fails.
|
||||
if let Some(first_path) = paths.first() {
|
||||
reveal_with_open_uri_portal(first_path, &connection)
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn reveal_with_filemanager1(
|
||||
path: &Path,
|
||||
paths: &[PathBuf],
|
||||
connection: &zbus::blocking::Connection,
|
||||
) -> crate::Result<()> {
|
||||
let uri = url::Url::from_file_path(path)
|
||||
.map_err(|_| crate::Error::FailedToConvertPathToFileUrl)?;
|
||||
let uris: Result<Vec<_>, _> = paths
|
||||
.iter()
|
||||
.map(|path| {
|
||||
url::Url::from_file_path(path)
|
||||
.map_err(|_| crate::Error::FailedToConvertPathToFileUrl)
|
||||
})
|
||||
.collect();
|
||||
let uris = uris?;
|
||||
let uri_strs: Vec<&str> = uris.iter().map(|uri| uri.as_str()).collect();
|
||||
|
||||
#[zbus::proxy(
|
||||
interface = "org.freedesktop.FileManager1",
|
||||
@@ -145,7 +238,7 @@ mod imp {
|
||||
|
||||
let proxy = FileManager1ProxyBlocking::new(connection)?;
|
||||
|
||||
proxy.ShowItems(vec![uri.as_str()], "")
|
||||
proxy.ShowItems(uri_strs, "")
|
||||
}
|
||||
|
||||
fn reveal_with_open_uri_portal(
|
||||
@@ -177,14 +270,22 @@ mod imp {
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod imp {
|
||||
use super::*;
|
||||
use objc2_app_kit::NSWorkspace;
|
||||
use objc2_foundation::{NSArray, NSString, NSURL};
|
||||
pub fn reveal_item_in_dir(path: &Path) -> crate::Result<()> {
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub fn reveal_items_in_dir(paths: &[PathBuf]) -> crate::Result<()> {
|
||||
unsafe {
|
||||
let path = path.to_string_lossy();
|
||||
let path = NSString::from_str(&path);
|
||||
let urls = vec![NSURL::fileURLWithPath(&path)];
|
||||
let mut urls = Vec::new();
|
||||
|
||||
for path in paths {
|
||||
let path = path.to_string_lossy();
|
||||
let path = NSString::from_str(&path);
|
||||
let url = NSURL::fileURLWithPath(&path);
|
||||
|
||||
urls.push(url);
|
||||
}
|
||||
|
||||
let urls = NSArray::from_retained_slice(&urls);
|
||||
|
||||
let workspace = NSWorkspace::new();
|
||||
|
||||
Reference in New Issue
Block a user