Skip to content

Commit

Permalink
Respect PAGER env var when paging in uv help command (#5511)
Browse files Browse the repository at this point in the history
## Summary

Closes #4931.

## Test Plan

Tried running the following commands locally to make sure that all cases
work:

```
unset PAGER
cargo run -- help venv
```

With no pager set, `uv` correctly finds `less` on the system as it did
before and passes the help output to it.

---

```
PAGER= cargo run -- help venv
```

This correctly prints out to stdout and does not use any pager.

---

```
PAGER=most cargo run -- help venv
```

This correctly opens the `most` pager as shown below:

<img width="1917" alt="Screenshot 2024-07-27 at 5 14 42 PM"
src="https://github.com/user-attachments/assets/dfaa5a83-b47e-4f5c-9be1-b0b1e9818932">

---------

Co-authored-by: Zanie Blue <contact@zanie.dev>
  • Loading branch information
krishnan-chandra and zanieb authored Oct 1, 2024
1 parent 6aa1e01 commit bc459c8
Showing 1 changed file with 145 additions and 24 deletions.
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 @@ -78,13 +80,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 @@ -110,25 +115,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,
}
}
}

0 comments on commit bc459c8

Please sign in to comment.