Changelog¶
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]¶
Added¶
- The reading view shows a document's frontmatter status, and
gschanges it (#69) — Qt keeps YAML frontmatter off the rendered page, so a document'sstatus:was invisible while reading and could only be changed by flipping to the write view and typing a value from memory. The whisper now ends withstatus: draftfor a document that carries one, andgsopens a card listing the values in the order the document declares them, on the one it currently has:j/kmove, a digit picks directly,Enterwritesstatus:and saves the file,Escleaves it alone.
A document opts in by listing its own legal values, so textli never invents a vocabulary for someone else's workflow:
Inline, flow ([draft, review]) and block-list forms all read. A document
without a statuses: line is untouched — nothing new in the whisper, and
gs says there is nothing to change. Only the status: line is rewritten
(added above statuses: when the document has none yet); the rest of the
frontmatter is left byte for byte, and the edit is one step on the write
view's undo stack. gs is a reading-view gesture: in the write view the
frontmatter is on screen already.
Changed¶
- PySide6 floor raised to 6.8 — Qt 6.8 is where the Markdown reader parks frontmatter in the document's metadata instead of rendering it as prose, which is what makes the status readable rather than a block of raw text at the top of the page.
[0.8.0] - 2026-08-15¶
Added¶
↵on an image in the reading view fills the window with it (#59) — a picture is drawn at the prose column's width, which is right for reading around it and too small for reading into it; a dense.graflidiagram or a chart with a dozen labels couldn't be inspected without leaving textli.Esc(or↵again) puts the page back with caret and scroll untouched. It works for every kind of image the page can hold — ordinary Markdown images, charts,.graflidiagrams and rendered math — and↵keeps its existing jobs, so a link or source reference under the caret still wins.
It covers the window rather than the card deliberately: charts and diagrams are rasterised at the column width and the card is only ~130px wider, so expanding into the card alone would have enlarged a diagram by about a tenth. Only ordinary Markdown images have a full-size original to enlarge from; the rendered kinds are scaled up from their bitmap, and that scaling is capped at 4x so a small formula becomes readable rather than a smear. Re-rendering those at the expanded size is left for later.
- The sheet now lies on a desk (#56) — the surround was a flat fill, which
is most of what you look at and read as a void rather than a room. It wears
the same two cues
paper.pyalready gives the page, grain and a horizontal light falloff, lit from the same place: the desk's light frame is centred on the card but spans the window, so one ramp runs bright at the sheet and sinks into the corners instead of each surface having its own light. It rides the existing⌘⇧P, and where a host supplies a canvas the desk stops at its edge — that canvas keeps its own pixels under the gentler wash, so an embedded textli never paints over its host. Two things a dark ground forces and the sheet's own numbers could not carry: the desk's grain is a translucent overlay rather than a colour-baked tile, since an embedding host paints the ground and the desk cannot bake a colour it does not know (which also means its tiles can never go stale against a palette switch), and its falloff shades toward black rather than the warm body ink the sheet uses — body ink is lighter than the dimmed surround, so reusing it brightened the corners it was meant to sink.
Changed¶
-
An image travels into and out of the full-window view instead of cutting (#64) —
↵grew the page into the expanded picture in a single frame, andEsccut back the same way, which left the reader re-finding their place because nothing connected the small picture to the large one. It now grows out of where it sits on the page, with the backdrop fading in behind it, and goes back the same way; the eye follows one object rather than reconciling two. Same durations and curves as the editor's own fade in and out, so there is one motion vocabulary rather than a second. Reversing mid-flight picks up from wherever the picture currently is, so a quick↵ Escdoesn't snap, and keys stay swallowed for the whole of the closing tween — it is still on screen, so nothing should land on the document underneath. -
A display formula is set on the left, indented, rather than centred (#65) — it still reads as lifted out of the sentence rather than starting one, and it lines up with the prose the way the rest of the page does. Two body ems of indent, resolved through the view's font metrics so it holds at any zoom and on any platform. An inline
$…$is untouched — it stays where it sits in its sentence. Charts and.graflidiagrams keep the centring they had. -
A picture is now drawn at most three quarters of the prose column (#62) — filling the measure also means being as tall as the aspect ratio makes it, which is a lot of page for something the reader is mostly reading around: a 4:3 image in a 692px column took 519px of height and now takes 389px. No detail is lost, since
↵fills the window with the original (#59). Charts and.graflidiagrams are rasterised at the capped width rather than rendered to the full column and drawn scaled down, so they stay crisp; the cost is slightly less room for a chart's labels. Still downscale only — a picture already narrower than the cap keeps its own size.
A width cap rather than a height one on purpose. A height cap would target the wasted space more directly but would depend on the window, so the same document would lay out differently on a laptop and a monitor; the sheet's geometry is meant to hold still. Alignment is unchanged: plain images stay left with the prose, while display formulas, charts and diagrams keep the centring they already had.
textli notes.mdnow opens the reading view (#58) — reading a document is the common case and writing one the exception, so reaching the page no longer costs a⌘Revery time.textli -w notes.md(--write) opens the write view instead;-r/--readkeeps working and now names the default, so an existing alias or script doesn't break, and asking for both at once is a usage error rather than one silently winning. Only the standalone CLI changes —ZenMarkdownEditor(start_in_read=…)keeps its meaning, and nothing an embedding host passes today behaves differently.
Resuming a file's view had to become symmetric to survive the flip. The
restore only ever toggled into the read view: a file left writing came
back writing because writing was what the app opened in, not because
anything carried it there. With reading the default, that half of "resumes
where you left it" would have quietly stopped holding, so the restore now
carries a file back in both directions. Reading is the default for a file
with no remembered view; -r or -w overrides whatever is remembered.
Fixed¶
Escquit the standalone app outright (#63) — it autosaved and closed, so one reflexive keypress ended the session and cost the reader their window and their place. Nothing was ever lost from the file, but nothing about reading a document suggests that key should end the program.Escstandalone now only steps back: leave visual mode, close an overlay, cancel the headings overview, put an expanded image away. With nothing left to back out of it flashes how to quit and does nothing else. An embedding host is untouched — there the editor is modal andEscis how it hands back, which is what the new flag defaults to, and⇧Escstill means cancel-and-discard.
⌘Q (Ctrl+Q elsewhere) now quits, because Esc was the only keyboard exit
there was: macOS hands Qt applications a ⌘Q through the default application
menu, but on Linux and Windows the window's close button would otherwise have
been the only way out.
- The caret and the selection flattened an image instead of marking it
(#61) — a wash reads well over text, where it tints the paper between the
letters, but over a picture it covers the content and dulls the thing you
are trying to look at. Measured against a
#cc4422test image, the caret took it to#92443aand a selection to#c28c85. Two mechanisms were doing it: textli's own block caret fills the glyph cell under it, and an image is one glyph, so the cell was the entire picture; and Qt paints the selection inside the document layout with no hook to exempt an image. An image under the caret or inside the selection is now redrawn from its own resource over whatever wash landed on it, and marked with four 4px corner brackets in the caret's colour at full strength (the wash's alpha is low because it covers a whole cell, and at that alpha a thin stroke barely registers) or the selection colour. Text is untouched.
The brackets sit inside the picture rather than around it, their outer face flush with its edge. An outline hung on the outside has to line up with the drawn edge exactly — and it didn't, because the laid-out rectangle needed the block's left margin added back and was taking the line's descent as part of the image's height — so it read as broken. Both are fixed, and drawing inside means nothing claims layout room the image never reserved; the cost is a few pixels of content at each corner.
A rendered formula keeps the wash. It is typeset text that happens to
arrive as a bitmap: it sits in a sentence, it is small, and a tint reads
over it exactly as it reads over the letters around it. Charts and .grafli
diagrams go the other way and get the brackets — they are pictures with
detail to inspect, and a wash flattens them the same way it flattens a
screenshot.
- An image wider than the prose column broke the page sideways instead of
scaling down (#60) — Qt draws an image at its natural pixel size, so a
2400px screenshot in a 700px column pushed the document's ideal width to
2408px and grew a horizontal scrollbar. Charts,
.graflidiagrams and math never did this, since they're rasterised at the column width and carry an explicit size; a plaincarried none. Wide images are now scaled down to the column with their aspect ratio kept, and the document resource is left at full resolution so↵(#59) still enlarges the original. Downscale only — an icon keeps its own size rather than being stretched to fill the measure. This also fixes@2xassets, which rendered at twice their intended size because Qt reads natural pixel size as logical size.
The real damage was second-order: authors and agents worked around the
overflow by shipping pre-shrunk files, and once ↵ could expand an image
that downscaled copy was all the detail there was. The bundled AI skill now
says to ship images at full resolution and why.
-
Nothing re-rendered when the reading column changed (#60) — a width step or the full-width toggle resized the card, but charts and diagrams kept the bitmap they were rasterised at, so widening the column left them small and soft. The read view now re-renders when the column moves, coalesced so a burst of width steps settles into one render, and carries the reader's position across as a fraction of the document rather than a pixel offset — a narrower column makes the same document taller. The threshold that triggers it sits above the scrollbar's width on purpose: fitting an image changes the document's height, which can bring the scrollbar in or out, which moves the column again, and chasing it exactly never settles.
-
Read-view prose sat too far apart to cohere, and list items had no grouping gap (#54) — the reading leading was set with Qt's
ProportionalHeight, which is a percentage of the font's natural line box rather than of the em. Against Literata's 1.485em box the configured 145% rendered at roughly 2.2em, well past the point where consecutive lines still read as one block: the eye lost the line on the return sweep and paragraphs came apart into evenly-spaced stripes. It also scaled each line by the tallest fragment on it, so a line carrying inline code was led differently than a plain one and the leading wobbled inside a single paragraph. Leading is now an absolute line box of 1.55em, resolved through the view's own font metrics so it means the same thing at every zoom and on every platform — the old gaps multiplied the point size as though a point were a pixel, which holds only at 72 dpi. List items now get an explicit gap of their own, smaller than a paragraph's, so the three breaks finally rank: line, then item, then paragraph. Headings and any block holding an image keep their natural height, which a fixed box would crop. That exemption also stopped the leading inflating an image's own line: a picture is the line's tallest fragment, so 145% of it left 45% of the image's height as blank space underneath — a measured 78px gap below a 173px picture, before the paragraph gap. -
CriticMarkup marks were dropped after an inline code span that wrapped across lines (#52) — CommonMark lets a code span cross a line, and hard-wrapped prose wraps long spans routinely, but the inline-code pattern refused newlines. The closing backtick of a wrapped span was then left over as an opener: it paired with the next backtick anywhere later in the document and everything between was masked as code, so any
{++,{--,{~~or{==in that stretch was read as documentation and silently skipped. The mark fell through to GFM strikethrough, which made the document look broken rather than the parser. Spans may now cross lines but stop at a blank one, so a genuinely unpaired backtick still can't swallow the rest of the file.
[0.7.1] - 2026-07-29¶
Fixed¶
- The paper sheet kept the previous palette when the theme switched with no editor open (#49) — the grain tile bakes the page colour into its colour table, but the cache was keyed by device-pixel-ratio alone, from back when that colour was a constant. A host switching palettes between editors (start dark, close the note, go light, reopen) got the old theme's grain under the new theme's ink. The cache is now keyed by page colour too, so a tile can never outlive its palette — and both palettes stay cached, so switching back and forth reuses tiles instead of rebuilding them.
[0.7.0] - 2026-07-27¶
Added¶
- PDF export from the CLI (#47) —
textli notes.md --pdfwritesnotes.pdfbeside the source and exits without opening a window, so a draft becomes a shareable paper from a script or a Makefile; an explicit path (--pdf out/paper.pdf) wins over the default. What lands on the page is the reading view's typeset page — the same document⌘Pprints, code band baked in — so charts,.graflidiagrams, math, tables and the annotation layer all come along as what they already are. Page size follows the system default, matching what the print dialog would have produced.ZenMarkdownEditor.export_pdf(path)is the same thing for embedders, and it leaves the reader in whichever view they were in.
Fixed¶
-
Printing on the dark theme baked a dark code band into the page (#46) — body ink comes from the view's stylesheet, which a printed document never carries, so on the dark palette a page came out as black prose on white with a near-black
#272421fence behind light-blue keywords. Printing and PDF export now always render on the light palette and restore the screen afterwards, scroll position included — paper isn't themed. A regression from #44, which is where the second palette first reached the print path. -
Long code lines lost their tail on paper — Qt's Markdown import marks fences non-breakable, which the wide reading column hides; on a page the overflow was cut off rather than wrapped, silently dropping the end of any line that didn't fit. Fences now wrap when printed.
[0.6.0] - 2026-07-26¶
Added¶
- Dark theme, as the counterpart to the warm palette (#44) — a second
palette in the same warm hue family: a low-key ground (
#1E1C19, shared with grafli so an embedded editor sits on its host's board instead of glowing against it) carrying the light theme's paper colour as its ink.⌘⇧Dswitches and persists it. Two counterpart palettes, not a theme engine: no third theme, no settings surface.
Colours moved out of constants.py into a new textli/theme.py — a frozen
Palette with LIGHT/DARK counterparts whose module globals are rebound
on switch, so every consumer reads the active value with no re-import.
Dark values are derived from the light theme's measured relationships
rather than carried across the inversion: roles readable by luminance
reproduce their contrast ratio against the ground they are actually drawn
on (syntax tokens against the code band, not the page), while roles
readable by hue — the amber number token measures only 1.74:1 on the light
band — keep their hue and clear a readability floor instead. Anything drawn
on a fill takes its ink from that fill through a single ink_on(), so a
pinned light ink can't survive the ground changing under it. Tests assert
those rules rather than the colours, including a guard that fails if any
module re-freezes the palette with a from theme import ….
Along the way the scrollbar stopped being the platform's near-white column — invisible on paper, the brightest thing on the page once the ground goes dark — and became a thin muted handle on a transparent track in both palettes.
- Theme API for embedding hosts (#44) —
textli.set_theme("dark"),toggle_theme()andis_dark()on the package root, plus atextli.theme_changedsignal. OpenZenMarkdownEditorandInlineVimEditorinstances listen to it and restyle themselves in place — re-running their stylesheets, syntax highlighting and the read view's render (math and chart images bake their ink) — so a host only makes the one call.ZenMarkdownEditor(..., theme_name="dark")opens straight onto the dark ground for a host that is already dark, andapply_theme()is there for hosts that change colours without going throughset_theme. Theme persistence belongs to the standalone app alone, so it can never override a host's choice.
[0.5.0] - 2026-07-23¶
Added¶
-
.graflidiagrams in the reading view (#42) — a Markdown image reference to a.graflifile (, resolved against the document's folder like any relative image) renders inline as the diagram grafli draws. textli shells out to grafli'srenderCLI — the one stable contract between the tools, no import and no Python coupling — and shows the produced image, rendered at the reading column's width and the display's pixel ratio so it stays crisp. Renders are cached by source path, mtime and target metrics, so re-renders (zoom, view toggles, file-watch reloads) don't re-invoke the CLI unless the diagram or the metrics actually changed. It degrades quietly: without grafli onPATH, or on a failed or timed-out render, the reference falls back to ordinary image-reference behavior — no error, no page break. Image refs only; a plain[link](d.grafli)keeps its stay-tuned notice. -
Charts in the reading view (#41) — a
<!-- chart: … -->marker on the line above a pipe table renders it as a typeset chart instead of a grid, the way$…$renders as a formula: the chart replaces the table on the page, the table stays one⌘Raway in the write view. Two types —bar(grouped bars, one per series column) andline(a polyline per series column) — drawn with a hand-rolled painter in the page's own palette and Literata labels, no chart library and no new dependency. The marker takes three keys at most:type,x=<column>(the x-axis labels, default the first column), andy=<col,col>(a subset of series columns, default all but the x one); series names come from the headers and a header's trailing unit (speed [m/s]) lifts to the y-axis. A baretableflag keeps the data on the page — chart first, table under it — for when the reader needs the exact values and not just the shape. Because the marker is a plain HTML comment the source stays portable pandoc Markdown — GitHub renders the table, pandoc converts it, the comment vanishes. Anything malformed — unknown type, anx=/y=that names no column, a non-numeric cell, a marker with no table — falls back to the plain table and the marker stays invisible, so a chart never breaks the page. A chart reviews like a formula: caret on it,ccomments orssuggests, and the annotation lands on the whole table source; it prints with the page. Seeexamples/charts.md. -
Installable AI skill (#39) —
textli skill installputs a bundled agent skill into your AI tools' skill directories (claude,codex,opencode, orall), teaching them to author Markdown for the reading view (headings as navigation, pandoc math, followablepath:linesource references) and to collaborate through the annotation layer: the human's{>>comments<<}come back as track-change suggestions, never silent rewrites, with a convergence protocol for when to leave the doc and ask directly. Genre playbooks ship as on-demand references — decision doc, learning doc, and a scientific paper guide with a worked example,examples/paper.md, whose object of research is textli itself.textli skill checkreports per-tool status (ok/stale/modified/missing) against the version stamped at install time,textli skill uninstallremoves, and baretextli skillprints the whole skill to stdout for tools without a skill directory.
[0.4.0] - 2026-07-17¶
Added¶
-
TeX math in documents — pandoc-style
$…$inline and$$…$$display math renders as typeset formulas in the reading view (STIX Two Math via ziamath — pure Python, no LaTeX install), sized and inked to blend with the Literata prose: inline math rides the text baseline, display math stands centered on its own line, and a formula can be commented or suggested on from the reading view like any other span — its image maps back to its$…$source, the mark renders over the formula, and it prints with the page. The write view tints math spans as you type. Delimiter rules are pandoc's and deliberately strict — prose dollars ("costs $5 and $10"),\$escapes, and$inside code never trigger math — and 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. A cheatsheet of the supported constructs ships asexamples/math.md. -
Follow source references from the reading view (#37) — notes about code cite it the way everyone writes it, in inline code:
textli/editor.py:2455,view.py:80-95, or justeditor.py. Those are now followable.↵opens the file in place as a read-only code page — monospace on the code band, syntax-highlighted, sized and widened for code rather than prose, with the referenced lines lifted out of the band onto the bright page — andgb/⌫walks back to exactly where you were reading. So a design doc can stay lean and still have its evidence one keystroke away, live, instead of pasted in and going stale. References resolve beside the document first, then up through its parent folders (a doc inmgc/groundwork/findstextli/editor.pywithout spelling out../../); a bareeditor.pyis looked up in the enclosing repository and, if two files share the name, whispers not found rather than guessing. The search never leaves that repository, and an unreadable folder reads as "not there" instead of failing. Prose chips are left alone — a reference needs a file extension or a line anchor, so--read,.mdandQWidgetstay unfollowable. A link to a text file opens as source too, while a link to something meant to be seen (page.html, an image) still goes to the system handler. Source pages are peeks, not buffers: no editing, no comments or suggestions (c/s/⌘Rwhisper), and they never enter the open history. -
Paper surface — the page is material now, not a flat hex: whisper-level procedural grain plus a horizontal light falloff (fully bright across the reading column, a few percent darker in warm ink toward the window edges) painted under the text of both views. Both cues sit below conscious notice — texture felt, not seen — so the zen direction stays intact while the page stops feeling clinically flat.
⌘⇧Ptoggles the surface off for the flat page (persists).
Fixed¶
- Rendering a large document is no longer slow: the read view's styling passes ran their format merges outside an edit block, so every one settled the whole document layout again — about 1 ms per merge on a 4,500-line file, against 1.5 µs batched — and the code-block pass matched each syntax span against every line of its fence rather than just the lines the span covers. Both costs grew faster than the document, so they were invisible on ordinary prose and crippling on anything long: together they took 15 s to render a 4,500-line file, now 0.37 s (and well under 0.1 s for an ordinary one).
[0.3.0] - 2026-07-12¶
Added¶
- A reading face for the read view — the rendered page is now set in Literata (a warm, book-oriented serif, bundled OFL), so long-form prose reads like a typeset page instead of the monospace source; fenced and inline code stay in JetBrains Mono, and the write view keeps its monospace column.
- Reading rhythm in the read view — rendered prose sits on generous line-height with clear space between paragraphs (code stays tight), scaling with the font zoom, so long-form reading breathes; pairs with the reading face.
- Undo / redo in NORMAL mode —
uundoes and⌃rredoes the last change in the write view (andInlineVimEditor), riding the editor's native undo stack, so a NORMAL-mode edit is reversible without dropping to INSERT for⌘Z. - Vim counts, VISUAL operators and yank/paste in the write view — a leading
count repeats the next motion or edit (
3j,2dd,5w,4x);vstarts a VISUAL selection that the motions extend, withd/y/cto delete, yank or change it; andyy/ywyank,p/Ppaste, sharing one register with the delete commands (soddthenpmoves a line). Text objects, dot-repeat, macros and named registers stay out — the write view keeps to vim essentials. - Headings overview (
gh) in the write view — the reading view's outline jump-list now works over the source too:ghparses the document's headings (skipping fenced code),j/kpreview each live,Enterkeeps the spot andEscrestores where you were — so a long draft navigates by structure in either view. - Find & replace — from a
/search in the write view,⇥reveals a replace field:↵replaces the current match and advances,⌃↵replaces every one (a single undo step). Replace targets the literal occurrences of the query (case-insensitive), not the fuzzy hit list — replacing a fuzzy match would be surprising. The reading view stays find-only, since its rendered page is read-only.
Fixed¶
- Embedded editors now render comments in the bundled Caveat handwriting face:
the public widgets (
ZenMarkdownEditor,InlineVimEditor) register the bundled fonts on construction (idempotently), so a host like grafli no longer falls back to the plain body font the way only the standalone CLI avoided.
[0.2.0] - 2026-07-10¶
Added¶
goopen-file dialog — switch files without leaving the editor, from either view: opening history is fuzzy-matched over full paths, the filesystem completes shell-style one segment at a time;Tabcompletes, folders descend. Works in the reading view too (and stays there)./in-document search — a live, ranked hit list (exact phrases above word matches; fuzzy matches never cross a word boundary), preview-as-you- select,Enterjumps,Escrestores,n/Nstep through highlighted hits in both views.- Overview live preview —
gh/gcopen on the current section and preview rows asj/kmove;Enterkeeps the spot,Escreturns exactly where you were. - Whisper status line — one faint line in the card's corner: vim mode, word count and session delta while writing; progress, minutes left and open review items while reading. Hidden while any overlay card is up.
- Position memory — every file resumes where you left it: view mode,
caret and read-view scroll (
-rand#heading-slugstill win). - Live reload of external edits — textli watches the open file and
reflects changes made outside it (an agent applying your comments, a
gitcheckout, another app) in place, keeping the view, caret and scroll, with a faint reloaded whisper — no restart to see them. Its own autosaves are never mistaken for external writes. If the buffer has unsaved local edits when the file changes underneath, it warns and keeps them (they win on the next save) instead of clobbering; two-sided conflict reconciliation is tracked separately. - Typewriter scrolling (
⌘T) — the caret line holds steady while the page moves; persists across sessions. - Follow links with
Enter— in either view, the caret on[text](url), an<autolink>or a bare URL follows it: web and mail targets open in the default browser,#heading-slugtargets jump within the document. Rendered links wear the zen link blue instead of Qt's palette default. - Code blocks stand out in the read view — fenced code sits on a full-width band in a deeper paper shade, with em-scaled breathing room around the code; a language tag brings calm zen-palette syntax highlighting (via Pygments, now a dependency): keywords blue, strings warm red, comments gray italic, numbers amber.
- Heading rhythm in the read view — clearly more air above a heading
(closing the previous section) than below it, per level and scaled with
the font zoom;
h1/h2carry a thin GitHub-style rule. - Inline-code chips & blockquote voice — inline code in prose wears a soft wash chip; blockquotes get hint-gray ink and a thin left bar.
- Section focus while reading —
⌘.now also works in the read view: everything outside the section under the caret rests behind a translucent paper wash, following the caret; comments and search stay intact beneath it. - Focus reading mode (
f) — an immersive read: the caret line locks to the centre and the page scrolls under it (typewriter-style, pinning at the document ends), while a spotlight centred on the reading line fades text by distance — brightness slides smoothly as you scroll instead of snapping at paragraph edges. Persists across sessions and supersedes⌘.while on. - Whisper breadcrumb while reading — the read-view status leads with
the section under the caret (
§ Architecture · 42% · ~7 min left), so a long document always tells you where you are; empty before the first heading, and it follows the caret. When the caret is on a link it turns into→ where Enter goes(filename, host, or#slug). - Table styling in the read view — Markdown tables get a bold header row in the code-band paper shade, thin warm gridlines, and cell padding; real table formatting, so it prints too.
- Visible read-view caret — a soft zen-blue block over the current glyph (vim-style) replaces Qt's near-invisible 1px line, so it's easy to see where you are when placing a comment; the letter still shows through.
- Comments read like margin notes — the inline comment editor wears the same tint a commented span gets in the text, set in a handwriting face (bundled Caveat, OFL) in dark red ink and sized to sit with the document. It starts small and grows with what you write — wrapping to a fixed width, scrolling once it reaches a max height — so a remark feels like annotating rather than filling a form field.
- Follow links to files — in the reading view
Enteron a link is routed by target: a.mdopens in place (withother.md#sectionlanding on the heading), a.graflishows a "not yet supported, stay tuned" notice, and anything else opens with the system handler.gb(orBackspace) walks back through the documents you followed, and a brief toast names where you land.glopens a links overview — the same jump-list asgh/gc— whoseEnterfollows the picked link. A link to a missing file whispers not found instead of creating one.
Fixed¶
- Reading view no longer cuts off or refuses to scroll after comments or
suggestions re-render the document (Qt's incremental layout corrupted by
post-
setMarkdownedits; now settled with a forced full relayout). - Commenting a whole fenced code block no longer breaks rendering from that point on.
- A bare HTML-looking token in the source (e.g.
<variant>outside code spans) no longer silently swallows every following paragraph in the read view — raw HTML now renders as the literal text that was typed. - Font zoom (
⌘+/⌘-/⌘0) now works in the reading view too; a size change re-renders the document and keeps the caret in place. - Print (
⌘P) in the reading view now prints the typeset page instead of the raw CriticMarkup source, with the code band baked in as a real background so fenced code stands out on paper. - Relative image links (
) now render in the reading view no matter where textli was launched from — resources resolve against the document's own folder, not the process working directory.
[0.1.0] - 2026-07-01¶
Added¶
- Initial extraction from grafli. The zen Markdown editor that grew inside grafli becomes its own package:
- Full-window, distraction-free writing surface with vim keybindings
(NORMAL/INSERT/VISUAL), section focus (
⌘.), word-jump overlay (⌘J), adjustable font size and content-column width (both persisted). - Rendered reading view (
⌘R) with vim caret navigation, headings overview (gh), and print (⌘P). - Inline review: CriticMarkup comments (
c,]c/[c) and track-changes suggestions (s,a/xaccept/reject,⇧A/⇧Xall,gcchanges overview,pclean preview) — stored inline in the Markdown, no sidecar. textliCLI: open any Markdown file,#heading-sluglocations,-rto start in the reading view; autosave while editing.- Embeddable widgets:
ZenMarkdownEditor(full-window) andInlineVimEditor(single-field vim editing) for PySide6 hosts. - Self-contained F1 help owned by the editor.