Files
md-manuscript/README.md
T
landon 19dddf30c7 Add offline live spell checking (Hunspell via spellbook)
Spelling is now checked live as you type, fully offline, using the
pure-Rust `spellbook` crate to read Hunspell .aff/.dic dictionaries (no C
libhunspell — the binary stays self-contained; ldd unchanged).

- Canadian (en-CA, default) and British (en-GB) English dictionaries are
  compiled in; more languages are auto-discovered from system Hunspell
  folders and ~/.config/md-manuscript/dictionaries/. A "Spelling" row in
  the top bar toggles live checking and picks the dictionary.
- Misspellings are underlined in red; right-clicking a word opens a menu
  of suggested corrections. A results-panel and one-click fixes work too.
- Checks run on a background thread, debounced ~400ms after the last edit.
  Prose is extracted with pulldown-cmark so code spans, code blocks and
  link targets are skipped; ALL-CAPS initialisms are ignored.
- LanguageTool still layers on top: when its results are fresh they own
  the underlines (spelling + grammar); once you edit, the offline checker
  resumes. The editor underlines, issues panel and fix-apply logic are
  now shared between the two sources.

Bundled dictionaries are SCOWL-derived under a permissive license (kept
in dictionaries/<lang>/license). Adds spell-check unit tests (tokenizer,
code-block skipping, offset mapping, en-CA/en-GB spelling); 40 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9kRuP7JvXoUGdNNeg5ZSs
2026-08-14 06:25:08 -05:00

11 KiB
Raw Blame History

md-manuscript

A small desktop application for Debian that treats a directory of markdown files as an ordered manuscript.

  • Left pane — every *.md file in the workspace directory, in a manual order you set by dragging the handle up and down.
  • Right pane — a markdown text editor for the selected file (optional live preview via the Preview checkbox).
  • Order + contents are saved and version-controlled with git. The order lives in order.json inside the workspace, so it syncs along with the files.
  • Export the whole manuscript as a single concatenated .odt file, with a chapter heading before each file's content.

Built in Rust with egui/eframe. The ODT writer is native (no pandoc/LibreOffice needed at runtime).

Build

cargo build --release
# binary: target/release/md-manuscript

Requires a Rust toolchain. The GUI needs a normal desktop session (X11 or Wayland) with OpenGL — the usual runtime libraries on Debian:

sudo apt install libgl1 libxkbcommon0 libwayland-client0 libx11-6

(Only needed to run the GUI; building does not require them.)

Install

install -Dm755 target/release/md-manuscript ~/.local/bin/md-manuscript

Optionally add a desktop launcher at ~/.local/share/applications/md-manuscript.desktop:

[Desktop Entry]
Type=Application
Name=md-manuscript
Exec=md-manuscript
Categories=Office;TextEditor;
Terminal=false

Use

  1. Set the Workspace path in the top bar (default: ~/Manuscript) — type it, or click 📂 to pick a folder in a native file browser — and press Open. The directory is created if it does not exist.

  2. Press Init git once to make the workspace a git repository. To sync with another machine, add a remote yourself:

    cd ~/Manuscript
    git remote add origin <url>
    git push -u origin main
    

    After that, the ⟳ Sync (git) button commits all changes, pull --rebases, and pushes.

    If you open a folder that is not itself a repository but sits inside one (a parent directory is a git work tree), the app asks whether to use the enclosing repository. Accept it to have ⟳ Sync commit and push to that repo; decline to leave the workspace on its own, where Init git creates a separate repository in the folder.

  3. Create files with New, edit on the right, Ctrl+S (or the Save button) to write to disk. Drag the handles to reorder. Use the Zoom slider above the editor to scale the editor text (0% = default; Reset returns to it), and the Contrast slider to brighten dim editor text (0% = theme default; higher pushes the text toward white in dark mode / black in light mode). Text always wraps to the pane width, so there is no horizontal scrolling. Inline markdown has keyboard shortcuts, applied to the current selection (press again on wrapped text to remove the markers):

    Shortcut Effect
    Ctrl/Cmd + B bold (**…**)
    Ctrl/Cmd + I italic (*…*)
    Ctrl/Cmd + E inline code
    Ctrl/Cmd + Shift + X strikethrough (~~…~~)
    Ctrl/Cmd + K link — [selection](url), with url selected to replace

    Press Ctrl/Cmd + F (or Edit ▸ Find / Replace…) to open the find/replace bar above the editor; Ctrl/Cmd + H opens it with the replace field focused. Type a search term to shade every match in the editor (the current one brighter); Enter / Shift+Enter (or / ) step through them, wrapping around, and scroll the match into view. Aa toggles case sensitivity. Replace swaps the current match and advances; Replace all swaps every match in the file at once. Esc closes the bar.

  4. Set the Chapter title for the open file if you want to override the auto-derived one (the field shows the automatic title as a hint). Leaving it blank falls back to a # Title: line in the header, and if there is none, to the chapter's position in the left pane followed by a period (1., 2., …). Tick Zero-pad # in the top bar to pad those numbered titles with leading zeros to a uniform width (e.g. 03. when there are 12 chapters). Titles are saved to titles.json and sync with git.

  5. Set the Export path (type it, or click 📂 to choose the .odt file in a native save dialog) and press Export ODT to produce the concatenated document. Each file becomes a chapter headed by its title, optionally followed by a caption from its # Slug: line. Every chapter starts on a new page (the first chapter excepted, so there is no blank leading page).

