1mod error;
7
8pub use error::VersionError;
9
10use colored::Colorize;
11use clap::{Arg, ArgAction, ArgMatches, Command, FromArgMatches};
12use std::process;
13
14#[derive(Clone, Debug)]
16pub struct ColorfulVersion {
17 package_name: &'static str,
18 version: &'static str,
19 author: &'static str,
20 colors: Colors,
21}
22
23#[derive(Clone, Debug)]
24struct Colors {
25 name_fg: (u8, u8, u8), name_bg: (u8, u8, u8), version_color: (u8, u8, u8), author_color: (u8, u8, u8), }
30
31impl Default for Colors {
32 fn default() -> Self {
33 Self {
34 name_fg: (255, 255, 255), name_bg: (170, 0, 255), version_color: (255, 255, 0), author_color: (0, 255, 255), }
43 }
44}
45
46impl ColorfulVersion {
47 #[must_use]
49 pub fn from_cargo() -> Self {
50 Self {
51 package_name: env!("CARGO_PKG_NAME"),
52 version: env!("CARGO_PKG_VERSION"),
53 author: env!("CARGO_PKG_AUTHORS"),
54 colors: Colors::default(),
55 }
56 }
57
58 #[must_use]
60 pub fn new(
61 package_name: &'static str,
62 version: &'static str,
63 author: &'static str,
64 ) -> Self {
65 Self {
66 package_name,
67 version,
68 author,
69 colors: Colors::default(),
70 }
71 }
72
73 pub fn with_hex_colors(
84 mut self,
85 name_fg: &str,
86 name_bg: &str,
87 version: &str,
88 author: &str,
89 ) -> Result<Self, VersionError> {
90 self.colors.name_fg = parse_hex(name_fg)?;
91 self.colors.name_bg = parse_hex(name_bg)?;
92 self.colors.version_color = parse_hex(version)?;
93 self.colors.author_color = parse_hex(author)?;
94 Ok(self)
95 }
96
97 #[must_use]
99 pub fn with_rgb_colors(
100 mut self,
101 name_fg: (u8, u8, u8),
102 name_bg: (u8, u8, u8),
103 version: (u8, u8, u8),
104 author: (u8, u8, u8),
105 ) -> Self {
106 self.colors.name_fg = name_fg;
107 self.colors.name_bg = name_bg;
108 self.colors.version_color = version;
109 self.colors.author_color = author;
110 self
111 }
112
113 pub fn print_and_exit(&self) -> ! {
115 self.print();
116 process::exit(0);
117 }
118
119 pub fn print(&self) {
122 let name = self.package_name
123 .truecolor(self.colors.name_fg.0, self.colors.name_fg.1, self.colors.name_fg.2)
124 .on_truecolor(self.colors.name_bg.0, self.colors.name_bg.1, self.colors.name_bg.2);
125
126 let version_text = format!(" v{}", self.version)
127 .truecolor(self.colors.version_color.0, self.colors.version_color.1, self.colors.version_color.2);
128
129 let author_text = format!(" by {}", self.author)
130 .truecolor(self.colors.author_color.0, self.colors.author_color.1, self.colors.author_color.2);
131
132 println!("{name}{version_text}{author_text}");
133 }
134
135 #[must_use]
138 pub fn to_string(&self) -> String {
139 format!("{} v{} by {}", self.package_name, self.version, self.author)
140 }
141
142 #[must_use]
145 pub fn to_colored_string(&self) -> String {
146 format!("{}{}{}",
147 self.package_name
148 .truecolor(self.colors.name_fg.0, self.colors.name_fg.1, self.colors.name_fg.2)
149 .on_truecolor(self.colors.name_bg.0, self.colors.name_bg.1, self.colors.name_bg.2),
150 format!(" v{}", self.version)
151 .truecolor(self.colors.version_color.0, self.colors.version_color.1, self.colors.version_color.2),
152 format!(" by {}", self.author)
153 .truecolor(self.colors.author_color.0, self.colors.author_color.1, self.colors.author_color.2)
154 )
155 }
156
157 pub fn check_and_exit(&self, matches: &ArgMatches) {
162 if matches.get_flag("clap_version_flag_version") {
163 self.print_and_exit();
164 }
165 }
166
167 #[must_use]
169 pub fn package_name(&self) -> &str {
170 self.package_name
171 }
172
173 #[must_use]
175 pub fn version(&self) -> &str {
176 self.version
177 }
178
179 #[must_use]
181 pub fn author(&self) -> &str {
182 self.author
183 }
184}
185
186fn parse_hex(hex: &str) -> Result<(u8, u8, u8), VersionError> {
194 let hex = hex.trim_start_matches('#');
195
196 match hex.len() {
197 6 => {
198 let r = u8::from_str_radix(&hex[0..2], 16)
199 .map_err(|_| VersionError::invalid_hex(hex))?;
200 let g = u8::from_str_radix(&hex[2..4], 16)
201 .map_err(|_| VersionError::invalid_hex(hex))?;
202 let b = u8::from_str_radix(&hex[4..6], 16)
203 .map_err(|_| VersionError::invalid_hex(hex))?;
204 Ok((r, g, b))
205 }
206 3 => {
207 let r = u8::from_str_radix(&hex[0..1].repeat(2), 16)
209 .map_err(|_| VersionError::invalid_hex(hex))?;
210 let g = u8::from_str_radix(&hex[1..2].repeat(2), 16)
211 .map_err(|_| VersionError::invalid_hex(hex))?;
212 let b = u8::from_str_radix(&hex[2..3].repeat(2), 16)
213 .map_err(|_| VersionError::invalid_hex(hex))?;
214 Ok((r, g, b))
215 }
216 _ => Err(VersionError::invalid_hex(hex)),
217 }
218}
219
220#[macro_export]
233macro_rules! colorful_version {
234 () => {
235 $crate::ColorfulVersion::from_cargo()
236 };
237
238 ($name_fg:expr, $name_bg:expr, $version:expr, $author:expr) => {
239 $crate::ColorfulVersion::from_cargo()
240 .with_hex_colors($name_fg, $name_bg, $version, $author)
241 .unwrap_or_else(|e| {
242 panic!("clap-version-flag: Invalid hex color format: {}", e)
243 })
244 };
245}
246
247pub trait ColorfulVersionExt {
249 fn with_colorful_version(self, version: &ColorfulVersion) -> Self;
251}
252
253impl ColorfulVersionExt for Command {
254 fn with_colorful_version(self, _version: &ColorfulVersion) -> Self {
255 self.arg(
257 Arg::new("clap_version_flag_version")
258 .short('V')
259 .long("version")
260 .action(ArgAction::SetTrue)
261 .help("Print version information")
262 .global(true)
263 )
264 }
265}
266
267pub fn parse_with_version<T: FromArgMatches>(
291 command: Command,
292 version: &ColorfulVersion,
293) -> Result<T, clap::Error> {
294 let command = command.with_colorful_version(version);
295 let matches = command.get_matches();
296
297 if matches.get_flag("clap_version_flag_version") {
299 version.print();
300 process::exit(0);
301 }
302
303 T::from_arg_matches(&matches)
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309
310 #[test]
311 fn test_hex_parsing() {
312 assert_eq!(parse_hex("#FFFFFF").unwrap(), (255, 255, 255));
313 assert_eq!(parse_hex("#000000").unwrap(), (0, 0, 0));
314 assert_eq!(parse_hex("#FF0000").unwrap(), (255, 0, 0));
315 assert_eq!(parse_hex("#FFF").unwrap(), (255, 255, 255));
316 assert_eq!(parse_hex("#F00").unwrap(), (255, 0, 0));
317 assert_eq!(parse_hex("FFFFFF").unwrap(), (255, 255, 255));
318
319 assert!(parse_hex("INVALID").is_err());
320 assert!(parse_hex("#GGG").is_err());
321 assert!(parse_hex("#12345").is_err());
322 }
323
324 #[test]
325 fn test_default_colors() {
326 let version = ColorfulVersion::from_cargo();
327 assert_eq!(version.colors.name_fg, (255, 255, 255));
328 assert_eq!(version.colors.name_bg, (170, 0, 255));
329 assert_eq!(version.colors.version_color, (255, 255, 0));
330 assert_eq!(version.colors.author_color, (0, 255, 255));
331 }
332
333 #[test]
334 fn test_macro() {
335 let version = colorful_version!();
336 assert!(!version.package_name().is_empty());
337
338 let custom = colorful_version!("#FFFFFF", "#AA00FF", "#FFFF00", "#00FFFF");
339 assert_eq!(custom.colors.name_fg, (255, 255, 255));
340 }
341
342 #[test]
343 fn test_command_extension() {
344 let version = colorful_version!();
345 let cmd = Command::new("testapp")
346 .with_colorful_version(&version);
347
348 assert_eq!(cmd.get_name(), "testapp");
350 }
351
352 #[test]
353 fn test_version_string_format() {
354 let version = ColorfulVersion::new("testapp", "1.2.3", "Test Author");
355 let plain = version.to_string();
356
357 assert_eq!(plain, "testapp v1.2.3 by Test Author");
359 }
360
361 #[test]
362 fn test_cargo_env_format() {
363 let version = ColorfulVersion::from_cargo();
364 let plain = version.to_string();
365
366 assert!(plain.contains(" v"));
368 assert!(plain.contains(" by "));
369 assert_eq!(plain, format!("{} v{} by {}",
370 env!("CARGO_PKG_NAME"),
371 env!("CARGO_PKG_VERSION"),
372 env!("CARGO_PKG_AUTHORS")
373 ));
374 }
375}