Reference · Lua 5.4 · Chromium · Android

The LemurX Lua Reference.

Everything a script can touch, in one page. Three layers, one language: the lemurx.* API in the browser process, a luakit-compatible layer (widget, webview, luakit.register_scheme, sqlite3, …) so luakit's rc.lua and its whole lib/ run unmodified on a phone, and a Lua state inside every renderer with the real DOM and a synchronous hook on every request.

files/lua/*.luaLua 5.4.7browser process · renderer process local privileged · UGC sandboxedrc.lua compatible

Pages

Overview

LemurX is Chromium for Android with a Lua 5.4 interpreter living inside the browser process, on its own sequenced thread. Scripts are plain .lua files in the app's private directory. There is no manifest, no permission prompt and no extension API standing between the script and the browser: a script talks to tabs, the network stack, the native Android UI, the DevTools Protocol and Chromium's own services through one table, lemurx.

Three layers are available, and they compose:

lemurx.*The native LemurX API. Tabs, network rules, HTTP, files, storage, the browser shell, declarative native UI, native-view surgery, input, intents, cookies, history, prefs, the Chrome DevTools Protocol. browser process
luakit layerluakit's entire C-level Lua surface, re-implemented on Chromium: widget{} trees mapped to Android views, webview bound to a tab, luakit.register_scheme, sqlite3, regex, soup, stylesheet, timer, download, ipc_channel. luakit's lib/, lousy/ and rc.lua ship as-is and run. luakit
rendererA Lua state in every renderer process (luakit's web extension model): page, dom_document, dom_element over main-world V8 handles, the synchronous send-request hook on every subresource, luakit.register_function to expose Lua to page JavaScript, ipc_channel back to the browser process. renderer process

How to read the badges used throughout this page:

browser runs in the browser process · renderer runs in a renderer process · luakit luakit-compatible API · UGC ok allowed in sandboxed shared scripts · local only refused (Lua error) in UGC scripts · signal an event you subscribe to with add_signal.

Which layer should I use? Reach for lemurx.* first — it is the smallest, most Android-shaped API. Use the luakit layer when you want luakit's object model (signals, property:: notifications), its widget tree, custom URL schemes, or when porting luakit modules. Use the renderer layer when a decision has to be made synchronously inside the page: per-request rewriting, DOM surgery before first paint, exposing native functions to page scripts.

Pages

Quick start

Where scripts live

PathTrustLoaded
/data/data/<pkg>/files/lua/*.lualocal · full powerAt startup, sorted by file name
/data/data/<pkg>/files/lua/ugc/*.luaUGC · sandboxedAfter local scripts, one lua_State each
files/luakit/ (lib/ config/ data/)luakit installSeeded from the APK; config/ is yours and never overwritten
apk assets/lua/init.luaruntimeFirst. Defines the lemurx.* sugar, then the luakit kernel loads

Name files 10_xxx.lua, 20_xxx.lua — the number is the load order. The first run copies a twenty-scene 00_tutorial.lua next to yours.

First script

files/lua/10_hello.lua
lemurx.tabs.on("loaded", function(ev)
    lemurx.toast("Loaded: " .. tostring(ev.title))
end)

lemurx.ui.render("toolbar.end", lemurx.ui.h("icon", {
    id = "hello", text = "λ", desc = "Say hi",
    onClick = function() lemurx.help() end,
}))

Pushing a script

shell
adb push 10_hello.lua /sdcard/
adb shell run-as <pkg> cp /sdcard/10_hello.lua files/lua/10_hello.lua
# restart LemurX

Debugging

  • lemurx.log(...) / print(...) go to logcat, tag LemurX. luakit's msg.* ends up in the same place.
  • lemurx.help() prints the module catalog; lemurx.help("tabs") one module.
  • Errors in callbacks are caught and logged with a traceback — one script cannot take down another, and Lua can never crash a page.
  • lemurx.ui.dump() / lemurx.ui.shell() print the native view tree with ids, for ui.style and surgery queries.

Pages

Conventions & trust domains

Threading

All scripts run on one sequenced Lua thread in the browser process. Most lemurx.* calls are synchronous: a call that needs the UI thread posts there and waits. Callbacks (events, clicks, timers, HTTP results) are delivered on the Lua thread, so there is no concurrency to reason about. The UI thread never blocks on Lua: a Java callback that needs a return value while Lua is busy is refused rather than waited for. The single exception is the luakit widget key-press verdict, which the UI thread awaits while cooperatively draining widget operations Lua enqueues.

  • Don't spin (while os.time() < t). Use lemurx.timer.after / every.
  • A synchronous lemurx.http.fetch blocks the Lua thread up to 20 s (UGC) / 120 s (local) — every script's callbacks queue behind it. Use the callback form or lemurx.async.
  • tabs.eval, cdp.send, ui.* are synchronous and fast (they wait on the UI or renderer, tens of milliseconds).

Return values

  • Queries return a table: {ok=true, ...} or {ok=false, error="why"}.
  • Actions return a boolean.
  • "Not found" is nil, not an error.
  • Type errors and privileged calls from UGC raise. Wrap with pcall when in doubt.

Local vs UGC

Scripts on your own device are unrestricted. Restrictions apply only to scripts other people wrote that you installed (UGC, files/lua/ugc/).

Local files/lua/*.luaUGC files/lua/ugc/*.lua
Lua stateOne shared main lua_StateOne lua_State per file
Standard libraryEverything: io os debug package require dofile loadfile load (bytecode allowed)base table string math utf8 coroutine; load text only; no dofile/loadfile/require
lemurx.*AllAll except the local only entries
luakit layerYes (kernel runs in the main state)No
requirefiles/lua/?.lua files/lua/?/init.lua files/lua/lib/?.lua + luakit config/ lib/—
lemurx.isPrivileged()truefalse
lemurx.scriptName()"""ugc/xxx.lua"
fs jailfiles/luafiles/lua/ugc
http.fetch limits8 MB · 120 s · any scheme2 MB · 20 s · http(s) only

UGC scripts may not use: lemurx.cookie history downloads perm cdp v8 dom ax blink prefs data page network emulation overlay css, net.addRule's requestBody/replaceRequestBody, arbitrary intent actions, or the luakit layer. Privilege is checked at runtime by state, not by scanning source — dynamic code in UGC is still UGC.

Why a separate state? With a shared state a UGC file could overwrite lemurx.tabs.eval and run under your privileges the next time one of your callbacks called it (confused deputy). Separate states remove the class of attack, so local scripts need no defensive coding at all. Why text-only load? The Lua VM does not verify bytecode; crafted bytecode is arbitrary memory access in the browser process.

Local scripts hand capabilities to UGC explicitly with lemurx.expose; UGC picks them up with lemurx.import. Values cross as JSON; functions never cross.

Pages

Events

Two event styles coexist. lemurx.* uses module.on(name, fn); the luakit layer uses the object protocol obj:add_signal(name, fn) (see Object protocol). Both deliver on the Lua thread.

lemurx.* events
-- Tab lifecycle. ev = {id, url, title, incognito, loading, canGoBack, canGoForward, muted, event, eventUrl}
lemurx.tabs.on("started" | "document" | "loaded" | "created" | "closed" | "selected", function(ev) end)
--  started  = navigation begins       document = DOM available (best moment to inject)
--  loaded   = load finished           created / closed / selected = tab strip changes

-- Shell events
lemurx.chrome.on("omnibox", function(ev) end)   -- {focus=true|false, text}
lemurx.chrome.on("back",    function(ev) end)   -- {id, url, title}
lemurx.chrome.on("menu",    function(ev) end)   -- {id, title, lua}

-- Back key. Once registered the key is yours; call lemurx.chrome.back() to let it through.
lemurx.input.onBack(function(ev) end)
lemurx.input.interceptBack(false)               -- hand it back to the system

-- DevTools events (local only)
lemurx.cdp.on("Network.responseReceived" | "*", function(ev) end)  -- {method, tab, params}

-- Timers (ms, min 10)
local id = lemurx.timer.every(60000, function() end)
lemurx.timer.after(2000, function() end)
lemurx.timer.cancel(id)
luakit-style signals
local webview = require("webview")
webview.add_signal("init", function(view)                 -- every webview, native tabs included
    view:add_signal("navigation-request", function(v, uri, reason)
        if uri:find("ads%.example") then return false end -- veto: the navigation never happens
    end)
    view:add_signal("property::title", function(v) msg.info("title now %s", v.title) end)
end)

Pages

luakit compatibility

luakit is the WebKitGTK browser whose entire UI is Lua. LemurX re-implements every C-level interface luakit exposes to Lua — the globals, the object protocol, all widget types, the webview, custom schemes, the web-extension (renderer) side — on top of Chromium and Android. luakit's own pure-Lua layer (lib/, lib/lousy/, config/rc.lua, config/theme.lua) is shipped byte-for-byte and required normally. That means require("webview"), require("window"), chrome.add(...), modes, binds, adblock, follow, formfiller, userscripts… all work.

Enabling the luakit shell

The kernel always loads (so widget{}, sqlite3{}, luakit.register_scheme are available to your scripts). Whether rc.lua runs — which hands the whole shell to luakit's window.lua: notebook, tab strip, status bar, vi-style modes — is a switch:

toggle rc.lua
lemurx.storage.set("luakit_rc", "1")        -- run config/rc.lua at startup (luakit shell)
lemurx.storage.set("luakit_autowrap", "1")  -- wrap every native tab as a luakit webview (default on)
-- edit files/luakit/config/rc.lua and theme.lua like you would on the desktop

Directories

luakitLemurX
luakit.install_path / install_paths.install_dirfiles/luakit (lib/ inside)
luakit.config_dir / xdg.config_dirfiles/luakit/config — yours; web modules resolve here first
luakit.data_dirfiles/luakit/data — sqlite files, sessions
luakit.cache_dircache/luakit
xdg.download_dir, pictures_dir, …Environment.getExternalStoragePublicDirectory(...)

Deliberate differences

  • Pixels are dp, font sizes are sp. Pango font strings ("monospace bold 12") and Pango markup in labels are translated.
  • Webview is a tab. widget{type="webview"} creates a real Chromium tab; putting it in a notebook/box moves the tab's content into the widget's rectangle. notebook:switch() selects the native tab and vice versa.
  • navigation-request is a NavigationThrottle. Returning false cancels before commit; no flash, no history entry.
  • Custom schemes serve navigations and subresources; a request nobody finishes times out after 30 s instead of hanging forever; a scheme with no handler at all returns 404 immediately.
  • regex is RE2, not PCRE: no back-references or look-behind.
  • luakit.spawn runs a real process (/system/bin/sh is available); unique.is_running() is always false (one process per package).
  • Web modules cannot touch the file system (Chromium's renderer sandbox): lfs in the renderer is a consistent empty implementation.
  • Lua is 5.4; a compatibility shim provides unpack, loadstring, setfenv/getfenv, table.getn/maxn, math.pow, string.gfind, package.loaders for luakit's 5.1-era lib/.
Licensing. luakit's lib/ and lousy/ are GPLv3 and ship as separate Lua files with their license. LemurX's kernel (kernel/*.lua, the C++ and Java hosts) is an independent implementation written from luakit's documentation and is not derived from luakit's C sources.

Pages

Lua in the renderer

Every renderer process hosts its own Lua state, the equivalent of luakit's web extension. It is loaded with require_web_module("name") from the browser side; the module source is resolved from files/luakit/config/ then files/luakit/lib/ and pushed over Mojo. Inside, a module sees luakit, page, dom_document, dom_element, ipc_channel, msg, soup and the safe standard library.

What makes this different from a content script:

  • Real DOM handles, main world. dom_element wraps a V8 node handle. The same node is always the same Lua object (usable as a table key, comparable with ==). Reads and writes are synchronous.
  • Not a script tag. Page CSP does not apply; the page cannot see or interfere with the Lua state.
  • Synchronous request hook. page's send-request signal fires for every subresource before it leaves the process — return a string to redirect, false to block, edit the headers table in place. Backed by a URLLoaderThrottle.
  • Native functions on window. luakit.register_function(uri_pattern, name, fn) makes window[name](...) return a Promise resolved from Lua.
  • Two-way IPC. ipc_channel signals in both directions, with targeting to a single page or broadcast to every renderer.
files/luakit/config/probe_wm.lua (renderer side)
local ui = ipc_channel("probe_wm")

luakit.add_signal("page-created", function(page)
    page:add_signal("send-request", function(p, uri, headers)
        if uri:find("doubleclick%.net") then return false end        -- block
        headers["DNT"] = "1"                                            -- edit headers
        if uri:sub(1, 7) == "http://" then return "https://" .. uri:sub(8) end  -- redirect
    end)
    page:add_signal("document-loaded", function(p)
        for _, a in ipairs(p.document.body:query("a[target=_blank]")) do a.attr.target = "_self" end
        ui:emit_signal("links_fixed", p.id, #p.document.body:query("a"))
    end)
end)

luakit.register_function("^https?://", "lemurPing", function(page, resolve, reject, msg)
    resolve("pong: " .. tostring(msg))    -- page JS: await lemurPing("hi")
end)
files/lua/20_probe.lua (browser side)
local wm = require_web_module("probe_wm")
wm:add_signal("links_fixed", function(_, page_id, n)
    lemurx.toast(("tab %d: %d links"):format(page_id, n))
end)
wm:emit_signal("rules", { block = {} })              -- broadcast to every renderer
wm:emit_signal(view, "rules", { block = {} })        -- or only the renderer hosting this webview

The full renderer API is under Classes · renderer.

Pages

Showcase scripts

Seven complete, single-file scripts that each do something no extension can. They live in the repository under chrome/lemur/lua/examples/; drop any of them into files/lua/.

10_shell_remix.lua

Turn LemurX into a read-only news app: persistent skin, a native site-switcher strip above the page, the real Home button replaced with "to top", the back key re-routed, custom bottom menu.

lemurx.theme · ui.render · ui.replace · input.onBack · menu
20_renderer_dom_lua.lua

Lua inside the renderer walks the main-world DOM, counts trackers, retargets _blank links, injects a bar, and exposes window.lemurHistory() which queries the browser's history DB through IPC.

page · dom_element · register_function · ipc_channel
30_request_surgeon.lua

Every subresource request passes through Lua synchronously: block trackers, strip utm_*/fbclid, upgrade http→https, set DNT, trim Referer. Rules are hot-swapped from the browser side without reloading.

