reslop 0.2.6 → 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 +27 -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 +61 -66
  10. package/lib/{files.js → common/files.js} +51 -52
  11. package/lib/common/find.js +61 -0
  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 +117 -51
  18. package/lib/dashboard/npm.js +133 -32
  19. package/lib/dashboard/tree.js +27 -30
  20. package/lib/{session → dashboard}/watch.js +41 -31
  21. package/lib/dashboard/workspace.js +151 -0
  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 -95
  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 +240 -71
  45. package/lib/{editor.js → input/editor.js} +7 -8
  46. package/lib/{keys.js → input/keys.js} +36 -39
  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/dashboard/blocks.js +490 -0
  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 +22 -8
  58. package/lib/render/footer.js +383 -0
  59. package/lib/render/frame.js +127 -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 +29 -13
  64. package/lib/render/npm.js +322 -196
  65. package/lib/render/packages.js +89 -0
  66. package/lib/render/primitives.js +109 -16
  67. package/lib/render/render.js +7 -11
  68. package/lib/render/repos.js +214 -0
  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} +19 -61
  72. package/lib/render/tiles.js +203 -47
  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 +36 -44
  98. package/lib/session/change-actions.js +34 -100
  99. package/lib/session/commit.js +122 -56
  100. package/lib/session/compose.js +197 -146
  101. package/lib/session/dashboard.js +183 -37
  102. package/lib/session/dispatch.js +120 -22
  103. package/lib/session/files-pane.js +60 -24
  104. package/lib/session/find.js +81 -0
  105. package/lib/session/import.js +82 -0
  106. package/lib/session/items.js +8 -17
  107. package/lib/session/lifecycle.js +96 -66
  108. package/lib/session/load.js +2 -4
  109. package/lib/session/navigation.js +8 -3
  110. package/lib/session/npm/history.js +249 -0
  111. package/lib/session/npm/npm.js +815 -0
  112. package/lib/session/npm/packages.js +150 -0
  113. package/lib/session/ops.js +125 -34
  114. package/lib/session/pointer.js +216 -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} +166 -59
  120. package/lib/session/tasks.js +321 -66
  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 +72 -59
  126. package/lib/session/workspace.js +416 -0
  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 -505
  142. package/lib/detect.js +0 -34
  143. package/lib/diff/edit.js +0 -33
  144. package/lib/git-worktree.js +0 -437
  145. package/lib/git.js +0 -79
  146. package/lib/npm-commands.js +0 -522
  147. package/lib/render/chrome.js +0 -628
  148. package/lib/render/dash-blocks.js +0 -245
  149. package/lib/render/present.js +0 -19
  150. package/lib/render/report.js +0 -43
  151. package/lib/review.js +0 -744
  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,628 +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
-
8
- const { itemPath, REPO_TASKS_LABEL, isTasksEntry, fileTotals } = files;
9
- const { ACTION_IDS, actionLetter } = keys;
10
- const { DASH_IDS, DASH_HIDDEN, buttonWord } = keys;
11
- const { FILES_HIDDEN, FILES_GIT_DISABLED, BRANCHES_DISABLED } = keys;
12
- const { COMMITS_DISABLED, TASKS_DISABLED, DIFF_DISABLED, UNIT_DISABLED } = keys;
13
- const { NPM_DISABLED } = keys;
14
- const { diffGitDisabled, branchGitDisabled, commitGitDisabled } = keys;
15
- const { CONFIRM, confirmPrefix, promptFromChoices } = keys;
16
- const { THEME, paint, visibleWidth } = ansi;
17
- const { truncateVisible, fg, RESET, EL, seq } = ansi;
18
- const { fill, branchLabel, isTaskView } = primitives;
19
-
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
- });
25
-
26
- const BUSY_PROGRESS = [
27
- 'pulling',
28
- 'pushing',
29
- 'loading',
30
- 'checking npm',
31
- 'updating reslop',
32
- 'npm i',
33
- 'npm uninstall',
34
- 'npm audit fix',
35
- 'running',
36
- 'checking out',
37
- 'creating branch',
38
- 'rebasing',
39
- 'dropping',
40
- 'committing',
41
- 'rewording',
42
- 'force pushing',
43
- ];
44
-
45
- const formatBusyStatus = (status, frame = 0) => {
46
- const kind = `${status ?? ''}`;
47
- if (!BUSY_PROGRESS.includes(kind)) return kind;
48
- const i = Math.abs(frame) % BUSY_BAR.length;
49
- return `${kind} ${BUSY_BAR[i]}`;
50
- };
51
-
52
- const INFO_STATUS = [
53
- 'copied',
54
- 'staged',
55
- 'unstaged',
56
- 'not staged',
57
- 'dropped',
58
- 'already staged',
59
- 'read only',
60
- 'nothing to review',
61
- 'nothing to commit',
62
- 'unified',
63
- 'mixed',
64
- 'side-by-side',
65
- 'dark',
66
- 'light',
67
- 'saved',
68
- 'reloaded',
69
- 'not a diff block',
70
- 'file scope',
71
- 'diff mode',
72
- 'committed',
73
- 'amended',
74
- 'reworded',
75
- 'fixup',
76
- 'updated',
77
- 'updating reslop',
78
- 'loading',
79
- 'checking npm',
80
- 'npm i',
81
- 'npm uninstall',
82
- 'npm audit fix',
83
- 'pulling',
84
- 'pushing',
85
- 'pulled',
86
- 'pushed',
87
- 'force pushing',
88
- 'force pushed',
89
- 'checking out',
90
- 'creating branch',
91
- 'rebasing',
92
- 'dropping',
93
- 'committing',
94
- 'checked out',
95
- 'created',
96
- 'empty branch name',
97
- 'empty commit message',
98
- ];
99
- const INFO_PREFIX = ['checked out ', 'created ', 'rebased onto ', 'dropped '];
100
-
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
- };
110
-
111
- const BUTTON_LEAD = ' ';
112
- const BUTTON_GAP = ' ';
113
- const BUTTON_EDGE = 1;
114
- const BACK_ARROW = '🢐';
115
- const STOP_MARK = '⊗';
116
- const BACK_KEY = 'esc';
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('');
146
-
147
- const pathSegments = (rel) => {
148
- const segs = [];
149
- if (!rel) return segs;
150
- const bits = rel.split('/');
151
- for (let i = 0; i < bits.length; i++) {
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 });
155
- }
156
- return segs;
157
- };
158
-
159
- const headerScreen = (view) => {
160
- const { pane } = view;
161
- if (
162
- pane === 'dashboard' ||
163
- pane === 'branches' ||
164
- pane === 'npm' ||
165
- pane === 'tasks'
166
- ) {
167
- return pane;
168
- }
169
- if (pane === 'commits') {
170
- return view.commitView === 'full' ? 'commits full' : 'commits brief';
171
- }
172
- if (pane === 'files') {
173
- return isTasksEntry(selectedFile(view)) ? REPO_TASKS_LABEL : '';
174
- }
175
- return isTaskView(view) ? REPO_TASKS_LABEL : '';
176
- };
177
-
178
- const headerTarget = (view) => {
179
- if (view.pane === 'files') {
180
- const entry = selectedFile(view);
181
- if (!entry || isTasksEntry(entry)) return '';
182
- return entry.path;
183
- }
184
- if (view.pane === 'unit') return view.reviewPath ?? '';
185
- if (isTaskView(view)) return '';
186
- return itemPath(view.item);
187
- };
188
-
189
- const headerLead = (view) => {
190
- const screen = headerScreen(view);
191
- const label = screen || headerTarget(view);
192
- const repo = view.repoName || '';
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 };
201
- };
202
-
203
- const headerText = (view) => {
204
- const { pieces, label } = headerLead(view);
205
- return `${piecesText(pieces)}${label}`;
206
- };
207
-
208
- const paintHeader = (view, width, color) => {
209
- const { pieces, label } = headerLead(view);
210
- const right = ' ';
211
- const used = visibleWidth(piecesText(pieces)) + visibleWidth(right);
212
- const room = Math.max(0, width - used);
213
- const clipped = truncateVisible(label, room);
214
- const pad = Math.max(0, room - visibleWidth(clipped));
215
- pieces.push(...pathSegments(clipped));
216
- if (pad) pieces.push({ text: ' '.repeat(pad), role: 'chrome' });
217
- pieces.push({ text: right, role: 'chrome' });
218
- if (!color) return piecesText(pieces);
219
- let out = `${seq(THEME.headerChromeFg, THEME.headerBg)}${EL}`;
220
- for (const piece of pieces) {
221
- out += `${fg(HEADER_ROLE_FG[piece.role])}${piece.text}`;
222
- }
223
- return `${out}${RESET}`;
224
- };
225
-
226
- const backFooterId = (view) => {
227
- const { pane } = view;
228
- if (pane === 'branches' || pane === 'commits' || pane === 'tasks') {
229
- return 'back';
230
- }
231
- if (pane === 'npm') return view.npmView ? 'npmStop' : 'back';
232
- return isTaskView(view) ? 'back' : '';
233
- };
234
-
235
- const backHintMark = (view) => {
236
- if (view.pane === 'npm' && view.npmView && view.npmRunning) return STOP_MARK;
237
- return BACK_ARROW;
238
- };
239
-
240
- const buttonGap = (index) => (index ? BUTTON_GAP : BUTTON_LEAD);
241
-
242
- const hintBody = (mark) => {
243
- const gap = mark === STOP_MARK ? ' ' : '';
244
- return `${mark}${gap}${BACK_KEY}`;
245
- };
246
-
247
- const buttonLabel = (id, short) => (short ? actionLetter(id) : buttonWord(id));
248
-
249
- const paintBackHint = (color, mark) => {
250
- const bgRgb = THEME.buttonBg;
251
- const rest = hintBody(mark).slice(mark.length);
252
- const arrow = paint(mark, THEME.buttonHotFg, bgRgb, color, true);
253
- return arrow + paint(rest, THEME.buttonFg, bgRgb, color);
254
- };
255
-
256
- const paintMarkedWord = (label, letter, color, mark) => {
257
- const at = label.indexOf(letter);
258
- if (at < 0) {
259
- return paint(label, mark.missFg, mark.bgRgb, color, mark.missBold);
260
- }
261
- const before = label.slice(0, at);
262
- const hot = label.slice(at, at + letter.length);
263
- const after = label.slice(at + letter.length);
264
- let out = paint(before, mark.fgRgb, mark.bgRgb, color);
265
- out += paint(hot, mark.hotFg, mark.bgRgb, color, true);
266
- return out + paint(after, mark.fgRgb, mark.bgRgb, color);
267
- };
268
-
269
- const footerIds = (hidden, extra, backId) => {
270
- const ids = backId ? [backId] : [];
271
- for (const add of extra) {
272
- if (!ids.includes(add)) ids.push(add);
273
- }
274
- for (const id of ACTION_IDS) {
275
- if (id === 'quit' || hidden.includes(id) || ids.includes(id)) continue;
276
- ids.push(id);
277
- }
278
- return ids;
279
- };
280
-
281
- const layoutButtons = (width, options = {}) => {
282
- const { compact = false, hidden = [], extra = [], disabled = [] } = options;
283
- const { backId = '', mark = BACK_ARROW } = options;
284
- const ids = footerIds(hidden, extra, backId);
285
- const limit = Math.max(0, width - BUTTON_EDGE);
286
- const build = (short) => {
287
- const hits = [];
288
- const parts = [];
289
- let x = 0;
290
- for (const id of ids) {
291
- const back = id === backId;
292
- const letter = back ? BACK_KEY : actionLetter(id);
293
- const label = back ? hintBody(mark) : buttonLabel(id, short);
294
- const piece = `${buttonGap(parts.length)}${label}`;
295
- const w = visibleWidth(piece);
296
- if (x + w > limit && parts.length) break;
297
- const off = disabled.includes(id);
298
- if (!off) hits.push({ id, x0: x, x1: x + w });
299
- const hint = back ? mark : '';
300
- parts.push({ id, piece, letter, label, disabled: off, back, mark: hint });
301
- x += w;
302
- }
303
- return { hits, parts };
304
- };
305
- if (compact) return build(true);
306
- const words = build(false);
307
- if (words.parts.length === ids.length) return words;
308
- return build(true);
309
- };
310
-
311
- const paintButtons = (layout, width, color) => {
312
- const fgRgb = THEME.buttonFg;
313
- const bgRgb = THEME.buttonBg;
314
- let out = '';
315
- let used = 0;
316
- for (let i = 0; i < layout.parts.length; i++) {
317
- const part = layout.parts[i];
318
- out += paint(buttonGap(i), fgRgb, bgRgb, color);
319
- if (part.back) out += paintBackHint(color, part.mark);
320
- else if (part.disabled) out += paint(part.label, fgRgb, bgRgb, color);
321
- else out += paintMarkedWord(part.label, part.letter, color, BUTTON_MARK);
322
- used += visibleWidth(part.piece);
323
- }
324
- return out + fill(width - used, fgRgb, bgRgb, color);
325
- };
326
-
327
- const isInfoStatus = (status) =>
328
- INFO_STATUS.includes(status) ||
329
- INFO_PREFIX.some((prefix) => status.startsWith(prefix));
330
-
331
- const joinStatus = (parts) => parts.filter(Boolean).join(' ');
332
-
333
- const countsKind = (sourceKind, revShort) => {
334
- if (sourceKind === 'pr' || sourceKind === 'mr') return sourceKind;
335
- return revShort ? 'commit' : 'worktree';
336
- };
337
-
338
- const isWorktree = (view) =>
339
- countsKind(view.sourceKind, view.revShort) === 'worktree';
340
-
341
- const notesLine = (counts) => {
342
- const feedback = counts.feedback ?? 0;
343
- const tasks = counts.tasks ?? 0;
344
- const code = counts.code ?? 0;
345
- return `feedback ${feedback} tasks ${tasks} code ${code}`;
346
- };
347
-
348
- const statusLead = (view) => {
349
- const counts = view.counts ?? {};
350
- const kind = countsKind(view.sourceKind, view.revShort);
351
- if (kind === 'worktree') return branchLabel(view.branch);
352
- if (kind === 'commit') {
353
- return `commit ${view.revShort} ${counts.commit ?? 0}`;
354
- }
355
- const label = view.sourceLabel || kind;
356
- return `${kind} ${label} ${counts.pr ?? 0}`;
357
- };
358
-
359
- const statusLeft = (view) =>
360
- ` ${joinStatus([statusLead(view), notesLine(view.counts ?? {})])}`;
361
-
362
- const statusDelta = (total) => {
363
- const stagedAdd = total.stagedAdded ?? 0;
364
- const unstagedAdd = total.unstagedAdded ?? 0;
365
- const stagedDel = total.stagedRemoved ?? 0;
366
- const unstagedDel = total.unstagedRemoved ?? 0;
367
- const hasDelta =
368
- stagedAdd || unstagedAdd || stagedDel || unstagedDel || total.added;
369
- if (!hasDelta && !total.removed) return '';
370
- const plus = `+${stagedAdd}/${stagedAdd + unstagedAdd}`;
371
- const minus = `-${stagedDel}/${stagedDel + unstagedDel}`;
372
- return `${plus} ${minus}`;
373
- };
374
-
375
- const statusStats = (view) => {
376
- const counts = view.counts ?? {};
377
- const staged = counts.staged ?? 0;
378
- const total = fileTotals(view.files ?? []);
379
- const hasGit =
380
- total.remaining || total.staged || total.added || total.removed;
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}`]);
384
- };
385
-
386
- const statusLayout = (view, status, width) => {
387
- const msg = formatBusyStatus(status, view.progressFrame ?? 0);
388
- const isError = msg && !isInfoStatus(status);
389
- const msgFg = isError ? THEME.errorFg : THEME.chromeFg;
390
- const stats = statusStats(view);
391
- const right = stats ? `${stats} ` : '';
392
- const rightW = visibleWidth(right);
393
- const msgW = visibleWidth(msg);
394
- const side = msg ? 1 : 0;
395
- const room = Math.max(0, width - rightW - msgW - side * 2);
396
- const clipped = truncateVisible(statusLeft(view), room);
397
- const leftW = visibleWidth(clipped);
398
- const extra = Math.max(0, width - leftW - rightW - msgW);
399
- const padLeft = msg ? Math.floor(extra / 2) : extra;
400
- const padRight = msg ? extra - padLeft : 0;
401
- return { clipped, msg, msgFg, right, padLeft, padRight };
402
- };
403
-
404
- const choiceHits = (choices, prefix, y) => {
405
- let x = visibleWidth(prefix);
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 };
409
- x += w;
410
- return hit;
411
- });
412
- };
413
-
414
- const paintChoicePrompt = (choices, width, color, prefix) => {
415
- const bgRgb = THEME.chromeBg;
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);
420
- for (const choice of choices) {
421
- const label = `${choice.lead}${choice.label}`;
422
- out += paintMarkedWord(label, choice.letter, color, QUIT_MARK);
423
- }
424
- return out + fill(pad, THEME.mutedFg, bgRgb, color);
425
- };
426
-
427
- const paintStatusLeft = (text, color) => {
428
- if (!color) return text;
429
- const bgRgb = THEME.chromeBg;
430
- const found = text.match(OLD_LOGS);
431
- if (found) {
432
- const token = found[0];
433
- const before = text.slice(0, found.index);
434
- const after = text.slice(found.index + token.length);
435
- return (
436
- paintStatusLeft(before, color) +
437
- paint(token, THEME.shaFg, bgRgb, color) +
438
- paintStatusLeft(after, color)
439
- );
440
- }
441
- let out = '';
442
- let last = 0;
443
- for (const match of text.matchAll(STATUS_MARK)) {
444
- const token = match[0];
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;
449
- }
450
- return out + paint(text.slice(last), THEME.mutedFg, bgRgb, color);
451
- };
452
-
453
- const paintBranchHead = (text, branch, color) => {
454
- const name = branchLabel(branch);
455
- if (!name || !text.startsWith(' ')) return null;
456
- const shown = text.startsWith(` ${name}`) ? name : text.slice(1);
457
- if (!shown || !name.startsWith(shown)) return null;
458
- const rest = text.slice(1 + shown.length);
459
- const bgRgb = THEME.chromeBg;
460
- return (
461
- paint(' ', THEME.mutedFg, bgRgb, color) +
462
- paint(shown, THEME.buttonHotFg, bgRgb, color, true) +
463
- paintStatusLeft(rest, color)
464
- );
465
- };
466
-
467
- const commitSaveHint = (view) =>
468
- view.mode === 'compose' &&
469
- view.commitView === 'full' &&
470
- view.compose?.kind === 'commit';
471
-
472
- const paintSaveHintBar = (width, color) => {
473
- const bgRgb = THEME.buttonBg;
474
- const plain = `${BUTTON_LEAD}${SAVE_HINT.replaceAll('**', '')}`;
475
- const pad = Math.max(0, width - visibleWidth(plain));
476
- if (!color) return plain + ' '.repeat(pad);
477
- let out = paint(BUTTON_LEAD, THEME.buttonFg, bgRgb, color);
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);
483
- }
484
- return out + fill(pad, THEME.buttonFg, bgRgb, color);
485
- };
486
-
487
- const paintFooter = (view, layout, width, color) => {
488
- if (commitSaveHint(view)) return paintSaveHintBar(width, color);
489
- return paintButtons(layout, width, color);
490
- };
491
-
492
- const footerHits = (view, layout) => (commitSaveHint(view) ? [] : layout.hits);
493
-
494
- const paintStatusLine = (view, status, width, color) => {
495
- const confirm = CONFIRM[view.mode];
496
- if (confirm) {
497
- const prefix = confirmPrefix(confirm, view);
498
- return paintChoicePrompt(confirm.choices, width, color, prefix);
499
- }
500
- const layout = statusLayout(view, status, width);
501
- const leftGap = ' '.repeat(layout.padLeft);
502
- const rightGap = ' '.repeat(layout.padRight);
503
- if (!color) {
504
- return layout.clipped + leftGap + layout.msg + rightGap + layout.right;
505
- }
506
- const bgRgb = THEME.chromeBg;
507
- const head = isWorktree(view)
508
- ? paintBranchHead(layout.clipped, view.branch, color)
509
- : null;
510
- const left = head ?? paintStatusLeft(layout.clipped, color);
511
- return (
512
- left +
513
- paint(leftGap, THEME.mutedFg, bgRgb, color) +
514
- paint(layout.msg, layout.msgFg, bgRgb, color) +
515
- paint(rightGap, THEME.mutedFg, bgRgb, color) +
516
- paintStatusLeft(layout.right, color)
517
- );
518
- };
519
-
520
- const statusChoiceHits = (view, y) => {
521
- const spec = CONFIRM[view.mode];
522
- if (!spec) return [];
523
- return choiceHits(spec.choices, confirmPrefix(spec, view), y);
524
- };
525
-
526
- const statusBranchHit = (view, width, y) => {
527
- if (CONFIRM[view.mode]) return null;
528
- const name = branchLabel(view.branch);
529
- if (!name || !isWorktree(view)) return null;
530
- const layout = statusLayout(view, view.status ?? '', width);
531
- const at = layout.clipped.indexOf(name);
532
- if (at < 0) return null;
533
- return { id: 'branch', y, x0: at, x1: at + visibleWidth(name) };
534
- };
535
-
536
- const withHidden = (ids, more) => [...ids, ...more];
537
-
538
- const hiddenActions = (view) => {
539
- const { pane } = view;
540
- if (pane === 'dashboard') return DASH_HIDDEN;
541
- if (pane === 'files') return FILES_HIDDEN;
542
- if (pane === 'unit') return withHidden(UNIT_DISABLED, ['feedback', 'tasks']);
543
- if (pane === 'branches') return BRANCHES_DISABLED;
544
- if (pane === 'commits') return COMMITS_DISABLED;
545
- if (pane === 'npm') return NPM_DISABLED;
546
- if (pane === 'tasks' || isTaskView(view)) {
547
- return withHidden(TASKS_DISABLED, ['prev', 'next']);
548
- }
549
- return withHidden(DIFF_DISABLED, ['feedback', 'tasks']);
550
- };
551
-
552
- const dimFooterActions = (view) => {
553
- const pane = view.pane ?? 'diff';
554
- if (pane === 'files') {
555
- return isTasksEntry(selectedFile(view)) ? FILES_GIT_DISABLED : [];
556
- }
557
- if (pane === 'diff' || pane === 'unit') return diffGitDisabled(view.item);
558
- if (pane === 'branches') return branchGitDisabled(selectedBranch(view));
559
- if (pane === 'commits') {
560
- return commitGitDisabled({
561
- ...selectedCommit(view),
562
- canCommit: (view.counts?.staged ?? 0) > 0,
563
- unstagedCurrent: !view.rev && !view.revShort,
564
- });
565
- }
566
- if (pane !== 'npm') return [];
567
- if (view.npmView) return view.npmRunning ? ['npmRerun'] : [];
568
- return view.npmLogs ? [] : ['npmLogs'];
569
- };
570
-
571
- const navExcept = (skip) => DASH_IDS.filter((id) => !skip.includes(id));
572
-
573
- const extraFooterActions = (view) => {
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
- ];
597
- }
598
- if (pane === 'npm') {
599
- if (view.npmView) {
600
- return [...navExcept(['dashRun']), 'npmVerbose', 'npmRerun'];
601
- }
602
- return [...DASH_IDS, 'npmEdit', 'npmNew', 'npmDrop', 'npmLogs'];
603
- }
604
- if (pane === 'tasks' || isTaskView(view)) return [...DASH_IDS, 'drop'];
605
- return navExcept(['dashDiffs']);
606
- };
607
-
608
- const footerLayout = (view, width) =>
609
- layoutButtons(width, {
610
- hidden: hiddenActions(view),
611
- extra: extraFooterActions(view),
612
- disabled: dimFooterActions(view),
613
- backId: backFooterId(view),
614
- mark: backHintMark(view),
615
- });
616
-
617
- module.exports = {
618
- formatBusyStatus,
619
- headerText,
620
- paintHeader,
621
- layoutButtons,
622
- footerLayout,
623
- paintFooter,
624
- footerHits,
625
- paintStatusLine,
626
- statusChoiceHits,
627
- statusBranchHit,
628
- };