mirror of
https://github.com/Narcooo/inkos.git
synced 2026-09-01 15:08:51 +08:00
feat(tui): auto-detect terminal background and switch color scheme
Detect light/dark terminal via $COLORFGBG env var (same method as Claude Code's initial seed). Provide distinct dark and light palettes so all UI colors remain readable on both backgrounds. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
||||
import type { TerminalTheme } from "../theme.js";
|
||||
|
||||
describe("detectTerminalTheme", () => {
|
||||
const originalEnv = process.env.COLORFGBG;
|
||||
|
||||
afterEach(() => {
|
||||
if (originalEnv === undefined) {
|
||||
delete process.env.COLORFGBG;
|
||||
} else {
|
||||
process.env.COLORFGBG = originalEnv;
|
||||
}
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
async function detect(colorfgbg?: string): Promise<TerminalTheme> {
|
||||
if (colorfgbg !== undefined) {
|
||||
process.env.COLORFGBG = colorfgbg;
|
||||
} else {
|
||||
delete process.env.COLORFGBG;
|
||||
}
|
||||
const mod = await import("../theme.js");
|
||||
return mod.detectTerminalTheme();
|
||||
}
|
||||
|
||||
it("defaults to dark when COLORFGBG is not set", async () => {
|
||||
expect(await detect()).toBe("dark");
|
||||
});
|
||||
|
||||
it("detects dark background from COLORFGBG with bg index 0", async () => {
|
||||
expect(await detect("15;0")).toBe("dark");
|
||||
});
|
||||
|
||||
it("detects dark background from three-part COLORFGBG", async () => {
|
||||
expect(await detect("15;0;0")).toBe("dark");
|
||||
});
|
||||
|
||||
it("detects light background from COLORFGBG with bg index 15", async () => {
|
||||
expect(await detect("0;15")).toBe("light");
|
||||
});
|
||||
|
||||
it("detects light background from bg index 7", async () => {
|
||||
expect(await detect("0;7")).toBe("light");
|
||||
});
|
||||
|
||||
it("treats index 8 as dark", async () => {
|
||||
expect(await detect("15;8")).toBe("dark");
|
||||
});
|
||||
|
||||
it("defaults to dark for unparseable values", async () => {
|
||||
expect(await detect("garbage")).toBe("dark");
|
||||
});
|
||||
});
|
||||
@@ -1,14 +1,68 @@
|
||||
export const WARM_ACCENT = "#d4a070";
|
||||
export const WARM_MUTED = "#8f8374";
|
||||
export const WARM_REPLY = "#a8c4d4";
|
||||
export const WARM_BORDER = "#6b6156";
|
||||
export type TerminalTheme = "dark" | "light";
|
||||
|
||||
// Semantic status colors
|
||||
export const STATUS_SUCCESS = "#7ec87e";
|
||||
export const STATUS_ERROR = "#e06060";
|
||||
export const STATUS_ACTIVE = "#d4a76a";
|
||||
export const STATUS_IDLE = "#7a7268";
|
||||
/**
|
||||
* Detect terminal background via $COLORFGBG (format: "fg;bg" or "fg;other;bg").
|
||||
* ANSI color indices 0-6 and 8 are dark; 7 and 9-15 are light.
|
||||
* Falls back to "dark" when the variable is missing or unparseable.
|
||||
*/
|
||||
export function detectTerminalTheme(): TerminalTheme {
|
||||
const raw = process.env.COLORFGBG;
|
||||
if (!raw) return "dark";
|
||||
const parts = raw.split(";");
|
||||
const bg = Number(parts[parts.length - 1]);
|
||||
if (Number.isNaN(bg)) return "dark";
|
||||
return bg <= 6 || bg === 8 ? "dark" : "light";
|
||||
}
|
||||
|
||||
// Role colors
|
||||
export const ROLE_USER = "#c0a480";
|
||||
export const ROLE_SYSTEM = "#b8a8d0";
|
||||
interface ThemePalette {
|
||||
readonly warmAccent: string;
|
||||
readonly warmMuted: string;
|
||||
readonly warmReply: string;
|
||||
readonly warmBorder: string;
|
||||
readonly statusSuccess: string;
|
||||
readonly statusError: string;
|
||||
readonly statusActive: string;
|
||||
readonly statusIdle: string;
|
||||
readonly roleUser: string;
|
||||
readonly roleSystem: string;
|
||||
}
|
||||
|
||||
const darkPalette: ThemePalette = {
|
||||
warmAccent: "#d4a070",
|
||||
warmMuted: "#8f8374",
|
||||
warmReply: "#a8c4d4",
|
||||
warmBorder: "#6b6156",
|
||||
statusSuccess: "#7ec87e",
|
||||
statusError: "#e06060",
|
||||
statusActive: "#d4a76a",
|
||||
statusIdle: "#7a7268",
|
||||
roleUser: "#c0a480",
|
||||
roleSystem: "#b8a8d0",
|
||||
};
|
||||
|
||||
const lightPalette: ThemePalette = {
|
||||
warmAccent: "#8b5e3c",
|
||||
warmMuted: "#7a6e62",
|
||||
warmReply: "#2a5a7a",
|
||||
warmBorder: "#b0a898",
|
||||
statusSuccess: "#2e7d32",
|
||||
statusError: "#c62828",
|
||||
statusActive: "#a06020",
|
||||
statusIdle: "#908478",
|
||||
roleUser: "#6b4c30",
|
||||
roleSystem: "#5c4a80",
|
||||
};
|
||||
|
||||
const palette = detectTerminalTheme() === "light" ? lightPalette : darkPalette;
|
||||
|
||||
// Named exports for backward compatibility
|
||||
export const WARM_ACCENT = palette.warmAccent;
|
||||
export const WARM_MUTED = palette.warmMuted;
|
||||
export const WARM_REPLY = palette.warmReply;
|
||||
export const WARM_BORDER = palette.warmBorder;
|
||||
export const STATUS_SUCCESS = palette.statusSuccess;
|
||||
export const STATUS_ERROR = palette.statusError;
|
||||
export const STATUS_ACTIVE = palette.statusActive;
|
||||
export const STATUS_IDLE = palette.statusIdle;
|
||||
export const ROLE_USER = palette.roleUser;
|
||||
export const ROLE_SYSTEM = palette.roleSystem;
|
||||
|
||||
Reference in New Issue
Block a user