v5-plugins-development-entries.md
A script defines global functions that Noctalia calls, and drives the UI through the API namespaces. Which globals apply depends on the entry kind:
| Function | Widget | Shortcut | Launcher | Desktop | Panel | Service | When |
|---|---|---|---|---|---|---|---|
update() | ✓ | ✓ | ✓ | ✓ | every update interval | ||
onClick() / onRightClick() | ✓ | ✓ | pointer press | ||||
onMiddleClick() | ✓ | middle press | |||||
onQuery(text) | ✓ | launcher text changed (behind the prefix) | |||||
onActivate(id) | ✓ | a launcher result was selected | |||||
onOpen(context) / onClose() | ✓ | the panel is opened / closed | |||||
onFrameTick(deltaMs) | ✓ | every frame, after desktopWidget.setNeedsFrameTick(true) | |||||
onIpc(event, payload) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | noctalia msg plugin … |
onConfigChanged() | ✓ | a plugin setting changed (see below) |
The top level of the script runs once at load - set up state and register noctalia.state.watch handlers there.
Section titled “Reacting to settings changes in a service”
When a user edits a plugin setting, the change reaches each entry differently:
getConfig(...) reads the new value automatically.onConfigChanged(), the host updates its settings in place and calls that function , noctalia.getConfig(...) returns the new values, and the service keeps all its in-memory state (timers, caches, connections). If a service does not define onConfigChanged(), the host instead restarts the service runtime (the top-level chunk re-runs with the new settings).Either way, noctalia.state is process-lifetime and survives a service restart, so any cache you store there must be keyed by the config it depends on , otherwise a service will keep serving data fetched for the old settings. For example, store the location alongside cached results and re-fetch when it differs:
function onConfigChanged() local loc = noctalia.getConfig("city") .. "|" .. noctalia.getConfig("country") if loc ~= noctalia.state.get("location") then noctalia.state.set("location", loc) refetch() -- location changed; invalidate and reload endend
Desktop widgets and panels use declarative ui.* trees. See Declarative UI for their rendering APIs and controls.
barWidget.* - widget presentation + settingsSection titled “barWidget.* - widget presentation + settings”
setText · setGlyph · setImage · setTooltip · clearTooltip · setFont · setColor · setGlyphColor · setVisible · isVertical · render · getConfig(key)
A bar widget can either patch the built-in glyph/text row with the imperative setters below, or describe composite content as a ui.* tree with barWidget.render(tree) - see Declarative UI.
local label = barWidget.getConfig("label")
function update() noctalia.setUpdateInterval(1000) barWidget.setGlyph("puzzle") barWidget.setText(label)end
function onClick() noctalia.notify("Hello", "you clicked me")end
Register a font file with noctalia.loadFont(path), then pass the returned family name to setFont. The font becomes usable anywhere text is drawn - setFont and a ui.* label’s fontFamily prop - and stays available across every surface once loaded.
setFont(family, baseline?) takes a font family name and an optional baseline mode:
| Baseline | Use |
|---|---|
"text" (default) | Normal text, cap-band centered. |
"textFixedHeight" | Text whose row height is locked to the font metrics (steady height in lists). |
"inkCentered" | Centers the glyph’s ink. |
"pictographic" | Art/icon fonts anchored at the ink top (keeps pose art steady, e.g. bongocat). |
local font = noctalia.loadFont("fonts/bongocat.otf")if font then barWidget.setFont(font, "pictographic")end
shortcut.* - control-center tileSection titled “shortcut.* - control-center tile”
setLabel(text) · setIcon(on [, off]) · setActive(bool) · setEnabled(bool)
local on = noctalia.state.get("toggled") == true
local function render() shortcut.setLabel(on and "On" or "Off") shortcut.setIcon("bulb") shortcut.setActive(on)endrender()
function onClick() on = not on noctalia.state.set("toggled", on) render()end
launcher.* - launcher providerSection titled “launcher.* - launcher provider”
A launcher provider answers queries behind a prefix declared in the manifest. The user types the prefix, and everything after it is passed to onQuery(text); the provider replies with launcher.setResults(query, results). onActivate(id) runs when a result is selected (the id is whatever you set on that result).
Manifest fields on a [[launcher_provider]] entry: prefix (the trigger - use a leading / to match the built-in providers and appear in the launcher’s / overview), glyph (default result icon), include_in_global_search (also answer the un-prefixed search; default false), and debounce_ms (wait this long after the last keystroke before running onQuery - set it for network-backed providers so you aren’t called on every character; default 0).
setResults(query, results) · setQuery(query) · getConfig(key)
query must echo the text from onQuery, so late results map back to the query they answer - the latest one wins. Calling it with an empty list clears the provider’s results.{ id, title, subtitle?, glyph?, icon?, badge?, query?, score? }. id is passed back to onActivate unless query is set. Results are ordered by score (descending), then insertion order.glyph (a Tabler/Nerd-Font name) or icon (a themed icon name) is the row’s leading visual. badge is a short string (e.g. an emoji or =) drawn in place of the icon - setting it hides glyph/icon. Use the subtitle for secondary text.setQuery(query) replaces the open launcher’s raw input text. Call it from onActivate to treat a result as a drill-in step: the launcher rewrites the input and stays open instead of closing, then re-runs onQuery with the new text. A result-level query field does the same declaratively - when that result is selected the launcher replaces the input with it and stays open, no onActivate needed. Either way, include your prefix to stay routed to the same provider, e.g. "/tr french ". (An onActivate that does not call setQuery closes the launcher as usual.)Use noctalia.fuzzyScore(pattern, text) to rank local lists with Noctalia’s native fuzzy matcher. It returns a numeric score, or nil when pattern does not match text.
Queries run off the UI thread, so a result can arrive after onQuery returns - publish a placeholder synchronously, then call setResults again from an async callback (HTTP, subprocess) when the real answer lands.
[[launcher_provider]]id = "translate"entry = "translator.luau"prefix = "/tr"glyph = "language"
function onQuery(text) if text == "" then launcher.setResults(text, { { id = "hint", title = "Type something to translate" } }) return end launcher.setResults(text, { { id = "loading", title = "Translating…", glyph = "loader" } }) noctalia.http({ url = endpoint(text) }, function(res) launcher.setResults(text, { { id = res.body, title = res.body, glyph = "language" } }) end)end
function onActivate(id) noctalia.copyToClipboard(id, "text/plain")end