radicle_term/
io.rs

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