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 {
if !io::stdout().is_terminal() {
return PagerWriter::Stdout(io::stdout().lock());
}
if std::env::var_os("NO_PAGER").is_some() {
return PagerWriter::Stdout(io::stdout().lock());
}
let pager_cmd = config_pager
.map(String::from)
.or_else(|| std::env::var("PAGER").ok())
.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));
}
#[test]
fn test_create_pager_non_tty() {
let writer = create_pager(None);
assert!(matches!(writer, PagerWriter::Stdout(_)));
}
#[test]
fn test_create_pager_with_config_non_tty() {
let writer = create_pager(Some("less -R"));
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());
}
}