rune.hooks
Hooks attach handlers to client events: input, output, connection lifecycle, GMCP, and more. For a task-oriented introduction, see Hooks & Events.
Quick reference
Section titled “Quick reference”rune.hooks.on(event, handler, opts?) -- attach a handler to an eventrune.hooks.enable(name) -- re-enable a named handlerrune.hooks.disable(name) -- disable without unregisteringrune.hooks.remove(name) -- unregister a named handlerrune.hooks.list() -- all handlers with event, priority, sourcerune.hooks.clear(event?) -- remove handlers for one event, or allrune.hooks.has(event) -- true if the event has handlersrune.hooks.count(event?) -- handlers for one event, or totalrune.hooks.remove_group(group) -- remove all handlers in a groupon returns a handle and accepts the
common options (name, group, priority).
rune.hooks.on
Section titled “rune.hooks.on”rune.hooks.on(event, handler, opts?) -> handleevent(string): an event name from the tables below.handler(function): receives the event’s arguments; return values matter only for data-flow events.opts(table, optional): common options.prioritydefaults to 50; lower runs first.
rune.hooks.on("connected", function(addr) rune.send("look")end, {name = "auto-look"})Data-flow events
Section titled “Data-flow events”Handlers run in priority order (lower first, default 50).
For output, prompt, echo, and input, a string, including "",
replaces the text for subsequent handlers, so rewrites chain in priority
order. nil and other values pass the current text through. false stops the
chain: it gags output or a prompt, hides an echo, or consumes an input
line.
Input handlers run before Rune echoes or processes each input line. A consumed line stops there; later lines still run. Rune saves the surviving text together as one history entry after the submission finishes.
| Event | Handler receives | Fired |
|---|---|---|
input |
one physical line, context | Before that line is echoed or processed; both modes |
output |
line object (:raw(), :clean()) |
Once for every complete server line |
prompt |
line object, confirmed boolean |
On cumulative partial-line observations and GA/EOR-confirmed prompts |
echo |
one display-safe physical line of final input | After input rewrites; skipped while the server has echo suppressed (passwords) |
Before echo handlers run, Rune makes terminal-active control characters
visible so the value is safe to display. Tabs remain tabs.
prompt drives the prompt overlay. With confirmed = false, the value is a
partial line. It may be Username: or only the start of an ordinary line, and
it may repeat as it grows. A line delimiter later sends the complete line
through output. With confirmed = true, a GA/EOR prompt boundary confirmed
the text as a prompt. If the boundary arrives in a later batch, the hook may
receive the same text first as partial and then as confirmed. An empty prompt
boundary does not fire the hook. Rune never uses a timer or prompt pattern to
confirm a partial line.
Finishing a partial line means one thing throughout Rune: the prompt overlay is committed to scrollback and open trigger spans close. Every user submission finishes the partial line before input hooks and any echo, history, or processing, even for a local slash command, a canceled submission, a disconnected submission, or one whose eventual send fails. Separately, a programmatic send from an alias, trigger, timer, or other callback finishes it only after the connection accepts the send. Failed sends and protocol traffic such as GMCP do not.
If the partial text was really the start of a fragmented ordinary line,
sending commits the visible prefix; the rest arrives through output as a new
line. This is the trade-off for immediate partial-line display without a timer
or prompt pattern.
rune.hooks.on("prompt", function(line, confirmed) if confirmed then rune.echo("Server-confirmed prompt: " .. line:clean()) endend)Handlers for partial lines should be safe to repeat. A confirmed prompt closes
open spans before prompt handlers run. Finishing a partial line on submission
or accepted send commits its latest processed overlay without calling the
prompt hook again.
Every input handler receives (text, context). The context is read-only, and
context.mode is always "command" or "verbatim". Each handler sees the
text returned by the preceding handler:
rune.hooks.on("input", function(text, context) if context.mode == "verbatim" then -- The handler receives one line in either mode. rune.echo("Sending verbatim line (" .. #text .. " bytes)") end return text:gsub("^l$", "look")end, { priority = 10 })In both modes, each physical line runs through the input hook chain, local echo,
and dispatch before the next line begins. Command batches skip whitespace-only
lines; Verbatim preserves them. An empty single-line Enter still invokes hooks.
false consumes only the current line. Existing one-argument handlers remain
valid because Lua ignores the context argument.
Replacements must remain single-line. A replacement containing LF or CR is
rejected before subsequent handlers run. Final Command replacements must also
exclude invalid UTF-8 and terminal controls. Invalid replacements
skip the affected line. To send several lines deliberately, call rune.send or
rune.send_raw and return false to consume the original line.
History is saved after processing as one entry containing the surviving lines.
Input hooks, echo hooks, and command handlers do not yet see that entry. Explicit
script additions are immediately visible to both history reads and later history
expansion. An empty final history string is not saved.
An accepted "" replacement still reaches echo and dispatch.
See the input hook compatibility note for the changes from whole-draft Verbatim hooks and earlier history timing.
All input handlers run in priority order, with lower numbers first. If none
returns false, Rune processes the final text after the last handler finishes.
Command mode applies slash commands, command separators, #N repeats, and
aliases. Verbatim mode sends each line without any of that command processing.
All input handlers finish before Rune echoes or dispatches the result.
History expansion is a fixed stage before all input hooks. With the
default history character, ! and !! repeat the last command, while
!prefix repeats the newest command beginning with prefix. These forms also
work as complete commands in a separator-chained line. Rune ignores local
slash commands, verbatim input, and earlier commands that still contain
history-expansion syntax for the current history character. If any expansion
has no match, Rune shows a warning and cancels the whole input line. Disable
the feature with:
rune.config.set("history_character", "")Only commands typed in normal input use history expansion. rune.send does
not perform it, and verbatim input bypasses command processing entirely.
Every input hook sees resolved text, regardless of priority. Hook rewrites do
not re-enter history expansion. Clearing input hooks does not disable expansion;
use history_character to configure or disable it.
Migration: The history-expansion hook no longer exists. Hooks that previously
inspected or produced history references before priority 100 now run after
expansion. To implement custom history syntax, disable built-in expansion and
resolve that syntax in your own input hook.
For output, prompt, and echo, the core handlers remain at priority 100:
trigger processing on output/prompt and the > styling on echo.
Register below 100 to run before them, or above 100 to see their results
(post-trigger rewrites; gagged lines never reach you).
Notification events
Section titled “Notification events”All handlers run; return values are ignored.
| Event | Args | Fired |
|---|---|---|
ready |
none | After core and user scripts load during startup or /reload, before Rune applies their settings and UI state |
connecting |
address | Dial started |
connected |
address | Connection established |
disconnecting |
none | Disconnect requested |
disconnected |
none | Connection closed |
reloading / reloaded |
none | Around /reload (order: reloading, ready, reloaded) |
loaded |
path | After /load or rune.load loads a file (not for startup auto-load) |
error |
message | On reported errors |
input_changed |
text | Whenever the input buffer changes, including typing or paste, history or completion, rune.input.set, and the post-submit draft |
window_size_changed |
width, height | On the first reported terminal size and every resize; rune.state.width/height already hold the new values |
gmcp |
package, data, raw JSON | On every GMCP message, before package-specific rune.gmcp.on handlers |
gmcp_enabled |
none | GMCP negotiated; the core handler sends Core.Hello |
window_size_changed receives the terminal columns and rows as numbers,
so a script can adapt an identified layout region at its own breakpoint. This
example assumes the active layout has a region with id = "sidebar"; see
Regions and inactive resources.
local function fit_sidebar(width) if width < 80 then rune.ui.regions.hide("sidebar") else rune.ui.regions.show("sidebar") endend
fit_sidebar(rune.state.width)rune.hooks.on("window_size_changed", fit_sidebar)/reload does not fire the resize hook. The explicit call using
rune.state.width sets the initial region state; the hook handles later
changes.
Named core handlers
Section titled “Named core handlers”Handlers the core registers under stable names, so you can disable or
replace them: log-output, log-echo (logging policy, priority 200),
gmcp-hello (the GMCP handshake), gmcp-reset, first-run-welcome,
and _completion_cache / _completion_input (tab-completion word harvesting,
priority 200).
Managing
Section titled “Managing”Standard registry management applies:
rune.hooks.get/enable/disable/remove(name), .list(), .count(),
.clear(), .remove_group(group): see
Registries. /hooks lists everything.
Related: Hooks & Events guide · rune.trigger · rune.gmcp