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 if let Ok(signer) = profile.signer() {
49 return Ok(signer);
50 }
51 let validator = PassphraseValidator::new(profile.keystore.clone());
52 let passphrase = match passphrase(validator) {
53 Ok(p) => p,
54 Err(inquire::InquireError::NotTTY) => {
55 return Err(anyhow::anyhow!(
56 "running in non-interactive mode, please set `{RAD_PASSPHRASE}` to unseal your key",
57 ));
58 }
59 Err(e) => return Err(e.into()),
60 };
61 let spinner = spinner("Unsealing key...");
62 let signer = MemorySigner::load(&profile.keystore, Some(passphrase))?;
63
64 spinner.finish();
65
66 Ok(Device::from(signer).boxed())
67}
68
69pub fn comment_select(issue: &Issue) -> anyhow::Result<(&CommentId, &Comment)> {
70 let comments = issue.comments().collect::<Vec<_>>();
71 let selection = Select::new(
72 "Which comment do you want to react to?",
73 (0..comments.len()).collect(),
74 )
75 .with_render_config(*CONFIG)
76 .with_formatter(&|i| comments[i.index].1.body().to_owned())
77 .prompt()?;
78
79 comments
80 .get(selection)
81 .copied()
82 .ok_or(anyhow!("failed to perform comment selection"))
83}
84
85pub fn reaction_select() -> anyhow::Result<Reaction> {
86 let emoji = Select::new(
87 "With which emoji do you want to react?",
88 vec!['🐙', '👾', '💯', '✨', '🙇', '🙅', '❤'],
89 )
90 .with_render_config(*CONFIG)
91 .prompt()?;
92 Ok(Reaction::new(emoji)?)
93}