use std::env;
use std::io::{self, IsTerminal, Write};
use std::process::{Child, ChildStdin, Command, Stdio};
pub struct Pager {
inner: Inner,
}
enum Inner {
Stdout,
Pager {
stdin: Option<ChildStdin>,
child: Child,
broken: bool,
},
}
impl Pager {
pub fn start(repo: Option<&gix::Repository>) -> Self {
let pager = resolve_pager(
io::stdout().is_terminal(),
|key| std::env::var(key),
repo.and_then(read_core_pager),
);
match pager.and_then(|p| spawn(&p)) {
Some((stdin, child)) => Self {
inner: Inner::Pager {
stdin: Some(stdin),
child,
broken: false,
},
},
None => Self {
inner: Inner::Stdout,
},
}
}
}
fn resolve_pager(
stdout_is_tty: bool,
env_var: impl Fn(&str) -> Result<String, env::VarError>,
core_pager: Option<String>,
) -> Option<String> {
if !stdout_is_tty {
return None;
}
if env_var("GIT_META_PAGER_IN_USE").is_ok() {
return None;
}
let from_env = |key: &str| env_var(key).ok().filter(|s| !s.is_empty());
let pager = from_env("GIT_PAGER")
.or_else(|| core_pager.filter(|s| !s.is_empty()))
.or_else(|| from_env("PAGER"))
.unwrap_or_else(|| "less".to_string());
if pager.is_empty() || pager == "cat" {
return None;
}
Some(pager)
}
fn read_core_pager(repo: &gix::Repository) -> Option<String> {
let config = repo.config_snapshot();
config
.string("core.pager")
.map(|v| v.to_string())
.filter(|s| !s.is_empty())
}
fn spawn(pager: &str) -> Option<(ChildStdin, Child)> {
let mut cmd = Command::new("sh");
cmd.arg("-c").arg(pager);
cmd.stdin(Stdio::piped());
if env::var_os("LESS").is_none() {
cmd.env("LESS", "FRX");
}
if env::var_os("LV").is_none() {
cmd.env("LV", "-c");
}
if env::var_os("MORE").is_none() {
cmd.env("MORE", "FRX");
}
cmd.env("GIT_META_PAGER_IN_USE", "1");
let mut child = cmd.spawn().ok()?;
let stdin = child.stdin.take()?;
Some((stdin, child))
}
impl Write for Pager {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let result = match &mut self.inner {
Inner::Stdout => io::stdout().write(buf),
Inner::Pager {
stdin: Some(stdin),
broken: false,
..
} => stdin.write(buf),
_ => return Ok(buf.len()),
};
match result {
Err(e) if e.kind() == io::ErrorKind::BrokenPipe => {
if let Inner::Pager { broken, .. } = &mut self.inner {
*broken = true;
}
Ok(buf.len())
}
other => other,
}
}
fn flush(&mut self) -> io::Result<()> {
let result = match &mut self.inner {
Inner::Stdout => io::stdout().flush(),
Inner::Pager {
stdin: Some(stdin),
broken: false,
..
} => stdin.flush(),
_ => return Ok(()),
};
match result {
Err(e) if e.kind() == io::ErrorKind::BrokenPipe => {
if let Inner::Pager { broken, .. } = &mut self.inner {
*broken = true;
}
Ok(())
}
other => other,
}
}
}
impl Drop for Pager {
fn drop(&mut self) {
if let Inner::Pager { stdin, child, .. } = &mut self.inner {
*stdin = None;
let _ = child.wait();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
fn fake_env<'a>(
pairs: &'a [(&'a str, &'a str)],
) -> impl Fn(&str) -> Result<String, env::VarError> + 'a {
let map: HashMap<&str, &str> = pairs.iter().copied().collect();
move |k: &str| {
map.get(k)
.map(|v| (*v).to_string())
.ok_or(env::VarError::NotPresent)
}
}
#[test]
fn returns_none_when_stdout_is_not_a_tty() {
let pager = resolve_pager(false, fake_env(&[]), None);
assert_eq!(pager, None);
}
#[test]
fn defaults_to_less_when_nothing_is_configured() {
let pager = resolve_pager(true, fake_env(&[]), None);
assert_eq!(pager.as_deref(), Some("less"));
}
#[test]
fn git_pager_env_takes_precedence_over_everything_else() {
let pager = resolve_pager(
true,
fake_env(&[("GIT_PAGER", "delta"), ("PAGER", "more")]),
Some("most".to_string()),
);
assert_eq!(pager.as_deref(), Some("delta"));
}
#[test]
fn core_pager_config_beats_pager_env() {
let pager = resolve_pager(
true,
fake_env(&[("PAGER", "more")]),
Some("delta".to_string()),
);
assert_eq!(pager.as_deref(), Some("delta"));
}
#[test]
fn pager_env_used_when_no_higher_priority_value() {
let pager = resolve_pager(true, fake_env(&[("PAGER", "bat")]), None);
assert_eq!(pager.as_deref(), Some("bat"));
}
#[test]
fn empty_string_in_chain_falls_through_to_next_link() {
let pager = resolve_pager(
true,
fake_env(&[("GIT_PAGER", ""), ("PAGER", "more")]),
Some("delta".to_string()),
);
assert_eq!(pager.as_deref(), Some("delta"));
}
#[test]
fn cat_disables_paging_entirely() {
let pager = resolve_pager(true, fake_env(&[("GIT_PAGER", "cat")]), None);
assert_eq!(pager, None);
}
#[test]
fn nested_invocation_skips_paging() {
let pager = resolve_pager(true, fake_env(&[("GIT_META_PAGER_IN_USE", "1")]), None);
assert_eq!(pager, None);
}
#[test]
fn multi_word_pager_command_is_preserved_for_shell() {
let pager = resolve_pager(true, fake_env(&[("PAGER", "less -SR")]), None);
assert_eq!(pager.as_deref(), Some("less -SR"));
}
}