use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::time::Duration;
use notify_debouncer_mini::{
new_debouncer,
notify::{self, RecommendedWatcher, RecursiveMode},
DebounceEventResult, Debouncer,
};
use tao::event_loop::EventLoopProxy;
use crate::template::{self, InjectData, TemplateRef};
const DEBOUNCE_MS: u64 = 100;
#[derive(Debug, Clone)]
pub enum UserEvent {
Reload(String),
}
pub struct WatchContext {
pub source: PathBuf,
pub template: TemplateRef,
pub params: HashMap<String, String>,
pub raw_mode: bool,
}
pub fn spawn_watcher(
ctx: WatchContext,
proxy: EventLoopProxy<UserEvent>,
) -> notify::Result<Debouncer<RecommendedWatcher>> {
let target = std::fs::canonicalize(&ctx.source).unwrap_or_else(|_| ctx.source.clone());
let watch_dir = target
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."));
let handler_ctx = HandlerContext {
target,
source_for_render: ctx.source.clone(),
template: ctx.template,
params: ctx.params,
raw_mode: ctx.raw_mode,
proxy,
};
let mut debouncer = new_debouncer(
Duration::from_millis(DEBOUNCE_MS),
move |result: DebounceEventResult| handle_events(&handler_ctx, result),
)?;
debouncer
.watcher()
.watch(&watch_dir, RecursiveMode::NonRecursive)?;
Ok(debouncer)
}
struct HandlerContext {
target: PathBuf,
source_for_render: PathBuf,
template: TemplateRef,
params: HashMap<String, String>,
raw_mode: bool,
proxy: EventLoopProxy<UserEvent>,
}
fn handle_events(ctx: &HandlerContext, result: DebounceEventResult) {
let events = match result {
Ok(ev) => ev,
Err(e) => {
eprintln!("tinyview: watch error: {e}");
return;
}
};
let touches_target = events.iter().any(|e| paths_match(&e.path, &ctx.target));
if !touches_target {
return;
}
let html = match recompose_html(ctx) {
Ok(h) => h,
Err(e) => {
eprintln!("tinyview: watch reload failed: {e}");
return;
}
};
if ctx.proxy.send_event(UserEvent::Reload(html)).is_err() {
}
}
fn paths_match(event_path: &Path, target: &Path) -> bool {
if event_path == target {
return true;
}
match std::fs::canonicalize(event_path) {
Ok(canon) => canon == *target,
Err(_) => false,
}
}
fn recompose_html(ctx: &HandlerContext) -> std::io::Result<String> {
let input = std::fs::read_to_string(&ctx.source_for_render)?;
if ctx.raw_mode {
return Ok(input);
}
let data = InjectData {
input: &input,
params: &ctx.params,
title: "tinyview",
path: Some(ctx.source_for_render.as_path()),
};
template::render(&ctx.template, &data).map_err(|e| {
std::io::Error::other(e.to_string())
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn paths_match_direct_equality() {
let p = PathBuf::from("/tmp/some-file.html");
assert!(paths_match(&p, &p));
}
#[test]
fn paths_match_canonicalizes() {
let dir = std::env::temp_dir();
let path = dir.join("tinyview_watch_paths_match.html");
std::fs::write(&path, b"x").expect("write tmp");
let canon = std::fs::canonicalize(&path).expect("canonicalize");
let weird = dir.join(".").join("tinyview_watch_paths_match.html");
assert!(paths_match(&weird, &canon));
let _ = std::fs::remove_file(&path);
}
}