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.
This commit is contained in:
Ilya Grigoriev 2025-01-13 23:07:28 -08:00
parent 8aa29169ea
commit f60014f3ee
5 changed files with 146 additions and 18 deletions

View file

@ -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

View file

@ -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",

View file

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

View file

@ -79,10 +79,19 @@ impl UiOutput {
Ok(UiOutput::Paged { child, child_stdin })
}
fn new_builtin_paged() -> streampager::Result<UiOutput> {
fn new_builtin_paged(config: &StreampagerConfig) -> streampager::Result<UiOutput> {
// 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<StreampagerWrappingMode> 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| {

View file

@ -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.