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
@@ -0,0 +1,89 @@
1
+ 'use strict';
2
+
3
+ const { THEME, paint, visibleWidth } = require('../term/ansi.js');
4
+ const { paintSegments } = require('./tiles.js');
5
+ const { npmContent } = require('./dashboard/blocks.js');
6
+ const primitives = require('./primitives.js');
7
+ const { MARK_W, markPrefix, paintCursorList, fieldWindow, editSpan } =
8
+ primitives;
9
+
10
+ const TILE = { key: 'n', title: 'npm' };
11
+ const TAIL = 1;
12
+ const LIST_PAD = 1;
13
+
14
+ const paintMarginRow = (line, width, color, selected) => {
15
+ const bg = selected ? THEME.buttonBg : THEME.ctxBg;
16
+ const fg = selected ? THEME.chromeFg : THEME.mutedFg;
17
+ const inner = Math.max(1, width - MARK_W - TAIL);
18
+ const body = paintSegments(line, inner, bg, color);
19
+ const prefix = markPrefix(selected);
20
+ const tail = ' '.repeat(TAIL);
21
+ if (!color) return `${prefix}${body}${tail}`;
22
+ return paint(prefix, fg, bg, color) + body + paint(tail, fg, bg, color);
23
+ };
24
+
25
+ const paintPackageEdit = (compose, width, color) => {
26
+ const prefix = markPrefix(true);
27
+ const rest = Math.max(1, width - MARK_W - TAIL);
28
+ const text = `${compose.text ?? ''}`.replaceAll('\n', ' ');
29
+ const win = fieldWindow(text, compose.cursor, compose.scrollCol, rest);
30
+ const gap = ' '.repeat(Math.max(0, rest - visibleWidth(win.text)));
31
+ const tail = ' '.repeat(TAIL);
32
+ const fg = THEME.chromeFg;
33
+ const bg = THEME.buttonBg;
34
+ let row = `${prefix}${win.text}${gap}${tail}`;
35
+ if (color) {
36
+ row = paint(prefix, fg, bg, color);
37
+ row += paint(win.text, THEME.buttonHotFg, bg, color);
38
+ row += paint(`${gap}${tail}`, fg, bg, color);
39
+ }
40
+ const textX = MARK_W + 1;
41
+ const edit = editSpan(1, textX, textX + rest, 0, text);
42
+ const cursor = { x: textX + win.col, scroll: win.scroll };
43
+ return { row, cursor, edits: [{ ...edit, scroll: win.scroll }] };
44
+ };
45
+
46
+ const paintBodyPackages = (view, width, color, bodyH, headerLines) => {
47
+ const ctx = { frame: view.progressFrame ?? 0 };
48
+ const inner = Math.max(1, width - MARK_W - TAIL);
49
+ const content = npmContent(view.packages, inner, null, ctx, TILE);
50
+ const headed = Boolean(content.titleLine) && content.lines.length > 0;
51
+ const rows = headed ? content.lines.slice(1) : content.lines;
52
+ const reserved = headed ? 1 : 0;
53
+ const last = Math.max(0, rows.length - 1);
54
+ const cursor = Math.min(view.packagesCursor ?? 0, last);
55
+ const pane = {
56
+ width,
57
+ color,
58
+ bodyH: Math.max(1, bodyH - reserved),
59
+ headerLines: headerLines + reserved,
60
+ offset: view.listScroll,
61
+ };
62
+ const paintRow = (line, selected) =>
63
+ paintMarginRow(line, width, color, selected);
64
+ const editing = view.compose && view.compose.kind === 'package';
65
+ let extra = null;
66
+ if (editing) {
67
+ const painted = paintPackageEdit(view.compose, width, color);
68
+ const at = 'index';
69
+ extra = { ...painted, at, index: cursor };
70
+ }
71
+ const painted = paintCursorList(rows, cursor, pane, paintRow, extra);
72
+ if (!headed) return painted;
73
+ const head = paintMarginRow(content.lines[0], width, color, false);
74
+ const body = painted.body.slice();
75
+ const pad = pane.bodyH >= LIST_PAD * 2 + 1 ? LIST_PAD : 0;
76
+ body.splice(pad, 0, head);
77
+ const nudge = (point) => {
78
+ if (!point || point.row < pad) return point;
79
+ return { ...point, row: point.row + 1 };
80
+ };
81
+ return {
82
+ ...painted,
83
+ body,
84
+ editHits: painted.editHits.map(nudge),
85
+ cursor: nudge(painted.cursor),
86
+ };
87
+ };
88
+
89
+ module.exports = { paintBodyPackages };
@@ -1,10 +1,10 @@
1
1
  'use strict';
