reslop 0.2.7 → 0.3.0

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 (158) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/README.md +16 -14
  3. package/bin/reslop.js +8 -5
  4. package/lib/agents/agents.js +504 -0
  5. package/lib/agents/allow.js +71 -0
  6. package/lib/agents/allow.json +16 -0
  7. package/lib/agents/models.js +460 -0
  8. package/lib/api.js +15 -12
  9. package/lib/cli.js +43 -68
  10. package/lib/{files.js → common/files.js} +48 -55
  11. package/lib/{find.js → common/find.js} +1 -1
  12. package/lib/common/format.js +101 -0
  13. package/lib/{utilities.js → common/process.js} +7 -95
  14. package/lib/common/tasks.json +27 -0
  15. package/lib/common/utilities.js +112 -0
  16. package/lib/dashboard/git.js +9 -16
  17. package/lib/dashboard/model.js +90 -49
  18. package/lib/dashboard/npm.js +17 -13
  19. package/lib/dashboard/tree.js +27 -30
  20. package/lib/{session → dashboard}/watch.js +36 -31
  21. package/lib/{workspace.js → dashboard/workspace.js} +23 -38
  22. package/lib/{deps.js → deps/deps.js} +31 -45
  23. package/lib/{deps-diff.js → deps/diff.js} +35 -25
  24. package/lib/{manifest.js → deps/manifest.js} +47 -30
  25. package/lib/diff/align.js +1 -2
  26. package/lib/diff/diff.js +2 -2
  27. package/lib/diff/display.js +29 -5
  28. package/lib/diff/inline.js +10 -14
  29. package/lib/diff/items.js +6 -4
  30. package/lib/diff/patch.js +39 -3
  31. package/lib/diff/unit.js +45 -16
  32. package/lib/{git-branches.js → git/branches.js} +53 -48
  33. package/lib/{git-deps.js → git/deps.js} +123 -96
  34. package/lib/git/git.js +71 -0
  35. package/lib/git/worktree.js +475 -0
  36. package/lib/highlight/bash.js +1 -2
  37. package/lib/highlight/csv.js +1 -2
  38. package/lib/highlight/dot.js +1 -2
  39. package/lib/{highlight.js → highlight/highlight.js} +42 -11
  40. package/lib/highlight/html.js +20 -18
  41. package/lib/highlight/json.js +1 -2
  42. package/lib/highlight/log.js +1 -2
  43. package/lib/highlight/txt.js +1 -2
  44. package/lib/{session → input}/actions.js +111 -48
  45. package/lib/{editor.js → input/editor.js} +7 -8
  46. package/lib/{keys.js → input/keys.js} +15 -43
  47. package/lib/input/line-edit.js +77 -0
  48. package/lib/input/menu-pick.js +70 -0
  49. package/lib/render/agents.js +640 -0
  50. package/lib/render/branches.js +4 -5
  51. package/lib/render/commits.js +54 -64
  52. package/lib/render/{dash-activity.js → dashboard/activity.js} +25 -40
  53. package/lib/render/{dash-blocks.js → dashboard/blocks.js} +131 -137
  54. package/lib/render/{dashboard.js → dashboard/dashboard.js} +25 -22
  55. package/lib/render/diff.js +166 -45
  56. package/lib/render/field.js +131 -0
  57. package/lib/render/files.js +9 -14
  58. package/lib/render/footer.js +383 -0
  59. package/lib/render/frame.js +123 -25
  60. package/lib/render/header.js +176 -0
  61. package/lib/render/log.js +319 -0
  62. package/lib/render/menu.js +60 -0
  63. package/lib/render/notes.js +16 -11
  64. package/lib/render/npm.js +322 -196
  65. package/lib/render/packages.js +9 -13
  66. package/lib/render/primitives.js +101 -15
  67. package/lib/render/render.js +7 -11
  68. package/lib/render/repos.js +13 -25
  69. package/lib/render/status.js +370 -0
  70. package/lib/render/status.json +85 -0
  71. package/lib/render/{dash-table.js → table.js} +13 -60
  72. package/lib/render/tiles.js +109 -48
  73. package/lib/render/unit.js +32 -13
  74. package/lib/{report-model.js → report/model.js} +4 -6
  75. package/lib/{report-parse.js → report/parse.js} +9 -14
  76. package/lib/{report-render.js → report/render.js} +46 -14
  77. package/lib/{report-run.js → report/run.js} +42 -30
  78. package/lib/{report-test.js → report/test.js} +56 -55
  79. package/lib/{report-tools.js → report/tools.js} +24 -33
  80. package/lib/review/agent-instructions.md +9 -0
  81. package/lib/review/format.js +426 -0
  82. package/lib/{review-merge.js → review/merge.js} +9 -8
  83. package/lib/review/review.js +190 -0
  84. package/lib/review/store.js +281 -0
  85. package/lib/review/templates.js +83 -0
  86. package/lib/runs/commands.js +14 -0
  87. package/lib/runs/logs.js +299 -0
  88. package/lib/runs/output.js +174 -0
  89. package/lib/runs/process.js +113 -0
  90. package/lib/{runs.js → runs/runs.js} +84 -36
  91. package/lib/runs/scripts.js +150 -0
  92. package/lib/session/agents/agents.js +958 -0
  93. package/lib/session/agents/job.js +320 -0
  94. package/lib/session/agents/jobs.js +355 -0
  95. package/lib/session/agents/options.js +153 -0
  96. package/lib/session/agents/plans.js +83 -0
  97. package/lib/session/branches.js +33 -39
  98. package/lib/session/change-actions.js +34 -100
  99. package/lib/session/commit.js +91 -52
  100. package/lib/session/compose.js +181 -145
  101. package/lib/session/dashboard.js +23 -22
  102. package/lib/session/dispatch.js +85 -21
  103. package/lib/session/files-pane.js +47 -26
  104. package/lib/session/find.js +16 -29
  105. package/lib/session/import.js +82 -0
  106. package/lib/session/items.js +8 -17
  107. package/lib/session/lifecycle.js +75 -72
  108. package/lib/session/load.js +2 -4
  109. package/lib/session/navigation.js +6 -3
  110. package/lib/session/npm/history.js +249 -0
  111. package/lib/session/npm/npm.js +815 -0
  112. package/lib/session/{packages.js → npm/packages.js} +21 -14
  113. package/lib/session/ops.js +125 -34
  114. package/lib/session/pointer.js +202 -10
  115. package/lib/session/prefs.js +112 -0
  116. package/lib/session/reads.js +109 -0
  117. package/lib/session/review.js +28 -10
  118. package/lib/session/run-panel.js +40 -0
  119. package/lib/{session.js → session/session.js} +156 -63
  120. package/lib/session/tasks.js +320 -65
  121. package/lib/session/templates.js +1 -1
  122. package/lib/session/terminal.js +94 -12
  123. package/lib/session/unit.js +35 -30
  124. package/lib/session/update.js +194 -6
  125. package/lib/session/view.js +65 -65
  126. package/lib/session/workspace.js +39 -78
  127. package/lib/{capabilities.js → source/capabilities.js} +3 -12
  128. package/lib/{github.js → source/github.js} +102 -32
  129. package/lib/{gitlab.js → source/gitlab.js} +87 -10
  130. package/lib/{remote.js → source/remote.js} +144 -40
  131. package/lib/source/review-threads.graphql +20 -0
  132. package/lib/source/source.js +74 -0
  133. package/lib/term/ansi.js +396 -0
  134. package/lib/{clipboard.js → term/clipboard.js} +3 -6
  135. package/lib/{select.js → term/select.js} +2 -2
  136. package/lib/term/themes/dark.json +122 -0
  137. package/lib/term/themes/light.json +122 -0
  138. package/lib/term/wide.txt +21 -0
  139. package/lib/{wrap.js → term/wrap.js} +4 -4
  140. package/package.json +2 -2
  141. package/lib/ansi.js +0 -545
  142. package/lib/detect.js +0 -34
  143. package/lib/diff/edit.js +0 -33
  144. package/lib/git-worktree.js +0 -455
  145. package/lib/git.js +0 -79
  146. package/lib/npm-commands.js +0 -522
  147. package/lib/render/chrome.js +0 -806
  148. package/lib/render/present.js +0 -19
  149. package/lib/render/report.js +0 -43
  150. package/lib/render/search.js +0 -25
  151. package/lib/review.js +0 -745
  152. package/lib/session/accessors.js +0 -26
  153. package/lib/session/edit-click.js +0 -57
  154. package/lib/session/npm.js +0 -452
  155. package/lib/session/progress.js +0 -41
  156. package/lib/source.js +0 -36
  157. package/lib/update.js +0 -192
  158. /package/lib/{git-reword-todo.js → git/reword-todo.js} +0 -0
