diffstalker 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/.dependency-cruiser.cjs +2 -2
  2. package/.githooks/pre-push +2 -2
  3. package/.github/workflows/release.yml +3 -0
  4. package/CHANGELOG.md +6 -0
  5. package/dist/App.js +278 -758
  6. package/dist/KeyBindings.js +103 -91
  7. package/dist/ModalController.js +166 -0
  8. package/dist/MouseHandlers.js +37 -30
  9. package/dist/NavigationController.js +290 -0
  10. package/dist/StagingOperations.js +199 -0
  11. package/dist/config.js +39 -0
  12. package/dist/core/CompareManager.js +134 -0
  13. package/dist/core/ExplorerStateManager.js +7 -3
  14. package/dist/core/GitStateManager.js +28 -771
  15. package/dist/core/HistoryManager.js +72 -0
  16. package/dist/core/RemoteOperationManager.js +109 -0
  17. package/dist/core/WorkingTreeManager.js +412 -0
  18. package/dist/index.js +57 -57
  19. package/dist/state/FocusRing.js +40 -0
  20. package/dist/state/UIState.js +82 -48
  21. package/dist/ui/PaneRenderers.js +3 -6
  22. package/dist/ui/modals/BaseBranchPicker.js +4 -7
  23. package/dist/ui/modals/CommitActionConfirm.js +4 -4
  24. package/dist/ui/modals/DiscardConfirm.js +4 -7
  25. package/dist/ui/modals/FileFinder.js +3 -6
  26. package/dist/ui/modals/HotkeysModal.js +24 -21
  27. package/dist/ui/modals/Modal.js +1 -0
  28. package/dist/ui/modals/RepoPicker.js +109 -0
  29. package/dist/ui/modals/ThemePicker.js +4 -7
  30. package/dist/ui/widgets/CommitPanel.js +26 -94
  31. package/dist/ui/widgets/CompareListView.js +1 -11
  32. package/dist/ui/widgets/DiffView.js +2 -27
  33. package/dist/ui/widgets/ExplorerContent.js +1 -4
  34. package/dist/ui/widgets/ExplorerView.js +1 -11
  35. package/dist/ui/widgets/FileList.js +2 -8
  36. package/dist/ui/widgets/Footer.js +1 -0
  37. package/dist/utils/ansi.js +38 -0
  38. package/dist/utils/ansiTruncate.js +1 -5
  39. package/dist/utils/displayRows.js +72 -59
  40. package/dist/utils/fileCategories.js +7 -0
  41. package/dist/utils/fileResolution.js +23 -0
  42. package/dist/utils/languageDetection.js +3 -2
  43. package/dist/utils/logger.js +32 -0
  44. package/metrics/v0.2.4.json +236 -0
  45. package/metrics/v0.2.5.json +236 -0
  46. package/package.json +1 -1
  47. package/dist/ui/modals/BranchPicker.js +0 -157
  48. package/dist/ui/modals/SoftResetConfirm.js +0 -68
  49. package/dist/ui/modals/StashListModal.js +0 -98
  50. package/dist/utils/layoutCalculations.js +0 -100
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Generic ring/cycle data structure for focus zone navigation.
3
+ * Tab cycles forward, Shift-Tab cycles backward, wrapping at boundaries.
4
+ */
5
+ export class FocusRing {
6
+ items;
7
+ index;
8
+ constructor(items, initialIndex = 0) {
9
+ this.items = items;
10
+ this.index = Math.min(initialIndex, Math.max(0, items.length - 1));
11
+ }
12
+ current() {
13
+ return this.items[this.index];
14
+ }
15
+ next() {
16
+ this.index = (this.index + 1) % this.items.length;
17
+ return this.items[this.index];
18
+ }
19
+ prev() {
20
+ this.index = (this.index - 1 + this.items.length) % this.items.length;
21
+ return this.items[this.index];
22
+ }
23
+ setCurrent(item) {
24
+ const idx = this.items.indexOf(item);
25
+ if (idx === -1)
26
+ return false;
27
+ this.index = idx;
28
+ return true;
29
+ }
30
+ setItems(items, defaultItem) {
31
+ this.items = items;
32
+ if (defaultItem !== undefined) {
33
+ const idx = items.indexOf(defaultItem);
34
+ this.index = idx !== -1 ? idx : 0;
35
+ }
36
+ else {
37
+ this.index = Math.min(this.index, Math.max(0, items.length - 1));
38
+ }
39
+ }
40
+ }
@@ -1,5 +1,36 @@
1
1
  import { EventEmitter } from 'node:events';
