use std::time::{Duration, Instant};
use fusevm::{Op, Value, VM};
use crate::compiler::{CompileError, Compiler};
use crate::parser::Word;
use crate::runtime::{to_tcl_string, Shared, TclError};
impl Compiler {
pub(crate) fn cmd_event_op(
&mut self,
id: u16,
name: &str,
args: &[Word],
) -> Result<(), CompileError> {
let count = u8::try_from(args.len())
.map_err(|_| self.err(format!("too many arguments for \"{name}\"")))?;
for arg in args {
self.word(arg)?;
}
self.emit(Op::Extended(id, count), 1 - args.len() as i32);
Ok(())
}
}
#[cfg(feature = "tk")]
const POLL_SLICE: Duration = Duration::from_millis(10);
mod flags {
pub const DONT_WAIT: i32 = 1 << 1;
pub const WINDOW: i32 = 1 << 2;
pub const FILE: i32 = 1 << 3;
pub const TIMER: i32 = 1 << 4;
pub const IDLE: i32 = 1 << 5;
pub const ALL: i32 = WINDOW | FILE | TIMER | IDLE;
}
fn state(interp: &Shared) -> std::sync::MutexGuard<'_, crate::runtime::State> {
interp.lock().expect("interpreter lock")
}
pub(crate) struct After {
id: u64,
script: String,
due: Option<Instant>,
}
#[derive(Default)]
pub(crate) struct Afters {
queue: Vec<After>,
next_id: u64,
}
impl Afters {
fn register(&mut self, script: String, due: Option<Instant>) -> String {
let id = self.next_id;
self.next_id += 1;
self.queue.insert(0, After { id, script, due });
format!("after#{id}")
}
fn take_due_timers(&mut self, now: Instant) -> Vec<String> {
let newest = self.next_id;
let mut due: Vec<(Instant, u64, String)> = Vec::new();
self.queue.retain(|a| match a.due {
Some(at) if at <= now && a.id < newest => {
due.push((at, a.id, a.script.clone()));
false
}
_ => true,
});
due.sort_by_key(|(at, id, _)| (*at, *id));
due.into_iter().map(|(_, _, script)| script).collect()
}
fn take_idle(&mut self) -> Option<String> {
let at = self.queue.iter().rposition(|a| a.due.is_none())?;
Some(self.queue.remove(at).script)
}
fn soonest(&self) -> Option<Instant> {
self.queue.iter().filter_map(|a| a.due).min()
}
}
pub(crate) fn after_op(interp: &Shared, vm: &mut VM, argc: u8) -> Result<(), TclError> {
let args = pop_args(vm, argc);
let result = after(interp, &args).map_err(TclError::plain)?;
vm.push(Value::Str(std::sync::Arc::new(result)));
Ok(())
}
fn after(interp: &Shared, args: &[String]) -> Result<String, String> {
let Some(first) = args.first() else {
return Err("wrong # args: should be \"after option ?arg ...?\"".to_string());
};
if let Ok(ms) = first.trim().parse::<i64>() {
return after_delay(interp, ms.max(0), &args[1..]);
}
let Some(sub) = crate::cmd_string::resolve(first, &["cancel", "idle", "info"]) else {
return Err(format!(
"bad argument \"{first}\": must be cancel, idle, info, or an integer"
));
};
match sub {
"cancel" => after_cancel(interp, &args[1..]),
"idle" => after_idle(interp, &args[1..]),
_ => after_info(interp, &args[1..]),
}
}
fn after_delay(interp: &Shared, ms: i64, scripts: &[String]) -> Result<String, String> {
if scripts.is_empty() {
sleep(Duration::from_millis(ms as u64));
return Ok(String::new());
}
let due = Instant::now() + Duration::from_millis(ms as u64);
Ok(state(interp).afters.register(joined(scripts), Some(due)))
}
fn after_idle(interp: &Shared, scripts: &[String]) -> Result<String, String> {
if scripts.is_empty() {
return Err("wrong # args: should be \"after idle script ?script ...?\"".to_string());
}
Ok(state(interp).afters.register(joined(scripts), None))
}
fn after_cancel(interp: &Shared, rest: &[String]) -> Result<String, String> {
if rest.is_empty() {
return Err("wrong # args: should be \"after cancel id|command\"".to_string());
}
let text = joined(rest);
let mut guard = state(interp);
let afters = &mut guard.afters;
let found = afters
.queue
.iter()
.position(|a| a.script == text)
.or_else(|| afters.queue.iter().position(|a| id_of(&text) == Some(a.id)));
if let Some(at) = found {
afters.queue.remove(at);
}
Ok(String::new())
}
fn after_info(interp: &Shared, rest: &[String]) -> Result<String, String> {
let guard = state(interp);
let afters = &guard.afters;
match rest {
[] => {
let ids: Vec<String> = afters
.queue
.iter()
.map(|a| format!("after#{}", a.id))
.collect();
Ok(crate::list::join(&ids))
}
[id] => {
let found = id_of(id).and_then(|id| afters.queue.iter().find(|a| a.id == id));
match found {
Some(a) => {
let kind = if a.due.is_some() { "timer" } else { "idle" };
Ok(crate::list::join(&[a.script.clone(), kind.to_string()]))
}
None => Err(format!("event \"{id}\" doesn't exist")),
}
}
_ => Err("wrong # args: should be \"after info ?id?\"".to_string()),
}
}
fn id_of(text: &str) -> Option<u64> {
text.strip_prefix("after#")?.parse().ok()
}
fn joined(scripts: &[String]) -> String {
match scripts {
[one] => one.clone(),
many => crate::cmd_list::concat(many),
}
}
pub(crate) fn update_op(interp: &Shared, vm: &mut VM, argc: u8) -> Result<(), TclError> {
let args = pop_args(vm, argc);
let mask = match args.as_slice() {
[] => flags::ALL,
[one] if one == "idletasks" => flags::IDLE,
[one] => {
return Err(TclError::plain(format!(
"bad option \"{one}\": must be idletasks"
)))
}
_ => {
return Err(TclError::plain(
"wrong # args: should be \"update ?idletasks?\"",
))
}
};
while service_one(interp, vm, mask | flags::DONT_WAIT)? {}
vm.push(empty());
Ok(())
}
pub(crate) fn vwait_op(interp: &Shared, vm: &mut VM, argc: u8) -> Result<(), TclError> {
let args = pop_args(vm, argc);
let name = match args.as_slice() {
[] => {
while service_one(interp, vm, flags::ALL | flags::DONT_WAIT)? {}
vm.push(empty());
return Ok(());
}
[name] => name.clone(),
_ => {
return Err(TclError::plain(
"\"vwait\" takes at most one variable name in this phase",
))
}
};
crate::runtime::flush_globals(vm, interp);
let before = crate::runtime::global_value(interp, &name);
loop {
if crate::runtime::global_value(interp, &name) != before {
break;
}
if !service_one(interp, vm, flags::ALL)? {
crate::runtime::reseed_globals(vm, interp);
return Err(TclError::plain(
"can't wait for variable(s)/channel(s): would wait forever",
));
}
}
crate::runtime::reseed_globals(vm, interp);
vm.push(empty());
Ok(())
}
fn service_one(interp: &Shared, vm: &mut VM, mask: i32) -> Result<bool, TclError> {
loop {
if mask & flags::TIMER != 0 {
let due = state(interp).afters.take_due_timers(Instant::now());
if !due.is_empty() {
for script in due {
run_script(interp, vm, &script);
}
return Ok(true);
}
}
#[cfg(feature = "tk")]
if unsafe { crate::tk::notifier::do_one_event_slot(mask | flags::DONT_WAIT) } != 0 {
return Ok(true);
}
if mask & flags::IDLE != 0 {
let idle = state(interp).afters.take_idle();
if let Some(script) = idle {
run_script(interp, vm, &script);
return Ok(true);
}
}
if mask & flags::DONT_WAIT != 0 {
return Ok(false);
}
if let Some(wait) = next_deadline(interp, mask) {
sleep(wait);
continue;
}
#[cfg(feature = "tk")]
{
return Ok(unsafe { crate::tk::notifier::do_one_event_slot(mask) } != 0);
}
#[cfg(not(feature = "tk"))]
return Ok(false);
}
}
fn next_deadline(interp: &Shared, mask: i32) -> Option<Duration> {
if mask & flags::TIMER == 0 {
return None;
}
let now = Instant::now();
let wait = state(interp)
.afters
.soonest()
.map(|due| due.saturating_duration_since(now))?;
#[cfg(feature = "tk")]
let wait = wait.min(POLL_SLICE);
Some(wait)
}
fn run_script(interp: &Shared, vm: &mut VM, script: &str) {
crate::runtime::flush_globals(vm, interp);
let outcome = crate::runtime::run_source(interp, script);
crate::runtime::reseed_globals(vm, interp);
if let Err(e) = outcome {
eprintln!("{}\n (\"after\" script)", e.msg);
}
}
fn sleep(d: Duration) {
#[cfg(feature = "tk")]
{
let ms = d.as_millis().min(i32::MAX as u128) as i32;
if ms > 0 {
unsafe { crate::tk::notifier::sleep_ms(ms) };
return;
}
}
std::thread::sleep(d);
}
fn pop_args(vm: &mut VM, argc: u8) -> Vec<String> {
let mut args: Vec<String> = (0..argc).map(|_| to_tcl_string(&vm.pop())).collect();
args.reverse();
args
}
fn empty() -> Value {
Value::Str(std::sync::Arc::new(String::new()))
}