reslop 0.2.5 → 0.2.7
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/CHANGELOG.md +22 -1
- package/LICENSE +1 -1
- package/README.md +11 -11
- package/bin/reslop.js +3 -8
- package/lib/ansi.js +72 -19
- package/lib/api.js +1 -1
- package/lib/capabilities.js +8 -8
- package/lib/cli.js +61 -75
- package/lib/clipboard.js +39 -47
- package/lib/dashboard/git.js +128 -0
- package/lib/dashboard/model.js +357 -0
- package/lib/dashboard/npm.js +290 -0
- package/lib/dashboard/tree.js +278 -0
- package/lib/deps-diff.js +197 -402
- package/lib/deps.js +113 -240
- package/lib/detect.js +8 -18
- package/lib/diff/align.js +41 -113
- package/lib/diff/diff.js +12 -43
- package/lib/diff/display.js +25 -36
- package/lib/diff/edit.js +6 -20
- package/lib/diff/inline.js +76 -144
- package/lib/diff/items.js +33 -47
- package/lib/diff/parse.js +28 -56
- package/lib/diff/patch.js +12 -33
- package/lib/diff/unit.js +79 -178
- package/lib/editor.js +52 -69
- package/lib/files.js +79 -91
- package/lib/find.js +61 -0
- package/lib/git-branches.js +254 -416
- package/lib/git-deps.js +148 -272
- package/lib/git-reword-todo.js +5 -13
- package/lib/git-worktree.js +161 -214
- package/lib/git.js +21 -49
- package/lib/github.js +67 -181
- package/lib/gitlab.js +40 -90
- package/lib/highlight/bash.js +55 -177
- package/lib/highlight/core.js +155 -141
- package/lib/highlight/css.js +25 -58
- package/lib/highlight/csv.js +19 -37
- package/lib/highlight/dart.js +72 -213
- package/lib/highlight/dot.js +14 -14
- package/lib/highlight/html.js +42 -63
- package/lib/highlight/js.js +177 -506
- package/lib/highlight/json.js +17 -37
- package/lib/highlight/log.js +19 -25
- package/lib/highlight/txt.js +7 -10
- package/lib/highlight.js +17 -46
- package/lib/keys.js +75 -83
- package/lib/manifest.js +67 -126
- package/lib/npm-commands.js +120 -169
- package/lib/remote.js +140 -190
- package/lib/render/branches.js +63 -61
- package/lib/render/chrome.js +412 -347
- package/lib/render/commits.js +160 -361
- package/lib/render/dash-activity.js +212 -0
- package/lib/render/dash-blocks.js +496 -0
- package/lib/render/dash-table.js +295 -0
- package/lib/render/dashboard.js +62 -0
- package/lib/render/diff.js +123 -258
- package/lib/render/files.js +60 -86
- package/lib/render/frame.js +82 -117
- package/lib/render/notes.js +150 -210
- package/lib/render/npm.js +213 -177
- package/lib/render/packages.js +93 -0
- package/lib/render/present.js +4 -11
- package/lib/render/primitives.js +77 -172
- package/lib/render/render.js +5 -14
- package/lib/render/report.js +43 -0
- package/lib/render/repos.js +226 -0
- package/lib/render/search.js +25 -0
- package/lib/render/tiles.js +355 -0
- package/lib/render/unit.js +15 -31
- package/lib/report-model.js +110 -33
- package/lib/report-parse.js +56 -37
- package/lib/report-render.js +102 -59
- package/lib/report-run.js +87 -77
- package/lib/report-test.js +221 -99
- package/lib/report-tools.js +96 -154
- package/lib/review-merge.js +33 -41
- package/lib/review.js +69 -129
- package/lib/runs.js +225 -0
- package/lib/select.js +53 -91
- package/lib/session/accessors.js +14 -21
- package/lib/session/actions.js +189 -65
- package/lib/session/branches.js +164 -222
- package/lib/session/change-actions.js +49 -119
- package/lib/session/commit.js +298 -352
- package/lib/session/compose.js +572 -715
- package/lib/session/dashboard.js +530 -0
- package/lib/session/dispatch.js +197 -171
- package/lib/session/files-pane.js +92 -45
- package/lib/session/find.js +94 -0
- package/lib/session/items.js +6 -17
- package/lib/session/lifecycle.js +420 -466
- package/lib/session/load.js +1 -14
- package/lib/session/navigation.js +4 -2
- package/lib/session/npm.js +403 -420
- package/lib/session/ops.js +5 -13
- package/lib/session/packages.js +143 -0
- package/lib/session/pointer.js +30 -14
- package/lib/session/progress.js +39 -30
- package/lib/session/review.js +1 -3
- package/lib/session/tasks.js +277 -236
- package/lib/session/templates.js +51 -59
- package/lib/session/terminal.js +85 -132
- package/lib/session/view.js +59 -80
- package/lib/session/watch.js +201 -212
- package/lib/session/workspace.js +455 -0
- package/lib/session.js +95 -161
- package/lib/update.js +44 -111
- package/lib/utilities.js +46 -88
- package/lib/workspace.js +166 -0
- package/lib/wrap.js +29 -55
- package/package.json +1 -1
package/lib/render/chrome.js
CHANGED
|
@@ -4,39 +4,32 @@ const ansi = require('../ansi.js');
|
|
|
4
4
|
const keys = require('../keys.js');
|
|
5
5
|
const files = require('../files.js');
|
|
6
6
|
const primitives = require('./primitives.js');
|
|
7
|
+
const blocks = require('./dash-blocks.js');
|
|
8
|
+
const { npmStatus } = blocks;
|
|
7
9
|
|
|
8
10
|
const { itemPath, REPO_TASKS_LABEL, isTasksEntry, fileTotals } = files;
|
|
9
|
-
const { ACTION_IDS,
|
|
10
|
-
const { buttonWord } = keys;
|
|
11
|
+
const { ACTION_IDS, actionLetter } = keys;
|
|
12
|
+
const { DASH_IDS, DASH_HIDDEN, buttonWord } = keys;
|
|
11
13
|
const { FILES_HIDDEN, FILES_GIT_DISABLED, BRANCHES_DISABLED } = keys;
|
|
12
14
|
const { COMMITS_DISABLED, TASKS_DISABLED, DIFF_DISABLED, UNIT_DISABLED } = keys;
|
|
13
15
|
const { NPM_DISABLED } = keys;
|
|
14
16
|
const { diffGitDisabled, branchGitDisabled, commitGitDisabled } = keys;
|
|
17
|
+
const { entryHasDiff, lineHasDiff, withoutMoves } = keys;
|
|
15
18
|
const { CONFIRM, confirmPrefix, promptFromChoices } = keys;
|
|
16
19
|
const { THEME, paint, visibleWidth } = ansi;
|
|
17
20
|
const { truncateVisible, fg, RESET, EL, seq } = ansi;
|
|
18
|
-
const { fill, branchLabel } = primitives;
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
slash: THEME.headerSlashFg,
|
|
24
|
-
file: THEME.headerFileFg,
|
|
25
|
-
};
|
|
21
|
+
const { fill, branchLabel, isTaskView } = primitives;
|
|
22
|
+
const searchPaint = require('./search.js');
|
|
23
|
+
const { paintPathText } = searchPaint;
|
|
24
|
+
const locate = require('../find.js');
|
|
25
|
+
const { activeHit, spansFor, fileMatches } = locate;
|
|
26
26
|
|
|
27
27
|
const WAVE = '·•●•·';
|
|
28
|
+
const BUSY_BAR = Array.from(WAVE, (_, frame) => {
|
|
29
|
+
const start = (WAVE.length - frame) % WAVE.length;
|
|
30
|
+
return WAVE.slice(start) + WAVE.slice(0, start);
|
|
31
|
+
});
|
|
28
32
|
|
|
29
|
-
const busyBars = (wave) => {
|
|
30
|
-
const n = wave.length;
|
|
31
|
-
const frames = new Array(n);
|
|
32
|
-
for (let frame = 0; frame < n; frame++) {
|
|
33
|
-
const start = (n - frame) % n;
|
|
34
|
-
frames[frame] = wave.slice(start) + wave.slice(0, start);
|
|
35
|
-
}
|
|
36
|
-
return frames;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const BUSY_BAR = busyBars(WAVE);
|
|
40
33
|
const BUSY_PROGRESS = [
|
|
41
34
|
'pulling',
|
|
42
35
|
'pushing',
|
|
@@ -71,6 +64,7 @@ const INFO_STATUS = [
|
|
|
71
64
|
'dropped',
|
|
72
65
|
'already staged',
|
|
73
66
|
'read only',
|
|
67
|
+
'transitive',
|
|
74
68
|
'nothing to review',
|
|
75
69
|
'nothing to commit',
|
|
76
70
|
'unified',
|
|
@@ -112,178 +106,190 @@ const INFO_STATUS = [
|
|
|
112
106
|
];
|
|
113
107
|
const INFO_PREFIX = ['checked out ', 'created ', 'rebased onto ', 'dropped '];
|
|
114
108
|
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
109
|
+
const LOGO = '👁️';
|
|
110
|
+
const HEADER_LEAD = ` ${LOGO} reslop: `;
|
|
111
|
+
const HEADER_ROLE_FG = {
|
|
112
|
+
chrome: THEME.headerChromeFg,
|
|
113
|
+
repo: THEME.headerRepoFg,
|
|
114
|
+
dir: THEME.headerDirFg,
|
|
115
|
+
slash: THEME.headerSlashFg,
|
|
116
|
+
file: THEME.headerFileFg,
|
|
117
|
+
};
|
|
120
118
|
|
|
121
119
|
const BUTTON_LEAD = ' ';
|
|
122
120
|
const BUTTON_GAP = ' ';
|
|
123
121
|
const BUTTON_EDGE = 1;
|
|
124
|
-
const buttonGap = (index) => (index ? BUTTON_GAP : BUTTON_LEAD);
|
|
125
122
|
const BACK_ARROW = '🢐';
|
|
126
|
-
const STOP_MARK = '⊗';
|
|
127
123
|
const BACK_KEY = 'esc';
|
|
128
|
-
|
|
124
|
+
|
|
125
|
+
const BUTTON_MARK = {
|
|
126
|
+
bgRgb: THEME.buttonBg,
|
|
127
|
+
fgRgb: THEME.buttonFg,
|
|
128
|
+
hotFg: THEME.buttonHotFg,
|
|
129
|
+
missFg: THEME.buttonHotFg,
|
|
130
|
+
missBold: true,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const QUIT_MARK = {
|
|
134
|
+
bgRgb: THEME.chromeBg,
|
|
135
|
+
fgRgb: THEME.mutedFg,
|
|
136
|
+
hotFg: THEME.warnFg,
|
|
137
|
+
missFg: THEME.mutedFg,
|
|
138
|
+
missBold: false,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const STATUS_MARK = /(\+[0-9]+(?:\/[0-9]+)?|-[0-9]+(?:\/[0-9]+)?)/g;
|
|
142
|
+
const OLD_LOGS = /old logs \S+/;
|
|
143
|
+
const SAVE_HINT = 'Press **Ctrl-S** or **Enter** at EOF to save';
|
|
144
|
+
const HINT_HOT_FG = 0xff;
|
|
145
|
+
|
|
146
|
+
const selectedFile = (view) => (view.files ?? [])[view.fileCursor ?? 0];
|
|
147
|
+
|
|
148
|
+
const selectedBranch = (view) => (view.branches ?? [])[view.branchCursor ?? 0];
|
|
149
|
+
|
|
150
|
+
const selectedCommit = (view) => (view.commits ?? [])[view.commitCursor ?? 0];
|
|
151
|
+
|
|
152
|
+
const piecesText = (pieces) => pieces.map((piece) => piece.text).join('');
|
|
129
153
|
|
|
130
154
|
const pathSegments = (rel) => {
|
|
131
155
|
const segs = [];
|
|
132
156
|
if (!rel) return segs;
|
|
133
157
|
const bits = rel.split('/');
|
|
134
158
|
for (let i = 0; i < bits.length; i++) {
|
|
135
|
-
if (i) {
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
if (!bits[i]) {
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
const last = i === bits.length - 1;
|
|
142
|
-
segs.push({ text: bits[i], role: last ? 'file' : 'dir' });
|
|
159
|
+
if (i) segs.push({ text: '/', role: 'slash' });
|
|
160
|
+
const role = i === bits.length - 1 ? 'file' : 'dir';
|
|
161
|
+
if (bits[i]) segs.push({ text: bits[i], role });
|
|
143
162
|
}
|
|
144
163
|
return segs;
|
|
145
164
|
};
|
|
146
165
|
|
|
147
166
|
const headerScreen = (view) => {
|
|
148
|
-
const pane = view
|
|
149
|
-
if (
|
|
167
|
+
const { pane } = view;
|
|
168
|
+
if (
|
|
169
|
+
pane === 'dashboard' ||
|
|
170
|
+
pane === 'repos' ||
|
|
171
|
+
pane === 'branches' ||
|
|
172
|
+
pane === 'npm' ||
|
|
173
|
+
pane === 'packages' ||
|
|
174
|
+
pane === 'tasks'
|
|
175
|
+
) {
|
|
176
|
+
return pane;
|
|
177
|
+
}
|
|
150
178
|
if (pane === 'commits') {
|
|
151
|
-
|
|
152
|
-
return `commits ${mode}`;
|
|
179
|
+
return view.commitView === 'full' ? 'commits full' : 'commits brief';
|
|
153
180
|
}
|
|
154
|
-
if (pane === 'npm') return 'npm';
|
|
155
181
|
if (pane === 'files') {
|
|
156
|
-
|
|
157
|
-
const entry = files[view.fileCursor ?? 0];
|
|
158
|
-
if (isTasksEntry(entry)) return REPO_TASKS_LABEL;
|
|
159
|
-
return '';
|
|
182
|
+
return isTasksEntry(selectedFile(view)) ? REPO_TASKS_LABEL : '';
|
|
160
183
|
}
|
|
161
|
-
|
|
162
|
-
return '';
|
|
184
|
+
return isTaskView(view) ? REPO_TASKS_LABEL : '';
|
|
163
185
|
};
|
|
164
186
|
|
|
165
187
|
const headerTarget = (view) => {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const entry = (view.files ?? [])[view.fileCursor ?? 0];
|
|
188
|
+
if (view.pane === 'files') {
|
|
189
|
+
const entry = selectedFile(view);
|
|
169
190
|
if (!entry || isTasksEntry(entry)) return '';
|
|
170
191
|
return entry.path;
|
|
171
192
|
}
|
|
172
|
-
if (pane === 'unit') return view.reviewPath ?? '';
|
|
173
|
-
if (
|
|
193
|
+
if (view.pane === 'unit') return view.reviewPath ?? '';
|
|
194
|
+
if (isTaskView(view)) return '';
|
|
174
195
|
return itemPath(view.item);
|
|
175
196
|
};
|
|
176
197
|
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
const headerPrefix = (view, label) => {
|
|
198
|
+
const headerLead = (view) => {
|
|
199
|
+
const screen = headerScreen(view);
|
|
200
|
+
const label = screen || headerTarget(view);
|
|
182
201
|
const repo = view.repoName || '';
|
|
183
|
-
|
|
184
|
-
if (repo)
|
|
185
|
-
|
|
202
|
+
const pieces = [{ text: HEADER_LEAD, role: 'chrome' }];
|
|
203
|
+
if (repo) {
|
|
204
|
+
pieces.push({ text: repo, role: 'repo' });
|
|
205
|
+
if (!label) pieces.push({ text: ' ', role: 'chrome' });
|
|
206
|
+
else if (screen) pieces.push({ text: ': ', role: 'chrome' });
|
|
207
|
+
else pieces.push({ text: '/', role: 'slash' });
|
|
208
|
+
}
|
|
209
|
+
return { pieces, label };
|
|
186
210
|
};
|
|
187
211
|
|
|
188
212
|
const headerText = (view) => {
|
|
189
|
-
const label =
|
|
190
|
-
return `${
|
|
213
|
+
const { pieces, label } = headerLead(view);
|
|
214
|
+
return `${piecesText(pieces)}${label}`;
|
|
191
215
|
};
|
|
192
216
|
|
|
193
|
-
const
|
|
194
|
-
const
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
const used = prefixW + visibleWidth(right);
|
|
200
|
-
const room = Math.max(0, width - used);
|
|
201
|
-
const clipped = truncateVisible(path, room);
|
|
202
|
-
const pad = Math.max(0, room - visibleWidth(clipped));
|
|
203
|
-
const pieces = [{ text: chrome, role: 'chrome' }];
|
|
204
|
-
if (repo) {
|
|
205
|
-
pieces.push({ text: repo, role: 'repo' });
|
|
206
|
-
if (path) {
|
|
207
|
-
const sep = headerSep(view);
|
|
208
|
-
const role = sep === '/' ? 'slash' : 'chrome';
|
|
209
|
-
pieces.push({ text: sep, role });
|
|
210
|
-
} else {
|
|
211
|
-
pieces.push({ text: ' ', role: 'chrome' });
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
for (const seg of pathSegments(clipped)) {
|
|
215
|
-
pieces.push(seg);
|
|
216
|
-
}
|
|
217
|
-
if (pad) {
|
|
218
|
-
pieces.push({ text: ' '.repeat(pad), role: 'chrome' });
|
|
219
|
-
}
|
|
220
|
-
pieces.push({ text: right, role: 'chrome' });
|
|
221
|
-
return pieces;
|
|
217
|
+
const headerFind = (view, label, clipped) => {
|
|
218
|
+
const query = view.find && view.find.query ? view.find.query : '';
|
|
219
|
+
const target = headerTarget(view);
|
|
220
|
+
if (!query || label !== target || !label) return [];
|
|
221
|
+
const hit = activeHit(view.files ?? [], view.find);
|
|
222
|
+
return spansFor(clipped, label, query, hit);
|
|
222
223
|
};
|
|
223
224
|
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
if (!color) {
|
|
227
|
-
return pieces.map((piece) => piece.text).join('');
|
|
228
|
-
}
|
|
229
|
-
const bgRgb = THEME.headerBg;
|
|
230
|
-
let out = `${seq(THEME.headerChromeFg, bgRgb)}${EL}`;
|
|
225
|
+
const paintHeaderRoles = (pieces) => {
|
|
226
|
+
let out = `${seq(THEME.headerChromeFg, THEME.headerBg)}${EL}`;
|
|
231
227
|
for (const piece of pieces) {
|
|
232
|
-
|
|
233
|
-
continue;
|
|
234
|
-
}
|
|
235
|
-
const roleFg = HEADER_ROLE_FG[piece.role] ?? THEME.headerChromeFg;
|
|
236
|
-
out += `${fg(roleFg)}${piece.text}`;
|
|
228
|
+
out += `${fg(HEADER_ROLE_FG[piece.role])}${piece.text}`;
|
|
237
229
|
}
|
|
238
|
-
return
|
|
230
|
+
return out;
|
|
239
231
|
};
|
|
240
232
|
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
233
|
+
const paintHeaderHits = (clipped, spans, color) => {
|
|
234
|
+
let out = '';
|
|
235
|
+
let offset = 0;
|
|
236
|
+
for (const seg of pathSegments(clipped)) {
|
|
237
|
+
const local = [];
|
|
238
|
+
const end = offset + seg.text.length;
|
|
239
|
+
for (const span of spans) {
|
|
240
|
+
const start = Math.max(span.start, offset);
|
|
241
|
+
const stop = Math.min(span.end, end);
|
|
242
|
+
if (start >= stop) continue;
|
|
243
|
+
local.push({
|
|
244
|
+
start: start - offset,
|
|
245
|
+
end: stop - offset,
|
|
246
|
+
on: span.on === true,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
const roleFg = HEADER_ROLE_FG[seg.role];
|
|
250
|
+
out += paintPathText(seg.text, roleFg, THEME.headerBg, color, false, local);
|
|
251
|
+
offset = end;
|
|
252
|
+
}
|
|
253
|
+
return out;
|
|
247
254
|
};
|
|
248
255
|
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
256
|
+
const paintHeader = (view, width, color) => {
|
|
257
|
+
const { pieces, label } = headerLead(view);
|
|
258
|
+
const right = ' ';
|
|
259
|
+
const used = visibleWidth(piecesText(pieces)) + visibleWidth(right);
|
|
260
|
+
const room = Math.max(0, width - used);
|
|
261
|
+
const clipped = truncateVisible(label, room);
|
|
262
|
+
const pad = ' '.repeat(Math.max(0, room - visibleWidth(clipped)));
|
|
263
|
+
const spans = headerFind(view, label, clipped);
|
|
264
|
+
if (!color || !spans.length) {
|
|
265
|
+
pieces.push(...pathSegments(clipped));
|
|
266
|
+
if (pad) pieces.push({ text: pad, role: 'chrome' });
|
|
267
|
+
pieces.push({ text: right, role: 'chrome' });
|
|
268
|
+
if (!color) return piecesText(pieces);
|
|
269
|
+
return `${paintHeaderRoles(pieces)}${RESET}`;
|
|
270
|
+
}
|
|
271
|
+
let out = paintHeaderRoles(pieces);
|
|
272
|
+
out += paintHeaderHits(clipped, spans, color);
|
|
273
|
+
out += `${fg(HEADER_ROLE_FG.chrome)}${pad}${right}`;
|
|
274
|
+
return `${out}${RESET}`;
|
|
255
275
|
};
|
|
256
276
|
|
|
257
|
-
const backFooterId = (view) =>
|
|
258
|
-
|
|
259
|
-
if (pane === 'branches' || pane === 'commits') return 'back';
|
|
260
|
-
if (pane === 'npm') return view.npmView ? 'npmStop' : 'back';
|
|
261
|
-
if (view.item && view.item.origin === 'task') return 'back';
|
|
262
|
-
return '';
|
|
263
|
-
};
|
|
277
|
+
const backFooterId = (view) =>
|
|
278
|
+
view.pane === 'npm' && view.npmView ? 'npmStop' : 'back';
|
|
264
279
|
|
|
265
|
-
const backHintMark = (
|
|
266
|
-
if (view.pane === 'npm' && view.npmView && view.npmRunning) return STOP_MARK;
|
|
267
|
-
return BACK_ARROW;
|
|
268
|
-
};
|
|
280
|
+
const backHintMark = () => BACK_ARROW;
|
|
269
281
|
|
|
270
|
-
const
|
|
271
|
-
const gap = mark === STOP_MARK ? ' ' : '';
|
|
272
|
-
return `${mark}${gap}${BACK_KEY}`;
|
|
273
|
-
};
|
|
282
|
+
const buttonGap = (index) => (index ? BUTTON_GAP : BUTTON_LEAD);
|
|
274
283
|
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
return buttonWord(id);
|
|
279
|
-
};
|
|
284
|
+
const hintBody = (mark) => `${mark}${BACK_KEY}`;
|
|
285
|
+
|
|
286
|
+
const buttonLabel = (id, short) => (short ? actionLetter(id) : buttonWord(id));
|
|
280
287
|
|
|
281
288
|
const paintBackHint = (color, mark) => {
|
|
282
289
|
const bgRgb = THEME.buttonBg;
|
|
283
|
-
const
|
|
290
|
+
const rest = hintBody(mark).slice(mark.length);
|
|
284
291
|
const arrow = paint(mark, THEME.buttonHotFg, bgRgb, color, true);
|
|
285
|
-
|
|
286
|
-
return arrow + rest;
|
|
292
|
+
return arrow + paint(rest, THEME.buttonFg, bgRgb, color);
|
|
287
293
|
};
|
|
288
294
|
|
|
289
295
|
const paintMarkedWord = (label, letter, color, mark) => {
|
|
@@ -294,59 +300,133 @@ const paintMarkedWord = (label, letter, color, mark) => {
|
|
|
294
300
|
const before = label.slice(0, at);
|
|
295
301
|
const hot = label.slice(at, at + letter.length);
|
|
296
302
|
const after = label.slice(at + letter.length);
|
|
297
|
-
let out =
|
|
298
|
-
if (before) out += paint(before, mark.fgRgb, mark.bgRgb, color);
|
|
303
|
+
let out = paint(before, mark.fgRgb, mark.bgRgb, color);
|
|
299
304
|
out += paint(hot, mark.hotFg, mark.bgRgb, color, true);
|
|
300
|
-
|
|
301
|
-
return out;
|
|
305
|
+
return out + paint(after, mark.fgRgb, mark.bgRgb, color);
|
|
302
306
|
};
|
|
303
307
|
|
|
304
|
-
const footerIds = (hidden, extra, backId) => {
|
|
308
|
+
const footerIds = (hidden, extra, backId, globals, tail, after) => {
|
|
305
309
|
const ids = backId ? [backId] : [];
|
|
306
|
-
|
|
307
|
-
if (id
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
310
|
+
const push = (id) => {
|
|
311
|
+
if (!ids.includes(id)) ids.push(id);
|
|
312
|
+
};
|
|
313
|
+
for (const add of extra) push(add);
|
|
314
|
+
for (const id of globals) push(id);
|
|
315
|
+
if (tail) {
|
|
316
|
+
for (const id of ACTION_IDS) {
|
|
317
|
+
if (id === 'quit' || hidden.includes(id) || ids.includes(id)) continue;
|
|
318
|
+
ids.push(id);
|
|
313
319
|
}
|
|
314
320
|
}
|
|
321
|
+
for (const id of after) push(id);
|
|
315
322
|
return ids;
|
|
316
323
|
};
|
|
317
324
|
|
|
325
|
+
const buttonPiece = (id, index, short, backId, mark) => {
|
|
326
|
+
const back = id === backId;
|
|
327
|
+
const letter = back ? BACK_KEY : actionLetter(id);
|
|
328
|
+
const label = back ? hintBody(mark) : buttonLabel(id, short);
|
|
329
|
+
const gap = buttonGap(index);
|
|
330
|
+
return {
|
|
331
|
+
id,
|
|
332
|
+
piece: `${gap}${label}`,
|
|
333
|
+
gap,
|
|
334
|
+
letter,
|
|
335
|
+
label,
|
|
336
|
+
back,
|
|
337
|
+
mark: back ? mark : '',
|
|
338
|
+
};
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const packGroup = (ids, short, start, backId, mark) => {
|
|
342
|
+
const parts = [];
|
|
343
|
+
for (let i = 0; i < ids.length; i++) {
|
|
344
|
+
parts.push(buttonPiece(ids[i], start + i, short, backId, mark));
|
|
345
|
+
}
|
|
346
|
+
return parts;
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const groupWidth = (parts) => {
|
|
350
|
+
let width = 0;
|
|
351
|
+
for (const part of parts) width += visibleWidth(part.piece);
|
|
352
|
+
return width;
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const placeGroup = (parts, hits, disabled, x) => {
|
|
356
|
+
const placed = [];
|
|
357
|
+
let cursor = x;
|
|
358
|
+
for (const part of parts) {
|
|
359
|
+
const w = visibleWidth(part.piece);
|
|
360
|
+
const off = disabled.includes(part.id);
|
|
361
|
+
if (!off) hits.push({ id: part.id, x0: cursor, x1: cursor + w });
|
|
362
|
+
placed.push({ ...part, disabled: off });
|
|
363
|
+
cursor += w;
|
|
364
|
+
}
|
|
365
|
+
return { placed, cursor };
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const splitFooter = (ids, options, limit) => {
|
|
369
|
+
const { disabled = [], backId = '', mark = BACK_ARROW } = options;
|
|
370
|
+
const { globals = [], after = [] } = options;
|
|
371
|
+
const rightIds = [];
|
|
372
|
+
for (const id of [...globals, ...after]) {
|
|
373
|
+
if (ids.includes(id) && !rightIds.includes(id)) rightIds.push(id);
|
|
374
|
+
}
|
|
375
|
+
const leftIds = ids.filter((id) => !rightIds.includes(id));
|
|
376
|
+
const pack = (leftShort, rightShort) => {
|
|
377
|
+
const left = packGroup(leftIds, leftShort, 0, backId, mark);
|
|
378
|
+
const start = left.length ? 1 : 0;
|
|
379
|
+
const right = packGroup(rightIds, rightShort, start, backId, mark);
|
|
380
|
+
return { left, right };
|
|
381
|
+
};
|
|
382
|
+
const wide = (packed) =>
|
|
383
|
+
groupWidth(packed.left) + groupWidth(packed.right) > limit;
|
|
384
|
+
let packed = pack(false, false);
|
|
385
|
+
if (wide(packed)) packed = pack(false, true);
|
|
386
|
+
if (wide(packed)) packed = pack(true, true);
|
|
387
|
+
const left = packed.left.slice();
|
|
388
|
+
const right = packed.right.slice();
|
|
389
|
+
while (groupWidth(left) + groupWidth(right) > limit && left.length) {
|
|
390
|
+
left.pop();
|
|
391
|
+
}
|
|
392
|
+
while (groupWidth(left) + groupWidth(right) > limit && right.length) {
|
|
393
|
+
right.pop();
|
|
394
|
+
}
|
|
395
|
+
const hits = [];
|
|
396
|
+
const leftPlace = placeGroup(left, hits, disabled, 0);
|
|
397
|
+
const parts = leftPlace.placed;
|
|
398
|
+
let x = leftPlace.cursor;
|
|
399
|
+
const gap = Math.max(0, limit - x - groupWidth(right));
|
|
400
|
+
if (gap && right.length) {
|
|
401
|
+
parts.push({ spacer: true, piece: ' '.repeat(gap) });
|
|
402
|
+
x += gap;
|
|
403
|
+
}
|
|
404
|
+
const rightPlace = placeGroup(right, hits, disabled, x);
|
|
405
|
+
parts.push(...rightPlace.placed);
|
|
406
|
+
return { hits, parts };
|
|
407
|
+
};
|
|
408
|
+
|
|
318
409
|
const layoutButtons = (width, options = {}) => {
|
|
319
|
-
const { compact = false, hidden = [], extra = [] } = options;
|
|
320
|
-
const {
|
|
321
|
-
const
|
|
322
|
-
const ids = footerIds(hidden, extra, backId);
|
|
410
|
+
const { compact = false, hidden = [], extra = [], disabled = [] } = options;
|
|
411
|
+
const { backId = '', mark = BACK_ARROW, globals = [] } = options;
|
|
412
|
+
const { tail = true, after = [] } = options;
|
|
413
|
+
const ids = footerIds(hidden, extra, backId, globals, tail, after);
|
|
414
|
+
const limit = Math.max(0, width - BUTTON_EDGE);
|
|
415
|
+
if (globals.length) return splitFooter(ids, options, limit);
|
|
323
416
|
const build = (short) => {
|
|
324
417
|
const hits = [];
|
|
325
418
|
const parts = [];
|
|
326
419
|
let x = 0;
|
|
327
|
-
const limit = Math.max(0, width - BUTTON_EDGE);
|
|
328
420
|
for (const id of ids) {
|
|
329
|
-
const
|
|
330
|
-
const
|
|
331
|
-
const label = buttonLabel(id, short, backId, hint);
|
|
332
|
-
const gap = buttonGap(parts.length);
|
|
333
|
-
const piece = `${gap}${label}`;
|
|
334
|
-
const w = visibleWidth(piece);
|
|
421
|
+
const part = buttonPiece(id, parts.length, short, backId, mark);
|
|
422
|
+
const w = visibleWidth(part.piece);
|
|
335
423
|
if (x + w > limit && parts.length) break;
|
|
336
|
-
const off =
|
|
424
|
+
const off = disabled.includes(id);
|
|
337
425
|
if (!off) hits.push({ id, x0: x, x1: x + w });
|
|
338
|
-
parts.push({
|
|
339
|
-
action: { id },
|
|
340
|
-
piece,
|
|
341
|
-
letter,
|
|
342
|
-
label,
|
|
343
|
-
disabled: off,
|
|
344
|
-
back,
|
|
345
|
-
mark: back ? hint : '',
|
|
346
|
-
});
|
|
426
|
+
parts.push({ ...part, disabled: off });
|
|
347
427
|
x += w;
|
|
348
428
|
}
|
|
349
|
-
return { hits, parts
|
|
429
|
+
return { hits, parts };
|
|
350
430
|
};
|
|
351
431
|
if (compact) return build(true);
|
|
352
432
|
const words = build(false);
|
|
@@ -355,72 +435,71 @@ const layoutButtons = (width, options = {}) => {
|
|
|
355
435
|
};
|
|
356
436
|
|
|
357
437
|
const paintButtons = (layout, width, color) => {
|
|
358
|
-
let out = '';
|
|
359
|
-
let used = 0;
|
|
360
438
|
const fgRgb = THEME.buttonFg;
|
|
361
439
|
const bgRgb = THEME.buttonBg;
|
|
440
|
+
let out = '';
|
|
441
|
+
let used = 0;
|
|
362
442
|
for (let i = 0; i < layout.parts.length; i++) {
|
|
363
443
|
const part = layout.parts[i];
|
|
364
|
-
|
|
444
|
+
if (part.spacer) {
|
|
445
|
+
out += paint(part.piece, fgRgb, bgRgb, color);
|
|
446
|
+
used += visibleWidth(part.piece);
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
const gap = part.gap ?? buttonGap(i);
|
|
365
450
|
out += paint(gap, fgRgb, bgRgb, color);
|
|
366
451
|
if (part.back) out += paintBackHint(color, part.mark);
|
|
367
452
|
else if (part.disabled) out += paint(part.label, fgRgb, bgRgb, color);
|
|
368
453
|
else out += paintMarkedWord(part.label, part.letter, color, BUTTON_MARK);
|
|
369
454
|
used += visibleWidth(part.piece);
|
|
370
455
|
}
|
|
371
|
-
|
|
372
|
-
out += fill(width - used, THEME.buttonFg, THEME.buttonBg, color);
|
|
373
|
-
}
|
|
374
|
-
return out;
|
|
456
|
+
return out + fill(width - used, fgRgb, bgRgb, color);
|
|
375
457
|
};
|
|
376
458
|
|
|
377
|
-
const isInfoStatus = (status) =>
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
if (status.startsWith(prefix)) return true;
|
|
381
|
-
}
|
|
382
|
-
return false;
|
|
383
|
-
};
|
|
459
|
+
const isInfoStatus = (status) =>
|
|
460
|
+
INFO_STATUS.includes(status) ||
|
|
461
|
+
INFO_PREFIX.some((prefix) => status.startsWith(prefix));
|
|
384
462
|
|
|
385
|
-
const joinStatus = (parts) =>
|
|
386
|
-
const bits = [];
|
|
387
|
-
for (const part of parts) {
|
|
388
|
-
if (!part) continue;
|
|
389
|
-
bits.push(part);
|
|
390
|
-
}
|
|
391
|
-
return bits.join(' ');
|
|
392
|
-
};
|
|
463
|
+
const joinStatus = (parts) => parts.filter(Boolean).join(' ');
|
|
393
464
|
|
|
394
465
|
const countsKind = (sourceKind, revShort) => {
|
|
395
466
|
if (sourceKind === 'pr' || sourceKind === 'mr') return sourceKind;
|
|
396
|
-
|
|
397
|
-
return 'worktree';
|
|
467
|
+
return revShort ? 'commit' : 'worktree';
|
|
398
468
|
};
|
|
399
469
|
|
|
470
|
+
const isWorktree = (view) =>
|
|
471
|
+
countsKind(view.sourceKind, view.revShort) === 'worktree';
|
|
472
|
+
|
|
400
473
|
const notesLine = (counts) => {
|
|
401
|
-
const
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
return `feedback ${feedback} tasks ${tasks} code ${code}`;
|
|
474
|
+
const done = counts.tasksDone ?? 0;
|
|
475
|
+
const total = counts.tasks ?? 0;
|
|
476
|
+
return `tasks ${done}/${total}`;
|
|
405
477
|
};
|
|
406
478
|
|
|
407
479
|
const statusLead = (view) => {
|
|
480
|
+
if (view.pane === 'repos') {
|
|
481
|
+
const tiles = view.repos && view.repos.tiles ? view.repos.tiles : [];
|
|
482
|
+
const count = tiles.length;
|
|
483
|
+
if (!count) return 'repos';
|
|
484
|
+
const at = (view.repos.cursor ?? 0) + 1;
|
|
485
|
+
return `${at}/${count}`;
|
|
486
|
+
}
|
|
408
487
|
const counts = view.counts ?? {};
|
|
409
488
|
const kind = countsKind(view.sourceKind, view.revShort);
|
|
410
|
-
if (kind === 'worktree') return branchLabel(view.branch
|
|
489
|
+
if (kind === 'worktree') return branchLabel(view.branch);
|
|
411
490
|
if (kind === 'commit') {
|
|
412
|
-
|
|
413
|
-
return `commit ${view.revShort} ${n}`;
|
|
491
|
+
return `commit ${view.revShort} ${counts.commit ?? 0}`;
|
|
414
492
|
}
|
|
415
|
-
const n = counts.pr ?? 0;
|
|
416
493
|
const label = view.sourceLabel || kind;
|
|
417
|
-
return `${kind} ${label} ${
|
|
494
|
+
return `${kind} ${label} ${counts.pr ?? 0}`;
|
|
418
495
|
};
|
|
419
496
|
|
|
497
|
+
const STATUS_QUIET = new Set(['npm', 'packages', 'branches']);
|
|
498
|
+
|
|
420
499
|
const statusLeft = (view) => {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
return ` ${
|
|
500
|
+
if (view.pane === 'repos') return ` ${statusLead(view)}`;
|
|
501
|
+
const notes = STATUS_QUIET.has(view.pane) ? '' : notesLine(view.counts ?? {});
|
|
502
|
+
return ` ${joinStatus([statusLead(view), notes])}`;
|
|
424
503
|
};
|
|
425
504
|
|
|
426
505
|
const statusDelta = (total) => {
|
|
@@ -433,28 +512,29 @@ const statusDelta = (total) => {
|
|
|
433
512
|
if (!hasDelta && !total.removed) return '';
|
|
434
513
|
const plus = `+${stagedAdd}/${stagedAdd + unstagedAdd}`;
|
|
435
514
|
const minus = `-${stagedDel}/${stagedDel + unstagedDel}`;
|
|
436
|
-
return
|
|
515
|
+
return `${plus} ${minus}`;
|
|
437
516
|
};
|
|
438
517
|
|
|
518
|
+
const DIFF_QUIET = new Set(['npm', 'packages', 'tasks', 'branches']);
|
|
519
|
+
|
|
439
520
|
const statusStats = (view) => {
|
|
440
521
|
const counts = view.counts ?? {};
|
|
522
|
+
const staged = counts.staged ?? 0;
|
|
441
523
|
const total = fileTotals(view.files ?? []);
|
|
442
|
-
const counted = (counts.staged ?? 0) + (counts.unstaged ?? 0);
|
|
443
524
|
const hasGit =
|
|
444
525
|
total.remaining || total.staged || total.added || total.removed;
|
|
445
|
-
const
|
|
446
|
-
const
|
|
447
|
-
const
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
return joinStatus([view.npmLogs,
|
|
526
|
+
const n = hasGit ? total.staged : staged;
|
|
527
|
+
const m = hasGit ? total.remaining : staged + (counts.unstaged ?? 0);
|
|
528
|
+
const delta = DIFF_QUIET.has(view.pane) ? '' : statusDelta(total);
|
|
529
|
+
const ratio = STATUS_QUIET.has(view.pane) ? '' : `${n}/${m}`;
|
|
530
|
+
const packages = view.pane === 'packages' ? npmStatus(view.packages) : '';
|
|
531
|
+
return joinStatus([view.npmLogs, delta, ratio, packages]);
|
|
451
532
|
};
|
|
452
533
|
|
|
453
534
|
const statusLayout = (view, status, width) => {
|
|
454
|
-
const
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
if (msg && !isInfoStatus(status)) msgFg = THEME.errorFg;
|
|
535
|
+
const msg = formatBusyStatus(status, view.progressFrame ?? 0);
|
|
536
|
+
const isError = msg && !isInfoStatus(status);
|
|
537
|
+
const msgFg = isError ? THEME.errorFg : THEME.chromeFg;
|
|
458
538
|
const stats = statusStats(view);
|
|
459
539
|
const right = stats ? `${stats} ` : '';
|
|
460
540
|
const rightW = visibleWidth(right);
|
|
@@ -470,60 +550,36 @@ const statusLayout = (view, status, width) => {
|
|
|
470
550
|
};
|
|
471
551
|
|
|
472
552
|
const choiceHits = (choices, prefix, y) => {
|
|
473
|
-
const { length } = choices;
|
|
474
|
-
const hits = new Array(length);
|
|
475
553
|
let x = visibleWidth(prefix);
|
|
476
|
-
|
|
477
|
-
const
|
|
478
|
-
const
|
|
479
|
-
const w = visibleWidth(piece);
|
|
480
|
-
hits[i] = { id: choice.letter, y, x0: x, x1: x + w };
|
|
554
|
+
return choices.map((choice) => {
|
|
555
|
+
const w = visibleWidth(`${choice.lead}${choice.label}`);
|
|
556
|
+
const hit = { id: choice.letter, y, x0: x, x1: x + w };
|
|
481
557
|
x += w;
|
|
482
|
-
|
|
483
|
-
|
|
558
|
+
return hit;
|
|
559
|
+
});
|
|
484
560
|
};
|
|
485
561
|
|
|
486
|
-
const paintChoicePrompt = (choices, width, color, prefix
|
|
562
|
+
const paintChoicePrompt = (choices, width, color, prefix) => {
|
|
487
563
|
const bgRgb = THEME.chromeBg;
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
if (!color) {
|
|
493
|
-
const pad = Math.max(0, width - visibleWidth(body));
|
|
494
|
-
return body + ' '.repeat(pad);
|
|
495
|
-
}
|
|
496
|
-
let out = '';
|
|
497
|
-
if (prefix) out += paint(prefix, THEME.mutedFg, bgRgb, color);
|
|
564
|
+
const body = `${prefix}${promptFromChoices(choices)}`;
|
|
565
|
+
const pad = Math.max(0, width - visibleWidth(body));
|
|
566
|
+
if (!color) return body + ' '.repeat(pad);
|
|
567
|
+
let out = paint(prefix, THEME.mutedFg, bgRgb, color);
|
|
498
568
|
for (const choice of choices) {
|
|
499
569
|
const label = `${choice.lead}${choice.label}`;
|
|
500
570
|
out += paintMarkedWord(label, choice.letter, color, QUIT_MARK);
|
|
501
571
|
}
|
|
502
|
-
const pad = Math.max(0, width - visibleWidth(body));
|
|
503
572
|
return out + fill(pad, THEME.mutedFg, bgRgb, color);
|
|
504
573
|
};
|
|
505
574
|
|
|
506
|
-
const STATUS_MARK = /(\+[0-9]+(?:\/[0-9]+)?|-[0-9]+(?:\/[0-9]+)?)/g;
|
|
507
|
-
const OLD_LOGS = /old logs \S+/;
|
|
508
|
-
|
|
509
|
-
const paintStatusMark = (token, color) => {
|
|
510
|
-
const bgRgb = THEME.chromeBg;
|
|
511
|
-
if (token.startsWith('+')) {
|
|
512
|
-
return paint(token, THEME.addLineFg, bgRgb, color);
|
|
513
|
-
}
|
|
514
|
-
return paint(token, THEME.delLineFg, bgRgb, color);
|
|
515
|
-
};
|
|
516
|
-
|
|
517
575
|
const paintStatusLeft = (text, color) => {
|
|
518
|
-
const bgRgb = THEME.chromeBg;
|
|
519
|
-
const muted = THEME.mutedFg;
|
|
520
576
|
if (!color) return text;
|
|
577
|
+
const bgRgb = THEME.chromeBg;
|
|
521
578
|
const found = text.match(OLD_LOGS);
|
|
522
579
|
if (found) {
|
|
523
|
-
const at = found.index;
|
|
524
580
|
const token = found[0];
|
|
525
|
-
const before = text.slice(0,
|
|
526
|
-
const after = text.slice(
|
|
581
|
+
const before = text.slice(0, found.index);
|
|
582
|
+
const after = text.slice(found.index + token.length);
|
|
527
583
|
return (
|
|
528
584
|
paintStatusLeft(before, color) +
|
|
529
585
|
paint(token, THEME.shaFg, bgRgb, color) +
|
|
@@ -534,24 +590,18 @@ const paintStatusLeft = (text, color) => {
|
|
|
534
590
|
let last = 0;
|
|
535
591
|
for (const match of text.matchAll(STATUS_MARK)) {
|
|
536
592
|
const token = match[0];
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
out += paintStatusMark(token, color);
|
|
542
|
-
last = start + token.length;
|
|
543
|
-
}
|
|
544
|
-
if (last < text.length) {
|
|
545
|
-
out += paint(text.slice(last), muted, bgRgb, color);
|
|
593
|
+
const tokenFg = token.startsWith('+') ? THEME.addLineFg : THEME.delLineFg;
|
|
594
|
+
out += paint(text.slice(last, match.index), THEME.mutedFg, bgRgb, color);
|
|
595
|
+
out += paint(token, tokenFg, bgRgb, color);
|
|
596
|
+
last = match.index + token.length;
|
|
546
597
|
}
|
|
547
|
-
return out;
|
|
598
|
+
return out + paint(text.slice(last), THEME.mutedFg, bgRgb, color);
|
|
548
599
|
};
|
|
549
600
|
|
|
550
601
|
const paintBranchHead = (text, branch, color) => {
|
|
551
602
|
const name = branchLabel(branch);
|
|
552
603
|
if (!name || !text.startsWith(' ')) return null;
|
|
553
|
-
const
|
|
554
|
-
const shown = text.startsWith(head) ? name : text.slice(1);
|
|
604
|
+
const shown = text.startsWith(` ${name}`) ? name : text.slice(1);
|
|
555
605
|
if (!shown || !name.startsWith(shown)) return null;
|
|
556
606
|
const rest = text.slice(1 + shown.length);
|
|
557
607
|
const bgRgb = THEME.chromeBg;
|
|
@@ -562,40 +612,23 @@ const paintBranchHead = (text, branch, color) => {
|
|
|
562
612
|
);
|
|
563
613
|
};
|
|
564
614
|
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
const bits = source.split('**');
|
|
570
|
-
const parts = [];
|
|
571
|
-
for (let i = 0; i < bits.length; i++) {
|
|
572
|
-
if (!bits[i]) continue;
|
|
573
|
-
parts.push({ text: bits[i], hot: i % 2 === 1 });
|
|
574
|
-
}
|
|
575
|
-
return parts;
|
|
576
|
-
};
|
|
577
|
-
|
|
578
|
-
const saveHintText = () => SAVE_HINT.replaceAll('**', '');
|
|
579
|
-
|
|
580
|
-
const commitSaveHint = (view) => {
|
|
581
|
-
if (view.mode !== 'compose' || view.commitView !== 'full') return false;
|
|
582
|
-
const compose = view.compose;
|
|
583
|
-
return Boolean(compose && compose.kind === 'commit');
|
|
584
|
-
};
|
|
615
|
+
const commitSaveHint = (view) =>
|
|
616
|
+
view.mode === 'compose' &&
|
|
617
|
+
view.commitView === 'full' &&
|
|
618
|
+
view.compose?.kind === 'commit';
|
|
585
619
|
|
|
586
620
|
const paintSaveHintBar = (width, color) => {
|
|
587
621
|
const bgRgb = THEME.buttonBg;
|
|
588
|
-
const plain = `${BUTTON_LEAD}${
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
return plain + ' '.repeat(pad);
|
|
592
|
-
}
|
|
622
|
+
const plain = `${BUTTON_LEAD}${SAVE_HINT.replaceAll('**', '')}`;
|
|
623
|
+
const pad = Math.max(0, width - visibleWidth(plain));
|
|
624
|
+
if (!color) return plain + ' '.repeat(pad);
|
|
593
625
|
let out = paint(BUTTON_LEAD, THEME.buttonFg, bgRgb, color);
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
626
|
+
const bits = SAVE_HINT.split('**');
|
|
627
|
+
for (let i = 0; i < bits.length; i++) {
|
|
628
|
+
const hot = i % 2 === 1;
|
|
629
|
+
const tone = hot ? HINT_HOT_FG : THEME.buttonFg;
|
|
630
|
+
out += paint(bits[i], tone, bgRgb, color, hot);
|
|
597
631
|
}
|
|
598
|
-
const pad = Math.max(0, width - visibleWidth(plain));
|
|
599
632
|
return out + fill(pad, THEME.buttonFg, bgRgb, color);
|
|
600
633
|
};
|
|
601
634
|
|
|
@@ -604,12 +637,28 @@ const paintFooter = (view, layout, width, color) => {
|
|
|
604
637
|
return paintButtons(layout, width, color);
|
|
605
638
|
};
|
|
606
639
|
|
|
607
|
-
const footerHits = (view, layout) =>
|
|
608
|
-
|
|
609
|
-
|
|
640
|
+
const footerHits = (view, layout) => (commitSaveHint(view) ? [] : layout.hits);
|
|
641
|
+
|
|
642
|
+
const paintFindStatus = (view, width, color) => {
|
|
643
|
+
const query = view.find.query ?? '';
|
|
644
|
+
const hits = fileMatches(view.files ?? [], query);
|
|
645
|
+
const miss = query.length > 0 && hits.length === 0;
|
|
646
|
+
const lead = ' /';
|
|
647
|
+
const caret = 1;
|
|
648
|
+
const room = Math.max(0, width - lead.length - caret);
|
|
649
|
+
const shown = truncateVisible(query, room);
|
|
650
|
+
const pad = Math.max(0, width - lead.length - visibleWidth(shown) - caret);
|
|
651
|
+
if (!color) return `${lead}${shown}${' '.repeat(caret + pad)}`;
|
|
652
|
+
const bgRgb = THEME.chromeBg;
|
|
653
|
+
const tone = miss ? THEME.errorFg : THEME.chromeFg;
|
|
654
|
+
let out = paint(lead, THEME.warnFg, bgRgb, color, true);
|
|
655
|
+
out += paint(shown, tone, bgRgb, color);
|
|
656
|
+
out += paint(' ', THEME.searchOnFg, THEME.searchOnBg, color);
|
|
657
|
+
return out + fill(pad, THEME.mutedFg, bgRgb, color);
|
|
610
658
|
};
|
|
611
659
|
|
|
612
660
|
const paintStatusLine = (view, status, width, color) => {
|
|
661
|
+
if (view.find) return paintFindStatus(view, width, color);
|
|
613
662
|
const confirm = CONFIRM[view.mode];
|
|
614
663
|
if (confirm) {
|
|
615
664
|
const prefix = confirmPrefix(confirm, view);
|
|
@@ -622,8 +671,7 @@ const paintStatusLine = (view, status, width, color) => {
|
|
|
622
671
|
return layout.clipped + leftGap + layout.msg + rightGap + layout.right;
|
|
623
672
|
}
|
|
624
673
|
const bgRgb = THEME.chromeBg;
|
|
625
|
-
const
|
|
626
|
-
const head = worktree
|
|
674
|
+
const head = isWorktree(view)
|
|
627
675
|
? paintBranchHead(layout.clipped, view.branch, color)
|
|
628
676
|
: null;
|
|
629
677
|
const left = head ?? paintStatusLeft(layout.clipped, color);
|
|
@@ -639,79 +687,98 @@ const paintStatusLine = (view, status, width, color) => {
|
|
|
639
687
|
const statusChoiceHits = (view, y) => {
|
|
640
688
|
const spec = CONFIRM[view.mode];
|
|
641
689
|
if (!spec) return [];
|
|
642
|
-
|
|
643
|
-
return choiceHits(spec.choices, prefix, y);
|
|
690
|
+
return choiceHits(spec.choices, confirmPrefix(spec, view), y);
|
|
644
691
|
};
|
|
645
692
|
|
|
646
693
|
const statusBranchHit = (view, width, y) => {
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
if (!branchLabel(branch)) return null;
|
|
651
|
-
if (countsKind(view.sourceKind, view.revShort) !== 'worktree') return null;
|
|
694
|
+
if (view.find || CONFIRM[view.mode]) return null;
|
|
695
|
+
const name = branchLabel(view.branch);
|
|
696
|
+
if (!name || !isWorktree(view)) return null;
|
|
652
697
|
const layout = statusLayout(view, view.status ?? '', width);
|
|
653
|
-
const name = branchLabel(branch);
|
|
654
698
|
const at = layout.clipped.indexOf(name);
|
|
655
699
|
if (at < 0) return null;
|
|
656
700
|
return { id: 'branch', y, x0: at, x1: at + visibleWidth(name) };
|
|
657
701
|
};
|
|
658
702
|
|
|
659
|
-
const
|
|
660
|
-
|
|
661
|
-
const selectedCommit = (view) => (view.commits ?? [])[view.commitCursor ?? 0];
|
|
703
|
+
const withHidden = (ids, more) => [...ids, ...more];
|
|
662
704
|
|
|
663
705
|
const hiddenActions = (view) => {
|
|
664
|
-
const pane = view
|
|
706
|
+
const { pane } = view;
|
|
707
|
+
if (pane === 'dashboard' || pane === 'repos') return DASH_HIDDEN;
|
|
665
708
|
if (pane === 'files') return FILES_HIDDEN;
|
|
666
|
-
if (pane === 'unit') return UNIT_DISABLED;
|
|
709
|
+
if (pane === 'unit') return withHidden(UNIT_DISABLED, ['feedback', 'tasks']);
|
|
667
710
|
if (pane === 'branches') return BRANCHES_DISABLED;
|
|
668
711
|
if (pane === 'commits') return COMMITS_DISABLED;
|
|
669
712
|
if (pane === 'npm') return NPM_DISABLED;
|
|
670
|
-
if (
|
|
671
|
-
|
|
713
|
+
if (pane === 'tasks' || isTaskView(view)) {
|
|
714
|
+
return withHidden(TASKS_DISABLED, ['prev', 'next']);
|
|
715
|
+
}
|
|
716
|
+
return withHidden(DIFF_DISABLED, ['feedback', 'tasks']);
|
|
672
717
|
};
|
|
673
718
|
|
|
674
719
|
const dimFooterActions = (view) => {
|
|
675
720
|
const pane = view.pane ?? 'diff';
|
|
676
721
|
if (pane === 'files') {
|
|
677
|
-
const
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
722
|
+
const entry = selectedFile(view);
|
|
723
|
+
if (isTasksEntry(entry)) return FILES_GIT_DISABLED;
|
|
724
|
+
return withoutMoves([], entryHasDiff(entry));
|
|
725
|
+
}
|
|
726
|
+
if (pane === 'unit') {
|
|
727
|
+
const off = [...diffGitDisabled(view.item)];
|
|
728
|
+
const lines = view.unitLines;
|
|
729
|
+
if (!lines || !lines.length) return off;
|
|
730
|
+
const line = lines[view.unitLine ?? 0];
|
|
731
|
+
return withoutMoves(off, lineHasDiff(line));
|
|
732
|
+
}
|
|
733
|
+
if (pane === 'diff') return diffGitDisabled(view.item);
|
|
683
734
|
if (pane === 'branches') return branchGitDisabled(selectedBranch(view));
|
|
684
735
|
if (pane === 'commits') {
|
|
685
|
-
const entry = selectedCommit(view);
|
|
686
|
-
const staged = (view.counts?.staged ?? 0) > 0;
|
|
687
|
-
const unstagedCurrent = !view.rev && !view.revShort;
|
|
688
736
|
return commitGitDisabled({
|
|
689
|
-
...
|
|
690
|
-
canCommit: staged,
|
|
691
|
-
unstagedCurrent,
|
|
737
|
+
...selectedCommit(view),
|
|
738
|
+
canCommit: (view.counts?.staged ?? 0) > 0,
|
|
739
|
+
unstagedCurrent: !view.rev && !view.revShort,
|
|
692
740
|
});
|
|
693
741
|
}
|
|
694
|
-
if (pane === '
|
|
695
|
-
|
|
742
|
+
if (pane === 'packages') {
|
|
743
|
+
const listed = view.packages && view.packages.modules;
|
|
744
|
+
const rows = listed && listed.packages ? listed.packages : [];
|
|
745
|
+
const cursor = view.packagesCursor ?? 0;
|
|
746
|
+
const onRow = rows.length > 0 && cursor < rows.length;
|
|
747
|
+
if (!onRow) return ['packageDrop', 'packageWanted', 'packageLatest'];
|
|
748
|
+
if (rows[cursor] && rows[cursor].transitive) {
|
|
749
|
+
return ['packageNew', 'packageDrop'];
|
|
750
|
+
}
|
|
696
751
|
return [];
|
|
697
752
|
}
|
|
698
|
-
if (pane
|
|
699
|
-
return [];
|
|
753
|
+
if (pane !== 'npm') return [];
|
|
754
|
+
if (view.npmView) return view.npmRunning ? ['npmRerun'] : [];
|
|
755
|
+
return view.npmLogs ? [] : ['npmLogs'];
|
|
700
756
|
};
|
|
701
757
|
|
|
758
|
+
const GIT_FOOTER = ['add', 'unstage', 'revert', 'ignore'];
|
|
759
|
+
|
|
702
760
|
const extraFooterActions = (view) => {
|
|
703
|
-
const pane = view
|
|
704
|
-
if (pane === '
|
|
705
|
-
|
|
706
|
-
|
|
761
|
+
const { pane } = view;
|
|
762
|
+
if (pane === 'repos') return ['theme'];
|
|
763
|
+
if (pane === 'dashboard') return ['theme', 'pull', 'push'];
|
|
764
|
+
if (pane === 'unit') return ['add', 'unstage', 'revert', 'feedback', 'code'];
|
|
765
|
+
if (pane === 'files') return [...GIT_FOOTER, 'code'];
|
|
766
|
+
if (pane === 'diff') return [...GIT_FOOTER, 'feedback', 'code', 'layout'];
|
|
767
|
+
if (pane === 'branches') {
|
|
768
|
+
return ['newBranch', 'rebase', 'drop', 'pull', 'push'];
|
|
769
|
+
}
|
|
770
|
+
if (pane === 'commits') {
|
|
771
|
+
return ['newCommit', 'apply', 'reword', 'drop', 'view', 'pull', 'push'];
|
|
707
772
|
}
|
|
708
|
-
if (pane === 'branches') return Object.keys(BRANCH_ACTIONS);
|
|
709
|
-
if (pane === 'commits') return Object.keys(COMMIT_ACTIONS);
|
|
710
773
|
if (pane === 'npm') {
|
|
711
774
|
if (view.npmView) return ['npmVerbose', 'npmRerun'];
|
|
712
775
|
return ['npmEdit', 'npmNew', 'npmDrop', 'npmLogs'];
|
|
713
776
|
}
|
|
714
|
-
|
|
777
|
+
if (pane === 'packages') {
|
|
778
|
+
return ['packageNew', 'packageDrop', 'packageWanted', 'packageLatest'];
|
|
779
|
+
}
|
|
780
|
+
if (pane === 'tasks' || isTaskView(view)) return ['drop', 'check'];
|
|
781
|
+
return GIT_FOOTER;
|
|
715
782
|
};
|
|
716
783
|
|
|
717
784
|
const footerLayout = (view, width) =>
|
|
@@ -721,13 +788,11 @@ const footerLayout = (view, width) =>
|
|
|
721
788
|
disabled: dimFooterActions(view),
|
|
722
789
|
backId: backFooterId(view),
|
|
723
790
|
mark: backHintMark(view),
|
|
791
|
+
globals: view.pane === 'repos' ? [] : DASH_IDS,
|
|
792
|
+
tail: false,
|
|
724
793
|
});
|
|
725
794
|
|
|
726
795
|
module.exports = {
|
|
727
|
-
QUIT_PROMPT,
|
|
728
|
-
PUSH_PROMPT,
|
|
729
|
-
updatePrompt,
|
|
730
|
-
dropPrompt,
|
|
731
796
|
formatBusyStatus,
|
|
732
797
|
headerText,
|
|
733
798
|
paintHeader,
|