Skip to content

Per-character pane tints: identity in hue, focus in luminance - #22

Merged
HarryCordewener merged 2 commits into
mainfrom
feat/character-pane-tint
Aug 11, 2026
Merged

Per-character pane tints: identity in hue, focus in luminance#22
HarryCordewener merged 2 commits into
mainfrom
feat/character-pane-tint

Conversation

@HarryCordewener

@HarryCordewenerHarryCordewener commented Aug 11, 2026

Copy link
Copy Markdown
Member

Per-character output-pane background colouring, so a workspace holding several characters says whose pane you are looking at without being read.

What it is

  • CharacterDefinition.Tint (Core) — a PaneTint: None (the default) plus six named colours, Slate / Teal / Moss / Ochre / Ember / Plum.
  • WorkspacePalette.Tint(theme, tint) (Tui) — resolves a name to a plane derived from the active theme's own surface. Core names the colour; only the Tui knows a hex for it, so the setting stays UI-agnostic.
  • F5 ▸ CHARACTER gains a tint row (tint None — this character's pane), a closed list cycled with ↑↓ like the log format.
  • --snapshot --view tint renders two characters' panes in two colours, one focused and one not.

The design decisions

Identity is hue, focus is luminance, and they are kept on separate channels. The character tint replaces the pane's base plane and the existing focus step is applied on top of it — PaneSurfaceTone is focused ? Focus(plane) : plane, where plane used to be Surface(theme) unconditionally.

A tint changes the plane's colour and not its brightness. The anchor is re-lit to the surface's own luma before it is mixed in (AtLuma), and luma is linear in the channels, so the blend is luminance-neutral by construction rather than by a constant somebody tuned. This is the legibility argument: a MU* server chooses the colours of the text drawn on this plane, so a tint that lightened or darkened the pane would change every contrast ratio the theme was designed around, on a palette the client neither controls nor can test. Measured off the real frames — untinted #222226 (luma 34.5) → Slate #1b2331 (luma 34.2); untinted focused #36363d (54.8) → Moss focused #2e3e28 (54.7).

It also means the focus step lands the same distance above a tinted plane as above a plain one, so a colour cannot make one character's pane look more focused than another's.

Two consequences stated rather than discovered. The tint says nothing on a monochrome terminal, deliberately — the cue that has to survive a lost hue is focus, and the rail and the tab title still name the character in words. And it is a truecolor cue: at the surface's luminance a 256-colour terminal will quantise the tints onto the untinted entry, which degrades to exactly the pane there would otherwise be (unacceptable for focus, which is why FocusSurvivesA256ColourTerminal exists and has no tint counterpart).

It costs no cells. Per-pane NAWS is derived from the pane rectangle, so an identity cue that spent a column would announce a new terminal size to every connected server the moment somebody picked a colour. PaneTintTests.TintingACharacterMovesNoPaneRectangle is the pin, at four terminal sizes, and it commits through SaveConfiguration — which is also what makes an F5 edit reach the panes now rather than at the user's next focus move.

A pane wears the colour of the window in front of it, resolved through the workspace's ownership record and never through _active. A pane can host several characters' windows as tabs and paints one rectangle; a background pane wearing the focused character's colour would say the opposite of what it means. Same rule as WindowSession.

A name, not a hex. A free colour here cannot be validated against a theme the user may change tomorrow, and a hex picked against a dark theme is a hole on a light one. Six hues spread around the wheel, because the failure this feature has is two characters whose colours a reader has to compare rather than recognise.

No migration and no schema bump. An absent field deserializes to None, which is precisely the behaviour the configuration already had — the same reasoning as ConnectAtStartup. The demo config carries no tints either, so every other snapshot in the gallery is unchanged; the tint view writes the colours onto the real CharacterDefinitions and everything downstream is the shipping path.

Verification

dotnet build SharpMUTerm.slnx0 Warning(s), 0 Error(s).

All five TUnit suites, run directly:

suitetotalfailed
Core8500
Tui15250
Graphics830
Scripting420
Web370

New tests:

  • Core (PaneTintConfigurationTests) — a new character is untinted; a document written before the field loads untinted; a v1 document comes through every migration step untinted; the value round-trips by name ("tint": "Moss", not an ordinal); every member round-trips. Plus DefinitionCloneTests: a duplicate carries the colour.
  • Tui (WorkspacePaletteTests) — a tinted plane is within a point of luma of the untinted one on every shipped theme; every tint is its own plane; None is the surface byte for byte; focus still lifts a tinted pane; the focus step is the same above a tinted plane as above a plain one; two tints differ in hue with brightness discounted.
  • Tui (PaneTintTests, end-to-end on painted cells) — the colour reaches the right pane and is on the frame inside that pane's rectangle; an untinted workspace is the plain surface; two characters' panes wear their own colours; ⌃→ lifts a tinted pane and leaves its colour behind; the plane follows the window in front; an unowned window (the web view) is untinted; and the rectangle pin above.
  • SnapshotViewEmptyConfigTests gains tint — and away/away-scrollback, which it was missing.

Mutation-checked: deleting the RefreshPaneFocus() call from SaveConfiguration fails 4 tests.

Rendered frames looked at: --view tint at 120×32 (two panes, two colours, one focused), --view worlds at 120×36 and 80×20 (the new F5 row, and that it still fits the 48-cell panel when the screen compacts).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added optional per-character pane tints with six named colors plus an untinted option.
    • Added tint selection to character settings, with changes applied immediately to existing panes.
    • Pane tints now appear across workspace surfaces while preserving focus brightness and layout dimensions.
    • Tint ownership follows the frontmost window’s character; unassigned windows remain untinted.
  • Compatibility
    • Existing configurations continue to load with untinted characters by default.
  • Documentation
    • Updated visual verification and design documentation to describe pane tint behavior and available options.

HarryCordewenerand others added 2 commits August 10, 2026 22:24
`CharacterDefinition.Tint` — a `PaneTint`, one of six named colours or
`None` — so a workspace holding several characters can say whose pane is
whose. Core only names the colour; what it resolves to is the Tui's
business, which is what keeps the setting UI-agnostic.
A name and not a `#rrggbb`: this is the plane the *game's* text is read
against, the game chooses that text's colours, and a hex cannot be
validated against a theme the user may change tomorrow. A closed set can
be, and a name survives the theme change a hex would not.
On the character and not on the world: `WorldDefinition.Accent` already
exists and is a foreground marking a world's windows. Two characters on
one world is exactly the case this is for, and a world-level colour
cannot tell them apart.
No migration, and the schema version does not move. An absent field
deserializes to `None`, which is precisely the behaviour the
configuration already had — the same reasoning as `ConnectAtStartup`, and
the opposite of the v2→v3 encoding step, where a stored value started
meaning something else. A migration that assigned colours would repaint
the workspace of every user who never asked for one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A character given a `tint` on F5 has its panes painted in that colour, so
a glance says whose pane you are looking at. `WorkspacePalette.Tint`
derives every colour from the active theme's own surface; the F5
CHARACTER form gains a `tint` row (a closed list, the enum's members);
`--view tint` renders two characters' panes side by side.
Identity is hue, focus is luminance, and the two are kept on separate
channels so neither can be read as the other. The tint changes the
plane's colour without changing its brightness — the anchor is re-lit to
the surface's own luma before it is mixed, and luma is linear in the
channels, so the blend is luminance-neutral by construction rather than
by a tuned constant. That is the legibility guarantee: the server picks
the colours of the text drawn on this plane, and a tint that moved the
brightness would change every contrast ratio the theme was designed
around, on a palette we cannot test. It also means the focus step lands
the same distance above a tinted plane as above a plain one, so a colour
cannot make one character's pane look more focused than another's.
Two consequences stated rather than discovered. The tint says nothing on
a monochrome terminal, deliberately — the cue that must survive a lost
hue is focus, and the rail and tab title still name the character in
words. And it is a truecolor cue: at the surface's luminance a
256-colour terminal quantises the tints onto the untinted entry, which
degrades to exactly the pane there would otherwise be. Unacceptable for
focus; fine here.
It costs no cells, for the NAWS reason the focus cue does not —
`PaneTintTests.TintingACharacterMovesNoPaneRectangle` is the pin, and it
commits through `SaveConfiguration`, which is also what makes an F5 edit
reach the panes now rather than at the next focus move. A pane wears the
colour of the window *in front of it*, resolved through the workspace's
ownership record and never through `_active`: a background pane wearing
the focused character's colour would say the opposite of what it means.
Also adds `away`/`away-scrollback` to the empty-config view sweep, which
they were missing from.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2bbf79f4-cefe-4c6c-b245-d15b31a1f43d

📥 Commits

Reviewing files that changed from the base of the PR and between 72bb802 and d4dc47f.

📒 Files selected for processing (14)
  • CLAUDE.md
  • docs/PLAN.md
  • docs/design/README.md
  • src/SharpMUTerm.Core/Configuration/CharacterDefinition.cs
  • src/SharpMUTerm.Core/Configuration/PaneTint.cs
  • src/SharpMUTerm.Tui/SharpMUTermApp.cs
  • src/SharpMUTerm.Tui/WorkspacePalette.cs
  • src/SharpMUTerm.Tui/WorldsScreenRenderer.cs
  • tests/SharpMUTerm.Core.Tests/Configuration/DefinitionCloneTests.cs
  • tests/SharpMUTerm.Core.Tests/Configuration/PaneTintConfigurationTests.cs
  • tests/SharpMUTerm.Tui.Tests/PaneTintTests.cs
  • tests/SharpMUTerm.Tui.Tests/ScreenModelTests.cs
  • tests/SharpMUTerm.Tui.Tests/SnapshotViewEmptyConfigTests.cs
  • tests/SharpMUTerm.Tui.Tests/WorkspacePaletteTests.cs

Walkthrough

Adds six named per-character pane tints and PaneTint.None. Tint-aware rendering preserves pane luminance, composes focus brightness, follows frontmost-window ownership, persists configuration, and leaves pane layout unchanged.

Changes

Pane Tinting

Layer / File(s)Summary
Tint configuration contract
src/SharpMUTerm.Core/Configuration/PaneTint.cs, src/SharpMUTerm.Core/Configuration/CharacterDefinition.cs, tests/SharpMUTerm.Core.Tests/Configuration/*
Adds the PaneTint enum, stores tint on characters, copies it during cloning, and validates configuration round trips.
Luminance-preserving palette rendering
src/SharpMUTerm.Tui/WorkspacePalette.cs, tests/SharpMUTerm.Tui.Tests/WorkspacePaletteTests.cs
Adds six tint anchors, preserves surface luminance, and applies focus scaling to tinted planes.
Settings and workspace integration
src/SharpMUTerm.Tui/WorldsScreenRenderer.cs, src/SharpMUTerm.Tui/SharpMUTermApp.cs, tests/SharpMUTerm.Tui.Tests/PaneTintTests.cs, tests/SharpMUTerm.Tui.Tests/ScreenModelTests.cs, tests/SharpMUTerm.Tui.Tests/SnapshotViewEmptyConfigTests.cs
Adds tint editing, resolves pane ownership, paints tinted panes and tab chips, refreshes saved settings, and verifies rendering and layout behavior.
Documentation and visual verification
CLAUDE.md, docs/PLAN.md, docs/design/README.md
Documents tint behavior and adds the tint snapshot view.

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

Sequence Diagram(s)

sequenceDiagram
participant CharacterForm
participant CharacterDefinition
participant WorkspacePalette
participant SharpMUTermApp
CharacterForm->>CharacterDefinition: set Tint
CharacterDefinition->>SharpMUTermApp: save configuration
SharpMUTermApp->>WorkspacePalette: resolve pane plane
WorkspacePalette-->>SharpMUTermApp: return adjusted color
SharpMUTermApp-->>CharacterForm: repaint pane surfaces
Loading

Possibly related PRs

Suggested reviewers:claude

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly and concisely describes the main change: per-character pane tints with hue-based identity and luminance-based focus.
Docstring Coverage✅ PassedDocstring coverage is 82.98% which is sufficient. The required threshold is 80.00%.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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

Sign up for freeto 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

@HarryCordewener