Skip to content

hue format preview — Feature Requirements (interactive backends)

Status: v1 shipped (FP0–FP6; the persistent config section is the one design-only remainder) · Date: 2026-08-18 · Scope: a toggle-able view mode that reformats the open file in memory through a pluggable formatter (in-process sparkles:dmd-fmt for D; external formatters via an opt-in shell-out seam) and draws a draggable vertical column ruler that sets the formatter's soft maximum line length, reformatting live as it moves. hue stays read-only: the file on disk is never touched.

NOTE

v1 shipped on feat/dmd-lsp/dmd-format: the provider registry + session (format_preview.d), the in-process dmd-fmt provider (format_dmd.d, HueDmdFmt), the toggle/swap/restore across both backends, the draggable ruler (GUI hairline, TUI cell restyle), the --format-preview CLI family, one-shot sink formatting, and the event-horizon ForkServer execution backend (SPEC §17/M18) with the worker thread as fallback. Still open: the persistent format config section (waits on config's loader, CFG20) — until it lands, external formatters have no user-facing enablement route (the registry's allowlist parameter is wired and tested) — and mapCursor-fidelity viewport mapping. Status legend and IDs: see the overview.

Design & rationale

Format preview decomposes the same way folding does — a provider seam, a presentation-independent state machine, shared rendering, and per-backend adapters that stay thin:

  1. Formatter providers (FPR) — who can format what — a registry keyed by document language. Two kinds: in-process (sparkles:dmd-fmt for D, compiled in under the HueDmdFmt version) and external (an argv template run over stdin/stdout). External formatters are a trust boundary: they run only when declared in user config (the media.md DGM3 opt-in posture).
  2. Preview state (FMV) — what is shown — one backend-neutral session per document holding the retained original buffer, the ruler column, the in-flight format, and the error state. The buffer swap is the diff-emphasis pattern: originals retained once, toggle-off is an instant swap, never a recompute.
  3. The ruler (RUL) — how the width is chosen — a vertical guide at the soft-max column, draggable with an ew-resize pointer shape, whose entire interaction machine (hover tolerance, drag state, clamping, coalescing) is defined once in cell space (ui-architecture UIA2; the containers spec splitter discipline: dedicated capture id per STM11, one composed pointer shape per frame per DCK9, a keyboard route through the same clamp per DCK12).

Two decisions are recorded here so they are not re-argued:

  • The preview is a buffer rewrite, not an overlay (FMV9). Overlays (overlays.md) decorate the source; the format preview replaces the displayed text and re-derives highlighting/layout through the standard pipeline. Only the producer-registry shape is borrowed.
  • Formatting never blocks the UI thread (FPR9). Requests dispatch to an execution backend (an event-horizon worker; later the fork server, FPR10) with single-flight, latest-wins coalescing — backpressure adapts to measured format latency instead of a hard-coded debounce, so small files reformat per pointer event while huge files chain at their natural cadence.
  • What is left on the UI thread is measured, not assumed (FPR12). Applying a width re-highlights, re-wraps and re-lays out the buffer, and that half outweighed the formatting the backend had been built to move away. The rule that follows is to parse each formatted buffer once and let every consumer of that parse — highlights, folds, the inspector — share it; format_bench.d keeps the phase breakdown honest. A diff-vs-original presentation (cheap via sparkles:diff, DVN2) is recorded as future composition (FMV10), not built in v1.

Preview mode (FMV)

IDRequirementStatusTraces to
FMV1hue must support a toggle-able format preview in view — off by default, in-memory only; the file on disk is never written.full (ca124bea5)read-only doctrine (SRC)
FMV2The toggle must succeed only when a language-matching formatter is available; otherwise it reports why (status/toast) and stays off — never a crash (the totality law).full (ca124bea5)FPR1
FMV3Entering the preview must format at the ruler width and re-highlight through the standard pipeline; unparseable input still renders (the formatter is total).full (ca124bea5)DocumentPipeline.fromSource; dmd-fmt formatText
FMV4Exiting must restore the original buffer instantly from retained originals — a swap, never a reformat.full (ca124bea5)the diff-emphasis swap pattern
FMV5The viewport must be preserved across enter/exit/reformat — through the shared scroll anchor (gui.md NAV5), since a reformat is a re-layout like any other. Cursor-accurate mapping via mapCursor over minimal edits is future work.full (ca124bea5, anchored 67623407) — mapCursor fidelity opendmd-fmt M5 mapCursor
FMV6Folds and search state must recompute against the displayed buffer on every swap (compose-at-recompute fidelity with folding).full (ca124bea5)folding.md FLD2
FMV7Both backends must show preview status: active formatter, current width, and any error.full (9f67e1d4c+916aa7fce)status line / chrome bar
FMV8CLI: --format-preview (with --format-width COL) starts view in preview; --formatter NAME picks the provider (a miss lists candidates). The one-shot ANSI/HTML sinks render the formatted buffer (no ruler).full (36b601ae5)CLI
FMV9The preview is a buffer rewrite, not an overlay — it replaces the displayed text; overlay machinery is not involved.full (ca124bea5)decision above; overlays.md
FMV10Future: a diff-vs-original presentation of the same preview, reusing the diff document model.not starteddiff-view.md DVN2

