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.
- package/CHANGELOG.md +12 -1
- package/LICENSE +1 -1
- package/README.md +11 -11
- package/bin/reslop.js +3 -8
- package/lib/ansi.js +32 -19
- package/lib/capabilities.js +8 -8
- package/lib/cli.js +40 -74
- package/lib/clipboard.js +39 -47
- package/lib/dashboard/git.js +128 -0
- package/lib/dashboard/model.js +332 -0
- package/lib/dashboard/npm.js +193 -0
- package/lib/dashboard/tree.js +278 -0
- package/lib/deps-diff.js +197 -402
- package/lib/deps.js +113 -240
- package/lib/detect.js +8 -18
- package/lib/diff/align.js +41 -113
- package/lib/diff/diff.js +12 -43
- package/lib/diff/display.js +25 -36
- package/lib/diff/edit.js +6 -20
- package/lib/diff/inline.js +76 -144
- package/lib/diff/items.js +33 -47
- package/lib/diff/parse.js +28 -56
- package/lib/diff/patch.js +12 -33
- package/lib/diff/unit.js +79 -178
- package/lib/editor.js +52 -69
- package/lib/files.js +71 -89
- package/lib/git-branches.js +254 -416
- package/lib/git-deps.js +147 -272
- package/lib/git-reword-todo.js +5 -13
- package/lib/git-worktree.js +141 -212
- package/lib/git.js +21 -49
- package/lib/github.js +67 -181
- package/lib/gitlab.js +40 -90
- package/lib/highlight/bash.js +55 -177
- package/lib/highlight/core.js +155 -141
- package/lib/highlight/css.js +25 -58
- package/lib/highlight/csv.js +19 -37
- package/lib/highlight/dart.js +72 -213
- package/lib/highlight/dot.js +14 -14
- package/lib/highlight/html.js +42 -63
- package/lib/highlight/js.js +177 -506
- package/lib/highlight/json.js +17 -37
- package/lib/highlight/log.js +19 -25
- package/lib/highlight/txt.js +7 -10
- package/lib/highlight.js +17 -46
- package/lib/keys.js +49 -82
- package/lib/manifest.js +67 -126
- package/lib/npm-commands.js +120 -169
- package/lib/remote.js +140 -190
- package/lib/render/branches.js +63 -61
- package/lib/render/chrome.js +220 -333
- package/lib/render/commits.js +160 -361
- package/lib/render/dash-activity.js +212 -0
- package/lib/render/dash-blocks.js +245 -0
- package/lib/render/dash-table.js +290 -0
- package/lib/render/dashboard.js +62 -0
- package/lib/render/diff.js +123 -258
- package/lib/render/files.js +40 -85
- package/lib/render/frame.js +78 -117
- package/lib/render/notes.js +139 -210
- package/lib/render/npm.js +213 -177
- package/lib/render/present.js +4 -11
- package/lib/render/primitives.js +72 -174
- package/lib/render/render.js +5 -14
- package/lib/render/report.js +43 -0
- package/lib/render/tiles.js +260 -0
- package/lib/render/unit.js +15 -31
- package/lib/report-model.js +110 -33
- package/lib/report-parse.js +56 -37
- package/lib/report-render.js +102 -59
- package/lib/report-run.js +87 -77
- package/lib/report-test.js +221 -99
- package/lib/report-tools.js +96 -154
- package/lib/review-merge.js +33 -41
- package/lib/review.js +68 -129
- package/lib/runs.js +225 -0
- package/lib/select.js +53 -91
- package/lib/session/accessors.js +14 -21
- package/lib/session/actions.js +61 -43
- package/lib/session/branches.js +166 -222
- package/lib/session/change-actions.js +49 -119
- package/lib/session/commit.js +274 -355
- package/lib/session/compose.js +556 -714
- package/lib/session/dashboard.js +385 -0
- package/lib/session/dispatch.js +162 -170
- package/lib/session/files-pane.js +76 -44
- package/lib/session/items.js +6 -17
- package/lib/session/lifecycle.js +393 -466
- package/lib/session/load.js +1 -14
- package/lib/session/navigation.js +2 -2
- package/lib/session/npm.js +403 -420
- package/lib/session/ops.js +5 -13
- package/lib/session/pointer.js +16 -14
- package/lib/session/progress.js +39 -30
- package/lib/session/review.js +1 -3
- package/lib/session/tasks.js +276 -235
- package/lib/session/templates.js +51 -59
- package/lib/session/terminal.js +85 -132
- package/lib/session/view.js +47 -81
- package/lib/session/watch.js +196 -212
- package/lib/session.js +81 -161
- package/lib/update.js +44 -111
- package/lib/utilities.js +46 -88
- package/lib/wrap.js +29 -55
- package/package.json +1 -1
package/lib/render/primitives.js
CHANGED
|
@@ -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,
|
|
8
|
-
const {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
64
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
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
|
-
|
|
138
|
-
|
|
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
|
|
173
|
-
const
|
|
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, !
|
|
118
|
+
return listLine(paintRow(entry, !extra && index === cursor), index);
|
|
188
119
|
};
|
|
189
|
-
|
|
190
|
-
|
|
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(
|
|
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,
|
|
220
|
-
const col = visibleWidth(
|
|
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(
|
|
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
|
|
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
|
-
|
|
319
|
-
CHECK_LEAD,
|
|
212
|
+
MARK_W,
|
|
320
213
|
fill,
|
|
321
|
-
|
|
322
|
-
|
|
214
|
+
markPrefix,
|
|
215
|
+
isListPane,
|
|
216
|
+
isTaskView,
|
|
217
|
+
branchLabel,
|
|
323
218
|
padVisible,
|
|
324
219
|
splitWidths,
|
|
325
220
|
paintSplitRule,
|
|
326
221
|
paintBodyFill,
|
|
327
|
-
|
|
222
|
+
paneResult,
|
|
223
|
+
editSpan,
|
|
328
224
|
listLine,
|
|
329
225
|
paintListWindow,
|
|
330
226
|
paintCursorList,
|
|
331
|
-
|
|
227
|
+
sliceVisible,
|
|
332
228
|
fieldWindow,
|
|
333
|
-
|
|
334
|
-
|
|
229
|
+
measureColumns,
|
|
230
|
+
padColumns,
|
|
231
|
+
joinColumns,
|
|
232
|
+
paintColumns,
|
|
335
233
|
};
|
package/lib/render/render.js
CHANGED
|
@@ -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 {
|
|
14
|
-
const {
|
|
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
|
-
|
|
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
|
+
};
|