1mod error;
14
15pub use error::VersionError;
16
17use clap::{Arg, ArgAction, ArgMatches, Command, FromArgMatches};
18use colored::Colorize;
19use std::process;
20use std::fmt;
21
22#[derive(Clone, Debug)]
24pub struct ColorfulVersion {
25 package_name: &'static str,
26 version: &'static str,
27 author: &'static str,
28 colors: Colors,
29}
30
31#[derive(Clone, Debug)]
32struct Colors {
33 name_fg: (u8, u8, u8), name_bg: (u8, u8, u8), version_color: (u8, u8, u8), author_color: (u8, u8, u8), }
38
39impl Default for Colors {
40 fn default() -> Self {
41 Self {
42 name_fg: (255, 255, 255), name_bg: (170, 0, 255), version_color: (255, 255, 0), author_color: (0, 255, 255), }
51 }
52}
53
54impl ColorfulVersion {
55 #[must_use]
57 pub fn from_cargo() -> Self {
58 Self {
59 package_name: env!("CARGO_PKG_NAME"),
60 version: env!("CARGO_PKG_VERSION"),
61 author: env!("CARGO_PKG_AUTHORS"),
62 colors: Colors::default(),
63 }
64 }
65
66 #[must_use]
68 pub fn new(package_name: &'static str, version: &'static str, author: &'static str) -> Self {
69 Self {
70 package_name,
71 version,
72 author,
73 colors: Colors::default(),
74 }
75 }
76
77 pub fn with_hex_colors(
88 mut self,
89 name_fg: &str,
90 name_bg: &str,
91 version: &str,
92 author: &str,
93 ) -> Result<Self, VersionError> {
94 self.colors.name_fg = parse_hex(name_fg)?;
95 self.colors.name_bg = parse_hex(name_bg)?;
96 self.colors.version_color = parse_hex(version)?;
97 self.colors.author_color = parse_hex(author)?;
98 Ok(self)
99 }
100
101 #[must_use]
103 pub fn with_rgb_colors(
104 mut self,
105 name_fg: (u8, u8, u8),
106 name_bg: (u8, u8, u8),
107 version: (u8, u8, u8),
108 author: (u8, u8, u8),
109 ) -> Self {
110 self.colors.name_fg = name_fg;
111 self.colors.name_bg = name_bg;
112 self.colors.version_color = version;
113 self.colors.author_color = author;
114 self
115 }
116
117 pub fn print_and_exit(&self) -> ! {
119 self.print();
120 process::exit(0);
121 }
122
123 pub fn print(&self) {
126 let name = self
127 .package_name
128 .truecolor(
129 self.colors.name_fg.0,
130 self.colors.name_fg.1,
131 self.colors.name_fg.2,
132 )
133 .on_truecolor(
134 self.colors.name_bg.0,
135 self.colors.name_bg.1,
136 self.colors.name_bg.2,
137 );
138
139 let version_text = format!(" v{}", self.version).truecolor(
140 self.colors.version_color.0,
141 self.colors.version_color.1,
142 self.colors.version_color.2,
143 );
144
145 let author_text = format!(" by {}", self.author).truecolor(
146 self.colors.author_color.0,
147 self.colors.author_color.1,
148 self.colors.author_color.2,
149 );
150
151 println!("{name}{version_text}{author_text}");
152 }
153
154 #[must_use]
157 pub fn as_plain_string(&self) -> String {
158 format!("{} v{} by {}", self.package_name, self.version, self.author)
159 }
160
161 #[must_use]
164 pub fn to_colored_string(&self) -> String {
165 format!(
166 "{}{}{}",
167 self.package_name
168 .truecolor(
169 self.colors.name_fg.0,
170 self.colors.name_fg.1,
171 self.colors.name_fg.2
172 )
173 .on_truecolor(
174 self.colors.name_bg.0,
175 self.colors.name_bg.1,
176 self.colors.name_bg.2
177 ),
178 format!(" v{}", self.version).truecolor(
179 self.colors.version_color.0,
180 self.colors.version_color.1,
181 self.colors.version_color.2
182 ),
183 format!(" by {}", self.author).truecolor(
184 self.colors.author_color.0,
185 self.colors.author_color.1,
186 self.colors.author_color.2
187 )
188 )
189 }
190
191 pub fn check_and_exit(&self, matches: &ArgMatches) {
196 if matches.get_flag("clap_version_flag_version") {
197 self.print_and_exit();
198 }
199 }
200
201 #[must_use]
203 pub fn package_name(&self) -> &str {
204 self.package_name
205 }
206
207 #[must_use]
209 pub fn version(&self) -> &str {
210 self.version
211 }
212
213 #[must_use]
215 pub fn author(&self) -> &str {
216 self.author
217 }
218}
219
220impl fmt::Display for ColorfulVersion {
222 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223 write!(
224 f,
225 "{} v{} by {}",
226 self.package_name, self.version, self.author
227 )
228 }
229}
230
231fn parse_hex(hex: &str) -> Result<(u8, u8, u8), VersionError> {
239 let hex = hex.trim_start_matches('#');
240
241 match hex.len() {
242 6 => {
243 let r =
244 u8::from_str_radix(&hex[0..2], 16).map_err(|_| VersionError::invalid_hex(hex))?;
245 let g =
246 u8::from_str_radix(&hex[2..4], 16).map_err(|_| VersionError::invalid_hex(hex))?;
247 let b =
248 u8::from_str_radix(&hex[4..6], 16).map_err(|_| VersionError::invalid_hex(hex))?;
249 Ok((r, g, b))
250 }
251 3 => {
252 let r = u8::from_str_radix(&hex[0..1].repeat(2), 16)
254 .map_err(|_| VersionError::invalid_hex(hex))?;
255 let g = u8::from_str_radix(&hex[1..2].repeat(2), 16)
256 .map_err(|_| VersionError::invalid_hex(hex))?;
257 let b = u8::from_str_radix(&hex[2..3].repeat(2), 16)
258 .map_err(|_| VersionError::invalid_hex(hex))?;
259 Ok((r, g, b))
260 }
261 _ => Err(VersionError::invalid_hex(hex)),
262 }
263}
264
265#[macro_export]
278macro_rules! colorful_version {
279 () => {
280 $crate::ColorfulVersion::from_cargo()
281 };
282
283 ($name_fg:expr, $name_bg:expr, $version:expr, $author:expr) => {
284 $crate::ColorfulVersion::from_cargo()
285 .with_hex_colors($name_fg, $name_bg, $version, $author)
286 .unwrap_or_else(|e| panic!("clap-version-flag: Invalid hex color format: {}", e))
287 };
288}
289
290pub trait ColorfulVersionExt {
292 fn with_colorful_version(self, version: &ColorfulVersion) -> Self;
294}
295
296impl ColorfulVersionExt for Command {
297 fn with_colorful_version(self, _version: &ColorfulVersion) -> Self {
298 self.arg(
300 Arg::new("clap_version_flag_version")
301 .short('V')
302 .long("version")
303 .action(ArgAction::SetTrue)
304 .help("Print version information")
305 .global(true),
306 )
307 }
308}
309
310pub fn parse_with_version<T: FromArgMatches>(
334 command: Command,
335 version: &ColorfulVersion,
336) -> Result<T, clap::Error> {
337 let command = command.with_colorful_version(version);
338 let matches = command.get_matches();
339
340 if matches.get_flag("clap_version_flag_version") {
342 version.print();
343 process::exit(0);
344 }
345
346 T::from_arg_matches(&matches)
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352
353 #[test]
354 fn test_hex_parsing() {
355 assert_eq!(parse_hex("#FFFFFF").unwrap(), (255, 255, 255));
356 assert_eq!(parse_hex("#000000").unwrap(), (0, 0, 0));
357 assert_eq!(parse_hex("#FF0000").unwrap(), (255, 0, 0));
358 assert_eq!(parse_hex("#FFF").unwrap(), (255, 255, 255));
359 assert_eq!(parse_hex("#F00").unwrap(), (255, 0, 0));
360 assert_eq!(parse_hex("FFFFFF").unwrap(), (255, 255, 255));
361
362 assert!(parse_hex("INVALID").is_err());
363 assert!(parse_hex("#GGG").is_err());
364 assert!(parse_hex("#12345").is_err());
365 }
366
367 #[test]
368 fn test_default_colors() {
369 let version = ColorfulVersion::from_cargo();
370 assert_eq!(version.colors.name_fg, (255, 255, 255));
371 assert_eq!(version.colors.name_bg, (170, 0, 255));
372 assert_eq!(version.colors.version_color, (255, 255, 0));
373 assert_eq!(version.colors.author_color, (0, 255, 255));
374 }
375
376 #[test]
377 fn test_macro() {
378 let version = colorful_version!();
379 assert!(!version.package_name().is_empty());
380
381 let custom = colorful_version!("#FFFFFF", "#AA00FF", "#FFFF00", "#00FFFF");
382 assert_eq!(custom.colors.name_fg, (255, 255, 255));
383 }
384
385 #[test]
386 fn test_command_extension() {
387 let version = colorful_version!();
388 let cmd = Command::new("testapp").with_colorful_version(&version);
389
390 assert_eq!(cmd.get_name(), "testapp");
392 }
393
394 #[test]
395 fn test_version_string_format() {
396 let version = ColorfulVersion::new("testapp", "1.2.3", "Test Author");
397 let plain = version.to_string();
398
399 assert_eq!(plain, "testapp v1.2.3 by Test Author");
401 }
402
403 #[test]
404 fn test_cargo_env_format() {
405 let version = ColorfulVersion::from_cargo();
406 let plain = version.to_string();
407
408 assert!(plain.contains(" v"));
410 assert!(plain.contains(" by "));
411 assert_eq!(
412 plain,
413 format!(
414 "{} v{} by {}",
415 env!("CARGO_PKG_NAME"),
416 env!("CARGO_PKG_VERSION"),
417 env!("CARGO_PKG_AUTHORS")
418 )
419 );
420 }
421
422 #[test]
423 fn test_display_trait() {
424 let version = ColorfulVersion::new("testapp", "1.2.3", "Test Author");
425 let display = format!("{}", version);
426 assert_eq!(display, "testapp v1.2.3 by Test Author");
427 }
428
429 #[test]
430 fn test_as_plain_string() {
431 let version = ColorfulVersion::new("myapp", "2.0.0", "John Doe");
432 let plain = version.as_plain_string();
433 assert_eq!(plain, "myapp v2.0.0 by John Doe");
434 }
435}