From d9523014252322d242329122ff8b2536ffbd30a3 Mon Sep 17 00:00:00 2001 From: petersamokhin Date: Tue, 5 Aug 2025 02:38:57 +0100 Subject: [PATCH 01/12] feature: reveal multiple items in dir --- plugins/opener/build.rs | 2 +- plugins/opener/guest-js/index.ts | 21 +++ .../commands/reveal_items_in_dir.toml | 13 ++ .../permissions/autogenerated/reference.md | 27 +++ plugins/opener/permissions/default.toml | 1 + .../opener/permissions/schemas/schema.json | 16 +- plugins/opener/src/commands.rs | 5 + plugins/opener/src/lib.rs | 9 +- plugins/opener/src/reveal_item_in_dir.rs | 156 ++++++++++++++++-- 9 files changed, 228 insertions(+), 22 deletions(-) create mode 100644 plugins/opener/permissions/autogenerated/commands/reveal_items_in_dir.toml diff --git a/plugins/opener/build.rs b/plugins/opener/build.rs index fbad4d3ac3..ed1f9ce83e 100644 --- a/plugins/opener/build.rs +++ b/plugins/opener/build.rs @@ -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) diff --git a/plugins/opener/guest-js/index.ts b/plugins/opener/guest-js/index.ts index b73ef53812..a0632ee5a4 100644 --- a/plugins/opener/guest-js/index.ts +++ b/plugins/opener/guest-js/index.ts @@ -95,3 +95,24 @@ export async function openPath(path: string, openWith?: string): Promise { 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 }) +} diff --git a/plugins/opener/permissions/autogenerated/commands/reveal_items_in_dir.toml b/plugins/opener/permissions/autogenerated/commands/reveal_items_in_dir.toml new file mode 100644 index 0000000000..74cba165e0 --- /dev/null +++ b/plugins/opener/permissions/autogenerated/commands/reveal_items_in_dir.toml @@ -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"] diff --git a/plugins/opener/permissions/autogenerated/reference.md b/plugins/opener/permissions/autogenerated/reference.md index 6ad6ba1fbf..d3b48b5df7 100644 --- a/plugins/opener/permissions/autogenerated/reference.md +++ b/plugins/opener/permissions/autogenerated/reference.md @@ -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 @@ -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. + + + + + + +`opener:allow-reveal-items-in-dir` + + + + +Enables the reveal_items_in_dir command without any pre-configured scope. + + + + + + + +`opener:deny-reveal-items-in-dir` + + + + +Denies the reveal_items_in_dir command without any pre-configured scope. + diff --git a/plugins/opener/permissions/default.toml b/plugins/opener/permissions/default.toml index 846d6e51be..3e51255522 100644 --- a/plugins/opener/permissions/default.toml +++ b/plugins/opener/permissions/default.toml @@ -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", ] diff --git a/plugins/opener/permissions/schemas/schema.json b/plugins/opener/permissions/schemas/schema.json index 78c80ba385..7a38d0ad2f 100644 --- a/plugins/opener/permissions/schemas/schema.json +++ b/plugins/opener/permissions/schemas/schema.json @@ -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`" } ] } diff --git a/plugins/opener/src/commands.rs b/plugins/opener/src/commands.rs index b00d5306e0..28c7723289 100644 --- a/plugins/opener/src/commands.rs +++ b/plugins/opener/src/commands.rs @@ -73,3 +73,8 @@ pub async fn open_path( 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) -> crate::Result<()> { + crate::reveal_items_in_dir(&paths) +} diff --git a/plugins/opener/src/lib.rs b/plugins/opener/src/lib.rs index 6bf0e5b281..1e7c1e21c4 100644 --- a/plugins/opener/src/lib.rs +++ b/plugins/opener/src/lib.rs @@ -25,7 +25,7 @@ pub use error::Error; type Result = std::result::Result; 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 { // we use `fn() -> R` to silence the unused generic error @@ -148,6 +148,10 @@ impl Opener { pub fn reveal_item_in_dir>(&self, p: P) -> Result<()> { crate::reveal_item_in_dir::reveal_item_in_dir(p) } + + pub fn reveal_items_in_dir>(&self, p: &Vec

) -> 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. @@ -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 { diff --git a/plugins/opener/src/reveal_item_in_dir.rs b/plugins/opener/src/reveal_item_in_dir.rs index 6e3dfc2c80..6782a6df2f 100644 --- a/plugins/opener/src/reveal_item_in_dir.rs +++ b/plugins/opener/src/reveal_item_in_dir.rs @@ -35,10 +35,48 @@ pub fn reveal_item_in_dir>(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>(paths: &Vec

) -> 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::{ @@ -54,7 +92,7 @@ mod imp { }, }; - pub fn reveal_item_in_dir(path: &Path) -> crate::Result<()> { + pub fn reveal_item_in_dir>(path: P) -> crate::Result<()> { let file = dunce::simplified(path); let _ = unsafe { CoInitialize(None) }; @@ -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 { @@ -80,7 +118,7 @@ mod imp { let mut info = SHELLEXECUTEINFOW { cbSize: std::mem::size_of::() 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") @@ -105,6 +143,50 @@ mod imp { Ok(()) } + + pub fn reveal_items_in_dir>(paths: &Vec

) -> 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( @@ -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>(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>(paths: &Vec

) -> 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>( + paths: &Vec

, connection: &zbus::blocking::Connection, ) -> crate::Result<()> { - let uri = url::Url::from_file_path(path) - .map_err(|_| crate::Error::FailedToConvertPathToFileUrl)?; + let uris: Result, _> = 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 +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( @@ -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>(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); @@ -193,4 +294,25 @@ mod imp { Ok(()) } + + pub fn reveal_items_in_dir>(paths: &Vec

) -> 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(()) + } } From 23f7eebed1e0b8ccd7fdb6e7bdd8159d8f4aec9a Mon Sep 17 00:00:00 2001 From: petersamokhin Date: Tue, 5 Aug 2025 23:30:56 +0100 Subject: [PATCH 02/12] feature: reveal multiple items in dir --- plugins/opener/src/reveal_item_in_dir.rs | 103 +++++++++++++++-------- 1 file changed, 68 insertions(+), 35 deletions(-) diff --git a/plugins/opener/src/reveal_item_in_dir.rs b/plugins/opener/src/reveal_item_in_dir.rs index 6782a6df2f..b61dd5fd47 100644 --- a/plugins/opener/src/reveal_item_in_dir.rs +++ b/plugins/opener/src/reveal_item_in_dir.rs @@ -73,8 +73,7 @@ pub fn reveal_items_in_dir>(paths: &Vec

) -> crate::Result<()> #[cfg(windows)] mod imp { - use super::*; - use std::path::Path; + use std::path::PathBuf; use windows::Win32::UI::Shell::Common::ITEMIDLIST; use windows::{ @@ -92,7 +91,7 @@ mod imp { }, }; - pub fn reveal_item_in_dir>(path: P) -> crate::Result<()> { + pub fn reveal_item_in_dir(path: &PathBuf) -> crate::Result<()> { let file = dunce::simplified(path); let _ = unsafe { CoInitialize(None) }; @@ -144,45 +143,78 @@ mod imp { Ok(()) } - pub fn reveal_items_in_dir>(paths: &Vec

) -> crate::Result<()> { + pub fn reveal_items_in_dir(paths: &[PathBuf]) -> crate::Result<()> { if paths.is_empty() { return Ok(()); } let first_path = dunce::simplified(&paths[0]); - let dir = first_path + let parent_dir = first_path .parent() .ok_or_else(|| crate::Error::NoParent(first_path.to_path_buf()))?; - let _ = unsafe { CoInitialize(None) }; + // On Windows, SHOpenFolderAndSelectItems requires all items to be in the same directory. + // We filter the paths to ensure they all share the same parent as the first path. + let files_in_same_dir: Vec<_> = paths + .iter() + .map(|p| dunce::simplified(p)) + .filter(|p| p.parent() == Some(parent_dir)) + .collect(); - let dir_h = HSTRING::from(dir); - let dir_item = unsafe { ILCreateFromPathW(&dir_h) }; + if files_in_same_dir.is_empty() { + // This case can happen if the original list had paths from different directories. + // We can't open multiple directories, so we do nothing. + return Ok(()); + } + + let _ = unsafe { CoInitialize(None) }; - let mut items_to_free: Vec<*const ITEMIDLIST> = Vec::with_capacity(paths.len() + 1); - items_to_free.push(dir_item); + let dir_hstring = HSTRING::from(parent_dir); + let dir_item = unsafe { ILCreateFromPathW(&dir_hstring) }; - let mut file_items: Vec<*const ITEMIDLIST> = Vec::with_capacity(paths.len()); + // Ensure dir_item is freed even if subsequent operations fail. + let mut created_file_items = Vec::new(); - 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; + for path in &files_in_same_dir { + let file_hstring = HSTRING::from(path.as_os_str()); + let file_item = unsafe { ILCreateFromPathW(&file_hstring) }; + if !file_item.is_null() { + created_file_items.push(file_item); } - 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); } + // The function expects a slice of *const ITEMIDLIST, so we must cast our *mut pointers. + let item_id_lists_const: Vec<*const ITEMIDLIST> = created_file_items + .iter() + .map(|&p| p as *const _) + .collect(); + let result = unsafe { - SHOpenFolderAndSelectItems(dir_item, Some(&file_items), 0).map_err(Into::into) + if let Err(e) = SHOpenFolderAndSelectItems(dir_item, Some(&item_id_lists_const), 0) { + // Fallback logic from the original function. + if e.code().0 == ERROR_FILE_NOT_FOUND.0 as i32 { + let mut info = SHELLEXECUTEINFOW { + cbSize: std::mem::size_of::() as _, + nShow: SW_SHOWNORMAL.0, + lpFile: PCWSTR(dir_hstring.as_ptr()), + lpVerb: w!("explore"), + ..Default::default() + }; + ShellExecuteExW(&mut info).map(|_| ()).map_err(Into::into) + } else { + Err(e.into()) + } + } else { + Ok(()) + } }; - for item in items_to_free { - unsafe { ILFree(Some(item)) }; + // Free all allocated ITEMIDLISTs + unsafe { + for item in created_file_items { + ILFree(Some(item)); + } + ILFree(Some(dir_item)); } result @@ -199,15 +231,16 @@ mod imp { mod imp { use super::*; use std::collections::HashMap; + use std::path::PathBuf; - pub fn reveal_item_in_dir>(path: P) -> crate::Result<()> { + pub fn reveal_item_in_dir(path: &PathBuf) -> crate::Result<()> { let connection = zbus::blocking::Connection::session()?; - reveal_with_filemanager1(&vec![path], &connection) - .or_else(|_| reveal_with_open_uri_portal(path, &connection)) + reveal_with_filemanager1(&[path.clone()], &connection) + .or_else(|_| reveal_with_open_uri_portal(&path, &connection)) } - pub fn reveal_items_in_dir>(paths: &Vec

) -> crate::Result<()> { + pub fn reveal_items_in_dir(paths: &[PathBuf]) -> crate::Result<()> { let connection = zbus::blocking::Connection::session()?; reveal_with_filemanager1(paths, &connection).or_else(|e| { @@ -220,8 +253,8 @@ mod imp { }) } - fn reveal_with_filemanager1>( - paths: &Vec

, + fn reveal_with_filemanager1( + paths: &[PathBuf], connection: &zbus::blocking::Connection, ) -> crate::Result<()> { let uris: Result, _> = paths @@ -277,13 +310,13 @@ mod imp { #[cfg(target_os = "macos")] mod imp { - use super::*; use objc2_app_kit::NSWorkspace; use objc2_foundation::{NSArray, NSString, NSURL}; + use std::path::PathBuf; - pub fn reveal_item_in_dir>(path: P) -> crate::Result<()> { + pub fn reveal_item_in_dir(path: &PathBuf) -> crate::Result<()> { unsafe { - let path = path.as_ref().to_string_lossy(); + let path = path.to_string_lossy(); let path = NSString::from_str(&path); let urls = vec![NSURL::fileURLWithPath(&path)]; let urls = NSArray::from_retained_slice(&urls); @@ -295,12 +328,12 @@ mod imp { Ok(()) } - pub fn reveal_items_in_dir>(paths: &Vec

) -> crate::Result<()> { + pub fn reveal_items_in_dir(paths: &[PathBuf]) -> crate::Result<()> { unsafe { let mut urls = Vec::new(); for path in paths.iter() { - let path = path.as_ref().to_string_lossy(); + let path = path.to_string_lossy(); let path = NSString::from_str(&path); let url = NSURL::fileURLWithPath(&path); From b0cdb148b776493fd1e2ae6ca826877d7d4adc80 Mon Sep 17 00:00:00 2001 From: petersamokhin Date: Wed, 6 Aug 2025 00:46:20 +0100 Subject: [PATCH 03/12] feature: reveal multiple items in dir --- plugins/opener/api-iife.js | 2 +- plugins/opener/build.rs | 7 ++++++- plugins/opener/src/init-iife.js | 2 +- plugins/opener/src/lib.rs | 2 +- plugins/opener/src/reveal_item_in_dir.rs | 12 +++++------- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/plugins/opener/api-iife.js b/plugins/opener/api-iife.js index 30415a61e3..22013a31c2 100644 --- a/plugins/opener/api-iife.js +++ b/plugins/opener/api-iife.js @@ -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",{path:n})},n.revealItemsInDir=async function(n){return e("plugin:opener|reveal_items_in_dir",{paths:n})},n}({});Object.defineProperty(window.__TAURI__,"opener",{value:__TAURI_PLUGIN_OPENER__})} diff --git a/plugins/opener/build.rs b/plugins/opener/build.rs index ed1f9ce83e..562bafbdfc 100644 --- a/plugins/opener/build.rs +++ b/plugins/opener/build.rs @@ -110,7 +110,12 @@ fn _f() { }; } -const COMMANDS: &[&str] = &["open_url", "open_path", "reveal_item_in_dir", "reveal_items_in_dir"]; +const COMMANDS: &[&str] = &[ + "open_url", + "open_path", + "reveal_item_in_dir", + "reveal_items_in_dir", +]; fn main() { tauri_plugin::Builder::new(COMMANDS) diff --git a/plugins/opener/src/init-iife.js b/plugins/opener/src/init-iife.js index 51f6f0684c..1e271b0d2d 100644 --- a/plugins/opener/src/init-iife.js +++ b/plugins/opener/src/init-iife.js @@ -1 +1 @@ -!function(){"use strict";"function"==typeof SuppressedError&&SuppressedError,window.addEventListener("click",(function(e){if(e.defaultPrevented||0!==e.button||e.metaKey||e.altKey)return;const t=e.composedPath().find((e=>e instanceof Node&&"A"===e.nodeName.toUpperCase()));if(!t||!t.href||"_blank"!==t.target&&!e.ctrlKey&&!e.shiftKey)return;const n=new URL(t.href);n.origin===window.location.origin||["http:","https:","mailto:","tel:"].every((e=>n.protocol!==e))||(e.preventDefault(),async function(e,t={},n){window.__TAURI_INTERNALS__.invoke(e,t,n)}("plugin:opener|open_url",{url:n}))}))}(); +!function(){"use strict";"function"==typeof SuppressedError&&SuppressedError,window.addEventListener("click",function(e){if(e.defaultPrevented||0!==e.button||e.metaKey||e.altKey)return;const t=e.composedPath().find(e=>e instanceof Node&&"A"===e.nodeName.toUpperCase());if(!t||!t.href||"_blank"!==t.target&&!e.ctrlKey&&!e.shiftKey)return;const n=new URL(t.href);n.origin===window.location.origin||["http:","https:","mailto:","tel:"].every(e=>n.protocol!==e)||(e.preventDefault(),async function(e,t={},n){window.__TAURI_INTERNALS__.invoke(e,t,n)}("plugin:opener|open_url",{url:n}))})}(); diff --git a/plugins/opener/src/lib.rs b/plugins/opener/src/lib.rs index 1e7c1e21c4..9fd6a06c49 100644 --- a/plugins/opener/src/lib.rs +++ b/plugins/opener/src/lib.rs @@ -149,7 +149,7 @@ impl Opener { crate::reveal_item_in_dir::reveal_item_in_dir(p) } - pub fn reveal_items_in_dir>(&self, p: &Vec

) -> Result<()> { + pub fn reveal_items_in_dir>(&self, p: &[P]) -> Result<()> { crate::reveal_item_in_dir::reveal_items_in_dir(p) } } diff --git a/plugins/opener/src/reveal_item_in_dir.rs b/plugins/opener/src/reveal_item_in_dir.rs index b61dd5fd47..47bb4ecdef 100644 --- a/plugins/opener/src/reveal_item_in_dir.rs +++ b/plugins/opener/src/reveal_item_in_dir.rs @@ -40,7 +40,7 @@ pub fn reveal_item_in_dir>(path: P) -> crate::Result<()> { /// ## Platform-specific: /// /// - **Android / iOS:** Unsupported. -pub fn reveal_items_in_dir>(paths: &Vec

) -> crate::Result<()> { +pub fn reveal_items_in_dir>(paths: &[P]) -> crate::Result<()> { let mut path_bufs = vec![]; for path in paths.iter() { @@ -184,10 +184,8 @@ mod imp { } // The function expects a slice of *const ITEMIDLIST, so we must cast our *mut pointers. - let item_id_lists_const: Vec<*const ITEMIDLIST> = created_file_items - .iter() - .map(|&p| p as *const _) - .collect(); + let item_id_lists_const: Vec<*const ITEMIDLIST> = + created_file_items.iter().map(|&p| p as *const _).collect(); let result = unsafe { if let Err(e) = SHOpenFolderAndSelectItems(dir_item, Some(&item_id_lists_const), 0) { @@ -312,9 +310,9 @@ mod imp { mod imp { use objc2_app_kit::NSWorkspace; use objc2_foundation::{NSArray, NSString, NSURL}; - use std::path::PathBuf; + use std::path::{Path, PathBuf}; - pub fn reveal_item_in_dir(path: &PathBuf) -> crate::Result<()> { + pub fn reveal_item_in_dir(path: &Path) -> crate::Result<()> { unsafe { let path = path.to_string_lossy(); let path = NSString::from_str(&path); From ec8e4a22629cf3d19c38d22094c2ce5c5f578293 Mon Sep 17 00:00:00 2001 From: petersamokhin Date: Wed, 6 Aug 2025 01:04:49 +0100 Subject: [PATCH 04/12] feature: reveal multiple items in dir --- plugins/opener/src/init-iife.js | 2 +- plugins/opener/src/reveal_item_in_dir.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/opener/src/init-iife.js b/plugins/opener/src/init-iife.js index 1e271b0d2d..51f6f0684c 100644 --- a/plugins/opener/src/init-iife.js +++ b/plugins/opener/src/init-iife.js @@ -1 +1 @@ -!function(){"use strict";"function"==typeof SuppressedError&&SuppressedError,window.addEventListener("click",function(e){if(e.defaultPrevented||0!==e.button||e.metaKey||e.altKey)return;const t=e.composedPath().find(e=>e instanceof Node&&"A"===e.nodeName.toUpperCase());if(!t||!t.href||"_blank"!==t.target&&!e.ctrlKey&&!e.shiftKey)return;const n=new URL(t.href);n.origin===window.location.origin||["http:","https:","mailto:","tel:"].every(e=>n.protocol!==e)||(e.preventDefault(),async function(e,t={},n){window.__TAURI_INTERNALS__.invoke(e,t,n)}("plugin:opener|open_url",{url:n}))})}(); +!function(){"use strict";"function"==typeof SuppressedError&&SuppressedError,window.addEventListener("click",(function(e){if(e.defaultPrevented||0!==e.button||e.metaKey||e.altKey)return;const t=e.composedPath().find((e=>e instanceof Node&&"A"===e.nodeName.toUpperCase()));if(!t||!t.href||"_blank"!==t.target&&!e.ctrlKey&&!e.shiftKey)return;const n=new URL(t.href);n.origin===window.location.origin||["http:","https:","mailto:","tel:"].every((e=>n.protocol!==e))||(e.preventDefault(),async function(e,t={},n){window.__TAURI_INTERNALS__.invoke(e,t,n)}("plugin:opener|open_url",{url:n}))}))}(); diff --git a/plugins/opener/src/reveal_item_in_dir.rs b/plugins/opener/src/reveal_item_in_dir.rs index 47bb4ecdef..d6e86048e8 100644 --- a/plugins/opener/src/reveal_item_in_dir.rs +++ b/plugins/opener/src/reveal_item_in_dir.rs @@ -235,7 +235,7 @@ mod imp { let connection = zbus::blocking::Connection::session()?; reveal_with_filemanager1(&[path.clone()], &connection) - .or_else(|_| reveal_with_open_uri_portal(&path, &connection)) + .or_else(|_| reveal_with_open_uri_portal(path, &connection)) } pub fn reveal_items_in_dir(paths: &[PathBuf]) -> crate::Result<()> { From 61ffc83a27f67ca178645e05f76ee3c2ff6409ed Mon Sep 17 00:00:00 2001 From: petersamokhin Date: Wed, 6 Aug 2025 02:00:14 +0100 Subject: [PATCH 05/12] feature: reveal multiple items in dir --- plugins/opener/src/lib.rs | 10 ++- plugins/opener/src/reveal_item_in_dir.rs | 96 ++++-------------------- 2 files changed, 21 insertions(+), 85 deletions(-) diff --git a/plugins/opener/src/lib.rs b/plugins/opener/src/lib.rs index 9fd6a06c49..f70ea499fb 100644 --- a/plugins/opener/src/lib.rs +++ b/plugins/opener/src/lib.rs @@ -146,11 +146,15 @@ impl Opener { } pub fn reveal_item_in_dir>(&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>(&self, p: &[P]) -> Result<()> { - crate::reveal_item_in_dir::reveal_items_in_dir(p) + pub fn reveal_items_in_dir(&self, paths: I) -> Result<()> + where + I: IntoIterator, + P: AsRef, + { + reveal_items_in_dir(paths) } } diff --git a/plugins/opener/src/reveal_item_in_dir.rs b/plugins/opener/src/reveal_item_in_dir.rs index d6e86048e8..467f945c42 100644 --- a/plugins/opener/src/reveal_item_in_dir.rs +++ b/plugins/opener/src/reveal_item_in_dir.rs @@ -21,7 +21,7 @@ pub fn reveal_item_in_dir>(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, @@ -40,12 +40,17 @@ pub fn reveal_item_in_dir>(path: P) -> crate::Result<()> { /// ## Platform-specific: /// /// - **Android / iOS:** Unsupported. -pub fn reveal_items_in_dir>(paths: &[P]) -> crate::Result<()> { - let mut path_bufs = vec![]; - - for path in paths.iter() { +/// - **Windows:** Only supports revealing items in the same directory. +pub fn reveal_items_in_dir(paths: I) -> crate::Result<()> +where + I: IntoIterator, + P: AsRef, +{ + let mut canonicalized = vec![]; + + for path in paths { let path = path.as_ref().canonicalize()?; - path_bufs.push(path); + canonicalized.push(path); } #[cfg(any( @@ -57,7 +62,7 @@ pub fn reveal_items_in_dir>(paths: &[P]) -> crate::Result<()> { target_os = "netbsd", target_os = "openbsd" ))] - return imp::reveal_items_in_dir(&path_bufs); + return imp::reveal_items_in_dir(&canonicalized); #[cfg(not(any( windows, @@ -91,58 +96,6 @@ mod imp { }, }; - pub fn reveal_item_in_dir(path: &PathBuf) -> 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()))?; - - let dir_h = HSTRING::from(dir); - let dir_item = unsafe { ILCreateFromPathW(&dir_h) }; - - let file_h = HSTRING::from(file.as_os_str()); - let file_item = unsafe { ILCreateFromPathW(&file_h) }; - - unsafe { - if let Err(e) = SHOpenFolderAndSelectItems(dir_item, Some(&[file_item]), 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). - if e.code().0 == ERROR_FILE_NOT_FOUND.0 as i32 { - let is_dir = file.is_dir(); - let mut info = SHELLEXECUTEINFOW { - cbSize: std::mem::size_of::() as _, - nShow: SW_SHOWNORMAL.0, - lpFile: PCWSTR(dir_h.as_ptr()), - lpClass: if is_dir { w!("folder") } else { PCWSTR::null() }, - lpVerb: if is_dir { - w!("explore") - } else { - PCWSTR::null() - }, - ..std::mem::zeroed() - }; - - ShellExecuteExW(&mut info).inspect_err(|_| { - ILFree(Some(dir_item)); - ILFree(Some(file_item)); - })?; - } - } - } - - unsafe { - ILFree(Some(dir_item)); - ILFree(Some(file_item)); - } - - Ok(()) - } - pub fn reveal_items_in_dir(paths: &[PathBuf]) -> crate::Result<()> { if paths.is_empty() { return Ok(()); @@ -231,13 +184,6 @@ mod imp { use std::collections::HashMap; use std::path::PathBuf; - pub fn reveal_item_in_dir(path: &PathBuf) -> crate::Result<()> { - let connection = zbus::blocking::Connection::session()?; - - reveal_with_filemanager1(&[path.clone()], &connection) - .or_else(|_| reveal_with_open_uri_portal(path, &connection)) - } - pub fn reveal_items_in_dir(paths: &[PathBuf]) -> crate::Result<()> { let connection = zbus::blocking::Connection::session()?; @@ -310,27 +256,13 @@ mod imp { mod imp { use objc2_app_kit::NSWorkspace; use objc2_foundation::{NSArray, NSString, NSURL}; - use std::path::{Path, PathBuf}; - - pub fn reveal_item_in_dir(path: &Path) -> crate::Result<()> { - unsafe { - let path = path.to_string_lossy(); - let path = NSString::from_str(&path); - let urls = vec![NSURL::fileURLWithPath(&path)]; - let urls = NSArray::from_retained_slice(&urls); - - let workspace = NSWorkspace::new(); - workspace.activateFileViewerSelectingURLs(&urls); - } - - Ok(()) - } + use std::path::PathBuf; pub fn reveal_items_in_dir(paths: &[PathBuf]) -> crate::Result<()> { unsafe { let mut urls = Vec::new(); - for path in paths.iter() { + for path in paths { let path = path.to_string_lossy(); let path = NSString::from_str(&path); let url = NSURL::fileURLWithPath(&path); From 23c5013860ed63daf2f6233285b3001e1977293f Mon Sep 17 00:00:00 2001 From: petersamokhin Date: Wed, 6 Aug 2025 02:06:45 +0100 Subject: [PATCH 06/12] feature: reveal multiple items in dir --- plugins/opener/README.md | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/plugins/opener/README.md b/plugins/opener/README.md index 02050e932d..1dffd1bbdc 100644 --- a/plugins/opener/README.md +++ b/plugins/opener/README.md @@ -2,13 +2,13 @@ -| Platform | Supported | -| -------- | --------- | -| Linux | ✓ | -| Windows | ✓ | -| macOS | ✓ | -| Android | ? | -| iOS | ? | +| Platform | Supported | Notes | +|----------|-----------|---------------------------------------------------------------------------| +| Linux | ✓ | | +| Windows | ✓ | Revealing multiple files placed in different directories is not supported | +| macOS | ✓ | | +| Android | ? | | +| iOS | ? | | ## Install @@ -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: on Windows, files have to be in the same directory +await revealItemsInDir([ '/path/to/file', '/path/to/another/file' ]) ``` ### Usage from Rust @@ -102,6 +106,10 @@ 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 + // Note: on Windows, files have to be in the same directory + opener.reveal_items_in_dir(&["/path/to/file"])?; Ok(()) }) .run(tauri::generate_context!()) From cbd918eb823e3be930e24d2cb32645ae7cf740f1 Mon Sep 17 00:00:00 2001 From: petersamokhin Date: Wed, 6 Aug 2025 02:09:12 +0100 Subject: [PATCH 07/12] feature: reveal multiple items in dir --- plugins/opener/guest-js/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/opener/guest-js/index.ts b/plugins/opener/guest-js/index.ts index a0632ee5a4..46b475b644 100644 --- a/plugins/opener/guest-js/index.ts +++ b/plugins/opener/guest-js/index.ts @@ -102,6 +102,7 @@ export async function revealItemInDir(path: string) { * #### Platform-specific: * * - **Android / iOS:** Unsupported. + * - **Windows:** Only supports revealing items in the same directory. * * @example * ```typescript From d0f88462ed0fbd1867f48974609c8c727bdf8468 Mon Sep 17 00:00:00 2001 From: Tony Date: Wed, 6 Aug 2025 12:15:34 +0800 Subject: [PATCH 08/12] Support multiple roots on Windows --- plugins/opener/README.md | 20 ++-- plugins/opener/guest-js/index.ts | 1 - plugins/opener/src/error.rs | 2 + plugins/opener/src/reveal_item_in_dir.rs | 128 +++++++++++++---------- 4 files changed, 83 insertions(+), 68 deletions(-) diff --git a/plugins/opener/README.md b/plugins/opener/README.md index 1dffd1bbdc..5be1b6c515 100644 --- a/plugins/opener/README.md +++ b/plugins/opener/README.md @@ -2,13 +2,13 @@ -| Platform | Supported | Notes | -|----------|-----------|---------------------------------------------------------------------------| -| Linux | ✓ | | -| Windows | ✓ | Revealing multiple files placed in different directories is not supported | -| macOS | ✓ | | -| Android | ? | | -| iOS | ? | | +| Platform | Supported | Notes | +| -------- | --------- | ----- | +| Linux | ✓ | | +| Windows | ✓ | | +| macOS | ✓ | | +| Android | ? | | +| iOS | ? | | ## Install @@ -77,8 +77,7 @@ await openPath('/path/to/file', 'firefox') await revealItemInDir('/path/to/file') // Reveal multiple paths with the system's default explorer -// Note: on Windows, files have to be in the same directory -await revealItemsInDir([ '/path/to/file', '/path/to/another/file' ]) +await revealItemsInDir(['/path/to/file', '/path/to/another/file']) ``` ### Usage from Rust @@ -108,8 +107,7 @@ fn main() { opener.reveal_item_in_dir("/path/to/file")?; // Reveal multiple paths with the system's default explorer - // Note: on Windows, files have to be in the same directory - opener.reveal_items_in_dir(&["/path/to/file"])?; + opener.reveal_items_in_dir(["/path/to/file"])?; Ok(()) }) .run(tauri::generate_context!()) diff --git a/plugins/opener/guest-js/index.ts b/plugins/opener/guest-js/index.ts index 46b475b644..a0632ee5a4 100644 --- a/plugins/opener/guest-js/index.ts +++ b/plugins/opener/guest-js/index.ts @@ -102,7 +102,6 @@ export async function revealItemInDir(path: string) { * #### Platform-specific: * * - **Android / iOS:** Unsupported. - * - **Windows:** Only supports revealing items in the same directory. * * @example * ```typescript diff --git a/plugins/opener/src/error.rs b/plugins/opener/src/error.rs index 157922fc60..36d781b44c 100644 --- a/plugins/opener/src/error.rs +++ b/plugins/opener/src/error.rs @@ -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)] diff --git a/plugins/opener/src/reveal_item_in_dir.rs b/plugins/opener/src/reveal_item_in_dir.rs index 467f945c42..1cea2641db 100644 --- a/plugins/opener/src/reveal_item_in_dir.rs +++ b/plugins/opener/src/reveal_item_in_dir.rs @@ -10,7 +10,7 @@ use std::path::Path; /// /// - **Android / iOS:** Unsupported. pub fn reveal_item_in_dir>(path: P) -> crate::Result<()> { - let path = path.as_ref().canonicalize()?; + let path = dunce::canonicalize(path.as_ref())?; #[cfg(any( windows, @@ -40,7 +40,6 @@ pub fn reveal_item_in_dir>(path: P) -> crate::Result<()> { /// ## Platform-specific: /// /// - **Android / iOS:** Unsupported. -/// - **Windows:** Only supports revealing items in the same directory. pub fn reveal_items_in_dir(paths: I) -> crate::Result<()> where I: IntoIterator, @@ -49,7 +48,7 @@ where let mut canonicalized = vec![]; for path in paths { - let path = path.as_ref().canonicalize()?; + let path = dunce::canonicalize(path.as_ref())?; canonicalized.push(path); } @@ -78,7 +77,8 @@ where #[cfg(windows)] mod imp { - use std::path::PathBuf; + use std::collections::HashMap; + use std::path::{Path, PathBuf}; use windows::Win32::UI::Shell::Common::ITEMIDLIST; use windows::{ @@ -101,74 +101,90 @@ mod imp { return Ok(()); } - let first_path = dunce::simplified(&paths[0]); - let parent_dir = first_path - .parent() - .ok_or_else(|| crate::Error::NoParent(first_path.to_path_buf()))?; - - // On Windows, SHOpenFolderAndSelectItems requires all items to be in the same directory. - // We filter the paths to ensure they all share the same parent as the first path. - let files_in_same_dir: Vec<_> = paths - .iter() - .map(|p| dunce::simplified(p)) - .filter(|p| p.parent() == Some(parent_dir)) - .collect(); - - if files_in_same_dir.is_empty() { - // This case can happen if the original list had paths from different directories. - // We can't open multiple directories, so we do nothing. - 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_hstring = HSTRING::from(parent_dir); - let dir_item = unsafe { ILCreateFromPathW(&dir_hstring) }; - - // Ensure dir_item is freed even if subsequent operations fail. - let mut created_file_items = Vec::new(); - - for path in &files_in_same_dir { - let file_hstring = HSTRING::from(path.as_os_str()); - let file_item = unsafe { ILCreateFromPathW(&file_hstring) }; - if !file_item.is_null() { - created_file_items.push(file_item); - } - } - - // The function expects a slice of *const ITEMIDLIST, so we must cast our *mut pointers. - let item_id_lists_const: Vec<*const ITEMIDLIST> = - created_file_items.iter().map(|&p| p as *const _).collect(); - - let result = unsafe { - if let Err(e) = SHOpenFolderAndSelectItems(dir_item, Some(&item_id_lists_const), 0) { - // Fallback logic from the original function. + 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::>>()?; + if let Err(e) = unsafe { + SHOpenFolderAndSelectItems( + parent_item_id_list.item, + Some( + &to_reveals_item_id_list + .iter() + .map(|item| item.item) + .collect::>(), + ), + 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 first_path = to_reveals[0]; + let is_dir = first_path.is_dir(); let mut info = SHELLEXECUTEINFOW { cbSize: std::mem::size_of::() as _, nShow: SW_SHOWNORMAL.0, - lpFile: PCWSTR(dir_hstring.as_ptr()), - lpVerb: w!("explore"), + 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() + }, ..Default::default() }; - ShellExecuteExW(&mut info).map(|_| ()).map_err(Into::into) - } else { - Err(e.into()) + + unsafe { ShellExecuteExW(&mut info) }?; } - } else { - Ok(()) } - }; + } - // Free all allocated ITEMIDLISTs - unsafe { - for item in created_file_items { - ILFree(Some(item)); + Ok(()) + } + + struct OwnedItemIdList { + hstring: HSTRING, + item: *const ITEMIDLIST, + } + + impl OwnedItemIdList { + fn new(path: &Path) -> crate::Result { + 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, + }) } - ILFree(Some(dir_item)); } + } - result + impl Drop for OwnedItemIdList { + fn drop(&mut self) { + if !self.item.is_null() { + unsafe { ILFree(Some(self.item)) }; + } + } } } From 3dedfcd75d5135a84832a0c13f8893f895197edf Mon Sep 17 00:00:00 2001 From: petersamokhin Date: Wed, 6 Aug 2025 22:23:31 +0100 Subject: [PATCH 09/12] feature: reveal multiple items in dir --- .../permissions/autogenerated/reference.md | 1 - .../opener/permissions/schemas/schema.json | 71 +++++-------------- 2 files changed, 16 insertions(+), 56 deletions(-) diff --git a/plugins/opener/permissions/autogenerated/reference.md b/plugins/opener/permissions/autogenerated/reference.md index d3b48b5df7..59b8acda6d 100644 --- a/plugins/opener/permissions/autogenerated/reference.md +++ b/plugins/opener/permissions/autogenerated/reference.md @@ -18,7 +18,6 @@ as well as reveal file in directories using default file explorer Description - diff --git a/plugins/opener/permissions/schemas/schema.json b/plugins/opener/permissions/schemas/schema.json index 7a38d0ad2f..a15c33ba1e 100644 --- a/plugins/opener/permissions/schemas/schema.json +++ b/plugins/opener/permissions/schemas/schema.json @@ -35,25 +35,17 @@ "DefaultPermission": { "description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.", "type": "object", - "required": [ - "permissions" - ], + "required": ["permissions"], "properties": { "version": { "description": "The version of the permission.", - "type": [ - "integer", - "null" - ], + "type": ["integer", "null"], "format": "uint64", "minimum": 1.0 }, "description": { "description": "Human-readable description of what the permission does. Tauri convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", - "type": [ - "string", - "null" - ] + "type": ["string", "null"] }, "permissions": { "description": "All permissions this set contains.", @@ -67,11 +59,7 @@ "PermissionSet": { "description": "A set of direct permissions grouped together under a new name.", "type": "object", - "required": [ - "description", - "identifier", - "permissions" - ], + "required": ["description", "identifier", "permissions"], "properties": { "identifier": { "description": "A unique identifier for the permission.", @@ -93,16 +81,11 @@ "Permission": { "description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.", "type": "object", - "required": [ - "identifier" - ], + "required": ["identifier"], "properties": { "version": { "description": "The version of the permission.", - "type": [ - "integer", - "null" - ], + "type": ["integer", "null"], "format": "uint64", "minimum": 1.0 }, @@ -112,10 +95,7 @@ }, "description": { "description": "Human-readable description of what the permission does. Tauri internal convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", - "type": [ - "string", - "null" - ] + "type": ["string", "null"] }, "commands": { "description": "Allowed or denied commands when using this permission.", @@ -139,10 +119,7 @@ }, "platforms": { "description": "Target platforms this permission applies. By default all platforms are affected by this permission.", - "type": [ - "array", - "null" - ], + "type": ["array", "null"], "items": { "$ref": "#/definitions/Target" } @@ -177,20 +154,14 @@ "properties": { "allow": { "description": "Data that defines what is allowed by the scope.", - "type": [ - "array", - "null" - ], + "type": ["array", "null"], "items": { "$ref": "#/definitions/Value" } }, "deny": { "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", - "type": [ - "array", - "null" - ], + "type": ["array", "null"], "items": { "$ref": "#/definitions/Value" } @@ -257,37 +228,27 @@ { "description": "MacOS.", "type": "string", - "enum": [ - "macOS" - ] + "enum": ["macOS"] }, { "description": "Windows.", "type": "string", - "enum": [ - "windows" - ] + "enum": ["windows"] }, { "description": "Linux.", "type": "string", - "enum": [ - "linux" - ] + "enum": ["linux"] }, { "description": "Android.", "type": "string", - "enum": [ - "android" - ] + "enum": ["android"] }, { "description": "iOS.", "type": "string", - "enum": [ - "iOS" - ] + "enum": ["iOS"] } ] }, @@ -357,4 +318,4 @@ ] } } -} \ No newline at end of file +} From 590f888c94497496ae95a32de73a9e42c56b4471 Mon Sep 17 00:00:00 2001 From: petersamokhin Date: Wed, 6 Aug 2025 22:32:43 +0100 Subject: [PATCH 10/12] feature: reveal multiple items in dir --- plugins/opener/Cargo.toml | 2 - .../permissions/autogenerated/reference.md | 1 + .../opener/permissions/schemas/schema.json | 71 ++++++++++++++----- 3 files changed, 56 insertions(+), 18 deletions(-) diff --git a/plugins/opener/Cargo.toml b/plugins/opener/Cargo.toml index 323914e5e8..6d07078236 100644 --- a/plugins/opener/Cargo.toml +++ b/plugins/opener/Cargo.toml @@ -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] diff --git a/plugins/opener/permissions/autogenerated/reference.md b/plugins/opener/permissions/autogenerated/reference.md index 59b8acda6d..d3b48b5df7 100644 --- a/plugins/opener/permissions/autogenerated/reference.md +++ b/plugins/opener/permissions/autogenerated/reference.md @@ -18,6 +18,7 @@ as well as reveal file in directories using default file explorer Description + diff --git a/plugins/opener/permissions/schemas/schema.json b/plugins/opener/permissions/schemas/schema.json index a15c33ba1e..7a38d0ad2f 100644 --- a/plugins/opener/permissions/schemas/schema.json +++ b/plugins/opener/permissions/schemas/schema.json @@ -35,17 +35,25 @@ "DefaultPermission": { "description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.", "type": "object", - "required": ["permissions"], + "required": [ + "permissions" + ], "properties": { "version": { "description": "The version of the permission.", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "format": "uint64", "minimum": 1.0 }, "description": { "description": "Human-readable description of what the permission does. Tauri convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "permissions": { "description": "All permissions this set contains.", @@ -59,7 +67,11 @@ "PermissionSet": { "description": "A set of direct permissions grouped together under a new name.", "type": "object", - "required": ["description", "identifier", "permissions"], + "required": [ + "description", + "identifier", + "permissions" + ], "properties": { "identifier": { "description": "A unique identifier for the permission.", @@ -81,11 +93,16 @@ "Permission": { "description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.", "type": "object", - "required": ["identifier"], + "required": [ + "identifier" + ], "properties": { "version": { "description": "The version of the permission.", - "type": ["integer", "null"], + "type": [ + "integer", + "null" + ], "format": "uint64", "minimum": 1.0 }, @@ -95,7 +112,10 @@ }, "description": { "description": "Human-readable description of what the permission does. Tauri internal convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", - "type": ["string", "null"] + "type": [ + "string", + "null" + ] }, "commands": { "description": "Allowed or denied commands when using this permission.", @@ -119,7 +139,10 @@ }, "platforms": { "description": "Target platforms this permission applies. By default all platforms are affected by this permission.", - "type": ["array", "null"], + "type": [ + "array", + "null" + ], "items": { "$ref": "#/definitions/Target" } @@ -154,14 +177,20 @@ "properties": { "allow": { "description": "Data that defines what is allowed by the scope.", - "type": ["array", "null"], + "type": [ + "array", + "null" + ], "items": { "$ref": "#/definitions/Value" } }, "deny": { "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", - "type": ["array", "null"], + "type": [ + "array", + "null" + ], "items": { "$ref": "#/definitions/Value" } @@ -228,27 +257,37 @@ { "description": "MacOS.", "type": "string", - "enum": ["macOS"] + "enum": [ + "macOS" + ] }, { "description": "Windows.", "type": "string", - "enum": ["windows"] + "enum": [ + "windows" + ] }, { "description": "Linux.", "type": "string", - "enum": ["linux"] + "enum": [ + "linux" + ] }, { "description": "Android.", "type": "string", - "enum": ["android"] + "enum": [ + "android" + ] }, { "description": "iOS.", "type": "string", - "enum": ["iOS"] + "enum": [ + "iOS" + ] } ] }, @@ -318,4 +357,4 @@ ] } } -} +} \ No newline at end of file From b6bd88290433795812c025476977bf04ffd80c39 Mon Sep 17 00:00:00 2001 From: petersamokhin Date: Fri, 8 Aug 2025 21:33:38 +0100 Subject: [PATCH 11/12] feature: reveal multiple items in dir --- plugins/opener/README.md | 17 ++++++------ plugins/opener/api-iife.js | 2 +- plugins/opener/build.rs | 7 +---- plugins/opener/guest-js/index.ts | 27 +++---------------- .../commands/reveal_items_in_dir.toml | 13 --------- .../permissions/autogenerated/reference.md | 27 ------------------- plugins/opener/permissions/default.toml | 1 - .../opener/permissions/schemas/schema.json | 16 ++--------- plugins/opener/src/commands.rs | 8 ++---- plugins/opener/src/lib.rs | 1 - 10 files changed, 19 insertions(+), 100 deletions(-) delete mode 100644 plugins/opener/permissions/autogenerated/commands/reveal_items_in_dir.toml diff --git a/plugins/opener/README.md b/plugins/opener/README.md index 5be1b6c515..bcb9265ca4 100644 --- a/plugins/opener/README.md +++ b/plugins/opener/README.md @@ -2,13 +2,13 @@ -| Platform | Supported | Notes | -| -------- | --------- | ----- | -| Linux | ✓ | | -| Windows | ✓ | | -| macOS | ✓ | | -| Android | ? | | -| iOS | ? | | +| Platform | Supported | +| -------- | --------- | +| Linux | ✓ | +| Windows | ✓ | +| macOS | ✓ | +| Android | ? | +| iOS | ? | ## Install @@ -77,7 +77,8 @@ await openPath('/path/to/file', 'firefox') await revealItemInDir('/path/to/file') // Reveal multiple paths with the system's default explorer -await revealItemsInDir(['/path/to/file', '/path/to/another/file']) +// Note: will be renamed to `revealItemsInDir` in the next major version +await revealItemInDir(['/path/to/file', '/path/to/another/file']) ``` ### Usage from Rust diff --git a/plugins/opener/api-iife.js b/plugins/opener/api-iife.js index 22013a31c2..dd976e5760 100644 --- a/plugins/opener/api-iife.js +++ b/plugins/opener/api-iife.js @@ -1 +1 @@ -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",{path:n})},n.revealItemsInDir=async function(n){return e("plugin:opener|reveal_items_in_dir",{paths: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__})} diff --git a/plugins/opener/build.rs b/plugins/opener/build.rs index 562bafbdfc..fbad4d3ac3 100644 --- a/plugins/opener/build.rs +++ b/plugins/opener/build.rs @@ -110,12 +110,7 @@ fn _f() { }; } -const COMMANDS: &[&str] = &[ - "open_url", - "open_path", - "reveal_item_in_dir", - "reveal_items_in_dir", -]; +const COMMANDS: &[&str] = &["open_url", "open_path", "reveal_item_in_dir"]; fn main() { tauri_plugin::Builder::new(COMMANDS) diff --git a/plugins/opener/guest-js/index.ts b/plugins/opener/guest-js/index.ts index a0632ee5a4..5e214c5515 100644 --- a/plugins/opener/guest-js/index.ts +++ b/plugins/opener/guest-js/index.ts @@ -86,33 +86,14 @@ export async function openPath(path: string, openWith?: string): Promise { * ```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 }) -} - -/** - * 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 }) +export async function revealItemInDir(path: string | string[]): Promise { + const paths = typeof path === 'string' ? [ path ] : path + return invoke('plugin:opener|reveal_item_in_dir', { paths }) } diff --git a/plugins/opener/permissions/autogenerated/commands/reveal_items_in_dir.toml b/plugins/opener/permissions/autogenerated/commands/reveal_items_in_dir.toml deleted file mode 100644 index 74cba165e0..0000000000 --- a/plugins/opener/permissions/autogenerated/commands/reveal_items_in_dir.toml +++ /dev/null @@ -1,13 +0,0 @@ -# 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"] diff --git a/plugins/opener/permissions/autogenerated/reference.md b/plugins/opener/permissions/autogenerated/reference.md index d3b48b5df7..6ad6ba1fbf 100644 --- a/plugins/opener/permissions/autogenerated/reference.md +++ b/plugins/opener/permissions/autogenerated/reference.md @@ -7,7 +7,6 @@ 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 @@ -107,32 +106,6 @@ Enables the reveal_item_in_dir command without any pre-configured scope. Denies the reveal_item_in_dir command without any pre-configured scope. - - - - - - -`opener:allow-reveal-items-in-dir` - - - - -Enables the reveal_items_in_dir command without any pre-configured scope. - - - - - - - -`opener:deny-reveal-items-in-dir` - - - - -Denies the reveal_items_in_dir command without any pre-configured scope. - diff --git a/plugins/opener/permissions/default.toml b/plugins/opener/permissions/default.toml index 3e51255522..846d6e51be 100644 --- a/plugins/opener/permissions/default.toml +++ b/plugins/opener/permissions/default.toml @@ -6,6 +6,5 @@ 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", ] diff --git a/plugins/opener/permissions/schemas/schema.json b/plugins/opener/permissions/schemas/schema.json index 7a38d0ad2f..78c80ba385 100644 --- a/plugins/opener/permissions/schemas/schema.json +++ b/plugins/opener/permissions/schemas/schema.json @@ -337,22 +337,10 @@ "markdownDescription": "Denies the reveal_item_in_dir command without any pre-configured scope." }, { - "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`", + "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`", "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-reveal-items-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-default-urls`" } ] } diff --git a/plugins/opener/src/commands.rs b/plugins/opener/src/commands.rs index 28c7723289..e22b41d418 100644 --- a/plugins/opener/src/commands.rs +++ b/plugins/opener/src/commands.rs @@ -69,12 +69,8 @@ pub async fn open_path( } } +/// 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) -} - -#[tauri::command] -pub async fn reveal_items_in_dir(paths: Vec) -> crate::Result<()> { +pub async fn reveal_item_in_dir(paths: Vec) -> crate::Result<()> { crate::reveal_items_in_dir(&paths) } diff --git a/plugins/opener/src/lib.rs b/plugins/opener/src/lib.rs index f70ea499fb..343e915580 100644 --- a/plugins/opener/src/lib.rs +++ b/plugins/opener/src/lib.rs @@ -222,7 +222,6 @@ impl Builder { commands::open_url, commands::open_path, commands::reveal_item_in_dir, - commands::reveal_items_in_dir, ]); if self.open_js_links_on_click { From 27535280b677593ffc42f3d57bdba6a112b4ead1 Mon Sep 17 00:00:00 2001 From: petersamokhin Date: Sat, 9 Aug 2025 01:37:49 +0100 Subject: [PATCH 12/12] feature: reveal multiple items in dir --- .changes/opener-reveal-multiple-items.md | 6 ++++++ plugins/opener/guest-js/index.ts | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .changes/opener-reveal-multiple-items.md diff --git a/.changes/opener-reveal-multiple-items.md b/.changes/opener-reveal-multiple-items.md new file mode 100644 index 0000000000..9f1198626c --- /dev/null +++ b/.changes/opener-reveal-multiple-items.md @@ -0,0 +1,6 @@ +--- +"opener": 'minor:enhance' +"opener-js": 'minor:enhance' +--- + +Allow reveal multiple items in the file explorer. \ No newline at end of file diff --git a/plugins/opener/guest-js/index.ts b/plugins/opener/guest-js/index.ts index 5e214c5515..6b40da19ce 100644 --- a/plugins/opener/guest-js/index.ts +++ b/plugins/opener/guest-js/index.ts @@ -94,6 +94,6 @@ export async function openPath(path: string, openWith?: string): Promise { * @since 2.0.0 */ export async function revealItemInDir(path: string | string[]): Promise { - const paths = typeof path === 'string' ? [ path ] : path + const paths = typeof path === 'string' ? [path] : path return invoke('plugin:opener|reveal_item_in_dir', { paths }) }