# luarocks — Lua rocks for OpenTTY

`luarocks` installs **rocks**: single-file Lua libraries that extend the
runtime (`string`, `math`, `graphics`, the kernel API, an HTTP server, …).
It mirrors how `pkg` handles applications, but the unit is a module under
`/lib/<name>.lua` instead of a program under `/bin/<name>`.

- `pkg` → `apps/<major>/sources.lua` → `/etc/sources` → `/bin`
- `luarocks` → `apps/<major>/rocks.lua` → `/etc/rocks` → `/lib`

Both catalogs are refreshed from the mirror (`luarocks update` /
`luarocks switch <version>`), and both are validated by
`tests/check_sources.py`.

---

## Rock format

A rock is one Lua file whose first lines carry the metadata:

```lua
-- rock: name=math
-- rock: version=1.1.0
-- rock: description=Numeric helpers missing from the runtime
-- rock: here=/lib/math.lua
-- rock: depends=string

local M = math or {}
...
return M
```

| Key | Required | Meaning |
|-----|----------|---------|
| `name` | yes | Rock name; the default is the file name without `.lua` |
| `version` | yes | Version string shown by `luarocks list`/`info` |
| `description` | recommended | One line for `list --available`/`search` |
| `here` | no | Install path; defaults to `/lib/<name>.lua` |
| `depends` | no | Comma-separated rock names; installed first |

Header parsing stops at the first line that is neither a comment nor blank, so
keep the `-- rock:` block at the top. A payload must **return** its value —
usually the table it extended — because `require` caches the result:

```lua
local math = require("math")
print(math.floor(2.7))
```

Payloads live in the versioned app catalog:

```
apps/1.19/lua/<rock>.lua     -- canonical source of each rock
apps/1.19/rocks.lua          -- generated catalog
src/etc/rocks                -- seed copy of the catalog (/etc/rocks)
src/lib/math.lua             -- seed copy of the math rock (/lib/math.lua)
```

Shipped rocks:

| Rock | What it adds |
|------|--------------|
| `math` | trig, logs, rounding, gcd/lcm, clamp, factorial |
| `string` | `format`, `rep`, `gsub`, `gmatch` and a pattern engine |
| `graphics` | Markdown/HTML viewers on top of the native LCDUI API |
| `kernel` | typed wrappers for the `/bin/init` payloads |
| `httpd` | HTTP/1.1 server with routing, query parsing and static files |
| `json` | `encode`/`decode`/`pretty`/`try_decode` with a null sentinel |
| `tablex` | `deepcopy`, `merge`, `map`/`filter`/`reduce`, `sortby`, `range` |
| `base64` | standard/URL-safe Base64 plus `hex`/`unhex`/`bytes` |
| `sha1` | SHA-1 and HMAC-SHA-1 (pure Lua, 32-bit arithmetic) |
| `sha256` | SHA-256 and HMAC-SHA-256 (pure Lua, 32-bit arithmetic) |
| `term` | ANSI colours, tables, progress bars, boxes, wrapping |
| `argparse` | options with short/long flags, types, defaults and `--help` |
| `log` | levelled loggers with timestamps, colours and sinks |
| `config` | INI parse/encode/load/save with dotted keys |
| `template` | Mustache-style variables, sections, comments, partials |
| `csv` | RFC 4180 parser/writer plus header/object helpers |
| `date` | format, parse, parts, add, diff and relative text |
| `re` | `findall`, `matchall`, `split`, `replace`, `escape` over `string` |
| `ui` | callback-based menus, prompts, confirms, alerts, progress, file picker |

`src/lib/math.lua` exists so a fresh device can `require("math")` before the
first `luarocks install`; `tests/check_sources.py` keeps it byte-identical to
the rock payload.

---

## Catalog format

`luarocks index` writes (and `luarocks update` downloads) a plain Lua table:

```lua
-- Generated by `luarocks index`. Do not edit by hand.
return {
    version = "1.19",
    server = "http://opentty.fun",
    rocks = {
        ["math"] = { version = "1.1.0", remote = "lua/math.lua",
                     here = "/lib/math.lua",
                     description = "Numeric helpers missing from the runtime",
                     depends = {} },
    }
}
```

