use crate::cli;
use crate::common::config::Config;
use crate::common::monitoring::{MonitorConfig, MonitoredFolder, RunStatus};
use crate::core::sorter;
use crate::rules::rules_file::RulesFile;
use anyhow::Result;
use chrono::Local;
use clap::{Args, Subcommand};
use colored::Colorize;
use std::path::PathBuf;
use std::thread;
use std::time::Duration;
#[derive(Args)]
#[command(about = "⏰ Manage background folder monitoring and automatic sorting")]
pub struct MonitorArgs {
#[clap(subcommand)]
pub command: MonitorCommands,
}
#[derive(Subcommand)]
pub enum MonitorCommands {
Add(AddMonitorArgs),
Remove(RemoveMonitorArgs),
List,
Status,
Daemon(DaemonArgs),
}
#[derive(Args)]
pub struct AddMonitorArgs {
#[arg(value_name = "PATH", help = "Path to the folder to monitor")]
pub path: String,
#[arg(
short,
long,
default_value = "3600",
help = "Interval in seconds between automatic sorts (default: 3600 = 1 hour)"
)]
pub interval: u64,
#[arg(
long,
help = "Comma-separated list of rule IDs to execute (omit for all rules)"
)]
pub rules: Option<String>,
}
#[derive(Args)]
pub struct RemoveMonitorArgs {
#[arg(value_name = "PATH", help = "Path to the folder to stop monitoring")]
pub path: String,
}
#[derive(Args)]
#[command(about = "Manage the monitoring daemon")]
pub struct DaemonArgs {
#[clap(subcommand)]
pub command: DaemonCommands,
}
#[derive(Subcommand)]
pub enum DaemonCommands {
Run(DaemonRunArgs),
Stop,
Status,
}
#[derive(Args)]
pub struct DaemonRunArgs {
#[arg(
short,
long,
default_value = "60",
help = "How often to check for due folders in seconds (default: 60)"
)]
pub check_interval: u64,
}
pub fn run(args: &MonitorArgs) -> Result<()> {
match &args.command {
MonitorCommands::Add(add_args) => add_folder(add_args),
MonitorCommands::Remove(remove_args) => remove_folder(remove_args),
MonitorCommands::List => list_folders(),
MonitorCommands::Status => show_status(),
MonitorCommands::Daemon(daemon_args) => run_daemon_command(daemon_args),
}
}
fn run_daemon_command(args: &DaemonArgs) -> Result<()> {
match &args.command {
DaemonCommands::Run(run_args) => run_daemon(run_args),
DaemonCommands::Stop => stop_daemon(),
DaemonCommands::Status => check_daemon_status(),
}
}
fn add_folder(args: &AddMonitorArgs) -> Result<()> {
cli::info(&format!("Adding folder to monitoring: {}", args.path));
let path = PathBuf::from(&args.path);
let rule_filter = args.rules.as_ref().map(|r| {
r.split(',')
.map(|s| s.trim().to_string())
.collect::<Vec<_>>()
});
let mut monitor_config = MonitorConfig::load()?;
monitor_config.add_folder(path.clone(), args.interval, rule_filter)?;
cli::success(&format!(
"Folder '{}' added to monitoring (interval: {} seconds)",
path.display(),
args.interval
));
log::info!(
"Added folder '{}' to monitoring with interval {} seconds",
path.display(),
args.interval
);
Ok(())
}
fn remove_folder(args: &RemoveMonitorArgs) -> Result<()> {
cli::info(&format!(
"Removing folder from monitoring: {}",
args.path
));
let path = PathBuf::from(&args.path);
let mut monitor_config = MonitorConfig::load()?;
monitor_config.remove_folder(&path)?;
cli::success(&format!(
"Folder '{}' removed from monitoring",
path.display()
));
log::info!("Removed folder '{}' from monitoring", path.display());
Ok(())
}
fn list_folders() -> Result<()> {
let monitor_config = MonitorConfig::load()?;
if monitor_config.folders.is_empty() {
cli::warning("No folders are currently being monitored");
cli::info("Use 'tooka monitor add <path>' to add a folder");
return Ok(());
}
cli::header("📋 Monitored Folders");
for (idx, folder) in monitor_config.folders.iter().enumerate() {
println!("{}", format!("Folder {}", idx + 1).bright_cyan().bold());
println!(" {} {}", "Path:".bright_white(), folder.path.display());
println!(
" {} {} seconds ({} minutes)",
"Interval:".bright_white(),
folder.interval_seconds,
folder.interval_seconds / 60
);
if let Some(rules) = &folder.rule_filter {
println!(
" {} {}",
"Rules:".bright_white(),
rules.join(", ").bright_yellow()
);
} else {
println!(" {} {}", "Rules:".bright_white(), "All rules".bright_yellow());
}
println!();
}
println!(
"{}",
format!("Total: {} folder(s)", monitor_config.folders.len())
.bright_green()
.bold()
);
Ok(())
}
fn show_status() -> Result<()> {
let monitor_config = MonitorConfig::load()?;
if monitor_config.folders.is_empty() {
cli::warning("No folders are currently being monitored");
cli::info("Use 'tooka monitor add <path>' to add a folder");
return Ok(());
}
cli::header("📊 Monitoring Status");
for (idx, folder) in monitor_config.folders.iter().enumerate() {
println!("{}", format!("Folder {}", idx + 1).bright_cyan().bold());
println!(" {} {}", "Path:".bright_white(), folder.path.display());
if let Some(last_run) = folder.last_run {
let local_time = last_run.with_timezone(&Local);
println!(
" {} {}",
"Last Run:".bright_white(),
local_time.format("%Y-%m-%d %H:%M:%S")
);
} else {
println!(" {} {}", "Last Run:".bright_white(), "Never".bright_black());
}
let status_str = match &folder.last_status {
RunStatus::NotRun => "Not run yet".bright_black().to_string(),
RunStatus::Success { files_processed } => {
format!("✓ Success ({} files)", files_processed).green().to_string()
}
RunStatus::Failed { error } => format!("✗ Failed: {}", error).red().to_string(),
};
println!(" {} {}", "Status:".bright_white(), status_str);
if let Some(seconds) = monitor_config.time_until_next_run(&folder.path) {
let minutes = seconds / 60;
let hours = minutes / 60;
let remaining_minutes = minutes % 60;
let time_str = if hours > 0 {
format!("{} hours, {} minutes", hours, remaining_minutes)
} else {
format!("{} minutes", minutes)
};
println!(
" {} {}",
"Next Run:".bright_white(),
time_str.bright_yellow()
);
} else if monitor_config.is_due(folder) {
println!(
" {} {}",
"Next Run:".bright_white(),
"Due now!".bright_green().bold()
);
} else {
println!(
" {} {}",
"Next Run:".bright_white(),
format!(
"In {} seconds",
folder.interval_seconds
)
.bright_yellow()
);
}
println!();
}
Ok(())
}
fn run_daemon(args: &DaemonRunArgs) -> Result<()> {
if MonitorConfig::is_daemon_running() {
cli::error("Monitoring daemon is already running");
cli::info("Use 'tooka monitor daemon stop' to stop the running daemon first");
return Ok(());
}
cli::header("⏰ Starting Monitoring Daemon");
cli::info("The daemon will run continuously and sort folders at their scheduled intervals");
cli::info("Press Ctrl+C to stop the daemon");
println!();
log::info!("Starting monitoring daemon with check interval {} seconds", args.check_interval);
let mut monitor_config = MonitorConfig::load()?;
let config = Config::load()?;
if monitor_config.folders.is_empty() {
cli::warning("No folders are currently being monitored");
cli::info("Use 'tooka monitor add <path>' to add a folder");
return Ok(());
}
MonitorConfig::write_pid()?;
let running = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
let r = running.clone();
if let Err(e) = ctrlc::set_handler(move || {
r.store(false, std::sync::atomic::Ordering::SeqCst);
}) {
cli::warning(&format!("Failed to set Ctrl-C handler: {}", e));
log::warn!("Failed to set Ctrl-C handler: {}", e);
}
for folder in &monitor_config.folders {
println!(
" 📁 {} (every {} seconds)",
folder.path.display().to_string().bright_cyan(),
folder.interval_seconds
);
}
println!();
while running.load(std::sync::atomic::Ordering::SeqCst) {
monitor_config = MonitorConfig::load()?;
for folder in monitor_config.folders.clone() {
if monitor_config.is_due(&folder) {
run_folder_sort(&mut monitor_config, &folder, &config)?;
}
}
thread::sleep(Duration::from_secs(args.check_interval));
}
println!();
cli::info("Stopping monitoring daemon...");
MonitorConfig::remove_pid()?;
cli::success("Daemon stopped successfully");
log::info!("Monitoring daemon stopped");
Ok(())
}
fn run_folder_sort(
monitor_config: &mut MonitorConfig,
folder: &MonitoredFolder,
_config: &Config,
) -> Result<()> {
let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S");
cli::info(&format!(
"[{}] Sorting folder: {}",
timestamp,
folder.path.display()
));
log::info!("Running scheduled sort for folder: {}", folder.path.display());
let rules_file = match RulesFile::load() {
Ok(rf) => rf,
Err(e) => {
let error_msg = format!("Failed to load rules: {}", e);
cli::error(&error_msg);
monitor_config.update_status(&folder.path, RunStatus::Failed { error: error_msg })?;
return Ok(());
}
};
let optimized_rules = match rules_file.optimized_with_filter(folder.rule_filter.as_deref()) {
Ok(rules) => rules,
Err(e) => {
let error_msg = format!("Failed to optimize rules: {}", e);
cli::error(&error_msg);
monitor_config.update_status(&folder.path, RunStatus::Failed { error: error_msg })?;
return Ok(());
}
};
let files = match sorter::collect_files(&folder.path) {
Ok(f) => f,
Err(e) => {
let error_msg = format!("Failed to collect files: {}", e);
cli::error(&error_msg);
monitor_config.update_status(&folder.path, RunStatus::Failed { error: error_msg })?;
return Ok(());
}
};
match sorter::sort_files(&files, &folder.path, &optimized_rules, false, Option::<fn()>::None) {
Ok(results) => {
let files_processed = results.len();
cli::success(&format!(
"[{}] Sorted {} file(s) in {}",
timestamp,
files_processed,
folder.path.display()
));
log::info!(
"Successfully sorted {} files in folder: {}",
files_processed,
folder.path.display()
);
monitor_config.update_status(
&folder.path,
RunStatus::Success { files_processed },
)?;
}
Err(e) => {
let error_msg = format!("Sorting failed: {}", e);
cli::error(&format!("[{}] {}", timestamp, &error_msg));
log::error!("Failed to sort folder {}: {}", folder.path.display(), e);
monitor_config.update_status(&folder.path, RunStatus::Failed { error: error_msg })?;
}
}
Ok(())
}
fn stop_daemon() -> Result<()> {
cli::info("🛑 Stopping monitoring daemon...");
if !MonitorConfig::is_daemon_running() {
cli::warning("Monitoring daemon is not running");
return Ok(());
}
match MonitorConfig::read_pid()? {
Some(pid) => {
log::info!("Stopping monitoring daemon with PID {}", pid);
#[cfg(unix)]
{
use std::process::Command;
let result = Command::new("kill")
.arg("-TERM")
.arg(pid.to_string())
.status();
match result {
Ok(status) if status.success() => {
thread::sleep(Duration::from_millis(500));
if MonitorConfig::is_daemon_running() {
cli::warning("Daemon still running, sending stronger signal...");
let _ = Command::new("kill")
.arg("-KILL")
.arg(pid.to_string())
.status();
thread::sleep(Duration::from_millis(200));
}
let _ = MonitorConfig::remove_pid();
cli::success("Monitoring daemon stopped successfully");
log::info!("Monitoring daemon stopped");
}
Ok(_) => {
cli::error(&format!("Failed to stop daemon with PID {}", pid));
let _ = MonitorConfig::remove_pid();
}
Err(e) => {
cli::error(&format!("Failed to send signal to daemon: {}", e));
let _ = MonitorConfig::remove_pid();
}
}
}
#[cfg(not(unix))]
{
cli::warning("Stopping daemon on Windows is not yet implemented");
cli::info("Please stop the daemon manually using Task Manager or Ctrl+C");
}
}
None => {
cli::warning("No PID file found for running daemon");
}
}
Ok(())
}
fn check_daemon_status() -> Result<()> {
if MonitorConfig::is_daemon_running() {
if let Ok(Some(pid)) = MonitorConfig::read_pid() {
cli::success(&format!("Monitoring daemon is running (PID: {})", pid));
log::info!("Monitoring daemon is running with PID {}", pid);
} else {
cli::success("Monitoring daemon is running");
}
} else {
cli::info("Monitoring daemon is not running");
log::info!("Monitoring daemon is not running");
}
Ok(())
}