ok/jj
1
0
Fork 0
forked from mirrors/jj

ui: add pager

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.

The pager program can be chosen via `ui.pager`. (defaults to Defaults to
$PAGER, and 'less' if that is unset (falling back to 'less' also makes
the tests pass).

Currently, commands are paginated if:

- they have "long" output (as defined by jj developers)
- jj is invoked in a terminal

The next commit will allow pagination to be turned off via a CLI option.
More complex pagination toggling (e.g. showing a pager even if the
output doesn't look like a terminal, using a pager for shorter ouput) is
left for a future PR.
This commit is contained in:
Glen Choo 2022-10-18 11:07:35 -07:00
parent f4f0fbbd5d
commit 7c2400f3e5
8 changed files with 97 additions and 2 deletions

View file

@ -35,6 +35,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### New features
* Commands with long output are paginated.
* The new `jj git remote rename` command allows git remotes to be renamed
in-place.

View file

@ -43,6 +43,17 @@ This setting overrides the `NO_COLOR` environment variable (if set).
ui.color = "never" # Turn off color
### Paginating output
The default pager is can be set via `ui.pager` or the `PAGER` environment
variable.
The priority is as follows (environment variables are marked with a `$`):
`ui.pager` > `$PAGER`
`less` is the default pager in the absence of any other setting.
### Editor
The default editor is set via `ui.editor`,

View file

@ -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);
}

View file

@ -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);
}

View file

@ -1332,6 +1332,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;
@ -1359,6 +1360,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();
@ -2031,6 +2033,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();
@ -2193,6 +2196,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)?;
@ -2288,6 +2292,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("@"))?;
@ -3604,6 +3609,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();

View file

@ -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") {

View file

@ -59,5 +59,6 @@ fn main() {
let (mut ui, result) = create_ui();
let result = result.and_then(|()| run(&mut ui, reload_log_filter));
let exit_code = handle_command_result(&mut ui, result);
ui.finalize_writes();
std::process::exit(exit_code);
}

View file

@ -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 crossterm::tty::IsTty;
use jujutsu_lib::settings::UserSettings;
@ -92,6 +93,13 @@ fn use_color(choice: ColorChoice) -> bool {
}
}
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();
@ -116,6 +124,18 @@ impl Ui {
}
}
/// Switches the output to use the pager, if allowed.
pub fn request_pager(&mut self) {
match self.output {
UiOutput::Paged { .. } => {}
UiOutput::Terminal { .. } => {
if io::stdout().is_tty() {
self.output = UiOutput::new_paged_else_terminal(&self.settings);
}
}
}
}
pub fn color(&self) -> bool {
self.color
}
@ -148,6 +168,7 @@ 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),
}
}
@ -155,6 +176,7 @@ impl Ui {
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),
}
}
@ -168,6 +190,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 +198,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 +236,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);
if let Err(e) = 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.
self.write_error(&format!("Failed to wait on pager {}", e))
.ok();
}
}
}
@ -249,13 +292,23 @@ impl Ui {
text,
output: match self.output {
UiOutput::Terminal { .. } => io::stdout(),
// TODO we don't actually need to write in this case, so it
// might be better to no-op
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 +318,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 {