clap_version_flag/lib.rs
1// Project: clap-version-flag
2// File: src/lib.rs
3// Author: Hadi Cahyadi <cumulus13@gmail.com>
4// Date: 2025-12-12
5// Description: Production-ready version with all fixes
6// License: MIT
7
8//! A production-ready crate for adding colorful version output to clap applications.
9//!
10//! This crate provides utilities to override the default `-V`/`--version` flag behavior
11//! in clap applications with colorful output using hex color codes.
12//!
13//! # Examples
14//!
15//! ## Basic usage with derive
16//! ```no_run
17//! use clap::Parser;
18//! use clap_version_flag::colorful_version;
19//!
20//! #[derive(Parser)]
21//! #[command(name = "myapp")]
22//! struct Cli {
23//! #[arg(short, long)]
24//! input: String,
25//! }
26//!
27//! fn main() {
28//! let version = colorful_version!();
29//! let cli = Cli::parse();
30//!
31//! // Check version flag manually if needed
32//! // version.print(); // This would print colored version
33//! }
34//! ```
35//!
36//! ## Using with custom colors
37//! ```no_run
38//! use clap_version_flag::colorful_version;
39//!
40//! let version = colorful_version!("#FF0000", "#0000FF", "#00FF00", "#FFFF00");
41//! version.print();
42//! ```
43
44mod error;
45pub mod macros;
46
47pub use error::VersionError;
48
49use clap::{Arg, ArgAction, ArgMatches, Command, FromArgMatches};
50use colored::Colorize;
51use std::fmt;
52use std::process;
53
54/// Configuration for colorful version output
55#[derive(Clone, Debug)]
56pub struct ColorfulVersion {
57 package_name: String,
58 version: String,
59 author: String,
60 colors: Colors,
61}
62
63#[derive(Clone, Debug)]
64struct Colors {
65 name_fg: (u8, u8, u8), // RGB for name foreground
66 name_bg: (u8, u8, u8), // RGB for name background
67 version_color: (u8, u8, u8), // RGB for version
68 author_color: (u8, u8, u8), // RGB for author
69}
70
71impl Default for Colors {
72 fn default() -> Self {
73 Self {
74 // Default colors as specified in requirements:
75 // name: light #FFFFFF on #AA00FF
76 // version: light #FFFF00
77 // author: light #00FFFF
78 name_fg: (255, 255, 255), // #FFFFFF
79 name_bg: (170, 0, 255), // #AA00FF
80 version_color: (255, 255, 0), // #FFFF00
81 author_color: (0, 255, 255), // #00FFFF
82 }
83 }
84}
85
86impl ColorfulVersion {
87 /// Creates a new ColorfulVersion with custom values
88 ///
89 /// # Examples
90 /// ```
91 /// use clap_version_flag::ColorfulVersion;
92 ///
93 /// let version = ColorfulVersion::new("myapp", "1.0.0", "John Doe");
94 /// assert_eq!(version.package_name(), "myapp");
95 /// ```
96 #[must_use]
97 pub fn new(
98 package_name: impl Into<String>,
99 version: impl Into<String>,
100 author: impl Into<String>,
101 ) -> Self {
102 Self {
103 package_name: package_name.into(),
104 version: version.into(),
105 author: author.into(),
106 colors: Colors::default(),
107 }
108 }
109
110 /// Sets custom hex colors for the version output
111 ///
112 /// # Arguments
113 /// * `name_fg` - Hex color for package name foreground (e.g., "#FFFFFF")
114 /// * `name_bg` - Hex color for package name background (e.g., "#AA00FF")
115 /// * `version` - Hex color for version text (e.g., "#FFFF00")
116 /// * `author` - Hex color for author text (e.g., "#00FFFF")
117 ///
118 /// # Errors
119 /// Returns `VersionError::InvalidHexColor` if any hex color is invalid
120 ///
121 /// # Examples
122 /// ```
123 /// use clap_version_flag::ColorfulVersion;
124 ///
125 /// let version = ColorfulVersion::new("myapp", "1.0.0", "John Doe")
126 /// .with_hex_colors("#FFFFFF", "#AA00FF", "#FFFF00", "#00FFFF")
127 /// .expect("Invalid hex colors");
128 /// ```
129 pub fn with_hex_colors(
130 mut self,
131 name_fg: &str,
132 name_bg: &str,
133 version: &str,
134 author: &str,
135 ) -> Result<Self, VersionError> {
136 self.colors.name_fg = parse_hex(name_fg)?;
137 self.colors.name_bg = parse_hex(name_bg)?;
138 self.colors.version_color = parse_hex(version)?;
139 self.colors.author_color = parse_hex(author)?;
140 Ok(self)
141 }
142
143 /// Sets custom RGB colors for the version output
144 ///
145 /// # Examples
146 /// ```
147 /// use clap_version_flag::ColorfulVersion;
148 ///
149 /// let version = ColorfulVersion::new("myapp", "1.0.0", "John Doe")
150 /// .with_rgb_colors((255, 0, 0), (0, 0, 255), (0, 255, 0), (255, 255, 0));
151 /// ```
152 #[must_use]
153 pub fn with_rgb_colors(
154 mut self,
155 name_fg: (u8, u8, u8),
156 name_bg: (u8, u8, u8),
157 version: (u8, u8, u8),
158 author: (u8, u8, u8),
159 ) -> Self {
160 self.colors.name_fg = name_fg;
161 self.colors.name_bg = name_bg;
162 self.colors.version_color = version;
163 self.colors.author_color = author;
164 self
165 }
166
167 /// Prints the colorful version to stdout and exits the process
168 ///
169 /// # Examples
170 /// ```no_run
171 /// use clap_version_flag::colorful_version;
172 ///
173 /// let version = colorful_version!();
174 /// version.print_and_exit(); // Prints and exits with code 0
175 /// ```
176 pub fn print_and_exit(&self) -> ! {
177 self.print();
178 process::exit(0);
179 }
180
181 /// Prints the colorful version to stdout
182 /// Format: "{package_name} v{version} by {author}"
183 ///
184 /// # Examples
185 /// ```
186 /// use clap_version_flag::ColorfulVersion;
187 ///
188 /// let version = ColorfulVersion::new("myapp", "1.0.0", "John Doe");
189 /// version.print(); // Prints colored output
190 /// ```
191 pub fn print(&self) {
192 let name = self
193 .package_name
194 .truecolor(
195 self.colors.name_fg.0,
196 self.colors.name_fg.1,
197 self.colors.name_fg.2,
198 )
199 .on_truecolor(
200 self.colors.name_bg.0,
201 self.colors.name_bg.1,
202 self.colors.name_bg.2,
203 );
204
205 let version_text = format!(" v{}", self.version).truecolor(
206 self.colors.version_color.0,
207 self.colors.version_color.1,
208 self.colors.version_color.2,
209 );
210
211 let author_text = format!(" by {}", self.author).truecolor(
212 self.colors.author_color.0,
213 self.colors.author_color.1,
214 self.colors.author_color.2,
215 );
216
217 println!("{name}{version_text}{author_text}");
218 }
219
220 /// Returns a plain text version string (for clap's version flag)
221 /// Format: "{package_name} v{version} by {author}"
222 ///
223 /// # Examples
224 /// ```
225 /// use clap_version_flag::ColorfulVersion;
226 ///
227 /// let version = ColorfulVersion::new("myapp", "1.0.0", "John Doe");
228 /// assert_eq!(version.as_plain_string(), "myapp v1.0.0 by John Doe");
229 /// ```
230 #[must_use]
231 pub fn as_plain_string(&self) -> String {
232 format!("{} v{} by {}", self.package_name, self.version, self.author)
233 }
234
235 /// Returns a colored version string if terminal supports colors
236 /// Format: "{package_name} v{version} by {author}"
237 ///
238 /// # Examples
239 /// ```
240 /// use clap_version_flag::ColorfulVersion;
241 ///
242 /// let version = ColorfulVersion::new("myapp", "1.0.0", "John Doe");
243 /// let colored = version.to_colored_string();
244 /// // colored string contains ANSI color codes
245 /// ```
246 #[must_use]
247 pub fn to_colored_string(&self) -> String {
248 format!(
249 "{}{}{}",
250 self.package_name
251 .truecolor(
252 self.colors.name_fg.0,
253 self.colors.name_fg.1,
254 self.colors.name_fg.2
255 )
256 .on_truecolor(
257 self.colors.name_bg.0,
258 self.colors.name_bg.1,
259 self.colors.name_bg.2
260 ),
261 format!(" v{}", self.version).truecolor(
262 self.colors.version_color.0,
263 self.colors.version_color.1,
264 self.colors.version_color.2
265 ),
266 format!(" by {}", self.author).truecolor(
267 self.colors.author_color.0,
268 self.colors.author_color.1,
269 self.colors.author_color.2
270 )
271 )
272 }
273
274 /// Checks if the version flag was used and handles it
275 ///
276 /// This method should be called after parsing command-line arguments.
277 /// If the version flag is found, it prints the colorful version and exits.
278 ///
279 /// # Examples
280 /// ```no_run
281 /// use clap::Command;
282 /// use clap_version_flag::{ColorfulVersion, ColorfulVersionExt};
283 ///
284 /// let version = ColorfulVersion::new("myapp", "1.0.0", "John Doe");
285 /// let matches = Command::new("myapp")
286 /// .with_colorful_version(&version)
287 /// .get_matches();
288 ///
289 /// version.check_and_exit(&matches);
290 /// ```
291 pub fn check_and_exit(&self, matches: &ArgMatches) {
292 if matches.get_flag("clap_version_flag_version") {
293 self.print_and_exit();
294 }
295 }
296
297 /// Returns the package name
298 ///
299 /// # Examples
300 /// ```
301 /// use clap_version_flag::ColorfulVersion;
302 ///
303 /// let version = ColorfulVersion::new("myapp", "1.0.0", "John Doe");
304 /// assert_eq!(version.package_name(), "myapp");
305 /// ```
306 #[must_use]
307 pub fn package_name(&self) -> &str {
308 &self.package_name
309 }
310
311 /// Returns the version
312 ///
313 /// # Examples
314 /// ```
315 /// use clap_version_flag::ColorfulVersion;
316 ///
317 /// let version = ColorfulVersion::new("myapp", "1.0.0", "John Doe");
318 /// assert_eq!(version.version(), "1.0.0");
319 /// ```
320 #[must_use]
321 pub fn version(&self) -> &str {
322 &self.version
323 }
324
325 /// Returns the author
326 ///
327 /// # Examples
328 /// ```
329 /// use clap_version_flag::ColorfulVersion;
330 ///
331 /// let version = ColorfulVersion::new("myapp", "1.0.0", "John Doe");
332 /// assert_eq!(version.author(), "John Doe");
333 /// ```
334 #[must_use]
335 pub fn author(&self) -> &str {
336 &self.author
337 }
338}
339
340/// Implement Display trait for ColorfulVersion
341impl fmt::Display for ColorfulVersion {
342 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
343 write!(
344 f,
345 "{} v{} by {}",
346 self.package_name, self.version, self.author
347 )
348 }
349}
350
351/// Parses a hex color string to RGB values
352///
353/// # Arguments
354/// * `hex` - Hex color string (e.g., "#FFFFFF", "#FFF", or "FFFFFF")
355///
356/// # Errors
357/// Returns `VersionError::InvalidHexColor` if the hex string is invalid
358///
359/// # Examples
360/// ```
361/// # use clap_version_flag::ColorfulVersion;
362/// // This is an internal function, example shown for completeness
363/// let version = ColorfulVersion::new("app", "1.0", "author")
364/// .with_hex_colors("#FFF", "#000", "#F00", "#0F0")
365/// .unwrap();
366/// ```
367fn parse_hex(hex: &str) -> Result<(u8, u8, u8), VersionError> {
368 let hex = hex.trim_start_matches('#');
369
370 match hex.len() {
371 6 => {
372 let r =
373 u8::from_str_radix(&hex[0..2], 16).map_err(|_| VersionError::invalid_hex(hex))?;
374 let g =
375 u8::from_str_radix(&hex[2..4], 16).map_err(|_| VersionError::invalid_hex(hex))?;
376 let b =
377 u8::from_str_radix(&hex[4..6], 16).map_err(|_| VersionError::invalid_hex(hex))?;
378 Ok((r, g, b))
379 }
380 3 => {
381 // Expand #RGB to #RRGGBB
382 let r = u8::from_str_radix(&hex[0..1].repeat(2), 16)
383 .map_err(|_| VersionError::invalid_hex(hex))?;
384 let g = u8::from_str_radix(&hex[1..2].repeat(2), 16)
385 .map_err(|_| VersionError::invalid_hex(hex))?;
386 let b = u8::from_str_radix(&hex[2..3].repeat(2), 16)
387 .map_err(|_| VersionError::invalid_hex(hex))?;
388 Ok((r, g, b))
389 }
390 _ => Err(VersionError::invalid_hex(hex)),
391 }
392}
393
394// NOTE: Main macros are now in src/macros.rs and re-exported
395// This ensures they're available to users while keeping code organized
396
397/// Extension trait for clap::Command to add colorful version flag
398pub trait ColorfulVersionExt {
399 /// Adds a version flag that will display colorful output when used
400 ///
401 /// # Examples
402 /// ```
403 /// use clap::Command;
404 /// use clap_version_flag::{ColorfulVersion, ColorfulVersionExt};
405 ///
406 /// let version = ColorfulVersion::new("myapp", "1.0.0", "John Doe");
407 /// let cmd = Command::new("myapp").with_colorful_version(&version);
408 /// ```
409 fn with_colorful_version(self, version: &ColorfulVersion) -> Self;
410}
411
412impl ColorfulVersionExt for Command {
413 fn with_colorful_version(self, _version: &ColorfulVersion) -> Self {
414 // Disable clap's built-in version flag and add our custom one
415 self.disable_version_flag(true).arg(
416 Arg::new("clap_version_flag_version")
417 .short('V')
418 .long("version")
419 .action(ArgAction::SetTrue)
420 .help("Print version information")
421 .global(true),
422 )
423 }
424}
425
426/// Helper function to parse command-line arguments with version handling
427///
428/// This function should be used instead of directly calling `get_matches()`.
429/// If the version flag is found, it prints the colorful version and exits.
430///
431/// # Examples
432/// ```no_run
433/// use clap::{Parser, CommandFactory};
434/// use clap_version_flag::{colorful_version, parse_with_version};
435///
436/// #[derive(Parser)]
437/// struct Cli {
438/// input: String,
439/// }
440///
441/// fn main() -> Result<(), Box<dyn std::error::Error>> {
442/// let version = colorful_version!();
443/// let cli: Cli = parse_with_version(Cli::command(), &version)?;
444///
445/// // Normal program execution continues here
446/// Ok(())
447/// }
448/// ```
449pub fn parse_with_version<T: FromArgMatches>(
450 command: Command,
451 version: &ColorfulVersion,
452) -> Result<T, clap::Error> {
453 let command = command.with_colorful_version(version);
454 let matches = command.get_matches();
455
456 // Check if version flag was used
457 if matches.get_flag("clap_version_flag_version") {
458 version.print();
459 process::exit(0);
460 }
461
462 T::from_arg_matches(&matches)
463}
464
465#[cfg(test)]
466mod tests {
467 use super::*;
468
469 #[test]
470 fn test_hex_parsing() {
471 assert_eq!(parse_hex("#FFFFFF").unwrap(), (255, 255, 255));
472 assert_eq!(parse_hex("#000000").unwrap(), (0, 0, 0));
473 assert_eq!(parse_hex("#FF0000").unwrap(), (255, 0, 0));
474 assert_eq!(parse_hex("#FFF").unwrap(), (255, 255, 255));
475 assert_eq!(parse_hex("#F00").unwrap(), (255, 0, 0));
476 assert_eq!(parse_hex("FFFFFF").unwrap(), (255, 255, 255));
477
478 assert!(parse_hex("INVALID").is_err());
479 assert!(parse_hex("#GGG").is_err());
480 assert!(parse_hex("#12345").is_err());
481 }
482
483 #[test]
484 fn test_default_colors() {
485 let version = ColorfulVersion::new("test", "1.0.0", "author");
486 assert_eq!(version.colors.name_fg, (255, 255, 255));
487 assert_eq!(version.colors.name_bg, (170, 0, 255));
488 assert_eq!(version.colors.version_color, (255, 255, 0));
489 assert_eq!(version.colors.author_color, (0, 255, 255));
490 }
491
492 #[test]
493 fn test_macro() {
494 let version = colorful_version!();
495 // In tests, this will use clap-version-flag's own package info
496 assert_eq!(version.package_name(), env!("CARGO_PKG_NAME"));
497 assert!(!version.package_name().is_empty());
498
499 let custom = colorful_version!("#FFFFFF", "#AA00FF", "#FFFF00", "#00FFFF");
500 assert_eq!(custom.colors.name_fg, (255, 255, 255));
501 assert_eq!(custom.colors.name_bg, (170, 0, 255));
502 }
503
504 #[test]
505 fn test_command_extension() {
506 let version = colorful_version!();
507 let cmd = Command::new("testapp").with_colorful_version(&version);
508
509 // Should compile without errors
510 assert_eq!(cmd.get_name(), "testapp");
511 }
512
513 #[test]
514 fn test_version_string_format() {
515 let version = ColorfulVersion::new("testapp", "1.2.3", "Test Author");
516 let plain = version.to_string();
517
518 // Verify format: "{name} v{version} by {author}"
519 assert_eq!(plain, "testapp v1.2.3 by Test Author");
520 }
521
522 #[test]
523 fn test_display_trait() {
524 let version = ColorfulVersion::new("testapp", "1.2.3", "Test Author");
525 let display = format!("{}", version);
526 assert_eq!(display, "testapp v1.2.3 by Test Author");
527 }
528
529 #[test]
530 fn test_as_plain_string() {
531 let version = ColorfulVersion::new("myapp", "2.0.0", "John Doe");
532 let plain = version.as_plain_string();
533 assert_eq!(plain, "myapp v2.0.0 by John Doe");
534 }
535
536 #[test]
537 fn test_getters() {
538 let version = ColorfulVersion::new("myapp", "1.0.0", "John Doe");
539 assert_eq!(version.package_name(), "myapp");
540 assert_eq!(version.version(), "1.0.0");
541 assert_eq!(version.author(), "John Doe");
542 }
543
544 #[test]
545 fn test_custom_colors_rgb() {
546 let version = ColorfulVersion::new("test", "1.0.0", "author").with_rgb_colors(
547 (255, 0, 0),
548 (0, 255, 0),
549 (0, 0, 255),
550 (255, 255, 0),
551 );
552
553 assert_eq!(version.colors.name_fg, (255, 0, 0));
554 assert_eq!(version.colors.name_bg, (0, 255, 0));
555 assert_eq!(version.colors.version_color, (0, 0, 255));
556 assert_eq!(version.colors.author_color, (255, 255, 0));
557 }
558
559 #[test]
560 fn test_custom_colors_hex() {
561 let version = ColorfulVersion::new("test", "1.0.0", "author")
562 .with_hex_colors("#FF0000", "#00FF00", "#0000FF", "#FFFF00")
563 .unwrap();
564
565 assert_eq!(version.colors.name_fg, (255, 0, 0));
566 assert_eq!(version.colors.name_bg, (0, 255, 0));
567 assert_eq!(version.colors.version_color, (0, 0, 255));
568 assert_eq!(version.colors.author_color, (255, 255, 0));
569 }
570
571 #[test]
572 fn test_short_hex() {
573 let version = ColorfulVersion::new("test", "1.0.0", "author")
574 .with_hex_colors("#F00", "#0F0", "#00F", "#FF0")
575 .unwrap();
576
577 assert_eq!(version.colors.name_fg, (255, 0, 0));
578 assert_eq!(version.colors.name_bg, (0, 255, 0));
579 assert_eq!(version.colors.version_color, (0, 0, 255));
580 assert_eq!(version.colors.author_color, (255, 255, 0));
581 }
582}