Snail OS · Lua

An app is a file on the card.

Put a .lua file in /apps on the SD card and the launcher lists it with its own name and icon. Delete it and it is gone. No build, no cable, no reflash.

--!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
A complete app. The two --! lines are what the launcher reads when it scans.

Three functions§

An app defines start(), which runs once when the app is opened, key(k), which runs on a button press, and draw(), which runs whenever the screen needs painting. Only draw() is required. The keys are up, down, left, right, ok and top; BACK never reaches the app, because it always leaves.

draw() must be a pure function of the app's state. It can run at any moment, and more than once for a single press. Decide things in key() and paint in draw().

Most apps are written by telling an agent§

Describe the app to Claude Code or Codex and hand it this platform: snailos.org/llms.txt is the whole reference in one plain-text fetch, and /dev/SKILL.md is the same thing as an installable skill. The device ships with a linter that plays the app against a stand-in for the device using the firmware's own copy of Lua, so the agent can grade its work before the file reaches the card. Tell your agent has the exact thing to paste. The pages below are the same material, for reading.

Why this can exist at all§

The X4's ESP32-C3 executes from flash through the MMU's instruction cache. The SD card sits behind SPI and is not in the address space, so no instruction can ever be fetched from it. That is why a compiled app must go into the firmware image; the restriction applies only to machine code.

A book is not machine code either, and the Reader opens books off the card all day. A card app is the same trick. The file on the card is source, and the interpreter that runs it is compiled into the firmware. The launcher scans /apps at boot and lists whatever the card carries.

There is a ceiling on how many it lists, and tools/sim_check.sh counts the .lua files against the compile-time constant, so a catalogue that outgrows it fails on a laptop instead of on a device.

Why Lua§

Two languages were written for this job before Lua was tried: a document format with no loops or assignment, and a bounded interpreter with literal loop counts and a call graph that could not recurse. Both existed because a real language was assumed not to fit on a chip with 64 KB of usable heap. That assumption was measured against the real firmware on 9 August 2026, one #include apart.

MeasurementLuaThe interpreter written to avoid it
Flash+114 KB
Peak RAM, 2048 at 400 moves23–31 KB43,472 B, whatever the game is
Largest single allocation1.5–2 KB10,240 B
Static RAM+4.4 KB

Lua ran the same game in half the RAM and asked the allocator for a block five times smaller. On this device contiguity fails before capacity, so the largest-allocation row is the one that decides whether an app runs at all. The whole argument for hand-rolling a language was memory. Both engines were deleted the same day, before either shipped.

The Store§

The device's Store app fetches snailos.org/apps/index.json and installs any listed app straight onto the card. The same files are on Get the apps, with their sources readable in the browser. An app you write is a text file; send it to someone and they can run it.

This section§

Next: Quickstart