use crate::style::stylesheet::global_stylesheet_manager;
use std::fs;
use std::time::{Duration, SystemTime};
pub struct CssWatcher {
path: String,
last_modified: Option<SystemTime>,
sheet_name: String,
poll_interval: Duration,
last_poll: SystemTime,
}
impl CssWatcher {
pub fn new(path: &str, sheet_name: &str) -> Self {
Self {
path: path.to_string(),
last_modified: None,
sheet_name: sheet_name.to_string(),
poll_interval: Duration::from_millis(500),
last_poll: SystemTime::now(),
}
}
pub fn set_poll_interval(&mut self, ms: u64) {
self.poll_interval = Duration::from_millis(ms);
}
pub fn poll(&mut self) -> Result<bool, String> {
let now = SystemTime::now();
let elapsed = now.duration_since(self.last_poll).unwrap_or_default();
if elapsed < self.poll_interval {
return Ok(false);
}
self.last_poll = now;
let metadata = fs::metadata(&self.path).map_err(|e| {
format!("CssWatcher: failed to read metadata for '{}': {}", self.path, e)
})?;
let modified = metadata.modified().map_err(|e| {
format!("CssWatcher: failed to get modified time for '{}': {}", self.path, e)
})?;
if self.last_modified.is_some_and(|last| modified <= last) {
return Ok(false);
}
let css = fs::read_to_string(&self.path)
.map_err(|e| format!("CssWatcher: failed to read '{}': {}", self.path, e))?;
let mut mgr = global_stylesheet_manager();
mgr.register(&self.sheet_name, &css, 0);
self.last_modified = Some(modified);
Ok(true)
}
pub fn reload(&mut self) -> Result<(), String> {
let metadata = fs::metadata(&self.path).map_err(|e| {
format!("CssWatcher: failed to read metadata for '{}': {}", self.path, e)
})?;
let modified = metadata.modified().map_err(|e| {
format!("CssWatcher: failed to get modified time for '{}': {}", self.path, e)
})?;
let css = fs::read_to_string(&self.path)
.map_err(|e| format!("CssWatcher: failed to read '{}': {}", self.path, e))?;
let mut mgr = global_stylesheet_manager();
mgr.register(&self.sheet_name, &css, 0);
self.last_modified = Some(modified);
Ok(())
}
pub fn path(&self) -> &str {
&self.path
}
}
impl Default for CssWatcher {
fn default() -> Self {
Self::new("style.css", "main")
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn css_watcher_poll_not_yet_ready() {
let mut watcher = CssWatcher::new("test.css", "test");
watcher.set_poll_interval(10_000);
let result = watcher.poll();
assert_eq!(result, Ok(false), "should skip poll when interval not elapsed");
}
#[test]
fn css_watcher_poll_nonexistent_file_returns_error() {
let mut watcher = CssWatcher::new("/tmp/nonexistent-file-839201.css", "test");
watcher.set_poll_interval(0);
let result = watcher.poll();
assert!(result.is_err(), "polling a non-existent file should return Err: got {:?}", result);
}
#[test]
fn css_watcher_reload_nonexistent_file_returns_error() {
let mut watcher = CssWatcher::new("/tmp/nonexistent-file-839202.css", "test");
let result = watcher.reload();
assert!(
result.is_err(),
"force-reloading a non-existent file should return Err: got {:?}",
result
);
}
#[test]
fn css_watcher_detects_file_change() {
let mut tmp = NamedTempFile::new().expect("failed to create temp file");
let path = tmp.path().to_string_lossy().to_string();
write!(tmp, "/* initial */").expect("write initial content");
let mut watcher = CssWatcher::new(&path, "test-watcher");
watcher.set_poll_interval(0);
let first = watcher.poll().expect("first poll should succeed");
assert!(first, "first poll should load initial content");
{
let mgr = global_stylesheet_manager();
assert!(mgr.len() >= 1, "manager should have at least 1 sheet after first poll");
}
write!(tmp, "/* modified */").expect("write modified content");
tmp.flush().expect("flush temp file");
let changed = watcher.poll().expect("poll should succeed");
assert!(changed, "poll should detect file modification");
{
let mut mgr = global_stylesheet_manager();
mgr.unregister("test-watcher");
}
}
}