Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

config: set $LESS and $LESSCHARSET even if $PAGER is set #3657

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* The `commit_summary_no_branches` template is superseded by
`templates.branch_list`.

* The `$LESS` and `$LESSCHARSET` environment variables are now respected, unless
you have configured a pager (via `ui.pager` or `$PAGER`).

### Deprecations

### New features
Expand Down
95 changes: 74 additions & 21 deletions cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,18 @@ fn env_base() -> config::Config {
builder = builder.set_override("ui.color", "never").unwrap();
}
if let Ok(value) = env::var("PAGER") {
builder = builder.set_override("ui.pager", value).unwrap();
builder = builder
.set_override(
"ui.pager.command",
config::Value::new(
None,
config::ValueKind::Array(vec![config::Value::new(
None,
config::ValueKind::String(value),
)]),
),
Comment on lines +330 to +336
Copy link
Member Author

Choose a reason for hiding this comment

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

This is pretty ugly. Is there a more convenient API in the config crate?

Copy link
Contributor

Choose a reason for hiding this comment

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

.set_override("ui.pager.command", vec![value]) appears to work.
https://docs.rs/config/0.13.4/config/enum.ValueKind.html#impl-From%3CVec%3CT%3E%3E-for-ValueKind

Copy link
Contributor

@ilyagr ilyagr May 10, 2024

Choose a reason for hiding this comment

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

Actually, I'm not sure this is correct if PAGER contains a command with arguments, e.g. PAGER="less -FRX".

I'm not sure whether that is considered a kosher value for PAGER, but it usually worked for most programs, I think. Something did make me switch to using PAGER=less and LESS=... for the options, though.

Copy link
Contributor

@ilyagr ilyagr May 10, 2024

Choose a reason for hiding this comment

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

It's possible this is still worth doing even if it doesn't work for PAGER="less -FRX", I'm unsure how common that is.

Or, we could allow ui.pager.command to be a string (perhaps in a future PR).

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually, I'm not sure this is correct if PAGER contains a command with arguments, e.g. PAGER="less -FRX"?

Good point. PAGER needs to be parsed as if it were executed by the shell. It might be simpler to just special case PAGER=less.

Copy link
Contributor

@ilyagr ilyagr Nov 8, 2024

Choose a reason for hiding this comment

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

Good idea to compare to Git. I found

https://github.com/git/git/blob/8f8d6eee531b3fa1a8ef14f169b0cb5035f7a772/Makefile#L357

Not sure what LV is about, but just following what they do seems OK to me.

Update: LV is about https://linux.die.net/man/1/lv. Haven't heard of it before. It seems obsolete (?), though it is still available in Debian.

Copy link
Contributor

@ilyagr ilyagr Nov 8, 2024

Choose a reason for hiding this comment

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

I think git sets it regardless of PAGER, anytime LESS is unset. So, it'd again break PAGER="less -RX".

Copy link
Contributor

@ilyagr ilyagr Nov 8, 2024

Choose a reason for hiding this comment

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

But I somehow can't reproduce this. I unset LESS, core.pager seems unset, but PAGER="less -R" git diff acts differently from PAGER="less -R +F" git diff. OTOH, PAGER="less +R" git diff prints color. So, I don't know what's going on with Git.

Update: Never mind, I think this matches the advertised Git behavior. BTW, it's documented in https://git-scm.com/docs/git-config#Documentation/git-config.txt-corepager.

I suppose if Git breaks "less -RX", we can too. The user can override it with "less -RX +F". At least, at the moment, copying Git seems like the easiest solution. We could be more conservative and only set LESS=R, but maybe it's not worth it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Re @yuja 's options from https://github.com/martinvonz/jj/pull/3657/files#r1833607331, I think setting LESS whenever it is unset would be between 2 and 3.

Copy link
Contributor

Choose a reason for hiding this comment

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

Re @yuja 's options from https://github.com/martinvonz/jj/pull/3657/files#r1833607331, I think setting LESS whenever it is unset would be between 2 and 3.

yes, it would be relatively easy because we just need to update Ui::new_paged().

In my list, 1-3 would be handled in config layer. 4 needs more context.

)
.unwrap();
}
if let Ok(value) = env::var("VISUAL") {
builder = builder.set_override("ui.editor", value).unwrap();
Expand Down Expand Up @@ -441,7 +452,10 @@ pub enum CommandNameAndArgs {
String(String),
Vec(NonEmptyCommandArgsVec),
Structured {
#[serde(default)]
env: HashMap<String, String>,
#[serde(default)]
env_default: HashMap<String, String>,
command: NonEmptyCommandArgsVec,
},
}
Expand All @@ -468,6 +482,7 @@ impl CommandNameAndArgs {
}
CommandNameAndArgs::Structured {
env: _,
env_default: _,
command: cmd,
} => (Cow::Borrowed(&cmd.0[0]), Cow::Borrowed(&cmd.0[1..])),
}
Expand All @@ -477,7 +492,15 @@ impl CommandNameAndArgs {
pub fn to_command(&self) -> Command {
let (name, args) = self.split_name_and_args();
let mut cmd = Command::new(name.as_ref());
if let CommandNameAndArgs::Structured { env, .. } = self {
if let CommandNameAndArgs::Structured {
env_default, env, ..
} = self
{
for (key, val) in env_default {
if std::env::var(key).is_err() {
cmd.env(key, val);
}
}
cmd.envs(env);
}
cmd.args(args.as_ref());
Expand All @@ -497,8 +520,15 @@ impl fmt::Display for CommandNameAndArgs {
CommandNameAndArgs::String(s) => write!(f, "{s}"),
// TODO: format with shell escapes
CommandNameAndArgs::Vec(a) => write!(f, "{}", a.0.join(" ")),
CommandNameAndArgs::Structured { env, command } => {
for (k, v) in env.iter() {
CommandNameAndArgs::Structured {
env_default,
env,
command,
} => {
for (k, v) in env_default {
Copy link
Contributor

Choose a reason for hiding this comment

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

Optional: Perhaps k.=v would work here? Or k~=v? Or (k=v)?

write!(f, "{k}={v} ")?;
}
for (k, v) in env {
write!(f, "{k}={v} ")?;
}
write!(f, "{}", command.0.join(" "))
Expand Down Expand Up @@ -533,21 +563,20 @@ mod tests {

#[test]
fn test_command_args() {
let config_text = r#"
Copy link
Contributor

Choose a reason for hiding this comment

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

Optional: I'd call this config_toml.

empty_array = []
empty_string = ""
"array" = ["emacs", "-nw"]
"string" = "emacs -nw"
structured = { command = ["emacs", "-nw"] }
with_env_default = { env_default = { KEY1 = "value1", KEY2 = "value2" }, command = ["emacs", "-nw"] }
with_env = { env = { KEY1 = "value1", KEY2 = "value2" }, command = ["emacs", "-nw"] }
"#;
let config = config::Config::builder()
.set_override("empty_array", Vec::<String>::new())
.unwrap()
.set_override("empty_string", "")
.unwrap()
.set_override("array", vec!["emacs", "-nw"])
.unwrap()
.set_override("string", "emacs -nw")
.unwrap()
.set_override("structured.env.KEY1", "value1")
.unwrap()
.set_override("structured.env.KEY2", "value2")
.unwrap()
.set_override("structured.command", vec!["emacs", "-nw"])
.unwrap()
.add_source(config::File::from_str(
config_text,
config::FileFormat::Toml,
))
.build()
.unwrap();

Expand Down Expand Up @@ -583,16 +612,40 @@ mod tests {
assert_eq!(
command_args,
CommandNameAndArgs::Structured {
env_default: hashmap! {},
env: hashmap! {},
command: NonEmptyCommandArgsVec(["emacs", "-nw",].map(|s| s.to_owned()).to_vec())
}
);
let (name, args) = command_args.split_name_and_args();
assert_eq!(name, "emacs");
assert_eq!(args, ["-nw"].as_ref());

let command_args: CommandNameAndArgs = config.get("with_env_default").unwrap();
Copy link
Contributor

@ilyagr ilyagr May 10, 2024

Choose a reason for hiding this comment

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

Optional: I was going to suggest adding a test with https://doc.rust-lang.org/std/env/fn.set_var.html here, but I'm not so sure after reading the safety warning there. cargo-nextest is, in principle, multithreaded.

You could also set up a test that sets some variable that very likely exists, like PATH or CARGO. I guess to make the test reliable, you could check if that variable is set first, and skip the test if it isn't.

assert_eq!(
command_args,
CommandNameAndArgs::Structured {
env_default: hashmap! {
"KEY1".to_string() => "value1".to_string(),
"KEY2".to_string() => "value2".to_string(),
},
env: hashmap! {},
command: NonEmptyCommandArgsVec(["emacs", "-nw",].map(|s| s.to_owned()).to_vec())
}
);

let command_args: CommandNameAndArgs = config.get("with_env").unwrap();
assert_eq!(
command_args,
CommandNameAndArgs::Structured {
env_default: hashmap! {},
env: hashmap! {
"KEY1".to_string() => "value1".to_string(),
"KEY2".to_string() => "value2".to_string(),
},
command: NonEmptyCommandArgsVec(["emacs", "-nw",].map(|s| s.to_owned()).to_vec())
}
);
let (name, args) = command_args.split_name_and_args();
assert_eq!(name, "emacs");
assert_eq!(args, ["-nw"].as_ref());
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion cli/src/config/misc.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ allow-filesets = false
always-allow-large-revsets = false
diff-instructions = true
paginate = "auto"
pager = { command = ["less", "-FRX"], env = { LESSCHARSET = "utf-8" } }
pager = { command = ["less"], env_default = { LESS="-FRX", LESSCHARSET = "utf-8" } }
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't it better to override the LESS variable as before?

I don't remember exactly, but some default-installed ~/.bashrc might set LESS=... If it didn't contain -R, jj's default would break.

Copy link
Contributor

Choose a reason for hiding this comment

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

Firstly, I think you don't need the - in LESS.

I'm not sure about Yuya's point. I have LESS set to FRX --mouse --wheel-lines=2, and I wouldn't like it overridden entirely. Perhaps we could print a warning after less quits if FRX was not in LESS and it was set? Perhaps we could prepend FRX to the variable?

Copy link
Contributor

Choose a reason for hiding this comment

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

I also think that, while it might make sense to add -R to LESS, forcibly adding -F (and possibly also -X) might be wrong if the user explicitly did not include them. For example, let's say we launch jj from a file manager like Midnight Commander or ranger. The user might then be relying on less not quitting after one screen to see the output at all. If we forcibly add -F, that would break this use-case. Forcibly adding -X might break it too, but I'm less sure.

So, in my opinion, if we want to hack around weird default values for LESS, we could do the analogue of LESS="R $LESS", but probably no more than that.

Copy link
Contributor

Choose a reason for hiding this comment

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

I now think that if some default ~/.bashrc sets LESS to some value that doesn't contain R, that's a bug in that default config. I don't think it happens often in practice.

So, it sounds like I'm converging to something that's very close to this PR as is. (See also https://github.com/martinvonz/jj/pull/3657/files#r1833620765).

One thing I'd now do differently is to move pager.env_default to pager_env_default, outside of pager, so ui.pager is a string again (by default, at least). If we set these environment variables whenever they are unset, it might be more consistent to set them even if the user sets ui.pager="less" on Windows. But the difference between that and the current behavior of the PR is not so great, it's more of a minor detail.

log-word-wrap = false
log-synthetic-elided-nodes = true

Expand Down