1use std::any::{Any, TypeId};
21use std::collections::HashMap;
22use std::rc::Rc;
23
24use tilo_resource::{Doc, Invoke, Resource};
25use ui_core::history_mode::HistoryDefinition;
26use ui_core::history_mode::checked_files::FileHistorySource;
27
28use super::surface::ResourceSelection;
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub enum RunMode {
33 Live,
36 Past,
39 Capture,
43}
44
45impl RunMode {
46 pub fn is_live(self) -> bool {
48 self == Self::Live
49 }
50}
51
52#[derive(Default)]
54pub(crate) struct Fixtures {
55 values: HashMap<TypeId, Rc<dyn Any>>,
56 collaborators: Vec<(String, ResourceSelection)>,
57 content: Option<tilo_content::ContentStore>,
58 _directory: Option<tempfile::TempDir>,
60}
61
62#[derive(Clone)]
64pub(crate) struct Run {
65 pub(crate) mode: RunMode,
66 pub(crate) fixtures: Rc<Fixtures>,
67 pub(crate) launched: Rc<Launched>,
69 pub(crate) private: bool,
71 pub(crate) file_history: Option<(FileHistorySource, &'static HistoryDefinition)>,
75 pub(crate) people: super::People,
77 pub(crate) opener: Option<super::launch::OpenHere>,
79}
80
81#[derive(Default)]
85pub(crate) struct Launched {
86 pub(crate) record: Option<ui_core::record::RecordId>,
88 pub(crate) arguments: Vec<String>,
89 pub(crate) options: Vec<ui_core::application::LaunchOption>,
90 pub(crate) file: Option<ui_core::application::OpenFile>,
91 pub(crate) handoff: Option<ui_core::application::PlaybackHandoff>,
92 pub(crate) lent: Lent,
94}
95
96#[derive(Default)]
100pub(crate) struct Lent(std::cell::RefCell<Vec<Box<dyn Any>>>);
101
102impl Lent {
103 pub(crate) fn new(services: Vec<Box<dyn Any>>) -> Self {
104 Self(std::cell::RefCell::new(services))
105 }
106
107 pub(crate) fn take<T: 'static>(&self) -> Option<T> {
109 let mut services = self.0.borrow_mut();
110 let index = services.iter().position(|service| service.is::<T>())?;
111 services
112 .remove(index)
113 .downcast::<T>()
114 .ok()
115 .map(|service| *service)
116 }
117}
118
119impl Run {
120 pub(crate) fn live(content: Option<tilo_content::ContentStore>) -> Self {
121 Self {
122 mode: RunMode::Live,
123 fixtures: Rc::new(Fixtures {
124 content,
125 ..Fixtures::default()
126 }),
127 launched: Rc::default(),
128 private: false,
129 file_history: None,
130 people: super::People::new(None),
131 opener: None,
132 }
133 }
134
135 pub(crate) fn in_private(mut self, private: bool) -> Self {
137 self.private = private;
138 self
139 }
140
141 pub(crate) fn showing_file_history(
143 mut self,
144 file_history: Option<(FileHistorySource, &'static HistoryDefinition)>,
145 ) -> Self {
146 self.file_history = file_history;
147 self
148 }
149
150 pub(crate) fn past() -> Self {
151 Self {
152 mode: RunMode::Past,
153 fixtures: Rc::default(),
154 launched: Rc::default(),
155 private: false,
156 file_history: None,
157 people: super::People::new(None),
158 opener: None,
159 }
160 }
161
162 pub(crate) fn opening_with(mut self, opener: Option<super::launch::OpenHere>) -> Self {
164 self.opener = opener;
165 self
166 }
167
168 pub(crate) fn open_here(
170 &self,
171 arguments: &[String],
172 ) -> Result<ui_core::application::ReplacementOutcome, String> {
173 Ok(self
174 .opener
175 .as_ref()
176 .ok_or_else(|| "this window shows the record it was opened on".to_owned())?
177 .open(arguments))
178 }
179
180 pub(crate) fn for_participant(mut self, me: Option<&str>) -> Self {
182 self.people = super::People::new(me);
183 self
184 }
185
186 pub(crate) fn launched(mut self, launched: Launched) -> Self {
188 self.launched = Rc::new(launched);
189 self
190 }
191
192 pub(crate) fn launch(&self) -> super::launch::LaunchInput<'_> {
193 super::launch::LaunchInput::new(&self.launched.arguments, self.launched.file.as_ref())
194 .with_content(self.fixtures.content.as_ref())
195 .with_handoff(self.launched.handoff.as_ref())
196 .with_options(&self.launched.options)
197 }
198
199 pub(crate) fn fixture<T: 'static>(&self) -> Option<Rc<T>> {
200 let value = self.fixtures.values.get(&TypeId::of::<T>())?;
201 Rc::clone(value).downcast::<T>().ok()
202 }
203 pub(crate) fn content(&self) -> Option<tilo_content::ContentStore> {
204 self.fixtures.content.clone()
205 }
206
207 pub(crate) fn collaborators(&self) -> &[(String, ResourceSelection)] {
209 &self.fixtures.collaborators
210 }
211}
212
213struct Version<R: Resource> {
215 author: String,
216 at_ms: i64,
217 invoke: Invoke<R>,
218}
219
220pub struct Fixture<R: Resource> {
224 scenario: Option<String>,
225 values: HashMap<TypeId, Rc<dyn Any>>,
226 history: Vec<Version<R>>,
227 collaborators: Vec<(String, ResourceSelection)>,
228 content: Option<tilo_content::ContentStore>,
229 directory: Option<tempfile::TempDir>,
230}
231
232impl<R: Resource> Fixture<R> {
233 pub(crate) fn new(scenario: Option<&str>, content: Option<tilo_content::ContentStore>) -> Self {
234 Self {
235 scenario: scenario.map(str::to_owned),
236 values: HashMap::new(),
237 history: Vec::new(),
238 collaborators: Vec::new(),
239 content,
240 directory: None,
241 }
242 }
243
244 pub fn scenario(&self) -> Option<&str> {
246 self.scenario.as_deref()
247 }
248
249 pub fn provide<T: 'static>(&mut self, value: T) {
253 self.values.insert(TypeId::of::<T>(), Rc::new(value));
254 }
255
256 pub fn directory(&mut self) -> std::io::Result<&std::path::Path> {
262 if self.directory.is_none() {
263 self.directory = Some(
264 tempfile::Builder::new()
265 .prefix("tilo-scenario-")
266 .tempdir()?,
267 );
268 }
269 match &self.directory {
270 Some(directory) => Ok(directory.path()),
271 None => Err(std::io::Error::other("no scenario directory")),
272 }
273 }
274
275 pub fn content(&mut self) -> &tilo_content::ContentStore {
280 self.content
281 .get_or_insert_with(tilo_content::ContentStore::memory)
282 }
283
284 pub fn version(&mut self, author: &str, at_ms: i64, invoke: Invoke<R>) {
290 self.history.push(Version {
291 author: author.to_owned(),
292 at_ms,
293 invoke,
294 });
295 }
296
297 pub fn collaborator(&mut self, name: &str, at: ResourceSelection) {
301 self.collaborators.push((name.to_owned(), at));
302 }
303
304 pub(crate) fn write_history(
306 &self,
307 doc: &mut Doc,
308 registry: &tilo_actions::ActionRegistry,
309 ) -> Result<(), String> {
310 for version in &self.history {
311 write_version::<R>(doc, registry, version)?;
312 }
313 Ok(())
314 }
315
316 pub(crate) fn has_history(&self) -> bool {
317 !self.history.is_empty()
318 }
319
320 pub(crate) fn into_run(self) -> Run {
322 Run {
323 mode: RunMode::Capture,
324 fixtures: Rc::new(Fixtures {
325 values: self.values,
326 collaborators: self.collaborators,
327 content: self.content,
328 _directory: self.directory,
329 }),
330 launched: Rc::default(),
331 private: false,
332 file_history: None,
333 people: super::People::new(None),
334 opener: None,
335 }
336 }
337
338 pub(crate) fn lent_content(&self) -> Option<&tilo_content::ContentStore> {
340 self.content.as_ref()
341 }
342}
343
344fn write_version<R: Resource>(
347 doc: &mut Doc,
348 registry: &tilo_actions::ActionRegistry,
349 version: &Version<R>,
350) -> Result<(), String> {
351 use tilo_actions::Action as _;
352 let action = version.invoke.build(version.at_ms)?;
353 let encoded = tilo_resource::encode::<R>(&action).map_err(|error| error.to_string())?;
354 let mut context = tilo_actions::ActionContext::for_resource(R::KIND, R::KIND, R::LABEL)
355 .with_change(action.change_kind())
356 .with_host_invoker(version.author.clone(), None);
357 if let Some(described) = action.describe(doc).or_else(|| {
358 tilo_actions::Described::of_published(
359 &encoded,
360 tilo_actions::ActionKey::new("", R::KIND, version.invoke.id),
361 )
362 }) {
363 context = context.with_described(described);
364 }
365 let envelope = tilo_actions::ActionEnvelope {
366 id: tilo_actions::ActionId::new(),
367 action: encoded,
368 base_heads: doc.get_heads().iter().map(|head| head.0).collect(),
369 timestamp: version.at_ms.div_euclid(1000),
370 locus: action.locus(),
371 actor: Some(version.author.as_bytes().to_vec()),
372 context: Some(context),
373 };
374 tilo_actions::replay(doc, &envelope, registry).map_err(|error| error.to_string())
375}