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 #[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
46pub 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 }
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}