send-request · ipc_channel · ui.render
40_custom_scheme.lua

lua://dash is a page generated on the fly with live tab list, screenshots and history search; lua://tabs is a JSON endpoint; lua://file/… serves local files. Page JS calls back into Lua with await luaDash(...).

register_scheme · scheme-request · request:finish · register_function
50_native_widget_shell.lua

A vim-style shell built from luakit widget{}s: horizontal tab strip, notebook holding the real tabs, status bar, command entry (:o :t :d :n :p :zi :q). One window over the native toolbar.

widget:window · notebook · box · entry · label · eventbox · key-press
60_navigation_gatekeeper.lua

Veto navigations before commit: unwrap redirector pages, jump from AMP to canonical, a night-time cool-down page for short-video sites, block cross-site popups; everything logged to sqlite and served at lua://gate.

navigation-request · new-window-decision · load_string · sqlite3
70_cdp_lab.lua

DevTools on the phone: per-site memory of 3G throttling, fake GPS, timezone, desktop UA and CPU slow-down; live request counters from Network.* events; a one-tap HTML "check-up" report with metrics and a screenshot.

cdp.attach · cdp.send · cdp.on · tabs.screenshot · fs.write

Pages

Recipes

Per-site skin

skin.lua
local skins = { ["github.com"] = "midnight", ["wikipedia.org"] = "paper" }
lemurx.tabs.on("selected", function(ev)
    for host, name in pairs(skins) do
        if ev.url and ev.url:find(host, 1, true) then return lemurx.theme.apply(name) end
    end
    lemurx.theme.apply("forest")
end)

Per-site memory: desktop mode, zoom, mute

memory.lua
local function host_of(url) return (url or ""):match("^%a+://([^/]+)") or "" end
lemurx.tabs.on("document", function(ev)
    local cfg = lemurx.storage.get("site:" .. host_of(ev.url))
    if not cfg then return end
    if cfg:find("desktop") then lemurx.tabs.setDesktop(ev.id, true) end
    if cfg:find("zoom150") then lemurx.tabs.setZoom(ev.id, 150) end
    if cfg:find("mute")    then lemurx.tabs.mute(ev.id, true) end
end)
-- remember: lemurx.storage.set("site:example.com", "desktop,zoom150")

Reader mode toggle

reader.lua
local reading = false
local function set_reading(on)
    reading = on
    lemurx.chrome.controls(on and "hidden" or "both")
    lemurx.chrome.setPullRefresh(not on)
    local t = lemurx.tabs.current()
    if t then
        lemurx.tabs.inject(t.id, on and [[
            (function(){var s=document.createElement('style');s.id='lemur-read';
            s.textContent='header,nav,aside,[class*=banner],[class*=fixed]{display:none!important}';
            document.head.appendChild(s)})()
        ]] or "document.getElementById('lemur-read')?.remove()")
    end
end
lemurx.ui.render("page.float", lemurx.ui.h("button", { id = "rd", text = "Read", onClick = function() set_reading(not reading) end }))

Two-tap back

back.lua
local armed = false
lemurx.input.onBack(function()
    if armed then armed = false; lemurx.input.interceptBack(false); lemurx.chrome.back(); return end
    armed = true
    lemurx.toast("Press back again")
    lemurx.timer.after(2000, function() armed = false end)
end)

