1use std::path::PathBuf;
20use std::sync::Arc;
21
22use bevy_app::prelude::*;
23use bevy_ecs::lifecycle::HookContext;
24use bevy_ecs::prelude::*;
25use bevy_ecs::world::DeferredWorld;
26use bevy_log::warn;
27use bevy_tasks::futures_lite::{Stream, StreamExt as _, stream};
28use bevy_tasks::{IoTaskPool, TaskPool};
29use rig_ecs::tasks::{Done, Running, Wake};
30use rig_tools::workspace::{Listing, Watch, Workspace};
31
32use crate::workspace::ProjectWorkspace;
33
34#[derive(Component, Clone, Debug, PartialEq, Eq)]
38#[component(immutable, on_insert = arm, on_discard = disarm)]
39pub struct FileWatch {
40 pub path: PathBuf,
42 pub recursive: bool,
44 pub listing: Listing,
46}
47
48impl FileWatch {
49 pub fn new(path: impl Into<PathBuf>) -> Self {
51 Self {
52 path: path.into(),
53 recursive: false,
54 listing: Listing::Visible,
55 }
56 }
57
58 pub fn tree(path: impl Into<PathBuf>) -> Self {
60 Self {
61 path: path.into(),
62 recursive: true,
63 listing: Listing::Visible,
64 }
65 }
66
67 pub fn listing(mut self, listing: Listing) -> Self {
69 self.listing = listing;
70 self
71 }
72}
73
74#[derive(Message, Clone, Debug, PartialEq, Eq)]
77pub struct FileChanges {
78 pub watch: Entity,
80 pub paths: Vec<PathBuf>,
83}
84
85pub struct WatchPlugin;
87
88impl Plugin for WatchPlugin {
89 fn build(&self, app: &mut App) {
90 app.add_message::<FileChanges>().add_observer(changed);
91 }
92}
93
94struct Seen(Vec<PathBuf>);
96
97fn arm(mut world: DeferredWorld, context: HookContext) {
99 let Some(watch) = world.get::<FileWatch>(context.entity).cloned() else {
100 return;
101 };
102 let workspace = world.get_resource::<ProjectWorkspace>().cloned();
103 let workspace = workspace.unwrap_or_default().0;
104 let wake = world.get_resource::<Wake>().cloned().unwrap_or_default();
105 let pool = IoTaskPool::get_or_init(TaskPool::default);
106 let running = Running::stream(pool, &wake, watch_stream(workspace, watch));
107 world.commands().entity(context.entity).insert(running);
108}
109
110fn disarm(mut world: DeferredWorld, context: HookContext) {
112 world
113 .commands()
114 .entity(context.entity)
115 .try_remove::<(Running, Done<Seen>)>();
116}
117
118fn changed(
120 inserted: On<Insert<Done<Seen>>>,
121 seen: Query<&Done<Seen>, With<FileWatch>>,
122 mut changes: MessageWriter<FileChanges>,
123) {
124 if let Ok(Done(Seen(paths))) = seen.get(inserted.entity) {
125 changes.write(FileChanges {
126 watch: inserted.entity,
127 paths: paths.clone(),
128 });
129 }
130}
131
132enum WatchState {
134 Unarmed(Arc<dyn Workspace>, FileWatch),
136 Armed(Watch),
138}
139
140fn watch_stream(workspace: Arc<dyn Workspace>, watch: FileWatch) -> impl Stream<Item = Seen> {
143 stream::unfold(
144 Some(WatchState::Unarmed(workspace, watch)),
145 |state| async move {
146 match state? {
147 WatchState::Unarmed(workspace, watch) => {
148 let armed =
150 workspace
151 .clone()
152 .watch(&watch.path, watch.recursive, watch.listing);
153 let files = files_now(&*workspace, &watch);
154 let next = match armed {
155 Ok(armed) => Some(WatchState::Armed(armed)),
156 Err(error) => {
157 warn!("cannot watch {}: {error}", watch.path.display());
158 None
159 }
160 };
161 Some((Seen(files), next))
162 }
163 WatchState::Armed(mut armed) => {
164 let paths = armed.next().await?;
165 Some((Seen(paths), Some(WatchState::Armed(armed))))
166 }
167 }
168 },
169 )
170}
171
172fn files_now(workspace: &dyn Workspace, watch: &FileWatch) -> Vec<PathBuf> {
174 let root = watch.path.as_path();
175 let mut files = workspace
176 .files(root, None, watch.listing)
177 .unwrap_or_default();
178 files.retain(|path| watch.recursive || path == root || path.parent() == Some(root));
179 files.sort();
180 files
181}
182
183#[cfg(test)]
184mod tests;