mirror of
https://github.com/sharkdp/fd
synced 2024-09-30 04:55:28 +00:00
parent
be815c261a
commit
bd649e2fd7
15
CHANGELOG.md
15
CHANGELOG.md
|
@ -1,3 +1,18 @@
|
|||
# Upcoming release
|
||||
|
||||
## Features
|
||||
|
||||
- Add --hyperlink option to add OSC 8 hyperlinks to output
|
||||
|
||||
|
||||
## Bugfixes
|
||||
|
||||
|
||||
## Changes
|
||||
|
||||
|
||||
## Other
|
||||
|
||||
# 10.1.0
|
||||
|
||||
## Features
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
6
doc/fd.1
vendored
6
doc/fd.1
vendored
|
@ -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
|
||||
|
|
|
@ -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>)]
|
||||
|
|
|
@ -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 {
|
||||
|
|
63
src/hyperlink.rs
Normal file
63
src/hyperlink.rs
Normal file
|
@ -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 {
|
||||
""
|
||||
}
|
|
@ -8,6 +8,7 @@ mod filesystem;
|
|||
mod filetypes;
|
||||
mod filter;
|
||||
mod fmt;
|
||||
mod hyperlink;
|
||||
mod output;
|
||||
mod regex_helper;
|
||||
mod walk;
|
||||
|
@ -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::*;
|
||||
|
|
|
@ -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)
|
||||
|
@ -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() {
|
||||
|
@ -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 {
|
||||
|
|
|
@ -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*.
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue