Skip to content

Triggers

A trigger reacts to a line the server sends. Two decisions define one: how it matches (the whole line exactly, a prefix, a substring, or a regex) and what it does (a string to send, or a Lua function to run).

A string action is a canned response:

rune.trigger.contains("You are hungry", "eat bread")

A function runs your own logic when the line matches: make decisions, track state, send commands, call any API.

rune.trigger.regex("^Your health is (\\d+)%\\.$", function(m)
if tonumber(m[1]) < 30 then
rune.send("quaff heal")
end
end)

A function can also shape the line itself: return a string to rewrite it (this is how you highlight) or false to gag it.

Use a string when a match should send a fixed command and nothing else. Use a function whenever you need logic, state, or control over the line; only a function can do any of those.

rune.trigger.exact(line, action, opts?) -- whole line matches exactly
rune.trigger.starts(prefix, action, opts?) -- line starts with prefix
rune.trigger.contains(text, action, opts?) -- line contains text
rune.trigger.regex(pattern, action, opts?) -- Go regexp, with captures

Matching runs against the clean line (ANSI stripped), so patterns don’t fight color codes. Pass raw = true to match the raw line instead. Regex patterns are validated at registration: a bad pattern raises immediately instead of failing silently at match time.

A string is sent as a command (; chaining and aliases apply). For regex triggers, %1, %2, and so on are substituted from captures:

rune.trigger.regex("^(\\w+) gives you a (.+)\\.$", "thank %1")

A function is called with (matches, ctx). matches is the array of regex captures (empty for the literal modes). ctx carries line, name, group, type, and matches; ctx.line is a line object with :raw() and :clean(). The return value controls the output:

Return Effect
nil Line passes through unchanged
a string Rewrites the line; this is how you highlight
false Gags the line

A string action never touches the line; it only sends. Rewriting and gagging are function-return features, plus the gag option.

Rewrites chain: later triggers match against and receive the rewritten line, so a highlighter and a tagger compose:

rune.trigger.contains("dragon", function(m, ctx)
return rune.style.red(ctx.line:clean())
end)
rune.trigger.contains("dragon", function(m, ctx)
return "!! " .. ctx.line:raw()
end)
-- output: "!! <red line>"

Triggers take the common options group, priority and once, plus a name to manage one by. A pattern cannot serve as the name, since several triggers can match the same line: that is what makes a highlighter and a tagger compose. For the same reason rune.trigger.exact does not name a trigger the way rune.alias.exact names an alias. Trigger-specific options:

Option Effect
gag Hides matching lines (no action required).
raw Matches against the raw line, ANSI codes included.
on "output" (default) for complete lines, or "prompt" for prompt text. See Prompt triggers.
span Collects a multi-line output message before firing. See Multi-line triggers.

Pure gag, no action needed:

rune.trigger.contains("The shopkeeper hums", nil, { gag = true })

Capture and act:

rune.trigger.regex("^(\\w+) tells you: follow me$", function(m)
rune.send("follow " .. m[1])
end)

Mirror to a pane (gag from the output pane, keep it in the named pane):

rune.trigger.regex("^\\[Auction\\] (.+)$", function(m, ctx)
rune.pane.write("auctions", ctx.line:raw())
return false
end)

Most triggers run after a complete line arrives. Some MUDs send login and command prompts without a newline, so waiting for a complete line would miss them. Rune shows the partial line at the end of each network batch; use on = "prompt" to match it:

rune.trigger.contains("Username:", "Ragnar",
{ on = "prompt", once = true })

A trigger observes one of two streams:

Setting Text it observes
on = "output" Complete server lines. This is the default.
on = "prompt" Partial lines and prompts confirmed by Telnet GA/EOR.

Before the server finishes the text, Rune cannot know whether it is a prompt or the beginning of an ordinary line. A prompt trigger may therefore run again as the partial line grows. If CR/LF eventually completes the line, that complete line runs through output triggers; if a GA/EOR prompt boundary confirms it as a prompt, it stays with the prompt triggers. Make repeated prompt actions harmless, or use once = true for one-shot work such as login automation.

Prompt triggers can rewrite or gag the displayed prompt just like output triggers. They cannot use span, because spans collect complete output lines.

Submitting input finishes the partial line before the input is processed. A command sent by Lua finishes it only after the connection accepts the send; failed sends and protocol traffic such as GMCP leave it open.

Plenty of server output spans lines: chat and tells that wrap, score sheets, who lists, quest logs, mail. A plain trigger only ever sees one line at a time; a span collects the block and fires the action once, with everything.

For wrapped messages, the end of the block is usually a pattern on the final line — many LPMuds leave the channel’s ANSI color reset on the last wrapped line (log raw output to find your server’s marker):

rune.trigger.regex("^(\\w+) tells you: (.+)$", function(m, ctx)
rune.pane.write("chat", "[Tell] " .. m[1] .. ": " .. ctx.text)
end, { span = { to = "\\x1b\\[0?m\\s*$", raw = true, max = 8 } })

span.to is the regex for the line that ends the message, inclusive. span.raw matches it against the raw line, since stripping removes escape codes. span.max (default 8) is the safety cap when the terminator never shows.

For fixed-shape blocks, skip to and use max alone — the span collects exactly that many lines:

-- A score block that is always 4 lines
rune.trigger.starts("You have scored", function(m, ctx)
parse_score(ctx.lines)
end, { span = { max = 4 } })

In the action, ctx.text is the joined text and ctx.lines has the individual lines. The collected lines have already been displayed by the time the action runs, so return values do nothing; gag = true is the exception and hides every collected line as it arrives:

-- Silence a multi-line broadcast entirely
rune.trigger.starts("The town crier bellows:", nil,
{ gag = true, span = { to = "\\x1b\\[0?m\\s*$", raw = true } })

Full semantics: rune.trigger reference.

Test output triggers without a server with /test <line>. It runs one complete line through the output triggers and shows the result. Multi-line spans collect across /test calls, one line per call. /test does not run prompt triggers.

Every constructor returns a handle:

local h = rune.trigger.contains("hungry", "eat bread", { name = "auto-eat" })
h:disable() h:enable() h:remove()

By name: rune.trigger.disable/enable/remove(name). The full list is in the API reference. In the client, /triggers shows every trigger with its state, mode, flags, group, and the file:line that registered it.

  • Patterns are Go regexp (RE2), not Lua patterns: \\d, \\w, and \\s work, backreferences and lookaround do not — see rune.regex for the syntax notes.
  • span requires on = "output". Partial-line observations leave a span open; a GA/EOR-confirmed prompt or a finished partial line closes it. See the full span semantics.
  • A trigger that errors three times in a row is quarantined.

Related: rune.trigger reference, Aliases, Hooks & Events, Panes