Lua · Quickstart
Nothing to an app, in one sitting.
You need an X4 running Snail OS, its microSD card, and a text editor. No toolchain, no account, no cable to the device.
Prefer to delegate? Tell your agent — a coding agent with the skill and the linter writes the file for you. This page is the same journey by hand.
1 · Get a file onto the card§
Two ways. Put the card in a computer and create /apps/hello.lua — make the /apps folder if it is not there. Or leave the card in the device and use the Drive app, which serves the card over Wi-Fi.
A third way needs no editor: open the Store app on the device and install an app from this site, then modify it. The published apps are written to be copied — their sources are here.
--!name Hello
--!icon book
local n = 0
function start()
snail.hint("OK counts BACK menu")
end
function key(k)
if k == "ok" then n = n + 1 end
end
function draw()
snail.center(true)
snail.title("Hello")
snail.gap()
snail.text("Pressed " .. n .. " times.")
end
2 · Open it§
Put the card back and walk to the launcher. The scan runs on every launcher entry, so the app is already listed — Hello, with the book icon. Open it, press OK a few times, press BACK.
That loop is the whole development cycle: edit the file, re-enter the launcher, open the app. The file is loaded fresh on every open.
3 · Break it, on purpose§
Delete the end on the last line and open the app again. The error appears on the device, naming the line. A runtime error does the same and unwinds to the engine, which keeps the OS running: a broken card app is a message on the screen rather than a dead device.
4 · Keep your own cursor§
The engine draws no cursor for you. snail.row() takes the selection as an argument, so the app decides which row is highlighted — and an app that owns its cursor can also filter its list, reorder it, or put two lists on one screen.
--!name Tally
--!icon chart
local NAMES = {"coffee", "water", "walk"}
local counts, cursor = {0, 0, 0}, 1
function start()
snail.hint("OK add UP/DOWN pick BACK menu")
end
function key(k)
if k == "up" and cursor > 1 then cursor = cursor - 1
elseif k == "down" and cursor < #NAMES then cursor = cursor + 1
elseif k == "ok" then counts[cursor] = counts[cursor] + 1 end
end
function draw()
snail.title("Today")
snail.gap()
for i, n in ipairs(NAMES) do
snail.row(n .. " " .. counts[i], i == cursor)
end
snail.rule()
snail.small("OK adds one to " .. NAMES[cursor])
end
Note what draw() does not do: it names no coordinate. An app appends to a display list and the engine lays it out, which is why a card app cannot paint over the header, escape the content band, or land a line half off the panel.
5 · Run it before the card does§
The linter plays the app against a stand-in for the device using the firmware's own copy of Lua. Three passes: 4,000 random-key frames with nothing loaded, the same 4,000 paired with a document staged, then 30,000 steps checking what the app saves. It counts every byte the interpreter allocates and fails an app whose peak crosses 70% of the 48 KB unpaired or 85% paired.
tools/lua-lint tally.lua
source: 597 B of 24576 (2%)
tally.lua: 4000 frames, 28001 draw calls, 0 ticks, no error
one frame draws: gap=1 row=3 rule=1 small=1 title=1
memory (unpaired): peak 15149 B of 49152 (31%), held 11495 B,
largest block 1024 B, 0 collections forced
worst frame: 15149 B at step 1777 of 4000
tally.lua: 4000 frames, 28001 draw calls, 0 ticks, no error
one frame draws: gap=1 row=3 rule=1 small=1 title=1
memory (paired): peak 15129 B of 49152 (31%), held 11495 B,
largest block 1024 B, 0 collections forced
worst frame: 15129 B at step 2992 of 4000
30000 steps: no error, and this app saves nothingRemove the two cursor guards in key() and the first pass stops with ERROR: attempt to concatenate a nil value (field '?'), because UP walks the cursor past the first row and NAMES[cursor] is then nil. That is a fault you would otherwise find while holding the device.
6 · Where next§
The reference has the rest of the API: snail.save and snail.load for state that survives a reboot, snail.image for sprites, snail.qr, and the status band. Fetching covers apps that read something off the network.