clap_version_flag/
lib.rs

1//! A production-ready crate for adding colorful version output to clap applications.
2//! 
3//! This crate provides utilities to override the default `-V`/`--version` flag behavior
4//! in clap applications with colorful output using hex color codes.
5
6mod error;
7
8pub use error::VersionError;
9
10use colored::Colorize;
11use clap::{Arg, ArgAction, ArgMatches, Command, FromArgMatches};
12use std::process;
13
14/// Configuration for colorful version output
15#[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),      // RGB for name foreground
26    name_bg: (u8, u8, u8),      // RGB for name background
27    version_color: (u8, u8, u8), // RGB for version
28    author_color: (u8, u8, u8),  // RGB for author
29}
30
31impl Default for Colors {
32    fn default() -> Self {
33        Self {
34            // Default colors as specified in requirements:
35            // name: light #FFFFFF on #AA00FF
36            // version: light #FFFF00
37            // author: light #00FFFF
38            name_fg: (255, 255, 255),   // #FFFFFF
39            name_bg: (170, 0, 255),     // #AA00FF
40            version_color: (255, 255, 0), // #FFFF00
41            author_color: (0, 255, 255),  // #00FFFF
42        }
43    }
44}
45
46impl ColorfulVersion {
47    /// Creates a new ColorfulVersion using values from Cargo.toml
48    #[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    /// Creates a new ColorfulVersion with custom values
59    #[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    /// Sets custom hex colors for the version output
74    ///
75    /// # Arguments
76    /// * `name_fg` - Hex color for package name foreground (e.g., "#FFFFFF")
77    /// * `name_bg` - Hex color for package name background (e.g., "#AA00FF")
78    /// * `version` - Hex color for version text (e.g., "#FFFF00")
79    /// * `author` - Hex color for author text (e.g., "#00FFFF")
80    ///
81    /// # Errors
82    /// Returns `VersionError::InvalidHexColor` if any hex color is invalid
83    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    /// Sets custom RGB colors for the version output
98    #[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    /// Prints the colorful version to stdout and exits the process
114    pub fn print_and_exit(&self) -> ! {
115        self.print();
116        process::exit(0);
117    }
118    
119    /// Prints the colorful version to stdout
120    /// Format: "{package_name} v{version} by {author}"
121    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    /// Returns a plain text version string (for clap's version flag)
136    /// Format: "{package_name} v{version} by {author}"
137    #[must_use]
138    pub fn to_string(&self) -> String {
139        format!("{} v{} by {}", self.package_name, self.version, self.author)
140    }
141    
142    /// Returns a colored version string if terminal supports colors
143    /// Format: "{package_name} v{version} by {author}"
144    #[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    /// Checks if the version flag was used and handles it
158    ///
159    /// This method should be called after parsing command-line arguments.
160    /// If the version flag is found, it prints the colorful version and exits.
161    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    /// Returns the package name
168    #[must_use]
169    pub fn package_name(&self) -> &str {
170        self.package_name
171    }
172    
173    /// Returns the version
174    #[must_use]
175    pub fn version(&self) -> &str {
176        self.version
177    }
178    
179    /// Returns the author
180    #[must_use]
181    pub fn author(&self) -> &str {
182        self.author
183    }
184}
185
186/// Parses a hex color string to RGB values
187///
188/// # Arguments
189/// * `hex` - Hex color string (e.g., "#FFFFFF", "#FFF", or "FFFFFF")
190///
191/// # Errors
192/// Returns `VersionError::InvalidHexColor` if the hex string is invalid
193fn 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            // Expand #RGB to #RRGGBB
208            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 for easy creation of ColorfulVersion from Cargo.toml
221///
222/// # Examples
223/// ```
224/// use clap_version_flag::colorful_version;
225///
226/// // With default colors
227/// let version = colorful_version!();
228///
229/// // With custom hex colors
230/// let version = colorful_version!("#FFFFFF", "#AA00FF", "#FFFF00", "#00FFFF");
231/// ```
232#[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
247/// Extension trait for clap::Command to add colorful version flag
248pub trait ColorfulVersionExt {
249    /// Adds a version flag that will display colorful output when used
250    fn with_colorful_version(self, version: &ColorfulVersion) -> Self;
251}
252
253impl ColorfulVersionExt for Command {
254    fn with_colorful_version(self, _version: &ColorfulVersion) -> Self {
255        // Just add the flag - actual handling is done in parse_with_version
256        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
267/// Helper function to parse command-line arguments with version handling
268///
269/// This function should be used instead of directly calling `get_matches()`.
270/// If the version flag is found, it prints the colorful version and exits.
271///
272/// # Examples
273/// ```no_run
274/// use clap::{Parser, CommandFactory};
275/// use clap_version_flag::{colorful_version, parse_with_version};
276///
277/// #[derive(Parser)]
278/// struct Cli {
279///     input: String,
280/// }
281///
282/// fn main() -> Result<(), Box<dyn std::error::Error>> {
283///     let version = colorful_version!();
284///     let cli: Cli = parse_with_version(Cli::command(), &version)?;
285///     
286///     // Normal program execution continues here
287///     Ok(())
288/// }
289/// ```
290pub 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    // Check if version flag was used
298    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        // Should compile without errors
349        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        // Verify format: "{name} v{version} by {author}"
358        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        // Should follow format: "{CARGO_PKG_NAME} v{CARGO_PKG_VERSION} by {CARGO_PKG_AUTHORS}"
367        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}