Add tick / delay / untick timer commands.
This commit is contained in:
parent
cfe2e6cd6d
commit
56edc3d484
@ -181,6 +181,7 @@ pub fn install_lua_globals(
|
|||||||
register_command!(connect_mud);
|
register_command!(connect_mud);
|
||||||
register_stateless_command!(create_match_table);
|
register_stateless_command!(create_match_table);
|
||||||
register_command!(cmd_delete_logs, "deletelogs");
|
register_command!(cmd_delete_logs, "deletelogs");
|
||||||
|
register_command!(delay);
|
||||||
register_command!(delete_mud);
|
register_command!(delete_mud);
|
||||||
register_command!(cmd_download_logs, "downloadlogs");
|
register_command!(cmd_download_logs, "downloadlogs");
|
||||||
register_command!(echo);
|
register_command!(echo);
|
||||||
@ -191,9 +192,12 @@ pub fn install_lua_globals(
|
|||||||
register_command!(mud_log, "log");
|
register_command!(mud_log, "log");
|
||||||
register_command!(panel_merge);
|
register_command!(panel_merge);
|
||||||
register_command!(sendmud_raw);
|
register_command!(sendmud_raw);
|
||||||
|
register_command!(tick);
|
||||||
register_stateless_command!(mud_trigger, "untrigger");
|
register_stateless_command!(mud_trigger, "untrigger");
|
||||||
register_stateless_command!(mud_untrigger, "unact");
|
register_stateless_command!(mud_untrigger, "unact");
|
||||||
register_stateless_command!(mud_untrigger, "unaction");
|
register_stateless_command!(mud_untrigger, "unaction");
|
||||||
|
register_command!(untick, "undelay");
|
||||||
|
register_command!(untick);
|
||||||
register_stateless_command!(mud_untrigger, "untrigger");
|
register_stateless_command!(mud_untrigger, "untrigger");
|
||||||
register_command!(vsplit);
|
register_command!(vsplit);
|
||||||
ctx.set_global("commands", cmd_table);
|
ctx.set_global("commands", cmd_table);
|
||||||
|
@ -2,6 +2,7 @@ use crate::{
|
|||||||
echo_to_term_frame,
|
echo_to_term_frame,
|
||||||
id_intern::intern_id,
|
id_intern::intern_id,
|
||||||
match_table::{create_match_table, match_table_add, match_table_remove},
|
match_table::{create_match_table, match_table_add, match_table_remove},
|
||||||
|
timer_host::TimerHostAccessContext,
|
||||||
GlobalLayoutCell, GlobalLayoutState, GlobalMemoCell, TermFrame,
|
GlobalLayoutCell, GlobalLayoutState, GlobalMemoCell, TermFrame,
|
||||||
};
|
};
|
||||||
use gc_arena::{Gc, Rootable};
|
use gc_arena::{Gc, Rootable};
|
||||||
@ -439,3 +440,238 @@ pub(super) fn frame_input<'gc>(ctx: Context<'gc>, _global_memo: &GlobalMemoCell)
|
|||||||
callback_return
|
callback_return
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn list_timers<'gc>(
|
||||||
|
ctx: Context<'gc>,
|
||||||
|
global_memo: &GlobalMemoCell,
|
||||||
|
cur_frame_id: TermFrame,
|
||||||
|
) -> Result<(), Error<'gc>> {
|
||||||
|
let timer_val = match global_memo.frame_registry.borrow().get(&cur_frame_id) {
|
||||||
|
None => Err(anyhow::Error::msg("Frame no longer exists"))?,
|
||||||
|
Some(frame_dat) => Table::from_value(ctx, frame_dat.timer_host.to_value(ctx)?)?,
|
||||||
|
};
|
||||||
|
if timer_val.iter().next().is_none() {
|
||||||
|
let _ = echo_to_term_frame(
|
||||||
|
global_memo,
|
||||||
|
&cur_frame_id,
|
||||||
|
"No timers are currently active.\r\n",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let _ = echo_to_term_frame(
|
||||||
|
global_memo,
|
||||||
|
&cur_frame_id,
|
||||||
|
&timer_val
|
||||||
|
.iter()
|
||||||
|
.map(|(_k, v)| {
|
||||||
|
let t = Table::from_value(ctx, v)?;
|
||||||
|
Ok(format!(
|
||||||
|
"{}: {} {} {{{}}}\r\n",
|
||||||
|
t.get::<&str, piccolo::String>(ctx, "name")?.to_str()?,
|
||||||
|
if t.get::<&str, bool>(ctx, "recurring")? {
|
||||||
|
"tick"
|
||||||
|
} else {
|
||||||
|
"delay"
|
||||||
|
},
|
||||||
|
t.get::<&str, i64>(ctx, "interval_millis")? as f64 / 1000.0,
|
||||||
|
t.get::<&str, piccolo::String>(ctx, "commands")?.to_str()?,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<String>, Error<'gc>>>()?
|
||||||
|
.iter()
|
||||||
|
.join(""),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GlobalCellTermFrameTimerHostAccess {
|
||||||
|
global_memo: GlobalMemoCell,
|
||||||
|
frame: TermFrame,
|
||||||
|
}
|
||||||
|
impl TimerHostAccessContext for GlobalCellTermFrameTimerHostAccess {
|
||||||
|
fn with_ref<F>(&self, f: F)
|
||||||
|
where
|
||||||
|
F: Fn(&mut crate::timer_host::TimerHost),
|
||||||
|
{
|
||||||
|
if let Some(frame_dat) = self
|
||||||
|
.global_memo
|
||||||
|
.frame_registry
|
||||||
|
.borrow_mut()
|
||||||
|
.get_mut(&self.frame)
|
||||||
|
{
|
||||||
|
f(&mut frame_dat.timer_host);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn tick<'gc>(
|
||||||
|
ctx: Context<'gc>,
|
||||||
|
global_memo: &GlobalMemoCell,
|
||||||
|
_global_layout: &UseStateSetter<GlobalLayoutCell>,
|
||||||
|
) -> Callback<'gc> {
|
||||||
|
let global_memo = global_memo.clone();
|
||||||
|
Callback::from_fn(&ctx, move |ctx, _ex, mut stack| {
|
||||||
|
let info: Table = ctx.get_global("info")?;
|
||||||
|
let cur_frame_id: TermFrame =
|
||||||
|
try_unwrap_frame(ctx, &info.get(ctx, ctx.intern_static(b"current_frame"))?)?;
|
||||||
|
|
||||||
|
if stack.is_empty() {
|
||||||
|
list_timers(ctx, &global_memo, cur_frame_id)?;
|
||||||
|
return Ok(CallbackReturn::Return);
|
||||||
|
}
|
||||||
|
|
||||||
|
let secs: f64 = f64::from_value(
|
||||||
|
ctx,
|
||||||
|
stack
|
||||||
|
.pop_front()
|
||||||
|
.ok_or_else(|| anyhow::Error::msg("Missing timer frequency"))?,
|
||||||
|
)?;
|
||||||
|
if secs < 0.001 {
|
||||||
|
Err(anyhow::Error::msg("Minimum timer duration is 0.001s"))?;
|
||||||
|
}
|
||||||
|
if secs >= (2.0_f64).powi(31) {
|
||||||
|
Err(anyhow::Error::msg("Timer duration is too long"))?;
|
||||||
|
}
|
||||||
|
let millis: u32 = (secs * 1000.0) as u32;
|
||||||
|
|
||||||
|
let commands = piccolo::String::from_value(
|
||||||
|
ctx,
|
||||||
|
stack
|
||||||
|
.pop_front()
|
||||||
|
.ok_or_else(|| anyhow::Error::msg("Missing timer commands"))?,
|
||||||
|
)?
|
||||||
|
.to_str()?;
|
||||||
|
|
||||||
|
let timer_name = match stack.len() {
|
||||||
|
0 => None,
|
||||||
|
1 => Some(stack.consume::<piccolo::String>(ctx)?.to_str()?.to_owned()),
|
||||||
|
_ => Err(anyhow::Error::msg(
|
||||||
|
"Extra arguments given; expecting #tick duration {command} optional_name",
|
||||||
|
))?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_id = match global_memo
|
||||||
|
.frame_registry
|
||||||
|
.borrow_mut()
|
||||||
|
.get_mut(&cur_frame_id)
|
||||||
|
{
|
||||||
|
None => Err(anyhow::Error::msg("Frame no longer exists"))?,
|
||||||
|
Some(frame_dat) => frame_dat.timer_host.add_record(
|
||||||
|
GlobalCellTermFrameTimerHostAccess {
|
||||||
|
global_memo: global_memo.clone(),
|
||||||
|
frame: cur_frame_id.clone(),
|
||||||
|
},
|
||||||
|
&global_memo,
|
||||||
|
true,
|
||||||
|
millis,
|
||||||
|
commands.to_owned(),
|
||||||
|
timer_name,
|
||||||
|
)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_id = new_id.into_value(ctx);
|
||||||
|
ctx.get_global::<Table>("info")?
|
||||||
|
.set(ctx, "last_timer", new_id)?;
|
||||||
|
stack.push_back(new_id);
|
||||||
|
|
||||||
|
Ok(piccolo::CallbackReturn::Return)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn delay<'gc>(
|
||||||
|
ctx: Context<'gc>,
|
||||||
|
global_memo: &GlobalMemoCell,
|
||||||
|
_global_layout: &UseStateSetter<GlobalLayoutCell>,
|
||||||
|
) -> Callback<'gc> {
|
||||||
|
let global_memo = global_memo.clone();
|
||||||
|
Callback::from_fn(&ctx, move |ctx, _ex, mut stack| {
|
||||||
|
let info: Table = ctx.get_global("info")?;
|
||||||
|
let cur_frame_id: TermFrame =
|
||||||
|
try_unwrap_frame(ctx, &info.get(ctx, ctx.intern_static(b"current_frame"))?)?;
|
||||||
|
|
||||||
|
if stack.is_empty() {
|
||||||
|
list_timers(ctx, &global_memo, cur_frame_id)?;
|
||||||
|
return Ok(CallbackReturn::Return);
|
||||||
|
}
|
||||||
|
|
||||||
|
let secs: f64 = f64::from_value(
|
||||||
|
ctx,
|
||||||
|
stack
|
||||||
|
.pop_front()
|
||||||
|
.ok_or_else(|| anyhow::Error::msg("Missing timer frequency"))?,
|
||||||
|
)?;
|
||||||
|
if secs < 0.001 {
|
||||||
|
Err(anyhow::Error::msg("Minimum timer duration is 0.001s"))?;
|
||||||
|
}
|
||||||
|
if secs >= (2.0_f64).powi(31) {
|
||||||
|
Err(anyhow::Error::msg("Timer duration is too long"))?;
|
||||||
|
}
|
||||||
|
let millis: u32 = (secs * 1000.0) as u32;
|
||||||
|
|
||||||
|
let commands = piccolo::String::from_value(
|
||||||
|
ctx,
|
||||||
|
stack
|
||||||
|
.pop_front()
|
||||||
|
.ok_or_else(|| anyhow::Error::msg("Missing timer commands"))?,
|
||||||
|
)?
|
||||||
|
.to_str()?;
|
||||||
|
|
||||||
|
let timer_name = match stack.len() {
|
||||||
|
0 => None,
|
||||||
|
1 => Some(stack.consume::<piccolo::String>(ctx)?.to_str()?.to_owned()),
|
||||||
|
_ => Err(anyhow::Error::msg(
|
||||||
|
"Extra arguments given; expecting #delay duration {command} optional_name",
|
||||||
|
))?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_id = match global_memo
|
||||||
|
.frame_registry
|
||||||
|
.borrow_mut()
|
||||||
|
.get_mut(&cur_frame_id)
|
||||||
|
{
|
||||||
|
None => Err(anyhow::Error::msg("Frame no longer exists"))?,
|
||||||
|
Some(frame_dat) => frame_dat.timer_host.add_record(
|
||||||
|
GlobalCellTermFrameTimerHostAccess {
|
||||||
|
global_memo: global_memo.clone(),
|
||||||
|
frame: cur_frame_id.clone(),
|
||||||
|
},
|
||||||
|
&global_memo,
|
||||||
|
false,
|
||||||
|
millis,
|
||||||
|
commands.to_owned(),
|
||||||
|
timer_name,
|
||||||
|
)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_id = new_id.into_value(ctx);
|
||||||
|
ctx.get_global::<Table>("info")?
|
||||||
|
.set(ctx, "last_timer", new_id)?;
|
||||||
|
stack.push_back(new_id);
|
||||||
|
|
||||||
|
Ok(piccolo::CallbackReturn::Return)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn untick<'gc>(
|
||||||
|
ctx: Context<'gc>,
|
||||||
|
global_memo: &GlobalMemoCell,
|
||||||
|
_global_layout: &UseStateSetter<GlobalLayoutCell>,
|
||||||
|
) -> Callback<'gc> {
|
||||||
|
let global_memo = global_memo.clone();
|
||||||
|
Callback::from_fn(&ctx, move |ctx, _ex, mut stack| {
|
||||||
|
let info: Table = ctx.get_global("info")?;
|
||||||
|
let cur_frame_id: TermFrame =
|
||||||
|
try_unwrap_frame(ctx, &info.get(ctx, ctx.intern_static(b"current_frame"))?)?;
|
||||||
|
let name = stack.consume::<piccolo::String>(ctx)?.to_str()?.to_owned();
|
||||||
|
|
||||||
|
match global_memo
|
||||||
|
.frame_registry
|
||||||
|
.borrow_mut()
|
||||||
|
.get_mut(&cur_frame_id)
|
||||||
|
{
|
||||||
|
None => Err(anyhow::Error::msg("Frame no longer exists"))?,
|
||||||
|
Some(frame_dat) => frame_dat.timer_host.remove_timer(name)?,
|
||||||
|
};
|
||||||
|
Ok(piccolo::CallbackReturn::Return)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -18,6 +18,7 @@ pub mod split_panel;
|
|||||||
pub mod telnet;
|
pub mod telnet;
|
||||||
pub mod term_split;
|
pub mod term_split;
|
||||||
pub mod term_view;
|
pub mod term_view;
|
||||||
|
pub mod timer_host;
|
||||||
pub mod websocket;
|
pub mod websocket;
|
||||||
use crate::lua_engine::{install_lua_globals, LuaState};
|
use crate::lua_engine::{install_lua_globals, LuaState};
|
||||||
use crate::split_panel::*;
|
use crate::split_panel::*;
|
||||||
|
@ -9,6 +9,7 @@ use crate::{
|
|||||||
command_handler::command_handler,
|
command_handler::command_handler,
|
||||||
lineengine::line::{Readline, ReadlineEvent},
|
lineengine::line::{Readline, ReadlineEvent},
|
||||||
term_split::TermSplit,
|
term_split::TermSplit,
|
||||||
|
timer_host::TimerHost,
|
||||||
GlobalLayoutCell, GlobalMemoCell, PanelDirection, SplitPanel,
|
GlobalLayoutCell, GlobalMemoCell, PanelDirection, SplitPanel,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -77,6 +78,7 @@ pub struct TermFrameData {
|
|||||||
pub fit: FitAddon,
|
pub fit: FitAddon,
|
||||||
pub node: Node,
|
pub node: Node,
|
||||||
pub readline: Readline,
|
pub readline: Readline,
|
||||||
|
pub timer_host: TimerHost,
|
||||||
pub retained_closures: Option<(
|
pub retained_closures: Option<(
|
||||||
Closure<dyn FnMut(String)>,
|
Closure<dyn FnMut(String)>,
|
||||||
Closure<dyn FnMut(Dims)>,
|
Closure<dyn FnMut(Dims)>,
|
||||||
@ -136,6 +138,7 @@ fn get_or_make_term_frame<'a>(
|
|||||||
}),
|
}),
|
||||||
initial_size,
|
initial_size,
|
||||||
),
|
),
|
||||||
|
timer_host: TimerHost::new(frame.clone()),
|
||||||
retained_closures: None,
|
retained_closures: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
173
src/timer_host.rs
Normal file
173
src/timer_host.rs
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
use anyhow::bail;
|
||||||
|
use piccolo::{Context, IntoValue, Table, Value};
|
||||||
|
use wasm_bindgen::{closure::Closure, JsCast};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
command_handler::execute_queue,
|
||||||
|
parsing::{parse_commands, ParsedCommand},
|
||||||
|
GlobalMemoCell, TermFrame,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct TimerHost {
|
||||||
|
timers: Vec<TimerRecord>,
|
||||||
|
frame_id: TermFrame,
|
||||||
|
next_id: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TimerHost {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Some(window) = web_sys::window() {
|
||||||
|
for timer in &self.timers {
|
||||||
|
if timer.recurring {
|
||||||
|
window.clear_interval_with_handle(timer.js_id);
|
||||||
|
} else {
|
||||||
|
window.clear_timeout_with_handle(timer.js_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TimerHost {
|
||||||
|
pub fn new(frame_id: TermFrame) -> Self {
|
||||||
|
Self {
|
||||||
|
timers: vec![],
|
||||||
|
frame_id,
|
||||||
|
next_id: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_record<AC>(
|
||||||
|
&mut self,
|
||||||
|
access_context: AC,
|
||||||
|
global: &GlobalMemoCell,
|
||||||
|
recurring: bool,
|
||||||
|
interval_millis: u32,
|
||||||
|
commands: String,
|
||||||
|
timer_name: Option<String>,
|
||||||
|
) -> anyhow::Result<String>
|
||||||
|
where
|
||||||
|
AC: TimerHostAccessContext + 'static,
|
||||||
|
{
|
||||||
|
let window = web_sys::window().ok_or_else(|| anyhow::Error::msg("Can't fetch window"))?;
|
||||||
|
let new_id = self.next_id;
|
||||||
|
self.next_id += 1;
|
||||||
|
let global = global.clone();
|
||||||
|
|
||||||
|
if timer_name
|
||||||
|
.as_ref()
|
||||||
|
.map(|tn| tn.starts_with('#'))
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
bail!(
|
||||||
|
"Timer name can't start with #, it is reserved for referencing timers by number."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let frame = self.frame_id.clone();
|
||||||
|
let callback_commands: Vec<ParsedCommand> = parse_commands(&commands).commands;
|
||||||
|
let callback: Closure<dyn FnMut()> = Closure::new(move || {
|
||||||
|
let mut cq = global.command_queue.borrow_mut();
|
||||||
|
for cmd in &callback_commands {
|
||||||
|
cq.push_back((frame.clone(), cmd.clone()));
|
||||||
|
}
|
||||||
|
drop(cq);
|
||||||
|
|
||||||
|
execute_queue(&global);
|
||||||
|
|
||||||
|
if !recurring {
|
||||||
|
access_context.with_ref(|timer_host| {
|
||||||
|
timer_host.timers.retain(|t| t.id != new_id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let js_id = if recurring {
|
||||||
|
window
|
||||||
|
.set_interval_with_callback_and_timeout_and_arguments_0(
|
||||||
|
callback.as_ref().unchecked_ref(),
|
||||||
|
interval_millis as i32,
|
||||||
|
)
|
||||||
|
.map_err(|_| anyhow::Error::msg("setInterval failed"))?
|
||||||
|
} else {
|
||||||
|
window
|
||||||
|
.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||||
|
callback.as_ref().unchecked_ref(),
|
||||||
|
interval_millis as i32,
|
||||||
|
)
|
||||||
|
.map_err(|_| anyhow::Error::msg("setTimeout failed"))?
|
||||||
|
};
|
||||||
|
|
||||||
|
self.timers.push(TimerRecord {
|
||||||
|
id: new_id,
|
||||||
|
js_id,
|
||||||
|
timer_name,
|
||||||
|
recurring,
|
||||||
|
interval_millis,
|
||||||
|
commands,
|
||||||
|
retained_closure: callback,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(format!("#{}", new_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_timer(&mut self, timer_name: String) -> anyhow::Result<()> {
|
||||||
|
let timer = if let Some(suffix) = timer_name.strip_prefix('#') {
|
||||||
|
let id = suffix
|
||||||
|
.parse::<u64>()
|
||||||
|
.map_err(|_| anyhow::Error::msg("Should follow # with a number"))?;
|
||||||
|
self.timers.iter().enumerate().find(|t| t.1.id == id)
|
||||||
|
} else {
|
||||||
|
self.timers
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.find(|t| t.1.timer_name.as_ref() == Some(&timer_name))
|
||||||
|
};
|
||||||
|
let timer = timer.ok_or_else(|| anyhow::Error::msg("No matching timer found."))?;
|
||||||
|
let window = web_sys::window().ok_or_else(|| anyhow::Error::msg("Can't fetch window"))?;
|
||||||
|
if timer.1.recurring {
|
||||||
|
window.clear_interval_with_handle(timer.1.js_id);
|
||||||
|
} else {
|
||||||
|
window.clear_timeout_with_handle(timer.1.js_id);
|
||||||
|
}
|
||||||
|
self.timers.remove(timer.0);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_value<'gc>(&self, ctx: Context<'gc>) -> anyhow::Result<Value<'gc>> {
|
||||||
|
let tbl = Table::new(&ctx);
|
||||||
|
for timer in self.timers.iter() {
|
||||||
|
let timer_tbl = Table::new(&ctx);
|
||||||
|
timer_tbl.set(
|
||||||
|
ctx,
|
||||||
|
"name",
|
||||||
|
timer
|
||||||
|
.timer_name
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| format!("#{}", timer.id)),
|
||||||
|
)?;
|
||||||
|
timer_tbl.set(ctx, "commands", timer.commands.clone())?;
|
||||||
|
timer_tbl.set(ctx, "recurring", timer.recurring)?;
|
||||||
|
timer_tbl.set(ctx, "interval_millis", timer.interval_millis)?;
|
||||||
|
tbl.set(ctx, timer.id as i64, timer_tbl)?;
|
||||||
|
}
|
||||||
|
Ok(tbl.into_value(ctx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait TimerHostAccessContext {
|
||||||
|
fn with_ref<F>(&self, f: F)
|
||||||
|
where
|
||||||
|
F: Fn(&mut TimerHost);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TimerRecord {
|
||||||
|
id: u64,
|
||||||
|
js_id: i32,
|
||||||
|
timer_name: Option<String>,
|
||||||
|
recurring: bool,
|
||||||
|
interval_millis: u32,
|
||||||
|
commands: String,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
retained_closure: Closure<dyn FnMut()>,
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user