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 match format_args(args) {
65 Ok(s) => {
66 emit(&s, false);
67 Ok(Value::Undef)
68 }
69 Err(e) => Err(e),
70 }
71 }
72 "error" | "warn" => match format_args(args) {
73 Ok(s) => {
74 emit(&s, true);
75 Ok(Value::Undef)
76 }
77 Err(e) => Err(e),
78 },
79 "dir" => {
82 let s = with_host(|h| h.inspect(&args.first().cloned().unwrap_or(Value::Undef)));
83 emit(&s, false);
84 Ok(Value::Undef)
85 }
86 "trace" => {
90 let msg = match format_args(args) {
91 Ok(s) => s,
92 Err(e) => return Some(Err(e)),
93 };
94 let line = if msg.is_empty() {
95 "Trace".to_string()
96 } else {
97 format!("Trace: {msg}")
98 };
99 emit(&line, true);
100 Ok(Value::Undef)
101 }
102 "assert" => {
105 let ok = with_host(|h| h.truthy(&args.first().cloned().unwrap_or(Value::Undef)));
106 if !ok {
107 let msg = match format_args(&args[1.min(args.len())..]) {
108 Ok(s) => s,
109 Err(e) => return Some(Err(e)),
110 };
111 let line = if msg.is_empty() {
112 "Assertion failed".to_string()
113 } else {
114 format!("Assertion failed: {msg}")
115 };
116 emit(&line, true);
117 }
118 Ok(Value::Undef)
119 }
120 "count" => {
121 let label = label_arg(args, "default");
122 let n = COUNTS.with(|c| {
123 let mut m = c.borrow_mut();
124 let e = m.entry(label.clone()).or_insert(0);
125 *e += 1;
126 *e
127 });
128 emit(&format!("{label}: {n}"), false);
129 Ok(Value::Undef)
130 }
131 "countReset" => {
132 let label = label_arg(args, "default");
133 COUNTS.with(|c| c.borrow_mut().remove(&label));
134 Ok(Value::Undef)
135 }
136 "group" | "groupCollapsed" => {
139 if !args.is_empty() {
140 match format_args(args) {
141 Ok(s) => emit(&s, false),
142 Err(e) => return Some(Err(e)),
143 }
144 }
145 GROUP_DEPTH.with(|d| d.set(d.get() + 1));
146 Ok(Value::Undef)
147 }
148 "groupEnd" => {
149 GROUP_DEPTH.with(|d| d.set(d.get().saturating_sub(1)));
150 Ok(Value::Undef)
151 }
152 "time" => {
153 let label = label_arg(args, "default");
154 TIMERS.with(|t| t.borrow_mut().insert(label, Instant::now()));
155 Ok(Value::Undef)
156 }
157 "timeEnd" | "timeLog" => {
158 let label = label_arg(args, "default");
159 let elapsed = TIMERS.with(|t| {
160 let m = t.borrow();
161 m.get(&label).map(|start| start.elapsed())
162 });
163 match elapsed {
164 Some(d) => {
165 let ms = d.as_secs_f64() * 1000.0;
166 let extra = if args.len() > 1 {
168 match format_args(&args[1..]) {
169 Ok(s) => format!(" {s}"),
170 Err(e) => return Some(Err(e)),
171 }
172 } else {
173 String::new()
174 };
175 emit(&format!("{label}: {ms:.3}ms{extra}"), false);
176 if method == "timeEnd" {
177 TIMERS.with(|t| t.borrow_mut().remove(&label));
178 }
179 }
180 None => emit(
181 &format!("Warning: No such label '{label}' for console.{method}()"),
182 true,
183 ),
184 }
185 Ok(Value::Undef)
186 }
187 "table" => {
190 match render_table(args) {
191 Some(t) => emit(&t, false),
192 None => match format_args(args) {
193 Ok(s) => emit(&s, false),
194 Err(e) => return Some(Err(e)),
195 },
196 }
197 Ok(Value::Undef)
198 }
199 "clear" => {
202 let is_tty = SINK.with(|s| s.borrow().is_some()) || std::io::stdout().is_terminal();
203 if is_tty {
204 emit("\u{1b}[2J\u{1b}[0f", false);
205 }
206 Ok(Value::Undef)
207 }
208 "timeStamp" | "profile" | "profileEnd" => Ok(Value::Undef),
211 _ => return None,
212 })
213}
214
215pub fn construct(args: &[Value]) -> Result<Value, String> {
219 let first = args.first().cloned().unwrap_or(Value::Undef);
220 let is_options = with_host(|h| match h.get(&first) {
222 Some(JsObj::Object(m)) => m.contains_key("stdout"),
223 _ => false,
224 });
225 let (stdout, stderr) = if is_options {
226 let out = crate::builtins::get_property(&first, "stdout").unwrap_or(Value::Undef);
227 let err = match crate::builtins::get_property(&first, "stderr") {
228 Ok(Value::Undef) | Err(_) => out.clone(),
229 Ok(v) => v,
230 };
231 (out, err)
232 } else {
233 let err = args
234 .get(1)
235 .cloned()
236 .filter(|v| !matches!(v, Value::Undef))
237 .unwrap_or_else(|| first.clone());
238 (first, err)
239 };
240 Ok(with_host(|h| {
241 let mut m = IndexMap::new();
242 m.insert("@@native".into(), h.new_str("Console"));
243 m.insert("@@stdout".into(), stdout);
244 m.insert("@@stderr".into(), stderr);
245 h.new_object(m)
246 }))
247}
248
249pub fn instance_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
253 let streams = with_host(|h| match h.get(recv) {
254 Some(JsObj::Object(m)) => Some((
255 m.get("@@stdout").cloned().unwrap_or(Value::Undef),
256 m.get("@@stderr").cloned().unwrap_or(Value::Undef),
257 )),
258 _ => None,
259 });
260 let prev = SINK.with(|s| s.borrow_mut().take());
261 SINK.with(|s| *s.borrow_mut() = streams);
262 let r = call(method, &args).unwrap_or(Ok(Value::Undef));
263 SINK.with(|s| *s.borrow_mut() = prev);
264 r
265}
266
267fn format_args(args: &[Value]) -> Result<String, String> {
270 super::util::format(args)
273}
274
275fn label_arg(args: &[Value], fallback: &str) -> String {
277 match args.first() {
278 Some(v) => with_host(|h| h.str_of(v)),
279 None => fallback.to_string(),
280 }
281}
282
283fn emit(line: &str, stderr: bool) {
287 let depth = GROUP_DEPTH.with(|d| d.get());
288 let out = if depth == 0 {
289 line.to_string()
290 } else {
291 let pad = " ".repeat(depth);
292 format!("{pad}{}", line.replace('\n', &format!("\n{pad}")))
293 };
294 let stream = SINK.with(|s| {
296 s.borrow().as_ref().and_then(|(o, e)| {
297 let target = if stderr { e } else { o };
298 matches!(target, Value::Obj(_)).then(|| target.clone())
299 })
300 });
301 if let Some(stream) = stream {
302 let payload = with_host(|h| h.new_str(format!("{out}\n")));
303 if crate::host::call_method(&stream, "write", vec![payload]).is_ok() {
304 return;
305 }
306 }
307 with_host(|h| h.write_out(&format!("{out}\n"), stderr));
308}
309
310fn render_table(args: &[Value]) -> Option<String> {
316 let data = args.first().cloned().unwrap_or(Value::Undef);
317 let restrict: Option<Vec<String>> =
318 with_host(|h| match h.get(args.get(1).unwrap_or(&Value::Undef)) {
319 Some(JsObj::Array(items)) => Some(items.iter().map(|v| h.str_of(v)).collect()),
320 _ => None,
321 });
322 let entries: Vec<(String, Value)> = with_host(|h| match h.get(&data) {
324 Some(JsObj::Array(items)) => items
325 .iter()
326 .enumerate()
327 .map(|(i, v)| (i.to_string(), v.clone()))
328 .collect(),
329 Some(JsObj::Object(m)) => m
330 .iter()
331 .filter(|(k, _)| !k.starts_with("@@"))
332 .map(|(k, v)| (k.clone(), v.clone()))
333 .collect(),
334 _ => Vec::new(),
335 });
336 if with_host(|h| !matches!(h.get(&data), Some(JsObj::Array(_) | JsObj::Object(_)))) {
337 return None;
338 }
339
340 let mut columns: Vec<String> = Vec::new();
343 let mut has_values = false;
344 for (_, val) in &entries {
345 match row_keys(val) {
346 Some(keys) => {
347 for k in keys {
348 if !columns.contains(&k) {
349 columns.push(k);
350 }
351 }
352 }
353 None => has_values = true,
354 }
355 }
356 if let Some(r) = &restrict {
357 columns = r.clone();
358 has_values = false;
359 }
360
361 let mut header = Vec::with_capacity(columns.len() + 2);
363 header.push("(index)".to_string());
364 header.extend(columns.iter().cloned());
365 if has_values {
366 header.push("Values".to_string());
367 }
368
369 let mut rows: Vec<Vec<String>> = Vec::with_capacity(entries.len());
370 for (idx, val) in &entries {
371 let is_primitive = row_keys(val).is_none();
372 let mut row = Vec::with_capacity(header.len());
373 row.push(idx.clone());
374 for col in &columns {
375 match row_get(val, col) {
376 Some(cell) => row.push(with_host(|h| h.inspect(&cell))),
377 None => row.push(String::new()),
378 }
379 }
380 if has_values {
381 row.push(if is_primitive {
382 with_host(|h| h.inspect(val))
383 } else {
384 String::new()
385 });
386 }
387 rows.push(row);
388 }
389
390 Some(draw_table(&header, &rows))
391}
392
393fn row_keys(val: &Value) -> Option<Vec<String>> {
396 with_host(|h| match h.get(val) {
397 Some(JsObj::Array(items)) => Some((0..items.len()).map(|i| i.to_string()).collect()),
398 Some(JsObj::Object(m)) => {
399 Some(m.keys().filter(|k| !k.starts_with("@@")).cloned().collect())
400 }
401 _ => None,
402 })
403}
404
405fn row_get(val: &Value, key: &str) -> Option<Value> {
407 with_host(|h| match h.get(val) {
408 Some(JsObj::Array(items)) => key
409 .parse::<usize>()
410 .ok()
411 .and_then(|i| items.get(i).cloned()),
412 Some(JsObj::Object(m)) => m.get(key).cloned(),
413 _ => None,
414 })
415}
416
417fn draw_table(header: &[String], rows: &[Vec<String>]) -> String {
419 let ncols = header.len();
420 let mut widths = vec![0usize; ncols];
421 for (i, cell) in header.iter().enumerate() {
422 widths[i] = cell.chars().count();
423 }
424 for row in rows {
425 for (i, cell) in row.iter().enumerate() {
426 widths[i] = widths[i].max(cell.chars().count());
427 }
428 }
429
430 let rule = |left: &str, mid: &str, right: &str| -> String {
431 let mut s = String::from(left);
432 for (i, w) in widths.iter().enumerate() {
433 if i > 0 {
434 s.push_str(mid);
435 }
436 s.push_str(&"─".repeat(w + 2));
437 }
438 s.push_str(right);
439 s
440 };
441 let render_row = |cells: &[String]| -> String {
442 let mut s = String::from("│");
443 for (i, w) in widths.iter().enumerate() {
444 let cell = cells.get(i).map(String::as_str).unwrap_or("");
445 s.push(' ');
446 s.push_str(&pad_center(cell, *w));
447 s.push_str(" │");
448 }
449 s
450 };
451
452 let mut lines = Vec::with_capacity(rows.len() + 4);
453 lines.push(rule("┌", "┬", "┐"));
454 lines.push(render_row(header));
455 lines.push(rule("├", "┼", "┤"));
456 for row in rows {
457 lines.push(render_row(row));
458 }
459 lines.push(rule("└", "┴", "┘"));
460 lines.join("\n")
461}
462
463fn pad_center(s: &str, w: usize) -> String {
465 let len = s.chars().count();
466 if len >= w {
467 return s.to_string();
468 }
469 let total = w - len;
470 let left = total / 2;
471 let right = total - left;
472 format!("{}{}{}", " ".repeat(left), s, " ".repeat(right))
473}