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.maybe_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, .. } => {
476 !self.quiet && self.progress_indicator && stderr.is_terminal()
477 }
478 UiOutput::Paged { .. } => false,
479 UiOutput::BuiltinPaged { .. } => false,
480 UiOutput::Null => false,
481 }
482 }
483
484 pub fn progress_output(&self) -> Option<ProgressOutput<std::io::Stderr>> {
485 self.use_progress_indicator()
486 .then(ProgressOutput::for_stderr)
487 }
488
489 pub fn status(&self) -> Box<dyn Write + '_> {
491 if self.quiet {
492 Box::new(io::sink())
493 } else {
494 Box::new(self.stderr())
495 }
496 }
497
498 pub fn status_formatter(&self) -> Option<Box<dyn Formatter + '_>> {
501 (!self.quiet).then(|| self.stderr_formatter())
502 }
503
504 pub fn hint_default(&self) -> HeadingLabeledWriter<Box<dyn Formatter + '_>, &'static str> {
506 self.hint_with_heading("Hint: ")
507 }
508
509 pub fn hint_no_heading(&self) -> LabeledScope<Box<dyn Formatter + '_>> {
511 let formatter = self
512 .status_formatter()
513 .unwrap_or_else(|| Box::new(PlainTextFormatter::new(io::sink())));
514 formatter.into_labeled("hint")
515 }
516
517 pub fn hint_with_heading<H: fmt::Display>(
519 &self,
520 heading: H,
521 ) -> HeadingLabeledWriter<Box<dyn Formatter + '_>, H> {
522 self.hint_no_heading().with_heading(heading)
523 }
524
525 pub fn warning_default(&self) -> HeadingLabeledWriter<Box<dyn Formatter + '_>, &'static str> {
527 self.warning_with_heading("Warning: ")
528 }
529
530 pub fn warning_no_heading(&self) -> LabeledScope<Box<dyn Formatter + '_>> {
532 self.stderr_formatter().into_labeled("warning")
533 }
534
535 pub fn warning_with_heading<H: fmt::Display>(
537 &self,
538 heading: H,
539 ) -> HeadingLabeledWriter<Box<dyn Formatter + '_>, H> {
540 self.warning_no_heading().with_heading(heading)
541 }
542
543 pub fn error_no_heading(&self) -> LabeledScope<Box<dyn Formatter + '_>> {
545 self.stderr_formatter().into_labeled("error")
546 }
547
548 pub fn error_with_heading<H: fmt::Display>(
550 &self,
551 heading: H,
552 ) -> HeadingLabeledWriter<Box<dyn Formatter + '_>, H> {
553 self.error_no_heading().with_heading(heading)
554 }
555
556 #[instrument(skip_all)]
558 pub fn finalize_pager(&mut self) {
559 let old_output = mem::replace(&mut self.output, UiOutput::new_terminal());
560 old_output.finalize(self);
561 }
562
563 pub fn can_prompt() -> bool {
564 io::stderr().is_terminal()
565 || env::var("JJ_INTERACTIVE")
566 .map(|v| v == "1")
567 .unwrap_or(false)
568 }
569
570 pub fn prompt(&self, prompt: &str) -> io::Result<String> {
571 if !Self::can_prompt() {
572 return Err(io::Error::new(
573 io::ErrorKind::Unsupported,
574 "Cannot prompt for input since the output is not connected to a terminal",
575 ));
576 }
577 write!(self.stderr(), "{prompt}: ")?;
578 self.stderr().flush()?;
579 let mut buf = String::new();
580 io::stdin().read_line(&mut buf)?;
581
582 if buf.is_empty() {
583 return Err(io::Error::new(
584 io::ErrorKind::UnexpectedEof,
585 "Prompt canceled by EOF",
586 ));
587 }
588
589 if let Some(trimmed) = buf.strip_suffix('\n') {
590 buf.truncate(trimmed.len());
591 }
592 Ok(buf)
593 }
594
595 pub fn prompt_choice(
598 &self,
599 prompt: &str,
600 choices: &[impl AsRef<str>],
601 default_index: Option<usize>,
602 ) -> io::Result<usize> {
603 self.prompt_choice_with(
604 prompt,
605 default_index.map(|index| {
606 choices
607 .get(index)
608 .expect("default_index should be within range")
609 .as_ref()
610 }),
611 |input| {
612 choices
613 .iter()
614 .position(|c| input == c.as_ref())
615 .ok_or("unrecognized response")
616 },
617 )
618 }
619
620 pub fn prompt_yes_no(&self, prompt: &str, default: Option<bool>) -> io::Result<bool> {
622 let default_str = match &default {
623 Some(true) => "(Yn)",
624 Some(false) => "(yN)",
625 None => "(yn)",
626 };
627 self.prompt_choice_with(
628 &format!("{prompt} {default_str}"),
629 default.map(|v| if v { "y" } else { "n" }),
630 |input| {
631 if input.eq_ignore_ascii_case("y") || input.eq_ignore_ascii_case("yes") {
632 Ok(true)
633 } else if input.eq_ignore_ascii_case("n") || input.eq_ignore_ascii_case("no") {
634 Ok(false)
635 } else {
636 Err("unrecognized response")
637 }
638 },
639 )
640 }
641
642 pub fn prompt_choice_with<T, E: fmt::Debug + fmt::Display>(
649 &self,
650 prompt: &str,
651 default: Option<&str>,
652 mut parse: impl FnMut(&str) -> Result<T, E>,
653 ) -> io::Result<T> {
654 let default = default.map(|text| (parse(text).expect("default should be valid"), text));
656
657 if !Self::can_prompt()
658 && let Some((value, text)) = default
659 {
660 writeln!(self.stderr(), "{prompt}: {text}")?;
662 return Ok(value);
663 }
664
665 loop {
666 let input = self.prompt(prompt)?;
667 let input = input.trim();
668 if input.is_empty() {
669 if let Some((value, _)) = default {
670 return Ok(value);
671 } else {
672 continue;
673 }
674 }
675 match parse(input) {
676 Ok(value) => return Ok(value),
677 Err(err) => writeln!(self.warning_no_heading(), "{err}")?,
678 }
679 }
680 }
681
682 pub fn prompt_password(&self, prompt: &str) -> io::Result<String> {
683 if !io::stdout().is_terminal() {
684 return Err(io::Error::new(
685 io::ErrorKind::Unsupported,
686 "Cannot prompt for input since the output is not connected to a terminal",
687 ));
688 }
689 rpassword::prompt_password(format!("{prompt}: "))
690 }
691
692 pub fn term_width(&self) -> usize {
693 term_width().unwrap_or(80).into()
694 }
695}
696
697#[derive(Debug)]
698pub struct ProgressOutput<W> {
699 output: W,
700 term_width: Option<u16>,
701}
702
703impl ProgressOutput<io::Stderr> {
704 pub fn for_stderr() -> Self {
705 Self {
706 output: io::stderr(),
707 term_width: None,
708 }
709 }
710}
711
712impl<W> ProgressOutput<W> {
713 pub fn for_test(output: W, term_width: u16) -> Self {
714 Self {
715 output,
716 term_width: Some(term_width),
717 }
718 }
719
720 pub fn term_width(&self) -> Option<u16> {
721 self.term_width.or_else(term_width)
723 }
724
725 pub fn output_guard(&self, text: String) -> OutputGuard {
728 OutputGuard {
729 text,
730 output: io::stderr(),
731 }
732 }
733}
734
735impl<W: Write> ProgressOutput<W> {
736 pub fn write_fmt(&mut self, fmt: fmt::Arguments<'_>) -> io::Result<()> {
737 self.output.write_fmt(fmt)
738 }
739
740 pub fn flush(&mut self) -> io::Result<()> {
741 self.output.flush()
742 }
743}
744
745pub struct OutputGuard {
746 text: String,
747 output: Stderr,
748}
749
750impl Drop for OutputGuard {
751 #[instrument(skip_all)]
752 fn drop(&mut self) {
753 self.output.write_all(self.text.as_bytes()).ok();
754 self.output.flush().ok();
755 }
756}
757
758#[cfg(unix)]
759fn duplicate_child_stdin(stdin: &ChildStdin) -> io::Result<std::os::fd::OwnedFd> {
760 use std::os::fd::AsFd as _;
761 stdin.as_fd().try_clone_to_owned()
762}
763
764#[cfg(windows)]
765fn duplicate_child_stdin(stdin: &ChildStdin) -> io::Result<std::os::windows::io::OwnedHandle> {
766 use std::os::windows::io::AsHandle as _;
767 stdin.as_handle().try_clone_to_owned()
768}
769
770fn format_error_with_sources(err: &dyn error::Error) -> impl fmt::Display {
771 iter::successors(Some(err), |&err| err.source()).format(": ")
772}
773
774fn term_width() -> Option<u16> {
775 if let Some(cols) = env::var("COLUMNS").ok().and_then(|s| s.parse().ok()) {
776 Some(cols)
777 } else {
778 crossterm::terminal::size().ok().map(|(cols, _)| cols)
779 }
780}