radicle_cli/terminal/
io.rs1use anyhow::anyhow;
2use radicle::cob::Reaction;
3use radicle::cob::issue::Issue;
4use radicle::cob::thread::{Comment, CommentId};
5use radicle::crypto::SigningKey;
6use radicle::crypto::ssh::Keystore;
7use radicle::profile::env::RAD_PASSPHRASE;
8use radicle::profile::{Profile, Signer, SignerError};
9
10pub use radicle_term::io::*;
11pub use radicle_term::spinner;
12
13use inquire::validator;
14
15#[derive(Clone)]
17pub struct PassphraseValidator {
18 keystore: Keystore,
19}
20
21impl PassphraseValidator {
22 #[must_use]
24 pub fn new(keystore: Keystore) -> Self {
25 Self { keystore }
26 }
27}
28
29impl inquire::validator::StringValidator for PassphraseValidator {
30 fn validate(
31 &self,
32 input: &str,
33 ) -> Result<validator::Validation, inquire::error::CustomUserError> {
34 let passphrase = Passphrase::from(input.to_owned());
35 if self.keystore.is_valid_passphrase(&passphrase)? {
36 Ok(validator::Validation::Valid)
37 } else {
38 Ok(validator::Validation::Invalid(
39 validator::ErrorMessage::from("Invalid passphrase, please try again"),
40 ))
41 }
42 }
43}
44
45pub fn signer(profile: &Profile) -> anyhow::Result<Signer> {
48 let err = match profile.signer() {
49 Ok(signer) => return Ok(signer),
50 Err(err) => err,
51 };
52
53 match err {
54 SignerError::LoadError(radicle::crypto::LoadError::InvalidPassphrase) => {
55 super::warning(format!(
56 "The passphrase for your Radicle key provided in the environment variable `{RAD_PASSPHRASE}` is invalid. Please try again."
57 ));
58 }
59 SignerError::AgentConnection(err) => {
60 super::warning(format!(
61 "Failed to connect to ssh-agent: {err}. Falling back to passphrase prompt."
62 ));
63 }
64 SignerError::Agent(radicle::crypto::ssh::agent::IntoSignerError::IdentityNotFound {
65 identity,
66 }) => {
67 super::warning(format!(
68 "The Radicle key for `{identity}` is not registered with ssh-agent. Please run `rad auth` to register it."
69 ));
70 }
71 err @ SignerError::LoadError(_)
72 | err @ SignerError::InvalidPublicKey(_)
73 | err @ SignerError::Agent(_)
74 | err @ SignerError::Keystore(_) => return Err(anyhow!(err)),
75 }
76
77 let validator = PassphraseValidator::new(profile.keystore.clone());
78 let passphrase = match passphrase(validator)? {
79 Some(p) => p,
80 None => {
81 anyhow::bail!(
82 "A passphrase is required to read your Radicle key. Unable to continue. Consider setting the environment variable `{RAD_PASSPHRASE}`.",
83 )
84 }
85 };
86 let spinner = spinner("Unsealing key…");
87 let signer = SigningKey::load(&profile.keystore, Some(passphrase))?;
88
89 spinner.finish();
90
91 Ok(Signer::Key(signer))
92}
93
94pub fn comment_select(issue: &Issue) -> anyhow::Result<(&CommentId, &Comment)> {
95 let comments = issue.comments().collect::<Vec<_>>();
96 let selection = Select::new(
97 "Which comment do you want to react to?",
98 (0..comments.len()).collect(),
99 )
100 .with_render_config(*CONFIG)
101 .with_formatter(&|i| comments.get(i.index).unwrap().1.body().to_owned())
102 .prompt()?;
103
104 comments
105 .get(selection)
106 .copied()
107 .ok_or(anyhow!("failed to perform comment selection"))
108}
109
110pub fn reaction_select() -> anyhow::Result<Reaction> {
111 let emoji = Select::new(
112 "With which emoji do you want to react?",
113 vec!['🐙', '👾', '💯', '✨', '🙇', '🙅', '❤'],
114 )
115 .with_render_config(*CONFIG)
116 .prompt()?;
117 Ok(Reaction::new(emoji)?)
118}