Perch talks to the local Discord client over its RPC socket, using the Application ID the user entered in Settings (see Connect Discord).
| Scope | Installs |
|---|---|
discord | perch.discord.read(), perch.discord.on_call, .on_notification, .on_ring, .on_status |
discord.control | perch.discord.toggle_mute(), .toggle_deafen(), .hangup(), .accept_ring(), .decline_ring(), .connect(), .disconnect(), .configure(options) |

perch.discord.read
perch.discord.read(): discord_state_t
The link status. Synchronous; every flag is false before the first update, and all false together means Discord is not running.
perch.discord.on_status
perch.discord.on_status: signal_t<discord_state_t>
Fires when the link status changes. The builtin package drives its settings rows from it:
--!strict
local rows = perch.settings.declare {
__order = { "link", "connect", "disconnect" },
link = settings.status("Discord account"),
connect = settings.button("Connection", "Connect", function() perch.discord.connect() end),
disconnect = settings.button("Sign out", "Disconnect", function() perch.discord.disconnect() end),
}
perch.discord.on_status:connect(function(s: discord_state_t)
rows.link:set(if s.connected then "Connected" else "Not connected", s.connected)
rows.connect:set_visible(not s.connected)
rows.disconnect:set_visible(s.connected)
end)
perch.discord.on_call
perch.discord.on_call: signal_t<discord_call_t>
Fires when a call is joined or left. Speaking rings, mute flips and participant changes during a call are pushed natively into the builtin Discord widgets and do not re-fire it.
Avatars are downloaded once into Perch’s avatar cache and referenced as file:// URLs; the first event may carry "" for avatars still downloading, followed by a second event once they land. Declare the slice you bind and copy it over:
--!strict
type face_refs_t = {
name: field_ref_t<string>, initial: field_ref_t<string>, avatar: field_ref_t<string>, hue_css: field_ref_t<string>,
speaking: field_ref_t<boolean>, muted: field_ref_t<boolean>,
}
type fields_t = { channel: field_ref_t<string>, started: field_ref_t<number>, faces: list_ref_t<face_refs_t> }
local call = widget.new { id = "call", label = "Call", urgency = urgency.ambient, fields = {
channel = field.string(""), started = field.time_ms(),
faces = field.list { name = field.string(""), initial = field.string(""), avatar = field.image(), hue_css = field.color(),
speaking = field.boolean(false), muted = field.boolean(false) },
} }
perch.discord.on_call:connect(function(c: discord_call_t)
if not c.in_call then
call:retire()
return
end
local faces = {}
for i, face in ipairs(c.faces) do
faces[i] = { name = face.name, initial = face.initial, avatar = face.avatar, hue_css = face.hue_css, speaking = face.speaking, muted = face.muted }
end
call:present({ channel = c.channel, started = c.started or 0, faces = faces })
end)
perch.discord.on_notification
perch.discord.on_notification: signal_t<discord_notification_t>
A message notification.
perch.discord.on_ring
perch.discord.on_ring: signal_t<discord_ring_t>
An incoming call started ringing. Discord sends no “stopped ringing” event; expire the ring yourself (the builtin package uses 32 seconds) or on the next on_call.
The control verbs
Scope discord.control. Each takes no arguments and returns nothing.
| Verb | Effect |
|---|---|
perch.discord.toggle_mute() | mute or unmute the microphone |
perch.discord.toggle_deafen() | deafen or undeafen |
perch.discord.hangup() | leave the current call |
perch.discord.accept_ring() | join the ringing DM call |
perch.discord.decline_ring() | dismiss the ring locally; Discord’s RPC has no reject |
perch.discord.connect() | sign back in to the RPC link |
perch.discord.disconnect() | sign out: forgets the token, keeps the Application ID |
ui.button { on_click = function() perch.discord.toggle_mute() end, w = 25, h = 25, radius = 5, bg = ui.bg.well,
style = { f.self_mute:is_true():set { color = ui.color.red, bg = "#f0474729" } },
ui.shape { src = "glyphs/mic.svg", w = 13, h = 13 } }
The same verbs answer the voice router through perch.voice.on_intent: toggle_mute, toggle_deafen, hangup, accept_call, decline_call.
perch.discord.configure
perch.discord.configure(options: discord_options_t): ()
Stores a new Application ID (digits; anything else counts as not set up), invalidates the token and reconnects. The builtin package’s settings page calls it; a third-party mod rarely needs to.
Types
export type discord_state_t = { connected: boolean, needs_auth: boolean, needs_setup: boolean, disconnected: boolean? }
| Field | Meaning |
|---|---|
connected | the RPC link is up |
needs_setup | no Application ID has been entered |
needs_auth | the user denied, or the authorization failed |
disconnected | the user signed out in Settings |
export type discord_call_t = {
in_call: boolean, started: number?, dm: boolean, server: string, server_icon: string, channel: string,
self_mute: boolean, self_deaf: boolean,
names: { string }, avatars: { string }, speaking: { number }, streaming: { number }, video: { number }, muted: { number }, deafened: { number },
count: number, faces: { discord_face_t }, has_faces: boolean, solo: discord_face_t?,
}
| Field | Meaning |
|---|---|
in_call | in a call |
started | call start, epoch milliseconds; only while in a call |
dm | a DM or group call (no server) |
server, server_icon | the server name and its icon as a file:// path, "" for DMs |
channel | the channel name |
self_mute, self_deaf | the local user’s voice settings |
names, avatars | one entry per participant; index 1 is the local user; an avatar is a file:// path or "" |
speaking, streaming, video, muted, deafened | 0-based indices into names; a deafened user is not repeated in muted |
count | number of participants |
faces, has_faces | up to 12 participants as records: the local user first, then speakers, then the rest |
solo | the last person who spoke, falling back to the local user; only when count > 0 |
export type discord_face_t = { name: string, initial: string, avatar: string, hue_css: string, speaking: boolean, streaming: boolean, muted: boolean, deafened: boolean }
One participant. initial is the uppercased first character; hue_css is a ready linear-gradient(...) for a monogram background (declare it field.color() and bind bg = it.hue_css).
export type discord_notification_t = { from: string, text: string, avatar: string, context: string, at: number }
context is "" for a DM, "#channel" for a server channel (then avatar is the server icon), or the group name; at is epoch milliseconds.
export type discord_ring_t = { active: boolean, from: string, avatar: string }
export type discord_options_t = { app_id: string? }
A ring, and the argument of configure.