diff --git a/breadd/src/core/subscriptions.rs b/breadd/src/core/subscriptions.rs index a95d388..9b218ff 100644 --- a/breadd/src/core/subscriptions.rs +++ b/breadd/src/core/subscriptions.rs @@ -35,7 +35,6 @@ impl SubscriptionTable { // swap_remove moves the last element into `idx`. We need to update by_id // for that element. But first, remove its stale entry (it was at the last // position before the swap); then re-insert it at the new position. - let _last_idx = self.entries.len() - 1; self.entries.swap_remove(idx); if idx < self.entries.len() { diff --git a/breadd/src/ipc/mod.rs b/breadd/src/ipc/mod.rs index f4aa092..f99ea74 100644 --- a/breadd/src/ipc/mod.rs +++ b/breadd/src/ipc/mod.rs @@ -313,9 +313,67 @@ impl Server { } fn matches_filter(event_name: &str, pattern: &str) -> bool { + // Delegate to the same glob logic used by the subscription table so that + // `bread events --filter "bread.device.**"` behaves identically to + // `bread.on("bread.device.**", ...)` in Lua. if pattern.ends_with(".*") { let prefix = &pattern[..pattern.len() - 1]; return event_name.starts_with(prefix); } - event_name == pattern + + if let Some(prefix) = pattern.strip_suffix(".**") { + if event_name == prefix || event_name.starts_with(&format!("{prefix}.")) { + return true; + } + return false; + } + + matches_glob_filter(pattern.as_bytes(), event_name.as_bytes()) +} + +fn matches_glob_filter(pattern: &[u8], text: &[u8]) -> bool { + if pattern.is_empty() { + return text.is_empty(); + } + + if pattern.len() >= 2 && pattern[0] == b'*' && pattern[1] == b'*' { + let rest = &pattern[2..]; + if rest.is_empty() { + return true; + } + for offset in 0..=text.len() { + if matches_glob_filter(rest, &text[offset..]) { + return true; + } + } + return false; + } + + match pattern[0] { + b'*' => { + let mut offset = 0; + loop { + if matches_glob_filter(&pattern[1..], &text[offset..]) { + return true; + } + if offset == text.len() || text[offset] == b'.' { + break; + } + offset += 1; + } + false + } + b'?' => { + if text.is_empty() || text[0] == b'.' { + return false; + } + matches_glob_filter(&pattern[1..], &text[1..]) + } + ch => { + if text.first().copied() != Some(ch) { + return false; + } + matches_glob_filter(&pattern[1..], &text[1..]) + } + } } diff --git a/breadd/src/lua/mod.rs b/breadd/src/lua/mod.rs index cf94a57..9acc814 100644 --- a/breadd/src/lua/mod.rs +++ b/breadd/src/lua/mod.rs @@ -840,6 +840,8 @@ impl LuaEngine { globals.set("bread", bread)?; self.install_require_loader()?; self.install_wait_helper()?; + self.install_log_helpers()?; + self.install_debounce()?; Ok(()) } @@ -1188,6 +1190,90 @@ impl LuaEngine { } } + fn install_log_helpers(&self) -> Result<()> { + // bread.log(msg) → tracing::info + // bread.warn(msg) → tracing::warn + // bread.error(msg) → tracing::error + // + // Each accepts any Lua value and coerces it to a string via tostring() + // so callers can do bread.log(some_table) without a crash. + self.lua.load(r#" + local _bread = bread + + local function stringify(v) + if type(v) == "string" then + return v + end + return tostring(v) + end + + function _bread.log(msg) + _bread.__log_info(stringify(msg)) + end + + function _bread.warn(msg) + _bread.__log_warn(stringify(msg)) + end + + function _bread.error(msg) + _bread.__log_error(stringify(msg)) + end + "#).exec()?; + + // Register the raw Rust-backed log functions that the Lua wrappers call. + let globals = self.lua.globals(); + let bread: mlua::Table = globals.get("bread")?; + + let info_fn = self.lua.create_function(|_, msg: String| { + tracing::info!(target: "bread.lua", "{}", msg); + Ok(()) + })?; + bread.set("__log_info", info_fn)?; + + let warn_fn = self.lua.create_function(|_, msg: String| { + tracing::warn!(target: "bread.lua", "{}", msg); + Ok(()) + })?; + bread.set("__log_warn", warn_fn)?; + + let error_fn = self.lua.create_function(|_, msg: String| { + tracing::error!(target: "bread.lua", "{}", msg); + Ok(()) + })?; + bread.set("__log_error", error_fn)?; + + Ok(()) + } + + fn install_debounce(&self) -> Result<()> { + // bread.debounce(delay_ms, fn) → wrapped_fn + // + // Returns a new function. When that function is called, it resets a + // timer. The original function is only called once the timer expires + // without being reset. Useful for rapid hardware events (e.g. monitor + // topology changes that fire multiple events in quick succession). + // + // Because the Lua runtime is single-threaded, we implement this in + // pure Lua using bread.cancel / bread.after. + self.lua.load(r#" + function bread.debounce(delay_ms, fn) + local timer_id = nil + return function(...) + local args = { ... } + if timer_id then + bread.cancel(timer_id) + timer_id = nil + end + timer_id = bread.after(delay_ms, function() + timer_id = nil + fn(table.unpack(args)) + end) + end + end + "#).exec()?; + Ok(()) + } + fn scan_module_decl(&self, path: &Path) -> Result { const MODULE_DECL_ABORT: &str = "__bread_module_decl__"; let lua = Lua::new();