1use colored::Colorize as _;
2use std::fmt::{Debug, Display};
3
4pub use interface::*;
5
6pub static VERSION_SHORT: &str = concat!("v", env!("CARGO_PKG_VERSION"));
7
8#[derive(Clone)]
9pub struct TextWrapper(pub textwrap::Options<'static>);
10
11impl Default for TextWrapper {
12 fn default() -> Self {
13 Self(
14 textwrap::Options::with_termwidth()
15 .word_splitter(textwrap::word_splitters::WordSplitter::NoHyphenation),
16 )
17 }
18}
19
20impl TextWrapper {
21 pub fn fill(&self, text: &str) -> String {
22 textwrap::fill(text, &self.0)
23 }
24}
25
26pub mod colors {
27 use colored::Color::{self, *};
28
29 pub const ERROR: Color = BrightRed;
30 pub const WARNING: Color = BrightYellow;
31 pub const ACTION_REQUEST: Color = BrightMagenta;
32 pub const VICTORY: Color = BrightGreen;
33}
34
35#[derive(Clone, Copy, Debug)]
36pub enum Label {
37 Error,
38 ActionRequest,
39 Victory,
40}
41
42impl Label {
43 pub fn color(&self) -> colored::Color {
44 match self {
45 Self::Error => colors::ERROR,
46 Self::ActionRequest => colors::ACTION_REQUEST,
47 Self::Victory => colors::VICTORY,
48 }
49 }
50
51 pub fn exit_code(&self) -> i8 {
52 match self {
53 Self::Victory => 0,
54 _ => 1,
55 }
56 }
57
58 pub fn as_str(&self) -> &'static str {
59 match self {
60 Self::Error => "error",
61 Self::ActionRequest => "action request",
62 Self::Victory => "victory",
63 }
64 }
65}
66
67#[derive(Debug)]
68pub struct Report {
69 label: Label,
70 msg: String,
71 details: String,
72}
73
74impl Report {
75 pub fn new(label: Label, msg: impl Display, details: impl Display) -> Self {
76 Self {
77 label,
78 msg: format!("{}", msg),
79 details: format!("{}", details),
80 }
81 }
82
83 pub fn error(msg: impl Display, details: impl Display) -> Self {
84 Self::new(Label::Error, msg, details)
85 }
86
87 pub fn action_request(msg: impl Display, details: impl Display) -> Self {
88 Self::new(Label::ActionRequest, msg, details)
89 }
90
91 pub fn victory(msg: impl Display, details: impl Display) -> Self {
92 Self::new(Label::Victory, msg, details)
93 }
94
95 pub fn exit_code(&self) -> i8 {
96 self.label.exit_code()
97 }
98
99 fn format(&self, wrapper: &TextWrapper) -> String {
100 static INDENT: &str = " ";
101 let head = if colored::control::SHOULD_COLORIZE.should_colorize() {
102 wrapper.fill(&format!(
103 "{} {}",
104 format!("{}:", self.label.as_str())
105 .color(self.label.color())
106 .bold(),
107 self.msg.color(self.label.color())
108 ))
109 } else {
110 wrapper.fill(&format!("{}: {}", self.label.as_str(), &self.msg))
111 };
112 let wrapper = TextWrapper(
113 wrapper
114 .clone()
115 .0
116 .initial_indent(INDENT)
117 .subsequent_indent(INDENT),
118 );
119 format!("{}\n{}\n", head, wrapper.fill(&self.details))
120 }
121
122 pub fn print(&self, wrapper: &TextWrapper) {
123 let s = self.format(wrapper);
124 if matches!(self.label, Label::Error) {
125 eprint!("{}", s)
126 } else {
127 print!("{}", s)
128 }
129 }
130}
131
132pub trait Reportable: Debug {
133 fn report(&self) -> Report;
134}
135
136#[cfg(not(feature = "cli"))]
137mod interface {}
138
139#[cfg(feature = "cli")]
140mod interface {
141 use std::fmt::Debug;
142
143 use crate::{opts, util};
144 use once_cell_regex::exports::once_cell::sync::Lazy;
145 use structopt::{
146 clap::{self, AppSettings},
147 StructOpt,
148 };
149
150 use super::*;
151
152 pub static GLOBAL_SETTINGS: &[AppSettings] = &[
153 AppSettings::ColoredHelp,
154 AppSettings::DeriveDisplayOrder,
155 AppSettings::VersionlessSubcommands,
156 ];
157
158 pub static SETTINGS: &[AppSettings] = &[AppSettings::SubcommandRequiredElseHelp];
159
160 pub fn bin_name(name: &str) -> String {
161 format!("cargo {}", name)
162 }
163
164 pub static VERSION_LONG: Lazy<String> = Lazy::new(|| match util::installed_commit_msg() {
165 Ok(Some(msg)) => format!("{}\n{}", VERSION_SHORT, util::format_commit_msg(msg)),
166 Ok(None) => VERSION_SHORT.to_owned(),
167 Err(err) => {
168 log::error!("failed to get current commit msg: {}", err);
169 VERSION_SHORT.to_owned()
170 }
171 });
172
173 #[derive(Clone, Copy, Debug, StructOpt)]
174 pub struct GlobalFlags {
175 #[structopt(
176 short = "v",
177 long = "verbose",
178 help = "Vomit out extensive logging (-vv for more)",
179 global = true,
180 multiple = true,
181 parse(from_occurrences = opts::NoiseLevel::from_occurrences),
182 )]
183 pub noise_level: opts::NoiseLevel,
184 #[structopt(
185 short = "y",
186 long = "non-interactive",
187 help = "Never prompt for input",
188 global = true
189 )]
190 pub non_interactive: bool,
191 }
192
193 #[derive(Clone, Copy, Debug, StructOpt)]
194 pub struct SkipDevTools {
195 #[structopt(
196 long = "skip-dev-tools",
197 help = "Skip optional tools that help when writing code"
198 )]
199 pub skip_dev_tools: bool,
200 }
201
202 #[derive(Clone, Copy, Debug, StructOpt)]
203 pub struct SkipTargetsInstall {
204 #[structopt(
205 long = "skip-targets-install",
206 help = "Skip installing android/ios targets for rust through rustup "
207 )]
208 pub skip_targets_install: bool,
209 }
210
211 #[derive(Clone, Copy, Debug, StructOpt)]
212 pub struct ReinstallDeps {
213 #[structopt(long = "reinstall-deps", help = "Reinstall dependencies")]
214 pub reinstall_deps: bool,
215 }
216
217 #[derive(Clone, Copy, Debug, StructOpt)]
218 pub struct Profile {
219 #[structopt(
220 long = "release",
221 help = "Build with release optimizations",
222 parse(from_flag = opts::Profile::from_flag),
223 )]
224 pub profile: opts::Profile,
225 }
226
227 #[derive(Clone, Copy, Debug, StructOpt)]
228 pub struct Filter {
229 #[structopt(
230 short = "f",
231 long = "filter",
232 help = "Filter logs by level",
233 possible_values = &opts::FilterLevel::variants(),
234 case_insensitive = true,
235 )]
236 pub filter: Option<opts::FilterLevel>,
237 }
238
239 pub trait Exec: Debug + StructOpt {
240 type Report: Reportable;
241
242 fn global_flags(&self) -> GlobalFlags;
243
244 fn exec(self, wrapper: &TextWrapper) -> Result<(), Self::Report>;
245 }
246
247 fn get_args(name: &str) -> Vec<String> {
248 let mut args: Vec<String> = std::env::args().collect();
249 if args.get(1).map(String::as_str) == Some(name) {
252 args.remove(1);
253 }
254 args
255 }
256
257 fn init_logging(noise_level: opts::NoiseLevel) {
258 use env_logger::{Builder, Env};
259 let default_level = match noise_level {
260 opts::NoiseLevel::Polite => "warn",
261 opts::NoiseLevel::LoudAndProud => {
262 "cargo_mobile=info,cargo_android=info,cargo_apple=info,hit=info"
263 }
264 opts::NoiseLevel::FranklyQuitePedantic => {
265 "info,cargo_mobile=debug,cargo_android=debug,cargo_apple=debug,hit=debug"
266 }
267 };
268 let env = Env::default().default_filter_or(default_level);
269 Builder::from_env(env).init();
270 }
271
272 #[derive(Debug)]
273 pub enum Exit {
274 Report(Report),
275 Clap(clap::Error),
276 }
277
278 impl Exit {
279 fn report(reportable: impl Reportable) -> Self {
280 log::info!("exiting with {:#?}", reportable);
281 Self::Report(reportable.report())
282 }
283
284 fn do_the_thing(self, wrapper: TextWrapper) -> ! {
285 match self {
286 Self::Report(report) => {
287 report.print(&wrapper);
288 std::process::exit(report.label.exit_code().into())
289 }
290 Self::Clap(err) => err.exit(),
291 }
292 }
293
294 pub fn main(inner: impl FnOnce(&TextWrapper) -> Result<(), Self>) {
295 let wrapper = TextWrapper::default();
296 if let Err(exit) = inner(&wrapper) {
297 exit.do_the_thing(wrapper)
298 }
299 }
300 }
301
302 pub fn exec<E: Exec>(name: &str) {
303 Exit::main(|wrapper| {
304 let args = get_args(name);
305 let input = E::from_iter_safe(&args).map_err(Exit::Clap)?;
306 init_logging(input.global_flags().noise_level);
307 log::debug!("raw args: {:#?}", args);
308 input.exec(wrapper).map_err(Exit::report)
309 })
310 }
311}