Skip to content

Commit

Permalink
Reveal file in file explorer (Seeker14491#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
bash authored Mar 17, 2023
1 parent 3dfa4fd commit 5ddb52d
Show file tree
Hide file tree
Showing 11 changed files with 296 additions and 13 deletions.
12 changes: 6 additions & 6 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,27 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Build
run: cargo build --verbose
run: cargo build --all-features --verbose
- name: Run tests
run: cargo test --verbose
run: cargo test --all-features --verbose

windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- name: Build
run: cargo build --verbose
run: cargo build --all-features --verbose
- name: Run tests
run: cargo test --verbose
run: cargo test --all-features --verbose

mac:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- name: Build
run: cargo build --verbose
run: cargo build --all-features --verbose
- name: Run tests
run: cargo test --verbose
run: cargo test --all-features --verbose

clippy_check:
runs-on: ubuntu-latest
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

### Added

- `reveal()` function, which opens the system's file explorer with the specified file or directory selected. It
requires the "reveal" feature to be enabled.

## [0.5.2] - 2023-01-29

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion opener-bin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ authors = ["Brian Bowman <seeker14491@gmail.com>"]
edition = "2018"

[dependencies]
opener = { path = "../opener" }
opener = { path = "../opener", features = ["reveal"] }
structopt = "0.3.1"
8 changes: 7 additions & 1 deletion opener-bin/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,22 @@ struct Args {
#[structopt(parse(from_os_str))]
path: PathBuf,

/// Open the path with the `open_browser()` function instead of the `open` function
/// Open the path with the `open_browser()` function
#[structopt(long = "browser")]
browser: bool,

/// Reveal the file in the file explorer instead of opening it
#[structopt(long = "reveal", short = "R", conflicts_with = "browser")]
reveal: bool,
}

fn main() {
let args = Args::from_args();

let open_result = if args.browser {
opener::open_browser(&args.path)
} else if args.reveal {
opener::reveal(&args.path)
} else {
opener::open(&args.path)
};
Expand Down
16 changes: 16 additions & 0 deletions opener/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,27 @@ appveyor = { repository = "Seeker14491/opener", branch = "master", service = "gi
travis-ci = { repository = "Seeker14491/opener", branch = "master" }
maintenance = { status = "passively-maintained" }

[features]
reveal = [
"dep:url",
"dep:dbus",
"dep:dunce",
"winapi/shtypes",
"winapi/objbase",
]

[dev-dependencies]
version-sync = "0.9"

[target.'cfg(target_os = "linux")'.dependencies]
bstr = "1"
dbus = { version = "0.9", optional = true, features = ["vendored"] }
url = { version = "2", optional = true }

[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3", features = ["shellapi"] }
dunce = { version = "1", optional = true }

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
93 changes: 93 additions & 0 deletions opener/src/freedesktop.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//! When working on this, there are a couple of things to test:
//! * Works in Flatpak packages (in Flatpak packages the OpenURI interface is used,
//! because FileManager1 is not available)
//! * Works with relative file paths
//! * Works with directories (and highlights them)
//! * Weird paths work: paths with spaces, unicode characters, non-unicode characters (e.g. `"\u{01}"`)
//! * Path to non-existent file generates an error for both implementations.
use crate::OpenError;
use dbus::arg::messageitem::MessageItem;
use dbus::arg::{Append, Variant};
use dbus::blocking::Connection;
use std::fs::File;
use std::path::Path;
use std::time::Duration;
use std::{error, fmt, io};
use url::Url;

const DBUS_TIMEOUT: Duration = Duration::from_secs(5);

// We should prefer the OpenURI interface, because it correctly handles runtimes such as Flatpak.
// However, OpenURI was broken in the original version of the interface (it did not highlight the items).
// This version is still in use by some distributions, which would result in degraded functionality for some users.
// That's why we're first trying to use the FileManager1 interface, falling back to the OpenURI interface.
// Source: https://chromium-review.googlesource.com/c/chromium/src/+/3009959
pub(crate) fn reveal_with_dbus(path: &Path) -> Result<(), OpenError> {
let connection = Connection::new_session().map_err(dbus_to_open_error)?;
reveal_with_filemanager1(path, &connection)
.or_else(|_| reveal_with_open_uri_portal(path, &connection))
}

fn reveal_with_filemanager1(path: &Path, connection: &Connection) -> Result<(), OpenError> {
let uri = path_to_uri(path)?;
let proxy = connection.with_proxy(
"org.freedesktop.FileManager1",
"/org/freedesktop/FileManager1",
DBUS_TIMEOUT,
);
proxy
.method_call(
"org.freedesktop.FileManager1",
"ShowItems",
(vec![uri.as_str()], ""),
)
.map_err(dbus_to_open_error)
}

fn reveal_with_open_uri_portal(path: &Path, connection: &Connection) -> Result<(), OpenError> {
let file = File::open(path).map_err(OpenError::Io)?;
let proxy = connection.with_proxy(
"org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop",
DBUS_TIMEOUT,
);
proxy
.method_call(
"org.freedesktop.portal.OpenURI",
"OpenDirectory",
("", file, empty_vardict()),
)
.map_err(dbus_to_open_error)
}

fn empty_vardict() -> impl Append {
dbus::arg::Dict::<&'static str, Variant<MessageItem>, _>::new(std::iter::empty())
}

fn path_to_uri(path: &Path) -> Result<Url, OpenError> {
let path = path.canonicalize().map_err(OpenError::Io)?;
Url::from_file_path(path).map_err(|_| uri_to_open_error())
}

fn uri_to_open_error() -> OpenError {
OpenError::Io(io::Error::new(
io::ErrorKind::InvalidInput,
FilePathToUriError,
))
}

fn dbus_to_open_error(error: dbus::Error) -> OpenError {
OpenError::Io(io::Error::new(io::ErrorKind::Other, error))
}

#[derive(Debug)]
struct FilePathToUriError;

impl fmt::Display for FilePathToUriError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "The given file path could not be converted to a URI")
}
}

impl error::Error for FilePathToUriError {}
25 changes: 25 additions & 0 deletions opener/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#![doc(html_root_url = "https://docs.rs/opener/0.5.2")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]

//! This crate provides the [`open`] function, which opens a file or link with the default program
//! configured on the system:
Expand Down Expand Up @@ -26,6 +27,8 @@
unused_qualifications
)]

#[cfg(all(feature = "reveal", target_os = "linux"))]
mod freedesktop;
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
mod linux_and_more;
#[cfg(target_os = "macos")]
Expand Down Expand Up @@ -107,6 +110,28 @@ where
}
}

/// Opens the default file explorer and reveals a file or folder in its containing folder.
///
/// ## Errors
/// This function may or may not return an error if the path does not exist.
///
/// ## Platform Implementation Details
/// - On Windows and Windows Subsystem for Linux (WSL) the `explorer.exe /select, <path>` command is used.
/// - On Mac the system `open -R` command is used.
/// - On non-WSL Linux the [`file-manager-interface`] or the [`org.freedesktop.portal.OpenURI`] DBus Interface is used if available,
/// falling back to opening the containing folder with [`open`].
/// - On other platforms, the containing folder is shown with [`open`].
///
/// [`org.freedesktop.portal.OpenURI`]: https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.OpenURI
/// [`file-manager-interface`]: https://freedesktop.org/wiki/Specifications/file-manager-interface/
#[cfg(feature = "reveal")]
pub fn reveal<P>(path: P) -> Result<(), OpenError>
where
P: AsRef<std::path::Path>,
{
sys::reveal(path.as_ref())
}

/// An error type representing the failure to open a path. Possibly returned by the [`open`]
/// function.
///
Expand Down
51 changes: 46 additions & 5 deletions opener/src/linux_and_more.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use crate::OpenError;
use std::ffi::OsStr;
use std::io;
use std::io::Write;
use std::process::{Child, Command, Stdio};
use std::{fs, io};

const XDG_OPEN_SCRIPT: &[u8] = include_bytes!("xdg-open");

