Perch
Lua APISystem

System

perch.system: performance, the transient signals, power and peripherals, downloads, mirrored notifications, the clipboard, and the system types.

5 min readUpdated Sep 9, 2026

The system libraries are split into narrow scopes so a mod discloses exactly what it watches. Sound has its own page: Audio.

ScopeInstalls
system.statsperch.system.performance.read()
system.transientsperch.audio.volume.on_change, perch.system.display.on_brightness_change, .on_night_light_change, perch.system.connectivity.on_change, perch.system.timers.on_set, .on_tick, .on_done, .on_cancel
system.powerperch.system.power.read(), .on_change, perch.system.peripherals.list()
system.downloadsperch.system.downloads.on_progress
system.notificationsperch.system.notifications.configure(options), .on_notification
system.clipboardperch.system.clipboard.read(), .on_change

The stats card with CPU, GPU and RAM, and two HUDs: a mirrored notification and a battery note

perch.system.performance.read

perch.system.performance.read(): performance_t

CPU, GPU and RAM usage. Synchronous; cpu and gpu are deltas sampled at most every 500 ms. There is no signal, so poll from a timer while your page is visible.

local poll = timer.new(1000)
poll.on_fire:connect(function()
  local s = perch.system.performance.read()
  stats:push({ cpu = if s.cpu >= 0 then s.cpu / 100 else 0 })
end)
stats.on_visibility:connect(function(visible: boolean)
  if visible then poll:start() else poll:stop() end
end)

The transient signals

Scope system.transients: short-lived changes, the kind a widget shows for two seconds.

perch.audio.volume.on_change: signal_t<volume_state_t>
perch.system.display.on_brightness_change: signal_t<brightness_state_t>
perch.system.display.on_night_light_change: signal_t<night_light_state_t>
perch.system.connectivity.on_change: signal_t<connectivity_event_t>
perch.system.timers.on_set: signal_t<timer_set_t>
perch.system.timers.on_tick: signal_t<timer_tick_t>
perch.system.timers.on_done: signal_t<timer_done_t>
perch.system.timers.on_cancel: signal_t<>

on_brightness_change reads internal panels through WMI and external monitors over DDC/CI while a change is in progress. on_night_light_change never fires in this release: Windows has no night light source yet. The timers signals describe the countdown a user sets by voice (“set a five minute timer”); on_tick fires once per second during the last ten seconds. Your own timers fire on_fire on the handle you created and never share a signal with these.

perch.system.timers.on_tick:connect(function(t: timer_tick_t)
  countdown:present({ secs = tostring(t.secs), name = t.name })
end)

perch.system.power

Scope system.power.

perch.system.power.read(): power_t
perch.system.power.on_change: signal_t<power_event_t>
perch.system.peripherals.list(): { peripheral_t }

read is the machine’s battery state; a desktop on mains reports has_battery = false. on_change fires when the charge crosses down through 20, 10 or 5 percent on battery (battery = true, name = "Battery"), and when a device with its own battery appears (battery = false, name the device). Read once at start; the signal only says what changed.

peripherals.list is every device that reports its own charge: a headset, a wireless mouse or keyboard, a controller. Served from a cache; poll it while visible for a live readout.

local rows = {}
for _, d in ipairs(perch.system.peripherals.list()) do
  if d.percent >= 0 then rows[#rows + 1] = { name = d.name, frac = d.percent / 100 } end
end
w:push({ rows = rows })

perch.system.downloads.on_progress

perch.system.downloads.on_progress: signal_t<download_progress_t>

Scope system.downloads. About once per second while a browser download is in progress. Browsers do not expose the total, so frac is -1; a negative value on a ui.bar shows the indeterminate shimmer.

perch.system.notifications

Scope system.notifications. Mirroring is off until the mod asks for it, and the Workshop page words this scope loudly.

perch.system.notifications.configure(options: notifications_options_t): ()
perch.system.notifications.on_notification: signal_t<system_notification_t>

configure({ mirror = true }) starts the listener; false stops it. Notifications from Discord and from Perch itself are never delivered. icon is an absolute path without a file:// prefix; add it before binding to a ui.image.

perch.system.notifications.on_notification:connect(function(n: system_notification_t)
  toast:present({ app = n.app, title = n.title, body = n.body, icon = if n.icon ~= "" then "file://" .. n.icon else "" })
end)

perch.system.clipboard

Scope system.clipboard. This scope reads everything the user copies, passwords and card numbers included. Ask for it only when copying is the point of the mod, and never send what you read off the machine.

perch.system.clipboard.read(): string
perch.system.clipboard.on_change: signal_t<string>

read is the clipboard’s current text, or "" when it holds something that is not text or another application has it locked. on_change fires with the new text; there is no baseline event at start.

perch.system.clipboard.on_change:connect(function(text: string)
  if text:match("^https?://%S+$") then w:present({ text = "Link copied" }) end
end)

Types

export type performance_t = { cpu: number, gpu: number, ram: number, cput: number, gput: number }

Percentages 0..100; -1 means not available. gpu comes from the WDDM engine counters and is -1 on drivers without them; the temperatures cput and gput are always -1 on Windows in this release.

export type volume_state_t = { level: number, muted: boolean }
export type brightness_state_t = { level: number }
export type night_light_state_t = { on: boolean }

level is 0..1 in both; volume is 0 while muted.

export type connectivity_event_t = { kind: "wifi" | "bt" | "usb", name: string, connected: boolean, pct: number }

name is "" when unknown; pct is the device’s battery percentage or -1.

export type timer_set_t = { name: string, mmss: string }
export type timer_tick_t = { name: string, secs: number }
export type timer_done_t = { name: string }

The voice timer: mmss is "MM:SS" or "H:MM:SS"; secs counts down.

export type power_t = { has_battery: boolean, percent: number, charging: boolean, plugged_in: boolean, saver: boolean, seconds_left: number }
export type power_event_t = { battery: boolean, name: string, pct: number }
export type peripheral_t = { name: string, percent: number }

percent and seconds_left are -1 when the machine cannot say; a peripheral’s percent is -1 when the device reports no usable capacity.

export type download_progress_t = { name: string, frac: number, active: boolean, bytes: number, rate: number }

name is "" while Chromium still hides it and the final name on completion; bytes received so far and rate in bytes per second, -1 when unknown.

export type notifications_options_t = { mirror: boolean? }
export type system_notification_t = { app: string, title: string, body: string, icon: string }

The argument of configure, and a mirrored notification.

esc
Type to search
navigate open