Repository navigation
feat: shape base-plus-combining-mark graphemes in raster output - #285
Draft
Alexey Zhokhov (donbeave) wants to merge 17 commits into
Draft
Alexey Zhokhov (donbeave) wants to merge 17 commits into
Alexey Zhokhov (donbeave) wants to merge 17 commits into
Conversation
Live capture reads grid, cursor, dimensions, title, and resolved palette under one terminal-state lock. Default capture records raw visual data; highlighted capture needs an explicit option. Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Add ResolvedLayout in render/svg.rs: live screenshots and recordings resolve their Style to it, offline snapshots resolve SnapshotRenderOptions to it, and both draw through render_svg_with_layout and GridRenderer::with_layout. RenderState gains from_parts so snapshots can supply resolved colors without a live emulator; encode gains encode_png_bytes shared by file and memory PNG output. render_svg, GridRenderer, and encode_png keep their previous behavior. Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Public render_snapshot_svg/render_snapshot_png (+ path wrappers) draw a TerminalSnapshot through the same renderer as live screenshots, with SnapshotRenderOptions carrying explicit cell metrics, scale, padding, and chrome switches. Hidden and clipped cursors stay hidden and are recorded in RenderEvidence with dimensions and title. Tests cover U03-A through U03-E and U03-G through U03-I. Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Add explicit PinnedFontSet (font bytes, face indices, fallback order, style selection) extending SnapshotRenderOptions. Under the pinned policy the rasterizer loads only the specified bytes: no system fonts and no ambient font-selection variables. Missing font, style face, or glyph returns a typed PinnedFontError; no host fallback, replacement glyph, or unselected synthetic style. Each strict PNG carries a versioned render contract (renderer and raster policy versions, font digests, geometry, scale, palette digest, cursor phase with blink intent). Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
U04-A re-renders C06 content under a rogue fontconfig path and family selection; U04-B checks digest sensitivity; U04-C checks typed missing-font, missing-face, and missing-glyph failures; U04-D checks system cache operations cannot move strict output; U04-E checks equal pinned inputs decode to equal pixels; U04-F checks the recorded system-font behavior is unchanged. Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
MIT, edition 2021, newest crates.io release (2024-11-12, stable 1.82 era, no declared rust-version; pure Rust with pre-1.90 dependencies). Uses ttf-parser 0.25, shared with the workspace, so no second parser enters the tree. Optional under recording-raster; the no-default-features build is unchanged. Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
One OFL-1.1 face with only U+6771 plus .notdef (3,884 bytes), subset at development time from google/fonts NotoSansJP[wght].ttf; SOURCE records the commit, digests, and subset command. Pinned tests read these bytes directly; nothing fetches fonts at test time. Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
…itles Replace scalar-count rejection with rustybuzz shaping: each cell grapheme shapes with one face selected per grapheme in fallback order, so a combining sequence never divides across faces. Cells and cursor repaint share draw_cell_text; titles shape per cluster from the shaper's own indices with ligatures off. Single scalars draw verbatim through the pinned cache; shaped runs map their advance onto the cell width and clip to the permitted region. Glyph evidence flows to RenderEvidence.shaped_graphemes. Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Each P02 Unicode case renders with pinned repo bytes (JetBrains Mono plus a vendored Noto Sans JP subset for CJK); cell and cursor repaint keep shaped positions; wide clipping stays in the permitted region; missing glyphs fail without image success; verbatim cells record no shaping. Structural ink assertions plus decoded image evidence; never digest-only acceptance. Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Ordinary offline rendering, live screenshots, and recordings rejected every multi-scalar grapheme (scalar-count check) even when a catalog face covered it, while pinned rendering shaped the same sequence. Cells and cursor repaint now share one shaping path on both backends with distinct selection policies: pinned keeps its set order and typed errors; the system backend shapes base-plus-combining-mark sequences with the first catalog candidate covering the full grapheme (host fallback order) and collects an undrawable sequence for the end-of-render failure. Single scalars keep the legacy verbatim path, so recorded pixels do not change; non-combining sequences (ZWJ emoji, regional indicators) keep the recorded report instead of a misrender, and shaped evidence stays pinned-only. unicode-properties 0.1.4 for General Category Mark detection: MIT/Apache-2.0, edition 2021, no_std, dependency-free, no declared rust-version; already in the tree via rustybuzz 0.20.1, so no new crate enters the lock. Optional under recording-raster; the no-default-features build is unchanged. Tests: ordinary offline, live screenshot, and recording coverage for e plus U+0301 and a plus U+0308 (ink in the cell, mark changes the image, live cell holds the two-scalar grapheme, recording exports animation frames). Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
…settings in contract R5: write_snapshot_png unconditionally built the system-font renderer, so bold text with a regular-only pinned set wrote a PNG while render_snapshot_png returned typed MissingFace. Both entry points now build the shared snapshot_renderer, and the path API maps staged-write failures through map_pinned_error, so both report the same typed pinned-font failures and decode to equal pixels. R6: RenderContract copied only part of PinnedCacheKey, so a padding change (10x21 vs 30x41 images) and the synthetic-style selection left the contract equal while pixels differed. The contract is now at schema version 2 and records the synthetic-style selection plus every normalized output setting (title chrome, content and canvas padding, backgrounds, window and border chrome, shadow, exact-size switch), each with an accessor; RenderContract::system takes the background override and exact-size flag alongside the layout. U04-G checks path/bytes parity for the MissingFace failure and the success pixels; U04-H checks the returned contract changes after each output-affecting option. Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
…ip mask P2 advance-only: shaping kept outline-less glyph advances in the run total but stored only outlined glyphs, so the draw loop never moved the pen for them. A width-1 cell holding space plus U+0301 rendered a successful blank PNG: the space advance was dropped and the mark landed outside the clip. Both backend loops now retain advance-only entries (Option outline) and the draw loop moves the pen for every entry while painting only drawable ones. P2 CGJ preflight: the cmap preflight required every scalar to map a standalone glyph, rejecting default-ignorables the shaper handles (a+CGJ failed as MissingGlyph although rustybuzz shapes base+CGJ). Coverage is now required only where visible glyphs are needed; the controls ride along in the shaping buffer and shaped output is inspected for genuine (.notdef on a visible scalar) holes. PERF: each shaped cell allocated a full-canvas clip mask (~12.4GB cumulative at 160x48/10x21: 7680 cells x 1600x1008 bytes). The renderer now keeps one mask per pixmap size and clears it per cell. A representative release workload (160x48 all-combining render) runs in ~0.3s with one mask allocation. Scope and evidence notes: shaping covers base-plus-combining-mark sequences only (ZWJ emoji and regional indicators keep the legacy report; no general emoji claim). Single-scalar cells keep the verbatim path on both backends, so single-scalar CJK was never rejected for continuation reasons. System cell and cursor repaint share the combining path; title shaping stays pinned-only. The two new dependencies (rustybuzz, unicode-properties) are optional under recording-raster, and shaped_graphemes records the input strings, not glyph IDs, positions, or faces. U05-H covers space-plus-mark and the noncomposable multi-glyph cluster with positional assertions (cell ink, cursor repaint, pinned titles); U05-I covers CGJ, variation sequences, and the unsupported-visible scalar negative control; the system backend gains an offline CGJ case. Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Move the synthetic-style fallback into PinnedFontSystem::style_positions so the single-scalar, grapheme, and title paths select faces the same way: exact style-slot matches first in fallback order, every face in fallback order when the slot is empty and synthetic styles are selected, and empty otherwise. Previously a regular-only set with synthetic styles enabled failed bold combining text and titles with MissingFace. Cover the fallback with the synthetic_style_fallback graphemes test: regular-only face renders bold-italic combining text, block-cursor repaint, and a visible title, while the same render without synthetic styles fails as a missing face. Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com>
Alexey Zhokhov (donbeave)
added a commit
to tailrocks/tuiscotti
that referenced
this pull request
Oct 27, 2026
Migrate tuiscotti onto the tui-test engine as its terminal runtime. Upstream (`tui-test`) owns the terminal engine: child processes, emulation, sessions, input, waiting, rendering, fonts, and pixel comparison. Tuiscotti keeps the application testing layer: pure-view capture, versioned samples, frozen baseline checks, explicit approval, static reports, and the offline CLI (`check` / `accept` / `render` / `report`, exit codes 0/1/2/3). The previous built-in live engine, renderer, recorder, and second approval engine are removed, leaving the two shipping crates (`tuiscotti`, `tuiscotti-cli`). ## Provisional fork dependency The new `tui-test` APIs this PR needs are still unmerged upstream, so this branch temporarily depends on a pinned fork via `[patch]`: - `donbeave/tui-test`, branch `goal/qual-stack-r5`, pinned at `81da26fa9dc29664a897dc802be816a4432c0618` The pin is provisional. Once the upstream PRs below merge and a registry release contains the new APIs, the `[patch]` is removed in favor of the registry version. ## Upstream PRs (all 11, all open) | PR | Change | Head SHA | Status | |----|--------|----------|--------| | [microsoft/tui-test#276](microsoft/tui-test#276) | Explicit read-only snapshot policy | `fc0150160c25ea83532b18f9507f9f1a155b91e4` | Reworked + verified | | [microsoft/tui-test#277](microsoft/tui-test#277) | RGBA image comparison for pixel equality | `4fb316aa56c65cc659b4a43ee4921c45e33cecc0` | Approved as-is; dependency-only, no rework | | [microsoft/tui-test#278](microsoft/tui-test#278) | Owned terminal snapshot API | `334c474d1c621c373333e9dc24e549623a133570` | Reworked + verified | | [microsoft/tui-test#279](microsoft/tui-test#279) | Raw byte input for Rust sessions | `3b1439c313e88d0a8867dfbcdadabb28624dc5b5` | Reworked + verified | | [microsoft/tui-test#280](microsoft/tui-test#280) | Public rendering for offline snapshots | `378036846a4e85174104e0f9c153b0a0f8f84161` | Reworked + verified | | [microsoft/tui-test#281](microsoft/tui-test#281) | Configurable quiet waits and request cancellation | `103d296494f251921f8cd61d3f6ce2f7e152690d` | Reworked + verified | | [microsoft/tui-test#282](microsoft/tui-test#282) | Mode-aware programmatic input | `212ba1db26987a45537708e98c82db77c90bb734` | Reworked + verified | | [microsoft/tui-test#283](microsoft/tui-test#283) | Explicit underline colors in rendering | `fff55c122ae210efbe9a68d1ac8683b1a8856d` | Reworked + verified | | [microsoft/tui-test#284](microsoft/tui-test#284) | Explicit pinned-font rendering | `2bdaa3f8d3692514a6cb9c1ab844e880ca8d0f23` | Reworked + verified | | [microsoft/tui-test#285](microsoft/tui-test#285) | Combined and wide cell text rendering | `edf2b525d6f8a5710cd56440545437ff4f146727` | Reworked + verified | | [microsoft/tui-test#286](microsoft/tui-test#286) | Explicit child launch settings | `d5d9bbab9ac4e8a5e0562175497661b7b1a4b158` | Reworked + verified | "Reworked + verified" means the PR went through a rework round after independent review, and its per-task suites plus the integrated stack re-ran green. #277 needed no changes — its head is unchanged since review — so it is listed as a pure dependency. Upstream merge order: #278 before #280 and #281; #280 before #283 and #284; #284 before #285. #276, #277, #279, #282, and #286 are independent and can merge in any order. ## Verification (at PR head `a056f8f60bae1bf81429b575076c86aed3e6717b`) - Tests: 209/209 passing in both the default-features and no-default-features lanes. - `cargo fmt --check` clean; `cargo clippy --all-targets -D warnings` clean (default and no-default features). - Downstream CI: [run 40408516574](https://github.com/tailrocks/tuiscotti/actions/runs/40408516574) (in progress, at `a056f8f`); prior run [40390626472](https://github.com/tailrocks/tuiscotti/actions/runs/40390626472) success at `9ae97ff`. ## Known limitations - Windows/macOS runner coverage is still pending; platform-specific behavior (for example raw-byte PTY paths) is unverified there. - The upstream PRs currently report only CLA checks; Rust CI on the upstream side still needs maintainer workflow approval, so upstream-side proof rests on the recorded per-task suites plus this downstream integration. - No crates.io release contains the new APIs yet, so registry adoption is blocked until the upstream PRs merge and release. The provisional fork pin above supports merging this PR only; it is not a release path. ## Depends-on / merge order This PR depends on all eleven upstream PRs merging (in the order noted above) plus a registry release; only then does the provisional `[patch]` come out. Nothing in this PR can ship to crates.io before that. Upstream maintainers: the stack is additive-only, and the pinned fork branch tracks the exact heads listed above. --------- Signed-off-by: Alexey Zhokhov <alexey@zhokhov.com> Signed-off-by: Muse Code <muse-code@meta.com> Signed-off-by: Muse Code <muse@example.local> Co-authored-by: Muse Code <muse-code@meta.com> Co-authored-by: Muse Code <muse@example.local>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Title: feat: shape base-plus-combining-mark graphemes in raster output
Depends-On: #284
Incremental diff vs #284 head:
d5b1469...ee633fa
Summary
Teaches the offline raster renderer to shape grapheme clusters made of a
base character plus combining marks (accents, CGJ, variation sequences),
so they draw as one cluster instead of dropping marks or misplacing them.
Shaping covers cells and cursor repaints on both font backends; titles stay
pinned-only. Emoji-presentation sequences keep the previous behavior.
New since the original PR head:
while only drawable ones paint (fixes blank output for space-plus-mark).
handles; genuine holes (placeholder glyph on a visible scalar) still fail.
combining render runs in about 0.3s with a single allocation).
recording-raster; recordings keepinput strings, not glyph data.
Head:
ee633fa5146110b5a792c3ebaa651f1a66d50af3Testing
Focused suites at head
48276ae, run locally withcargo test:--features recording-font-jetbrains-mono-styles --test graphemes:19 tests run, 19 passed, 0 failed (includes new
synthetic_style_fallback)--test system_graphemes: 7 passed, 0 failedKnown limitations
recording-rasterfont features; Windows andmacOS results are pending upstream CI on this PR.