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.com".to_string(), Vec::new()))
57 } else {
58 Some(("less".to_string(), vec!["-R".to_string()]))
60 }
61}
62
63fn 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 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 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 #[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 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 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 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}