Cross-origin data in a floating panel

fx.lua
local h = lemurx.ui.h
lemurx.timer.every(60000, function()
    lemurx.http.fetch("https://api.exchangerate.host/latest?base=USD&symbols=CNY", nil, function(r)
        if not r.ok then return end
        local rate = r.body:match('"CNY":([%d%.]+)')
        lemurx.ui.render("page.float", h("text", { id = "fx", text = "USD/CNY " .. rate,
            background = "#CC000000", color = "#FFFFFFFF", padding = {10, 6}, radius = 12, y = 120 }))
    end)
end)

Read a price out of page JavaScript with CDP

price.lua
local t = lemurx.tabs.current()
lemurx.cdp.attach(t.id)
local price = lemurx.v8.eval(t.id, "window.__INITIAL_STATE__?.price?.current")
lemurx.log("price", price)

A luakit chrome page in five lines

hello_chrome.lua
local chrome = require("chrome")   -- luakit's lib/chrome.lua, unmodified
chrome.add("hello", function(view, meta)
    return "<h1>Hello from Lua</h1><p>path: " .. meta.path .. "</p>"
end)
-- open luakit://hello/anything

Pages

Pitfalls & limits

  • net.addRule and subresources. Browser-side rules apply to navigations (document / sub_frame) and browser-initiated requests. For per-request control of page subresources use the renderer's send-request signal, which sees every img/script/xhr/fetch.
  • tabs.hide on the current tab blanks the screen — select another tab first.
  • theme.set({dark=...}) goes through LemurX's dark-mode switch and marks the app for restart; avoid toggling it often.
  • toolbar.* slots are 150 dp wide — one button, maybe two.
  • page.top / page.bottom are PopupWindows and are rebuilt on rotation; ui.update is much cheaper than re-render.
  • UI calls block. Don't ui.find forty times in one callback — dump once and filter in Lua.
  • eval returns JSON-serialisable values only. DOM nodes come back as nil; use the renderer layer if you need nodes.
  • Load order is file-name order. Cross-script globals need matching number prefixes; better, put shared code in files/lua/lib/ and require it.
  • Local scripts have all of os and io. os.exit() kills the browser process; io.open can read and write anything in the app's private directory. That is the point — don't teach it to beginners.
  • UGC scripts are isolated from each other too. Communicate through lemurx.storage or a local bridge script.
  • The bridge moves data, not functions. An imported function cannot take a Lua callback; poll storage or subscribe to events instead.
  • luakit init timing. webview.add_signal("init") only fires for webviews created after registration. For tabs that already exist, walk lemurx.tabs.list() and call __lk.webview_for_tab(id[, true]) (LemurX helper) to hook or wrap them, as the showcase scripts do.
  • A scheme request must be answered. If a handler is attached but never calls request:finish, the navigation fails after 30 s.

Pages

Publishing UGC scripts

  1. Place the file in files/lua/ugc/. Privilege drop and state isolation are automatic; you don't detect anything yourself.
  2. lemurx.ugc.scan(source) must return an empty table before listing. load(string) works (text only); dofile/loadfile/require/io/os do not exist.
  3. Anything that waits for the network uses http.fetch(url, opts, cb) or lemurx.async/await.
  4. If you need local capabilities (cookies, logged-in requests), document which bridge script the user must install, fetch it with lemurx.import("name"), and degrade gracefully when it is nil.
  5. No phishing-style full-screen with a hidden URL bar. Review rejects it.
  6. All outbound requests go through lemurx.http.fetch; list the domains in the file header comment.
  7. Provide a reset: on uninstall the script should theme.reset() + ui.unmount("*") + net.clearRules().
  8. Version in the first line: -- myskin v1.0.0.

Modules · lemurx.*

lemurx core

browserUGC ok
lemurx.version
Runtime version string.
lemurx.log(...)
Write to logcat (tag LemurX). print is an alias. Arguments are tostringed and tab-joined.
lemurx.toast(msg)
System toast.
lemurx.help([module])
Print the API catalog (lemurx.catalog) or one module of it.
lemurx.isPrivileged() -> boolean
true in local scripts, false in UGC.
lemurx.scriptName() -> string
"" for local scripts, "ugc/xxx.lua" for UGC.
lemurx.open(url[, opts]) -> tabId
Alias of lemurx.tabs.open.
lemurx.browser.info() -> {isLemurX, versionName, versionCode, chromiumVersion}
Build information: product flag, app version and the pinned Chromium version.

Modules · lemurx.*

lemurx.async · await · sleep

browserUGC ok

Coroutine sugar so waiting on the network never blocks the shared Lua thread.

lemurx.async(fn)
Run fn as a coroutine task. Errors inside are logged with a traceback, not swallowed.
lemurx.await(fn, ...) -> results
Inside async only. Calls fn(..., callback) — any function whose last parameter is a callback (http.fetch, timer.after, your own) — yields, and returns what the callback received. Raises when called outside async.
lemurx.sleep(ms)
Inside async only. Yields for ms milliseconds.
lemurx.async(function()
    local a = lemurx.await(lemurx.http.fetch, "https://a.example/api")
    local b = lemurx.await(lemurx.http.fetch, "https://b.example/api", { method = "POST", body = "{}" })
    lemurx.sleep(300)
    lemurx.ui.update("fx", { text = a.status .. "/" .. b.status })
end)

Modules · lemurx.*

lemurx.expose · import · exposed

browser

The only door between the local state and UGC states. Arguments and results are JSON-marshalled (nil/boolean/number/string/table); functions never cross. Exposed functions run with local privilege; an error inside becomes a Lua error on the UGC side.

lemurx.expose(name, fn | table)local only
Publish a function or a table of functions/values under name. Non-function members are copied by value.
lemurx.import(name) -> proxy | nilUGC
Fetch a proxy for something a local script exposed. nil if nothing is exposed under that name — always handle it.
lemurx.exposed() -> {names}
Currently exposed names.

Modules · lemurx.*

lemurx.tabs

browserUGC ok

Tab objects are plain tables: {id, url, title, incognito, loading, canGoBack, canGoForward, muted}. Tab ids are stable integers and are also the id of the corresponding luakit webview and renderer page.

lemurx.tabs.list() -> {tab...}
All tabs in the current model.
lemurx.tabs.current() -> tab | nil
The selected tab.
lemurx.tabs.open(url[, {background=, hidden=, incognito=}]) -> tabId
Open a tab. hidden creates it without showing it (useful for scraping).
lemurx.tabs.close(id) · select(id) · hide(id) · show(id) · freeze(id)
Tab strip operations. freeze discards the renderer while keeping the entry.
lemurx.tabs.navigate(id, url) · reload(id) · reloadBypassCache(id) · stop(id)
Load control.
lemurx.tabs.back(id) · forward(id) · go(id, index) · offset(id, n)
History navigation; go takes an absolute entry index, offset a relative one.
lemurx.tabs.history(id) -> {ok, current, entries={{index,url,virtualUrl,title,transition,timestamp}...}, canGoBack, canGoForward}
The tab's NavigationController.
lemurx.tabs.frames(id) -> {main={...}, frames={...}}
The RenderFrameHost tree.
lemurx.tabs.eval(id, js) -> value
Run JavaScript in the main world of the main frame and return its JSON-serialisable result.
lemurx.tabs.inject(id, js[, {world="isolated"|"main", frames="main"|"all"}])
Plant a script. Default: isolated world (immune to page CSP and page JS), main frame only. frames="all" reaches iframes.
lemurx.tabs.html(id) -> string
Current document outerHTML.
lemurx.tabs.screenshot(id[, {base64=true}]) -> {ok, path, abs, width, height[, data]}
JPEG of the visible content, saved under lua/captures; data is base64 when requested.
lemurx.tabs.setDesktop(id, on)
Desktop UA + viewport for this tab.
lemurx.tabs.setZoom(id, percent) · getZoom(id) -> percent
Page zoom.
lemurx.tabs.mute(id, on) · isMuted(id) -> boolean
Audio mute.
lemurx.tabs.setUserAgent(id, ua)
UA for later navigations and navigator.userAgent.
lemurx.tabs.setHeaders(id, {["X-Foo"]="bar"})
Extra request headers for this tab's navigations.
lemurx.tabs.setJavaScript(on) · isJavaScript() -> boolean
Global JavaScript switch.
lemurx.tabs.viewport([id]) -> {x, y, w, h, contentOffsetY, pageScale, density, viewportWidth, viewportHeight, scrollX, scrollY}
Where the web content sits on screen and how CSS pixels map to screen pixels. This is what lemurx.agent uses to unify web and native coordinates.
lemurx.tabs.on(event, fn)event
"started" | "document" | "loaded" | "created" | "closed" | "selected". fn(ev) receives the tab table plus event, eventUrl. See Events.

