diffstalker 0.2.3 → 0.2.4

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 (46) hide show
  1. package/.dependency-cruiser.cjs +2 -2
  2. package/dist/App.js +278 -758
  3. package/dist/KeyBindings.js +103 -91
  4. package/dist/ModalController.js +166 -0
  5. package/dist/MouseHandlers.js +37 -30
  6. package/dist/NavigationController.js +290 -0
  7. package/dist/StagingOperations.js +199 -0
  8. package/dist/config.js +39 -0
  9. package/dist/core/CompareManager.js +134 -0
  10. package/dist/core/ExplorerStateManager.js +7 -3
  11. package/dist/core/GitStateManager.js +28 -771
  12. package/dist/core/HistoryManager.js +72 -0
  13. package/dist/core/RemoteOperationManager.js +109 -0
  14. package/dist/core/WorkingTreeManager.js +412 -0
  15. package/dist/index.js +57 -57
  16. package/dist/state/FocusRing.js +40 -0
  17. package/dist/state/UIState.js +82 -48
  18. package/dist/ui/PaneRenderers.js +3 -6
  19. package/dist/ui/modals/BaseBranchPicker.js +4 -7
  20. package/dist/ui/modals/CommitActionConfirm.js +4 -4
  21. package/dist/ui/modals/DiscardConfirm.js +4 -7
  22. package/dist/ui/modals/FileFinder.js +3 -6
  23. package/dist/ui/modals/HotkeysModal.js +17 -21
  24. package/dist/ui/modals/Modal.js +1 -0
  25. package/dist/ui/modals/RepoPicker.js +109 -0
  26. package/dist/ui/modals/ThemePicker.js +4 -7
  27. package/dist/ui/widgets/CommitPanel.js +26 -94
  28. package/dist/ui/widgets/CompareListView.js +1 -11
  29. package/dist/ui/widgets/DiffView.js +2 -27
  30. package/dist/ui/widgets/ExplorerContent.js +1 -4
  31. package/dist/ui/widgets/ExplorerView.js +1 -11
  32. package/dist/ui/widgets/FileList.js +2 -8
  33. package/dist/ui/widgets/Footer.js +1 -0
  34. package/dist/utils/ansi.js +38 -0
  35. package/dist/utils/ansiTruncate.js +1 -5
  36. package/dist/utils/displayRows.js +72 -59
  37. package/dist/utils/fileCategories.js +7 -0
  38. package/dist/utils/fileResolution.js +23 -0
  39. package/dist/utils/languageDetection.js +3 -2
  40. package/dist/utils/logger.js +32 -0
  41. package/metrics/v0.2.4.json +236 -0
  42. package/package.json +1 -1
  43. package/dist/ui/modals/BranchPicker.js +0 -157
  44. package/dist/ui/modals/SoftResetConfirm.js +0 -68
  45. package/dist/ui/modals/StashListModal.js +0 -98
  46. 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
  }
@@ -4,7 +4,8 @@ const hotkeyGroups = [
4
4
  title: 'Navigation',
5
5
  entries: [
6
6
  { key: 'j/k', description: 'Move up/down' },
7
- { key: 'Tab', description: 'Toggle pane focus' },
7
+ { key: 'Tab', description: 'Next focus zone' },
8
+ { key: 'Shift+Tab', description: 'Previous focus zone' },
8
9
  ],
9
10
  },
10
11
  {
@@ -21,12 +22,8 @@ const hotkeyGroups = [
21
22
  title: 'Actions',
22
23
  entries: [
23
24
  { key: 'c', description: 'Commit panel' },
24
- { key: 'r', description: 'Refresh' },
25
+ { key: 'r', description: 'Repo picker' },
25
26
  { 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
27
  ],
31
28
  },
32
29
  {
@@ -73,10 +70,6 @@ const hotkeyGroups = [
73
70
  { key: 'i/Enter', description: 'Edit message' },
74
71
  { key: 'a', description: 'Toggle amend' },
75
72
  { 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
73
  ],
81
74
  },
82
75
  {
@@ -110,6 +103,7 @@ export class HotkeysModal {
110
103
  box;
111
104
  screen;
112
105
  onClose;
106
+ screenClickHandler = null;
113
107
  constructor(screen, onClose) {
114
108
  this.screen = screen;
115
109
  this.onClose = onClose;
@@ -158,15 +152,16 @@ export class HotkeysModal {
158
152
  }
159
153
  }
160
154
  setupKeyHandlers() {
161
- this.box.key(['escape', 'enter', '?', 'q'], () => {
162
- this.close();
155
+ this.box.key(['escape', 'enter'], () => {
156
+ this.destroy();
163
157
  this.onClose();
164
158
  });
165
- // Close on click anywhere
166
- this.box.on('click', () => {
167
- this.close();
159
+ // Close on any mouse click (screen-level catches clicks outside the modal too)
160
+ this.screenClickHandler = () => {
161
+ this.destroy();
168
162
  this.onClose();
169
- });
163
+ };
164
+ this.screen.on('click', this.screenClickHandler);
170
165
  }
171
166
  /**
172
167
  * Calculate the visible width of a string (excluding blessed tags).
@@ -214,7 +209,7 @@ export class HotkeysModal {
214
209
  }
215
210
  // Footer
216
211
  lines.push('');
217
- lines.push('{gray-fg}Press Esc, Enter, or ? to close{/gray-fg}');
212
+ lines.push('{gray-fg}Press Esc, Enter, ?, or click to close{/gray-fg}');
218
213
  this.box.setContent(lines.join('\n'));
219
214
  this.screen.render();
220
215
  }
@@ -229,12 +224,13 @@ export class HotkeysModal {
229
224
  }
230
225
  return lines;
231
226
  }
232
- close() {
227
+ destroy() {
228
+ if (this.screenClickHandler) {
229
+ this.screen.removeListener('click', this.screenClickHandler);
230
+ this.screenClickHandler = null;
231
+ }
233
232
  this.box.destroy();
234
233
  }
235
- /**
236
- * Focus the modal.
237
- */
238
234
  focus() {
239
235
  this.box.focus();
240
236
  }
@@ -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
  }