@@ -1,806 +0,0 @@
1
- 'use strict';
2
-
3
- const ansi = require('../ansi.js');
4
- const keys = require('../keys.js');
5
- const files = require('../files.js');
6
- const primitives = require('./primitives.js');
7
- const blocks = require('./dash-blocks.js');
8
- const { npmStatus } = blocks;
9
-
10
- const { itemPath, REPO_TASKS_LABEL, isTasksEntry, fileTotals } = files;
11
- const { ACTION_IDS, actionLetter } = keys;
12
- const { DASH_IDS, DASH_HIDDEN, buttonWord } = keys;
13
- const { FILES_HIDDEN, FILES_GIT_DISABLED, BRANCHES_DISABLED } = keys;
14
- const { COMMITS_DISABLED, TASKS_DISABLED, DIFF_DISABLED, UNIT_DISABLED } = keys;
15
- const { NPM_DISABLED } = keys;
16
- const { diffGitDisabled, branchGitDisabled, commitGitDisabled } = keys;
17
- const { entryHasDiff, lineHasDiff, withoutMoves } = keys;
18
- const { CONFIRM, confirmPrefix, promptFromChoices } = keys;
19
- const { THEME, paint, visibleWidth } = ansi;
20
- const { truncateVisible, fg, RESET, EL, seq } = ansi;
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
-
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
- });
32
-
33
- const BUSY_PROGRESS = [
34
- 'pulling',
35
- 'pushing',
36
- 'loading',
37
- 'checking npm',
38
- 'updating reslop',
39
- 'npm i',
40
- 'npm uninstall',
41
- 'npm audit fix',
42
- 'running',
43
- 'checking out',
44
- 'creating branch',
45
- 'rebasing',
46
- 'dropping',
47
- 'committing',
48
- 'rewording',
49
- 'force pushing',
50
- ];
51
-
52
- const formatBusyStatus = (status, frame = 0) => {
53
- const kind = `${status ?? ''}`;
54
- if (!BUSY_PROGRESS.includes(kind)) return kind;
55
- const i = Math.abs(frame) % BUSY_BAR.length;
56
- return `${kind} ${BUSY_BAR[i]}`;
57
- };
58
-
59
- const INFO_STATUS = [
60
- 'copied',
61
- 'staged',
62
- 'unstaged',
63
- 'not staged',
64
- 'dropped',
65
- 'already staged',
66
- 'read only',
67
- 'transitive',
68
- 'nothing to review',
69
- 'nothing to commit',
70
- 'unified',
71
- 'mixed',
72
- 'side-by-side',
73
- 'dark',
74
- 'light',
75
- 'saved',
76
- 'reloaded',
77
- 'not a diff block',
78
- 'file scope',
79
- 'diff mode',
80
- 'committed',
81
- 'amended',
82
- 'reworded',
83
- 'fixup',
84
- 'updated',
85
- 'updating reslop',
86
- 'loading',
87
- 'checking npm',
88
- 'npm i',
89
- 'npm uninstall',
90
- 'npm audit fix',
91
- 'pulling',
92
- 'pushing',
93
- 'pulled',
94
- 'pushed',
95
- 'force pushing',
96
- 'force pushed',
97
- 'checking out',
98
- 'creating branch',
99
- 'rebasing',
100
- 'dropping',
101
- 'committing',
102
- 'checked out',
103
- 'created',
104
- 'empty branch name',
105
- 'empty commit message',
106
- ];
107
- const INFO_PREFIX = ['checked out ', 'created ', 'rebased onto ', 'dropped '];
108
-
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
- };
118
-
119
- const BUTTON_LEAD = ' ';
120
- const BUTTON_GAP = ' ';
121
- const BUTTON_EDGE = 1;
122
- const BACK_ARROW = '🢐';
123
- const BACK_KEY = 'esc';
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('');
153
-
154
- const pathSegments = (rel) => {
155
- const segs = [];
156
- if (!rel) return segs;
157
- const bits = rel.split('/');
158
- for (let i = 0; i < bits.length; i++) {
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 });
162
- }
163
- return segs;
164
- };
165
-
166
- const headerScreen = (view) => {
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
- }
178
- if (pane === 'commits') {
179
- return view.commitView === 'full' ? 'commits full' : 'commits brief';
180
- }
181
- if (pane === 'files') {
182
- return isTasksEntry(selectedFile(view)) ? REPO_TASKS_LABEL : '';
183
- }
184
- return isTaskView(view) ? REPO_TASKS_LABEL : '';
185
- };
186
-
187
- const headerTarget = (view) => {
188
- if (view.pane === 'files') {
189
- const entry = selectedFile(view);
190
- if (!entry || isTasksEntry(entry)) return '';
191
- return entry.path;
192
- }
193
- if (view.pane === 'unit') return view.reviewPath ?? '';
194
- if (isTaskView(view)) return '';
195
- return itemPath(view.item);
196
- };
197
-
198
- const headerLead = (view) => {
199
- const screen = headerScreen(view);
200
- const label = screen || headerTarget(view);
201
- const repo = view.repoName || '';
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 };
210
- };
211
-
212
- const headerText = (view) => {
213
- const { pieces, label } = headerLead(view);
214
- return `${piecesText(pieces)}${label}`;
215
- };
216
-
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);
223
- };
224
-
225
- const paintHeaderRoles = (pieces) => {
226
- let out = `${seq(THEME.headerChromeFg, THEME.headerBg)}${EL}`;
227
- for (const piece of pieces) {
228
- out += `${fg(HEADER_ROLE_FG[piece.role])}${piece.text}`;
229
- }
230
- return out;
231
- };
232
-
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;
254
- };
255
-
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}`;
275
- };
276
-
277
- const backFooterId = (view) =>
278
- view.pane === 'npm' && view.npmView ? 'npmStop' : 'back';
279
-
280
- const backHintMark = () => BACK_ARROW;
281
-
282
- const buttonGap = (index) => (index ? BUTTON_GAP : BUTTON_LEAD);
283
-
284
- const hintBody = (mark) => `${mark}${BACK_KEY}`;
285
-
286
- const buttonLabel = (id, short) => (short ? actionLetter(id) : buttonWord(id));
287
-
288
- const paintBackHint = (color, mark) => {
289
- const bgRgb = THEME.buttonBg;
290
- const rest = hintBody(mark).slice(mark.length);
291
- const arrow = paint(mark, THEME.buttonHotFg, bgRgb, color, true);
292
- return arrow + paint(rest, THEME.buttonFg, bgRgb, color);
293
- };
294
-
295
- const paintMarkedWord = (label, letter, color, mark) => {
296
- const at = label.indexOf(letter);
297
- if (at < 0) {
298
- return paint(label, mark.missFg, mark.bgRgb, color, mark.missBold);
299
- }
300
- const before = label.slice(0, at);
301
- const hot = label.slice(at, at + letter.length);
302
- const after = label.slice(at + letter.length);
303
- let out = paint(before, mark.fgRgb, mark.bgRgb, color);
304
- out += paint(hot, mark.hotFg, mark.bgRgb, color, true);
305
- return out + paint(after, mark.fgRgb, mark.bgRgb, color);
306
- };
307
-
308
- const footerIds = (hidden, extra, backId, globals, tail, after) => {
309
- const ids = backId ? [backId] : [];
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);
319
- }
320
- }
321
- for (const id of after) push(id);
322
- return ids;
323
- };
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
-
409
- const layoutButtons = (width, options = {}) => {
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);
416
- const build = (short) => {
417
- const hits = [];
418
- const parts = [];
419
- let x = 0;
420
- for (const id of ids) {
421
- const part = buttonPiece(id, parts.length, short, backId, mark);
422
- const w = visibleWidth(part.piece);
423
- if (x + w > limit && parts.length) break;
424
- const off = disabled.includes(id);
425
- if (!off) hits.push({ id, x0: x, x1: x + w });
426
- parts.push({ ...part, disabled: off });
427
- x += w;
428
- }
429
- return { hits, parts };
430
- };
431
- if (compact) return build(true);
432
- const words = build(false);
433
- if (words.parts.length === ids.length) return words;
434
- return build(true);
435
- };
436
-
437
- const paintButtons = (layout, width, color) => {
438
- const fgRgb = THEME.buttonFg;
439
- const bgRgb = THEME.buttonBg;
440
- let out = '';
441
- let used = 0;
442
- for (let i = 0; i < layout.parts.length; i++) {
443
- const part = layout.parts[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);
450
- out += paint(gap, fgRgb, bgRgb, color);
451
- if (part.back) out += paintBackHint(color, part.mark);
452
- else if (part.disabled) out += paint(part.label, fgRgb, bgRgb, color);
453
- else out += paintMarkedWord(part.label, part.letter, color, BUTTON_MARK);
454
- used += visibleWidth(part.piece);
455
- }
456
- return out + fill(width - used, fgRgb, bgRgb, color);
457
- };
458
-
459
- const isInfoStatus = (status) =>
460
- INFO_STATUS.includes(status) ||
461
- INFO_PREFIX.some((prefix) => status.startsWith(prefix));
462
-
463
- const joinStatus = (parts) => parts.filter(Boolean).join(' ');
464
-
465
- const countsKind = (sourceKind, revShort) => {
466
- if (sourceKind === 'pr' || sourceKind === 'mr') return sourceKind;
467
- return revShort ? 'commit' : 'worktree';
468
- };
469
-
470
- const isWorktree = (view) =>
471
- countsKind(view.sourceKind, view.revShort) === 'worktree';
472
-
473
- const notesLine = (counts) => {
474
- const done = counts.tasksDone ?? 0;
475
- const total = counts.tasks ?? 0;
476
- return `tasks ${done}/${total}`;
477
- };
478
-
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
- }
487
- const counts = view.counts ?? {};
488
- const kind = countsKind(view.sourceKind, view.revShort);
489
- if (kind === 'worktree') return branchLabel(view.branch);
490
- if (kind === 'commit') {
491
- return `commit ${view.revShort} ${counts.commit ?? 0}`;
492
- }
493
- const label = view.sourceLabel || kind;
494
- return `${kind} ${label} ${counts.pr ?? 0}`;
495
- };
496
-
497
- const STATUS_QUIET = new Set(['npm', 'packages', 'branches']);
498
-
499
- const statusLeft = (view) => {
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])}`;
503
- };
504
-
505
- const statusDelta = (total) => {
506
- const stagedAdd = total.stagedAdded ?? 0;
507
- const unstagedAdd = total.unstagedAdded ?? 0;
508
- const stagedDel = total.stagedRemoved ?? 0;
509
- const unstagedDel = total.unstagedRemoved ?? 0;
510
- const hasDelta =
511
- stagedAdd || unstagedAdd || stagedDel || unstagedDel || total.added;
512
- if (!hasDelta && !total.removed) return '';
513
- const plus = `+${stagedAdd}/${stagedAdd + unstagedAdd}`;
514
- const minus = `-${stagedDel}/${stagedDel + unstagedDel}`;
515
- return `${plus} ${minus}`;
516
- };
517
-
518
- const DIFF_QUIET = new Set(['npm', 'packages', 'tasks', 'branches']);
519
-
520
- const statusStats = (view) => {
521
- const counts = view.counts ?? {};
522
- const staged = counts.staged ?? 0;
523
- const total = fileTotals(view.files ?? []);
524
- const hasGit =
525
- total.remaining || total.staged || total.added || total.removed;
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]);
532
- };
533
-
534
- const statusLayout = (view, status, width) => {
535
- const msg = formatBusyStatus(status, view.progressFrame ?? 0);
536
- const isError = msg && !isInfoStatus(status);
537
- const msgFg = isError ? THEME.errorFg : THEME.chromeFg;
538
- const stats = statusStats(view);
539
- const right = stats ? `${stats} ` : '';
540
- const rightW = visibleWidth(right);
541
- const msgW = visibleWidth(msg);
542
- const side = msg ? 1 : 0;
543
- const room = Math.max(0, width - rightW - msgW - side * 2);
544
- const clipped = truncateVisible(statusLeft(view), room);
545
- const leftW = visibleWidth(clipped);
546
- const extra = Math.max(0, width - leftW - rightW - msgW);
547
- const padLeft = msg ? Math.floor(extra / 2) : extra;
548
- const padRight = msg ? extra - padLeft : 0;
549
- return { clipped, msg, msgFg, right, padLeft, padRight };
550
- };
551
-
552
- const choiceHits = (choices, prefix, y) => {
553
- let x = visibleWidth(prefix);
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 };
557
- x += w;
558
- return hit;
559
- });
560
- };
561
-
562
- const paintChoicePrompt = (choices, width, color, prefix) => {
563
- const bgRgb = THEME.chromeBg;
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);
568
- for (const choice of choices) {
569
- const label = `${choice.lead}${choice.label}`;
570
- out += paintMarkedWord(label, choice.letter, color, QUIT_MARK);
571
- }
572
- return out + fill(pad, THEME.mutedFg, bgRgb, color);
573
- };
574
-
575
- const paintStatusLeft = (text, color) => {
576
- if (!color) return text;
577
- const bgRgb = THEME.chromeBg;
578
- const found = text.match(OLD_LOGS);
579
- if (found) {
580
- const token = found[0];
581
- const before = text.slice(0, found.index);
582
- const after = text.slice(found.index + token.length);
583
- return (
584
- paintStatusLeft(before, color) +
585
- paint(token, THEME.shaFg, bgRgb, color) +
586
- paintStatusLeft(after, color)
587
- );
588
- }
589
- let out = '';
590
- let last = 0;
591
- for (const match of text.matchAll(STATUS_MARK)) {
592
- const token = match[0];
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;
597
- }
598
- return out + paint(text.slice(last), THEME.mutedFg, bgRgb, color);
599
- };
600
-
601
- const paintBranchHead = (text, branch, color) => {
602
- const name = branchLabel(branch);
603
- if (!name || !text.startsWith(' ')) return null;
604
- const shown = text.startsWith(` ${name}`) ? name : text.slice(1);
605
- if (!shown || !name.startsWith(shown)) return null;
606
- const rest = text.slice(1 + shown.length);
607
- const bgRgb = THEME.chromeBg;
608
- return (
609
- paint(' ', THEME.mutedFg, bgRgb, color) +
610
- paint(shown, THEME.buttonHotFg, bgRgb, color, true) +
611
- paintStatusLeft(rest, color)
612
- );
613
- };
614
-
615
- const commitSaveHint = (view) =>
616
- view.mode === 'compose' &&
617
- view.commitView === 'full' &&
618
- view.compose?.kind === 'commit';
619
-
620
- const paintSaveHintBar = (width, color) => {
621
- const bgRgb = THEME.buttonBg;
622
- const plain = `${BUTTON_LEAD}${SAVE_HINT.replaceAll('**', '')}`;
623
- const pad = Math.max(0, width - visibleWidth(plain));
624
- if (!color) return plain + ' '.repeat(pad);
625
- let out = paint(BUTTON_LEAD, THEME.buttonFg, bgRgb, color);
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);
631
- }
632
- return out + fill(pad, THEME.buttonFg, bgRgb, color);
633
- };
634
-
635
- const paintFooter = (view, layout, width, color) => {
636
- if (commitSaveHint(view)) return paintSaveHintBar(width, color);
637
- return paintButtons(layout, width, color);
638
- };
639
-
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);
658
- };
659
-
660
- const paintStatusLine = (view, status, width, color) => {
661
- if (view.find) return paintFindStatus(view, width, color);
662
- const confirm = CONFIRM[view.mode];
663
- if (confirm) {
664
- const prefix = confirmPrefix(confirm, view);
665
- return paintChoicePrompt(confirm.choices, width, color, prefix);
666
- }
667
- const layout = statusLayout(view, status, width);
668
- const leftGap = ' '.repeat(layout.padLeft);
669
- const rightGap = ' '.repeat(layout.padRight);
670
- if (!color) {
671
- return layout.clipped + leftGap + layout.msg + rightGap + layout.right;
672
- }
673
- const bgRgb = THEME.chromeBg;
674
- const head = isWorktree(view)
675
- ? paintBranchHead(layout.clipped, view.branch, color)
676
- : null;
677
- const left = head ?? paintStatusLeft(layout.clipped, color);
678
- return (
679
- left +
680
- paint(leftGap, THEME.mutedFg, bgRgb, color) +
681
- paint(layout.msg, layout.msgFg, bgRgb, color) +
682
- paint(rightGap, THEME.mutedFg, bgRgb, color) +
683
- paintStatusLeft(layout.right, color)
684
- );
685
- };
686
-
687
- const statusChoiceHits = (view, y) => {
688
- const spec = CONFIRM[view.mode];
689
- if (!spec) return [];
690
- return choiceHits(spec.choices, confirmPrefix(spec, view), y);
691
- };
692
-
693
- const statusBranchHit = (view, width, y) => {
694
- if (view.find || CONFIRM[view.mode]) return null;
695
- const name = branchLabel(view.branch);
696
- if (!name || !isWorktree(view)) return null;
697
- const layout = statusLayout(view, view.status ?? '', width);
698
- const at = layout.clipped.indexOf(name);
699
- if (at < 0) return null;
700
- return { id: 'branch', y, x0: at, x1: at + visibleWidth(name) };
701
- };
702
-
703
- const withHidden = (ids, more) => [...ids, ...more];
704
-
705
- const hiddenActions = (view) => {
706
- const { pane } = view;
707
- if (pane === 'dashboard' || pane === 'repos') return DASH_HIDDEN;
708
- if (pane === 'files') return FILES_HIDDEN;
709
- if (pane === 'unit') return withHidden(UNIT_DISABLED, ['feedback', 'tasks']);
710
- if (pane === 'branches') return BRANCHES_DISABLED;
711
- if (pane === 'commits') return COMMITS_DISABLED;
712
- if (pane === 'npm') return NPM_DISABLED;
713
- if (pane === 'tasks' || isTaskView(view)) {
714
- return withHidden(TASKS_DISABLED, ['prev', 'next']);
715
- }
716
- return withHidden(DIFF_DISABLED, ['feedback', 'tasks']);
717
- };
718
-
719
- const dimFooterActions = (view) => {
720
- const pane = view.pane ?? 'diff';
721
- if (pane === 'files') {
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);
734
- if (pane === 'branches') return branchGitDisabled(selectedBranch(view));
735
- if (pane === 'commits') {
736
- return commitGitDisabled({
737
- ...selectedCommit(view),
738
- canCommit: (view.counts?.staged ?? 0) > 0,
739
- unstagedCurrent: !view.rev && !view.revShort,
740
- });
741
- }
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
- }
751
- return [];
752
- }
753
- if (pane !== 'npm') return [];
754
- if (view.npmView) return view.npmRunning ? ['npmRerun'] : [];
755
- return view.npmLogs ? [] : ['npmLogs'];
756
- };
757
-
758
- const GIT_FOOTER = ['add', 'unstage', 'revert', 'ignore'];
759
-
760
- const extraFooterActions = (view) => {
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'];
772
- }
773
- if (pane === 'npm') {
774
- if (view.npmView) return ['npmVerbose', 'npmRerun'];
775
- return ['npmEdit', 'npmNew', 'npmDrop', 'npmLogs'];
776
- }
777
- if (pane === 'packages') {
778
- return ['packageNew', 'packageDrop', 'packageWanted', 'packageLatest'];
779
- }
780
- if (pane === 'tasks' || isTaskView(view)) return ['drop', 'check'];
781
- return GIT_FOOTER;
782
- };
783
-
784
- const footerLayout = (view, width) =>
785
- layoutButtons(width, {
786
- hidden: hiddenActions(view),
787
- extra: extraFooterActions(view),
788
- disabled: dimFooterActions(view),
789
- backId: backFooterId(view),
790
- mark: backHintMark(view),
791
- globals: view.pane === 'repos' ? [] : DASH_IDS,
792
- tail: false,
793
- });
794
-
795
- module.exports = {
796
- formatBusyStatus,
797
- headerText,
798
- paintHeader,
799
- layoutButtons,
800
- footerLayout,
801
- paintFooter,
802
- footerHits,
803
- paintStatusLine,
804
- statusChoiceHits,
805
- statusBranchHit,
806
- };