PocketOS

PocketOS App Development Guide

Single-file apps only (current OS). The Store installs one .lua per app to /apps/<id>/main.lua (see pocketos/store.lua store.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, require only OS modules, persist via pocketos.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.
  • y positions 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

FieldMeaning
pocketos.containerYour root Div (build UI here)
pocketos.app_colorThe 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.psilutil fns, timer callbacks, pub/sub
pocketos.os_configGlobal 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 container
  • TextBox — text; alignment=core.ALIGN.CENTER, set_value(str), auto-wraps
  • PushButtontext, callback, active_fg_bg (pressed colors), min_width
  • Checkboxlabel, box_fg_bg, callback(value)
  • TextField — typed input; get_value(), set_value(), max_len, on_unfocus=fn
  • NumberField — numeric input; default/min/max, allow_decimal
  • NumericSpinbox — up/down arrows number picker (whole_num_precision, fractal_precision)
  • TabBartabs={{name=..., color=...}}, callback(index)
  • Sidebar — usually OS-managed; you can use it for in-app tabs
  • ListBox — scrollable list: add child Divs, they stack; scroll_height
  • MultiPane — swap between panes: panes={a, b, c}, set_value(i)
  • AppMultiPane — like MultiPane with page dots + scroll/drag navigation
  • Rectangle — colored box / border: border=core.border(1, colors.gray)
  • DataIndicatorlabel, format="%d", value, unit, set_value
  • HorizontalBar / VerticalBar — fill bars, set_value(0..1)
  • IndicatorLightlabel, colors=cpair(on_color, off_color)
  • StateIndicator — state text blocks: states={{color=..., text=...}...}
  • SignalBar — signal strength icon
  • Waiting — 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 as err (“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

  1. Host your .lua anywhere HTTP-reachable (gist raw URL is perfect).
  2. 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" }
    ] }
    id must be unique (it names /apps/<id>/), color is one of red/orange/yellow/lime/green/cyan/lightblue/blue/purple/magenta/pink/ white/gray/brown.
  3. url can 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 (like store-apps/ does).
  4. 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.dispatch instead.
  • os.pullEvent works but blocks the UI — avoid it; use set_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).