mirror of
https://github.com/sharkdp/fd.git
synced 2024-09-19 00:41:29 +02: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
|
# 10.1.0
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
@ -65,7 +65,7 @@ default-features = false
|
|||||||
features = ["nu-ansi-term"]
|
features = ["nu-ansi-term"]
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[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]
|
[target.'cfg(all(unix, not(target_os = "redox")))'.dependencies]
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
|
@ -139,6 +139,8 @@ _fd() {
|
|||||||
always\:"always use colorized output"
|
always\:"always use colorized output"
|
||||||
))'
|
))'
|
||||||
|
|
||||||
|
'--hyperlink[add hyperlinks to output paths]'
|
||||||
|
|
||||||
+ '(threads)'
|
+ '(threads)'
|
||||||
{-j+,--threads=}'[set the number of threads for searching and executing]:number of 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.
|
Always colorize output.
|
||||||
.RE
|
.RE
|
||||||
.TP
|
.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
|
.BI "\-j, \-\-threads " num
|
||||||
Set number of threads to use for searching & executing (default: number of available CPU cores).
|
Set number of threads to use for searching & executing (default: number of available CPU cores).
|
||||||
.TP
|
.TP
|
||||||
|
@ -509,6 +509,13 @@ pub struct Opts {
|
|||||||
)]
|
)]
|
||||||
pub color: ColorWhen,
|
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
|
/// Set number of threads to use for searching & executing (default: number
|
||||||
/// of available CPU cores)
|
/// of available CPU cores)
|
||||||
#[arg(long, short = 'j', value_name = "num", hide_short_help = true, value_parser = str::parse::<NonZeroUsize>)]
|
#[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
|
/// Whether or not to strip the './' prefix for search results
|
||||||
pub strip_cwd_prefix: bool,
|
pub strip_cwd_prefix: bool,
|
||||||
|
|
||||||
|
/// Whether or not to use hyperlinks on paths
|
||||||
|
pub hyperlink: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
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 filetypes;
|
||||||
mod filter;
|
mod filter;
|
||||||
mod fmt;
|
mod fmt;
|
||||||
|
mod hyperlink;
|
||||||
mod output;
|
mod output;
|
||||||
mod regex_helper;
|
mod regex_helper;
|
||||||
mod walk;
|
mod walk;
|
||||||
@ -258,6 +259,7 @@ fn construct_config(mut opts: Opts, pattern_regexps: &[String]) -> Result<Config
|
|||||||
threads: opts.threads().get(),
|
threads: opts.threads().get(),
|
||||||
max_buffer_time: opts.max_buffer_time,
|
max_buffer_time: opts.max_buffer_time,
|
||||||
ls_colors,
|
ls_colors,
|
||||||
|
hyperlink: opts.hyperlink,
|
||||||
interactive_terminal,
|
interactive_terminal,
|
||||||
file_types: opts.filetype.as_ref().map(|values| {
|
file_types: opts.filetype.as_ref().map(|values| {
|
||||||
use crate::cli::FileType::*;
|
use crate::cli::FileType::*;
|
||||||
|
@ -8,6 +8,7 @@ use crate::dir_entry::DirEntry;
|
|||||||
use crate::error::print_error;
|
use crate::error::print_error;
|
||||||
use crate::exit_codes::ExitCode;
|
use crate::exit_codes::ExitCode;
|
||||||
use crate::fmt::FormatTemplate;
|
use crate::fmt::FormatTemplate;
|
||||||
|
use crate::hyperlink::PathUrl;
|
||||||
|
|
||||||
fn replace_path_separator(path: &str, new_path_separator: &str) -> String {
|
fn replace_path_separator(path: &str, new_path_separator: &str) -> String {
|
||||||
path.replace(std::path::MAIN_SEPARATOR, new_path_separator)
|
path.replace(std::path::MAIN_SEPARATOR, new_path_separator)
|
||||||
@ -83,9 +84,17 @@ fn print_entry_colorized<W: Write>(
|
|||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
// Split the path between the parent and the last component
|
// Split the path between the parent and the last component
|
||||||
let mut offset = 0;
|
let mut offset = 0;
|
||||||
|
let mut has_hyperlink = false;
|
||||||
let path = entry.stripped_path(config);
|
let path = entry.stripped_path(config);
|
||||||
let path_str = path.to_string_lossy();
|
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() {
|
if let Some(parent) = path.parent() {
|
||||||
offset = parent.to_string_lossy().len();
|
offset = parent.to_string_lossy().len();
|
||||||
for c in path_str[offset..].chars() {
|
for c in path_str[offset..].chars() {
|
||||||
@ -123,6 +132,10 @@ fn print_entry_colorized<W: Write>(
|
|||||||
ls_colors.style_for_indicator(Indicator::Directory),
|
ls_colors.style_for_indicator(Indicator::Directory),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
if has_hyperlink {
|
||||||
|
write!(stdout, "\x1B]8;;\x1B\\")?;
|
||||||
|
}
|
||||||
|
|
||||||
if config.null_separator {
|
if config.null_separator {
|
||||||
write!(stdout, "\0")?;
|
write!(stdout, "\0")?;
|
||||||
} else {
|
} else {
|
||||||
|
@ -316,6 +316,9 @@ impl TestEnv {
|
|||||||
} else {
|
} else {
|
||||||
cmd.arg("--no-global-ignore-file");
|
cmd.arg("--no-global-ignore-file");
|
||||||
}
|
}
|
||||||
|
// Make sure LS_COLORS is unset to ensure consistent
|
||||||
|
// color output
|
||||||
|
cmd.env("LS_COLORS", "");
|
||||||
cmd.args(args);
|
cmd.args(args);
|
||||||
|
|
||||||
// Run *fd*.
|
// Run *fd*.
|
||||||
|
@ -2672,3 +2672,21 @@ fn test_gitignore_parent() {
|
|||||||
te.assert_output_subdirectory("sub", &["--hidden"], "");
|
te.assert_output_subdirectory("sub", &["--hidden"], "");
|
||||||
te.assert_output_subdirectory("sub", &["--hidden", "--search-path", "."], "");
|
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 New Issue
Block a user