diff --git a/Cargo.lock b/Cargo.lock index bf638b421a..5060da4411 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2618,6 +2618,7 @@ dependencies = [ "chrono", "clap", "glob", + "hostname", "lscolors", "number_prefix", "once_cell", diff --git a/Cargo.toml b/Cargo.toml index 14e700ee0a..13b9970085 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -284,6 +284,7 @@ fundu = "2.0.0" gcd = "2.3" glob = "0.3.1" half = "2.3" +hostname = "0.3" indicatif = "0.17" itertools = "0.12.0" libc = "0.2.150" diff --git a/src/uu/hostname/Cargo.toml b/src/uu/hostname/Cargo.toml index a9b033d123..1fe1017096 100644 --- a/src/uu/hostname/Cargo.toml +++ b/src/uu/hostname/Cargo.toml @@ -16,7 +16,7 @@ path = "src/hostname.rs" [dependencies] clap = { workspace = true } -hostname = { version = "0.3", features = ["set"] } +hostname = { workspace = true, features = ["set"] } uucore = { workspace = true, features = ["wide"] } [target.'cfg(target_os = "windows")'.dependencies] diff --git a/src/uu/ls/Cargo.toml b/src/uu/ls/Cargo.toml index 96cf7df1a0..a82a1f37e0 100644 --- a/src/uu/ls/Cargo.toml +++ b/src/uu/ls/Cargo.toml @@ -31,6 +31,7 @@ uucore = { workspace = true, features = [ ] } once_cell = { workspace = true } selinux = { workspace = true, optional = true } +hostname = { workspace = true } [[bin]] name = "ls" diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index c6b10677c6..deb8aac3df 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -155,6 +155,7 @@ pub mod options { pub static GROUP_DIRECTORIES_FIRST: &str = "group-directories-first"; pub static ZERO: &str = "zero"; pub static DIRED: &str = "dired"; + pub static HYPERLINK: &str = "hyperlink"; } const DEFAULT_TERM_WIDTH: u16 = 80; @@ -418,6 +419,7 @@ pub struct Config { group_directories_first: bool, line_ending: LineEnding, dired: bool, + hyperlink: bool, } // Fields that can be removed or added to the long format @@ -566,6 +568,25 @@ fn extract_color(options: &clap::ArgMatches) -> bool { } } +/// Extracts the hyperlink option to use based on the options provided. +/// +/// # Returns +/// +/// A boolean representing whether to hyperlink files. +fn extract_hyperlink(options: &clap::ArgMatches) -> bool { + let hyperlink = options + .get_one::(options::HYPERLINK) + .unwrap() + .as_str(); + + match hyperlink { + "always" | "yes" | "force" => true, + "auto" | "tty" | "if-tty" => std::io::stdout().is_terminal(), + "never" | "no" | "none" => false, + _ => unreachable!("should be handled by clap"), + } +} + /// Extracts the quoting style to use based on the options provided. /// /// # Arguments @@ -736,10 +757,9 @@ impl Config { } let sort = extract_sort(options); - let time = extract_time(options); - let mut needs_color = extract_color(options); + let hyperlink = extract_hyperlink(options); let opt_block_size = options.get_one::(options::size::BLOCK_SIZE); let opt_si = opt_block_size.is_some() @@ -1020,6 +1040,7 @@ impl Config { group_directories_first: options.get_flag(options::GROUP_DIRECTORIES_FIRST), line_ending: LineEnding::from_zero_flag(options.get_flag(options::ZERO)), dired, + hyperlink, }) } } @@ -1154,6 +1175,19 @@ pub fn uu_app() -> Command { .help("generate output designed for Emacs' dired (Directory Editor) mode") .action(ArgAction::SetTrue), ) + .arg( + Arg::new(options::HYPERLINK) + .long(options::HYPERLINK) + .help("hyperlink file names WHEN") + .value_parser([ + "always", "yes", "force", "auto", "tty", "if-tty", "never", "no", "none", + ]) + .require_equals(true) + .num_args(0..=1) + .default_missing_value("always") + .default_value("never") + .value_name("WHEN"), + ) // The next four arguments do not override with the other format // options, see the comment in Config::from for the reason. // Ideally, they would use Arg::override_with, with their own name @@ -2959,6 +2993,18 @@ fn display_file_name( // infer it because the color codes mess up term_grid's width calculation. let mut width = name.width(); + if config.hyperlink { + let hostname = hostname::get().unwrap_or(OsString::from("")); + let hostname = hostname.to_string_lossy(); + + let absolute_path = fs::canonicalize(&path.p_buf).unwrap_or_default(); + let absolute_path = absolute_path.to_string_lossy(); + + // TODO encode path + // \x1b = ESC, \x07 = BEL + name = format!("\x1b]8;;file://{hostname}{absolute_path}\x07{name}\x1b]8;;\x07"); + } + if let Some(ls_colors) = &config.color { let md = path.md(out); name = if md.is_some() { diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index fcd57170d4..8bc2b75ac7 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -3855,3 +3855,33 @@ fn test_posixly_correct() { .succeeds() .stdout_contains_line("total 8"); } + +#[test] +fn test_ls_hyperlink() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + let file = "a.txt"; + + at.touch(file); + + let path = at.root_dir_resolved(); + let separator = std::path::MAIN_SEPARATOR_STR; + + let result = scene.ucmd().arg("--hyperlink").succeeds(); + assert!(result.stdout_str().contains("\x1b]8;;file://")); + assert!(result + .stdout_str() + .contains(&format!("{path}{separator}{file}\x07{file}\x1b]8;;\x07"))); + + let result = scene.ucmd().arg("--hyperlink=always").succeeds(); + assert!(result.stdout_str().contains("\x1b]8;;file://")); + assert!(result + .stdout_str() + .contains(&format!("{path}{separator}{file}\x07{file}\x1b]8;;\x07"))); + + scene + .ucmd() + .arg("--hyperlink=never") + .succeeds() + .stdout_is(format!("{file}\n")); +}