Modules · lemurx.*

lemurx.net

browserUGC ok

Declarative rules applied in the browser's network path to navigations and browser-initiated requests. They can rewrite response bodies, which no extension API allows. For per-subresource logic use the renderer send-request signal.

lemurx.net.addRule(rule) -> ruleId
lemurx.net.addRule({
    match = "*://*.doubleclick.net/*",           -- wildcard URL, host fragment, or "<all_urls>"
    action = "block" | "redirect" | "modify",
    types = { "document", "sub_frame", "script", "image", "xmlhttprequest", "media" },
    redirectUrl = "https://...",
    requestHeaders = { ["X-Foo"] = "bar" },      removeRequestHeaders = { "Cookie" },
    responseHeaders = { ["X-Bar"] = "baz" },     removeResponseHeaders = { "Content-Security-Policy" },
    replaceBody = { ["old"] = "new" },           -- or { {from=, to=} }; text responses ≤ 2 MB
    requestBody = "...",                         -- local only: replace the POST body
    replaceRequestBody = { a = "b" },            -- local only
})
lemurx.net.removeRule(id) · clearRules() · listRules() -> {rule...}
Rule management. Rules do not persist across restarts; re-add them in your script.

Modules · lemurx.*

lemurx.http

browserUGC ok

Requests issued by the browser process: no CORS, cookies not sent. UGC: http(s), 2 MB, 20 s. Local: 8 MB, 120 s.

lemurx.http.fetch(url[, opts][, cb]) -> response | requestId

opts = {method, headers, body, timeout, redirect=true, binary=false}. Response: {ok, status, headers, url, bytes, body[, base64=true]}.

With cb the call returns a request id immediately and the response arrives on the Lua thread later — prefer this form, or use lemurx.await.

lemurx.http.save(url, path[, opts]) -> {ok, ...}
Fetch, then write into lemurx.fs (path relative to the fs root).

Modules · lemurx.*

lemurx.fs

browserUGC ok

Jailed file access. Paths are relative to root(); .. escaping is refused. Local scripts also have the full io library for anything outside the jail.

lemurx.fs.root() -> path
files/lua locally, files/lua/ugc for UGC.
lemurx.fs.read(path[, {base64=true}]) -> {ok, path, size, data}
Read a file.
lemurx.fs.write(path, data[, {append=true, base64=true}]) -> boolean
Write / append.
lemurx.fs.list(path) -> {{name, size, dir, mtime}...}
Directory listing.
lemurx.fs.exists(path) · mkdir(path) · remove(path)
Booleans.

Modules · lemurx.*

storage · clipboard · db · timer

browserUGC ok
lemurx.storage.get(key[, default]) -> string · set(key, value) · delete(key) · list() -> {keys}
Persistent string key/value store.
lemurx.clipboard.get() -> string · set(text)
System clipboard.
lemurx.db.exec(sql[, {bind...}]) -> {ok, rowsAffected}
SQLite at files/lua/lemurx.db. For separate databases use luakit's sqlite3{}.
lemurx.db.query(sql[, {bind...}]) -> {ok, rows={{col=value}...}}
Query rows as tables.
lemurx.timer.after(ms, fn) -> id · every(ms, fn) -> id · cancel(id)
Timers on the Lua thread. Minimum 10 ms.

Modules · lemurx.*

lemurx.chrome

browserUGC ok

The browser shell: toolbar, bottom bar, omnibox, system bars. Colors are "#AARRGGBB".

lemurx.chrome.controls("shown"|"hidden"|"both")
How the top/bottom bars react to scrolling.
lemurx.chrome.hideUrlBar(on) · fullscreen(on)
Hide the URL bar / go immersive.
lemurx.chrome.setUrlBarText(text) · getUrlBarText() -> string · focusUrlBar(on)
Omnibox text and focus.
lemurx.chrome.setToolbarColor(argb) · setStatusBarColor(argb)
Immediate colors (use theme.set to persist).
lemurx.chrome.setDarkMode(on) · isDarkMode() -> boolean
Dark mode. Marks the app for restart.
lemurx.chrome.setPullRefresh(on)
Pull-to-refresh gesture.
lemurx.chrome.setMenuButtonVisible(on) · hideBottomToolbar(on)
Menu button; the button groups either side of the URL bar.
lemurx.chrome.hideButton("back"|"forward"|"home"|"tabs"|"tools"|"menu"|"search", on)
Individual shell buttons.
lemurx.chrome.back() · forward()
Trigger the shell's own back/forward (including "let the system handle back").
lemurx.chrome.info() -> {controls, darkMode, urlBarText, urlBarFocused}
Current shell state.
lemurx.chrome.on("omnibox"|"back"|"menu", fn)event
See Events.

Modules · lemurx.*

lemurx.theme

browserUGC ok

A persisted skin, replayed automatically on every launch and Activity re-creation.

lemurx.theme.set(theme)
Merge semantics — pass only the keys you change.
lemurx.theme.set({
    toolbar = "#FF1B5E20", statusBar = "#FF0D3B13", navBar = "#FF1B5E20", bottomBar = "#FF1B5E20",
    iconTint = "#FFE8F5E9", accent = "#FF00C853",   -- accent = default button color in ui.render
    dark = true,
    hideButtons = { "tools" },
    urlBarHidden = false, bottomBarHidden = false, menuButton = true, pullRefresh = false,
})

To delete a key, reset() and set again (a nil value cannot be expressed in a Lua table).

lemurx.theme.get() -> {ok, theme, dark}
Current persisted theme.
lemurx.theme.reset()
Forget the theme and restore defaults as far as possible.
lemurx.theme.apply(name[, extra])
Apply a preset ("forest" | "paper" | "midnight" | "sakura") merged with extra.
lemurx.theme.presets.<name>
The preset table — add your own.

Modules · lemurx.*

lemurx.ui.render · h · update · unmount

browserUGC ok

Declarative native Android view trees mounted into named slots of the shell. Callbacks are plain Lua functions. Re-rendering a slot replaces its tree; trees are re-mounted after Activity re-creation or bottom-bar style changes.

Slots (lemurx.ui.slots())

SlotWhere
toolbar.start / toolbar.endLeft / right button strip of the top bar (150 dp)
bottom.start / bottom.barFirst cell / end of the bottom bar
page.top / page.bottomFull-width strip directly above / below the web content
page.center / page.floatCentered / floating over the page (gravity, x, y)
view:<native id>Any ViewGroup you find in ui.dump()
lemurx.ui.h(type, props[, children]) -> node
Build a node. Types: row column stack scroll hscroll text button icon image edit switch progress divider spacer.
lemurx.ui.render(slot, node)
Mount a tree into a slot.
local h = lemurx.ui.h
lemurx.ui.render("page.top", h("row", { background = "#E61B5E20", padding = {12, 6} }, {
    h("text",   { id = "st", text = "status", color = "#FFFFFFFF", weight = 1, maxLines = 1, ellipsize = true }),
    h("switch", { id = "sw", text = "Desktop", color = "#FFFFFFFF",
                  onChange = function(ev) lemurx.tabs.setDesktop(lemurx.tabs.current().id, ev.checked) end }),
    h("button", { id = "close", text = "Hide", onClick = function() lemurx.ui.unmount("page.top") end }),
}))
lemurx.ui.update(id, props)
Change properties of a mounted node in place (cheap; preferred over re-render).
lemurx.ui.unmount(slot | id | "*")
Remove a slot's tree, one node, or everything — including trees planted by replace/insert, which puts the native views back.
lemurx.ui.slots() -> {slot...}
Available slot names.

Node properties

GroupProperties
Commonid · width/height ("match" "wrap" dp) · weight gravity align · padding/margin (8, {h,v}, {l,t,r,b}, {left=8}, paddingH/V, marginH/V) · background ("#AARRGGBB", "drawable:name", {color, radius, stroke, strokeColor, gradient={...}, orientation, ripple=true}) · radius visible enabled alpha elevation rotation desc tooltip minWidth minHeight · onClick onLongClick onChange
Text (text/button/edit/switch)text color size(sp) bold italic font("monospace" "serif" "file:x.ttf") maxLines ellipsize singleLine underline strike lineSpacing
icon / imageicon ("ic_xxx" APK drawable, "android:ic_menu_search", "file:icons/a.png", "data:image/png;base64,…"), text fallback (emoji works), tint, scale (fit crop center fill)
edithint value inputType(text number url password multiline) maxLength hintColor; onChange fires with ev.action="change" per keystroke and "submit" on IME confirm, ev.text is the content
switchchecked; onChange gets ev.checked
progressvalue max indeterminate color

Callback argument everywhere: ev = {id, action="click"|"longclick"|"change"|"submit", slot, text?, checked?}.

Modules · lemurx.*

lemurx.ui.style · replace · insert · detach · move · on

