1use crate::host::{with_host, JsObj};
10use fusevm::Value;
11use indexmap::IndexMap;
12use std::cell::{Cell, RefCell};
13use std::collections::HashMap;
14use std::io::IsTerminal;
15use std::time::Instant;
16
17pub const METHODS: &[&str] = &[
18 "log",
19 "info",
20 "debug",
21 "error",
22 "warn",
23 "dir",
24 "dirxml",
25 "trace",
26 "assert",
27 "count",
28 "countReset",
29 "group",
30 "groupCollapsed",
31 "groupEnd",
32 "time",
33 "timeEnd",
34 "timeLog",
35 "table",
36 "clear",
37 "timeStamp",
38 "profile",
39 "profileEnd",
40];
41
42pub const CONSOLE_METHODS: &[&str] = METHODS;
45
46thread_local! {
47 static GROUP_DEPTH: Cell<usize> = const { Cell::new(0) };
49 static COUNTS: RefCell<HashMap<String, u64>> = RefCell::new(HashMap::new());
51 static TIMERS: RefCell<HashMap<String, Instant>> = RefCell::new(HashMap::new());
53 static SINK: RefCell<Option<(Value, Value)>> = const { RefCell::new(None) };
57}
58
59pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
60 Some(match method {
61 "log" | "info" | "debug" | "dirxml" => {
62 emit(&format_args(args), false);
63 Ok(Value::Undef)
64 }
65 "error" | "warn" => {
66 emit(&format_args(args), true);
67 Ok(Value::Undef)
68 }
69 "dir" => {
72 let s = with_host(|h| h.inspect(&args.first().cloned().unwrap_or(Value::Undef)));
73 emit(&s, false);
74 Ok(Value::Undef)
75 }
76 "trace" => {
80 let msg = format_args(args);
81 let line = if msg.is_empty() {
82 "Trace".to_string()
83 } else {
84 format!("Trace: {msg}")
85 };
86 emit(&line, true);
87 Ok(Value::Undef)
88 }
89 "assert" => {
92 let ok = with_host(|h| h.truthy(&args.first().cloned().unwrap_or(Value::Undef)));
93 if !ok {
94 let msg = format_args(&args[1.min(args.len())..]);
95 let line = if msg.is_empty() {
96 "Assertion failed".to_string()
97 } else {
98 format!("Assertion failed: {msg}")
99 };
100 emit(&line, true);
101 }
102 Ok(Value::Undef)
103 }
104 "count" => {
105 let label = label_arg(args, "default");
106 let n = COUNTS.with(|c| {
107 let mut m = c.borrow_mut();
108 let e = m.entry(label.clone()).or_insert(0);
109 *e += 1;
110 *e
111 });
112 emit(&format!("{label}: {n}"), false);
113 Ok(Value::Undef)
114 }
115 "countReset" => {
116 let label = label_arg(args, "default");
117 COUNTS.with(|c| c.borrow_mut().remove(&label));
118 Ok(Value::Undef)
119 }
120 "group" | "groupCollapsed" => {
123 if !args.is_empty() {
124 emit(&format_args(args), false);
125 }
126 GROUP_DEPTH.with(|d| d.set(d.get() + 1));
127 Ok(Value::Undef)
128 }
129 "groupEnd" => {
130 GROUP_DEPTH.with(|d| d.set(d.get().saturating_sub(1)));
131 Ok(Value::Undef)
132 }
133 "time" => {
134 let label = label_arg(args, "default");
135 TIMERS.with(|t| t.borrow_mut().insert(label, Instant::now()));
136 Ok(Value::Undef)
137 }
138 "timeEnd" | "timeLog" => {
139 let label = label_arg(args, "default");
140 let elapsed = TIMERS.with(|t| {
141 let m = t.borrow();
142 m.get(&label).map(|start| start.elapsed())
143 });
144 match elapsed {
145 Some(d) => {
146 let ms = d.as_secs_f64() * 1000.0;
147 let extra = if args.len() > 1 {
149 format!(" {}", format_args(&args[1..]))
150 } else {
151 String::new()
152 };
153 emit(&format!("{label}: {ms:.3}ms{extra}"), false);
154 if method == "timeEnd" {
155 TIMERS.with(|t| t.borrow_mut().remove(&label));
156 }
157 }
158 None => emit(
159 &format!("Warning: No such label '{label}' for console.{method}()"),
160 true,
161 ),
162 }
163 Ok(Value::Undef)
164 }
165 "table" => {
168 match render_table(args) {
169 Some(t) => emit(&t, false),
170 None => emit(&format_args(args), false),
171 }
172 Ok(Value::Undef)
173 }
174 "clear" => {
177 let is_tty = SINK.with(|s| s.borrow().is_some()) || std::io::stdout().is_terminal();
178 if is_tty {
179 emit("\u{1b}[2J\u{1b}[0f", false);
180 }
181 Ok(Value::Undef)
182 }
183 "timeStamp" | "profile" | "profileEnd" => Ok(Value::Undef),
186 _ => return None,
187 })
188}
189
190pub fn construct(args: &[Value]) -> Result<Value, String> {
194 let first = args.first().cloned().unwrap_or(Value::Undef);
195 let is_options = with_host(|h| match h.get(&first) {
197 Some(JsObj::Object(m)) => m.contains_key("stdout"),
198 _ => false,
199 });
200 let (stdout, stderr) = if is_options {
201 let out = crate::builtins::get_property(&first, "stdout").unwrap_or(Value::Undef);
202 let err = match crate::builtins::get_property(&first, "stderr") {
203 Ok(Value::Undef) | Err(_) => out.clone(),
204 Ok(v) => v,
205 };
206 (out, err)
207 } else {
208 let err = args
209 .get(1)
210 .cloned()
211 .filter(|v| !matches!(v, Value::Undef))
212 .unwrap_or_else(|| first.clone());
213 (first, err)
214 };
215 Ok(with_host(|h| {
216 let mut m = IndexMap::new();
217 m.insert("@@native".into(), h.new_str("Console"));
218 m.insert("@@stdout".into(), stdout);
219 m.insert("@@stderr".into(), stderr);
220 h.new_object(m)
221 }))
222}
223
224pub fn instance_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
228 let streams = with_host(|h| match h.get(recv) {
229 Some(JsObj::Object(m)) => Some((
230 m.get("@@stdout").cloned().unwrap_or(Value::Undef),
231 m.get("@@stderr").cloned().unwrap_or(Value::Undef),
232 )),
233 _ => None,
234 });
235 let prev = SINK.with(|s| s.borrow_mut().take());
236 SINK.with(|s| *s.borrow_mut() = streams);
237 let r = call(method, &args).unwrap_or(Ok(Value::Undef));
238 SINK.with(|s| *s.borrow_mut() = prev);
239 r
240}
241
242fn format_args(args: &[Value]) -> String {
245 super::util::format(args)
248}
249
250fn label_arg(args: &[Value], fallback: &str) -> String {
252 match args.first() {
253 Some(v) => with_host(|h| h.str_of(v)),
254 None => fallback.to_string(),
255 }
256}
257
258fn emit(line: &str, stderr: bool) {
262 let depth = GROUP_DEPTH.with(|d| d.get());
263 let out = if depth == 0 {
264 line.to_string()
265 } else {
266 let pad = " ".repeat(depth);
267 format!("{pad}{}", line.replace('\n', &format!("\n{pad}")))
268 };
269 let stream = SINK.with(|s| {
271 s.borrow().as_ref().and_then(|(o, e)| {
272 let target = if stderr { e } else { o };
273 matches!(target, Value::Obj(_)).then(|| target.clone())
274 })
275 });
276 if let Some(stream) = stream {
277 let payload = with_host(|h| h.new_str(format!("{out}\n")));
278 if crate::host::call_method(&stream, "write", vec![payload]).is_ok() {
279 return;
280 }
281 }
282 with_host(|h| h.write_out(&format!("{out}\n"), stderr));
283}
284
285fn render_table(args: &[Value]) -> Option<String> {
291 let data = args.first().cloned().unwrap_or(Value::Undef);
292 let restrict: Option<Vec<String>> =
293 with_host(|h| match h.get(args.get(1).unwrap_or(&Value::Undef)) {
294 Some(JsObj::Array(items)) => Some(items.iter().map(|v| h.str_of(v)).collect()),
295 _ => None,
296 });
297 let entries: Vec<(String, Value)> = with_host(|h| match h.get(&data) {
299 Some(JsObj::Array(items)) => items
300 .iter()
301 .enumerate()
302 .map(|(i, v)| (i.to_string(), v.clone()))
303 .collect(),
304 Some(JsObj::Object(m)) => m
305 .iter()
306 .filter(|(k, _)| !k.starts_with("@@"))
307 .map(|(k, v)| (k.clone(), v.clone()))
308 .collect(),
309 _ => Vec::new(),
310 });
311 if with_host(|h| !matches!(h.get(&data), Some(JsObj::Array(_) | JsObj::Object(_)))) {
312 return None;
313 }
314
315 let mut columns: Vec<String> = Vec::new();
318 let mut has_values = false;
319 for (_, val) in &entries {
320 match row_keys(val) {
321 Some(keys) => {
322 for k in keys {
323 if !columns.contains(&k) {
324 columns.push(k);
325 }
326 }
327 }
328 None => has_values = true,
329 }
330 }
331 if let Some(r) = &restrict {
332 columns = r.clone();
333 has_values = false;
334 }
335
336 let mut header = Vec::with_capacity(columns.len() + 2);
338 header.push("(index)".to_string());
339 header.extend(columns.iter().cloned());
340 if has_values {
341 header.push("Values".to_string());
342 }
343
344 let mut rows: Vec<Vec<String>> = Vec::with_capacity(entries.len());
345 for (idx, val) in &entries {
346 let is_primitive = row_keys(val).is_none();
347 let mut row = Vec::with_capacity(header.len());
348 row.push(idx.clone());
349 for col in &columns {
350 match row_get(val, col) {
351 Some(cell) => row.push(with_host(|h| h.inspect(&cell))),
352 None => row.push(String::new()),
353 }
354 }
355 if has_values {
356 row.push(if is_primitive {
357 with_host(|h| h.inspect(val))
358 } else {
359 String::new()
360 });
361 }
362 rows.push(row);
363 }
364
365 Some(draw_table(&header, &rows))
366}
367
368fn row_keys(val: &Value) -> Option<Vec<String>> {
371 with_host(|h| match h.get(val) {
372 Some(JsObj::Array(items)) => Some((0..items.len()).map(|i| i.to_string()).collect()),
373 Some(JsObj::Object(m)) => {
374 Some(m.keys().filter(|k| !k.starts_with("@@")).cloned().collect())
375 }
376 _ => None,
377 })
378}
379
380fn row_get(val: &Value, key: &str) -> Option<Value> {
382 with_host(|h| match h.get(val) {
383 Some(JsObj::Array(items)) => key
384 .parse::<usize>()
385 .ok()
386 .and_then(|i| items.get(i).cloned()),
387 Some(JsObj::Object(m)) => m.get(key).cloned(),
388 _ => None,
389 })
390}
391
392fn draw_table(header: &[String], rows: &[Vec<String>]) -> String {
394 let ncols = header.len();
395 let mut widths = vec![0usize; ncols];
396 for (i, cell) in header.iter().enumerate() {
397 widths[i] = cell.chars().count();
398 }
399 for row in rows {
400 for (i, cell) in row.iter().enumerate() {
401 widths[i] = widths[i].max(cell.chars().count());
402 }
403 }
404
405 let rule = |left: &str, mid: &str, right: &str| -> String {
406 let mut s = String::from(left);
407 for (i, w) in widths.iter().enumerate() {
408 if i > 0 {
409 s.push_str(mid);
410 }
411 s.push_str(&"─".repeat(w + 2));
412 }
413 s.push_str(right);
414 s
415 };
416 let render_row = |cells: &[String]| -> String {
417 let mut s = String::from("│");
418 for (i, w) in widths.iter().enumerate() {
419 let cell = cells.get(i).map(String::as_str).unwrap_or("");
420 s.push(' ');
421 s.push_str(&pad_center(cell, *w));
422 s.push_str(" │");
423 }
424 s
425 };
426
427 let mut lines = Vec::with_capacity(rows.len() + 4);
428 lines.push(rule("┌", "┬", "┐"));
429 lines.push(render_row(header));
430 lines.push(rule("├", "┼", "┤"));
431 for row in rows {
432 lines.push(render_row(row));
433 }
434 lines.push(rule("└", "┴", "┘"));
435 lines.join("\n")
436}
437
438fn pad_center(s: &str, w: usize) -> String {
440 let len = s.chars().count();
441 if len >= w {
442 return s.to_string();
443 }
444 let total = w - len;
445 let left = total / 2;
446 let right = total - left;
447 format!("{}{}{}", " ".repeat(left), s, " ".repeat(right))
448}