From 87eebbbf8c448022a1843575ec31f09409d32726 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 13 Mar 2026 10:57:16 -0500 Subject: [PATCH] refactor(shell): Pull out hyperlink logic into anstyle-hyperlink After seeing - annotate-snippets needing a `Hyperlink` - people directly editing Cargo for ansi escape code progress detection I figured it would be good to pull this general logic out. --- Cargo.lock | 13 ++++++- Cargo.toml | 2 + src/cargo/core/shell.rs | 66 ++++++-------------------------- src/cargo/util/hostname.rs | 77 -------------------------------------- src/cargo/util/mod.rs | 2 - 5 files changed, 26 insertions(+), 134 deletions(-) delete mode 100644 src/cargo/util/hostname.rs diff --git a/Cargo.lock b/Cargo.lock index 2fbf2a48511..b65445ae7ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -94,6 +94,16 @@ version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +[[package]] +name = "anstyle-hyperlink" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "251c63848f1535e0cc16db89fe57637274fadca43ab63b12324b2a0e839f17cb" +dependencies = [ + "libc", + "percent-encoding", +] + [[package]] name = "anstyle-lossy" version = "1.1.4" @@ -355,6 +365,7 @@ dependencies = [ "annotate-snippets", "anstream 1.0.0", "anstyle", + "anstyle-hyperlink", "anyhow", "base64", "blake3", @@ -4590,7 +4601,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys 0.61.2", diff --git a/Cargo.toml b/Cargo.toml index 3600cedde0f..dd2053dd1ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ repository = "https://github.com/rust-lang/cargo" annotate-snippets = { version = "0.12.12", features = ["simd"] } anstream = "1.0.0" anstyle = "1.0.13" +anstyle-hyperlink = "1.0.1" anyhow = "1.0.102" base64 = "0.22.1" blake3 = "1.8.3" @@ -159,6 +160,7 @@ path = "src/cargo/lib.rs" annotate-snippets.workspace = true anstream.workspace = true anstyle.workspace = true +anstyle-hyperlink = { workspace = true, features = ["file"] } anyhow.workspace = true base64.workspace = true blake3.workspace = true diff --git a/src/cargo/core/shell.rs b/src/cargo/core/shell.rs index 442b1ec4152..8d7ed28878a 100644 --- a/src/cargo/core/shell.rs +++ b/src/cargo/core/shell.rs @@ -8,9 +8,10 @@ use anstream::AutoStream; use anstyle::Style; use crate::util::errors::CargoResult; -use crate::util::hostname; use crate::util::style::*; +pub use anstyle_hyperlink::Hyperlink; + /// An abstraction around console output that remembers preferences for output /// verbosity and color. pub struct Shell { @@ -22,7 +23,6 @@ pub struct Shell { /// Flag that indicates the current line needs to be cleared before /// printing. Used when a progress bar is currently displayed. needs_clear: bool, - hostname: Option, } impl fmt::Debug for Shell { @@ -62,7 +62,6 @@ impl Shell { }, verbosity: Verbosity::Verbose, needs_clear: false, - hostname: None, } } @@ -72,7 +71,6 @@ impl Shell { output: ShellOut::Write(AutoStream::never(out)), // strip all formatting on write verbosity: Verbosity::Verbose, needs_clear: false, - hostname: None, } } @@ -334,8 +332,10 @@ impl Shell { stdout, hyperlinks, .. } => stdout.current_choice() == anstream::ColorChoice::AlwaysAnsi && *hyperlinks, }; - Hyperlink { - url: supports_hyperlinks.then_some(url), + if supports_hyperlinks { + Hyperlink::with_url(url) + } else { + Hyperlink::default() } } @@ -347,41 +347,22 @@ impl Shell { } => stderr.current_choice() == anstream::ColorChoice::AlwaysAnsi && *hyperlinks, }; if supports_hyperlinks { - Hyperlink { url: Some(url) } + Hyperlink::with_url(url) } else { - Hyperlink { url: None } + Hyperlink::default() } } - pub fn out_file_hyperlink(&mut self, path: &std::path::Path) -> Hyperlink { - let url = self.file_hyperlink(path); + pub fn out_file_hyperlink(&mut self, path: &std::path::Path) -> Hyperlink { + let url = anstyle_hyperlink::path_to_url(path); url.map(|u| self.out_hyperlink(u)).unwrap_or_default() } - pub fn err_file_hyperlink(&mut self, path: &std::path::Path) -> Hyperlink { - let url = self.file_hyperlink(path); + pub fn err_file_hyperlink(&mut self, path: &std::path::Path) -> Hyperlink { + let url = anstyle_hyperlink::path_to_url(path); url.map(|u| self.err_hyperlink(u)).unwrap_or_default() } - fn file_hyperlink(&mut self, path: &std::path::Path) -> Option { - let mut url = url::Url::from_file_path(path).ok()?; - // Do a best-effort of setting the host in the URL to avoid issues with opening a link - // scoped to the computer you've SSH'ed into - let hostname = if cfg!(windows) { - // Not supported correctly on windows - None - } else { - if let Some(hostname) = self.hostname.as_deref() { - Some(hostname) - } else { - self.hostname = hostname().ok().and_then(|h| h.into_string().ok()); - self.hostname.as_deref() - } - }; - let _ = url.set_host(hostname); - Some(url) - } - fn unstable_flags_rustc_unicode(&self) -> bool { match &self.output { ShellOut::Write(_) => false, @@ -692,29 +673,6 @@ mod tests { } } -pub struct Hyperlink { - url: Option, -} - -impl Default for Hyperlink { - fn default() -> Self { - Self { url: None } - } -} - -impl fmt::Display for Hyperlink { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Some(url) = self.url.as_ref() else { - return Ok(()); - }; - if f.alternate() { - write!(f, "\x1B]8;;\x1B\\") - } else { - write!(f, "\x1B]8;;{url}\x1B\\") - } - } -} - #[cfg(unix)] mod imp { use super::{Shell, TtyWidth}; diff --git a/src/cargo/util/hostname.rs b/src/cargo/util/hostname.rs deleted file mode 100644 index 3f53c9cf6db..00000000000 --- a/src/cargo/util/hostname.rs +++ /dev/null @@ -1,77 +0,0 @@ -// Copied from https://github.com/BurntSushi/ripgrep/blob/7099e174acbcbd940f57e4ab4913fee4040c826e/crates/cli/src/hostname.rs - -use std::{ffi::OsString, io}; - -/// Returns the hostname of the current system. -/// -/// It is unusual, although technically possible, for this routine to return -/// an error. It is difficult to list out the error conditions, but one such -/// possibility is platform support. -/// -/// # Platform specific behavior -/// -/// On Unix, this returns the result of the `gethostname` function from the -/// `libc` linked into the program. -pub fn hostname() -> io::Result { - #[cfg(unix)] - { - gethostname() - } - #[cfg(not(unix))] - { - Err(io::Error::new( - io::ErrorKind::Other, - "hostname could not be found on unsupported platform", - )) - } -} - -#[cfg(unix)] -fn gethostname() -> io::Result { - use std::os::unix::ffi::OsStringExt; - - // SAFETY: There don't appear to be any safety requirements for calling - // sysconf. - let limit = unsafe { libc::sysconf(libc::_SC_HOST_NAME_MAX) }; - if limit == -1 { - // It is in theory possible for sysconf to return -1 for a limit but - // *not* set errno, in which case, io::Error::last_os_error is - // indeterminate. But untangling that is super annoying because std - // doesn't expose any unix-specific APIs for inspecting the errno. (We - // could do it ourselves, but it just doesn't seem worth doing?) - return Err(io::Error::last_os_error()); - } - let Ok(maxlen) = usize::try_from(limit) else { - let msg = format!("host name max limit ({}) overflowed usize", limit); - return Err(io::Error::new(io::ErrorKind::Other, msg)); - }; - // maxlen here includes the NUL terminator. - let mut buf = vec![0; maxlen]; - // SAFETY: The pointer we give is valid as it is derived directly from a - // Vec. Similarly, `maxlen` is the length of our Vec, and is thus valid - // to write to. - let rc = unsafe { libc::gethostname(buf.as_mut_ptr().cast::(), maxlen) }; - if rc == -1 { - return Err(io::Error::last_os_error()); - } - // POSIX says that if the hostname is bigger than `maxlen`, then it may - // write a truncate name back that is not necessarily NUL terminated (wtf, - // lol). So if we can't find a NUL terminator, then just give up. - let Some(zeropos) = buf.iter().position(|&b| b == 0) else { - let msg = "could not find NUL terminator in hostname"; - return Err(io::Error::new(io::ErrorKind::Other, msg)); - }; - buf.truncate(zeropos); - buf.shrink_to_fit(); - Ok(OsString::from_vec(buf)) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn print_hostname() { - println!("{:?}", hostname()); - } -} diff --git a/src/cargo/util/mod.rs b/src/cargo/util/mod.rs index 260e719db79..926a36c9923 100644 --- a/src/cargo/util/mod.rs +++ b/src/cargo/util/mod.rs @@ -13,7 +13,6 @@ pub use self::flock::{FileLock, Filesystem}; pub use self::graph::Graph; pub use self::hasher::StableHasher; pub use self::hex::{hash_u64, short_hash, to_hex}; -pub use self::hostname::hostname; pub use self::into_url::IntoUrl; pub use self::into_url_with_base::IntoUrlWithBase; pub(crate) use self::io::LimitErrorReader; @@ -47,7 +46,6 @@ pub mod frontmatter; pub mod graph; mod hasher; pub mod hex; -mod hostname; pub mod important_paths; pub mod interning; pub mod into_url;