Skip to content

API reference

Both engines expose the same API. In JavaScript, the network calls (http.*, rss.fetch, rss.fresh) and sleep return promises, so await them. Everything else is synchronous in both languages. Lua calls simply block, since scripts never run on the UI thread.

Tables in Lua and objects in JS are interchangeable in the descriptions below: { status, ok } means a table with those keys in Lua and an object with those properties in JS.

Call Returns
log(text, ctx?) nothing; appends a line
notify(title, body, ctx?) true if a notification was posted
status(text) nothing; pins or clears a header line
http.get(url, headers?) { status, ok, body, headers }
http.request(opts) same as http.get
http.cached(url, seconds?) same, plus cached
rss.fetch(url) / rss.parse(xml) { title, link, items }
rss.fresh(url) same, only unseen items
store.get(key) / store.set(key, value) string or nil
json.encode(v) / json.decode(s) Lua only
clipboard.get() / clipboard.set(text) string or nil
device.battery() { level, charging }
device.network() "wifi", "cell", "eth", "net" or "offline"
device.unlocks() today’s unlock count
apps.list() [{ label, pkg, profile }]
notifications.recent(n?) [{ pkg, app, title, text, time }]
launcher.run(command) true if sent
sleep(ms) nothing
args, argline arguments of a command run
event new what triggered an event run

Appends a line to the log under the run’s ── name · time ── header, with the script’s name at the end, muted (… · weather). ctx is an optional table / object of strings that decides what tapping and long-pressing do:

Key Effect
url tap opens the link; long-press offers open, share and copy
copy text that “Copy text” copies instead of the visible line
phone tap dials the number; long-press offers call, message and copy
image new an image URL (http or https): the picture shows under the line, as wide as the log; long-press offers “Open image”

Other keys are kept with the line. Keys the launcher uses internally (commands, script identity, app references and the like) are dropped, so a script can’t plant a line that runs a command when tapped.

log("Kolahal on GitHub", { url = "https://github.com/mtwabpin/kolahal" })
log("Your code is 4821", { copy = "4821" })
log("xkcd #327", { image = "https://imgs.xkcd.com/comics/exploits_of_a_mom.png" })
print("print() also logs", 42) -- arguments joined with tabs

Posts an Android notification, if notifications are enabled for this script (script notify <name> on or the switch in the script manager) and allowed for Kolahal. ctx.url is opened when the notification is tapped. Returns true if a notification was posted.

notify("Build finished", "main is green", { url = "https://ci.example.org" })

Pins one line for this script into the header (first line of text, at most 80 characters). status(nil) / status(null) or an empty string clears it. It only works in saved scripts, since it is keyed by the script’s name. Tapping the status line runs the script.

status("HN: " .. count .. " new")
status(nil)

Whatever the script returns is printed as an output line (tables and objects as JSON; long-press copies it). A one-liner run with lua / js that produces nothing prints nil.

Responses are { status, ok, body, headers }: the numeric status, ok for 2xx, the body as a string (read up to 4 MB) and response headers with lowercase names. A User-Agent of Kolahal-Launcher/0.1 is sent unless you set your own. Network errors raise an error (Lua) or reject the promise (JS). An HTTP error status does not, so check ok. Plain http:// URLs are allowed. new Requests to this phone or your local network (a router, a home server, a VPN address like Tailscale’s 100.x) need @uses lan; without it they fail, even after a redirect. A JavaScript run makes at most 4 requests at a time; more wait their turn.

local r = http.get("https://api.github.com/repos/neovim/neovim", { Accept = "application/json" })
if not r.ok then error("HTTP " .. r.status) end
local repo = json.decode(r.body)
return repo.stargazers_count

Any method. opts = { url, method, body, contentType, headers }; only url is required and method defaults to GET. A string body is sent as is; a table / object body is encoded as JSON. contentType defaults to text/plain; charset=utf-8, so set it for JSON APIs.

