Lua lets RETUI grow without turning it into a plugin free-for-all.
I wanted people to build small things that feel native to the launcher. I did not want every small idea to begin with Android Studio, or every reactive panel to require a Termux setup.
There was a gap between XML and a shell
Configuration files are good at changing values. Termux is good at real shell work. Neither is the natural home for a counter, a tiny habit tracker, a context-aware suggestion, or a panel with a few buttons and persistent state.
Lua fills that gap. RETUI owns the runtime, storage, renderer, permissions, and editor. The script describes what it wants to show and how it should react. It does not get handed the entire Android application.
module -new lua counter module -edit counter module -check counter module -show counter
The older `widget` command still works for compatibility, but Lua modules now live in the same module system as built-ins and Termux-backed panels.
One script can choose the right amount of space
The metadata at the top tells RETUI what kind of surface the script belongs to. This is a small decision with a big effect: not every useful script needs to occupy the dock.
-- type = "module"A glanceable panel that can live in the module dock, render state, and expose focused actions.
-- type = "suggest"A script that contributes suggestion chips while you type, without becoming a visible module.
-- type = "app"A larger terminal-styled surface with its own input loop, native layouts, actions, and local commands.
`-- retui = "1"` declares the API version the script targets. Missing version metadata still falls back to version 1 for compatibility, but writing it down makes the script's expectations honest.
A module can stay very small
The starter shape is intentionally readable. Load some state, render it, and respond when an action arrives.
-- name = "Counter"
-- type = "module"
-- retui = "1"
local prefs = require "prefs"
function on_resume()
prefs.count = prefs.count or 0
ui:set_title("Counter")
ui:show_text("Count: " .. prefs.count)
ui:suggest_action("+1", "increase")
end
function on_action(action)
if action == "increase" then prefs.count = prefs.count + 1 end
on_resume()
end`prefs` survives launcher restarts and backup or restore, so small settings and state do not need a database. Module-owned text files are also available when a script genuinely needs them, but they remain inside that script's own folder.
Native does not have to mean inflexible
A script can show text, key-value rows, tables, progress, buttons, dividers, and small row or column layouts. Plain text follows the active launcher font. Explicit `pre`, `ascii`, and `code` blocks use monospace when alignment actually matters.
There are two places for actions. Module buttons remain visible inside the panel. Suggestion actions keep the panel clean and use the launcher's action strip. I like that distinction because a tiny display should not become a wall of controls just because the API can draw them.
ui:show_action(label, value)Put a native action button in the active module or app surface.
ui:suggest_command(label, command)Place an executable command in the module action strip.
ui:render({...})Describe a small native layout when plain terminal text is not enough.
Opening a module renders it again, and tapping its title forces a refresh. A script does not need to waste its first action chip on a permanent refresh button.
Lua apps are for ideas that need a room
A focused Lua app gets a larger surface and receives ordinary submitted text through `on_input(text)`. RETUI still owns the frame, editor, sandbox, and storage; the script owns the small experience inside it.
lua -new app scratch lua -edit scratch lua -check scratch lua -app scratch
Inside the app, colon commands such as `:help`, `:refresh`, `:config`, `:edit`, `:clear`, and `:close` remain under RETUI's control. That gives every Lua app a predictable escape route and maintenance surface.
Permission should be visible in the file
A script that asks for a sensitive RETUI capability has to say so in its own header.
-- permissions = "network,clipboard"
The supported declarations are `network`, `clipboard`, `vibrate`, `local-files`, `active-tick`, `notifications`, `apps`, `intents`, and `shortcuts`. Approval is tied to the current script and permission set. Change the code or add a capability and RETUI asks again.
module -info counter module -check counter module -approve counter
These are script-level permissions. They do not add new Android manifest permissions. Sensitive APIs are also checked when called, not merely guessed from a scan of the source.
A broken script should be recoverable
Custom code will fail sometimes. A short runtime timeout turns accidental infinite loops into Lua errors instead of letting them hang the launcher forever. When a module fails, RETUI exposes routes to edit it, check it, copy the saved error, or disable it without deleting the work.
module -copy-error counter module -disable counter module -edit counter module -enable counter
`module -export <id>` copies a shareable package to the clipboard. RETUI does not silently install whatever happens to be there. A received script goes through the editor so you can see it, name it, and approve what it declares.
You do not need to memorise the API
The runtime includes JSON, dates, formatting, strings, current launcher colours, timers, network callbacks, app and intent helpers, local settings, and small configuration forms. That is a toolbox, not an onboarding checklist.
Start from something tiny. Use the browser-based Lua module builder if filling in a form is a better first step than writing the script by hand. Then open the generated Lua in RETUI's editor and make it yours.
Lua is not there to disguise a second operating system inside the launcher. It is there so one personal idea can become one small RETUI surface without asking permission from an app-store-shaped world first.