use std::path::PathBuf;
use std::sync::Arc;
use bevy_app::prelude::*;
use bevy_ecs::lifecycle::HookContext;
use bevy_ecs::prelude::*;
use bevy_ecs::world::DeferredWorld;
use bevy_log::warn;
use bevy_tasks::futures_lite::{Stream, StreamExt as _, stream};
use bevy_tasks::{IoTaskPool, TaskPool};
use rig_ecs::tasks::{Done, Running, Wake};
use rig_tools::workspace::{Listing, Watch, Workspace};
use crate::workspace::ProjectWorkspace;
#[derive(Component, Clone, Debug, PartialEq, Eq)]
#[component(immutable, on_insert = arm, on_discard = disarm)]
pub struct FileWatch {
pub path: PathBuf,
pub recursive: bool,
pub listing: Listing,
}
impl FileWatch {
pub fn new(path: impl Into<PathBuf>) -> Self {
Self {
path: path.into(),
recursive: false,
listing: Listing::Visible,
}
}
pub fn tree(path: impl Into<PathBuf>) -> Self {
Self {
path: path.into(),
recursive: true,
listing: Listing::Visible,
}
}
pub fn listing(mut self, listing: Listing) -> Self {
self.listing = listing;
self
}
}
#[derive(Message, Clone, Debug, PartialEq, Eq)]
pub struct FileChanges {
pub watch: Entity,
pub paths: Vec<PathBuf>,
}
pub struct WatchPlugin;
impl Plugin for WatchPlugin {
fn build(&self, app: &mut App) {
app.add_message::<FileChanges>().add_observer(changed);
}
}
struct Seen(Vec<PathBuf>);
fn arm(mut world: DeferredWorld, context: HookContext) {
let Some(watch) = world.get::<FileWatch>(context.entity).cloned() else {
return;
};
let workspace = world.get_resource::<ProjectWorkspace>().cloned();
let workspace = workspace.unwrap_or_default().0;
let wake = world.get_resource::<Wake>().cloned().unwrap_or_default();
let pool = IoTaskPool::get_or_init(TaskPool::default);
let running = Running::stream(pool, &wake, watch_stream(workspace, watch));
world.commands().entity(context.entity).insert(running);
}
fn disarm(mut world: DeferredWorld, context: HookContext) {
world
.commands()
.entity(context.entity)
.try_remove::<(Running, Done<Seen>)>();
}
fn changed(
inserted: On<Insert<Done<Seen>>>,
seen: Query<&Done<Seen>, With<FileWatch>>,
mut changes: MessageWriter<FileChanges>,
) {
if let Ok(Done(Seen(paths))) = seen.get(inserted.entity) {
changes.write(FileChanges {
watch: inserted.entity,
paths: paths.clone(),
});
}
}
enum WatchState {
Unarmed(Arc<dyn Workspace>, FileWatch),
Armed(Watch),
}
fn watch_stream(workspace: Arc<dyn Workspace>, watch: FileWatch) -> impl Stream<Item = Seen> {
stream::unfold(
Some(WatchState::Unarmed(workspace, watch)),
|state| async move {
match state? {
WatchState::Unarmed(workspace, watch) => {
let armed =
workspace
.clone()
.watch(&watch.path, watch.recursive, watch.listing);
let files = files_now(&*workspace, &watch);
let next = match armed {
Ok(armed) => Some(WatchState::Armed(armed)),
Err(error) => {
warn!("cannot watch {}: {error}", watch.path.display());
None
}
};
Some((Seen(files), next))
}
WatchState::Armed(mut armed) => {
let paths = armed.next().await?;
Some((Seen(paths), Some(WatchState::Armed(armed))))
}
}
},
)
}
fn files_now(workspace: &dyn Workspace, watch: &FileWatch) -> Vec<PathBuf> {
let root = watch.path.as_path();
let mut files = workspace
.files(root, None, watch.listing)
.unwrap_or_default();
files.retain(|path| watch.recursive || path == root || path.parent() == Some(root));
files.sort();
files
}
#[cfg(test)]
mod tests;