local r = http.request{
url = "https://ntfy.sh/my-topic",
method = "POST",
body = "backup done",
}
local r2 = http.request{
url = "https://httpbin.org/post",
method = "POST",
body = { hello = "world" },
contentType = "application/json",
headers = { Authorization = "Bearer " .. store.get("token") },
}

A GET that reuses this script’s stored response while it is younger than seconds (default 600). The result has an extra cached field (true when it came from the cache). Only successful responses are cached.

local r = http.cached("https://open.er-api.com/v6/latest/USD", 6 * 3600)

Feeds come back as { title, link, items }, where each item is { title, link, id, date, summary }. rss.fetch and rss.fresh raise an error on an HTTP failure, so wrap them in pcall / try if you want a friendly message.

Downloads and parses a feed.

Parses feed XML you already have. Synchronous in both languages.

Like rss.fetch, but returns only items this script hasn’t seen before (by item id; the last 500 ids per URL are remembered). Items are only marked as seen if the run succeeds, so a script that fails halfway sees the same items next time.

local feed = rss.fresh("https://lobste.rs/rss")
for i, item in ipairs(feed.items) do
if i > 5 then break end
log(item.title, { url = item.link })
end

Persistent strings, private to each script. store.get returns nil / null for a missing key; store.set(key, nil) deletes it. Store anything structured as JSON. Deleting a script deletes its store; one-liners typed at the prompt share one scratch store. A value holds up to 1 MB, and a script’s store up to 1,000 keys and 16 MB; past that store.set raises “store full”.

local n = tonumber(store.get("runs") or "0") + 1
store.set("runs", tostring(n))
store.set("list", json.encode({ "a", "b" }))

Lua only; JavaScript has the built-in JSON. json.decode turns objects into tables and arrays into 1-based sequences.

local data = json.decode('{"ip":"203.0.113.7"}')
return json.encode({ ip = data.ip, ok = true })

Needs @uses clipboard (what a script may use). Android only lets the focused app read the clipboard, so clipboard.get() usually returns nil in timer runs. clipboard.set works any time.

-- @uses clipboard
clipboard.set("copied by a script")

{ level, charging }: level is 0–100 (or nil if unknown), charging is true when plugged in.

The active network: "wifi", "cell", "eth", "net" (something else) or "offline".

Today’s unlock count, as in the header.

local b = device.battery()
return string.format("%d%% %s · %s · %d unlocks", b.level or -1,
b.charging and "charging" or "", device.network(), device.unlocks())

Launchable apps in every profile, sorted by label: [{ label, pkg, profile }], where profile is "personal" or "other" (work, private, clone). Needs @uses apps.

// @uses apps
const work = apps.list().filter(a => a.profile !== "personal");
return `${work.length} apps outside the personal profile`;

The most recent notifications, newest first: [{ pkg, app, title, text, time }] (time in milliseconds since the epoch). n defaults to 20, at most 50. Needs notification access; returns an empty list without it. Needs @uses notifications.

-- @uses notifications
for _, n in ipairs(notifications.recent(5)) do
log(n.app .. ": " .. (n.title or "") .. " " .. (n.text or ""))
end

Runs a terminal command, as if typed. Only when the script was started from the prompt (a command script, script run, or a one-liner); in timer and event runs it logs an error and returns false.

-- @command
-- @desc work mode: pause notifications, open the work mail
launcher.run("work on")
launcher.run("open gmail -w")

Pauses for up to 5000 ms per call. In JS: await sleep(1000).

When a script runs as a command, args holds the words after its name (a table in Lua, an array in JS; quotes group words) and argline the same words joined by spaces. Both are empty in timer runs.

// @command
// @desc greet: greet <name>
return `hello, ${argline || "world"} (${args.length} args)`;

new In a run started by an @on trigger, event describes what happened. It always carries the event’s type (one of unlock, charger, unplug, wifi, offline, notification, boot, headset); events such as notification add details about the notification. Outside event runs, don’t rely on it being set. The editor’s inline docs list the fields for each event.