diff --git a/cli/src/config-schema.json b/cli/src/config-schema.json index 69f4c01edd4..539b9f553f3 100644 --- a/cli/src/config-schema.json +++ b/cli/src/config-schema.json @@ -108,6 +108,35 @@ "description": "Pager to use for displaying command output", "default": "less -FRX" }, + "builtin-pager": { + "type": "object", + "description": "':builtin' pager configuration", + "properties": { + "alternate-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 `alternate-screen=\"if-long-or-slow\"`", + "default": 2000 + } + } + }, "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 c5685b38a9b..f0a78932aa4 100644 --- a/cli/src/config/misc.toml +++ b/cli/src/config/misc.toml @@ -41,6 +41,11 @@ show-cryptographic-signatures = false [ui.movement] edit = false +[ui.builtin-pager] +alternate-screen = "never" +long-or-slow-delay-millis = 2000 +wrapping = "anywhere" + [snapshot] max-new-file-size = "1MiB" auto-track = "all()" diff --git a/cli/src/ui.rs b/cli/src/ui.rs index 4a5f2436cab..79cd908c262 100644 --- a/cli/src/ui.rs +++ b/cli/src/ui.rs @@ -81,10 +81,18 @@ 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/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 `alternate-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 @@ -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 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 { + alternate_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.alternate_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), } @@ -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.builtin-pager")?)) } _ => Ok(PagerConfig::External(pager_cmd)), } @@ -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| { diff --git a/docs/config.md b/docs/config.md index c139bd25f06..bbe43810029 100644 --- a/docs/config.md +++ b/docs/config.md @@ -559,8 +559,9 @@ 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,which has +[further configuration options in `ui.builtin-pager` +table](#builtin-pager-configuration). 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: @@ -582,6 +583,46 @@ paginate = "auto" paginate = "never" ``` +### Builtin pager configuration + +Our builtin pager is based on +[`streampager`](https://github.com/markbt/streampager/) +but is configured within `jj`'s config. Here are the possible configuration options. + +#### Wrapping + +Wrapping performed by the pager happens *in addition to* any +wrapping that `jj` itself does. + +```toml +[ui.builtin-pager] +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.builtin-pager] +# 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.