radicle_term/
io.rs

1use std::ffi::OsStr;
2use std::fmt::Write;
3use std::process::Stdio;
4use std::sync::LazyLock;
5use std::{env, fmt, io, process};
6
7use inquire::ui::{ErrorMessageRenderConfig, StyleSheet, Styled};
8use inquire::validator;
9use inquire::InquireError;
10use inquire::{ui::Color, ui::RenderConfig, Confirm, CustomType, Password};
11use zeroize::Zeroizing;
12
13use crate::command;
14use crate::format;
15use crate::{style, Paint, Size};
16
17pub use inquire;
18pub use inquire::Select;
19
20pub(crate) const SYMBOL_ERROR: &str = "✗";
21pub(crate) const SYMBOL_SUCCESS: &str = "✓";
22pub(crate) const SYMBOL_WARNING: &str = "!";
23
24pub const PREFIX_ERROR: Paint<&str> = Paint::red(SYMBOL_ERROR);
25pub const PREFIX_SUCCESS: Paint<&str> = Paint::green(SYMBOL_SUCCESS);
26pub const PREFIX_WARNING: Paint<&str> = Paint::yellow(SYMBOL_WARNING);
27
28pub const TAB: &str = "    ";
29
30/// Passphrase input.
31pub type Passphrase = Zeroizing<String>;
32
33/// Render configuration.
34pub static CONFIG: LazyLock<RenderConfig> = LazyLock::new(|| RenderConfig {
35    prompt: StyleSheet::new().with_fg(Color::LightCyan),
36    prompt_prefix: Styled::new("?").with_fg(Color::LightBlue),
37    answered_prompt_prefix: Styled::new(SYMBOL_SUCCESS).with_fg(Color::LightGreen),
38    answer: StyleSheet::new(),
39    highlighted_option_prefix: Styled::new(SYMBOL_SUCCESS).with_fg(Color::LightYellow),
40    selected_option: Some(StyleSheet::new().with_fg(Color::LightYellow)),
41    option: StyleSheet::new(),
42    help_message: StyleSheet::new().with_fg(Color::DarkGrey),
43    default_value: StyleSheet::new().with_fg(Color::LightBlue),
44    error_message: ErrorMessageRenderConfig::default_colored()
45        .with_prefix(Styled::new(SYMBOL_ERROR).with_fg(Color::LightRed)),
46    ..RenderConfig::default_colored()
47});
48
49#[macro_export]
50macro_rules! info {
51    ($writer:expr; $($arg:tt)*) => ({
52        writeln!($writer, $($arg)*).ok();
53    });
54    ($($arg:tt)*) => ({
55        println!("{}", format_args!($($arg)*));
56    })
57}
58
59#[macro_export]
60macro_rules! success {
61    // Pattern when a writer is provided.
62    ($writer:expr; $($arg:tt)*) => ({
63        $crate::io::success_args($writer, format_args!($($arg)*));
64    });
65    // Pattern without writer.
66    ($($arg:tt)*) => ({
67        $crate::io::success_args(&mut std::io::stdout(), format_args!($($arg)*));
68    });
69}
70
71#[macro_export]
72macro_rules! tip {
73    ($($arg:tt)*) => ({
74        $crate::io::tip_args(format_args!($($arg)*));
75    })
76}
77
78#[macro_export]
79macro_rules! notice {
80    // Pattern when a writer is provided.
81    ($writer:expr; $($arg:tt)*) => ({
82        $crate::io::notice_args($writer, format_args!($($arg)*));
83    });
84    ($($arg:tt)*) => ({
85        $crate::io::notice_args(&mut std::io::stdout(), format_args!($($arg)*));
86    })
87}
88
89pub use info;
90pub use notice;
91pub use success;
92pub use tip;
93
94pub fn success_args<W: io::Write>(w: &mut W, args: fmt::Arguments) {
95    writeln!(w, "{PREFIX_SUCCESS} {args}").ok();
96}
97
98pub fn tip_args(args: fmt::Arguments) {
99    println!(
100        "{} {}",
101        format::yellow("*"),
102        style(format!("{args}")).italic()
103    );
104}
105
106pub fn notice_args<W: io::Write>(w: &mut W, args: fmt::Arguments) {
107    writeln!(w, "{} {args}", Paint::new(SYMBOL_WARNING).dim()).ok();
108}
109
110pub fn columns() -> Option<usize> {
111    crossterm::terminal::size()
112        .map(|(cols, _)| cols as usize)
113        .ok()
114}
115
116pub fn rows() -> Option<usize> {
117    crossterm::terminal::size()
118        .map(|(_, rows)| rows as usize)
119        .ok()
120}
121
122pub fn viewport() -> Option<Size> {
123    crossterm::terminal::size()
124        .map(|(cols, rows)| Size::new(cols as usize, rows as usize))
125        .ok()
126}
127
128pub fn headline(headline: impl fmt::Display) {
129    println!();
130    println!("{}", style(headline).bold());
131    println!();
132}
133
134pub fn header(header: &str) {
135    println!();
136    println!("{}", style(format::yellow(header)).bold().underline());
137    println!();
138}
139
140pub fn blob(text: impl fmt::Display) {
141    println!("{}", style(text.to_string().trim()).dim());
142}
143
144pub fn blank() {
145    println!()
146}
147
148pub fn print(msg: impl fmt::Display) {
149    println!("{msg}");
150}
151
152pub fn prefixed(prefix: &str, text: &str) -> String {
153    text.split('\n').fold(String::new(), |mut s, line| {
154        writeln!(&mut s, "{prefix}{line}").ok();
155        s
156    })
157}
158
159pub fn help(name: &str, version: &str, description: &str, usage: &str) {
160    println!("rad-{name} {version}\n{description}\n{usage}");
161}
162
163pub fn manual(name: &str) -> io::Result<process::ExitStatus> {
164    let mut child = process::Command::new("man")
165        .arg(name)
166        .stderr(Stdio::null())
167        .spawn()?;
168
169    child.wait()
170}
171
172pub fn usage(name: &str, usage: &str) {
173    println!(
174        "{} {}\n{}",
175        PREFIX_ERROR,
176        Paint::red(format!("Error: rad-{name}: invalid usage")),
177        Paint::red(prefixed(TAB, usage)).dim()
178    );
179}
180
181pub fn println(prefix: impl fmt::Display, msg: impl fmt::Display) {
182    println!("{prefix} {msg}");
183}
184
185pub fn indented(msg: impl fmt::Display) {
186    println!("{TAB}{msg}");
187}
188
189pub fn subcommand(msg: impl fmt::Display) {
190    println!("{}", style(format!("Running `{msg}`...")).dim());
191}
192
193pub fn warning(warning: impl fmt::Display) {
194    println!(
195        "{} {} {warning}",
196        PREFIX_WARNING,
197        Paint::yellow("Warning:").bold(),
198    );
199}
200
201pub fn error(error: impl fmt::Display) {
202    println!("{PREFIX_ERROR} {} {error}", Paint::red("Error:"));
203}
204
205pub fn hint(hint: impl fmt::Display) {
206    println!("{}", format::hint(format!("{SYMBOL_ERROR} Hint: {hint}")));
207}
208
209pub fn ask<D: fmt::Display>(prompt: D, default: bool) -> bool {
210    let prompt = prompt.to_string();
211
212    Confirm::new(&prompt)
213        .with_default(default)
214        .with_render_config(*CONFIG)
215        .prompt()
216        .unwrap_or_default()
217}
218
219pub fn confirm<D: fmt::Display>(prompt: D) -> bool {
220    ask(prompt, true)
221}
222
223pub fn abort<D: fmt::Display>(prompt: D) -> bool {
224    ask(prompt, false)
225}
226
227pub fn input<S, E>(message: &str, default: Option<S>, help: Option<&str>) -> anyhow::Result<S>
228where
229    S: fmt::Display + std::str::FromStr<Err = E> + Clone,
230    E: fmt::Debug + fmt::Display,
231{
232    let mut input = CustomType::<S>::new(message).with_render_config(*CONFIG);
233
234    input.default = default;
235    input.help_message = help;
236
237    let value = input.prompt()?;
238
239    Ok(value)
240}
241
242pub fn passphrase<V: validator::StringValidator + 'static>(
243    validate: V,
244) -> Result<Passphrase, inquire::InquireError> {
245    Ok(Passphrase::from(
246        Password::new("Passphrase:")
247            .with_render_config(*CONFIG)
248            .with_display_mode(inquire::PasswordDisplayMode::Masked)
249            .without_confirmation()
250            .with_validator(validate)
251            .prompt()?,
252    ))
253}
254
255pub fn passphrase_confirm<K: AsRef<OsStr>>(
256    prompt: &str,
257    var: K,
258) -> Result<Passphrase, anyhow::Error> {
259    if let Ok(p) = env::var(var) {
260        Ok(Passphrase::from(p))
261    } else {
262        Ok(Passphrase::from(
263            Password::new(prompt)
264                .with_render_config(*CONFIG)
265                .with_display_mode(inquire::PasswordDisplayMode::Masked)
266                .with_custom_confirmation_message("Repeat passphrase:")
267                .with_custom_confirmation_error_message("The passphrases don't match.")
268                .with_help_message("Leave this blank to keep your radicle key unencrypted")
269                .prompt()?,
270        ))
271    }
272}
273
274pub fn passphrase_stdin() -> Result<Passphrase, anyhow::Error> {
275    let mut input = String::new();
276    std::io::stdin().read_line(&mut input)?;
277
278    Ok(Passphrase::from(input.trim_end().to_owned()))
279}
280
281pub fn select<'a, T>(prompt: &str, options: &'a [T], help: &str) -> Result<&'a T, InquireError>
282where
283    T: fmt::Display + Eq + PartialEq,
284{
285    let selection = Select::new(prompt, options.iter().collect::<Vec<_>>())
286        .with_vim_mode(true)
287        .with_help_message(help)
288        .with_render_config(*CONFIG);
289
290    selection.with_starting_cursor(0).prompt()
291}
292
293pub fn markdown(content: &str) {
294    if !content.is_empty() && command::bat(["-p", "-l", "md"], content).is_err() {
295        blob(content);
296    }
297}