Skip to main content

mittens_engine/engine/repl/
pipe.rs

1use crate::engine::ecs;
2use crate::scripting::ast::ComponentExpression;
3use crate::scripting::component_registry::subtree_to_ce_ast;
4use crate::scripting::unparser::unparse_component;
5
6use super::repl_backend::ReplBackend;
7use super::util;
8
9#[derive(Debug, Clone)]
10enum PipeValue {
11    Id(ecs::ComponentId),
12    Tree(ComponentExpression),
13}
14
15fn source_ls(
16    backend: &ReplBackend,
17    world: &ecs::World,
18    args: &[&str],
19) -> Result<Vec<ecs::ComponentId>, String> {
20    if !args.is_empty() {
21        return Err("ls takes no arguments (in pipes)".to_string());
22    }
23    Ok(backend.current_listing(world))
24}
25
26fn source_cat(
27    backend: &ReplBackend,
28    world: &ecs::World,
29    args: &[&str],
30) -> Result<Vec<ComponentExpression>, String> {
31    let target = match args {
32        [] => backend.cwd(),
33        [one] => backend.resolve_path_or_item(world, one)?,
34        _ => return Err("cat takes at most one argument (in pipes)".to_string()),
35    };
36
37    match target {
38        None => {
39            let root_ids: Vec<ecs::ComponentId> = world
40                .all_components()
41                .filter(|&cid| world.parent_of(cid).is_none())
42                .collect();
43            let mut out = Vec::new();
44            for cid in root_ids {
45                out.push(subtree_to_ce_ast(world, cid)?);
46            }
47            Ok(out)
48        }
49        Some(root) => Ok(vec![subtree_to_ce_ast(world, root)?]),
50    }
51}
52
53fn stage_grep(world: &ecs::World, input: Vec<PipeValue>, pattern: &str) -> Vec<PipeValue> {
54    let needle = pattern.to_ascii_lowercase();
55    let mut out: Vec<PipeValue> = Vec::new();
56
57    // If the stream contains serialized trees, grep operates on the MMS source
58    // form of each subtree and emits whole matching subtrees.
59    let any_trees = input.iter().any(|v| matches!(v, PipeValue::Tree(_)));
60    if any_trees {
61        fn ce_matches(ce: &ComponentExpression, needle: &str) -> bool {
62            // Match against the unparsed MMS text — covers type name,
63            // builder names, literal args, and nested children.
64            unparse_component(ce).to_ascii_lowercase().contains(needle)
65        }
66
67        fn collect_matching(
68            ce: &ComponentExpression,
69            needle: &str,
70            out: &mut Vec<ComponentExpression>,
71        ) {
72            if ce_matches(ce, needle) {
73                out.push(ce.clone());
74                return;
75            }
76            for stmt in &ce.body.statements {
77                if let crate::scripting::ast::Statement::Expression(
78                    crate::scripting::ast::Expression::Component(child),
79                ) = stmt
80                {
81                    collect_matching(child, needle, out);
82                }
83            }
84        }
85
86        for value in input {
87            let PipeValue::Tree(ce) = value else {
88                continue;
89            };
90            let mut matches: Vec<ComponentExpression> = Vec::new();
91            collect_matching(&ce, &needle, &mut matches);
92            for m in matches {
93                println!("{}", unparse_component(&m));
94                out.push(PipeValue::Tree(m));
95            }
96        }
97        return out;
98    }
99
100    // Otherwise, grep operates on live components in the world and prints matching props.
101    for (i, value) in input.into_iter().enumerate() {
102        let PipeValue::Id(cid) = value else {
103            continue;
104        };
105        let Some(node) = world.get_component_node(cid) else {
106            continue;
107        };
108
109        let type_name = node.component.name().to_string();
110        let meta: [(String, String); 3] = [
111            ("name".to_string(), node.name.clone()),
112            ("type".to_string(), type_name),
113            ("guid".to_string(), node.guid.to_string()),
114        ];
115
116        let mut matches: Vec<(String, String)> = Vec::new();
117        for (k, v) in &meta {
118            if k.to_ascii_lowercase().contains(&needle) || v.to_ascii_lowercase().contains(&needle)
119            {
120                matches.push((k.clone(), v.clone()));
121            }
122        }
123
124        // Match against the unparsed CE form for body content.
125        let ce_text = subtree_to_ce_ast(world, cid)
126            .map(|ce| unparse_component(&ce))
127            .unwrap_or_default();
128        if ce_text.to_ascii_lowercase().contains(&needle) {
129            matches.push(("mms".to_string(), ce_text));
130        }
131
132        if matches.is_empty() {
133            continue;
134        }
135        out.push(PipeValue::Id(cid));
136        if let Some(line) = util::format_ls_line(world, i, cid) {
137            println!("{}", line);
138        }
139        for (k, v) in matches {
140            println!("🐈   {} = {}", k, v);
141        }
142    }
143
144    out
145}
146
147fn sink_print_summary(world: &ecs::World, items: Vec<PipeValue>) {
148    if items.is_empty() {
149        println!("🐈 (empty)");
150        return;
151    }
152
153    for (i, item) in items.into_iter().enumerate() {
154        match item {
155            PipeValue::Id(cid) => {
156                if let Some(line) = util::format_ls_line(world, i, cid) {
157                    println!("{}", line);
158                }
159            }
160            PipeValue::Tree(ce) => {
161                println!("🐈 {}: {}", i, unparse_component(&ce));
162            }
163        }
164    }
165}
166
167/// Execute a command line containing pipes (`|`).
168///
169/// Piping moves *component objects* (ComponentIds) between stages.
170///
171/// Supported sources: `ls`, `cat [path]`
172/// Supported stages: `grep <pattern>`
173/// Supported sinks: trailing `|` (prints ls-style summary)
174pub fn try_exec_piped(
175    backend: &mut ReplBackend,
176    world: &ecs::World,
177    cmd: &str,
178) -> Result<bool, String> {
179    if !cmd.contains('|') {
180        return Ok(false);
181    }
182
183    let raw_parts: Vec<&str> = cmd.split('|').collect();
184    if raw_parts.len() < 2 {
185        return Ok(false);
186    }
187
188    let source_part = raw_parts[0].trim();
189    if source_part.is_empty() {
190        return Err("pipe: missing source command".to_string());
191    }
192
193    let mut stage_parts: Vec<&str> = raw_parts[1..].iter().map(|s| s.trim()).collect();
194
195    let mut has_trailing_sink = false;
196    if stage_parts.last().is_some_and(|s| s.is_empty()) {
197        has_trailing_sink = true;
198        stage_parts.pop();
199    }
200
201    let mut src_it = source_part.split_whitespace();
202    let Some(src_verb) = src_it.next() else {
203        return Err("pipe: missing source verb".to_string());
204    };
205    let src_args: Vec<&str> = src_it.collect();
206
207    let mut items: Vec<PipeValue> = match src_verb {
208        "ls" => source_ls(backend, world, &src_args)?
209            .into_iter()
210            .map(PipeValue::Id)
211            .collect(),
212        "cat" => source_cat(backend, world, &src_args)?
213            .into_iter()
214            .map(PipeValue::Tree)
215            .collect(),
216        _ => {
217            return Err(format!(
218                "pipe: unsupported source '{}'; try 'ls' or 'cat'",
219                src_verb
220            ));
221        }
222    };
223
224    for stage in stage_parts {
225        if stage.is_empty() {
226            return Err("pipe: empty stage only allowed at end (trailing '|')".to_string());
227        }
228
229        let mut it = stage.split_whitespace();
230        let Some(verb) = it.next() else {
231            return Err("pipe: invalid stage".to_string());
232        };
233
234        match verb {
235            "grep" => {
236                let pattern = it.collect::<Vec<&str>>().join(" ");
237                if pattern.trim().is_empty() {
238                    return Err("pipe: grep requires a pattern".to_string());
239                }
240                items = stage_grep(world, items, pattern.trim());
241            }
242            _ => return Err(format!("pipe: unknown stage '{}'", verb)),
243        }
244    }
245
246    if has_trailing_sink {
247        sink_print_summary(world, items);
248    }
249
250    Ok(true)
251}