radicle_cli/terminal/
io.rs1use 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#[derive(Clone)]
18pub struct PassphraseValidator {
19 keystore: Keystore,
20}
21
22impl PassphraseValidator {
23 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<BoxedDevice> {
48 match profile.signer() {
49 Ok(signer) => return Ok(signer),
50 Err(err) if !err.prompt_for_passphrase() => return Err(anyhow!(err)),
51 Err(_) => {
52 }
55 }
56
57 let validator = PassphraseValidator::new(profile.keystore.clone());
58 let passphrase = match passphrase(validator)? {
59 Some(p) => p,
60 None => {
61 anyhow::bail!(
62 "A passphrase is required to read your Radicle key. Unable to continue. Consider setting the environment variable `{RAD_PASSPHRASE}`.",
63 )
64 }
65 };
66 let spinner = spinner("Unsealing key...");
67 let signer = MemorySigner::load(&profile.keystore, Some(passphrase))?;
68
69 spinner.finish();
70
71 Ok(Device::from(signer).boxed())
72}
73
74pub fn comment_select(issue: &Issue) -> anyhow::Result<(&CommentId, &Comment)> {
75 let comments = issue.comments().collect::<Vec<_>>();
76 let selection = Select::new(
77 "Which comment do you want to react to?",
78 (0..comments.len()).collect(),
79 )
80 .with_render_config(*CONFIG)
81 .with_formatter(&|i| comments[i.index].1.body().to_owned())
82 .prompt()?;
83
84 comments
85 .get(selection)
86 .copied()
87 .ok_or(anyhow!("failed to perform comment selection"))
88}
89
90pub fn reaction_select() -> anyhow::Result<Reaction> {
91 let emoji = Select::new(
92 "With which emoji do you want to react?",
93 vec!['🐙', '👾', '💯', '✨', '🙇', '🙅', '❤'],
94 )
95 .with_render_config(*CONFIG)
96 .prompt()?;
97 Ok(Reaction::new(emoji)?)
98}