Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
}
}
}