1use std::env;
16use std::error;
17use std::fmt;
18use std::io;
19use std::io::IsTerminal as _;
20use std::io::PipeWriter;
21use std::io::Stderr;
22use std::io::StderrLock;
23use std::io::Stdout;
24use std::io::StdoutLock;
25use std::io::Write;
26use std::iter;
27use std::mem;
28use std::process::Child;
29use std::process::ChildStdin;
30use std::process::Stdio;
31use std::thread;
32use std::thread::JoinHandle;
33
34use itertools::Itertools as _;
35use jj_lib::config::ConfigGetError;
36use jj_lib::config::StackedConfig;
37use tracing::instrument;
38
39use crate::command_error::CommandError;
40use crate::config::CommandNameAndArgs;
41use crate::formatter::Formatter;
42use crate::formatter::FormatterExt as _;
43use crate::formatter::FormatterFactory;
44use crate::formatter::HeadingLabeledWriter;
45use crate::formatter::LabeledScope;
46use crate::formatter::PlainTextFormatter;
47
48const BUILTIN_PAGER_NAME: &str = ":builtin";
49
50enum UiOutput {
51 Terminal {
52 stdout: Stdout,
53 stderr: Stderr,
54 },
55 Paged {
56 child: Child,
57 child_stdin: ChildStdin,
58 },
59 BuiltinPaged {
60 out_wr: PipeWriter,
61 err_wr: PipeWriter,
62 pager_thread: JoinHandle<streampager::Result<()>>,
63 },
64 Null,
65}
66
67impl UiOutput {
68 fn new_terminal() -> Self {
69 Self::Terminal {
70 stdout: io::stdout(),
71 stderr: io::stderr(),
72 }
73 }
74
75 fn new_paged(pager_cmd: &CommandNameAndArgs) -> io::Result<Self> {
76 let mut cmd = pager_cmd.to_command();
77 tracing::info!(?cmd, "spawning pager");
78 let mut child = cmd.stdin(Stdio::piped()).spawn()?;
79 let child_stdin = child.stdin.take().unwrap();
80 Ok(Self::Paged { child, child_stdin })
81 }
82
83 fn new_builtin_paged(config: &StreampagerConfig) -> streampager::Result<Self> {
84 let streampager_config = streampager::config::Config {
85 wrapping_mode: config.wrapping.into(),
86 interface_mode: config.streampager_interface_mode(),
87 show_ruler: config.show_ruler,
88 scroll_past_eof: false,
93 ..Default::default()
94 };
95 let mut pager =
98 streampager::Pager::new_using_system_terminal_with_config(streampager_config)?;
99
100 let (out_rd, out_wr) = io::pipe()?;
104 let (err_rd, err_wr) = io::pipe()?;
105 pager.add_stream(out_rd, "")?;
106 pager.add_error_stream(err_rd, "stderr")?;
107
108 Ok(Self::BuiltinPaged {
109 out_wr,
110 err_wr,
111 pager_thread: thread::spawn(|| pager.run()),
112 })
113 }
114
115 fn finalize(self, ui: &Ui) {
116 match self {
117 Self::Terminal { .. } => { }
118 Self::Paged {
119 mut child,
120 child_stdin,
121 } => {
122 drop(child_stdin);
123 if let Err(err) = child.wait() {
124 writeln!(
128 ui.warning_default(),
129 "Failed to wait on pager: {err}",
130 err = format_error_with_sources(&err),
131 )
132 .ok();
133 }
134 }
135 Self::BuiltinPaged {
136 out_wr,
137 err_wr,
138 pager_thread,
139 } => {
140 drop(out_wr);
141 drop(err_wr);
142 match pager_thread.join() {
143 Ok(Ok(())) => {}
144 Ok(Err(err)) => {
145 writeln!(
146 ui.warning_default(),
147 "Failed to run builtin pager: {err}",
148 err = format_error_with_sources(&err),
149 )
150 .ok();
151 }
152 Err(_) => {
153 writeln!(ui.warning_default(), "Builtin pager crashed.").ok();
154 }
155 }
156 }
157 Self::Null => {}
158 }
159 }
160}
161
162pub enum UiStdout<'a> {
163 Terminal(StdoutLock<'static>),
164 Paged(&'a ChildStdin),
165 Builtin(&'a PipeWriter),
166 Null(io::Sink),
167}
168
169pub enum UiStderr<'a> {
170 Terminal(StderrLock<'static>),
171 Paged(&'a ChildStdin),
172 Builtin(&'a PipeWriter),
173 Null(io::Sink),
174}
175
176macro_rules! for_outputs {
177 ($ty:ident, $output:expr, $pat:pat => $expr:expr) => {
178 match $output {
179 $ty::Terminal($pat) => $expr,
180 $ty::Paged($pat) => $expr,
181 $ty::Builtin($pat) => $expr,
182 $ty::Null($pat) => $expr,
183 }
184 };
185}
186
187impl Write for UiStdout<'_> {
188 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
189 for_outputs!(Self, self, w => w.write(buf))
190 }
191
192 fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
193 for_outputs!(Self, self, w => w.write_all(buf))
194 }
195
196 fn flush(&mut self) -> io::Result<()> {
197 for_outputs!(Self, self, w => w.flush())
198 }
199}
200
201impl Write for UiStderr<'_> {
202 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
203 for_outputs!(Self, self, w => w.write(buf))
204 }
205
206 fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
207 for_outputs!(Self, self, w => w.write_all(buf))
208 }
209
210 fn flush(&mut self) -> io::Result<()> {
211 for_outputs!(Self, self, w => w.flush())
212 }
213}
214
215pub struct Ui {
216 quiet: bool,
217 pager: PagerConfig,
218 progress_indicator: bool,
219 formatter_factory: FormatterFactory,
220 output: UiOutput,
221}
222
223#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, clap::ValueEnum)]
224#[serde(rename_all = "kebab-case")]
225pub enum ColorChoice {
226 Always,
227 Never,
228 Debug,
229 Auto,
230}
231
232impl fmt::Display for ColorChoice {
233 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234 let s = match self {
235 Self::Always => "always",
236 Self::Never => "never",
237 Self::Debug => "debug",
238 Self::Auto => "auto",
239 };
240 write!(f, "{s}")
241 }
242}
243
244fn prepare_formatter_factory(
245 config: &StackedConfig,
246 stdout: &Stdout,
247) -> Result<FormatterFactory, ConfigGetError> {
248 let terminal = stdout.is_terminal();
249 let (color, debug) = match config.get("ui.color")? {
250 ColorChoice::Always => (true, false),
251 ColorChoice::Never => (false, false),
252 ColorChoice::Debug => (true, true),
253 ColorChoice::Auto => (terminal, false),
254 };
255 if color {
256 FormatterFactory::color(config, debug)
257 } else if terminal {
258 Ok(FormatterFactory::sanitized())
261 } else {
262 Ok(FormatterFactory::plain_text())
263 }
264}
265
266#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize)]
267#[serde(rename_all(deserialize = "kebab-case"))]
268pub enum PaginationChoice {
269 Never,
270 Auto,
271}
272
273#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize)]
274#[serde(rename_all(deserialize = "kebab-case"))]
275pub enum StreampagerAlternateScreenMode {
276 QuitIfOnePage,
277 FullScreenClearOutput,
278 QuitQuicklyOrClearOutput,
279}
280
281#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize)]
282#[serde(rename_all(deserialize = "kebab-case"))]
283enum StreampagerWrappingMode {
284 None,
285 Word,
286 Anywhere,
287}
288
289impl From<StreampagerWrappingMode> for streampager::config::WrappingMode {
290 fn from(val: StreampagerWrappingMode) -> Self {
291 match val {
292 StreampagerWrappingMode::None => Self::Unwrapped,
293 StreampagerWrappingMode::Word => Self::WordBoundary,
294 StreampagerWrappingMode::Anywhere => Self::GraphemeBoundary,
295 }
296 }
297}
298
299#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize)]
300#[serde(rename_all(deserialize = "kebab-case"))]
301struct StreampagerConfig {
302 interface: StreampagerAlternateScreenMode,
303 wrapping: StreampagerWrappingMode,
304 show_ruler: bool,
305 }
309
310impl StreampagerConfig {
311 fn streampager_interface_mode(&self) -> streampager::config::InterfaceMode {
312 use StreampagerAlternateScreenMode::*;
313 use streampager::config::InterfaceMode;
314 match self.interface {
315 FullScreenClearOutput => InterfaceMode::FullScreen,
317 QuitIfOnePage => InterfaceMode::Hybrid,
318 QuitQuicklyOrClearOutput => InterfaceMode::Delayed(std::time::Duration::from_secs(2)),
319 }
320 }
321}
322
323enum PagerConfig {
324 Disabled,
325 Builtin(StreampagerConfig),
326 External(CommandNameAndArgs),
327}
328
329impl PagerConfig {
330 fn from_config(config: &StackedConfig) -> Result<Self, ConfigGetError> {
331 if matches!(config.get("ui.paginate")?, PaginationChoice::Never) {
332 return Ok(Self::Disabled);
333 };
334 let args: CommandNameAndArgs = config.get("ui.pager")?;
335 if args.as_str() == Some(BUILTIN_PAGER_NAME) {
336 Ok(Self::Builtin(config.get("ui.streampager")?))
337 } else {
338 Ok(Self::External(args))
339 }
340 }
341}
342
343impl Ui {
344 pub fn null() -> Self {
345 Self {
346 quiet: true,
347 pager: PagerConfig::Disabled,
348 progress_indicator: false,
349 formatter_factory: FormatterFactory::plain_text(),
350 output: UiOutput::Null,
351 }
352 }
353
354 pub fn with_config(config: &StackedConfig) -> Result<Self, CommandError> {
355 let formatter_factory = prepare_formatter_factory(config, &io::stdout())?;
356 Ok(Self {
357 quiet: config.get("ui.quiet")?,
358 formatter_factory,
359 pager: PagerConfig::from_config(config)?,
360 progress_indicator: config.get("ui.progress-indicator")?,
361 output: UiOutput::new_terminal(),
362 })
363 }
364
365 pub fn reset(&mut self, config: &StackedConfig) -> Result<(), CommandError> {
366 self.quiet = config.get("ui.quiet")?;
367 self.pager = PagerConfig::from_config(config)?;
368 self.progress_indicator = config.get("ui.progress-indicator")?;
369 self.formatter_factory = prepare_formatter_factory(config, &io::stdout())?;
370 Ok(())
371 }
372
373 #[instrument(skip_all)]
375 pub fn request_pager(&mut self) {
376 if !matches!(&self.output, UiOutput::Terminal { stdout, .. } if stdout.is_terminal()) {
377 return;
378 }
379
380 let new_output = match &self.pager {
381 PagerConfig::Disabled => {
382 return;
383 }
384 PagerConfig::Builtin(streampager_config) => {
385 UiOutput::new_builtin_paged(streampager_config)
386 .inspect_err(|err| {
387 writeln!(
388 self.warning_default(),
389 "Failed to set up builtin pager: {err}",
390 err = format_error_with_sources(err),
391 )
392 .ok();
393 })
394 .ok()
395 }
396 PagerConfig::External(command_name_and_args) => {
397 UiOutput::new_paged(command_name_and_args)
398 .inspect_err(|err| {
399 writeln!(
401 self.warning_default(),
402 "Failed to spawn pager '{name}': {err}",
403 name = command_name_and_args.split_name(),
404 err = format_error_with_sources(err),
405 )
406 .ok();
407 writeln!(self.hint_default(), "Consider using the `:builtin` pager.").ok();
408 })
409 .ok()
410 }
411 };
412 if let Some(output) = new_output {
413 self.output = output;
414 }
415 }
416
417 pub fn color(&self) -> bool {
418 self.formatter_factory.is_color()
419 }
420
421 pub fn new_formatter<'output, W: Write + 'output>(
422 &self,
423 output: W,
424 ) -> Box<dyn Formatter + 'output> {
425 self.formatter_factory.new_formatter(output)
426 }
427
428 pub fn stdout(&self) -> UiStdout<'_> {
430 match &self.output {
431 UiOutput::Terminal { stdout, .. } => UiStdout::Terminal(stdout.lock()),
432 UiOutput::Paged { child_stdin, .. } => UiStdout::Paged(child_stdin),
433 UiOutput::BuiltinPaged { out_wr, .. } => UiStdout::Builtin(out_wr),
434 UiOutput::Null => UiStdout::Null(io::sink()),
435 }
436 }
437
438 pub fn stdout_formatter(&self) -> Box<dyn Formatter + '_> {
443 for_outputs!(UiStdout, self.stdout(), w => self.new_formatter(w))
444 }
445
446 pub fn stderr(&self) -> UiStderr<'_> {
448 match &self.output {
449 UiOutput::Terminal { stderr, .. } => UiStderr::Terminal(stderr.lock()),
450 UiOutput::Paged { child_stdin, .. } => UiStderr::Paged(child_stdin),
451 UiOutput::BuiltinPaged { err_wr, .. } => UiStderr::Builtin(err_wr),
452 UiOutput::Null => UiStderr::Null(io::sink()),
453 }
454 }
455
456 pub fn stderr_formatter(&self) -> Box<dyn Formatter + '_> {
458 for_outputs!(UiStderr, self.stderr(), w => self.new_formatter(w))
459 }
460
461 pub fn stderr_for_child(&self) -> io::Result<Stdio> {
463 match &self.output {
464 UiOutput::Terminal { .. } => Ok(Stdio::inherit()),
465 UiOutput::Paged { child_stdin, .. } => Ok(duplicate_child_stdin(child_stdin)?.into()),
466 UiOutput::BuiltinPaged { err_wr, .. } => Ok(err_wr.try_clone()?.into()),
467 UiOutput::Null => Ok(Stdio::null()),
468 }
469 }
470
471 pub fn use_progress_indicator(&self) -> bool {
474 match &self.output {
475 UiOutput::Terminal { stderr, .. } => self.progress_indicator && stderr.is_terminal(),
476 UiOutput::Paged { .. } => false,
477 UiOutput::BuiltinPaged { .. } => false,
478 UiOutput::Null => false,
479 }
480 }
481
482 pub fn progress_output(&self) -> Option<ProgressOutput<std::io::Stderr>> {
483 self.use_progress_indicator()
484 .then(ProgressOutput::for_stderr)
485 }
486
487 pub fn status(&self) -> Box<dyn Write + '_> {
489 if self.quiet {
490 Box::new(io::sink())
491 } else {
492 Box::new(self.stderr())
493 }
494 }
495
496 pub fn status_formatter(&self) -> Option<Box<dyn Formatter + '_>> {
499 (!self.quiet).then(|| self.stderr_formatter())
500 }
501
502 pub fn hint_default(&self) -> HeadingLabeledWriter<Box<dyn Formatter + '_>, &'static str> {
504 self.hint_with_heading("Hint: ")
505 }
506
507 pub fn hint_no_heading(&self) -> LabeledScope<Box<dyn Formatter + '_>> {
509 let formatter = self
510 .status_formatter()
511 .unwrap_or_else(|| Box::new(PlainTextFormatter::new(io::sink())));
512 formatter.into_labeled("hint")
513 }
514
515 pub fn hint_with_heading<H: fmt::Display>(
517 &self,
518 heading: H,
519 ) -> HeadingLabeledWriter<Box<dyn Formatter + '_>, H> {
520 self.hint_no_heading().with_heading(heading)
521 }
522
523 pub fn warning_default(&self) -> HeadingLabeledWriter<Box<dyn Formatter + '_>, &'static str> {
525 self.warning_with_heading("Warning: ")
526 }
527
528 pub fn warning_no_heading(&self) -> LabeledScope<Box<dyn Formatter + '_>> {
530 self.stderr_formatter().into_labeled("warning")
531 }
532
533 pub fn warning_with_heading<H: fmt::Display>(
535 &self,
536 heading: H,
537 ) -> HeadingLabeledWriter<Box<dyn Formatter + '_>, H> {
538 self.warning_no_heading().with_heading(heading)
539 }
540
541 pub fn error_no_heading(&self) -> LabeledScope<Box<dyn Formatter + '_>> {
543 self.stderr_formatter().into_labeled("error")
544 }
545
546 pub fn error_with_heading<H: fmt::Display>(
548 &self,
549 heading: H,
550 ) -> HeadingLabeledWriter<Box<dyn Formatter + '_>, H> {
551 self.error_no_heading().with_heading(heading)
552 }
553
554 #[instrument(skip_all)]
556 pub fn finalize_pager(&mut self) {
557 let old_output = mem::replace(&mut self.output, UiOutput::new_terminal());
558 old_output.finalize(self);
559 }
560
561 pub fn can_prompt() -> bool {
562 io::stderr().is_terminal()
563 || env::var("JJ_INTERACTIVE")
564 .map(|v| v == "1")
565 .unwrap_or(false)
566 }
567
568 pub fn prompt(&self, prompt: &str) -> io::Result<String> {
569 if !Self::can_prompt() {
570 return Err(io::Error::new(
571 io::ErrorKind::Unsupported,
572 "Cannot prompt for input since the output is not connected to a terminal",
573 ));
574 }
575 write!(self.stderr(), "{prompt}: ")?;
576 self.stderr().flush()?;
577 let mut buf = String::new();
578 io::stdin().read_line(&mut buf)?;
579
580 if buf.is_empty() {
581 return Err(io::Error::new(
582 io::ErrorKind::UnexpectedEof,
583 "Prompt canceled by EOF",
584 ));
585 }
586
587 if let Some(trimmed) = buf.strip_suffix('\n') {
588 buf.truncate(trimmed.len());
589 }
590 Ok(buf)
591 }
592
593 pub fn prompt_choice(
596 &self,
597 prompt: &str,
598 choices: &[impl AsRef<str>],
599 default_index: Option<usize>,
600 ) -> io::Result<usize> {
601 self.prompt_choice_with(
602 prompt,
603 default_index.map(|index| {
604 choices
605 .get(index)
606 .expect("default_index should be within range")
607 .as_ref()
608 }),
609 |input| {
610 choices
611 .iter()
612 .position(|c| input == c.as_ref())
613 .ok_or("unrecognized response")
614 },
615 )
616 }
617
618 pub fn prompt_yes_no(&self, prompt: &str, default: Option<bool>) -> io::Result<bool> {
620 let default_str = match &default {
621 Some(true) => "(Yn)",
622 Some(false) => "(yN)",
623 None => "(yn)",
624 };
625 self.prompt_choice_with(
626 &format!("{prompt} {default_str}"),
627 default.map(|v| if v { "y" } else { "n" }),
628 |input| {
629 if input.eq_ignore_ascii_case("y") || input.eq_ignore_ascii_case("yes") {
630 Ok(true)
631 } else if input.eq_ignore_ascii_case("n") || input.eq_ignore_ascii_case("no") {
632 Ok(false)
633 } else {
634 Err("unrecognized response")
635 }
636 },
637 )
638 }
639
640 pub fn prompt_choice_with<T, E: fmt::Debug + fmt::Display>(
647 &self,
648 prompt: &str,
649 default: Option<&str>,
650 mut parse: impl FnMut(&str) -> Result<T, E>,
651 ) -> io::Result<T> {
652 let default = default.map(|text| (parse(text).expect("default should be valid"), text));
654
655 if !Self::can_prompt()
656 && let Some((value, text)) = default
657 {
658 writeln!(self.stderr(), "{prompt}: {text}")?;
660 return Ok(value);
661 }
662
663 loop {
664 let input = self.prompt(prompt)?;
665 let input = input.trim();
666 if input.is_empty() {
667 if let Some((value, _)) = default {
668 return Ok(value);
669 } else {
670 continue;
671 }
672 }
673 match parse(input) {
674 Ok(value) => return Ok(value),
675 Err(err) => writeln!(self.warning_no_heading(), "{err}")?,
676 }
677 }
678 }
679
680 pub fn prompt_password(&self, prompt: &str) -> io::Result<String> {
681 if !io::stdout().is_terminal() {
682 return Err(io::Error::new(
683 io::ErrorKind::Unsupported,
684 "Cannot prompt for input since the output is not connected to a terminal",
685 ));
686 }
687 rpassword::prompt_password(format!("{prompt}: "))
688 }
689
690 pub fn term_width(&self) -> usize {
691 term_width().unwrap_or(80).into()
692 }
693}
694
695#[derive(Debug)]
696pub struct ProgressOutput<W> {
697 output: W,
698 term_width: Option<u16>,
699}
700
701impl ProgressOutput<io::Stderr> {
702 pub fn for_stderr() -> Self {
703 Self {
704 output: io::stderr(),
705 term_width: None,
706 }
707 }
708}
709
710impl<W> ProgressOutput<W> {
711 pub fn for_test(output: W, term_width: u16) -> Self {
712 Self {
713 output,
714 term_width: Some(term_width),
715 }
716 }
717
718 pub fn term_width(&self) -> Option<u16> {
719 self.term_width.or_else(term_width)
721 }
722
723 pub fn output_guard(&self, text: String) -> OutputGuard {
726 OutputGuard {
727 text,
728 output: io::stderr(),
729 }
730 }
731}
732
733impl<W: Write> ProgressOutput<W> {
734 pub fn write_fmt(&mut self, fmt: fmt::Arguments<'_>) -> io::Result<()> {
735 self.output.write_fmt(fmt)
736 }
737
738 pub fn flush(&mut self) -> io::Result<()> {
739 self.output.flush()
740 }
741}
742
743pub struct OutputGuard {
744 text: String,
745 output: Stderr,
746}
747
748impl Drop for OutputGuard {
749 #[instrument(skip_all)]
750 fn drop(&mut self) {
751 self.output.write_all(self.text.as_bytes()).ok();
752 self.output.flush().ok();
753 }
754}
755
756#[cfg(unix)]
757fn duplicate_child_stdin(stdin: &ChildStdin) -> io::Result<std::os::fd::OwnedFd> {
758 use std::os::fd::AsFd as _;
759 stdin.as_fd().try_clone_to_owned()
760}
761
762#[cfg(windows)]
763fn duplicate_child_stdin(stdin: &ChildStdin) -> io::Result<std::os::windows::io::OwnedHandle> {
764 use std::os::windows::io::AsHandle as _;
765 stdin.as_handle().try_clone_to_owned()
766}
767
768fn format_error_with_sources(err: &dyn error::Error) -> impl fmt::Display {
769 iter::successors(Some(err), |&err| err.source()).format(": ")
770}
771
772fn term_width() -> Option<u16> {
773 if let Some(cols) = env::var("COLUMNS").ok().and_then(|s| s.parse().ok()) {
774 Some(cols)
775 } else {
776 crossterm::terminal::size().ok().map(|(cols, _)| cols)
777 }
778}