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:
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. luakitpage, 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 processHow 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.
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
| Path | Trust | Loaded |
|---|---|---|
/data/data/<pkg>/files/lua/*.lua | local · full power | At startup, sorted by file name |
/data/data/<pkg>/files/lua/ugc/*.lua | UGC · sandboxed | After local scripts, one lua_State each |
files/luakit/ (lib/ config/ data/) | luakit install | Seeded from the APK; config/ is yours and never overwritten |
apk assets/lua/init.lua | runtime | First. 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
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
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, tagLemurX. luakit'smsg.*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, forui.styleand 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). Uselemurx.timer.after / every. - A synchronous
lemurx.http.fetchblocks the Lua thread up to 20 s (UGC) / 120 s (local) — every script's callbacks queue behind it. Use the callback form orlemurx.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
pcallwhen 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/*.lua | UGC files/lua/ugc/*.lua | |
|---|---|---|
| Lua state | One shared main lua_State | One lua_State per file |
| Standard library | Everything: io os debug package require dofile loadfile load (bytecode allowed) | base table string math utf8 coroutine; load text only; no dofile/loadfile/require |
lemurx.* | All | All except the local only entries |
| luakit layer | Yes (kernel runs in the main state) | No |
require | files/lua/?.lua files/lua/?/init.lua files/lua/lib/?.lua + luakit config/ lib/ | — |
lemurx.isPrivileged() | true | false |
lemurx.scriptName() | "" | "ugc/xxx.lua" |
fs jail | files/lua | files/lua/ugc |
http.fetch limits | 8 MB · 120 s · any scheme | 2 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.
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.
-- 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)
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:
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
| luakit | LemurX |
|---|---|
luakit.install_path / install_paths.install_dir | files/luakit (lib/ inside) |
luakit.config_dir / xdg.config_dir | files/luakit/config — yours; web modules resolve here first |
luakit.data_dir | files/luakit/data — sqlite files, sessions |
luakit.cache_dir | cache/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 anotebook/boxmoves the tab's content into the widget's rectangle.notebook:switch()selects the native tab and vice versa. - navigation-request is a NavigationThrottle. Returning
falsecancels 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. regexis RE2, not PCRE: no back-references or look-behind.luakit.spawnruns a real process (/system/bin/shis available);unique.is_running()is alwaysfalse(one process per package).- Web modules cannot touch the file system (Chromium's renderer sandbox):
lfsin 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.loadersfor luakit's 5.1-eralib/.
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_elementwraps 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'ssend-requestsignal fires for every subresource before it leaves the process — return a string to redirect,falseto block, edit theheaderstable in place. Backed by aURLLoaderThrottle. - Native functions on
window.luakit.register_function(uri_pattern, name, fn)makeswindow[name](...)return a Promise resolved from Lua. - Two-way IPC.
ipc_channelsignals in both directions, with targeting to a single page or broadcast to every renderer.
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)
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/.
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 · menuLua 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.
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.
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(...).
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.
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.
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.
Pages
Recipes
Per-site skin
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
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
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
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
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
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
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/anythingPages
Pitfalls & limits
net.addRuleand subresources. Browser-side rules apply to navigations (document / sub_frame) and browser-initiated requests. For per-request control of page subresources use the renderer'ssend-requestsignal, which sees every img/script/xhr/fetch.tabs.hideon the current tab blanks the screen —selectanother 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.bottomare PopupWindows and are rebuilt on rotation;ui.updateis much cheaper than re-render.- UI calls block. Don't
ui.findforty times in one callback —dumponce and filter in Lua. evalreturns JSON-serialisable values only. DOM nodes come back asnil; 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/andrequireit. - Local scripts have all of
osandio.os.exit()kills the browser process;io.opencan 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.storageor a local bridge script. - The bridge moves data, not functions. An imported function cannot take a Lua callback; poll
storageor subscribe to events instead. - luakit
inittiming.webview.add_signal("init")only fires for webviews created after registration. For tabs that already exist, walklemurx.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
- Place the file in
files/lua/ugc/. Privilege drop and state isolation are automatic; you don't detect anything yourself. lemurx.ugc.scan(source)must return an empty table before listing.load(string)works (text only);dofile/loadfile/require/io/osdo not exist.- Anything that waits for the network uses
http.fetch(url, opts, cb)orlemurx.async/await. - 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 isnil. - No phishing-style full-screen with a hidden URL bar. Review rejects it.
- All outbound requests go through
lemurx.http.fetch; list the domains in the file header comment. - Provide a
reset: on uninstall the script shouldtheme.reset()+ui.unmount("*")+net.clearRules(). - Version in the first line:
-- myskin v1.0.0.
Modules · lemurx.*
lemurx core
browserUGC oklemurx.version- Runtime version string.
lemurx.log(...)- Write to logcat (tag
LemurX).printis an alias. Arguments aretostringed and tab-joined. lemurx.toast(msg)- System toast.
lemurx.help([module])- Print the API catalog (
lemurx.catalog) or one module of it. lemurx.isPrivileged() -> booleantruein local scripts,falsein 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 okCoroutine sugar so waiting on the network never blocks the shared Lua thread.
lemurx.async(fn)- Run
fnas a coroutine task. Errors inside are logged with a traceback, not swallowed. lemurx.await(fn, ...) -> results- Inside
asynconly. Callsfn(..., 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 outsideasync. lemurx.sleep(ms)- Inside
asynconly. Yields formsmilliseconds.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
browserThe 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.
nilif nothing is exposed under that name — always handle it. lemurx.exposed() -> {names}- Currently exposed names.
Modules · lemurx.*
lemurx.tabs
browserUGC okTab 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.
hiddencreates it without showing it (useful for scraping). lemurx.tabs.close(id)·select(id)·hide(id)·show(id)·freeze(id)- Tab strip operations.
freezediscards 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;
gotakes an absolute entry index,offseta 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;datais 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.agentuses to unify web and native coordinates. lemurx.tabs.on(event, fn)event"started" | "document" | "loaded" | "created" | "closed" | "selected".fn(ev)receives the tab table plusevent,eventUrl. See Events.
Modules · lemurx.*
lemurx.net
browserUGC okDeclarative 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 okRequests 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
cbthe call returns a request id immediately and the response arrives on the Lua thread later — prefer this form, or uselemurx.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 okJailed 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() -> pathfiles/lualocally,files/lua/ugcfor 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 oklemurx.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'ssqlite3{}. 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 okThe 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.setto 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 okA 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 (anilvalue 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 withextra. lemurx.theme.presets.<name>- The preset table — add your own.
Modules · lemurx.*
lemurx.ui.render · h · update · unmount
browserUGC okDeclarative 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())
| Slot | Where |
|---|---|
toolbar.start / toolbar.end | Left / right button strip of the top bar (150 dp) |
bottom.start / bottom.bar | First cell / end of the bottom bar |
page.top / page.bottom | Full-width strip directly above / below the web content |
page.center / page.float | Centered / 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
| Group | Properties |
|---|---|
| Common | id · 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 / image | icon ("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) |
| edit | hint 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 |
| switch | checked; onChange gets ev.checked |
| progress | value 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 okThe 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=truedisables 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=falselets 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 oklemurx.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;
rendersupersedes 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)withev.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, yofdump/findnodes. 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 okCoordinates 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); calllemurx.chrome.back()to pass it through. lemurx.input.interceptBack(on)falsereturns the key to the system.
Modules · lemurx.*
lemurx.intent
browserUGC restrictedlemurx.intent.startActivity({action, url, package, extras})- Launch an Android Intent. UGC may only use
VIEW SEND SENDTO WEB_SEARCH MAINandlemurx.*actions. lemurx.intent.sendBroadcast({action, extras})- Broadcast an Intent (same UGC restriction).
Modules · lemurx.*
Privileged: cookie · history · downloads · perm · prefs · data · features
browserlocal onlylemurx.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 onlyThe 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 onlyThin wrappers over cdp.send, defined in init.lua. tab is a tab id.
lemurx.v8.eval(tab, expr[, opts]) -> value, rawRuntime.evaluatewith the value unwrapped.lemurx.v8.call(tab, fnSource, argsJson) -> valueRuntime.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.getMetricsas a name→value table. lemurx.page.navigate(tab, url)·reload(tab, ignoreCache)·stop(tab)·enablePaint(tab, on)·cookies(tab)·screenshot(tab, opts)Page.*;screenshotdelegates totabs.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 onlyThe 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=nilmeans 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
refor by screen coordinates, choosing the most reliable path per layer (web →input.taprelative to the web view, orel.click()withvia="js"; native →performClick, falling back to coordinates; system →performAction, falling back to a gesture). Actions:tap|click{ref|x,y,long,dx,dy,via}longPresstype|text{text,ref,clear,enter,append}swipe|scroll{dir,ref,distance,duration|x1,y1,x2,y2}key{key}backhomerecentsnotificationsnavigate{url}open{url}eval{js}launch{package|url}wait{ms}screenshotdone{result}. A bare string ("back") is accepted. lemurx.agent.run(policy[, opts])- The loop.
policy(obs, text, step)returns the next action table, ornil/{type="done"}to stop.opts:{maxSteps=20, settle=600, onStep=fn(step, obs, action, result), onDone=fn(out)}. Run it insidelemurx.async; called outside a coroutine the result arrives viaonDone. 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
actcalls need noobs; 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.
ensureopens 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"|…)performActionon 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])dispatchGesturein 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 oklemurx.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 onlyThe 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 originatingwebview. See Custom schemes. luakit.spawn(cmd[, callback(exit_reason, exit_status)]) -> pid- Run a process asynchronously.
cmdis shell-split (a sufficient subset ofg_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 (
fnreturningtruere-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
luakitbrowserrenderersoup.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
luakitbrowserOne 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
luakitbrowserstylesheet{ 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
luakitbrowsertimer{ 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
luakitbrowserregex{ 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
luakitbrowserrendererLua 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
luakitbrowserxdg.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
luakitbrowserrenderermsg.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)signalmsg.add_signal("log", fn)— howlog_chromebuildsluakit://log.
Modules · luakit
unique
luakitbrowserunique.new(name)·is_running() -> false·send_message(str)- Single-instance control. Android runs one process per package, so
is_runningis alwaysfalse; external entry points (Intents opening links) are replayed as a"message"(msg, screen)signal sounique_instance.luaworks unchanged.
Modules · luakit
lfs
luakitbrowserlfs.attributes(path[, name])·symlinkattributes·dir(path)·mkdir·rmdir·chdir·currentdir·touch- The LuaFileSystem subset luakit's
lib/uses, over POSIXstat/opendir.dirreturns an iterator including.and... In the renderer,lfsis a sandbox-consistent empty implementation.
Modules · luakit
ipc_channel · require_web_module
luakitbrowserBrowser ↔ 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.luainto every renderer (now and future). Resolved fromfiles/luakit/config/, thenfiles/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(awebviewor its id). Arguments are JSON-marshalled. ch:add_signal(name, fn(ch, ...))signal- Receive from renderers. luakit convention: web modules pass
page.idas the first argument when the sender matters.
Modules · luakit
Custom schemes · request
luakitbrowserAfter 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
luakitbrowserrendererEvery luakit object (widget, download, timer, page, dom_element, …) follows the same protocol:
type(obj)returns the class name ("widget","page", …).pairsworks 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 whatemit_signalreturns (luakit'sLUA_MULTRETsemantics) — this is hownavigation-requestreturningfalsevetoes. - Classes have module-level signals too:
widget.add_signal("create", fn(w)),luakit.add_signal(...). - After
destroyonlyobj.is_alivemay be read; anything else raises.
Classes · luakit
widget
luakitbrowserwidget{ 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.luainitialise theirs. "key-press"(w, mods, key)signal- Return
trueto consume.modsis a table of"Control" "Shift" "Mod1"…;keyis 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
luakitbrowserA 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.
certificateis 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. Seestylesheet. 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
callbackit 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)signalstatus:"provisional" | "redirected" | "committed" | "first-visual" | "finished" | "failed"."navigation-request"(view, uri, reason) -> falsesignal- Return
falseto cancel. Implemented as aNavigationThrottle: nothing loads, no history entry.reasonmirrors luakit ("link-clicked","form-submitted","back-forward","reload","form-resubmitted","other"). "new-window-decision"(view, uri, reason) -> falsesignal- Popups /
target=_blank. Returnfalseto block; otherwise the new tab is wrapped and itsparent_viewset. "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
luakitbrowserA 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;
fullscreenmaps to immersive mode. win:set_default_size(w, h)- Requested size in dp;
0means 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-presswhen no child consumed them.
Classes · luakit
widget:box (hbox / vbox)
luakitbrowserA LinearLayout.
box.homogeneous·box.spacing- Equal-size children; gap in dp.
box:pack(child[, {expand=, fill=, padding=, start=false}])- Append (or prepend with
start=falsesemantics ofpack_end).expandbecomes 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
luakitbrowserPages 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.luadraws 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
luakitbrowserlabel.text- Pango markup (
<span foreground='#fff' weight='bold'>,<b>,<i>,<u>,<tt>) translated to AndroidSpanned. label.fg·label.bg·label.font·label.padding·label.align·label.textwidth·label.selectable- Styling.
textwidthin characters (0 = unconstrained); ellipsised when exceeded.
Classes · luakit
widget:entry
luakitbrowserAn 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
trueto consume — luakit'smodes/bindsrely on this).
Classes · luakit
widget:eventbox
luakitbrowsereb.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
luakitbrowseroverlay.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)
luakitbrowserpaned: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
luakitbrowserscrolled.child·scrolled.scrollbars·scrolled.scroll -> {x, y, xmax, ymax}- A
ScrollView/HorizontalScrollViewchosen from the child's orientation; scroll position is read/write.
Classes · luakit
widget:image
luakitbrowserimage: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
luakitbrowserspinner:start()·spinner:stop()·spinner.started- Indeterminate
ProgressBar.
Classes · luakit
widget:drawing_area
luakitbrowserda:invalidate()·da.bg·da.min_size- A plain View you size and color; useful as a separator or a colored gauge.
Classes · luakit
download
luakitbrowserdownload{ 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.errorstatus ∈ "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)
luakitrendererAvailable 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
pagesignals 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 asdom_element); callresolve(value)orreject(err), possibly later. Errors thrown byfnreject 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
luakitrendererOne 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,
falseto block, nothing to allow. Mutations to theheaderstable are applied. Backed by a rendererURLLoaderThrottle. "document-loaded"(page)signal- DOMContentLoaded for the main frame.
"destroy"(page)signal- Frame gone.
Classes · renderer
dom_document
luakitrendererdoc.body -> dom_element- The body element.
doc.window -> {scroll_x, scroll_y, inner_width, inner_height}- Viewport metrics.
doc:query(css) -> {dom_element...}querySelectorAllas 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
luakitrendererA 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.
eventis a JS object proxy (read properties, call methods such asevent:preventDefault()). "destroy"signal- Emitted for every element of a page when the page unloads.
Classes · renderer
ipc_channel (web module side)
luakitrendereripc_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.idfirst if the receiver needs to know which tab. ui:add_signal(name, fn(ch, page, ...))signal- Receive from the browser.
pageis the targeted page, ornilfor a broadcast — identical to luakit.
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.