Skip to content

rune.hooks

Hooks attach handlers to client events: input, output, connection lifecycle, GMCP, and more. For a task-oriented introduction, see Hooks & Events.

rune.hooks.on(event, handler, opts?) -- attach a handler to an event
rune.hooks.enable(name) -- re-enable a named handler
rune.hooks.disable(name) -- disable without unregistering
rune.hooks.remove(name) -- unregister a named handler
rune.hooks.list() -- all handlers with event, priority, source
rune.hooks.clear(event?) -- remove handlers for one event, or all
rune.hooks.has(event) -- true if the event has handlers
rune.hooks.count(event?) -- handlers for one event, or total
rune.hooks.remove_group(group) -- remove all handlers in a group

on returns a handle and accepts the common options (name, group, priority).

rune.hooks.on(event, handler, opts?) -> handle
  • event (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. priority defaults to 50; lower runs first.
rune.hooks.on("connected", function(addr)
rune.send("look")
end, {name = "auto-look"})

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())
end
end)

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).

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")
end
end
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.

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).

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