use funct::{Fault, Funct, Value};
use std::collections::{HashMap, VecDeque};
use std::io::{BufRead, BufReader, Write};
use std::process::{Child, ChildStdin, Command, Stdio};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
const MAX_BUFFERED_LINES: usize = 4096;
struct Proc {
child: Child,
stdin: ChildStdin,
lines: Arc<Mutex<VecDeque<String>>>,
}
#[derive(Default)]
struct ProcRegistry {
next: i64,
procs: HashMap<i64, Proc>,
}
impl ProcRegistry {
fn spawn(&mut self, cmd: &str, args: &[String]) -> i64 {
let spawned = Command::new(cmd)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn();
let mut child = match spawned {
Ok(c) => c,
Err(_) => return -1, };
let stdin = child.stdin.take().expect("stdin piped");
let stdout = child.stdout.take().expect("stdout piped");
let lines: Arc<Mutex<VecDeque<String>>> = Arc::default();
let sink = lines.clone();
std::thread::spawn(move || {
for line in BufReader::new(stdout).lines() {
let Ok(line) = line else { break };
let mut q = sink.lock().unwrap();
if q.len() >= MAX_BUFFERED_LINES {
q.pop_front();
}
q.push_back(line);
}
});
self.next += 1;
let id = self.next;
self.procs.insert(
id,
Proc {
child,
stdin,
lines,
},
);
id
}
}
impl Drop for ProcRegistry {
fn drop(&mut self) {
for (_, p) in self.procs.iter_mut() {
let _ = p.child.kill();
let _ = p.child.wait();
}
}
}
pub struct WidgetHost {
pub render_requested: Arc<AtomicBool>,
pub animating: Arc<AtomicBool>,
pub clipboard: Arc<Mutex<String>>,
}
impl WidgetHost {
pub fn take_render_request(&self) -> bool {
self.render_requested.swap(false, Ordering::SeqCst)
}
}
pub fn install_widget_host(vm: &mut Funct) -> WidgetHost {
vm.set_module_root(concat!(env!("CARGO_MANIFEST_DIR"), "/examples/widgets"));
let registry: Arc<Mutex<ProcRegistry>> = Arc::default();
let render_requested = Arc::new(AtomicBool::new(false));
let animating = Arc::new(AtomicBool::new(false));
let clipboard = Arc::new(Mutex::new(String::new()));
let need = |name: &str, args: &[Value], n: usize| -> Result<(), Fault> {
if args.len() != n {
return Err(Fault::new(format!(
"{} expects {} argument(s), got {}",
name,
n,
args.len()
)));
}
Ok(())
};
let handle_of = |name: &str, v: &Value| -> Result<i64, Fault> {
match v {
Value::Int(i) => Ok(*i),
other => Err(Fault::new(format!(
"{}: handle must be Int, got {}",
name,
other.type_name()
))),
}
};
let reg = registry.clone();
vm.register_raw("proc_spawn", move |_vm, args| {
let (cmd, extra): (String, Vec<String>) = match args.as_slice() {
[Value::Str(c)] => (c.to_string(), vec![]),
[Value::Str(c), Value::List(items)] => {
let mut a = Vec::new();
for it in items.iter() {
match it {
Value::Str(s) => a.push(s.to_string()),
other => {
return Err(Fault::new(format!(
"proc_spawn: args must be strings, got {}",
other.type_name()
)))
}
}
}
(c.to_string(), a)
}
_ => return Err(Fault::new("proc_spawn expects (cmd) or (cmd, [args])")),
};
Ok(Value::Int(reg.lock().unwrap().spawn(&cmd, &extra)))
});
let reg = registry.clone();
vm.register_raw("proc_write", move |_vm, args| {
need("proc_write", &args, 2)?;
let id = handle_of("proc_write", &args[0])?;
let line = match &args[1] {
Value::Str(s) => s.to_string(),
other => {
return Err(Fault::new(format!(
"proc_write: expected Str, got {}",
other.type_name()
)))
}
};
let mut r = reg.lock().unwrap();
let ok = match r.procs.get_mut(&id) {
Some(p) => writeln!(p.stdin, "{}", line)
.and_then(|_| p.stdin.flush())
.is_ok(),
None => false,
};
Ok(Value::Bool(ok))
});
let reg = registry.clone();
vm.register_raw("proc_read", move |_vm, args| {
need("proc_read", &args, 1)?;
let id = handle_of("proc_read", &args[0])?;
let r = reg.lock().unwrap();
let line = r
.procs
.get(&id)
.and_then(|p| p.lines.lock().unwrap().pop_front())
.unwrap_or_default();
Ok(Value::str(line))
});
let reg = registry.clone();
vm.register_raw("proc_alive", move |_vm, args| {
need("proc_alive", &args, 1)?;
let id = handle_of("proc_alive", &args[0])?;
let mut r = reg.lock().unwrap();
let alive = match r.procs.get_mut(&id) {
Some(p) => matches!(p.child.try_wait(), Ok(None)),
None => false,
};
Ok(Value::Bool(alive))
});
let reg = registry.clone();
vm.register_raw("proc_kill", move |_vm, args| {
need("proc_kill", &args, 1)?;
let id = handle_of("proc_kill", &args[0])?;
let mut r = reg.lock().unwrap();
if let Some(mut p) = r.procs.remove(&id) {
let _ = p.child.kill();
let _ = p.child.wait();
}
Ok(Value::Unit)
});
let rr = render_requested.clone();
vm.register_raw("request_render", move |_vm, _args| {
rr.store(true, Ordering::SeqCst);
Ok(Value::Unit)
});
let an = animating.clone();
vm.register_raw("set_animating", move |_vm, args| {
need("set_animating", &args, 1)?;
match &args[0] {
Value::Bool(b) => {
an.store(*b, Ordering::SeqCst);
Ok(Value::Unit)
}
other => Err(Fault::new(format!(
"set_animating: expected Bool, got {}",
other.type_name()
))),
}
});
vm.register0("time", || {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs_f64())
.unwrap_or(0.0)
});
vm.register1("host_env", |name: String| {
std::env::var(name).unwrap_or_default()
});
vm.register1("widget_asset", |rel: String| format!("assets/{}", rel));
vm.register_raw("host_log", |_vm, args| {
let parts: Vec<String> = args.iter().map(|v| format!("{}", v)).collect();
eprintln!("[widget] {}", parts.join(" "));
Ok(Value::Unit)
});
let cb = clipboard.clone();
vm.register_raw("clipboard_set", move |_vm, args| {
need("clipboard_set", &args, 1)?;
match &args[0] {
Value::Str(s) => {
*cb.lock().unwrap() = s.to_string();
Ok(Value::Bool(true))
}
other => Err(Fault::new(format!(
"clipboard_set: expected Str, got {}",
other.type_name()
))),
}
});
vm.register2("uniform_set", |_name: String, _value: Value| {});
vm.register5(
"mask_paint",
|_name: String, _x: f64, _y: f64, _r: f64, _v: f64| {},
);
vm.register3("oklch", |l: f64, c: f64, h: f64| {
format!("oklch({l:.3} {c:.3} {h:.1})")
});
vm.register2("emit", |_kind: String, _payload: Value| {});
WidgetHost {
render_requested,
animating,
clipboard,
}
}