diff --git a/crates/uv/src/commands/help.rs b/crates/uv/src/commands/help.rs index 3a6c0f56b781..60837eae6392 100644 --- a/crates/uv/src/commands/help.rs +++ b/crates/uv/src/commands/help.rs @@ -1,4 +1,6 @@ -use std::ffi::OsStr; +use std::ffi::OsString; +use std::path::PathBuf; +use std::str::FromStr; use std::{fmt::Display, fmt::Write}; use anstream::{stream::IsTerminal, ColorChoice}; @@ -71,13 +73,16 @@ pub(crate) fn help(query: &[String], printer: Printer, no_pager: bool) -> Result let should_page = !no_pager && !is_root && is_terminal; if should_page { - if let Ok(less) = which("less") { - // When using less, we use the command name as the file name and can support colors - let prompt = format!("help: uv {}", query.join(" ")); - spawn_pager(less, &["-R", "-P", &prompt], &help_ansi)?; - } else if let Ok(more) = which("more") { - // When using more, we skip the ANSI color codes - spawn_pager(more, &[], &help)?; + if let Some(pager) = Pager::try_from_env() { + let content = if pager.supports_colors() { + help_ansi + } else { + Either::Right(help.clone()) + }; + pager.spawn( + format!("{}: {}", "uv help".bold(), query.join(" ")), + &content, + )?; } else { writeln!(printer.stdout(), "{help_ansi}")?; } @@ -103,25 +108,141 @@ fn find_command<'a>( find_command(&query[1..], subcommand) } -/// Spawn a paging command to display contents. -fn spawn_pager(command: impl AsRef, args: &[&str], contents: impl Display) -> Result<()> { - use std::io::Write; +#[derive(Debug)] +enum PagerKind { + Less, + More, + Other(String), +} + +#[derive(Debug)] +struct Pager { + kind: PagerKind, + args: Vec, + path: Option, +} + +impl PagerKind { + fn default_args(&self, prompt: String) -> Vec { + match self { + Self::Less => vec!["-R".to_string(), "-P".to_string(), prompt], + Self::More => vec![], + Self::Other(_) => vec![], + } + } +} + +impl std::fmt::Display for PagerKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Less => write!(f, "less"), + Self::More => write!(f, "more"), + Self::Other(name) => write!(f, "{name}"), + } + } +} + +impl FromStr for Pager { + type Err = (); + + fn from_str(s: &str) -> Result { + let mut split = s.split_ascii_whitespace(); + + // Empty string + let Some(first) = split.next() else { + return Err(()); + }; + + match first { + "less" => Ok(Self { + kind: PagerKind::Less, + args: split.map(str::to_string).collect(), + path: None, + }), + "more" => Ok(Self { + kind: PagerKind::More, + args: split.map(str::to_string).collect(), + path: None, + }), + _ => Ok(Self { + kind: PagerKind::Other(first.to_string()), + args: split.map(str::to_string).collect(), + path: None, + }), + } + } +} + +impl Pager { + /// Display `contents` using the pager. + fn spawn(self, prompt: String, contents: impl Display) -> Result<()> { + use std::io::Write; + + let command = self + .path + .as_ref() + .map(|path| path.as_os_str().to_os_string()) + .unwrap_or(OsString::from(self.kind.to_string())); + + let args = if self.args.is_empty() { + self.kind.default_args(prompt) + } else { + self.args + }; - let mut child = std::process::Command::new(command) - .args(args) - .stdin(std::process::Stdio::piped()) - .spawn()?; + let mut child = std::process::Command::new(command) + .args(args) + .stdin(std::process::Stdio::piped()) + .spawn()?; - let mut stdin = child - .stdin - .take() - .ok_or_else(|| anyhow!("Failed to take child process stdin"))?; + let mut stdin = child + .stdin + .take() + .ok_or_else(|| anyhow!("Failed to take child process stdin"))?; - let contents = contents.to_string(); - let writer = std::thread::spawn(move || stdin.write_all(contents.as_bytes())); + let contents = contents.to_string(); + let writer = std::thread::spawn(move || stdin.write_all(contents.as_bytes())); - drop(child.wait()); - drop(writer.join()); + drop(child.wait()); + drop(writer.join()); - Ok(()) + Ok(()) + } + + /// Get a pager to use and its path, if available. + /// + /// Supports the `PAGER` environment variable, otherwise checks for `less` and `more` in the + /// search path. + fn try_from_env() -> Option { + if let Some(pager) = std::env::var_os("PAGER") { + if !pager.is_empty() { + return Pager::from_str(&pager.to_string_lossy()).ok(); + } + } + + if let Ok(less) = which("less") { + Some(Pager { + kind: PagerKind::Less, + args: vec![], + path: Some(less), + }) + } else if let Ok(more) = which("more") { + Some(Pager { + kind: PagerKind::More, + args: vec![], + path: Some(more), + }) + } else { + None + } + } + + fn supports_colors(&self) -> bool { + match self.kind { + // The `-R` flag is required for color support. We will provide it by default. + PagerKind::Less => self.args.is_empty() || self.args.iter().any(|arg| arg == "-R"), + PagerKind::More => false, + PagerKind::Other(_) => false, + } + } }