Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## Unreleased

### Fixed

- **Some worktree colours barely looked coloured.** Every colour has to be dark
enough for white text to stay readable, and VS Code's default chrome is dark
too, so the two could drift close: the dullest, darkest quarter of the palette
sat ΔE 22 from an unthemed window. That combination is gone. The worst case is
now 27, and telling worktrees apart from each other is unaffected — collisions
among four stay at seven percent.

Existing worktrees change colour the next time `gwt add` or `gwt open` themes
them.

## 0.8.2

Nothing to reinstall — the shell wrapper is unchanged.
Expand Down
Binary file added docs/worktree-colours.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 13 additions & 6 deletions src/lib/ideTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ const FORMATTING = { tabSize: 2, insertSpaces: true };
// so worktrees collide by the birthday problem rather than by any flaw: with
// four open, two are perceptually identical (ΔE < 5) about one time in five.
//
// Varying lightness and saturation as well turns one wheel into four, which
// drops that to roughly one in twelve. Both variants of each keep white text
// Varying lightness and saturation as well turns one wheel into three, which
// drops that to roughly one in fourteen. Both variants of each keep white text
// above WCAG AA — measured at 4.53:1 in the worst case, against the 4.5 floor —
// so the readability the fixed values protected is not traded away for it.
//
Expand All @@ -24,8 +24,16 @@ const FORMATTING = { tabSize: 2, insertSpaces: true };
// determinism (the same branch would look different on another machine) and
// breaks under two concurrent `gwt add` — a real scenario for anyone running
// parallel agents. Not worth it for the remaining eight percent.
const SATURATIONS = [0.6, 0.42] as const;
const LIGHTNESSES = [0.3, 0.22] as const;
// Three combinations, not the four that hue × lightness × saturation allows.
// The darkest and dullest of them (0.42 / 0.22) sat ΔE 22 from VS Code's default
// dark chrome — close enough that a themed window read as barely themed at all.
// Dropping it lifts the worst case to 27 and costs nothing measurable:
// collisions among four worktrees stay at seven percent either way.
const COMBINATIONS = [
{ saturation: 0.6, lightness: 0.3 },
{ saturation: 0.6, lightness: 0.22 },
{ saturation: 0.42, lightness: 0.3 },
] as const;

// A plain `h * 31 + c` hash keeps its low bits close for inputs that are close:
// `feature-a` and `feature-b` differ by 1 in the hash and so landed 1° apart on
Expand Down Expand Up @@ -77,8 +85,7 @@ export function pickColor(branch: string): PaletteEntry {
// Separate bits for each axis: the low bits already carry the hue, so reusing
// them would tie lightness to hue and waste the extra dimension.
const hue = h % 360;
const lightness = LIGHTNESSES[(h >>> 9) & 1];
const saturation = SATURATIONS[(h >>> 10) & 1];
const { saturation, lightness } = COMBINATIONS[(h >>> 9) % COMBINATIONS.length];
return { bg: hslToHex(hue, saturation, lightness), fg: "#ffffff" };
}

Expand Down
20 changes: 20 additions & 0 deletions tests/theme.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,26 @@ describe("pickColor", () => {
assert.ok(rate < 0.12, `${(rate * 100).toFixed(0)}% collided, expected under 12%`);
});

// Telling worktrees apart from each other is not enough: a themed window has
// to look themed. Every colour is dark, because white text has to stay
// readable on it, and VS Code's default chrome is dark too — so the two can
// drift close. One combination did, and read as an unthemed window.
test("every colour is visibly different from the default dark chrome", () => {
const DEFAULT_CHROME = "#181818";
let worst = Infinity;
let worstBranch = "";
for (let i = 0; i < 2000; i++) {
const branch = `branch/${i}`;
const d = deltaE(pickColor(branch).bg, DEFAULT_CHROME);
if (d < worst) {
worst = d;
worstBranch = branch;
}
}
// Measured at 27 with three combinations, 22 with the four-way palette.
assert.ok(worst >= 25, `${worstBranch} is only ΔE ${worst.toFixed(0)} from the default`);
});

test("the palette really uses more than one wheel", () => {
const seen = new Set<string>();
for (let i = 0; i < 500; i++) {
Expand Down
Loading