/ radicle-cli / src / terminal.rs
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  }