Uh oh!
There was an error while loading. Please reload this page.
Per-character pane tints: identity in hue, focus in luminance - #22
Merged
Conversation
`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>
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (14)
WalkthroughAdds six named per-character pane tints and ChangesPane Tinting
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
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
Comment |
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 12, 2026
Merged
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 freeto 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.
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) — aPaneTint: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.tintrow (tint None — this character's pane), a closed list cycled with ↑↓ like the log format.--snapshot --view tintrenders 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 —
PaneSurfaceToneisfocused ? Focus(plane) : plane, whereplaneused to beSurface(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
FocusSurvivesA256ColourTerminalexists 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.TintingACharacterMovesNoPaneRectangleis the pin, at four terminal sizes, and it commits throughSaveConfiguration— 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 asWindowSession.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 asConnectAtStartup. The demo config carries no tints either, so every other snapshot in the gallery is unchanged; thetintview writes the colours onto the realCharacterDefinitions and everything downstream is the shipping path.Verification
dotnet build SharpMUTerm.slnx— 0 Warning(s), 0 Error(s).All five TUnit suites, run directly:
New tests:
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. PlusDefinitionCloneTests: a duplicate carries the colour.WorkspacePaletteTests) — a tinted plane is within a point of luma of the untinted one on every shipped theme; every tint is its own plane;Noneis 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.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.SnapshotViewEmptyConfigTestsgainstint— andaway/away-scrollback, which it was missing.Mutation-checked: deleting the
RefreshPaneFocus()call fromSaveConfigurationfails 4 tests.Rendered frames looked at:
--view tintat 120×32 (two panes, two colours, one focused),--view worldsat 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