2
+ import { FocusRing } from './FocusRing.js';
3
+ /** Map each focus zone to its derived currentPane value. */
4
+ export const ZONE_TO_PANE = {
5
+ fileList: 'files',
6
+ diffView: 'diff',
7
+ commitMessage: 'commit',
8
+ commitAmend: 'commit',
9
+ historyList: 'history',
10
+ historyDiff: 'diff',
11
+ compareList: 'compare',
12
+ compareDiff: 'diff',
13
+ explorerTree: 'explorer',
14
+ explorerContent: 'diff',
15
+ };
16
+ /** Ordered list of focus zones per tab (Tab order). */
17
+ export const TAB_ZONES = {
18
+ diff: ['fileList', 'diffView'],
19
+ commit: ['fileList', 'commitMessage', 'commitAmend'],
20
+ history: ['historyList', 'historyDiff'],
21
+ compare: ['compareList', 'compareDiff'],
22
+ explorer: ['explorerTree', 'explorerContent'],
23
+ };
24
+ /** Default focus zone when switching to each tab. */
25
+ export const DEFAULT_TAB_ZONE = {
26
+ diff: 'fileList',
27
+ commit: 'commitMessage',
28
+ history: 'historyList',
29
+ compare: 'compareList',
30
+ explorer: 'explorerTree',
31
+ };
2
32
  const DEFAULT_STATE = {
33
+ focusedZone: 'fileList',
3
34
  currentPane: 'files',
4
35
  bottomTab: 'diff',
5
36
  selectedIndex: 0,
@@ -21,8 +52,6 @@ const DEFAULT_STATE = {
21
52
  hideGitignored: true,
22
53
  flatViewMode: false,
23
54
  splitRatio: 0.4,
24
- activeModal: null,
25
- pendingDiscard: null,
26
55
  commitInputFocused: false,
27
56
  };
28
57
  /**
@@ -31,37 +60,54 @@ const DEFAULT_STATE = {
31
60
  */
32
61
  export class UIState extends EventEmitter {
33
62
  _state;
63
+ focusRing;
34
64
  constructor(initialState = {}) {
35
65
  super();
36
66
  this._state = { ...DEFAULT_STATE, ...initialState };
67
+ const tab = this._state.bottomTab;
68
+ const zones = TAB_ZONES[tab];
69
+ this.focusRing = new FocusRing(zones);
70
+ if (this._state.focusedZone) {
71
+ this.focusRing.setCurrent(this._state.focusedZone);
72
+ }
73
+ // Ensure currentPane is in sync
74
+ this._state.currentPane = ZONE_TO_PANE[this._state.focusedZone];
37
75
  }
38
76
  get state() {
39
77
  return this._state;
40
78
  }
41
79
  update(partial) {
42
80
  this._state = { ...this._state, ...partial };
81
+ // Derive currentPane from focusedZone
82
+ this._state.currentPane = ZONE_TO_PANE[this._state.focusedZone];
43
83
  this.emit('change', this._state);
44
84
  }
45
85
  // Navigation
46
86
  setPane(pane) {
47
- if (this._state.currentPane !== pane) {
48
- this.update({ currentPane: pane });
49
- this.emit('pane-change', pane);
87
+ // Map pane to the first matching zone for the current tab
88
+ const zones = TAB_ZONES[this._state.bottomTab];
89
+ const zone = zones.find((z) => ZONE_TO_PANE[z] === pane);
90
+ if (zone) {
91
+ this.setFocusedZone(zone);
92
+ }
93
+ }
94
+ setFocusedZone(zone) {
95
+ if (this._state.focusedZone !== zone) {
96
+ this.focusRing.setCurrent(zone);
97
+ const oldPane = this._state.currentPane;
98
+ this.update({ focusedZone: zone });
99
+ if (this._state.currentPane !== oldPane) {
100
+ this.emit('pane-change', this._state.currentPane);
101
+ }
50
102
  }
51
103
  }
52
104
  setTab(tab) {
53
105
  if (this._state.bottomTab !== tab) {
54
- // Map tab to appropriate pane
55
- const paneMap = {
56
- diff: 'files',
57
- commit: 'commit',
58
- history: 'history',
59
- compare: 'compare',
60
- explorer: 'explorer',
61
- };
106
+ const defaultZone = DEFAULT_TAB_ZONE[tab];
107
+ this.focusRing.setItems(TAB_ZONES[tab], defaultZone);
62
108
  this.update({
63
109
  bottomTab: tab,
64
- currentPane: paneMap[tab],
110
+ focusedZone: defaultZone,
65
111
  });
66
112
  this.emit('tab-change', tab);
67
113
  }
@@ -155,51 +201,39 @@ export class UIState extends EventEmitter {
155
201
  setSplitRatio(ratio) {
156
202
  this.update({ splitRatio: Math.min(0.85, Math.max(0.15, ratio)) });
157
203
  }
158
- // Modals
159
- openModal(modal) {
160
- this.update({ activeModal: modal });
161
- this.emit('modal-change', modal);
162
- }
163
- closeModal() {
164
- this.update({ activeModal: null });
165
- this.emit('modal-change', null);
166
- }
167
- toggleModal(modal) {
168
- if (this._state.activeModal === modal) {
169
- this.closeModal();
170
- }
171
- else {
172
- this.openModal(modal);
173
- }
174
- }
175
- // Discard confirmation
176
- setPendingDiscard(file) {
177
- this.update({ pendingDiscard: file });
178
- }
179
204
  // Commit input focus
180
205
  setCommitInputFocused(focused) {
181
206
  this.update({ commitInputFocused: focused });
182
207
  }
183
- // Helper for toggling between panes
184
- togglePane() {
185
- const { bottomTab, currentPane } = this._state;
186
- if (bottomTab === 'diff' || bottomTab === 'commit') {
187
- this.setPane(currentPane === 'files' ? 'diff' : 'files');
188
- }
189
- else if (bottomTab === 'history') {
190
- this.setPane(currentPane === 'history' ? 'diff' : 'history');
208
+ // Focus zone cycling
209
+ advanceFocus() {
210
+ const zone = this.focusRing.next();
211
+ const oldPane = this._state.currentPane;
212
+ this.update({ focusedZone: zone });
213
+ if (this._state.currentPane !== oldPane) {
214
+ this.emit('pane-change', this._state.currentPane);
191
215
  }
192
- else if (bottomTab === 'compare') {
193
- this.setPane(currentPane === 'compare' ? 'diff' : 'compare');
194
- }
195
- else if (bottomTab === 'explorer') {
196
- this.setPane(currentPane === 'explorer' ? 'diff' : 'explorer');
216
+ }
217
+ retreatFocus() {
218
+ const zone = this.focusRing.prev();
219
+ const oldPane = this._state.currentPane;
220
+ this.update({ focusedZone: zone });
221
+ if (this._state.currentPane !== oldPane) {
222
+ this.emit('pane-change', this._state.currentPane);
197
223
  }
198
224
  }
225
+ /** Backward compat alias for advanceFocus(). */
226
+ togglePane() {
227
+ this.advanceFocus();
228
+ }
199
229
  // Reset repo-specific state when switching repositories
200
230
  resetForNewRepo() {
231
+ const defaultZone = DEFAULT_TAB_ZONE[this._state.bottomTab];
232
+ this.focusRing.setItems(TAB_ZONES[this._state.bottomTab], defaultZone);
201
233
  this._state = {
202
234
  ...this._state,
235
+ focusedZone: defaultZone,
236
+ currentPane: ZONE_TO_PANE[defaultZone],
203
237
  selectedIndex: 0,
204
238
  fileListScrollOffset: 0,
205
239
  diffScrollOffset: 0,
@@ -31,19 +31,16 @@ export function renderTopPane(state, files, historyCommits, compareDiff, compare
31
31
  /**
32
32
  * Render the bottom pane content for the current tab.
33
33
  */
34
- export function renderBottomPane(state, diff, historyState, compareSelectionState, explorerSelectedFile, commitFlowState, stagedCount, currentTheme, width, bottomPaneHeight, selectedHunkIndex, isFileStaged, combinedFileDiffs, branch, remoteState, stashList, headCommit) {
34
+ export function renderBottomPane(state, diff, historyState, compareSelectionState, explorerSelectedFile, commitFlowState, stagedCount, currentTheme, width, bottomPaneHeight, selectedHunkIndex, isFileStaged, combinedFileDiffs, focusedZone) {
35
35
  if (state.bottomTab === 'commit') {
36
36
  const panelOpts = {
37
37
  state: commitFlowState,
38
38
  stagedCount,
39
39
  width,
40
- branch,
41
- remoteState,
42
- stashList,
43
- headCommit,
40
+ focusedZone,
44
41
  };
45
42
  const totalRows = getCommitPanelTotalRows(panelOpts);
46
- const content = formatCommitPanel(commitFlowState, stagedCount, width, branch, remoteState, stashList, headCommit, state.diffScrollOffset, bottomPaneHeight);
43
+ const content = formatCommitPanel(commitFlowState, stagedCount, width, state.diffScrollOffset, bottomPaneHeight, focusedZone);
47
44
  return { content, totalRows, hunkCount: 0, hunkBoundaries: [] };
48
45
  }
49
46
  if (state.bottomTab === 'history') {
@@ -49,14 +49,14 @@ export class BaseBranchPicker {
49
49
  this.render();
50
50
  }
51
51
  setupKeyHandlers() {
52
- this.box.key(['escape', 'q'], () => {
53
- this.close();
52
+ this.box.key(['escape'], () => {
53
+ this.destroy();
54
54
  this.onCancel();
55
55
  });
56
56
  this.box.key(['enter', 'space'], () => {
57
57
  const selected = this.branches[this.selectedIndex];
58
58
  if (selected) {
59
- this.close();
59
+ this.destroy();
60
60
  this.onSelect(selected);
61
61
  }
62
62
  });
@@ -98,12 +98,9 @@ export class BaseBranchPicker {
98
98
  this.box.setContent(lines.join('\n'));
99
99
  this.screen.render();
100
100
  }
101
- close() {
101
+ destroy() {
102
102
  this.box.destroy();
103
103
  }
104
- /**
105
- * Focus the modal.
106
- */
107
104
  focus() {
108
105
  this.box.focus();
109
106
  }
@@ -35,11 +35,11 @@ export class CommitActionConfirm {
35
35
  }
36
36
  setupKeyHandlers() {
37
37
  this.box.key(['y', 'Y'], () => {
38
- this.close();
38
+ this.destroy();
39
39
  this.onConfirm();
40
40
  });
41
- this.box.key(['n', 'N', 'escape', 'q'], () => {
42
- this.close();
41
+ this.box.key(['n', 'N', 'escape'], () => {
42
+ this.destroy();
43
43
  this.onCancel();
44
44
  });
45
45
  }
@@ -57,7 +57,7 @@ export class CommitActionConfirm {
57
57
  this.box.setContent(lines.join('\n'));
58
58
  this.screen.render();
59
59
  }
60
- close() {
60
+ destroy() {
61
61
  this.box.destroy();
62
62
  }
63
63
  focus() {
@@ -40,11 +40,11 @@ export class DiscardConfirm {
40
40
  }
41
41
  setupKeyHandlers() {
42
42
  this.box.key(['y', 'Y'], () => {
43
- this.close();
43
+ this.destroy();
44
44
  this.onConfirm();
45
45
  });
46
- this.box.key(['n', 'N', 'escape', 'q'], () => {
47
- this.close();
46
+ this.box.key(['n', 'N', 'escape'], () => {
47
+ this.destroy();
48
48
  this.onCancel();
49
49
  });
50
50
  }
@@ -65,12 +65,9 @@ export class DiscardConfirm {
65
65
  this.box.setContent(lines.join('\n'));
66
66
  this.screen.render();
67
67
  }
68
- close() {
68
+ destroy() {
69
69
  this.box.destroy();
70
70
  }
71
- /**
72
- * Focus the modal.
73
- */
74
71
  focus() {
75
72
  this.box.focus();
76
73
  }
@@ -82,14 +82,14 @@ export class FileFinder {
82
82
  setupKeyHandlers() {
83
83
  // Handle escape to cancel
84
84
  this.textbox.key(['escape'], () => {
85
- this.close();
85
+ this.destroy();
86
86
  this.onCancel();
87
87
  });
88
88
  // Handle enter to select
89
89
  this.textbox.key(['enter'], () => {
90
90
  if (this.results.length > 0) {
91
91
  const selected = this.results[this.selectedIndex];
92
- this.close();
92
+ this.destroy();
93
93
  this.onSelect(selected.path);
94
94
  }
95
95
  });
@@ -187,15 +187,12 @@ export class FileFinder {
187
187
  this.box.setContent(lines.join('\n'));
188
188
  this.screen.render();
189
189
  }
190
- close() {
190
+ destroy() {
191
191
  if (this.debounceTimer)
192
192
  clearTimeout(this.debounceTimer);
193
193
  this.textbox.destroy();
194
194
  this.box.destroy();
195
195
  }
196
- /**
197
- * Focus the modal input.
198
- */
199
196
  focus() {
200
197
  this.textbox.focus();
201
198
  }
@@ -1,10 +1,14 @@
1
+ import { createRequire } from 'node:module';
1
2
  import blessed from 'neo-blessed';
3
+ const require = createRequire(import.meta.url);
4
+ const { version } = require('../../../package.json');
2
5
  const hotkeyGroups = [
3
6
  {
4
7
  title: 'Navigation',
5
8
  entries: [
6
9
  { key: 'j/k', description: 'Move up/down' },
7
- { key: 'Tab', description: 'Toggle pane focus' },
10
+ { key: 'Tab', description: 'Next focus zone' },
11
+ { key: 'Shift+Tab', description: 'Previous focus zone' },
8
12
  ],
9
13
  },
10
14
  {
@@ -21,12 +25,8 @@ const hotkeyGroups = [
21
25
  title: 'Actions',
22
26
  entries: [
23
27
  { key: 'c', description: 'Commit panel' },
24
- { key: 'r', description: 'Refresh' },
28
+ { key: 'r', description: 'Repo picker' },
25
29
  { key: 'q', description: 'Quit' },
26
- { key: 'P', description: 'Push to remote' },
27
- { key: 'F', description: 'Fetch from remote' },
28
- { key: 'R', description: 'Pull --rebase' },
29
- { key: 'S', description: 'Stash save (global)' },
30
30
  ],
31
31
  },
32
32
  {
@@ -73,10 +73,6 @@ const hotkeyGroups = [
73
73
  { key: 'i/Enter', description: 'Edit message' },
74
74
  { key: 'a', description: 'Toggle amend' },
75
75
  { key: 'Ctrl+a', description: 'Toggle amend (typing)' },
76
- { key: 'o', description: 'Pop stash' },
77
- { key: 'l', description: 'Stash list modal' },
78
- { key: 'b', description: 'Branch picker' },
79
- { key: 'X', description: 'Soft reset HEAD~1' },
80
76
  ],
81
77
  },
82
78
  {
@@ -110,6 +106,7 @@ export class HotkeysModal {
110
106
  box;
111
107
  screen;
112
108
  onClose;
109
+ screenClickHandler = null;
113
110
  constructor(screen, onClose) {
114
111
  this.screen = screen;
115
112
  this.onClose = onClose;
@@ -158,15 +155,16 @@ export class HotkeysModal {
158
155
  }
159
156
  }
160
157
  setupKeyHandlers() {
161
- this.box.key(['escape', 'enter', '?', 'q'], () => {
162
- this.close();
158
+ this.box.key(['escape', 'enter'], () => {
159
+ this.destroy();
163
160
  this.onClose();
164
161
  });
165
- // Close on click anywhere
166
- this.box.on('click', () => {
167
- this.close();
162
+ // Close on any mouse click (screen-level catches clicks outside the modal too)
163
+ this.screenClickHandler = () => {
164
+ this.destroy();
168
165
  this.onClose();
169
- });
166
+ };
167
+ this.screen.on('click', this.screenClickHandler);
170
168
  }
171
169
  /**
172
170
  * Calculate the visible width of a string (excluding blessed tags).
@@ -214,7 +212,11 @@ export class HotkeysModal {
214
212
  }
215
213
  // Footer
216
214
  lines.push('');
217
- lines.push('{gray-fg}Press Esc, Enter, or ? to close{/gray-fg}');
215
+ const closeHint = 'Press Esc, Enter, ?, or click to close';
216
+ const versionLabel = `v${version}`;
217
+ const innerWidth = width - 4; // account for border + padding
218
+ const gap = Math.max(1, innerWidth - closeHint.length - versionLabel.length);
219
+ lines.push(`{gray-fg}${closeHint}${' '.repeat(gap)}${versionLabel}{/gray-fg}`);
218
220
  this.box.setContent(lines.join('\n'));
219
221
  this.screen.render();
220
222
  }
@@ -229,12 +231,13 @@ export class HotkeysModal {
229
231
  }
230
232
  return lines;
231
233
  }
232
- close() {
234
+ destroy() {
235
+ if (this.screenClickHandler) {
236
+ this.screen.removeListener('click', this.screenClickHandler);
237
+ this.screenClickHandler = null;
238
+ }
233
239
  this.box.destroy();
234
240
  }
235
- /**
236
- * Focus the modal.
237
- */
238
241
  focus() {
239
242
  this.box.focus();
240
243
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,109 @@
1
+ import blessed from 'neo-blessed';
2
+ import { abbreviateHomePath } from '../../config.js';
3
+ /**
4
+ * RepoPicker modal for switching between recently-visited repositories.
5
+ */
6
+ export class RepoPicker {
7
+ box;
8
+ screen;
9
+ repos;
10
+ selectedIndex;
11
+ currentRepo;
12
+ onSelect;
13
+ onCancel;
14
+ constructor(screen, repos, currentRepo, onSelect, onCancel) {
15
+ this.screen = screen;
16
+ this.repos = repos;
17
+ this.currentRepo = currentRepo;
18
+ this.onSelect = onSelect;
19
+ this.onCancel = onCancel;
20
+ // Find current repo index
21
+ this.selectedIndex = repos.indexOf(currentRepo);
22
+ if (this.selectedIndex < 0)
23
+ this.selectedIndex = 0;
24
+ // Create modal box
25
+ const screenWidth = screen.width;
26
+ const width = Math.min(70, screenWidth - 4);
27
+ const maxVisibleRepos = Math.min(repos.length, 15);
28
+ const height = maxVisibleRepos + 6; // repos + header + footer + borders + padding
29
+ this.box = blessed.box({
30
+ parent: screen,
31
+ top: 'center',
32
+ left: 'center',
33
+ width,
34
+ height,
35
+ border: {
36
+ type: 'line',
37
+ },
38
+ style: {
39
+ border: {
40
+ fg: 'cyan',
41
+ },
42
+ },
43
+ tags: true,
44
+ keys: true,
45
+ scrollable: true,
46
+ alwaysScroll: true,
47
+ });
48
+ // Setup key handlers
49
+ this.setupKeyHandlers();
50
+ // Initial render
51
+ this.render();
52
+ }
53
+ setupKeyHandlers() {
54
+ this.box.key(['escape'], () => {
55
+ this.destroy();
56
+ this.onCancel();
57
+ });
58
+ this.box.key(['enter', 'space'], () => {
59
+ const selected = this.repos[this.selectedIndex];
60
+ if (selected) {
61
+ this.destroy();
62
+ this.onSelect(selected);
63
+ }
64
+ });
65
+ this.box.key(['up', 'k'], () => {
66
+ this.selectedIndex = Math.max(0, this.selectedIndex - 1);
67
+ this.render();
68
+ });
69
+ this.box.key(['down', 'j'], () => {
70
+ this.selectedIndex = Math.min(this.repos.length - 1, this.selectedIndex + 1);
71
+ this.render();
72
+ });
73
+ }
74
+ render() {
75
+ const lines = [];
76
+ // Header
77
+ lines.push('{bold}{cyan-fg} Recent Repositories{/cyan-fg}{/bold}');
78
+ lines.push('');
79
+ if (this.repos.length === 0) {
80
+ lines.push('{gray-fg}No recent repositories{/gray-fg}');
81
+ }
82
+ else {
83
+ // Repo list
84
+ for (let i = 0; i < this.repos.length; i++) {
85
+ const repo = this.repos[i];
86
+ const isSelected = i === this.selectedIndex;
87
+ const isCurrent = repo === this.currentRepo;
88
+ let line = isSelected ? '{cyan-fg}{bold}> ' : ' ';
89
+ line += abbreviateHomePath(repo);
90
+ if (isSelected)
91
+ line += '{/bold}{/cyan-fg}';
92
+ if (isCurrent)
93
+ line += ' {gray-fg}(current){/gray-fg}';
94
+ lines.push(line);
95
+ }
96
+ }
97
+ // Footer
98
+ lines.push('');
99
+ lines.push('{gray-fg}j/k: navigate | Enter: select | Esc: cancel{/gray-fg}');
100
+ this.box.setContent(lines.join('\n'));
101
+ this.screen.render();
102
+ }
103
+ destroy() {
104
+ this.box.destroy();
105
+ }
106
+ focus() {
107
+ this.box.focus();
108
+ }
109
+ }
@@ -45,13 +45,13 @@ export class ThemePicker {
45
45
  this.render();
46
46
  }
47
47
  setupKeyHandlers() {
48
- this.box.key(['escape', 'q'], () => {
49
- this.close();
48
+ this.box.key(['escape'], () => {
49
+ this.destroy();
50
50
  this.onCancel();
51
51
  });
52
52
  this.box.key(['enter', 'space'], () => {
53
53
  const selected = themeOrder[this.selectedIndex];
54
- this.close();
54
+ this.destroy();
55
55
  this.onSelect(selected);
56
56
  });
57
57
  this.box.key(['up', 'k'], () => {
@@ -94,12 +94,9 @@ export class ThemePicker {
94
94
  this.box.setContent(lines.join('\n'));
95
95
  this.screen.render();
96
96
  }
97
- close() {
97
+ destroy() {
98
98
  this.box.destroy();
99
99
  }
100
- /**
101
- * Focus the modal.
102
- */
103
100
  focus() {
104
101
  this.box.focus();
105
102
  }