1use std::io::{IsTerminal, Write};
12use std::process::{Command, Stdio};
13
14pub trait Pager {
17 fn show(&self, content: &str) -> std::io::Result<()>;
20}
21
22#[derive(Debug, Default, Clone, Copy)]
25pub struct SystemPager;
26
27fn 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
40fn pager_command() -> Option<(String, Vec<String>)> {
43 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 Some(("less".to_string(), vec!["-R".to_string()]))
58 }
59}
60
61fn 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 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 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 #[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 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 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}