Skip to content

mdfried (Rust) ​

mdfried renders Markdown headings through native OSC text sizing, shaped images, or ordinary text, exposing the integration costs of each route.

Last reviewed: September 14, 2026.

FieldValue
LanguageRust, edition 2024
LicenseGPL-3.0-or-later in the manifest; GPL text
Repositorybenjajaja/mdfried
DocumentationRepository README
CategoryMarkdown pager; OSC/image/plain heading consumer
Package version0.22.5 at the inspected revision
Revision27c80b90bd408f60b094e50c09888e22cd444624
Commit dateSeptember 4, 2026
Main rendering dependenciesRatatui, ratatui-image, cosmic-text, in-tree mdfrier
InspectionLocal pinned source and tests read; tests and terminal probes not run

Overview ​

What it solves ​

A Markdown pager benefits from headings that are visibly different in size, not merely bold or colored. mdfried combines that typography with scrolling, images, search, and link navigation. Its text-sizing implementation is embedded in a real retained terminal application rather than a standalone OSC demo. README, view.

The key finding is that there are three heading routes, not one renderer with interchangeable output encodings. OSC headings use cell-width prewrapping; image headings use font shaping and raster layout; ordinary headings use a placeholder representation when there is no font renderer. Setup, section construction, image shaping.

Design philosophy ​

The README states the distinction plainly:

Not bold, not just styled, actually BIGGER, making markdown much more readable in a terminal.

That visual priority explains both the native text path and the willingness to rasterize headings. It also motivates specialized two-row sections instead of trying to express every heading as a normal one-row Ratatui span.

The trade-off is that the large-heading path does not automatically inherit ordinary-text semantics such as inline styling, search matches, or cell-diff ownership. Those differences must be evaluated separately from screenshots. See concepts for the separation of appearance, occupancy, and identity.

How it works ​

setup_graphics asks ratatui-image::Picker to discover capabilities, including text sizing unless configured to ignore it. Text-sizing support returns SetupResult::TextSizing immediately. A halfblock fallback returns AsciiArt; otherwise setup constructs a font renderer. Setup selection.

The parser and section builder then choose their representations:

text
text-sizing support -> cell-width prewrap -> Header(text, tier, None)
font renderer       -> HeaderPlaceholder -> shaped image -> Header(..., Some(proto))
no font renderer    -> HeaderPlaceholder remains ordinary text

SectionContent::Header without an image is painted by BigText. A header with an image uses ratatui-image::Image. Ordinary placeholders use Paragraph. The worker asynchronously replaces image-heading placeholders after rasterizing their content. Sections, worker, view.

Protocol and API ​

BigText::new(text, tier, color) is a small Ratatui widget. It always emits s=2, and selects a fractional numerator and denominator by heading tier. The source's wire template is:

text
ESC ] 66 ; s=2:n=numerator:d=denominator:w=width ; chunk ESC \

The ratio table is:

Headingn/dIntended visual scale 2*n/dOccupied height
H17/722 rows
H25/65/32 rows
H33/43/22 rows
H42/34/32 rows
H53/56/52 rows
H6 / other1/32/32 rows

“Intended” matters for H1: n=7:d=7 is noncanonical and violates the specification's requirement that nonzero d exceed n. A canonical full-size request would omit the fraction. The source and stored snapshots establish what is emitted, not which terminals accept that spelling.

No h or v alignment key is supplied. Fractions alter glyph scale within the allocated box; they do not reduce its two-row occupancy. The explicit w controls horizontal allocation in units that are then multiplied by s=2.

Measurement and geometry ​

The parser's line conversion duplicates the ratio table and computes the ordinary-width wrapping budget as width / 2 * d / n, with integer arithmetic in that order. BigText::size_ratio contains the same mapping, but the parser does not call that function. Two copies must therefore stay aligned.

At emission, unicode_chunks iterates a Vec<char>, not grapheme clusters. It accumulates UnicodeWidthChar values up to denominator d; a wide scalar is always emitted in its own chunk. Full chunks request w=n; partial chunks request ceil(chunk_width*n/d). Chunking and emission.

The prewrap formula and emitted occupancy are not equivalent. Consider H2 in a 20-column viewport with six CJK scalars of width two each. This arithmetic counterexample follows directly from the two algorithms:

text
prewrap budget:       20 / 2 * 6 / 5 = 12 ordinary columns
six wide scalars:     6 * 2 = 12, so the line fits that budget
each emitted chunk:  w = ceil(2 * 5 / 6) = 2
actual requested sum: 6 * (s=2 * w=2) = 24 terminal columns

Thus a line admitted for 20 columns requests 24 columns. This is a source-derived counterexample, not a captured terminal failure. Per-chunk rounding, especially the wide-scalar isolation rule, must be included in the measurement oracle. A whole-line proportional estimate is insufficient.

The image path has different geometry: header_images uses cosmic-text, Shaping::Advanced, pixel width from terminal font metrics, and a tier factor (12-tier)/12. It creates one image per shaped layout run, with two terminal rows per image. That scale curve is not the OSC ratio table.

Capability and fallback ​

The setup query includes configurable timeout and ignore_text_sizing_protocol. A discovered TextSizingProtocol capability selects the native path before font selection, avoiding raster setup when the terminal can draw sized text itself. Setup.

