Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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: 1 addition & 1 deletion plugins/opener/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ fn _f() {
};
}

const COMMANDS: &[&str] = &["open_url", "open_path", "reveal_item_in_dir"];
const COMMANDS: &[&str] = &["open_url", "open_path", "reveal_item_in_dir", "reveal_items_in_dir"];

fn main() {
tauri_plugin::Builder::new(COMMANDS)
Expand Down
21 changes: 21 additions & 0 deletions plugins/opener/guest-js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,24 @@ export async function openPath(path: string, openWith?: string): Promise<void> {
export async function revealItemInDir(path: string) {
return invoke('plugin:opener|reveal_item_in_dir', { path })
}

/**
* Reveal paths with the system's default explorer.
*
* #### Platform-specific:
*
* - **Android / iOS:** Unsupported.
*
* @example
* ```typescript
* import { revealItemsInDir } from '@tauri-apps/plugin-opener';
* await revealItemsInDir(['/path/to/file']);
* ```
*
* @param paths The paths to reveal.
*
* @since 2.0.0
*/
export async function revealItemsInDir(paths: string[]) {
return invoke('plugin:opener|reveal_items_in_dir', { paths })
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!

"$schema" = "../../schemas/schema.json"

[[permission]]
identifier = "allow-reveal-items-in-dir"
description = "Enables the reveal_items_in_dir command without any pre-configured scope."
commands.allow = ["reveal_items_in_dir"]

[[permission]]
identifier = "deny-reveal-items-in-dir"
description = "Denies the reveal_items_in_dir command without any pre-configured scope."
commands.deny = ["reveal_items_in_dir"]
27 changes: 27 additions & 0 deletions plugins/opener/permissions/autogenerated/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ as well as reveal file in directories using default file explorer

- `allow-open-url`
- `allow-reveal-item-in-dir`
- `allow-reveal-items-in-dir`
- `allow-default-urls`

## Permission Table
Expand Down Expand Up @@ -106,6 +107,32 @@ Enables the reveal_item_in_dir command without any pre-configured scope.

Denies the reveal_item_in_dir command without any pre-configured scope.

</td>
</tr>

<tr>
<td>

`opener:allow-reveal-items-in-dir`

</td>
<td>

Enables the reveal_items_in_dir command without any pre-configured scope.

</td>
</tr>

<tr>
<td>

`opener:deny-reveal-items-in-dir`

</td>
<td>

Denies the reveal_items_in_dir command without any pre-configured scope.

</td>
</tr>
</table>
1 change: 1 addition & 0 deletions plugins/opener/permissions/default.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ as well as reveal file in directories using default file explorer"""
permissions = [
"allow-open-url",
"allow-reveal-item-in-dir",
"allow-reveal-items-in-dir",
"allow-default-urls",
]
16 changes: 14 additions & 2 deletions plugins/opener/permissions/schemas/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -337,10 +337,22 @@
"markdownDescription": "Denies the reveal_item_in_dir command without any pre-configured scope."
},
{
"description": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`",
"description": "Enables the reveal_items_in_dir command without any pre-configured scope.",
"type": "string",
"const": "allow-reveal-items-in-dir",
"markdownDescription": "Enables the reveal_items_in_dir command without any pre-configured scope."
},
{
"description": "Denies the reveal_items_in_dir command without any pre-configured scope.",
"type": "string",
"const": "deny-reveal-items-in-dir",
"markdownDescription": "Denies the reveal_items_in_dir command without any pre-configured scope."
},
{
"description": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-reveal-items-in-dir`\n- `allow-default-urls`",
"type": "string",
"const": "default",
"markdownDescription": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-default-urls`"
"markdownDescription": "This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer\n#### This default permission set includes:\n\n- `allow-open-url`\n- `allow-reveal-item-in-dir`\n- `allow-reveal-items-in-dir`\n- `allow-default-urls`"
}
]
}
Expand Down
5 changes: 5 additions & 0 deletions plugins/opener/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,8 @@ pub async fn open_path<R: Runtime>(
pub async fn reveal_item_in_dir(path: PathBuf) -> crate::Result<()> {
crate::reveal_item_in_dir(path)
}

#[tauri::command]
pub async fn reveal_items_in_dir(paths: Vec<PathBuf>) -> crate::Result<()> {
crate::reveal_items_in_dir(&paths)
}
9 changes: 7 additions & 2 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 @@ -148,6 +148,10 @@ 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)
}

pub fn reveal_items_in_dir<P: AsRef<Path>>(&self, p: &Vec<P>) -> Result<()> {
crate::reveal_item_in_dir::reveal_items_in_dir(p)
}
}

/// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] and [`tauri::Window`] to access the opener APIs.
Expand Down Expand Up @@ -213,7 +217,8 @@ impl Builder {
.invoke_handler(tauri::generate_handler![
commands::open_url,
commands::open_path,
commands::reveal_item_in_dir
commands::reveal_item_in_dir,
commands::reveal_items_in_dir,
]);

if self.open_js_links_on_click {
Expand Down
156 changes: 139 additions & 17 deletions plugins/opener/src/reveal_item_in_dir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,48 @@ pub fn reveal_item_in_dir<P: AsRef<Path>>(path: P) -> crate::Result<()> {
Err(crate::Error::UnsupportedPlatform)
}

/// Reveal the paths the system's default explorer.
///
/// ## Platform-specific:
///
/// - **Android / iOS:** Unsupported.
pub fn reveal_items_in_dir<P: AsRef<Path>>(paths: &Vec<P>) -> crate::Result<()> {
let mut path_bufs = vec![];

for path in paths.iter() {
let path = path.as_ref().canonicalize()?;
path_bufs.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(&path_bufs);

#[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)
}

#[cfg(windows)]
mod imp {
use super::*;
use std::path::Path;

use windows::Win32::UI::Shell::Common::ITEMIDLIST;
use windows::{
core::{w, HSTRING, PCWSTR},
Win32::{
Expand All @@ -54,7 +92,7 @@ mod imp {
},
};

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

let _ = unsafe { CoInitialize(None) };
Expand All @@ -63,10 +101,10 @@ mod imp {
.parent()
.ok_or_else(|| crate::Error::NoParent(file.to_path_buf()))?;

let dir = HSTRING::from(dir);
let dir_item = unsafe { ILCreateFromPathW(&dir) };
let dir_h = HSTRING::from(dir);
let dir_item = unsafe { ILCreateFromPathW(&dir_h) };

let file_h = HSTRING::from(file);
let file_h = HSTRING::from(file.as_os_str());
let file_item = unsafe { ILCreateFromPathW(&file_h) };

unsafe {
Expand All @@ -80,7 +118,7 @@ mod imp {
let mut info = SHELLEXECUTEINFOW {
cbSize: std::mem::size_of::<SHELLEXECUTEINFOW>() as _,
nShow: SW_SHOWNORMAL.0,
lpFile: PCWSTR(dir.as_ptr()),
lpFile: PCWSTR(dir_h.as_ptr()),
lpClass: if is_dir { w!("folder") } else { PCWSTR::null() },
lpVerb: if is_dir {
w!("explore")
Expand All @@ -105,6 +143,50 @@ mod imp {

Ok(())
}

pub fn reveal_items_in_dir<P: AsRef<Path>>(paths: &Vec<P>) -> crate::Result<()> {
if paths.is_empty() {
return Ok(());
}

let first_path = dunce::simplified(&paths[0]);
let dir = first_path
.parent()
.ok_or_else(|| crate::Error::NoParent(first_path.to_path_buf()))?;

let _ = unsafe { CoInitialize(None) };

let dir_h = HSTRING::from(dir);
let dir_item = unsafe { ILCreateFromPathW(&dir_h) };

let mut items_to_free: Vec<*const ITEMIDLIST> = Vec::with_capacity(paths.len() + 1);
items_to_free.push(dir_item);

let mut file_items: Vec<*const ITEMIDLIST> = Vec::with_capacity(paths.len());

for path in paths {
let simplified_path = dunce::simplified(path);
if simplified_path.parent() != Some(dir) {
// All items must be in the same directory.
// You might want to return an error here.
continue;
}
let file_h = HSTRING::from(simplified_path.as_os_str());
let file_item = unsafe { ILCreateFromPathW(&file_h) };
file_items.push(file_item);
items_to_free.push(file_item);
}

let result = unsafe {
SHOpenFolderAndSelectItems(dir_item, Some(&file_items), 0).map_err(Into::into)
};

for item in items_to_free {
unsafe { ILFree(Some(item)) };
}

result
}
}

#[cfg(any(
Expand All @@ -115,24 +197,42 @@ mod imp {
target_os = "openbsd"
))]
mod imp {

use std::collections::HashMap;

use super::*;
use std::collections::HashMap;

pub fn reveal_item_in_dir(path: &Path) -> crate::Result<()> {
pub fn reveal_item_in_dir<P: AsRef<Path>>(path: P) -> crate::Result<()> {
let connection = zbus::blocking::Connection::session()?;

reveal_with_filemanager1(path, &connection)
reveal_with_filemanager1(&vec![path], &connection)
.or_else(|_| reveal_with_open_uri_portal(path, &connection))
}

fn reveal_with_filemanager1(
path: &Path,
pub fn reveal_items_in_dir<P: AsRef<Path>>(paths: &Vec<P>) -> crate::Result<()> {
let connection = zbus::blocking::Connection::session()?;

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<P: AsRef<Path>>(
paths: &Vec<P>,
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 +245,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 @@ -180,9 +280,10 @@ 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<()> {

pub fn reveal_item_in_dir<P: AsRef<Path>>(path: P) -> crate::Result<()> {
unsafe {
let path = path.to_string_lossy();
let path = path.as_ref().to_string_lossy();
let path = NSString::from_str(&path);
let urls = vec![NSURL::fileURLWithPath(&path)];
let urls = NSArray::from_retained_slice(&urls);
Expand All @@ -193,4 +294,25 @@ mod imp {

Ok(())
}

pub fn reveal_items_in_dir<P: AsRef<Path>>(paths: &Vec<P>) -> crate::Result<()> {
unsafe {
let mut urls = Vec::new();

for path in paths.iter() {
let path = path.as_ref().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();
workspace.activateFileViewerSelectingURLs(&urls);
}

Ok(())
}
}