Repository navigation
Releases: Ceesaxp/Writ.app
Release list
v0.7.0 — PlantUML diagrams, and a split that stays where you put it
Added
-
PlantUML diagrams, rendered by a server you choose.
```plantumlfences render as diagrams — but only when you ask twice. Point Settings at a renderer URL, and addplantuml: trueto a document's front matter. Both are required; neither is set out of the box.The double gate is deliberate. Everything else Writ renders — math, Mermaid, syntax highlighting — is bundled and happens on your machine. PlantUML can't be: there is no engine to bundle. So rendering a diagram means sending that part of your document to a server, and that is a decision per document, not a global switch you forget you flipped. Without both, the fence stays visible as source and nothing leaves your machine.
-
An
emoji: falsefront-matter pragma. Turns off:shortcode:substitution for documents dense with colons, or when you simply want the text left alone.
Fixed
-
The editor/preview split keeps its proportions. Pick Split view, drag the divider, quit, come back — and the preview came back narrow, every time. Two separate bugs, either of which alone pinned the preview to its 280pt minimum.
The first destroyed the value: entering Split uncollapsed the preview, AppKit posted a resize notification for that transient geometry, and it was saved as though you had dragged it there. The guard meant to prevent exactly that depended on the order of an assignment, and the order was wrong.
The second meant the restore could never have worked anyway. The divider position was computed from the outline sidebar's width — but a collapsed pane in AppKit keeps its old width rather than reporting zero, so with the sidebar hidden the calculation was off by its entire width, and the minimum-width clamp quietly swallowed the result. It had only ever worked with the sidebar open.
-
Changing the PlantUML endpoint re-renders the open document instead of leaving the old diagram up.
-
The Preferences window no longer opens 1029pt wide.
-
PDF export's font scale now scales body text, not just headings.
-
HTML export picks up the theme, so the inlined stylesheet matches what you saw.
Performance
Typing on large documents regressed in 0.6.0 and is fixed. Bisected to the autolink scanner, with two independent causes: the HTML escape helper was copying the whole output buffer once per node, and autolink candidate scanning was allocating a string per scan. Both are gone, and the performance gate is back in place.
Notes on the edges
No PlantUML engine ships with Writ, so the feature does nothing until you point it at a server you run or choose to trust. Export never initiates a diagram request — it uses only what the preview has already fetched.
The split fix stores its proportion under a new key, so the first Split view after upgrading opens at 50/50 regardless of where you had left the divider in 0.6.0. Drag it once and it stays.
This build is signed by IPAP TECH LTD rather than Andrei Popov — the same Apple team, post-migration certificate. That is the publisher name macOS will show you; nothing else changes, and upgrading over 0.6.0 needs no extra step.
Under the hood
Writ is now built for two channels from one codebase: the notarized DMG, and the Mac App Store. That brought a privacy manifest, a sandbox audit, and a reproducible archive path.
Download: Writ-0.7.0.dmg (6.2 MB, notarized + stapled)
v0.6.0 — block (rectangular) selection
Added
-
Block (rectangular) selection. Select a vertical rectangle across several lines and edit every line at once — the thing that otherwise costs one trip per line: turning N lines into a list, adding
|borders to a table, stripping a column of leading junk.⌥-drag with the mouse, or ⌥⇧↑/↓ to start a block and ⌥⇧←/→ to widen one that's already live.
Escdrops it.Typing replaces every row's fragment. A zero-width block — a column of carets with no width — inserts at all of them at once, so parking one at column 0 across twenty lines and typing
-makes a list in a single keystroke. Backspace and forward-delete take one grapheme per row and never cross a line boundary. ⌘C joins the rows with newlines, ⌘X cuts the rectangle, and ⌘V distributes a single clipboard line across every row. Every gesture is exactly one ⌘Z.⌥⇧←/→is the standard "extend selection by word" binding everywhere in macOS, so Writ only takes it over while a block is actually live. With no block, word-extend behaves as it always did.
Notes on the edges
A line shorter than the block's left edge gets a caret at its end rather than being padded with spaces — Writ never edits text you didn't select. Under soft wrap a block covers a wrapped line's first visual row only; it works on logical lines, which is what the list-and-table cases want. BBEdit disables rectangular selection under soft wrap entirely.
Undo restores the text but not the rectangle. ⌥-double-click collapses to a plain caret instead of selecting a word.
Under the hood
All the selection arithmetic lives in an AppKit-free model, which is why it could be tested properly: 113 tests, 57 of them new.
The project also gained a headless harness that drives the real NSTextView — the production view on the production TextKit 1 stack, in an off-screen window, with no run loop. App-level editor tests had been recorded as blocked by a Swift 6 toolchain conflict since 0.4.x; the blocker turned out to be the test host rather than the test bundle, so editor regressions can now be covered directly instead of by proxy.
Download: Writ-0.6.0.dmg (6.1 MB, notarized + stapled)
v0.5.6 — PDF export no longer crashes
Fixed
-
PDF export no longer crashes. Two separate crashes, both on the export path, both fixed.
The first killed the app from a background thread. AppKit runs WKWebView's print operation on a thread it spawns itself, so the completion callback arrived off the main thread — and the export teardown it triggered calls WebKit APIs that are main-thread-only. WebKit answers an off-main call by killing the process outright. This fired on every PDF export at default settings, because the default PDF font scale of 85% puts a stylesheet-removal step on the teardown path. Latent since 0.4.8; it surfaced on macOS 27.
The second trapped whenever the preview pane had never been shown in that window. Source-only layout collapses the preview, and macOS never builds the view of a collapsed pane — so the web view the export needs did not exist yet. Exporting from Source-only layout trapped every time, whatever your export settings.
Export now brings the preview up on demand, waits for it to finish rendering, and prints from it. Source-only and Split produce identical PDFs, and the window does not visibly change while it happens.
-
A failed export now tells you. Several paths could leave you with a blank PDF, or no file at all, while the status bar still said "Exported". Those now report failure. In particular, exporting while Writ was in the background could silently produce a blank document — WebKit will not draw for a window the system considers hidden, so Writ now brings the document window forward before printing.
-
Two exports at once no longer clobber each other. Triggering a second export while one was running produced two blank files and could crash the app. The second is now refused with "PDF export already running".
Known limitation
Very large documents (multi-megabyte) can still export blank on the first export from a never-shown preview; exporting again immediately gives the correct file. The preview reports that it has rendered before WebKit has finished laying out a DOM that size. Normal-sized documents are unaffected. Fixing it properly needs a real layout-quiesced signal and is tracked for a follow-up.
Under the hood
The project gained its first app-level test target (WritTests, 10 tests). It is deliberately hostless — a test host would launch a document window and a WKWebView on every run.
Download: Writ-0.5.6.dmg (6.0 MB, notarized + stapled)
v0.5.5 — No more window growth on layout switch
Fixed
-
The window no longer grows every time you switch layout. Cycling Source → Preview → Source widened the document window by a whole editor pane each time, ratcheting outward until it hit the edge of the screen. Switching layout now leaves the window exactly where you put it.
The cause was an ordering bug: returning to Source uncollapsed the editor before collapsing the preview, so for a moment both panes were live. A collapsed pane remembers the width it had when it was hidden — the full window width, in a single-pane layout — so the pair briefly asked for roughly twice the space available, and AppKit granted it by widening the window. It never shrank back.
That also explains the two things that made this one hard to pin down: only Preview → Source triggered it (the other direction already collapsed first), and it took a few cycles to show up (the hidden pane's remembered width has to drift up to full width first). Saving the document appeared to fix it only because a window already at screen width has nowhere left to grow.
Entering Split could widen the window for the same underlying reason, and is fixed too. Showing the outline sidebar still widens the window to make room for it — that part is intentional, and standard macOS behavior.
Download: Writ-0.5.5.dmg (6.0 MB, notarized + stapled)
v0.5.4 — Plain-text paste + scroll-sync fix, configurable line height
Fixed
- Rich-text paste no longer poisons the document. Pasting from a browser or word processor used to drag foreign paragraph styles — hanging indents, mismatched line heights — into your text. Paste is now plain-text only, and the syntax highlighter self-heals paragraph styles on every pass so nothing drifts.
- Scroll-sync echo in Split mode. The editor↔preview handshake no longer snaps the editor back to block starts as you scroll. Suppression timers were replaced with an expected-position mechanism, and continuous sync now runs only in Split layout (switching layout does a one-shot pane alignment instead).
New
- Configurable source-view line height (folded in from 0.5.3). Long, heavily-formatted documents read dense at the font's exact natural metrics. A stepped Line height slider in Settings (1.00–1.50, default 1.25) applies a fixed multiple; every open editor re-applies live. Line positions stay exact — the highlighter's per-fragment font swaps can't shift them.
Download: Writ-0.5.4.dmg (6.1 MB, notarized + stapled)
v0.5.2 — Fix sleep/wake crash + Default layout preference
Fixed
-
Sleep/wake crash + hang.
NSSplitView.autosaveNamehas a bug on macOS Sequoia that fires when display geometry changes during sleep/wake. Sometimes it aborts Writ outright with anNSInvalidArgumentExceptiondeep inside_autosaveSubviewLayoutIfNecessary; sometimes that exception gets swallowed and orphans an internal NSOperation that deadlocks the main thread when a later power-state notification's observer waits on it. Either path loses unsaved state.Fix: don't use AppKit's autosave path at all. The editor↔preview divider position is now persisted to UserDefaults under
WritEditorPreviewSplitFraction, saved only while in split mode so collapse animations can't poison the stored value.If you saw Writ crash or hang after waking from sleep on 0.5.1, this is the fix.
New
- "Default layout" preference (Source / Split / Preview). Defaults to Source so the app opens fast and the preview pane stays out of the way until you need it. Change it in Writ → Settings.
Download: Writ-0.5.2.dmg (7.1 MB, notarized + stapled)
v0.5.1 — Typing on long docs is fast again
The headline win is keystroke latency on multi-hundred-line docs.
Fixed
- Typing latency:
Walker.utf16Offsetin the syntax highlighter was O(N) per cmark node — pinned by Instruments at 54% of a 672 ms typing-hang window on a 1000-line doc. Replaced with a cachedLineTableso per-node lookups are O(line_length). Typing on long docs is now smooth. - TextKit 1 migration: NSTextView is now forced onto TK1. TK2's lazy viewport layout was forcing fragment re-measurement inside the paint cycle even after sub-ms storage edits, contributing to the perceived lag. TK1's eager layout matches Writ's doc-size targets and removes a class of TK2-specific quirks.
Polish
math: falsein front matter now also suppresses the$auto-pair while you're in the source view (closes #27).- Custom CSS hot-reloads when the file changes on disk — no need to re-pick it.
- Preview pipeline: JSON encoding moved off main thread; parser priority lowered so it can't preempt keystrokes; debounce bumped to 500 ms so parses don't fire after every character at normal typing speed.
- Front-matter info cached in the text view, invalidated on storage edits.
Download: Writ-0.5.1.dmg (7.0 MB, notarized + stapled)
Writ 0.5.0 — Alternative Preview Themes + Custom CSS
The headline 0.5.0 release. Two issues that together let users dress the preview pane to taste — four curated themes for common reading styles, plus a custom-CSS escape hatch for everyone whose taste falls outside the curated set.
Closes #19 — Alternative preview themes
New Preview theme picker in Settings. Four bundled themes, each with light and dark variants (follows the system):
- GitHub (default) — the current look (San Francisco body, JetBrains-Mono code, blue links, GitHub-flavored typography)
- Serif (academic) — Iowan Old Style / Charter / Georgia body, justified text, generous leading, centered title block, drop-cap on the first paragraph after a doc-header. For long-form writing that wants to look like a paper.
- Mono (typewriter) — SF Mono throughout (body and headings), dashed dividers, tighter line height. Man-page / printout aesthetic for technical docs.
- Sans modern — Inter / SF Pro body, airy spacing, ornamental `* * *` thematic break. Softer chrome than GitHub for prose-heavy writing.
Theme changes apply live — no document reload, no flicker — via a JS bridge call (`window.Writ.setPreviewTheme(name)`) that swaps the active stylesheet's href. HTML and PDF exports bundle the active theme so the exported file matches the live preview.
The structural rules (code blocks, alerts, math/mermaid containers, front-matter card, document header) stay consistent across themes — they live in the base `theme.css`, and each theme is just a small CSS overlay defining typography and palette.
Closes #6 — Custom CSS for preview
Below the theme picker: Custom preview CSS row with a Choose… button. Point Writ at any `.css` file and its contents are inlined on top of the active theme. Press Clear to remove. The choice persists across launches via a security-scoped bookmark, so the custom CSS survives sandbox restarts.
The custom CSS is loaded as an inline `<style id="writ-custom-style">` element (Swift reads the file via the bookmark, JS sets the style.textContent). Inlining instead of ``-ing avoids the cross-origin file:// load issues sandboxed WKWebViews trip over. Exports include the contents inline too.
The picker handles both the new file case and the clear case via a single button that flips between Choose… and Clear depending on the current state.
Heads up
Hot-reload on external edits (FSEvents-driven) didn't make this release — currently you re-pick the file to pick up edits made elsewhere. Small follow-up tracked for 0.5.1 if there's demand.
Install
Open `Writ-0.5.0.dmg`, drag Writ to Applications. Notarized + stapled.
What's next
The 0.5.0 milestone is closed. Outstanding items belong to 1.0.0 (#11 workspace/project) or are unmilestoned (small QoL like alt-fonts bundling). Open to direction on the 0.6.x line.
Writ 0.4.12
Third small drop toward 0.5.0 — bridges to the bigger themes + custom CSS work coming in the actual minor-version bump.
Closes #1 — Skip spell-check inside code
New checkbox in Settings: "Skip spell check inside code (inline and fenced)", off by default. When on, the macOS spell checker ignores any text inside ```…``` and ` ``` …``` ` — so library names, technical jargon, configuration keys, and code excerpts no longer get red squiggles for being "misspelled".
Implementation goes through two paths so existing markers update immediately when you toggle the preference:
- Filter at the spell-check results delegate (`textView(_:didCheckTextIn:…)`) — any new check whose range intersects a code span gets dropped.
- Clear for already-drawn squiggles via `NSText.setSpellingState(_:range:)` and the TextKit 2 `removeRenderingAttribute(.spellingState, for:)` API. `.spellingState` is a rendering attribute, not a storage attribute, so this lives on the layout manager, not in `NSTextStorage`.
Toggling the preference OFF triggers a fresh `checkTextInDocument` so previously-suppressed squiggles come back.
Install
Open `Writ-0.4.12.dmg`, drag Writ to Applications. Notarized + stapled.
What's next
0.5.0 — #19 alternative preview themes (serif / mono / sans, light + dark) + #6 custom CSS for preview. The headline minor-version bump.
Writ 0.4.11
Second of three staged drops toward 0.5.0. Two render-side text transformations.
Closes #16 — Emoji shortcodes
Write :rocket:, get 🚀. The HTML preview and PDF/HTML export now substitute :name: tokens against a curated table of ~200 GitHub-style emoji shortcodes:
```
Ship it 🚀 — ✨ looks great! 👍
```
renders as: Ship it 🚀 — ✨ looks great! 👍
Unknown tokens (:not_a_real:) pass through verbatim, no warning. Inside backticks / fenced code blocks the literal :rocket: stays as text. :+1: and :-1: aliases included alongside :thumbsup: / :thumbsdown:.
Closes #21 — Autolink bare URLs (GFM-style)
Bare https://example.com and www.example.com are now clickable in the rendered output without requiring <…> brackets or explicit [text](url) syntax. Trailing punctuation stays outside the link, and balanced parens are handled:
Visit https://foo.com.→ "https://foo.com\" link + trailing period(see https://foo.com)→ "https://foo.com\" link + trailing close-parenwww.example.com→ linked withhttp://prefix in the href- URLs inside backticks stay literal
- URLs inside an existing
[text](url)aren't double-wrapped
Implementation
Both live in the new InlineTextTransform helper in WritParser, called from HTMLEmitter for every Text AST node. Editor source stays unchanged — these are purely render-side. 11 unit tests cover the edge cases.
Install
Open `Writ-0.4.11.dmg`, drag Writ to Applications. Notarized + stapled.
What's next
0.5.0 — #1 (skip spell-check in backticks) + #19 (alternative preview themes) + #6 (custom CSS for preview). The headline minor-version bump.