use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant, SystemTime};
const DEFAULT_POLL_INTERVAL: Duration = Duration::from_millis(250);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PolledChangeKind {
Modified,
Deleted,
Created,
}
#[derive(Debug, Clone)]
pub struct PolledChange {
pub path: PathBuf,
pub kind: PolledChangeKind,
}
pub struct FilesystemPoller {
mtimes: HashMap<PathBuf, SystemTime>,
poll_interval: Duration,
last_poll: Option<Instant>,
}
impl FilesystemPoller {
pub fn new(poll_interval: Duration) -> Self {
Self {
mtimes: HashMap::new(),
poll_interval,
last_poll: None,
}
}
pub fn with_default_interval() -> Self {
Self::new(DEFAULT_POLL_INTERVAL)
}
pub fn register_path<P: AsRef<Path>>(&mut self, path: P) -> std::io::Result<()> {
let path_ref = path.as_ref();
let modified = std::fs::metadata(path_ref)?.modified()?;
self.mtimes.insert(path_ref.to_path_buf(), modified);
Ok(())
}
pub fn track_expected<P: AsRef<Path>>(&mut self, path: P) {
self.mtimes
.insert(path.as_ref().to_path_buf(), SystemTime::UNIX_EPOCH);
}
pub fn deregister_path<P: AsRef<Path>>(&mut self, path: P) -> bool {
self.mtimes.remove(path.as_ref()).is_some()
}
pub fn watched_paths(&self) -> Vec<PathBuf> {
let mut paths: Vec<PathBuf> = self.mtimes.keys().cloned().collect();
paths.sort();
paths
}
pub fn watched_count(&self) -> usize {
self.mtimes.len()
}
pub fn poll(&mut self) -> Vec<PolledChange> {
if let Some(last) = self.last_poll {
if last.elapsed() < self.poll_interval {
return Vec::new();
}
}
self.last_poll = Some(Instant::now());
self.diff_against_disk()
}
pub fn force_poll(&mut self) -> Vec<PolledChange> {
self.last_poll = Some(Instant::now());
self.diff_against_disk()
}
fn diff_against_disk(&mut self) -> Vec<PolledChange> {
let mut ordered: Vec<PathBuf> = self.mtimes.keys().cloned().collect();
ordered.sort();
let mut changes = Vec::new();
let mut updates: Vec<(PathBuf, SystemTime)> = Vec::new();
let mut removals: Vec<PathBuf> = Vec::new();
for path in ordered {
let stored = match self.mtimes.get(&path) {
Some(stored) => *stored,
None => continue,
};
match std::fs::metadata(&path).and_then(|meta| meta.modified()) {
Ok(current) => {
if stored == SystemTime::UNIX_EPOCH {
changes.push(PolledChange {
path: path.clone(),
kind: PolledChangeKind::Created,
});
updates.push((path, current));
} else if current > stored {
changes.push(PolledChange {
path: path.clone(),
kind: PolledChangeKind::Modified,
});
updates.push((path, current));
}
}
Err(_) => {
if stored != SystemTime::UNIX_EPOCH {
changes.push(PolledChange {
path: path.clone(),
kind: PolledChangeKind::Deleted,
});
removals.push(path);
}
}
}
}
for (path, modified) in updates {
self.mtimes.insert(path, modified);
}
for path in removals {
self.mtimes.remove(&path);
}
changes
}
}
pub fn read_shader_source<P: AsRef<Path>>(path: P) -> std::io::Result<String> {
let contents = std::fs::read_to_string(path)?;
match contents.strip_prefix('\u{FEFF}') {
Some(stripped) => Ok(stripped.to_owned()),
None => Ok(contents),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_interval_constant() {
let poller = FilesystemPoller::with_default_interval();
assert_eq!(poller.poll_interval, DEFAULT_POLL_INTERVAL);
assert!(poller.last_poll.is_none());
assert_eq!(poller.watched_count(), 0);
}
#[test]
fn test_strip_bom_via_str() {
let with_bom = "\u{FEFF}fn main() {}";
assert_eq!(with_bom.strip_prefix('\u{FEFF}'), Some("fn main() {}"));
}
}