Single-file apps only (current OS). The Store installs one
.luaper app to/apps/<id>/main.lua(seepocketos/store.luastore.install+shell.run_user_app). A/pocketos/apps/<app>/...multi-file layout is reserved for the future and does not install today — write agents self-contained code,requireonly OS modules, persist viapocketos.config.
PocketOS apps are single Lua files distributed through the App Store. When launched, the OS runs your file with a sandboxed environment (the SDK) already set up — you just build a UI inside the container you’re given.
The basics
-- every app gets these globals (and more):
-- pocketos - OS services + your container + your config
-- core - graphics core (cpair, ALIGN, ...)
-- Div, TextBox, PushButton, ... - element constructors
-- colors, keys, util, fs, http, textutils, os, peripheral, ...
local c = pocketos.container -- Div filling the app area (23 x 18)
local cpair = core.cpair
TextBox{parent=c, y=1, text="My App", alignment=core.ALIGN.CENTER,
fg_bg=cpair(colors.cyan, colors._INHERIT)}
PushButton{parent=c, y=3, text="Say hi",
fg_bg=cpair(colors.green, colors.black),
active_fg_bg=cpair(colors.white, colors.black),
callback=function ()
print("hi") -- goes to the OS console if you exit the OS
end}
That’s a complete app. No os.pullEvent loop needed — the OS owns the event
loop and dispatches to your elements.
Layout
- The app container is a Div at x=1, y=1 with width=23, height=18.
ypositions 1..18 are yours. (y=1 is a good title line.)- Sidebar is managed by the OS; ESC always returns to the home grid.
pocketos — OS services
| Field | Meaning |
|---|---|
pocketos.container | Your root Div (build UI here) |
pocketos.app_color | The color your manifest entry chose |
pocketos.config.load() | Your app’s config table (/apps/<id>/config/config.json) |
pocketos.config.save(tbl) | Save it (atomic write) |
pocketos.config.read_file(name) / write_file(name, data) | Arbitrary files in your config folder |
pocketos.go_home() | Return to the home grid |
pocketos.set_handlers{...} | Register event handlers (see below) |
pocketos.transmit(channel, payload) | Send via the pocket’s modem |
pocketos.util / pocketos.tcd / pocketos.psil | util fns, timer callbacks, pub/sub |
pocketos.os_config | Global OS settings (read via os_config.get("UserName")) |
Config
Each app gets its own persistent folder at /apps/<id>/config/:
local cfg = pocketos.config.load() -- table (empty on first run)
cfg.high_score = cfg.high_score or 0
cfg.high_score = cfg.high_score + 1
pocketos.config.save(cfg) -- persists as JSON
Events
Apps never block. To react to OS events while open:
pocketos.set_handlers{
-- called for every modem_message while your app is open
on_modem = function (channel, payload, distance) end,
-- called for every modem_message ALWAYS -- even while your app is
-- closed or another app is open (offline inbox pattern). used by Chat.
background_modem = function (channel, payload, distance) end,
}
For timers use pocketos.tcd.dispatch(seconds, fn) — it plays nice with the
UI loop, unlike os.sleep (which you should avoid in callbacks).
If you need a blocking request/response protocol (see store-apps/bank.lua
for a full example): send, then loop os.pullEvent() until the response or a
deadline — forward timer events to pocketos.tcd.handle(id), queue
unrelated events with os.queueEvent(...) when done, and arm a short
os.startTimer each iteration so you can’t hang forever.
Elements quick reference
All constructors take a table with parent, and usually x, y, width,
height, fg_bg (a core.cpair(fg, bg)), hidden:
Div— plain containerTextBox— text;alignment=core.ALIGN.CENTER,set_value(str), auto-wrapsPushButton—text,callback,active_fg_bg(pressed colors),min_widthCheckbox—label,box_fg_bg,callback(value)TextField— typed input;get_value(),set_value(),max_len,on_unfocus=fnNumberField— numeric input;default/min/max,allow_decimalNumericSpinbox— up/down arrows number picker (whole_num_precision,fractal_precision)TabBar—tabs={{name=..., color=...}},callback(index)Sidebar— usually OS-managed; you can use it for in-app tabsListBox— scrollable list: add child Divs, they stack;scroll_heightMultiPane— swap between panes:panes={a, b, c},set_value(i)AppMultiPane— like MultiPane with page dots + scroll/drag navigationRectangle— colored box / border:border=core.border(1, colors.gray)DataIndicator—label,format="%d",value,unit,set_valueHorizontalBar/VerticalBar— fill bars,set_value(0..1)IndicatorLight—label,colors=cpair(on_color, off_color)StateIndicator— state text blocks:states={{color=..., text=...}...}SignalBar— signal strength iconWaiting— loading spinner animation
Common element methods: get_value(), set_value(), show(), hide(),
enable(), disable(), remove_all() (containers), get_child(id).
colors._INHERIT as fg or bg means “use parent’s color” — handy for text on
any background.
Talking to the bank backend (for shop-style apps)
There is no built-in bank client — apps that need banking ship their own
~30-line protocol client (see store-apps/bank.lua and store-apps/shop.lua
for complete, copy-pasteable examples).
Protocol (JSON over the BankChannel from OS config):
request: { t = "pkt.bank.req/1", id = <n>, src = <your computer id>, op = "...", args = {...} }
response: { t = "pkt.bank.res/1", id = <n>, ok = <bool>, data = <table>, err = <string> }
Useful ops for shops:
LINK_PAY { <computer id>, <shop account uuid>, <amount in spurs> }— charge the user’s linked sub-account (the one they enrolled in the Bank app). Numismatics enforces their spend limit; errors come back aserr(“Insufficient funds”, limit reached, “pocket not linked”, …).LINK_AVAILABLE { <computer id> }— how much the user can still spend.ACCOUNTS {}— list all bank accounts (name the shop’s one, verify it).
The flow for a shop app: list items → user picks → LINK_PAY to the
shop’s account → deliver (mail, chest pickup, whatever). If payment fails,
show err.
Publishing to your store
- Host your
.luaanywhere HTTP-reachable (gist raw URL is perfect). - Add it to your manifest JSON (see
store-apps/manifest.json):{ "apps": [ { "id": "myapp", "name": "My App", "icon": "M", "color": "lime", "version": "1.0.0", "description": "Does a thing", "url": "https://gist.githubusercontent.com/.../myapp.lua" } ] }idmust be unique (it names/apps/<id>/),coloris one of red/orange/yellow/lime/green/cyan/lightblue/blue/purple/magenta/pink/ white/gray/brown. urlcan be relative:"apps/myapp.lua"resolves against the manifest’s own directory. Move hosts by editing one config line, or keep the manifest and apps together (likestore-apps/does).- Set the manifest URL in Config (or during OOBE), open Store, install.
Versions & rollback: when you publish a new version, the Store shows
Update for installed users. The old code is kept at /apps/<id>/prev.lua,
so a bad release can be reverted with the Store’s Rollback button. Updates
are always user-initiated — nothing auto-installs.
Rules of the sandbox
- Your code runs once at app open; keep references to elements to update them.
- Don’t loop forever in callbacks (UI freezes). Use
tcd.dispatchinstead. os.pullEventworks but blocks the UI — avoid it; useset_handlers.- Errors in your app body are caught: the container shows the error text instead of crashing the OS.
A complete example
See store-apps/notes.lua — a small notes app using config persistence,
ListBox, TextField, and PushButtons. For networking patterns, read
store-apps/bank.lua (blocking request/response) and store-apps/chat.lua
(background inbox + live updates).