use std::fmt; use inquire::ui::{ErrorMessageRenderConfig, StyleSheet, Styled}; use inquire::InquireError; use inquire::{ui::Color, ui::RenderConfig, Confirm, CustomType, Password, Select}; use once_cell::sync::Lazy; use radicle::cob::issue::Issue; use radicle::cob::thread::{Comment, CommentId}; use radicle::crypto::ssh::keystore::{MemorySigner, Passphrase}; use radicle::crypto::Signer; use radicle::profile; use radicle::profile::Profile; use super::command; use super::format; use super::spinner::spinner; use super::Error; use super::{style, Paint}; pub const ERROR_PREFIX: Paint<&str> = Paint::red("✗"); pub const ERROR_HINT_PREFIX: Paint<&str> = Paint::yellow("✗"); pub const WARNING_PREFIX: Paint<&str> = Paint::yellow("!"); pub const TAB: &str = " "; /// Render configuration. pub static CONFIG: Lazy = Lazy::new(|| RenderConfig { prompt: StyleSheet::new().with_fg(Color::LightCyan), prompt_prefix: Styled::new("?").with_fg(Color::LightBlue), answered_prompt_prefix: Styled::new("✓").with_fg(Color::LightGreen), answer: StyleSheet::new(), highlighted_option_prefix: Styled::new("*").with_fg(Color::LightYellow), help_message: StyleSheet::new().with_fg(Color::DarkGrey), error_message: ErrorMessageRenderConfig::default_colored() .with_prefix(Styled::new("✗").with_fg(Color::LightRed)), ..RenderConfig::default_colored() }); #[macro_export] macro_rules! info { ($($arg:tt)*) => ({ println!("{}", format_args!($($arg)*)); }) } #[macro_export] macro_rules! success { ($($arg:tt)*) => ({ $crate::terminal::io::success_args(format_args!($($arg)*)); }) } #[macro_export] macro_rules! tip { ($($arg:tt)*) => ({ $crate::terminal::io::tip_args(format_args!($($arg)*)); }) } pub use info; pub use success; pub use tip; pub fn success_args(args: fmt::Arguments) { println!("{} {args}", Paint::green("✓")); } pub fn tip_args(args: fmt::Arguments) { println!("👉 {}", style(format!("{args}")).italic()); } pub fn columns() -> Option { termion::terminal_size().map(|(cols, _)| cols as usize).ok() } pub fn headline(headline: impl fmt::Display) { println!(); println!("{}", style(headline).bold()); println!(); } pub fn header(header: &str) { println!(); println!("{}", style(format::yellow(header)).bold().underline()); println!(); } pub fn blob(text: impl fmt::Display) { println!("{}", style(text.to_string().trim()).dim()); } pub fn blank() { println!() } pub fn print(msg: impl fmt::Display) { println!("{msg}"); } pub fn prefixed(prefix: &str, text: &str) -> String { text.split('\n') .map(|line| format!("{prefix}{line}\n")) .collect() } pub fn help(name: &str, version: &str, description: &str, usage: &str) { println!("rad-{name} {version}\n{description}\n{usage}"); } pub fn usage(name: &str, usage: &str) { println!( "{} {}\n{}", ERROR_PREFIX, Paint::red(format!("Error: rad-{name}: invalid usage")), Paint::red(prefixed(TAB, usage)).dim() ); } pub fn println(prefix: impl fmt::Display, msg: impl fmt::Display) { println!("{prefix} {msg}"); } pub fn indented(msg: impl fmt::Display) { println!("{TAB}{msg}"); } pub fn subcommand(msg: impl fmt::Display) { println!("{} {}", style("$").dim(), style(msg).dim()); } pub fn warning(warning: &str) { println!( "{} {} {warning}", WARNING_PREFIX, Paint::yellow("Warning:").bold(), ); } pub fn error(error: impl fmt::Display) { println!("{ERROR_PREFIX} {error}"); } pub fn fail(header: &str, error: &anyhow::Error) { let err = error.to_string(); let err = err.trim_end(); let separator = if err.contains('\n') { ":\n" } else { ": " }; println!( "{ERROR_PREFIX} {}{}{error}", Paint::red(header).bold(), Paint::red(separator), ); if let Some(Error::WithHint { hint, .. }) = error.downcast_ref::() { println!("{} {}", ERROR_HINT_PREFIX, Paint::yellow(hint)); blank(); } } pub fn ask(prompt: D, default: bool) -> bool { let prompt = prompt.to_string(); Confirm::new(&prompt) .with_default(default) .with_render_config(*CONFIG) .prompt() .unwrap_or_default() } pub fn confirm(prompt: D) -> bool { ask(prompt, true) } pub fn abort(prompt: D) -> bool { ask(prompt, false) } /// Get the signer. First we try getting it from ssh-agent, otherwise we prompt the user. pub fn signer(profile: &Profile) -> anyhow::Result> { if let Ok(signer) = profile.signer() { return Ok(signer); } let passphrase = passphrase()?; let spinner = spinner("Unsealing key..."); let signer = MemorySigner::load(&profile.keystore, passphrase)?; spinner.finish(); Ok(signer.boxed()) } pub fn input(message: &str, default: Option) -> anyhow::Result where S: fmt::Display + std::str::FromStr + Clone, E: fmt::Debug + fmt::Display, { let input = CustomType::::new(message).with_render_config(*CONFIG); let value = match default { Some(default) => input.with_default(default).prompt()?, None => input.prompt()?, }; Ok(value) } pub fn passphrase() -> Result { if let Some(p) = profile::env::passphrase() { Ok(p) } else { Ok(Passphrase::from( Password::new("Passphrase:") .with_render_config(*CONFIG) .with_display_mode(inquire::PasswordDisplayMode::Masked) .without_confirmation() .prompt()?, )) } } pub fn passphrase_confirm(prompt: &str) -> Result { if let Some(p) = profile::env::passphrase() { Ok(p) } else { Ok(Passphrase::from( Password::new(prompt) .with_render_config(*CONFIG) .with_display_mode(inquire::PasswordDisplayMode::Masked) .with_custom_confirmation_message("Repeat passphrase:") .with_custom_confirmation_error_message("The passphrases don't match.") .with_help_message("This passphrase protects your radicle identity") .prompt()?, )) } } pub fn passphrase_stdin() -> Result { let mut input = String::new(); std::io::stdin().read_line(&mut input)?; Ok(Passphrase::from(input.trim_end().to_owned())) } pub fn select<'a, T>( prompt: &str, options: &'a [T], active: &'a T, ) -> Result, InquireError> where T: fmt::Display + Eq + PartialEq, { let active = options.iter().position(|o| o == active); let selection = Select::new(prompt, options.iter().collect::>()).with_render_config(*CONFIG); if let Some(active) = active { selection.with_starting_cursor(active).prompt_skippable() } else { selection.prompt_skippable() } } pub fn comment_select(issue: &Issue) -> Option<(&CommentId, &Comment)> { let comments = issue.comments().collect::>(); let selection = Select::new( "Which comment do you want to react to?", (0..comments.len()).collect(), ) .with_render_config(*CONFIG) .with_formatter(&|i| comments[i.index].1.body().to_owned()) .prompt() .ok()?; comments.get(selection).copied() } pub fn markdown(content: &str) { if !content.is_empty() && command::bat(["-p", "-l", "md"], content).is_err() { blob(content); } } pub mod proposal { use std::fmt::Write as _; use radicle::{ cob::identity::{self, Proposal}, git::Oid, identity::Identity, }; use super::*; use crate::terminal::format; pub fn revision_select( proposal: &Proposal, ) -> Option<(&identity::RevisionId, &identity::Revision)> { let revisions = proposal.revisions().collect::>(); let selection = Select::new( "Which revision do you want to select?", (0..revisions.len()).collect(), ) .with_vim_mode(true) .with_formatter(&|ix| revisions[ix.index].0.to_string()) .with_render_config(*CONFIG) .prompt() .ok()?; revisions.get(selection).copied() } pub fn revision_commit_select<'a>( proposal: &'a Proposal, previous: &'a Identity, ) -> Option<(&'a identity::RevisionId, &'a identity::Revision)> { let revisions = proposal .revisions() .filter(|(_, r)| r.is_quorum_reached(previous)) .collect::>(); let selection = Select::new( "Which revision do you want to commit?", (0..revisions.len()).collect(), ) .with_formatter(&|ix| revisions[ix.index].0.to_string()) .with_render_config(*CONFIG) .prompt() .ok()?; revisions.get(selection).copied() } pub fn diff(proposal: &identity::Revision, previous: &Identity) -> anyhow::Result { use similar::{ChangeTag, TextDiff}; let new = serde_json::to_string_pretty(&proposal.proposed)?; let previous = serde_json::to_string_pretty(&previous.doc)?; let diff = TextDiff::from_lines(&previous, &new); let mut buf = String::new(); for change in diff.iter_all_changes() { match change.tag() { ChangeTag::Delete => write!(buf, "{}", format::negative(format!("-{change}")))?, ChangeTag::Insert => write!(buf, "{}", format::positive(format!("+{change}")))?, ChangeTag::Equal => write!(buf, " {change}")?, }; } Ok(buf) } }