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