diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..721cb14 Binary files /dev/null and b/.DS_Store differ diff --git a/CHANGELOG.md b/CHANGELOG.md index e2f0ea1..32d0553 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/docs/worktree-colours.png b/docs/worktree-colours.png new file mode 100644 index 0000000..df5326f Binary files /dev/null and b/docs/worktree-colours.png differ diff --git a/src/lib/ideTheme.ts b/src/lib/ideTheme.ts index 51f5f21..944dbcc 100644 --- a/src/lib/ideTheme.ts +++ b/src/lib/ideTheme.ts @@ -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. // @@ -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 @@ -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" }; } diff --git a/tests/theme.test.ts b/tests/theme.test.ts index 2d12e2c..400d4ad 100644 --- a/tests/theme.test.ts +++ b/tests/theme.test.ts @@ -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(); for (let i = 0; i < 500; i++) {