1use std::io::Write;
16use std::process::{Child, Command, Stdio};
17
18use tempfile::NamedTempFile;
19
20pub fn page_content(content: &str, _title: &str) -> anyhow::Result<()> {
21 let mut tmp = NamedTempFile::new()?;
22 tmp.write_all(content.as_bytes())?;
23 tmp.flush()?;
24
25 let path = tmp.path();
26
27 if let Ok(pager_env) = std::env::var("PAGER") {
28 let parts: Vec<&str> = pager_env.split_whitespace().collect();
29 if let Some((cmd, args)) = parts.split_first() {
30 let status = Command::new(cmd).args(args).arg(path).status();
31 if let Ok(s) = status {
32 if s.success() {
33 return Ok(());
34 }
35 }
36 }
37 }
38
39 if let Ok(status) = Command::new("less").args(["-R", "-S"]).arg(path).status() {
40 if status.success() {
41 return Ok(());
42 }
43 }
44
45 if let Ok(status) = Command::new("more").arg(path).status() {
46 if status.success() {
47 return Ok(());
48 }
49 }
50
51 anyhow::bail!("no external pager available")
52}
53
54pub struct PagerWriter {
60 stdin: Option<std::process::ChildStdin>,
61 child: Option<Child>,
62}
63
64impl PagerWriter {
65 pub fn is_file_mode(&self) -> bool {
68 self.child.is_some()
69 }
70}
71
72impl Write for PagerWriter {
73 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
74 match self.stdin.as_mut() {
75 Some(stdin) => stdin.write(buf),
76 None => Err(std::io::Error::new(
77 std::io::ErrorKind::BrokenPipe,
78 "pager stdin closed",
79 )),
80 }
81 }
82
83 fn flush(&mut self) -> std::io::Result<()> {
84 match self.stdin.as_mut() {
85 Some(stdin) => stdin.flush(),
86 None => Ok(()),
87 }
88 }
89}
90
91impl Drop for PagerWriter {
92 fn drop(&mut self) {
93 if let Some(mut stdin) = self.stdin.take() {
94 let _ = writeln!(
95 stdin,
96 "\n\x1b[2m-- end of results (press q to quit) --\x1b[0m"
97 );
98 }
99 if let Some(mut child) = self.child.take() {
100 let _ = child.wait();
101 }
102 }
103}
104
105pub fn page_stream(title: &str) -> anyhow::Result<PagerWriter> {
112 let spawn_piped = |cmd: &mut Command| -> Option<PagerWriter> {
113 if let Ok(mut child) = cmd.stdin(Stdio::piped()).spawn() {
114 let stdin = child.stdin.take()?;
115 Some(PagerWriter {
116 stdin: Some(stdin),
117 child: Some(child),
118 })
119 } else {
120 None
121 }
122 };
123
124 if let Ok(pager_env) = std::env::var("PAGER") {
125 let parts: Vec<&str> = pager_env.split_whitespace().collect();
126 if let Some((cmd, args)) = parts.split_first() {
127 if let Some(w) = spawn_piped(Command::new(cmd).args(args)) {
128 return Ok(w);
129 }
130 }
131 }
132
133 let prompt = if title.is_empty() {
134 String::from("-Pline %lt/%L")
135 } else {
136 format!("-P{title} line %lt/%L")
137 };
138
139 if let Some(w) = spawn_piped(Command::new("less").args(["-R", "-S", &prompt])) {
140 return Ok(w);
141 }
142
143 if let Some(w) = spawn_piped(&mut Command::new("more")) {
144 return Ok(w);
145 }
146
147 anyhow::bail!("no external pager available")
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn page_content_with_cat_pager() {
156 std::env::set_var("PAGER", "cat");
157 let result = page_content("hello world\n", "test title");
158 std::env::remove_var("PAGER");
159 assert!(result.is_ok());
160 }
161
162 #[test]
163 fn page_content_with_true_pager() {
164 std::env::set_var("PAGER", "true");
165 let result = page_content("test content", "");
166 std::env::remove_var("PAGER");
167 assert!(result.is_ok());
168 }
169
170 #[test]
171 fn page_content_writes_temp_file() {
172 std::env::set_var("PAGER", "grep -q hello");
173 let result = page_content("hello world", "title");
174 std::env::remove_var("PAGER");
175 assert!(result.is_ok());
176 }
177
178 #[test]
179 fn page_content_empty_string() {
180 std::env::set_var("PAGER", "true");
181 let result = page_content("", "empty");
182 std::env::remove_var("PAGER");
183 assert!(result.is_ok());
184 }
185
186 #[test]
187 fn page_content_large_content() {
188 let content = "x".repeat(100_000);
189 std::env::set_var("PAGER", "true");
190 let result = page_content(&content, "big");
191 std::env::remove_var("PAGER");
192 assert!(result.is_ok());
193 }
194
195 #[test]
196 fn page_content_multiline() {
197 let content = "line1\nline2\nline3\n";
198 std::env::set_var("PAGER", "wc -l");
199 let result = page_content(content, "lines");
200 std::env::remove_var("PAGER");
201 assert!(result.is_ok());
202 }
203
204 #[test]
205 fn page_stream_with_cat() {
206 std::env::set_var("PAGER", "cat");
207 let mut writer = page_stream("test").unwrap();
208 writer.write_all(b"streaming content\n").unwrap();
209 drop(writer);
210 std::env::remove_var("PAGER");
211 }
212
213 #[test]
214 fn page_stream_write_multiple() {
215 std::env::set_var("PAGER", "cat");
216 let mut writer = page_stream("").unwrap();
217 writer.write_all(b"line 1\n").unwrap();
218 writer.write_all(b"line 2\n").unwrap();
219 drop(writer);
220 std::env::remove_var("PAGER");
221 }
222
223 #[test]
224 fn pager_writer_is_file_mode_with_child() {
225 std::env::set_var("PAGER", "cat");
226 let writer = page_stream("title").unwrap();
227 assert!(writer.is_file_mode());
228 std::env::remove_var("PAGER");
229 }
230
231 #[test]
232 fn pager_writer_is_file_mode_without_child() {
233 let writer = PagerWriter {
234 stdin: None,
235 child: None,
236 };
237 assert!(!writer.is_file_mode());
238 }
239
240 #[test]
241 fn pager_writer_write_without_stdin_returns_broken_pipe() {
242 let mut writer = PagerWriter {
243 stdin: None,
244 child: None,
245 };
246 let result = writer.write(b"data");
247 assert!(result.is_err());
248 assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::BrokenPipe);
249 }
250
251 #[test]
252 fn pager_writer_flush_without_stdin_ok() {
253 let mut writer = PagerWriter {
254 stdin: None,
255 child: None,
256 };
257 assert!(writer.flush().is_ok());
258 }
259
260 #[test]
261 fn page_stream_empty_title() {
262 std::env::set_var("PAGER", "true");
263 let writer = page_stream("");
264 std::env::remove_var("PAGER");
265 assert!(writer.is_ok());
266 }
267
268 #[test]
269 fn page_stream_nonempty_title() {
270 std::env::set_var("PAGER", "true");
271 let writer = page_stream("my table");
272 std::env::remove_var("PAGER");
273 assert!(writer.is_ok());
274 }
275
276 #[test]
277 fn pager_writer_drop_writes_end_marker() {
278 std::env::set_var("PAGER", "cat");
279 let writer = page_stream("").unwrap();
280 drop(writer);
281 std::env::remove_var("PAGER");
282 }
283}