use std::path::{Path, PathBuf};
use std::sync::mpsc;
use notify::{Event, EventKind, RecommendedWatcher, Watcher};
use crate::tcss::ast::Stylesheet;
use crate::tcss::error::TcssError;
use crate::tcss::parser::parse_stylesheet;
use crate::tcss::theme::{Theme, extract_themes};
use crate::tcss::variable::VariableMap;
#[derive(Clone, Debug)]
pub struct StylesheetLoader {
stylesheet: Stylesheet,
globals: VariableMap,
themes: Vec<Theme>,
path: Option<PathBuf>,
generation: u64,
}
#[derive(Clone, Debug)]
pub enum StylesheetEvent {
Reloaded {
generation: u64,
},
Error(String),
}
impl StylesheetLoader {
pub fn new() -> Self {
Self {
stylesheet: Stylesheet::new(),
globals: VariableMap::new(),
themes: Vec::new(),
path: None,
generation: 0,
}
}
pub fn load_string(css: &str) -> Result<Self, TcssError> {
let stylesheet = parse_stylesheet(css)?;
let (globals, themes) = extract_themes(&stylesheet);
Ok(Self {
stylesheet,
globals,
themes,
path: None,
generation: 1,
})
}
pub fn load_file(path: &Path) -> Result<Self, TcssError> {
let css = std::fs::read_to_string(path).map_err(|e| TcssError::Parse(e.to_string()))?;
let stylesheet = parse_stylesheet(&css)?;
let (globals, themes) = extract_themes(&stylesheet);
Ok(Self {
stylesheet,
globals,
themes,
path: Some(path.to_path_buf()),
generation: 1,
})
}
pub fn reload(&mut self) -> Result<StylesheetEvent, TcssError> {
let path = self
.path
.as_ref()
.ok_or_else(|| TcssError::Parse("no file path set for reload".into()))?
.clone();
let css = std::fs::read_to_string(&path).map_err(|e| TcssError::Parse(e.to_string()))?;
let stylesheet = parse_stylesheet(&css)?;
let (globals, themes) = extract_themes(&stylesheet);
self.stylesheet = stylesheet;
self.globals = globals;
self.themes = themes;
self.generation += 1;
Ok(StylesheetEvent::Reloaded {
generation: self.generation,
})
}
pub fn reload_string(&mut self, css: &str) -> Result<StylesheetEvent, TcssError> {
let stylesheet = parse_stylesheet(css)?;
let (globals, themes) = extract_themes(&stylesheet);
self.stylesheet = stylesheet;
self.globals = globals;
self.themes = themes;
self.generation += 1;
Ok(StylesheetEvent::Reloaded {
generation: self.generation,
})
}
pub fn stylesheet(&self) -> &Stylesheet {
&self.stylesheet
}
pub fn globals(&self) -> &VariableMap {
&self.globals
}
pub fn themes(&self) -> &[Theme] {
&self.themes
}
pub fn generation(&self) -> u64 {
self.generation
}
pub fn path(&self) -> Option<&Path> {
self.path.as_deref()
}
}
impl Default for StylesheetLoader {
fn default() -> Self {
Self::new()
}
}
pub fn watch_stylesheet(
path: &Path,
) -> Result<(RecommendedWatcher, mpsc::Receiver<StylesheetEvent>), TcssError> {
let (tx, rx) = mpsc::channel();
let watched_path = path.to_path_buf();
let watcher_tx = tx.clone();
let mut watcher = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
match res {
Ok(event) => {
if matches!(event.kind, EventKind::Modify(_) | EventKind::Create(_)) {
match std::fs::read_to_string(&watched_path) {
Ok(css) => match parse_stylesheet(&css) {
Ok(_) => {
let _ = watcher_tx.send(StylesheetEvent::Reloaded {
generation: 0, });
}
Err(e) => {
let _ = watcher_tx.send(StylesheetEvent::Error(e.to_string()));
}
},
Err(e) => {
let _ = watcher_tx.send(StylesheetEvent::Error(e.to_string()));
}
}
}
}
Err(e) => {
let _ = watcher_tx.send(StylesheetEvent::Error(e.to_string()));
}
}
})
.map_err(|e| TcssError::Parse(format!("failed to create watcher: {e}")))?;
watcher
.watch(path, notify::RecursiveMode::NonRecursive)
.map_err(|e| TcssError::Parse(format!("failed to watch path: {e}")))?;
Ok((watcher, rx))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_loader() {
let loader = StylesheetLoader::new();
assert!(loader.stylesheet().is_empty());
assert!(loader.globals().is_empty());
assert!(loader.themes().is_empty());
assert_eq!(loader.generation(), 0);
assert!(loader.path().is_none());
}
#[test]
fn load_from_string() {
let css = "Label { color: red; }";
let loader = StylesheetLoader::load_string(css);
assert!(loader.is_ok());
let loader = match loader {
Ok(l) => l,
Err(_) => unreachable!(),
};
assert_eq!(loader.stylesheet().len(), 1);
assert_eq!(loader.generation(), 1);
}
#[test]
fn load_extracts_globals() {
let css = ":root { $fg: white; $bg: #1e1e2e; }";
let loader = StylesheetLoader::load_string(css);
assert!(loader.is_ok());
let loader = match loader {
Ok(l) => l,
Err(_) => unreachable!(),
};
assert_eq!(loader.globals().len(), 2);
assert!(loader.globals().contains("fg"));
}
#[test]
fn load_extracts_themes() {
let css = r#"
.dark { $fg: white; }
.light { $fg: #4c4f69; }
"#;
let loader = StylesheetLoader::load_string(css);
assert!(loader.is_ok());
let loader = match loader {
Ok(l) => l,
Err(_) => unreachable!(),
};
assert_eq!(loader.themes().len(), 2);
}
#[test]
fn generation_increments() {
let css1 = "Label { color: red; }";
let result = StylesheetLoader::load_string(css1);
assert!(result.is_ok());
let mut loader = match result {
Ok(l) => l,
Err(_) => unreachable!(),
};
assert_eq!(loader.generation(), 1);
let css2 = "Label { color: blue; }";
let event = loader.reload_string(css2);
assert!(event.is_ok());
assert_eq!(loader.generation(), 2);
}
#[test]
fn reload_from_string() {
let css1 = "Label { color: red; }";
let result = StylesheetLoader::load_string(css1);
assert!(result.is_ok());
let mut loader = match result {
Ok(l) => l,
Err(_) => unreachable!(),
};
let css2 = r#"
:root { $fg: white; }
.dark { $bg: #1e1e2e; }
Label { color: $fg; }
"#;
let event = loader.reload_string(css2);
assert!(event.is_ok());
assert_eq!(loader.globals().len(), 1);
assert_eq!(loader.themes().len(), 1);
assert_eq!(loader.generation(), 2);
}
#[test]
fn loader_accessors() {
let css = r#"
:root { $fg: white; }
.dark { $bg: black; }
Label { color: $fg; }
"#;
let result = StylesheetLoader::load_string(css);
assert!(result.is_ok());
let loader = match result {
Ok(l) => l,
Err(_) => unreachable!(),
};
assert!(!loader.stylesheet().is_empty());
assert!(!loader.globals().is_empty());
assert!(!loader.themes().is_empty());
assert_eq!(loader.generation(), 1);
assert!(loader.path().is_none());
}
#[test]
fn load_file_not_found() {
let result = StylesheetLoader::load_file(Path::new("/nonexistent/file.tcss"));
assert!(result.is_err());
}
#[test]
fn reload_without_path_errors() {
let css = "Label { color: red; }";
let result = StylesheetLoader::load_string(css);
assert!(result.is_ok());
let mut loader = match result {
Ok(l) => l,
Err(_) => unreachable!(),
};
let result = loader.reload();
assert!(result.is_err());
}
}