A desktop productivity organism.
A fire burns in a window on your machine. Your work is its only fuel. It begins dying the moment you stop, it climbs from a spark in dry leaves to an omniversal flame across 2600 continuous stages, and if you let it go out, it takes your session down with it.
This is a native desktop application (Electron), and it runs as a desktop pet: a small, transparent, always-on-top flame with no window frame, no taskbar entry, and click-through enabled so it floats over your work without ever getting in the way. There is no window to open or manage — it lives in the system tray. It runs locally, reads your OS directly, and makes no network requests of any kind.
The fire is always decaying. Work does not add progress — work offsets decay.
There is no pause, no banked credit, no coasting. Every frame of a healthy fire is live evidence that you are working right now.
dL/dt = gain(work) − decay(ascension, starvation) − poison(distraction)
Higher forms cost exponentially more to sustain. A campfire nearly maintains itself. A quasar demands near-continuous focus. The Omniversal Flame is asymptotically reachable and is meant to be nearly impossible to hold.
npm install
npm startRequires Node 20+.
A transparent flame appears in a corner of your screen and a flame icon appears in your system tray. Right-click the tray icon for everything: move it to a different corner, resize it, make it clickable/draggable (it's click-through by default), customize its look, pick a theme, declare a Sabbath, edit settings, or quit.
- Transparent — only the fire is visible; there is no panel or box.
- Click-through — clicks pass straight to whatever is behind it. Toggle this from the tray to drag it somewhere new.
- Always-on-top, all-workspaces — a productivity pet you cannot bury or escape by switching desktops.
- Glanceable — no numbers by default. The epoch name whispers in only when you climb, when you're starving, or during a shutdown countdown.
- Tray-only — no dock icon, no taskbar entry, no Alt-Tab clutter.
For a full framed window with the complete HUD (useful for debugging or if you
just want the dashboard), set window.mode to "window" in
~/.fire/config.json.
Right-click the tray → Customize… opens a small panel. Everything in it is
applied to the pet as you drag and saved to ~/.fire/config.json, so it
survives a restart. There is also a Theme submenu of one-click presets
(Classic Fire, Cold Fire, Toxic, Violet Void, Molten Gold, Inferno, Vortex,
3D Rotating Column, Voxel Figure, Voxel Flame, …).
The fire shader stays one shader — every fire control is a modulation of it. Most defaults are identity; the only exceptions are the Effects (embers and bloom ship on by tasteful defaults). Set those two to 0 and the fire is exactly the original — nothing else changes the base look until you move a control.
| Group | Controls |
|---|---|
| Scene | Fire (the productivity pet) or Voxel — a pixelated 3D scene of rotating figures. Pick a figure: flame · cube · sphere · torus · a humanoid figure · a woven knot/mesh · an obsidian cube of black glass · a space cube of drifting nebula and stars · a faceted crystal gem · a plasma orb threaded with electric arcs · a DNA helix · a spinning galaxy. Each rotates and has a small idle animation. |
| Colour | Hue rotation · a colour cycle (living colour — the hue drifts on its own) · saturation · brightness · a custom base → mid → tip gradient blended over the physical blackbody ramp (Custom palette). Applies to both scenes. |
| Motion | Animation speed (also the voxel spin speed) · turbulence |
| Shape | Size (master zoom — shrink it so the big cosmic tiers don't fill the screen) · width · height · swirl (bias a plume toward a spinning vortex) |
| 3D | Rotating column — wraps the fire texture around a vertical cylinder and spins it in depth, with round limb shading |
| Effects | Embers — rising, twinkling sparks off the flame (more when you're working). Bloom & rays — soft glow bloom plus god-rays and a 4-point starburst on the bright cosmic tiers. Both are on by tasteful defaults; set them to 0 for the plain fire. |
| Pixelate | Chunky pixel blocks. Works on the fire; strongest in the voxel scene. |
There are one-click presets for the animated looks too — Aurora, Iridescent, Rainbow Cycle, Ember Storm, Nova Bloom — and for the new voxel scenes (Crystal Gem, Plasma Orb, DNA Helix, Galaxy).
The Voxel scene renders on its own canvas and is a separate raymarched
shader (shaders/voxel.frag) — the fire itself is never touched. Fire is the
default; switching scenes is live. (Importing your own .obj/.glb models is a
planned follow-up; today's figures are built-in.)
All of it lives under theme in ~/.fire/config.json if you would rather edit
it by hand; out-of-range values are clamped on load, so you can't break the
renderer from the file. See config.example.json.
Right-click the tray → Progression (climb pacing), or open Customize… and scroll to the Progression section. This reshapes how the fire's look paces the 2600-stage climb using a mathematical growth curve, without touching the game: your stages, epochs, ceiling and ledger all still run on the real ascension — only the picture's tempo changes.
Pick a curve and tune a couple of knobs:
| Curve | Feel |
|---|---|
| Linear | the default — the look tracks the ladder one-to-one |
| Eased | slow-in / slow-out — a gentle build and settle (power) |
| Logistic | a smoulder, then an explosion, then a plateau — set where it lands (midpoint, rate) |
| Gompertz | a lag where nothing seems to happen, then a burst (lag) |
| Richards | front-loaded or back-loaded drama (nu asymmetry) |
| Bézier | an authored ease |
For example Late Bloom keeps the whole first two-thirds of the ladder a low
ember and compresses all the cosmic spectacle into the top. Every curve is forced
monotone and endpoint-pinned (a spark still precedes a star; stage 0 and stage
2600 look exactly as they do today), so you can't create a discontinuity or a
backwards ladder — even by hand-editing progression in ~/.fire/config.json.
The default is identity, so a fresh install is unchanged until you opt in.
Right-click the tray → Trackers. You can keep small things next to the fire, shown one at a time in a quiet ticker carousel that cycles under the flame:
| Type | What it is |
|---|---|
| Counter | A number you bump up or down by a step you set — days-since, reps, a score, coffees. |
| Note | A short phrase you edit — your current focus, a reminder, a mantra. |
| Countdown | Counts down from a duration to zero, then a desktop notification fires. |
| Stopwatch | Counts up since you start it — time on task. |
Open Trackers → Manage trackers… to add, rename, edit, and delete them. For a
quick change without opening anything, the tray Trackers submenu can bump a
counter by its step or start/pause/reset a timer directly. Timers keep real time
across a restart, and everything is saved under trackers in
~/.fire/config.json. Trackers are independent of the fire — they never change
how it looks or burns.
Right-click the tray icon → Start at login. The pet then launches with your
session every time. (On Windows/macOS this uses the OS login-item registry; on
Linux/WSL it writes ~/.config/autostart/fire.desktop.) You can also set
"autostart": true in ~/.fire/config.json.
fire sees whatever desktop it can actually reach:
| You are on | Sensor | Notes |
|---|---|---|
| Windows | native Win32 | full experience |
| WSL | Windows, via interop | reads the Windows desktop, not the Linux one |
| Linux/X11 | xdotool + xprintidle | sudo apt install xdotool xprintidle |
The WSL case is deliberate and important. A WSL-local sensor would report a
serene, idle desktop while you are three hours into Shorts on the Windows host.
fire runs a resident powershell.exe probe across the interop boundary so it
watches the desktop you actually use.
Recommended: for the full experience (a shame overlay that can cover Windows apps, correct always-on-top behavior), run it natively on Windows — clone to the Windows filesystem and
npm install && npm startfrom PowerShell. Running under WSLg works and is fine for development, but the overlay is confined to the WSLg surface.
Two sensors, sampled at 4 Hz:
- Foreground window — process name + title bar text →
FUEL/NEUTRAL/POISON - Input activity — milliseconds since last input, and a burst count
idle > 120s → −0.30 you are gone. the fire notices.
idle > 45s → 0.00 staring is not working.
POISON window → −1.00 actively hostile
FUEL, typing hard → +1.00
FUEL, reading → +0.35 thinking counts. it should.
NEUTRAL → ×0.15
Ten minutes of Shorts erases three epochs.
fire reads only a process name, a window title, and a millisecond idle
timestamp. It does not read keystrokes, key codes, clipboard, page content, or
window contents. It has no network code. Everything lives in ~/.fire/.
26 epochs × 100 substages = 2600 stages. The renderer consumes ascension as a raw float and morphs continuously, so adjacent stages are nearly identical and there are no sprite swaps anywhere.
Spark · Ember · Kindling Flame · Campfire · Bonfire · Pyre · Wildfire ·
Firestorm · Fire Tornado · Living Flame · Forge-Heart · Solar Flare ·
Protostar · Main-Sequence Star · Blue Giant · Supernova · Pulsar · Magnetar ·
Quasar · Accretion Disk · Galactic Core · Galaxy Cluster · Cosmic Web ·
Universe-Flame · Multiversal Conflagration · Omniversal Flame
At Living Flame (epoch 9) the fire stops being a thing and starts being someone. It looks back at you. Everything above that is cosmological.
Your sustained work quality sets a hard ceiling you cannot exceed:
| Sustained work | You top out around |
|---|---|
| reading only | Fire Tornado |
| a solid day | Pulsar |
| flawless | Omniversal Flame (asymptotic) |
| Rung | Trigger | Effect | Default |
|---|---|---|---|
0 GUTTER |
unfuelled 20s | fire visibly suffers | on |
1 WARN |
lost an epoch | notification naming what you lost | on |
2 SHAME |
L < 40 |
fullscreen overlay + your permanent record | on |
3 BLOCK |
L < 15 |
block poison windows | off |
4 LOCK |
extinction | force OS screen lock | off |
5 NUCLEAR |
extinction | OS shutdown | off, and disarmed |
Enable rungs in ~/.fire/config.json.
Shutdown can destroy unsaved work, so it requires deliberate consent:
The flag alone does nothing. The phrase must match exactly.
Six independent guards must all pass before the machine powers off. It will not fire if:
- it is not armed (flag + exact phrase)
- the
lockrung is disabled (shutdown may never outrank a rung you turned off) - the machine is dormant
- the app launched less than 120 seconds ago
- a sleep/crash gap was detected this session
- a countdown is already running
Every guard is re-checked at the moment of firing, not just when the
countdown starts. The countdown is 60 seconds, fullscreen, and abortable with
Esc or the button. All of this is covered by tests in test/safety.test.js.
If the machine is asleep, locked, hibernating, or crashed, you cannot be working, so you cannot be failing. Ascension freezes completely.
Two independent mechanisms, because neither alone is enough:
- Power events (
suspend/lock-screen/shutdown) — fast and explicit, but silent on hard crashes, power loss, and some lid-close paths. - The wall-clock gap guard — if real time advanced far past our tick loop, the process was frozen, and that interval is discarded unconditionally.
The guard is the backstop that makes sleep, crash, lid-close, and power-cut all resolve identically with no special cases.
A crash is never a death. If the sensor itself fails, fire goes dormant
rather than assuming you stopped working — it refuses to judge what it cannot see.
Declared away-time is available too, and is logged rather than punished:
window.fire.sabbath(true) // stepping away on purpose~/.fire/ledger.jsonl is append-only and never rewritten. Every death you
have ever caused stays in it permanently, and is shown back to you in the shame
overlay.
{"t":"2026-07-20T18:04:11Z","event":"EXTINCTION","peakL":1840,
"peakEpoch":"Quasar","livedFor":19442,"killedBy":"POISON:Shorts","punishment":"LOCK"}~/.fire/state.json is checkpointed every 5s via atomic tmp+fsync+rename, so a
kill -9 costs you at most five seconds.
src/main/ truth lives here
engine.js the heat equation — pure, synchronous, no I/O
tiers.js the 26 epochs
sensors/ index.js · windows.js · linux.js · probe.ps1
classify.js {proc,title} → FUEL | NEUTRAL | POISON
session.js dormancy: power events + wall-clock gap guard
punish.js the ladder and the safety contract
store.js atomic state + append-only ledger
themes.js the look, as data: schema, presets, clamp gate (pure)
progression.js climb pacing: growth-curve remap of ascension, clamp gate (pure)
trackers.js user trackers (counters/notes/timers), as data + transitions (pure)
tray-icon.js generated flame PNG for the tray (pure encoder + Electron glue)
src/renderer/ the fire itself
gl.js WebGL2 host for the fire shader
voxel.js WebGL2 host for the pixelated 3D scene (sibling of gl.js)
shaders/ fire.frag (continuous morph, zero branching) + voxel.frag
hud.js 4 Hz sim → 60 fps render, everything interpolated; scene switch
settings.html/js the live Customize… panel
trackers.html/js the trackers manager panel; a ticker carousel in the HUD
engine.js is pure — step(state, {w, dt}) → newState, no Electron, no clock,
no I/O. That is what makes the entire simulation testable without a display.
npm test109 tests covering the heat equation, equilibrium math, dormancy, the gap guard, classification against real window titles, every nuclear safety guard, the theme system (clamp/validation, presets, scene/figure selection, the generated tray-icon PNG), the progression curves (identity, monotonicity, endpoint pinning, the remap LUT), and the trackers (clamp gate, counter/timer transitions, countdown expiry, formatting).
Everything lives in ~/.fire/config.json. To make it crueller, raise
DECAY_BASE or POISON_RATE. To make the top reachable, raise CLIMB_RATE.
Custom rules are checked before the built-ins:
"rules": [
{ "class": "POISON", "title": "hacker ?news", "why": "HN" },
{ "class": "FUEL", "title": "reddit\\.com/r/rust", "why": "research" }
]
{ "punish": { "lock": true }, "nuclear": { "enabled": true, "consent": "I ACCEPT THE LOSS OF UNSAVED WORK" } }