1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
//! Terminal user interface primitives used by [Scarb] and its extensions.
//!
//! This crate focuses mainly on two areas:
//!
//! 1. [`Ui`] and [`components`]: Serving a unified interface for communication with the user,
//!    either via:
//!     - rendering human-readable messages or interactive widgets,
//!     - or printing machine-parseable JSON-NL messages, depending on runtime configuration.
//! 2. [`args`]: Providing reusable [`clap`] arguments for common tasks.
//!
//! There are also re-export from various TUI crates recommended for use in Scarb ecosystem,
//! such as [`indicatif`] or [`console`].
//!
//! [scarb]: https://docs.swmansion.com/scarb

#![deny(clippy::dbg_macro)]
#![deny(clippy::disallowed_methods)]
#![deny(missing_docs)]
#![deny(rustdoc::broken_intra_doc_links)]
#![deny(rustdoc::missing_crate_level_docs)]
#![deny(rustdoc::private_intra_doc_links)]
#![warn(rust_2018_idioms)]

use clap::ValueEnum;
pub use indicatif::{
    BinaryBytes, DecimalBytes, FormattedDuration, HumanBytes, HumanCount, HumanDuration,
    HumanFloatCount,
};

pub use message::*;
pub use verbosity::*;
pub use widget::*;

use crate::components::TypedMessage;

pub mod args;
pub mod components;
mod message;
mod verbosity;
mod widget;

/// The requested format of output (either textual or JSON).
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, ValueEnum)]
pub enum OutputFormat {
    /// Render human-readable messages and interactive widgets.
    #[default]
    Text,
    /// Render machine-parseable JSON-NL messages.
    Json,
}

/// An abstraction around console output which stores preferences for output format (human vs JSON),
/// colour, etc.
///
/// All human-oriented messaging (basically all writes to `stdout`) must go through this object.
#[derive(Debug)]
pub struct Ui {
    verbosity: Verbosity,
    output_format: OutputFormat,
}

impl Ui {
    /// Create a new [`Ui`] instance configured with the given verbosity and output format.
    pub fn new(verbosity: Verbosity, output_format: OutputFormat) -> Self {
        Self {
            verbosity,
            output_format,
        }
    }

    /// Get the verbosity level of this [`Ui`] instance.
    pub fn verbosity(&self) -> Verbosity {
        self.verbosity
    }

    /// Get the output format of this [`Ui`] instance.
    pub fn output_format(&self) -> OutputFormat {
        self.output_format
    }

    /// Print the message to standard output if not in quiet verbosity mode.
    pub fn print<T: Message>(&self, message: T) {
        if self.verbosity >= Verbosity::Normal {
            self.do_print(message);
        }
    }

    /// Print the message to the standard output only in verbose mode.
    pub fn verbose<T: Message>(&self, message: T) {
        if self.verbosity >= Verbosity::Verbose {
            self.do_print(message);
        }
    }

    /// Display an interactive widget and return a handle for further interaction.
    ///
    /// The widget will be only displayed if not in quiet mode, and if the output format is text.
    pub fn widget<T: Widget>(&self, widget: T) -> Option<T::Handle> {
        if self.output_format == OutputFormat::Text && self.verbosity >= Verbosity::Normal {
            let handle = widget.text();
            Some(handle)
        } else {
            None
        }
    }

    /// Print a warning to the user.
    pub fn warn(&self, message: impl AsRef<str>) {
        self.print(TypedMessage::styled("warn", "yellow", message.as_ref()))
    }

    /// Print an error to the user.
    pub fn error(&self, message: impl AsRef<str>) {
        self.print(TypedMessage::styled("error", "red", message.as_ref()))
    }

    /// Nicely format an [`anyhow::Error`] for display to the user, and print it with [`Ui::error`].
    pub fn anyhow(&self, error: &anyhow::Error) {
        // NOTE: Some errors, particularly ones from `toml_edit` like to add trailing newlines.
        //   This isn't a big problem for users, but it's causing issues in tests, where trailing
        //   whitespace collides with `indoc`.
        self.error(format!("{error:?}").trim())
    }

    fn do_print<T: Message>(&self, message: T) {
        match self.output_format {
            OutputFormat::Text => message.print_text(),
            OutputFormat::Json => message.print_json(),
        }
    }

    /// Forces colorization on or off for stdout.
    ///
    /// This overrides the default for the current process and changes the return value of
    /// the [`Ui::has_colors_enabled`] function.
    pub fn force_colors_enabled(&self, enable: bool) {
        console::set_colors_enabled(enable);
    }

    /// Returns `true` if colors should be enabled for stdout.
    ///
    /// This honors the [clicolors spec](http://bixense.com/clicolors/).
    ///
    /// * `CLICOLOR != 0`: ANSI colors are supported and should be used when the program isn't piped.
    /// * `CLICOLOR == 0`: Don't output ANSI color escape codes.
    /// * `CLICOLOR_FORCE != 0`: ANSI colors should be enabled no matter what.
    pub fn has_colors_enabled(&self) -> bool {
        console::colors_enabled()
    }
}