Skip to content

A legibility floor for every colour, and a trigger route that keeps the line where it was - #31

Merged
HarryCordewener merged 8 commits into
mainfrom
fix/readable-colours
Aug 12, 2026
Merged

A legibility floor for every colour, and a trigger route that keeps the line where it was#31
HarryCordewener merged 8 commits into
mainfrom
fix/readable-colours

Conversation

@HarryCordewener

@HarryCordewener HarryCordewener commented Aug 12, 2026

Copy link
Copy Markdown
Member

Two reported defects that share a cause.

"we are using unreadable colors by default against our backgrounds. Such as Freeze being purple against a blue background."

"we failed to solve the issue of Triggers — we should be able to Highlight text and send it to the pane where we found it. That way, players can highlight character name text."

The second is mostly the first: a highlight rule does fire, and the colour it fires in is invisible.

The measurement

Contrast (WCAG) of each colour against the plane it is actually painted on. The F2 highlight picker's palette:

name dark light name dark light
purple 1.27 8.43 gold 8.55 1.26
blue 1.40 7.69 yellow 11.16 1.04
black 1.75 18.79 white 11.99 1.12
green 2.33 4.60 cyan 9.56 1.12
teal 2.51 4.27 pink 7.79 1.38
red 3.00 3.58 magenta 3.82 2.81
silver 6.59 1.63 grey 3.04 3.53

Six of sixteen fail on dark, nine on light, and only grey clears 3:1 on both. That is the finding the design turns on: a palette of fixed hexes cannot serve two themes, so the resolution has to happen where the colour meets its plane rather than where it is chosen.

The reported bug is one cell of the same grid — the freeze bar took its accent from the theme's index 5 and painted it on the pane: #800080 on a #36363d focused pane is 1.27:1. And the client's own hexes were all picked against a dark theme, so on Light the accent measured 1.42:1, the draft pen 1.26:1 and the notice 1.73:1. The Light theme's chrome has never been readable, and no snapshot showed it because every frame in the gallery renders Dark.

The rule

Core.Text.Contrast — WCAG luminance and ratio, plus Legible(fg, plane, floor): the foreground moved the smallest distance that clears the floor, returned byte-identical when it already does.

  • Direction is the plane's, not the colour's — a dark plane lifts, a light plane darkens. One function, three themes. The pivot is relative luminance against 0.18, because #808080 looks like half way written down and is 0.216.
  • Hue survives while there is headroom and then desaturates, and it has to: pure #0000ff has a relative luminance of 0.0722 and tops out at 1.88:1 on a dark pane at full blue.
  • The floor is 3.0:1 and deliberately not 4.5. A game's own de-emphasis is spoken in exactly the colours a 4.5 floor would erase.

MarkupFormatter applies it to every foreground it paints — against the span's own background when it has one, the theme's reading plane when it does not. That plane is per theme, not per pane: the extreme of the fourteen a pane can wear, which is the worst case rather than an approximation of one, so a focus change never re-formats a buffer. F7's keep text legible (default on) switches it off to exactly the previous bytes.

The client's own hexes become ChromeInk, derived from the theme and held to the same floor. ScreenPalette is deliberately untouched — those sit on the settings screens' own fixed backdrop, which no theme moves.

What the frame audit found that reading the source did not

The design was implemented and then the paint was measured. Five more, each with a plausible-looking call site:

what measured
the trigger left-rule 1.42 (Light)
the header ribbon's chip 1.53 (Light)
a world's own accent on the rail 1.03 (Light)
the unread badge on a tab 1.42 (Light)
the command line's ink 2.43 (Solarized)

