Reading & review¶
⌘R renders the Markdown into a typeset reading view. It's not a static
preview: a caret moves through the rendered text with vim motions, and the
whole review workflow — comments and suggested changes — lives here.
⌘R again returns to the source, caret kept in place.
Leave a file while reading and it remembers: reopening it resumes the reading view at the very spot you left, so a long review survives any number of sessions.
Navigating¶
h j k l,w / b / e,0 / $— move the caret through the prose.gg / G— document start / end;⌃d / ⌃u,⌃f / ⌃b, Space — half-page and full-page scrolling.gh— headings overview: an outline jump-list of the document, opened on the section you're in.j/kmove the selection and preview it live — the view follows; Enter stays at the previewed spot, a digit jumps directly, Esc returns you exactly where you were./— search with a live fuzzy hit list (same as the write view);n/Nstep through the hits.- Enter with the caret on a link follows it, routed by target: a
.mdfile opens in place (so a folder of linked notes reads like a small wiki, andother.md#sectionlands on that heading), web and mail open in your default browser,#headingjumps within the document, and anything else (.html, images, PDFs…) opens with the system handler. A.graflilink says it's not supported yet; a link to a missing file whispers not found rather than creating one. Links are set in the zen link blue so they read as links without shouting; clicking works too. - Enter on a
`path:line`reference in inline code opens that source file read-only at that line — see Source references. gbor Backspace — back to the document (or source file) you followed the last link or reference from, exactly where you left it.gl— links overview: the same jump-list popup asgh, listing every link with where it points;j/kpreview, Enter follows the selection.go— open another file without leaving the reading view (see Opening files).
The whisper status¶
The faint line in the card's corner tracks the read: the section you're in,
how far through the document you are, roughly how many reading minutes
remain, and what still awaits review
(§ Architecture · 42% · ~7 min left · 3 changes · 2 comments). The
section breadcrumb follows the caret — so a long document always tells you
where you are without opening the headings overview — and is absent before
the first heading. When the caret is on a link the breadcrumb turns into
→ where ++enter++ goes (a filename, host, or #slug), so you see the
destination before committing. The review counts disappear as you resolve
them: an empty whisper is a finished review. A document that declares a
status carries it at the end of the line (… · status: draft) — see below.
Document status¶
Qt's Markdown reader keeps YAML frontmatter off the page, so a document's
status: is invisible while reading it. textli puts it back in the whisper —
but only for a document that says which values it accepts:
The statuses: line is the opt-in. A document without it is untouched: no
status in the whisper, and gs says there is none to change. The values may
be written inline (draft, review, final), in flow style
([draft, review, final]), or as a block list:
gs opens a card listing them in the order the document declares, opening
on the one it currently carries (on the first, when status: is unset — the
whisper shows status: — until then). j/k move, a digit picks directly,
Enter writes the value into status: and saves the file, Esc leaves it
alone. The status: line is added above statuses: if the document doesn't
have one yet; nothing else in the frontmatter is rewritten, and the change is
one step on the write view's undo stack.
gs is a reading-view gesture. In the write view the frontmatter is right
there on screen — type the value.
Typography¶
The rendered page is set in Literata, a warm serif made for long-form reading, so the read view reads like a typeset page rather than the monospace source — while fenced and inline code keep the monospace face. (The write view stays in its monospace column.) Prose sits on generous leading with clear space between paragraphs, tuned for sustained reading and scaling with the font zoom; code stays tight.
Headings breathe asymmetrically — more space above (closing the previous
section) than below (starting their own) — and h1/h2 carry a thin
rule, GitHub-style, so section breaks are visible from across the room.
Inline code wears a soft chip wash so identifiers pop while scanning,
and blockquotes get hint-gray ink with a thin bar at the left — a
different voice for somebody else's words.
Tables get the paper palette too: a bold header row in the code-band shade, thin warm gridlines, and cell padding for air — real table formatting, so it prints with the rest of the page.
The caret is a soft blue block over the current glyph — vim-style, easy to find on the warm page when you're placing a comment, without pulling the eye the way a hard cursor would.
⌘. turns on section focus: everything outside the section under the
caret rests behind a translucent paper wash and follows the caret as you
move — the rendered twin of the write view's paragraph focus.
f turns on focus reading mode — a deeper, immersive read. The caret
line holds at the centre of the view and the page scrolls under it
(typewriter-style; at the very start or end of the document the caret
travels to the top/bottom instead), while a spotlight centred on the
reading line fades the text away by distance. Because the fade keys off the
caret's position rather than paragraph edges, brightness slides smoothly as
you scroll — a heading or a short line never makes it jump. It persists
across sessions and supersedes ⌘. while it's on (only one focus at a
time). Comments, marks and search stay live beneath the wash.
Code blocks¶
Fenced code sits on a full-width band in a deeper paper shade, so the code
part of a document is visible at a glance. A language tag on the fence
(```python) adds calm syntax highlighting drawn from the zen palette —
keywords in the title blue, strings in the warm red, comments in gray
italic, numbers and constants in amber; everything else stays body ink. No
tag means no colors: the band alone marks the block.
Printing (⌘P) from the reading view prints the typeset page, not the raw
source, and carries the code band onto paper. Images referenced by a
relative path () render against the document's own
folder, so they show wherever you launched textli from.
Mathematics¶
Write math the way pandoc reads it — $E = mc^2$ inline, $$…$$ for a
display formula — and the reading view sets it as real typeset
mathematics: STIX Two Math glyphs in the page's ink, sized to the prose,
inline math riding the text baseline, display math centered on its own
line. Because the source is plain pandoc math, the same file converts to
LaTeX or PDF untouched when a draft grows into a paper. In the write view,
math spans are tinted so a formula reads as a formula while you type it.
The delimiter rules are pandoc's, deliberately strict so prose never turns
into math by accident: the opening $ must hug a non-space, the closing
$ must not be followed by a digit — so "costs $5 and $10" stays prose —
\$ escapes a literal dollar, and a $ inside inline code or a fenced
block is always code. Rendering covers the TeX math subset (fractions,
integrals, sums, roots, matrices, Greek — no custom macros); a formula
that doesn't parse falls back to its raw TeX in a code chip, so a typo
mid-edit never breaks the page. See
examples/math.md
for a tour.
A formula reviews like any other span: select it and c comments it, s
suggests a replacement — or just put the caret on it and press c. The
mark renders over the formula itself, and the annotation lands on the
$…$ source, so what you're reviewing is the maths, not a picture of it.
Charts¶
A pipe table with a <!-- chart: … --> marker on the line right above it
renders as a typeset chart instead of a grid:
<!-- chart: bar x=Quarter -->
| Quarter | 2025 | 2026 |
| ------- | ---- | ---- |
| Q1 | 3.2 | 4.1 |
| Q2 | 5.1 | 4.9 |
The chart replaces the table on the page the way $…$ is replaced by its
formula — the table itself is one ⌘R away in the write view. Because
the marker is an ordinary HTML comment, the source stays plain pandoc
Markdown: GitHub renders the table, pandoc converts it, the comment
vanishes. Two chart types for now — bar (grouped bars, one per series
column) and line (a polyline per series column) — drawn in the page's own
palette and Literata labels, no chrome.
The marker takes three keys at most. type is the word after chart:.
x=<column> names the column whose values label the x axis (default: the
first column). y=<col,col> picks a subset of the series columns (default:
every column but the x one). Series names come from the headers, and a
header's trailing unit — speed [m/s] — lifts to the y-axis label. A bare
table flag keeps the data on the page: the chart renders first and the
table follows it, for when the reader needs the exact values and not just
the shape (<!-- chart: bar x=Quarter table -->). That's the whole
vocabulary: no colors, no sizes, no titles.
Anything the marker gets wrong — an unknown type, an x= that names no
column, a non-numeric cell, a marker with no table under it — falls back to
the plain table, and the marker stays the invisible comment it is. A chart
never breaks the page. It reviews like a formula, too: put the caret on it
and c comments or s suggests, and the annotation lands on the whole
table source. See
examples/charts.md
for a tour.
Images¶
An image is drawn at most three quarters of the prose column's width — filling
the measure also means being as tall as the picture's shape makes it, and that
is a lot of page for something you are mostly reading around. A full-resolution
screenshot can't push the page sideways either. Smaller images keep their own
size — an icon is never stretched to fill the measure. The column
is what the picture follows, so widening or narrowing it (⌘⇧→ / ⌘⇧←)
refits every image, chart and diagram on the page. Plain images and display
formulas sit left with the prose — a formula alone in its paragraph is
indented, so it reads as lifted out of the sentence. Charts and diagrams are
centred.
When the caret rests on an image, or a selection covers one — picking out a span for a comment, say — the picture keeps its own colours and is marked with four corner brackets instead of the wash text gets. A tint over text colours the paper between the letters; over a picture it covers the content. The brackets sit just inside the edges, so they cover a few pixels of the picture rather than claiming room around it. Charts and diagrams count as pictures here; a rendered formula doesn't — it's typeset text that happens to arrive as an image, so it keeps the ordinary wash.
Press ↵ on any image to fill the window with it for a closer look, and
Esc (or ↵ again) to come back. That enlarges the file, not the
scaled-down copy on the page — so keep source images at full resolution
rather than shrinking them to fit. Charts, diagrams and formulas have no
original to go back to (they're drawn at the column's width), so they
enlarge only as far as stays legible.
Diagrams¶
A Markdown image reference to a .grafli file —
, resolved against the document's folder like any
other relative image — renders inline as the diagram itself, drawn by
grafli. textli shells out to grafli's
render CLI (the one stable contract between the two tools) and shows the
result, rendered crisp for the reading column and your display. The
diagram is the picture: the .grafli source stays a plain file beside your
document, editable in grafli, and the Markdown stays portable — GitHub and
pandoc see an ordinary image reference.
It degrades quietly. Without grafli on your PATH, or when a render fails,
the reference falls back to what any image reference does when its target
can't be shown — no error, no interruption. Because textli doesn't watch the
.grafli file itself, a diagram you've changed refreshes on the next render
of the document: a ⌘R round-trip, a file reload, or a zoom.
Note the shapes are different:  is an image and renders;
a plain link [text](d.grafli) is followed like any other link, and for now
says diagram-opening isn't supported yet.
Source references¶
Notes about code cite it the way everyone writes it, in inline code:
`textli/editor.py:2455`, `view.py:80-95`, or just
`editor.py`. In the reading view those are followable — Enter on
one opens the file in place, read-only, at that line, and gb (or
Backspace) brings you back exactly where you were. A design doc can
stay lean and still have its evidence one keystroke away, live rather than
pasted in and going stale.
The page you land on is unmistakably code: monospace on the code band,
syntax-highlighted, sized and widened for code instead of prose, with the
referenced lines lifted out of the band onto the bright page. ⌘+/⌘-
zoom it, / searches it, vim motions move through it — it simply isn't
editable. c, s and ⌘R whisper instead of acting: textli annotates
Markdown documents, and a file you're peeking at isn't one. A source page
is transient, too — it never enters your opening history.
Where it looks:
- Beside the document first, then up through its parent folders — so a
doc in
mgc/groundwork/findstextli/editor.pywithout spelling out../../. - A bare name (
editor.py— the way prose actually names a module) is then looked up in the enclosing repository. If two files share the name, textli whispers not found rather than guessing. - Never past that repository (or your home folder). An unreadable folder reads as "not there" instead of failing.
A reference needs a file extension or a line anchor, so prose chips like
--read, .md or QWidget are left alone. Links work too:
[the module](../textli/editor.py) opens as source, while a link to
something meant to be seen — page.html, an image, a PDF — still goes to
the system handler.
Comments¶
Select a span with v + motions, then:
c— comment the selection (or, with the caret on an existing commented span, reveal and edit that comment). With the caret on a bare formula,ccomments that formula — the image is one character, tedious to visual-select.]c/[c— step to the next / previous comment.- Enter — reveal-edit the active comment;
⇧Ddeletes it.
Commented spans get a soft highlighter wash in the rendered text, so review feedback is visible without shouting. The comment editor opens as a small note tinted like the mark it leaves, in a handwriting face and dark red ink; it grows as you write — wrapping to width, scrolling once it's tall enough — so leaving a remark feels like annotating the margin rather than filling in a form.
Suggestions (track changes)¶
s— suggest a change: with a selection, propose replacement text (leave it empty to propose deletion); without one, propose an insertion at the caret.]s/[s— step to the next / previous suggestion.a/x— accept / reject the suggestion under the caret and advance to the next open one.⇧A/⇧X— accept / reject all suggestions at once.gc— changes overview: a jump-list of every suggestion and comment, with the same live preview asgh(j/kfollow, Enter keeps, Esc restores).p— clean preview: read the prose as if every suggestion were accepted; the source stays untouched until you actually accept.
Removed text is struck through, added text is set in a calm red — accepting or rejecting animates the change into (or out of) the document, and every resolution is undoable in the source.
The format: CriticMarkup¶
Everything above is plain text in your file, using CriticMarkup:
Because the marks live inline, review round-trips need nothing but the Markdown file itself: hand it to a colleague, an AI agent, or a git branch, and the annotations arrive with it.