cli: make pager configurable per command

Allow `ui.pager` to be set as a table like so:

    [ui]
    pager.status = ":none"
    pager.log = { command = ["less", "-FRX"], env = { LESSCHARSET = "utf-8" } }
    pager.diff = ["delta", "..."]
    pager.default = "..."

`ui.pager.log` is used for `jj log`, `ui.pager.diff` for `jj diff`,
etc. The full name of the highest level subcommand is used, i.e. `jj
operation diff` uses `ui.pager.operation`. The value ":none" disables
paging for the selected command. If a command is not specifically
mentioned, the value of `ui.pager.default` is used.

The old behaviour is unchanged, i.e. `ui.pager = ["less", "-FRX"]` will
set the pager for all subcommands.
This commit is contained in:
Bryce Berger 2025-01-08 22:43:57 -05:00
parent 7df0f16fe0
commit 79d45f6c96
No known key found for this signature in database
GPG key ID: 58CA4F9FEF8F4296
19 changed files with 85 additions and 40 deletions

View file

@ -884,7 +884,7 @@ fn handle_clap_error(ui: &mut Ui, err: &clap::Error, hints: &[ErrorHint]) -> io:
match err.kind() {
clap::error::ErrorKind::DisplayHelp
| clap::error::ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand => ui.request_pager(),
| clap::error::ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand => ui.request_pager(""),
_ => {}
};
// Definitions for exit codes and streams come from

View file

@ -148,7 +148,7 @@ pub fn cmd_bookmark_list(
.labeled("bookmark_list")
};
ui.request_pager();
ui.request_pager("bookmark");
let mut formatter = ui.stdout_formatter();
let mut found_deleted_local_bookmark = false;

View file