Formatter providers (FPR)

IDRequirementStatusTraces to
FPR1A formatter registry must expose: language match, availability probe, and format(source, path, width) → text-or-error.full (172df4a74)proposed format_preview.d
FPR2The in-process provider is sparkles:dmd-fmt for D, compiled in under the HueDmdFmt version (the application/no-gui/unittest configurations; never android).full (66856704f)sparkles:dmd-fmt
FPR3External providers run an argv template ({width}/{path} placeholders) over stdin → stdout; nonzero exit or spawn failure yields an error outcome and leaves the buffer unchanged.full (172df4a74)core-cli runCaptured
FPR4External formatters are opt-in: they run only when declared in user config (allowlist, off by default) — a trust boundary.partial (172df4a74) — the registry takes only caller-approved entries; the user-facing enablement route waits on CFG20media.md DGM3 posture
FPR5Availability is probed lazily on first toggle and cached per session (no startup cost).full (172df4a74)isInPath
FPR6With multiple candidates the order is deterministic (in-process first, then config order); selection persists in config and cycles via a command.partial (ca124bea5+36b601ae5) — order, cycle and --formatter shipped; persistence waits on CFG20config.md
FPR7The ruler width maps to the formatter's soft maximum line length; the base config comes from .editorconfig discovery (dfmt keys honored), the ruler overriding only that one knob.full (66856704f+36b601ae5)dmd-fmt M7 configFor
FPR8Formatter errors must never blank the view: the last good buffer stays, the error goes to status (FMV7).full (ca124bea5)totality
FPR9Formatting never runs on the UI thread: requests dispatch to an event-horizon execution backend with single-flight, latest-wins coalescing (no fixed debounce — throughput adapts to measured latency); apply/re-highlight stays on the UI thread; one-shot sinks format synchronously.full (172df4a74)sparkles:event-horizon
FPR10The in-process backend becomes the event-horizon fork server (CoW-forked child per request: initialized-once DMD globals inherited, crash isolation, lock-free parallelism); a single worker thread serialized on the formatter's global lock is the non-Posix/already-threaded fallback.full (6de3fff6c+3247d25e8)sparkles:event-horizon ForkServer; dmd-fmt D4
FPR11Results memoize in a bounded cache: width → content digest → shared text+highlights (width→output is a step function, so distinct widths dedupe); LRU over distinct outputs with byte + entry caps; keyed by formatter fingerprint and source identity. Hits apply synchronously; stale completions are inserted before discard.full (172df4a74)proposed FormatCache
FPR12An applied width parses the buffer once: the re-highlight keeps its tree-sitter layers and the model adopts them, so the rebuild's fold scan does not re-parse the bytes just parsed. The cache retains parses under its own, much tighter cap than text+highlights, so reversing a drag over recent widths redisplays without re-parsing; an evicted parse costs one re-parse, never correctness.full (2fa5bacbe+e1c608cef)sparkles:syntax highlightParsed

The column ruler (RUL)

