reslop 0.2.5 → 0.2.6

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 (105) hide show
  1. package/CHANGELOG.md +12 -1
  2. package/LICENSE +1 -1
  3. package/README.md +11 -11
  4. package/bin/reslop.js +3 -8
  5. package/lib/ansi.js +32 -19
  6. package/lib/capabilities.js +8 -8
  7. package/lib/cli.js +40 -74
  8. package/lib/clipboard.js +39 -47
  9. package/lib/dashboard/git.js +128 -0
  10. package/lib/dashboard/model.js +332 -0
  11. package/lib/dashboard/npm.js +193 -0
  12. package/lib/dashboard/tree.js +278 -0
  13. package/lib/deps-diff.js +197 -402
  14. package/lib/deps.js +113 -240
  15. package/lib/detect.js +8 -18
  16. package/lib/diff/align.js +41 -113
  17. package/lib/diff/diff.js +12 -43
  18. package/lib/diff/display.js +25 -36
  19. package/lib/diff/edit.js +6 -20
  20. package/lib/diff/inline.js +76 -144
  21. package/lib/diff/items.js +33 -47
  22. package/lib/diff/parse.js +28 -56
  23. package/lib/diff/patch.js +12 -33
  24. package/lib/diff/unit.js +79 -178
  25. package/lib/editor.js +52 -69
  26. package/lib/files.js +71 -89
  27. package/lib/git-branches.js +254 -416
  28. package/lib/git-deps.js +147 -272
  29. package/lib/git-reword-todo.js +5 -13
  30. package/lib/git-worktree.js +141 -212
  31. package/lib/git.js +21 -49
  32. package/lib/github.js +67 -181
  33. package/lib/gitlab.js +40 -90
  34. package/lib/highlight/bash.js +55 -177
  35. package/lib/highlight/core.js +155 -141
  36. package/lib/highlight/css.js +25 -58
  37. package/lib/highlight/csv.js +19 -37
  38. package/lib/highlight/dart.js +72 -213
  39. package/lib/highlight/dot.js +14 -14
  40. package/lib/highlight/html.js +42 -63
  41. package/lib/highlight/js.js +177 -506
  42. package/lib/highlight/json.js +17 -37
  43. package/lib/highlight/log.js +19 -25
  44. package/lib/highlight/txt.js +7 -10
  45. package/lib/highlight.js +17 -46
  46. package/lib/keys.js +49 -82
  47. package/lib/manifest.js +67 -126
  48. package/lib/npm-commands.js +120 -169
  49. package/lib/remote.js +140 -190
  50. package/lib/render/branches.js +63 -61
  51. package/lib/render/chrome.js +220 -333
  52. package/lib/render/commits.js +160 -361
  53. package/lib/render/dash-activity.js +212 -0
  54. package/lib/render/dash-blocks.js +245 -0
  55. package/lib/render/dash-table.js +290 -0
  56. package/lib/render/dashboard.js +62 -0
  57. package/lib/render/diff.js +123 -258
  58. package/lib/render/files.js +40 -85
  59. package/lib/render/frame.js +78 -117
  60. package/lib/render/notes.js +139 -210
  61. package/lib/render/npm.js +213 -177
  62. package/lib/render/present.js +4 -11
  63. package/lib/render/primitives.js +72 -174
  64. package/lib/render/render.js +5 -14
  65. package/lib/render/report.js +43 -0
  66. package/lib/render/tiles.js +260 -0
  67. package/lib/render/unit.js +15 -31
  68. package/lib/report-model.js +110 -33
  69. package/lib/report-parse.js +56 -37
  70. package/lib/report-render.js +102 -59
  71. package/lib/report-run.js +87 -77
  72. package/lib/report-test.js +221 -99
  73. package/lib/report-tools.js +96 -154
  74. package/lib/review-merge.js +33 -41
  75. package/lib/review.js +68 -129
  76. package/lib/runs.js +225 -0
  77. package/lib/select.js +53 -91
  78. package/lib/session/accessors.js +14 -21
  79. package/lib/session/actions.js +61 -43
  80. package/lib/session/branches.js +166 -222
  81. package/lib/session/change-actions.js +49 -119
  82. package/lib/session/commit.js +274 -355
  83. package/lib/session/compose.js +556 -714
  84. package/lib/session/dashboard.js +385 -0
  85. package/lib/session/dispatch.js +162 -170
  86. package/lib/session/files-pane.js +76 -44
  87. package/lib/session/items.js +6 -17
  88. package/lib/session/lifecycle.js +393 -466
  89. package/lib/session/load.js +1 -14
  90. package/lib/session/navigation.js +2 -2
  91. package/lib/session/npm.js +403 -420
  92. package/lib/session/ops.js +5 -13
  93. package/lib/session/pointer.js +16 -14
  94. package/lib/session/progress.js +39 -30
  95. package/lib/session/review.js +1 -3
  96. package/lib/session/tasks.js +276 -235
  97. package/lib/session/templates.js +51 -59
  98. package/lib/session/terminal.js +85 -132
  99. package/lib/session/view.js +47 -81
  100. package/lib/session/watch.js +196 -212
  101. package/lib/session.js +81 -161
  102. package/lib/update.js +44 -111
  103. package/lib/utilities.js +46 -88
  104. package/lib/wrap.js +29 -55
  105. package/package.json +1 -1
