From d3ffa734e15254634075c01f54d23dccdc8bbbf2 Mon Sep 17 00:00:00 2001 From: hetaoBackend Date: Wed, 23 Sep 2026 14:25:21 +0800 Subject: [PATCH] fix(tui): switch theme appearance with arrow keys --- .../src/tui/features/settings/theme-picker.ts | 28 +++++++++++-------- .../features/settings/theme-picker.test.ts | 27 ++++++++++++++---- 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/packages/tui/src/tui/features/settings/theme-picker.ts b/packages/tui/src/tui/features/settings/theme-picker.ts index 723f874b..1f24db4f 100644 --- a/packages/tui/src/tui/features/settings/theme-picker.ts +++ b/packages/tui/src/tui/features/settings/theme-picker.ts @@ -11,6 +11,9 @@ import type { export type TuiThemeAppearanceChoice = 'auto' | TuiResolvedAppearance; +const APPEARANCE_CHOICES = ['light', 'auto', 'dark'] as const satisfies + readonly TuiThemeAppearanceChoice[]; + export interface TuiThemePickerOptions { readonly themes: readonly TuiThemeDefinition[]; readonly currentThemeId: string; @@ -64,9 +67,10 @@ export class TuiThemePicker implements Component { this.selectedIndex = (this.selectedIndex - 1 + themes.length) % themes.length; } else if (keys.matches(data, 'tui.select.down')) { this.selectedIndex = (this.selectedIndex + 1) % themes.length; - } else if (matchesKey(data, 'a')) { - this.appearance = cycleAppearance(this.appearance); - this.options.setAppearance(this.appearance); + } else if (matchesKey(data, 'left')) { + this.shiftAppearance(-1); + } else if (matchesKey(data, 'right')) { + this.shiftAppearance(1); } else { return; } @@ -162,6 +166,14 @@ export class TuiThemePicker implements Component { return this.appearance === 'auto' ? this.options.currentAppearance : this.appearance; } + private shiftAppearance(step: -1 | 1): void { + const index = APPEARANCE_CHOICES.indexOf(this.appearance); + const next = APPEARANCE_CHOICES[Math.max(0, Math.min(index + step, APPEARANCE_CHOICES.length - 1))]!; + if (next === this.appearance) return; + this.appearance = next; + this.options.setAppearance(next); + } + private restore(): void { const theme = this.options.themes.find((candidate) => candidate.id === this.originalThemeId); if (theme) this.options.preview(this.originalThemeId); @@ -193,12 +205,6 @@ export class TuiThemePicker implements Component { } } -function cycleAppearance(current: TuiThemeAppearanceChoice): TuiThemeAppearanceChoice { - if (current === 'auto') return 'light'; - if (current === 'light') return 'dark'; - return 'auto'; -} - function appearanceLabel( choice: TuiThemeAppearanceChoice, detected: TuiResolvedAppearance, @@ -243,6 +249,6 @@ const CURRENT_BADGE = ' current '; function footerText(width: number, busy: boolean): string { if (busy) return 'Saving…'; return width >= 64 - ? '↑↓ preview · a appearance · Enter save · Esc cancel' - : '↑↓ · a · Enter · Esc'; + ? '↑↓ theme · ←→ appearance · Enter save · Esc cancel' + : '↑↓ ←→ · Enter · Esc'; } diff --git a/packages/tui/test/unit/tui/features/settings/theme-picker.test.ts b/packages/tui/test/unit/tui/features/settings/theme-picker.test.ts index e7a2f07f..072e540d 100644 --- a/packages/tui/test/unit/tui/features/settings/theme-picker.test.ts +++ b/packages/tui/test/unit/tui/features/settings/theme-picker.test.ts @@ -74,15 +74,30 @@ describe('TuiThemePicker', () => { expect(preview).toHaveBeenLastCalledWith('minimax'); }); - it('cycles the appearance with the a key', () => { + it('selects light, auto, and dark with the left and right arrows', () => { const { picker, setAppearance } = build(); - picker.handleInput('a'); + picker.handleInput('\u001b[D'); expect(setAppearance).toHaveBeenLastCalledWith('light'); - picker.handleInput('a'); + picker.handleInput('\u001b[D'); + expect(setAppearance).toHaveBeenCalledTimes(1); + + picker.handleInput('\u001b[C'); + expect(setAppearance).toHaveBeenLastCalledWith('auto'); + picker.handleInput('\u001b[C'); expect(setAppearance).toHaveBeenLastCalledWith('dark'); + picker.handleInput('\u001b[C'); + expect(setAppearance).toHaveBeenCalledTimes(3); + }); + + it('ignores a as an appearance shortcut', () => { + const { picker, preview, setAppearance, requestRender } = build(); + picker.handleInput('a'); - expect(setAppearance).toHaveBeenLastCalledWith('auto'); + + expect(setAppearance).not.toHaveBeenCalled(); + expect(preview).not.toHaveBeenCalled(); + expect(requestRender).not.toHaveBeenCalled(); }); it('saves the focused theme and closes', async () => { @@ -99,7 +114,7 @@ describe('TuiThemePicker', () => { const { picker, save, onClose } = build(); picker.handleInput('\u001b[B'); - picker.handleInput('a'); + picker.handleInput('\u001b[D'); picker.handleInput('\r'); await vi.waitFor(() => expect(onClose).toHaveBeenCalled()); @@ -119,7 +134,7 @@ describe('TuiThemePicker', () => { const { picker, preview, setAppearance, save, onClose } = build(); picker.handleInput('\u001b[B'); - picker.handleInput('a'); + picker.handleInput('\u001b[D'); preview.mockClear(); picker.handleInput('\u001b');