terminal.rs
1 pub mod args; 2 pub use args::{Args, Error, Help}; 3 pub mod format; 4 pub mod io; 5 pub use io::signer; 6 pub mod comment; 7 pub mod highlight; 8 pub mod issue; 9 pub mod patch; 10 11 use std::ffi::OsString; 12 use std::process; 13 14 pub use radicle_term::*; 15 16 use radicle::profile::Profile; 17 18 use crate::terminal; 19 20 /// Context passed to all commands. 21 pub trait Context { 22 /// Return the currently active profile, or an error if no profile is active. 23 fn profile(&self) -> Result<Profile, anyhow::Error>; 24 } 25 26 impl Context for Profile { 27 fn profile(&self) -> Result<Profile, anyhow::Error> { 28 Ok(self.clone()) 29 } 30 } 31 32 impl<F> Context for F 33 where 34 F: Fn() -> Result<Profile, anyhow::Error>, 35 { 36 fn profile(&self) -> Result<Profile, anyhow::Error> { 37 self() 38 } 39 } 40 41 /// A command that can be run. 42 pub trait Command<A: Args, C: Context> { 43 /// Run the command, given arguments and a context. 44 fn run(self, args: A, context: C) -> anyhow::Result<()>; 45 } 46 47 impl<F, A: Args, C: Context> Command<A, C> for F 48 where 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 56 pub fn run_command<A, C>(help: Help, cmd: C) -> ! 57 where 58 A: Args, 59 C: Command<A, fn() -> anyhow::Result<Profile>>, 60 { 61 let args = std::env::args_os().skip(1).collect(); 62 63 run_command_args(help, cmd, args) 64 } 65 66 pub fn run_command_args<A, C>(help: Help, cmd: C, args: Vec<OsString>) -> ! 67 where 68 A: Args, 69 C: Command<A, fn() -> anyhow::Result<Profile>>, 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 term::help(help.name, help.version, help.description, help.usage); 85 process::exit(0); 86 } 87 Some(Error::HelpManual) => { 88 let Ok(status) = term::manual(help.name) else { 89 perror(help.name, "failed to load manual page"); 90 process::exit(1); 91 }; 92 process::exit(status.code().unwrap_or(0)); 93 } 94 Some(Error::Usage) => { 95 term::usage(help.name, help.usage); 96 process::exit(1); 97 } 98 Some(Error::WithHint { hint, .. }) => Some(hint), 99 None => None, 100 }; 101 perror(help.name, &err); 102 103 if let Some(hint) = hint { 104 eprintln!("{}", Paint::yellow(hint)); 105 } 106 process::exit(1); 107 } 108 }; 109 110 match cmd.run(options, self::profile) { 111 Ok(()) => process::exit(0), 112 Err(err) => { 113 terminal::fail(help.name, &err); 114 process::exit(1); 115 } 116 } 117 } 118 119 /// Get the default profile. Fails if there is no profile. 120 pub fn profile() -> Result<Profile, anyhow::Error> { 121 match Profile::load() { 122 Ok(profile) => Ok(profile), 123 Err(e) => Err(args::Error::WithHint { 124 err: anyhow::anyhow!("Could not load radicle profile: {e}"), 125 hint: "To setup your radicle profile, run `rad auth`.", 126 } 127 .into()), 128 } 129 } 130 131 pub fn perror(name: &str, err: impl std::fmt::Display) { 132 eprintln!( 133 "{ERROR_PREFIX} {} rad {}: {err}", 134 Paint::red("Error:"), 135 name, 136 ); 137 } 138 139 pub fn fail(_name: &str, error: &anyhow::Error) { 140 let err = error.to_string(); 141 let err = err.trim_end(); 142 143 for line in err.lines() { 144 println!("{ERROR_PREFIX} {} {line}", Paint::red("Error:")); 145 } 146 147 if let Some(Error::WithHint { hint, .. }) = error.downcast_ref::<Error>() { 148 println!("{ERROR_HINT_PREFIX} {}", Paint::yellow(hint)); 149 } 150 }