session accent: derive from theme mode with collision avoidance
- Dark themes: warm hues (0-120), Light themes: cool hues (180-300) - Collision check against 17 major theme colors with 10° threshold, clamped to the hue band so it never drifts into an unrelated range - Hex values stored at construction instead of round-tripping through ANSI escape parsing (removes #ansiToHex) - WCAG AA contrast bisection on light surfaces - Per-session uniqueness via name hash within the hue band
This commit is contained in:
@@ -839,7 +839,9 @@ export class StatusLineComponent implements Component {
|
||||
const gapWidth = Math.max(1, topFillWidth - leftWidth - rightWidth);
|
||||
const sessionName =
|
||||
effectiveSettings.sessionAccent !== false ? this.session.sessionManager?.getSessionName() : undefined;
|
||||
const accentHex = sessionName ? getSessionAccentHex(sessionName, theme.accentSurfaceLuminance) : undefined;
|
||||
const accentHex = sessionName
|
||||
? getSessionAccentHex(sessionName, theme.getMajorThemeColorHexes(), theme.accentSurfaceLuminance)
|
||||
: undefined;
|
||||
const gapColor = getSessionAccentAnsi(accentHex) ?? theme.getFgAnsi("border");
|
||||
const gapFill = `${gapColor}${theme.boxRound.horizontal.repeat(gapWidth)}\x1b[39m`;
|
||||
return leftGroup + gapFill + rightGroup;
|
||||
|
||||
@@ -486,7 +486,9 @@ const sessionNameSegment: StatusLineSegment = {
|
||||
if (!name) return { content: "", visible: false };
|
||||
|
||||
const ansi =
|
||||
getSessionAccentAnsi(getSessionAccentHex(name, theme.accentSurfaceLuminance)) ?? theme.getFgAnsi("accent");
|
||||
getSessionAccentAnsi(
|
||||
getSessionAccentHex(name, theme.getMajorThemeColorHexes(), theme.accentSurfaceLuminance),
|
||||
) ?? theme.getFgAnsi("accent");
|
||||
return { content: `${ansi}${sanitizeStatusText(name)}\x1b[39m`, visible: true };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1067,7 +1067,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
} else {
|
||||
const accentEnabled = !isSettingsInitialized() || settings.get("statusLine.sessionAccent") !== false;
|
||||
const sessionName = accentEnabled ? this.sessionManager.getSessionName() : undefined;
|
||||
const hex = sessionName ? getSessionAccentHex(sessionName, theme.accentSurfaceLuminance) : undefined;
|
||||
const hex = sessionName
|
||||
? getSessionAccentHex(sessionName, theme.getMajorThemeColorHexes(), theme.accentSurfaceLuminance)
|
||||
: undefined;
|
||||
const ansi = getSessionAccentAnsi(hex);
|
||||
if (ansi) {
|
||||
this.editor.borderColor = (str: string) => `${ansi}${str}\x1b[39m`;
|
||||
@@ -2763,7 +2765,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
if (!key.sessionAccentEnabled || !key.sessionName) {
|
||||
return this.#cacheWorkingMessageAccent(key, undefined);
|
||||
}
|
||||
const hex = getSessionAccentHex(key.sessionName, key.accentSurfaceLuminance);
|
||||
const hex = getSessionAccentHex(key.sessionName, theme.getMajorThemeColorHexes(), key.accentSurfaceLuminance);
|
||||
const main = getSessionAccentAnsi(hex);
|
||||
const dim = getSessionAccentAnsi(adjustHsv(hex, { s: 0.55, v: 0.65 }));
|
||||
return this.#cacheWorkingMessageAccent(key, main && dim ? { main, dim } : undefined);
|
||||
|
||||
@@ -1383,9 +1383,23 @@ const langMap: Record<string, SymbolKey> = {
|
||||
bin: "lang.binary",
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve a theme color value (hex string or 256-color index) to a CSS hex string.
|
||||
* Empty string represents the default terminal color.
|
||||
*/
|
||||
function resolveToHex(value: string | number, isLight: boolean): string {
|
||||
if (typeof value === "number") return ansi256ToHex(value);
|
||||
if (value === "") return isLight ? "#000000" : "#e5e5e7";
|
||||
return value;
|
||||
}
|
||||
|
||||
export class Theme {
|
||||
#fgColors: Record<ThemeColor, string>;
|
||||
#bgColors: Record<ThemeBg, string>;
|
||||
/** Resolved hex strings for foreground colors — populated at construction. */
|
||||
readonly #hexFgColors: Record<ThemeColor, string>;
|
||||
/** Resolved hex strings for background colors — populated at construction. */
|
||||
readonly #hexBgColors: Record<ThemeBg, string>;
|
||||
#symbols: SymbolMap;
|
||||
#spinnerFramesOverrides: Partial<Record<SpinnerType, string[]>>;
|
||||
/**
|
||||
@@ -1398,7 +1412,6 @@ export class Theme {
|
||||
readonly statusLineLuminance: number | undefined;
|
||||
/** WCAG relative luminance of the status-line background — basis for accent contrast. */
|
||||
readonly #statusLineContrastLuminance: number | undefined;
|
||||
|
||||
constructor(
|
||||
fgColors: Record<ThemeColor, string | number>,
|
||||
bgColors: Record<ThemeBg, string | number>,
|
||||
@@ -1409,13 +1422,19 @@ export class Theme {
|
||||
) {
|
||||
this.statusLineLuminance = colorLuma(bgColors.statusLineBg);
|
||||
this.#statusLineContrastLuminance = relativeLuminance(bgColors.statusLineBg);
|
||||
const slIsLight = this.statusLineLuminance !== undefined && this.statusLineLuminance > 0.5;
|
||||
|
||||
this.#fgColors = {} as Record<ThemeColor, string>;
|
||||
this.#hexFgColors = {} as Record<ThemeColor, string>;
|
||||
for (const [key, value] of Object.entries(fgColors) as [ThemeColor, string | number][]) {
|
||||
this.#fgColors[key] = fgAnsi(value, mode);
|
||||
this.#hexFgColors[key] = resolveToHex(value, slIsLight);
|
||||
}
|
||||
this.#bgColors = {} as Record<ThemeBg, string>;
|
||||
this.#hexBgColors = {} as Record<ThemeBg, string>;
|
||||
for (const [key, value] of Object.entries(bgColors) as [ThemeBg, string | number][]) {
|
||||
this.#bgColors[key] = bgAnsi(value, mode);
|
||||
this.#hexBgColors[key] = resolveToHex(value, slIsLight);
|
||||
}
|
||||
// Build symbol map from preset + overrides
|
||||
const baseSymbols = SYMBOL_PRESETS[symbolPreset];
|
||||
@@ -1443,6 +1462,70 @@ export class Theme {
|
||||
return this.isLight ? this.#statusLineContrastLuminance : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resolved CSS hex string for a foreground theme color.
|
||||
*/
|
||||
getColorHex(color: ThemeColor): string {
|
||||
const hex = this.#hexFgColors[color];
|
||||
if (hex === undefined) throw new Error(`Unknown theme color: ${color}`);
|
||||
return hex || (this.isLight ? "#000000" : "#e5e5e7");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all foreground and background theme colors as CSS hex strings.
|
||||
* Skips colors resolved to the default terminal color (unstyled).
|
||||
*/
|
||||
getAllThemeColorHexes(): string[] {
|
||||
const hexes: string[] = [];
|
||||
for (const hex of Object.values(this.#hexFgColors)) {
|
||||
if (hex) hexes.push(hex);
|
||||
}
|
||||
for (const hex of Object.values(this.#hexBgColors)) {
|
||||
if (hex) hexes.push(hex);
|
||||
}
|
||||
return hexes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the most visually dominant theme colors as CSS hex strings — accent,
|
||||
* border, success, error, warning, heading, link, diff markers, etc.
|
||||
* These are the colors the session accent could visually clash with.
|
||||
* Skips colors resolved to the default terminal color (unstyled).
|
||||
*/
|
||||
getMajorThemeColorHexes(): string[] {
|
||||
const majors: ThemeColor[] = [
|
||||
"accent",
|
||||
"border",
|
||||
"borderAccent",
|
||||
"borderMuted",
|
||||
"success",
|
||||
"error",
|
||||
"warning",
|
||||
"mdHeading",
|
||||
"mdLink",
|
||||
"mdCode",
|
||||
"mdCodeBlock",
|
||||
"mdQuoteBorder",
|
||||
"mdListBullet",
|
||||
"toolDiffAdded",
|
||||
"toolDiffRemoved",
|
||||
"customMessageLabel",
|
||||
"thinkingText",
|
||||
];
|
||||
const hexes: string[] = [];
|
||||
for (const key of majors) {
|
||||
const hex = this.#hexFgColors[key];
|
||||
if (hex) hexes.push(hex);
|
||||
}
|
||||
return hexes;
|
||||
}
|
||||
/**
|
||||
* Get the resolved CSS hex string for the theme's accent color.
|
||||
*/
|
||||
getAccentColorHex(): string {
|
||||
return this.getColorHex("accent");
|
||||
}
|
||||
|
||||
fg(color: ThemeColor, text: string): string {
|
||||
const ansi = this.#fgColors[color];
|
||||
if (!ansi) throw new Error(`Unknown theme color: ${color}`);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { hslToHex, relativeLuminance } from "@oh-my-pi/pi-utils";
|
||||
import { hexToHsv, hslToHex, relativeLuminance } from "@oh-my-pi/pi-utils";
|
||||
|
||||
/**
|
||||
* Derive a stable hue (0-359) from a string using djb2 hash.
|
||||
@@ -25,23 +25,97 @@ function accentLuminanceCap(surfaceLuminance: number): number {
|
||||
return Math.max(0, (surfaceLuminance + 0.05) / ACCENT_MIN_CONTRAST - 0.05);
|
||||
}
|
||||
|
||||
/** Minimum angular distance in hue degrees from any theme color to avoid visual collision. */
|
||||
const MIN_HUE_DISTANCE = 10;
|
||||
/** Saturation threshold below which hue is meaningless (near-gray). */
|
||||
const MIN_SATURATION_FOR_HUE = 0.1;
|
||||
|
||||
/** Angular distance between two hue values (0-360). */
|
||||
function hueDistance(a: number, b: number): number {
|
||||
const d = Math.abs(a - b);
|
||||
return Math.min(d, 360 - d);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a stable CSS hex accent color from a session name.
|
||||
* Parse hue (0-360) from a hex color string.
|
||||
* Returns undefined for near-gray colors where hue is not meaningful.
|
||||
*/
|
||||
function hexToHue(hex: string): number | undefined {
|
||||
const hsv = hexToHsv(hex);
|
||||
if (hsv.s < MIN_SATURATION_FOR_HUE) return undefined;
|
||||
return hsv.h;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a hue at least {@link MIN_HUE_DISTANCE} from all occupied hues,
|
||||
* clamped to [lo, hi] to prevent leaving the intended hue band.
|
||||
* Returns `target` unchanged if no safe hue exists within bounds.
|
||||
*/
|
||||
function findSafeHue(target: number, occupied: number[], lo: number, hi: number): number {
|
||||
if (occupied.length === 0) return target;
|
||||
if (occupied.every(h => hueDistance(target, h) >= MIN_HUE_DISTANCE)) {
|
||||
return target;
|
||||
}
|
||||
for (let d = 1; d <= hi - lo; d++) {
|
||||
for (const dir of [1, -1]) {
|
||||
const candidate = Math.max(lo, Math.min(hi, target + d * dir));
|
||||
if (occupied.every(h => hueDistance(candidate, h) >= MIN_HUE_DISTANCE)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
// fallback: keep the original target if no safe spot exists within the band
|
||||
return target;
|
||||
}
|
||||
|
||||
/** Hue range low and high for dark themes (warm: red → yellow → green). */
|
||||
const DARK_HUE_START = 0;
|
||||
const DARK_HUE_END = 120;
|
||||
/** Hue range low and high for light themes (cool: cyan → blue → purple). */
|
||||
const LIGHT_HUE_START = 180;
|
||||
const LIGHT_HUE_END = 300;
|
||||
|
||||
/**
|
||||
* Derive a stable CSS hex accent color from a session name and the active theme.
|
||||
*
|
||||
* Picks a hue from a **dark/light-specific range** so the accent feels natural
|
||||
* for the theme type (warm on dark, cool on light). The session name hash
|
||||
* determines the exact hue within the range. The result is checked against
|
||||
* all theme color hues and shifted if it lands within {@link MIN_HUE_DISTANCE}
|
||||
* of an existing theme hue, but is clamped to the hue band so it never
|
||||
* drifts into an unrelated part of the spectrum.
|
||||
*
|
||||
* On dark themes (`surfaceLuminance` undefined) the accent is vivid (high
|
||||
* saturation, high lightness). On light themes the lightness is reduced until the
|
||||
* accent's perceived luminance clears {@link ACCENT_MIN_CONTRAST} against the
|
||||
* actual surface it renders on — so it stays legible on near-white *and* mid-light
|
||||
* backgrounds — while keeping the same per-session hue.
|
||||
* backgrounds.
|
||||
*
|
||||
* @param name — session name for per-session uniqueness.
|
||||
* @param themeColorHexes — all theme colors to check collision against.
|
||||
* @param surfaceLuminance — undefined on dark themes; WCAG luminance of the
|
||||
* status-line background on light themes.
|
||||
*/
|
||||
export function getSessionAccentHex(name: string, surfaceLuminance?: number): string {
|
||||
const hue = nameToHue(name);
|
||||
export function getSessionAccentHex(name: string, themeColorHexes: string[], surfaceLuminance?: number): string {
|
||||
// 1. Pick hue range based on theme mode
|
||||
const hueStart = surfaceLuminance === undefined ? DARK_HUE_START : LIGHT_HUE_START;
|
||||
const hueEnd = surfaceLuminance === undefined ? DARK_HUE_END : LIGHT_HUE_END;
|
||||
const range = hueEnd - hueStart;
|
||||
|
||||
// 2. Session name picks within the range
|
||||
let targetHue = hueStart + (nameToHue(name) % range);
|
||||
|
||||
// 3. Shift away if too close to any theme color — stays within [hueStart, hueEnd]
|
||||
const themeHues = themeColorHexes.map(hexToHue).filter((h): h is number => h !== undefined);
|
||||
targetHue = findSafeHue(targetHue, themeHues, hueStart, hueEnd);
|
||||
|
||||
// 4. Lightness/contrast — vivid on dark, bisected for AA on light
|
||||
if (surfaceLuminance === undefined) {
|
||||
return hslToHex(hue, ACCENT_SATURATION, ACCENT_DARK_LIGHTNESS);
|
||||
return hslToHex(targetHue, ACCENT_SATURATION, ACCENT_DARK_LIGHTNESS);
|
||||
}
|
||||
|
||||
const cap = accentLuminanceCap(surfaceLuminance);
|
||||
const top = hslToHex(hue, ACCENT_SATURATION, ACCENT_DARK_LIGHTNESS);
|
||||
const top = hslToHex(targetHue, ACCENT_SATURATION, ACCENT_DARK_LIGHTNESS);
|
||||
if ((relativeLuminance(top) ?? 0) <= cap) return top;
|
||||
|
||||
// Bisect lightness: `lo` always yields luminance <= cap, `hi` always above it.
|
||||
@@ -49,13 +123,13 @@ export function getSessionAccentHex(name: string, surfaceLuminance?: number): st
|
||||
let hi = ACCENT_DARK_LIGHTNESS;
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const mid = (lo + hi) / 2;
|
||||
if ((relativeLuminance(hslToHex(hue, ACCENT_SATURATION, mid)) ?? 0) > cap) {
|
||||
if ((relativeLuminance(hslToHex(targetHue, ACCENT_SATURATION, mid)) ?? 0) > cap) {
|
||||
hi = mid;
|
||||
} else {
|
||||
lo = mid;
|
||||
}
|
||||
}
|
||||
return hslToHex(hue, ACCENT_SATURATION, lo);
|
||||
return hslToHex(targetHue, ACCENT_SATURATION, lo);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -97,10 +97,22 @@ describe("InteractiveMode working-message session accent cache", () => {
|
||||
const renamedName = "Beta session";
|
||||
const { mode, sessionManager } = await createHarness(initialName);
|
||||
const initialAnsi = defined(
|
||||
sessionColor.getSessionAccentAnsi(sessionColor.getSessionAccentHex(initialName, theme.accentSurfaceLuminance)),
|
||||
sessionColor.getSessionAccentAnsi(
|
||||
sessionColor.getSessionAccentHex(
|
||||
initialName,
|
||||
theme.getMajorThemeColorHexes(),
|
||||
theme.accentSurfaceLuminance,
|
||||
),
|
||||
),
|
||||
);
|
||||
const renamedAnsi = defined(
|
||||
sessionColor.getSessionAccentAnsi(sessionColor.getSessionAccentHex(renamedName, theme.accentSurfaceLuminance)),
|
||||
sessionColor.getSessionAccentAnsi(
|
||||
sessionColor.getSessionAccentHex(
|
||||
renamedName,
|
||||
theme.getMajorThemeColorHexes(),
|
||||
theme.accentSurfaceLuminance,
|
||||
),
|
||||
),
|
||||
);
|
||||
const getHex = vi.spyOn(sessionColor, "getSessionAccentHex");
|
||||
|
||||
@@ -143,7 +155,13 @@ describe("InteractiveMode working-message session accent cache", () => {
|
||||
const sessionName = "Toggle session";
|
||||
const { mode } = await createHarness(sessionName);
|
||||
const accentAnsi = defined(
|
||||
sessionColor.getSessionAccentAnsi(sessionColor.getSessionAccentHex(sessionName, theme.accentSurfaceLuminance)),
|
||||
sessionColor.getSessionAccentAnsi(
|
||||
sessionColor.getSessionAccentHex(
|
||||
sessionName,
|
||||
theme.getMajorThemeColorHexes(),
|
||||
theme.accentSurfaceLuminance,
|
||||
),
|
||||
),
|
||||
);
|
||||
const getHex = vi.spyOn(sessionColor, "getSessionAccentHex");
|
||||
|
||||
|
||||
@@ -1,34 +1,55 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { getThemeByName } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
|
||||
|
||||
import { getSessionAccentHex } from "@oh-my-pi/pi-coding-agent/utils/session-color";
|
||||
import { relativeLuminance } from "@oh-my-pi/pi-utils";
|
||||
import { hexToHsv, relativeLuminance } from "@oh-my-pi/pi-utils";
|
||||
|
||||
const NO_THEME_COLORS: string[] = [];
|
||||
|
||||
const lum = (hex: string): number => relativeLuminance(hex) ?? 0;
|
||||
const contrast = (a: number, b: number): number => (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);
|
||||
|
||||
const names = Array.from({ length: 600 }, (_, i) => `analyze-debian-trixie-${i}`);
|
||||
|
||||
// Shipped light statusLineBg surfaces (WCAG luminance), near-white through mid-light.
|
||||
const SURFACES: Record<string, number> = {
|
||||
"light-catppuccin crust (#dce0e8)": lum("#dce0e8"),
|
||||
"light-poimandres (#7390aa)": lum("#7390aa"),
|
||||
};
|
||||
|
||||
describe("getSessionAccentHex", () => {
|
||||
it("is deterministic per name and surface", () => {
|
||||
expect(getSessionAccentHex("analyze debian trixie")).toBe(getSessionAccentHex("analyze debian trixie"));
|
||||
expect(getSessionAccentHex("x", 0.7)).toBe(getSessionAccentHex("x", 0.7));
|
||||
it("is deterministic per name and parameters", () => {
|
||||
expect(getSessionAccentHex("analyze debian trixie", NO_THEME_COLORS)).toBe(
|
||||
getSessionAccentHex("analyze debian trixie", NO_THEME_COLORS),
|
||||
);
|
||||
expect(getSessionAccentHex("x", NO_THEME_COLORS, 0.7)).toBe(getSessionAccentHex("x", NO_THEME_COLORS, 0.7));
|
||||
});
|
||||
|
||||
it("uses warm hues (0-120) on dark themes", () => {
|
||||
for (const name of names) {
|
||||
const h = hexToHsv(getSessionAccentHex(name, NO_THEME_COLORS)).h;
|
||||
expect(h).toBeGreaterThanOrEqual(0);
|
||||
expect(h).toBeLessThanOrEqual(120);
|
||||
}
|
||||
});
|
||||
|
||||
it("uses cool hues (180-300) on light themes", () => {
|
||||
for (const name of names) {
|
||||
const h = hexToHsv(getSessionAccentHex(name, NO_THEME_COLORS, 0.5)).h;
|
||||
expect(h).toBeGreaterThanOrEqual(180);
|
||||
expect(h).toBeLessThanOrEqual(300);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps vivid (bright) accents on dark themes (undefined surface)", () => {
|
||||
const maxDark = Math.max(...names.map(n => lum(getSessionAccentHex(n))));
|
||||
const maxDark = Math.max(...names.map(n => lum(getSessionAccentHex(n, NO_THEME_COLORS))));
|
||||
expect(maxDark).toBeGreaterThan(0.5);
|
||||
});
|
||||
|
||||
it("clears AA-large WCAG contrast against light surfaces, including mid-light", () => {
|
||||
for (const bg of Object.values(SURFACES)) {
|
||||
for (const name of names) {
|
||||
const hex = getSessionAccentHex(name, bg);
|
||||
expect(contrast(lum(hex), bg)).toBeGreaterThanOrEqual(2.99); // ~3:1, float margin
|
||||
const hex = getSessionAccentHex(name, NO_THEME_COLORS, bg);
|
||||
expect(contrast(lum(hex), bg)).toBeGreaterThanOrEqual(2.99);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -36,7 +57,48 @@ describe("getSessionAccentHex", () => {
|
||||
it("never produces a lighter accent on light themes than on dark for the same name", () => {
|
||||
const nearWhite = SURFACES["light-catppuccin crust (#dce0e8)"];
|
||||
for (const name of names) {
|
||||
expect(lum(getSessionAccentHex(name, nearWhite))).toBeLessThanOrEqual(lum(getSessionAccentHex(name)) + 1e-9);
|
||||
expect(lum(getSessionAccentHex(name, NO_THEME_COLORS, nearWhite))).toBeLessThanOrEqual(
|
||||
lum(getSessionAccentHex(name, NO_THEME_COLORS)) + 1e-9,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSessionAccentHex with real Theme", () => {
|
||||
it("stays in the cool band and avoids theme hues on light-catppuccin", async () => {
|
||||
const theme = await getThemeByName("light-catppuccin");
|
||||
if (!theme) return; // skip if theme not found
|
||||
const colors = theme.getMajorThemeColorHexes();
|
||||
const surface = theme.accentSurfaceLuminance;
|
||||
const themeHues = colors.map(c => hexToHsv(c).h).filter(h => hexToHsv(colors[0]).s >= 0.1);
|
||||
|
||||
for (const name of ["alpha", "beta", "gamma", "delta", "epsilon", "zeta"]) {
|
||||
const hex = getSessionAccentHex(name, colors, surface);
|
||||
const h = hexToHsv(hex).h;
|
||||
expect(h).toBeGreaterThanOrEqual(180);
|
||||
expect(h).toBeLessThanOrEqual(300);
|
||||
for (const th of themeHues) {
|
||||
const dist = Math.min(Math.abs(h - th), 360 - Math.abs(h - th));
|
||||
expect(dist).toBeGreaterThanOrEqual(10);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("stays in the warm band and avoids theme hues on dark-catppuccin", async () => {
|
||||
const theme = await getThemeByName("dark-catppuccin");
|
||||
if (!theme) return;
|
||||
const colors = theme.getMajorThemeColorHexes();
|
||||
const themeHues = colors.map(c => hexToHsv(c).h).filter(h => hexToHsv(colors[0]).s >= 0.1);
|
||||
|
||||
for (const name of ["alpha", "beta", "gamma", "delta", "epsilon", "zeta"]) {
|
||||
const hex = getSessionAccentHex(name, colors);
|
||||
const h = hexToHsv(hex).h;
|
||||
expect(h).toBeGreaterThanOrEqual(0);
|
||||
expect(h).toBeLessThanOrEqual(120);
|
||||
for (const th of themeHues) {
|
||||
const dist = Math.min(Math.abs(h - th), 360 - Math.abs(h - th));
|
||||
expect(dist).toBeGreaterThanOrEqual(10);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -102,7 +102,7 @@ describe("status line session accent", () => {
|
||||
return component;
|
||||
}
|
||||
|
||||
const accentAnsi = getSessionAccentAnsi(getSessionAccentHex("Named session"));
|
||||
const accentAnsi = getSessionAccentAnsi(getSessionAccentHex("Named session", theme.getMajorThemeColorHexes()));
|
||||
|
||||
it("paints the gap with the session accent when enabled", () => {
|
||||
expect(accentAnsi).toBeDefined();
|
||||
|
||||
Reference in New Issue
Block a user