authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-27 11:13:08-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-27 11:27:28-07:00
logbb5a7aff22be6cf47f808f3d65e597869ba30486
tree63b7da82817af19fe3db281780bc4c13ec89c956
parent1c5ad4e548ef0556a3810f749c2cee0e5e51f289
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

docs: AGENTS.md and arc/ architecture essays

A short AGENTS.md maps the crates, the rules that matter (edits are ops that append one revision, write what the spec and OneNote write, OneNote 2010 is the reference) and the sync basics, and links into arc/: the file format, sync and SMB locking, the canvas editor, the UI kit, platforms, and why the tests are trusted. The repo gitignore keeps AGENTS.md despite the global ignore and skips nested target/ directories. Assisted-by: claude-opus-5.5 Assisted-by: claude-opus-5

9 files changed, 952 insertions(+), 1 deletions(-)

.gitignore+5
...@@ -10,3 +10,8 @@ __pycache__/...@@ -10,3 +10,8 @@ __pycache__/
10/tools/w7/payload/vendor/10/tools/w7/payload/vendor/
11/resources/11/resources/
12/.env12/.env
13
14# The global ignore excludes AGENTS.md; this repo keeps its own.
15!AGENTS.md
16# Build output of nested crates outside the workspace.
17**/target/
AGENTS.md created+92
...@@ -0,0 +1,92 @@
1# Snowbound
2
3Snowbound is a modern remake of OneNote 2010 that stays fully interoperable
4with it. Notes are rich text on a free canvas, stored in OneNote's own `.one`
5files, so Snowbound and OneNote 2010 can open the same notebook side by side.
6A notebook is just a folder. Put it on an SMB share and several people can
7edit it together from either app, with no cloud service involved. Snowbound
8runs on macOS and Linux, and has an iOS app built around the same core.
9
10## Crates
11
12| Path | Owns | Depends on |
13| --- | --- | --- |
14| `crates/onestore` | The file format: revision stores (`.one`, `.onetoc2`), the page model, ops, the commit protocol. No network, no SQLite, no `unsafe`. | none |
15| `crates/notebook` | From editor to disk or share: discovery, notebook structure, sessions, the SQLite replica, sync and merging, conflict pages, the embedded SMB client (feature `smb`). | onestore |
16| `crates/draw` | The wgpu renderer that page and chrome both paint through, and the text-editing core (keys, chords, carets) they share. | none |
17| `crates/canvas` | The page: editor, OneNote-faithful layout, page scene, interaction, the page's accessibility tree. | onestore, draw |
18| `crates/ui` | The immediate-mode interface kit and OneNote's chrome controls. Knows nothing of notebooks. | draw |
19| `crates/snowbound` | The desktop app: winit window, platform glue, sidebar, menus, templates. | all of the above |
20| `crates/mobile`, `apps/ios` | A C surface over canvas and notebook, and the UIKit app built on it. | canvas, notebook, and below |
21| `tools/`, `corpus/`, `fuzz/` | Lab harnesses (Python, PowerShell, AutoHotkey), the corpus of OneNote-verified files, and a separate cargo-fuzz workspace. | |
22
23Dependencies point from the apps toward the format, never back. The format
24crate never learns about storage engines, networks or pixels. `canvas` emits
25ops but never touches storage. `ui` never sees a notebook. The hosts are where
26these pieces meet.
27
28## Rules that matter most
29
30- **Edits are ops, and each edit appends one revision.** `onestore` and
31 `notebook` never recreate a whole page or section from a model. Only creating
32 a section or page, and opening a file, handle whole images. A design that
33 diffs whole pages to save them is the wrong design.
34- **Write what the spec requires and what OneNote writes; read tolerantly.**
35 Emit every property MS-ONESTORE and MS-ONE require (even where OneNote
36 tolerates an omission), plus what OneNote itself stores. Readers accept
37 whatever is out there.
38- **OneNote 2010 is the reference.** When behaviour is in question, observe it
39 in the Windows 7 lab (`tools/w7`) before deciding. A storage feature is done
40 when a file Snowbound wrote cold-opens in a fresh OneNote and reads back as
41 intended ([testing](arc/testing.md)).
42- **Jujutsu, not git.** The repository is managed with `jj`. Don't run `git`.
43- **Disposable copies only.** Never point the app, a replay script or a lab
44 harness at an original notebook.
45
46## Sync in one breath
47
48A `.one` file is a revision store. Every client, OneNote included, commits by
49appending a revision and then rewriting the 1024-byte header, which makes the
50header plus the file length a cheap *stamp* of the committed state. Snowbound
51queues edits as ops in a local SQLite replica. A background thread polls the
52stamp. If the stamp is unchanged, it publishes the queued batch as one
53appended revision. If it has changed, it reads the file once and replays the
54queue on top. Where an op can't merge, the result is what OneNote 2010 makes:
55a read-only conflict page under the page. OneNote coordinates writers through
56share modes and one-byte locks on the section file itself, and macOS's
57`smbfs` can't reproduce those, so Snowbound carries its own SMB client that
58takes exactly OneNote's locks. Offline is just a sync step that fails: the
59queue waits and publishes later. [More in the sync essay.](arc/sync.md)
60
61## Architecture notes
62
63- [The file format and `onestore`](arc/file-format.md): revision stores,
64 object spaces, transactions, and what "append one revision" means.
65- [The data layer and sync](arc/sync.md): ops, sessions, the replica,
66 publishing, merging, conflict pages, SMB and locking, offline.
67- [The page: editor and canvas](arc/canvas.md): layout, OneNote-faithful
68 geometry, the page view, accessibility.
69- [The interface kit and the renderer](arc/ui.md): immediate mode,
70 custom drawing, the OneNote shell and its motion.
71- [Platforms](arc/platforms.md): macOS, Linux, and iOS with UIKit around the
72 canvas.
73- [Testing, and why the code can be trusted](arc/testing.md): the native
74 lab, oracles, failure injection, fuzzing, and the two test tiers.
75
76The crate READMEs ([onestore](crates/onestore/README.md),
77[notebook](crates/notebook/README.md)) are the API references.
78[tools/TESTING.md](tools/TESTING.md) lists the test lanes.
79
80## Working here
81
82```sh
83cargo test --workspace --all-features
84cargo clippy --workspace --all-targets --all-features -- -D warnings
85python3 -m unittest discover -s tools -p 'test_*.py' # VM-free corpus gates (needs Pillow)
86python3 tools/check_public.py /absolute/new/results # everything above, from a clean checkout
87python3 tools/canvas/build_macos.py && target/Snowbound.app/Contents/MacOS/Snowbound --notebook COPY
88```
89
90The MS-ONESTORE and MS-ONE specifications are Microsoft Open Specifications.
91Local copies, design history and raw lab evidence are kept out of version
92control.
arc/canvas.md created+115
...@@ -0,0 +1,115 @@
1# The page: editor and canvas
2
3A OneNote page is a free canvas, but it doesn't feel like a drawing app. You
4click anywhere and type, and a text box (an *outline*) appears and grows as you
5write. Outlines wrap, lists indent, tags hang in the margin. `canvas` is the
6crate that makes a page behave that way and look the way OneNote 2010 draws
7it. It depends on `onestore`'s page model and on `draw`. It knows nothing
8about storage, windows or the interface kit, which is why the same page runs
9inside the desktop app and inside a UIKit view on iOS.
10
11## Layers
12
13```text
14onestore::page::Page stored identities, formatting, unknown content kept aside
15 │
16canvas::document text outlines as paragraphs of rich text, UTF-16 positions
17canvas::editor CanvasEditor: edits, selection, composition, undo, the ops each edit lowers to
18canvas::layout, outline shaping (parley), Windows line metrics, outline and table geometry
19 │
20canvas::gpu::page PageScene: the page as draw primitives; pictures decoded off-thread
21canvas::interaction PageView: hit layers, drags, grid, handles, key routing, scroll, zoom, AccessKit
22 │
23host translates platform events in; carries out the Requests that come back
24```
25
26A host feeds `PageView` pointer, key and text-input events. It gets back a
27`Response` saying whether the page, the selection or only the view changed,
28plus the occasional `Request` the page can't do itself: show a date picker,
29read the clipboard, open the character palette. Everything platform-specific
30stays on the host's side of that line.
31
32## Editing emits ops
33
34`CanvasEditor` holds the working page. When an edit changes what's stored, the
35editor records the `onestore::op`s the change lowers to at that moment. The
36host collects them and hands them to the section as one `Edit`. No page is
37ever rebuilt or diffed to be saved, so the work of saving a keystroke follows
38the size of the edit, not the size of the page.
39
40Undo lives in the editor, not in storage. Each history entry keeps the inverse
41of the change it made. Undoing applies that inverse and emits its ops as a new
42edit, so the file only ever moves forward, like OneNote's. Undoing a deletion
43brings back the original identities, so internal links to those paragraphs
44survive an undo.
45
46Input-method composition (marked text) stays inside the editor until it
47commits, and only then becomes ops. When another client changes the open page,
48the editor compares the stored page with what it last read plus the ops it has
49handed out, and reloads in place.
50
51## OneNote-faithful geometry
52
53The goal is that a page looks the same in Snowbound as in OneNote: same wraps,
54same line heights, same places. That took measuring, not guessing.
55
56- **Stored geometry is not laid-out geometry.** An outline's stored width and
57 height are constraints and hints. The displayed box comes from layout, so the
58 canvas lays out as OneNote does and never treats a stored size as a clip.
59- **Line boxes use Windows metrics.** OneNote measures lines with a font's
60 Windows ascent and descent, not the typographic or horizontal-header values
61 most text stacks pick. The difference is small per line and large per page.
62 Drawing and hit-testing share these line boxes.
63- **Substitutes match metrics.** Where the system lacks Calibri, Arial, Times
64 New Roman or Courier New, bundled metric-compatible substitutes (Carlito,
65 Arimo, Tinos, Cousine) stand in, under the stored font name.
66- **OneNote's constants are OneNote's.** New outlines take OneNote's default
67 width. Dragging snaps to its grid, anchored at the page's margin origin. Tags
68 sit in a column to the left of the text with OneNote's spacing, and a tag's
69 colour paints the whole paragraph as it does there.
70
71These rules were established against OneNote's own output: its XML export
72gives outline sizes, its PDF export gives exact line breaks, and screenshots
73give placement. The comparators in `tools/canvas` (with the probes
74`layout-probe` and `page-probe`) keep checking them.
75
76Equations draw in two dimensions from the tree `onestore::page::Math` parses.
77The linear text stays the editable source. Ink draws stroke by stroke in page
78coordinates. Page templates' background art is recreated as vector art and
79recognised by the stored picture's hash. OneNote's bitmaps aren't shipped.
80
81## Content the editor doesn't understand
82
83Nothing is lost for being unfamiliar. A paragraph the canvas can't draw
84becomes a labelled placeholder that keeps its place in the flow, and the rest
85of its outline stays editable. An object the editor can't hold draws as stored
86and stays read-only. Its bytes are never touched either way. Wherever a
87remaining "can't edit this" state exists, the aim is to remove it by teaching
88the editor the structure, not by flattening the content.
89
90## Pictures and memory
91
92The page scene reads only picture headers when it's built. Pictures in and
93near the view decode on a background thread at the size they're shown, within
94a fixed memory budget, and pictures long out of view are let go. No number or
95size of pictures can make a page fail to open. Opening a page happens on its
96own thread too. The current page stays live until the new one is laid out and
97the pictures it shows first are ready.
98
99## Accessibility
100
101The page builds an AccessKit tree. Each text outline appears as its own
102editable text area, whose runs carry the canvas's real line boxes and
103character positions, so a screen reader's caret and selection land where the
104eye does. Native selection and replacement actions go through the editor's
105history like any other edit. The tree updates only while an assistive client
106is listening, and never for a caret blink. The interface kit around the page
107doesn't have a tree yet.
108
109## Search, dates, conflicts
110
111Smaller modules follow the same pattern of reproducing OneNote's behaviour
112precisely. `search` matches the way OneNote 2010 searches: word prefixes,
113ignoring case and diacritics, title matches first. `date` edits the title's
114date and time fields the way OneNote stores a changed page date. `conflict`
115shows conflict pages with OneNote's highlight.
arc/file-format.md created+183
...@@ -0,0 +1,183 @@
1# The file format and `onestore`
2
3Everything Snowbound promises (opening the notebooks you already have, editing
4them beside OneNote, collaborating without a server) comes down to one thing:
5reading and writing OneNote 2010's files exactly as OneNote does. `onestore` is
6the crate that owns that. It knows nothing about SQLite, networks or pixels. It
7parses files, turns them into a model an editor can work with, and turns edits
8back into bytes OneNote will accept.
9
10## A revision store
11
12A notebook is a folder. Each section is a `.one` file. The folder's
13`Open Notebook.onetoc2` lists the sections and section groups in order, and a
14section group is a subfolder with its own `.onetoc2`. Both kinds of file share
15one container format, the *revision store* described in Microsoft's
16MS-ONESTORE specification. The content inside them follows MS-ONE.
17
18A revision store is closer to a small database than to a document. Logically
19it looks like this (physically, list fragments and revision data interleave as
20the file grows):
21
22```text
23┌──────────────────────┐
24│ header (1024 bytes) │ commit point: transaction count, file version GUID, list roots
25├──────────────────────┤
26│ file node lists │ chains of fragments; each fragment ends by pointing at the next
27│ ├ root list │ object spaces in the file
28│ ├ per object space │ its revisions, in order
29│ ├ transaction log │ how many list entries each committed transaction added
30│ └ file data store │ embedded pictures and attachments
31├──────────────────────┤
32│ revision 1 │ objects the revision declares, in object groups
33│ revision 2 ─dep─► 1 │ only what changed, plus the ancestors that point at it
34│ revision 3 ─dep─► 2 │
35│ … (appended) │
36└──────────────────────┘
37```
38
39- **Object spaces** partition the content. A section has a root space for
40 section-wide metadata and the page series, plus one space per page. A
41 conflict page, when there is one, gets a space of its own too.
42- **Revisions** are per space. A revision declares the objects it adds or
43 changes and depends on the revision before it. OneNote itself typically
44 appends a small dependent revision for each edit: the changed objects plus
45 the chain of containers above them, whose modification times moved.
46- **Objects** have a type (a JCID) and a property set. They reference each
47 other by *ExtendedGUID*: a GUID plus a small integer. Inside a revision these
48 are compressed to compact IDs through a global ID table.
49- **The header** is the commit point. Bytes appended past the old end of the
50 file mean nothing until the header's transaction count says a transaction
51 holding them has committed.
52
53External payloads (large pictures, attachments, recordings) can live beside
54the section as `.onebin` files in a `_onefiles` folder. The section refers to
55them by name.
56
57## What "append one revision" means
58
59Every edit Snowbound makes ends as a `Transaction`: bytes appended at the old
60end of the file, a few small patches inside it (the tail of each list gains a
61link to its new fragment, and the transaction log gains an entry), and a new
62header. A transaction is written for a particular base, named by its `Stamp`:
63the base's header and its length.
64
65```text
66exclusive lock ─► stamp still matches? no ─► NotCommitted, reread and rebase
67 │ yes
68 ▼
69 append new fragments, patch list tails ─► flush
70 header fields that describe the append ─► flush
71 transaction count (the commit) ─► flush
72 file version GUID (wakes cached readers)─► flush ─► unlock
73```
74
75The stamp works because every committed transaction rewrites the header,
76including the file version GUID. So equal stamps mean the same committed image,
77and a commit never has to compare the file's body. The flushes are what make a
78torn write recoverable. At any cut point, the file is either the old image with
79some ignored bytes past its end, or the new image. The version GUID goes last
80because OneNote's cached readers watch it, not the transaction count.
81Publishing it any earlier was once a real race with native readers.
82
83A commit ends in one of three states. Callers must honour them:
84
85| State | Meaning |
86| --- | --- |
87| `NotCommitted` | Nothing was published. Reread before retrying. |
88| `Unknown` | The reply was lost after the point of no return, so it may have landed. Reread and find out before doing anything else. |
89| `Committed` | Durable. Only cleanup failed. Never replay it. |
90
91The only operations that handle a whole image are creating a section or page,
92and opening a file. Everything else is an appended revision. The project holds
93this as a rule rather than an optimisation. A writer that regenerates a page
94from a model does page-sized work (parsing, diffing, rereading the file) for
95every keystroke. On a share, that work happens inside the writer lock every
96other client is waiting on.
97
98## Kept open: `Section`
99
100`Section` is a section file parsed once and kept in memory across edits. Ops
101apply to its spaces in memory. `seal` then turns everything that changed into
102one `Transaction` that appends one revision per changed space. A seal checks
103only what it appends: every fragment is linked from its list's old tail, every
104declared object parses and resolves, reference counts are the incremental
105counts, and the log entries have the CRC the state predicts. The full-file
106validator runs when a file opens and throughout the tests. The section borrows
107its bytes from an arena that lives beside it, so there is no self-reference and
108no `unsafe` (the crate forbids it).
109
110The writer caps revision dependency chains with a checkpoint revision, because
111native cold opens fail on very long chains. It also keeps a small reservation
112after a transaction-log fragment that ends the file, because OneNote does.
113
114## The page model
115
116`onestore::page` is the editable view of a page: title, outlines, paragraphs
117of text with formatting spans, lists, tags, tables, pictures, attachments, ink,
118and equations. Every node carries its stored identity. Content outside the
119model isn't dropped. It becomes `Unsupported`, which keeps its identity, type
120and layout, and its bytes stay untouched in the file. The canvas edits this
121model and the notebook crate stores it. Neither needs to know how it is encoded.
122
123## Ops
124
125`onestore::op` is how an edit is expressed: what the editor emits, what the
126queue stores and what `Section::apply` writes. An `Edit` is one user action (a
127list of ops and a timestamp), and it applies entirely or not at all. The ops are
128object-level, for example "replace this UTF-16 range of this text object",
129"split this paragraph here, naming the new paragraph", "move this subtree
130before that sibling" or "add these table rows". A refusal names the target,
131identity or structure at fault.
132
133Three properties make ops work as a sync currency:
134
135- **The emitter chooses new identities.** Replaying the same op on another copy
136 of the section creates the same objects, so an edit keeps its meaning across
137 retries, rebases and devices. The identity scheme copies OneNote's own
138 (`{page guid},n`, counting from 1). An earlier scheme that used 0 was accepted
139 by OneNote's integrity check, but OneNote then silently dropped elements in
140 roughly one build in five. Only a large native gate caught that.
141- **Ranges are UTF-16 code units.** That is how the file stores text, and it is
142 what UIKit's text input speaks. Ranges that would split a surrogate pair or a
143 hidden field are refused.
144- **Ops are plain data.** They serialize, they carry no UI types, and payload
145 bytes travel beside them by hash.
146
147`op::model` interprets ops on the page model without touching bytes. The
148lowering code (`op::lower`, which turns a model change into ops) uses it to
149predict what each op leaves, and the tests use it as an oracle.
150
151## Documented, observed, and reverse-engineered
152
153MS-ONESTORE and MS-ONE are good, but they are not the whole truth:
154
155- Some things OneNote writes aren't in the spec, such as the author initials it
156 stores beside every author name. Snowbound writes them too.
157- Some things the spec requires aren't needed by OneNote to open a file.
158 Snowbound writes them anyway, because other readers exist.
159- Ink, equations and media recordings are absent from MS-ONE entirely. Ink and
160 math were reverse-engineered, each against an independent oracle. For ink,
161 the stroke extents from OneNote's own export. For math, the MathML it exports,
162 matched byte for byte. Recordings and embedded objects are read and kept, but
163 never authored.
164
165Readers stay tolerant (files in the wild are older, odder, or written by other
166tools), while the writer stays strict.
167
168## Protected sections
169
170Password-protected sections keep their encrypted structure. With the
171`protected` feature, `onestore` opens OneNote 2010's AES wrapper for a supplied
172password. Edits are sealed back under the section's key, with a fresh IV for
173each object. Plaintext never reaches the replica's cache. A protected section is
174edited directly in its file and never queued.
175
176## Looking inside
177
178The crate's examples are the fastest way to get a feel for a file.
179`inventory`, `inspect` and `document` dump the lists, revisions and document
180model. `tools/notebook_report.py` renders a readable report of a whole
181notebook, and the `onestore-diagnostic` binary in `notebook` is a small HTML
182editor for poking at one. The [crate README](../crates/onestore/README.md) is
183the reference for the public surface.
arc/platforms.md created+98
...@@ -0,0 +1,98 @@
1# Platforms
2
3Snowbound is one core with thin hosts around it. The file format, sync, the
4page editor and the renderer are identical everywhere. What changes per
5platform is the window, the input plumbing, and how much of the interface is
6drawn by Snowbound versus the operating system.
7
8```text
9 macOS Linux iOS
10shell snowbound + ui snowbound + ui UIKit (apps/ios, Swift)
11glue snowbound/src/macos snowbound/src/linux crates/mobile (C ABI, staticlib)
12page canvas ─────────────────────────────────────────────────────►
13paint draw (wgpu: Metal) draw (Vulkan or GL) draw (Metal, CAMetalLayer)
14data notebook::session + embedded SMB client ────────────────────►
15format onestore ───────────────────────────────────────────────────►
16```
17
18## Desktop: `snowbound`
19
20The desktop app is a winit window with the `ui` kit's chrome and a `canvas`
21page inside it. `snowbound` picks a platform module at compile time
22(`macos.rs` or `linux.rs`, both mounted as `platform`), and everything else
23in the crate is shared: library and settings, the sidebar, menus, page and
24section management, templates, and screenshot and replay support.
25Accessibility for the page goes through AccessKit's winit adapter on both.
26
27### macOS
28
29- The title bar is transparent. The app draws its own around AppKit's traffic
30 lights, as part of the same frame as the rest of the chrome.
31- AppKit supplies the open and save panels, alerts, the date picker, date
32 formatting for new page titles and conflict labels, the account's full name
33 (used as the author, as OneNote uses Office's user name), and the caret and
34 selection colours.
35- Frames present inside Core Animation's transaction, so a resize pairs each
36 frame with the window's new size instead of stretching the last one.
37- A notebook on a mounted SMB share is opened through the embedded SMB client,
38 signed in with the password the keychain keeps for that mount (see
39 [sync](sync.md) for why the mount itself isn't enough).
40- `tools/canvas/build_macos.py` builds and ad-hoc signs `target/Snowbound.app`.
41
42### Linux
43
44- X11 and Wayland. Title bars come from the window manager, or on Wayland from
45 the compositor where it offers server-side decorations and a client-side
46 frame otherwise. The app draws its own window controls only where no frame
47 could be made.
48- zenity or kdialog provide the pickers and alerts. The XDG settings portal
49 provides the colour scheme. Text conventions come from the C library's
50 locale. Fontconfig is loaded at run time, so builds need no headers for it.
51- Wayland's clipboard goes through the window's own connection, since not every
52 compositor offers a clipboard to clients without a window.
53- `crates/snowbound/linux` packages a tarball, plus an installer that adds the
54 launcher entry the Wayland desktop needs to find the icon.
55
56### Windows and older macOS
57
58The readme names both as goals. Nothing platform-specific exists for them yet.
59Keeping `ui` and `draw` free of platform toolkits is what keeps them within
60reach.
61
62## iOS: native around the canvas
63
64On iOS the split moves. Touch text editing depends on affordances that users
65know by feel and that are expensive to imitate: the loupe, selection handles,
66the edit menu, autocorrect, dictation and hardware keyboard commands. So UIKit
67owns everything around the page:
68
69- **UIKit** handles navigation (a three-column split view of notebooks, pages
70 and the page), scrolling and zoom (`UIScrollView`), the keyboard and text
71 input (`UITextInput`), caret, selection highlight and handles, the format
72 bar, and connecting to servers.
73- **`canvas`** draws the page into a `CAMetalLayer` and makes every editing
74 decision. The C surface exposes the active outline's text as the flat UTF-16
75 model `UITextInput` speaks. That is the same unit `onestore` ops measure
76 text in, so autocorrect and dictation become ordinary text ops with no
77 translation layer.
78- **`crates/mobile`** is that surface: a static library with a C header,
79 covering libraries, shares, sections and views, built by an Xcode build
80 phase (`apps/ios/build-rust.sh`). All views share one GPU device and one
81 renderer, and text engines are pooled across views.
82
83No `ui` crate ships on iOS. Everything below the view is the desktop's code:
84`notebook::session` with its replica and background publishing, the embedded
85SMB client (credentials kept in the Keychain, as Files keeps its own), conflict
86pages, search, and page management through the same ops. Notebooks from Files
87are read and written under `NSFileCoordinator`, so file providers see every
88change.
89
90## What stays shared, on purpose
91
92- **Behaviour**: the editor, hit-testing, the placement grid, undo, conflicts
93 and search all live in `canvas` or `notebook`. A host that reimplements one
94 of them has made a bug.
95- **Storage**: every platform writes through the same ops and the same
96 replica, and publishes through the same sync step.
97- **Look of the page**: the page renders through `draw` everywhere, so a page
98 looks the same on a phone as on the desktop.
arc/sync.md created+184
...@@ -0,0 +1,184 @@
1# The data layer and sync
2
3OneNote 2010 got multi-user editing without a server. A notebook is a folder
4on a file share. Every client edits the section files in place, and the
5format's append-only revisions plus some careful file locking let clients
6merge each other's work. Snowbound joins that arrangement as one more peer. It
7has no service, no account and no sync protocol of its own. The share is the
8source of truth, and whatever OneNote can do to a file while Snowbound is
9using it, Snowbound has to handle.
10
11This essay follows an edit from the keyboard to the share, then covers what
12happens when the share is busy, changed or gone.
13
14## Three threads
15
16```text
17UI thread (winit, UIKit) section thread sync thread
18──────────────────────── ────────────────────────────── ──────────────────────────────
19editor emits Edit (ops) ───► apply to the parsed Section poll the stamp (header + length)
20 and returns at once write each burst to SQLite unchanged: ask for a seal,
21page reads ◄──────────────── answered from the Section publish it
22events ◄──────────────────── Changed, Rejected, published changed: read once, ask
23 seal ◄────────────────────────── for a rebase
24 rebase onto the new image ◄───── acknowledge: base += transaction
25```
26
27- **The UI thread only emits ops.** The editor records the ops each change
28 lowers to as it makes the change. Submitting them to the session returns
29 immediately. Parsing, revision building and SQLite never run on a frame.
30- **The section thread** (`notebook::working`) is the only owner of the parsed
31 `onestore::Section`. It applies edits, writes each burst of them to the
32 replica in one durable commit, and answers page reads. A rebase runs on a
33 fresh thread that builds its own section while the old one keeps answering
34 reads, so opening a page never waits on the network.
35- **The sync thread** (`notebook::worker`, stepping `notebook::sync`) does all
36 network I/O. It polls, publishes and rereads. When the queue is idle it
37 reads nothing but the header.
38
39`notebook::session` wraps this up for apps. `Notebook` covers discovery and
40structure, `Section` covers one section's pages, edits, events and conflict
41pages. Both desktop and iOS use exactly this surface.
42
43## The replica
44
45Each open section has a replica: a SQLite database in the app's cache,
46identified by the section's logical identity, so the same file reopens the
47same queue after a relaunch. It holds:
48
49- **the base**: the last remote image the queue applies to, stored in chunks so
50 that acknowledging a publication rewrites only the chunks the transaction
51 touched;
52- **batches of edits**: each edit's ops, serialized, grouped into the batch
53 that will publish together;
54- **payloads**: picture and attachment bytes, stored once each, keyed by hash.
55
56The working state is never stored. It is the base with each sealed batch
57replayed and the open batch applied, rebuilt on open. The database runs in WAL
58mode with full synchronous commits, and every setting is read back to check it
59took.
60
61A keystroke that publishes immediately costs three commits: the edit, the seal
62(which also records the publication attempt), and the receipt. Each one is
63ordered against something outside the cache. The edit must be durable before
64`apply` answers. The attempt must be durable before any byte reaches the
65share, because an attempt that might have landed is never replayed blindly.
66The receipt follows the file's own commit. Merging any two of them would open a
67window where a crash forgets or duplicates work.
68
69## A sync step
70
71```text
72stamp = remote.stamp() one small, uncoordinated read
73if stamp == base.stamp:
74 publish the sealed batch (if any) Ok ─► acknowledge
75 NotCommitted ─► retry later
76 Unknown ─► attempted; confirm before anything else
77else:
78 image = remote.read() coordinated snapshot, only now
79 rebase the queue onto image merge, maybe conflict pages
80 base := image
81```
82
83A batch stays open while the sync thread is busy, so keystrokes that arrive
84during one round trip publish together in the next. Polling costs a
85kilobyte-sized read. A publication costs one appended revision, not the
86section.
87
88Uncertainty is handled explicitly. A publication attempt is recorded before
89the first network byte. If the reply is lost, the attempt confirms only when
90the remote holds its revisions (or every page it changed, as it changed them).
91Otherwise it waits as `AwaitingConfirmation`, and `release` resolves it
92explicitly: it exports a recovery archive first, then republishes or abandons.
93
94## Merging
95
96When the stamp has moved, someone else committed. The queue replays on the new
97image one op at a time (`notebook::merge`):
98
99- an op whose objects the remote left alone applies as it is;
100- a text op on text the remote also changed shifts past the remote's changes,
101 provided its range stays clear of them;
102- a move, deletion or setting the remote already made is dropped as done;
103- anything else conflicts.
104
105Conflicts follow OneNote 2010 exactly, because the other clients are OneNote.
106The conflicting op is dropped, along with every later op that names what it
107named. The remote's version stays the page. The local version becomes a
108read-only **conflict page** under it, with the conflicting objects marked and
109the page labelled with its author, so OneNote and Snowbound both show the
110same bar, the same highlighted paragraphs and the same Delete Conflict Page. A
111conflict never blocks the queue: it becomes one more queued edit. Page-list
112edits merge the way OneNote merges page series (a page someone moved keeps
113their placement, and a page deleted remotely but edited locally comes back as a
114copy). Each rule was checked against a capture of two real OneNote clients
115doing it first (`corpus/conflict-page`).
116
117## Why an embedded SMB client
118
119OneNote doesn't use lock files. It coordinates through the section file
120itself, with share modes on open and one-byte locks far past the end of the
121data:
122
123| Role | Open | Byte locks |
124| --- | --- | --- |
125| reader | read, sharing read/write/delete | shared lock on the reader byte, held for the read |
126| writer | read/write, denying other writers | the reader byte shared, plus an exclusive writer byte |
127| maintenance (Optimize) | as a writer | an exclusive range that covers the reader byte |
128
129Maintenance rewrites the file under a new name and renames it into place, so a
130handle to the old file goes stale even though its contents still parse.
131
132Snowbound has to take exactly these locks, or OneNote will either trample it
133or be locked out. macOS's `smbfs` can't express them. POSIX byte-range locks
134are unsupported. `flock` of either kind becomes an exclusive lock over the
135whole file, which fails OneNote's reads. The only exclusive-byte primitive is
136private and has no shared form. On top of that, `smbfs` serves reads from its
137own lease-backed cache, defers closes, writes whole cached pages back on
138`fsync`, and leaves AppleDouble `._` files beside sections it writes. On iOS
139an app sees a share only through Files, with no control over locks.
140
141So `notebook::smb` speaks SMB2 itself (on the `smb2` crate's message layer)
142and takes OneNote's opens and bytes exactly, without leases. It never denies
143OneNote a read. The leases matter because a Windows client holding a lease
144keeps its byte locks local and only reveals them when another open breaks the
145lease. So a peer must open before it locks, and never hold handles between
146operations. A commit is a handful of compound requests: open; take the locks,
147check identity and stamp; write appended bytes and patches, then flush; write
148the header, the counter and the version, each flushed in order; close.
149
150A notebook on a share that macOS has mounted is opened through the embedded
151client with the account the system keeps for that mount. It falls back to the
152mount only when it can't sign in that way.
153
154A file's identity is its root object space, not its server file ID, which
155changes when maintenance replaces the file. Polling opens by path each time
156for the same reason.
157
158## Offline
159
160Offline is just a sync step that fails. Edits keep landing in the replica, and
161the sync thread retries with backoff and wakes on new local edits. A notebook's
162last listing lets it open while the server is unreachable, and a section opens
163from its replica. On reconnect the queue publishes, or rebases and publishes,
164through the same path as always. A failed directory read keeps the previous
165catalog, so an unreachable share never looks like an emptied notebook.
166
167## Notebook structure
168
169Sections, groups and the notebook's own colour live in the `.onetoc2` files,
170which are revision stores edited through the same transaction path. Moving or
171renaming a file also rewrites two header fields OneNote checks on open (the
172parent TOC's identity and a CRC of the file's name). Without them OneNote
173treats the file as a stranger and re-identifies it. Deleting sends sections and
174pages to `OneNote_RecycleBin`, as OneNote does.
175
176## Recovery and migration
177
178`export_recovery` snapshots a replica into a single read-only archive: base and
179remote images, queue, attempts, receipts and media. It is the evidence behind
180any decision that could lose work. Cache schema conversions check their own
181output: every converted page must equal the page the old cache held, or the
182conversion rolls back and names the archive it made first.
183
184The [notebook README](../crates/notebook/README.md) documents the API in detail.
arc/testing.md created+147
...@@ -0,0 +1,147 @@
1# Testing, and why the code can be trusted
2
3Snowbound writes into other people's notebooks, often while their copy of
4OneNote is writing to the same file. A bug doesn't just crash an app. It can
5corrupt a shared notebook, or silently drop a paragraph someone else typed.
6The testing strategy follows from that. **Never grade the code with itself.**
7Every important claim is checked against something the code under test didn't
8produce: a real OneNote, a separate model, a second path through the system,
9or a disk that loses bytes on purpose.
10
11This essay explains where the confidence comes from. The how-to for running
12each lane is in [tools/TESTING.md](../tools/TESTING.md) and the crate READMEs.
13
14## The reference is a real OneNote
15
16The only authority on "OneNote accepts this" is OneNote. The lab (`tools/w7`)
17runs OneNote 2010 in disposable QEMU clones of a sealed Windows 7 image. An
18agent inside each clone runs AutoHotkey and PowerShell (OneNote's COM API),
19returns screenshots and files, and the clone is discarded afterwards.
20
21Every storage feature goes through the same loop:
22
23```text
24observe author the edit in OneNote (COM, or driving its UI) and dump what it stored
25write make Snowbound store the same thing through ops; export a candidate notebook
26cold-open a fresh clone with a fresh OneNote cache opens the candidate
27 ─► integrity check passes, XML export, screenshots, PDF where layout matters
28gate a VM-free test compares the capture with what the candidate meant to say
29install candidate + capture become a corpus row (corpus/<feature>/…)
30```
31
32*Cold* matters. A warm OneNote has its own cached copy and can hide a file it
33would reject. Some failures appear only at scale. OneNote once silently dropped
34elements in about one build in five because of an identity choice its integrity
35check accepted, and it refuses revision chains past a certain depth. Gates
36therefore include large mixed candidates, not only one small row per feature.
37
38The corpus is what makes this sustainable. Each row keeps the native capture
39next to the candidate, and its `tools/test_*.py` gate checks it without a VM,
40on every run, forever. Re-running the VM step is needed only when the bytes
41Snowbound writes change.
42
43## Oracles inside the build
44
45Most checking needs no VM. It works by making independent views of one edit
46agree:
47
48- **The model oracle.** `onestore::op::model` interprets ops on the page model
49 without going near bytes. After a random edit is applied to a `Section`,
50 sealed and read back, the page must equal what the model predicted.
51 Refused edits must leave the page unchanged.
52- **Differential editing.** For every editor action, alone and in random
53 sequences with undo and redo, on every page of a set of corpus sections,
54 four views must agree: the page stored through the ops, the model's
55 prediction from those ops, the sealed image read back, and the editor's own
56 page (`crates/canvas/tests/ops_differential.rs`).
57- **Seals check themselves.** Each seal validates exactly what it appended. The
58 full-file validator runs on open and over the tests' images.
59- **Layout agrees with itself and with OneNote.** Incremental layout must
60 equal a fresh layout after long structural histories. Undo and redo must
61 restore geometry exactly. Unicode range replacement is compared with plain
62 string replacement across hundreds of combinations. Wraps and heights are
63 compared with OneNote's own XML and PDF exports.
64- **Conversions verify their output.** A cache schema migration must reproduce
65 every page the old cache held, or it rolls back.
66
67## Failure, on purpose
68
69Durability claims are tested by making things fail at every point that
70matters.
71
72- **Torn writes.** The storage crash model is a disk that, at an injected
73 failure, persists a random subset of the bytes written since the last flush.
74 Every I/O point of ordinary commits and counter-rollover commits is cut.
75 Afterwards the file must read as the old image or the new one, never a
76 third thing. The same model drives the commit fuzzer.
77- **Crashes in the replica.** SQLite transactions are cut while their frames
78 are in the log but the commit frame isn't, at every step of edit, seal,
79 publish and acknowledge. Reopening must recover the queue as it was.
80- **Lost replies.** `tools/smb-proxy.py` sits between the client and a lab
81 Samba server and withholds a chosen request or response. The matrix cuts
82 every message of a commit. Each outcome must keep its promise:
83 `NotCommitted` really didn't publish, `Committed` really did, and `Unknown`
84 cases really went either way, with the confirmation that follows finding
85 out which.
86- **Power cuts.** The lab can kill a VM outright mid-workload, and the
87 surviving files are then cold-opened by a real OneNote.
88- **Schedules.** Deterministic multi-actor tests run several replicas editing
89 offline and publishing through a fault-injecting in-memory remote, then
90 check convergence and conflict pages.
91
92## Real clients on a real share
93
94The collaboration lab puts several OneNote 2010 clients and several Snowbound
95writers and readers on one section in a Samba share. They make random edits
96through outages, reconnects, offline stretches and OneNote's own maintenance
97(Optimize rewrites the file underneath everyone). The pass criteria are strict:
98
99- every intent from every client is in the final file exactly once, or is
100 accounted for as a conflict page;
101- a wire trace shows OneNote was never refused a read; the only refusals it
102 met are the ones its own protocol makes between two writers;
103- a fresh OneNote cold-opens the result and agrees.
104
105## Fuzzing
106
107`fuzz/` is its own cargo-fuzz workspace. It covers parsing (storage, property
108streams, revisions, documents), the commit protocol with interruptions, edits
109of many kinds, the page model, protected sections, the offline queue, and the
110canvas editor's state machine. The parsers see arbitrary bytes, and the
111writers see arbitrary sequences of edits whose results must still validate.
112
113## Two tiers of tests
114
115Tests fall into two kinds, and the suite is being split to match:
116
117- **Correctness tests** are deterministic and run on every change: `cargo test`
118 over the workspace, clippy, and the Python gates over retained captures.
119 `tools/check_public.py` runs all of it from a clean checkout, with no private
120 notebooks, VMs or credentials.
121- **Sweeps and fuzzing** are random sequences over large corpora and
122 coverage-guided fuzzers. They are opt-in (today, sweeps widen through
123 environment variables such as `CANVAS_SWEEP_*` and `OPS_SWEEP_SECTIONS`) and
124 are meant to run for days or weeks on dedicated machines. What they find gets
125 reduced to a small deterministic test in the first tier.
126
127Lab lanes (real OneNote, Samba VMs, the proxy) sit beside both tiers. In Rust
128they appear as ignored tests that name the environment they need, and the rest
129live in Python harnesses under `tools/`.
130
131## What counts as evidence
132
133The project is strict about what a result establishes, and that strictness is
134itself a source of confidence:
135
136- An ignored test, a missing capture, an unreachable lab, a mocked harness or
137 an iOS build that only compiled establishes nothing about native
138 compatibility.
139- A process exit, a VM interruption and a physical power loss are different
140 fault models, and one never stands in for another. The evidence covers
141 transport failures and VM cuts, not every server's lock implementation or
142 real hardware losing power.
143- Notebooks under test are always disposable copies with recorded hashes.
144 Nothing points an editing harness, a replay or the app at an original.
145- Accessibility is tested by asserting on the AccessKit tree the app builds
146 and on the running app's accessibility hierarchy. Nobody turns a screen
147 reader on to listen for it.
arc/ui.md created+127
...@@ -0,0 +1,127 @@
1# The interface kit and the renderer
2
3Snowbound draws its own interface. The section tabs, toolbar, page tabs,
4sidebar, menus and command palette are all painted by the app through one GPU
5renderer, the same one that paints the page. This essay explains why, and how
6the `ui` and `draw` crates are built to make that pleasant rather than
7heroic.
8
9## The look: half OneNote, half now
10
11The shell is a deliberate homage. Section tabs lean over one another at 45°
12with a faint highlight along their tops. The open tab lies on top, in its
13section's colour, and frames the page. That colour shades gently down the
14window and becomes the accent for the whole interface, easing to the next
15section's colour when you switch. Page tabs sit on the right, and the open
16one joins the page with rounded inside corners. It is OneNote 2010's layout,
17drawn with soft shadows, concentric corner radii and a dark appearance. Call
18it half-skeuomorphic: the shapes that made OneNote's notebook metaphor
19legible, without the 2010 chrome.
20
21The motion comes from [File Pilot](https://filepilot.tech): things move
22quickly and never feel like they're waiting on an animation. Animated values
23ease exponentially toward their targets with a short half-life. That is
24frame-rate independent, a retargeted animation continues smoothly from where
25it is, and it settles fast. A context menu opens instantly at the pointer. A
26drop-down grows out of its anchor.
27
28## Why not native widgets
29
30- **The page has to be custom-drawn anyway.** OneNote-faithful layout needs
31 Windows line metrics, OneNote's grid, and its tag and list geometry. No
32 platform text view does that, so a renderer and a text-editing core exist
33 regardless. Drawing the chrome with them costs little more.
34- **One renderer, one pass.** The page and the chrome share a glyph atlas, an
35 image cache and a frame. A text field in the toolbar edits with exactly the
36 keys, chords, click counts and caret movement the page uses, because both go
37 through `draw::edit`.
38- **The shell is not a platform idiom.** Leaning section tabs and a
39 colour-framed page don't exist in AppKit or GTK. The app would be fighting
40 its toolkit.
41- **Portability.** The same interface runs on macOS and Linux today, and the
42 project aims further (Windows, and old versions of OS X).
43
44The platform still owns what it's best at and what people expect to be
45native. That means file pickers, alerts and date pickers (AppKit on macOS,
46zenity or kdialog on Linux), the caret and selection colours, each platform's
47editing chords, the keychain, the traffic lights and window frames. On iOS the
48split goes further (see [platforms](platforms.md)).
49
50## Immediate mode, with memory
51
52`ui` is an immediate-mode kit in the sense Casey Muratori and Ryan Fleury use
53the term. The *API* is immediate: every frame, builder code declares boxes
54from application state, and there's no widget tree to keep in sync with the
55model. The *implementation* remembers plenty:
56
57```text
58frame N
59 route input ─── against frame N-1's layout (hover, press, focus, drags, wheel)
60 build ───────── app code declares boxes; each reads its Signal (clicked, dragging, events…)
61 solve layout ── per axis: pixels · label size · fraction of an ancestor · sum of children
62 overflow is shared out by each box's strictness
63 paint ───────── Layers: primitives under a clip, or a Custom box the host paints
64 after paint ─── work the frame asked for (page requests, saving) runs, then asks for a frame
65```
66
67- **Identity** is a hash of the parent's id and a part the builder chooses. A
68 cache keyed by those ids keeps hover, press, focus, scroll and animation
69 state between frames.
70- **Input is answered one layout late.** Events route against the previous
71 frame's boxes before building, so a box can read what happened to it while
72 it is being declared. It sounds odd and is invisible in practice.
73- **Layout is solved after building**, per axis. A box can be sized in fixed
74 pixels, by its label, as a fraction of an ancestor, or by its children. When
75 siblings overflow, each gives up space according to its *strictness*. That
76 one knob covers most of what flexbox is usually needed for.
77- **Nothing runs while nothing changes.** The kit reports whether it wants
78 another frame (queued input, an animation still settling) and when a timed
79 change such as a caret blink is due. The app sleeps in between.
80- **Lists are virtual for free.** `ui::list` builds only the rows in view from
81 the source data, and holds its place on the selection as items arrive and
82 leave above it. No separate model sits between the data and the rows.
83
84The page is a **custom box**. `ui` routes it the pointer, wheel, key and
85input-method events that land on it, in order. The host hands them to
86`canvas`'s `PageView` and paints the page into a clipped layer of the same
87frame. The page's scrollbars are ordinary `ui` widgets; the canvas only
88reports its scroll bounds.
89
90`ui::popup` builds menus, filterable lists, a colour grid and a fuzzy command
91palette on a popup layer that takes input above everything else. `ui::shell`
92has the OneNote-specific controls: section tabs and compact toolbar buttons.
93The kit doesn't know what a notebook is. `snowbound` assembles the window from
94these parts.
95
96## `draw`: the renderer
97
98`draw` is a small wgpu renderer. A frame is a list of layers, each with its
99own transform and clip. A layer is a flat sequence of primitives: glyph runs,
100SVG icons (tinted or in their own colours), SVG paths filled or stroked,
101rounded or gradient rectangles, pen strokes and raster images.
102
103- **Glyphs** are rasterized with Swash into an atlas, at quarter-pixel phases,
104 so text placed at fractional positions stays crisp and doesn't shimmer as it
105 moves. Icons and paths rasterize once per size and phase into the same
106 atlas.
107- **The renderer never measures text.** Text reaches it through a trait that
108 visits positioned glyph runs. Line metrics stay with whoever laid the text
109 out, which is how the page keeps its Windows metrics while the chrome uses
110 ordinary ones.
111- **Batches** merge consecutive primitives that share a texture and clip,
112 without reordering paint.
113- **Caches are bounded.** The glyph atlas and image cache have fixed budgets.
114 Eviction keeps everything the current frame needs and rebuilds after
115 pressure. Images are filtered in linear light with premultiplied alpha.
116
117GPU readback tests pin the rasterization down: successive quarter-pixel
118translations have to move the ink's centroid in quarter-pixel steps, and
119repaint, cache eviction and a new renderer must all produce identical pixels.
120
121## Testing an interface you can't click
122
123A covered window gets no redraws, so interaction is scripted.
124`SNOWBOUND_REPLAY` feeds the app a file of pointer, key, wait and snapshot
125steps. `--screenshot` draws the whole window offscreen in each appearance
126(the README's screenshots are made this way). `ui`'s own tests build frames
127headlessly and assert on layout, routing and signals.
tools/canvas/README.md+1-1
...@@ -50,7 +50,7 @@ snowbound title bar · toolbar · section frame · page tabs · the page as a...@@ -50,7 +50,7 @@ snowbound title bar · toolbar · section frame · page tabs · the page as a
5050
51`--notebook FOLDER` opens a notebook laid out as OneNote lays one out. The title bar shows the application's icon and the notebook's name; below it two rows of toolbar buttons follow the Home ribbon's Basic Text group, the first nine tags and zoom (only zoom acts yet). Readable top-level sections are tabs in the notebook's order, each leaning over the next at 45° with a faint highlight along its top, and the open one on top, casting a soft shadow and framing the page and the page tabs on the right in its colour, which shades slightly down the window; a tab that opens rises to meet the frame as its outline fades in. The frame's top corners and the page's corners are rounded concentric with the window's. The open page's tab is the page's colour and joins it with rounded inside corners; subpages are indented. The interface follows the system's light or dark appearance, and in the dark one the page's paper darkens and text left at OneNote's automatic colour turns light (except on a highlight), resolved when painting so nothing re-lays out; explicit colours, highlights, pictures and ink keep theirs, except that the background art of OneNote 2010's page templates, recognised by the picture's hash, paints from bundled vector recreations drawn for the paper. Sections take their stored colour's hue at a saturation and lightness chosen for each appearance. `--ui-font FONT_FILE_OR_FOLDER` sets the interface's typeface in place of the system's; fonts are not bundled. The section's colour is also the interface accent, easing to the next section's when another opens. The search box, New Page and the page-tab toggle sit above the page tabs. Command-F searches the page titles; Enter opens the first match and Escape clears it. A page that also changed on another computer shows Keep mine / Keep theirs above it. `--section FILE TITLE` opens one section the same way at that page. Section files must be regular files; discovery rejects symbolic links.51`--notebook FOLDER` opens a notebook laid out as OneNote lays one out. The title bar shows the application's icon and the notebook's name; below it two rows of toolbar buttons follow the Home ribbon's Basic Text group, the first nine tags and zoom (only zoom acts yet). Readable top-level sections are tabs in the notebook's order, each leaning over the next at 45° with a faint highlight along its top, and the open one on top, casting a soft shadow and framing the page and the page tabs on the right in its colour, which shades slightly down the window; a tab that opens rises to meet the frame as its outline fades in. The frame's top corners and the page's corners are rounded concentric with the window's. The open page's tab is the page's colour and joins it with rounded inside corners; subpages are indented. The interface follows the system's light or dark appearance, and in the dark one the page's paper darkens and text left at OneNote's automatic colour turns light (except on a highlight), resolved when painting so nothing re-lays out; explicit colours, highlights, pictures and ink keep theirs, except that the background art of OneNote 2010's page templates, recognised by the picture's hash, paints from bundled vector recreations drawn for the paper. Sections take their stored colour's hue at a saturation and lightness chosen for each appearance. `--ui-font FONT_FILE_OR_FOLDER` sets the interface's typeface in place of the system's; fonts are not bundled. The section's colour is also the interface accent, easing to the next section's when another opens. The search box, New Page and the page-tab toggle sit above the page tabs. Command-F searches the page titles; Enter opens the first match and Escape clears it. A page that also changed on another computer shows Keep mine / Keep theirs above it. `--section FILE TITLE` opens one section the same way at that page. Section files must be regular files; discovery rejects symbolic links.
5252
53Launched without a file, the app reopens the notebooks open when it last closed, at the one shown last, or shows "No notebooks open" with New Notebook and Open Existing. Settings (the open notebooks, the sidebar and recent fonts) are JSON in `~/Library/Application Support/Snowbound/settings.json` or `$XDG_CONFIG_HOME/snowbound`; `--settings FILE` uses another file, `--notebook FOLDER` adds a notebook to them, and `--screenshot` never writes them. Open Existing takes a notebook folder, its `Open Notebook.onetoc2` or a section file, which opens in the notebook its folders make up. A notebook on a mounted SMB share opens through the mount; the embedded SMB client is not yet used for it. The notebook button left of the section tabs opens OneNote 2010's navigation bar: the open notebooks with their sections and section groups, the open section marked; dragging a row reorders a folder or, onto a group, moves into it, and right-clicking a row, a section tab or a page tab offers OneNote's commands (Rename, Delete to the notebook's recycle bin, New Section, New Section Group, Close This Notebook; Delete, New Page, Make Subpage, Promote Subpage). Page tabs drag to reorder. The `+` above the page tabs adds a page at the end of the section, titled with its date and time as OneNote titles one, and puts the caret in its title; until its body is typed in, templates offer our recreations of OneNote 2010's art (Ivy first), its page colours and Informal Meeting Notes' content, written as ops (`corpus/notebook-management`).53Launched without a file, the app reopens the notebooks open when it last closed, at the one shown last, or shows "No notebooks open" with New Notebook and Open Existing. Settings (the open notebooks, the sidebar and recent fonts) are JSON in `~/Library/Application Support/Snowbound/settings.json` or `$XDG_CONFIG_HOME/snowbound`; `--settings FILE` uses another file, `--notebook FOLDER` adds a notebook to them, and `--screenshot` never writes them. Open Existing takes a notebook folder, its `Open Notebook.onetoc2` or a section file, which opens in the notebook its folders make up. A notebook on a mounted SMB share opens through the embedded SMB client, signed in with the password the system keychain holds, and through the mount only when that fails. The notebook button left of the section tabs opens OneNote 2010's navigation bar: the open notebooks with their sections and section groups, the open section marked; dragging a row reorders a folder or, onto a group, moves into it, and right-clicking a row, a section tab or a page tab offers OneNote's commands (Rename, Delete to the notebook's recycle bin, New Section, New Section Group, Close This Notebook; Delete, New Page, Make Subpage, Promote Subpage). Page tabs drag to reorder. The `+` above the page tabs adds a page at the end of the section, titled with its date and time as OneNote titles one, and puts the caret in its title; until its body is typed in, templates offer our recreations of OneNote 2010's art (Ivy first), its page colours and Informal Meeting Notes' content, written as ops (`corpus/notebook-management`).
5454
55The `ui` crate builds every frame from the application's state. A cache keyed by stable box ids keeps hover, press, focus, scroll and animation values, and the previous frame's layout routes the frame's input before building, so a frame answers input one layout late and paints with its own. Sizes are solved per axis after building: fixed, label-sized, a fraction of an ancestor, or the sum of children, with overflow shared out by each box's strictness. The page is a custom box: `ui` routes it the pointer, wheel, key and input-method events that land on it, in order, and the host hands them to `PageView` and paints the page in a clipped layer of the same frame. Page scrollbars are `ui` widgets over that box; the canvas reports only its scroll bounds. Text fields edit through `draw::edit`, as the page does, so keys, clicks and drags select and move the same way in both. Work the frame asks for (page requests, saving) runs after the frame is painted and requests the frame that shows it. Opening a section or page reads it on a thread of its own while the current page stays live; the newest read is laid out and replaces the page once the pictures it shows first are drawn, or after 200 ms.55The `ui` crate builds every frame from the application's state. A cache keyed by stable box ids keeps hover, press, focus, scroll and animation values, and the previous frame's layout routes the frame's input before building, so a frame answers input one layout late and paints with its own. Sizes are solved per axis after building: fixed, label-sized, a fraction of an ancestor, or the sum of children, with overflow shared out by each box's strictness. The page is a custom box: `ui` routes it the pointer, wheel, key and input-method events that land on it, in order, and the host hands them to `PageView` and paints the page in a clipped layer of the same frame. Page scrollbars are `ui` widgets over that box; the canvas reports only its scroll bounds. Text fields edit through `draw::edit`, as the page does, so keys, clicks and drags select and move the same way in both. Work the frame asks for (page requests, saving) runs after the frame is painted and requests the frame that shows it. Opening a section or page reads it on a thread of its own while the current page stays live; the newest read is laid out and replaces the page once the pictures it shows first are drawn, or after 200 ms.
5656