Entry Scripts
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.
Reacting to settings changes in a service
Section titled “Reacting to settings changes in a service”When a user edits a plugin setting, the change reaches each entry differently:
- Widgets, desktop widgets, panels are rebuilt, so their next
getConfig(...)reads the new value automatically. - Services are long-lived. If a service defines
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 defineonConfigChanged(), 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 endendDesktop widgets and panels use declarative ui.* trees. See Declarative UI
for their rendering APIs and controls.
barWidget.* - widget presentation + settings
Section 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")endCustom fonts
Section titled “Custom fonts”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")endshortcut.* - control-center tile
Section 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()endlauncher.* - launcher provider
Section 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)
querymust echo thetextfromonQuery, 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.- Each result is a table:
{ id, title, subtitle?, glyph?, icon?, badge?, query?, score? }.idis passed back toonActivateunlessqueryis set. Results are ordered byscore(descending), then insertion order. glyph(a Tabler/Nerd-Font name) oricon(a themed icon name) is the row’s leading visual.badgeis a short string (e.g. an emoji or=) drawn in place of the icon - setting it hidesglyph/icon. Use thesubtitlefor secondary text.setQuery(query)replaces the open launcher’s raw input text. Call it fromonActivateto treat a result as a drill-in step: the launcher rewrites the input and stays open instead of closing, then re-runsonQuerywith the new text. A result-levelqueryfield does the same declaratively - when that result is selected the launcher replaces the input with it and stays open, noonActivateneeded. Either way, include your prefix to stay routed to the same provider, e.g."/tr french ". (AnonActivatethat does not callsetQuerycloses 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