1use std::{io, sync::LazyLock};
2
3use crate::toolchain::{ToolchainClient, ToolchainError, ToolchainVersion};
4use clap::builder::styling;
5use humansize::DECIMAL;
6use indicatif::ProgressStyle;
7use miette::Diagnostic;
8use thiserror::Error;
9use tokio_util::{future::FutureExt, sync::CancellationToken};
10
11#[derive(Debug, Error, Diagnostic)]
12pub enum CliError {
13 #[error(transparent)]
14 #[diagnostic(code(arm_toolchain::cli::interactive_prompt_failed))]
15 Inquire(#[from] inquire::InquireError),
16
17 #[error(transparent)]
18 #[diagnostic(transparent)]
19 Toolchain(ToolchainError),
20
21 #[error("No ARM toolchain is enabled on this system")]
22 #[diagnostic(code(arm_toolchain::cli::no_toolchain_enabled))]
23 #[diagnostic(help("Install and activate a toolchain by running the `use latest` subcommand."))]
24 NoToolchainEnabled,
25
26 #[error("The toolchain {:?} is not installed.", version.name)]
27 #[diagnostic(code(arm_toolchain::cli::toolchain_missing))]
28 #[diagnostic(help("Install and activate it by running the `install {version}` subcommand."))]
29 ToolchainNotInstalled { version: ToolchainVersion },
30
31 #[error("No ARM toolchains are installed on this system")]
32 #[diagnostic(code(arm_toolchain::cli::no_toolchains_installed))]
33 #[diagnostic(help("There is nothing to remove."))]
34 NoToolchainsToRemove,
35
36 #[error("The toolchain {:?} is not installed.", version.name)]
37 #[diagnostic(code(arm_toolchain::cli::remove_missing))]
38 CannotRemoveMissingToolchain { version: ToolchainVersion },
39}
40
41impl From<ToolchainError> for CliError {
42 fn from(value: ToolchainError) -> Self {
43 match value {
44 ToolchainError::ToolchainNotInstalled { version } => {
46 Self::ToolchainNotInstalled { version }
47 }
48 other => Self::Toolchain(other),
49 }
50 }
51}
52
53impl From<io::Error> for CliError {
54 fn from(value: io::Error) -> Self {
55 ToolchainError::from(value).into()
56 }
57}
58
59#[derive(Debug, clap::Subcommand)]
63pub enum ArmToolchainCmd {
64 #[clap(
73 visible_alias("add"),
74 visible_alias("i"),
75 )]
76 Install(InstallArgs),
77 #[clap(
85 visible_alias("uninstall"),
86 visible_alias("rm"),
87 )]
88 Remove(RemoveArgs),
89 Run(RunArgs),
98 #[clap(
100 visible_alias("which"),
101 visible_alias("where"),
102 visible_alias("print"),
103 )]
104 Locate(LocateArgs),
105 #[clap(
107 visible_alias("set"),
108 visible_alias("activate"),
109 )]
110 Use(UseArgs),
111 #[clap(visible_alias("ls"))]
113 List,
114 PurgeCache,
116}
117
118impl ArmToolchainCmd {
119 pub async fn run(self) -> Result<(), CliError> {
121 match self {
122 ArmToolchainCmd::Install(config) => {
123 install(config).await?;
124 }
125 ArmToolchainCmd::Remove(args) => {
126 remove(args).await?;
127 }
128 ArmToolchainCmd::Run(args) => {
129 run(args).await?;
130 }
131 ArmToolchainCmd::Locate(args) => {
132 locate(args).await?;
133 }
134 ArmToolchainCmd::Use(args) => {
135 use_cmd(args).await?;
136 }
137 ArmToolchainCmd::List => {
138 list().await?;
139 }
140 ArmToolchainCmd::PurgeCache => {
141 purge_cache().await?;
142 }
143 }
144
145 Ok(())
146 }
147}
148
149mod install;
150pub use install::*;
151
152mod run;
153pub use run::*;
154
155mod use_cmd;
156pub use use_cmd::*;
157
158mod remove;
159pub use remove::*;
160
161#[derive(Debug, clap::Args)]
163pub struct LocateArgs {
164 #[arg(short = 'T', long)]
166 toolchain: Option<ToolchainVersion>,
167 #[clap(default_value = "install-dir")]
169 what: LocateWhat,
170}
171
172#[derive(Debug, Clone, Default, PartialEq, clap::ValueEnum)]
173enum LocateWhat {
174 #[default]
176 InstallDir,
177 Bin,
179 Lib,
181 Multilib,
184}
185
186pub async fn locate(args: LocateArgs) -> Result<(), CliError> {
188 let client = ToolchainClient::using_data_dir().await?;
189 let version = args
190 .toolchain
191 .or_else(|| client.active_toolchain())
192 .ok_or(CliError::NoToolchainEnabled)?;
193
194 let toolchain = client.toolchain(&version).await?;
195
196 match args.what {
197 LocateWhat::InstallDir => {
198 println!("{}", toolchain.path.display());
199 }
200 LocateWhat::Bin => {
201 println!("{}", toolchain.host_bin_dir().display());
202 }
203 LocateWhat::Lib => {
204 println!("{}", toolchain.lib_dir().display());
205 }
206 LocateWhat::Multilib => {
207 println!("{}", toolchain.multilib_dir().display());
208 }
209 }
210
211 Ok(())
212}
213
214pub async fn list() -> Result<(), CliError> {
216 let client = ToolchainClient::using_data_dir().await?;
217
218 let active = client.active_toolchain();
219 let installed = client.installed_versions().await?;
220
221 println!(
222 "Active: {}",
223 active
224 .map(|v| v.to_string())
225 .unwrap_or_else(|| "None".to_string())
226 );
227
228 println!();
229 println!("Installed:");
230
231 if installed.is_empty() {
232 println!("- (None)");
233 }
234
235 for version in installed {
236 println!("- {version}");
237 }
238
239 Ok(())
240}
241
242pub async fn purge_cache() -> Result<(), CliError> {
244 let client = ToolchainClient::using_data_dir().await?;
245 let bytes = client.purge_cache().await?;
246
247 println!(
248 "ARM Toolchain download cache purged ({} deleted)",
249 humansize::format_size(bytes, DECIMAL)
250 );
251
252 Ok(())
253}
254
255macro_rules! msg {
256 ($label:expr, $($rest:tt)+) => {
257 {
258 use owo_colors::OwoColorize;
259 eprintln!("{:>12} {}", $label.green().bold(), format_args!($($rest)+))
260 }
261 };
262}
263pub(crate) use msg;
264
265pub fn ctrl_c_cancel() -> CancellationToken {
271 let cancel_token = CancellationToken::new();
272
273 tokio::spawn({
274 let cancel_token = cancel_token.clone();
275 async move {
276 if let Some(wait_result) = tokio::signal::ctrl_c()
277 .with_cancellation_token(&cancel_token)
278 .await
279 {
280 wait_result.unwrap();
285 cancel_token.cancel();
286 eprintln!("Cancelled.");
287 }
288
289 tokio::signal::ctrl_c().await.unwrap();
290 std::process::exit(1);
291 }
292 });
293
294 cancel_token
295}
296
297const PROGRESS_CHARS: &str = "=> ";
298
299pub static PROGRESS_STYLE_DL: LazyLock<ProgressStyle> = LazyLock::new(|| {
300 ProgressStyle::with_template("{percent:>3.bold}% [{bar:40.blue}] ({bytes}/{total_bytes}, {eta} remaining) {bytes_per_sec}")
301 .expect("progress style valid")
302 .progress_chars(PROGRESS_CHARS)
303});
304
305pub static PROGRESS_STYLE_DL_MSG: LazyLock<ProgressStyle> = LazyLock::new(|| {
306 ProgressStyle::with_template("{percent:>3.bold}% [{bar:40.blue}] ({bytes}/{total_bytes}) {msg}")
307 .expect("progress style valid")
308 .progress_chars(PROGRESS_CHARS)
309});
310
311pub static PROGRESS_STYLE_VERIFY: LazyLock<ProgressStyle> = LazyLock::new(|| {
312 ProgressStyle::with_template("{percent:>3.bold}% [{bar:40.green}] {msg} ({eta} remaining)")
313 .expect("progress style valid")
314 .progress_chars(PROGRESS_CHARS)
315});
316
317pub static PROGRESS_STYLE_EXTRACT_SPINNER: LazyLock<ProgressStyle> = LazyLock::new(|| {
318 ProgressStyle::with_template("{spinner:.green} {msg}")
319 .expect("progress style valid")
320 .tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏✓")
321});
322
323pub static PROGRESS_STYLE_EXTRACT: LazyLock<ProgressStyle> = LazyLock::new(|| {
324 ProgressStyle::with_template("{percent:>3.bold}% [{bar:40.dim}] {msg} ({eta} remaining)")
325 .expect("progress style valid")
326 .progress_chars(PROGRESS_CHARS)
327});
328
329pub static PROGRESS_STYLE_DELETE_SPINNER: LazyLock<ProgressStyle> = LazyLock::new(|| {
330 ProgressStyle::with_template("{spinner:.red} {msg}")
331 .expect("progress style valid")
332 .tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏✓")
333});
334
335pub static PROGRESS_STYLE_DELETE: LazyLock<ProgressStyle> = LazyLock::new(|| {
336 ProgressStyle::with_template("{percent:>3.bold}% [{bar:40.red}] {msg} ({eta} remaining)")
337 .expect("progress style valid")
338 .progress_chars(PROGRESS_CHARS)
339});
340
341pub const STYLES: styling::Styles = styling::Styles::styled()
342 .header(styling::AnsiColor::Green.on_default().bold())
343 .usage(styling::AnsiColor::Green.on_default().bold())
344 .literal(styling::AnsiColor::Blue.on_default().bold())
345 .placeholder(styling::AnsiColor::Cyan.on_default());