1use std::io::Write;
8use std::process::{Command, Stdio};
9
10#[derive(Debug, Clone)]
16pub struct SystemPager {
17 command: String,
19}
20
21impl SystemPager {
22 pub fn new() -> Self {
25 Self {
26 command: std::env::var("PAGER").unwrap_or_else(|_| "less".into()),
27 }
28 }
29
30 pub fn show(&self, content: &str) -> std::io::Result<()> {
35 let mut child = Command::new(&self.command)
36 .stdin(Stdio::piped())
37 .stdout(Stdio::inherit())
38 .stderr(Stdio::inherit())
39 .spawn()?;
40
41 if let Some(ref mut stdin) = child.stdin {
42 stdin.write_all(content.as_bytes())?;
43 }
44
45 drop(child.stdin.take());
47
48 child.wait()?;
49 Ok(())
50 }
51}
52
53impl Default for SystemPager {
54 fn default() -> Self {
55 Self::new()
56 }
57}
58
59#[derive(Debug, Clone)]
68pub struct Pager {
69 enabled: bool,
71 command: String,
73 color: bool,
75}
76
77impl Pager {
78 pub fn new() -> Self {
81 Self {
82 enabled: true,
83 command: std::env::var("PAGER").unwrap_or_else(|_| "less".into()),
84 color: true,
85 }
86 }
87
88 pub fn enabled(mut self, value: bool) -> Self {
90 self.enabled = value;
91 self
92 }
93
94 pub fn command(mut self, cmd: impl Into<String>) -> Self {
96 self.command = cmd.into();
97 self
98 }
99
100 pub fn color(mut self, value: bool) -> Self {
102 self.color = value;
103 self
104 }
105
106 pub fn is_enabled(&self) -> bool {
108 self.enabled
109 }
110
111 pub fn command_str(&self) -> &str {
113 &self.command
114 }
115
116 pub fn is_color(&self) -> bool {
118 self.color
119 }
120
121 pub fn show(&self, content: &str) -> std::io::Result<()> {
126 if !self.enabled {
127 let stdout = std::io::stdout();
129 let mut handle = stdout.lock();
130 handle.write_all(content.as_bytes())?;
131 handle.flush()?;
132 return Ok(());
133 }
134
135 let display = if !self.color {
136 strip_ansi_escapes(content)
138 } else {
139 content.to_string()
140 };
141
142 let pager = SystemPager {
143 command: self.command.clone(),
144 };
145 pager.show(&display)
146 }
147}
148
149impl Default for Pager {
150 fn default() -> Self {
151 Self::new()
152 }
153}
154
155#[derive(Debug)]
164pub struct PagerContext {
165 pager: Pager,
167 content: String,
169 enabled: bool,
171}
172
173impl PagerContext {
174 pub fn new(pager: Pager) -> Self {
176 let enabled = pager.enabled;
177 Self {
178 pager,
179 content: String::new(),
180 enabled,
181 }
182 }
183
184 pub fn feed(&mut self, text: &str) {
186 self.content.push_str(text);
187 }
188
189 pub fn flush(&mut self) -> std::io::Result<()> {
192 if !self.content.is_empty() {
193 let result = self.pager.show(&self.content);
194 self.content.clear();
195 result
196 } else {
197 Ok(())
198 }
199 }
200
201 pub fn content(&self) -> &str {
203 &self.content
204 }
205
206 pub fn is_enabled(&self) -> bool {
208 self.enabled
209 }
210}
211
212impl Write for PagerContext {
213 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
214 let s = String::from_utf8_lossy(buf);
215 self.feed(&s);
216 Ok(buf.len())
217 }
218
219 fn flush(&mut self) -> std::io::Result<()> {
220 Ok(())
221 }
222}
223
224impl Drop for PagerContext {
225 fn drop(&mut self) {
226 if self.enabled && !self.content.is_empty() {
227 let _ = self.pager.show(&self.content);
228 }
229 }
230}
231
232fn strip_ansi_escapes(s: &str) -> String {
238 use regex::Regex;
239 let re = Regex::new(r"\x1b\[[0-9;]*[a-zA-Z]").unwrap();
241 re.replace_all(s, "").to_string()
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 #[test]
249 fn test_system_pager_creation() {
250 let pager = SystemPager::new();
251 assert!(!pager.command.is_empty());
253 }
254
255 #[test]
256 fn test_pager_defaults() {
257 let pager = Pager::new();
258 assert!(pager.is_enabled());
259 assert!(pager.is_color());
260 assert!(!pager.command_str().is_empty());
261 }
262
263 #[test]
264 fn test_pager_builder() {
265 let pager = Pager::new()
266 .enabled(false)
267 .command("more")
268 .color(false);
269 assert!(!pager.is_enabled());
270 assert!(!pager.is_color());
271 assert_eq!(pager.command_str(), "more");
272 }
273
274 #[test]
275 fn test_pager_disabled_show() {
276 let pager = Pager::new().enabled(false);
277 assert!(pager.show("test").is_ok());
279 }
280
281 #[test]
282 fn test_pager_context_feed() {
283 let pager = Pager::new().enabled(false);
284 let mut ctx = PagerContext::new(pager);
285 ctx.feed("Hello, ");
286 ctx.feed("World!");
287 assert_eq!(ctx.content(), "Hello, World!");
288 }
289
290 #[test]
291 fn test_pager_context_write_trait() {
292 use std::io::Write;
293 let pager = Pager::new().enabled(false);
294 let mut ctx = PagerContext::new(pager);
295 write!(ctx, "Hello {}!", "World").unwrap();
296 assert!(ctx.content().contains("Hello"));
297 assert!(ctx.content().contains("World"));
298 }
299
300 #[test]
301 fn test_strip_ansi_escapes_basic() {
302 let input = "\x1b[31mhello\x1b[0m world";
303 let result = strip_ansi_escapes(input);
304 assert_eq!(result, "hello world");
305 }
306
307 #[test]
308 fn test_strip_ansi_escapes_no_ansi() {
309 let input = "hello world";
310 let result = strip_ansi_escapes(input);
311 assert_eq!(result, "hello world");
312 }
313
314 #[test]
315 fn test_pager_context_flush() {
316 let pager = Pager::new().enabled(false);
317 let mut ctx = PagerContext::new(pager);
318 ctx.feed("test");
319 assert!(ctx.flush().is_ok());
320 assert!(ctx.content().is_empty());
321 }
322}