Micromachee
A tiny 8-bit console that lives in your bar. 128×128, eight colours, thirty frames a second, and one Lua file per game.
Click the ▦ in your Omarchy bar, pick a cart off the shelf, play it with the
arrow keys, press escape, go back to work.
The whole machine
| Screen | 128 × 128 |
| Colours | 8 — black, navy, red, orange, yellow, green, blue, white |
| Buttons | left, right, up, down, O (z), X (x) |
| Speed | 30 fps |
| A game | one Lua file, 24 KB maximum |
That is the entire specification. There is no sprite editor, no map, no sound chip, no cartridge container — a game draws with rectangles, lines, circles, pixels and text, and that turns out to be plenty.
Writing a game
micromachee new "My Game" # a starting cart that already plays
micromachee check mygame.lua # does it hold up for a minute of play?
micromachee shot mygame.lua --frames 90 --hold 2 -o look.png
A cart looks like this, and this is the complete API:
-- title: My Game
-- author: you
-- about: one line about it -- 48 characters, and it is cut there
-- mega: no -- optional; keeps it out of Mega Micromachee
function _init() end -- once, at the start (optional)
function _update() end -- 30 times a second (optional)
function _draw() end -- 30 times a second (required)
cls(c) fill the screen
pset(x,y,c) pget(x,y) one pixel, written or read back
rect(x,y,w,h,c) rectb(…) filled box, outlined box
line(x0,y0,x1,y1,c)
circ(x,y,r,c) circb(…) filled circle, outlined circle
print(text,x,y,c) 3×5 font, 32 characters to a line
btn(i) btnp(i) held / pressed just now
t() seconds since the cart started
rnd(n) flr(n) mid(lo,v,hi)
score(n) tell the console your score
sfx(n) play one of eight sounds, 0-7
Lua's own math.*, table.* and string.* are all there too. Colours are
0–7, buttons 0–5, and everything clips at the screen edge so drawing
off-screen is safe and free.
Sound is eight fixed effects — blip, hit, boom, pickup, jump, hurt, win, lose —
and a cart picks one by index the way it picks a colour by index. It cannot
choose a frequency, because the console owns how it sounds in the same way a
theme owns how it looks. The bank is generated by the helper rather than
shipped as files, so nothing here carries audio and micromachee sounds
rewrites it. The console can be muted from a button on its own body, and the
helper then does not send the sound at all.
Only three rules: fit in 24 KB, parse as Lua, define _draw. Metadata is
optional comments. There is no magic header to forget on your first attempt.
Two things that are not obvious and save a lot of code:
pgetreads the framebuffer back, so you can do collision against what you drew last frame instead of keeping a parallel model of the world.tunnelis a cave flyer in sixty lines because of it.score(n)is fire-and-forget. The console keeps the records, per cart. A game cannot read, lower, or forge another game's high score.
It is built for agents to write
check and shot exist so that writing a game needs no bar, no compositor and
nobody watching. check loads the cart and plays it blind for 1,800 frames —
a full minute — mashing buttons, and reports the first frame that errors.
shot renders a frame to a PNG you can actually look at, with --hold so you
can photograph a game mid-play rather than only its title card.
That is the whole loop: write, check, look, fix. Three of the four carts here were written by an agent working exactly that way.
Mega Micromachee
The whole shelf, a few seconds at a time. Ten seconds of Pong; survive and you move on, fail and it costs a life. Every fifth round the clock loses a second and every game runs faster, down to five seconds at 2.5x — the cart you handled comfortably at round two is a scramble at round twenty.
It is the first entry on the shelf and there is no mega.lua: a cart cannot
load another cart, and should not be able to. The helper already loads carts and
runs frames, so the meta-game is a state machine wrapped around what it does
anyway.
Speed is more updates, not a bigger step. Running _update twice as often
makes a game faster while every cart's own arithmetic stays exactly as its
author wrote it. Scaling a delta would need every cart to have been written in
terms of one, and none of them were.
The one thing it needed from carts was a way to know you had failed — every
cart tracked that already, as alive or over or dead, and none of it was
visible from outside. So carts call lose() at the point they already knew.
The panel
Two states and almost no chrome: the shelf, and a console. Each cart on the shelf shows its cover; choosing one raises that cover full size with press X to start, so there is a beat to look at it and get your hands on the keys before anything moves.
The console has a d-pad and two buttons under the screen. They light when you
hold the key and they can be pressed with the mouse — both routes end at the
same button bitmask the cart reads, so there is only one notion of "held".
Each carries the key that works it, because a cart printing PRESS O has no
way to tell you that O is the z key.
Covers are drawn by the cart, in _cover(), on the same screen with the same
eight colours — see CLAUDE.md. A cart without one gets a frame of
itself being played.
Making a game from a sentence
On the shelf: + MAKE A GAME. Give it a name and a sentence, and a cart gets written, checked, and — if it does not run — fixed and checked again. What comes back is playable immediately; it sits as a draft until you keep it.
micromachee make "Space Rocks" "dodge falling rocks, faster over time"
micromachee revise space-rocks "make the ship smaller and add a shield"
micromachee publish space-rocks # onto your shelf
micromachee discard space-rocks
The strict rules are what make this work. A generated cart is never
trusted — it has to parse, load, and survive six hundred frames of button
mashing before it is written anywhere. When it fails, the console can say
exactly what went wrong (_update failed on frame 41: attempt to index a nil value) and that goes back to the model, so the next attempt is informed rather
than another guess. The rules are not a constraint on generation; they are the
thing that makes it reliable.
Drafts live in their own folder and the shelf looks there too, so a new game is
playable with no publishing step and nothing special anywhere in the player.
Keeping one is a file move. Nothing leaves your machine — sharing a cart
with anyone else is scripts/publish.sh, which is a separate, deliberate act.
It needs Anthropic credentials: ANTHROPIC_API_KEY, or an ant auth login
profile. Without either, the panel says so rather than failing quietly.
In a browser
web/ is the whole console again, in JavaScript: it fetches the published
catalog, runs the carts with real Lua 5.4 (wasmoon, the same version the helper
embeds) and draws to a canvas. No server and no build step — but wasmoon is not
vendored, so fetch it first and serve the folder:
cd web && npm install
Nothing binary is committed to this repository. wasmoon ships a WebAssembly
module and it is left in node_modules/ where you can see where it came from,
rather than checked in as an opaque blob.
The meta-game is there too, as web/mega.js — a port of helper/src/mega.rs.
A second implementation of anything is a liability unless something checks it, and "looks right" is not a check at 128x128:
cd web && npm install && node check.mjs
That runs every cart to the same frame in the browser console and in the real helper and compares all 16384 pixels. It reports the first coordinate that differs and both colour indexes, because that is what tells you which primitive is wrong. All seven carts are identical at frames 1, 90, 300, 600 and 900, with and without buttons held.
node megacheck.mjs does the same for the meta-game: it reads the constants
back out of mega.rs and walks both difficulty curves round by round, because a
ramp that quietly differs between the desktop and the site is exactly the drift
nobody notices until two people compare scores.
Nothing is copied by hand: web/console-data.js — the font, the palettes and
the catalog url — is generated from the Rust source by a test that fails if the
committed copy is stale.
Playing without installing
The bar widget is one front end. play is a protocol — one base64 PNG per line
on stdout, a button bitmask on stdin — so anything can be the screen:
micromachee list # what is on the shelf
micromachee tty rogue # play it, right here
tty runs the cart in this process and draws its framebuffer straight to the
terminal — no PNG, no base64, no second process. Pixels become half-blocks, two
to a character cell, so 128x128 lands in 128x64 of terminal. Give it a window at
least 128x68 for one cell per pixel; it will halve the resolution to fit a
smaller one and say so. Arrows or WASD, z and x, q to quit.
Raw mode and the window size come from stty, so this pulls in no crate — the
same reasoning that made the PNG encoder hand-written.
A terminal cannot report a key being released, so a press is held briefly and
then let go on a timer — btnp games are exact, and btn games feel right
because key-repeat keeps renewing the press.
Installing
./install.sh
ln -s "$PWD" ~/.config/omarchy/plugins/io.pixygon.micromachee
Then add the Micromachee widget to your bar. install.sh needs no root, and
it copies the shipped carts across as well as building the helper.
That copy matters: the shelf looks in ./carts and then in the data directory,
and the widget is started by Quickshell from wherever Quickshell happens to be,
which is never this repo. Without the carts in the data directory the widget
comes up with an empty shelf while micromachee tty in the repo works fine.
Carts live in ~/.local/share/omarchy-micromachee/carts/. Installing a game is
copying a .lua file there — there is no install step, no index to rebuild, no
registry to tell.
micromachee sync pulls the published shelf from a catalog on the internet.
Every entry carries a SHA-256 and sync checks it, so a cart that arrived
mangled is refused rather than saved. The catalog is treated as somebody
else's file throughout: an entry's id has to be a plain cart name, because an
id becomes a filename and .. or a leading / would let whoever serves the
catalog choose where on your disk a file lands; and both the catalog and each
cart are read with a hard byte ceiling, so an endpoint that never stops sending
is refused rather than buffered.
A cart's title, author and about are the one part of it that is shown
rather than run, and after a sync they may have come off the internet. They
are cleaned where they are read — angle brackets, control characters and the
invisible bidirectional overrides all go — because a Qt Text field defaults to
sniffing its content for markup, and a title of <img src="http://…"> would
otherwise have the bar fetch that URL the moment it drew. The panel pins those
fields to Text.PlainText as well; the cleaning is what protects the parts of
the bar this repository does not own. That is one way for a file to travel, not
how carts work — copying a file into that directory still does the same job.
A cart already on the shelf is never quietly replaced. It might be one you
wrote, or one of these that you changed, and a routine sync eating that would
be unforgivable. But the same checksum that catches a mangled download also says
whether your copy is still the published one, so sync now names the ones that
differ:
0 new, 13 already here, 0 failed
1 cart(s) here differ from the shelf: picross
`micromachee sync --update` replaces them and keeps your copies as .lua.bak
sync --update takes the shelf's version and writes yours to <id>.lua.bak
beside it, which the console ignores because it only reads .lua. Without that
line a machine that installed early sits on the old shelf indefinitely and the
only symptom is a cart with a name nobody uses any more.
Removing it
rm ~/.config/omarchy/plugins/io.pixygon.micromachee # the symlink
rm ~/.local/bin/omarchy-micromachee # the helper
rm -rf ~/.local/share/omarchy-micromachee # carts and drafts
rm -rf ~/.local/state/omarchy-micromachee # high scores, saves, settings
Remove the widget from your bar in Omarchy's plugin settings first. Nothing else on your system is touched — the installer never uses root and writes only to those paths.
What it does on your machine
Stated plainly, because a plugin runs unsandboxed:
-
It builds a helper binary, on your machine, from this source.
install.shcompileshelper/with cargo and installs it to~/.local/bin. Nothing is downloaded and executed, nothing is piped to a shell, and no binary is committed to this repository. Without cargo the install stops and tells you to install Rust.There used to be a fallback that downloaded a prebuilt helper and checked it against a SHA-256 committed here. That proved the bytes matched the ones we published; it did not prove those bytes were built from this source, because nothing tied the executable to the commit. A marketplace security review raised it and they were right — "verified" meant less than it looked like it meant. Restoring it would need a CI build with signed provenance that the installer actually checks, and until that exists the honest thing is not to ship a downloaded executable at all.
-
It runs Lua that you may not have written. That is the product: a cart is a program. Carts get
math,stringandtableand nothing else — noio, noos, norequire— plus a per-frame instruction budget and a memory ceiling so a bad one cannot hang your bar. It is a restricted interpreter in the helper's process, not a security boundary. Read a cart from a stranger before you run it. It is one file and at most 24K, which is the point. -
It reaches the network only when you ask.
syncfetches the published shelf over HTTPS.make/revisesend your prompt to the Anthropic API. Nothing else talks to anything. -
Credentials, if you use
make. It readsANTHROPIC_API_KEY, or a token from an existingant auth loginprofile. The key is never written anywhere and never printed. -
It writes only under your home.
~/.local/bin,~/.local/share/omarchy-micromacheeand~/.local/state/omarchy-micromachee. Reinstalling never overwrites a cart you already have.
Dependencies and licences
Micromachee is MIT (see LICENSE). What it builds on:
| mlua | MIT | Rust bindings for Lua |
| Lua 5.4 | MIT | vendored and built by mlua |
| serde_json | MIT / Apache-2.0 | the one line of JSON the bar reads |
| wasmoon | MIT | Lua 5.4 in WebAssembly, for the browser build only |
Everything else — the PNG encoder, SHA-256, the palettes, the font — is written
here rather than pulled in. curl is used for network fetches and stty for
the terminal player; both are expected to be present already.
Publishing
micromachee catalog # write catalog.json from carts/
scripts/publish.sh --dry-run # what would go up, and where
scripts/publish.sh # upload the carts and the catalog, then check
Shipping bumps the version, and both the catalog url and the web player's data
are keyed to it. scripts/release.sh does them in order and verifies each,
because either one missed leaves a release that looks fine and is broken in a
way only the next person to install it discovers:
pearl ship && scripts/release.sh
catalog.json is generated, never hand-edited: the hand-kept one went stale
the moment a cart was added, listing four of seven with byte counts for versions
that no longer existed. publish.sh regenerates it from the very carts it is
about to upload, so the two cannot disagree, uploads the catalog last so nothing
ever points at a half-published shelf, and then fetches back what is actually
served rather than trusting the upload's own reply.
What is in the box
| cart | |
|---|---|
snake |
eat, grow, do not bite yourself |
breakout |
the wall, the ball, the bat |
meteor |
fly, shoot, do not get hit |
tunnel |
fly the cave; collision is done by reading the screen |
How it is built
manifest.json what Omarchy reads
Panel.qml the bar button, the shelf, the screen — drawing only
Service.qml runs the helper; frames in, button bits out
helper/ the console itself, in Rust
console.rs framebuffer, palette, 3×5 font, every drawing primitive
png.rs a PNG encoder in ninety lines and no dependencies
vm.rs Lua, and the limits a stranger's code runs under
cart.rs the format, which is "a Lua file"
shelf.rs where carts live and how they arrive
The bar runs micromachee play <id> as a long-lived process. It prints one
base64 PNG per line and reads a button bitmask on stdin — so the game loop is
ordinary Rust that can be driven from a shell script, and the QML layer stays
thin enough to be obviously correct.
Frames are indexed PNGs at bit depth 4, which puts a full 128×128 frame in about 8 KB, and the deflate stream is uncompressed — a compressor would be more code than the encoder it lives in, to save a few percent on a local pipe.
What a cart can and cannot do
A cart gets math, string and table. io, os, package, require,
dofile and loadfile are not loaded, so a cart cannot open a file or run a
program — the first version did load them, and a test cart wrote to /tmp to
prove the point. There is also an instruction budget per frame, so an endless
loop ends the cart rather than freezing your bar, and a memory ceiling.
It is still a Lua interpreter in your process rather than a real sandbox. Treat a cart like any other script you were sent — read it before you run it. It is one file and it is meant to be read.
License
MIT.