use mcp_host::managers::progress::ProgressTracker;
use mcp_host::protocol::types::ClientInfo;
use notify::RecommendedWatcher;
use serde::Deserialize;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::time::Instant;
use super::utils::log_to_file;
pub const CONFIG_FILE: &str = ".dictate.toml";
pub const WATCHER_CHECK_INTERVAL_SECS: u64 = 10;
pub const STALINT_CHECK_TIMEOUT_SECS: u64 = 60;
pub const DEFAULT_STALINT_LIMIT: usize = 10;
#[derive(Debug, Clone, Deserialize)]
pub struct LinterConfig {
pub command: String,
}
#[derive(Debug, Default, Clone, Deserialize)]
pub struct DictateConfig {
#[serde(default)]
pub decree: HashMap<String, DecreeSettings>,
}
#[derive(Debug, Default, Clone, Deserialize)]
pub struct DecreeSettings {
#[serde(default)]
pub linter: Option<LinterConfig>,
}
pub fn load_config() -> Option<DictateConfig> {
let cwd = std::env::current_dir().ok()?;
let config_path = cwd.join(CONFIG_FILE);
if !config_path.exists() {
return None;
}
let content = std::fs::read_to_string(&config_path).ok()?;
toml::from_str(&content).ok()
}
#[allow(clippy::struct_excessive_bools)]
pub struct ServerState {
pub paths: HashSet<String>,
pub dirty: bool,
pub last_check: Instant,
pub is_watching: bool,
#[allow(dead_code)]
pub watcher: Option<RecommendedWatcher>,
pub client: ClientInfo,
pub can_write: bool,
pub stalint_paths: Vec<String>,
pub config: Option<DictateConfig>,
pub config_dirty: bool,
pub progress_tracker: Arc<ProgressTracker>,
}
impl Default for ServerState {
fn default() -> Self {
let (tx, _rx) = tokio::sync::mpsc::unbounded_channel();
Self::new(tx)
}
}
impl ServerState {
pub fn new(
notif_tx: tokio::sync::mpsc::UnboundedSender<mcp_host::transport::JsonRpcNotification>,
) -> Self {
Self {
paths: HashSet::new(),
dirty: false,
last_check: Instant::now(),
is_watching: false,
watcher: None,
client: ClientInfo::default(),
can_write: true,
stalint_paths: Vec::new(),
config: None,
config_dirty: false,
progress_tracker: Arc::new(ProgressTracker::new(notif_tx)),
}
}
pub fn reload_config(&mut self) {
let old_config = self.config.take();
self.config = load_config();
if let Some(ref cfg) = self.config {
log_to_file(&format!(
"Reloaded config with {} decrees",
cfg.decree.len()
));
} else {
log_to_file("Config removed or invalid");
}
let old_has_linter = old_config
.as_ref()
.is_some_and(|c| c.decree.values().any(|d| d.linter.is_some()));
let new_has_linter = self
.config
.as_ref()
.is_some_and(|c| c.decree.values().any(|d| d.linter.is_some()));
if old_has_linter != new_has_linter {
log_to_file(&format!(
"Linter availability changed: {old_has_linter} -> {new_has_linter}"
));
}
}
pub fn ensure_config_loaded(&mut self) {
if self.config.is_some() {
return;
}
let config = load_config();
if let Some(ref cfg) = config {
log_to_file(&format!("Loaded config with {} decrees", cfg.decree.len()));
}
self.config = config;
}
}