Skip to main content

wx_uploader/
cli.rs

1//! Command-line interface for the wx-uploader application
2//!
3//! This module handles argument parsing, colored help display, and CLI-specific
4//! functionality for the WeChat uploader tool.
5
6use clap::Parser;
7use colored::*;
8use std::path::PathBuf;
9
10/// Command-line arguments for the wx-uploader application
11#[derive(Parser, Debug)]
12#[command(
13    author,
14    version,
15    about = "A tool to upload articles to WeChat Official Account",
16    override_help = "Run with --help to see colored help",
17    color = clap::ColorChoice::Always,
18    styles = clap::builder::Styles::styled()
19        .header(clap::builder::styling::AnsiColor::Yellow.on_default())
20        .usage(clap::builder::styling::AnsiColor::Green.on_default())
21        .literal(clap::builder::styling::AnsiColor::Green.on_default())
22        .placeholder(clap::builder::styling::AnsiColor::Green.on_default())
23)]
24pub struct Args {
25    /// Path to markdown file or directory to upload
26    #[arg(
27        help = "Path to markdown file or directory to upload. Files uploaded regardless of status. Directories skip published files. Set theme and code highlighter in frontmatter - see help for complete lists."
28    )]
29    pub path: PathBuf,
30
31    /// Enable verbose logging with detailed tracing information
32    #[arg(
33        short,
34        long,
35        help = "Enable verbose logging with detailed tracing information"
36    )]
37    pub verbose: bool,
38
39    /// Force refresh WeChat access token before operation
40    #[arg(
41        short = 'r',
42        long = "refresh",
43        help = "Force refresh WeChat access token before operation. This gets a new token from WeChat API."
44    )]
45    pub clear_cache: bool,
46}
47
48/// Print colored help message with detailed information about usage and features
49pub fn print_colored_help() {
50    // Force colors to be enabled
51    colored::control::set_override(true);
52
53    println!(
54        "{}",
55        "A tool to upload articles to WeChat Official Account"
56            .bright_white()
57            .bold()
58    );
59    println!();
60    println!(
61        "{}: {} [OPTIONS] <PATH>",
62        "Usage".bright_green().bold(),
63        "wx-uploader".bright_cyan()
64    );
65    println!();
66    println!("{}", "Arguments:".bright_yellow().bold());
67    println!(
68        "  {}  Path to markdown file or directory to upload. Files uploaded regardless of status.",
69        "<PATH>".bright_cyan()
70    );
71    println!(
72        "          Directories skip published files. Set theme and code highlighter in frontmatter - see help"
73    );
74    println!("          for complete lists.");
75    println!();
76    println!("{}", "Options:".bright_yellow().bold());
77    println!(
78        "  {}, {}  Enable verbose logging with detailed tracing information",
79        "-v".bright_cyan(),
80        "--verbose".bright_cyan()
81    );
82    println!(
83        "  {}, {}  Force refresh WeChat access token",
84        "-r".bright_cyan(),
85        "--refresh".bright_cyan()
86    );
87    println!(
88        "  {}, {}     Print help",
89        "-h".bright_cyan(),
90        "--help".bright_cyan()
91    );
92    println!(
93        "  {}, {}  Print version",
94        "-V".bright_cyan(),
95        "--version".bright_cyan()
96    );
97    println!();
98    println!(
99        "{}: Set {} and {} environment variables",
100        "REQUIREMENTS".bright_red().bold(),
101        "WECHAT_APP_ID".bright_cyan(),
102        "WECHAT_APP_SECRET".bright_cyan()
103    );
104    println!(
105        "{}: Set {} for automatic cover image generation",
106        "OPTIONAL".bright_blue().bold(),
107        "OPENAI_API_KEY".bright_cyan()
108    );
109    println!();
110    println!(
111        "{}: Supports {} themes and {} code highlighters via frontmatter:",
112        "THEMING".bright_green().bold(),
113        "8".bright_white().bold(),
114        "10".bright_white().bold()
115    );
116    println!("  {}", "---".bright_black());
117    println!("  {}: \"My Article\"", "title".bright_cyan());
118    println!(
119        "  {}: \"lapis\"        {} Themes: {}",
120        "theme".bright_cyan(),
121        "#".bright_black(),
122        "default, lapis, maize, orangeheart, phycat, pie, purple, rainbow".white()
123    );
124    println!(
125        "  {}: \"github\"        {} Highlighters: {}",
126        "code".bright_cyan(),
127        "#".bright_black(),
128        "github, github-dark, vscode, atom-one-light, atom-one-dark,".white()
129    );
130    println!(
131        "                        {}",
132        "solarized-light, solarized-dark, monokai, dracula, xcode".white()
133    );
134    println!("  {}: \"draft\"", "published".bright_cyan());
135    println!(
136        "  {}: \"cover.png\"     {} Auto-generated if missing with OpenAI",
137        "cover".bright_cyan(),
138        "#".bright_black()
139    );
140    println!("  {}", "---".bright_black());
141    println!();
142    println!("{}", "EXAMPLES:".bright_blue().bold());
143    println!(
144        "  {}      {} Upload single file (force)",
145        "wx-uploader article.md".bright_white().bold(),
146        "#".bright_black()
147    );
148    println!(
149        "  {}     {} Process directory (skip published)",
150        "wx-uploader ./articles/".bright_white().bold(),
151        "#".bright_black()
152    );
153    println!(
154        "  {}      {} Verbose logging",
155        "wx-uploader -v ./blog/".bright_white().bold(),
156        "#".bright_black()
157    );
158    println!(
159        "  {}  {} Force refresh token & upload",
160        "wx-uploader -r article.md".bright_white().bold(),
161        "#".bright_black()
162    );
163}
164
165/// Validates command-line arguments
166pub fn validate_args(args: &Args) -> Result<(), String> {
167    if !args.path.exists() {
168        return Err(format!("Path does not exist: {}", args.path.display()));
169    }
170
171    if !args.path.is_file() && !args.path.is_dir() {
172        return Err(format!(
173            "Path must be a file or directory: {}",
174            args.path.display()
175        ));
176    }
177
178    Ok(())
179}
180
181/// Initializes logging based on the verbose flag
182pub fn init_logging(verbose: bool) {
183    if verbose {
184        tracing_subscriber::fmt::init();
185    }
186}
187
188/// Display startup banner with configuration information
189pub fn display_banner(args: &Args) {
190    if !args.verbose {
191        return;
192    }
193
194    println!();
195    println!("{}", "wx-uploader".bright_cyan().bold());
196    println!("{}", "=".repeat(20).bright_black());
197    println!("Path: {}", args.path.display().to_string().bright_white());
198    println!(
199        "Mode: {}",
200        if args.path.is_file() {
201            "Single file"
202        } else {
203            "Directory"
204        }
205        .bright_green()
206    );
207    println!("Verbose: {}", args.verbose.to_string().bright_blue());
208    println!("{}", "=".repeat(20).bright_black());
209    println!();
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215    use std::fs;
216    use tempfile::TempDir;
217
218    #[test]
219    fn test_validate_args_file_exists() {
220        let temp_dir = TempDir::new().unwrap();
221        let file_path = temp_dir.path().join("test.md");
222        fs::write(&file_path, "test content").unwrap();
223
224        let args = Args {
225            path: file_path,
226            verbose: false,
227            clear_cache: false,
228        };
229
230        assert!(validate_args(&args).is_ok());
231    }
232
233    #[test]
234    fn test_validate_args_dir_exists() {
235        let temp_dir = TempDir::new().unwrap();
236        let args = Args {
237            path: temp_dir.path().to_path_buf(),
238            verbose: false,
239            clear_cache: false,
240        };
241
242        assert!(validate_args(&args).is_ok());
243    }
244
245    #[test]
246    fn test_validate_args_path_not_exists() {
247        let args = Args {
248            path: PathBuf::from("nonexistent/path"),
249            verbose: false,
250            clear_cache: false,
251        };
252
253        assert!(validate_args(&args).is_err());
254        assert!(validate_args(&args).unwrap_err().contains("does not exist"));
255    }
256
257    #[test]
258    fn test_init_logging_verbose() {
259        // This test mainly ensures the function doesn't panic
260        // In practice, logging setup would be tested differently
261        init_logging(true);
262        init_logging(false);
263    }
264
265    #[test]
266    fn test_display_banner() {
267        let temp_dir = TempDir::new().unwrap();
268        let args = Args {
269            path: temp_dir.path().to_path_buf(),
270            verbose: true,
271            clear_cache: false,
272        };
273
274        // This test mainly ensures the function doesn't panic
275        display_banner(&args);
276
277        let args = Args {
278            path: temp_dir.path().to_path_buf(),
279            verbose: false,
280            clear_cache: false,
281        };
282
283        display_banner(&args);
284    }
285
286    #[test]
287    fn test_args_parsing() {
288        // This test verifies the Args structure can be created
289        let args = Args {
290            path: PathBuf::from("test.md"),
291            verbose: true,
292            clear_cache: false,
293        };
294
295        assert_eq!(args.path, PathBuf::from("test.md"));
296        assert!(args.verbose);
297    }
298}