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
25pub trait Context {
27 fn profile(&self) -> Result<Profile, anyhow::Error>;
29 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
43pub trait Command<A: Args, C: Context> {
45 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
58pub 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 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 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
142pub 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 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}