Perch never calls a function by name. Every event is a signal you connect a function to, and the connection is what subscribes you. A signal is named on_ plus what happened, and there is one signal per event.
perch.on_start:connect(function() ... end)
perch.media.on_change:connect(function(m: media_state_t) ... end)
w.on_visibility:connect(function(visible: boolean) ... end)
tick.on_fire:connect(function() ... end)
Handlers receive only their payload; their return value is ignored. Several handlers on one signal run in connection order. Connecting inside a handler is safe: the new handler runs from the next fire. Every payload has a named type; annotate the parameter with it and Studio completes the fields.
Connecting and disconnecting
| Method | Meaning |
|---|---|
signal:connect(fn): connection_t | attach a handler |
signal:once(fn): connection_t | attach a handler that detaches itself before its first run |
connection:disconnect() | detach |
connection:connected(): boolean | whether it is still attached |
connect with anything but a function raises connect: expected a function, got <type>.
local conn: connection_t
conn = perch.media.on_change:connect(function(m: media_state_t)
if m.has_player then
seed(m)
conn:disconnect() -- only the first real state was wanted
end
end)
Lifecycle
perch.on_configure: signal_t<any>
perch.on_start: signal_t<>
perch.on_stop: signal_t<>
perch.on_configure fires once before perch.on_start with the mod’s stored settings, keyed by the row keys you declared, and again on every change the user commits. Annotate the parameter with your own settings type. It is the place to diff a settings.list into widgets.
perch.on_start fires when the mod is live: every register, present and push made earlier has been applied. Declare presence, start timers and drive settings rows here.
perch.on_stop fires when the mod is stopping: Perch is quitting, or every widget was disabled. Timers are cleared afterwards; a request in flight is dropped and its callback never runs.
type settings_t = { zone: string?, fmt24: boolean? }
perch.on_configure:connect(function(s: settings_t)
zone = s.zone or perch.time.zone()
end)
perch.on_start:connect(function()
w:present(payload())
tick:start()
end)
Timers
Timers are the only way a mod wakes itself up. There is no per-frame hook and no tick; for text that changes every second use ui.ticker, which runs on the view side.
timer.new
timer.new(ms: number, opts: timer_options_t?): timer_t
A timer handle, not yet running. ms is the period, floored to 250 ms for a repeating timer and 16 ms for a one-shot ({ once = true }). A non-positive period raises.
local poll = timer.new(60 * 1000)
poll.on_fire:connect(function() refresh() end)
perch.on_start:connect(function() poll:start() end)
local settle = timer.new(500, { once = true })
settle.on_fire:connect(function() w:push({ ready = true }) end)
settle:start()
timer.at
timer.at(hour: number, minute: number, opts: timer_at_options_t?): timer_t
A one-shot for a wall-clock time: it fires at that time today, or tomorrow if it has passed. hour is 0..23, minute 0..59; { zone = "Europe/Berlin" } reads the clock in that zone. Every :start() recomputes the delay from the clock, so re-arming the handle inside its own handler fires at the same time the next day.
local alarm = timer.at(7, 30)
alarm.on_fire:connect(function()
w:present({ line = "Good morning" })
alarm:start() -- tomorrow, same time
end)
alarm:start()
The handle
| Member | Meaning |
|---|---|
t.on_fire | signal_t<>; fires each period with no payload |
t:start(ms?) | arm it, or re-arm it (restarts the period; the argument changes it) |
t:stop() | disarm it; connections survive a later start |
t:running() | whether it is armed |
Timers are cleared when the mod stops.
Callbacks
A call that has to wait takes its callback as the last argument and returns nothing. HTTP calls back exactly once with (res, err); a Steam lookup calls back at once when Perch already knows the answer and when it lands otherwise. Both run on the main thread. See Net and Steam.
perch.net.http.get(url, function(res: http_response_t?, err: string?)
if not res then perch.log(err) return end
w:push({ body = res.body })
end)
Every signal
Each row exists only when the scope in the second column is declared. The payload types are on the library pages.
| Signal | Scope | Payload | Fires |
|---|---|---|---|
perch.on_configure | always | your settings table | before start, and on every settings change |
perch.on_start, perch.on_stop | always | none | the mod goes live, or stops |
w.on_visibility | always | boolean | the page card comes on screen or leaves |
t.on_fire | always | none | a timer period elapses |
perch.media.on_change | media | media_state_t | now-playing state changed |
perch.discord.on_call | discord | discord_call_t | a call was joined or left |
perch.discord.on_notification | discord | discord_notification_t | a message notification |
perch.discord.on_ring | discord | discord_ring_t | an incoming call started ringing |
perch.discord.on_status | discord | discord_state_t | the link status changed |
perch.weather.on_change | weather | weather_state_t | a forecast arrived or the unit changed |
perch.steam.on_game_change | steam | game_change_t | a game started or stopped |
perch.steam.on_app_info, on_artwork, on_store_details, on_news, on_player_count, on_player_history, on_search | steam | the matching steam_*_t | a lookup landed |
perch.audio.volume.on_change | system.transients | volume_state_t | output volume or mute changed |
perch.system.display.on_brightness_change | system.transients | brightness_state_t | display brightness changed |
perch.system.display.on_night_light_change | system.transients | night_light_state_t | night light toggled |
perch.system.connectivity.on_change | system.transients | connectivity_event_t | a device connected or disconnected |
perch.system.timers.on_set, on_tick, on_done, on_cancel | system.transients | timer_set_t, timer_tick_t, timer_done_t, none | a voice timer was set, counts down, finished, was cancelled |
perch.system.power.on_change | system.power | power_event_t | a battery threshold or a device with a battery |
perch.system.downloads.on_progress | system.downloads | download_progress_t | a browser download progressed |
perch.system.notifications.on_notification | system.notifications | system_notification_t | a Windows notification was mirrored |
perch.system.clipboard.on_change | system.clipboard | string | the clipboard changed |
perch.apps.on_focus_change | apps | focused_app_t | the foreground application changed |
handle.on_press | input.hotkeys | none | a claimed shortcut was pressed |
channel.on_message | mods.messaging | T, string | another mod published on that channel |
socket.on_message | net.websocket:<host> | string | a complete message arrived |
socket.on_close | net.websocket:<host> | number, string | the socket closed, cleanly or by failing |
Types
export type signal_t<T...> = {
connect: (self: signal_t<T...>, handler: (T...) -> ()) -> connection_t,
once: (self: signal_t<T...>, handler: (T...) -> ()) -> connection_t,
}
Every on_* in the API. T... is what the handler receives: signal_t<media_state_t> hands over one table, signal_t<number, string> two values, signal_t<> nothing.
export type connection_t = {
disconnect: (self: connection_t) -> (),
connected: (self: connection_t) -> boolean,
}
What connect and once return.
export type timer_t = {
on_fire: signal_t<>,
start: (self: timer_t, ms: number?) -> timer_t,
stop: (self: timer_t) -> timer_t,
running: (self: timer_t) -> boolean,
}
export type timer_options_t = { once: boolean? }
export type timer_at_options_t = { zone: string? }
The timer handle and the option tables of timer.new and timer.at.
declare timer: {
new: (ms: number, opts: timer_options_t?) -> timer_t,
at: (hour: number, minute: number, opts: timer_at_options_t?) -> timer_t,
}