Skip to content

Commit

Permalink
lua: add wezterm.time.call_after and wezterm.reload_configuration
Browse files Browse the repository at this point in the history
These allow setting up some basic timers and constructing time-dependent
config files.

Needs to be used respsonsibly!
  • Loading branch information
wez committed Jul 18, 2022
1 parent c81dabd commit dd32761
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 10 deletions.
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 14 additions & 9 deletions config/src/lua.rs
Expand Up @@ -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",
Expand Down Expand Up @@ -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)?)?;
Expand Down Expand Up @@ -225,12 +231,6 @@ fn shell_quote_arg<'lua>(_: &'lua Lua, arg: String) -> mlua::Result<String> {
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.
Expand Down Expand Up @@ -454,11 +454,16 @@ fn font_with_fallback<'lua>(
Ok(text_style)
}

fn action_callback<'lua>(lua: &'lua Lua, callback: mlua::Function) -> mlua::Result<KeyAssignment> {
pub fn wrap_callback<'lua>(lua: &'lua Lua, callback: mlua::Function) -> mlua::Result<String> {
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<KeyAssignment> {
let user_event_id = wrap_callback(lua, callback)?;
Ok(KeyAssignment::EmitEvent(user_event_id))
}

Expand Down Expand Up @@ -528,7 +533,7 @@ fn split_by_newlines<'lua>(_: &'lua Lua, text: String) -> mlua::Result<Vec<Strin
///
/// wezterm.emit("event-name", "foo", "bar");
/// ```
fn register_event<'lua>(
pub fn register_event<'lua>(
lua: &'lua Lua,
(name, func): (String, mlua::Function),
) -> mlua::Result<()> {
Expand Down
37 changes: 37 additions & 0 deletions 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.

10 changes: 10 additions & 0 deletions 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.
3 changes: 3 additions & 0 deletions lua-api-crates/time-funcs/Cargo.toml
Expand Up @@ -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" }
137 changes: 136 additions & 1 deletion 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<Option<ConfigSubscription>> = 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<Rc<mlua::Lua>>) -> mlua::Result<()> {
if let Some(lua) = lua {
let scheduled_events: Vec<ScheduledEvent> = 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::<ScheduledEvent>::new())?;
let time_mod = get_or_create_sub_module(lua, "time")?;

time_mod.set(
Expand Down Expand Up @@ -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<ScheduledEvent> =
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(())
Expand All @@ -47,6 +176,12 @@ fn strftime<'lua>(_: &'lua Lua, format: String) -> mlua::Result<String> {
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<Utc>,
Expand Down
8 changes: 8 additions & 0 deletions promise/src/spawn.rs
Expand Up @@ -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};

Expand All @@ -19,6 +20,8 @@ lazy_static::lazy_static! {
static ref SCOPED_EXECUTOR: Mutex<Option<Arc<Executor<'static>>>> = 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()
Expand All @@ -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",
Expand All @@ -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.
Expand Down

0 comments on commit dd32761

Please sign in to comment.