watchdiff_tui/
cli.rs

1use std::path::PathBuf;
2use clap::{Parser, ValueEnum};
3use crate::diff::DiffAlgorithmType;
4
5#[derive(Parser)]
6#[command(name = "watchdiff")]
7#[command(author = "WatchDiff Team")]
8#[command(version = "0.1.0")]
9#[command(about = "A high-performance file watcher with beautiful TUI showing real-time diffs")]
10#[command(long_about = "WatchDiff monitors file changes in real-time, respects .gitignore patterns, and displays beautiful diffs in a terminal user interface. Perfect for development workflow monitoring.")]
11pub struct Cli {
12    /// Directory to watch for changes
13    #[arg(value_name = "PATH", help = "Path to watch (defaults to current directory)")]
14    pub path: Option<PathBuf>,
15
16    /// Watch mode - how to handle file events
17    #[arg(short, long, default_value = "auto", help = "File watching mode")]
18    pub mode: WatchMode,
19
20    /// Maximum number of events to keep in memory
21    #[arg(long, default_value = "1000", help = "Maximum events to store")]
22    pub max_events: usize,
23
24    /// Enable verbose logging
25    #[arg(short, long, help = "Enable verbose output")]
26    pub verbose: bool,
27
28    /// Disable colors in output
29    #[arg(long, help = "Disable colored output")]
30    pub no_color: bool,
31
32    /// Show only specific file types
33    #[arg(long, value_delimiter = ',', help = "File extensions to watch (e.g., rs,py,js)")]
34    pub extensions: Option<Vec<String>>,
35
36    /// Ignore additional patterns beyond .gitignore
37    #[arg(long, value_delimiter = ',', help = "Additional patterns to ignore")]
38    pub ignore: Option<Vec<String>>,
39
40    /// Diff context lines
41    #[arg(long, default_value = "3", help = "Number of context lines in diffs")]
42    pub context: usize,
43
44    /// Output format for non-TUI mode
45    #[arg(long, default_value = "tui", help = "Output format")]
46    pub output: OutputFormat,
47
48    /// Polling interval in milliseconds (for polling mode)
49    #[arg(long, default_value = "1000", help = "Polling interval in ms")]
50    pub poll_interval: u64,
51    
52    /// Diff algorithm to use
53    #[arg(long, default_value = "myers", help = "Diff algorithm (myers, patience, lcs)")]
54    pub algorithm: DiffAlgorithmType,
55    
56    /// Export patches to directory (TUI mode only)
57    #[arg(long, help = "Export patches to specified directory")]
58    pub export_dir: Option<PathBuf>,
59}
60
61#[derive(Debug, Clone, ValueEnum)]
62pub enum WatchMode {
63    /// Automatic detection (native events with polling fallback)
64    Auto,
65    /// Use native file system events
66    Native,
67    /// Use polling-based watching
68    Polling,
69}
70
71#[derive(Debug, Clone, ValueEnum)]
72pub enum OutputFormat {
73    /// Terminal user interface (default)
74    Tui,
75    /// JSON output for scripting
76    Json,
77    /// Plain text output
78    Text,
79    /// Compact single-line format
80    Compact,
81}
82
83impl Cli {
84    pub fn get_watch_path(&self) -> PathBuf {
85        self.path.clone().unwrap_or_else(|| {
86            std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
87        })
88    }
89
90    pub fn should_watch_extension(&self, path: &std::path::Path) -> bool {
91        if let Some(ref extensions) = self.extensions {
92            if let Some(ext) = path.extension().and_then(|s| s.to_str()) {
93                extensions.iter().any(|e| e.eq_ignore_ascii_case(ext))
94            } else {
95                false
96            }
97        } else {
98            true // Watch all files if no extensions specified
99        }
100    }
101
102    pub fn get_ignore_patterns(&self) -> Vec<String> {
103        self.ignore.clone().unwrap_or_default()
104    }
105
106    pub fn setup_logging(&self) {
107        let level = if self.verbose {
108            tracing::Level::DEBUG
109        } else {
110            tracing::Level::INFO
111        };
112
113        tracing_subscriber::fmt()
114            .with_max_level(level)
115            .with_target(false)
116            .with_thread_ids(false)
117            .with_file(false)
118            .with_line_number(false)
119            .init();
120    }
121
122    pub fn validate(&self) -> Result<(), String> {
123        let path = self.get_watch_path();
124        
125        if !path.exists() {
126            return Err(format!("Path does not exist: {}", path.display()));
127        }
128
129        if !path.is_dir() {
130            return Err(format!("Path is not a directory: {}", path.display()));
131        }
132
133        if self.max_events == 0 {
134            return Err("Max events must be greater than 0".to_string());
135        }
136
137        if self.poll_interval == 0 {
138            return Err("Poll interval must be greater than 0".to_string());
139        }
140
141        Ok(())
142    }
143}
144
145impl Default for Cli {
146    fn default() -> Self {
147        Self {
148            path: None,
149            mode: WatchMode::Auto,
150            max_events: 1000,
151            verbose: false,
152            no_color: false,
153            extensions: None,
154            ignore: None,
155            context: 3,
156            output: OutputFormat::Tui,
157            poll_interval: 1000,
158            algorithm: DiffAlgorithmType::Myers,
159            export_dir: None,
160        }
161    }
162}