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 if stderr {
283 eprintln!("{out}");
284 } else {
285 println!("{out}");
286 }
287}
288
289fn render_table(args: &[Value]) -> Option<String> {
295 let data = args.first().cloned().unwrap_or(Value::Undef);
296 let restrict: Option<Vec<String>> =
297 with_host(|h| match h.get(args.get(1).unwrap_or(&Value::Undef)) {
298 Some(JsObj::Array(items)) => Some(items.iter().map(|v| h.str_of(v)).collect()),
299 _ => None,
300 });
301 let entries: Vec<(String, Value)> = with_host(|h| match h.get(&data) {
303 Some(JsObj::Array(items)) => items
304 .iter()
305 .enumerate()
306 .map(|(i, v)| (i.to_string(), v.clone()))
307 .collect(),
308 Some(JsObj::Object(m)) => m
309 .iter()
310 .filter(|(k, _)| !k.starts_with("@@"))
311 .map(|(k, v)| (k.clone(), v.clone()))
312 .collect(),
313 _ => Vec::new(),
314 });
315 if with_host(|h| !matches!(h.get(&data), Some(JsObj::Array(_) | JsObj::Object(_)))) {
316 return None;
317 }
318
319 let mut columns: Vec<String> = Vec::new();
322 let mut has_values = false;
323 for (_, val) in &entries {
324 match row_keys(val) {
325 Some(keys) => {
326 for k in keys {
327 if !columns.contains(&k) {
328 columns.push(k);
329 }
330 }
331 }
332 None => has_values = true,
333 }
334 }
335 if let Some(r) = &restrict {
336 columns = r.clone();
337 has_values = false;
338 }
339
340 let mut header = Vec::with_capacity(columns.len() + 2);
342 header.push("(index)".to_string());
343 header.extend(columns.iter().cloned());
344 if has_values {
345 header.push("Values".to_string());
346 }
347
348 let mut rows: Vec<Vec<String>> = Vec::with_capacity(entries.len());
349 for (idx, val) in &entries {
350 let is_primitive = row_keys(val).is_none();
351 let mut row = Vec::with_capacity(header.len());
352 row.push(idx.clone());
353 for col in &columns {
354 match row_get(val, col) {
355 Some(cell) => row.push(with_host(|h| h.inspect(&cell))),
356 None => row.push(String::new()),
357 }
358 }
359 if has_values {
360 row.push(if is_primitive {
361 with_host(|h| h.inspect(val))
362 } else {
363 String::new()
364 });
365 }
366 rows.push(row);
367 }
368
369 Some(draw_table(&header, &rows))
370}
371
372fn row_keys(val: &Value) -> Option<Vec<String>> {
375 with_host(|h| match h.get(val) {
376 Some(JsObj::Array(items)) => Some((0..items.len()).map(|i| i.to_string()).collect()),
377 Some(JsObj::Object(m)) => {
378 Some(m.keys().filter(|k| !k.starts_with("@@")).cloned().collect())
379 }
380 _ => None,
381 })
382}
383
384fn row_get(val: &Value, key: &str) -> Option<Value> {
386 with_host(|h| match h.get(val) {
387 Some(JsObj::Array(items)) => key
388 .parse::<usize>()
389 .ok()
390 .and_then(|i| items.get(i).cloned()),
391 Some(JsObj::Object(m)) => m.get(key).cloned(),
392 _ => None,
393 })
394}
395
396fn draw_table(header: &[String], rows: &[Vec<String>]) -> String {
398 let ncols = header.len();
399 let mut widths = vec![0usize; ncols];
400 for (i, cell) in header.iter().enumerate() {
401 widths[i] = cell.chars().count();
402 }
403 for row in rows {
404 for (i, cell) in row.iter().enumerate() {
405 widths[i] = widths[i].max(cell.chars().count());
406 }
407 }
408
409 let rule = |left: &str, mid: &str, right: &str| -> String {
410 let mut s = String::from(left);
411 for (i, w) in widths.iter().enumerate() {
412 if i > 0 {
413 s.push_str(mid);
414 }
415 s.push_str(&"─".repeat(w + 2));
416 }
417 s.push_str(right);
418 s
419 };
420 let render_row = |cells: &[String]| -> String {
421 let mut s = String::from("│");
422 for (i, w) in widths.iter().enumerate() {
423 let cell = cells.get(i).map(String::as_str).unwrap_or("");
424 s.push(' ');
425 s.push_str(&pad_center(cell, *w));
426 s.push_str(" │");
427 }
428 s
429 };
430
431 let mut lines = Vec::with_capacity(rows.len() + 4);
432 lines.push(rule("┌", "┬", "┐"));
433 lines.push(render_row(header));
434 lines.push(rule("├", "┼", "┤"));
435 for row in rows {
436 lines.push(render_row(row));
437 }
438 lines.push(rule("└", "┴", "┘"));
439 lines.join("\n")
440}
441
442fn pad_center(s: &str, w: usize) -> String {
444 let len = s.chars().count();
445 if len >= w {
446 return s.to_string();
447 }
448 let total = w - len;
449 let left = total / 2;
450 let right = total - left;
451 format!("{}{}{}", " ".repeat(left), s, " ".repeat(right))
452}