diff --git a/src/tui/components/PowerlineThemeSelector.tsx b/src/tui/components/PowerlineThemeSelector.tsx index aaf150f6..11d84082 100644 --- a/src/tui/components/PowerlineThemeSelector.tsx +++ b/src/tui/components/PowerlineThemeSelector.tsx @@ -106,7 +106,7 @@ export const PowerlineThemeSelector: React.FC = ({ }) => { const isRegular = mode === 'regular'; const themes = useMemo( - () => (isRegular ? ['none', ...getPowerlineThemes().filter((name) => name !== 'custom')] : getPowerlineThemes()), + () => (isRegular ? ['none', ...getPowerlineThemes().filter(name => name !== 'custom')] : getPowerlineThemes()), [isRegular] ); const currentTheme = isRegular @@ -142,11 +142,11 @@ export const PowerlineThemeSelector: React.FC = ({ ...(isRegular ? { theme: themeName === 'none' ? undefined : themeName } : { - powerline: { - ...latestSettingsRef.current.powerline, - theme: themeName - } - }) + powerline: { + ...latestSettingsRef.current.powerline, + theme: themeName + } + }) }); }, [selectedIndex, themes, isRegular]); diff --git a/src/tui/components/__tests__/PowerlineThemeSelector.test.ts b/src/tui/components/__tests__/PowerlineThemeSelector.test.ts index 5cbb2c60..467566cd 100644 --- a/src/tui/components/__tests__/PowerlineThemeSelector.test.ts +++ b/src/tui/components/__tests__/PowerlineThemeSelector.test.ts @@ -180,7 +180,7 @@ describe('PowerlineThemeSelector helpers', () => { }); it('writes settings.theme when navigating in regular mode', async () => { - const firstTheme = getPowerlineThemes().filter((name) => name !== 'custom')[0]; + const firstTheme = getPowerlineThemes().find(name => name !== 'custom'); expect(firstTheme).toBeDefined(); const stdin = createMockStdin(); diff --git a/src/utils/__tests__/renderer-compact-labels.test.ts b/src/utils/__tests__/renderer-compact-labels.test.ts index a4baabb8..f65d538a 100644 --- a/src/utils/__tests__/renderer-compact-labels.test.ts +++ b/src/utils/__tests__/renderer-compact-labels.test.ts @@ -12,7 +12,11 @@ import { type Settings } from '../../types/Settings'; import type { WidgetItem } from '../../types/Widget'; -import { toggleCompactLabel } from '../../widgets/shared/raw-or-labeled'; +import { + COMPACT_LABELS, + formatRawOrLabeledValue, + toggleCompactLabel +} from '../../widgets/shared/raw-or-labeled'; import { stripSgrCodes } from '../ansi'; import { migrateConfig } from '../migrations'; import { @@ -105,3 +109,13 @@ describe('compact labels rendering', () => { expect('compactLabels' in SettingsSchema_v1.shape).toBe(false); }); }); + +describe('compact label presets', () => { + const compactItem: WidgetItem = { id: 'x', type: 'model', metadata: { compactLabel: 'true' } }; + const plainItem: WidgetItem = { id: 'x', type: 'model' }; + + it.each(Object.entries(COMPACT_LABELS))('maps %j to %j', (full, compact) => { + expect(formatRawOrLabeledValue(compactItem, full, 'V')).toBe(`${compact}V`); + expect(formatRawOrLabeledValue(plainItem, full, 'V')).toBe(`${full}V`); + }); +}); diff --git a/src/utils/__tests__/renderer-regular-theme.test.ts b/src/utils/__tests__/renderer-regular-theme.test.ts index ebf8d32b..e57f9ea1 100644 --- a/src/utils/__tests__/renderer-regular-theme.test.ts +++ b/src/utils/__tests__/renderer-regular-theme.test.ts @@ -154,7 +154,7 @@ describe('renderer regular-mode theme', () => { // Every shipped theme must behave identically in regular mode: same slot // rule, same bg[]-as-foreground palette, at all three color levels. describe('uniform cycle across all themes and levels', () => { - const themeNames = getPowerlineThemes().filter((name) => name !== 'custom'); + const themeNames = getPowerlineThemes().filter(name => name !== 'custom'); const levelName = (colorLevel: 1 | 2 | 3): 'ansi16' | 'ansi256' | 'truecolor' => (colorLevel === 1 ? 'ansi16' : colorLevel === 2 ? 'ansi256' : 'truecolor'); it('has a non-empty bg palette at every level for every theme', () => { @@ -167,7 +167,7 @@ describe('renderer regular-mode theme', () => { } }); - it.each(themeNames.flatMap((name) => ([1, 2, 3] as const).map((colorLevel) => ({ name, colorLevel }))))( + it.each(themeNames.flatMap(name => ([1, 2, 3] as const).map(colorLevel => ({ name, colorLevel }))))( 'cycles $name uniformly at colorLevel $colorLevel', ({ name, colorLevel }) => { const theme = getPowerlineTheme(name); @@ -175,16 +175,17 @@ describe('renderer regular-mode theme', () => { const palette = (theme?.[String(colorLevel) as '1' | '2' | '3']?.bg ?? []).map(bgToFg); const first = palette[0]; const second = palette[1]; - expect(first).toBeDefined(); - expect(second).toBeDefined(); + if (first === undefined || second === undefined) { + throw new Error(`theme '${name}' has an empty bg palette at ${levelName(colorLevel)}`); + } const line = renderWidgets(themedSettings(name, colorLevel), [ { id: '1', type: 'custom-text', customText: 'A' }, { id: '2', type: 'custom-text', customText: 'B' } ]); - expect(line).toContain(fg(first as string, levelName(colorLevel))); - expect(line).toContain(fg(second as string, levelName(colorLevel))); + expect(line).toContain(fg(first, levelName(colorLevel))); + expect(line).toContain(fg(second, levelName(colorLevel))); } ); }); diff --git a/src/utils/__tests__/separator-font-fallback.test.ts b/src/utils/__tests__/separator-font-fallback.test.ts index 5a2c34f4..7d252170 100644 --- a/src/utils/__tests__/separator-font-fallback.test.ts +++ b/src/utils/__tests__/separator-font-fallback.test.ts @@ -6,7 +6,10 @@ import { } from 'vitest'; import type { RenderContext } from '../../types/RenderContext'; -import { DEFAULT_SETTINGS, type Settings } from '../../types/Settings'; +import { + DEFAULT_SETTINGS, + type Settings +} from '../../types/Settings'; import type { WidgetItem } from '../../types/Widget'; import { containsPowerlineGlyph, diff --git a/src/utils/__tests__/widgets.test.ts b/src/utils/__tests__/widgets.test.ts index 8043a8ad..caa85392 100644 --- a/src/utils/__tests__/widgets.test.ts +++ b/src/utils/__tests__/widgets.test.ts @@ -222,8 +222,11 @@ describe('compact label keybind reservation', () => { expect(capable.has('model')).toBe(true); expect(capable.has('session-cost')).toBe(true); expect(capable.has('context-bar')).toBe(true); + // Presets now cover every labeled widget, tokens and usage included. + expect(capable.has('tokens-input')).toBe(true); + expect(capable.has('session-usage')).toBe(true); + expect(capable.has('weekly-usage')).toBe(true); expect(capable.has('git-branch')).toBe(false); - expect(capable.has('tokens-input')).toBe(false); }); }); diff --git a/src/utils/cli.ts b/src/utils/cli.ts index aea685df..458bc179 100644 --- a/src/utils/cli.ts +++ b/src/utils/cli.ts @@ -4,7 +4,6 @@ import type { WidgetItem } from '../types/Widget'; import { WidgetItemSchema } from '../types/Widget'; import { getPowerlineThemes } from './colors'; - import { getConfigLoadError, getConfigPath, @@ -203,7 +202,7 @@ function getThemeWarnings(settings: Settings): string[] { if (!settings.theme || settings.theme === 'custom' || getPowerlineThemes().includes(settings.theme)) { return []; } - return [`unknown theme '${settings.theme}' (known: ${getPowerlineThemes().filter((name) => name !== 'custom').join(', ')})`]; + return [`unknown theme '${settings.theme}' (known: ${getPowerlineThemes().filter(name => name !== 'custom').join(', ')})`]; } interface SettingsLoad { settings: Settings } diff --git a/src/utils/powerline.ts b/src/utils/powerline.ts index 1ef68ea1..37f831af 100644 --- a/src/utils/powerline.ts +++ b/src/utils/powerline.ts @@ -169,9 +169,7 @@ export function setCachedPowerlineFontStatus(status: PowerlineFontStatus): void } export function getCachedPowerlineFontStatus(): PowerlineFontStatus { - if (cachedFontStatus === null) { - cachedFontStatus = checkPowerlineFonts(); - } + cachedFontStatus ??= checkPowerlineFonts(); return cachedFontStatus; } diff --git a/src/utils/renderer.ts b/src/utils/renderer.ts index 1f3ab86d..cced9c13 100644 --- a/src/utils/renderer.ts +++ b/src/utils/renderer.ts @@ -38,8 +38,8 @@ import { isGradientSpec, parseGradientSpec } from './gradient'; -import { getTerminalWidth } from './terminal'; import { fontSafeSeparator } from './powerline'; +import { getTerminalWidth } from './terminal'; import { getWidget, widgetPreservesColors @@ -163,12 +163,12 @@ function renderPowerlineStatusLine( // Get separator configuration; PUA glyphs degrade to '|' without a // Powerline/Nerd Font instead of rendering as mojibake - const separators = ((config.separators as string[] | undefined) ?? ['\uE0B0']).map((separator) => fontSafeSeparator(separator)); + const separators = ((config.separators as string[] | undefined) ?? ['\uE0B0']).map(separator => fontSafeSeparator(separator)); const invertBgs = (config.separatorInvertBackground as boolean[] | undefined) ?? separators.map(() => false); // Get caps arrays or fallback to empty arrays - const startCaps = ((config.startCaps as string[] | undefined) ?? []).map((cap) => fontSafeSeparator(cap)); - const endCaps = ((config.endCaps as string[] | undefined) ?? []).map((cap) => fontSafeSeparator(cap)); + const startCaps = ((config.startCaps as string[] | undefined) ?? []).map(cap => fontSafeSeparator(cap)); + const endCaps = ((config.endCaps as string[] | undefined) ?? []).map(cap => fontSafeSeparator(cap)); // Get the cap for this line (cycle through if more lines than caps) const capLineIndex = context.lineIndex ?? lineIndex; diff --git a/src/widgets/shared/__tests__/raw-or-labeled.test.ts b/src/widgets/shared/__tests__/raw-or-labeled.test.ts index f1acf246..b11c9062 100644 --- a/src/widgets/shared/__tests__/raw-or-labeled.test.ts +++ b/src/widgets/shared/__tests__/raw-or-labeled.test.ts @@ -32,8 +32,8 @@ describe('formatRawOrLabeledValue', () => { expect(formatRawOrLabeledValue(compact, 'Cost: ', '$2.45')).toBe('$2.45'); }); - it('keeps labels without a preset unchanged in compact mode', () => { - expect(formatRawOrLabeledValue(toggleCompactLabel(ITEM), 'In: ', '15.2k')).toBe('In: 15.2k'); + it('keeps unmapped labels unchanged in compact mode', () => { + expect(formatRawOrLabeledValue(toggleCompactLabel(ITEM), 'Branch: ', 'main')).toBe('Branch: main'); }); it('rawValue wins over compact labels', () => { @@ -99,7 +99,8 @@ describe('startsWithCompactLabel', () => { expect(startsWithCompactLabel('Model: Opus')).toBe(true); expect(startsWithCompactLabel('Context: [bar]')).toBe(true); expect(startsWithCompactLabel('Cost: $1')).toBe(true); - expect(startsWithCompactLabel('In: 15k')).toBe(false); + expect(startsWithCompactLabel('In: 15k')).toBe(true); + expect(startsWithCompactLabel('Branch: main')).toBe(false); expect(startsWithCompactLabel('')).toBe(false); }); }); diff --git a/src/widgets/shared/raw-or-labeled.ts b/src/widgets/shared/raw-or-labeled.ts index bdb67e6e..ce50a734 100644 --- a/src/widgets/shared/raw-or-labeled.ts +++ b/src/widgets/shared/raw-or-labeled.ts @@ -10,10 +10,40 @@ import type { // label (Cost: $2.46 -> $2.46 — the value's own $ is the glyph). Add entries // here to cover more widgets — everything routed through the helper picks // them up automatically. -const COMPACT_LABELS: Record = { +export const COMPACT_LABELS: Record = { 'Model: ': 'M: ', 'Context: ': 'Ctx: ', - 'Cost: ': '' + 'Cost: ': '', + 'Session: ': 'S: ', + 'Weekly: ': 'W: ', + 'Weekly Sonnet: ': 'WS: ', + 'Weekly Opus: ': 'WO: ', + 'Weekly Fable: ': 'WF: ', + 'In: ': 'I: ', + 'Out: ': 'O: ', + 'Total: ': 'T: ', + 'Cached: ': 'C: ', + 'Turn: ': 'Tr: ', + 'Cache: ': 'Ca: ', + 'Cache Read: ': 'CR: ', + 'Cache Write: ': 'CW: ', + 'Cache Hit: ': 'CH: ', + 'Block: ': 'B: ', + 'Block ': 'B ', + 'Reset: ': 'R: ', + 'Reset ': 'R ', + 'Weekly Reset: ': 'WR: ', + 'Weekly Reset ': 'WR ', + 'Overage: ': 'Ov: ', + 'Overage Used: ': 'OvU: ', + 'Overage Left: ': 'OvL: ', + 'Ctx Used: ': 'CU: ', + 'Ctx Left: ': 'CL: ', + // Both usable-context variants share the plain presets: the (u) marker + // distinguishes widgets that are rarely shown side by side. + 'Ctx(u) Used: ': 'CU: ', + 'Ctx(u) Left: ': 'CL: ', + 'Claude: ': 'CC: ' }; const COMPACT_LABEL_METADATA_KEY = 'compactLabel';