mittens_engine/engine/repl/
pipe.rs1use 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 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 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 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 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
167pub 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}