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