- `version` — the app-catalog major; also the `apps/<version>/` path segment.
- `server` — mirror base; the payload URL is
  `<server>/apps/<version>/<remote>`.
- `remote` — path relative to `apps/<version>/`.
- `here` — absolute install path on the device.
- `depends` — rock names resolved before this one.

The catalog is generated, never hand-edited: `luarocks index` reads the
`-- rock:` headers, so the header is the single source of truth.

---

## Commands

```
luarocks install <rock>...       install rocks (and their dependencies)
luarocks remove <rock>...        delete installed rocks
luarocks list [--available]      installed rocks, or the catalog
luarocks search <term>           search name + description
luarocks info <rock>             show metadata and install state
luarocks update                  refresh /etc/rocks from the mirror
luarocks switch <version>        point /etc/rocks at another catalog version
luarocks index <dir> [out] [prefix]
                                 build a catalog from `-- rock:` headers
luarocks make <file> [dir]       copy a payload into a catalog directory
luarocks version                 version and current catalog
```

`install`, `remove`, `update` and `switch` require root (`os.getuid() == 0`).
Installed state is kept in `/etc/rocks.installed`; `remove` deletes the file
listed there, so a rock installed by hand is not touched.

---

## Publishing a rock

1. Add the payload under `apps/<major>/lua/<name>.lua` with the `-- rock:`
   header (and `-- rock: depends=` when it needs another rock).
2. Regenerate the catalog:

   ```sh
   # io.dirs only lists /tmp, /mnt/<sub> and the global mounts, so index a
   # staged copy instead of apps/<major>/lua directly.
   cp apps/1.19/lua/*.lua data/mnt/opentty/rocks/
   OPENTTY_JVM_OPTS="-Dopentty.headless=1" \
       bash pc/run.sh --user opentty --pass opentty --smoke -- \
       /tmp/opencode/rocks-index.lua
   ```

   where the script runs
   `luarocks index /mnt/opentty/rocks /mnt/opentty/rocks.lua lua/`.

3. Copy the result to both `apps/<major>/rocks.lua` and `src/etc/rocks`, then
   run `python3 tests/check_sources.py` (it verifies the payload exists, the
   header matches the catalog, dependencies resolve, and the seed copy is in
   sync).

---

## Tests

| Suite | What it checks |
|-------|----------------|
| `tests/check_sources.py` | rock payloads exist, header ↔ catalog agree, dependencies resolve, `/etc/rocks` and `/lib/math.lua` seeds match |
| `tests/lua/run_modules.sh` | every payload loads on the real interpreter, extends instead of replacing the native tables, and exposes its helpers |
| `tests/check_syntax.sh` | every payload parses as plain Lua |

`tests/lua/run_modules.sh` stages `apps/1.19/lua/*.lua` under
`data/mnt/opentty/rocks/` and loads each file the way `require` does on-device
(the OpenTTY `load(code)` compiles **and runs** the chunk).

---

## Library roadmap

Ideas for the next rocks, in rough order of usefulness. Each one is a single
file that extends the runtime and returns its table.

| Rock | What it adds | Notes |
|------|--------------|-------|
| `semver` | `parse`, `compare`, `satisfies` | for `pkg`/`luarocks` version checks |
| `path` | `join`, `basename`, `dirname`, `normalize`, `ext` | the same string surgery is repeated in many apps |
| `test` | tiny `describe`/`it`/`assert` runner writing a report | would let apps ship their own suites like `tests/lua` |
| `queue` | FIFO/LIFO, priority queue, ring buffer | for schedulers and background workers |
| `crypto` | `xor`, `rot13`, `random_bytes`, `crc32` | small utilities; real ciphers need more than pure Lua can afford |
| `net` | HTTP client helpers: `get`, `post`, query building, headers | wraps `socket.http` with a JSON/urlencode-friendly API |

Guidelines for new rocks:

- extend the existing global (`local M = string or {}`) instead of replacing it;
- keep the file self-contained: no `dofile`, no `require` of a path;
- avoid `string.format`/`string.rep`/`string.gsub` unless the `string` rock is a
  declared dependency;
- remember the runtime throws on `n / 0` (no `inf`/`nan` literals) and has no
  exponent literals (`1e10`);
- add a `tests/lua/check_modules.lua` case for every new rock.
