fix: unify colour resolution behind a terminal profile - #94
fix: unify colour resolution behind a terminal profile#94Ayman Bagabas (aymanbagabas) wants to merge 3 commits into
Conversation
A screenshot and a color assertion disagreed about what a cell was painted. `render/svg.rs` carried a private sixteen-color table and `assert/color.rs` carried a different one, so `expect --fg "#800000"` passed on a cell the screenshot drew `#e88388`. Both tables are deleted here and both callers resolve through one profile, which is what makes them agree by construction rather than by coincidence. The palette had to become configurable to fix it anyway: the two tables could only be collapsed by choosing which one was right, and that choice belongs to the user rather than to whichever module was read first. The shipped default is the VGA/xterm palette that `TERM=xterm-256color` already promises, which is what the assertion side used. A profile is read from `shell-use.toml` and sets scrollback and colors. Only the sixteen ANSI slots and the three defaults are configurable; indices 16-255 are the xterm color cube and gray ramp, which are fixed by the spec, so a config that could move them would let two sessions disagree about what `--fg 196` means. Profiles are named, and `--profile` selects one. The file is looked up nearest first, project before user, so a repository can pin the terminal its tests expect. Resolution happens in the CLI rather than the daemon: the daemon is long-lived and shared, so it has no single working directory to resolve a project-local config against, and a resolved profile travels on `Request::Open` the same way timeouts already do. Absent settings take the default, and the field is `#[serde(default)]`, so a client that predates this keeps the behavior it had. Scrollback moves from a hardcoded 5,000 to a configurable 10,000, matching alacritty's own default. Two things are deliberately errors rather than silent fallbacks: an unknown profile name, which reports the ones that exist, and a config file that does not parse, which would otherwise run the session with settings nobody asked for. A *missing* file stays fine, since running without one is normal. Screenshots will look different: the default background is now black rather than the previous dark blue-gray, and the palette is saturated rather than muted. Both are recoverable in a profile. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
main moved the request types out of the core crate: `crates/shell-use/src/protocol.rs` was deleted, its wire types now live in `crates/shell-use-cli/src/protocol.rs`, and the core speaks `Operation` / `OpenOptions` from the new `api.rs`. The profile this branch adds followed that move. It is a field on `OpenOptions` and `RunOptions` rather than a loose `spawn` argument, and the wire `Request::Open` carries it as before, so the client still resolves the config file and the daemon still never reads one. The bindings default it, which is the behaviour they have today. Colour resolution kept main's renames (`session`, `cell`, `matched`) and this branch's palette argument, so assertions and screenshots still resolve through the one profile. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes a correctness gap where screenshots and expect --fg/--bg "#rrggbb" could disagree on the resolved RGB for the same ANSI palette index, by centralizing palette resolution in a session “profile” that is passed through open/run and used by both renderer and assertions.
Changes:
- Introduces
Profile/Colorstypes (TOML-backed) and resolves them in the CLI via--config/--profile(with search fallback). - Wires the resolved profile into session creation (scrollback) and into both SVG rendering and color assertion matching.
- Adds lifecycle tests to ensure screenshots and assertions stay consistent, including with a custom profile.
Show a summary per file
| File | Description |
|---|---|
| SKILL.md | Documents new open flags and adds a configuration section. |
| README.md | Documents configuration/profile usage and color resolution behavior. |
| crates/shell-use/src/session.rs | Stores a per-session Profile and uses it for emulator scrollback. |
| crates/shell-use/src/render/svg.rs | Removes private theme table; renders using provided Colors. |
| crates/shell-use/src/profile.rs | Adds profile/config parsing and unified color resolution logic. |
| crates/shell-use/src/lib.rs | Exposes the new profile module. |
| crates/shell-use/src/engine.rs | Passes profile through open/run; uses it for expect and screenshot rendering. |
| crates/shell-use/src/assert/color.rs | Makes #rrggbb matching resolve via session Colors (unified with renderer). |
| crates/shell-use/src/api.rs | Adds profile: Profile to OpenOptions / RunOptions. |
| crates/shell-use/Cargo.toml | Adds toml dependency for profile/config parsing. |
| Cargo.toml | Adds workspace toml dependency version. |
| Cargo.lock | Locks new transitive dependencies for toml. |
| crates/shell-use-cli/tests/session_lifecycle.rs | Adds end-to-end regression tests for screenshot/assertion color agreement and profile selection errors. |
| crates/shell-use-cli/src/protocol.rs | Extends Request::Open to carry profile (serde default for compatibility). |
| crates/shell-use-cli/src/main.rs | Resolves ProfileArgs and includes it in requests for open/run. |
| crates/shell-use-cli/src/cli.rs | Adds ProfileArgs (--config, --profile) to open/run commands. |
| bindings/python/native/src/lib.rs | Supplies default profile for native open/run operations. |
| bindings/js/native/lib.rs | Supplies default profile for native open/run operations. |
Review details
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 17/18 changed files
- Comments generated: 2
- Review effort level: Lite
| pub fn rgb(&self, index: u8) -> Rgb { | ||
| match index { | ||
| 0..=15 => self.ansi()[index as usize], | ||
| 16..=231 => { | ||
| let i = index as u16 - 16; | ||
| let level = |c: u16| -> u8 { | ||
| if c == 0 { | ||
| 0 | ||
| } else { | ||
| (c * 40 + 55) as u8 | ||
| } | ||
| }; | ||
| Rgb::new(level((i / 36) % 6), level((i / 6) % 6), level(i % 6)) | ||
| } | ||
| 232..=255 => { | ||
| let v = ((index as u16 - 232) * 10 + 8) as u8; | ||
| Rgb::new(v, v, v) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Measured this rather than guessing, and I am leaving it as is — happy to change it if you disagree.
Benchmark of the two shapes, release, 4000 lookups (an 80x24 screenshot resolves roughly that many):
| per lookup | per screenshot | |
|---|---|---|
ansi()[i] |
5.9 ns | 23.5 µs |
| 16-arm match | 1.3 ns | 5.3 µs |
So the array is ~4.5x slower per lookup but the whole saving is 18 µs per screenshot, against writing thousands of formatted SVG elements and a file. Adding #[inline] to ansi() does not close it (5.9 → 5.1 ns) because the index is runtime-variable, so the array still has to be materialised. The fix would be sixteen lines of match to buy that.
One correction to the premise, though: on the next PR in the stack (#95) Colors::rgb stops being the resolution path entirely — it is only reachable from tests there, and the live path becomes AlacrittyEmu::color, which reads colors.ansi() directly. So if this is worth doing it is worth doing there, not here.
The ellipsis in the `open` usage line means the flag before it can be repeated. Adding `--config` and `--profile` after it moved that mark onto `--profile`, which takes a single name. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Ayman Bagabas <ayman.bagabas@gmail.com>
|
Review pass on the stack — validated each comment before touching anything, and two of the six turned out not to need a change. Fixed here: the Not changed, thread left open for you: the The other four are on #95 and #96, two of which were real bugs:
|
First of a three PR stack: #94 → #95 → #96.
A screenshot and a color assertion disagreed about what color a cell was, because each resolved palette indices through its own hardcoded table.
render/svg.rshad a privateThemewhere red was#e88388;assert/color.rshad anANSI16table where red was#800000. Nothing kept them in sync and nothing noticed they had drifted.The bug
Same program, same red text, on
main:Asserting the color the image actually paints failed, and asserting a color that appears nowhere passed. Either the picture was lying or the assertion was, and a test suite could not tell you which.
#e88388#800000Both tables are deleted. One
Colorstype resolves every index once, so the renderer and the assertion cannot disagree by construction.--fg '#800000'now passes and matches the picture.Profiles
The palette is no longer a private constant, so a session can be given one.
--configpicks the file and--profilethe entry; discovery cascades./shell-use.tomlthen~/.shell-use/shell-use.toml.$ shell-use run --config shell-use.toml --profile solarized -- bashA profile is exactly 19 values: the 16 ANSI colors plus foreground, background, and cursor. It is read only for the life of the session, so a program cannot rewrite the palette a test was pinned against. Anything outside 0-15 comes from a static xterm table, and
scrollbackdefaults to 10000.Notes for review
--fg <index>compares the cell's palette index, so it was never affected by this bug and still is not. Only#rrggbbresolves through the palette."fg": 1), not RGB, so existing baselines survive a palette change. There are tests pinning that.#c0c0c0on background#000000, which is what the assertion side already used, so no assertion changes meaning.