Skip to main content

rich/
pager.rs

1//! Paging long output through the system pager.
2//!
3//! Port of `rich/pager.py` plus the pager-selection logic upstream inherits from
4//! `pydoc.get_pager` (rich's `SystemPager` simply delegates to `pydoc.pager`).
5//!
6//! [`Console::page`](crate::console::Console::page) buffers everything printed
7//! inside a closure and hands it to a [`Pager`] — the Rust analogue of upstream's
8//! `with console.pager():` block, matching this crate's other capture-style
9//! methods (`capture`, `export_text`, …).
10
11use std::io::{IsTerminal, Write};
12use std::process::{Command, Stdio};
13
14/// Something that can display a block of content a screenful at a time. Mirrors
15/// `rich.pager.Pager`.
16pub trait Pager {
17    /// Show `content`, returning an error only if the content could not be
18    /// displayed at all.
19    fn show(&self, content: &str) -> std::io::Result<()>;
20}
21
22/// Pages through the pager program installed on the system. Mirrors
23/// `rich.pager.SystemPager` (which defers to `pydoc.pager`).
24#[derive(Debug, Default, Clone, Copy)]
25pub struct SystemPager;
26
27/// Write `content` straight to stdout — `pydoc`'s `plain_pager`, used when
28/// there's no terminal to page in (piped/redirected output, `TERM=dumb`) or when
29/// no pager program could be started.
30fn plain(content: &str) -> std::io::Result<()> {
31    let stdout = std::io::stdout();
32    let mut handle = stdout.lock();
33    handle.write_all(content.as_bytes())?;
34    if !content.ends_with('\n') {
35        handle.write_all(b"\n")?;
36    }
37    handle.flush()
38}
39
40/// The pager command to run, as `(program, args)`. Port of `pydoc.get_pager`'s
41/// selection order: `MANPAGER`, then `PAGER`, then a platform default.
42fn pager_command() -> Option<(String, Vec<String>)> {
43    // An explicit pager wins. It's a command *line*, so split off any arguments
44    // (e.g. `PAGER="less -R"`), matching pydoc handing the string to a shell.
45    for variable in ["MANPAGER", "PAGER"] {
46        if let Ok(value) = std::env::var(variable) {
47            let mut parts = value.split_whitespace().map(str::to_string);
48            if let Some(program) = parts.next() {
49                return Some((program, parts.collect()));
50            }
51        }
52    }
53    if cfg!(windows) {
54        // Unlike pydoc's shell invocation, Command only fills in `.exe` on
55        // Windows. The system pager is `more.com`, so name it explicitly.
56        Some(("more.com".to_string(), Vec::new()))
57    } else {
58        // `less -R` keeps ANSI styling readable; pydoc tries `pager` then `less`.
59        Some(("less".to_string(), vec!["-R".to_string()]))
60    }
61}
62
63/// Whether we're attached to a terminal that can host a pager. Port of
64/// `pydoc.get_pager`'s isatty + `TERM in (dumb, emacs)` guards.
65fn can_page() -> bool {
66    if !std::io::stdin().is_terminal() || !std::io::stdout().is_terminal() {
67        return false;
68    }
69    !matches!(
70        std::env::var("TERM").unwrap_or_default().as_str(),
71        "dumb" | "emacs"
72    )
73}
74
75impl Pager for SystemPager {
76    fn show(&self, content: &str) -> std::io::Result<()> {
77        if !can_page() {
78            return plain(content);
79        }
80        let Some((program, args)) = pager_command() else {
81            return plain(content);
82        };
83        // Spawn the pager with our content on its stdin, inheriting stdout/stderr
84        // so it can drive the terminal. Any failure (no such program, broken
85        // pipe from the user quitting early) falls back to plain output.
86        let child = Command::new(&program)
87            .args(&args)
88            .stdin(Stdio::piped())
89            .spawn();
90        let mut child = match child {
91            Ok(child) => child,
92            Err(_) => return plain(content),
93        };
94        if let Some(mut stdin) = child.stdin.take() {
95            // A pager the user quits early closes the pipe; that's not an error.
96            let _ = stdin.write_all(content.as_bytes());
97            drop(stdin);
98        }
99        child.wait()?;
100        Ok(())
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use std::sync::Mutex;
108
109    /// A test pager that records what it was asked to show.
110    #[derive(Default)]
111    struct RecordingPager {
112        shown: Mutex<Vec<String>>,
113    }
114
115    impl Pager for RecordingPager {
116        fn show(&self, content: &str) -> std::io::Result<()> {
117            self.shown.lock().unwrap().push(content.to_string());
118            Ok(())
119        }
120    }
121
122    #[test]
123    fn custom_pager_receives_content() {
124        let pager = RecordingPager::default();
125        pager.show("hello").unwrap();
126        assert_eq!(
127            pager.shown.lock().unwrap().as_slice(),
128            &["hello".to_string()]
129        );
130    }
131
132    #[test]
133    fn explicit_pager_env_var_wins_and_splits_args() {
134        // Serialised via the env guard below; MANPAGER takes precedence over PAGER.
135        let _guard = EnvGuard::set(&[("MANPAGER", Some("myp --opt")), ("PAGER", Some("other"))]);
136        let (program, args) = pager_command().expect("a pager command");
137        assert_eq!(program, "myp");
138        assert_eq!(args, vec!["--opt".to_string()]);
139    }
140
141    #[test]
142    fn falls_back_to_a_platform_default() {
143        let _guard = EnvGuard::set(&[("MANPAGER", None), ("PAGER", None)]);
144        let (program, _) = pager_command().expect("a pager command");
145        assert_eq!(program, if cfg!(windows) { "more.com" } else { "less" });
146    }
147
148    #[test]
149    fn pager_env_var_wins_when_manpager_is_empty() {
150        let _guard = EnvGuard::set(&[("MANPAGER", Some("")), ("PAGER", Some("myp --plain"))]);
151        let (program, args) = pager_command().expect("a pager command");
152        assert_eq!(program, "myp");
153        assert_eq!(args, vec!["--plain".to_string()]);
154    }
155
156    #[cfg(windows)]
157    #[test]
158    fn windows_default_pager_displays_piped_content() {
159        let _guard = EnvGuard::set(&[("MANPAGER", None), ("PAGER", None)]);
160        let (program, args) = pager_command().expect("a pager command");
161        // CI has no terminal, so exercise the selected executable directly:
162        // SystemPager::show would correctly take the plain-output fallback.
163        let mut child = Command::new(program)
164            .args(args)
165            .stdin(Stdio::piped())
166            .stdout(Stdio::piped())
167            .stderr(Stdio::piped())
168            .spawn()
169            .expect("the default Windows pager must resolve and start");
170        let content = "rs-rich Windows pager smoke test\r\nsecond line reaches the pager\r\n";
171        child
172            .stdin
173            .take()
174            .unwrap()
175            .write_all(content.as_bytes())
176            .unwrap();
177        let output = child.wait_with_output().unwrap();
178        assert!(
179            output.status.success(),
180            "pager failed: {}",
181            String::from_utf8_lossy(&output.stderr)
182        );
183        assert_eq!(
184            String::from_utf8(output.stdout)
185                .unwrap()
186                .replace("\r\n", "\n")
187                .trim_end_matches('\n'),
188            content.replace("\r\n", "\n").trim_end_matches('\n')
189        );
190    }
191
192    /// Set/restore env vars around a test. The environment tests share a lock so
193    /// they can't interleave (tests run in parallel threads).
194    struct EnvGuard {
195        previous: Vec<(String, Option<String>)>,
196        _lock: std::sync::MutexGuard<'static, ()>,
197    }
198
199    static ENV_LOCK: Mutex<()> = Mutex::new(());
200
201    impl EnvGuard {
202        fn set(vars: &[(&str, Option<&str>)]) -> Self {
203            let lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
204            let previous = vars
205                .iter()
206                .map(|(key, _)| ((*key).to_string(), std::env::var(key).ok()))
207                .collect();
208            for (key, value) in vars {
209                match value {
210                    Some(value) => std::env::set_var(key, value),
211                    None => std::env::remove_var(key),
212                }
213            }
214            EnvGuard {
215                previous,
216                _lock: lock,
217            }
218        }
219    }
220
221    impl Drop for EnvGuard {
222        fn drop(&mut self) {
223            for (key, value) in &self.previous {
224                match value {
225                    Some(value) => std::env::set_var(key, value),
226                    None => std::env::remove_var(key),
227                }
228            }
229        }
230    }
231}