Repository navigation
feat: add PNG screenshot output - #199
Merged
cpendery (cpendery) merged 2 commits intoSep 11, 2026
Merged
Conversation
cpendery (cpendery)
force-pushed
the
cpendery/png-screenshots
branch
from
September 8, 2026 23:05
4fbc3b9 to
ed14fb4
Compare
cpendery (cpendery)
changed the base branch from
main
to
cpendery/named-restart
September 8, 2026 23:05
cpendery (cpendery)
force-pushed
the
cpendery/png-screenshots
branch
from
September 10, 2026 17:17
ed14fb4 to
28610dc
Compare
cpendery (cpendery)
removed this pull request from stack #205
September 10, 2026 17:17
cpendery (cpendery)
added this pull request to stack #218
September 10, 2026 17:17
cpendery (cpendery)
force-pushed
the
cpendery/png-screenshots
branch
from
September 10, 2026 22:22
28610dc to
ca2f0bc
Compare
cpendery (cpendery)
marked this pull request as ready for review
September 10, 2026 22:22
cpendery (cpendery)
force-pushed
the
cpendery/png-screenshots
branch
from
September 11, 2026 16:55
ca2f0bc to
45121c7
Compare
cpendery (cpendery)
force-pushed
the
cpendery/png-screenshots
branch
from
September 11, 2026 20:30
45121c7 to
1899a94
Compare
An error occurred while trying to automatically change base from
cpendery/named-restart
to
main
September 11, 2026 20:55
cpendery (cpendery)
pushed a commit
that referenced
this pull request
Oct 16, 2026
…rofile (#220) Screenshots and recordings are drawn from constants, so the only way to change how output looks is to edit the renderer. This makes appearance configurable from `tui-test.toml`, under `[recording.style]`, and lets each profile override it. ## What it looks like Every option on its own, each a single change from the default. Tiles are scaled by one factor, not fitted to a column, so the sizes here are the sizes rendered: <img alt="every tui-test style option shown one change at a time" src="https://raw.githubusercontent.com/microsoft/tui-test/db6edabc21c2f3eb7b032434275ee1729469610d/static/style-options.png" width="860"> `font_size` drives the cell size, so the whole image grows with it: <img alt="the same session at four font sizes" src="https://raw.githubusercontent.com/microsoft/tui-test/db6edabc21c2f3eb7b032434275ee1729469610d/static/style-font-sizes.png" width="860"> There are two gaps, and each side of either can differ. `canvas_padding` is 24 on every side; `content_padding` is 8 top, 15 at the sides, 14 at the bottom. Either takes `0`, and one number insets the grid evenly once the title bar is gone: <img alt="the same session with padding removed and with even padding" src="https://raw.githubusercontent.com/microsoft/tui-test/db6edabc21c2f3eb7b032434275ee1729469610d/static/style-padding.png" width="700"> A recording is drawn from the same style as a screenshot, so a `.gif` matches its `.svg`: | Default | Midnight | Bordered | | :---: | :---: | :---: | | <img alt="a tui-test recording in the default style" src="https://raw.githubusercontent.com/microsoft/tui-test/db6edabc21c2f3eb7b032434275ee1729469610d/static/style-anim-default.gif" width="270"> | <img alt="a tui-test recording in the midnight style" src="https://raw.githubusercontent.com/microsoft/tui-test/db6edabc21c2f3eb7b032434275ee1729469610d/static/style-anim-midnight.gif" width="270"> | <img alt="a tui-test recording in the bordered style" src="https://raw.githubusercontent.com/microsoft/tui-test/db6edabc21c2f3eb7b032434275ee1729469610d/static/style-anim-bordered.gif" width="270"> | | no `[recording.style]` | dark chrome **and** palette | a stroke instead of a shadow | Every image here and in the README was generated by the CLI from the config shown beside it. ## The knobs ```toml [recording.style] font_size = 20 title_font_size = 14 canvas_background = "#101014" canvas_padding = 32 # around the window; per side below content_padding = 15 # between the window and the grid [recording.style.font] family = "Berkeley Mono, monospace" bold = "Berkeley Mono Bold" files = ["fonts/BerkeleyMono.ttf"] [recording.style.window] title_bar = true traffic_lights = false background = "#18181c" foreground = "#eeeeee" divider = "#303038" [recording.style.border] width = 1 color = "#454550" radius = 8 [recording.style.shadow] enabled = true color = "#080812" offset = 5 spread = 7 # Either gap can be set per side; a side left unnamed keeps its own default. [recording.style.canvas_padding] bottom = 48 ``` ## Per profile A profile carries its own `[recording]` and overrides the file's key by key, at every depth. What matters is what it *keeps*: ```toml # What every profile gets unless it says otherwise. [recording] directory = "./artifacts" [recording.style] font_size = 17 canvas_background = "#101014" canvas_padding = 30 [recording.style.window] background = "#1a1a22" foreground = "#d8d8e8" divider = "#2a2a36" # Docs: record somewhere else, in bigger type. [profiles.docs.recording] directory = "./docs/media" [profiles.docs.recording.style] font_size = 24 # CI: keep the file's directory, strip the chrome so artifacts stay small. [profiles.ci.recording.style] canvas_padding = 8 [profiles.ci.recording.style.window] title_bar = false [profiles.ci.recording.style.shadow] enabled = false # Demo: a light look for slides. [profiles.demo.recording.style] canvas_background = "#f6f6f8" font_size = 20 [profiles.demo.recording.style.window] background = "#e8e8ef" foreground = "#33333a" [profiles.demo.recording.style.border] width = 2 color = "#8a8aa0" ``` <img alt="three profiles overriding the same file-level recording settings" src="https://raw.githubusercontent.com/microsoft/tui-test/db6edabc21c2f3eb7b032434275ee1729469610d/static/style-profiles.png" width="860"> | Profile | directory | what it keeps from the file | | --- | --- | --- | | `docs` | `./docs/media` *(own)* | the canvas, its 30px padding, the title bar colors | | `ci` | `./artifacts` *(file)* | the font size, and the title bar colors it did not name | | `demo` | `./artifacts` *(file)* | the 30px padding and the divider it did not name | `ci` names no directory at all and still gets the file's; it names one key inside `[window]` and keeps the siblings. That config is a file the test reads, so the example here, the one in the reference, and the one under test cannot drift apart. Note that **whether** a recording is kept is not a `[recording]` key. `main` moved retention to `[trace] mode`, and the engine derives the recording mode from it, so `[recording]` carries the directory and the style only. ## Worth a closer look **Config that did not mean what it said.** `border.width` and `border.color` were accepted and validated by a renderer that never drew them, and the raster path resolved faces through a process-wide catalog built before any config was read — so `font.family` changed a screenshot and not a recording of the same session. Both are now drawn. The catalog keeps the expensive system scan process-wide and computes only the style-dependent ranking per style; building it under the lock rather than beside it is the difference between 38s and 0.6s across the font tests. **Inheritance.** A profile naming one style key used to replace the file's style whole, falling back to the *built-in* defaults rather than the file's — so `font_size = 24` silently discarded the background and padding the file had set. It now merges key by key. Serde has already filled absent keys by the time the typed config exists, so which keys a profile actually named is recovered from a second raw parse; the typed parse still runs first and is the one that reports errors, with spans intact. **The gap above the grid** was 4px against 15px at the sides, so text sat against the title bar — and against the panel edge with `title_bar = false`. It is now 8: rendering 4, 8, 12 and 15 side by side, 8 is where the text reads level with the side inset, because a row already carries leading above its glyph while a character sits tight in its cell horizontally. That is a deliberate change to the default look, so the golden was regenerated and the tests that pinned a dimension now derive it. **Reading font files.** A config file is found in the working directory, so checking out an untrusted repository hands one to the tool. `font.files` went straight to fontdb, which reads a path to the end: `files = ["/dev/zero"]` climbed to 5 GB of RSS in about a second, and a fifo blocked forever — both while holding the process-wide catalog lock, which made the wedge permanent. Font files are now read here, only when regular and only up to a cap, and lock poisoning is recovered rather than fatal. **Three backgrounds.** A recording has the area around the panel, the title bar, and the terminal's own under `[colors]`. Two were called `background`, so the outermost pair is now `canvas_background` and `canvas_padding`. **`--profile default`** errored on any config without a `[profiles.default]` table, although the flag's own help says it defaults to `default`. Pre-existing, found while checking the inheritance above; naming the default is now the same as omitting the flag, and every other unknown profile is still an error. ## Merged with what landed on main Four PRs landed while this was open and all touched the same surface. Each was resolved by integrating rather than picking a side: - **`Customize capture backgrounds` (#197)** added `--background` / `--transparent`. `render_svg` and `GridRenderer` now carry both the style and the capture background; the flags override the configured canvas for one capture, and naming neither leaves the style in charge — where main previously fell back to a hardcoded constant. - **`add named session restart` (#198)** replaced `spawn`'s positional parameter list with a `SpawnSpec`. Took main's shape wholesale and let `style` join the destructured options, which is strictly better: the spec is stored for restart, so a restarted session keeps its style for free. - **`add PNG screenshot output` (#199)** added `exact_pixel_size` and an `exact_size` flag for un-rounded screenshot dimensions. Both are now style-aware, so a PNG screenshot honours the configured gaps exactly as the SVG does. - **`shorten terminal reference guidance` (#219)** merged cleanly. ## Testing `cargo fmt`, both clippy invocations, the full workspace suite, the feature-gated suite, and the Python and JS binding suites all pass. **CI is green on all 13 checks.** Generating the images is also the end-to-end exercise: config file to daemon to session to `screenshot` and `record`, rather than a unit test handing a `Style` to a renderer. Writing the reference documentation is what caught four wrong defaults and the fact that a family is a CSS font stack that face selection was treating as one unmatchable string; the documented defaults and the per-profile example are pinned by tests rather than trusted. The golden rewrites itself under `TUI_TEST_UPDATE_GOLDEN` so a deliberate restyle lands in a reviewable diff. Because the default gap above the grid moved, every test that pinned an exact pixel size had to follow it: the Rust suites, the Python and JS binding suites, and the Windows-gated render snapshots. Those were updated rather than loosened — the numbers are the point of those tests. The snapshots were regenerated with `UPDATE_TUI_TEST_RENDER_SNAPSHOTS`, after first checking that this machine reproduces the committed Windows bytes exactly with the old gap, which shows the render does not depend on the platform's fonts. --------- Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
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.
No description provided.