That single capability marker is broader than this widget's actual demands: integer scaling, fractions, explicit width, and their combined behavior. The local call site does not independently prove each combination, nor validate H1's equal numerator and denominator. This review did not audit the dependency's probe implementation or run it against terminals.

Without text sizing, graphics support can select the shaped-image path. Setup loads bundled and system fonts, attempts terminal-font detection, and can ask the user to choose a family. This is an image-rendering input, not a guarantee that native OSC and raster glyphs use identical fonts. Font setup.

Without that renderer, header placeholders remain ordinary colored text with hash prefixes. This preserves a readable heading route but does not promise identical wrapping, typography, selection, or height across all three modes. Placeholder construction, view.

Layout and clipping ​

BigText does not render a normal span into each occupied cell. It constructs one raw string containing erases, cursor motion, autowrap changes, optional color, and OSC records. It stores that entire string in the area's first cell and marks all other cells CellDiffOption::Skip. Widget implementation.

The erase sequence clears two rows, moving down and back up before painting. It disables DECAWM with CSI ? 7 l; the same function does not restore it. This is a raw Ratatui skip hack, not a backend-supported multicell primitive. It relies on terminal side effects that the ordinary cell model cannot infer.

view only paints completed headings when they are fully visible according to its two-row guard. Images used for ordinary document pictures have a sliced scrolling path, but large headings are suppressed at partial visibility rather than cropped row by row. View branches.

The widget only rejects zero width or zero height. A nonempty one-row area can still cause the raw sequence to erase and draw two rows, and area_width limits the erases rather than truncating the text. The CJK counterexample shows why prewrapping cannot substitute for an emission-bound check.

Other bounds deserve validation: the view uses expressions such as inner_area.bottom() - 2, and image sizing multiplies u16 metrics before conversion to wider types. These are source-derived small-viewport and large- dimension risks, not reproduced crashes. View, image geometry.

Retained state and interaction ​

The application retains typed sections, their heights, document identity, scroll position, and interaction state. Its render loop passes buffers to a render thread and drops intermediate frames rather than accumulating input behind slow image output. Section model, render loop.

That substantial retained application model should not be confused with a multicell-aware buffer. Skip hides covered cells from ordinary diffing, while the actual erase and paint operations live inside an opaque first-cell symbol. Correct transitions between old and new occupied rectangles require evidence beyond equality of those raw symbol strings.

Heading text preserves the raw Markdown inline source. The parser separately extracts links, but does not convert the heading itself into the same styled inline spans used for body paragraphs. Emphasis markers and link syntax can therefore remain in the big text. Heading parser.

Extracted heading links are appended as ordinary lines below the heading, where normal link rendering and interaction can operate. That is a deliberate semantic split, not clickable regions inside the enlarged heading. Line conversion.

Search similarly does not cover heading text: SectionContent::add_search only processes Lines, with an explicit TODO: search in headers. Header-anchor navigation exists separately in the model and does not close that gap. Search implementation, anchor navigation.

Safety and evidence ​

The OSC writer extends its string directly with input scalars. It does not enforce escape-code-safe UTF-8 or reject controls that can terminate a payload. The width-based chunker does not enforce the protocol's 4096-byte payload limit; arbitrarily many zero-width scalars can accumulate without filling its budget. BigText, protocol.

Color handling also illustrates why raw protocol output needs a typed boundary: Color::Indexed is formatted as CSI index m, not the usual indexed-foreground CSI 38;5;index m. This is a source-level encoding discrepancy, not a report of a tested palette failure. Color branch.

Tests read include header_wrapping_tier_1, the application resize/wrapping assertions, and stored snapshots containing s=2:n=7:d=7. They establish intended section splitting and raw output structure, not terminal interpretation. Section test, application tests, snapshot.

No upstream tests, GUI/terminal screenshots, or adversarial reproductions were run. Validation should require independent occupancy arithmetic, grapheme preservation, canonical metadata, payload safety, and damage tests covering resize, removal, movement, and partial visibility.

Strengths ​

  • Demonstrates native text, shaped image, and ordinary fallback in one pager.
  • Selects native sizing before unnecessary font setup and image generation.
  • Carries explicit two-row heading sections through scrolling and layout.
  • Uses advanced shaping for image headings rather than scalar-width estimates.
  • Preserves access to heading links through separate ordinary-text lines.

Weaknesses ​

  • Prewrap estimates disagree with per-chunk occupancy for wide text.
  • Scalar chunks can split graphemes and do not bound payload bytes.
  • H1 emits a noncanonical fraction; capability evidence is coarser than usage.
  • Opaque raw symbols and skipped cells bypass normal retained-buffer semantics.
  • Heading inline styling and search do not match the body-text path.

Key design decisions and trade-offs ​

DecisionRationaleTrade-off
Prefer OSC over raster headingsAvoid font setup and image transportDepends on combined sizing behavior
Fix occupancy at two rowsSimplify section heightsSmall fractions still reserve two rows
Prewrap with a proportional budgetReuse ordinary text wrappingMisses per-chunk rounding costs
Isolate wide scalarsWork around observed terminal behaviorLoses grapheme guarantees and packing efficiency
Put raw output in one buffer cellIntegrate without a new backend primitiveDiffing cannot understand the covered owners
Put heading links below the headingReuse normal interaction machineryVisual text and link hit regions diverge

The comparison and Sparkles proposal should borrow route selection and explicit geometry, not the raw-cell transport shortcut.

Sources ​