Skip to main content

radicle_cli/terminal/
io.rs

1use anyhow::anyhow;
2use radicle::cob::issue::Issue;
3use radicle::cob::thread::{Comment, CommentId};
4use radicle::cob::Reaction;
5use radicle::crypto::ssh::keystore::MemorySigner;
6use radicle::crypto::ssh::Keystore;
7use radicle::node::device::{BoxedDevice, Device};
8use radicle::profile::env::RAD_PASSPHRASE;
9use radicle::profile::Profile;
10
11pub use radicle_term::io::*;
12pub use radicle_term::spinner;
13
14use inquire::validator;
15
16/// Validates secret key passphrases.
17#[derive(Clone)]
18pub struct PassphraseValidator {
19    keystore: Keystore,
20}
21
22impl PassphraseValidator {
23    /// Create a new validator.
24    #[must_use]
25    pub fn new(keystore: Keystore) -> Self {
26        Self { keystore }
27    }
28}
29
30impl inquire::validator::StringValidator for PassphraseValidator {
31    fn validate(
32        &self,
33        input: &str,
34    ) -> Result<validator::Validation, inquire::error::CustomUserError> {
35        let passphrase = Passphrase::from(input.to_owned());
36        if self.keystore.is_valid_passphrase(&passphrase)? {
37            Ok(validator::Validation::Valid)
38        } else {
39            Ok(validator::Validation::Invalid(
40                validator::ErrorMessage::from("Invalid passphrase, please try again"),
41            ))
42        }
43    }
44}
45
46/// Get the signer. First we try getting it from ssh-agent, otherwise we prompt the user,
47/// if we're connected to a TTY.
48pub fn signer(profile: &Profile) -> anyhow::Result<BoxedDevice> {
49    match profile.signer() {
50        Ok(signer) => return Ok(signer),
51        Err(err) if !err.prompt_for_passphrase() => return Err(anyhow!(err)),
52        Err(_) => {
53            // The error returned is potentially recoverable by prompting
54            // the user for the correct passphrase.
55        }
56    }
57
58    let validator = PassphraseValidator::new(profile.keystore.clone());
59    let passphrase = match passphrase(validator)? {
60        Some(p) => p,
61        None => {
62            anyhow::bail!(
63                "A passphrase is required to read your Radicle key. Unable to continue. Consider setting the environment variable `{RAD_PASSPHRASE}`.",
64            )
65        }
66    };
67    let spinner = spinner("Unsealing key...");
68    let signer = MemorySigner::load(&profile.keystore, Some(passphrase))?;
69
70    spinner.finish();
71
72    Ok(Device::from(signer).boxed())
73}
74
75pub fn comment_select(issue: &Issue) -> anyhow::Result<(&CommentId, &Comment)> {
76    let comments = issue.comments().collect::<Vec<_>>();
77    let selection = Select::new(
78        "Which comment do you want to react to?",
79        (0..comments.len()).collect(),
80    )
81    .with_render_config(*CONFIG)
82    .with_formatter(&|i| comments.get(i.index).unwrap().1.body().to_owned())
83    .prompt()?;
84
85    comments
86        .get(selection)
87        .copied()
88        .ok_or(anyhow!("failed to perform comment selection"))
89}
90
91pub fn reaction_select() -> anyhow::Result<Reaction> {
92    let emoji = Select::new(
93        "With which emoji do you want to react?",
94        vec!['🐙', '👾', '💯', '✨', '🙇', '🙅', '❤'],
95    )
96    .with_render_config(*CONFIG)
97    .prompt()?;
98    Ok(Reaction::new(emoji)?)
99}