Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions plugins/opener/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
7 changes: 7 additions & 0 deletions plugins/opener/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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!())
Expand Down
2 changes: 1 addition & 1 deletion plugins/opener/api-iife.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions plugins/opener/guest-js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
}
5 changes: 3 additions & 2 deletions plugins/opener/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
2 changes: 2 additions & 0 deletions plugins/opener/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
14 changes: 11 additions & 3 deletions plugins/opener/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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 {
Expand Down
187 changes: 144 additions & 43 deletions plugins/opener/src/reveal_item_in_dir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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::{
Expand All @@ -54,56 +96,95 @@ mod imp {
},
};

pub fn reveal_item_in_dir(path: &Path) -> crate::Result<()> {
let file = dunce::simplified(path);

let _ = unsafe { CoInitialize(None) };

let dir = file
.parent()
.ok_or_else(|| crate::Error::NoParent(file.to_path_buf()))?;
pub fn reveal_items_in_dir(paths: &[PathBuf]) -> crate::Result<()> {
if paths.is_empty() {
return Ok(());
}

let dir = HSTRING::from(dir);
let dir_item = unsafe { ILCreateFromPathW(&dir) };
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 file_h = HSTRING::from(file);
let file_item = unsafe { ILCreateFromPathW(&file_h) };
let _ = unsafe { CoInitialize(None) };

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,
})
}
}
}

Ok(())
impl Drop for OwnedItemIdList {
fn drop(&mut self) {
if !self.item.is_null() {
unsafe { ILFree(Some(self.item)) };
}
}
}
}

Expand All @@ -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",
Expand All @@ -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(
Expand Down Expand Up @@ -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();
Expand Down
Loading