2
2
 
3
- const ansi = require('../ansi.js');
4
- const actions = require('../session/actions.js');
3
+ const { clamp, trimText } = require('../common/utilities.js');
4
+ const ansi = require('../term/ansi.js');
5
+ const { LIST_PANES } = require('../input/actions.js');
5
6
 
6
7
  const { THEME, paint, visibleWidth, graphemes, graphemeWidth } = ansi;
7
- const { LIST_PANES } = actions;
8
8
 
9
9
  const GUTTER_W = 2;
10
10
  const FILE_MARK = '▶';
@@ -17,11 +17,14 @@ const fill = (width, fgRgb, bgRgb, color) =>
17
17
 
18
18
  const markPrefix = (selected) => ` ${selected ? FILE_MARK : ' '} `;
19
19
 
20
- const isListPane = (pane) => LIST_PANES.includes(pane) || pane === 'dashboard';
20
+ const isListPane = (pane) => {
21
+ if (LIST_PANES.includes(pane)) return true;
22
+ return pane === 'dashboard' || pane === 'repos';
23
+ };
21
24
 
22
25
  const isTaskView = (view) => view.item?.origin === 'task';
23
26
 
24
- const branchLabel = (name) => `${name ?? ''}`.trim();
27
+ const branchLabel = (name) => trimText(name);
25
28
 
26
29
  const padVisible = (text, width, align) => {
27
30
  const gap = ' '.repeat(Math.max(0, width - visibleWidth(text)));
@@ -45,6 +48,11 @@ const paintBodyFill = (width, color, kind) => {
45
48
  return ctx(leftW) + paintSplitRule(color) + ctx(rightW);
46
49
  };
47
50
 
51
+ const lazyRows = (length, paint) => ({ length, paint });
52
+
53
+ const bodyRow = (body, index) =>
54
+ Array.isArray(body) ? body[index] : body.paint(index);
55
+
48
56
  const paneResult = (fields = {}) => ({
49
57
  body: fields.body ?? [],
50
58
  fileHits: fields.fileHits ?? [],
@@ -54,6 +62,7 @@ const paneResult = (fields = {}) => ({
54
62
  splitBody: fields.splitBody === true,
55
63
  listScroll: fields.listScroll ?? 0,
56
64
  editHits: fields.editHits ?? [],
65
+ scrollBar: fields.scrollBar ?? null,
57
66
  });
58
67
 
59
68
  const editSpan = (x0, textX, x1, start, text) => ({
@@ -78,7 +87,7 @@ const listWindowStart = (focus, offset, listH, count) => {
78
87
  let start = offset ?? 0;
79
88
  if (focus < start) start = focus;
80
89
  if (focus >= start + listH) start = focus - listH + 1;
81
- return Math.max(0, Math.min(start, count - listH));
90
+ return clamp(start, 0, count - listH);
82
91
  };
83
92
 
84
93
  const rowCursor = (cursor, row) => {
@@ -89,10 +98,11 @@ const rowCursor = (cursor, row) => {
89
98
 
90
99
  const paintListWindow = (count, focus, pane, lineAt) => {
91
100
  const { width, color, bodyH, headerLines, offset } = pane;
92
- const pad = bodyH >= LIST_PAD * 2 + 1 ? LIST_PAD : 0;
101
+ const padded = bodyH >= LIST_PAD * 2 + 1 ? LIST_PAD : 0;
102
+ const pad = pane.pad === 0 ? 0 : padded;
93
103
  const listH = bodyH - pad * 2;
94
104
  const start = listWindowStart(focus, offset, listH, count);
95
- const blank = paintBodyFill(width, color, 'files');
105
+ const blank = pane.blank ?? paintBodyFill(width, color, 'files');
96
106
  const body = pad ? [blank] : [];
97
107
  const fileHits = [];
98
108
  const editHits = [];
@@ -104,18 +114,38 @@ const paintListWindow = (count, focus, pane, lineAt) => {
104
114
  if (line.cursor) cursor = rowCursor(line.cursor, row);
105
115
  for (const edit of line.edits) editHits.push({ ...edit, row });
106
116
  if (line.hit) {
107
- fileHits.push({ y: headerLines + pad + i + 1, cursor: line.index });
117
+ const y = headerLines + pad + i + 1;
118
+ for (const span of line.spans ?? []) {
119
+ fileHits.push({ y, cursor: line.index, ...span });
120
+ }
121
+ fileHits.push({ y, cursor: line.index });
108
122
  }
109
123
  }
110
124
  if (pad) body.push(blank);
111
125
  return paneResult({ body, fileHits, editHits, cursor, listScroll: start });
112
126
  };
113
127
 
128
+ const insertedAt = (extra, count) => {
129
+ if (extra.at === 'start') return 0;
130
+ if (extra.at !== 'index') return count;
131
+ const index = extra.index ?? count;
132
+ return clamp(index, 0, count);
133
+ };
134
+
135
+ const lineFromPaint = (painted, index) => {
136
+ if (typeof painted === 'string') return listLine(painted, index);
137
+ const line = listLine(painted.row, index);
138
+ if (painted.hit === false) line.hit = false;
139
+ if (painted.spans) line.spans = painted.spans;
140
+ return line;
141
+ };
142
+
114
143
  const paintCursorList = (items, cursor, pane, paintRow, extra = null) => {
115
144
  const lineAt = (index) => {
116
145
  const entry = items[index];
117
146
  if (!entry) return null;
118
- return listLine(paintRow(entry, !extra && index === cursor), index);
147
+ const selected = !extra && index === cursor;
148
+ return lineFromPaint(paintRow(entry, selected, index), index);
119
149
  };
120
150
  if (!extra) return paintListWindow(items.length, cursor, pane, lineAt);
121
151
  const edit = listLine(extra.row, -1, extra.cursor, extra.edits);
@@ -124,10 +154,12 @@ const paintCursorList = (items, cursor, pane, paintRow, extra = null) => {
124
154
  const replaced = (i) => (i === at ? edit : lineAt(i));
125
155
  return paintListWindow(items.length, at, pane, replaced);
126
156
  }
127
- const first = extra.at === 'start';
128
- const at = first ? 0 : items.length;
129
- const shift = first ? 1 : 0;
130
- const inserted = (i) => (i === at ? edit : lineAt(i - shift));
157
+ const at = insertedAt(extra, items.length);
158
+ const inserted = (i) => {
159
+ if (i === at) return edit;
160
+ const index = i < at ? i : i - 1;
161
+ return lineAt(index);
162
+ };
131
163
  return paintListWindow(items.length + 1, at, pane, inserted);
132
164
  };
133
165
 
@@ -156,7 +188,7 @@ const sliceVisible = (text, skip, width) => {
156
188
 
157
189
  const fieldWindow = (text, cursor, scroll, width) => {
158
190
  if (width <= 0) return { text: '', col: 0, scroll: 0 };
159
- const at = Math.max(0, Math.min(cursor ?? 0, text.length));
191
+ const at = clamp(cursor ?? 0, 0, text.length);
160
192
  const col = visibleWidth(text.slice(0, at));
161
193
  let next = Math.max(0, scroll ?? 0);
162
194
  if (col < next) next = col;
@@ -206,10 +238,67 @@ const paintColumns = (parts, widths, fgRgb, bgRgb, color, tones) => {
206
238
  return text;
207
239
  };
208
240
 
241
+ const paintPathText = (text, fg, bg, color, bold, spans) => {
242
+ if (!spans.length) return paint(text, fg, bg, color, bold);
243
+ let out = '';
244
+ let at = 0;
245
+ for (const span of spans) {
246
+ if (span.start > at) {
247
+ out += paint(text.slice(at, span.start), fg, bg, color, bold);
248
+ }
249
+ const on = span.on === true;
250
+ const markFg = on ? THEME.searchOnFg : THEME.searchFg;
251
+ const markBg = on ? THEME.searchOnBg : THEME.searchBg;
252
+ const slice = text.slice(span.start, span.end);
253
+ out += paint(slice, markFg, markBg, color, true);
254
+ at = span.end;
255
+ }
256
+ if (at < text.length) out += paint(text.slice(at), fg, bg, color, bold);
257
+ return out;
258
+ };
259
+
260
+ const scrollThumb = (count, inner, start) => {
261
+ if (count <= inner || inner < 1) return null;
262
+ let size = Math.max(1, Math.round((inner * inner) / count));
263
+ if (inner > 1) size = Math.min(size, inner - 1);
264
+ const travel = inner - size;
265
+ const maxStart = count - inner;
266
+ const at = maxStart <= 0 ? 0 : Math.round((start * travel) / maxStart);
267
+ return { size, travel, maxStart, at };
268
+ };
269
+
270
+ const scrollMark = (index, count, inner, start) => {
271
+ const thumb = scrollThumb(count, inner, start);
272
+ if (!thumb) return '';
273
+ const end = thumb.at + thumb.size;
274
+ if (index >= thumb.at && index < end) return 'thumb';
275
+ return 'track';
276
+ };
277
+
278
+ const scrollAtThumb = (top, thumb) => {
279
+ if (!thumb || thumb.travel <= 0) return 0;
280
+ const at = clamp(top, 0, thumb.travel);
281
+ return Math.round((at * thumb.maxStart) / thumb.travel);
282
+ };
283
+
284
+ const placeHits = (hits, origin, width, side) => {
285
+ const placed = [];
286
+ for (const hit of hits) {
287
+ const x0 = (hit.x0 ?? 0) + origin;
288
+ const x1 = (hit.x1 ?? width) + origin;
289
+ placed.push({ ...hit, x0, x1, side });
290
+ }
291
+ return placed;
292
+ };
293
+
209
294
  module.exports = {
295
+ scrollThumb,
296
+ scrollMark,
297
+ scrollAtThumb,
298
+ placeHits,
210
299
  GUTTER_W,
211
- FILE_MARK,
212
300
  MARK_W,
301
+ LIST_PAD,
213
302
  fill,
214
303
  markPrefix,
215
304
  isListPane,
@@ -220,8 +309,11 @@ module.exports = {
220
309
  paintSplitRule,
221
310
  paintBodyFill,
222
311
  paneResult,
312
+ lazyRows,
313
+ bodyRow,
223
314
  editSpan,
224
315
  listLine,
316
+ listWindowStart,
225
317
  paintListWindow,
226
318
  paintCursorList,
227
319
  sliceVisible,
@@ -230,4 +322,5 @@ module.exports = {
230
322
  padColumns,
231
323
  joinColumns,
232
324
  paintColumns,
325
+ paintPathText,
233
326
  };
@@ -1,16 +1,11 @@
1
1
  'use strict';
2
2
 
3
- const chrome = require('./chrome.js');
4
- const notes = require('./notes.js');
5
- const diff = require('./diff.js');
6
- const present = require('./present.js');
7
- const frame = require('./frame.js');
8
-
9
- const { formatBusyStatus, headerText, layoutButtons } = chrome;
10
- const { todoBodyWidth, noteInnerWidth } = notes;
11
- const { paintDiffLine, codeInnerWidth } = diff;
12
- const { presentCursor, presentRows } = present;
13
- const { renderFrame } = frame;
3
+ const { formatBusyStatus } = require('./status.js');
4
+ const { headerText } = require('./header.js');
5
+ const { layoutButtons } = require('./footer.js');
6
+ const { todoBodyWidth, noteInnerWidth } = require('./notes.js');
7
+ const { paintDiffLine, codeInnerWidth, activeDigits } = require('./diff.js');
8
+ const { renderFrame, presentCursor, presentRows } = require('./frame.js');
14
9
 
15
10
  module.exports = {
16
11
  formatBusyStatus,
@@ -20,6 +15,7 @@ module.exports = {
20
15
  noteInnerWidth,
21
16
  paintDiffLine,
22
17
  codeInnerWidth,
18
+ activeDigits,
23
19
  renderFrame,
24
20
  presentCursor,
25
21
  presentRows,
@@ -0,0 +1,214 @@
1
+ 'use strict';
2
+
3
+ const { THEME } = require('../term/ansi.js');
4
+ const { paintBodyFill, paneResult } = require('./primitives.js');
5
+ const tiles = require('./tiles.js');
6
+ const { RUN_ICON } = tiles;
7
+ const { seg, spinner, equalGrid, pageWindow, placeEqualTiles } = tiles;
8
+ const { paintTile, composeBands, tileHits, pageFill } = tiles;
9
+ const { segmentsWidth, clipSegments, blinkTone } = tiles;
10
+ const { size } = require('../common/format.js');
11
+ const { isHot } = require('./table.js');
12
+
13
+ const REPO_MIN_H = 6;
14
+ const MARGIN_MIN_H = 12;
15
+ const EXT_LIMIT = 3;
16
+ const DIM_BACKS = {
17
+ head: THEME.dashDimHeadBg,
18
+ tile: THEME.dashDimTileBg,
19
+ row: THEME.dashDimRowBg,
20
+ };
21
+ const extName = (key) => {
22
+ const text = `${key ?? ''}`;
23
+ if (!text || text === '(none)') return 'none';
24
+ return text.startsWith('.') ? text.slice(1) : text;
25
+ };
26
+
27
+ const spin = (frame) => seg(spinner(frame), 'warn', true);
28
+
29
+ const rowPending = (row) => {
30
+ if (!row.filesReady || !row.commitsReady) return true;
31
+ if (!row.diffReady || !row.tasksReady) return true;
32
+ return !row.runsReady || !row.npmReady;
33
+ };
34
+
35
+ const fitAside = (left, right, width) => {
36
+ const rightW = segmentsWidth(right);
37
+ const room = Math.max(0, width - rightW - 1);
38
+ const clipped = clipSegments(left, room);
39
+ const gap = Math.max(1, width - segmentsWidth(clipped) - rightW);
40
+ return [...clipped, seg(' '.repeat(gap)), ...right];
41
+ };
42
+
43
+ const titleLine = (row, ctx, selected, width) => {
44
+ const hot = isHot(row.activityAt, ctx.now);
45
+ const tone = hot ? blinkTone(ctx.frame) : 'text';
46
+ const left = [seg(row.name, tone, selected || hot)];
47
+ if (hot) left.push(seg(' ●', tone, true));
48
+ const right = [];
49
+ if (rowPending(row)) right.push(spin(ctx.frame), seg(' '));
50
+ const label = row.filesReady ? size(row.bytes) : '…';
51
+ const sizeTone = row.filesReady ? 'text' : 'muted';
52
+ right.push(seg(label, sizeTone, row.filesReady));
53
+ return fitAside(left, right, width);
54
+ };
55
+
56
+ const extLine = (row, ctx) => {
57
+ if (!row.filesReady) return [spin(ctx.frame)];
58
+ const exts = row.exts.slice(0, EXT_LIMIT);
59
+ if (!exts.length) return [seg('no files', 'muted')];
60
+ const segs = [];
61
+ for (let i = 0; i < exts.length; i++) {
62
+ if (i) segs.push(seg(' '));
63
+ segs.push(seg(extName(exts[i].key), 'sha'));
64
+ segs.push(seg(` ${size(exts[i].bytes)}`, 'text'));
65
+ }
66
+ return segs;
67
+ };
68
+
69
+ const countValue = (ready, text, frame) =>
70
+ ready ? seg(text, 'text', true) : spin(frame);
71
+
72
+ const countLine = (row, ctx) => [
73
+ seg('tasks: ', 'muted'),
74
+ countValue(row.tasksReady, `${row.tasksDone}/${row.tasks}`, ctx.frame),
75
+ seg(' commits: ', 'muted'),
76
+ countValue(row.commitsReady, `${row.commits}`, ctx.frame),
77
+ ];
78
+
79
+ const resultNote = (run) => {
80
+ const result = run.result;
81
+ if (!result) return [];
82
+ if (typeof result.tests === 'number') {
83
+ const note = [seg(` ${result.tests}`, 'muted')];
84
+ if (result.failed) note.push(seg(` ${result.failed} fail`, 'error'));
85
+ return note;
86
+ }
87
+ if (typeof result.errors === 'number') {
88
+ return [seg(` ${result.errors} err`, result.errors ? 'error' : 'muted')];
89
+ }
90
+ return [];
91
+ };
92
+
93
+ const runLine = (row, ctx) => {
94
+ if (!row.runsReady) return [seg('run: ', 'muted'), spin(ctx.frame)];
95
+ const run = row.run;
96
+ if (!run) return [seg('run: ', 'muted'), seg('idle', 'muted')];
97
+ if (run.status === 'running') {
98
+ const seen = run.done + run.failed;
99
+ const total = run.expected > 0 ? `/${run.expected}` : '';
100
+ const fail = run.failed ? ` ${run.failed} fail` : '';
101
+ return [
102
+ seg(`${spinner(ctx.frame)} `, 'warn', true),
103
+ seg(run.name || 'run', 'text'),
104
+ seg(` ${seen}${total}${fail}`, 'warn'),
105
+ ];
106
+ }
107
+ const icon = RUN_ICON[run.status] ?? ['?', 'muted'];
108
+ return [
109
+ seg(`${icon[0]} `, icon[1], true),
110
+ seg(run.name || 'run', 'text'),
111
+ seg(` ${run.status}`, icon[1]),
112
+ ...resultNote(run),
113
+ ];
114
+ };
115
+
116
+ const npmLine = (row, ctx) => {
117
+ if (!row.npmReady) return [seg('deps: ', 'muted'), spin(ctx.frame)];
118
+ const npm = row.npm;
119
+ if (!npm || !npm.hasManifest) return [seg('no package.json', 'muted')];
120
+ const modules = npm.modules || { count: 0, bytes: 0 };
121
+ const segs = [
122
+ seg('deps: ', 'muted'),
123
+ seg(`${npm.deps}`, 'text'),
124
+ seg(' dev: ', 'muted'),
125
+ seg(`${npm.dev}`, 'text'),
126
+ seg(' all: ', 'muted'),
127
+ seg(`${modules.count}`, 'text'),
128
+ seg(' (', 'muted'),
129
+ seg(size(modules.bytes), 'text'),
130
+ seg(')', 'muted'),
131
+ ];
132
+ if (npm.audit) {
133
+ segs.push(
134
+ seg(' '),
135
+ seg('🚨 ', 'error'),
136
+ seg(`${npm.audit}`, 'error', true),
137
+ );
138
+ }
139
+ if (npm.outdated) {
140
+ segs.push(
141
+ seg(' '),
142
+ seg('⚠️ ', 'warn'),
143
+ seg(`${npm.outdated}`, 'warn', true),
144
+ );
145
+ }
146
+ return segs;
147
+ };
148
+
149
+ const diffLine = (row, ctx) => {
150
+ if (!row.diffReady) return [seg('diff: ', 'muted'), spin(ctx.frame)];
151
+ const diff = row.diff;
152
+ const added = diff.stagedAdded + diff.unstagedAdded;
153
+ const removed = diff.stagedRemoved + diff.unstagedRemoved;
154
+ return [
155
+ seg('diff: ', 'muted'),
156
+ seg(`+${diff.stagedAdded}/${added}`, 'add'),
157
+ seg(' '),
158
+ seg(`-${diff.stagedRemoved}/${removed}`, 'del'),
159
+ ];
160
+ };
161
+
162
+ const repoContent = (row, ctx, selected, width) => ({
163
+ titleLine: titleLine(row, ctx, selected, width),
164
+ lines: [
165
+ extLine(row, ctx),
166
+ countLine(row, ctx),
167
+ runLine(row, ctx),
168
+ diffLine(row, ctx),
169
+ npmLine(row, ctx),
170
+ ],
171
+ });
172
+
173
+ const paintRepos = (model, bands, color) => {
174
+ const painted = new Map();
175
+ const ctx = { now: model.now, frame: model.frame };
176
+ const selected = model.tiles[model.cursor];
177
+ const selectedId = selected ? selected.id : '';
178
+ for (const band of bands) {
179
+ for (const place of band.tiles) {
180
+ const row = model.tiles.find((tile) => tile.id === place.id);
181
+ if (!row) continue;
182
+ const tile = { id: row.id, key: '', title: row.name };
183
+ const inner = Math.max(0, place.w - 2);
184
+ const picked = row.id === selectedId;
185
+ const content = repoContent(row, ctx, picked, inner);
186
+ const backs = picked ? null : DIM_BACKS;
187
+ const rows = paintTile(tile, content, place.w, band.h, color, backs);
188
+ painted.set(place.id, rows);
189
+ }
190
+ }
191
+ return painted;
192
+ };
193
+
194
+ const paintBodyRepos = (view, width, color, bodyH, headerLines) => {
195
+ const model = view.repos;
196
+ if (!model || !model.tiles.length) {
197
+ const blank = paintBodyFill(width, color, 'files');
198
+ return paneResult({ body: new Array(bodyH).fill(blank) });
199
+ }
200
+ const margin = bodyH >= MARGIN_MIN_H ? 1 : 0;
201
+ const height = Math.max(1, bodyH - margin * 2);
202
+ const grid = equalGrid(width, height, model.tiles.length, 34, REPO_MIN_H);
203
+ const page = pageWindow(model.tiles.length, model.cursor, grid.cap);
204
+ const shown = model.tiles.slice(page.start, page.end);
205
+ const bands = placeEqualTiles(shown, grid);
206
+ const painted = paintRepos(model, bands, color);
207
+ const rows = composeBands(bands, painted, width, height, color);
208
+ const pad = new Array(margin).fill(pageFill(width, color));
209
+ const body = [...pad, ...rows, ...pad];
210
+ const fileHits = tileHits(bands, headerLines + margin);
211
+ return { ...paneResult({ body, fileHits }), gridCols: grid.cols };
212
+ };
213
+
214
+ module.exports = { paintBodyRepos };