# Inkhaven 1.2.20 — Consolidation, hardening & global themes

The 1.2.19 revision cycle moved fast and left a duplication audit behind
it. 1.2.20 pays that down — fix the bugs the audit surfaced, collapse the
copy-paste into shared helpers, then spend the headroom on the
highest-value polish carryovers. The cheapest time to consolidate the
stemmer / book-walk / AI-stream foundations is *before* the next feature
cycle stacks more copies on them.

It isn't all internal: the cycle also ships three things a writer sees
directly — a **live echo overlay** in the editor, **EPUB cover images**,
and **user-global config** with **15 ready-made colour schemes**.

Tests 1121 → 1160. One new Rust dependency (`fs2`, for the disk
pre-flight — a tiny cross-platform free-space wrapper); everything else
reuses the existing stack.

## What a writer sees

### Live echo overlay (`Ctrl+B Shift+K`)

The `echo-repetition` doctor scan from 1.2.19 now has an inline companion.
Toggle it and every occurrence in the open paragraph of a word echoing
across nearby paragraphs of the chapter is underlined — you catch a reused
distinctive word *as you write*, not at scan time.

The hard part of an echo signal is that it's cross-paragraph (unlike a
filter word you can't decide it from the current line). The overlay reads
the chapter's paragraphs — the open one **live**, so unsaved edits count —
runs the same detector the scan uses, and underlines the words whose echo
window covers the open paragraph. Two-level caching keeps it free: siblings
are re-read only on paragraph navigation, and the echo pass is skipped
while the open text is unchanged, so there's no per-keystroke I/O.

It paints in its own colour (`theme.style_warning_echo_fg`, a muted purple
by default) — deliberately distinct from the repeated-phrase magenta so a
cross-paragraph echo and a within-paragraph repeat never read as the same
finding. Master switch `editor.echo_overlay`; reuses the `editor.echo_*`
tunables; multilingual via the Snowball stemmer (`ё`→`е`-folded for
Russian). New [Tutorial 61](../Tutorials/61-echo-overlay-and-long-paragraphs.md).

### EPUB cover images

`inkhaven epub` now embeds a cover by convention. Drop a `cover.png` (or
`cover.jpg` / `cover.jpeg`) next to `inkhaven.hjson` and it ships — no flag,
no config key:

```text
$ inkhaven epub --book-name "My Novel"
Exporting `My Novel` → EPUB (12 chapters)…
  embedding cover (image/png, 184 KB)
EPUB: my-novel.epub (12 chapters · 424 KB)
```

The cover becomes the reader's library thumbnail (EPUB 3 `cover-image`
property, with an EPUB 2 `<meta name="cover">` for older readers) and the
book's opening page. An empty or unreadable file is skipped with a warning
rather than failing the export; no cover file keeps the text-only output
byte-identical.

### User-global config + 15 colour schemes

Some preferences are yours, not the project's — your colours, your
keybindings. 1.2.20 adds a **config cascade**: after a project's
`inkhaven.hjson` is read, Inkhaven layers your global override files on top,
so a personal preference applies to *every* project without per-project
edits.

Precedence, lowest → highest: built-in defaults → project `inkhaven.hjson`
→ `~/.config/inkhaven/config.hjson` → `~/.config/inkhaven/conf/*.hjson`
(sorted). The global files are **partial** (only the keys you set override)
and **win over the project** — deliberate, because `inkhaven init` writes a
full config, so a project-wins cascade would mask your global colours
entirely. A malformed global file is skipped with a warning (it never
bricks every project); a malformed project file stays fatal. Honours
`$XDG_CONFIG_HOME`.

(Research note: HJSON has no `include` directive and `serde-hjson` adds
none, so a textual include preprocessor would have been fragile — the
value-merge cascade is both simpler and more robust.)

To make it immediately useful, the repo ships **`color_styles/`** — the 15
most popular classic, dark, and light schemes as drop-in `theme` overrides:

- **Dark** — Dracula, Nord, Gruvbox Dark, Solarized Dark, Tokyo Night,
  Monokai, One Dark, Material Ocean, Catppuccin Mocha (the built-in
  default).
- **Light** — Solarized Light, Gruvbox Light, GitHub Light, Catppuccin
  Latte, One Light, Ayu Light.

```bash
mkdir -p ~/.config/inkhaven/conf
cp color_styles/nord.hjson ~/.config/inkhaven/conf/   # Nord, every project
```

New [Tutorial 62](../Tutorials/62-global-config-and-themes.md) +
[`color_styles/README.md`](../../color_styles/README.md).

## Hardening

- **`paragraph-too-long` doctor class** — `inkhaven doctor --scan` flags a
  paragraph whose read time at `editor.reading_wpm` exceeds
  `editor.paragraph_long_secs` (default 180s ≈ 600 words). Info, no autofix
  — a wall of text can be a deliberate breathless effect.
- **Low-disk pre-flight** — the TUI warns at startup when the project's
  volume has less than `editor.disk_warn_mb` free (default 100 MiB), before
  a long session or export. Atomic writes already fail safely on a full
  disk; this is the proactive half.
- **Uncommitted-changes quit guard** — on exit, if the project is a git
  repo with uncommitted changes, a confirm modal asks before quitting
  (`editor.warn_uncommitted_on_exit`, default on). The open buffer
  autosaves first, so the check reflects your latest edits.

## The bug the audit found

**Russian `ё`-fold in three older surfaces.** The 1.2.19 detectors fold
`ё`→`е` before Snowball stemming (the Russian algorithm treats the two as
unified). Three older surfaces stemmed Russian *without* the fold and so
mis-grouped any `ё`-word: the concordance (`пошёл` wouldn't group with
`пошла`), the style-warning overlays, and the character/place name
highlighter (`Фёдор`, `Алёша` failed to light up). All nine production stem
sites now route through one shared helper that always folds — so it can't
silently regress again.

## Consolidation (internal)

Six duplication categories from the audit collapsed into shared helpers,
each net-negative LOC with the same public behaviour (except the `ё`-fold,
the one intended change): a shared `text::normalize_stem` (fold + stem,
fixes the bug above), `cli::resolve_user_book` (was copied 6×),
`cli::book_walk` for filesystem chapter-prose collection (5 near-identical
walks), `ai::stream::collect_blocking` (5 inlined drain loops), a shared
`typst_prose` strip/split, and one home for `is_scene_break`. Where two
copies had genuinely diverged, only the truly-identical subset was unified
and the boundary documented, rather than forcing a false merge.

## Config keys added

| Key | Default | Purpose |
| --- | --- | --- |
| `editor.echo_overlay` | `false` | Start the live echo overlay on. |
| `theme.style_warning_echo_fg` | `#b48ead` | Echo-overlay colour. |
| `theme.style_warning_echo_modifier` | `""` | Echo-overlay text modifier. |
| `editor.paragraph_long_secs` | `180` | Read-time threshold for `paragraph-too-long`. |
| `editor.disk_warn_mb` | `100` | Low-disk pre-flight threshold (MiB). |
| `editor.warn_uncommitted_on_exit` | `true` | Confirm before quitting a dirty git repo. |

Plus the user-global override layer (`~/.config/inkhaven/`) — see
[`CONFIGURATION.md` → "Global overrides"](../CONFIGURATION.md#global-overrides-1220).
