Neopolitan
A Neapolitan dough calculator that lives in the Omarchy bar. Baker's percentages, yeast scaled for how warm and how long you ferment, and the weights for the whole batch — without leaving the desktop for a spreadsheet.
Click the bread-loaf glyph in the status bar: batch weights on top, the inputs underneath, and the results recalculate as you type.
What it computes
Standard baker's-percentage back-solve. Total dough is fixed by doughball count × ball weight, and flour is the unknown that makes the percentages add up:
totalDough = count × ballWeight
flour = totalDough / (1 + hydration + saltRatio + yeastRatio)
water = flour × hydration
salt = flour × saltRatio
yeast = flour × yeastRatio
The yeast percentage you enter is calibrated at 70 °F over 1410 total ferment minutes on active dry yeast. The calculator scales away from that reference using the accepted rules of thumb:
| Factor | Rule |
|---|---|
| Temperature | Activity doubles per 17 °F rise, so a colder ferment needs more yeast |
| Time | Inversely proportional to total ferment minutes (bulk rest + balled) |
| Type | Instant yeast is ~25% more potent by weight — 0.75× active dry |
At exactly the reference conditions all three factors are 1.0 and the entered percentage passes through untouched. That invariant is asserted in the tests.
With the shipped defaults (8 × 280 g, 62% hydration, 2.6% salt, 0.06% ADY, 70 °F ferment, 30 min bulk rest, 23 h balled) you get:
Flour (00) 1360.4 g
Water 843.4 g
Salt 35.37 g
Active dry 0.82 g
Total dough 2240.0 g
Water temperature feeds the procedure only — it never moves a gram.
Features
- Live batch weights, rounded to the precision each ingredient is weighed at
- Every variable that moves a number, with plain labels
- The five-step procedure, with your temps and times interpolated (folded away by default, since the popup is already tall — click the header to open it)
- Saved presets — name a batch setup, click to recall, delete with a confirm
- A user-owned default batch, editable as JSON with live reload (see Settings)
- Copy puts the whole recipe on the clipboard as plain text (
wl-copy) - Export writes a printable recipe to
~/Downloads
Install
omarchy plugin add https://github.com/tpelicano/neopolitan.git --enable --yes
Or, to develop against a local clone:
make link # symlink this repo into ~/.config/omarchy/plugins/ and rescan
make enable # add the widget to the bar
The widget lands in the bar's right section; move it with
omarchy bar move tpelicano.neopolitan --section center.
Requirements
Omarchy 4 (Quattro) or newer — the plugin runs inside omarchy-shell, the
Quickshell host. Nothing needs to be installed to build or package it.
| Needs | For | If missing |
|---|---|---|
wl-copy (wl-clipboard) |
Copy | Ships with Omarchy. Copy does nothing without it |
| Chromium or Chrome | Export → PDF | Button becomes Export HTML; the HTML is still written |
omarchy-launch-config-editor |
⚙ settings button | Ships with Omarchy |
| Node.js | Running the test suite only | Not needed to use the plugin |
No fonts are installed. The bar glyph is a Nerd Font bread loaf; if your font
lacks it, set barLabel to doughballs or flour for a text label instead.
Uninstall
omarchy plugin remove tpelicano.neopolitan
That removes the widget from the bar and deletes the plugin directory. It does not touch your saved batches, so reinstalling picks up where you left off. To remove those too:
rm -rf ~/.local/state/omarchy/neopolitan
If you installed with make link, use make unlink (or delete the symlink at
~/.config/omarchy/plugins/tpelicano.neopolitan) instead.
Nothing else is left behind: the plugin writes only its own state directory and
its entry in ~/.config/omarchy/shell.json, which omarchy plugin remove
clears.
Settings
Your default batch
The ⚙ button in the panel opens presets.json in your editor (whatever
omarchy is set to use). Saving it reloads the panel immediately — no
restart, no reopening.
The defaults block is your own baseline: what ↻ returns to. Set it once to
your house recipe and reset always brings you home:
{
"defaults": { "doughballCount": 16, "doughballWeightG": 250, "hydrationPercent": 65 },
"lastInput": { "...": "where you left off" },
"presets": []
}
Any key you leave out falls back to the shipped value, and anything out of
range or unparseable is clamped rather than trusted — so a bad hand-edit
degrades instead of producing NaN g.
Don't want to edit JSON? Right-click ↻ to make the batch currently on screen your default.
lastInput is just where you left off; it is written for you as you work and
is independent of defaults.
Widget preferences
Two keys on the widget's entry in ~/.config/omarchy/shell.json. Omarchy 4.0.1
ships no UI that renders a plugin's settings schema yet, so these are
hand-edited for now (shell.json hot-reloads on save):
| Key | Default | What it does |
|---|---|---|
barLabel |
icon |
icon, doughballs (8×280 beside the glyph), or flour (the flour weight) |
exportDir |
~/Downloads |
Where Export writes |
Scripting
The plugin registers a neopolitan IPC target, so the recipe is reachable from
a script or a keybinding without opening the panel:
omarchy-shell neopolitan recipe # print the current batch as plain text
omarchy-shell neopolitan copy # same, onto the clipboard
omarchy-shell neopolitan exportRecipe # write the printable files
omarchy-shell neopolitan toggle # open/close the panel
Where state lives
Widget preferences live in shell.json like any bar widget. The recipe itself —
the current batch and every saved preset — lives in its own file:
~/.local/state/omarchy/neopolitan/presets.json
That split is deliberate. The bar-widget settings schema has no float type, so 62.5% hydration and 0.06% yeast have no faithful representation in it; and a settings write-back replaces a widget's layout entry wholesale, which would silently drop any recipe keys smuggled in alongside. Presets also want to be an array of up to 50 objects, which has no business in a bar layout entry.
The file is plain JSON and safe to hand-edit — anything malformed, truncated or from a future schema version degrades to the defaults rather than throwing. It is watched, so an edit from any source lands live; the panel ignores the echo of its own writes so editing and using it at the same time is safe.
Export
Export renders the recipe to a self-contained HTML file (no external assets, so
nothing to fetch) and then prints it to PDF with headless Chromium, which is
already on an Omarchy box. Both land in exportDir:
~/Downloads/dough-recipe-8x280g.html
~/Downloads/dough-recipe-8x280g.pdf
If no Chromium/Chrome is found the button reads Export HTML and stops after the HTML — open it and press Ctrl+P. If Chromium is present but fails, the HTML is still on disk and the status line says so. It never silently does nothing.
Development
make test # node unit tests + omarchy plugin validate + qmllint
make watch # restart the shell on every save (see below)
make reload # one-shot shell restart
Model.js holds the entire data layer — the math, the formatting, the
procedure, the renderers, and the preset-store shape — as plain functions with
no QML types, so QML and node --test load the same file and the tests test
what ships.
On reloading a symlinked checkout: the shell hot-reloads plugin code by
watching ~/.config/omarchy/plugins with inotifywait -r, which does not
traverse symlinks. A symlinked working tree is discovered and mounted fine, but
saves inside it never reach that watcher.
Measured on Omarchy 4.0.0.alpha, neither omarchy-shell shell rescanPlugins nor
re-creating the symlink swaps the code of an already-mounted bar widget — the
watcher logs the reload, but the running instance keeps the old QML. Only
omarchy restart shell reliably applies an edit, so that is what make watch
and make reload do. Installing the plugin normally (omarchy plugin add,
a real directory) is not affected.
Layout
| File | What it is |
|---|---|
manifest.json |
The plugin contract the shell and omarchy-plugin-validate enforce |
Model.js |
All the math, formatting and store logic — pure, testable, no QML |
BarWidget.qml |
The bar button; loads the panel eagerly and hands it the bar |
Panel.qml |
The popup and the state machine: results, inputs, procedure, presets, export |
DecimalField.qml |
Fractional number entry — the kit's NumberField is int-only |
ResultTile.qml |
One batch-weight card |
SectionHeader.qml |
Section header with an optional fold chevron |
License
MIT