browserUGC ok

The shell is ordinary Android views. Lua can restyle, replace, insert next to, remove, move, or take over clicks on any of them — including views in Dialogs and PopupWindows (the tools drawer). Operations are recorded by query and replayed after Activity re-creation.

Query: {id=, text=, desc=, class=, clickable=, max=} — id is a resource name or a Lua node id, text/class match by substring. Known shell ids: toolbar toolbar_buttons_left toolbar_buttons home_button tab_switcher_button tab_back_button tab_forward_button menu_tools menu_button_lemur location_bar url_bar search_box search_box_text ivLogo ntp_favorites.

lemurx.ui.style(query, style)
Apply node properties (same set as render) to existing views. once=true disables replay.
lemurx.ui.style({ id = "url_bar" }, { size = 16, bold = true })
lemurx.ui.style({ class = "TabSwitcherButtonView" }, { tint = "#FFFF5252" })
lemurx.ui.style({ text = "History" }, { background = "#FF000000", once = true })
lemurx.ui.replace(query, node)
Swap a native view for a Lua tree, same layout slot. unmount(id | "*") restores the original.
lemurx.ui.insert(query, node[, {position="after"|"before"|"into"}])
Insert a Lua tree next to (or into) a native view.
lemurx.ui.detach(query) · restore([query])
Take a native view out of its parent; put it (or all detached views) back.
lemurx.ui.move(query, {parent=, index=, before=, after=})
Re-parent or reorder a native view.
lemurx.ui.on(query, "click"|"longclick"|"touch"|"focus"|"text", fn[, {consume=true, move=, max=}]) -> {key}
Take over an event on native views. consume=false lets the original handler run too.
lemurx.ui.off(key | "*")
Remove handlers.
lemurx.ui.shell() -> {found, ids, rects}
Known shell view ids with presence and coordinates.
lemurx.ui.children(query) -> {nodes}
Children of a ViewGroup.

Modules · lemurx.*

lemurx.ui automation & dialogs

browserUGC ok
lemurx.ui.dump([{maxDepth=12, maxNodes=800, gone=true}]) -> {ok, nodes, windows, roots}
The whole view tree across all windows (Activity, Dialogs, PopupWindows).
lemurx.ui.find(query) -> {ok, count, nodes={{class,id,text,desc,clickable,enabled,visible,x,y,w,h}...}}
Find views.
lemurx.ui.click(query) · longClick(query)
Perform clicks.
lemurx.ui.setText(query, text) · getText(query) -> string
TextView contents.
lemurx.ui.visible(query, on) · enabled(query, on)
Visibility / enabled state.
lemurx.ui.show({id, type="button"|"text"|"edit", text, gravity, x, y, background, color, textSize, radius, paddingH, paddingV, width, height, hint, onClick}) -> id
Single floating control (older API; render supersedes it).
lemurx.ui.remove(id) · clear()
Remove floating controls.
lemurx.ui.dialog({title, message, ok, cancel, onOk=fn, onCancel=fn})
Native dialog; callbacks get {id, action}.
lemurx.ui.prompt({title, hint, value, onOk=fn})
Text prompt; onOk(ev) with ev.text.
lemurx.ui.alert(title, message[, okText])
Simple alert.
lemurx.ui.tap(x, y[, "dp"]) · longPress(x, y) · swipe(x1, y1, x2, y2[, ms])
Synthetic touch in screen coordinates, delivered to the topmost window (Activity, Dialog or PopupWindow). Same coordinate system as the x, y of dump/find nodes.
lemurx.ui.screenshot([{scale=, quality=, base64=}]) -> {ok, path, abs, width, height, scale[, data]}
JPEG of the whole browser window (shell + page), saved under lua/captures.

Every node returned by dump/find carries a ref; lemurx.ui.click({ref=12}) addresses that exact view again.

Modules · lemurx.*

lemurx.input

browserUGC ok

Coordinates default to dp relative to the web content area.

lemurx.input.tap(x, y[, tabId[, "dp"|"px"]])
Also tap({x=, y=, tab=, unit=}).
lemurx.input.swipe(x1, y1, x2, y2[, durationMs[, tabId]])
Swipe gesture.
lemurx.input.type(text[, tabId])
Type into the focused element.
lemurx.input.key("enter"|"back"|"tab"|"space"|"esc"|"delete"[, tabId])
Key event.
lemurx.input.onBack(fn)event
Own the back key. fn(ev); call lemurx.chrome.back() to pass it through.
lemurx.input.interceptBack(on)
false returns the key to the system.

Modules · lemurx.*

lemurx.intent

browserUGC restricted
lemurx.intent.startActivity({action, url, package, extras})
Launch an Android Intent. UGC may only use VIEW SEND SENDTO WEB_SEARCH MAIN and lemurx.* actions.
lemurx.intent.sendBroadcast({action, extras})
Broadcast an Intent (same UGC restriction).

Modules · lemurx.*

Privileged: cookie · history · downloads · perm · prefs · data · features

browserlocal only
lemurx.cookie.get(url) -> {{name, value, domain, path, secure, httpOnly, expires}...}
Cookies for a URL.
lemurx.cookie.set(url, "name=value; path=/; secure")
Set-Cookie syntax.
lemurx.history.query([text]) -> {{url, title, timestamp, domain}...}
Search browsing history.
lemurx.downloads.list() -> {...} · enqueue(url[, tabId]) -> id
Download manager.
lemurx.perm.set(origin, type, "allow"|"block"|"ask") · get(origin, type)
type: geolocation camera microphone notifications javascript cookies images popups sound autoplay.
lemurx.prefs.get(name[, "bool"|"int"|"double"|"string"|"long"]) -> {ok, name, exists, value, isDefault, managed}
Chromium PrefService.
lemurx.prefs.set(name, value) · set(name, type, value) · clear(name)
Write / reset a pref.
lemurx.data.clear({"history","cache","site_data"}[, "15m"|"hour"|"day"|"week"|"all"])
BrowsingDataRemover.
lemurx.features.enabled(name) -> boolean · param(feature, key) -> string
Read-only ChromeFeatureList.

Modules · lemurx.*

lemurx.cdp

browserlocal only

The complete Chrome DevTools Protocol, attached in-process — no USB, no desktop, no warning bar. Runtime and Page are enabled on attach; other domains enable themselves on first use.

lemurx.cdp.attach(tabId) · detach(tabId)
Attach to a tab's DevToolsAgentHost.
lemurx.cdp.send(tabId, method, params[, timeoutMs | {timeout=, sessionId=}]) -> {ok, id, result} | {ok=false, error}
Synchronous command.
lemurx.cdp.send(tab, "DOM.getDocument", { depth = 1 })
lemurx.cdp.send(tab, "Network.emulateNetworkConditions", { offline = false, latency = 300, downloadThroughput = 51200, uploadThroughput = 25600 })
lemurx.cdp.on(method | "*", fn)event
Protocol events: fn({method, tab, params}).
lemurx.cdp.targets() -> {{id, type, title, url, ...}...}
All targets: page, iframe, worker, service_worker, browser.
lemurx.cdp.host(targetId | "browser", method, params)
Send to any target, including the browser target.
lemurx.cdp.inspect(tabId, x, y) · version()
Hit-test a point / protocol version.

Modules · lemurx.*

v8 · dom · ax · blink · page · network · emulation · overlay · css · runtime · sw

browserlocal only

Thin wrappers over cdp.send, defined in init.lua. tab is a tab id.

lemurx.v8.eval(tab, expr[, opts]) -> value, raw
Runtime.evaluate with the value unwrapped.
lemurx.v8.call(tab, fnSource, argsJson) -> value
Runtime.callFunctionOn.
lemurx.dom.document(tab) · query(tab, selector) · box(tab, selector)
DOM tree, node ids for a selector, box model.
lemurx.ax.tree(tab)
Full accessibility tree as a Lua table.
lemurx.blink.frames(tab) · metrics(tab)
Frame tree; Performance.getMetrics as a name→value table.
lemurx.page.navigate(tab, url) · reload(tab, ignoreCache) · stop(tab) · enablePaint(tab, on) · cookies(tab) · screenshot(tab, opts)
Page.*; screenshot delegates to tabs.screenshot.
lemurx.network.enable(tab) · setOffline(tab, on) · emulate(tab, {latency, download, upload}) · clear(tab) · headers(tab, {...})
Network.*: offline, throttling, extra headers.
lemurx.emulation.setUA(tab, ua) · setDevice(tab, {width, height, scale, mobile}) · setGeo(tab, lat, lng) · setTimezone(tab, id) · setTouch(tab, on)
Emulation.*.
lemurx.overlay.inspect(tab[, mode]) · hide(tab)
Inspect-mode highlight overlay.
lemurx.css.getComputed(tab, nodeId)
Computed style for a node.
lemurx.runtime.contexts(tab)
Execution contexts.
lemurx.sw.targets() · send(target, method, params)
Service-worker targets and messaging.