The status bar shows a live word count for the current file, plus the net words added (or removed) to it since the current session started. The count updates as you type, including unsaved edits.

Spelling (offline)

Spelling is checked live as you type, entirely offline — no server, no network. Misspelled words get a red underline; right-click one for a menu of suggested corrections (click a suggestion to apply it). It uses Hunspell dictionaries read by the pure-Rust spellbook crate, so the binary stays self-contained.

  • Two dictionaries are built in — Canadian English (en-CA, the default) and British English (en-GB). Pick one from the Dictionary dropdown on the Spelling row of the top bar; the choice is remembered.
  • More languages are discovered automatically from the usual Hunspell folders (/usr/share/hunspell, /usr/share/myspell, …) and from a per-user folder, ~/.config/md-manuscript/dictionaries/. Drop a matching xx_YY.aff + xx_YY.dic pair in there (e.g. from your distro's hunspell-de-de package) and it appears in the dropdown.
  • Untick “Check as I type” on the Spelling row to turn the underlines off.
  • Code spans, fenced/indented code blocks and link targets are skipped, and ALL-CAPS initialisms (ODT, HTTP) are left alone, to cut false positives.

When you run a LanguageTool check (below), its richer spelling-and-grammar results take over the underlines until you next edit the text — at which point the live offline checker resumes. In other words, LanguageTool is used when it's available and current; the offline checker is the always-on default.

The bundled dictionaries live under dictionaries/ and are derived from SCOWL under a permissive license (kept alongside them in each license file).

Grammar & spelling (LanguageTool)

The editor can additionally check the current file against a LanguageTool server — typically a local instance, so your manuscript never leaves your machine.

  1. Run a LanguageTool server. The standalone server listens on http://localhost:8010 by default (the official Docker image uses :8081 — just point the app at whichever you run):

    # Standalone (needs Java):
    java -cp 'languagetool-server.jar' org.languagetool.server.HTTPServer --port 8010
    # or via Docker:
    docker run -d -p 8010:8010 --entrypoint java \
      erikvl87/languagetool -cp languagetool-server.jar \
      org.languagetool.server.HTTPServer --port 8010 --allow-origin '*'
    
  2. Open Settings ▸ LanguageTool… (from the menu bar, or the button on the Grammar row) and set the connection:

    • Schemehttp for a local server, https for a remote one (TLS is supported).
    • Host / domain — an IP address or hostname (default localhost).
    • Port — the server's port (default 8010).
    • Token — optional; sent as an Authorization: Bearer header for a server behind an auth proxy, or a premium API key. Leave blank for an unauthenticated local server.
    • Languageauto to detect, or a code like en-US, en-GB, de-DE.

    Press Test connection to confirm the settings reach a working server. All fields are remembered in the config.

  3. Open a file and press ✓ Check. The check runs in the background (the UI stays responsive) and issues appear two ways:

    • Underlines in the editor — red for spelling, blue for grammar/style.
    • A "Grammar & spelling" panel at the bottom listing each issue with its explanation and suggested fixes. Click a suggestion to apply it; the remaining underlines re-align automatically.

Editing the text after a check hides the underlines (their positions would no longer be accurate) and disables the fix buttons until you re-check. The checker sends the raw file text, so occasional markdown tokens (e.g. a # heading marker) may be flagged.

Header stripping on export

Manuscript files often carry editorial notes and metadata above the prose that should not appear in the finished document. On export, each file is cleaned up:

  • HTML comments (<!-- ... -->, including multi-line ones) are removed.

  • The header block — everything above the Draft marker line (set in the top bar; default ### Rough Draft:) — is dropped. From that header:

    • # Title: becomes the chapter heading (unless a manual override is set), and
    • # Slug: becomes an italic caption printed beneath the heading.

    Any other header text is discarded. Files with no marker line are exported whole, but stray # Title: / # Slug: lines are still lifted out. Clear the Draft marker field to disable header splitting entirely.

For example, this source file:

# Title: The Gate
# Slug: in which the door will not open
<!-- remember to foreshadow the key -->
outline: she arrives, she knocks, nothing

### Rough Draft:

The prose that actually gets exported begins here.

exports as a chapter titled The Gate, captioned in which the door will not open, containing only the prose below the marker.

Files the app writes

Location Purpose
<workspace>/*.md your manuscript files
<workspace>/order.json the manual ordering (committed to git)
<workspace>/titles.json chapter-title overrides (committed to git)
~/.config/md-manuscript/config.json last workspace, export path, prefs

Markdown supported in ODT export

Headings, paragraphs, bold, italic, both, inline code, fenced/indented code blocks (monospace, whitespace preserved), strikethrough, ordered and unordered (nested) lists, block quotes, horizontal rules, and links. Images are rendered as a text placeholder (not embedded).

A quick reference for all of this — plus the chapter metadata, draft-marker header, and comment conventions — is built in: open Help ▸ Markdown cheatsheet from the menu bar.