1use crate::{
11 user_config::elements::{PagerSetting, PaginateSetting, StreampagerConfig},
12 write_str::WriteStr,
13};
14use std::{
15 io::{self, IsTerminal, PipeWriter, Stdout, Write},
16 process::{Child, ChildStdin, Stdio},
17 thread::{self, JoinHandle},
18};
19use tracing::warn;
20
21pub enum PagedOutput {
33 Terminal {
35 stdout: Stdout,
37 },
38 ExternalPager {
40 child: Child,
42 child_stdin: Option<ChildStdin>,
47 },
48 BuiltinPager {
50 out_writer: Option<PipeWriter>,
55 pager_thread: Option<JoinHandle<streampager::Result<()>>>,
59 },
60}
61
62impl PagedOutput {
63 pub fn terminal() -> Self {
65 Self::Terminal {
66 stdout: io::stdout(),
67 }
68 }
69
70 pub fn request_pager(
80 pager: &PagerSetting,
81 paginate: PaginateSetting,
82 streampager_config: &StreampagerConfig,
83 ) -> Self {
84 if matches!(paginate, PaginateSetting::Never) {
86 return Self::terminal();
87 }
88
89 if !io::stdout().is_terminal() {
91 return Self::terminal();
92 }
93
94 match pager {
95 PagerSetting::Builtin => Self::spawn_builtin_pager(streampager_config),
96 PagerSetting::External(command_and_args) => {
97 let mut cmd = command_and_args.to_command();
99 cmd.stdin(Stdio::piped());
100
101 match cmd.spawn() {
102 Ok(mut child) => {
103 let child_stdin = child
104 .stdin
105 .take()
106 .expect("child stdin should be present when piped");
107 Self::ExternalPager {
108 child,
109 child_stdin: Some(child_stdin),
110 }
111 }
112 Err(error) => {
113 warn!(
114 "failed to spawn pager '{}': {error}",
115 command_and_args.command_name()
116 );
117 Self::terminal()
118 }
119 }
120 }
121 }
122 }
123
124 fn spawn_builtin_pager(config: &StreampagerConfig) -> Self {
126 let streampager_config = streampager::config::Config {
127 wrapping_mode: config.streampager_wrapping_mode(),
128 interface_mode: config.streampager_interface_mode(),
129 show_ruler: config.show_ruler,
130 scroll_past_eof: false,
133 ..Default::default()
134 };
135
136 let pager_result = streampager::Pager::new_using_system_terminal_with_config(
139 streampager_config,
140 )
141 .and_then(|mut pager| {
142 let (out_reader, out_writer) = io::pipe()?;
144 pager.add_stream(out_reader, "")?;
145 Ok((pager, out_writer))
146 });
147
148 match pager_result {
149 Ok((pager, out_writer)) => Self::BuiltinPager {
150 out_writer: Some(out_writer),
151 pager_thread: Some(thread::spawn(|| pager.run())),
152 },
153 Err(error) => {
154 warn!("failed to set up builtin pager: {error}");
155 Self::terminal()
156 }
157 }
158 }
159
160 pub fn is_interactive(&self) -> bool {
169 match self {
170 Self::Terminal { stdout, .. } => stdout.is_terminal(),
171 Self::ExternalPager { .. } | Self::BuiltinPager { .. } => true,
174 }
175 }
176
177 pub fn finalize(mut self) {
186 self.finalize_inner();
187 }
188
189 fn stdout(&mut self) -> &mut dyn Write {
196 match self {
197 Self::Terminal { stdout, .. } => stdout,
198 Self::ExternalPager { child_stdin, .. } => child_stdin
199 .as_mut()
200 .expect("stdout should not be called after finalize"),
201 Self::BuiltinPager { out_writer, .. } => out_writer
202 .as_mut()
203 .expect("stdout should not be called after finalize"),
204 }
205 }
206
207 fn finalize_inner(&mut self) {
208 match self {
209 Self::Terminal { .. } => {
210 }
212 Self::ExternalPager { child, child_stdin } => {
213 let Some(stdin) = child_stdin.take() else {
215 return;
216 };
217
218 drop(stdin);
220
221 if let Err(error) = child.wait()
224 && error.kind() != io::ErrorKind::BrokenPipe
225 {
226 warn!("failed to wait on pager: {error}");
227 }
228 }
232 Self::BuiltinPager {
233 out_writer,
234 pager_thread,
235 } => {
236 let Some(writer) = out_writer.take() else {
238 return;
239 };
240
241 drop(writer);
243
244 if let Some(thread) = pager_thread.take() {
246 match thread.join() {
247 Ok(Ok(())) => {}
248 Ok(Err(error)) => {
249 warn!("failed to run builtin pager: {error}");
250 }
251 Err(_) => {
252 warn!("builtin pager thread panicked");
253 }
254 }
255 }
256 }
257 }
258 }
259}
260
261impl Drop for PagedOutput {
262 fn drop(&mut self) {
263 if std::thread::panicking() {
264 match self {
267 Self::Terminal { .. } => {}
268 Self::ExternalPager { child_stdin, .. } => {
269 drop(child_stdin.take());
270 }
271 Self::BuiltinPager { out_writer, .. } => {
272 drop(out_writer.take());
273 }
274 }
275 return;
276 }
277 self.finalize_inner();
278 }
279}
280
281impl WriteStr for PagedOutput {
282 fn write_str(&mut self, s: &str) -> io::Result<()> {
283 squelch_broken_pipe(self.stdout().write_all(s.as_bytes()))
284 }
285
286 fn write_str_flush(&mut self) -> io::Result<()> {
287 squelch_broken_pipe(self.stdout().flush())
288 }
289}
290
291fn squelch_broken_pipe(res: io::Result<()>) -> io::Result<()> {
292 match res {
293 Ok(()) => Ok(()),
294 Err(e) if e.kind() == io::ErrorKind::BrokenPipe => Ok(()),
295 Err(e) => Err(e),
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302 use crate::user_config::elements::{StreampagerInterface, StreampagerWrapping};
303
304 #[test]
305 fn test_terminal_output() {
306 let mut output = PagedOutput::terminal();
307 let _ = output.stdout();
309 output.finalize();
310 }
311
312 #[test]
313 fn test_terminal_output_drop() {
314 let mut output = PagedOutput::terminal();
316 let _ = output.stdout();
317 }
319
320 #[test]
321 fn test_request_pager_never_paginate() {
322 let pager = PagerSetting::default();
323 let streampager = StreampagerConfig {
324 interface: StreampagerInterface::QuitIfOnePage,
325 wrapping: StreampagerWrapping::Word,
326 show_ruler: true,
327 };
328 let output = PagedOutput::request_pager(&pager, PaginateSetting::Never, &streampager);
329 assert!(matches!(output, PagedOutput::Terminal { .. }));
330 output.finalize();
331 }
332
333 #[test]
334 #[cfg(unix)]
335 fn test_external_pager_write_and_finalize() {
336 let mut child = std::process::Command::new("cat")
338 .stdin(Stdio::piped())
339 .stdout(Stdio::null())
340 .spawn()
341 .expect("failed to spawn cat");
342
343 let child_stdin = child.stdin.take().expect("stdin should be piped");
344
345 let mut output = PagedOutput::ExternalPager {
346 child,
347 child_stdin: Some(child_stdin),
348 };
349
350 writeln!(output.stdout(), "hello pager").expect("write should succeed");
352
353 output.finalize();
355 }
356
357 #[test]
358 #[cfg(unix)]
359 fn test_external_pager_drop_without_finalize() {
360 let mut child = std::process::Command::new("cat")
361 .stdin(Stdio::piped())
362 .stdout(Stdio::null())
363 .spawn()
364 .expect("failed to spawn cat");
365
366 let child_stdin = child.stdin.take().expect("stdin should be piped");
367
368 let mut output = PagedOutput::ExternalPager {
369 child,
370 child_stdin: Some(child_stdin),
371 };
372
373 writeln!(output.stdout(), "hello pager").expect("write should succeed");
374
375 drop(output);
377 }
378
379 #[test]
380 #[cfg(unix)]
381 fn test_external_pager_double_finalize_is_idempotent() {
382 let mut child = std::process::Command::new("cat")
383 .stdin(Stdio::piped())
384 .stdout(Stdio::null())
385 .spawn()
386 .expect("failed to spawn cat");
387
388 let child_stdin = child.stdin.take().expect("stdin should be piped");
389
390 let mut output = PagedOutput::ExternalPager {
391 child,
392 child_stdin: Some(child_stdin),
393 };
394
395 output.finalize_inner();
397 output.finalize_inner();
398 }
400
401 #[test]
402 #[cfg(unix)]
403 fn test_external_pager_early_exit_squelches_broken_pipe() {
404 let mut child = std::process::Command::new("true")
406 .stdin(Stdio::piped())
407 .spawn()
408 .expect("failed to spawn true");
409
410 let child_stdin = child.stdin.take().expect("stdin should be piped");
411
412 let _ = child.wait();
414
415 let mut output = PagedOutput::ExternalPager {
416 child,
417 child_stdin: Some(child_stdin),
418 };
419
420 output
421 .write_str("hello\n")
422 .expect("BrokenPipe should be squelched for write_str");
423 let error = output
424 .stdout()
425 .write(b"hello\n")
426 .expect_err("Write should fail with BrokenPipe");
427 assert_eq!(error.kind(), io::ErrorKind::BrokenPipe);
428 output
429 .write_str_flush()
430 .expect("BrokenPipe should be squelched for write_str_flush");
431 output.finalize();
432 }
433}