@@ -2,68 +2,30 @@
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) => LIST_PANES.includes(pane) || pane === 'dashboard';
38
21
 
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
- };
22
+ const isTaskView = (view) => view.item?.origin === 'task';
23
+
24
+ const branchLabel = (name) => `${name ?? ''}`.trim();
61
25
 
62
26
  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}`;
27
+ const gap = ' '.repeat(Math.max(0, width - visibleWidth(text)));
28
+ return align === 'end' ? `${text}${gap}` : `${gap}${text}`;
67
29
  };
68
30
 
69
31
  const splitWidths = (width) => {
@@ -75,49 +37,12 @@ const splitWidths = (width) => {
75
37
  const paintSplitRule = (color) =>
76
38
  fill(1, THEME.splitGutterFg, THEME.splitGutterBg, color);
77
39
 
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
40
  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;
41
+ const ctx = (n) => fill(n, THEME.ctxFg, THEME.ctxBg, color);
42
+ if (kind === 'files') return ctx(Math.max(1, width - 1)) + ctx(1);
43
+ if (kind !== 'split') return ctx(width);
44
+ const { leftW, rightW } = splitWidths(width);
45
+ return ctx(leftW) + paintSplitRule(color) + ctx(rightW);
121
46
  };
122
47
 
123
48
  const paneResult = (fields = {}) => ({
@@ -131,11 +56,29 @@ const paneResult = (fields = {}) => ({
131
56
  editHits: fields.editHits ?? [],
132
57
  });
133
58
 
134
- const NO_EDITS = [];
59
+ const editSpan = (x0, textX, x1, start, text) => ({
60
+ x0,
61
+ textX,
62
+ x1,
63
+ start,
64
+ text,
65
+ limit: start + text.length,
66
+ live: true,
67
+ });
135
68
 
136
- const listLine = (row, index, cursor = null, edits = NO_EDITS) => {
137
- const hit = index >= 0;
138
- return { row, index, hit, cursor, edits };
69
+ const listLine = (row, index, cursor = null, edits = NO_EDITS) => ({
70
+ row,
71
+ index,
72
+ hit: index >= 0,
73
+ cursor,
74
+ edits,
75
+ });
76
+
77
+ const listWindowStart = (focus, offset, listH, count) => {
78
+ let start = offset ?? 0;
79
+ if (focus < start) start = focus;
80
+ if (focus >= start + listH) start = focus - listH + 1;
81
+ return Math.max(0, Math.min(start, count - listH));
139
82
  };
140
83
 
141
84
  const rowCursor = (cursor, row) => {
@@ -150,11 +93,10 @@ const paintListWindow = (count, focus, pane, lineAt) => {
150
93
  const listH = bodyH - pad * 2;
151
94
  const start = listWindowStart(focus, offset, listH, count);
152
95
  const blank = paintBodyFill(width, color, 'files');
153
- const body = [];
96
+ const body = pad ? [blank] : [];
154
97
  const fileHits = [];
155
98
  const editHits = [];
156
99
  let cursor = null;
157
- if (pad) body.push(blank);
158
100
  for (let i = 0; i < listH; i++) {
159
101
  const line = lineAt(start + i);
160
102
  if (!line) break;
@@ -169,30 +111,29 @@ const paintListWindow = (count, focus, pane, lineAt) => {
169
111
  return paneResult({ body, fileHits, editHits, cursor, listScroll: start });
170
112
  };
171
113
 
172
- const editLine = (extraRow) => {
173
- const { row, cursor, edit, edits = [] } = extraRow;
174
- return listLine(row, -1, cursor, edit ? [...edits, edit] : edits);
175
- };
176
-
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;
114
+ const paintCursorList = (items, cursor, pane, paintRow, extra = null) => {
115
+ const lineAt = (index) => {
185
116
  const entry = items[index];
186
117
  if (!entry) return null;
187
- return listLine(paintRow(entry, !edit && index === cursor), index);
118
+ return listLine(paintRow(entry, !extra && index === cursor), index);
188
119
  };
189
- const focus = edit ? extraAt : cursor;
190
- return paintListWindow(items.length + extra, focus, pane, lineAt);
120
+ if (!extra) return paintListWindow(items.length, cursor, pane, lineAt);
121
+ const edit = listLine(extra.row, -1, extra.cursor, extra.edits);
122
+ if (extra.at === 'replace') {
123
+ const at = extra.index;
124
+ const replaced = (i) => (i === at ? edit : lineAt(i));
125
+ return paintListWindow(items.length, at, pane, replaced);
126
+ }
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));
131
+ return paintListWindow(items.length + 1, at, pane, inserted);
191
132
  };
192
133
 
193
134
  const sliceVisible = (text, skip, width) => {
194
135
  if (width <= 0) return '';
195
- const parts = graphemes(`${text ?? ''}`);
136
+ const parts = graphemes(text);
196
137
  let used = 0;
197
138
  let i = 0;
198
139
  const drop = Math.max(0, skip);
@@ -214,59 +155,16 @@ const sliceVisible = (text, skip, width) => {
214
155
  };
215
156
 
216
157
  const fieldWindow = (text, cursor, scroll, width) => {
217
- const raw = `${text ?? ''}`;
218
158
  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;
159
+ const at = Math.max(0, Math.min(cursor ?? 0, text.length));
160
+ const col = visibleWidth(text.slice(0, at));
161
+ let next = Math.max(0, scroll ?? 0);
223
162
  if (col < next) next = col;
224
163
  if (col >= next + width) next = col - width + 1;
225
- const shown = sliceVisible(raw, next, width);
164
+ const shown = sliceVisible(text, next, width);
226
165
  return { text: shown, col: col - next, scroll: next };
227
166
  };
228
167
 
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
168
  const measureColumns = (entries, rowFields, align) => {
271
169
  const keys = Object.keys(align);
272
170
  const widths = Object.fromEntries(keys.map((key) => [key, 0]));
@@ -282,7 +180,7 @@ const measureColumns = (entries, rowFields, align) => {
282
180
  const padColumns = (fields, widths, align) => {
283
181
  const parts = {};
284
182
  for (const key of Object.keys(align)) {
285
- const width = widths?.[key] ?? visibleWidth(fields[key]);
183
+ const width = widths[key] ?? visibleWidth(fields[key]);
286
184
  parts[key] = padVisible(fields[key], width, align[key]);
287
185
  }
288
186
  return parts;
@@ -309,27 +207,27 @@ const paintColumns = (parts, widths, fgRgb, bgRgb, color, tones) => {
309
207
  };
310
208
 
311
209
  module.exports = {
312
- measureColumns,
313
- padColumns,
314
- joinColumns,
315
- paintColumns,
316
210
  GUTTER_W,
317
211
  FILE_MARK,
318
- LIST_PANES,
319
- CHECK_LEAD,
212
+ MARK_W,
320
213
  fill,
321
- paintBar,
322
- paintCheckBar,
214
+ markPrefix,
215
+ isListPane,
216
+ isTaskView,
217
+ branchLabel,
323
218
  padVisible,
324
219
  splitWidths,
325
220
  paintSplitRule,
326
221
  paintBodyFill,
327
- windowRows,
222
+ paneResult,
223
+ editSpan,
328
224
  listLine,
329
225
  paintListWindow,
330
226
  paintCursorList,
331
- paintListEdit,
227
+ sliceVisible,
332
228
  fieldWindow,
333
- branchLabel,
334
- paneResult,
229
+ measureColumns,
230
+ padColumns,
231
+ joinColumns,
232
+ paintColumns,
335
233
  };
@@ -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,260 @@
1
+ 'use strict';
2
+
3
+ const ansi = require('../ansi.js');
4
+ const { THEME, paint, visibleWidth, truncateVisible } = ansi;
5
+
6
+ const GAP_X = 2;
7
+ const GAP_Y = 1;
8
+ const MIN_W = 34;
9
+ const MIN_H = 6;
10
+ const MAX_COLS = 4;
11
+ const RULE = '─';
12
+ const ELLIPSIS = '…';
13
+ const BAR_ON = '█';
14
+ const BAR_OFF = '░';
15
+ const SPARK = '▁▂▃▄▅▆▇█';
16
+ const SPINNER = '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏';
17
+
18
+ const seg = (text, tone = 'text', bold = false) => ({
19
+ text: `${text}`,
20
+ tone,
21
+ bold,
22
+ });
23
+
24
+ const toneColors = () => ({
25
+ text: THEME.chromeFg,
26
+ muted: THEME.mutedFg,
27
+ hot: THEME.buttonHotFg,
28
+ add: THEME.addLineFg,
29
+ del: THEME.delLineFg,
30
+ warn: THEME.warnFg,
31
+ error: THEME.errorFg,
32
+ sha: THEME.shaFg,
33
+ trend: THEME.dashTrendFg,
34
+ key: THEME.dashKeyFg,
35
+ });
36
+
37
+ const headColors = () => ({
38
+ ...toneColors(),
39
+ muted: THEME.dashHeadMuted,
40
+ del: THEME.dashDelFg,
41
+ });
42
+
43
+ const segmentsWidth = (segs) => {
44
+ let width = 0;
45
+ for (const item of segs) width += visibleWidth(item.text);
46
+ return width;
47
+ };
48
+
49
+ const clipSegments = (segs, width) => {
50
+ if (segmentsWidth(segs) <= width) {
51
+ return segs.map((item) => ({ ...item }));
52
+ }
53
+ const markW = visibleWidth(ELLIPSIS);
54
+ const budget = markW <= width ? width - markW : width;
55
+ const out = [];
56
+ let used = 0;
57
+ for (const item of segs) {
58
+ const room = budget - used;
59
+ if (room <= 0) break;
60
+ const text = truncateVisible(item.text, room);
61
+ if (!text) continue;
62
+ used += visibleWidth(text);
63
+ out.push({ ...item, text });
64
+ if (visibleWidth(text) < visibleWidth(item.text)) break;
65
+ }
66
+ if (markW > width) return out;
67
+ if (!out.length) return [seg(ELLIPSIS)];
68
+ out[out.length - 1].text += ELLIPSIS;
69
+ return out;
70
+ };
71
+
72
+ const paintSegments = (segs, width, bg, color, fill = ' ', colors = null) => {
73
+ const clipped = clipSegments(segs, width);
74
+ const pad = Math.max(0, width - segmentsWidth(clipped));
75
+ const ink = colors ?? toneColors();
76
+ if (!color) {
77
+ return clipped.map((item) => item.text).join('') + fill.repeat(pad);
78
+ }
79
+ let out = '';
80
+ for (const item of clipped) {
81
+ out += paint(item.text, ink[item.tone], bg, color, item.bold);
82
+ }
83
+ return out + paint(fill.repeat(pad), ink.muted, bg, color);
84
+ };
85
+
86
+ const barSegments = (ratio, width, tone = 'add') => {
87
+ const clamped = Math.min(1, Math.max(0, Number.isFinite(ratio) ? ratio : 0));
88
+ const on = Math.round(clamped * width);
89
+ return [
90
+ seg(BAR_ON.repeat(on), tone),
91
+ seg(BAR_OFF.repeat(width - on), 'muted'),
92
+ ];
93
+ };
94
+
95
+ const sparkline = (values, width) => {
96
+ const count = values.length;
97
+ if (!count || width < 1) return '';
98
+ const max = Math.max(...values);
99
+ const top = SPARK.length - 1;
100
+ let out = '';
101
+ for (let i = 0; i < width; i++) {
102
+ const span = width <= 1 ? count - 1 : (i * (count - 1)) / (width - 1);
103
+ const at = Math.round(span);
104
+ const level = max > 0 ? Math.round((values[at] / max) * top) : 0;
105
+ out += SPARK[level];
106
+ }
107
+ return out;
108
+ };
109
+
110
+ const spinner = (frame) => SPINNER[Math.abs(frame ?? 0) % SPINNER.length];
111
+
112
+ const spread = (total, parts) => {
113
+ const base = Math.floor(total / parts);
114
+ const extra = total % parts;
115
+ return Array.from({ length: parts }, (_, i) => base + (i < extra ? 1 : 0));
116
+ };
117
+
118
+ const placeBand = (shown, from, size, width) => {
119
+ const room = Math.max(0, width - (size - 1) * GAP_X);
120
+ const widths = spread(room, size);
121
+ const tiles = [];
122
+ let x = 0;
123
+ for (let i = 0; i < size; i++) {
124
+ tiles.push({ id: shown[from + i].id, x, w: widths[i] });
125
+ x += widths[i] + GAP_X;
126
+ }
127
+ return tiles;
128
+ };
129
+
130
+ const placeTiles = (shown, cols, width, height) => {
131
+ const rows = Math.ceil(shown.length / cols);
132
+ const sizes = spread(shown.length, rows);
133
+ const heights = spread(height - (rows - 1) * GAP_Y, rows);
134
+ const bands = [];
135
+ let from = 0;
136
+ let y = 0;
137
+ for (let row = 0; row < rows; row++) {
138
+ const tiles = placeBand(shown, from, sizes[row], width);
139
+ bands.push({ y, h: Math.max(1, heights[row]), tiles });
140
+ from += sizes[row];
141
+ y += heights[row] + GAP_Y;
142
+ }
143
+ return bands;
144
+ };
145
+
146
+ const layoutTiles = (width, height, tiles) => {
147
+ const cap = Math.floor((width + GAP_X) / (MIN_W + GAP_X));
148
+ const maxCols = Math.max(1, Math.min(MAX_COLS, cap));
149
+ const ranked = [...tiles].sort((left, right) => left.rank - right.rank);
150
+ for (let count = tiles.length; count >= 1; count--) {
151
+ const keep = new Set(ranked.slice(0, count).map((tile) => tile.id));
152
+ const shown = tiles.filter((tile) => keep.has(tile.id));
153
+ const cols = Math.min(maxCols, count);
154
+ const rows = Math.ceil(count / cols);
155
+ const rowH = Math.floor((height - (rows - 1) * GAP_Y) / rows);
156
+ if (rowH >= MIN_H || count === 1) {
157
+ return placeTiles(shown, cols, width, height);
158
+ }
159
+ }
160
+ return [];
161
+ };
162
+
163
+ const titleSegments = (tile, badge = []) => {
164
+ const keyed = tile.title.startsWith(tile.key);
165
+ const rest = keyed ? tile.title.slice(tile.key.length) : ` ${tile.title}`;
166
+ const segs = [seg(tile.key, 'key', true), seg(rest)];
167
+ return badge.length ? [...segs, seg(' '), ...badge] : segs;
168
+ };
169
+
170
+ const paintFramed = (line, width, bg, color, fill = ' ', colors = null) => {
171
+ const edge = paintSegments([seg(' ')], 1, bg, color);
172
+ const inner = Math.max(0, width - 2);
173
+ return edge + paintSegments(line, inner, bg, color, fill, colors) + edge;
174
+ };
175
+
176
+ const paintHead = (tile, content, width, color) => {
177
+ const fill = color ? ' ' : RULE;
178
+ const bg = THEME.dashHeadBg;
179
+ const colors = headColors();
180
+ if (content.titleLine) {
181
+ return paintFramed(content.titleLine, width, bg, color, fill, colors);
182
+ }
183
+ const segs = [seg(' '), ...titleSegments(tile, content.badge)];
184
+ return paintSegments(segs, width, bg, color, fill, colors);
185
+ };
186
+
187
+ const rowBackground = (index, filled) => {
188
+ if (filled && index % 2 === 1) return THEME.dashRowBg;
189
+ return THEME.dashTileBg;
190
+ };
191
+
192
+ const paintTile = (tile, content, width, height, color) => {
193
+ const { lines, footer } = content;
194
+ const rows = [paintHead(tile, content, width, color)];
195
+ const bodyRoom = footer ? height - 2 : height - 1;
196
+ for (let i = 0; i < bodyRoom; i++) {
197
+ const bg = rowBackground(i, i < lines.length);
198
+ rows.push(paintFramed(lines[i] ?? [], width, bg, color));
199
+ }
200
+ if (footer && height > 1) {
201
+ const bg = rowBackground(bodyRoom, true);
202
+ rows.push(paintFramed(footer, width, bg, color));
203
+ }
204
+ return rows;
205
+ };
206
+
207
+ const pageFill = (width, color) =>
208
+ paint(' '.repeat(Math.max(0, width)), THEME.chromeFg, THEME.dashBg, color);
209
+
210
+ const composeBands = (bands, painted, width, height, color) => {
211
+ const rows = [];
212
+ const blank = pageFill(width, color);
213
+ for (const band of bands) {
214
+ if (rows.length) {
215
+ for (let gap = 0; gap < GAP_Y; gap++) rows.push(blank);
216
+ }
217
+ const last = band.tiles.at(-1);
218
+ const rest = pageFill(width - last.x - last.w, color);
219
+ for (let line = 0; line < band.h; line++) {
220
+ let row = '';
221
+ for (let i = 0; i < band.tiles.length; i++) {
222
+ if (i) row += pageFill(GAP_X, color);
223
+ row += painted.get(band.tiles[i].id)[line];
224
+ }
225
+ rows.push(row + rest);
226
+ }
227
+ }
228
+ while (rows.length < height) rows.push(blank);
229
+ return rows.slice(0, height);
230
+ };
231
+
232
+ const tileHits = (bands, headerLines) => {
233
+ const hits = [];
234
+ for (const band of bands) {
235
+ for (const tile of band.tiles) {
236
+ for (let line = 0; line < band.h; line++) {
237
+ const y = headerLines + band.y + line + 1;
238
+ hits.push({ y, x0: tile.x, x1: tile.x + tile.w, cursor: tile.id });
239
+ }
240
+ }
241
+ }
242
+ return hits;
243
+ };
244
+
245
+ module.exports = {
246
+ GAP_X,
247
+ GAP_Y,
248
+ seg,
249
+ segmentsWidth,
250
+ clipSegments,
251
+ barSegments,
252
+ sparkline,
253
+ spinner,
254
+ titleSegments,
255
+ layoutTiles,
256
+ paintTile,
257
+ composeBands,
258
+ tileHits,
259
+ pageFill,
260
+ };