1use super::{pipe, util};
2use crate::engine::ecs;
3use slotmap::KeyData;
4use std::collections::HashSet;
5use std::io::Write;
6
7pub struct ReplBackend {
12 cwd: Option<ecs::ComponentId>,
13}
14
15impl ReplBackend {
16 pub fn new() -> Self {
17 Self { cwd: None }
18 }
19
20 pub(crate) fn cwd(&self) -> Option<ecs::ComponentId> {
21 self.cwd
22 }
23
24 fn format_component_id_short(id: ecs::ComponentId) -> String {
25 let s = format!("{:?}", id);
26 if let (Some(l), Some(r)) = (s.find('('), s.rfind(')')) {
27 if r > l + 1 {
28 return s[l + 1..r].to_string();
29 }
30 }
31 s
32 }
33
34 fn parse_component_id_short(s: &str) -> Option<ecs::ComponentId> {
35 let (idx_str, ver_str) = s.split_once('v')?;
37 let idx: u32 = idx_str.parse().ok()?;
38 let version: u32 = ver_str.parse().ok()?;
39 let ffi = (u64::from(version) << 32) | u64::from(idx);
40 Some(KeyData::from_ffi(ffi).into())
41 }
42
43 pub(crate) fn current_listing(&self, world: &ecs::World) -> Vec<ecs::ComponentId> {
44 self.listing_for(world, self.cwd)
45 }
46
47 fn listing_for(
48 &self,
49 world: &ecs::World,
50 cwd: Option<ecs::ComponentId>,
51 ) -> Vec<ecs::ComponentId> {
52 match cwd {
53 None => world
54 .all_components()
55 .filter(|&cid| world.parent_of(cid).is_none())
56 .collect(),
57 Some(cwd) => world.children_of(cwd).to_vec(),
58 }
59 }
60
61 fn resolve_in_listing(
62 &self,
63 world: &ecs::World,
64 listing: &[ecs::ComponentId],
65 segment: &str,
66 ) -> Result<ecs::ComponentId, String> {
67 let (key_part, name_part) = segment.split_once(':').unwrap_or((segment, ""));
68
69 if let Ok(idx) = key_part.parse::<usize>() {
71 let cid = listing
72 .get(idx)
73 .copied()
74 .ok_or_else(|| format!("index out of range: {}", idx))?;
75 if !name_part.is_empty() {
76 let actual_name = world
77 .get_component_node(cid)
78 .map(|n| n.name.as_str())
79 .unwrap_or("<deleted>");
80 if actual_name != name_part {
81 return Err(format!(
82 "index {} resolved, but name mismatch: expected '{}' got '{}'",
83 idx, name_part, actual_name
84 ));
85 }
86 }
87 return Ok(cid);
88 }
89
90 if let Ok(guid) = key_part.parse::<uuid::Uuid>() {
92 for cid in listing.iter().copied() {
93 if let Some(node) = world.get_component_node(cid) {
94 if node.guid == guid {
95 if !name_part.is_empty() && node.name != name_part {
96 return Err(format!(
97 "guid {} resolved, but name mismatch: expected '{}' got '{}'",
98 guid, name_part, node.name
99 ));
100 }
101 return Ok(cid);
102 }
103 }
104 }
105 return Err(format!("guid not found: {}", guid));
106 }
107
108 if let Some(cid) = Self::parse_component_id_short(key_part) {
110 if listing.iter().any(|&c| c == cid) {
111 if !name_part.is_empty() {
112 let actual_name = world
113 .get_component_node(cid)
114 .map(|n| n.name.as_str())
115 .unwrap_or("<deleted>");
116 if actual_name != name_part {
117 return Err(format!(
118 "id {} resolved, but name mismatch: expected '{}' got '{}'",
119 key_part, name_part, actual_name
120 ));
121 }
122 }
123 return Ok(cid);
124 }
125 return Err(format!("id not in current listing: {}", key_part));
126 }
127
128 let mut matches: Vec<ecs::ComponentId> = Vec::new();
130 for cid in listing.iter().copied() {
131 if let Some(node) = world.get_component_node(cid) {
132 if node.name == key_part {
133 matches.push(cid);
134 }
135 }
136 }
137
138 match matches.len() {
139 0 => Err(format!("not found: {}", key_part)),
140 1 => Ok(matches[0]),
141 _ => Err(format!(
142 "ambiguous name: {} (use 'ls' + index, guid, or id token)",
143 key_part
144 )),
145 }
146 }
147
148 fn cd_path(&self, world: &ecs::World, path: &str) -> Result<Option<ecs::ComponentId>, String> {
149 let is_abs = path.starts_with('/');
150 let mut cur: Option<ecs::ComponentId> = if is_abs { None } else { self.cwd };
151
152 let segments = path.split('/').filter(|s| !s.is_empty());
153 for seg in segments {
154 match seg {
155 "." => {}
156 ".." => {
157 cur = cur.and_then(|cwd| world.parent_of(cwd));
158 }
159 _ => {
160 let listing = self.listing_for(world, cur);
161 let next = self.resolve_in_listing(world, &listing, seg)?;
162 cur = Some(next);
163 }
164 }
165 }
166
167 Ok(cur)
168 }
169
170 pub(crate) fn resolve_path_or_item(
171 &self,
172 world: &ecs::World,
173 arg: &str,
174 ) -> Result<Option<ecs::ComponentId>, String> {
175 match arg {
176 "/" => Ok(None),
177 "." => Ok(self.cwd),
178 ".." => Ok(self.cwd.and_then(|cwd| world.parent_of(cwd))),
179 _ if arg.contains('/') => self.cd_path(world, arg),
180 _ if arg.parse::<uuid::Uuid>().is_ok() => {
181 let guid = arg
184 .parse::<uuid::Uuid>()
185 .map_err(|e| format!("invalid guid: {}", e))?;
186
187 world
188 .component_id_by_guid(guid)
189 .map(Some)
190 .ok_or_else(|| format!("guid not found: {}", guid))
191 }
192 _ => {
193 let listing = self.current_listing(world);
194 Ok(Some(self.resolve_in_listing(world, &listing, arg)?))
195 }
196 }
197 }
198
199 pub fn exec(&mut self, world: &ecs::World, cmd: &str) {
203 let cmd = cmd.trim();
204 if cmd.is_empty() {
205 return;
206 }
207
208 if cmd.contains('|') {
210 match pipe::try_exec_piped(self, world, cmd) {
211 Ok(true) => return,
212 Ok(false) => {}
213 Err(e) => {
214 println!("🐈 pipe: {}", e);
215 return;
216 }
217 }
218 }
219
220 if let Some(cwd) = self.cwd {
222 if world.get_component_node(cwd).is_none() {
223 self.cwd = None;
224 }
225 }
226
227 let mut it = cmd.split_whitespace();
228 let Some(verb) = it.next() else {
229 return;
230 };
231
232 match verb {
233 "help" => {
234 println!("🐈 Commands:");
235 println!("🐈 ls");
236 println!("🐈 tree [max_depth]");
237 println!("🐈 cd <name>");
238 println!("🐈 cd <index>");
239 println!("🐈 cd <guid>");
240 println!("🐈 cd <path>");
241 println!("🐈 cd ..");
242 println!("🐈 cd /");
243 println!("🐈 pwd");
244 println!("🐈 type [path]");
245 println!("🐈 cat <path>");
246 println!("🐈 ls | grep <pattern>");
247 println!("🐈 cat <path> | grep <pattern>");
248 println!("🐈 <cmd> | (trailing pipe prints summary)");
249 println!("🐈 clear");
250 }
251 "clear" | "cls" => {
252 print!("\x1b[2J\x1b[H\x1b[3J");
254 let _ = std::io::stdout().flush();
255 }
256 "pwd" => match self.cwd {
257 None => println!("🐈 /"),
258 Some(mut cur) => {
259 let mut parts: Vec<String> = Vec::new();
260 loop {
261 let Some(node) = world.get_component_node(cur) else {
262 break;
263 };
264 parts.push(format!(
265 "{}:{}",
266 Self::format_component_id_short(cur),
267 node.name
268 ));
269 match world.parent_of(cur) {
270 Some(p) => cur = p,
271 None => break,
272 }
273 }
274 parts.reverse();
275 println!("🐈 /{}", parts.join("/"));
276 }
277 },
278 "type" | "kind" => {
279 let target = match it.next() {
282 None => self.cwd,
283 Some(arg) => match self.resolve_path_or_item(world, arg) {
284 Ok(t) => t,
285 Err(e) => {
286 println!("🐈 type: {}", e);
287 return;
288 }
289 },
290 };
291
292 match target {
293 None => println!("🐈 type: <root>"),
294 Some(cid) => {
295 let Some(node) = world.get_component_node(cid) else {
296 println!("🐈 type: not found");
297 return;
298 };
299 let kind = node.component.name();
300 println!(
301 "🐈 type: {} (name='{}' id={} guid={})",
302 kind,
303 node.name,
304 Self::format_component_id_short(cid),
305 node.guid
306 );
307 }
308 }
309 }
310 "ls" => {
311 let ids: Vec<ecs::ComponentId> = self.current_listing(world);
312
313 if ids.is_empty() {
314 println!("🐈 (empty)");
315 return;
316 }
317
318 for (i, cid) in ids.into_iter().enumerate() {
319 if let Some(line) = util::format_ls_line(world, i, cid) {
320 println!("{}", line);
321 }
322 }
323 }
324 "tree" => {
325 let arg = it.next();
326 if it.next().is_some() {
327 println!("🐈 tree: usage: tree [max_depth]");
328 return;
329 }
330
331 let max_depth: Option<usize> = match arg {
332 None => None,
333 Some(s) => match s.parse::<usize>() {
334 Ok(v) => Some(v),
335 Err(_) => {
336 println!("🐈 tree: invalid max_depth: {}", s);
337 return;
338 }
339 },
340 };
341
342 self.print_tree(world, max_depth);
343 }
344 "cat" => {
345 let target = match it.next() {
349 None => self.cwd,
350 Some(arg) => match self.resolve_path_or_item(world, arg) {
351 Ok(t) => t,
352 Err(e) => {
353 println!("🐈 cat: {}", e);
354 return;
355 }
356 },
357 };
358
359 use crate::scripting::component_registry::subtree_to_ce_ast;
360 use crate::scripting::unparser::unparse_component;
361 match target {
362 Some(root) => match subtree_to_ce_ast(world, root) {
363 Ok(ce) => println!("{}", unparse_component(&ce)),
364 Err(e) => println!("🐈 cat: {}", e),
365 },
366 None => {
367 let root_ids: Vec<ecs::ComponentId> = world
368 .all_components()
369 .filter(|&cid| world.parent_of(cid).is_none())
370 .collect();
371 for (i, cid) in root_ids.iter().enumerate() {
372 if i > 0 {
373 println!();
374 }
375 match subtree_to_ce_ast(world, *cid) {
376 Ok(ce) => println!("{}", unparse_component(&ce)),
377 Err(e) => println!("🐈 cat: {}", e),
378 }
379 }
380 }
381 }
382 }
383 "cd" => {
384 let Some(arg) = it.next() else {
385 println!(
386 "🐈 usage: cd <name> | cd <index> | cd <guid> | cd <path> | cd .. | cd /"
387 );
388 return;
389 };
390
391 match arg {
392 "/" => {
393 self.cwd = None;
394 }
395 ".." => {
396 self.cwd = self.cwd.and_then(|cwd| world.parent_of(cwd));
397 }
398 name => {
399 if name.contains('/') {
403 match self.cd_path(world, name) {
404 Ok(new_cwd) => self.cwd = new_cwd,
405 Err(e) => println!("🐈 cd: {}", e),
406 }
407 return;
408 }
409
410 let candidates: Vec<ecs::ComponentId> = self.current_listing(world);
411
412 if let Ok(idx) = name.parse::<usize>() {
414 if let Some(cid) = candidates.get(idx).copied() {
415 self.cwd = Some(cid);
416 } else {
417 println!("🐈 cd: index out of range: {}", idx);
418 }
419 return;
420 }
421
422 if let Ok(guid) = name.parse::<uuid::Uuid>() {
424 let mut found: Option<ecs::ComponentId> = None;
425 for cid in candidates.iter().copied() {
426 if let Some(node) = world.get_component_node(cid) {
427 if node.guid == guid {
428 found = Some(cid);
429 break;
430 }
431 }
432 }
433
434 if found.is_none() {
436 found = world.component_id_by_guid(guid);
437 }
438
439 match found {
440 Some(cid) => self.cwd = Some(cid),
441 None => println!("🐈 cd: guid not found: {}", guid),
442 }
443 return;
444 }
445
446 let mut matches: Vec<ecs::ComponentId> = Vec::new();
448 for cid in candidates.iter().copied() {
449 if let Some(node) = world.get_component_node(cid) {
450 if node.name == name {
451 matches.push(cid);
452 }
453 }
454 }
455
456 match matches.len() {
457 0 => println!("🐈 cd: not found: {}", name),
458 1 => self.cwd = Some(matches[0]),
459 _ => {
460 println!("🐈 cd: ambiguous name: {}", name);
461 println!("🐈 hint: use 'ls' then 'cd <index>' or 'cd <guid>'");
462 }
463 }
464 }
465 }
466 }
467 _ => println!("🐈 unknown command: {}", verb),
468 }
469 }
470
471 fn print_tree(&self, world: &ecs::World, max_depth: Option<usize>) {
472 let mut visited: HashSet<ecs::ComponentId> = HashSet::new();
473
474 match self.cwd {
475 None => {
476 println!("🐈 /");
477 let roots: Vec<ecs::ComponentId> = world
478 .all_components()
479 .filter(|&cid| world.parent_of(cid).is_none())
480 .collect();
481
482 for (i, cid) in roots.into_iter().enumerate() {
483 self.print_tree_node(world, cid, 0, i, &mut visited, max_depth);
484 }
485 }
486 Some(cwd) => {
487 if let Some(line) = util::format_ls_line(world, 0, cwd) {
489 println!("{}", line);
490 } else {
491 println!("🐈 tree: cwd deleted");
492 return;
493 }
494
495 let children: Vec<ecs::ComponentId> = world.children_of(cwd).to_vec();
496 for (i, cid) in children.into_iter().enumerate() {
497 self.print_tree_node(world, cid, 1, i, &mut visited, max_depth);
498 }
499 }
500 }
501 }
502
503 fn print_tree_node(
504 &self,
505 world: &ecs::World,
506 cid: ecs::ComponentId,
507 depth: usize,
508 index_in_parent: usize,
509 visited: &mut HashSet<ecs::ComponentId>,
510 max_depth: Option<usize>,
511 ) {
512 if let Some(limit) = max_depth {
513 if depth > limit {
514 return;
515 }
516 }
517
518 let indent = " ".repeat(depth);
519
520 if !visited.insert(cid) {
521 if let Some(line) = util::format_ls_line(world, index_in_parent, cid) {
523 println!("{}{} 🌀(cycle)", indent, line);
524 }
525 return;
526 }
527
528 if let Some(line) = util::format_ls_line(world, index_in_parent, cid) {
529 println!("{}{}", indent, line);
530 } else {
531 return;
532 }
533
534 let children: Vec<ecs::ComponentId> = world.children_of(cid).to_vec();
535 for (i, ch) in children.into_iter().enumerate() {
536 self.print_tree_node(world, ch, depth + 1, i, visited, max_depth);
537 }
538 }
539
540 pub fn exec_all<I>(&mut self, world: &ecs::World, commands: I)
542 where
543 I: IntoIterator<Item = String>,
544 {
545 for cmd in commands {
546 self.exec(world, &cmd);
547 }
548 }
549}