diffstalker 0.2.0 → 0.2.2
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.
- package/.dependency-cruiser.cjs +67 -0
- package/.githooks/pre-commit +2 -0
- package/.githooks/pre-push +15 -0
- package/.github/workflows/release.yml +8 -0
- package/README.md +43 -35
- package/bun.lock +82 -3
- package/dist/App.js +555 -552
- package/dist/FollowMode.js +85 -0
- package/dist/KeyBindings.js +228 -0
- package/dist/MouseHandlers.js +192 -0
- package/dist/core/ExplorerStateManager.js +423 -78
- package/dist/core/GitStateManager.js +260 -119
- package/dist/git/diff.js +102 -17
- package/dist/git/status.js +16 -54
- package/dist/git/test-helpers.js +67 -0
- package/dist/index.js +60 -53
- package/dist/ipc/CommandClient.js +6 -7
- package/dist/state/UIState.js +39 -4
- package/dist/ui/PaneRenderers.js +76 -0
- package/dist/ui/modals/FileFinder.js +193 -0
- package/dist/ui/modals/HotkeysModal.js +12 -3
- package/dist/ui/modals/ThemePicker.js +1 -2
- package/dist/ui/widgets/CommitPanel.js +1 -1
- package/dist/ui/widgets/CompareListView.js +123 -80
- package/dist/ui/widgets/DiffView.js +228 -180
- package/dist/ui/widgets/ExplorerContent.js +15 -28
- package/dist/ui/widgets/ExplorerView.js +148 -43
- package/dist/ui/widgets/FileList.js +62 -95
- package/dist/ui/widgets/FlatFileList.js +65 -0
- package/dist/ui/widgets/Footer.js +25 -11
- package/dist/ui/widgets/Header.js +17 -52
- package/dist/ui/widgets/fileRowFormatters.js +73 -0
- package/dist/utils/ansiTruncate.js +0 -1
- package/dist/utils/displayRows.js +101 -21
- package/dist/utils/fileCategories.js +37 -0
- package/dist/utils/fileTree.js +148 -0
- package/dist/utils/flatFileList.js +67 -0
- package/dist/utils/layoutCalculations.js +5 -3
- package/eslint.metrics.js +15 -0
- package/metrics/.gitkeep +0 -0
- package/metrics/v0.2.1.json +268 -0
- package/metrics/v0.2.2.json +229 -0
- package/package.json +9 -2
- package/dist/utils/ansiToBlessed.js +0 -125
- package/dist/utils/mouseCoordinates.js +0 -165
- package/dist/utils/rowCalculations.js +0 -246
|
@@ -17,15 +17,14 @@ export class CommandClient {
|
|
|
17
17
|
return new Promise((resolve, reject) => {
|
|
18
18
|
const socket = net.createConnection(this.socketPath);
|
|
19
19
|
let buffer = '';
|
|
20
|
-
|
|
21
|
-
const cleanup = () => {
|
|
22
|
-
clearTimeout(timeoutId);
|
|
23
|
-
socket.destroy();
|
|
24
|
-
};
|
|
25
|
-
timeoutId = setTimeout(() => {
|
|
20
|
+
const timeoutId = setTimeout(() => {
|
|
26
21
|
cleanup();
|
|
27
22
|
reject(new Error(`Command timed out after ${this.timeout}ms`));
|
|
28
23
|
}, this.timeout);
|
|
24
|
+
function cleanup() {
|
|
25
|
+
clearTimeout(timeoutId);
|
|
26
|
+
socket.destroy();
|
|
27
|
+
}
|
|
29
28
|
socket.on('connect', () => {
|
|
30
29
|
socket.write(JSON.stringify(command) + '\n');
|
|
31
30
|
});
|
|
@@ -38,7 +37,7 @@ export class CommandClient {
|
|
|
38
37
|
try {
|
|
39
38
|
resolve(JSON.parse(json));
|
|
40
39
|
}
|
|
41
|
-
catch
|
|
40
|
+
catch {
|
|
42
41
|
reject(new Error(`Invalid JSON response: ${json}`));
|
|
43
42
|
}
|
|
44
43
|
}
|
package/dist/state/UIState.js
CHANGED
|
@@ -13,12 +13,13 @@ const DEFAULT_STATE = {
|
|
|
13
13
|
compareSelectedIndex: 0,
|
|
14
14
|
includeUncommitted: false,
|
|
15
15
|
explorerSelectedIndex: 0,
|
|
16
|
+
selectedHunkIndex: 0,
|
|
16
17
|
wrapMode: false,
|
|
17
18
|
autoTabEnabled: false,
|
|
18
19
|
mouseEnabled: true,
|
|
19
|
-
showMiddleDots: false,
|
|
20
20
|
hideHiddenFiles: true,
|
|
21
21
|
hideGitignored: true,
|
|
22
|
+
flatViewMode: false,
|
|
22
23
|
splitRatio: 0.4,
|
|
23
24
|
activeModal: null,
|
|
24
25
|
pendingDiscard: null,
|
|
@@ -111,6 +112,22 @@ export class UIState extends EventEmitter {
|
|
|
111
112
|
setExplorerSelectedIndex(index) {
|
|
112
113
|
this.update({ explorerSelectedIndex: Math.max(0, index) });
|
|
113
114
|
}
|
|
115
|
+
// Hunk selection
|
|
116
|
+
setSelectedHunkIndex(index) {
|
|
117
|
+
this.update({ selectedHunkIndex: Math.max(0, index) });
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Silently clamp selectedHunkIndex to valid range without emitting events.
|
|
121
|
+
* Called during render to sync state with actual hunk count.
|
|
122
|
+
*/
|
|
123
|
+
clampSelectedHunkIndex(hunkCount) {
|
|
124
|
+
if (hunkCount <= 0) {
|
|
125
|
+
this._state.selectedHunkIndex = 0;
|
|
126
|
+
}
|
|
127
|
+
else if (this._state.selectedHunkIndex >= hunkCount) {
|
|
128
|
+
this._state.selectedHunkIndex = hunkCount - 1;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
114
131
|
// Display toggles
|
|
115
132
|
toggleWrapMode() {
|
|
116
133
|
this.update({ wrapMode: !this._state.wrapMode, diffScrollOffset: 0 });
|
|
@@ -121,15 +138,15 @@ export class UIState extends EventEmitter {
|
|
|
121
138
|
toggleMouse() {
|
|
122
139
|
this.update({ mouseEnabled: !this._state.mouseEnabled });
|
|
123
140
|
}
|
|
124
|
-
toggleMiddleDots() {
|
|
125
|
-
this.update({ showMiddleDots: !this._state.showMiddleDots });
|
|
126
|
-
}
|
|
127
141
|
toggleHideHiddenFiles() {
|
|
128
142
|
this.update({ hideHiddenFiles: !this._state.hideHiddenFiles });
|
|
129
143
|
}
|
|
130
144
|
toggleHideGitignored() {
|
|
131
145
|
this.update({ hideGitignored: !this._state.hideGitignored });
|
|
132
146
|
}
|
|
147
|
+
toggleFlatViewMode() {
|
|
148
|
+
this.update({ flatViewMode: !this._state.flatViewMode, fileListScrollOffset: 0 });
|
|
149
|
+
}
|
|
133
150
|
// Split ratio
|
|
134
151
|
adjustSplitRatio(delta) {
|
|
135
152
|
const newRatio = Math.min(0.85, Math.max(0.15, this._state.splitRatio + delta));
|
|
@@ -179,4 +196,22 @@ export class UIState extends EventEmitter {
|
|
|
179
196
|
this.setPane(currentPane === 'explorer' ? 'diff' : 'explorer');
|
|
180
197
|
}
|
|
181
198
|
}
|
|
199
|
+
// Reset repo-specific state when switching repositories
|
|
200
|
+
resetForNewRepo() {
|
|
201
|
+
this._state = {
|
|
202
|
+
...this._state,
|
|
203
|
+
selectedIndex: 0,
|
|
204
|
+
fileListScrollOffset: 0,
|
|
205
|
+
diffScrollOffset: 0,
|
|
206
|
+
historySelectedIndex: 0,
|
|
207
|
+
historyScrollOffset: 0,
|
|
208
|
+
compareSelectedIndex: 0,
|
|
209
|
+
compareScrollOffset: 0,
|
|
210
|
+
explorerSelectedIndex: 0,
|
|
211
|
+
explorerScrollOffset: 0,
|
|
212
|
+
explorerFileScrollOffset: 0,
|
|
213
|
+
selectedHunkIndex: 0,
|
|
214
|
+
};
|
|
215
|
+
this.emit('change', this._state);
|
|
216
|
+
}
|
|
182
217
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { formatFileList } from './widgets/FileList.js';
|
|
2
|
+
import { formatFlatFileList } from './widgets/FlatFileList.js';
|
|
3
|
+
import { formatHistoryView } from './widgets/HistoryView.js';
|
|
4
|
+
import { formatCompareListView } from './widgets/CompareListView.js';
|
|
5
|
+
import { formatExplorerView } from './widgets/ExplorerView.js';
|
|
6
|
+
import { formatDiff, formatCombinedDiff, formatHistoryDiff } from './widgets/DiffView.js';
|
|
7
|
+
import { formatCommitPanel } from './widgets/CommitPanel.js';
|
|
8
|
+
import { formatExplorerContent } from './widgets/ExplorerContent.js';
|
|
9
|
+
/**
|
|
10
|
+
* Render the top pane content for the current tab.
|
|
11
|
+
*/
|
|
12
|
+
export function renderTopPane(state, files, historyCommits, compareDiff, compareSelection, explorerState, width, topPaneHeight, hunkCounts, flatFiles) {
|
|
13
|
+
if (state.bottomTab === 'history') {
|
|
14
|
+
return formatHistoryView(historyCommits, state.historySelectedIndex, state.currentPane === 'history', width, state.historyScrollOffset, topPaneHeight);
|
|
15
|
+
}
|
|
16
|
+
if (state.bottomTab === 'compare') {
|
|
17
|
+
const commits = compareDiff?.commits ?? [];
|
|
18
|
+
const compareFiles = compareDiff?.files ?? [];
|
|
19
|
+
return formatCompareListView(commits, compareFiles, compareSelection, state.currentPane === 'compare', width, state.compareScrollOffset, topPaneHeight);
|
|
20
|
+
}
|
|
21
|
+
if (state.bottomTab === 'explorer') {
|
|
22
|
+
const displayRows = explorerState?.displayRows ?? [];
|
|
23
|
+
return formatExplorerView(displayRows, state.explorerSelectedIndex, state.currentPane === 'explorer', width, state.explorerScrollOffset, topPaneHeight, explorerState?.isLoading ?? false, explorerState?.error ?? null);
|
|
24
|
+
}
|
|
25
|
+
// Default: diff/commit tab file list
|
|
26
|
+
if (state.flatViewMode && flatFiles) {
|
|
27
|
+
return formatFlatFileList(flatFiles, state.selectedIndex, state.currentPane === 'files', width, state.fileListScrollOffset, topPaneHeight);
|
|
28
|
+
}
|
|
29
|
+
return formatFileList(files, state.selectedIndex, state.currentPane === 'files', width, state.fileListScrollOffset, topPaneHeight, hunkCounts);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Render the bottom pane content for the current tab.
|
|
33
|
+
*/
|
|
34
|
+
export function renderBottomPane(state, diff, historyState, compareSelectionState, explorerSelectedFile, commitFlowState, stagedCount, currentTheme, width, bottomPaneHeight, selectedHunkIndex, isFileStaged, combinedFileDiffs) {
|
|
35
|
+
if (state.bottomTab === 'commit') {
|
|
36
|
+
const content = formatCommitPanel(commitFlowState, stagedCount, width);
|
|
37
|
+
return { content, totalRows: 0, hunkCount: 0, hunkBoundaries: [] };
|
|
38
|
+
}
|
|
39
|
+
if (state.bottomTab === 'history') {
|
|
40
|
+
const selectedCommit = historyState?.selectedCommit ?? null;
|
|
41
|
+
const commitDiff = historyState?.commitDiff ?? null;
|
|
42
|
+
const { content, totalRows } = formatHistoryDiff(selectedCommit, commitDiff, width, state.diffScrollOffset, bottomPaneHeight, currentTheme, state.wrapMode);
|
|
43
|
+
return { content, totalRows, hunkCount: 0, hunkBoundaries: [] };
|
|
44
|
+
}
|
|
45
|
+
if (state.bottomTab === 'compare') {
|
|
46
|
+
const compareDiff = compareSelectionState?.diff ?? null;
|
|
47
|
+
if (compareDiff) {
|
|
48
|
+
const { content, totalRows } = formatDiff(compareDiff, width, state.diffScrollOffset, bottomPaneHeight, currentTheme, state.wrapMode);
|
|
49
|
+
return { content, totalRows, hunkCount: 0, hunkBoundaries: [] };
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
content: '{gray-fg}Select a commit or file to view diff{/gray-fg}',
|
|
53
|
+
totalRows: 0,
|
|
54
|
+
hunkCount: 0,
|
|
55
|
+
hunkBoundaries: [],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (state.bottomTab === 'explorer') {
|
|
59
|
+
const content = formatExplorerContent(explorerSelectedFile?.path ?? null, explorerSelectedFile?.content ?? null, width, state.explorerFileScrollOffset, bottomPaneHeight, explorerSelectedFile?.truncated ?? false, state.wrapMode);
|
|
60
|
+
return { content, totalRows: 0, hunkCount: 0, hunkBoundaries: [] };
|
|
61
|
+
}
|
|
62
|
+
// Flat mode: show combined unstaged+staged diff with section headers
|
|
63
|
+
if (state.flatViewMode && combinedFileDiffs) {
|
|
64
|
+
const result = formatCombinedDiff(combinedFileDiffs.unstaged, combinedFileDiffs.staged, width, state.diffScrollOffset, bottomPaneHeight, currentTheme, state.wrapMode, selectedHunkIndex);
|
|
65
|
+
return {
|
|
66
|
+
content: result.content,
|
|
67
|
+
totalRows: result.totalRows,
|
|
68
|
+
hunkCount: result.hunkCount,
|
|
69
|
+
hunkBoundaries: result.hunkBoundaries,
|
|
70
|
+
hunkMapping: result.hunkMapping,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// Default: diff tab — pass selectedHunkIndex for hunk gutter
|
|
74
|
+
const { content, totalRows, hunkCount, hunkBoundaries } = formatDiff(diff, width, state.diffScrollOffset, bottomPaneHeight, currentTheme, state.wrapMode, selectedHunkIndex, isFileStaged);
|
|
75
|
+
return { content, totalRows, hunkCount, hunkBoundaries };
|
|
76
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import blessed from 'neo-blessed';
|
|
2
|
+
import { Fzf } from 'fzf';
|
|
3
|
+
const MAX_RESULTS = 15;
|
|
4
|
+
/**
|
|
5
|
+
* Highlight matched characters in a display path using fzf position data.
|
|
6
|
+
* The positions set refers to indices in the original full path,
|
|
7
|
+
* so we need an offset when the display path is truncated.
|
|
8
|
+
*/
|
|
9
|
+
function highlightMatch(displayPath, positions, offset) {
|
|
10
|
+
let result = '';
|
|
11
|
+
for (let i = 0; i < displayPath.length; i++) {
|
|
12
|
+
if (positions.has(i + offset)) {
|
|
13
|
+
result += `{yellow-fg}${displayPath[i]}{/yellow-fg}`;
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
result += displayPath[i];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* FileFinder modal for fuzzy file search.
|
|
23
|
+
*/
|
|
24
|
+
export class FileFinder {
|
|
25
|
+
box;
|
|
26
|
+
textbox;
|
|
27
|
+
screen;
|
|
28
|
+
fzf;
|
|
29
|
+
allPaths;
|
|
30
|
+
results = [];
|
|
31
|
+
selectedIndex = 0;
|
|
32
|
+
query = '';
|
|
33
|
+
onSelect;
|
|
34
|
+
onCancel;
|
|
35
|
+
constructor(screen, allPaths, onSelect, onCancel) {
|
|
36
|
+
this.screen = screen;
|
|
37
|
+
this.allPaths = allPaths;
|
|
38
|
+
this.fzf = new Fzf(allPaths, { limit: MAX_RESULTS, forward: false });
|
|
39
|
+
this.onSelect = onSelect;
|
|
40
|
+
this.onCancel = onCancel;
|
|
41
|
+
// Create modal box
|
|
42
|
+
const width = Math.min(80, screen.width - 10);
|
|
43
|
+
const height = MAX_RESULTS + 6; // results + input + header + borders + padding
|
|
44
|
+
this.box = blessed.box({
|
|
45
|
+
parent: screen,
|
|
46
|
+
top: 'center',
|
|
47
|
+
left: 'center',
|
|
48
|
+
width,
|
|
49
|
+
height,
|
|
50
|
+
border: {
|
|
51
|
+
type: 'line',
|
|
52
|
+
},
|
|
53
|
+
style: {
|
|
54
|
+
border: {
|
|
55
|
+
fg: 'cyan',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
tags: true,
|
|
59
|
+
keys: false, // We'll handle keys ourselves
|
|
60
|
+
});
|
|
61
|
+
// Create text input
|
|
62
|
+
this.textbox = blessed.textarea({
|
|
63
|
+
parent: this.box,
|
|
64
|
+
top: 1,
|
|
65
|
+
left: 1,
|
|
66
|
+
width: width - 4,
|
|
67
|
+
height: 1,
|
|
68
|
+
inputOnFocus: true,
|
|
69
|
+
style: {
|
|
70
|
+
fg: 'white',
|
|
71
|
+
bg: 'default',
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
// Setup key handlers
|
|
75
|
+
this.setupKeyHandlers();
|
|
76
|
+
// Initial render with all files
|
|
77
|
+
this.updateResults();
|
|
78
|
+
this.render();
|
|
79
|
+
}
|
|
80
|
+
setupKeyHandlers() {
|
|
81
|
+
// Handle escape to cancel
|
|
82
|
+
this.textbox.key(['escape'], () => {
|
|
83
|
+
this.close();
|
|
84
|
+
this.onCancel();
|
|
85
|
+
});
|
|
86
|
+
// Handle enter to select
|
|
87
|
+
this.textbox.key(['enter'], () => {
|
|
88
|
+
if (this.results.length > 0) {
|
|
89
|
+
const selected = this.results[this.selectedIndex];
|
|
90
|
+
this.close();
|
|
91
|
+
this.onSelect(selected.item);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
// Handle up/down for navigation (Ctrl+j/k since j/k are for typing)
|
|
95
|
+
this.textbox.key(['C-j', 'down'], () => {
|
|
96
|
+
this.selectedIndex = Math.min(this.results.length - 1, this.selectedIndex + 1);
|
|
97
|
+
this.render();
|
|
98
|
+
});
|
|
99
|
+
this.textbox.key(['C-k', 'up'], () => {
|
|
100
|
+
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
101
|
+
this.render();
|
|
102
|
+
});
|
|
103
|
+
// Handle tab for next result
|
|
104
|
+
this.textbox.key(['tab'], () => {
|
|
105
|
+
this.selectedIndex = (this.selectedIndex + 1) % Math.max(1, this.results.length);
|
|
106
|
+
this.render();
|
|
107
|
+
});
|
|
108
|
+
// Handle shift-tab for previous result
|
|
109
|
+
this.textbox.key(['S-tab'], () => {
|
|
110
|
+
this.selectedIndex =
|
|
111
|
+
(this.selectedIndex - 1 + this.results.length) % Math.max(1, this.results.length);
|
|
112
|
+
this.render();
|
|
113
|
+
});
|
|
114
|
+
// Update results on keypress
|
|
115
|
+
this.textbox.on('keypress', () => {
|
|
116
|
+
// Defer to next tick to get updated value
|
|
117
|
+
setImmediate(() => {
|
|
118
|
+
const newQuery = this.textbox.getValue() || '';
|
|
119
|
+
if (newQuery !== this.query) {
|
|
120
|
+
this.query = newQuery;
|
|
121
|
+
this.selectedIndex = 0;
|
|
122
|
+
this.updateResults();
|
|
123
|
+
this.render();
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
updateResults() {
|
|
129
|
+
if (!this.query) {
|
|
130
|
+
// fzf returns nothing for empty query, show first N files
|
|
131
|
+
this.results = this.allPaths
|
|
132
|
+
.slice(0, MAX_RESULTS)
|
|
133
|
+
.map((item) => ({ item, positions: new Set(), start: 0, end: 0, score: 0 }));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this.results = this.fzf.find(this.query);
|
|
137
|
+
}
|
|
138
|
+
render() {
|
|
139
|
+
const lines = [];
|
|
140
|
+
const width = this.box.width - 4;
|
|
141
|
+
// Header
|
|
142
|
+
lines.push('{bold}{cyan-fg}Find File{/cyan-fg}{/bold}');
|
|
143
|
+
lines.push(''); // Space for input
|
|
144
|
+
lines.push('');
|
|
145
|
+
// Results
|
|
146
|
+
if (this.results.length === 0 && this.query) {
|
|
147
|
+
lines.push('{gray-fg}No matches{/gray-fg}');
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
for (let i = 0; i < this.results.length; i++) {
|
|
151
|
+
const result = this.results[i];
|
|
152
|
+
const isSelected = i === this.selectedIndex;
|
|
153
|
+
// Truncate path if needed
|
|
154
|
+
const fullPath = result.item;
|
|
155
|
+
const maxLen = width - 4;
|
|
156
|
+
let displayPath = fullPath;
|
|
157
|
+
let offset = 0;
|
|
158
|
+
if (displayPath.length > maxLen) {
|
|
159
|
+
offset = displayPath.length - (maxLen - 1);
|
|
160
|
+
displayPath = '…' + displayPath.slice(offset);
|
|
161
|
+
// Account for the '…' prefix: display index 0 is '…', actual content starts at 1
|
|
162
|
+
offset = offset - 1;
|
|
163
|
+
}
|
|
164
|
+
// Highlight matched characters using fzf positions
|
|
165
|
+
const highlighted = highlightMatch(displayPath, result.positions, offset);
|
|
166
|
+
if (isSelected) {
|
|
167
|
+
lines.push(`{cyan-fg}{bold}> ${highlighted}{/bold}{/cyan-fg}`);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
lines.push(` ${highlighted}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Pad to fill space
|
|
175
|
+
while (lines.length < MAX_RESULTS + 3) {
|
|
176
|
+
lines.push('');
|
|
177
|
+
}
|
|
178
|
+
// Footer
|
|
179
|
+
lines.push('{gray-fg}Enter: select | Esc: cancel | Ctrl+j/k or ↑↓: navigate{/gray-fg}');
|
|
180
|
+
this.box.setContent(lines.join('\n'));
|
|
181
|
+
this.screen.render();
|
|
182
|
+
}
|
|
183
|
+
close() {
|
|
184
|
+
this.textbox.destroy();
|
|
185
|
+
this.box.destroy();
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Focus the modal input.
|
|
189
|
+
*/
|
|
190
|
+
focus() {
|
|
191
|
+
this.textbox.focus();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -45,6 +45,7 @@ const hotkeyGroups = [
|
|
|
45
45
|
{
|
|
46
46
|
title: 'Toggles',
|
|
47
47
|
entries: [
|
|
48
|
+
{ key: 'h', description: 'Flat file view' },
|
|
48
49
|
{ key: 'm', description: 'Mouse mode' },
|
|
49
50
|
{ key: 'w', description: 'Wrap mode' },
|
|
50
51
|
{ key: 'f', description: 'Follow mode' },
|
|
@@ -57,6 +58,9 @@ const hotkeyGroups = [
|
|
|
57
58
|
entries: [
|
|
58
59
|
{ key: 'Enter', description: 'Enter directory' },
|
|
59
60
|
{ key: 'Backspace', description: 'Go up' },
|
|
61
|
+
{ key: '/', description: 'Find file' },
|
|
62
|
+
{ key: 'Ctrl+P', description: 'Find file (any tab)' },
|
|
63
|
+
{ key: 'g', description: 'Show changes only' },
|
|
60
64
|
],
|
|
61
65
|
},
|
|
62
66
|
{
|
|
@@ -67,8 +71,13 @@ const hotkeyGroups = [
|
|
|
67
71
|
],
|
|
68
72
|
},
|
|
69
73
|
{
|
|
70
|
-
title: 'Diff',
|
|
71
|
-
entries: [
|
|
74
|
+
title: 'Diff (pane focus)',
|
|
75
|
+
entries: [
|
|
76
|
+
{ key: 'n', description: 'Next hunk' },
|
|
77
|
+
{ key: 'N', description: 'Previous hunk' },
|
|
78
|
+
{ key: 's', description: 'Toggle hunk staged/unstaged' },
|
|
79
|
+
{ key: 'd', description: 'Discard changes' },
|
|
80
|
+
],
|
|
72
81
|
},
|
|
73
82
|
];
|
|
74
83
|
/**
|
|
@@ -186,7 +195,7 @@ export class HotkeysModal {
|
|
|
186
195
|
this.box.setContent(lines.join('\n'));
|
|
187
196
|
this.screen.render();
|
|
188
197
|
}
|
|
189
|
-
renderGroups(groups,
|
|
198
|
+
renderGroups(groups, _colWidth) {
|
|
190
199
|
const lines = [];
|
|
191
200
|
for (const group of groups) {
|
|
192
201
|
lines.push(`{bold}{gray-fg}${group.title}{/gray-fg}{/bold}`);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import blessed from 'neo-blessed';
|
|
2
|
-
import { themes, themeOrder
|
|
2
|
+
import { themes, themeOrder } from '../../themes.js';
|
|
3
3
|
/**
|
|
4
4
|
* ThemePicker modal for selecting diff themes.
|
|
5
5
|
*/
|
|
@@ -85,7 +85,6 @@ export class ThemePicker {
|
|
|
85
85
|
// Preview section
|
|
86
86
|
lines.push('');
|
|
87
87
|
lines.push('{gray-fg}Preview:{/gray-fg}');
|
|
88
|
-
const previewTheme = getTheme(themeOrder[this.selectedIndex]);
|
|
89
88
|
// Simple preview - just show add/del colors
|
|
90
89
|
lines.push(` {green-fg}+ added line{/green-fg}`);
|
|
91
90
|
lines.push(` {red-fg}- deleted line{/red-fg}`);
|
|
@@ -11,7 +11,7 @@ export function formatCommitPanel(state, stagedCount, width) {
|
|
|
11
11
|
lines.push(title);
|
|
12
12
|
lines.push('');
|
|
13
13
|
// Message input area
|
|
14
|
-
const borderChar =
|
|
14
|
+
const borderChar = '\u2502';
|
|
15
15
|
const borderColor = state.inputFocused ? 'cyan' : 'gray';
|
|
16
16
|
// Top border
|
|
17
17
|
const innerWidth = Math.max(20, width - 6);
|