Modules · lemurx.*

lemurx.agent · lemurx.system

browserlocal only

The observe → act half of a GUI agent; deciding is left to whatever model the script talks to over lemurx.http. Three layers, all inert until called: the web page (tabs.screenshot, set-of-marks, CDP), the browser shell (ui.screenshot, ui.dump/find) and other apps (lemurx.system.*, an AccessibilityService the user enables once in Settings › Accessibility). Every element is reported in screen pixels; web elements are mapped through lemurx.tabs.viewport().

lemurx.agent.observe([opts]) -> obs
One observation: {mode="browser"|"system", tab, viewport, screenshot, elements={...}}. opts: {tab, screenshot=true, scale=0.5, quality=60, base64=false, web=true, maxWeb=120, text=false, native=true, maxNative=200, system=nil|true|false}. system=nil means auto: when the accessibility service is connected and LemurX is not in the foreground, the whole-device UI tree is observed instead (mode="system").
lemurx.agent.describe(obs[, opts]) -> string
Compact text for a model: one line per element with a ref — w7 (web, data-lemurx-mark), n12 (native view), s5 (other app) — plus role, text and @(x,y w×h).
lemurx.agent.act(action[, obs]) -> result
Execute by ref or by screen coordinates, choosing the most reliable path per layer (web → input.tap relative to the web view, or el.click() with via="js"; native → performClick, falling back to coordinates; system → performAction, falling back to a gesture). Actions: tap|click{ref|x,y,long,dx,dy,via} longPress type|text{text,ref,clear,enter,append} swipe|scroll{dir,ref,distance,duration|x1,y1,x2,y2} key{key} back home recents notifications navigate{url} open{url} eval{js} launch{package|url} wait{ms} screenshot done{result}. A bare string ("back") is accepted.
lemurx.agent.run(policy[, opts])
The loop. policy(obs, text, step) returns the next action table, or nil / {type="done"} to stop. opts: {maxSteps=20, settle=600, onStep=fn(step, obs, action, result), onDone=fn(out)}. Run it inside lemurx.async; called outside a coroutine the result arrives via onDone.
lemurx.agent.mark(tab[, {max, text}]) · find(obs, "text" | {role="button"}) · look([opts]) · wait(ms)
Set-of-marks on its own; pick an element out of an observation; observe and remember so later act calls need no obs; sleep.

lemurx.system.* is the no-root equivalent of adb shell uiautomator dump / input tap / screencap. Off by default; the user can switch it off in system settings at any time; it stops answering when the Lua master switch is off; UGC scripts never see it.

lemurx.system.status() -> {connected, enabledInSettings, canScreenshot, component}
Service state.
lemurx.system.openSettings() · ensure() -> boolean
Take the user to Settings › Accessibility to enable "LemurX automation" — LemurX never enables it by itself. ensure opens settings when needed and reports readiness.
lemurx.system.foreground() -> {package, class, title, self} · windows()
What is in front.
lemurx.system.tree([{flat=true, interactiveOnly=true, all=false, maxNodes=1500}])
Accessibility node tree; nodes are {ref, class, id, text, desc, clickable, editable, …, x, y, w, h} in screen pixels.
lemurx.system.find("text" | {id=, text=, clickable=, editable=, package=})
Query nodes.
lemurx.system.click(ref | query) · longClick · focus · scroll(ref, "down"|"up"|…)
performAction on a node.
lemurx.system.setText(ref | {editable=true}, text)
ACTION_SET_TEXT, falling back to clipboard paste.
lemurx.system.tap(x, y) · longPress(x, y) · swipe(x1, y1, x2, y2[, ms])
dispatchGesture in screen pixels.
lemurx.system.global("back"|"home"|"recents"|"notifications"|"quickSettings"|"lock"|"power")
performGlobalAction.
lemurx.system.screenshot([{scale=0.5}])
Whole-screen JPEG (API 30+).
lemurx.system.launch(package | url | {action=, data=, extras=})
Start another app.
lemurx.system.on("window"|"content"|"click"|"focus"|"text"|"notification"|"connected"|"disconnected", fn)event
Accessibility events. Subscribing turns event delivery on (system.events(true) underneath).

Modules · lemurx.*

lemurx.ugc · catalog · help

browserUGC ok
lemurx.ugc.forbidden -> {patterns}
The list of privileged names UGC may not use.
lemurx.ugc.scan(source) -> {hits}
Static pre-check of a script for forbidden names. A hint for authors; the real boundary is runtime.
lemurx.catalog -> {{name, title, priv, apis={...}}...}
Machine-readable API catalog, the source of lemurx.help().

Modules · luakit

luakit

luakitbrowserlocal only

