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