use crate::host::{is_callable, with_host, JsObj};
use fusevm::Value;
use indexmap::IndexMap;
use std::io::{self, Write};
pub const METHODS: &[&str] = &[
"createInterface",
"clearLine",
"clearScreenDown",
"cursorTo",
"moveCursor",
"emitKeypressEvents",
];
pub const INTERFACE_METHODS: &[&str] = &[
"question",
"write",
"close",
"pause",
"resume",
"prompt",
"setPrompt",
"getPrompt",
"on",
"once",
"addListener",
"prependListener",
"removeListener",
"off",
"removeAllListeners",
];
pub const PROMISES_METHODS: &[&str] = &["createInterface"];
pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
Some(match method {
"createInterface" => Ok(create_interface(args, false)),
"cursorTo" => {
let x = super::arg_num(args, 1);
let y = args.get(2).filter(|v| !matches!(v, Value::Undef));
let seq = match y {
Some(yv) => format!(
"\x1b[{};{}H",
with_host(|h| h.to_number(yv)) as i64 + 1,
x as i64 + 1
),
None => format!("\x1b[{}G", x as i64 + 1),
};
write_stdout(&seq);
Ok(Value::Bool(true))
}
"moveCursor" => {
let dx = super::arg_num(args, 1) as i64;
let dy = super::arg_num(args, 2) as i64;
let mut seq = String::new();
if dx > 0 {
seq.push_str(&format!("\x1b[{dx}C"));
} else if dx < 0 {
seq.push_str(&format!("\x1b[{}D", -dx));
}
if dy > 0 {
seq.push_str(&format!("\x1b[{dy}B"));
} else if dy < 0 {
seq.push_str(&format!("\x1b[{}A", -dy));
}
write_stdout(&seq);
Ok(Value::Bool(true))
}
"clearLine" => {
let dir = super::arg_num(args, 1);
let seq = if dir < 0.0 {
"\x1b[1K"
} else if dir > 0.0 {
"\x1b[0K"
} else {
"\x1b[2K"
};
write_stdout(seq);
Ok(Value::Bool(true))
}
"clearScreenDown" => {
write_stdout("\x1b[0J");
Ok(Value::Bool(true))
}
"emitKeypressEvents" => Ok(Value::Undef),
_ => return None,
})
}
pub fn construct(args: &[Value]) -> Result<Value, String> {
Ok(create_interface(args, false))
}
pub fn constant(name: &str) -> Option<Value> {
match name {
"Interface" => Some(with_host(|h| h.alloc(JsObj::Builtin("Interface".into())))),
_ => None,
}
}
pub fn promises_call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
match method {
"createInterface" => Some(Ok(create_interface(args, true))),
_ => None,
}
}
fn create_interface(args: &[Value], promises: bool) -> Value {
let (input, output) = match args.first() {
Some(o) if opt_prop(o, "input").is_some() => (
opt_prop(o, "input").unwrap_or(Value::Undef),
opt_prop(o, "output").unwrap_or(Value::Undef),
),
_ => (
args.first().cloned().unwrap_or(Value::Undef),
args.get(1).cloned().unwrap_or(Value::Undef),
),
};
with_host(|h| {
let listeners = h.new_object(IndexMap::new());
let prompt = h.new_str("> ");
let mut m = IndexMap::new();
m.insert("@@native".into(), h.new_str("Interface"));
m.insert("@@input".into(), input);
m.insert("@@output".into(), output);
m.insert("@@prompt".into(), prompt);
m.insert("@@listeners".into(), listeners);
if promises {
let flag = h.new_str("1");
m.insert("@@promises".into(), flag);
}
h.new_object(m)
})
}
pub fn instance_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
match method {
"question" => {
let query = with_host(|h| args.first().map(|v| h.str_of(v)).unwrap_or_default());
write_stdout(&query);
let line = read_line();
if read_hidden(recv, "@@promises") == "1" {
let line_val = with_host(|h| h.new_str(line));
return crate::builtins::promise_resolve_pub(line_val);
}
let cb = args
.iter()
.rev()
.find(|v| with_host(|h| is_callable(h, v)))
.cloned();
if let Some(cb) = cb {
let line_val = with_host(|h| h.new_str(line));
crate::host::invoke(&cb, vec![line_val], None)?;
}
Ok(Value::Undef)
}
"write" => {
let data = with_host(|h| args.first().map(|v| h.str_of(v)).unwrap_or_default());
write_output(recv, &data);
Ok(Value::Undef)
}
"prompt" => {
let p = read_hidden(recv, "@@prompt");
write_output(recv, &p);
Ok(Value::Undef)
}
"setPrompt" => {
let p = with_host(|h| args.first().map(|v| h.str_of(v)).unwrap_or_default());
with_host(|h| {
let pv = h.new_str(p);
if let Some(JsObj::Object(m)) = h.get_mut(recv) {
m.insert("@@prompt".into(), pv);
}
});
Ok(Value::Undef)
}
"getPrompt" => {
let prompt = read_hidden(recv, "@@prompt");
Ok(with_host(|h| h.new_str(prompt)))
}
"on" | "once" | "addListener" | "prependListener" => {
if let (Some(ev), Some(cb)) = (args.first(), args.get(1)) {
let event = with_host(|h| h.str_of(ev));
store_listener(recv, &event, cb.clone());
}
Ok(recv.clone())
}
"removeListener" | "off" | "removeAllListeners" => Ok(recv.clone()),
"close" | "pause" | "resume" => Ok(Value::Undef),
_ => Err(crate::host::type_error(&format!(
"{method} is not a function"
))),
}
}
fn read_hidden(recv: &Value, key: &str) -> String {
with_host(|h| match h.get(recv) {
Some(JsObj::Object(p)) => p.get(key).map(|v| h.str_of(v)).unwrap_or_default(),
_ => String::new(),
})
}
fn store_listener(recv: &Value, event: &str, cb: Value) {
let listeners = with_host(|h| match h.get(recv) {
Some(JsObj::Object(p)) => p.get("@@listeners").cloned(),
_ => None,
});
let Some(listeners) = listeners else { return };
with_host(|h| {
let arr = match h.get(&listeners) {
Some(JsObj::Object(p)) => p.get(event).cloned(),
_ => None,
};
let arr = arr.filter(|a| matches!(h.get(a), Some(JsObj::Array(_))));
match arr {
Some(a) => {
if let Some(JsObj::Array(items)) = h.get_mut(&a) {
items.push(cb);
}
}
None => {
let a = h.new_array(vec![cb]);
if let Some(JsObj::Object(p)) = h.get_mut(&listeners) {
p.insert(event.to_string(), a);
}
}
}
});
}
fn read_line() -> String {
let mut line = String::new();
let _ = io::stdin().read_line(&mut line);
while line.ends_with('\n') || line.ends_with('\r') {
line.pop();
}
line
}
fn write_stdout(s: &str) {
let mut out = io::stdout();
let _ = out.write_all(s.as_bytes());
let _ = out.flush();
}
fn write_output(recv: &Value, s: &str) {
let out = opt_prop(recv, "@@output").unwrap_or(Value::Undef);
if matches!(out, Value::Obj(_)) {
let payload = with_host(|h| h.new_str(s.to_string()));
if crate::host::call_method(&out, "write", vec![payload]).is_ok() {
return;
}
}
write_stdout(s);
}
fn opt_prop(v: &Value, key: &str) -> Option<Value> {
with_host(|h| match h.get(v) {
Some(JsObj::Object(p)) => p.get(key).cloned(),
_ => None,
})
}