1use anyhow::Result;
2use std::ffi::OsStr;
3use std::io::{self, IsTerminal, Write};
4use std::process::{Command, Stdio};
5
6const DEFAULT_PAGER: &str = "less";
7const DEFAULT_PAGER_ARGS: &[&str] = &["-F", "-R", "-X"];
8
9pub fn print_or_page(output: &str) -> Result<()> {
10 if !should_use_pager(
11 std::io::stdin().is_terminal(),
12 std::io::stdout().is_terminal(),
13 std::env::var_os("TERM").as_deref(),
14 ) {
15 return print_direct(output);
16 }
17
18 match page_with_less(output) {
19 Ok(()) => Ok(()),
20 Err(err) if pager_missing(&err) => print_direct(output),
21 Err(err) => Err(err),
22 }
23}
24
25fn should_use_pager(
26 stdin_is_terminal: bool,
27 stdout_is_terminal: bool,
28 term: Option<&OsStr>,
29) -> bool {
30 stdin_is_terminal
31 && stdout_is_terminal
32 && term.is_none_or(|value| !value.eq_ignore_ascii_case(OsStr::new("dumb")))
33}
34
35fn print_direct(output: &str) -> Result<()> {
36 let mut stdout = io::stdout().lock();
37 stdout.write_all(output.as_bytes())?;
38 stdout.flush()?;
39 Ok(())
40}
41
42fn page_with_less(output: &str) -> Result<()> {
43 let mut child = Command::new(DEFAULT_PAGER)
44 .args(DEFAULT_PAGER_ARGS)
45 .stdin(Stdio::piped())
46 .spawn()?;
47
48 if let Some(mut stdin) = child.stdin.take() {
49 match stdin.write_all(output.as_bytes()) {
50 Ok(()) => {}
51 Err(err) if err.kind() == io::ErrorKind::BrokenPipe => {}
52 Err(err) => return Err(err.into()),
53 }
54 }
55
56 let _ = child.wait()?;
57 Ok(())
58}
59
60fn pager_missing(err: &anyhow::Error) -> bool {
61 err.downcast_ref::<io::Error>()
62 .is_some_and(|io_err| io_err.kind() == io::ErrorKind::NotFound)
63}
64
65#[cfg(test)]
66mod tests {
67 use super::should_use_pager;
68 use std::ffi::OsStr;
69
70 #[test]
71 fn pager_requires_tty_input_and_output() {
72 assert!(!should_use_pager(
73 false,
74 true,
75 Some(OsStr::new("xterm-256color"))
76 ));
77 assert!(!should_use_pager(
78 true,
79 false,
80 Some(OsStr::new("xterm-256color"))
81 ));
82 assert!(should_use_pager(
83 true,
84 true,
85 Some(OsStr::new("xterm-256color"))
86 ));
87 }
88
89 #[test]
90 fn pager_skips_dumb_terminals() {
91 assert!(!should_use_pager(true, true, Some(OsStr::new("dumb"))));
92 assert!(should_use_pager(true, true, None));
93 }
94}