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}