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
@@ -2,68 +2,31 @@
2
2
 
3
3
  const ansi = require('../ansi.js');
4
4
  const actions = require('../session/actions.js');
5
- const { LIST_PANES } = actions;
6
5
 
7
- const { THEME, paint, visibleWidth, truncateVisible, seq, EL, RESET } = ansi;
8
- const { graphemes, graphemeWidth } = ansi;
6
+ const { THEME, paint, visibleWidth, graphemes, graphemeWidth } = ansi;
7
+ const { LIST_PANES } = actions;
9
8
 
10
9
  const GUTTER_W = 2;
11
10
  const FILE_MARK = '▶';
11
+ const MARK_W = visibleWidth(` ${FILE_MARK} `);
12
12
  const LIST_PAD = 1;
13
- const CHECK_DONE = 'xX';
14
- const CHECK_TOKEN = /\[([ xX])\]/;
15
- const CHECK_LEAD = /^\[[ xX]\] /;
13
+ const NO_EDITS = [];
16
14
 
17
15
  const fill = (width, fgRgb, bgRgb, color) =>
18
16
  paint(' '.repeat(Math.max(0, width)), fgRgb, bgRgb, color);
19
17
 
20
- const paintBar = (text, width, fgRgb, bgRgb, color) => {
21
- const clipped = truncateVisible(text, width);
22
- const pad = Math.max(0, width - visibleWidth(clipped));
23
- const filled = clipped + ' '.repeat(pad);
24
- if (!color) return filled;
25
- return `${seq(fgRgb, bgRgb)}${EL}${filled}${RESET}`;
26
- };
18
+ const markPrefix = (selected) => ` ${selected ? FILE_MARK : ' '} `;
27
19
 
28
- const splitCheck = (text) => {
29
- const match = CHECK_TOKEN.exec(text);
30
- if (!match) return null;
31
- const start = match.index;
32
- const token = match[0];
33
- const before = text.slice(0, start);
34
- const mark = match[1];
35
- const after = text.slice(start + token.length);
36
- return { before, mark, token, after };
37
- };
20
+ const isListPane = (pane) =>
21
+ LIST_PANES.includes(pane) || pane === 'dashboard' || pane === 'repos';
38
22
 
39
- const paintCheckBar = (text, width, fgRgb, bgRgb, color, plain = false) => {
40
- const clipped = truncateVisible(text, width);
41
- const parts = color ? splitCheck(clipped) : null;
42
- if (!parts) return paintBar(text, width, fgRgb, bgRgb, color);
43
- const pad = Math.max(0, width - visibleWidth(clipped));
44
- const done = CHECK_DONE.includes(parts.mark);
45
- let chipFg = THEME.checkFg;
46
- let chipBg = THEME.checkBg;
47
- if (plain) {
48
- chipFg = fgRgb;
49
- chipBg = bgRgb;
50
- } else if (done) {
51
- chipFg = THEME.checkDoneFg;
52
- chipBg = THEME.checkDoneBg;
53
- }
54
- const after = `${parts.after}${' '.repeat(pad)}`;
55
- let out = `${seq(fgRgb, bgRgb)}${EL}`;
56
- if (parts.before) out += paint(parts.before, fgRgb, bgRgb, color);
57
- out += paint(parts.token, chipFg, chipBg, color);
58
- if (after) out += paint(after, fgRgb, bgRgb, color);
59
- return out;
60
- };
23
+ const isTaskView = (view) => view.item?.origin === 'task';
24
+
25
+ const branchLabel = (name) => `${name ?? ''}`.trim();
61
26
 
62
27
  const padVisible = (text, width, align) => {
63
- const n = Math.max(0, width - visibleWidth(text));
64
- const gap = ' '.repeat(n);
65
- if (align === 'end') return `${text}${gap}`;
66
- return `${gap}${text}`;
28
+ const gap = ' '.repeat(Math.max(0, width - visibleWidth(text)));
29
+ return align === 'end' ? `${text}${gap}` : `${gap}${text}`;
67
30
  };
