Skip to content

Commit

Permalink
Add hyperlink support to fd
Browse files Browse the repository at this point in the history
  • Loading branch information
tmccombs committed Jun 9, 2024
1 parent be815c2 commit bd649e2
Show file tree
Hide file tree
Showing 11 changed files with 133 additions and 1 deletion.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
# Upcoming release

## Features

- Add --hyperlink option to add OSC 8 hyperlinks to output


## Bugfixes


## Changes


## Other

# 10.1.0

## Features
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ default-features = false
features = ["nu-ansi-term"]

[target.'cfg(unix)'.dependencies]
nix = { version = "0.29.0", default-features = false, features = ["signal", "user"] }
nix = { version = "0.29.0", default-features = false, features = ["signal", "user", "hostname"] }

[target.'cfg(all(unix, not(target_os = "redox")))'.dependencies]
libc = "0.2"
Expand Down
2 changes: 2 additions & 0 deletions contrib/completion/_fd
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ _fd() {
always\:"always use colorized output"
))'

'--hyperlink[add hyperlinks to output paths]'

+ '(threads)'
{-j+,--threads=}'[set the number of threads for searching and executing]:number of threads'

Expand Down
6 changes: 6 additions & 0 deletions doc/fd.1
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,12 @@ Do not colorize output.
Always colorize output.
.RE
.TP
.B "\-\-hyperlink
Specify that the output should use terminal escape codes to indicate a hyperlink to a
file url pointing to the path.
Such hyperlinks will only actually be included if color output would be used, since
that is likely correlated with the output being used on a terminal.
.TP
.BI "\-j, \-\-threads " num
Set number of threads to use for searching & executing (default: number of available CPU cores).
.TP
Expand Down
7 changes: 7 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,13 @@ pub struct Opts {
)]
pub color: ColorWhen,

/// Add a terminal hyperlink to a file:// url for each path in the output.
///
/// This doesn't do anything for options that don't use the defualt output such as
/// --exec and --format.
#[arg(long, alias = "hyper", help = "Add hyperlinks to output paths")]
pub hyperlink: bool,

/// Set number of threads to use for searching & executing (default: number
/// of available CPU cores)
#[arg(long, short = 'j', value_name = "num", hide_short_help = true, value_parser = str::parse::<NonZeroUsize>)]
Expand Down
3 changes: 3 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ pub struct Config {

/// Whether or not to strip the './' prefix for search results
pub strip_cwd_prefix: bool,

/// Whether or not to use hyperlinks on paths
pub hyperlink: bool,
}

impl Config {
Expand Down
63 changes: 63 additions & 0 deletions src/hyperlink.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
use crate::filesystem::absolute_path;
use std::fmt::{self, Formatter, Write};
use std::path::{Path, PathBuf};
use std::sync::OnceLock;

pub(crate) struct PathUrl(PathBuf);

#[cfg(unix)]
static HOSTNAME: OnceLock<String> = OnceLock::new();

impl PathUrl {
pub(crate) fn new(path: &Path) -> Option<PathUrl> {
Some(PathUrl(absolute_path(path).ok()?))
}
}

impl fmt::Display for PathUrl {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "file://{}", host())?;
let bytes = self.0.as_os_str().as_encoded_bytes();
for &byte in bytes.iter() {
encode(f, byte)?;
}
Ok(())
}
}

fn encode(f: &mut Formatter, byte: u8) -> fmt::Result {
match byte {
b'0'..=b'9'
| b'A'..=b'Z'
| b'a'..=b'z'
| b'/'
| b':'
| b'-'
| b'.'
| b'_'
| b'~'
| 128.. => f.write_char(byte.into()),
#[cfg(windows)]
b'\\' => f.write_char('/'),
_ => {
write!(f, "%{:X}", byte)
}
}
}

#[cfg(unix)]
fn host() -> &'static str {
HOSTNAME
.get_or_init(|| {
nix::unistd::gethostname()
.ok()
.and_then(|h| h.into_string().ok())
.unwrap_or_default()
})
.as_ref()
}

#[cfg(not(unix))]
const fn host() -> &'static str {
""
}
2 changes: 2 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod filesystem;
mod filetypes;
mod filter;
mod fmt;
mod hyperlink;
mod output;
mod regex_helper;
mod walk;
Expand Down Expand Up @@ -258,6 +259,7 @@ fn construct_config(mut opts: Opts, pattern_regexps: &[String]) -> Result<Config
threads: opts.threads().get(),
max_buffer_time: opts.max_buffer_time,
ls_colors,
hyperlink: opts.hyperlink,
interactive_terminal,
file_types: opts.filetype.as_ref().map(|values| {
use crate::cli::FileType::*;
Expand Down
13 changes: 13 additions & 0 deletions src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::dir_entry::DirEntry;
use crate::error::print_error;
use crate::exit_codes::ExitCode;
use crate::fmt::FormatTemplate;
use crate::hyperlink::PathUrl;

fn replace_path_separator(path: &str, new_path_separator: &str) -> String {
path.replace(std::path::MAIN_SEPARATOR, new_path_separator)
Expand Down Expand Up @@ -83,9 +84,17 @@ fn print_entry_colorized<W: Write>(
) -> io::Result<()> {
// Split the path between the parent and the last component
let mut offset = 0;
let mut has_hyperlink = false;
let path = entry.stripped_path(config);
let path_str = path.to_string_lossy();

if config.hyperlink {
if let Some(url) = PathUrl::new(entry.path()) {
write!(stdout, "\x1B]8;;{}\x1B\\", url)?;
has_hyperlink = true;
}
}

if let Some(parent) = path.parent() {
offset = parent.to_string_lossy().len();
for c in path_str[offset..].chars() {
Expand Down Expand Up @@ -123,6 +132,10 @@ fn print_entry_colorized<W: Write>(
ls_colors.style_for_indicator(Indicator::Directory),
)?;

if has_hyperlink {
write!(stdout, "\x1B]8;;\x1B\\")?;
}

if config.null_separator {
write!(stdout, "\0")?;
} else {
Expand Down
3 changes: 3 additions & 0 deletions tests/testenv/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,9 @@ impl TestEnv {
} else {
cmd.arg("--no-global-ignore-file");
}
// Make sure LS_COLORS is unset to ensure consistent
// color output
cmd.env("LS_COLORS", "");
cmd.args(args);

// Run *fd*.
Expand Down
18 changes: 18 additions & 0 deletions tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2672,3 +2672,21 @@ fn test_gitignore_parent() {
te.assert_output_subdirectory("sub", &["--hidden"], "");
te.assert_output_subdirectory("sub", &["--hidden", "--search-path", "."], "");
}

#[test]
fn test_hyperlink() {
let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES);

#[cfg(unix)]
let hostname = nix::unistd::gethostname().unwrap().into_string().unwrap();
#[cfg(not(unix))]
let hostname = "";

let expected = format!(
"\x1b]8;;file://{}{}/a.foo\x1b\\a.foo\x1b]8;;\x1b\\",
hostname,
get_absolute_root_path(&te),
);

te.assert_output(&["--color=always", "--hyperlink", "a.foo"], &expected);
}

0 comments on commit bd649e2

Please sign in to comment.