Expand All @@ -14,6 +14,27 @@ pub(crate) fn open(path: &OsStr) -> Result<(), OpenError> {
}
}

#[cfg(all(feature = "reveal", target_os = "linux"))]
pub(crate) fn reveal(path: &std::path::Path) -> Result<(), OpenError> {
if crate::is_wsl() {
reveal_in_windows_explorer(path)
} else {
crate::freedesktop::reveal_with_dbus(path).or_else(|_| reveal_fallback(path))
}
}

#[cfg(all(feature = "reveal", not(target_os = "linux")))]
pub(crate) fn reveal(path: &std::path::Path) -> Result<(), OpenError> {
reveal_fallback(path)
}

#[cfg(feature = "reveal")]
fn reveal_fallback(path: &std::path::Path) -> Result<(), OpenError> {
let path = path.canonicalize().map_err(OpenError::Io)?;
let parent = path.parent().unwrap_or(std::path::Path::new("/"));
open(parent.as_os_str())
}

fn wsl_open(path: &OsStr) -> Result<(), OpenError> {
let result = open_with_wslview(path);
if let Ok(mut child) = result {
Expand Down Expand Up @@ -77,18 +98,37 @@ fn open_with_internal_xdg_open(path: &OsStr) -> Result<Child, OpenError> {
Ok(sh)
}

#[cfg(all(feature = "reveal", target_os = "linux"))]
fn reveal_in_windows_explorer(path: &std::path::Path) -> Result<(), OpenError> {
let converted_path = crate::wsl_to_windows_path(path.as_os_str());
let converted_path = converted_path.as_deref();
let path = match converted_path {
None => path,
Some(x) => std::path::Path::new(x),
};
Command::new("explorer.exe")
.arg("/select,")
.arg(path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map_err(OpenError::Io)?;
Ok(())
}

#[cfg(target_os = "linux")]
pub(crate) fn is_wsl() -> bool {
if is_docker() {
return false;
}

if let Ok(true) = fs::read_to_string("/proc/sys/kernel/osrelease")
if let Ok(true) = std::fs::read_to_string("/proc/sys/kernel/osrelease")
.map(|osrelease| osrelease.to_ascii_lowercase().contains("microsoft"))
{
return true;
}

if let Ok(true) = fs::read_to_string("/proc/version")
if let Ok(true) = std::fs::read_to_string("/proc/version")
.map(|version| version.to_ascii_lowercase().contains("microsoft"))
{
return true;
Expand All @@ -97,10 +137,11 @@ pub(crate) fn is_wsl() -> bool {
false
}

#[cfg(target_os = "linux")]
fn is_docker() -> bool {
let has_docker_env = fs::metadata("/.dockerenv").is_ok();
let has_docker_env = std::fs::metadata("/.dockerenv").is_ok();

let has_docker_cgroup = fs::read_to_string("/proc/self/cgroup")
let has_docker_cgroup = std::fs::read_to_string("/proc/self/cgroup")
.map(|cgroup| cgroup.to_ascii_lowercase().contains("docker"))
.unwrap_or(false);

Expand Down
15 changes: 15 additions & 0 deletions opener/src/macos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,18 @@ pub(crate) fn open(path: &OsStr) -> Result<(), OpenError> {

crate::wait_child(&mut open, "open")
}

#[cfg(feature = "reveal")]
pub(crate) fn reveal(path: &std::path::Path) -> Result<(), OpenError> {
let mut open = Command::new("open")
.arg("-R")
.arg("--")
.arg(path)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn()
.map_err(OpenError::Io)?;

crate::wait_child(&mut open, "open")
}
5 changes: 5 additions & 0 deletions opener/src/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ use std::{io, ptr};
use winapi::ctypes::c_int;
use winapi::um::shellapi::ShellExecuteW;

#[cfg(feature = "reveal")]
mod reveal;
#[cfg(feature = "reveal")]
pub(crate) use self::reveal::reveal;

pub(crate) fn open(path: &OsStr) -> Result<(), OpenError> {
const SW_SHOW: c_int = 5;

Expand Down
Loading

0 comments on commit 5ddb52d

Please sign in to comment.