ADR-0022: Reading aids
- Status: accepted
- Date: 2026-09-25
- Status update (Saturday, September 26, 2026): Integrated in the terminal reader: RSVP, bionic reading, the reading ruler and current line, terminal text spacing, and the reading level, with keys and
[reading_aids]settings. Difficult-word marking and the syllable display are in the library but not shown by the reader yet. The GUI has the Fonts dialog. - Status update (Saturday, September 26, 2026): the saved
[reading_aids]settings are plain data intextweaver_store::reading_aids; this crate keeps its working types and converts withFromboth ways (settings). Font choice and resolution (FontFamily,FontSettings, the reading fonts,resolve) moved totextweaver-fonts, which this crate re-exports;describeandto_cssstay here as functions, because they needTextSpacing.
Context
star shipped a cluster of reading aids for readers with dyslexia, low vision, and attention difficulties (docs/star-gaps.md, "Reading aids"; the wiki page "Accessibility and WCAG reading aids"): RSVP (one word at a time in a floating panel, at one of nine screen positions), a reading ruler and a current-line band, text spacing (WCAG 1.4.12), bionic reading, syllable splitting, reading fonts fetched on first use, a Flesch-Kincaid readout, and a difficult-word overlay. Each lived inside the Qt GUI or the curses TUI, mixed into window classes, so none could be tested without a toolkit, and the TUI and GUI versions drifted.
star's audits (the wiki's "star WCAG perceivable operable audit", "star TUI palette contrast audit", and "star TUI reflow and A_DIM audit") found concrete faults in these aids:
- The RSVP context words were drawn at 47 % alpha, failing 4.5:1 on every theme; the terminal box drew them dimmed, which failed on every dark theme.
- The RSVP word size was labelled in points and drawn in pixels, 25 % too small at 96 DPI.
- The terminal RSVP box was never capped: a word longer than the screen lost its tail with no sign.
- The RSVP panel could cover the caret it mirrored (WCAG 2.4.11).
- The ruler was painted over the text at 22 % opacity, pulling body text below 4.5:1 on four themes; the current-line band reused the selection color at 1.1 to 1.9:1 against the page.
- Text spacing had no paragraph spacing, and word spacing was in pixels, so it did not grow with the font.
- The reading level took the first 50,000 characters, counted punctuation as words, and ended a sentence at every
Dr.. - Bionic reading bolded the letters of
COVID19and code-like tokens.
Decision
A new crate, textweaver-aids, depending only on textweaver-core and textweaver-text, holds every aid as pure data in, data out: no threads, no clocks, no terminal or GUI types. The TUI and the GUI render its results. Positions are canonical CharPos (ADR-0002), and words come from textweaver-text's word units, so every aid agrees with navigation and speech about where a word starts and ends.
- RSVP (
rsvp). AWordTrackis built once from a document or range: each word unit, its display text with attached punctuation ("(see),"), its optimal recognition point (the Spritz table: grapheme 1, 2, 3, 4, or 5 by length), and its sentence and paragraph.Rsvpis a state machine over it. Every time-dependent call takesnow, milliseconds on the caller's clock, so tests pass literal numbers and the TUI passes anInstant's elapsed time.tick(now)shows at most one new word per call, so a late tick never skips a word; it keeps the rhythm when slightly late and restarts it when very late. Timing is words per minute (300 by default, 50 to 1,500) with extra time in percent after clauses (50), sentences (100), paragraphs (150), and long words (10 per letter over 8, at most 80).Pacing::Externalkeeps star's behavior: the word follows speech positions throughfollow(pos), shifted bylead_words. Seeking by word, sentence, and paragraph uses star's "more than three words in rewinds to the start" rule for going back. star's nine positions, keys, and GUI placement arithmetic are kept (RsvpPosition,gui_origin), and a terminal layout (tui_box) caps the box to the area, cuts long words with a visible…, keeps the pivot in a fixed column, and moves off a given caret row. Every state change returns anRsvpEventwith a short message to announce, andstatus()gives a sentence for the status line. - Bionic reading (
bionic). star's rule,max(1, round(len × 0.4))leading letters of each word of two or more letters, with the ratio configurable (0.1 to 0.9). Lengths count graphemes. Words with digits, URLs, email addresses, code-like tokens, inline`code`, andCodemarkers are skipped. The output is a list of ranges to embolden; the text never changes. - Text spacing (
spacing). Line height, paragraph spacing, letter spacing, and word spacing, all in multiples of the font size (em), with WCAG 1.4.12's values as a preset and a check that lists which settings fall below them (information, not an error: 1.4.12 asks that content survive those values, and a reader may prefer less). Validation rejects values outside supported ranges;clamped()repairs them. Outputs: CSS for HTML views, CSS custom properties for templates, and the nearest terminal equivalent (blank rows and extra spaces).from_starconverts star's percent and pixel settings. - Fonts (
fonts, added during this work). A family (system UI, sans-serif, serif, monospace, one of star's three reading fonts, or any named family), size in points (6 to 144, advice below 12), and weight (100 to 900). Font files were not bundled at first; since 2026-09-25 thetextweaver-fontscrate bundles Atkinson Hyperlegible Next and Mono and OpenDyslexic (SIL OFL 1.1), which resolve as installed, so only Lexend keeps the download described next. Each reading font records its SIL Open Font License 1.1, home page, and the commit-pinned download URLs star used.resolve()takes a predicate saying which families are installed and returns the family to use, whether it fell back, and which reading font could be downloaded; a missing reading font falls back to the other reading fonts, then to the platform's sans-serif.describe()joins font and spacing into points the GUI can apply;to_css()writes a font stack that works on any platform. - Reading ruler and current line (
ruler). Given the rows a view shows (each a char range and the logical line it belongs to) and the reading position,ruler_rowsmarks each rowFocus,Band,Masked, orNormal, by screen row or by whole wrapped line. Masking is opt-in.TermStyle::recommendedgives terminal cues that never rely on color: the focus line underlined with a gutter bar, the band with a lighter gutter mark, dimming only for masking. - Difficult words (
difficult). star's rule (four or more letters, Zipf frequency below 4.5) against anyFrequencyList, loaded fromword<TAB>zipflines or a ranked list. Capitalised words mid-sentence (names), URLs, and code are skipped.wordfreq's data is not vendored: it is CC BY-SA 4.0 (share-alike, compatible with textweaver's GPL-3.0 but not permissive); SUBTLEX's terms are research-only, and Norvig's counts derive from the LDC Web 1T corpus. - SCOWL word levels, built in (amended 2026-09-25, to also use SCOWL).
third_party/scowl/scowl-levels.zipholds 225,038 words by their smallest SCOWL size (35 to 80) from therel-2026.02.25release, derived bytools/scowl_levels.py(reproducible byte for byte) and kept with SCOWL's copyright notices. AWordListtrait covers both kinds of list;ScowlList::builtin()unpacks the list on first use (27 ms in a release build) into one sorted index (one binary search per word), and a word is difficult when its size is aboveDifficultOptions::max_level(50 by default); a word SCOWL lacks counts as rarer than 80. Marking 1 MB of varied text takes 130 ms. SCOWL ranks dictionary inclusion, not frequency, so it is a rough guide; a frequency list can still be loaded. - Reading level (
level). Flesch-Kincaid grade and Flesch reading ease over a whole document or range, counted with the text crate's word and sentence units; syllables by vowel groups with the usual English corrections. Grades differ from other tools by a few tenths because syllable estimates differ. - Syllable splitting (
syllables). Display text such asread·a·bil·i·tywith anOffsetMapback to canonical positions (ADR-0005): original chars areLiteralspans, separatorsInsertedspans, so highlights and clicks map exactly. Splitting is rule-based (vowel groups, silente, consonant plusle, digraphs and onset blends, a few suffixes), with no dictionary: a hyphenation dependency was not wanted for this component. It is an approximation; TeX-pattern hyphenation (thehyphercrate: MIT or Apache-2.0, no dependencies, 27 KiB for English) is the upgrade if wanted. - HTML helper (
html).wrap_rangesescapes text and wraps ranges in tags, for bionic and difficult-word marking in HTML views.
Dependencies. One addition to the workspace table: unicode-width 0.2 (MIT or Apache-2.0), for terminal cell widths in the RSVP box; it was already in the dependency tree through ratatui. hypher was tried and removed.
Consequences
- Every aid is unit-tested and property-tested without a toolkit: RSVP operation sequences never leave bounds or skip words, the terminal box stays inside its area at any size, ranges stay in bounds and ordered, and syllable offset maps pass
check_invariants. - Frontends share one behavior. The TUI and the GUI differ only in how they paint.
- Building a
WordTrackcosts about as much as segmenting the range into words and sentences withtextweaver-text(about 250 to 400 ms per megabyte in a release build on the development machine, nearly all of it UAX #29 segmentation). Large documents should build off the input thread or a window at a time;WordTrack::range()says where a window ends. - Contrast is the theme's job (
textweaver-theme): the aids never choose colors, and their guidance says which roles to use and that no state may be shown by color alone. - Follow-ups: a frequency-based list to refine SCOWL's levels (12dicts' tiers are public domain); dictionary-quality syllables if wanted; a photometric check of RSVP at high rates against WCAG 2.3.1 (the frontends should change only the word's glyphs, never flash the panel).
See also
- Reading aids: the user guide.
- Reading aids demo: try RSVP, bionic reading, and the ruler in a browser.
- Settings: the
[reading_aids]settings. - ADR-0020: Themes: the colors the aids use.
- Documentation index