Skip to main content

mittens_engine/engine/repl/
repl_backend.rs

1use super::{pipe, util};
2use crate::engine::ecs;
3use slotmap::KeyData;
4use std::collections::HashSet;
5use std::io::Write;
6
7/// Runs REPL commands against engine state.
8///
9/// This is intended to be called from the main thread (e.g. inside `Universe::update()`),
10/// after commands are received from the stdin thread.
11pub 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        // slotmap::KeyData debug format is "<idx>v<version>".
36        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        // 1) Numeric index into listing.
70        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        // 2) GUID.
91        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        // 3) Short ComponentId token (e.g. 7v1).
109        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        // 4) Name.
129        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                // For single-item GUID lookups (not path segments), allow global resolution.
182                // This keeps `cd /a/b/c` semantics local, but makes `cat <guid>` usable anywhere.
183                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    /// Execute a single REPL command.
200    ///
201    /// This currently only reads from `world` and updates internal REPL state (cwd).
202    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        // Pipe system (component-object pipes).
209        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 the cwd component was deleted, reset to root.
221        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                // Clear screen + move cursor to home. (Many terminals also treat 3J as clear scrollback.)
253                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                // If no arg is provided, default to the current working directory.
280                // At root (cwd=None), print a sentinel.
281                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                // If no arg is provided, default to the current working directory.
346                // - At root (cwd=None): dump the whole scene (all roots)
347                // - Otherwise: dump the cwd subtree
348                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                        // Path form (supports absolute/relative):
400                        //   cd /7v1:root/8v1:child
401                        //   cd 7v1:child/grandchild
402                        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                        // 1) If it's a numeric index, treat it as an index into the last listing.
413                        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                        // 2) If it parses as a UUID, match on GUID.
423                        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 not found in the current listing, allow a global jump-by-guid.
435                            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                        // 3) Otherwise, treat it as a name.
447                        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                // Print the current component first.
488                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            // Defensive: should be a tree, but avoid infinite loops.
522            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    /// Execute all queued commands.
541    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}