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 · outputName · 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.
outputName() returns the connector name of the monitor this widget instance’s bar is on (or nil when unknown).
Each placement of the widget gets its own value, so a widget on two bars can scope its content per monitor - unlike
noctalia.focusedOutputName(), which returns the same globally focused output everywhere. Cross-reference
noctalia.outputs() by name when you need
geometry.
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