radicle_cli/
terminal.rs

1pub mod args;
2pub use args::{Args, Error, Help};
3pub mod format;
4pub mod io;
5pub use io::signer;
6pub mod cob;
7pub mod comment;
8pub mod highlight;
9pub mod issue;
10pub mod json;
11pub mod patch;
12pub mod upload_pack;
13
14use std::ffi::OsString;
15use std::process;
16
17pub use radicle_term::*;
18
19use radicle::profile::{Home, Profile};
20
21use crate::terminal;
22
23/// Context passed to all commands.
24pub trait Context {
25    /// Return the currently active profile, or an error if no profile is active.
26    fn profile(&self) -> Result<Profile, anyhow::Error>;
27    /// Return the Radicle home.
28    fn home(&self) -> Result<Home, std::io::Error>;
29}
30
31impl Context for Profile {
32    fn profile(&self) -> Result<Profile, anyhow::Error> {
33        Ok(self.clone())
34    }
35
36    fn home(&self) -> Result<Home, std::io::Error> {
37        Ok(self.home.clone())
38    }
39}
40
41/// A command that can be run.
42pub trait Command<A: Args, C: Context> {
43    /// Run the command, given arguments and a context.
44    fn run(self, args: A, context: C) -> anyhow::Result<()>;
45}
46
47impl<F, A: Args, C: Context> Command<A, C> for F
48where
49    F: FnOnce(A, C) -> anyhow::Result<()>,
50{
51    fn run(self, args: A, context: C) -> anyhow::Result<()> {
52        self(args, context)
53    }
54}
55
56pub fn run_command<A, C>(help: Help, cmd: C) -> !
57where
58    A: Args,
59    C: Command<A, DefaultContext>,
60{
61    let args = std::env::args_os().skip(1).collect();
62
63    run_command_args(help, cmd, args)
64}
65
66pub fn run_command_args<A, C>(help: Help, cmd: C, args: Vec<OsString>) -> !
67where
68    A: Args,
69    C: Command<A, DefaultContext>,
70{
71    use io as term;
72
73    let options = match A::from_args(args) {
74        Ok((opts, unparsed)) => {
75            if let Err(err) = args::finish(unparsed) {
76                term::error(err);
77                process::exit(1);
78            }
79            opts
80        }
81        Err(err) => {
82            let hint = match err.downcast_ref::<Error>() {
83                Some(Error::Help) => {
84                    help.print();
85                    process::exit(0);
86                }
87                // Print the manual, or the regular help if there's an error.
88                Some(Error::HelpManual { name }) => {
89                    let Ok(status) = term::manual(name) else {
90                        help.print();
91                        process::exit(0);
92                    };
93                    if !status.success() {
94                        help.print();
95                        process::exit(0);
96                    }
97                    process::exit(status.code().unwrap_or(0));
98                }
99                Some(Error::Usage) => {
100                    term::usage(help.name, help.usage);
101                    process::exit(1);
102                }
103                Some(Error::WithHint { hint, .. }) => Some(hint),
104                None => None,
105            };
106            io::error(format!("rad {}: {err}", help.name));
107
108            if let Some(hint) = hint {
109                io::hint(hint);
110            }
111            process::exit(1);
112        }
113    };
114
115    match cmd.run(options, DefaultContext) {
116        Ok(()) => process::exit(0),
117        Err(err) => {
118            terminal::fail(help.name, &err);
119            process::exit(1);
120        }
121    }
122}
123
124/// Gets the default profile. Fails if there is no profile.
125pub struct DefaultContext;
126
127impl Context for DefaultContext {
128    fn home(&self) -> Result<Home, std::io::Error> {
129        radicle::profile::home()
130    }
131
132    fn profile(&self) -> Result<Profile, anyhow::Error> {
133        match Profile::load() {
134            Ok(profile) => Ok(profile),
135            Err(radicle::profile::Error::NotFound(path)) => Err(args::Error::WithHint {
136                err: anyhow::anyhow!("Radicle profile not found in '{}'.", path.display()),
137                hint: "To setup your radicle profile, run `rad auth`.",
138            }
139            .into()),
140            Err(radicle::profile::Error::Config(e)) => Err(e.into()),
141            Err(e) => Err(anyhow::anyhow!("Could not load radicle profile: {e}")),
142        }
143    }
144}
145
146pub fn fail(_name: &str, error: &anyhow::Error) {
147    let err = error.to_string();
148    let err = err.trim_end();
149
150    for line in err.lines() {
151        io::error(line);
152    }
153
154    // Catch common node errors, and offer a hint.
155    if let Some(e) = error.downcast_ref::<radicle::node::Error>() {
156        if e.is_connection_err() {
157            io::hint("to start your node, run `rad node start`.");
158        }
159    }
160    if let Some(Error::WithHint { hint, .. }) = error.downcast_ref::<Error>() {
161        io::hint(hint);
162    }
163}