diff --git a/examples/custom-backend/main.rs b/examples/custom-backend/main.rs index 9c911b58269..c10c1975ed9 100644 --- a/examples/custom-backend/main.rs +++ b/examples/custom-backend/main.rs @@ -65,6 +65,7 @@ fn main() { let (mut ui, result) = create_ui(); let result = result.and_then(|()| run(&mut ui)); let exit_code = handle_command_result(&mut ui, result); + ui.finalize_writes(); std::process::exit(exit_code); } diff --git a/examples/custom-command/main.rs b/examples/custom-command/main.rs index ccd136b19e2..96bd3743e54 100644 --- a/examples/custom-command/main.rs +++ b/examples/custom-command/main.rs @@ -63,5 +63,6 @@ fn main() { let (mut ui, result) = create_ui(); let result = result.and_then(|()| run(&mut ui)); let exit_code = handle_command_result(&mut ui, result); + ui.finalize_writes(); std::process::exit(exit_code); } diff --git a/src/commands.rs b/src/commands.rs index e2eb7c975df..2b060a3c18f 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1331,6 +1331,7 @@ fn show_color_words_diff_line( } fn cmd_diff(ui: &mut Ui, command: &CommandHelper, args: &DiffArgs) -> Result<(), CommandError> { + ui.request_pager(); let workspace_command = command.workspace_helper(ui)?; let from_tree; let to_tree; @@ -1358,6 +1359,7 @@ fn cmd_diff(ui: &mut Ui, command: &CommandHelper, args: &DiffArgs) -> Result<(), } fn cmd_show(ui: &mut Ui, command: &CommandHelper, args: &ShowArgs) -> Result<(), CommandError> { + ui.request_pager(); let workspace_command = command.workspace_helper(ui)?; let commit = workspace_command.resolve_single_rev(&args.revision)?; let parents = commit.parents(); @@ -2014,6 +2016,7 @@ fn log_template(settings: &UserSettings) -> String { } fn cmd_log(ui: &mut Ui, command: &CommandHelper, args: &LogArgs) -> Result<(), CommandError> { + ui.request_pager(); let workspace_command = command.workspace_helper(ui)?; let default_revset = ui.settings().default_revset(); @@ -2151,6 +2154,7 @@ fn show_patch( } fn cmd_obslog(ui: &mut Ui, command: &CommandHelper, args: &ObslogArgs) -> Result<(), CommandError> { + ui.request_pager(); let workspace_command = command.workspace_helper(ui)?; let start_commit = workspace_command.resolve_single_rev(&args.revision)?; @@ -2246,6 +2250,7 @@ fn cmd_interdiff( command: &CommandHelper, args: &InterdiffArgs, ) -> Result<(), CommandError> { + ui.request_pager(); let workspace_command = command.workspace_helper(ui)?; let from = workspace_command.resolve_single_rev(args.from.as_deref().unwrap_or("@"))?; let to = workspace_command.resolve_single_rev(args.to.as_deref().unwrap_or("@"))?; @@ -3562,6 +3567,7 @@ fn cmd_op_log( command: &CommandHelper, _args: &OperationLogArgs, ) -> Result<(), CommandError> { + ui.request_pager(); let workspace_command = command.workspace_helper(ui)?; let repo = workspace_command.repo(); let head_op = repo.operation().clone(); diff --git a/src/config.rs b/src/config.rs index ae1f7d35d6c..a9827a561b8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -58,6 +58,9 @@ fn env_base() -> config::Config { // should override $NO_COLOR." https://no-color.org/ builder = builder.set_override("ui.color", "never").unwrap(); } + if let Ok(value) = env::var("PAGER") { + builder = builder.set_override("ui.pager", value).unwrap(); + } if let Ok(value) = env::var("VISUAL") { builder = builder.set_override("ui.editor", value).unwrap(); } else if let Ok(value) = env::var("EDITOR") { diff --git a/src/main.rs b/src/main.rs index 242b7e17053..baf1686d983 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,5 +27,6 @@ fn main() { let (mut ui, result) = create_ui(); let result = result.and_then(|()| run(&mut ui)); let exit_code = handle_command_result(&mut ui, result); + ui.finalize_writes(); std::process::exit(exit_code); } diff --git a/src/ui.rs b/src/ui.rs index 19195b8d2db..a3c79afb51e 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -14,8 +14,9 @@ use std::io::{Stderr, Stdout, Write}; use std::path::{Path, PathBuf}; +use std::process::{Child, ChildStdin, Command, Stdio}; use std::str::FromStr; -use std::{fmt, io}; +use std::{fmt, io, mem}; use atty::Stream; use jujutsu_lib::settings::UserSettings; @@ -25,6 +26,7 @@ use crate::formatter::{Formatter, FormatterFactory}; pub struct Ui { color: bool, progress_indicator: bool, + paginate: PaginationChoice, cwd: PathBuf, formatter_factory: FormatterFactory, output: UiOutput, @@ -92,18 +94,67 @@ fn use_color(choice: ColorChoice) -> bool { } } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PaginationChoice { + Always, + Never, + Auto, +} + +impl Default for PaginationChoice { + fn default() -> Self { + PaginationChoice::Auto + } +} + +impl FromStr for PaginationChoice { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s { + "always" => Ok(PaginationChoice::Always), + "never" => Ok(PaginationChoice::Never), + "auto" => Ok(PaginationChoice::Auto), + _ => Err("must be one of always, never, or auto"), + } + } +} + +fn pagination_setting(settings: &UserSettings) -> PaginationChoice { + settings + .config() + .get_string("ui.paginate") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or_default() +} + +fn pager_setting(settings: &UserSettings) -> String { + settings + .config() + .get_string("ui.pager") + .unwrap_or_else(|_| "less".to_string()) +} + impl Ui { pub fn for_terminal(settings: UserSettings) -> Ui { let cwd = std::env::current_dir().unwrap(); let color = use_color(color_setting(&settings)); + let paginate = pagination_setting(&settings); let progress_indicator = progress_indicator_setting(&settings); let formatter_factory = FormatterFactory::prepare(&settings, color); + let output = match paginate { + PaginationChoice::Always => UiOutput::new_paged_else_terminal(&settings), + _ => UiOutput::new_terminal(), + }; + Ui { color, + paginate, cwd, formatter_factory, progress_indicator, - output: UiOutput::new_terminal(), + output, settings, } } @@ -116,6 +167,20 @@ impl Ui { } } + /// Switches the output to use the pager, if allowed. + pub fn request_pager(&mut self) { + if !matches!(self.paginate, PaginationChoice::Auto) { + return; + } + if matches!(self.output, UiOutput::Paged { .. }) { + return; + } + if !atty::is(Stream::Stdout) { + return; + } + self.output = UiOutput::new_paged_else_terminal(&self.settings); + } + pub fn color(&self) -> bool { self.color } @@ -148,6 +213,7 @@ impl Ui { pub fn stdout_formatter<'a>(&'a self) -> Box { match &self.output { UiOutput::Terminal { stdout, .. } => self.new_formatter(stdout.lock()), + UiOutput::Paged { child_stdin, .. } => self.new_formatter(child_stdin), } } @@ -155,6 +221,7 @@ impl Ui { pub fn stderr_formatter<'a>(&'a self) -> Box { match &self.output { UiOutput::Terminal { stderr, .. } => self.new_formatter(stderr.lock()), + UiOutput::Paged { child_stdin, .. } => self.new_formatter(child_stdin), } } @@ -168,6 +235,7 @@ impl Ui { let data = text.as_bytes(); match &mut self.output { UiOutput::Terminal { stdout, .. } => stdout.write_all(data), + UiOutput::Paged { child_stdin, .. } => child_stdin.write_all(data), } } @@ -175,12 +243,14 @@ impl Ui { let data = text.as_bytes(); match &mut self.output { UiOutput::Terminal { stderr, .. } => stderr.write_all(data), + UiOutput::Paged { child_stdin, .. } => child_stdin.write_all(data), } } pub fn write_fmt(&mut self, fmt: fmt::Arguments<'_>) -> io::Result<()> { match &mut self.output { UiOutput::Terminal { stdout, .. } => stdout.write_fmt(fmt), + UiOutput::Paged { child_stdin, .. } => child_stdin.write_fmt(fmt), } } @@ -211,6 +281,24 @@ impl Ui { pub fn flush(&mut self) -> io::Result<()> { match &mut self.output { UiOutput::Terminal { stdout, .. } => stdout.flush(), + UiOutput::Paged { child_stdin, .. } => child_stdin.flush(), + } + } + + pub fn finalize_writes(&mut self) { + if let UiOutput::Paged { + mut child, + child_stdin, + } = mem::replace(&mut self.output, UiOutput::new_terminal()) + { + drop(child_stdin); + child + .wait() + // It's possible (though unlikely) that this write fails, but + // this function gets called so late that there's not much we + // can do about it. + .map_err(|e| self.write_error(&format!("Failed to wait on pager '{}'", e))) + .ok(); } } @@ -249,13 +337,21 @@ impl Ui { text, output: match self.output { UiOutput::Terminal { .. } => io::stdout(), + UiOutput::Paged { .. } => io::stdout(), }, } } } enum UiOutput { - Terminal { stdout: Stdout, stderr: Stderr }, + Terminal { + stdout: Stdout, + stderr: Stderr, + }, + Paged { + child: Child, + child_stdin: ChildStdin, + }, } impl UiOutput { @@ -265,6 +361,23 @@ impl UiOutput { stderr: io::stderr(), } } + + fn new_paged_else_terminal(settings: &UserSettings) -> UiOutput { + let pager_cmd = pager_setting(settings); + let child_result = Command::new(pager_cmd).stdin(Stdio::piped()).spawn(); + match child_result { + Ok(mut child) => { + let child_stdin = child.stdin.take().unwrap(); + UiOutput::Paged { child, child_stdin } + } + Err(e) => { + io::stderr() + .write_fmt(format_args!("Failed to spawn pager: '{}'", e)) + .ok(); + UiOutput::new_terminal() + } + } + } } pub struct OutputGuard {