Skip to content

Commit

Permalink
ui: add pager
Browse files Browse the repository at this point in the history
Teach Ui's writing functions to write to a pager without touching the
process's file descriptors. This is done by introducing UiOutput::Paged,
which spawns a pager that Ui's functions can write to.

Pager behavior is controlled by two config variables:
- ui.pager: the program to use as the pager. Defaults to $PAGER, and
  'less' if that is unset (falling back to 'less' also makes the tests
  pass).
- ui.paginate: whether to paginate output. Supported values are 'auto',
  'always' and 'never'. Defaults to 'auto'.

When using ui.paginate = 'auto', commands use the pager iff:

- they have "long" output (as defined by jj developers)
- jj is invoked in a terminal
  • Loading branch information
chooglen committed Nov 17, 2022
1 parent ab4832c commit 6ef8312
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 3 deletions.
1 change: 1 addition & 0 deletions examples/custom-backend/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
1 change: 1 addition & 0 deletions examples/custom-command/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
6 changes: 6 additions & 0 deletions src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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)?;
Expand Down Expand Up @@ -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("@"))?;
Expand Down Expand Up @@ -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();
Expand Down
3 changes: 3 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
119 changes: 116 additions & 3 deletions src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand Down Expand Up @@ -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<Self, Self::Err> {
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,
}
}
Expand All @@ -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
}
Expand Down Expand Up @@ -148,13 +213,15 @@ impl Ui {
pub fn stdout_formatter<'a>(&'a self) -> Box<dyn Formatter + 'a> {
match &self.output {
UiOutput::Terminal { stdout, .. } => self.new_formatter(stdout.lock()),
UiOutput::Paged { child_stdin, .. } => self.new_formatter(child_stdin),
}
}

/// Creates a formatter for the locked stderr stream.
pub fn stderr_formatter<'a>(&'a self) -> Box<dyn Formatter + 'a> {
match &self.output {
UiOutput::Terminal { stderr, .. } => self.new_formatter(stderr.lock()),
UiOutput::Paged { child_stdin, .. } => self.new_formatter(child_stdin),
}
}

Expand All @@ -168,19 +235,22 @@ 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),
}
}

pub fn write_stderr(&mut self, text: &str) -> io::Result<()> {
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),
}
}

Expand Down Expand Up @@ -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();
}
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down

0 comments on commit 6ef8312

Please sign in to comment.