@ -89,7 +89,7 @@ pub fn cmd_config_list(
}
if !annotated_values.is_empty() {
ui.request_pager();
ui.request_pager("config");
let mut formatter = ui.stdout_formatter();
for annotated in &annotated_values {
template.format(annotated, formatter.as_mut())?;

View file

@ -123,7 +123,7 @@ pub(crate) fn cmd_diff(
}
let diff_renderer = workspace_command.diff_renderer_for(&args.format)?;
ui.request_pager();
ui.request_pager("diff");
diff_renderer.show_diff(
ui,
ui.stdout_formatter().as_mut(),

View file

@ -119,7 +119,7 @@ pub(crate) fn cmd_evolog(
.labeled("node");
}
ui.request_pager();
ui.request_pager("evolog");
let mut formatter = ui.stdout_formatter();
let formatter = formatter.as_mut();

View file

@ -97,7 +97,7 @@ fn render_file_annotation(
template_render: &TemplateRenderer<Commit>,
annotation: &FileAnnotation,
) -> Result<(), CommandError> {
ui.request_pager();
ui.request_pager("file");
let mut formatter = ui.stdout_formatter();
for (line_no, (commit_id, line)) in annotation.lines().enumerate() {
let commit_id = commit_id.expect("should reached to the empty ancestor");

View file

@ -51,7 +51,7 @@ pub(crate) fn cmd_file_list(
let matcher = workspace_command
.parse_file_patterns(ui, &args.paths)?
.to_matcher();
ui.request_pager();
ui.request_pager("file");
for (name, _value) in tree.entries_matching(matcher.as_ref()) {
writeln!(
ui.stdout(),

View file

@ -83,14 +83,14 @@ pub(crate) fn cmd_file_show(
return Err(user_error(format!("No such path: {ui_path}")));
}
if !value.is_tree() {
ui.request_pager();
ui.request_pager("file");
write_tree_entries(ui, &workspace_command, [(path, Ok(value))])?;
return Ok(());
}
}
let matcher = fileset_expression.to_matcher();
ui.request_pager();
ui.request_pager("file");
write_tree_entries(
ui,
&workspace_command,

View file

@ -52,7 +52,7 @@ pub(crate) fn cmd_help(
) -> Result<(), CommandError> {
if let Some(name) = &args.keyword {
let keyword = find_keyword(name).expect("clap should check this with `value_parser`");
ui.request_pager();
ui.request_pager("help");
write!(ui.stdout(), "{}", keyword.content)?;
return Ok(());

View file

@ -78,7 +78,7 @@ pub(crate) fn cmd_interdiff(
.parse_file_patterns(ui, &args.paths)?
.to_matcher();
let diff_renderer = workspace_command.diff_renderer_for(&args.format)?;
ui.request_pager();
ui.request_pager("interdiff");
diff_renderer.show_inter_diff(
ui,
ui.stdout_formatter().as_mut(),

View file

@ -173,7 +173,7 @@ pub(crate) fn cmd_log(
}
{
ui.request_pager();
ui.request_pager("log");
let mut formatter = ui.stdout_formatter();
let formatter = formatter.as_mut();

View file

@ -137,7 +137,7 @@ pub fn cmd_op_diff(
};
let op_summary_template = workspace_command.operation_summary_template();
ui.request_pager();
ui.request_pager("operation");
let mut formatter = ui.stdout_formatter();
write!(formatter, "From operation: ")?;
op_summary_template.format(&from_op, &mut *formatter)?;

View file

@ -186,7 +186,7 @@ fn do_op_log(
None
};
ui.request_pager();
ui.request_pager("operation");
let mut formatter = ui.stdout_formatter();
let formatter = formatter.as_mut();
let limit = args.limit.unwrap_or(usize::MAX);

View file

@ -93,7 +93,7 @@ pub fn cmd_op_show(
.labeled("operation")
};
ui.request_pager();
ui.request_pager("operation");
let mut formatter = ui.stdout_formatter();
template.format(&op, formatter.as_mut())?;

View file

@ -59,7 +59,7 @@ pub(crate) fn cmd_show(
};
let template = workspace_command.parse_commit_template(ui, &template_string)?;
let diff_renderer = workspace_command.diff_renderer_for(&args.format)?;
ui.request_pager();
ui.request_pager("show");
let mut formatter = ui.stdout_formatter();
let formatter = formatter.as_mut();
template.format(&commit, formatter)?;

View file

@ -58,7 +58,7 @@ pub(crate) fn cmd_status(
let matcher = workspace_command
.parse_file_patterns(ui, &args.paths)?
.to_matcher();
ui.request_pager();
ui.request_pager("status");
let mut formatter = ui.stdout_formatter();
let formatter = formatter.as_mut();

View file

@ -76,7 +76,7 @@ fn cmd_tag_list(
.labeled("tag_list")
};
ui.request_pager();
ui.request_pager("tag");
let mut formatter = ui.stdout_formatter();
for (name, target) in view.tags() {

View file

@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::collections::HashMap;
use std::env;
use std::error;
use std::fmt;
@ -49,6 +50,7 @@ use crate::formatter::LabeledWriter;
use crate::formatter::PlainTextFormatter;
const BUILTIN_PAGER_NAME: &str = ":builtin";
const BUILTIN_PAGER_NONE_NAME: &str = ":none";
enum UiOutput {
Terminal {
@ -257,13 +259,28 @@ impl Write for UiStderr<'_> {
pub struct Ui {
quiet: bool,
pager_cmd: CommandNameAndArgs,
pager_cmd: PagerCmd,
paginate: PaginationChoice,
progress_indicator: bool,
formatter_factory: FormatterFactory,
output: UiOutput,
}
enum PagerCmd {
Default(CommandNameAndArgs),
PerSubcommand(HashMap<String, CommandNameAndArgs>),
}
impl PagerCmd {
fn from_config(config: &StackedConfig) -> Result<Self, ConfigGetError> {
const KEY: &str = "ui.pager";
config
.get(KEY)
.map(Self::Default)
.or_else(|_| config.get(KEY).map(Self::PerSubcommand))
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ColorChoice {
@ -333,7 +350,7 @@ impl Ui {
Ok(Ui {
quiet: config.get("ui.quiet")?,
formatter_factory,
pager_cmd: config.get("ui.pager")?,
pager_cmd: PagerCmd::from_config(config)?,
paginate: config.get("ui.paginate")?,
progress_indicator: config.get("ui.progress-indicator")?,
output: UiOutput::new_terminal(),
@ -343,7 +360,7 @@ impl Ui {
pub fn reset(&mut self, config: &StackedConfig) -> Result<(), CommandError> {
self.quiet = config.get("ui.quiet")?;
self.paginate = config.get("ui.paginate")?;
self.pager_cmd = config.get("ui.pager")?;
self.pager_cmd = PagerCmd::from_config(config)?;
self.progress_indicator = config.get("ui.progress-indicator")?;
self.formatter_factory = prepare_formatter_factory(config, &io::stdout())?;
Ok(())
@ -351,7 +368,7 @@ impl Ui {
/// Switches the output to use the pager, if allowed.
#[instrument(skip_all)]
pub fn request_pager(&mut self) {
pub fn request_pager(&mut self, subcommand: &str) {
match self.paginate {
PaginationChoice::Never => return,
PaginationChoice::Auto => {}
@ -360,25 +377,37 @@ impl Ui {
return;
}
let use_builtin_pager = matches!(
&self.pager_cmd, CommandNameAndArgs::String(name) if name == BUILTIN_PAGER_NAME);
let new_output = if use_builtin_pager {
Some(UiOutput::new_builtin())
} else {
UiOutput::new_paged(&self.pager_cmd)
.inspect_err(|err| {
// The pager executable couldn't be found or couldn't be run
writeln!(
self.warning_default(),
"Failed to spawn pager '{name}': {err}",
name = self.pager_cmd.split_name(),
err = format_error_with_sources(err),
)
.ok();
writeln!(self.hint_default(), "Consider using the `:builtin` pager.").ok();
})
.ok()
let show_err = |cmd: &CommandNameAndArgs, err: &_| {
writeln!(
self.warning_default(),
"Failed to spawn pager '{name}': {err}",
name = cmd.split_name(),
err = format_error_with_sources(err)
)
.ok();
writeln!(self.hint_default(), "Consider using the `:builtin` pager.").ok();
};
let new_output = match &self.pager_cmd {
PagerCmd::Default(CommandNameAndArgs::String(name)) if name == BUILTIN_PAGER_NAME => {
Some(UiOutput::new_builtin())
}
PagerCmd::Default(cmd) => UiOutput::new_paged(cmd)
.inspect_err(|err| show_err(cmd, err))
.ok(),
PagerCmd::PerSubcommand(map) => {
match map.get(subcommand).or_else(|| map.get("default")) {
Some(CommandNameAndArgs::String(name)) if name == BUILTIN_PAGER_NONE_NAME => {
None
}
Some(cmd) => UiOutput::new_paged(cmd)
.inspect_err(|err| show_err(cmd, err))
.ok(),
None => None,
}
}
};
if let Some(output) = new_output {
self.output = output;
}

View file

@ -578,6 +578,22 @@ pager = "delta"
format = "git"
```
### Setting the pager per subcommand
A different pager can be configured for each subcommand by using a table. For
example:
```toml
[ui]
# set to ":none" to disable paging for a command
pager.status = ":none"
pager.log = { command = ["less", "-FRX"], env = { LESSCHARSET = "utf-8" } }
pager.diff = ["delta", "--dark"]
# use pager.default for all subcommands not otherwise mentioned
pager.default = ":builtin"
```
## Aliases
You can define aliases for commands, including their arguments. For example: