radicle_cli/commands/
config.rs1#![allow(clippy::or_fun_call)]
2use std::ffi::OsString;
3use std::path::Path;
4use std::str::FromStr;
5
6use anyhow::anyhow;
7use radicle::node::Alias;
8use radicle::profile::{Config, ConfigError, ConfigPath, RawConfig};
9
10use crate::terminal as term;
11use crate::terminal::args::{Args, Error, Help};
12use crate::terminal::Element as _;
13
14pub const HELP: Help = Help {
15 name: "config",
16 description: "Manage your local Radicle configuration",
17 version: env!("RADICLE_VERSION"),
18 usage: r#"
19Usage
20
21 rad config [<option>...]
22 rad config show [<option>...]
23 rad config init --alias <alias> [<option>...]
24 rad config edit [<option>...]
25 rad config get <key> [<option>...]
26 rad config schema [<option>...]
27 rad config set <key> <value> [<option>...]
28 rad config unset <key> [<option>...]
29 rad config push <key> <value> [<option>...]
30 rad config remove <key> <value> [<option>...]
31
32 If no argument is specified, prints the current radicle configuration as JSON.
33 To initialize a new configuration file, use `rad config init`.
34
35Options
36
37 --help Print help
38
39"#,
40};
41
42#[derive(Default)]
43enum Operation {
44 #[default]
45 Show,
46 Get(String),
47 Schema,
48 Set(String, String),
49 Push(String, String),
50 Remove(String, String),
51 Unset(String),
52 Init,
53 Edit,
54}
55
56pub struct Options {
57 op: Operation,
58 alias: Option<Alias>,
59}
60
61impl Args for Options {
62 fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
63 use lexopt::prelude::*;
64
65 let mut parser = lexopt::Parser::from_args(args);
66 let mut op: Option<Operation> = None;
67 let mut alias = None;
68
69 #[allow(clippy::never_loop)]
70 while let Some(arg) = parser.next()? {
71 match arg {
72 Long("help") | Short('h') => {
73 return Err(Error::Help.into());
74 }
75 Long("alias") => {
76 let value = parser.value()?;
77 let input = value.to_string_lossy();
78 let input = Alias::from_str(&input)?;
79
80 alias = Some(input);
81 }
82 Value(val) if op.is_none() => match val.to_string_lossy().as_ref() {
83 "show" => op = Some(Operation::Show),
84 "schema" => op = Some(Operation::Schema),
85 "edit" => op = Some(Operation::Edit),
86 "init" => op = Some(Operation::Init),
87 "get" => {
88 let key = parser.value()?;
89 let key = key.to_string_lossy();
90 op = Some(Operation::Get(key.to_string()));
91 }
92 "set" => {
93 let key = parser.value()?;
94 let key = key.to_string_lossy();
95 let value = parser.value()?;
96 let value = value.to_string_lossy();
97
98 op = Some(Operation::Set(key.to_string(), value.to_string()));
99 }
100 "push" => {
101 let key = parser.value()?;
102 let key = key.to_string_lossy();
103 let value = parser.value()?;
104 let value = value.to_string_lossy();
105
106 op = Some(Operation::Push(key.to_string(), value.to_string()));
107 }
108 "remove" => {
109 let key = parser.value()?;
110 let key = key.to_string_lossy();
111 let value = parser.value()?;
112 let value = value.to_string_lossy();
113
114 op = Some(Operation::Remove(key.to_string(), value.to_string()));
115 }
116 "unset" => {
117 let key = parser.value()?;
118 let key = key.to_string_lossy();
119 op = Some(Operation::Unset(key.to_string()));
120 }
121 unknown => anyhow::bail!("unknown operation '{unknown}'"),
122 },
123 _ => return Err(anyhow!(arg.unexpected())),
124 }
125 }
126
127 Ok((
128 Options {
129 op: op.unwrap_or_default(),
130 alias,
131 },
132 vec![],
133 ))
134 }
135}
136
137pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
138 let home = ctx.home()?;
139 let path = home.config();
140
141 match options.op {
142 Operation::Show => {
143 let profile = ctx.profile()?;
144 term::json::to_pretty(&profile.config, path.as_path())?.print();
145 }
146 Operation::Schema => {
147 term::json::to_pretty(&schemars::schema_for!(Config), path.as_path())?.print();
148 }
149 Operation::Get(key) => {
150 let mut temp_config = RawConfig::from_file(&path)?;
151 let key: ConfigPath = key.into();
152 let value = temp_config
153 .get_mut(&key)
154 .ok_or_else(|| ConfigError::Custom(format!("{key} does not exist")))?;
155 print_value(value)?;
156 }
157 Operation::Set(key, value) => {
158 let mut temp_config = RawConfig::from_file(&path)?;
159 let value = temp_config.set(&key.into(), value.into())?;
160 temp_config.write(&path)?;
161 print_value(&value)?;
162 }
163 Operation::Push(key, value) => {
164 let mut temp_config = RawConfig::from_file(&path)?;
165 let value = temp_config.push(&key.into(), value.into())?;
166 temp_config.write(&path)?;
167 print_value(&value)?;
168 }
169 Operation::Remove(key, value) => {
170 let mut temp_config = RawConfig::from_file(&path)?;
171 let value = temp_config.remove(&key.into(), value.into())?;
172 temp_config.write(&path)?;
173 print_value(&value)?;
174 }
175 Operation::Unset(key) => {
176 let mut temp_config = RawConfig::from_file(&path)?;
177 let value = temp_config.unset(&key.into())?;
178 temp_config.write(&path)?;
179 print_value(&value)?;
180 }
181 Operation::Init => {
182 if path.try_exists()? {
183 anyhow::bail!("configuration file already exists at `{}`", path.display());
184 }
185 Config::init(
186 options.alias.ok_or(anyhow!(
187 "an alias must be provided to initialize a new configuration"
188 ))?,
189 &path,
190 )?;
191 term::success!(
192 "Initialized new Radicle configuration at {}",
193 path.display()
194 );
195 }
196 Operation::Edit => match term::editor::Editor::new(&path)?.extension("json").edit()? {
197 Some(_) => {
198 term::success!("Successfully made changes to the configuration at {path:?}")
199 }
200 None => term::info!("No changes were made to the configuration at {path:?}"),
201 },
202 }
203
204 Ok(())
205}
206
207fn print_value(value: &serde_json::Value) -> anyhow::Result<()> {
209 match value {
210 serde_json::Value::Null => {}
211 serde_json::Value::Bool(b) => term::print(b),
212 serde_json::Value::Array(a) => a.iter().try_for_each(print_value)?,
213 serde_json::Value::Number(n) => term::print(n),
214 serde_json::Value::String(s) => term::print(s),
215 serde_json::Value::Object(o) => {
216 term::json::to_pretty(&o, Path::new("config.json"))?.print()
217 }
218 }
219 Ok(())
220}