fix: up highlight made legible for transparent bg, bring back mouse nav
This commit is contained in:
@@ -11,8 +11,8 @@ const cfg = {
|
||||
"audio-seek-forward": parseBindingSpec(["shift-."]),
|
||||
"audio-seek-backward": parseBindingSpec(["shift-,"]),
|
||||
sort: parseBindingSpec([","]),
|
||||
quit: parseBindingSpec(["q"]),
|
||||
command: parseBindingSpec([":"]),
|
||||
quit: parseBindingSpec(["Q"]),
|
||||
command: parseBindingSpec([":", "q"]),
|
||||
"tab-next": parseBindingSpec(["]"]),
|
||||
} as Record<string, ReturnType<typeof parseBindingSpec>>;
|
||||
|
||||
@@ -95,7 +95,8 @@ check("down -> move-down", sim([E("down")]), "move-down");
|
||||
check("gg -> goto-top", sim([E("g"), E("g")]), "goto-top");
|
||||
check("G -> goto-bottom", sim([E("g", { shift: true })]), "goto-bottom");
|
||||
check("space -> toggle-select", sim([E("space")]), "toggle-select");
|
||||
check("q -> quit", sim([E("q")]), "quit");
|
||||
check("q -> command", sim([E("q")]), "command");
|
||||
check("Q -> quit (shift+q)", sim([E("q", { shift: true })]), "quit");
|
||||
check(": -> command", sim([E(":")]), "command");
|
||||
check("] -> tab-next", sim([E("]")]), "tab-next");
|
||||
check(
|
||||
|
||||
@@ -214,6 +214,17 @@ test("direct tab switches re-sync the tab cursor", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("setTabCursor moves the cursor directly (mouse click support), activateTabCursor opens it", () => {
|
||||
withNav((nav) => {
|
||||
nav.setTabCursor(TABS.SETTINGS);
|
||||
expect(nav.tabCursor()).toBe(TABS.SETTINGS);
|
||||
expect(nav.activeTab()).toBe(TABS.FEED); // cursor only — active tab untouched
|
||||
nav.activateTabCursor();
|
||||
expect(nav.activeTab()).toBe(TABS.SETTINGS);
|
||||
expect(nav.atRootTab()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test("tab-switch resets mode/visual/command state", () => {
|
||||
withNav((nav) => {
|
||||
nav.enterVisual();
|
||||
|
||||
89
tests/system-theme.test.ts
Normal file
89
tests/system-theme.test.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* system-theme.test.ts — dark/light mode detection for the "system" theme.
|
||||
*
|
||||
* Covers the pure luminance helper `detectModeFromBackground` and the
|
||||
* `generateSystemTheme` defaults when the terminal cannot answer OSC queries
|
||||
* (empty palette): the fallback background/foreground must follow the detected
|
||||
* mode (dark → white-on-black, light → black-on-white) instead of always
|
||||
* assuming a dark terminal.
|
||||
*/
|
||||
import { test, expect } from "bun:test";
|
||||
import { RGBA, type TerminalColors } from "@opentui/core";
|
||||
import {
|
||||
detectModeFromBackground,
|
||||
generateSystemTheme,
|
||||
} from "../src/utils/system-theme";
|
||||
|
||||
/** A TerminalColors with no real values — simulates a terminal that can't
|
||||
* answer OSC palette/background queries (e.g. tmux without forwarding). */
|
||||
const EMPTY: TerminalColors = {
|
||||
palette: Array.from({ length: 16 }, () => null),
|
||||
defaultForeground: null,
|
||||
defaultBackground: null,
|
||||
cursorColor: null,
|
||||
mouseForeground: null,
|
||||
mouseBackground: null,
|
||||
tekForeground: null,
|
||||
tekBackground: null,
|
||||
highlightBackground: null,
|
||||
highlightForeground: null,
|
||||
};
|
||||
|
||||
test("detectModeFromBackground maps dark backgrounds to dark", () => {
|
||||
expect(detectModeFromBackground("#181825")).toBe("dark");
|
||||
expect(detectModeFromBackground("#000000")).toBe("dark");
|
||||
});
|
||||
|
||||
test("detectModeFromBackground maps light backgrounds to light", () => {
|
||||
expect(detectModeFromBackground("#ffffff")).toBe("light");
|
||||
expect(detectModeFromBackground("#f0f0f0")).toBe("light");
|
||||
expect(detectModeFromBackground("#c8c8c8")).toBe("light");
|
||||
});
|
||||
|
||||
test("detectModeFromBackground returns null without a background", () => {
|
||||
expect(detectModeFromBackground(null)).toBeNull();
|
||||
expect(detectModeFromBackground(undefined)).toBeNull();
|
||||
expect(detectModeFromBackground("")).toBeNull();
|
||||
});
|
||||
|
||||
test("empty palette in dark mode falls back to light-on-dark text", () => {
|
||||
const theme = generateSystemTheme(EMPTY, "dark").theme;
|
||||
// fg defaults to #ffffff; r/g/b are 0..1 floats
|
||||
expect((theme.text as RGBA).r).toBeGreaterThan(0.9);
|
||||
expect((theme.text as RGBA).g).toBeGreaterThan(0.9);
|
||||
expect((theme.text as RGBA).b).toBeGreaterThan(0.9);
|
||||
});
|
||||
|
||||
test("system theme declares a transparent background", () => {
|
||||
const theme = generateSystemTheme(EMPTY, "dark").theme;
|
||||
// The system theme lets the terminal's own background show through.
|
||||
expect(theme.transparent).toBe(true);
|
||||
expect((theme.background as RGBA).a).toBe(0);
|
||||
});
|
||||
|
||||
test("empty palette in light mode falls back to dark-on-light text", () => {
|
||||
const theme = generateSystemTheme(EMPTY, "light").theme;
|
||||
// fg defaults to #000000
|
||||
expect((theme.text as RGBA).r).toBeLessThan(0.1);
|
||||
expect((theme.text as RGBA).g).toBeLessThan(0.1);
|
||||
expect((theme.text as RGBA).b).toBeLessThan(0.1);
|
||||
});
|
||||
|
||||
test("empty palette in light mode uses a light background, not black", () => {
|
||||
const theme = generateSystemTheme(EMPTY, "light").theme;
|
||||
// bg defaults to #ffffff; the diff/panel grays derive from it, so the
|
||||
// backgroundPanel must be light (high luminance), not near-black.
|
||||
expect((theme.backgroundPanel as RGBA).r).toBeGreaterThan(0.5);
|
||||
});
|
||||
|
||||
test("a real light background yields a light theme regardless of mode arg", () => {
|
||||
const colors: TerminalColors = {
|
||||
...EMPTY,
|
||||
defaultBackground: "#f5f5f5",
|
||||
defaultForeground: "#111111",
|
||||
};
|
||||
const theme = generateSystemTheme(colors, "dark").theme;
|
||||
// The actual terminal background wins over the mode default.
|
||||
expect(detectModeFromBackground(colors.defaultBackground)).toBe("light");
|
||||
expect((theme.backgroundPanel as RGBA).r).toBeGreaterThan(0.5);
|
||||
});
|
||||
@@ -1,11 +1,11 @@
|
||||
/**
|
||||
* PaneRow tests — the 1:3:3 parent|current|preview layout primitive.
|
||||
* PaneRow tests — the 1:2:2 parent|current|preview layout primitive.
|
||||
*
|
||||
* Verified through the opentui test renderer's captured frames (the same
|
||||
* mechanism the `.harness` drive uses), since `flexGrow` ratios are only
|
||||
* observable as rendered column widths and border colors.
|
||||
*
|
||||
* • Unit: three columns render at 1:3:3 (e.g. 14/43/43 of 100) even when the
|
||||
* • Unit: three columns render at 1:2:2 (e.g. 20/40/40 of 100) even when the
|
||||
* parent and preview children are null, and the blank parent keeps its
|
||||
* slot with a muted placeholder.
|
||||
* • Integration: toggling `focused` moves the accent focus ring onto/off the
|
||||
@@ -125,9 +125,9 @@ afterAll(async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// ── Unit: three columns at 1:3:3 regardless of null children ───────────────
|
||||
// ── Unit: three columns at 1:2:2 regardless of null children ───────────────
|
||||
describe("PaneRow layout", () => {
|
||||
test("renders three columns at 1:3:3 even with null parent/preview", async () => {
|
||||
test("renders three columns at 1:2:2 even with null parent/preview", async () => {
|
||||
const { spans, destroy } = await renderPaneRow({
|
||||
parent: null,
|
||||
current: () => <text>ITEM</text>,
|
||||
@@ -138,15 +138,15 @@ describe("PaneRow layout", () => {
|
||||
const widths = columnWidths(spans);
|
||||
expect(widths).toHaveLength(3);
|
||||
const [p, c, v] = widths;
|
||||
// 100-wide row splits as 14 / 43 / 43 (1/7 : 3/7 : 3/7, borders included).
|
||||
expect(p).toBe(14);
|
||||
expect(c).toBe(43);
|
||||
expect(v).toBe(43);
|
||||
// Exact 1:3:3 proportion (within 1 col rounding).
|
||||
expect(c).toBeGreaterThanOrEqual(p * 3 - 1);
|
||||
expect(c).toBeLessThanOrEqual(p * 3 + 1);
|
||||
expect(v).toBeGreaterThanOrEqual(p * 3 - 1);
|
||||
expect(v).toBeLessThanOrEqual(p * 3 + 1);
|
||||
// 100-wide row splits as 20 / 40 / 40 (1/5 : 2/5 : 2/5, borders included).
|
||||
expect(p).toBe(20);
|
||||
expect(c).toBe(40);
|
||||
expect(v).toBe(40);
|
||||
// Exact 1:2:2 proportion (within 1 col rounding).
|
||||
expect(c).toBeGreaterThanOrEqual(p * 2 - 1);
|
||||
expect(c).toBeLessThanOrEqual(p * 2 + 1);
|
||||
expect(v).toBeGreaterThanOrEqual(p * 2 - 1);
|
||||
expect(v).toBeLessThanOrEqual(p * 2 + 1);
|
||||
// Parent keeps a visibly non-zero slot and renders the muted placeholder.
|
||||
expect(p).toBeGreaterThan(4);
|
||||
const body = spans.lines
|
||||
@@ -156,7 +156,7 @@ describe("PaneRow layout", () => {
|
||||
expect(body).toContain("ITEM");
|
||||
});
|
||||
|
||||
test("keeps the 1/7 parent slot across widths (ratio stable)", async () => {
|
||||
test("keeps the 1/5 parent slot across widths (ratio stable)", async () => {
|
||||
const { spans, destroy } = await renderPaneRow({
|
||||
parent: null,
|
||||
current: () => <text>x</text>,
|
||||
@@ -165,9 +165,9 @@ describe("PaneRow layout", () => {
|
||||
});
|
||||
cleanups.push(destroy);
|
||||
const [p, c, v] = columnWidths(spans);
|
||||
expect(p).toBe(10); // 70 → 10 / 30 / 30
|
||||
expect(c).toBe(30);
|
||||
expect(v).toBe(30);
|
||||
expect(p).toBe(14); // 70 → 14 / 28 / 28
|
||||
expect(c).toBe(28);
|
||||
expect(v).toBe(28);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user