1use clap::Parser;
7use colored::*;
8use std::path::PathBuf;
9
10#[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 #[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 #[arg(
33 short,
34 long,
35 help = "Enable verbose logging with detailed tracing information"
36 )]
37 pub verbose: bool,
38
39 #[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
48pub fn print_colored_help() {
50 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
165pub 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
181pub fn init_logging(verbose: bool) {
183 if verbose {
184 tracing_subscriber::fmt::init();
185 }
186}
187
188pub 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 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 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 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}