Skip to content

Commit

Permalink
built-in pager: allow configuring streampager options
Browse files Browse the repository at this point in the history
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 #4203 (comment)

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.
  • Loading branch information
ilyagr committed Jan 29, 2025
1 parent 5dbc4f1 commit 004f9f1
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 18 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 29 additions & 0 deletions cli/src/config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,35 @@
"description": "Pager to use for displaying command output",
"default": "less -FRX"
},
"streampager": {
"type": "object",
"description": "':builtin' (streampager-based) pager configuration",
"properties": {
"clear-screen": {
"description": "Whether to clear screen on startup/exit",
"enum": [
"never",
"always",
"if-long-or-slow"
],
"default": "never"
},
"wrapping": {
"description": "Whether to wrap long lines",
"enum": [
"anywhere",
"word",
"none"
],
"default": "anywhere"
},
"long-or-slow-delay-millis": {
"type": "integer",
"description": "Maximum time to wait before rendering full-screen interface if `clear-screen=\"if-long-or-slow\"`",
"default": 2000
}
}
},
"diff": {
"type": "object",
"description": "Options for how diffs are displayed",
Expand Down
5 changes: 5 additions & 0 deletions cli/src/config/misc.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ show-cryptographic-signatures = false
[ui.movement]
edit = false

[ui.streampager]
clear-screen = "never"
long-or-slow-delay-millis = 2000
wrapping = "anywhere"

[snapshot]
max-new-file-size = "1MiB"
auto-track = "all()"
Expand Down
90 changes: 75 additions & 15 deletions cli/src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,18 @@ 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/if https://github.com/facebook/sapling/pull/1011 is merged.
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 `clear-screen=never`, 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
Expand Down Expand Up @@ -266,9 +274,59 @@ pub enum PaginationChoice {
Auto,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize)]
#[serde(rename_all(deserialize = "kebab-case"))]
pub enum StreampagerAlternateScreenMode {
Always,
Never,
IfLongOrSlow,
}

#[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 {
clear_screen: StreampagerAlternateScreenMode,
long_or_slow_delay_millis: u64,
wrapping: StreampagerWrappingMode,
}

impl StreampagerConfig {
fn streampager_interface_mode(&self) -> streampager::config::InterfaceMode {
use streampager::config::InterfaceMode;
use StreampagerAlternateScreenMode::*;
match self.clear_screen {
// InterfaceMode::Direct not implemented
Always => InterfaceMode::FullScreen,
Never => InterfaceMode::Hybrid,
IfLongOrSlow => InterfaceMode::Delayed(std::time::Duration::from_millis(
self.long_or_slow_delay_millis,
)),
}
}
}

enum PagerConfig {
Disabled,
Builtin,
Builtin(StreampagerConfig),
External(CommandNameAndArgs),
}

Expand All @@ -280,7 +338,7 @@ impl PagerConfig {
let pager_cmd: CommandNameAndArgs = config.get("ui.pager")?;
match pager_cmd {
CommandNameAndArgs::String(name) if name == BUILTIN_PAGER_NAME => {
Ok(PagerConfig::Builtin)
Ok(PagerConfig::Builtin(config.get("ui.streampager")?))
}
_ => Ok(PagerConfig::External(pager_cmd)),
}
Expand Down Expand Up @@ -318,16 +376,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| {
Expand Down
44 changes: 42 additions & 2 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -559,8 +559,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:
Expand All @@ -582,6 +582,46 @@ 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/right like `less -S
```

#### Clearing the screen on startup or exit

```toml
[ui.streampager]
# Do not clear screen on exit. Use a full-screen interface for
# long output only. Like `less -FX`.
alternate-screen = "never" # (default).
# Request an alternate screen from the terminal: always use a
# full-screen interface, ask the terminal to clear the screen on
# exit. Like `less +F +X`.
alternate-screen = "always"
# Use the alternate screen if the input is either long or takes
# more than `long-or-slow-delay-millis` to finish. Similar but not
# identical to `less -F +X`
alternate-screen = "if-long-or-slow"

# Only relevant in the `if-long-or-slow` mode
long-or-slow-delay-millis = 2000 # (defaults to 2 seconds)
```


### Processing contents to be paged

If you'd like to pass the output through a formatter e.g.
Expand Down

0 comments on commit 004f9f1

Please sign in to comment.