diff --git a/Cargo.lock b/Cargo.lock index bab43c0433df..2bb8fda8fbfc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4475,6 +4475,7 @@ dependencies = [ "rustc-hash 2.0.0", "serde", "serde_json", + "tempfile", "textwrap", "thiserror", "tikv-jemallocator", @@ -4506,6 +4507,7 @@ dependencies = [ "uv-types", "uv-virtualenv", "uv-warnings", + "which", ] [[package]] diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index 19bfbffe4ea6..67f7a50a2ce7 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -59,6 +59,7 @@ regex = { workspace = true } rustc-hash = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +tempfile = { workspace = true } textwrap = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } @@ -69,6 +70,7 @@ tracing-subscriber = { workspace = true, features = ["json"] } tracing-tree = { workspace = true } unicode-width = { workspace = true } url = { workspace = true } +which = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies] mimalloc = { version = "0.1.39" } diff --git a/crates/uv/src/commands/help.rs b/crates/uv/src/commands/help.rs index aa83f8a822a2..3f79b3c0884d 100644 --- a/crates/uv/src/commands/help.rs +++ b/crates/uv/src/commands/help.rs @@ -1,8 +1,10 @@ -use std::fmt::Write; +use std::{fmt::Display, fmt::Write}; +use anstream::ColorChoice; use anyhow::{anyhow, Result}; use clap::CommandFactory; -use itertools::Itertools; +use itertools::{Either, Itertools}; +use which::which; use super::ExitStatus; use crate::printer::Printer; @@ -30,7 +32,24 @@ pub(crate) fn help(query: &[String], printer: Printer) -> Result { let mut command = command.clone(); let help = command.render_long_help(); - writeln!(printer.stderr(), "{}", help.ansi())?; + + let help_ansi = match anstream::Stderr::choice(&std::io::stderr()) { + ColorChoice::Always | ColorChoice::AlwaysAnsi => Either::Left(help.ansi()), + ColorChoice::Never => Either::Right(help.clone()), + // We just asked anstream for a choice, that can't be auto + ColorChoice::Auto => unreachable!(), + }; + + if which("less").is_ok() { + // When using less, we use the command name as the file name and can support colors + let prompt = format!("help: uv {}", query.join(" ")); + write_and_spawn(&query.join("-"), &help_ansi, "less", &["-R", "-P", &prompt])?; + } else if which("more").is_ok() { + // When using more, we skip the ANSI color codes + write_and_spawn(&query.join("-"), &help, "more", &[])?; + } else { + writeln!(printer.stderr(), "{help_ansi}")?; + } Ok(ExitStatus::Success) } @@ -49,3 +68,30 @@ fn find_command<'a>( let subcommand = cmd.find_subcommand(next).ok_or((query, cmd))?; find_command(&query[1..], subcommand) } + +/// Write the contents of documentation to disk and spawn the given command to +/// display it. +fn write_and_spawn(name: &str, contents: impl Display, command: &str, args: &[&str]) -> Result<()> { + // Note this implementation is based on Cargo's `help` command which displays a `man` page. + // See https://github.com/rust-lang/cargo/blob/e98f702d83d3075b2232b4502114f177826fe06d/src/bin/cargo/commands/help.rs + use std::io::Write; + + let prefix = format!("uv-{name}."); + let mut tmp = tempfile::Builder::new().prefix(&prefix).tempfile()?; + let f = tmp.as_file_mut(); + write!(f, "{contents}")?; + f.flush()?; + + let path = tmp.path(); + // Use a path relative to the temp directory so that it can work on + // cygwin/msys systems which don't handle windows-style paths. + let mut relative_name = std::ffi::OsString::from("./"); + relative_name.push(path.file_name().unwrap()); + let mut cmd = std::process::Command::new(command) + .args(args) + .arg(relative_name) + .current_dir(path.parent().unwrap()) + .spawn()?; + drop(cmd.wait()); + Ok(()) +} diff --git a/crates/uv/tests/help.rs b/crates/uv/tests/help.rs index b8d8f26c48f3..f521a2ab4bd9 100644 --- a/crates/uv/tests/help.rs +++ b/crates/uv/tests/help.rs @@ -11,8 +11,6 @@ fn help() { success: true exit_code: 0 ----- stdout ----- - - ----- stderr ----- An extremely fast Python package manager. Usage: uv [OPTIONS] @@ -109,6 +107,7 @@ fn help() { -V, --version Print version + ----- stderr ----- "###); } @@ -230,8 +229,6 @@ fn help_subcommand() { success: true exit_code: 0 ----- stdout ----- - - ----- stderr ----- Manage Python installations Usage: python @@ -248,6 +245,7 @@ fn help_subcommand() { -h, --help Print help (see a summary with '-h') + ----- stderr ----- "###); } @@ -259,8 +257,6 @@ fn help_subsubcommand() { success: true exit_code: 0 ----- stdout ----- - - ----- stderr ----- Download and install Python versions Usage: install [OPTIONS] [TARGETS]... @@ -280,6 +276,7 @@ fn help_subsubcommand() { -h, --help Print help (see a summary with '-h') + ----- stderr ----- "###); } @@ -449,8 +446,6 @@ fn help_with_global_option() { success: true exit_code: 0 ----- stdout ----- - - ----- stderr ----- An extremely fast Python package manager. Usage: uv [OPTIONS] @@ -547,6 +542,7 @@ fn help_with_global_option() { -V, --version Print version + ----- stderr ----- "###); }