Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Respect PAGER env var when paging in uv help command #5511

Merged
merged 9 commits into from
Oct 1, 2024
169 changes: 145 additions & 24 deletions crates/uv/src/commands/help.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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}")?;
}
Expand All @@ -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<OsStr>, 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<String>,
path: Option<PathBuf>,
}

impl PagerKind {
fn default_args(&self, prompt: String) -> Vec<String> {
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<Self, Self::Err> {
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<Pager> {
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,
}
}
}
Loading