From f60014f3ee2b97a61c8828f4f4a3118c40780381 Mon Sep 17 00:00:00 2001 From: Ilya Grigoriev Date: Mon, 13 Jan 2025 23:07:28 -0800 Subject: [PATCH] built-in pager: allow configuring streampager options This also changes the default to be closer to `less -FRX`. Since this default last changed very recently in #4203, I didn't mention this in the Changelog. As discussed in https://github.com/jj-vcs/jj/pull/4203#discussion_r1914372214 I initially kept the config closer to streampager's (see https://github.com/jj-vcs/jj/compare/main...ilyagr:jj:streamopts?expand=1), but then decided to make it more generic, smaller, and hopefully easier to understand. --- CHANGELOG.md | 2 +- cli/src/config-schema.json | 24 ++++++++++ cli/src/config/misc.toml | 4 ++ cli/src/ui.rs | 91 +++++++++++++++++++++++++++++++------- docs/config.md | 43 +++++++++++++++++- 5 files changed, 146 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a283c840..87d776240 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,7 +67,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). * The builtin pager is switched to [streampager](https://github.com/markbt/streampager/). It can handle large - inputs better. + inputs better and can be configured. * Conflicts materialized in the working copy before `jj 0.19.0` may no longer be parsed correctly. If you are using version 0.18.0 or earlier, check out a diff --git a/cli/src/config-schema.json b/cli/src/config-schema.json index 0d87927c0..567be4a39 100644 --- a/cli/src/config-schema.json +++ b/cli/src/config-schema.json @@ -107,6 +107,30 @@ "description": "Pager to use for displaying command output", "default": "less -FRX" }, + "streampager": { + "type": "object", + "description": "':builtin' (streampager-based) pager configuration", + "properties": { + "interface": { + "description": "Whether to quit automatically, whether to clear screen on startup/exit", + "enum": [ + "quit-if-one-page", + "full-screen-clear-output", + "quit-quickly-or-clear-output" + ], + "default": "never" + }, + "wrapping": { + "description": "Whether to wrap long lines", + "enum": [ + "anywhere", + "word", + "none" + ], + "default": "anywhere" + } + } + }, "diff": { "type": "object", "description": "Options for how diffs are displayed", diff --git a/cli/src/config/misc.toml b/cli/src/config/misc.toml index c5685b38a..a8505fcfe 100644 --- a/cli/src/config/misc.toml +++ b/cli/src/config/misc.toml @@ -41,6 +41,10 @@ show-cryptographic-signatures = false [ui.movement] edit = false +[ui.streampager] +interface = "quit-if-one-page" +wrapping = "anywhere" + [snapshot] max-new-file-size = "1MiB" auto-track = "all()" diff --git a/cli/src/ui.rs b/cli/src/ui.rs index 7e8891d37..7424a6276 100644 --- a/cli/src/ui.rs +++ b/cli/src/ui.rs @@ -79,10 +79,19 @@ impl UiOutput { Ok(UiOutput::Paged { child, child_stdin }) } - fn new_builtin_paged() -> streampager::Result { + fn new_builtin_paged(config: &StreampagerConfig) -> streampager::Result { + // This uselessly reads ~/.config/streampager/streampager.toml, even + // though we then override the important options. + // TODO(ilyagr): Fix this once a version of streampager with + // https://github.com/facebook/sapling/pull/1011 is released. let mut pager = streampager::Pager::new_using_stdio()?; - // TODO: should we set the interface mode to be "less -FRX" like? - // It will override the user-configured values. + pager.set_wrapping_mode(config.wrapping); + pager.set_interface_mode(config.streampager_interface_mode()); + // We could make scroll-past-eof configurable, but I'm guessing people + // will not miss it. If we do make it configurable, we should mention + // that it's a bad idea to turn this on if `interface=quit-if-one-page`, + // as it can leave a lot of empty lines on the screen after exiting. + pager.set_scroll_past_eof(false); // Use native pipe, which can be attached to child process. The stdout // stream could be an in-process channel, but the cost of extra syscalls @@ -257,9 +266,59 @@ pub enum PaginationChoice { Auto, } +#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize)] +#[serde(rename_all(deserialize = "kebab-case"))] +pub enum StreampagerAlternateScreenMode { + QuitIfOnePage, + FullScreenClearOutput, + QuitQuicklyOrClearOutput, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize)] +#[serde(rename_all(deserialize = "kebab-case"))] +enum StreampagerWrappingMode { + None, + Word, + Anywhere, +} + +impl From for streampager::config::WrappingMode { + fn from(val: StreampagerWrappingMode) -> Self { + use streampager::config::WrappingMode; + match val { + StreampagerWrappingMode::None => WrappingMode::Unwrapped, + StreampagerWrappingMode::Word => WrappingMode::WordBoundary, + StreampagerWrappingMode::Anywhere => WrappingMode::GraphemeBoundary, + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize)] +#[serde(rename_all(deserialize = "kebab-case"))] +struct StreampagerConfig { + interface: StreampagerAlternateScreenMode, + wrapping: StreampagerWrappingMode, + // TODO: Add an `quit-quickly-delay-seconds` floating point option or a + // `quit-quickly-delay` option that takes a 's' or 'ms' suffix. Note that as + // of this writing, floating point numbers do not work with `--config` +} + +impl StreampagerConfig { + fn streampager_interface_mode(&self) -> streampager::config::InterfaceMode { + use streampager::config::InterfaceMode; + use StreampagerAlternateScreenMode::*; + match self.interface { + // InterfaceMode::Direct not implemented + FullScreenClearOutput => InterfaceMode::FullScreen, + QuitIfOnePage => InterfaceMode::Hybrid, + QuitQuicklyOrClearOutput => InterfaceMode::Delayed(std::time::Duration::from_secs(2)), + } + } +} + enum PagerConfig { Disabled, - Builtin, + Builtin(StreampagerConfig), External(CommandNameAndArgs), } @@ -270,7 +329,7 @@ impl PagerConfig { }; match config.get("ui.pager")? { CommandNameAndArgs::String(name) if name == BUILTIN_PAGER_NAME => { - Ok(PagerConfig::Builtin) + Ok(PagerConfig::Builtin(config.get("ui.streampager")?)) } _ => Ok(PagerConfig::External(config.get("ui.pager")?)), } @@ -318,16 +377,18 @@ impl Ui { PagerConfig::Disabled => { return; } - PagerConfig::Builtin => UiOutput::new_builtin_paged() - .inspect_err(|err| { - writeln!( - self.warning_default(), - "Failed to set up builtin pager: {err}", - err = format_error_with_sources(err), - ) - .ok(); - }) - .ok(), + PagerConfig::Builtin(streampager_config) => { + UiOutput::new_builtin_paged(streampager_config) + .inspect_err(|err| { + writeln!( + self.warning_default(), + "Failed to set up builtin pager: {err}", + err = format_error_with_sources(err), + ) + .ok(); + }) + .ok() + } PagerConfig::External(command_name_and_args) => { UiOutput::new_paged(command_name_and_args) .inspect_err(|err| { diff --git a/docs/config.md b/docs/config.md index 3efaa87fe..81550014f 100644 --- a/docs/config.md +++ b/docs/config.md @@ -575,8 +575,8 @@ a `$`): `less -FRX` is the default pager in the absence of any other setting, except on Windows where it is `:builtin`. -The special value `:builtin` enables usage of the [integrated pager called -`streampager`](https://github.com/markbt/streampager/). +The special value `:builtin` enables usage of the [integrated +pager](#builtin-pager). If you are using a standard Linux distro, your system likely already has `$PAGER` set and that will be preferred over the built-in. To use the built-in: @@ -598,6 +598,45 @@ paginate = "auto" paginate = "never" ``` +### Builtin pager + +Our builtin pager is based on +[`streampager`](https://github.com/markbt/streampager/) but is configured within +`jj`'s config. It is configured via the `ui.streampager` table. + +#### Wrapping + +Wrapping performed by the pager happens *in addition to* any +wrapping that `jj` itself does. + +```toml +[ui.streampager] +wrapping = "anywhere" # wrap at screen edge (default) +wrapping = "word" # wrap on word boundaries +wrapping = "none" # strip long lines, allow scrolling + # left and right like `less -S` +``` + +#### Auto-exit, clearing the screen on startup or exit + +You can configure whether the pager clears the screen on startup or exit, and +whether it quits automatically on short inputs. When the pager auto-quits, +features like word-wrapping are disabled. + +```toml +[ui.streampager] +# Do not clear screen on exit. Use a full-screen interface for long +# output only. Like `less -FX`. +interface = "quit-if-one-page" # (default). +# Always use a full-screen interface, ask the terminal to clear the +# screen on exit. Like `less -+FX`. +interface = "full-screen-clear-output" +# Use the alternate screen if the input is either long or takes more +# than 2 seconds to finish. Similar but not identical to `less -F -+X`. +interface = "quit-quickly-or-clear-output" +``` + + ### Processing contents to be paged If you'd like to pass the output through a formatter e.g.