68
31
 
69
32
  const splitWidths = (width) => {
@@ -75,49 +38,12 @@ const splitWidths = (width) => {
75
38
  const paintSplitRule = (color) =>
76
39
  fill(1, THEME.splitGutterFg, THEME.splitGutterBg, color);
77
40
 
78
- const paintSplitBlank = (width, color) => {
79
- const { leftW, rightW } = splitWidths(width);
80
- return (
81
- fill(leftW, THEME.ctxFg, THEME.ctxBg, color) +
82
- paintSplitRule(color) +
83
- fill(rightW, THEME.ctxFg, THEME.ctxBg, color)
84
- );
85
- };
86
-
87
41
  const paintBodyFill = (width, color, kind) => {
88
- if (kind === 'files') {
89
- const inner = Math.max(1, width - 1);
90
- const main = fill(inner, THEME.ctxFg, THEME.ctxBg, color);
91
- const edge = fill(1, THEME.ctxFg, THEME.ctxBg, color);
92
- return main + edge;
93
- }
94
- if (kind === 'split') return paintSplitBlank(width, color);
95
- return fill(width, THEME.ctxFg, THEME.ctxBg, color);
96
- };
97
-
98
- const windowRows = (rows, focus, cap) => {
99
- if (rows.length <= cap) return { rows, offset: 0 };
100
- let start = focus - cap + 1;
101
- if (start < 0) start = 0;
102
- if (start + cap > rows.length) start = rows.length - cap;
103
- return { rows: rows.slice(start, start + cap), offset: start };
104
- };
105
-
106
- const listWindowStart = (focus, offset, listH, count) => {
107
- let start = offset ?? 0;
108
- if (focus < start) start = focus;
109
- if (focus >= start + listH) start = focus - listH + 1;
110
- const maxStart = Math.max(0, count - listH);
111
- if (start > maxStart) start = maxStart;
112
- if (start < 0) start = 0;
113
- return start;
114
- };
115
-
116
- const extraIdx = (extraRow, cursor, count) => {
117
- if (!extraRow) return -1;
118
- if (extraRow.at === 'start') return 0;
119
- if (extraRow.at === 'replace') return extraRow.index ?? cursor;
120
- return count;
42
+ const ctx = (n) => fill(n, THEME.ctxFg, THEME.ctxBg, color);
43
+ if (kind === 'files') return ctx(Math.max(1, width - 1)) + ctx(1);
44
+ if (kind !== 'split') return ctx(width);
45
+ const { leftW, rightW } = splitWidths(width);
46
+ return ctx(leftW) + paintSplitRule(color) + ctx(rightW);
121
47
  };
122
48
 
123
49
  const paneResult = (fields = {}) => ({
@@ -131,11 +57,29 @@ const paneResult = (fields = {}) => ({
131
57
  editHits: fields.editHits ?? [],
132
58
  });
133
59
 
134
- const NO_EDITS = [];
60
+ const editSpan = (x0, textX, x1, start, text) => ({
61
+ x0,
62
+ textX,
63
+ x1,
64
+ start,
65
+ text,
66
+ limit: start + text.length,
67
+ live: true,
68
+ });
135
69
 
136
- const listLine = (row, index, cursor = null, edits = NO_EDITS) => {
137
- const hit = index >= 0;
138
- return { row, index, hit, cursor, edits };
70
+ const listLine = (row, index, cursor = null, edits = NO_EDITS) => ({
71
+ row,
72
+ index,
73
+ hit: index >= 0,
74
+ cursor,
75
+ edits,
76
+ });
77
+
78
+ const listWindowStart = (focus, offset, listH, count) => {
79
+ let start = offset ?? 0;
80
+ if (focus < start) start = focus;
81
+ if (focus >= start + listH) start = focus - listH + 1;
82
+ return Math.max(0, Math.min(start, count - listH));
139
83
  };
140
84
 
141
85
  const rowCursor = (cursor, row) => {
@@ -150,11 +94,10 @@ const paintListWindow = (count, focus, pane, lineAt) => {
150
94
  const listH = bodyH - pad * 2;
151
95
  const start = listWindowStart(focus, offset, listH, count);
152
96
  const blank = paintBodyFill(width, color, 'files');
153
- const body = [];
97
+ const body = pad ? [blank] : [];
154
98
  const fileHits = [];
155
99
  const editHits = [];
156
100
  let cursor = null;
157
- if (pad) body.push(blank);
158
101
  for (let i = 0; i < listH; i++) {
159
102
  const line = lineAt(start + i);
160
103
  if (!line) break;
@@ -169,30 +112,35 @@ const paintListWindow = (count, focus, pane, lineAt) => {
169
112
  return paneResult({ body, fileHits, editHits, cursor, listScroll: start });
170
113
  };
171
114
 
172
- const editLine = (extraRow) => {
173
- const { row, cursor, edit, edits = [] } = extraRow;
174
- return listLine(row, -1, cursor, edit ? [...edits, edit] : edits);
115
+ const insertedAt = (extra, count) => {
116
+ if (extra.at === 'start') return 0;
117
+ if (extra.at !== 'index') return count;
118
+ const index = extra.index ?? count;
119
+ return Math.max(0, Math.min(index, count));
175
120
  };
176
121
 
177
- const paintCursorList = (items, cursor, pane, paintRow, extraRow = null) => {
178
- const edit = extraRow ? editLine(extraRow) : null;
179
- const inserting = edit !== null && extraRow.at === 'start';
180
- const extra = edit && extraRow.at !== 'replace' ? 1 : 0;
181
- const extraAt = extraIdx(extraRow, cursor, items.length);
182
- const lineAt = (idx) => {
183
- if (edit && idx === extraAt) return edit;
184
- const index = inserting ? idx - 1 : idx;
122
+ const paintCursorList = (items, cursor, pane, paintRow, extra = null) => {
123
+ const lineAt = (index) => {
185
124
  const entry = items[index];
186
125
  if (!entry) return null;
187
- return listLine(paintRow(entry, !edit && index === cursor), index);
126
+ const selected = !extra && index === cursor;
127
+ return listLine(paintRow(entry, selected, index), index);
188
128
  };
189
- const focus = edit ? extraAt : cursor;
190
- return paintListWindow(items.length + extra, focus, pane, lineAt);
129
+ if (!extra) return paintListWindow(items.length, cursor, pane, lineAt);
130
+ const edit = listLine(extra.row, -1, extra.cursor, extra.edits);
131
+ if (extra.at === 'replace') {
132
+ const at = extra.index;
133
+ const replaced = (i) => (i === at ? edit : lineAt(i));
134
+ return paintListWindow(items.length, at, pane, replaced);
135
+ }
136
+ const at = insertedAt(extra, items.length);
137
+ const inserted = (i) => (i === at ? edit : lineAt(i < at ? i : i - 1));
138
+ return paintListWindow(items.length + 1, at, pane, inserted);
191
139
  };
192
140
 
193
141
  const sliceVisible = (text, skip, width) => {
194
142
  if (width <= 0) return '';
195
- const parts = graphemes(`${text ?? ''}`);
143
+ const parts = graphemes(text);
196
144
  let used = 0;
197
145
  let i = 0;
198
146
  const drop = Math.max(0, skip);
@@ -214,59 +162,16 @@ const sliceVisible = (text, skip, width) => {
214
162
  };
215
163
 
216
164
  const fieldWindow = (text, cursor, scroll, width) => {
217
- const raw = `${text ?? ''}`;
218
165
  if (width <= 0) return { text: '', col: 0, scroll: 0 };
219
- const at = Math.max(0, Math.min(cursor ?? 0, raw.length));
220
- const col = visibleWidth(raw.slice(0, at));
221
- let next = scroll ?? 0;
222
- if (next < 0) next = 0;
166
+ const at = Math.max(0, Math.min(cursor ?? 0, text.length));
167
+ const col = visibleWidth(text.slice(0, at));
168
+ let next = Math.max(0, scroll ?? 0);
223
169
  if (col < next) next = col;
224
170
  if (col >= next + width) next = col - width + 1;
225
- const shown = sliceVisible(raw, next, width);
171
+ const shown = sliceVisible(text, next, width);
226
172
  return { text: shown, col: col - next, scroll: next };
227
173
  };
228
174
 
229
- const branchLabel = (name) => `${name ?? ''}`.trim();
230
-
231
- const paintListEdit = (compose, width, color, edgeW = 0) => {
232
- const inner = Math.max(1, width - edgeW);
233
- const prefix = ` ${FILE_MARK} `;
234
- const prefixW = visibleWidth(prefix);
235
- const rest = Math.max(1, inner - prefixW);
236
- const text = `${compose.text ?? ''}`.replaceAll('\n', ' ');
237
- const win = fieldWindow(text, compose.cursor, compose.scrollCol, rest);
238
- const clipped = win.text;
239
- const pad = Math.max(0, rest - visibleWidth(clipped));
240
- const left = `${prefix}${clipped}${' '.repeat(pad)}`;
241
- const fgRgb = THEME.chromeFg;
242
- const bgRgb = THEME.buttonBg;
243
- const edge = fill(edgeW, fgRgb, THEME.ctxBg, color);
244
- let row = `${left}${edge}`;
245
- if (color) {
246
- const gap = ' '.repeat(pad);
247
- row = `${seq(fgRgb, bgRgb)}${EL}`;
248
- row += paint(prefix, fgRgb, bgRgb, color);
249
- row += paint(clipped, THEME.buttonHotFg, bgRgb, color);
250
- row += paint(gap, fgRgb, bgRgb, color);
251
- row += edge;
252
- }
253
- const flat = text;
254
- return {
255
- row,
256
- cursor: { x: prefixW + win.col + 1, scroll: win.scroll },
257
- edit: {
258
- x0: 1,
259
- textX: prefixW + 1,
260
- x1: prefixW + 1 + rest,
261
- start: 0,
262
- text: flat,
263
- limit: flat.length,
264
- scroll: win.scroll,
265
- live: true,
266
- },
267
- };
268
- };
269
-
270
175
  const measureColumns = (entries, rowFields, align) => {
271
176
  const keys = Object.keys(align);
272
177
  const widths = Object.fromEntries(keys.map((key) => [key, 0]));
@@ -282,7 +187,7 @@ const measureColumns = (entries, rowFields, align) => {
282
187
  const padColumns = (fields, widths, align) => {
283
188
  const parts = {};
284
189
  for (const key of Object.keys(align)) {
285
- const width = widths?.[key] ?? visibleWidth(fields[key]);
190
+ const width = widths[key] ?? visibleWidth(fields[key]);
286
191
  parts[key] = padVisible(fields[key], width, align[key]);
287
192
  }
288
193
  return parts;
@@ -309,27 +214,27 @@ const paintColumns = (parts, widths, fgRgb, bgRgb, color, tones) => {
309
214
  };
310
215
 
311
216
  module.exports = {
312
- measureColumns,
313
- padColumns,
314
- joinColumns,
315
- paintColumns,
316
217
  GUTTER_W,
317
218
  FILE_MARK,
318
- LIST_PANES,
319
- CHECK_LEAD,
219
+ MARK_W,
320
220
  fill,
321
- paintBar,
322
- paintCheckBar,
221
+ markPrefix,
222
+ isListPane,
223
+ isTaskView,
224
+ branchLabel,
323
225
  padVisible,
324
226
  splitWidths,
325
227
  paintSplitRule,
326
228
  paintBodyFill,
327
- windowRows,
229
+ paneResult,
230
+ editSpan,
328
231
  listLine,
329
232
  paintListWindow,
330
233
  paintCursorList,
331
- paintListEdit,
234
+ sliceVisible,
332
235
  fieldWindow,
333
- branchLabel,
334
- paneResult,
236
+ measureColumns,
237
+ padColumns,
238
+ joinColumns,
239
+ paintColumns,
335
240
  };
@@ -5,30 +5,21 @@ const notes = require('./notes.js');
5
5
  const diff = require('./diff.js');
6
6
  const present = require('./present.js');
7
7
  const frame = require('./frame.js');
8
- const { renderFrame } = frame;
9
8
 
10
- const { QUIT_PROMPT, PUSH_PROMPT } = chrome;
11
- const { updatePrompt, dropPrompt } = chrome;
12
9
  const { formatBusyStatus, headerText, layoutButtons } = chrome;
13
- const { delCharBg, addCharBg, paintDiffLine, codeInnerWidth } = diff;
14
- const { todoInnerWidth, todoBodyWidth, noteInnerWidth } = notes;
10
+ const { todoBodyWidth, noteInnerWidth } = notes;
11
+ const { paintDiffLine, codeInnerWidth } = diff;
15
12
  const { presentCursor, presentRows } = present;
13
+ const { renderFrame } = frame;
16
14
 
17
15
  module.exports = {
18
- QUIT_PROMPT,
19
- PUSH_PROMPT,
20
- updatePrompt,
21
- dropPrompt,
22
- delCharBg,
23
- addCharBg,
24
- paintDiffLine,
16
+ formatBusyStatus,
25
17
  headerText,
26
18
  layoutButtons,
27
- todoInnerWidth,
28
19
  todoBodyWidth,
29
20
  noteInnerWidth,
21
+ paintDiffLine,
30
22
  codeInnerWidth,
31
- formatBusyStatus,
32
23
  renderFrame,
33
24
  presentCursor,
34
25
  presentRows,
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
3
+ const commands = require('../npm-commands.js');
4
+ const { formatTable } = commands;
5
+ const reportRender = require('../report-render.js');
6
+ const { summaryRows, problemRows, displayMessage, quoteLines } = reportRender;
7
+
8
+ const reportProblem = (item) => {
9
+ const title = item.name || item.message || item.code || 'problem';
10
+ const mark = item.severity === 'warning' ? '△' : '✖';
11
+ const count = item.count > 1 ? ` × ${item.count}` : '';
12
+ const lines = ['', `${mark} ${title.split('\n')[0]}${count}`, ''];
13
+ lines.push(...formatTable(problemRows(item)));
14
+ const message = displayMessage(item);
15
+ if (message) lines.push('', ...quoteLines(message));
16
+ if (item.trace) {
17
+ lines.push('', 'Stack');
18
+ for (const row of item.trace.split(/\r?\n/)) {
19
+ const frame = row.trim().replace(/^at\s+/, '');
20
+ if (frame) lines.push(` at ${frame}`);
21
+ }
22
+ }
23
+ if (item.related.length) {
24
+ lines.push('', 'Related failures');
25
+ for (const row of item.related) lines.push(` ↳ ${row}`);
26
+ }
27
+ return lines;
28
+ };
29
+
30
+ const renderReport = (doc) => {
31
+ const lines = [];
32
+ for (const item of doc.reports) {
33
+ if (lines.length) lines.push('');
34
+ const mark = item.ok ? '✔' : '✖';
35
+ lines.push(`${mark} ${item.tool} · exit ${doc.exit}`, '');
36
+ lines.push(...formatTable(summaryRows(item.summary)));
37
+ for (const entry of item.problems) lines.push(...reportProblem(entry));
38
+ }
39
+ if (doc.stderr) lines.push('', 'Stderr', doc.stderr);
40
+ return `${lines.join('\n')}\n`;
41
+ };
42
+
43
+ module.exports = { renderReport };
@@ -0,0 +1,226 @@
1
+ 'use strict';
2
+
3
+ const ansi = require('../ansi.js');
4
+ const { THEME } = ansi;
5
+ const primitives = require('./primitives.js');
6
+ const { paintBodyFill, paneResult } = primitives;
7
+ const tiles = require('./tiles.js');
8
+ const { seg, spinner, equalGrid, pageWindow, placeEqualTiles } = tiles;
9
+ const { paintTile, composeBands, tileHits, pageFill } = tiles;
10
+ const { segmentsWidth, clipSegments } = tiles;
11
+ const table = require('./dash-table.js');
12
+ const { size, isHot } = table;
13
+
14
+ const REPO_MIN_H = 6;
15
+ const MARGIN_MIN_H = 12;
16
+ const EXT_LIMIT = 3;
17
+ const DIM_BACKS = {
18
+ head: THEME.dashDimHeadBg,
19
+ tile: THEME.dashDimTileBg,
20
+ row: THEME.dashDimRowBg,
21
+ };
22
+ const RUN_ICON = {
23
+ passed: ['✔', 'add'],
24
+ failed: ['✖', 'error'],
25
+ aborted: ['⊘', 'muted'],
26
+ };
27
+
28
+ const extName = (key) => {
29
+ const text = `${key ?? ''}`;
30
+ if (!text || text === '(none)') return 'none';
31
+ return text.startsWith('.') ? text.slice(1) : text;
32
+ };
33
+
34
+ const blinkTone = (frame) => (Math.abs(frame) % 2 ? 'hot' : 'warn');
35
+
36
+ const spin = (frame) => seg(spinner(frame), 'warn', true);
37
+
38
+ const rowPending = (row) =>
39
+ !row.filesReady ||
40
+ !row.commitsReady ||
41
+ !row.diffReady ||
42
+ !row.tasksReady ||
43
+ !row.runsReady ||
44
+ !row.npmReady;
45
+
46
+ const fitAside = (left, right, width) => {
47
+ const rightW = segmentsWidth(right);
48
+ const room = Math.max(0, width - rightW - 1);
49
+ const clipped = clipSegments(left, room);
50
+ const gap = Math.max(1, width - segmentsWidth(clipped) - rightW);
51
+ return [...clipped, seg(' '.repeat(gap)), ...right];
52
+ };
53
+
54
+ const titleLine = (row, ctx, selected, width) => {
55
+ const hot = isHot(row.activityAt, ctx.now);
56
+ const tone = hot ? blinkTone(ctx.frame) : 'text';
57
+ const left = [seg(row.name, tone, selected || hot)];
58
+ if (hot) left.push(seg(' ●', tone, true));
59
+ const right = [];
60
+ if (rowPending(row)) right.push(spin(ctx.frame), seg(' '));
61
+ const label = row.filesReady ? size(row.bytes) : '…';
62
+ const sizeTone = row.filesReady ? 'text' : 'muted';
63
+ right.push(seg(label, sizeTone, row.filesReady));
64
+ return fitAside(left, right, width);
65
+ };
66
+
67
+ const extLine = (row, ctx) => {
68
+ if (!row.filesReady) return [spin(ctx.frame)];
69
+ const exts = row.exts.slice(0, EXT_LIMIT);
70
+ if (!exts.length) return [seg('no files', 'muted')];
71
+ const segs = [];
72
+ for (let i = 0; i < exts.length; i++) {
73
+ if (i) segs.push(seg(' '));
74
+ segs.push(seg(extName(exts[i].key), 'sha'));
75
+ segs.push(seg(` ${size(exts[i].bytes)}`, 'text'));
76
+ }
77
+ return segs;
78
+ };
79
+
80
+ const countValue = (ready, text, frame) =>
81
+ ready ? seg(text, 'text', true) : spin(frame);
82
+
83
+ const countLine = (row, ctx) => [
84
+ seg('tasks: ', 'muted'),
85
+ countValue(row.tasksReady, `${row.tasksDone}/${row.tasks}`, ctx.frame),
86
+ seg(' commits: ', 'muted'),
87
+ countValue(row.commitsReady, `${row.commits}`, ctx.frame),
88
+ ];
89
+
90
+ const resultNote = (run) => {
91
+ const result = run.result;
92
+ if (!result) return [];
93
+ if (typeof result.tests === 'number') {
94
+ const note = [seg(` ${result.tests}`, 'muted')];
95
+ if (result.failed) note.push(seg(` ${result.failed} fail`, 'error'));
96
+ return note;
97
+ }
98
+ if (typeof result.errors === 'number') {
99
+ return [seg(` ${result.errors} err`, result.errors ? 'error' : 'muted')];
100
+ }
101
+ return [];
102
+ };
103
+
104
+ const runLine = (row, ctx) => {
105
+ if (!row.runsReady) return [seg('run: ', 'muted'), spin(ctx.frame)];
106
+ const run = row.run;
107
+ if (!run) return [seg('run: ', 'muted'), seg('idle', 'muted')];
108
+ if (run.status === 'running') {
109
+ const seen = run.done + run.failed;
110
+ const total = run.expected > 0 ? `/${run.expected}` : '';
111
+ const fail = run.failed ? ` ${run.failed} fail` : '';
112
+ return [
113
+ seg(`${spinner(ctx.frame)} `, 'warn', true),
114
+ seg(run.name || 'run', 'text'),
115
+ seg(` ${seen}${total}${fail}`, 'warn'),
116
+ ];
117
+ }
118
+ const icon = RUN_ICON[run.status] ?? ['?', 'muted'];
119
+ return [
120
+ seg(`${icon[0]} `, icon[1], true),
121
+ seg(run.name || 'run', 'text'),
122
+ seg(` ${run.status}`, icon[1]),
123
+ ...resultNote(run),
124
+ ];
125
+ };
126
+
127
+ const npmLine = (row, ctx) => {
128
+ if (!row.npmReady) return [seg('deps: ', 'muted'), spin(ctx.frame)];
129
+ const npm = row.npm;
130
+ if (!npm || !npm.hasManifest) return [seg('no package.json', 'muted')];
131
+ const modules = npm.modules || { count: 0, bytes: 0 };
132
+ const segs = [
133
+ seg('deps: ', 'muted'),
134
+ seg(`${npm.deps}`, 'text'),
135
+ seg(' dev: ', 'muted'),
136
+ seg(`${npm.dev}`, 'text'),
137
+ seg(' all: ', 'muted'),
138
+ seg(`${modules.count}`, 'text'),
139
+ seg(' (', 'muted'),
140
+ seg(size(modules.bytes), 'text'),
141
+ seg(')', 'muted'),
142
+ ];
143
+ if (npm.audit) {
144
+ segs.push(
145
+ seg(' '),
146
+ seg('🚨 ', 'error'),
147
+ seg(`${npm.audit}`, 'error', true),
148
+ );
149
+ }
150
+ if (npm.outdated) {
151
+ segs.push(
152
+ seg(' '),
153
+ seg('⚠️ ', 'warn'),
154
+ seg(`${npm.outdated}`, 'warn', true),
155
+ );
156
+ }
157
+ return segs;
158
+ };
159
+
160
+ const diffLine = (row, ctx) => {
161
+ if (!row.diffReady) return [seg('diff: ', 'muted'), spin(ctx.frame)];
162
+ const diff = row.diff;
163
+ const added = diff.stagedAdded + diff.unstagedAdded;
164
+ const removed = diff.stagedRemoved + diff.unstagedRemoved;
165
+ return [
166
+ seg('diff: ', 'muted'),
167
+ seg(`+${diff.stagedAdded}/${added}`, 'add'),
168
+ seg(' '),
169
+ seg(`-${diff.stagedRemoved}/${removed}`, 'del'),
170
+ ];
171
+ };
172
+
173
+ const repoContent = (row, ctx, selected, width) => ({
174
+ titleLine: titleLine(row, ctx, selected, width),
175
+ lines: [
176
+ extLine(row, ctx),
177
+ countLine(row, ctx),
178
+ runLine(row, ctx),
179
+ diffLine(row, ctx),
180
+ npmLine(row, ctx),
181
+ ],
182
+ });
183
+
184
+ const paintRepos = (model, bands, color) => {
185
+ const painted = new Map();
186
+ const ctx = { now: model.now, frame: model.frame };
187
+ const selected = model.tiles[model.cursor];
188
+ const selectedId = selected ? selected.id : '';
189
+ for (const band of bands) {
190
+ for (const place of band.tiles) {
191
+ const row = model.tiles.find((tile) => tile.id === place.id);
192
+ if (!row) continue;
193
+ const tile = { id: row.id, key: '', title: row.name };
194
+ const inner = Math.max(0, place.w - 2);
195
+ const picked = row.id === selectedId;
196
+ const content = repoContent(row, ctx, picked, inner);
197
+ const backs = picked ? null : DIM_BACKS;
198
+ const rows = paintTile(tile, content, place.w, band.h, color, backs);
199
+ painted.set(place.id, rows);
200
+ }
201
+ }
202
+ return painted;
203
+ };
204
+
205
+ const paintBodyRepos = (view, width, color, bodyH, headerLines) => {
206
+ const model = view.repos;
207
+ if (!model || !model.tiles.length) {
208
+ const blank = paintBodyFill(width, color, 'files');
209
+ return paneResult({ body: new Array(bodyH).fill(blank) });
210
+ }
211
+ const margin = bodyH >= MARGIN_MIN_H ? 1 : 0;
212
+ const height = Math.max(1, bodyH - margin * 2);
213
+ const grid = equalGrid(width, height, model.tiles.length, 34, REPO_MIN_H);
214
+ const page = pageWindow(model.tiles.length, model.cursor, grid.cap);
215
+ const shown = model.tiles.slice(page.start, page.end);
216
+ const bands = placeEqualTiles(shown, grid);
217
+ model.cols = grid.cols;
218
+ const painted = paintRepos(model, bands, color);
219
+ const rows = composeBands(bands, painted, width, height, color);
220
+ const pad = new Array(margin).fill(pageFill(width, color));
221
+ const body = [...pad, ...rows, ...pad];
222
+ const fileHits = tileHits(bands, headerLines + margin);
223
+ return paneResult({ body, fileHits });
224
+ };
225
+
226
+ module.exports = { REPO_MIN_H, extName, paintBodyRepos };
@@ -0,0 +1,25 @@
1
+ 'use strict';
2
+
3
+ const ansi = require('../ansi.js');
4
+ const { THEME, paint } = ansi;
5
+
6
+ const paintPathText = (text, fg, bg, color, bold, spans) => {
7
+ if (!spans.length) return paint(text, fg, bg, color, bold);
8
+ let out = '';
9
+ let at = 0;
10
+ for (const span of spans) {
11
+ if (span.start > at) {
12
+ out += paint(text.slice(at, span.start), fg, bg, color, bold);
13
+ }
14
+ const on = span.on === true;
15
+ const markFg = on ? THEME.searchOnFg : THEME.searchFg;
16
+ const markBg = on ? THEME.searchOnBg : THEME.searchBg;
17
+ const slice = text.slice(span.start, span.end);
18
+ out += paint(slice, markFg, markBg, color, true);
19
+ at = span.end;
20
+ }
21
+ if (at < text.length) out += paint(text.slice(at), fg, bg, color, bold);
22
+ return out;
23
+ };
24
+
25
+ module.exports = { paintPathText };