A desktop novel-writing app for Windows, macOS and Linux, built with Tauri v2 (Rust
backend) and plain HTML/CSS/JS (no framework, no bundler). One .novel file holds one novel.
Built collaboratively with an AI coding agent (Claude Code / Claude Opus). Architecture notes and the invariants contributors must not break are in CLAUDE.md.
- Chapters and scenes. Chapters are folders; the scene is what the editor edits. Each scene has a status (draft / review / done), POV character, location and time of day. Reorder scenes and chapters by dragging; right-click for rename, duplicate, delete and Move to Chapter. Deleting is immediate: Undo is in the toast that appears (for 8 seconds, latest deletion only).
- Editor. Visual (WYSIWYG) mode and a raw Markdown mode; headings, bold/italic, per-selection font. Pasted HTML (from Word, the web) is reduced to the formatting the app supports.
- Corkboard — the scenes as cards grouped by chapter, with filters, sorting and drag-to-reorder.
- Character graph (
Ctrl/Cmd+G) — relationships as a network; node positions are saved in the.novel. Exports PNG. - World map — a background image with location pins, routes and drawing layers. Exports PNG.
The image is stored inline (as a data URL) in the
.novel, so it grows the file by roughly its own size. - Margin notes (
Ctrl/Cmd+Shift+Mon a selection) — typed, colour-coded comments anchored by character offset so they survive edits. Five built-in types plus custom ones (Settings). Exportable to TXT / Markdown. - Characters, locations, races panels, word/character counters and a word goal.
- Focus mode (
Ctrl/Cmd+Shift+F,Escto leave) and an optional typewriter mode (Settings). - Compile Novel — order scenes, add part dividers and section breaks, title page and table of contents, presets, then export PDF, EPUB, DOCX, HTML or TXT. All of these are generated by the app itself, without external tools. The PDF always uses the bundled IBM Plex Mono (the only embedded font, chosen because it covers Cyrillic); characters it lacks come out blank and are listed in a warning. Chapter headings and the TOC title follow the novel's export language (English or Russian, auto-detected unless set in the Compile settings).
- Quick exports of the whole novel as TXT, HTML or Markdown (top-bar Export menu).
- Full Unicode support, Cyrillic included — in word counts, shortcuts and every export format.
The desktop app keeps everything outside the .novel file in two per-user directories named
after the app identifier com.novelcraft.desktop:
| App config dir | App local data dir | |
|---|---|---|
| Linux | ~/.config/com.novelcraft.desktop/ |
~/.local/share/com.novelcraft.desktop/ |
| macOS | ~/Library/Application Support/com.novelcraft.desktop/ |
same as config |
| Windows | %APPDATA%\com.novelcraft.desktop\ |
%LOCALAPPDATA%\com.novelcraft.desktop\ |
- The novel itself is the
.novelfile you chose. While there are unsaved changes, the app writes them to that file every 30 seconds on its own;Ctrl/Cmd+Sjust does it now. - Crash recovery: every 10 seconds while there are unsaved changes, a copy goes to
working.novelin the config dir. If one is left over at the next launch (after a crash, say), the app offers to restore it. If the file it came from can no longer be written (see Security below), it is restored as an unsaved novel and Save asks where to put it. The copy is deleted once the novel is saved, or when you close and choose not to save. - Backups: each time a save overwrites a
.novel, the previous version is first copied tobackups/<hash of the file's path>/<unix seconds>.novelin the local data dir — at most one copy per 10 minutes per file, none if an identical copy exists. The newest 20 are kept, plus the newest of each day for 30 days. Restore with File ▸ Restore from Backup… (or the button in Settings); the current file is backed up first, so a restore can itself be undone. Backups are keyed by path: after moving or renaming a.novelits old backups are no longer listed (they are still on disk;source.txtin each folder names the original file). - Recent files (the last 5) in
recent.json, the file to reopen at launch inlast_file.txt, window size and position in.window-state.json— all in the config dir. - UI preferences (native title bar, typewriter mode, note display) are stored in the webview's
local storage, not in the
.novel. Per-novel settings (font, word goal, note types, compile presets, export language) are in the.novel.
In browser mode (see Development) there are no backups or recent files, the recovery copy goes to
the browser's localStorage (a novel with a large map image can exceed its quota — the app then
says so), and Save downloads the file.
Ctrl on Windows/Linux, Cmd on macOS.
| Action | Shortcut |
|---|---|
| Save / Save As | Ctrl+S / Ctrl+Shift+S |
| Open | Ctrl+O |
| Bold / Italic | Ctrl+B / Ctrl+I |
| Undo / Redo | Ctrl+Z / Ctrl+Shift+Z |
| Focus mode | Ctrl+Shift+F (Esc to leave) |
| Add margin note | Ctrl+Shift+M |
| Previous / next note (while a note card is open) | Ctrl+↑ / Ctrl+↓ |
| Character graph | Ctrl+G |
| Toggle left / right panel | Ctrl+\ / Ctrl+/ |
| Close the open overlay (graph, map, corkboard, compile, note card) | Esc |
Shortcuts are matched by the physical key when the layout produces a non-Latin character, so
they work on Cyrillic and other non-Latin layouts (Ctrl+S is the same key as Ctrl+Ы). Latin
layouts such as AZERTY or Dvorak keep their own letters, and AltGr combinations are never treated
as shortcuts, so characters like Polish ś still type.
Plain JSON, one novel per file:
Scenes store their text twice: content (HTML, what the visual editor and the exports use) and
markdownContent.
Older files open in newer builds (migrateNovel in src/app.js). A pre-scenes chapter with its
own content becomes a chapter with one scene; a character's old free-text relationships field is
kept as relationshipNotes. A multi-novel library file (top-level novels array) opens its active
novel as unsaved, so it can't be overwritten with a single novel, and offers to split the
library into one file per novel.
Security. A .novel from someone else is treated as untrusted: scene HTML is sanitized against
an allowlist on load, the webview runs under a strict CSP, and the backend only reads or writes
paths you picked in a file dialog (or opened in an earlier session, via the recent list / last
file). The details, which contributors must preserve, are in CLAUDE.md.
- Rust stable (via https://rustup.rs).
- Node.js 20.11 or newer. The Tauri CLI alone runs on Node 18, but the test suites use
import.meta.dirname. CI uses the current LTS. - The system packages Tauri v2 needs (Tauri prerequisites):
- Linux (Debian/Ubuntu) — exactly what CI installs:
Tauri v2 needs webkit2gtk 4.1 (Ubuntu 22.04 / Debian 12 or newer); the 4.0 package of older distributions does not work. These are required even for
sudo apt-get install -y build-essential libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils
cargo check, because thewebkit2gtk-sysbuild script probes them withpkg-config. - macOS — Xcode Command Line Tools (
xcode-select --install). - Windows — Microsoft C++ Build Tools ("Desktop development with C++") and the WebView2 runtime (preinstalled on Windows 11). The MSI and NSIS installer tools are downloaded by the Tauri CLI on first build.
- Linux (Debian/Ubuntu) — exactly what CI installs:
npm install
npm run build # = tauri build: release binary + installersThe bare executable is src-tauri/target/release/novelcraft (.exe on Windows); the installers
are under src-tauri/target/release/bundle/: msi/ and nsis/ on Windows, macos/ (the .app)
and dmg/ on macOS, deb/, rpm/ and appimage/ on Linux. Tauri cannot cross-compile desktop
bundles, so each platform has to be built on that platform — the release workflow
does this on GitHub's runners. To build only one bundle type:
npm run build -- --bundles appimage (or deb, nsis, msi, app, dmg, …).
Builds are not code-signed; see RELEASING.md.
Optional fonts. The editor offers Lora, Merriweather, Inter and Roboto (and the Markdown mode
uses Roboto Mono). Each is taken from the system if installed, otherwise from
src/fonts/<name>.woff2, otherwise a generic fallback is shown. Those .woff2 files are not in the
repository (they are git-ignored); npm run fonts is meant to download them before a build, but
currently saves nothing — see the note in scripts/fetch-fonts.mjs.
Release builds therefore ship without them. (The PDF fonts in src/fonts/pdf/ are committed and
always bundled.)
Icons. src-tauri/icons/ is generated from assets/app-icon.png by npm run icons, and
assets/app-icon.png itself is drawn by node scripts/make-icon.mjs. Both outputs are committed;
rerun them only when changing the icon.
npm run dev # = tauri dev: the app with the frontend in src/ served as-is and reloaded on changeThere is no frontend build step: src/ is what the app loads. For UI work without Rust you can also
run the frontend in a browser — the app detects that Tauri is missing and falls back to file inputs
and downloads. Serve it over http (e.g. python3 -m http.server -d src) rather than opening
src/index.html from disk: under file:// the browser refuses the fetch() that loads the PDF
fonts, so PDF export fails.
| Command | What it runs | Needs |
|---|---|---|
npm test |
node:test unit tests in test/ (pure logic lifted out of src/app.js) |
Node only |
npm run test:e2e |
e2e/browser.e2e.mjs and e2e/flows.e2e.mjs: the real src/ in Chromium via playwright-core, under the desktop CSP |
a Chromium (below) |
npm run test:desktop |
e2e/desktop.e2e.mjs: the built app over WebDriver |
Linux, a debug build, tauri-driver, WebKitWebDriver, a display |
cd src-tauri && cargo test |
backend unit tests (file-access allowlist, atomic writes, backups) | Rust (+ the system packages above on Linux) |
Chromium for test:e2e. playwright-core never downloads a browser. The harness tries, in order:
$CHROME_PATH; Playwright's own Chromium (from $PLAYWRIGHT_BROWSERS_PATH, or the default cache
after npx playwright-core install chromium); an installed Google Chrome. A Playwright Chromium only
works with the playwright-core version it was made for, which is why that dependency is pinned to
an exact version in package.json. flows.e2e.mjs switches to a UTF-8 locale by itself if yours
isn't one (Chromium on Linux otherwise saves a download named Роман.novel as download). A test
there marked { todo: 'bug: …' } documents a known bug without failing the run; drop the todo
when the bug is fixed.
Desktop suite (Linux only).
sudo apt-get install -y webkit2gtk-driver xvfb xdotool # WebKitWebDriver, a virtual display, dialog driving
cargo install tauri-driver --locked
npx tauri build --debug --no-bundle # the suite runs src-tauri/target/debug/novelcraft
xvfb-run -a npm run test:desktop # or plain `npm run test:desktop` inside a desktop sessionIf the binary, tauri-driver or a DISPLAY is missing, the whole suite is skipped, not failed —
check the summary for skipped tests. The two native file-dialog tests additionally need xdotool.
NOVELCRAFT_BIN points the suite at another binary. It runs the app with temporary
XDG_CONFIG_HOME/XDG_DATA_HOME, so your own recent files and backups are not touched.
CI (ci.yml) runs on every pull request to main: syntax check,
npm test, npm run test:e2e and cargo check first (the required Quick check job), then
cargo test plus the desktop suite, and a release build on all three platforms.
See RELEASING.md.
MIT License with the Commons Clause. © 2026 UnKnownHelp69.
- Free to use — personally or professionally — and free to copy, modify and share, as long as nothing is charged for it.
- Not for sale: you may not sell NovelCraft, a modified version of it, or a paid service or support whose value comes mainly from it. Copies and modified versions must keep the license, Commons Clause included.
- Your writing is yours. The license covers the program only. What you write and export with it — texts, books, PDF/EPUB/DOCX files — belongs to you, and you may publish and sell it freely.
Because of the selling restriction this is a source-available license, not an OSI open-source one. Versions published before this change remain available under the plain MIT License.
{ "version": "1.0", "title": "My Novel", "settings": { "fontSize": 18, "defaultFont": "Georgia, serif", "wordGoal": 80000, "customNoteTypes": [], "noteTypeColors": {}, "language": null, // null = auto-detect "worldMap": { "backgroundImage": "", "locations": {}, "routes": [], "layers": [], "baseLayers": {} }, "compilationPresets": [] }, "chapters": [ { "id": "…", "order": 0, "title": "Chapter 1", "wordCount": 0, "collapsed": false, "scenes": [ { "id": "…", "order": 0, "title": "Scene 1", "content": "<p>…</p>", "markdownContent": "…", "wordCount": 0, "povCharacter": null, "location": null, "timeOfDay": "morning", "status": "draft", "notes": [], "createdAt": "…", "modifiedAt": "…" } ] } ], "characters": [ /* …, relationships: [{ targetCharacterId, type, strength, status, … }], graphPosition: { x, y } */ ], "locations": [ /* name, type, description, history, atmosphere, notes */ ], "races": [ /* name, description, appearance, culture, history, traits */ ] }