Skip to content

Commit

Permalink
Refactor paging support
Browse files Browse the repository at this point in the history
  • Loading branch information
zanieb committed Oct 1, 2024
1 parent 0bfd48b commit bbefd11
Showing 1 changed file with 147 additions and 62 deletions.
209 changes: 147 additions & 62 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,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}")?;
Expand All @@ -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<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 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<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 bbefd11

Please sign in to comment.