diff --git a/Cargo.lock b/Cargo.lock index 302fd3ff3ea..37f36492dc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4529,7 +4529,10 @@ dependencies = [ "anyhow", "chrono", "config", + "lazy_static", "luahelper", + "promise", + "smol", "spa", "wezterm-dynamic", ] diff --git a/config/src/lua.rs b/config/src/lua.rs index 7a95b4d527a..c3281d60ec7 100644 --- a/config/src/lua.rs +++ b/config/src/lua.rs @@ -153,6 +153,13 @@ end .set_name("=searcher")? .eval()?; + wezterm_mod.set( + "reload_configuration", + lua.create_function(|_, _: ()| { + crate::reload(); + Ok(()) + })?, + )?; wezterm_mod.set("config_file", config_file_str)?; wezterm_mod.set( "config_dir", @@ -196,7 +203,6 @@ end wezterm_mod.set("split_by_newlines", lua.create_function(split_by_newlines)?)?; wezterm_mod.set("on", lua.create_function(register_event)?)?; wezterm_mod.set("emit", lua.create_async_function(emit_event)?)?; - wezterm_mod.set("sleep_ms", lua.create_async_function(sleep_ms)?)?; wezterm_mod.set("shell_join_args", lua.create_function(shell_join_args)?)?; wezterm_mod.set("shell_quote_arg", lua.create_function(shell_quote_arg)?)?; wezterm_mod.set("shell_split", lua.create_function(shell_split)?)?; @@ -225,12 +231,6 @@ fn shell_quote_arg<'lua>(_: &'lua Lua, arg: String) -> mlua::Result { Ok(shlex::quote(&arg).into_owned().to_string()) } -async fn sleep_ms<'lua>(_: &'lua Lua, milliseconds: u64) -> mlua::Result<()> { - let duration = std::time::Duration::from_millis(milliseconds); - smol::Timer::after(duration).await; - Ok(()) -} - /// Returns the system hostname. /// Errors may occur while retrieving the hostname from the system, /// or if the hostname isn't a UTF-8 string. @@ -454,11 +454,16 @@ fn font_with_fallback<'lua>( Ok(text_style) } -fn action_callback<'lua>(lua: &'lua Lua, callback: mlua::Function) -> mlua::Result { +pub fn wrap_callback<'lua>(lua: &'lua Lua, callback: mlua::Function) -> mlua::Result { let callback_count: i32 = lua.named_registry_value(LUA_REGISTRY_USER_CALLBACK_COUNT)?; let user_event_id = format!("user-defined-{}", callback_count); lua.set_named_registry_value(LUA_REGISTRY_USER_CALLBACK_COUNT, callback_count + 1)?; register_event(lua, (user_event_id.clone(), callback))?; + Ok(user_event_id) +} + +fn action_callback<'lua>(lua: &'lua Lua, callback: mlua::Function) -> mlua::Result { + let user_event_id = wrap_callback(lua, callback)?; Ok(KeyAssignment::EmitEvent(user_event_id)) } @@ -528,7 +533,7 @@ fn split_by_newlines<'lua>(_: &'lua Lua, text: String) -> mlua::Result( +pub fn register_event<'lua>( lua: &'lua Lua, (name, func): (String, mlua::Function), ) -> mlua::Result<()> { diff --git a/docs/config/lua/wezterm.time/call_after.md b/docs/config/lua/wezterm.time/call_after.md new file mode 100644 index 00000000000..28ed44bc7da --- /dev/null +++ b/docs/config/lua/wezterm.time/call_after.md @@ -0,0 +1,37 @@ +# `wezterm.time.call_after(interval_seconds, function)` + +*Since: nightly builds only* + +Arranges to call your callback function after the specified number of seconds +have elapsed. + +Here's a contrived example that demonstrates a configuration that +varies based on the time. In this case, the idea is that the background +color is derived from the current number of minutes past the hour. + +In order for the value to be picked up for the next minute, `call_after` +is used to schedule a callback 60 seconds later and it then generates +a background color by extracting the current minute value and scaing +it to the range 0-255 and using that to assign a background color: + +```lua +local wezterm = require 'wezterm' + +-- Reload the configuration every minute +wezterm.time.call_after(60, function(window, pane) + wezterm.reload_configuration() +end) + +local amount = math.ceil((tonumber(wezterm.time.now():format("%M")) / 60) * 255) + +return { + colors = { + background = "rgb(" .. amount .. "," .. amount .. "," .. amount .. ")", + } +} +``` + +With great power comes great responsibility: if you schedule a lot of frequent +callbacks, or frequently reload your configuration in this way, you may +increase the CPU load on your system because you are asking it to work harder. + diff --git a/docs/config/lua/wezterm/reload_configuration.md b/docs/config/lua/wezterm/reload_configuration.md new file mode 100644 index 00000000000..a3bdf2056a5 --- /dev/null +++ b/docs/config/lua/wezterm/reload_configuration.md @@ -0,0 +1,10 @@ +# `wezterm.reload_configuration()` + +*Since: nightly builds only* + +Immediately causes the configuration to be reloaded and re-applied. + +If you call this at the file scope in your config you will create +an infinite loop that renders wezterm unresponsive, so don't do that! + +The intent is for this to be used from an event or timer callback function. diff --git a/lua-api-crates/time-funcs/Cargo.toml b/lua-api-crates/time-funcs/Cargo.toml index a5f4d6dbeab..80a4608edcb 100644 --- a/lua-api-crates/time-funcs/Cargo.toml +++ b/lua-api-crates/time-funcs/Cargo.toml @@ -10,5 +10,8 @@ anyhow = "1.0" chrono = {version="0.4", features=["unstable-locales"]} config = { path = "../../config" } luahelper = { path = "../../luahelper" } +lazy_static = "1.4" +promise = { path = "../../promise" } +smol = "1.2" spa = "0.3" wezterm-dynamic = { path = "../../wezterm-dynamic" } diff --git a/lua-api-crates/time-funcs/src/lib.rs b/lua-api-crates/time-funcs/src/lib.rs index b5530b50b98..90011a8f72c 100644 --- a/lua-api-crates/time-funcs/src/lib.rs +++ b/lua-api-crates/time-funcs/src/lib.rs @@ -1,8 +1,121 @@ use chrono::prelude::*; use config::lua::mlua::{self, Lua, MetaMethod, UserData, UserDataMethods}; -use config::lua::{get_or_create_module, get_or_create_sub_module}; +use config::lua::{emit_event, get_or_create_module, get_or_create_sub_module, wrap_callback}; +use config::ConfigSubscription; +use std::rc::Rc; +use std::sync::Mutex; + +lazy_static::lazy_static! { + static ref CONFIG_SUBSCRIPTION: Mutex> = Mutex::new(None); +} + +/// We contrive to call this from the main thread in response to the +/// config being reloaded. +/// It spawns a task for each of the timers that have been configured +/// by the user via `wezterm.time.call_after`. +fn schedule_all(lua: Option>) -> mlua::Result<()> { + if let Some(lua) = lua { + let scheduled_events: Vec = lua.named_registry_value(SCHEDULED_EVENTS)?; + let generation = config::configuration().generation(); + for event in scheduled_events { + event.schedule(generation); + } + } + Ok(()) +} + +/// Helper to schedule !Send futures to run with access to the lua +/// config on the main thread +fn schedule_trampoline() { + promise::spawn::spawn(async move { + config::with_lua_config_on_main_thread(|lua| async move { + schedule_all(lua)?; + Ok(()) + }) + .await + }) + .detach(); +} + +/// Called by the config subsystem when the config is reloaded. +/// We use it to schedule our setup function that will schedule +/// the call_after functions from the main thread. +pub fn config_was_reloaded() -> bool { + if promise::spawn::is_scheduler_configured() { + promise::spawn::spawn_into_main_thread(async move { + schedule_trampoline(); + }) + .detach(); + } + + true +} + +/// Keeps track of `call_after` state +#[derive(Debug, Clone)] +struct ScheduledEvent { + /// The name of the registry entry that will resolve to + /// their callback function + user_event_id: String, + /// The delay after which to run their callback + interval_seconds: u64, +} + +impl ScheduledEvent { + /// Schedule a task with the scheduler runtime. + /// Note that this will extend the lifetime of the lua context + /// until their timeout completes and their function is called. + /// That can lead to exponential growth in callbacks on each + /// config reload, which is undesirable! + /// To address that, we pass in the current configuration generation + /// at the time that we called schedule. + /// Later, after our interval has elapsed, if the generation + /// doesn't match the then-current generation we skip performing + /// the actual callback. + /// That means that for large intervals we may keep more memory + /// occupied, but we won't run the callback twice for the first + /// reload, or 4 times for the second and so on. + fn schedule(self, generation: usize) { + let event = self; + promise::spawn::spawn(async move { + config::with_lua_config_on_main_thread(move |lua| async move { + if let Some(lua) = lua { + event.run(&lua, generation).await?; + } + Ok(()) + }) + .await + }) + .detach(); + } + + async fn run(self, lua: &Lua, generation: usize) -> mlua::Result<()> { + let duration = std::time::Duration::from_secs(self.interval_seconds); + smol::Timer::after(duration).await; + // Skip doing anything of consequence if the generation has + // changed. + if config::configuration().generation() == generation { + let args = lua.pack_multi(())?; + emit_event(&lua, (self.user_event_id, args)).await?; + } + Ok(()) + } +} + +impl UserData for ScheduledEvent { + fn add_methods<'lua, M: UserDataMethods<'lua, Self>>(_methods: &mut M) {} +} + +const SCHEDULED_EVENTS: &str = "wezterm-scheduled-events"; pub fn register(lua: &Lua) -> anyhow::Result<()> { + { + let mut sub = CONFIG_SUBSCRIPTION.lock().unwrap(); + if sub.is_none() { + sub.replace(config::subscribe_to_config_reload(config_was_reloaded)); + } + } + lua.set_named_registry_value(SCHEDULED_EVENTS, Vec::::new())?; let time_mod = get_or_create_sub_module(lua, "time")?; time_mod.set( @@ -30,8 +143,24 @@ pub fn register(lua: &Lua) -> anyhow::Result<()> { })?, )?; + time_mod.set( + "call_after", + lua.create_function(|lua, (interval_seconds, func): (u64, mlua::Function)| { + let user_event_id = wrap_callback(lua, func)?; + let mut scheduled_events: Vec = + lua.named_registry_value(SCHEDULED_EVENTS)?; + scheduled_events.push(ScheduledEvent { + user_event_id, + interval_seconds, + }); + lua.set_named_registry_value(SCHEDULED_EVENTS, scheduled_events)?; + Ok(()) + })?, + )?; + // For backwards compatibility let wezterm_mod = get_or_create_module(lua, "wezterm")?; + wezterm_mod.set("sleep_ms", lua.create_async_function(sleep_ms)?)?; wezterm_mod.set("strftime", lua.create_function(strftime)?)?; wezterm_mod.set("strftime_utc", lua.create_function(strftime_utc)?)?; Ok(()) @@ -47,6 +176,12 @@ fn strftime<'lua>(_: &'lua Lua, format: String) -> mlua::Result { Ok(local.format(&format).to_string()) } +async fn sleep_ms<'lua>(_: &'lua Lua, milliseconds: u64) -> mlua::Result<()> { + let duration = std::time::Duration::from_millis(milliseconds); + smol::Timer::after(duration).await; + Ok(()) +} + #[derive(Clone, Debug)] pub struct Time { utc: DateTime, diff --git a/promise/src/spawn.rs b/promise/src/spawn.rs index 6637f2c5af0..43485a1c828 100644 --- a/promise/src/spawn.rs +++ b/promise/src/spawn.rs @@ -2,6 +2,7 @@ use anyhow::{anyhow, Result}; use async_executor::Executor; use flume::{bounded, unbounded, Receiver, TryRecvError}; use std::future::Future; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::task::{Poll, Waker}; @@ -19,6 +20,8 @@ lazy_static::lazy_static! { static ref SCOPED_EXECUTOR: Mutex>>> = Mutex::new(None); } +static SCHEDULER_CONFIGURED: AtomicBool = AtomicBool::new(false); + fn schedule_runnable(runnable: Runnable, high_pri: bool) { let func = if high_pri { ON_MAIN_THREAD.lock() @@ -29,6 +32,10 @@ fn schedule_runnable(runnable: Runnable, high_pri: bool) { func(runnable); } +pub fn is_scheduler_configured() -> bool { + SCHEDULER_CONFIGURED.load(Ordering::Relaxed) +} + /// Set callbacks for scheduling normal and low priority futures. /// Why this and not "just tokio"? In a GUI application there is typically /// a special GUI processing loop that may need to run on the "main thread", @@ -39,6 +46,7 @@ fn schedule_runnable(runnable: Runnable, high_pri: bool) { pub fn set_schedulers(main: ScheduleFunc, low_pri: ScheduleFunc) { *ON_MAIN_THREAD.lock().unwrap() = Box::new(main); *ON_MAIN_THREAD_LOW_PRI.lock().unwrap() = Box::new(low_pri); + SCHEDULER_CONFIGURED.store(true, Ordering::Relaxed); } /// Spawn a new thread to execute the provided function.