mod config;
mod engine;
use clap::Parser as ClapParser;
use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
use std::fs;
use std::io::{self, Read};
use std::path::{Path, PathBuf};
use std::process::{self, Command};
use std::sync::mpsc::channel;
use std::thread;
use std::time::{Duration, Instant};
use chrono::Local;
#[derive(ClapParser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
input_file: Option<PathBuf>,
#[arg(short, long)]
output_file: Option<PathBuf>,
#[arg(long, requires("input_file"))]
watch: bool,
#[arg(long)]
on_finish: Option<String>,
}
fn process_file(
input_path: &Path,
output_path: &Option<PathBuf>,
on_finish_cmd: &Option<String>,
) -> Result<(), String> {
let input_content = fs::read_to_string(input_path)
.map_err(|e| format!("Error reading input file '{}': {}", input_path.display(), e))?;
let default_runners = config::default_runners();
let output_content =
engine::process_markdown(&input_content, &default_runners, Some(input_path))?;
if let Some(path) = output_path {
fs::write(path, &output_content)
.map_err(|e| format!("Error writing to output file '{}': {}", path.display(), e))?;
println!("✅ Successfully generated '{}'", path.display());
} else {
print!("{}", output_content);
}
if let Some(cmd) = on_finish_cmd {
run_post_command(cmd, input_path, output_path.as_deref())?;
}
Ok(())
}
fn run_post_command(
command_template: &str,
input_path: &Path,
output_path: Option<&Path>,
) -> Result<(), String> {
let output_path_str = output_path.and_then(|p| p.to_str()).unwrap_or("");
let input_path_str = input_path.to_str().unwrap_or("");
let command_to_run = command_template
.replace("{{input}}", input_path_str)
.replace("{{output}}", output_path_str);
println!("🚀 Running on-finish command: {}", command_to_run);
let mut parts = command_to_run.split_whitespace();
let command = parts.next().unwrap_or("");
if command.is_empty() {
return Err("The on-finish command was empty.".to_string());
}
let args: Vec<&str> = parts.collect();
let status = Command::new(command)
.args(args)
.status()
.map_err(|e| format!("Failed to execute on-finish command: {}", e))?;
if !status.success() {
eprintln!(
"⚠️ Warning: The on-finish command failed with exit code: {}",
status
);
}
Ok(())
}
fn main() {
let args = Args::parse();
if args.input_file.is_none() {
let mut buffer = String::new();
io::stdin().read_to_string(&mut buffer).unwrap();
let default_runners = config::default_runners();
match engine::process_markdown(&buffer, &default_runners, None) {
Ok(out) => print!("{}", out),
Err(e) => {
eprintln!("\n❌ Error: {}", e);
process::exit(1);
}
}
return;
}
let input_path = args.input_file.as_ref().unwrap();
if let Err(e) = process_file(input_path, &args.output_file, &args.on_finish) {
eprintln!("❌ Error: {}", e);
if !args.watch {
process::exit(1);
}
}
if !args.watch {
return;
}
println!(
"\n👀 Watching for changes in '{}'. Press Ctrl+C to exit.",
input_path.display()
);
let (tx, rx) = channel();
let mut watcher: RecommendedWatcher = Watcher::new(
tx,
Config::default().with_poll_interval(Duration::from_millis(500)),
)
.expect("Failed to create file watcher");
watcher
.watch(input_path.as_ref(), RecursiveMode::NonRecursive)
.expect("Failed to watch input file");
let debounce_duration = Duration::from_millis(200);
let mut last_event_time: Option<Instant> = None;
loop {
if let Ok(res) = rx.try_recv() {
match res {
Ok(event) if event.kind.is_modify() || event.kind.is_create() => {
last_event_time = Some(Instant::now());
}
Err(e) => eprintln!("watch error: {:?}", e),
_ => {} }
}
if let Some(last_event) = last_event_time {
if last_event.elapsed() >= debounce_duration {
let now = Local::now();
println!(
"\n🔄 [{}] Detected change, reprocessing '{}'...",
now.format("%H:%M:%S"),
input_path.display()
);
if let Err(e) = process_file(input_path, &args.output_file, &args.on_finish) {
eprintln!("❌ Error: {}", e);
}
last_event_time = None;
}
}
thread::sleep(Duration::from_millis(50));
}
}