just playing with tangled
1// Copyright 2020 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::io::{IsTerminal as _, Stderr, StderrLock, Stdout, StdoutLock, Write};
16use std::process::{Child, ChildStdin, Stdio};
17use std::str::FromStr;
18use std::thread::JoinHandle;
19use std::{env, fmt, io, mem};
20
21use minus::Pager as MinusPager;
22use tracing::instrument;
23
24use crate::command_error::{config_error_with_message, CommandError};
25use crate::config::CommandNameAndArgs;
26use crate::formatter::{Formatter, FormatterFactory, HeadingLabeledWriter, LabeledWriter};
27
28const BUILTIN_PAGER_NAME: &str = ":builtin";
29
30enum UiOutput {
31 Terminal {
32 stdout: Stdout,
33 stderr: Stderr,
34 },
35 Paged {
36 child: Child,
37 child_stdin: ChildStdin,
38 },
39 BuiltinPaged {
40 pager: BuiltinPager,
41 },
42}
43
44/// A builtin pager
45pub struct BuiltinPager {
46 pager: MinusPager,
47 dynamic_pager_thread: JoinHandle<()>,
48}
49
50impl std::io::Write for &BuiltinPager {
51 fn flush(&mut self) -> io::Result<()> {
52 // no-op since this is being run in a dynamic pager mode.
53 Ok(())
54 }
55
56 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
57 let string = std::str::from_utf8(buf).map_err(std::io::Error::other)?;
58 self.pager.push_str(string).map_err(std::io::Error::other)?;
59 Ok(buf.len())
60 }
61}
62
63impl Default for BuiltinPager {
64 fn default() -> Self {
65 Self::new()
66 }
67}
68
69impl BuiltinPager {
70 pub fn finalize(self) {
71 let dynamic_pager_thread = self.dynamic_pager_thread;
72 dynamic_pager_thread.join().unwrap();
73 }
74
75 pub fn new() -> Self {
76 let pager = MinusPager::new();
77 // Prefer to be cautious and only kill the pager instead of the whole process
78 // like minus does by default.
79 pager
80 .set_exit_strategy(minus::ExitStrategy::PagerQuit)
81 .expect("Able to set the exit strategy");
82 let pager_handle = pager.clone();
83
84 BuiltinPager {
85 pager,
86 dynamic_pager_thread: std::thread::spawn(move || {
87 // This thread handles the actual paging.
88 minus::dynamic_paging(pager_handle).unwrap();
89 }),
90 }
91 }
92}
93
94impl UiOutput {
95 fn new_builtin() -> UiOutput {
96 UiOutput::BuiltinPaged {
97 pager: BuiltinPager::new(),
98 }
99 }
100 fn new_terminal() -> UiOutput {
101 UiOutput::Terminal {
102 stdout: io::stdout(),
103 stderr: io::stderr(),
104 }
105 }
106
107 fn new_paged(pager_cmd: &CommandNameAndArgs) -> io::Result<UiOutput> {
108 let mut child = pager_cmd.to_command().stdin(Stdio::piped()).spawn()?;
109 let child_stdin = child.stdin.take().unwrap();
110 Ok(UiOutput::Paged { child, child_stdin })
111 }
112}
113
114pub enum UiStdout<'a> {
115 Terminal(StdoutLock<'static>),
116 Paged(&'a ChildStdin),
117 Builtin(&'a BuiltinPager),
118}
119
120pub enum UiStderr<'a> {
121 Terminal(StderrLock<'static>),
122 Paged(&'a ChildStdin),
123 Builtin(&'a BuiltinPager),
124}
125
126macro_rules! for_outputs {
127 ($ty:ident, $output:expr, $pat:pat => $expr:expr) => {
128 match $output {
129 $ty::Terminal($pat) => $expr,
130 $ty::Paged($pat) => $expr,
131 $ty::Builtin($pat) => $expr,
132 }
133 };
134}
135
136impl Write for UiStdout<'_> {
137 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
138 for_outputs!(Self, self, w => w.write(buf))
139 }
140
141 fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
142 for_outputs!(Self, self, w => w.write_all(buf))
143 }
144
145 fn flush(&mut self) -> io::Result<()> {
146 for_outputs!(Self, self, w => w.flush())
147 }
148}
149
150impl Write for UiStderr<'_> {
151 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
152 for_outputs!(Self, self, w => w.write(buf))
153 }
154
155 fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
156 for_outputs!(Self, self, w => w.write_all(buf))
157 }
158
159 fn flush(&mut self) -> io::Result<()> {
160 for_outputs!(Self, self, w => w.flush())
161 }
162}
163
164pub struct Ui {
165 color: bool,
166 quiet: bool,
167 pager_cmd: CommandNameAndArgs,
168 paginate: PaginationChoice,
169 progress_indicator: bool,
170 formatter_factory: FormatterFactory,
171 output: UiOutput,
172}
173
174fn progress_indicator_setting(config: &config::Config) -> bool {
175 config.get_bool("ui.progress-indicator").unwrap_or(true)
176}
177
178#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
179pub enum ColorChoice {
180 Always,
181 Never,
182 #[default]
183 Auto,
184}
185
186impl FromStr for ColorChoice {
187 type Err = &'static str;
188
189 fn from_str(s: &str) -> Result<Self, Self::Err> {
190 match s {
191 "always" => Ok(ColorChoice::Always),
192 "never" => Ok(ColorChoice::Never),
193 "auto" => Ok(ColorChoice::Auto),
194 _ => Err("must be one of always, never, or auto"),
195 }
196 }
197}
198
199impl fmt::Display for ColorChoice {
200 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201 let s = match self {
202 ColorChoice::Always => "always",
203 ColorChoice::Never => "never",
204 ColorChoice::Auto => "auto",
205 };
206 write!(f, "{s}")
207 }
208}
209
210fn color_setting(config: &config::Config) -> ColorChoice {
211 config
212 .get_string("ui.color")
213 .ok()
214 .and_then(|s| s.parse().ok())
215 .unwrap_or_default()
216}
217
218fn use_color(choice: ColorChoice) -> bool {
219 match choice {
220 ColorChoice::Always => true,
221 ColorChoice::Never => false,
222 ColorChoice::Auto => io::stdout().is_terminal(),
223 }
224}
225
226fn be_quiet(config: &config::Config) -> bool {
227 config.get_bool("ui.quiet").unwrap_or_default()
228}
229
230#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Deserialize)]
231#[serde(rename_all(deserialize = "kebab-case"))]
232pub enum PaginationChoice {
233 Never,
234 #[default]
235 Auto,
236}
237
238fn pagination_setting(config: &config::Config) -> Result<PaginationChoice, CommandError> {
239 config
240 .get::<PaginationChoice>("ui.paginate")
241 .map_err(|err| config_error_with_message("Invalid `ui.paginate`", err))
242}
243
244fn pager_setting(config: &config::Config) -> Result<CommandNameAndArgs, CommandError> {
245 config
246 .get::<CommandNameAndArgs>("ui.pager")
247 .map_err(|err| config_error_with_message("Invalid `ui.pager`", err))
248}
249
250impl Ui {
251 pub fn with_config(config: &config::Config) -> Result<Ui, CommandError> {
252 let color = use_color(color_setting(config));
253 let quiet = be_quiet(config);
254 // Sanitize ANSI escape codes if we're printing to a terminal. Doesn't affect
255 // ANSI escape codes that originate from the formatter itself.
256 let sanitize = io::stdout().is_terminal();
257 let formatter_factory = FormatterFactory::prepare(config, color, sanitize)?;
258 let progress_indicator = progress_indicator_setting(config);
259 Ok(Ui {
260 color,
261 quiet,
262 formatter_factory,
263 pager_cmd: pager_setting(config)?,
264 paginate: pagination_setting(config)?,
265 progress_indicator,
266 output: UiOutput::new_terminal(),
267 })
268 }
269
270 pub fn reset(&mut self, config: &config::Config) -> Result<(), CommandError> {
271 self.color = use_color(color_setting(config));
272 self.quiet = be_quiet(config);
273 self.paginate = pagination_setting(config)?;
274 self.pager_cmd = pager_setting(config)?;
275 self.progress_indicator = progress_indicator_setting(config);
276 let sanitize = io::stdout().is_terminal();
277 self.formatter_factory = FormatterFactory::prepare(config, self.color, sanitize)?;
278 Ok(())
279 }
280
281 /// Switches the output to use the pager, if allowed.
282 #[instrument(skip_all)]
283 pub fn request_pager(&mut self) {
284 match self.paginate {
285 PaginationChoice::Never => return,
286 PaginationChoice::Auto => {}
287 }
288
289 match self.output {
290 UiOutput::Terminal { .. } if io::stdout().is_terminal() => {
291 if self.pager_cmd == CommandNameAndArgs::String(BUILTIN_PAGER_NAME.into()) {
292 self.output = UiOutput::new_builtin();
293 return;
294 }
295
296 match UiOutput::new_paged(&self.pager_cmd) {
297 Ok(pager_output) => {
298 self.output = pager_output;
299 }
300 Err(e) => {
301 // The pager executable couldn't be found or couldn't be run
302 writeln!(
303 self.warning_default(),
304 "Failed to spawn pager '{name}': {e}. Consider using the `:builtin` \
305 pager.",
306 name = self.pager_cmd.split_name(),
307 )
308 .ok();
309 }
310 }
311 }
312 UiOutput::Terminal { .. } | UiOutput::BuiltinPaged { .. } | UiOutput::Paged { .. } => {}
313 }
314 }
315
316 pub fn color(&self) -> bool {
317 self.color
318 }
319
320 pub fn new_formatter<'output, W: Write + 'output>(
321 &self,
322 output: W,
323 ) -> Box<dyn Formatter + 'output> {
324 self.formatter_factory.new_formatter(output)
325 }
326
327 /// Locked stdout stream.
328 pub fn stdout(&self) -> UiStdout<'_> {
329 match &self.output {
330 UiOutput::Terminal { stdout, .. } => UiStdout::Terminal(stdout.lock()),
331 UiOutput::Paged { child_stdin, .. } => UiStdout::Paged(child_stdin),
332 UiOutput::BuiltinPaged { pager } => UiStdout::Builtin(pager),
333 }
334 }
335
336 /// Creates a formatter for the locked stdout stream.
337 ///
338 /// Labels added to the returned formatter should be removed by caller.
339 /// Otherwise the last color would persist.
340 pub fn stdout_formatter(&self) -> Box<dyn Formatter + '_> {
341 for_outputs!(UiStdout, self.stdout(), w => self.new_formatter(w))
342 }
343
344 /// Locked stderr stream.
345 pub fn stderr(&self) -> UiStderr<'_> {
346 match &self.output {
347 UiOutput::Terminal { stderr, .. } => UiStderr::Terminal(stderr.lock()),
348 UiOutput::Paged { child_stdin, .. } => UiStderr::Paged(child_stdin),
349 UiOutput::BuiltinPaged { pager } => UiStderr::Builtin(pager),
350 }
351 }
352
353 /// Creates a formatter for the locked stderr stream.
354 pub fn stderr_formatter(&self) -> Box<dyn Formatter + '_> {
355 for_outputs!(UiStderr, self.stderr(), w => self.new_formatter(w))
356 }
357
358 /// Stderr stream to be attached to a child process.
359 pub fn stderr_for_child(&self) -> io::Result<Stdio> {
360 match &self.output {
361 UiOutput::Terminal { .. } => Ok(Stdio::inherit()),
362 UiOutput::Paged { child_stdin, .. } => Ok(duplicate_child_stdin(child_stdin)?.into()),
363 // Stderr does not get redirected through the built-in pager.
364 UiOutput::BuiltinPaged { .. } => Ok(Stdio::inherit()),
365 }
366 }
367
368 /// Whether continuous feedback should be displayed for long-running
369 /// operations
370 pub fn use_progress_indicator(&self) -> bool {
371 match &self.output {
372 UiOutput::Terminal { stderr, .. } => self.progress_indicator && stderr.is_terminal(),
373 UiOutput::Paged { .. } => false,
374 UiOutput::BuiltinPaged { .. } => false,
375 }
376 }
377
378 pub fn progress_output(&self) -> Option<ProgressOutput> {
379 self.use_progress_indicator().then(|| ProgressOutput {
380 output: io::stderr(),
381 })
382 }
383
384 /// Writer to print an update that's not part of the command's main output.
385 pub fn status(&self) -> Box<dyn Write + '_> {
386 if self.quiet {
387 Box::new(std::io::sink())
388 } else {
389 Box::new(self.stderr())
390 }
391 }
392
393 /// A formatter to print an update that's not part of the command's main
394 /// output. Returns `None` if `--quiet` was requested.
395 pub fn status_formatter(&self) -> Option<Box<dyn Formatter + '_>> {
396 (!self.quiet).then(|| self.stderr_formatter())
397 }
398
399 /// Writer to print hint with the default "Hint: " heading.
400 pub fn hint_default(
401 &self,
402 ) -> Option<HeadingLabeledWriter<Box<dyn Formatter + '_>, &'static str, &'static str>> {
403 self.hint_with_heading("Hint: ")
404 }
405
406 /// Writer to print hint without the "Hint: " heading.
407 pub fn hint_no_heading(&self) -> Option<LabeledWriter<Box<dyn Formatter + '_>, &'static str>> {
408 (!self.quiet).then(|| LabeledWriter::new(self.stderr_formatter(), "hint"))
409 }
410
411 /// Writer to print hint with the given heading.
412 pub fn hint_with_heading<H: fmt::Display>(
413 &self,
414 heading: H,
415 ) -> Option<HeadingLabeledWriter<Box<dyn Formatter + '_>, &'static str, H>> {
416 (!self.quiet).then(|| HeadingLabeledWriter::new(self.stderr_formatter(), "hint", heading))
417 }
418
419 /// Writer to print warning with the default "Warning: " heading.
420 pub fn warning_default(
421 &self,
422 ) -> HeadingLabeledWriter<Box<dyn Formatter + '_>, &'static str, &'static str> {
423 self.warning_with_heading("Warning: ")
424 }
425
426 /// Writer to print warning without the "Warning: " heading.
427 pub fn warning_no_heading(&self) -> LabeledWriter<Box<dyn Formatter + '_>, &'static str> {
428 LabeledWriter::new(self.stderr_formatter(), "warning")
429 }
430
431 /// Writer to print warning with the given heading.
432 pub fn warning_with_heading<H: fmt::Display>(
433 &self,
434 heading: H,
435 ) -> HeadingLabeledWriter<Box<dyn Formatter + '_>, &'static str, H> {
436 HeadingLabeledWriter::new(self.stderr_formatter(), "warning", heading)
437 }
438
439 /// Writer to print error without the "Error: " heading.
440 pub fn error_no_heading(&self) -> LabeledWriter<Box<dyn Formatter + '_>, &'static str> {
441 LabeledWriter::new(self.stderr_formatter(), "error")
442 }
443
444 /// Writer to print error with the given heading.
445 pub fn error_with_heading<H: fmt::Display>(
446 &self,
447 heading: H,
448 ) -> HeadingLabeledWriter<Box<dyn Formatter + '_>, &'static str, H> {
449 HeadingLabeledWriter::new(self.stderr_formatter(), "error", heading)
450 }
451
452 /// Waits for the pager exits.
453 #[instrument(skip_all)]
454 pub fn finalize_pager(&mut self) {
455 match mem::replace(&mut self.output, UiOutput::new_terminal()) {
456 UiOutput::Paged {
457 mut child,
458 child_stdin,
459 } => {
460 drop(child_stdin);
461 if let Err(e) = child.wait() {
462 // It's possible (though unlikely) that this write fails, but
463 // this function gets called so late that there's not much we
464 // can do about it.
465 writeln!(self.warning_default(), "Failed to wait on pager: {e}").ok();
466 }
467 }
468 UiOutput::BuiltinPaged { pager } => {
469 pager.finalize();
470 }
471 _ => { /* no-op */ }
472 }
473 }
474
475 pub fn can_prompt() -> bool {
476 io::stdout().is_terminal()
477 || env::var("JJ_INTERACTIVE")
478 .map(|v| v == "1")
479 .unwrap_or(false)
480 }
481
482 #[allow(unknown_lints)] // XXX FIXME (aseipp): nightly bogons; re-test this occasionally
483 #[allow(clippy::assigning_clones)]
484 pub fn prompt(&self, prompt: &str) -> io::Result<String> {
485 if !Self::can_prompt() {
486 return Err(io::Error::new(
487 io::ErrorKind::Unsupported,
488 "Cannot prompt for input since the output is not connected to a terminal",
489 ));
490 }
491 write!(self.stdout(), "{prompt}: ")?;
492 self.stdout().flush()?;
493 let mut buf = String::new();
494 io::stdin().read_line(&mut buf)?;
495
496 if let Some(trimmed) = buf.strip_suffix('\n') {
497 buf = trimmed.to_owned();
498 } else if buf.is_empty() {
499 return Err(io::Error::new(
500 io::ErrorKind::UnexpectedEof,
501 "Prompt cancelled by EOF",
502 ));
503 }
504
505 Ok(buf)
506 }
507
508 /// Repeat the given prompt until the input is one of the specified choices.
509 pub fn prompt_choice(
510 &self,
511 prompt: &str,
512 choices: &[impl AsRef<str>],
513 default: Option<&str>,
514 ) -> io::Result<String> {
515 if !Self::can_prompt() {
516 if let Some(default) = default {
517 // Choose the default automatically without waiting.
518 writeln!(self.stdout(), "{prompt}: {default}")?;
519 return Ok(default.to_owned());
520 }
521 }
522
523 loop {
524 let choice = self.prompt(prompt)?.trim().to_owned();
525 if choice.is_empty() {
526 if let Some(default) = default {
527 return Ok(default.to_owned());
528 }
529 }
530 if choices.iter().any(|c| choice == c.as_ref()) {
531 return Ok(choice);
532 }
533
534 writeln!(self.warning_no_heading(), "unrecognized response")?;
535 }
536 }
537
538 /// Prompts for a yes-or-no response, with yes = true and no = false.
539 pub fn prompt_yes_no(&self, prompt: &str, default: Option<bool>) -> io::Result<bool> {
540 let default_str = match &default {
541 Some(true) => "(Yn)",
542 Some(false) => "(yN)",
543 None => "(yn)",
544 };
545 let default_choice = default.map(|c| if c { "Y" } else { "N" });
546
547 let choice = self.prompt_choice(
548 &format!("{} {}", prompt, default_str),
549 &["y", "n", "yes", "no", "Yes", "No", "YES", "NO"],
550 default_choice,
551 )?;
552 Ok(choice.starts_with(['y', 'Y']))
553 }
554
555 pub fn prompt_password(&self, prompt: &str) -> io::Result<String> {
556 if !io::stdout().is_terminal() {
557 return Err(io::Error::new(
558 io::ErrorKind::Unsupported,
559 "Cannot prompt for input since the output is not connected to a terminal",
560 ));
561 }
562 rpassword::prompt_password(format!("{prompt}: "))
563 }
564
565 pub fn term_width(&self) -> Option<u16> {
566 term_width()
567 }
568}
569
570#[derive(Debug)]
571pub struct ProgressOutput {
572 output: Stderr,
573}
574
575impl ProgressOutput {
576 pub fn write_fmt(&mut self, fmt: fmt::Arguments<'_>) -> io::Result<()> {
577 self.output.write_fmt(fmt)
578 }
579
580 pub fn flush(&mut self) -> io::Result<()> {
581 self.output.flush()
582 }
583
584 pub fn term_width(&self) -> Option<u16> {
585 // Terminal can be resized while progress is displayed, so don't cache it.
586 term_width()
587 }
588
589 /// Construct a guard object which writes `text` when dropped. Useful for
590 /// restoring terminal state.
591 pub fn output_guard(&self, text: String) -> OutputGuard {
592 OutputGuard {
593 text,
594 output: io::stderr(),
595 }
596 }
597}
598
599pub struct OutputGuard {
600 text: String,
601 output: Stderr,
602}
603
604impl Drop for OutputGuard {
605 #[instrument(skip_all)]
606 fn drop(&mut self) {
607 _ = self.output.write_all(self.text.as_bytes());
608 _ = self.output.flush();
609 }
610}
611
612#[cfg(unix)]
613fn duplicate_child_stdin(stdin: &ChildStdin) -> io::Result<std::os::fd::OwnedFd> {
614 use std::os::fd::AsFd as _;
615 stdin.as_fd().try_clone_to_owned()
616}
617
618#[cfg(windows)]
619fn duplicate_child_stdin(stdin: &ChildStdin) -> io::Result<std::os::windows::io::OwnedHandle> {
620 use std::os::windows::io::AsHandle as _;
621 stdin.as_handle().try_clone_to_owned()
622}
623
624fn term_width() -> Option<u16> {
625 if let Some(cols) = env::var("COLUMNS").ok().and_then(|s| s.parse().ok()) {
626 Some(cols)
627 } else {
628 crossterm::terminal::size().ok().map(|(cols, _)| cols)
629 }
630}