use std::io::{self, IsTerminal, Write};
use std::process::{Child, Command, Stdio};
pub enum PagerWriter {
Pager {
child: Child,
stdin: Option<std::process::ChildStdin>,
},
Stdout(
io::StdoutLock<'static>,
),
}
impl Write for PagerWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self {
Self::Pager { stdin: Some(s), .. } => match s.write(buf) {
Err(e) if e.kind() == io::ErrorKind::BrokenPipe => Ok(0),
other => other,
},
Self::Pager { stdin: None, .. } => Ok(0), Self::Stdout(out) => out.write(buf),
}
}
fn flush(&mut self) -> io::Result<()> {
match self {
Self::Pager { stdin: Some(s), .. } => s.flush(),
Self::Pager { stdin: None, .. } => Ok(()),
Self::Stdout(out) => out.flush(),
}
}
}
impl Drop for PagerWriter {
fn drop(&mut self) {
self.finish();
}
}
impl PagerWriter {
pub fn finish(&mut self) {
if let Self::Pager { stdin, child } = self {
*stdin = None;
let _ = child.wait();
}
}
}
pub fn is_broken_pipe(err: &anyhow::Error) -> bool {
err.chain().any(|cause| {
cause
.downcast_ref::<io::Error>()
.is_some_and(|e| e.kind() == io::ErrorKind::BrokenPipe)
})
}
pub fn create_pager(config_pager: Option<&str>) -> PagerWriter {
create_pager_in(&PagerEnv::from_process(), config_pager)
}
struct PagerEnv {
is_tty: bool,
no_pager: bool,
pager: Option<String>,
}
impl PagerEnv {
fn from_process() -> Self {
Self {
is_tty: io::stdout().is_terminal(),
no_pager: std::env::var_os("NO_PAGER").is_some(),
pager: std::env::var("PAGER").ok(),
}
}
}
fn create_pager_in(env: &PagerEnv, config_pager: Option<&str>) -> PagerWriter {
if !env.is_tty {
return PagerWriter::Stdout(io::stdout().lock());
}
if env.no_pager {
return PagerWriter::Stdout(io::stdout().lock());
}
let pager_cmd = config_pager
.map(String::from)
.or_else(|| env.pager.clone())
.unwrap_or_else(|| "less".to_string());
if pager_cmd.is_empty() {
return PagerWriter::Stdout(io::stdout().lock());
}
let parts = match shell_words::split(&pager_cmd) {
Ok(parts) if !parts.is_empty() => parts,
_ => return PagerWriter::Stdout(io::stdout().lock()),
};
let (program, args) = (parts[0].as_str(), &parts[1..]);
let mut cmd = Command::new(program);
cmd.args(args).stdin(Stdio::piped());
if std::env::var_os("LESS").is_none() {
cmd.env("LESS", "FRX");
}
match cmd.spawn() {
Ok(mut child) => {
let stdin = child.stdin.take();
PagerWriter::Pager { child, stdin }
}
Err(_) => {
PagerWriter::Stdout(io::stdout().lock())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_broken_pipe() {
let err = anyhow::anyhow!(io::Error::new(io::ErrorKind::BrokenPipe, "pipe broke"));
assert!(is_broken_pipe(&err));
}
#[test]
fn test_is_broken_pipe_nested() {
let inner = io::Error::new(io::ErrorKind::BrokenPipe, "pipe broke");
let err = anyhow::anyhow!(inner).context("writing output");
assert!(is_broken_pipe(&err));
}
#[test]
fn test_is_not_broken_pipe() {
let err = anyhow::anyhow!("some other error");
assert!(!is_broken_pipe(&err));
let err = anyhow::anyhow!(io::Error::new(io::ErrorKind::NotFound, "not found"));
assert!(!is_broken_pipe(&err));
}
fn env(is_tty: bool, no_pager: bool, pager: Option<&str>) -> PagerEnv {
PagerEnv {
is_tty,
no_pager,
pager: pager.map(String::from),
}
}
#[test]
fn test_create_pager_non_tty() {
let writer = create_pager_in(&env(false, false, None), None);
assert!(matches!(writer, PagerWriter::Stdout(_)));
}
#[test]
fn test_create_pager_with_config_non_tty() {
let writer = create_pager_in(&env(false, false, Some("less")), Some("less -R"));
assert!(matches!(writer, PagerWriter::Stdout(_)));
}
#[test]
fn test_create_pager_tty_no_pager_wins() {
let writer = create_pager_in(&env(true, true, Some("less")), Some("less -R"));
assert!(matches!(writer, PagerWriter::Stdout(_)));
}
#[test]
fn test_create_pager_tty_empty_command_falls_back() {
let writer = create_pager_in(&env(true, false, None), Some(""));
assert!(matches!(writer, PagerWriter::Stdout(_)));
let writer = create_pager_in(&env(true, false, Some("")), None);
assert!(matches!(writer, PagerWriter::Stdout(_)));
}
#[test]
fn test_create_pager_tty_unparsable_command_falls_back() {
let writer = create_pager_in(&env(true, false, None), Some("less '"));
assert!(matches!(writer, PagerWriter::Stdout(_)));
}
#[test]
fn test_create_pager_tty_spawn_failure_falls_back() {
let writer = create_pager_in(
&env(true, false, None),
Some("/nonexistent/rledger-test-pager-binary"),
);
assert!(matches!(writer, PagerWriter::Stdout(_)));
}
#[test]
fn test_pager_writer_stdout_write() {
let mut writer = PagerWriter::Stdout(io::stdout().lock());
let result = writer.write(b"test");
assert!(result.is_ok());
}
}