Skip to content

Conversation

@spectral54
Copy link
Contributor

@spectral54 spectral54 commented Oct 23, 2025

When $PAGER is set in the environment, jj uses that instead of the default (:builtin on Windows, less -FRX everywhere else). Commonly, users will have PAGER=less in their environment for various reasons, and this is respected by jj. This means that every jj command, even one that only outputs one or two lines, will still invoke a full screen pager. It also means that every jj command which uses escape sequences for color, which is most of them, will be output through a pager that doesn't handle that well, so users see output that looks like this, which isn't very readable:

ESC[1mESC[38;5;2m@ESC[0m  ESC[1mESC[38;5;13mspmESC[38;5;8mwzlkq...

To fix this issue that new users stumble upon, ignore $PAGER from the environment, and always use our per-platform default. Users can override this and select whatever pager they want by setting ui.pager in their jj config.

This seems like the least bad option for resolving #3502. The cases I considered were:

  1. User doesn't have PAGER set. No change.
  2. User has PAGER=less in their environment. We'll still run less, just with -FRX, so this seems fine. This case is surprisingly common.
  3. User has PAGER set because they prefer another pager. We'll ignore that preference and run less -FRX.
  4. User has PAGER set because less isn't available on their platform. This is uncommon except for Windows, where we'll run :builtin instead of less -FRX by default anyway.

This may cause some users who have intentionally set and configured PAGER to be frustrated that we aren't respecting that value, but it's generally not possible to respect that value in all cases and have a consistent and usable experience out of the box for the majority of users.

Alternatives considered

  1. Disable color and OSC8 hyperlinks if PAGER is set, since we can't be sure the pager supports the color codes.
  2. Don't paginate by default if PAGER is set. This seems counterintuitive, but would at least resolve the problem. Users would assume that the jj CLI doesn't support paginating, and either wrap it in a pager themselves (this is a bad outcome) or find ui.pager and change the setting.
  3. Set LESS (iff it's not set already), then invoke PAGER. This means that users setting things like LESS=i breaks our output as well, and cases where PAGER isn't 'less' aren't fixed.

Fixes #3502

Checklist

If applicable:

  • I have updated CHANGELOG.md
  • I have updated the documentation (README.md, docs/, demos/)
  • I have updated the config schema (cli/src/config-schema.json)
  • I have added/updated tests to cover my changes

@spectral54 spectral54 requested a review from a team as a code owner October 23, 2025 01:10
Copy link
Contributor

@PhilipMetzger PhilipMetzger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a deprecation process question and a minor nit

Comment on lines -673 to -675
if let Ok(value) = env::var("PAGER") {
layer.set_value("ui.pager", value).unwrap();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think we can sensibly deprecate this in 3 releases, so currently adding a warning here may be the option before straight up removing it. I'm up making the removal processes shorter if you deem this to hard for Google.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we need a deprecation process and multiple releases? I consider the current behavior to be a bug. The only ones who would benefit from a deprecation warning would be those that intentionally set $PAGER to some non-less value. I don't have any actual metrics, but this is likely relatively small (vanishingly small amongst the general population, but only 'relatively' small amongst early jj adopters, I'm assuming).

Contrast with the number of users broken by this bug, whose first experience with jj is that it's completely unusable and they start off with a very negative experience. We can override this at Google and never show the deprecation warning at all, so there's no pressure to shorten this time period from us. But as jj usage is growing outside of Google, I think we should have the out-of-the-box experience be the best it can be, even if it means a sudden change in behavior (a slight regression) for a small subset of current users.

(Also, I'm just not sure how to even implement the deprecation warning: we can't check here because we don't know what the value of ui.pager is going to be from the user's config; we can't check that $PAGER is set when using ui.pager, because if users follow the advice to preserve $PAGER, they'll set ui.pager to be $PAGER... what we'd need is a check when loading the user configs that ui.pager ISN'T set in that config layer but IS set in $PAGER; I don't know if we have anything like that kind of logic already)

} else if let Ok(value) = env::var("EDITOR") {
layer.set_value("ui.editor", value).unwrap();
}
// [#3502] Intentionally NOT respecting $PAGER here.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Move the comment to the place where you removed the code and explain the decision a bit more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Elaborated a little bit and converted [#3502] to a link to the issue. I intentionally put it at the bottom because if someone is going to come and try to re-add $PAGER handling, they're most likely to put it at the bottom of the function, so it's more likely to be seen here.

Let me know if you disagree, I don't mind moving it back to the original location, it's where I had it originally.

@ilyagr
Copy link
Contributor

ilyagr commented Oct 23, 2025

I have mixed feelings about this (might go into more details later about various use-cases; I'm not sure how common they are or even a good idea), but it seems worth trying just to see if it makes people happier or angrier and from the general principle I like of trying to make more people's first impression of jj better.

One suggestion I have is to create a new :envpager "pager", in addition to :builtin, that uses the old logic and $PAGER. Then, it'd be easier for people to restore the old behavior (also for us to change our mind about what the default should be on different systems, if needed).

I'm not sure that this is absolutely necessary, but it seems like an easy addition with upsides.

Update: There are two main motivations I have for suggesting this:

  • It provides a form answer to anyone unhappy about this change: "set pager=":envpager" in your config".
  • I might want to in fact do that. In brief, my use-case is that I set up less -FR as my pager. I then have
# In ~/.config/lesskey
^q toggle-option -redraw-screen\nq

Now, I can quit less with q to clear the screen, or I can quit less with ^q to keep the output of less on the screen. I keep meaning to write up this trick.

Of course, I could configure jj with less -FR, but I want this to work the same for git, jj, delta, bat, ...

@spectral54
Copy link
Contributor Author

One suggestion I have is to create a new :envpager "pager", in addition to :builtin, that uses the old logic and $PAGER. Then, it'd be easier for people to restore the old behavior (also for us to change our mind about what the default should be on different systems, if needed).

Good idea. Done!

When `$PAGER` is set in the environment, jj uses that instead of the
default (`:builtin` on Windows, `less -FRX` everywhere else). Commonly,
users will have `PAGER=less` in their environment for various reasons,
and this is respected by jj. This means that every jj command, even one
that only outputs one or two lines, will still invoke a full screen
pager. It also means that every jj command which uses escape sequences
for color, which is most of them, will be output through a pager that
doesn't handle that well, so users see output that looks like this,
which isn't very readable:

```
ESC[1mESC[38;5;2m@ESC[0m  ESC[1mESC[38;5;13mspmESC[38;5;8mwzlkq...
```

To fix this issue that new users stumble upon, ignore `$PAGER` from the
environment, and always use our per-platform default. Users can restore
the previous behavior by setting `ui.pager=:envpager` in their jj
config, or they can of course select whichever pager they like if they
want a different setting for `jj` vs. other commands.

This seems like the least bad option for resolving jj-vcs#3502. The cases I
considered were:

1. User doesn't have `PAGER` set. No change.
2. User has `PAGER=less` in their environment. We'll still run `less`,
just with `-FRX`, so this seems fine. This case is surprisingly common.
3. User has `PAGER` set because they prefer another pager. We'll ignore
that preference and run `less -FRX`, they'll need to discover
`:envpager`.
4. User has `PAGER` set because `less` isn't available on their
platform. This is uncommon except for Windows, where we'll run
`:builtin` instead of `less -FRX` by default anyway.

This may cause some users who have intentionally set and configured
`PAGER` to be frustrated that we aren't respecting that value, but it's
generally not possible to respect that value in all cases _and_ have a
consistent and usable experience out of the box for the majority of
users.

#### Alternatives considered

1. Disable color and OSC8 hyperlinks if `PAGER` is set, since we can't
be sure the pager supports the color codes.
2. Don't paginate by default if `PAGER` is set. This seems
counterintuitive, but would at least resolve the problem. Users would
assume that the `jj` CLI doesn't support paginating, and either wrap it
in a pager themselves (this is a bad outcome) or find `ui.pager` and
change the setting.
3. Set `LESS` (iff it's not set already), then invoke `PAGER`. This
means that users setting things like `LESS=i` breaks our output as well,
and cases where `PAGER` isn't 'less' aren't fixed.

Fixes jj-vcs#3502
use crate::formatter::PlainTextFormatter;

const BUILTIN_PAGER_NAME: &str = ":builtin";
const ENV_PAGER_NAME: &str = ":envpager";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: just ":env"?

Copy link
Contributor

@ilyagr ilyagr Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wondered about this, and thought that :envpager might be nice since it answers the question of "what part of the environment is relevant here?". Environment could also refer to a dotfile, or something weird.

But my opinion is not very strong, and :env is undeniably shorter. The downside of envpager is that it can be confused with pagerenv or env-pager. We can go with whatever you and Kyle agree on.

Ok(Self::External(args))
match (args.as_str(), env::var("PAGER")) {
(Some(BUILTIN_PAGER_NAME), _) => Ok(Self::Builtin(config.get("ui.streampager")?)),
(Some(ENV_PAGER_NAME), Ok(pager_env)) => Ok(Self::External((&pager_env).into())),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

":envpager" should be an error if $PAGER isn't set.

BTW, I personally don't think we need :envpager. Since the user has to configure it manually, they can just set their pager command as usual. I don't think it's common to temporarily override $PAGER. If they do, they can use sh -c $PAGER or something.

Copy link
Contributor

@ilyagr ilyagr Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sh command is a good point. Apparently, this was already discussed in #7821 (comment).

I would still prefer having :env/:envpager. That sh command can take some quoting if you want to put it into a jj config set command and can be messed up by putting extra quotes around $PAGER, both mistakes resulting in subtle problems that may not be immediately apparent. It doesn't work on Windows.

But I'm not entirely sure; if your or others' preference against it is strong enough, we'll probably manage without :env as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My feeling is that many users who previously relied on $PAGER would be happy with the new default or configuring ui.pager to their preferred pager. Maybe we can add :envpager later if there are complaints about that?

BTW, I don't have concern about platform compatibility of sh -c because $PAGER is very Unix thing.

[#7747](https://github.com/jj-vcs/jj/issues/7747)

* Fixed `PAGER` being set in the environment causing ANSI escape sequences to be
displayed to the terminal. [#3502](https://github.com/jj-vcs/jj/issues/3502)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I'd call this a bugfix. Perhaps delete this and add "See [#3502](...) for motivation" to the item in "Breaking Changes"?

}
if !env::var("NO_COLOR").unwrap_or_default().is_empty() {
// "User-level configuration files and per-instance command-line arguments
// should override $NO_COLOR." https://no-color.org/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re commit message: a reminder for later to sync it up with what the commit actually does before merging. Right now, it doesn't mention :envpager AFAICT.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I think I didn't read it carefully enough. Perhaps swap the first and the second paragraph of the commit message? Or just keep it as is if you prefer, it's fine.

layer.set_value("ui.editor", value).unwrap();
}
// Intentionally NOT respecting $PAGER here as it's very likely to create a bad
// out-of-the-box experience for users, see http://github.com/jj-vcs/jj/issues/3502.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd perhaps edit this to "... as it often creates a bad out-of-the-box experience for new users, ..."

Which pager to use can be customized by setting `ui.pager`. When choosing a
pager, ensure that it either supports color codes or that you disable color (see
[Colorizing output](#colorizing-output)).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd add something like: "Unlike some other Unix tools, jj does not respect the PAGER environment variable by default. You can set ui.pager to :envpager to make jj respect PAGER.

Ok(Self::External(args))
match (args.as_str(), env::var("PAGER")) {
(Some(BUILTIN_PAGER_NAME), _) => Ok(Self::Builtin(config.get("ui.streampager")?)),
(Some(ENV_PAGER_NAME), Ok(pager_env)) => Ok(Self::External((&pager_env).into())),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably better to error if env::var("PAGER") is not OK, or is there some reason not to?

This might require finagling with the error types a little bit, you could change the return type to CommandError or perhaps abuse ConfigGetError::NotFound("`PAGER` environment variable"). The former is probably nicer.

By default, jj will paginate output that would scroll off the screen. It does
this by passing output through `less -FRX` on most platforms (on Windows it uses
[the pager](#builtin-pager) that is built-in to jj). It is possible the default
will change to `:builtin` for all platforms in the future.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd delete the last sentence, since you haven't introduced the ui.pager variable yet, and this PR takes us in a different direction anyway. If we decide to, we can change the default to :builtin later either way.

@ilyagr ilyagr changed the title cli: ignore $PAGER in environment cli: ignore $PAGER in environment by default Oct 25, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Setting PAGER=less in the environment causes ANSI escape sequences to appear by default

5 participants