From afc74e255a4a69e2fa0e3b5cb7cc359fec253380 Mon Sep 17 00:00:00 2001 From: ddelange <14880945+ddelange@users.noreply.github.com> Date: Sat, 19 Sep 2026 21:22:53 +0200 Subject: [PATCH] feat(glyphs): toggle every glyph slot between none and default with Shift+Tab Blanking a multi-symbol widget to save width on the status line was one Backspace per row, five of them on Cache Timer. Shift+Tab now blanks every slot, and blanks back to the defaults, so the key is reversible. --- docs/USAGE.md | 2 +- .../__tests__/symbol-override-editor.test.tsx | 65 +++++++++++++++++++ src/widgets/shared/symbol-override.tsx | 8 ++- 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/docs/USAGE.md b/docs/USAGE.md index 785fb8682..3746f891e 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -273,7 +273,7 @@ Widget picker: The keybind footer in the TUI only shows shortcuts that apply to the currently selected widget. Plain-letter shortcuts ignore Ctrl and Alt/Option modifiers, so combinations such as `Ctrl+S` do not also trigger the letter action. Widget-specific shortcuts: -- **Glyph widgets** (Git Branch, Git Worktree, Git Worktree Mode, Git Staged, Git Unstaged, Git Untracked, Git Conflicts, Git Ahead/Behind, Git Status, Git Changes, Git Insertions, Git Deletions, Git Clean Status, Git Is Fork, JJ Revision, JJ Bookmarks, JJ Workspace, JJ Changes, JJ Insertions, JJ Deletions): `g` set custom glyphs for the widget's symbols; Backspace in the editor renders without one, and multi-symbol widgets (Ahead/Behind, Status, Conflicts, Changes, Clean Status) edit each part in one list +- **Glyph widgets** (Git Branch, Git Worktree, Git Worktree Mode, Git Staged, Git Unstaged, Git Untracked, Git Conflicts, Git Ahead/Behind, Git Status, Git Changes, Git Insertions, Git Deletions, Git Clean Status, Git Is Fork, JJ Revision, JJ Bookmarks, JJ Workspace, JJ Changes, JJ Insertions, JJ Deletions): `g` set custom glyphs for the widget's symbols; Backspace in the editor renders without one, Shift+Tab toggles every symbol between none and default, and multi-symbol widgets (Ahead/Behind, Status, Conflicts, Changes, Clean Status) edit each part in one list - **Git Branch**: `l` toggle clickable branch links (GitHub, GitLab, self-hosted), `w` set a maximum visible width (blank removes the limit) - **Git Root Dir**: `l` cycle IDE links (`off` → `VS Code` → `Cursor`), `w` set a maximum visible width (blank removes the limit) - **Git PR**: `s` toggle review status, `t` toggle title (renders "MR" for GitLab origins) diff --git a/src/widgets/shared/__tests__/symbol-override-editor.test.tsx b/src/widgets/shared/__tests__/symbol-override-editor.test.tsx index d8817703b..28afdf2f4 100644 --- a/src/widgets/shared/__tests__/symbol-override-editor.test.tsx +++ b/src/widgets/shared/__tests__/symbol-override-editor.test.tsx @@ -157,4 +157,69 @@ describe('SymbolSlotsEditor', () => { cleanupEditor(rendered); } }); + + it('blanks every slot on Shift+Tab', async () => { + const rendered = renderEditor({ id: 'git-status', type: 'git-status' }); + + try { + await waitFor(() => { + expect(rendered.stdout.getOutput()).toContain('Conflicts:'); + }); + // Any new frame after Shift+Tab means the toggle was handled + rendered.stdout.clearOutput(); + rendered.stdin.write('\x1B[Z'); + await waitFor(() => { + expect(rendered.stdout.getOutput()).toContain('Conflicts:'); + }); + rendered.stdin.write('\r'); + await waitFor(() => { + expect(rendered.onComplete).toHaveBeenCalledTimes(1); + }); + + const updated = rendered.onComplete.mock.calls[0]?.[0] as WidgetItem | undefined; + expect(updated?.metadata).toEqual({ + symbolConflicts: '', + symbolStaged: '', + symbolUnstaged: '', + symbolUntracked: '' + }); + } finally { + cleanupEditor(rendered); + } + }); + + it('restores every default on a second Shift+Tab', async () => { + const rendered = renderEditor({ + id: 'git-status', + type: 'git-status', + metadata: { + symbolConflicts: '', + symbolStaged: '', + symbolUnstaged: '', + symbolUntracked: '' + } + }); + + try { + await waitFor(() => { + expect(rendered.stdout.getOutput()).toContain('Conflicts:'); + }); + // Any new frame after Shift+Tab means the toggle was handled + rendered.stdout.clearOutput(); + rendered.stdin.write('\x1B[Z'); + await waitFor(() => { + expect(rendered.stdout.getOutput()).toContain('Conflicts:'); + }); + rendered.stdin.write('\r'); + await waitFor(() => { + expect(rendered.onComplete).toHaveBeenCalledTimes(1); + }); + + // Every slot back at its default stores no overrides at all. + const updated = rendered.onComplete.mock.calls[0]?.[0] as WidgetItem | undefined; + expect(updated?.metadata).toBeUndefined(); + } finally { + cleanupEditor(rendered); + } + }); }); diff --git a/src/widgets/shared/symbol-override.tsx b/src/widgets/shared/symbol-override.tsx index 62cec918c..be45ab75e 100644 --- a/src/widgets/shared/symbol-override.tsx +++ b/src/widgets/shared/symbol-override.tsx @@ -103,6 +103,12 @@ const SymbolSlotsEditor: React.FC = setSelectedIndex(selectedIndex - 1 < 0 ? slots.length - 1 : selectedIndex - 1); } else if (key.downArrow && slots.length > 1) { setSelectedIndex(selectedIndex + 1 > slots.length - 1 ? 0 : selectedIndex + 1); + } else if (key.tab && key.shift) { + // Widgets with several glyphs are mostly blanked whole to save + // width on the status line, which is one Backspace per row + // otherwise. Blanked rows toggle back so the key is reversible. + const allBlank = values.every(value => value.length === 0); + setValues(values.map((_, index) => (allBlank ? slots[index]?.defaultSymbol ?? '' : ''))); } else if (key.tab) { setValues(values.map((value, index) => ( index === selectedIndex ? slots[selectedIndex]?.defaultSymbol ?? '' : value @@ -121,7 +127,7 @@ const SymbolSlotsEditor: React.FC = Glyphs {slots.length > 1 - ? '↑↓ row, type to set, Tab default, Backspace none, Enter save, ESC cancel' + ? '↑↓ row, type to set, Tab default, Backspace none, Shift+Tab toggle all none/default, Enter save, ESC cancel' : 'Type any character or emoji, Tab default, Backspace none, Enter save, ESC cancel'}