From 25156a4a96e48eabdbd8e7b2d7477f53e04fa4fb Mon Sep 17 00:00:00 2001 From: Krishnan Chandra Date: Sat, 27 Jul 2024 17:08:32 -0400 Subject: [PATCH 1/9] Respect PAGER env var in help --- crates/uv/src/commands/help.rs | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/crates/uv/src/commands/help.rs b/crates/uv/src/commands/help.rs index 3a6c0f56b781..dfb4a6a47601 100644 --- a/crates/uv/src/commands/help.rs +++ b/crates/uv/src/commands/help.rs @@ -71,15 +71,31 @@ 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)?; - } else { - writeln!(printer.stdout(), "{help_ansi}")?; + match std::env::var_os("PAGER") { + Some(pager) if !pager.is_empty() => { + // When using a pager, we use the command name as the file name and can support colors + let prompt = format!("help: uv {}", query.join(" ")); + match pager.to_str() { + Some("less") => spawn_pager(&pager, &["-R", "-P", &prompt], &help_ansi)?, + Some("more") => spawn_pager(&pager, &[], &help)?, + Some(_) => spawn_pager(&pager, &[], &help)?, + None => { + writeln!(printer.stdout(), "{help_ansi}")?; + } + } + } + _ => { + 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)?; + } else { + writeln!(printer.stdout(), "{help_ansi}")?; + } + } } } else { writeln!(printer.stdout(), "{help_ansi}")?; From 76a37f1ac22be49fd989ae0be1c713cad67fc8cf Mon Sep 17 00:00:00 2001 From: Krishnan Chandra Date: Sat, 27 Jul 2024 17:17:15 -0400 Subject: [PATCH 2/9] Change behavior when PAGER var is blank --- crates/uv/src/commands/help.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/uv/src/commands/help.rs b/crates/uv/src/commands/help.rs index dfb4a6a47601..790c63bf1d36 100644 --- a/crates/uv/src/commands/help.rs +++ b/crates/uv/src/commands/help.rs @@ -72,14 +72,14 @@ pub(crate) fn help(query: &[String], printer: Printer, no_pager: bool) -> Result if should_page { match std::env::var_os("PAGER") { - Some(pager) if !pager.is_empty() => { + Some(pager) => { // When using a pager, we use the command name as the file name and can support colors let prompt = format!("help: uv {}", query.join(" ")); match pager.to_str() { Some("less") => spawn_pager(&pager, &["-R", "-P", &prompt], &help_ansi)?, Some("more") => spawn_pager(&pager, &[], &help)?, - Some(_) => spawn_pager(&pager, &[], &help)?, - None => { + Some(x) if !x.is_empty() => spawn_pager(&pager, &[], &help)?, + _ => { writeln!(printer.stdout(), "{help_ansi}")?; } } From 5ac3b38609a66eded187a9ee4185565b38076f2c Mon Sep 17 00:00:00 2001 From: Krishnan Chandra Date: Sat, 27 Jul 2024 17:19:15 -0400 Subject: [PATCH 3/9] Move var initialization --- crates/uv/src/commands/help.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/uv/src/commands/help.rs b/crates/uv/src/commands/help.rs index 790c63bf1d36..61fc13cb8f43 100644 --- a/crates/uv/src/commands/help.rs +++ b/crates/uv/src/commands/help.rs @@ -74,9 +74,11 @@ pub(crate) fn help(query: &[String], printer: Printer, no_pager: bool) -> Result match std::env::var_os("PAGER") { Some(pager) => { // When using a pager, we use the command name as the file name and can support colors - let prompt = format!("help: uv {}", query.join(" ")); match pager.to_str() { - Some("less") => spawn_pager(&pager, &["-R", "-P", &prompt], &help_ansi)?, + Some("less") => { + let prompt = format!("help: uv {}", query.join(" ")); + spawn_pager(&pager, &["-R", "-P", &prompt], &help_ansi)? + } Some("more") => spawn_pager(&pager, &[], &help)?, Some(x) if !x.is_empty() => spawn_pager(&pager, &[], &help)?, _ => { @@ -84,7 +86,7 @@ pub(crate) fn help(query: &[String], printer: Printer, no_pager: bool) -> Result } } } - _ => { + None => { 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(" ")); From 4f3538d45025e89d6fecbcd57b79c6593ca7fafb Mon Sep 17 00:00:00 2001 From: Krishnan Chandra Date: Sat, 27 Jul 2024 17:26:28 -0400 Subject: [PATCH 4/9] Collapse `more` into the generic case --- crates/uv/src/commands/help.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/uv/src/commands/help.rs b/crates/uv/src/commands/help.rs index 61fc13cb8f43..aaabbe8d7931 100644 --- a/crates/uv/src/commands/help.rs +++ b/crates/uv/src/commands/help.rs @@ -79,7 +79,6 @@ pub(crate) fn help(query: &[String], printer: Printer, no_pager: bool) -> Result let prompt = format!("help: uv {}", query.join(" ")); spawn_pager(&pager, &["-R", "-P", &prompt], &help_ansi)? } - Some("more") => spawn_pager(&pager, &[], &help)?, Some(x) if !x.is_empty() => spawn_pager(&pager, &[], &help)?, _ => { writeln!(printer.stdout(), "{help_ansi}")?; From 66df89ae712b6b74d333ee632e48deef31e063d7 Mon Sep 17 00:00:00 2001 From: Krishnan Chandra Date: Sat, 27 Jul 2024 17:35:06 -0400 Subject: [PATCH 5/9] Fix clippy error --- crates/uv/src/commands/help.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/uv/src/commands/help.rs b/crates/uv/src/commands/help.rs index aaabbe8d7931..4f70af0b4efe 100644 --- a/crates/uv/src/commands/help.rs +++ b/crates/uv/src/commands/help.rs @@ -77,7 +77,7 @@ pub(crate) fn help(query: &[String], printer: Printer, no_pager: bool) -> Result match pager.to_str() { Some("less") => { let prompt = format!("help: uv {}", query.join(" ")); - spawn_pager(&pager, &["-R", "-P", &prompt], &help_ansi)? + spawn_pager(&pager, &["-R", "-P", &prompt], &help_ansi)?; } Some(x) if !x.is_empty() => spawn_pager(&pager, &[], &help)?, _ => { From 64b9799d694e7417af16d78b5b9826ea5a247550 Mon Sep 17 00:00:00 2001 From: Krishnan Chandra Date: Tue, 30 Jul 2024 14:11:03 -0400 Subject: [PATCH 6/9] Parse path --- crates/uv/src/commands/help.rs | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/crates/uv/src/commands/help.rs b/crates/uv/src/commands/help.rs index 4f70af0b4efe..302825ef3007 100644 --- a/crates/uv/src/commands/help.rs +++ b/crates/uv/src/commands/help.rs @@ -75,12 +75,22 @@ pub(crate) fn help(query: &[String], printer: Printer, no_pager: bool) -> Result Some(pager) => { // When using a pager, we use the command name as the file name and can support colors match pager.to_str() { - Some("less") => { - let prompt = format!("help: uv {}", query.join(" ")); - spawn_pager(&pager, &["-R", "-P", &prompt], &help_ansi)?; + Some(pager_str) => { + let pager_name = std::path::Path::new(pager_str) + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or(""); + + if pager_name.is_empty() { + writeln!(printer.stdout(), "{help_ansi}")?; + } else if pager_name == "less" { + let prompt = format!("help: uv {}", query.join(" ")); + spawn_pager(&pager, &["-R", "-P", &prompt], &help_ansi)?; + } else { + spawn_pager(&pager, &[], &help)?; + } } - Some(x) if !x.is_empty() => spawn_pager(&pager, &[], &help)?, - _ => { + None => { writeln!(printer.stdout(), "{help_ansi}")?; } } From a6d776c05cc2a12079d5f671af19afd0803e32e3 Mon Sep 17 00:00:00 2001 From: Krishnan Chandra Date: Tue, 30 Jul 2024 16:06:09 -0400 Subject: [PATCH 7/9] Parse args --- crates/uv/src/commands/help.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/crates/uv/src/commands/help.rs b/crates/uv/src/commands/help.rs index 302825ef3007..44efeee39e57 100644 --- a/crates/uv/src/commands/help.rs +++ b/crates/uv/src/commands/help.rs @@ -76,7 +76,11 @@ pub(crate) fn help(query: &[String], printer: Printer, no_pager: bool) -> Result // When using a pager, we use the command name as the file name and can support colors match pager.to_str() { Some(pager_str) => { - let pager_name = std::path::Path::new(pager_str) + let mut parts = pager_str.split_whitespace(); + let pager_command = parts.next().unwrap_or(""); + let pager_args: Vec<&str> = parts.collect(); + + let pager_name = std::path::Path::new(pager_command) .file_name() .and_then(|name| name.to_str()) .unwrap_or(""); @@ -85,9 +89,14 @@ pub(crate) fn help(query: &[String], printer: Printer, no_pager: bool) -> Result writeln!(printer.stdout(), "{help_ansi}")?; } else if pager_name == "less" { let prompt = format!("help: uv {}", query.join(" ")); - spawn_pager(&pager, &["-R", "-P", &prompt], &help_ansi)?; + let args = if pager_args.is_empty() { + vec!["-R", "-P", &prompt] + } else { + pager_args + }; + spawn_pager(pager_command, &args, &help_ansi)?; } else { - spawn_pager(&pager, &[], &help)?; + spawn_pager(pager_command, &pager_args, &help)?; } } None => { From 0bfd48b06ba346b8b832724653afdcb0d0c16386 Mon Sep 17 00:00:00 2001 From: Krishnan Chandra Date: Tue, 30 Jul 2024 16:08:13 -0400 Subject: [PATCH 8/9] Fix slightly --- crates/uv/src/commands/help.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/uv/src/commands/help.rs b/crates/uv/src/commands/help.rs index 44efeee39e57..2521485f0828 100644 --- a/crates/uv/src/commands/help.rs +++ b/crates/uv/src/commands/help.rs @@ -85,7 +85,7 @@ pub(crate) fn help(query: &[String], printer: Printer, no_pager: bool) -> Result .and_then(|name| name.to_str()) .unwrap_or(""); - if pager_name.is_empty() { + if pager_command.is_empty() { writeln!(printer.stdout(), "{help_ansi}")?; } else if pager_name == "less" { let prompt = format!("help: uv {}", query.join(" ")); From bbefd11ee1e133c2b01fb76f0be2eb12a2d8d622 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Tue, 1 Oct 2024 12:43:41 -0500 Subject: [PATCH 9/9] Refactor paging support --- crates/uv/src/commands/help.rs | 209 +++++++++++++++++++++++---------- 1 file changed, 147 insertions(+), 62 deletions(-) diff --git a/crates/uv/src/commands/help.rs b/crates/uv/src/commands/help.rs index 2521485f0828..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,51 +73,18 @@ pub(crate) fn help(query: &[String], printer: Printer, no_pager: bool) -> Result let should_page = !no_pager && !is_root && is_terminal; if should_page { - match std::env::var_os("PAGER") { - Some(pager) => { - // When using a pager, we use the command name as the file name and can support colors - match pager.to_str() { - Some(pager_str) => { - let mut parts = pager_str.split_whitespace(); - let pager_command = parts.next().unwrap_or(""); - let pager_args: Vec<&str> = parts.collect(); - - let pager_name = std::path::Path::new(pager_command) - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or(""); - - if pager_command.is_empty() { - writeln!(printer.stdout(), "{help_ansi}")?; - } else if pager_name == "less" { - let prompt = format!("help: uv {}", query.join(" ")); - let args = if pager_args.is_empty() { - vec!["-R", "-P", &prompt] - } else { - pager_args - }; - spawn_pager(pager_command, &args, &help_ansi)?; - } else { - spawn_pager(pager_command, &pager_args, &help)?; - } - } - None => { - writeln!(printer.stdout(), "{help_ansi}")?; - } - } - } - None => { - 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)?; - } else { - writeln!(printer.stdout(), "{help_ansi}")?; - } - } + 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}")?; } } else { writeln!(printer.stdout(), "{help_ansi}")?; @@ -139,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 mut child = std::process::Command::new(command) - .args(args) - .stdin(std::process::Stdio::piped()) - .spawn()?; + 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 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, + } + } }