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
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#[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 ($writer:expr; $($arg:tt)*) => ({
63 $crate::io::success_args($writer, format_args!($($arg)*));
64 });
65 ($($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 ($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}