- Rust 99.9%
- Just 0.1%
The GitHub repo is deliberately kept as a mirror, but its main is frozen at b08f1f0 (pre-thor-deployment) with a .gitmodules that still points every submodule back at GitHub — the same trap finding 3 caught in install.md. An agent following this link lands on stale code, not the current tree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FGisjtKSjUcVGw1jX1Hf8M |
||
|---|---|---|
| .claude/skills/ply-engine | ||
| assets/fonts | ||
| src | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CLAUDE.md | ||
| justfile | ||
| LICENSE | ||
| PRODUCT.md | ||
| README.md | ||
openbooks-desktop
Desktop client for OpenBooks, built on ply-engine — a Rust UI engine that renders the same code on Linux, macOS, Windows, Android, iOS and the web.
just run # inside the superproject, starts the api first
cargo run # needs openbooks-api on :38081
OPENBOOKS_API=http://box:38081 cargo run # point it somewhere else
The Settings screen points it somewhere else for good: the address is saved
to settings.json beside the snapshot and read on every launch, overriding
OPENBOOKS_API.
Other platforms go through plyx (cargo install plyx): plyx web, plyx apk,
plyx ios. Only the desktop build has been exercised so far.
Works offline, read-only
Every successful sync writes a JSON snapshot to the platform's app data directory
(~/Library/Application Support/openbooks/snapshot.json on macOS). On launch the app
shows that snapshot before it touches the network, so the window is never empty and a
dead API is not a blank screen.
When the API can't be reached the app is strictly read-only: the record form and the per-row delete buttons aren't rendered at all. Nothing is queued for later. That's deliberate — a replay-on-reconnect queue is a good way to post the same payment twice, and this is a ledger. Recording comes back on its own once a sync succeeds. A failed sync never overwrites a good snapshot.
Layout
client.rs |
HTTP. net::get is idempotent per request id and responses are evicted a few frames after you stop reading them, so each refresh uses a generation-stamped id. |
cache.rs |
The snapshot, via ply's Storage on a background job. |
settings.rs |
The saved API address, same storage as the snapshot. |
api.rs |
Wire types. Integer cents, dates as strings. |
msg.rs |
Ply handlers must be 'static, so they queue a Msg and the main loop is the only place state changes. |
widgets.rs, theme.rs |
Buttons, cards, tables, colour and spacing. |
views/ |
One file per screen. |
Tests
cargo test # money and date logic
cargo test --no-default-features # the above, plus headless render checks
money.rs and date.rs carry the fiddly logic — penny rounding, leap years.
To look at a screen without sitting at the window:
OPENBOOKS_VIEW=reports OPENBOOKS_SHOT=/tmp/reports.png cargo run
It renders ~120 frames, writes the real framebuffer and exits. Worth doing after any layout change: the headless tests below assert what was drawn, never how it looks, and both bugs found this way — a wordmark overlapped by a button, and tofu boxes where the font had no arrow glyph — were invisible to them.
views/mod.rs lays each screen out with no window (Ply::new_headless) and asserts
what it drew, which is how the offline read-only rule is pinned down: a delete control
must render while online and be absent offline, not merely disabled. Those tests need
--no-default-features because ply's eval() syncs a live OS accessibility tree and
panics without a window, so they're compiled out whenever a11y is on.
The two-legged transaction is composed in main.rs::submit — debit positive, credit
negative, summing to zero. Users pick an amount, an account and a category; no
accounting vocabulary reaches the UI.