IDRequirementStatusTraces to
RUL1An active preview draws a vertical ruler at the width column in both interactive backends — GUI: a RuleEdge.centerX hairline; TUI: restyled column cells with in blank cells (a vertical rule op is a silent no-op in GridCanvas; documented here so it is not "fixed" into a blank column).full (9f67e1d4c+916aa7fce)RaylibCanvas.rule; the TUI divider paint pattern
RUL2The ruler is mouse-draggable: hover within tolerance arms it, press captures under a dedicated capture id (STM11), drag reformats live.full (9f67e1d4c+916aa7fce)containers.md STM8/STM11
RUL3Hover and drag must show the ew-resize pointer shape, contributed through the one composed per-frame shape (DCK9) — the GUI dock-shape arguments, the TUI OSC 22 path.full (9f67e1d4c+916aa7fce)PointerShape.ewResize
RUL4Drag reformatting is coalesced latest-wins through the single-flight backend (FPR9): only the newest column formats next, the UI thread never blocks, and the drawn ruler tracks the pointer at frame rate even mid-format.full (172df4a74+9f67e1d4c)FPR9
RUL5The column is clamped to [1, 300] — the floor is 1 because the formatter is total at any width, so the gutter is a legitimate target rather than an input to defend against; the keyboard nudge (</>) goes through the same clamp as the drag (DCK12).full (ca124bea5+9f67e1d4c)containers.md DCK12
RUL6The TUI must handle bare pointer-move (hover without a button) in the document pane — SGR any-motion reporting is already enabled; today those events fall through unread.full (916aa7fce)tui.md TIN
RUL7Pixel/cell ↔ document-column conversion goes through a shared geometry helper (gutter- and horizontal-scroll-aware), not re-inlined arithmetic.partial (9f67e1d4c) — the helper exists and the ruler uses it; the pre-existing inlined sites remainproposed FrameGeom helpers
RUL8The entire interaction machine — hover tolerance, drag state, clamp, coalescing, status text — is defined once, backend-neutrally, in cell space; backends contribute only coordinate translation, capture/shape plumbing, and paint (UIA2). The GuiState/workspace state split must not widen.full (9f67e1d4c)ui-architecture.md UIA2

Milestones

MilestoneScopeStatusRequirements
FP0Link spike: hue + sparkles:dmd-fmt under HueDmdFmt; repeated in-process format loopfull (66856704f)FPR2
FP1Provider registry + external seam + session + single-flight service + bounded cachefull (172df4a74)FPR1, FPR3FPR9, FPR11
FP2Toggle: commands, bindings, buffer swap/restore, statusfull (ca124bea5)FMV1FMV7
FP3Ruler machine + GUI adapterfull (9f67e1d4c)RUL1RUL5, RUL7, RUL8
FP4TUI adapterfull (916aa7fce)RUL1, RUL3, RUL6
FP5Config + CLI + formatter selection; one-shot sinkspartial (36b601ae5) — flags + one-shot sinks; CFG20 openFMV8, FPR4, FPR6
FP6Fork-server execution backend (event-horizon ForkServer)full (6de3fff6c+3247d25e8)FPR10
FP7Parse-once apply path + the per-phase drag benchmarkfull (2fa5bacbe+e1c608cef+c8826e987)FPR12

Module coverage (format preview)

SourceKey symbolsRequirements
apps/hue/src/format_preview.dFormatterRegistry, FormatPreviewSession, FormatCache, RulerGeomFPR*, FMV*, RUL8
apps/hue/src/format_dmd.dthe HueDmdFmt in-process providerFPR2, FPR7
apps/hue/src/gui.dGUI adapter: capture id, shape, hairline paint, chrome chipRUL1RUL4, RUL7
apps/hue/src/tui.d / workspace.dTUI adapter: column restyle, bare-move branch, OSC 22 shape, statusRUL1, RUL3, RUL6
apps/hue/src/keymap.dtoggleFormatPreview, formatterNext, width nudges, formatPreviewActiveFMV1, RUL5
apps/hue/src/format_bench.d@benchmark legs for each phase of an applied widthFPR9, FPR12
libs/event-horizon/…/forkserver.dForkServer (zygote + CoW fork per request + shared-memory arena)FPR10

Relationship to existing specs

PieceRole in format preview
sparkles:dmd-fmtthe D formatter (formatText, configFor, mapCursor)
folding.mdthe structural model this spec mirrors; composes per FMV6
ui-architecture.md UIA2one definition per visual/interaction (RUL8)
containers.md STM8/DCK*the splitter-drag discipline the ruler reuses
config.mdthe persistent format section (FPR4, FPR6)
lantern.mdthe keybindings (<leader>vf, <leader>vF, </>)
diff-view.md DVN2the future diff presentation (FMV10)
overlays.mdwhat this deliberately is not (FMV9)

GUI requirements · TUI requirements · Folding · Config · Overview