FrameContrastTests is that audit as a test: every emitted SGR pair over 24 views × 3 themes. It exempts the powerline wedges and box-drawing rules (fill boundaries and dividers, not text), the solid blocks (F2's swatch is a colour sample, shown as the pane will paint it), and the framework's [dim]. The half blocks are not exempt is the trigger rule and the focus marker, and one of them was a real defect this caught.

One thing is outside the floor's reach and is named rather than hidden. SharpConsoleUI resolves [dim] to a fixed #808080 through no option we hold: 4.01:1 on Dark, 2.52:1 on Solarized Dark's focused pane. Reaching it means giving up [dim] across every renderer for an explicit floor-checked grey — a sweep, for a near miss on one theme. The exemption is a named predicate with the number in it.

--theme <name> is new on the snapshot CLI, which is what makes the Light and Solarized frames verifiable at all.

Triggers

Four changes. No schema change and no migration.

  • route gains an explicit (none) — the rule adds no destination and the line follows whatever the other matched rules decided. That is what SpawnTarget = null has always meant; F2 labelled it main, which reads as a destination, so "highlight it and leave it where it was" looked inexpressible. It is the default for a new rule.
  • main becomes a real destination — the matching session's own window. It earns a reserved word rather than being spelt as the window's title because one trigger set is shared by every character that lists it and a title can only name one of them.
  • Gag suppresses the default delivery only. Explicit destinations survive it — already true of a spawn pane, now true of main, so route: main + gag keeps the line where it used to delete it.
  • Destinations are deduplicated. They were not, and WorldSession raises one SpawnLine per entry, so a highlight rule pointed at the same pane as its capture rule delivered every line twice.

What is deliberately not changed: a highlight rule needs no route to reach the pane a capture rule sent the line to. There is one line and one set of destinations, and every matched rule's highlight is on it — which is the user's own framing ("it should still follow the original route, as long as it does not change where it routes to").

Verification

dotnet build SharpMUTerm.slnx     Build succeeded. 0 Warning(s) 0 Error(s)

Core        total: 936    failed: 0
Tui         total: 1757   failed: 0
Graphics    total: 83     failed: 0
Scripting   total: 42     failed: 0
Web         total: 37     failed: 0

Frame-verified: the freeze bar's painted cells went from 1.27:1 to 3.02:1 on the default dark theme, still recognisably violet; and every one of the 72 frames the audit walks is clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_015nuKnWthnELNkrd86q5KWN

Summary by CodeRabbit

  • New Features

    • Improved text readability across themes with automatic contrast adjustment.
    • Added a default-on “keep text legible” display option.
    • Added theme-aware colors for interface elements, highlights, panels, tabs, and status messages.
    • Added theme selection support for snapshot rendering.
    • Expanded trigger routing with explicit main-window and no-route options.
  • Bug Fixes

    • Prevented duplicate trigger deliveries while preserving explicit destinations, including gagged lines.
    • Improved readability of picker colors, ANSI text, and highlighted spans against their actual backgrounds.

HarryCordewener and others added 5 commits August 11, 2026 22:09
…a line

Two reported defects with one cause. Measured every colour the client can paint
against the plane it lands on: only `grey` of the sixteen picker names clears
3:1 on both the dark and light themes, and the freeze bar's accent is 1.27:1 on
a focused dark pane -- which is the reported "purple against a blue background".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nuKnWthnELNkrd86q5KWN
…nds on

The reported defect is one cell of a grid. The freeze bar took its accent from
the theme's index 5 and painted it on the pane: #800080 on a #36363d focused
pane is 1.27:1, very nearly the same colour twice. Measured across the whole
grid, six of the F2 picker's sixteen names fail 3:1 on the dark theme, nine
fail on the light one, and only `grey` clears both -- a palette of fixed hexes
cannot serve two themes, so the resolution has to happen where the colour meets
its plane rather than where it is chosen.

Core.Text.Contrast is that rule: WCAG luminance and ratio, plus Legible(), which
moves a foreground the smallest distance that clears the floor and leaves
anything already legible byte-identical. Direction is the plane's, not the
colour's -- a dark plane lifts and a light plane darkens -- which is what lets
one function serve all three themes. Hue survives while there is headroom and
then desaturates, because pure #0000ff tops out at 1.88:1 on a dark pane at full
blue and a rule that held hue absolutely would leave the commonest unreadable
colour in MU* output unreadable.

The floor is 3.0:1 and deliberately not 4.5: a game's own de-emphasis is spoken
in exactly the colours a 4.5 floor would erase.

MarkupFormatter applies it to every foreground it paints, against the span's own
background when it has one and the theme's reading plane when it does not. That
plane is per *theme*, not per pane -- the extreme of the fourteen a pane can wear,
which is the worst case rather than an approximation of one -- so a focus change
never re-formats a buffer. F7's `keep text legible` (default on) switches it off
to exactly the previous bytes.

The client's own hexes become ChromeInk, derived from the theme and held to the
same floor. That fixes a second defect nobody had reported: on the Light theme
the accent was 1.42:1, the draft pen 1.26:1 and the notice 1.73:1, so its chrome
has never been readable -- and no snapshot showed it because every frame in the
gallery renders Dark.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nuKnWthnELNkrd86q5KWN
…at means main

Four changes, no schema change and no migration.

`route` gains an explicit `(none)`: the rule adds no destination and the line
follows whatever the other matched rules decided. That is what SpawnTarget=null
has always meant -- F2 labelled it `main`, which reads as a destination, so
"highlight it and leave it where it was" looked like something the screen could
not express. It is the default for a new rule.

`main` becomes a real destination: the matching session's own window. It earns a
reserved word rather than being spelt as the window's title because one trigger
set is shared by every character that lists it, and a title can only name one of
them.

Gag suppresses the default delivery only. Explicit destinations survive it --
already true of a spawn pane, now true of main -- so `route: main` plus gag keeps
the line where before it deleted it.

Destinations are deduplicated. They were not, and the session raises one event
per entry, so a highlight rule pointed at the same pane as its capture rule
delivered every line twice.

Also folds in five colour defects the frame audit found after the fact -- the
trigger left-rule, the header chip, a world's accent on the rail, the unread
badge on a tab, and the command line's ink on Solarized's armed band -- and adds
FrameContrastTests, which walks every emitted SGR pair over 24 views x 3 themes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nuKnWthnELNkrd86q5KWN
…-theme

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nuKnWthnELNkrd86q5KWN
RuleRow had its own `?? "main"` beside the route field's. After the field
learned that a null target is *no destination* rather than the main window, the
list would have gone on calling it `main` -- the two surfaces disagreeing about
one rule, which is the shape of the confusion the rename exists to remove.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nuKnWthnELNkrd86q5KWN
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The change adds WCAG-based theme contrast correction, theme-derived TUI chrome colors, explicit trigger destinations, (none) route handling, snapshot theme selection, and tests for contrast, routing, palette behavior, and rendered frames.

Changes

Theme contrast and rendering

Layer / File(s) Summary
Contrast calculation and text rendering
src/SharpMUTerm.Core/Text/Contrast.cs, src/SharpMUTerm.Tui/MarkupFormatter.cs, src/SharpMUTerm.Tui/TriggersScreenRenderer.cs, tests/*
Adds a 3:1 contrast floor, configurable text correction, background-aware span handling, and legible trigger swatches.
Theme-derived chrome and application wiring
src/SharpMUTerm.Tui/ChromeInk.cs, src/SharpMUTerm.Tui/WorkspacePalette.cs, src/SharpMUTerm.Tui/SharpMUTermApp.cs, src/SharpMUTerm.Tui/*Renderer.cs
Replaces fixed chrome colors with theme-derived inks and applies contrast-aware colors across application views.
Contrast and palette validation
tests/SharpMUTerm.Core.Tests/ContrastTests.cs, tests/SharpMUTerm.Tui.Tests/LegiblePaletteTests.cs, tests/SharpMUTerm.Tui.Tests/FrameContrastTests.cs, tests/SharpMUTerm.Tui.Tests/FrameGrid.cs
Tests contrast math, theme palettes, chrome inks, and emitted frame color pairs across themes and views.

Trigger routing

Layer / File(s) Summary
Explicit main-window routing
src/SharpMUTerm.Core/Automation/Trigger.cs, src/SharpMUTerm.Core/Automation/TriggerEngine.cs, src/SharpMUTerm.Core/Session/WorldSession.cs
Adds the main destination, separates it from (none), preserves explicit delivery under gagging, and deduplicates pane targets.
Route editing and end-to-end delivery
src/SharpMUTerm.Tui/TriggersScreenRenderer.cs, src/SharpMUTerm.Tui/TriggersScreenView.cs, tests/SharpMUTerm.Core.Tests/TriggerRouteMainTests.cs, tests/SharpMUTerm.Tui.Tests/TriggerKeepItHereTests.cs, tests/SharpMUTerm.Tui.Tests/TriggersScreenEditingTests.cs
Updates route choices and rendering, then verifies shared destinations, main-window resolution, highlight preservation, and duplicate prevention.

Options and snapshots

Layer / File(s) Summary
Legibility option and themed snapshots
src/SharpMUTerm.Core/Configuration/PreferenceSettings.cs, src/SharpMUTerm.Tui/OptionsScreenRenderer.cs, src/SharpMUTerm.Tui/Program.cs, tests/SharpMUTerm.Tui.Tests/Screen*Tests.cs
Adds the default-enabled F7 setting and applies the selected built-in theme to snapshot configuration. Updates related screen expectations.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 48.59% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes both primary changes: the colour legibility floor and trigger routing that preserves the line's existing destination.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@docs/superpowers/specs/2026-08-11-legible-colour-and-trigger-routing-design.md`:
- Line 10: Update the blockquote content in the documentation so the blank
separator line is represented by a `>` marker, preserving continuous quoted
content and satisfying markdownlint MD028.

In `@src/SharpMUTerm.Tui/TriggersScreenRenderer.cs`:
- Line 306: Update the route assignment in the trigger action editor so the
value is trimmed before comparing it with NoRoute. Store null for
whitespace-padded or exact NoRoute values; otherwise store the normalized
trimmed route in entry.Trigger.Actions.SpawnTarget.

In `@src/SharpMUTerm.Tui/WorkspacePalette.cs`:
- Around line 432-436: Update ChromePlane to include HeaderChip(theme) among the
candidate planes passed to Extreme, preserving the existing pane, backdrop, and
surface candidates. Add the corresponding palette assertion for HeaderChip in
every supported theme definition or validation set.

In `@tests/SharpMUTerm.Tui.Tests/FrameContrastTests.cs`:
- Around line 123-156: Update Pairs to locate the next Sgr or Csi match once per
outer iteration, then process the intervening plain-text span with a single
inner loop before handling that escape sequence. Preserve Apply updates, escape
skipping, character filtering, and pair counting while eliminating repeated
Sgr.Match/Csi.Match scans from each character position.
- Around line 98-101: The escape-processing loop in Pairs should avoid calling
Sgr.Match or Csi.Match for ordinary characters. Check frame[i] for '\x1b' before
invoking either regex, while preserving existing handling for escape sequences;
alternatively, parse each escape sequence once and advance past it.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e4920b7c-138f-4d03-b147-09e3bcdea329

📥 Commits

Reviewing files that changed from the base of the PR and between e47a370 and 66e590d.

📒 Files selected for processing (33)
  • CLAUDE.md
  • docs/superpowers/specs/2026-08-11-legible-colour-and-trigger-routing-design.md
  • src/SharpMUTerm.Core/Automation/Trigger.cs
  • src/SharpMUTerm.Core/Automation/TriggerEngine.cs
  • src/SharpMUTerm.Core/Configuration/PreferenceSettings.cs
  • src/SharpMUTerm.Core/Session/WorldSession.cs
  • src/SharpMUTerm.Core/Text/Contrast.cs
  • src/SharpMUTerm.Tui/ChromeInk.cs
  • src/SharpMUTerm.Tui/MarkupFormatter.cs
  • src/SharpMUTerm.Tui/OptionsScreenRenderer.cs
  • src/SharpMUTerm.Tui/PaneDropRenderer.cs
  • src/SharpMUTerm.Tui/PrefixPanel.cs
  • src/SharpMUTerm.Tui/Program.cs
  • src/SharpMUTerm.Tui/RailRenderer.cs
  • src/SharpMUTerm.Tui/SharpMUTermApp.cs
  • src/SharpMUTerm.Tui/TabTitles.cs
  • src/SharpMUTerm.Tui/TriggersScreenRenderer.cs
  • src/SharpMUTerm.Tui/TriggersScreenView.cs
  • src/SharpMUTerm.Tui/UnreadBadge.cs
  • src/SharpMUTerm.Tui/WorkspacePalette.cs
  • tests/SharpMUTerm.Core.Tests/ContrastTests.cs
  • tests/SharpMUTerm.Core.Tests/TriggerRouteMainTests.cs
  • tests/SharpMUTerm.Tui.Tests/FrameContrastTests.cs
  • tests/SharpMUTerm.Tui.Tests/LegiblePaletteTests.cs
  • tests/SharpMUTerm.Tui.Tests/MarkupFormatterTests.cs
  • tests/SharpMUTerm.Tui.Tests/PaneDropRendererTests.cs
  • tests/SharpMUTerm.Tui.Tests/ScreenChoiceListTests.cs
  • tests/SharpMUTerm.Tui.Tests/ScreenCursorTests.cs
  • tests/SharpMUTerm.Tui.Tests/ScreenModelTests.cs
  • tests/SharpMUTerm.Tui.Tests/TabActivityIndicatorTests.cs
  • tests/SharpMUTerm.Tui.Tests/TabTitlesTests.cs
  • tests/SharpMUTerm.Tui.Tests/TriggerKeepItHereTests.cs
  • tests/SharpMUTerm.Tui.Tests/TriggersScreenEditingTests.cs

Comment thread docs/superpowers/specs/2026-08-11-legible-colour-and-trigger-routing-design.md Outdated
Comment thread src/SharpMUTerm.Tui/TriggersScreenRenderer.cs
Comment thread src/SharpMUTerm.Tui/WorkspacePalette.cs
Comment thread tests/SharpMUTerm.Tui.Tests/FrameContrastTests.cs
Comment thread tests/SharpMUTerm.Tui.Tests/FrameContrastTests.cs
Three of CodeRabbit's five on PR #31.

FrameContrastTests.Pairs asked both regexes at every character. Regex.Match
searches *forward*, so each plain character re-scanned the same upcoming escape
from a later start -- quadratic in the length of every unstyled run, and a frame
is mostly padding. It now only asks at an escape; the 72-case suite goes
1.54s -> 1.32s, and the cost stops scaling with frame size.

The route trim: the finding was that `v == NoRoute` compares before `v.Trim()`,
so a padded "(none)" would be stored as a capture pane by that name. It is not --
ScreenField.WindowName's Set is `value => set(value.Trim())`, so the lambda is
handed an already-trimmed value and the `v.Trim()` in it is redundant. Pinned
rather than changed, since the two halves of that live in different files and
nothing else held them together.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nuKnWthnELNkrd86q5KWN

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/SharpMUTerm.Tui.Tests/FrameContrastTests.cs (1)

131-174: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Decode the terminal frame before counting contrast pairs.

Pairs treats terminal output as a linear glyph stream. It does not apply cursor movement, erases, or cell overwrites. The test can count text that is not present in the final rendered frame.

Replace the custom Sgr and Csi walker with FrameGrid.Decode. Derive contrast pairs from the final painted cells.

Based on learnings: “use the shared FrameGrid helper for ANSI frame parsing and inspection.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/SharpMUTerm.Tui.Tests/FrameContrastTests.cs` around lines 131 - 174,
Replace the custom Sgr/Csi parsing loop used to build contrast pairs with
FrameGrid.Decode, then inspect the decoded final painted cells to count only
visible glyphs and their foreground/background colors. Remove the bespoke ANSI
walker and preserve the existing filtering behavior from Count for spaces,
fills, and cells lacking complete color information.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@tests/SharpMUTerm.Tui.Tests/FrameContrastTests.cs`:
- Around line 131-174: Replace the custom Sgr/Csi parsing loop used to build
contrast pairs with FrameGrid.Decode, then inspect the decoded final painted
cells to count only visible glyphs and their foreground/background colors.
Remove the bespoke ANSI walker and preserve the existing filtering behavior from
Count for spaces, fills, and cells lacking complete color information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e90c2008-b832-4423-b907-e4885e77c961

📥 Commits

Reviewing files that changed from the base of the PR and between 66e590d and 6012640.

📒 Files selected for processing (3)
  • docs/superpowers/specs/2026-08-11-legible-colour-and-trigger-routing-design.md
  • tests/SharpMUTerm.Tui.Tests/FrameContrastTests.cs
  • tests/SharpMUTerm.Tui.Tests/TriggersScreenEditingTests.cs

CodeRabbit's outside-diff finding, and it was right about the premise: a frame
is cursor-addressed, so a walker that reads it linearly counts every glyph the
driver *wrote* rather than the ones left on screen.

The literal suggestion (use FrameGrid.Decode) is not implementable -- Decode
keeps glyphs and drops colour, and Backgrounds keeps backgrounds and drops
foregrounds, so neither can answer a contrast question. FrameGrid gains a
colour-aware `Cells` instead, and FrameContrastTests drops its own walker for
it. That also stops this suite being the third copy of a parser FrameGrid's own
remarks warn about ("a suite going quietly green on a frame it has misread").

Measured before changing: on all 72 genuine per-view frames the two walks agree
on the pair set exactly. The linear walk was a superset of the screen, so it
could have raised a false alarm but never missed an offender -- which is the
safe direction for an audit, and is why this is a tidy-up rather than a fix.

Also adds the guard that matters when swapping walkers: a walker handed the
wrong dimensions decodes nothing, and an audit over nothing passes. Counted in
cells rather than distinct pairs -- a settings screen is painted from the fixed
ScreenPalette and is legitimately down to four pairs (F1's composer), while the
thinnest real frame still paints 201 glyphs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nuKnWthnELNkrd86q5KWN
@HarryCordewener

Copy link
Copy Markdown
Member Author

Taken, though not in the shape suggested — and the premise was right, so worth recording what the measurement said.

FrameGrid.Decode cannot do this: it keeps glyphs and drops colour, and Backgrounds keeps backgrounds and drops foregrounds, so neither half can answer a contrast question. FrameGrid gains a colour-aware Cells instead (glyph + foreground + background per position) and FrameContrastTests drops its own walker for it. That also stops this suite being the third copy of a parser FrameGrid's own remarks warn about — "a suite going quietly green on a frame it has misread".

Measured before changing anything: on all 72 genuine per-view frames the linear walk and the grid walk agree on the pair set exactly. That is expected rather than lucky — every painted cell appears in the stream, so the stream is a superset of the screen. The error direction was therefore a false alarm on a colour nobody sees, never a missed offender, which is the safe way round for an audit. So this is a tidy-up that removes a caveat, not a fix for a live hole.

One thing did come out of chasing it that is worth having: swapping walkers is exactly the change that can make this suite vacuous, since a walker handed the wrong dimensions decodes nothing and an audit over nothing passes every assertion under it. There is now a floor on painted cells. It is counted in cells rather than distinct pairs deliberately — a settings screen is painted from the fixed ScreenPalette and is legitimately down to four pairs (F1's composer), while the thinnest real frame here still paints 201 glyphs, so a pair-count floor would be policing how colourful a view is instead of whether the frame parsed.

c5b8f04. 72/72 still pass, whole suite green.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1b1d3149-860c-462c-9a89-28c6e9b4f79a

📥 Commits

Reviewing files that changed from the base of the PR and between 6012640 and c5b8f04.

📒 Files selected for processing (2)
  • tests/SharpMUTerm.Tui.Tests/FrameContrastTests.cs
  • tests/SharpMUTerm.Tui.Tests/FrameGrid.cs

Comment thread tests/SharpMUTerm.Tui.Tests/FrameGrid.cs Outdated
CodeRabbit was right that ApplySgr mistracked three legal sequences: `CSI m`
(ECMA-48 makes it `CSI 0 m`, and splitting an empty string yields no codes, so
the loop ran no body and both colours stayed standing), and `39`/`49`, which
return one channel to default. None appears in any frame this driver emits --
19,681 SGR sequences across the 72 frames, all of them explicit `0;38;2;…` --
so it corrected no live reading. It is fixed because this walker is what every
suite asking about painted cells goes through.

Chasing it found a live one, and my first fix for it was wrong in the same way.
Backgrounds tested for the reset with `parameters.Contains("49")`, which reads
the 49 in a truecolor *argument* -- `38;2;49;5;6`, a foreground whose red channel
is 49 -- as the reset code and clears a background that sequence never mentions.
Splitting on `;` does not see that either: only a walk that consumes `38;2;r;g;b`
as one unit can tell an SGR code from a colour argument.

So Backgrounds is now a projection of the same walk Cells is, which is what this
file's own remarks already argued for ("three copies of a parser can drift into
disagreeing about which cells are painted, and the failure that produces is a
suite going quietly green on a frame it has misread"). It was three; it is one.

FrameGridCellsTests covers the walk directly on hand-written frames. Three of
its nine fail against the unfixed parser, verified by reverting it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nuKnWthnELNkrd86q5KWN
@HarryCordewener
HarryCordewener merged commit 94967fd into main Aug 12, 2026
2 of 3 checks passed
@HarryCordewener
HarryCordewener deleted the fix/readable-colours branch August 12, 2026 18:19

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/SharpMUTerm.Tui.Tests/FrameGrid.cs (1)

228-231: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Treat omitted SGR parameters as reset parameters.

Line 228 removes empty fields. Therefore, CSI ; m produces no SGR codes and preserves the active colours. Omitted SGR parameters default to zero, so this sequence must reset the colours.

Map empty fields to 0 instead of filtering them out. Add direct coverage for "\u001b[;m".

Proposed fix
 var codes = parameters.Split(';')
-    .Where(p => p.Length > 0)
-    .Select(p => int.Parse(p, CultureInfo.InvariantCulture))
+    .Select(p => p.Length == 0
+        ? 0
+        : int.Parse(p, CultureInfo.InvariantCulture))
     .ToList();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/SharpMUTerm.Tui.Tests/FrameGrid.cs` around lines 228 - 231, Update the
SGR parameter parsing around the codes construction to retain empty fields and
map each omitted field to integer 0 instead of filtering it out, so CSI ; m
resets colours. Add direct test coverage for the escape sequence "\u001b[;m".
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@tests/SharpMUTerm.Tui.Tests/FrameGrid.cs`:
- Around line 228-231: Update the SGR parameter parsing around the codes
construction to retain empty fields and map each omitted field to integer 0
instead of filtering it out, so CSI ; m resets colours. Add direct test coverage
for the escape sequence "\u001b[;m".

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 624b85e3-0727-4ced-8ae1-b76d249d5e16

📥 Commits

Reviewing files that changed from the base of the PR and between c5b8f04 and fa07203.

📒 Files selected for processing (2)
  • tests/SharpMUTerm.Tui.Tests/FrameGrid.cs
  • tests/SharpMUTerm.Tui.Tests/FrameGridCellsTests.cs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant