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.
Files changed (114) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/LICENSE +1 -1
  3. package/README.md +11 -11
  4. package/bin/reslop.js +3 -8
  5. package/lib/ansi.js +72 -19
  6. package/lib/api.js +1 -1
  7. package/lib/capabilities.js +8 -8
  8. package/lib/cli.js +61 -75
  9. package/lib/clipboard.js +39 -47
  10. package/lib/dashboard/git.js +128 -0
  11. package/lib/dashboard/model.js +357 -0
  12. package/lib/dashboard/npm.js +290 -0
  13. package/lib/dashboard/tree.js +278 -0
  14. package/lib/deps-diff.js +197 -402
  15. package/lib/deps.js +113 -240
  16. package/lib/detect.js +8 -18
  17. package/lib/diff/align.js +41 -113
  18. package/lib/diff/diff.js +12 -43
  19. package/lib/diff/display.js +25 -36
  20. package/lib/diff/edit.js +6 -20
  21. package/lib/diff/inline.js +76 -144
  22. package/lib/diff/items.js +33 -47
  23. package/lib/diff/parse.js +28 -56
  24. package/lib/diff/patch.js +12 -33
  25. package/lib/diff/unit.js +79 -178
  26. package/lib/editor.js +52 -69
  27. package/lib/files.js +79 -91
  28. package/lib/find.js +61 -0
  29. package/lib/git-branches.js +254 -416
  30. package/lib/git-deps.js +148 -272
  31. package/lib/git-reword-todo.js +5 -13
  32. package/lib/git-worktree.js +161 -214
  33. package/lib/git.js +21 -49
  34. package/lib/github.js +67 -181
  35. package/lib/gitlab.js +40 -90
  36. package/lib/highlight/bash.js +55 -177
  37. package/lib/highlight/core.js +155 -141
  38. package/lib/highlight/css.js +25 -58
  39. package/lib/highlight/csv.js +19 -37
  40. package/lib/highlight/dart.js +72 -213
  41. package/lib/highlight/dot.js +14 -14
  42. package/lib/highlight/html.js +42 -63
  43. package/lib/highlight/js.js +177 -506
  44. package/lib/highlight/json.js +17 -37
  45. package/lib/highlight/log.js +19 -25
  46. package/lib/highlight/txt.js +7 -10
  47. package/lib/highlight.js +17 -46
  48. package/lib/keys.js +75 -83
  49. package/lib/manifest.js +67 -126
  50. package/lib/npm-commands.js +120 -169
  51. package/lib/remote.js +140 -190
  52. package/lib/render/branches.js +63 -61
  53. package/lib/render/chrome.js +412 -347
  54. package/lib/render/commits.js +160 -361
  55. package/lib/render/dash-activity.js +212 -0
  56. package/lib/render/dash-blocks.js +496 -0
  57. package/lib/render/dash-table.js +295 -0
  58. package/lib/render/dashboard.js +62 -0
  59. package/lib/render/diff.js +123 -258
  60. package/lib/render/files.js +60 -86
  61. package/lib/render/frame.js +82 -117
  62. package/lib/render/notes.js +150 -210
  63. package/lib/render/npm.js +213 -177
  64. package/lib/render/packages.js +93 -0
  65. package/lib/render/present.js +4 -11
  66. package/lib/render/primitives.js +77 -172
  67. package/lib/render/render.js +5 -14
  68. package/lib/render/report.js +43 -0
  69. package/lib/render/repos.js +226 -0
  70. package/lib/render/search.js +25 -0
  71. package/lib/render/tiles.js +355 -0
  72. package/lib/render/unit.js +15 -31
  73. package/lib/report-model.js +110 -33
  74. package/lib/report-parse.js +56 -37
  75. package/lib/report-render.js +102 -59
  76. package/lib/report-run.js +87 -77
  77. package/lib/report-test.js +221 -99
  78. package/lib/report-tools.js +96 -154
  79. package/lib/review-merge.js +33 -41
  80. package/lib/review.js +69 -129
  81. package/lib/runs.js +225 -0
  82. package/lib/select.js +53 -91
  83. package/lib/session/accessors.js +14 -21
  84. package/lib/session/actions.js +189 -65
  85. package/lib/session/branches.js +164 -222
  86. package/lib/session/change-actions.js +49 -119
  87. package/lib/session/commit.js +298 -352
  88. package/lib/session/compose.js +572 -715
  89. package/lib/session/dashboard.js +530 -0
  90. package/lib/session/dispatch.js +197 -171
  91. package/lib/session/files-pane.js +92 -45
  92. package/lib/session/find.js +94 -0
  93. package/lib/session/items.js +6 -17
  94. package/lib/session/lifecycle.js +420 -466
  95. package/lib/session/load.js +1 -14
  96. package/lib/session/navigation.js +4 -2
  97. package/lib/session/npm.js +403 -420
  98. package/lib/session/ops.js +5 -13
  99. package/lib/session/packages.js +143 -0
  100. package/lib/session/pointer.js +30 -14
  101. package/lib/session/progress.js +39 -30
  102. package/lib/session/review.js +1 -3
  103. package/lib/session/tasks.js +277 -236
  104. package/lib/session/templates.js +51 -59
  105. package/lib/session/terminal.js +85 -132
  106. package/lib/session/view.js +59 -80
  107. package/lib/session/watch.js +201 -212
  108. package/lib/session/workspace.js +455 -0
  109. package/lib/session.js +95 -161
  110. package/lib/update.js +44 -111
  111. package/lib/utilities.js +46 -88
  112. package/lib/workspace.js +166 -0
  113. package/lib/wrap.js +29 -55
  114. package/package.json +1 -1
@@ -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, BRANCH_ACTIONS, COMMIT_ACTIONS, actionLetter } = keys;
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 HEADER_ROLE_FG = {
21
- repo: THEME.headerRepoFg,
22
- dir: THEME.headerDirFg,
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 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`;
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
- const LOGO = '👁️';
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
- 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' });
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.pane ?? 'diff';
149
- if (pane === 'branches') return 'branches';
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
- const mode = view.commitView === 'full' ? 'full' : 'brief';
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
- const files = view.files ?? [];
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
- if (view.item && view.item.origin === 'task') return REPO_TASKS_LABEL;
162
- return '';
184
+ return isTaskView(view) ? REPO_TASKS_LABEL : '';
163
185
  };
164
186
 
165
187
  const headerTarget = (view) => {
166
- const pane = view.pane ?? 'diff';
167
- if (pane === 'files') {
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 (!view.item || view.item.origin === 'task') return '';
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 headerLabel = (view) => headerScreen(view) || headerTarget(view);
178
-
179
- const headerSep = (view) => (headerScreen(view) ? ': ' : '/');
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
- if (repo && label) return ` ${LOGO} reslop: ${repo}${headerSep(view)}`;
184
- if (repo) return ` ${LOGO} reslop: ${repo} `;
185
- return ` ${LOGO} reslop: `;
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 = headerLabel(view);
190
- return `${headerPrefix(view, label)}${label}`;
213
+ const { pieces, label } = headerLead(view);
214
+ return `${piecesText(pieces)}${label}`;
191
215
  };
192
216
 
193
- const headerPieces = (view, width) => {
194
- const repo = view.repoName || '';
195
- const path = headerLabel(view);
196
- const right = ' ';
197
- const chrome = ` ${LOGO} reslop: `;
198
- const prefixW = visibleWidth(headerPrefix(view, path));
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 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}`;
225
+ const paintHeaderRoles = (pieces) => {
226
+ let out = `${seq(THEME.headerChromeFg, THEME.headerBg)}${EL}`;
231
227
  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}`;
228
+ out += `${fg(HEADER_ROLE_FG[piece.role])}${piece.text}`;
237
229
  }
238
- return `${out}${RESET}`;
230
+ return out;
239
231
  };
240
232
 
241
- const BUTTON_MARK = {
242
- bgRgb: THEME.buttonBg,
243
- fgRgb: THEME.buttonFg,
244
- hotFg: THEME.buttonHotFg,
245
- missFg: THEME.buttonHotFg,
246
- missBold: true,
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 QUIT_MARK = {
250
- bgRgb: THEME.chromeBg,
251
- fgRgb: THEME.mutedFg,
252
- hotFg: THEME.warnFg,
253
- missFg: THEME.mutedFg,
254
- missBold: false,
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
- const pane = view.pane ?? 'diff';
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 = (view) => {
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 hintBody = (mark) => {
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 buttonLabel = (id, short, backId, mark) => {
276
- if (id === backId) return hintBody(mark);
277
- if (short) return actionLetter(id);
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 body = hintBody(mark);
290
+ const rest = hintBody(mark).slice(mark.length);
284
291
  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;
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
- if (after) out += paint(after, mark.fgRgb, mark.bgRgb, color);
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
- 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);
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 { disabled: disabledIds = [], backId = '', mark = '' } = options;
321
- const hint = mark || BACK_ARROW;
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 back = id === backId;
330
- 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}`;
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 = disabledIds.includes(id);
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, width: x };
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
- const gap = buttonGap(i);
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
- if (used < width) {
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
- 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
- };
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
- if (revShort) return 'commit';
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 feedback = counts.feedback ?? 0;
402
- const tasks = counts.tasks ?? 0;
403
- const code = counts.code ?? 0;
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
- const n = counts.commit ?? 0;
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} ${n}`;
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
- const body = joinStatus([statusLead(view), notesLine(view.counts ?? {})]);
422
- if (!body) return '';
423
- return ` ${body}`;
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 joinStatus([plus, minus]);
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 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]);
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 shown = formatBusyStatus(status, view.progressFrame ?? 0);
455
- const msg = shown;
456
- let msgFg = THEME.chromeFg;
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
- 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 };
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
- return hits;
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 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);
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, at);
526
- const after = text.slice(at + token.length);
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 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);
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 head = ` ${name}`;
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 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
- };
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}${saveHintText()}`;
589
- if (!color) {
590
- const pad = Math.max(0, width - visibleWidth(plain));
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
- 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);
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
- if (commitSaveHint(view)) return [];
609
- return layout.hits;
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 worktree = countsKind(view.sourceKind, view.revShort) === 'worktree';
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
- const prefix = confirmPrefix(spec, view);
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
- 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;
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 selectedBranch = (view) => (view.branches ?? [])[view.branchCursor ?? 0];
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.pane ?? 'diff';
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 (view.item && view.item.origin === 'task') return TASKS_DISABLED;
671
- return DIFF_DISABLED;
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 files = view.files ?? [];
678
- const entry = files[view.fileCursor ?? 0];
679
- if (!entry || entry.kind !== 'tasks') return [];
680
- return FILES_GIT_DISABLED;
681
- }
682
- if (pane === 'diff' || pane === 'unit') return diffGitDisabled(view.item);
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
- ...entry,
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 === 'npm' && view.npmView) {
695
- if (view.npmRunning) return ['npmRerun'];
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 === 'npm' && !view.npmLogs) return ['npmLogs'];
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.pane ?? 'diff';
704
- if (pane === 'files') {
705
- const scope = view.fileScope === 'file' ? 'diff' : 'file';
706
- return ['npm', scope, 'commit', 'pull', 'push'];
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
- return [];
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,