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
25pub type Passphrase = Zeroizing<String>;
27
28pub 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 ($writer:expr; $($arg:tt)*) => ({
58 $crate::io::success_args($writer, format_args!($($arg)*));
59 });
60 ($($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 ($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}