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        Some(("more".to_string(), Vec::new()))
55    } else {
56        // `less -R` keeps ANSI styling readable; pydoc tries `pager` then `less`.
57        Some(("less".to_string(), vec!["-R".to_string()]))
58    }
59}
60
61/// Whether we're attached to a terminal that can host a pager. Port of
62/// `pydoc.get_pager`'s isatty + `TERM in (dumb, emacs)` guards.
63fn can_page() -> bool {
64    if !std::io::stdin().is_terminal() || !std::io::stdout().is_terminal() {
65        return false;
66    }
67    !matches!(
68        std::env::var("TERM").unwrap_or_default().as_str(),
69        "dumb" | "emacs"
70    )
71}
72
73impl Pager for SystemPager {
74    fn show(&self, content: &str) -> std::io::Result<()> {
75        if !can_page() {
76            return plain(content);
77        }
78        let Some((program, args)) = pager_command() else {
79            return plain(content);
80        };
81        // Spawn the pager with our content on its stdin, inheriting stdout/stderr
82        // so it can drive the terminal. Any failure (no such program, broken
83        // pipe from the user quitting early) falls back to plain output.
84        let child = Command::new(&program)
85            .args(&args)
86            .stdin(Stdio::piped())
87            .spawn();
88        let mut child = match child {
89            Ok(child) => child,
90            Err(_) => return plain(content),
91        };
92        if let Some(mut stdin) = child.stdin.take() {
93            // A pager the user quits early closes the pipe; that's not an error.
94            let _ = stdin.write_all(content.as_bytes());
95            drop(stdin);
96        }
97        child.wait()?;
98        Ok(())
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use std::sync::Mutex;
106
107    /// A test pager that records what it was asked to show.
108    #[derive(Default)]
109    struct RecordingPager {
110        shown: Mutex<Vec<String>>,
111    }
112
113    impl Pager for RecordingPager {
114        fn show(&self, content: &str) -> std::io::Result<()> {
115            self.shown.lock().unwrap().push(content.to_string());
116            Ok(())
117        }
118    }
119
120    #[test]
121    fn custom_pager_receives_content() {
122        let pager = RecordingPager::default();
123        pager.show("hello").unwrap();
124        assert_eq!(
125            pager.shown.lock().unwrap().as_slice(),
126            &["hello".to_string()]
127        );
128    }
129
130    #[test]
131    fn explicit_pager_env_var_wins_and_splits_args() {
132        // Serialised via the env guard below; MANPAGER takes precedence over PAGER.
133        let _guard = EnvGuard::set(&[("MANPAGER", Some("myp --opt")), ("PAGER", Some("other"))]);
134        let (program, args) = pager_command().expect("a pager command");
135        assert_eq!(program, "myp");
136        assert_eq!(args, vec!["--opt".to_string()]);
137    }
138
139    #[test]
140    fn falls_back_to_a_platform_default() {
141        let _guard = EnvGuard::set(&[("MANPAGER", None), ("PAGER", None)]);
142        let (program, _) = pager_command().expect("a pager command");
143        assert_eq!(program, if cfg!(windows) { "more" } else { "less" });
144    }
145
146    /// Set/restore env vars around a test. The two env tests share a lock so
147    /// they can't interleave (tests run in parallel threads).
148    struct EnvGuard {
149        previous: Vec<(String, Option<String>)>,
150        _lock: std::sync::MutexGuard<'static, ()>,
151    }
152
153    static ENV_LOCK: Mutex<()> = Mutex::new(());
154
155    impl EnvGuard {
156        fn set(vars: &[(&str, Option<&str>)]) -> Self {
157            let lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
158            let previous = vars
159                .iter()
160                .map(|(key, _)| ((*key).to_string(), std::env::var(key).ok()))
161                .collect();
162            for (key, value) in vars {
163                match value {
164                    Some(value) => std::env::set_var(key, value),
165                    None => std::env::remove_var(key),
166                }
167            }
168            EnvGuard {
169                previous,
170                _lock: lock,
171            }
172        }
173    }
174
175    impl Drop for EnvGuard {
176        fn drop(&mut self) {
177            for (key, value) in &self.previous {
178                match value {
179                    Some(value) => std::env::set_var(key, value),
180                    None => std::env::remove_var(key),
181                }
182            }
183        }
184    }
185}