reslop 0.2.5 → 0.2.6

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