1use std::io::Write;
8use std::process::{Command, Stdio};
9
10#[derive(Debug, Clone)]
19pub struct SystemPager {
20 program: String,
22 args: Vec<String>,
24}
25
26impl SystemPager {
27 pub fn new() -> Self {
31 let pager_cmd = std::env::var("PAGER").unwrap_or_else(|_| default_pager());
32 let (program, args) = split_pager_command(&pager_cmd);
33 Self { program, args }
34 }
35
36 pub fn show(&self, content: &str) -> std::io::Result<()> {
41 let mut child = Command::new(&self.program)
42 .args(&self.args)
43 .stdin(Stdio::piped())
44 .stdout(Stdio::inherit())
45 .stderr(Stdio::inherit())
46 .spawn()?;
47
48 if let Some(ref mut stdin) = child.stdin {
49 stdin.write_all(content.as_bytes())?;
50 }
51
52 drop(child.stdin.take());
54
55 child.wait()?;
56 Ok(())
57 }
58}
59
60impl Default for SystemPager {
61 fn default() -> Self {
62 Self::new()
63 }
64}
65
66#[derive(Debug, Clone)]
75pub struct Pager {
76 enabled: bool,
78 command: String,
80 color: bool,
82}
83
84impl Pager {
85 pub fn new() -> Self {
88 Self {
89 enabled: true,
90 command: std::env::var("PAGER").unwrap_or_else(|_| default_pager()),
91 color: true,
92 }
93 }
94
95 pub fn enabled(mut self, value: bool) -> Self {
97 self.enabled = value;
98 self
99 }
100
101 pub fn command(mut self, cmd: impl Into<String>) -> Self {
103 self.command = cmd.into();
104 self
105 }
106
107 pub fn color(mut self, value: bool) -> Self {
109 self.color = value;
110 self
111 }
112
113 pub fn is_enabled(&self) -> bool {
115 self.enabled
116 }
117
118 pub fn command_str(&self) -> &str {
120 &self.command
121 }
122
123 pub fn is_color(&self) -> bool {
125 self.color
126 }
127
128 pub fn show(&self, content: &str) -> std::io::Result<()> {
133 if !self.enabled {
134 let stdout = std::io::stdout();
136 let mut handle = stdout.lock();
137 handle.write_all(content.as_bytes())?;
138 handle.flush()?;
139 return Ok(());
140 }
141
142 let display = if !self.color {
143 crate::export::strip_ansi_escapes(content)
146 } else {
147 content.to_string()
148 };
149
150 let (program, args) = split_pager_command(&self.command);
151 let pager = SystemPager { program, args };
152 pager.show(&display)
153 }
154}
155
156impl Default for Pager {
157 fn default() -> Self {
158 Self::new()
159 }
160}
161
162#[derive(Debug)]
171pub struct PagerContext {
172 pager: Pager,
174 content: String,
176 enabled: bool,
178}
179
180impl PagerContext {
181 pub fn new(pager: Pager) -> Self {
183 let enabled = pager.enabled;
184 Self {
185 pager,
186 content: String::new(),
187 enabled,
188 }
189 }
190
191 pub fn feed(&mut self, text: &str) {
193 self.content.push_str(text);
194 }
195
196 pub fn flush(&mut self) -> std::io::Result<()> {
199 if !self.content.is_empty() {
200 let result = self.pager.show(&self.content);
201 self.content.clear();
202 result
203 } else {
204 Ok(())
205 }
206 }
207
208 pub fn content(&self) -> &str {
210 &self.content
211 }
212
213 pub fn is_enabled(&self) -> bool {
215 self.enabled
216 }
217}
218
219impl Write for PagerContext {
220 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
221 let s = String::from_utf8_lossy(buf);
222 self.feed(&s);
223 Ok(buf.len())
224 }
225
226 fn flush(&mut self) -> std::io::Result<()> {
227 Ok(())
228 }
229}
230
231impl Drop for PagerContext {
232 fn drop(&mut self) {
233 if self.enabled && !self.content.is_empty() {
234 let _ = self.pager.show(&self.content);
235 }
236 }
237}
238
239fn default_pager() -> String {
245 if cfg!(windows) {
246 "more".into()
247 } else {
248 "less".into()
249 }
250}
251
252fn split_pager_command(cmd: &str) -> (String, Vec<String>) {
257 let mut parts: Vec<&str> = cmd.split_whitespace().collect();
258 if parts.is_empty() {
259 return (String::new(), vec![]);
260 }
261 let program = parts.remove(0).to_string();
262 let args: Vec<String> = parts.into_iter().map(String::from).collect();
263 (program, args)
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn test_system_pager_creation() {
272 let pager = SystemPager::new();
273 assert!(!pager.program.is_empty());
275 }
276
277 #[test]
278 fn test_pager_defaults() {
279 let pager = Pager::new();
280 assert!(pager.is_enabled());
281 assert!(pager.is_color());
282 assert!(!pager.command_str().is_empty());
283 }
284
285 #[test]
286 fn test_pager_builder() {
287 let pager = Pager::new().enabled(false).command("more").color(false);
288 assert!(!pager.is_enabled());
289 assert!(!pager.is_color());
290 assert_eq!(pager.command_str(), "more");
291 }
292
293 #[test]
294 fn test_pager_disabled_show() {
295 let pager = Pager::new().enabled(false);
296 assert!(pager.show("test").is_ok());
298 }
299
300 #[test]
301 fn test_pager_context_feed() {
302 let pager = Pager::new().enabled(false);
303 let mut ctx = PagerContext::new(pager);
304 ctx.feed("Hello, ");
305 ctx.feed("World!");
306 assert_eq!(ctx.content(), "Hello, World!");
307 }
308
309 #[test]
310 fn test_pager_context_write_trait() {
311 use std::io::Write;
312 let pager = Pager::new().enabled(false);
313 let mut ctx = PagerContext::new(pager);
314 write!(ctx, "Hello {}!", "World").unwrap();
315 assert!(ctx.content().contains("Hello"));
316 assert!(ctx.content().contains("World"));
317 }
318
319 #[test]
320 fn test_strip_ansi_via_export() {
321 let input = "\x1b[31mhello\x1b[0m world";
323 let result = crate::export::strip_ansi_escapes(input);
324 assert_eq!(result, "hello world");
325 }
326
327 #[test]
328 fn test_pager_context_flush() {
329 let pager = Pager::new().enabled(false);
330 let mut ctx = PagerContext::new(pager);
331 ctx.feed("test");
332 assert!(ctx.flush().is_ok());
333 assert!(ctx.content().is_empty());
334 }
335}