Lua scripting reference¶
Every builtin the Lua script host registers, with its signature, parameters, and behaviour. The host runs Lua 5.4. For the task-oriented introduction see Scripting.
The same surface in the other hosts, under the same names:
candela, rhai. One structural
difference shapes those names: a candela host function is keyed by name alone
and cannot be overloaded on arity, so a call with two forms gets two names on
every host: page(path) and page_current(), computed_style(prop) and
computed_style_all().
Selecting the host¶
Each script file picks its host from its own extension: .cdl runs under
candela, .lua under Lua, .rhai under Rhai. Files of one language combine
into a single program; an app that ships two languages runs both hosts, sharing
signals but not functions.
An inline <script> block has no extension. It joins the app's one external
language when there is exactly one, and candela otherwise.
lumen.toml overrides all of it and puts every script on one engine:
Reaching the builtins¶
Builtins are Lua globals; there is no import or require step.
Handles (Signal, ArraySignal, Node, NodeQuery, Event) are userdata
with methods, called with the colon form. The chained signals accessor works
with either form, because the value is always the last argument:
signals.count.set(5) and signals.count:set(5) both write 5.
Arrays follow Lua convention where the host builds them: ArraySignal:get(i),
the numeric signals.users[i] subscript, and every returned sequence table are
1-indexed.
print(...) is captured into the script command stream instead of writing to
stdout; arguments are joined with a tab.
Lifecycle hooks¶
Define these as global functions. Each is optional; a missing hook is a no-op.
| Hook | Fires |
|---|---|
on_start() |
Once at app construction, before the first tick. No element is queryable yet: get_by_id returns nil. |
on_ready() |
Once per mount, on the first tick after the element tree is published. Queries resolve here. Re-armed after a hot reload, so it runs again on the fresh tree. |
on_close() |
On an OS close request, before teardown. Return false to veto the close and keep the window open. |
on_archive_done(tag, dest, count) |
When an extraction started through the lumen-archive module finishes; count is the number of files written. |
on_archive_error(tag, message) |
When an extraction started through the lumen-archive module is refused or fails. |
on_audio_end(path) |
When a track played through the lumen-audio module reaches its end; path is the path passed to audio_play. |
Top-level statements run once when the script loads, so that is where handler
functions and on(...) registrations are defined. A top-level local survives
as an upvalue of any function that closes over it. Across a hot reload the
signal values, the on(...) routing table, and live event bindings are
preserved.
Event handlers¶
Each event dispatches to a global handler function named below. Handlers are optional.
| Handler | Arguments |
|---|---|
on_click(id) |
Element id. Suppressed when a double-click fires on the same element in the same tick. |
on_double_click(id) |
Element id. |
on_long_press(id) |
Element id. |
on_toggle(id, checked) |
Element id, boolean. |
on_slider(id, value) |
Element id, number. |
on_text_input(id, text) |
Element id, current text. Fires on every edit that changes the text, and once more on Enter commit. |
on_drop(target_id, payload) |
Drop-zone id, the source's text payload. |
on_drag_start(source_id, payload) |
Drag-source id, its text payload. |
on_file_dropped(id, path) |
Element id, dropped file path. |
on_file_picked(tag, path) |
Dialog tag, chosen path. Empty path on cancel. |
on_files_picked(tag, paths) |
Dialog tag, paths joined with \|. |
on_folder_picked(tag, path) |
Dialog tag, chosen folder. |
on_hotkey(name) |
Registered hotkey name. |
on_hotkey_release(name) |
Registered hotkey name, on release of the chord. |
on_notification_action(id, action_id) |
Notification id, pressed button's action id. |
on_clipboard(tag, text) |
Read tag, clipboard text. Empty when the clipboard holds no text. |
on_menu(id) |
Menu item id. |
on_tray(id) |
Tray icon id. |
on_recent_files(tag, paths) |
Request tag, recorded paths joined with \|, most recent first. |
on_autostart_enabled(tag) |
Request tag; the app is set to launch at login. |
on_autostart_disabled(tag) |
Request tag; the app is not set to launch at login. |
on_second_instance(args) |
A second launch's command-line arguments, joined with \|. |
on_dialog_accepted(id) |
Dialog id. |
on_dialog_rejected(id) |
Dialog id. |
on_timer(name) |
Timer name. |
on_fetch(tag, body) |
Request tag, response body (2xx only). |
on_fetch_error(tag, message) |
Request tag, error text. |
on_http(tag, response) |
Request tag, the response table described under Networking. |
on_download_progress(tag, received, total) |
Download tag, bytes so far, the size the server declared or -1. Fires only for a transfer started by the lumen-download module. |
on_download_done(tag, path) |
Download tag, the path the downloaded file was written to. |
on_download_error(tag, message) |
Download tag, error text. |
on_process_stdout(tag, line) |
Tag given to process.start, one line the program wrote to standard output. |
on_process_stderr(tag, line) |
Tag, one line the program wrote to standard error. |
on_process_exit(tag, code) |
Tag, the program's exit code. Always the last event for a tag. |
Per-element routing¶
Routes one (event, id) pair to the script function named handler (a string),
bypassing the global handler for that pair only. event is the name without the
on_ prefix ("click", "toggle", "timer", "file_picked", ...). A handler
registered for save also matches template-instance ids ending in :save.
Signals¶
Handles¶
signal returns a handle to the named scalar signal, seeding it with default
the first time that name is seen. A value pushed in before the script loaded
wins over the default. signal_array returns a handle to the named reactive
array that drives <for each="name">.
| Method | Returns | Behaviour |
|---|---|---|
Signal:get() |
any | Current value. |
Signal:set(value) |
Replace the value. | |
ArraySignal:set(table) |
Replace all rows. | |
ArraySignal:push(item) |
Append one row. | |
ArraySignal:len() |
integer | Row count. |
ArraySignal:get(index) |
any | One row; 1-indexed. nil out of range. |
ArraySignal:all() |
table | Every row. |
Rows are string-keyed tables; their fields become the values a <for> block
binds to.
Chained access¶
signals is a pre-bound global. Each .name or [index] step extends the
property path, and a terminal method commits:
signals.count.set(5)
signals.user.name.set("Alice")
signals.users[1].name.set("Bo")
signals.bg.set_color("#ff8800")
local n = signals.count.get()
The Lua type of the argument picks the stored type: an integer stores an
integer, a number a float, a string a string, a boolean a boolean. Hex colours
need the explicit set_color method. Index subscripts render as [N] in the
property key, so signals.users[1].name is the key users[1].name.
Typed accessors¶
These predate the chained form and remain available.
| Builtin | Returns | Behaviour |
|---|---|---|
signal_get_int(name) |
integer | Read as an integer; nil on miss. |
signal_set_int(name, value) |
Write an integer. | |
signal_get_float(name) |
number | Read as a float; nil on miss. |
signal_set_float(name, value) |
Write a float. | |
signal_get_bool(name) |
boolean | Read as a boolean; nil on miss. |
signal_set_bool(name, value) |
Write a boolean. | |
signal_get_color(name) |
table | Read as { r, g, b, a } with integer channels; nil on miss. |
signal_set_color(name, hex) |
Write a hex colour: #rgb, #rgba, #rrggbb, or #rrggbbaa. An unparseable value is ignored. |
|
signals_all() |
table | The whole signal set as a name-to-value table. |
is_valid(id) |
boolean | Whether the element with that id currently passes validation. true for an element with no validation state. |
Derived signals¶
Registers a computed signal recomputed whenever any dependency changes. deps
is a sequence of Signal handles or signal-name strings; f receives the
dependency values in deps order and returns the new value:
local a = signal("a", 1)
local b = signal("b", 2)
local sum = derive("sum", { a, b }, function(a, b) return a + b end)
A derivation runs once after registration, then on every change to a dependency. Derived-of-derived chains settle within the same tick. A derivation that errors is retried on the next tick.
Timers¶
| Builtin | Behaviour |
|---|---|
set_timeout(name, ms) |
One-shot timer; fires on_timer(name) after ms milliseconds. |
set_interval(name, ms) |
Repeating timer; fires on_timer(name) every ms milliseconds. |
cancel_timer(name) |
Cancel the named timer. |
Timer names are unique: setting a timer with an existing name replaces it. Negative delays clamp to zero. Cancelling from inside the timer's own handler takes effect before the next fire.
Frame callbacks¶
| Builtin | Behaviour |
|---|---|
request_frame() |
Ask for one on_frame(dt) call on the next tick. |
One request buys one callback. A handler that wants to keep animating asks
again from inside itself, and an animation that is finished simply stops
asking, which parks the app. dt is the seconds since the previous callback,
capped so a stalled tick slows an animation instead of teleporting it, and
zero on the frame that starts a loop because no interval has passed yet.
function on_ready()
request_frame()
end
function on_frame(dt)
signals.angle = signals.angle + dt * 2.0
request_frame()
end
Use this rather than a repeating timer for anything visual: a timer fires on a wall-clock schedule that has nothing to do with when the app draws.
Query and traverse the element tree¶
Node is a cheap handle. Reads resolve against the snapshot rebuilt each tick,
so a handle to a removed element stops resolving.
| Builtin | Returns | Behaviour |
|---|---|---|
query(selector) |
NodeQuery |
Every element matching a CSS selector, document order. A malformed selector raises. |
get_by_id(id) |
Node or nil |
Element with that id. |
document() |
Node or nil |
The document root. document is also a namespace table; calling it returns the root. |
NodeQuery method |
Returns | Behaviour |
|---|---|---|
len() |
integer | Match count. |
is_empty() |
boolean | Whether there are no matches. |
first() |
Node or nil |
First match. |
nth(i) |
Node or nil |
Match at zero-based i. |
iter() |
table | Every match, 1-indexed. |
collect() |
table | Every match, 1-indexed. |
single() |
Node |
The one match; raises when the count is not exactly one. |
get_single() |
Node or nil |
The one match, or nil for any other count. |
Node method |
Returns | Behaviour |
|---|---|---|
parent() |
Node or nil |
Parent. |
first_child() / last_child() |
Node or nil |
Bounding child. |
next() / prev() |
Node or nil |
Sibling. |
children() |
table | Children in document order, 1-indexed. |
closest(selector) |
Node or nil |
Nearest ancestor-or-self matching the selector. |
exists() / valid() |
boolean | Whether the handle is in the current snapshot. |
handle() |
integer | The raw packed handle. |
Mutate the element tree¶
Every mutator returns the receiver, so calls chain; read-backs end the chain. Mutations queue a command applied later in the same tick.
| Builtin or method | Returns | Behaviour |
|---|---|---|
create(tag) / document.create(tag) |
Node |
Create a detached element. The handle is valid for the rest of the tick; attach it before the tick ends. |
Node:clone_deep() |
Node |
Deep-clone the subtree into a fresh detached element. |
Node:set_attr(name, value) |
Node |
Set an attribute. id, class, text, and disabled route to their typed component; anything else lands in the attribute map. |
Node:remove_attr(name) |
Node |
Remove an attribute. |
Node:set_id(id) |
Node |
Set the id attribute. |
Node:set_text(text) |
Node |
Replace the text content. |
Node:set_inner_markup(markup) |
Node |
Replace the children with a parsed markup fragment. Do not feed untrusted content. A no-op when the app runs from a precompiled artifact, which links no parser. |
Node:add_class(class) |
Node |
Add one class. |
Node:remove_class(class) |
Node |
Remove one class. |
Node:toggle_class(class) |
Node |
Toggle one class. |
Node:set_class(classes) |
Node |
Replace the whole class list. |
Node:set_style(name, value) / Node:style_set(name, value) |
Node |
Set one inline style property. |
Node:style_remove(name) |
Node |
Remove one inline style property. |
Node:append(child) |
Node |
Append child under the receiver. |
Node:insert_before(child, reference) |
Node |
Insert child before reference under the receiver. |
Node:set_parent(parent) / Node:move_to(parent) |
Node |
Reparent the receiver under parent. |
Node:replace_with(new) |
Node |
Replace the receiver with new, despawning the receiver's subtree. Returns new. |
Node:remove() |
Detach and despawn the receiver and its subtree. Terminal. |
Read element state¶
| Method | Returns | Behaviour |
|---|---|---|
Node:get_attr(name) |
string or nil |
One attribute value. |
Node:text() |
string or nil |
Text content. |
Node:id() |
string or nil |
The id attribute. |
Node:has_class(class) |
boolean | Whether the class list contains class. |
Node:style_get(name) |
string or nil |
One inline style override. |
Node:computed_style(name) |
string or nil |
One resolved style property after the cascade. |
Node:computed_style() / Node:computed_style_all() |
table | Every resolved style property. |
Node:inline_style() |
table | Every inline style override. |
Node:attrs() |
table | Every attribute. |
Node:classes() |
table | The class list, 1-indexed. |
Introspection¶
| Builtin or method | Returns | Behaviour |
|---|---|---|
Node:rect() |
table or nil |
Post-layout border box: x, y, width, height, client_x, client_y. Local x / y are relative to the parent; client_* are window coordinates. |
Node:content_rect() |
table or nil |
Same keys, for the content box (padding and border removed). |
Node:scroll() |
table or nil |
x, y, max_x, max_y. |
Node:is_visible() |
boolean | Effective visibility. |
Node:z_index() |
integer | Resolved stacking order. |
Node:matched_rules() |
table | Every CSS rule that matched, each a table of selector, specificity (a three-element sequence), source, source_order, and declarations. |
Node:entity_id() |
table or nil |
index and generation of the backing entity. |
Node:components() |
table | Names of the introspectable components on the element. |
Node:component(name) |
table or nil |
Field map of one component. Raises for a name outside the introspectable set. |
Node:outer_markup() |
string | The subtree serialized to markup text. |
Node:inner_markup() |
string | The children serialized to markup text. |
dump_tree() |
string | Whole-tree structural dump. |
pointer_state() |
table | x, y, inside, buttons, and a nested modifiers table of shift, ctrl, alt, super. |
frame_info() |
table | frame, dt_ms, dirty_count. |
The introspectable components are LayoutBox, Visuals, Opacity, ZIndex,
Visible, TextContent, LumenClasses, LumenAttributes, InlineStyle, and
Style.
Element events¶
Bind a function to one element and one event type. Binding returns a function that unbinds when called.
| Method | Returns | Behaviour |
|---|---|---|
Node:on(event_type, handler) |
function | Bind for the bubble phase. |
Node:on(event_type, handler, capture) |
function | Bind for the capture phase when capture is true. |
Node:on_capture(event_type, handler) |
function | Bind for the capture phase. |
local off = get_by_id("save"):on("click", function(e)
e:prevent_default()
print(e:event_type())
end)
-- later
off()
The handler receives an Event:
Event method |
Returns | Value |
|---|---|---|
target() |
Node |
The element the event originated on. |
current_target() |
Node |
The element whose handler is running. |
event_type() |
string | Event type name. |
key() |
string | Key name for keyboard events. |
value() |
string | Text value for input / change / submit. |
button() |
integer | 0 primary, 1 middle, 2 secondary. |
x() / y() |
number | Pointer position relative to the target. |
client_x() / client_y() |
number | Pointer position in window coordinates. |
delta_x() / delta_y() |
number | Wheel delta. |
position() |
table | x, y, client_x, client_y. |
modifiers() |
table | shift, ctrl, alt, super. |
prevent_default() |
Cancel the default action. | |
stop_propagation() |
Stop the event reaching further elements. | |
stop_immediate_propagation() |
Stop the event entirely, including other handlers on this element. |
Event types¶
click, dblclick, pointerdown, pointerup, pointermove, pointerenter,
pointerleave, wheel, keydown, keyup, input, change, focus, blur,
submit, scroll.
Dispatch runs capture (root down to the target), then the target, then bubble
(target up to the root). focus, blur, pointerenter, pointerleave, and
scroll do not bubble.
input fires per keystroke: every edit that changes the text raises one,
carrying the buffer as it stands after that edit. A caret move raises nothing.
change and submit come from the commit signal instead, so they fire when the
field is committed with Enter. Only click has a default action (link
navigation); prevent_default on a click skips it.
window, document, history¶
Pre-bound global tables.
| Call | Returns | Behaviour |
|---|---|---|
window.set_href(path) |
Navigate to a page path. | |
window.href() |
string | The current page path. |
window.reload() |
Re-navigate to the current page. | |
window.title() |
string | Window title. |
window.set_title(title) |
Set the window title. | |
window.dpr() |
number | Device pixel ratio. |
window.size() |
table | { width, height } in logical pixels. |
window.set_size(width, height) |
Resize the window, in logical pixels. | |
window.location.path() |
string | The current page path. |
window.location.query() |
string | The query string of the request the document is being rendered for, without the leading ?. |
window.location.hash() |
string | The fragment of the request the document is being rendered for, without the leading #. |
history.back() |
Step one entry back. | |
history.forward() |
Step one entry forward. | |
history.go(delta) |
Step delta entries; negative goes back. |
|
document.root() |
Node or nil |
The document root. |
document.query(selector) |
NodeQuery |
Matching elements, document order. |
document.get_by_id(id) |
Node or nil |
Element with that id. |
document.focused() |
Node or nil |
The focused element. |
document.hovered() |
Node or nil |
The hovered element. |
document.create(tag) |
Node |
Create a detached element. |
Page navigation¶
| Builtin | Returns | Behaviour |
|---|---|---|
page(path) |
Navigate to a page path ("settings", "/user/7", "/"). |
|
page() / page_current() |
string | The active page key. |
page_back() |
boolean | Step one entry back in the page history. |
page_forward() |
boolean | Step one entry forward. |
See Pages for the file layout these paths resolve against.
Dialogs¶
Each opens a native dialog and delivers the result to the matching handler,
keyed by tag. A cancelled dialog still fires once, with an empty path.
| Builtin | Behaviour |
|---|---|
pick_file(tag) |
Open-file dialog; fires on_file_picked(tag, path). |
pick_files(tag) |
Multi-select dialog; fires on_files_picked(tag, paths) with paths joined by \|. |
pick_folder(tag) |
Folder picker; fires on_folder_picked(tag, path). |
save_file(tag, default_name) |
Save dialog seeded with default_name; fires on_file_picked(tag, path). |
pick_file_filtered(tag, spec) |
Open-file dialog with extension filters. spec is pipe-separated Label:ext1,ext2 groups; a * extension means no filter. |
OS integration¶
| Builtin | Behaviour |
|---|---|
notify(title, body) |
Show an OS notification. |
notify_ex(id, title, body, options, actions) |
Show an OS notification. options is pipe-separated key:value entries, where icon takes a themed name or path and urgency takes "low", "normal", or "critical". actions is pipe-separated id:Label buttons; a press fires on_notification_action(id, action_id). An empty string in either position means the defaults. |
clipboard_write(text) |
Put text on the system clipboard. |
clipboard_read(tag) |
Request the clipboard text; fires on_clipboard(tag, text) on the next tick. |
copy_image(path) |
Copy the image at path to the system clipboard. Relative paths resolve against the app directory. |
save_clipboard_image(path) |
Write the clipboard image to path as PNG. Relative paths resolve against the app directory. |
tray_icon(id, icon_path, tooltip) |
Register or replace a tray icon; clicks fire on_tray(id). An empty tooltip disables it. A relative icon_path resolves against the app directory. |
tray_icon_menu(id, icon_path, tooltip, menu, template) |
Register a tray icon with a context menu, given as pipe-separated id:Label entries where - is a separator; a pick fires on_menu(id). template is the macOS monochrome-icon flag, ignored elsewhere. A relative icon_path resolves against the app directory. |
unregister_tray(id) |
Remove a tray icon. |
register_hotkey(name, accelerator) |
Register a global hotkey ("CommandOrControl+S", "Alt+Space", "F11"); fires on_hotkey(name). |
unregister_hotkey(name) |
Remove a global hotkey. |
open_url(url) |
Open url with the default browser, or the mail client for mailto:. |
open_path(path) |
Open path with the platform's default application. Relative paths resolve against the app directory. |
reveal_path(path) |
Show path in the platform's file manager. |
keep_awake(name, reason) |
Hold off the screensaver and system sleep under name. Repeating a live name replaces its request. |
allow_sleep(name) |
Release the inhibit registered under name. |
add_recent_file(path, label) |
Record path as recently opened. An empty label derives one from the path's file name. |
list_recent_files(tag) |
Request the recent-files list; fires on_recent_files(tag, paths) with paths joined by \|, most recent first. |
clear_recent_files() |
Remove every entry from the recent-files list. |
set_autostart(on) |
Enable or disable launching this app at login. |
query_autostart(tag) |
Request the autostart state; fires on_autostart_enabled(tag) or on_autostart_disabled(tag). |
open_menu(id) |
Open menu id by setting the __menu_open:id signal to true. |
close_menu(id) |
Close menu id. |
See OS integration for the markup these pair with.
Styling and theming¶
| Builtin | Behaviour |
|---|---|
set_class(id, classes) |
Replace the class list on the element with that id. |
set_root_class(classes) |
Replace the class list on the root element, which drives theme-token selectors. |
set_color_scheme(name) |
Switch the color scheme: "default" (follow the OS), "force-light", "force-dark", "prefer-light", "prefer-dark". An unknown name is ignored with a warning. |
set_class takes an element id and Node:set_class takes none because it
already has the element. They share a name and do the same thing through
different routes: reach for the global when all you have is an id, and the
method when you are holding a handle.
Audio¶
These functions come from the lumen-audio runtime module and exist only
when the app declares it under [dependencies] in lumen.toml; see
OS integration.
| Builtin | Behaviour |
|---|---|
audio_play(path) |
Load and play the track at path (app-relative wav or ogg, resolved through the app's asset sources, so a packed archive and lumen://app/... URIs work); resets position to zero. |
audio_pause() |
Pause, holding position. |
audio_resume() |
Resume a paused transport. |
audio_stop() |
Stop and rewind. |
audio_seek(secs) |
Seek to secs, clamped to the track duration. |
audio_volume(level) |
Set output volume in 0.0 to 1.0. |
The module writes the audio_position, audio_duration, and audio_playing
signals each tick, so markup binds to them directly.
Networking¶
| Builtin | Behaviour |
|---|---|
fetch(url, tag) |
HTTP GET. A 2xx reply fires on_fetch(tag, body); a transport failure or non-2xx fires on_fetch_error(tag, message). |
http(request) |
General HTTP request. Fires on_http(tag, response) for every completed request. |
request is a table: url and tag are required; method defaults to
"GET"; headers is a table of header name to value; body is a string;
timeout_ms is a positive number.
response is a table of ok (true for a 2xx status), status (0 on a
transport failure), headers (names lowercased), body, and error (empty on
success).
Requests run off the UI thread; the reply is delivered on the tick thread, so a handler may touch signals and the element tree freely.
Request and response¶
The readers give back what arrived with the request the document is being rendered for, and an empty string when there is none to read; a desktop app has none. The three writers queue an answer that only a server render applies.
| Builtin | Returns | Behaviour |
|---|---|---|
request_header(name) |
string | The named request header, matched without regard to case. |
request_cookie(name) |
string | The named request cookie. |
request_body() |
string | The request body. |
response_status(status) |
Answer with HTTP status status, clamped to 100..=599. |
|
response_header(name, value) |
Set a response header; setting the same name twice replaces the value. | |
redirect(location) |
Answer with a redirect to location, a path or an absolute URL, instead of a document. |
Data helpers¶
| Builtin | Returns | Behaviour |
|---|---|---|
parse_json(json) |
any | Parse JSON into a table or scalar. nil on a parse error. |
parse_markdown(src) |
table | Parse markdown into block tables of id, kind, level, text, lang. kind is one of h, p, code, li, hr; level carries the heading level. |
local_id(source, suffix) |
string | The sibling id suffix inside the same template instance as source. local_id("user-card:btn", "label") is "user-card:label"; a source without a : returns suffix. |
Translation¶
| Builtin | Returns | Behaviour |
|---|---|---|
t(key) |
string | The active locale's string for key, or key itself when untranslated. |
tr(key) |
string | Alias for t. |
set_locale(tag) |
nil | Switch the app to the BCP-47 locale tag. Marked text, placeholders, tooltip bodies, format output and the base writing direction follow on the same tick. A locale with no catalogue falls back to the authored text; a tag that is not BCP-47 is logged and ignored. |
locale() |
string | The BCP-47 tag of the locale the app is running in, including the one it started in when no script has switched it. |
See Translation and formatting for the catalogue format.
Formatting¶
| Builtin | Returns | Behaviour |
|---|---|---|
format_number(n) |
string |
n written the way the active locale writes numbers. |
format_currency(amount, currency) |
string |
amount as money in the ISO-4217 code currency, positioned and rounded the way the locale and the currency ask for. |
format_date(iso) |
string |
The date in iso (YYYY-MM-DD, time optional) written for the locale. |
format_time(iso) |
string |
The time in iso (YYYY-MM-DDTHH:MM[:SS]) written for the locale. |
format_datetime(iso) |
string |
Date and time from iso written for the locale. |
format_relative(seconds) |
string |
seconds from now as the locale says it: -7200 is 2 hours ago. |
A trailing zone on a timestamp (Z, +02:00) is read and dropped; nothing is
converted between time zones. A value the call cannot read comes back as it was
given, the way t returns an untranslated key.
See Translation and formatting for the markup attribute that formats an element's text.
Canvas¶
These functions come from the lumen-canvas runtime module and exist only
when the app declares it under [dependencies] in lumen.toml, along with
the tag it brings:
Every call names a canvas by the id on its <canvas>
element. Colours are components in 0..1, angles are radians, and coordinates
are canvas units, which are scaled onto the element's box whenever layout
gives it one the declaration did not ask for.
| Builtin | Returns | Behaviour |
|---|---|---|
canvas.width(id) |
int |
The drawing width, in canvas units. |
canvas.height(id) |
int |
The drawing height, in canvas units. |
canvas.resize(id, width, height) |
Set the drawing space. This empties the canvas, as writing width on an HTML canvas does. |
|
canvas.clear(id) |
Erase everything the canvas holds. | |
canvas.begin_path(id) |
Start a new path. | |
canvas.move_to(id, x, y) |
Start a subpath at a point. | |
canvas.line_to(id, x, y) |
Straight segment to a point. | |
canvas.quad_to(id, cx, cy, x, y) |
Quadratic segment through one control point. | |
canvas.bezier_to(id, c1x, c1y, c2x, c2y, x, y) |
Cubic segment through two control points. | |
canvas.arc(id, x, y, radius, start, end) |
Circular arc, angles in radians, joined to the current subpath. | |
canvas.rect(id, x, y, width, height) |
Add a closed rectangle to the path. | |
canvas.close_path(id) |
Close the current subpath back to its start. | |
canvas.fill(id) |
Fill the current path. | |
canvas.stroke(id) |
Stroke the current path. | |
canvas.fill_rect(id, x, y, width, height) |
Fill one rectangle, leaving the path alone. | |
canvas.stroke_rect(id, x, y, width, height) |
Stroke one rectangle, leaving the path alone. | |
canvas.set_fill_rgba(id, r, g, b, a) |
Set the fill colour from four components. | |
canvas.set_fill_style(id, color) |
bool |
Set the fill colour from CSS text (#rgb, #rrggbbaa, rgb(), rgba(), the CSS level 1 names, transparent). false when the text is none of those, and the colour is left alone. |
canvas.set_stroke_rgba(id, r, g, b, a) |
Set the stroke colour from four components. | |
canvas.set_stroke_style(id, color) |
bool |
Set the stroke colour from CSS text, on the same terms. |
canvas.set_line_width(id, width) |
Stroke width, in canvas units. | |
canvas.set_line_cap(id, cap) |
bool |
butt, round, or square. |
canvas.set_line_join(id, join) |
bool |
miter, round, or bevel. |
canvas.set_global_alpha(id, alpha) |
Multiply every later draw by this alpha. | |
canvas.save(id) |
Push the drawing state: colours, line style, alpha, transform. | |
canvas.restore(id) |
Pop it. Popping past the bottom leaves the state alone. | |
canvas.translate(id, x, y) |
Move the origin. | |
canvas.rotate(id, radians) |
Rotate the transform. | |
canvas.scale(id, x, y) |
Scale the transform. | |
canvas.reset_transform(id) |
Drop back to the identity. | |
canvas.set_transform(id, a, b, c, d, e, f) |
Replace the transform outright. | |
canvas.set_font(id, font) |
bool |
"[weight] <size>px [family]", as in "bold 16px Inter". The size is required; false when it is missing. |
canvas.fill_text(id, text, x, y) |
Draw text in the fill colour, with (x, y) on the alphabetic baseline, shaped with the app's own fonts. |
A path is stored in canvas units and placed by the transform at the moment it is filled or stroked, so changing the transform between building a path and filling it moves the result. The HTML canvas bakes the transform into each segment as it is added; this is the one place the two differ.
Pixel buffers¶
A canvas is write-only: what it holds is a list of drawing calls bound for the
GPU, not an image, so there is nothing to read a pixel back from. Buffers are
the read-write half - plain CPU pixels a script creates, edits, loads from a
PNG, saves to one, and draws onto a canvas. A pixel is one packed
0xRRGGBBAA integer with straight (not premultiplied) alpha, so what a script
writes is what it reads back.
| Builtin | Returns | Behaviour |
|---|---|---|
canvas.buffer_new(width, height) |
int |
A new transparent buffer; 0 when it was refused, with the reason on stderr. |
canvas.buffer_free(buffer) |
bool |
Release a buffer. false when the handle names none. |
canvas.buffer_width(buffer) |
int |
Its width, or 0. |
canvas.buffer_height(buffer) |
int |
Its height, or 0. |
canvas.buffer_get_pixel(buffer, x, y) |
int |
One pixel as 0xRRGGBBAA. A pixel outside the buffer reads 0. |
canvas.buffer_set_pixel(buffer, x, y, rgba) |
Write one pixel. Outside the buffer, nothing happens. | |
canvas.buffer_get_region(buffer, x, y, width, height) |
int[] |
A rectangle of pixels, row-major. Pixels off the edge read 0, so the result is always the size that was asked for. |
canvas.buffer_put_region(buffer, x, y, width, height, pixels) |
Write a rectangle back. A short array is a partial write. | |
canvas.buffer_fill_rect(buffer, x, y, width, height, rgba) |
Fill a rectangle with one colour. | |
canvas.buffer_load_png(path) |
int |
Decode a PNG into a new buffer; 0 when it could not be read. The path resolves against the app directory. |
canvas.buffer_save_png(buffer, path) |
bool |
Write a buffer out as a PNG. |
canvas.draw_buffer(id, buffer, x, y) |
Draw a buffer onto a canvas at its own size. | |
canvas.draw_buffer_scaled(id, buffer, x, y, width, height) |
Draw a buffer stretched into a box. |
Three settings bound what one call may ask for, from the module's config
table: region_cap (pixels in one region call), buffer_pixel_cap (pixels in
one buffer), and buffer_count_cap (buffers at once). A call over a cap
answers empty or 0 and says why on stderr.
Refusals never raise. An unknown canvas id keeps the drawing, in case the
element has not been mounted yet, and reports once if no element ever matches;
an unknown buffer handle reads as an empty buffer. See
<canvas> for the element, its sizing, and what is not
implemented yet.
Filesystem¶
These functions come from the lumen-fs runtime module and exist only when
the app declares it under [dependencies] in lumen.toml:
| Builtin | Returns | Behaviour |
|---|---|---|
files.exists(path) |
boolean | Whether anything exists at path. Symlinks are followed. |
files.is_dir(path) |
boolean | Whether path is a directory that exists. |
files.list(path) |
table | The entry names directly inside path, sorted. Names, not paths, and one level deep. A directory that cannot be read gives an empty list. |
files.mkdir(path) |
boolean | Create path and every directory above it. A directory already there is success. |
files.remove(path) |
boolean | Remove one file, or one directory that is already empty. A directory holding anything is refused; a path that is not there answers false. |
files.copy(src, dest) |
boolean | Copy one file, creating the directories dest sits under. A directory source is refused. |
files.read(path) |
string | The utf-8 contents of path, or an empty string when it is not there. |
files.write(path, contents) |
boolean | Write contents to path. The write is atomic (temp file + rename), so a reader never sees a truncated file. |
files.read_bytes(path) |
table | The bytes of path as numbers of 0 to 255. A missing file, or one past the cap, gives an empty list. |
files.write_bytes(path, bytes) |
boolean | Write a list of 0-to-255 numbers as raw bytes, atomically. A value outside that range refuses the whole write. |
files.data_dir() |
string | The directory this app saves data in, created when missing. |
A relative path names a file the app ships, so it reads the same wherever the
app was started from; an absolute path is left alone. Saved state goes under
files.data_dir() instead, because the app directory is read-only once the
app is installed:
data_dir() follows the platform convention for user data ($XDG_DATA_HOME,
else ~/.local/share, on Linux; ~/Library/Application Support on macOS;
%APPDATA% on Windows) and names one directory per app from
[app] id, so two apps on a machine keep their saves apart.
A call that cannot do what it was asked answers false or an empty value and
prints one lumen-fs: line on stderr, so a script branches on the value it
got back. Two cases stay silent, because probing for state that has not been
saved yet is ordinary: reading a file that is not there, and removing one.
files.read_bytes reads up to 8 MiB by default. Raise or lower it with the
module's read_bytes_cap setting, in bytes, between 1 KiB and 256 MiB:
On Windows nothing loads a module beside the executable, so this surface
exists only in a
lumenc package --static
build, which compiles the declared modules in.
Archives¶
These functions come from the lumen-archive runtime module and exist only
when the app declares it under [dependencies] in lumen.toml:
| Builtin | Returns | Behaviour |
|---|---|---|
archive.extract(src, dest, tag) |
boolean | Unpack the archive at src into the directory dest, creating it. true when the job was taken; false when it was not, which also fires archive_error. |
Both paths resolve against the app directory, and the extraction runs off the
tick loop, so the call answers before any bytes are read. The outcome arrives
as an event keyed by tag:
| Event | Handler | Arguments |
|---|---|---|
archive_done |
on_archive_done |
tag, dest (the resolved destination), count (files written) |
archive_error |
on_archive_error |
tag, message |
on("archive_done", tag, fn) registers a handler for one job and wins over
the fallback, the same as any other event.
function on_start()
archive.extract("themes.zip", "themes", "themes")
end
function on_archive_done(tag, dest, count)
signal("status", ""):set(count .. " files")
end
function on_archive_error(tag, message)
signal("status", ""):set(message)
end
zip, tar, and gzip-compressed tar are read. The container is taken from the file's leading bytes, so an archive saved under a name that disagrees with its contents still unpacks; the extension decides only when the bytes say nothing.
What an archive may write is settled before anything is written. An entry
naming an absolute path, climbing out with .., carrying a Windows drive or
UNC prefix, or resolving outside dest ends the whole extraction with an
archive_error naming it, rather than being passed over. Entries written
before the refused one stay on disk, so a destination that took a failed
extraction is one to discard rather than keep using. Symbolic and hard links
are skipped, because a link inside the destination can point outside it once
extraction is over; count is the files written, so it leaves them out.
Existing files are overwritten and missing parent directories are created.
Four extractions run at once by default; a fifth is refused until one
finishes, as is a second job under a tag already running. Change the limit
with the module's max_concurrent setting:
Selecting part of an archive, stripping leading path components, per-entry progress, listing an archive without unpacking it, and writing an archive are not part of this surface.
On Windows nothing loads a module beside the executable, so this surface
exists only in a
lumenc package --static
build, which compiles the declared modules in.
Downloads¶
This function comes from the lumen-download runtime module and exists only
when the app declares it under [dependencies] in lumen.toml:
| Builtin | Returns | Behaviour |
|---|---|---|
download.to_file(url, path, tag, checksum) |
boolean | Start downloading url to path, reporting under tag. True once the transfer is running, false when the call was refused. |
Every argument is required. checksum is sha256: followed by 64 hex digits,
sha1: followed by 40, a bare hex string of either length, or an empty string
to check nothing; neither the prefix nor the digits are case sensitive. Any
other spelling fails the call. sha1 is accepted because it is what most package
and artifact registries publish, and the digest here checks a file against a
corrupt transfer rather than standing in for a signature.
The call answers as soon as the transfer starts, and the transfer reports through three handlers keyed by the tag it was given:
| Handler | Arguments |
|---|---|
on_download_progress(tag, received, total) |
Download tag, bytes so far, the size the server declared or -1. |
on_download_done(tag, path) |
Download tag, the path the file was written to. |
on_download_error(tag, message) |
Download tag, error text. |
A per-tag on("download_done", tag, handler) registration wins over the
fallback, the same routing every event gets.
Every refusal arrives as download_error under the tag the call named, and
the call itself answers false.
function on_start()
download.to_file("https://example.com/pack.zip", "cache/pack.zip", "pack", "")
end
function on_download_progress(tag, received, total)
if total > 0 then
signal("pack_progress", "0"):set(received * 100 // total)
end
end
function on_download_done(tag, path) signal("pack_path", ""):set(path) end
function on_download_error(tag, message) print("pack failed: " .. message) end
A relative path names a file beside the app and an absolute one is left
alone, the way every app path resolves; directories that do not exist yet are
created on the way. The bytes land in a sibling temp file and are renamed into
place only once the body has finished and the checksum has verified, so the
destination never holds a half file and a failed transfer never replaces what
was already there.
Progress is reported at most ten times a second per tag, so a handler that writes a signal cannot become the transfer's bottleneck, and once more with the final count just before the done handler runs.
A reply that is not 2xx fails the download, which is where this parts company
with fetch: fetch hands a script whatever came back so it can branch on the
status, and a download that did not get the file has nothing to write.
Redirects are followed, up to ten in a chain, and an https URL is verified
against the bundled web-PKI roots.
Three settings, all optional:
| Key | Default | Meaning |
|---|---|---|
timeout_ms |
none | How long a stalled server has to start answering: name resolution, connecting, and the response headers. The body is not on a deadline, so a large download is never cut off part way. |
max_bytes |
none | The largest body accepted. Anything past it fails and writes nothing. |
max_concurrent |
4 |
How many transfers run at once, from 1 to 64. A call past the limit fails. |
[dependencies]
lumen-download = { bundled = true, config = { timeout_ms = 15000, max_concurrent = 2 } }
One tag means one download: a call naming a tag that is already downloading is
refused rather than replacing the transfer under way, because both would report
under the same key. A call with no tag has nowhere to report, so it answers
false and prints one lumen-download: line on stderr.
On Windows nothing loads a module beside the executable, so this surface
exists only in a
lumenc package --static
build, which compiles the declared modules in.
Processes¶
These functions come from the lumen-process runtime module and exist only
when the app declares it under [dependencies] in lumen.toml:
| Builtin | Returns | Behaviour |
|---|---|---|
process.start(cmd, args, tag) |
boolean | Start cmd with the argument list args, reporting under tag. true once the program is running. |
args is a list of strings. A cmd with a path separator in it names a
program the app ships and resolves against the app directory; a bare cmd is
looked up on PATH. The child runs in the app directory, reads end of file
from its input, and has both its output streams captured.
The call answers as soon as the program is running. Everything after that arrives as an event carrying the tag, so one handler serves several children:
| Event | Fallback handler |
|---|---|
process_stdout |
on_process_stdout(tag, line) |
process_stderr |
on_process_stderr(tag, line) |
process_exit |
on_process_exit(tag, code) |
A per-child on("process_exit", tag, handler) registration wins over the
fallback, like every other event.
process_exit is always the last event for a tag. Its code is the program's
own exit code, 128 plus the signal that killed it on Unix, or -1 for an
ending the system does not describe.
A line is what the program wrote before a newline; the newline is not part of it, and the last stretch before the program ends is a line even without one. Bytes that are not utf-8 are replaced, and a line longer than 64 KiB arrives in pieces. There is one handler call per line, so a chatty program calls the handler a lot.
A program that cannot start answers false and prints one lumen-process:
line on stderr. It fires no event at all, because the tag never named a running
program, so branch on the value the call gave back rather than waiting for an
exit that never comes.
There is no way to write to a child's input, no way to end a child from a script, and no per-child environment or working directory. A child is not ended when the app exits: a program still running outlives the app that started it.
On Windows nothing loads a module beside the executable, so this surface
exists only in a
lumenc package --static
build, which compiles the declared modules in.
Embedder commands¶
Two builtins emit a command with no built-in effect under lumenc run. They
exist for embedders that read the script command stream directly.
| Builtin | Behaviour |
|---|---|
add_clicks(n) |
Emit an add-clicks command carrying n. |
set_string(key, value) |
Emit a set-string command carrying key and value. |
Two more write directly to the element tree by id, without a node handle:
| Builtin | Behaviour |
|---|---|
set_text(target_id, text) |
Replace the text content of the element with that id. |
set_src(target_id, path) |
Swap the asset path of an <image> at run time. Paths are app-relative. |
Native functions¶
Functions an embedder or a plugin registers appear as bare globals here, and one
registered under a namespace of its own appears as a global table:
gpio.level(21). A function registered with declared parameter types raises on a
call whose arguments do not match; an untyped one takes whatever it is passed.
See FFI and SDKs.
Reserved global¶
__lumen_event_handlers is the table the host keeps bound event handlers in,
keyed by token. Do not write to it.