The global luakit table (luakit's clib/luakit.c). Properties: config_dir data_dir cache_dir execpath confpath resource_path(rw) verbose nounique dev_paths enable_spell_checking(rw) spell_checking_languages(rw) process_limit(rw) options website_data webkit2 windows webkit_version webkit_user_agent_version chromium_version selection install_path install_paths version web_process_id.

luakit.register_scheme(name)
Register a URL scheme. Requests for it arrive as scheme-request::<name> on the originating webview. See Custom schemes.
luakit.spawn(cmd[, callback(exit_reason, exit_status)]) -> pid
Run a process asynchronously. cmd is shell-split (a sufficient subset of g_shell_parse_argv).
luakit.spawn_sync(cmd) -> status, stdout, stderr
Run and wait.
luakit.exec(cmd)
Replace-process semantics are not available on Android; behaves as spawn.
luakit.time() -> seconds
Monotonic seconds.
luakit.uri_encode(s[, allowed]) · uri_decode(s[, illegal])
Percent-encoding.
luakit.idle_add(fn) · idle_remove(fn) -> boolean
Run once when idle (fn returning true re-schedules).
luakit.allow_certificate(host, cert)
Trust a certificate for a host (used by webview:allow_certificate).
luakit.save_file(title, window, default_folder, default_name) -> path | nil
Save dialog.
luakit.wch_lower(s) · wch_upper(s)
Unicode case mapping.
luakit.clear_favicon_database()
No-op placeholder (Chromium manages favicons).
luakit.quit()
Exit the browser process.
luakit.add_signal(name, fn) · emit_signal · remove_signalsignal
Module-level signals: "page-created"(page) and "web-extension-created" (renderer side), "download-start"(download, view), "can-close", "scheme-request::<name>".

Modules · luakit

soup

luakitbrowserrenderer
soup.parse_uri(str) -> {scheme, user, password, host, port, path, query, fragment} | nil
Parse with GURL. A missing scheme defaults to http://.
soup.uri_tostring(tbl) -> string
Inverse of parse_uri.
soup.proxy_uri
"default" | "no_proxy" | "http://host:port" … (read/write).
soup.accept_policy
"always" | "never" | "no_third_party" (read/write).
soup.cookies_storage
Path value kept for compatibility; Chromium owns the cookie store.

Modules · luakit

sqlite3

luakitbrowser

One independent connection per object (luakit's history, bookmarks, quickmarks and adblock each open their own). Relative filenames resolve under luakit.data_dir.

sqlite3{ filename = path } -> db
Open or create.
db:exec(sql[, bindings]) -> rows | nil
Multiple statements allowed. Bindings: {1=..., 2=...} positional or {[":name"]=...} named. Rows are tables keyed by column name.
db:compile(sql) -> statement · stmt:exec([bindings]) -> rows
Prepared statements.
db:changes() -> n · db:close() · db.filename
Row count of the last statement; close; read-only path.

Modules · luakit

stylesheet

luakitbrowser
stylesheet{ source = "body { ... }" } -> ss
A user stylesheet object.
ss.source
Read/write. Changing it re-injects into every webview that has it attached.
view.stylesheets[ss] = true | false
Attach / detach to a webview; applied on every commit.

Modules · luakit

timer

luakitbrowser
timer{ interval = ms } -> t
Create (not started).
t:start() · t:stop()
Start / stop.
t.started · t.interval
Read-only state; interval is read/write (changing it while running restarts).
"timeout"signal
Fires every period until stopped.

Modules · luakit

regex

luakitbrowser
regex{ pattern = "^https?://" } -> r
Compile with RE2. PCRE-only syntax (back-references, look-behind) errors at construction.
r:match(s) -> boolean · r.pattern
Partial match; read-only pattern.

Modules · luakit

utf8

luakitbrowserrenderer

Lua 5.4's standard utf8 library with luakit's quirks preserved: utf8.len returns the byte length for invalid sequences instead of nil; string.wlen is provided by the 5.1 compatibility shim.

Modules · luakit

xdg

luakitbrowser
xdg.cache_dir · config_dir · data_dir
App-private directories.
xdg.desktop_dir · documents_dir · download_dir · music_dir · pictures_dir · public_share_dir · templates_dir · videos_dir
Public external storage directories.
xdg.system_data_dirs · system_config_dirs
Arrays. Unknown keys return nil.

Modules · luakit

msg

luakitbrowserrenderer
msg.fatal(fmt, ...) · error · warn · info · verbose · debug
Formatted logging to logcat, tagged with the calling module. Renderer-side messages are forwarded to the browser process.
"log"(time, level, group, text)signal
msg.add_signal("log", fn) — how log_chrome builds luakit://log.

Modules · luakit

unique

luakitbrowser
unique.new(name) · is_running() -> false · send_message(str)
Single-instance control. Android runs one process per package, so is_running is always false; external entry points (Intents opening links) are replayed as a "message"(msg, screen) signal so unique_instance.lua works unchanged.

Modules · luakit

lfs

luakitbrowser
lfs.attributes(path[, name]) · symlinkattributes · dir(path) · mkdir · rmdir · chdir · currentdir · touch
The LuaFileSystem subset luakit's lib/ uses, over POSIX stat/opendir. dir returns an iterator including . and ... In the renderer, lfs is a sandbox-consistent empty implementation.

Modules · luakit

ipc_channel · require_web_module

luakitbrowser

Browser ↔ renderer messaging (luakit's clib/ipc.c + web_module.c). Transport is Mojo. Signals emitted before a renderer exists are queued and replayed when it comes up, preserving luakit's "web modules are ready when the page is created" assumption.

require_web_module(name) -> ipc_channel
Load name.lua into every renderer (now and future). Resolved from files/luakit/config/, then files/luakit/lib/. Returns the channel of the same name.
ipc_channel(name) -> ch
Get or create a named channel.
ch:emit_signal([view, ]name, ...)
Send to every renderer, or only the one hosting view (a webview or its id). Arguments are JSON-marshalled.
ch:add_signal(name, fn(ch, ...))signal
Receive from renderers. luakit convention: web modules pass page.id as the first argument when the sender matters.

Modules · luakit

Custom schemes · request

luakitbrowser

After luakit.register_scheme("foo"), every navigation and subresource for foo:// is routed to Lua as a "scheme-request::foo"(view, uri, request) signal on the originating webview (falling back to the selected tab's webview when the origin is a worker). luakit:// is registered by lib/chrome.lua and serves all of luakit's chrome pages.

request:finish(data[, mime])
Send the response. May be called later (async), within 30 s.
request.finished -> boolean
Read-only.
"scheme-request::<name>"(view, uri, request)signal
On webview. Return without finishing to let another handler answer; a scheme with no handler attached returns 404 immediately.
luakit.register_scheme("lua")
webview.add_signal("init", function(view)
    view:add_signal("scheme-request::lua", function(v, uri, request)
        if uri:match("^lua://tabs") then
            request:finish(__luakit.json_encode(lemurx.tabs.list()), "application/json")
        end
    end)
end)

Classes · luakit

Object protocol & signals

luakitbrowserrenderer

Every luakit object (widget, download, timer, page, dom_element, …) follows the same protocol:

  • type(obj) returns the class name ("widget", "page", …). pairs works on the object's public properties.
  • Properties are read/written as fields; a successful write emits "property::<name>".
  • obj:add_signal(name, fn) · obj:emit_signal(name, ...) · obj:remove_signal(name, fn) · obj:remove_signals(name).
  • Handlers run in registration order as fn(obj, ...). The first handler returning a non-nil value stops propagation and that value is what emit_signal returns (luakit's LUA_MULTRET semantics) — this is how navigation-request returning false vetoes.
  • Classes have module-level signals too: widget.add_signal("create", fn(w)), luakit.add_signal(...).
  • After destroy only obj.is_alive may be read; anything else raises.

Classes · luakit

widget

luakitbrowser

widget{ type = "...", <props> } constructs a widget. Every non-webview type is backed by a real Android View kept by the Java host LemurXWidgetHost; property reads/writes and method calls cross synchronously, events come back asynchronously — except key-press, whose "consumed?" verdict the UI thread waits for.

Types: window hbox vbox hpaned vpaned notebook overlay stack eventbox scrolled label entry image spinner drawing_area webview.

w.type · w.parent · w.is_alive
Read-only.
w.visible · w.focused · w.can_focus · w.tooltip · w.opacity
Read/write.
w.width · w.height · w.min_size · w.margin · w.margin_left/right/top/bottom · w.align
Geometry in dp. align = { h = "left"|"center"|"right"|"fill", v = ... }.
w.bg · w.fg · w.font · w.css
GTK/CSS colors ("#rrggbb", names), Pango font strings ("monospace bold 12", size in sp), a CSS subset.
w:show() · w:hide() · w:focus() · w:destroy()
Common methods.
w:send_key(key, mods) · w:query_tooltip() · w:css_reset() · w:rect() -> {x, y, w, h}
Synthetic key event; tooltip text; reset CSS; on-screen rectangle.
widget.add_signal("create", fn(w))class signal
Fires for every new widget — how webview.lua/window.lua initialise theirs.
"key-press"(w, mods, key)signal
Return true to consume. mods is a table of "Control" "Shift" "Mod1"…; key is the GDK keysym name ("Escape", "Return", "a").
"button-press" · "button-release" · "button-double-click"(w, mods, button)signal
Pointer/touch events (button 1). Attaching a handler enables the host-side listener.
"mouse-enter" · "mouse-leave" · "scroll" · "focus" · "unfocus" · "resize" · "parent-set" · "destroy"signal
Other widget events.

Classes · luakit

widget:webview

luakitbrowser

A luakit webview is a LemurX tab. widget{type="webview"} opens a tab; with luakit_autowrap on (default) every tab the user opens natively is wrapped too, so webview.add_signal("init") and luakit.windows-style code see them. view.id equals the tab id and the renderer page.id. Packing a webview into a notebook or box moves the tab's content into that widget's rectangle.

view.uri
Read/write — assigning navigates.
view.title · view.progress · view.is_loading · view.is_playing_audio · view.private · view.id · view.web_process_id · view.hovered_uri · view.favicon · view.favicon_uri · view.certificate · view.source
Read-only state. certificate is PEM.
view.session_state
Read/write serialized navigation history (used by session.lua, undoclose.lua).
view.history -> {index, items={{uri, title}...}}
Back/forward list.
view.scroll -> {x, y, xmax, ymax, xpage_size, ypage_size}
Read/write scroll position.
view.zoom_level · view.user_agent · view.enable_javascript · view.editable · view.enable_scripts
Read/write settings. All WebKitSettings keys (default_font_size, …) are accepted and mapped where Chromium has an equivalent.
view.stylesheets
Set-like table: view.stylesheets[ss] = true. See stylesheet.
view.inspector · view:show_inspector() · close_inspector()
DevTools front-end for this tab.
view:go_back() · go_forward() · can_go_back() · can_go_forward() · reload() · reload_bypass_cache() · stop() · loading()
Navigation control.
view:eval_js(script[, {source=, callback=fn(ret, err)}])
Main-world evaluation; with callback it is asynchronous.
view:load_string(html, base_uri)
Show generated HTML without a network request or history entry.
view:search(text, case_sensitive, forward, wrap) · search_next() · search_previous() · clear_search()
Find in page.
view:ssl_trusted() -> boolean | nil · allow_certificate(host, cert)
TLS state.
view:save(path) · view:crash() · view:set_pdfjs(on)
Save page (MHTML); kill the renderer; PDF viewer preference.
"load-status"(view, status, uri, err)signal
status: "provisional" | "redirected" | "committed" | "first-visual" | "finished" | "failed".
"navigation-request"(view, uri, reason) -> falsesignal
Return false to cancel. Implemented as a NavigationThrottle: nothing loads, no history entry. reason mirrors luakit ("link-clicked", "form-submitted", "back-forward", "reload", "form-resubmitted", "other").
"new-window-decision"(view, uri, reason) -> falsesignal
Popups / target=_blank. Return false to block; otherwise the new tab is wrapped and its parent_view set.
"scheme-request::<name>"(view, uri, request)signal
See Custom schemes.
"favicon" · "crashed" · "web-extension-loaded" · "property::uri" · "property::title" · "property::progress" · "property::is_playing_audio" …signal
State changes.

Classes · luakit

widget:window

luakitbrowser

A full-screen overlay FrameLayout on the Activity's content view. Showing a window hides the native toolbar and bottom bar; destroying (or hiding) the last window brings them back. luakit.windows lists live windows.

win.child
The single child widget.
win.title · win.decorated · win.urgency_hint · win.fullscreen · win.maximized · win.screen · win.root_win_xid
Accepted for compatibility; fullscreen maps to immersive mode.
win:set_default_size(w, h)
Requested size in dp; 0 means fill.
win:set_dark_mode(on)
Dark-mode hint for the overlay.
"destroy" · "can-close" · "delete-event" · "key-press"signal
Window signals. Hardware keys reach the window's key-press when no child consumed them.

Classes · luakit

widget:box (hbox / vbox)

luakitbrowser

A LinearLayout.

box.homogeneous · box.spacing
Equal-size children; gap in dp.
box:pack(child[, {expand=, fill=, padding=, start=false}])
Append (or prepend with start=false semantics of pack_end). expand becomes layout weight.
box:reorder(child, index)
Move a child (1-based).
box:remove(child) · box:replace(old, new)
Remove / swap.
box.children -> {widget...}
Current children.
"add"(box, child) · "remove"(box, child)signal
Child changes.

Classes · luakit

widget:notebook

luakitbrowser

Pages are usually webviews. Switching a page selects the corresponding native tab, and selecting a tab natively switches the notebook. Indices are 1-based; -1 means last.

nb:append(child) -> index · nb:insert(index, child) -> index
Add pages.
nb:remove(child)
Remove a page (the widget survives).
nb:count() -> n · nb:current() -> index
Page count; selected page index.
nb:atindex(i) -> widget · nb[i] · nb:indexof(child) -> index | nil
Lookup both ways.
nb:switch(i) · nb:reorder(child, i)
Select / move a page.
nb:set_title(child, text) · get_title(child)
Tab labels (used when show_tabs).
nb.show_tabs · nb.show_border
Built-in tab strip on/off (luakit's window.lua draws its own and hides this).
"switch-page"(nb, view, index) · "page-added"(nb, view, index) · "page-removed"(nb, view) · "page-reordered"(nb, view, index)signal
Page lifecycle.

Classes · luakit

widget:label

luakitbrowser
label.text
Pango markup (<span foreground='#fff' weight='bold'>, <b>, <i>, <u>, <tt>) translated to Android Spanned.
label.fg · label.bg · label.font · label.padding · label.align · label.textwidth · label.selectable
Styling. textwidth in characters (0 = unconstrained); ellipsised when exceeded.

Classes · luakit

widget:entry

luakitbrowser

An EditText. Focusing it raises the soft keyboard.

entry.text · entry.position · entry.show_frame · entry.fg/bg/font
Contents, caret position, frame, styling.
entry:insert_text(text[, position]) · entry:select_region(start[, end])
Edit helpers.
"changed"(entry) · "activate"(entry) · "key-press"(entry, mods, key)signal
Text changed; IME "done"/Enter; keys (return true to consume — luakit's modes/binds rely on this).

Classes · luakit

widget:eventbox

luakitbrowser
eb.child · eb.bg
Single child; background.
"button-press" · "button-release" · "button-double-click" · "mouse-enter" · "mouse-leave" · "scroll"signal
Touch → GTK-style pointer events. luakit's tab strip is a row of eventboxes.

Classes · luakit

widget:overlay · widget:stack

luakitbrowser
overlay.child · overlay:pack(child) · overlay:remove(child)
A FrameLayout: one main child, overlays stacked on top.
stack:pack(child) · stack.visible_child · stack.homogeneous
Show exactly one child at a time.

Classes · luakit

widget:paned (hpaned / vpaned)

luakitbrowser
paned:pack1(child[, resize, shrink]) · pack2(child[, resize, shrink])
Two children split horizontally or vertically.
paned.top/bottom · paned.left/right · paned.position
Child accessors (by orientation); divider position in dp.

Classes · luakit

widget:scrolled

luakitbrowser
scrolled.child · scrolled.scrollbars · scrolled.scroll -> {x, y, xmax, ymax}
A ScrollView/HorizontalScrollView chosen from the child's orientation; scroll position is read/write.

Classes · luakit

widget:image

luakitbrowser
image:filename(path)
Load from a file (luakit's tab icons ship in resources/icons/).
image:icon(name[, size])
Icon by name (APK drawable or Android system icon).
image:scale(w, h)
Scale to dp.
image:set_favicon_for_uri(uri) -> boolean
Show the favicon Chromium has for uri (tab_favicons.lua).

Classes · luakit

widget:spinner

luakitbrowser
spinner:start() · spinner:stop() · spinner.started
Indeterminate ProgressBar.

Classes · luakit

widget:drawing_area

luakitbrowser
da:invalidate() · da.bg · da.min_size
A plain View you size and color; useful as a separator or a colored gauge.

Classes · luakit

download

luakitbrowser
download{ uri = "https://..." } -> d · d:start() · d:cancel()
Create and control a download (via LemurX's download manager).
d.uri · d.destination(rw) · d.allow_overwrite(rw) · d.status · d.progress · d.current_size · d.total_size · d.content_length · d.mime_type · d.suggested_filename · d.elapsed_time · d.error
status ∈ "created" | "started" | "cancelled" | "failed" | "finished".
"decide-destination"(d, suggested) · "created-destination"(d, path) · "error"(d, msg) · "finished"(d) · "property::*"signal
Download lifecycle; luakit.add_signal("download-start", fn(d, view)) catches downloads the browser starts on its own.

Classes · renderer

luakit (web module side)

luakitrenderer

Available inside web modules loaded with require_web_module. One Lua state per renderer process; a process may host several pages.

luakit.web_process_id · luakit.version · luakit.install_path · luakit.resource_path
Process identity and paths.
luakit.add_signal("page-created", fn(page))signal
A main frame was created in this process. Attach page signals here.
luakit.register_function(uri_pattern, name, fn(page, resolve, reject, ...))
For documents whose URL matches the Lua pattern, define window[name](...) returning a Promise. Arguments arrive as Lua values (DOM nodes as dom_element); call resolve(value) or reject(err), possibly later. Errors thrown by fn reject automatically.
luakit.idle_add(fn) · idle_remove(fn) · luakit.time() · uri_encode · uri_decode · wch_lower · wch_upper
Same as the browser side.

Classes · renderer

page

luakitrenderer

One page per main frame in the process. page.id equals the browser-side webview.id / tab id, so a web module can name the tab when talking to the UI process.

page.uri · page.id
Current URL; tab id.
page.document -> dom_document
The live document.
page:eval_js(script[, {source=}]) -> value | nil, err
Synchronous main-world evaluation. If the result is a JS function, a callable Lua function is returned.
page:wrap_js(body[, {argnames}]) -> fn
Compile a JS function body into a Lua-callable function.
"send-request"(page, uri, headers) -> string | false | nilsignal
Every subresource request of this page, synchronously, before it is sent. Return a string to redirect, false to block, nothing to allow. Mutations to the headers table are applied. Backed by a renderer URLLoaderThrottle.
"document-loaded"(page)signal
DOMContentLoaded for the main frame.
"destroy"(page)signal
Frame gone.

Classes · renderer

dom_document

luakitrenderer
doc.body -> dom_element
The body element.
doc.window -> {scroll_x, scroll_y, inner_width, inner_height}
Viewport metrics.
doc:query(css) -> {dom_element...}
querySelectorAll as a Lua array.
doc:create_element(tag[, attrs[, inner_text]]) -> dom_element
Create a detached element.
doc:element_from_point(x, y) -> dom_element | nil
Hit test.
"destroy"signal
Document unloaded.

Classes · renderer

dom_element

luakitrenderer

A main-world V8 node handle. Identity is preserved: the same node yields the same Lua object within a page, so a == b and using elements as table keys both work. All operations are synchronous.

el.tag_name · el.text_content · el.inner_html · el.child_count · el.src · el.href · el.value · el.checked · el.type
Common properties (read/write where the DOM allows).
el.parent · el.first_child · el.last_child · el.prev_sibling · el.next_sibling · el.document · el.owner_document
Tree navigation.
el.attr.<name> · el.style.<name>
Attribute and inline-style proxies: el.attr.target = "_self", el.style.display = "none".
el.rect -> {left, top, width, height} · el:client_rects()
Geometry.
el:query(css) -> {dom_element...}
Scoped querySelectorAll.
el:append(child) · el:remove()
Mutation.
el:click() · el:focus() · el:submit()
Actions.
el:add_event_listener(type, capture, fn(el, event)) · remove_event_listener(type, capture, fn)
DOM events into Lua. event is a JS object proxy (read properties, call methods such as event:preventDefault()).
"destroy"signal
Emitted for every element of a page when the page unloads.

Classes · renderer

ipc_channel (web module side)

luakitrenderer
ipc_channel(name) -> ui
The renderer end of the channel that require_web_module(name) returned in the browser.
ui:emit_signal(name, ...)
Send to the browser process. Pass page.id first if the receiver needs to know which tab.
ui:add_signal(name, fn(ch, page, ...))signal
Receive from the browser. page is the targeted page, or nil for a broadcast — identical to luakit.
That's the whole surface. If something you need is missing, lemurx.help() on a device prints the catalog of the build you are running, and every module above has a matching test harness in the tree.