Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[shell] Show in finder/explorer #999

Closed
betamos opened this issue May 5, 2022 · 17 comments · Fixed by #2019
Closed

[shell] Show in finder/explorer #999

betamos opened this issue May 5, 2022 · 17 comments · Fixed by #2019
Assignees
Labels
enhancement New feature or request plugin: shell

Comments

@betamos
Copy link

betamos commented May 5, 2022

Describe the problem

The shell.open API can be used to open local files (in their respective app) or opening a directory in the finder/explorer-equivalent. In addition to this, I need to "show in finder", i.e. open a directory with a set of files/directories selected.

Describe the solution you'd like

At a minimum, a function that accepts one file or directory and opens its containing directory with that file/dir selected. Ideally, multiple files can be provided, but I think this may be harder to do cross-platform.

Alternatives considered

No response

Additional context

If API additions are difficult, I'm also interested in workarounds.

@FabianLars
Copy link
Member

FabianLars commented May 5, 2022

So the main problem here indeed is cross-platform support. Linux is basically ruled out, since xdg-open doesn't support it, we could read the user's preferred explorer app and compare it with a predefined list and spawn them directly.

On the other hand, windows and macos support is fairly easy:

// These are rust commands, not tauri ones btw
// Windows
Command::new("explorer")
        .args(["/select,", "\"path\\to\\file""]) // The comma after select is not a typo
        .spawn()
        .unwrap();
// Alternatively there is a win32 api for this iirc which needs to be used for longer file paths i think.

// macOS
Command::new( "open" )
        .args(["-R", "path/to/file"]) // i don't have a mac so not 100% sure
        .spawn()
        .unwrap();

All that said, i think this would be better suited in a tauri plugin, especially because of linux.

@betamos
Copy link
Author

betamos commented May 5, 2022

Thanks @FabianLars . Your solution would work even with the JS shell API as well right? I can try these out, and with a fallback (see below) this should work OK for linux as well.

All that said, i think this would be better suited in a tauri plugin

IIUC there's a broader discussion about small core (with an extensive plugin system) vs big functional core, which I will not weigh in on here :) Just for reference, here's some prior art that could be good to know:

Electron has a shell.showItemInFolder(fullPath) API, a quick-n-dirty github search shows 5-15k results, so it does at the very least appear popular.

Here's their Linux version:

https://github.com/electron/electron/blob/31c2b5703a75a5ff2bf0e68f472d9002fe866bfb/shell/common/platform_util_linux.cc#L68

So I guess the answer is it works, but not with a command only.

However, they have a sensible fallback (just open the directory without selecting anything). For my case, this fallback is certainly better than omitting the feature altogether.

@FabianLars
Copy link
Member

Your solution would work even with the JS shell API as well right?

Hopefully, yes.

IIUC there's a broader discussion about small core (with an extensive plugin system) vs big functional core

Well, i don't want to dig in that discussion either (although generally i think it makes sense to not integrate everything into the core), but for me the main selling point of plugins is that it can be implemented regardless of tauri's release cycle and feature-freeze.

Here's their Linux version:

Ah yeah, totally forgot about the dbus option (probably cause we don't use that anywhere yet), but of course this is a little bit more complicated 🤷

@lucasfernog
Copy link
Member

We use dbus indirectly in the notifications via notify-rust.

@AlexTMjugador
Copy link

AlexTMjugador commented Oct 25, 2022

A viable workaround command for Linux is dbus-send, which uses D-Bus as highlighted before to do the trick:

dbus-send --session --dest=org.freedesktop.FileManager1 --type=method_call \
/org/freedesktop/FileManager1 org.freedesktop.FileManager1.ShowItems \
array:string:"file:///path/to/file" string:""

This command was suggested by Eduardo Trápani at this Stack Exchange question.

The specification of the related D-Bus interface can be found here.

I think that it is worth noting that, at least on my Debian Unstable KDE desktop, which uses Dolphin 22.08.1 as a file manager, the command fails for paths that contain a comma because dbus-send interprets commas as path separators. Percent-encoding the comma does not work, as Dolphin does not interpret such escape sequences.

@elibroftw
Copy link

elibroftw commented Dec 5, 2022

https://gitlab.freedesktop.org/dbus/dbus/-/issues/76 there's even a patch to allow the change! I do wonder how electron/chromium does it. I tried to dig into their code base months ago and stopped as soon as I found out dbus was used in someway.

@elibroftw
Copy link

elibroftw commented Dec 5, 2022

Okay I wrote a generalized Tauri command (Rust 1.58+) with some leeway on Linux.

tauri command code
use std::fs;
#[cfg(target_os = "linux")]
use std::{fs::metadata, path::PathBuf};
use std::path::PathBuf;
use std::process::Command;
#[cfg(target_os = "linux")]
use fork::{daemon, Fork}; // dep: fork = "0.1"

#[tauri::command]
fn show_in_folder(path: String) {
  #[cfg(target_os = "windows")]
  {
    Command::new("explorer")
        .args(["/select,", &path]) // The comma after select is not a typo
        .spawn()
        .unwrap();
  }

  #[cfg(target_os = "linux")]
  {
    if path.contains(",") {
      // see https://gitlab.freedesktop.org/dbus/dbus/-/issues/76
      let new_path = match metadata(&path).unwrap().is_dir() {
        true => path,
        false => {
          let mut path2 = PathBuf::from(path);
          path2.pop();
          path2.into_os_string().into_string().unwrap()
        }
      };
      Command::new("xdg-open")
          .arg(&new_path)
          .spawn()
          .unwrap();
    } else {
      if let Ok(Fork::Child) = daemon(false, false) {
        Command::new("dbus-send")
            .args(["--session", "--dest=org.freedesktop.FileManager1", "--type=method_call",
                  "/org/freedesktop/FileManager1", "org.freedesktop.FileManager1.ShowItems",
                  format!("array:string:\"file://{path}\"").as_str(), "string:\"\""])
            .spawn()
            .unwrap();
      }
    }
  }

  #[cfg(target_os = "macos")]
  {
    Command::new("open")
        .args(["-R", &path])
        .spawn()
        .unwrap();
  }
}
main.rs main
tauri::Builder::default()
    .invoke_handler(tauri::generate_handler![YOUR_OTHER_DEFINED_CMDS, show_in_folder])
Client Side Usage
import { tauri } from '@tauri-apps/api';

async function show_in_folder(path) {
    await tauri.invoke('show_in_folder', {path});
}

@amrbashir
Copy link
Member

@elibroftw for the windows implementation, I generally dislike this method as it has a weird behavior of expanding the navigation pane. I personally prefer to use the raw win32 API ShellExecuteW with either open or explore.

@elibroftw
Copy link

elibroftw commented Dec 6, 2022

I've used this command multiple times with my music player and not once has it expanded the navigation panel. The github desktop implementation however always expands the navigation panel. Yes the native code is probably better but this is a hack.

@aleph1
Copy link

aleph1 commented Apr 20, 2023

@elibroftw, when I try to compile your code, I get the following error:

error: cannot find macro `__cmd__OTHER_CMDS` in this scope
.invoke_handler(tauri::generate_handler![OTHER_CMDS, show_in_folder]);

Any ideas?

@FabianLars
Copy link
Member

@aleph1 Remove OTHER_CMDS. This was just to show that this is where your own commands would be registered too.

@elibroftw
Copy link

elibroftw commented Feb 12, 2024

2024 Update.

I encountered an error using Command::new on Linux Mint, so here is the implementation via the dbus crate.

I'm going to try to create a PR to the Tauri shell API during reading week. The DX and UX can be better for everyone.

Cargo.toml
[target.'cfg(target_os = "linux")'.dependencies]
dbus = "0.9"
util_commands.rs

latest in my template repo

use std::process::Command;
// State is used by linux
use tauri::{Manager, State};

#[cfg(not(target_os = "windows"))]
use std::path::PathBuf;

#[cfg(target_os = "linux")]
use crate::DbusState;
#[cfg(target_os = "linux")]
use std::time::Duration;

#[cfg(target_os = "linux")]
#[tauri::command]
pub fn show_item_in_folder(path: String, dbus_state: State<DbusState>) -> Result<(), String> {
  let dbus_guard = dbus_state.0.lock().map_err(|e| e.to_string())?;

  // see https://gitlab.freedesktop.org/dbus/dbus/-/issues/76
  if dbus_guard.is_none() || path.contains(",") {
    let mut path_buf = PathBuf::from(&path);
    let new_path = match path_buf.is_dir() {
      true => path,
      false => {
        path_buf.pop();
        path_buf.into_os_string().into_string().unwrap()
      }
    };
    Command::new("xdg-open")
      .arg(&new_path)
      .spawn()
      .map_err(|e| format!("{e:?}"))?;
  } else {
    // https://docs.rs/dbus/latest/dbus/
    let dbus = dbus_guard.as_ref().unwrap();
    let proxy = dbus.with_proxy(
      "org.freedesktop.FileManager1",
      "/org/freedesktop/FileManager1",
      Duration::from_secs(5),
    );
    let (_,): (bool,) = proxy
      .method_call(
        "org.freedesktop.FileManager1",
        "ShowItems",
        (vec![format!("file://{path}")], ""),
      )
      .map_err(|e| e.to_string())?;
  }
 Ok(())
}

#[cfg(not(target_os = "linux"))]
#[tauri::command]
pub fn show_item_in_folder(path: String) -> Result<(), String> {
  #[cfg(target_os = "windows")]
  {
    Command::new("explorer")
      .args(["/select,", &path]) // The comma after select is not a typo
      .spawn()
      .map_err(|e| e.to_string())?;
  }

  #[cfg(target_os = "macos")]
  {
    let path_buf = PathBuf::from(&path);
    if path_buf.is_dir() {
      Command::new("open")
        .args([&path])
        .spawn()
        .map_err(|e| e.to_string())?;
    } else {
      Command::new("open")
        .args(["-R", &path])
        .spawn()
        .map_err(|e| e.to_string())?;
    }
  }
  Ok(())
}
main.rs
#[cfg(target_os = "linux")]
pub struct DbusState(Mutex<Option<dbus::blocking::SyncConnection>>);

mod show_in_folder;
use show_in_folder::{show_item_in_folder};

// in main ...
tauri::Builder::default()
    .invoke_handler(tauri::generate_handler![
      show_item_in_folder
    ])
    .setup(|app| {
        #[cfg(target_os = "linux")]
          app.manage(DbusState(Mutex::new(dbus::blocking::SyncConnection::new_session().ok())));
})
JavaScript Usage
import { tauri } from '@tauri-apps/api';

async function show_in_folder(path) {
    await tauri.invoke('show_in_folder', {path});
}

English OS localization strings I'm using in frontend. Looking for thoughts

"revealFile_win32": "Show in File Explorer",
"revealFile_darwin": "Reveal in Finder",
"revealFile_linux": "Show in folder",

@wxllow
Copy link

wxllow commented Feb 26, 2024

2024 Update.

I encountered an error using Command::new on Linux Mint, so here is the implementation via the dbus crate.

I'm going to try to create a PR to the Tauri shell API during reading week. The DX and UX can be better for everyone.

Cargo.toml
util_commands.rs
main.rs
JavaScript Usage
English OS localization strings I'm using in frontend. Looking for thoughts

"revealFile_win32": "Show in File Explorer",
"revealFile_darwin": "Reveal in Finder",
"revealFile_linux": "Show in folder",
  .method_call(
        "org.freedesktop.FileManager1",
        "ShowItems",
        (vec![path], ""),
      )

should be

       .method_call(
               "org.freedesktop.FileManager1",
               "ShowItems",
               (vec![format!("file://{}", path)], ""),
           )

@elibroftw
Copy link

I used to use file:// but still yet to find a case where it works with file:// but not without. I'll change it though.

@amrbashir amrbashir transferred this issue from tauri-apps/tauri Feb 27, 2024
@amrbashir amrbashir changed the title [feat] Show in finder/explorer [feat][shell] Show in finder/explorer Feb 27, 2024
@FabianLars FabianLars added the enhancement New feature or request label Feb 27, 2024
@FabianLars FabianLars changed the title [feat][shell] Show in finder/explorer [shell] Show in finder/explorer Feb 27, 2024
@wxllow
Copy link

wxllow commented Feb 28, 2024

I used to use file:// but still yet to find a case where it works with file:// but not without. I'll change it though.

Ah makes sense... For me, it doesn't work without the file:// on either Nautilus or Dolphin. I'm not really that familiar with how DBus works so im not sure why. But from looking other implementations of the call (such as electron's, and also these docs) ,it seems that file:// is the way to do it

@elibroftw
Copy link

Docs say it must be a URI so I was just playing around with if that was a hard requirement or not.

@thewh1teagle
Copy link
Contributor

For anyone who look for open file selected on Linux / Windows / macOS

you can use the crate showfile

Setup

cargo add showfile

Usage

fn main() {
  let path = "/some/path/to/file";
  showfile::show_path_in_file_manager(path);
}

It works with finder on macOS

explorer.exe on Windows

and Nautilus on Ubuntu (and possible more distros)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request plugin: shell
Projects
Status: 📬Proposal
Development

Successfully merging a pull request may close this issue.

9 participants