use crate::context::queue::GenerationConfig;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Instant;
use crate::telemetry::ProgressRuntime;
#[derive(Clone)]
pub struct WatchConfig {
pub workspace_root: PathBuf,
pub debounce_ms: u64,
pub batch_window_ms: u64,
pub max_batch_size: usize,
pub ignore_patterns: Vec<String>,
pub max_queue_size: usize,
pub auto_create_frames: bool,
pub frame_batch_size: usize,
pub auto_generate_frames: bool,
pub generation_config: Option<GenerationConfig>,
pub session_id: Option<String>,
pub progress: Option<Arc<ProgressRuntime>>,
}
impl Default for WatchConfig {
fn default() -> Self {
Self {
workspace_root: PathBuf::from("."),
debounce_ms: 100,
batch_window_ms: 50,
max_batch_size: 100,
ignore_patterns: vec![
"**/.git/**".to_string(),
"**/.meld/**".to_string(),
"**/target/**".to_string(),
"**/node_modules/**".to_string(),
"**/.DS_Store".to_string(),
"**/*.swp".to_string(),
"**/*.tmp".to_string(),
],
max_queue_size: 10000,
auto_create_frames: true,
frame_batch_size: 50,
auto_generate_frames: false,
generation_config: None,
session_id: None,
progress: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ChangeEvent {
Created(PathBuf),
Modified(PathBuf),
Removed(PathBuf),
Renamed { from: PathBuf, to: PathBuf },
}
pub(crate) struct EventBatcher {
config: WatchConfig,
pending_events: HashMap<PathBuf, ChangeEvent>,
last_event_time: HashMap<PathBuf, Instant>,
}
impl EventBatcher {
pub(crate) fn new(config: WatchConfig) -> Self {
Self {
config,
pending_events: HashMap::new(),
last_event_time: HashMap::new(),
}
}
pub(crate) fn add_event(&mut self, event: ChangeEvent) -> bool {
let path = match &event {
ChangeEvent::Created(p) | ChangeEvent::Modified(p) | ChangeEvent::Removed(p) => {
p.clone()
}
ChangeEvent::Renamed { to, .. } => to.clone(),
};
if self.should_ignore(&path) {
return false;
}
let now = Instant::now();
let debounce_window = std::time::Duration::from_millis(self.config.debounce_ms);
if let Some(last_time) = self.last_event_time.get(&path) {
if now.duration_since(*last_time) < debounce_window {
self.pending_events.insert(path.clone(), event);
return false;
}
}
self.pending_events.insert(path.clone(), event);
self.last_event_time.insert(path, now);
self.pending_events.len() >= self.config.max_batch_size
}
pub(crate) fn take_batch(&mut self) -> Vec<ChangeEvent> {
let events: Vec<_> = self.pending_events.values().cloned().collect();
self.pending_events.clear();
self.last_event_time.clear();
events
}
fn should_ignore(&self, path: &Path) -> bool {
let path_str = path.to_string_lossy();
for pattern in &self.config.ignore_patterns {
if self.matches_pattern(&path_str, pattern) {
return true;
}
}
false
}
fn matches_pattern(&self, path: &str, pattern: &str) -> bool {
let path_normalized = path.replace('\\', "/");
let pattern_normalized = pattern.replace('\\', "/");
if pattern_normalized.contains("**") {
let parts: Vec<&str> = pattern_normalized.split("**").collect();
if parts.len() == 2 {
let prefix = parts[0];
let suffix = parts[1];
if prefix.is_empty() {
return path_normalized.contains(suffix);
} else if suffix.is_empty() {
return path_normalized.starts_with(prefix);
} else {
return path_normalized.starts_with(prefix) && path_normalized.contains(suffix);
}
}
}
if pattern_normalized.contains('*') {
let parts: Vec<&str> = pattern_normalized.split('*').collect();
if parts.len() == 2 {
return path_normalized.starts_with(parts[0]) && path_normalized.contains(parts[1]);
}
}
path_normalized == pattern_normalized || path_normalized.contains(&pattern_normalized)
}
}