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 thiserror::Error;
12use zeroize::Zeroizing;
13
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
30pub type Passphrase = Zeroizing<String>;
32
33pub 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#[derive(Clone)]
55pub enum PaintTarget {
56 Stdout,
57 Stderr,
58 Hidden,
59}
60
61impl PaintTarget {
62 pub fn writer(&self) -> Box<dyn io::Write> {
63 match self {
64 PaintTarget::Stdout => Box::new(io::stdout()),
65 PaintTarget::Stderr => Box::new(io::stderr()),
66 PaintTarget::Hidden => Box::new(io::sink()),
67 }
68 }
69}
70
71#[macro_export]
72macro_rules! info {
73 ($writer:expr; $($arg:tt)*) => ({
74 writeln!($writer, $($arg)*).ok();
75 });
76 ($($arg:tt)*) => ({
77 println!("{}", format_args!($($arg)*));
78 })
79}
80
81#[macro_export]
82macro_rules! success {
83 ($writer:expr; $($arg:tt)*) => ({
85 $crate::io::success_args($writer, format_args!($($arg)*));
86 });
87 ($($arg:tt)*) => ({
89 $crate::io::success_args(&mut std::io::stdout(), format_args!($($arg)*));
90 });
91}
92
93#[macro_export]
94macro_rules! tip {
95 ($($arg:tt)*) => ({
96 $crate::io::tip_args(format_args!($($arg)*));
97 })
98}
99
100#[macro_export]
101macro_rules! notice {
102 ($writer:expr; $($arg:tt)*) => ({
104 $crate::io::notice_args($writer, format_args!($($arg)*));
105 });
106 ($($arg:tt)*) => ({
107 $crate::io::notice_args(&mut std::io::stdout(), format_args!($($arg)*));
108 })
109}
110
111pub use info;
112pub use notice;
113pub use success;
114pub use tip;
115
116pub fn success_args<W: io::Write>(w: &mut W, args: fmt::Arguments) {
117 writeln!(w, "{PREFIX_SUCCESS} {args}").ok();
118}
119
120pub fn tip_args(args: fmt::Arguments) {
121 println!(
122 "{} {}",
123 format::yellow("*"),
124 style(format!("{args}")).italic()
125 );
126}
127
128pub fn notice_args<W: io::Write>(w: &mut W, args: fmt::Arguments) {
129 writeln!(w, "{} {args}", Paint::new(SYMBOL_WARNING).dim()).ok();
130}
131
132pub fn columns() -> Option<usize> {
133 crossterm::terminal::size()
134 .map(|(cols, _)| cols as usize)
135 .ok()
136}
137
138pub fn rows() -> Option<usize> {
139 crossterm::terminal::size()
140 .map(|(_, rows)| rows as usize)
141 .ok()
142}
143
144pub fn viewport() -> Option<Size> {
145 crossterm::terminal::size()
146 .map(|(cols, rows)| Size::new(cols as usize, rows as usize))
147 .ok()
148}
149
150pub fn headline(headline: impl fmt::Display) {
151 println!();
152 println!("{}", style(headline).bold());
153 println!();
154}
155
156pub fn header(header: &str) {
157 println!();
158 println!("{}", style(format::yellow(header)).bold().underline());
159 println!();
160}
161
162pub fn blob(text: impl fmt::Display) {
163 println!("{}", style(text.to_string().trim()).dim());
164}
165
166pub fn blank() {
167 println!()
168}
169
170pub fn print(msg: impl fmt::Display) {
171 println!("{msg}");
172}
173
174pub fn prefixed(prefix: &str, text: &str) -> String {
175 text.split('\n').fold(String::new(), |mut s, line| {
176 writeln!(&mut s, "{prefix}{line}").ok();
177 s
178 })
179}
180
181pub fn help(name: &str, version: &str, description: &str, usage: &str) {
182 println!("rad-{name} {version}\n{description}\n{usage}");
183}
184
185pub fn manual(name: &str) -> io::Result<process::ExitStatus> {
186 let mut child = process::Command::new("man")
187 .arg(name)
188 .stderr(Stdio::null())
189 .spawn()?;
190
191 child.wait()
192}
193
194pub fn usage(name: &str, usage: &str) {
195 println!(
196 "{} {}\n{}",
197 PREFIX_ERROR,
198 Paint::red(format!("Error: rad-{name}: invalid usage")),
199 Paint::red(prefixed(TAB, usage)).dim()
200 );
201}
202
203pub fn println(prefix: impl fmt::Display, msg: impl fmt::Display) {
204 println!("{prefix} {msg}");
205}
206
207pub fn indented(msg: impl fmt::Display) {
208 println!("{TAB}{msg}");
209}
210
211pub fn subcommand(msg: impl fmt::Display) {
212 println!("{}", style(format!("Running `{msg}`...")).dim());
213}
214
215pub fn warning(warning: impl fmt::Display) {
216 println!(
217 "{} {} {warning}",
218 PREFIX_WARNING,
219 Paint::yellow("Warning:").bold(),
220 );
221}
222
223pub fn error(error: impl fmt::Display) {
224 println!("{PREFIX_ERROR} {} {error}", Paint::red("Error:"));
225}
226
227pub fn hint(hint: impl fmt::Display) {
228 println!("{}", format::hint(format!("{SYMBOL_ERROR} Hint: {hint}")));
229}
230
231pub fn ask<D: fmt::Display>(prompt: D, default: bool) -> bool {
232 let prompt = prompt.to_string();
233
234 Confirm::new(&prompt)
235 .with_default(default)
236 .with_render_config(*CONFIG)
237 .prompt()
238 .unwrap_or_default()
239}
240
241pub fn confirm<D: fmt::Display>(prompt: D) -> bool {
242 ask(prompt, true)
243}
244
245pub fn abort<D: fmt::Display>(prompt: D) -> bool {
246 ask(prompt, false)
247}
248
249#[non_exhaustive]
250#[derive(Error, Debug)]
251pub enum InputError<Custom> {
252 #[error(transparent)]
253 Io(#[from] io::Error),
254
255 #[error(transparent)]
256 Custom(Custom),
257}
258
259impl From<InputError<std::convert::Infallible>> for io::Error {
260 fn from(val: InputError<std::convert::Infallible>) -> Self {
261 match val {
262 InputError::Io(err) => err,
263 InputError::Custom(_) => unreachable!("infallible cannot be constructed"),
264 }
265 }
266}
267
268pub fn input<T, E>(
272 message: &str,
273 default: Option<T>,
274 help: Option<&str>,
275) -> Result<Option<T>, InputError<E>>
276where
277 T: fmt::Display + std::str::FromStr<Err = E> + Clone,
278 E: std::error::Error + Send + Sync + 'static,
279{
280 let mut input = CustomType::<T>::new(message).with_render_config(*CONFIG);
281
282 input.default = default;
283 input.help_message = help;
284
285 match input.prompt() {
286 Ok(value) => Ok(Some(value)),
287 Err(err) => handle_inquire_error(err),
288 }
289}
290
291fn handle_inquire_error<T, E>(error: InquireError) -> Result<Option<T>, InputError<E>>
298where
299 E: std::error::Error + Send + Sync + 'static,
300{
301 use InquireError::*;
302
303 let inner = match error {
304 OperationCanceled | OperationInterrupted | NotTTY => None,
305 InvalidConfiguration(err) => {
306 panic!("{err}")
319 }
320 IO(err) => Some(InputError::Io(err)),
321 Custom(err) => {
322 match err.downcast::<E>() {
323 Ok(err) => Some(InputError::Custom(*err)),
324 Err(err) => {
325 panic!("inquire returned an unexpected error: {err:?}")
328 }
329 }
330 }
331 };
332
333 match inner {
334 Some(err) => Err(err),
335 None => Ok(None),
336 }
337}
338
339pub fn passphrase<V: validator::StringValidator + 'static>(
340 validate: V,
341) -> io::Result<Option<Passphrase>> {
342 match Password::new("Passphrase:")
343 .with_render_config(*CONFIG)
344 .with_display_mode(inquire::PasswordDisplayMode::Masked)
345 .without_confirmation()
346 .with_validator(validate)
347 .prompt()
348 {
349 Ok(p) => Ok(Some(Passphrase::from(p))),
350 Err(err) => handle_inquire_error(err).map_err(InputError::into),
351 }
352}
353
354pub fn passphrase_confirm<K: AsRef<OsStr>>(prompt: &str, var: K) -> io::Result<Option<Passphrase>> {
355 if let Ok(p) = env::var(var) {
356 return Ok(Some(Passphrase::from(p)));
357 }
358
359 match Password::new(prompt)
360 .with_render_config(*CONFIG)
361 .with_display_mode(inquire::PasswordDisplayMode::Masked)
362 .with_custom_confirmation_message("Repeat passphrase:")
363 .with_custom_confirmation_error_message("The passphrases don't match.")
364 .with_help_message("Leave this blank to keep your radicle key unencrypted")
365 .prompt()
366 {
367 Ok(p) => Ok(Some(Passphrase::from(p))),
368 Err(err) => handle_inquire_error(err).map_err(InputError::into),
369 }
370}
371
372pub fn passphrase_stdin() -> io::Result<Passphrase> {
373 let mut input = String::new();
374 std::io::stdin().read_line(&mut input)?;
375
376 Ok(Passphrase::from(input.trim_end().to_owned()))
377}
378
379pub fn select<'a, T>(prompt: &str, options: &'a [T], help: &str) -> Result<&'a T, InquireError>
380where
381 T: fmt::Display + Eq + PartialEq,
382{
383 let selection = Select::new(prompt, options.iter().collect::<Vec<_>>())
384 .with_vim_mode(true)
385 .with_help_message(help)
386 .with_render_config(*CONFIG);
387
388 selection.with_starting_cursor(0).prompt()
389}