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.
- package/CHANGELOG.md +22 -1
- package/LICENSE +1 -1
- package/README.md +11 -11
- package/bin/reslop.js +3 -8
- package/lib/ansi.js +72 -19
- package/lib/api.js +1 -1
- package/lib/capabilities.js +8 -8
- package/lib/cli.js +61 -75
- package/lib/clipboard.js +39 -47
- package/lib/dashboard/git.js +128 -0
- package/lib/dashboard/model.js +357 -0
- package/lib/dashboard/npm.js +290 -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 +79 -91
- package/lib/find.js +61 -0
- package/lib/git-branches.js +254 -416
- package/lib/git-deps.js +148 -272
- package/lib/git-reword-todo.js +5 -13
- package/lib/git-worktree.js +161 -214
- 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 +75 -83
- 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 +412 -347
- package/lib/render/commits.js +160 -361
- package/lib/render/dash-activity.js +212 -0
- package/lib/render/dash-blocks.js +496 -0
- package/lib/render/dash-table.js +295 -0
- package/lib/render/dashboard.js +62 -0
- package/lib/render/diff.js +123 -258
- package/lib/render/files.js +60 -86
- package/lib/render/frame.js +82 -117
- package/lib/render/notes.js +150 -210
- package/lib/render/npm.js +213 -177
- package/lib/render/packages.js +93 -0
- package/lib/render/present.js +4 -11
- package/lib/render/primitives.js +77 -172
- package/lib/render/render.js +5 -14
- package/lib/render/report.js +43 -0
- package/lib/render/repos.js +226 -0
- package/lib/render/search.js +25 -0
- package/lib/render/tiles.js +355 -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 +69 -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 +189 -65
- package/lib/session/branches.js +164 -222
- package/lib/session/change-actions.js +49 -119
- package/lib/session/commit.js +298 -352
- package/lib/session/compose.js +572 -715
- package/lib/session/dashboard.js +530 -0
- package/lib/session/dispatch.js +197 -171
- package/lib/session/files-pane.js +92 -45
- package/lib/session/find.js +94 -0
- package/lib/session/items.js +6 -17
- package/lib/session/lifecycle.js +420 -466
- package/lib/session/load.js +1 -14
- package/lib/session/navigation.js +4 -2
- package/lib/session/npm.js +403 -420
- package/lib/session/ops.js +5 -13
- package/lib/session/packages.js +143 -0
- package/lib/session/pointer.js +30 -14
- package/lib/session/progress.js +39 -30
- package/lib/session/review.js +1 -3
- package/lib/session/tasks.js +277 -236
- package/lib/session/templates.js +51 -59
- package/lib/session/terminal.js +85 -132
- package/lib/session/view.js +59 -80
- package/lib/session/watch.js +201 -212
- package/lib/session/workspace.js +455 -0
- package/lib/session.js +95 -161
- package/lib/update.js +44 -111
- package/lib/utilities.js +46 -88
- package/lib/workspace.js +166 -0
- package/lib/wrap.js +29 -55
- package/package.json +1 -1
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const tiles = require('./tiles.js');
|
|
4
|
+
const { seg, spinner } = tiles;
|
|
5
|
+
const table = require('./dash-table.js');
|
|
6
|
+
const { ago, duration, isHot, waiting, cell, cellSegs, flexCell } = table;
|
|
7
|
+
const { tableLines, stat, pairRows, titleAside, captionOver } = table;
|
|
8
|
+
|
|
9
|
+
const NAME_MAX = 18;
|
|
10
|
+
const RUN_ICON = {
|
|
11
|
+
passed: ['✔', 'add'],
|
|
12
|
+
failed: ['✖', 'error'],
|
|
13
|
+
aborted: ['⊘', 'muted'],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const blankCell = () => cell('', 'muted', 'r');
|
|
17
|
+
|
|
18
|
+
const ageCell = (at, now) => cell(at ? ago(at, now) : '', 'muted', 'r');
|
|
19
|
+
|
|
20
|
+
const rebaseStep = (rebase) => {
|
|
21
|
+
const total = rebase.total ? `/${rebase.total}` : '';
|
|
22
|
+
return `${rebase.step || '?'}${total}`;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const commitRow = (entry, ctx, hot) => [
|
|
26
|
+
cell(entry.short, hot ? 'warn' : 'sha', 'l', true),
|
|
27
|
+
flexCell(entry.subject, hot ? 'warn' : 'text'),
|
|
28
|
+
ageCell(entry.at, ctx.now),
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const commitsBlock = (model, width, height, ctx, tile) => {
|
|
32
|
+
const data = model.commits;
|
|
33
|
+
if (!data.ready) return waiting('reading git…');
|
|
34
|
+
const { total, recent } = data.info;
|
|
35
|
+
const badge = [];
|
|
36
|
+
if (data.rebase) {
|
|
37
|
+
const step = rebaseStep(data.rebase);
|
|
38
|
+
badge.push(seg(`⟳ rebase ${step}`, 'warn', true));
|
|
39
|
+
}
|
|
40
|
+
const titleLine = titleAside(tile, [seg(`${total}`, 'muted')], width, badge);
|
|
41
|
+
if (!recent.length) return { titleLine, lines: [[seg('none', 'muted')]] };
|
|
42
|
+
const hot = isHot(data.changedAt, ctx.now);
|
|
43
|
+
const shown = recent.slice(0, Math.max(0, height));
|
|
44
|
+
const rows = shown.map((entry, i) => commitRow(entry, ctx, hot && i === 0));
|
|
45
|
+
return { titleLine, lines: tableLines(rows, width) };
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const trackText = (entry) => {
|
|
49
|
+
if (entry.gone) return 'gone';
|
|
50
|
+
if (!entry.upstream) return '';
|
|
51
|
+
const ahead = entry.ahead ? `↑${entry.ahead}` : '';
|
|
52
|
+
const behind = entry.behind ? `↓${entry.behind}` : '';
|
|
53
|
+
return ahead + behind || '=';
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const branchOrder = (list) =>
|
|
57
|
+
[...list].sort((left, right) => {
|
|
58
|
+
if (left.current !== right.current) return left.current ? -1 : 1;
|
|
59
|
+
return right.at - left.at || left.name.localeCompare(right.name, 'en');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const blinkTone = (frame) => (Math.abs(frame) % 2 ? 'hot' : 'warn');
|
|
63
|
+
|
|
64
|
+
const branchRow = (entry, data, ctx) => {
|
|
65
|
+
const hot = isHot(data.hot.get(entry.name) ?? 0, ctx.now);
|
|
66
|
+
const tone = hot ? blinkTone(ctx.frame) : 'text';
|
|
67
|
+
const mark = entry.current ? '▶' : ' ';
|
|
68
|
+
const name = entry.name.slice(0, NAME_MAX);
|
|
69
|
+
const track = trackText(entry);
|
|
70
|
+
const trackTone = entry.behind || entry.gone ? 'error' : 'warn';
|
|
71
|
+
return [
|
|
72
|
+
cellSegs([
|
|
73
|
+
seg(mark, 'sha', true),
|
|
74
|
+
seg(` ${name}`, tone, entry.current || hot),
|
|
75
|
+
]),
|
|
76
|
+
cell(track, track === '=' ? 'muted' : trackTone),
|
|
77
|
+
flexCell(entry.subject),
|
|
78
|
+
ageCell(entry.at, ctx.now),
|
|
79
|
+
];
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const branchExtras = (data, ctx) => {
|
|
83
|
+
const rows = [];
|
|
84
|
+
if (data.rebase) {
|
|
85
|
+
const step = cell(rebaseStep(data.rebase), 'warn', 'l', true);
|
|
86
|
+
rows.push([cell('rebase', 'muted'), step]);
|
|
87
|
+
}
|
|
88
|
+
const last = data.switches[0];
|
|
89
|
+
if (last) {
|
|
90
|
+
rows.push([
|
|
91
|
+
cell('switched', 'muted'),
|
|
92
|
+
flexCell(`${last.from} → ${last.to}`),
|
|
93
|
+
ageCell(last.at, ctx.now),
|
|
94
|
+
]);
|
|
95
|
+
}
|
|
96
|
+
return rows;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const branchesBlock = (model, width, height, ctx, tile) => {
|
|
100
|
+
const data = model.branches;
|
|
101
|
+
if (!data.ready) return waiting('reading git…');
|
|
102
|
+
const badge = data.rebase ? [seg('⟳', 'warn', true)] : [];
|
|
103
|
+
const aside = [seg(`${data.list.length}`, 'muted')];
|
|
104
|
+
const titleLine = titleAside(tile, aside, width, badge);
|
|
105
|
+
if (!data.list.length) {
|
|
106
|
+
return { titleLine, lines: [[seg('no branches', 'muted')]] };
|
|
107
|
+
}
|
|
108
|
+
const extras = branchExtras(data, ctx);
|
|
109
|
+
const room = Math.max(1, height - extras.length);
|
|
110
|
+
const shown = branchOrder(data.list).slice(0, room);
|
|
111
|
+
const rows = shown.map((entry) => branchRow(entry, data, ctx));
|
|
112
|
+
const lines = tableLines(rows, width);
|
|
113
|
+
const fits = extras.slice(0, Math.max(0, height - lines.length));
|
|
114
|
+
return { titleLine, lines: [...lines, ...tableLines(fits, width)] };
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const runIcon = (run, ctx) => {
|
|
118
|
+
if (run.status === 'running') return [spinner(ctx.frame), 'warn'];
|
|
119
|
+
return RUN_ICON[run.status] ?? ['?', 'muted'];
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const countCell = (value, tone) => {
|
|
123
|
+
if (typeof value !== 'number') return blankCell();
|
|
124
|
+
return cell(`${value}`, tone, 'r');
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const failTone = (value) => (value ? 'error' : 'muted');
|
|
128
|
+
|
|
129
|
+
const testMetrics = (done, ok, fail, total) => [
|
|
130
|
+
countCell(done, 'text'),
|
|
131
|
+
countCell(ok, ok ? 'add' : 'muted'),
|
|
132
|
+
countCell(fail, failTone(fail)),
|
|
133
|
+
countCell(total, 'text'),
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
const runMetrics = (run) => {
|
|
137
|
+
const result = run.result;
|
|
138
|
+
if (result && typeof result.tests === 'number') {
|
|
139
|
+
const fail = result.failed ?? 0;
|
|
140
|
+
const ok = result.passed ?? Math.max(0, result.tests - fail);
|
|
141
|
+
return testMetrics(result.tests, ok, fail, result.tests);
|
|
142
|
+
}
|
|
143
|
+
const live = run.status === 'running' && run.source !== 'reslop';
|
|
144
|
+
if (!live) return [blankCell(), blankCell(), blankCell(), blankCell()];
|
|
145
|
+
const total = run.expected > 0 ? run.expected : null;
|
|
146
|
+
return testMetrics(run.done + run.failed, run.done, run.failed, total);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const lintNote = (run) => {
|
|
150
|
+
const result = run.result;
|
|
151
|
+
if (!result || typeof result.tests === 'number') return [];
|
|
152
|
+
const { errors, warnings } = result;
|
|
153
|
+
if (typeof errors !== 'number') return [];
|
|
154
|
+
const note = [seg(` ${errors} err`, failTone(errors))];
|
|
155
|
+
if (typeof warnings !== 'number') return note;
|
|
156
|
+
note.push(seg(` ${warnings} warn`, warnings ? 'warn' : 'muted'));
|
|
157
|
+
return note;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const durationCell = (run, ctx) => {
|
|
161
|
+
const running = run.status === 'running';
|
|
162
|
+
const end = running ? ctx.now : run.endedAt;
|
|
163
|
+
if (!run.startedAt || !end) return blankCell();
|
|
164
|
+
return cell(duration(end - run.startedAt), running ? 'warn' : 'muted', 'r');
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const named = (icon, tone, name, nameTone) => ({
|
|
168
|
+
segs: [seg(icon, tone, true), seg(' '), seg(name, nameTone)],
|
|
169
|
+
align: 'l',
|
|
170
|
+
flex: true,
|
|
171
|
+
min: 1,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const runHeader = () => [
|
|
175
|
+
cell(''),
|
|
176
|
+
cell('done', 'muted', 'r'),
|
|
177
|
+
cell('ok', 'muted', 'r'),
|
|
178
|
+
cell('fail', 'muted', 'r'),
|
|
179
|
+
cell('total', 'muted', 'r'),
|
|
180
|
+
cell('duration', 'muted', 'r'),
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
const runRow = (run, ctx) => {
|
|
184
|
+
const icon = runIcon(run, ctx);
|
|
185
|
+
const tone = run.status === 'running' ? 'text' : 'muted';
|
|
186
|
+
const label = named(icon[0], icon[1], run.name, tone);
|
|
187
|
+
label.segs.push(...lintNote(run));
|
|
188
|
+
return [label, ...runMetrics(run), durationCell(run, ctx)];
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const runsBlock = (model, width, height, ctx, tile) => {
|
|
192
|
+
const rows = [];
|
|
193
|
+
if (model.busy) {
|
|
194
|
+
rows.push([named(spinner(ctx.frame), 'warn', model.busy, 'warn')]);
|
|
195
|
+
}
|
|
196
|
+
for (const run of model.runs) rows.push(runRow(run, ctx));
|
|
197
|
+
if (!rows.length) {
|
|
198
|
+
const items = [
|
|
199
|
+
stat('status', 'idle', 'muted'),
|
|
200
|
+
stat('usage', 'reslop t -- <command>', 'muted'),
|
|
201
|
+
];
|
|
202
|
+
return { badge: [], lines: tableLines(pairRows(items, 1), width) };
|
|
203
|
+
}
|
|
204
|
+
const shown = rows.slice(0, Math.max(1, height));
|
|
205
|
+
const lines = tableLines([runHeader(), ...shown], width);
|
|
206
|
+
return {
|
|
207
|
+
titleLine: captionOver(tile, lines[0]),
|
|
208
|
+
lines: lines.slice(1),
|
|
209
|
+
};
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
module.exports = { commitsBlock, branchesBlock, runsBlock, runMetrics };
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const files = require('../files.js');
|
|
4
|
+
const { shortAge } = files;
|
|
5
|
+
const npmInfo = require('../dashboard/npm.js');
|
|
6
|
+
const { TOP_PACKAGES } = npmInfo;
|
|
7
|
+
const utilities = require('../utilities.js');
|
|
8
|
+
const { parseVersion, cmpVersion } = utilities;
|
|
9
|
+
const ansi = require('../ansi.js');
|
|
10
|
+
const { THEME, visibleWidth } = ansi;
|
|
11
|
+
const { minuteOf } = require('../dashboard/model.js');
|
|
12
|
+
const tiles = require('./tiles.js');
|
|
13
|
+
const { seg, barSegments, spinner } = tiles;
|
|
14
|
+
const table = require('./dash-table.js');
|
|
15
|
+
const { num, size, delta, waiting, cell, cellSegs, flexCell } = table;
|
|
16
|
+
const { tableLines, pickGroups, labelOf, stat, pairRows } = table;
|
|
17
|
+
const { titleAside, captionOver } = table;
|
|
18
|
+
|
|
19
|
+
const BAR_W = 6;
|
|
20
|
+
const FOLDER = '📁';
|
|
21
|
+
const MARK_GAP = 1;
|
|
22
|
+
const ACTIVITY_LABEL = 'activity ';
|
|
23
|
+
const PULSE = '∙';
|
|
24
|
+
const ACTIVITY_MARK = [
|
|
25
|
+
['fail', 'x'],
|
|
26
|
+
['commit', '◉'],
|
|
27
|
+
['pass', '*'],
|
|
28
|
+
['abort', '!'],
|
|
29
|
+
['branch', '⎇'],
|
|
30
|
+
['rebase', '↻'],
|
|
31
|
+
['npm', '✦'],
|
|
32
|
+
];
|
|
33
|
+
const UNKNOWN = '…';
|
|
34
|
+
|
|
35
|
+
const share = (value, total) => (total > 0 ? value / total : 0);
|
|
36
|
+
|
|
37
|
+
const markCell = (isDir) => cell(isDir ? FOLDER : '');
|
|
38
|
+
|
|
39
|
+
const nameCell = (key, isDir, hot = false) => {
|
|
40
|
+
const name = cell(labelOf(key, isDir), hot ? 'warn' : 'sha', 'l', hot);
|
|
41
|
+
name.gap = MARK_GAP;
|
|
42
|
+
return name;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const fileName = (key, isDir, hot) => {
|
|
46
|
+
const name = nameCell(key, isDir, hot);
|
|
47
|
+
name.flex = true;
|
|
48
|
+
name.keep = true;
|
|
49
|
+
return name;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const fileRow = (group, isDir, data) => {
|
|
53
|
+
const hot = (isDir ? data.hot.dirs : data.hot.exts).has(group.key);
|
|
54
|
+
const ratio = share(group.bytes, data.total.bytes);
|
|
55
|
+
return [
|
|
56
|
+
markCell(isDir),
|
|
57
|
+
fileName(group.key, isDir, hot),
|
|
58
|
+
cell(`${group.files}`, 'muted', 'r'),
|
|
59
|
+
cell(size(group.bytes), 'text', 'r'),
|
|
60
|
+
cell(num(group.lines), 'muted', 'r'),
|
|
61
|
+
cellSegs(barSegments(ratio, BAR_W, 'sha')),
|
|
62
|
+
];
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const filesTotal = (data, ctx) => {
|
|
66
|
+
const { total } = data;
|
|
67
|
+
const name = cell('');
|
|
68
|
+
name.flex = true;
|
|
69
|
+
name.gap = MARK_GAP;
|
|
70
|
+
const row = [
|
|
71
|
+
cell(''),
|
|
72
|
+
name,
|
|
73
|
+
cell(`${total.files}`, 'text', 'r', true),
|
|
74
|
+
cell(size(total.bytes), 'text', 'r', true),
|
|
75
|
+
cell(num(total.lines), 'text', 'r', true),
|
|
76
|
+
cell(''),
|
|
77
|
+
];
|
|
78
|
+
const moved = data.delta ? data.delta.lines : 0;
|
|
79
|
+
const change = delta(moved, ctx.now, data.delta?.at);
|
|
80
|
+
if (change) row.push(cellSegs([change]));
|
|
81
|
+
return row;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const filesBlock = (model, width, height, ctx, tile) => {
|
|
85
|
+
const data = model.files;
|
|
86
|
+
if (!data.ready) return waiting('scanning…');
|
|
87
|
+
const picked = pickGroups(data.dirs, data.exts, height - 1);
|
|
88
|
+
const labels = [
|
|
89
|
+
cell(''),
|
|
90
|
+
cell(''),
|
|
91
|
+
cell('files', 'muted', 'r'),
|
|
92
|
+
cell('size', 'muted', 'r'),
|
|
93
|
+
cell('lines', 'muted', 'r'),
|
|
94
|
+
cell(''),
|
|
95
|
+
];
|
|
96
|
+
const rows = [labels];
|
|
97
|
+
for (const group of picked.dirs) rows.push(fileRow(group, true, data));
|
|
98
|
+
for (const group of picked.exts) rows.push(fileRow(group, false, data));
|
|
99
|
+
rows.push(filesTotal(data, ctx));
|
|
100
|
+
const lines = tableLines(rows, width);
|
|
101
|
+
return {
|
|
102
|
+
titleLine: captionOver(tile, lines[0]),
|
|
103
|
+
lines: lines.slice(1, -1),
|
|
104
|
+
footer: lines.at(-1),
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const ratio = (sign, part, total) => `${sign}${part}/${total}`;
|
|
109
|
+
|
|
110
|
+
const changeCells = (group, bold = false) => [
|
|
111
|
+
cell(ratio('+', group.stagedAdded, group.added), 'add', 'r', bold),
|
|
112
|
+
cell(ratio('-', group.stagedRemoved, group.removed), 'del', 'r', bold),
|
|
113
|
+
cell(`${group.staged}/${group.remaining}`, 'text', 'r', bold),
|
|
114
|
+
cell(shortAge(group.date), 'muted', 'r'),
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
const diffRow = (group, isDir) => [
|
|
118
|
+
markCell(isDir),
|
|
119
|
+
nameCell(group.key, isDir),
|
|
120
|
+
...changeCells(group),
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
const diffsHeader = (data, ctx) => {
|
|
124
|
+
const name = cell('');
|
|
125
|
+
name.gap = MARK_GAP;
|
|
126
|
+
const row = [cell(''), name, ...changeCells(data.totals, true)];
|
|
127
|
+
const moved = data.delta ? data.delta.added + data.delta.removed : 0;
|
|
128
|
+
const change = delta(moved, ctx.now, data.delta?.at);
|
|
129
|
+
if (change) row[row.length - 1] = cellSegs([change], 'r');
|
|
130
|
+
return row;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const heatIndex = (level) => {
|
|
134
|
+
if (level >= 301) return 4;
|
|
135
|
+
if (level >= 81) return 3;
|
|
136
|
+
if (level >= 16) return 2;
|
|
137
|
+
if (level >= 1) return 1;
|
|
138
|
+
return 0;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const heatColor = (index) => {
|
|
142
|
+
if (index === 1) return THEME.heat1;
|
|
143
|
+
if (index === 2) return THEME.heat2;
|
|
144
|
+
if (index === 3) return THEME.heat3;
|
|
145
|
+
if (index === 4) return THEME.heat4;
|
|
146
|
+
return null;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const markGlyph = (marks) => {
|
|
150
|
+
const list = marks ?? [];
|
|
151
|
+
for (const [kind, glyph] of ACTIVITY_MARK) {
|
|
152
|
+
if (list.includes(kind)) return glyph;
|
|
153
|
+
}
|
|
154
|
+
return '';
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const diffGlyph = (bucket) => {
|
|
158
|
+
if (!bucket) return '';
|
|
159
|
+
const added = bucket.added || 0;
|
|
160
|
+
const removed = bucket.removed || 0;
|
|
161
|
+
if (removed > added) return '-';
|
|
162
|
+
if (added > 0 || removed > 0 || bucket.level > 0) return '+';
|
|
163
|
+
return '';
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const pulseGlyph = (level) => {
|
|
167
|
+
const index = heatIndex(level);
|
|
168
|
+
if (index >= 4) return '◎';
|
|
169
|
+
if (index >= 3) return '•';
|
|
170
|
+
return '◦';
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const activityCell = (bucket, blink, track, pulse) => {
|
|
174
|
+
const level = bucket ? bucket.level : 0;
|
|
175
|
+
const glyph = markGlyph(bucket ? bucket.marks : null);
|
|
176
|
+
const index = heatIndex(level);
|
|
177
|
+
const mark = glyph || diffGlyph(bucket);
|
|
178
|
+
const phase = blink ? pulseGlyph(level) : PULSE;
|
|
179
|
+
const ch = pulse ? phase : mark || ' ';
|
|
180
|
+
const item = seg(ch, blink ? 'blink' : 'heat', Boolean(glyph) && !pulse);
|
|
181
|
+
let bg = track ? THEME.heatTrack : null;
|
|
182
|
+
const heat = heatColor(index);
|
|
183
|
+
if (heat) bg = heat;
|
|
184
|
+
if (blink) bg = THEME.heatBlink;
|
|
185
|
+
item.bg = bg;
|
|
186
|
+
return item;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const activityCells = (activity, ctx, width) => {
|
|
190
|
+
const minutes = activity && activity.minutes ? activity.minutes : [];
|
|
191
|
+
const byAt = new Map();
|
|
192
|
+
let start = null;
|
|
193
|
+
for (const row of minutes) {
|
|
194
|
+
byAt.set(row.at, row);
|
|
195
|
+
if (start === null || row.at < start) start = row.at;
|
|
196
|
+
}
|
|
197
|
+
const current = minuteOf(ctx.now);
|
|
198
|
+
const flash = Boolean(activity && activity.live);
|
|
199
|
+
const on = Math.abs(ctx.frame ?? 0) % 2 === 1;
|
|
200
|
+
const cells = [];
|
|
201
|
+
for (let i = 0; i < width; i++) {
|
|
202
|
+
const at = current - (width - 1 - i);
|
|
203
|
+
const last = i === width - 1;
|
|
204
|
+
const pulse = flash && last;
|
|
205
|
+
const blink = pulse && on;
|
|
206
|
+
const track = start !== null && at >= start;
|
|
207
|
+
cells.push(activityCell(byAt.get(at), blink, track, pulse));
|
|
208
|
+
}
|
|
209
|
+
return cells;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const activityFooter = (data, width, ctx) => {
|
|
213
|
+
const room = width - 1 - ACTIVITY_LABEL.length;
|
|
214
|
+
if (room < 2) return null;
|
|
215
|
+
const cells = activityCells(data.activity, ctx, room);
|
|
216
|
+
return [seg(ACTIVITY_LABEL, 'muted'), ...cells];
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const diffsBlock = (model, width, height, ctx, tile) => {
|
|
220
|
+
const data = model.diffs;
|
|
221
|
+
if (!data.files) {
|
|
222
|
+
const rows = pairRows([stat('changes', 'working tree clean', 'muted')], 1);
|
|
223
|
+
return { badge: [], lines: tableLines(rows, width) };
|
|
224
|
+
}
|
|
225
|
+
const footer = activityFooter(data, width, ctx);
|
|
226
|
+
const room = footer ? height - 1 : height;
|
|
227
|
+
const picked = pickGroups(data.dirs, data.exts, room);
|
|
228
|
+
const rows = [diffsHeader(data, ctx)];
|
|
229
|
+
for (const group of picked.dirs) rows.push(diffRow(group, true));
|
|
230
|
+
for (const group of picked.exts) rows.push(diffRow(group, false));
|
|
231
|
+
const lines = tableLines(rows, width, 2, true);
|
|
232
|
+
return {
|
|
233
|
+
titleLine: captionOver(tile, lines[0]),
|
|
234
|
+
lines: lines.slice(1),
|
|
235
|
+
footer,
|
|
236
|
+
footerFlush: true,
|
|
237
|
+
};
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const countSegs = (label, value) => [
|
|
241
|
+
seg(`${label}: `, 'muted'),
|
|
242
|
+
seg(`${value}`, 'text'),
|
|
243
|
+
];
|
|
244
|
+
|
|
245
|
+
const markSegs = (value, tone, mark) => {
|
|
246
|
+
if (!value) return [];
|
|
247
|
+
return [seg(' '), seg(mark), seg(`${value}`, tone, true)];
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const npmAside = (data) => {
|
|
251
|
+
const modules = data.modules;
|
|
252
|
+
return [
|
|
253
|
+
...countSegs('deps', data.deps),
|
|
254
|
+
seg(' '),
|
|
255
|
+
...countSegs('dev', data.dev),
|
|
256
|
+
seg(' '),
|
|
257
|
+
...countSegs('all', modules.count),
|
|
258
|
+
seg(' (', 'muted'),
|
|
259
|
+
seg(size(modules.bytes), 'text'),
|
|
260
|
+
seg(')', 'muted'),
|
|
261
|
+
...markSegs(data.audit, 'error', '🚨 '),
|
|
262
|
+
...markSegs(data.outdated, 'warn', '⚠️ '),
|
|
263
|
+
];
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const npmStatus = (data) => {
|
|
267
|
+
if (!data || !data.ready || !data.hasManifest) return '';
|
|
268
|
+
const modules = data.modules || { count: 0, bytes: 0 };
|
|
269
|
+
let text = `deps: ${data.deps} dev: ${data.dev}`;
|
|
270
|
+
text += ` all: ${modules.count} (${size(modules.bytes)})`;
|
|
271
|
+
if (data.audit) text += ` 🚨 ${data.audit}`;
|
|
272
|
+
if (data.outdated) text += ` ⚠️ ${data.outdated}`;
|
|
273
|
+
return text;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const cmpText = (left, right) => {
|
|
277
|
+
const a = parseVersion(left);
|
|
278
|
+
const b = parseVersion(right);
|
|
279
|
+
if (!a || !b) return null;
|
|
280
|
+
return cmpVersion(a, b);
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const boundHolds = (cmp, op) => {
|
|
284
|
+
if (op === '<') return cmp < 0;
|
|
285
|
+
if (op === '<=') return cmp <= 0;
|
|
286
|
+
if (op === '>') return cmp > 0;
|
|
287
|
+
return cmp >= 0;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const inClause = (version, clause) => {
|
|
291
|
+
const ver = parseVersion(version);
|
|
292
|
+
if (!ver) return false;
|
|
293
|
+
const bounds = [...clause.matchAll(/(>=|<=|>|<)\s*(\d+\.\d+\.\d+)/g)];
|
|
294
|
+
if (!bounds.length) return false;
|
|
295
|
+
for (const match of bounds) {
|
|
296
|
+
const bound = parseVersion(match[2]);
|
|
297
|
+
if (!bound) return false;
|
|
298
|
+
if (!boundHolds(cmpVersion(ver, bound), match[1])) return false;
|
|
299
|
+
}
|
|
300
|
+
return true;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const inRange = (version, range) =>
|
|
304
|
+
`${range ?? ''}`.split('||').some((clause) => inClause(version, clause));
|
|
305
|
+
|
|
306
|
+
const hasRange = (range) => /[<>]=?\s*\d+\.\d+\.\d+/.test(`${range ?? ''}`);
|
|
307
|
+
|
|
308
|
+
const vulnTone = (text, pkg) => {
|
|
309
|
+
if (!pkg.audit || !text || text === UNKNOWN) return '';
|
|
310
|
+
const fix = pkg.fix || '';
|
|
311
|
+
if (fix) {
|
|
312
|
+
const cmp = cmpText(text, fix);
|
|
313
|
+
if (cmp === null) return text === pkg.current ? 'error' : '';
|
|
314
|
+
return cmp < 0 ? 'error' : 'add';
|
|
315
|
+
}
|
|
316
|
+
const range = pkg.range || '';
|
|
317
|
+
if (hasRange(range)) {
|
|
318
|
+
if (inRange(text, range)) return 'error';
|
|
319
|
+
const cmp = cmpText(text, pkg.current);
|
|
320
|
+
if (cmp !== null && cmp > 0) return 'add';
|
|
321
|
+
}
|
|
322
|
+
return text === pkg.current ? 'error' : '';
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const isOutdated = (pkg) => {
|
|
326
|
+
const current = pkg.current || '';
|
|
327
|
+
const wanted = pkg.wanted || '';
|
|
328
|
+
const latest = pkg.latest || '';
|
|
329
|
+
if (!current || current === UNKNOWN) return false;
|
|
330
|
+
if (wanted && wanted !== UNKNOWN && wanted !== current) return true;
|
|
331
|
+
return Boolean(latest && latest !== UNKNOWN && latest !== current);
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const fixesOutdated = (text, pkg) => {
|
|
335
|
+
if (!isOutdated(pkg) || !text || text === UNKNOWN) return false;
|
|
336
|
+
const current = pkg.current || '';
|
|
337
|
+
const wanted = pkg.wanted || '';
|
|
338
|
+
const latest = pkg.latest || '';
|
|
339
|
+
if (text === wanted && wanted !== current) return true;
|
|
340
|
+
return text === latest && latest !== current;
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const versionTone = (text, pkg) => {
|
|
344
|
+
const vuln = vulnTone(text, pkg);
|
|
345
|
+
if (vuln) return vuln;
|
|
346
|
+
if (fixesOutdated(text, pkg)) return 'add';
|
|
347
|
+
if (text === pkg.current && isOutdated(pkg)) return 'warn';
|
|
348
|
+
return 'muted';
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const packageMark = (pkg, marks) => {
|
|
352
|
+
if (!marks) return cell('');
|
|
353
|
+
const segs = [];
|
|
354
|
+
if (pkg.audit) segs.push(seg('🛑 vulnerability detected', 'error'));
|
|
355
|
+
if (isOutdated(pkg)) {
|
|
356
|
+
if (segs.length) segs.push(seg(' '));
|
|
357
|
+
segs.push(seg('⚠️ outdated', 'warn'));
|
|
358
|
+
}
|
|
359
|
+
if (!segs.length) return cell('');
|
|
360
|
+
return cellSegs(segs);
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const nameTone = (pkg) => {
|
|
364
|
+
if (pkg.transitive) return 'pkg';
|
|
365
|
+
if (pkg.dev) return 'dev';
|
|
366
|
+
return 'dep';
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const packageName = (pkg) => {
|
|
370
|
+
const segs = [seg(pkg.name, nameTone(pkg))];
|
|
371
|
+
if (pkg.chain) segs.push(seg(` 🢒 ${pkg.chain}`, 'chain'));
|
|
372
|
+
return {
|
|
373
|
+
segs,
|
|
374
|
+
align: 'l',
|
|
375
|
+
flex: true,
|
|
376
|
+
keep: true,
|
|
377
|
+
measure: visibleWidth(pkg.name),
|
|
378
|
+
};
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const versionCell = (text, pkg) => cell(text, versionTone(text, pkg), 'r');
|
|
382
|
+
|
|
383
|
+
const packageHead = () => {
|
|
384
|
+
const title = flexCell('');
|
|
385
|
+
title.keep = true;
|
|
386
|
+
return [
|
|
387
|
+
title,
|
|
388
|
+
cell(''),
|
|
389
|
+
cell('current', 'muted', 'r'),
|
|
390
|
+
cell('wanted', 'muted', 'r'),
|
|
391
|
+
cell('latest', 'muted', 'r'),
|
|
392
|
+
cell('size', 'muted', 'r'),
|
|
393
|
+
];
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const packageRow = (pkg, marks) => {
|
|
397
|
+
const current = pkg.current || UNKNOWN;
|
|
398
|
+
const wanted = pkg.wanted || UNKNOWN;
|
|
399
|
+
const latest = pkg.latest || UNKNOWN;
|
|
400
|
+
return [
|
|
401
|
+
packageName(pkg),
|
|
402
|
+
packageMark(pkg, marks),
|
|
403
|
+
versionCell(current, pkg),
|
|
404
|
+
versionCell(wanted, pkg),
|
|
405
|
+
versionCell(latest, pkg),
|
|
406
|
+
cell(size(pkg.bytes), 'muted', 'r'),
|
|
407
|
+
];
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const packageLines = (packages, width, limit) => {
|
|
411
|
+
const all = limit === null;
|
|
412
|
+
const headed = packages.length > 0 && (all || limit > 1);
|
|
413
|
+
const room = all ? packages.length : Math.max(0, limit - (headed ? 1 : 0));
|
|
414
|
+
const shown = packages.slice(0, room);
|
|
415
|
+
const rows = shown.map((pkg) => packageRow(pkg, all));
|
|
416
|
+
if (headed) rows.unshift(packageHead());
|
|
417
|
+
return tableLines(rows, width);
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const npmContent = (data, width, limit, ctx, tile) => {
|
|
421
|
+
if (!data || !data.ready) return waiting('reading package.json…');
|
|
422
|
+
if (!data.hasManifest) return waiting('no package.json');
|
|
423
|
+
const badge = data.running ? [seg(spinner(ctx.frame), 'warn', true)] : [];
|
|
424
|
+
const titleLine = titleAside(tile, npmAside(data), width, badge);
|
|
425
|
+
const listed = data.modules && data.modules.packages;
|
|
426
|
+
const packages = listed ? listed : [];
|
|
427
|
+
const short = limit === null ? packages : packages.slice(0, TOP_PACKAGES);
|
|
428
|
+
return { titleLine, lines: packageLines(short, width, limit) };
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
const npmBlock = (model, width, height, ctx, tile) =>
|
|
432
|
+
npmContent(model.npm, width, height, ctx, tile);
|
|
433
|
+
|
|
434
|
+
const TASK_GAP = 2;
|
|
435
|
+
|
|
436
|
+
const ratioLabel = (done, total) => `${done}/${total}`;
|
|
437
|
+
|
|
438
|
+
const taskSpecs = (data) => {
|
|
439
|
+
const rows = [{ title: '', done: data.done, total: data.total, bold: true }];
|
|
440
|
+
for (const kind of data.kinds) {
|
|
441
|
+
rows.push({
|
|
442
|
+
title: kind.title,
|
|
443
|
+
done: kind.done,
|
|
444
|
+
total: kind.total,
|
|
445
|
+
bold: false,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
return rows;
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const widest = (rows, read) => {
|
|
452
|
+
let width = 0;
|
|
453
|
+
for (const row of rows) {
|
|
454
|
+
const size = read(row).length;
|
|
455
|
+
if (size > width) width = size;
|
|
456
|
+
}
|
|
457
|
+
return width;
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
const taskBarWidth = (width, nameW, ratioW) => {
|
|
461
|
+
const room = width - nameW - ratioW - TASK_GAP * 2;
|
|
462
|
+
return room > 0 ? room : 0;
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
const taskLine = (row, barW) => {
|
|
466
|
+
const tone = row.bold ? 'text' : 'muted';
|
|
467
|
+
const line = [cell(row.title, 'text', 'l', row.bold)];
|
|
468
|
+
if (barW > 0) {
|
|
469
|
+
line.push(cellSegs(barSegments(share(row.done, row.total), barW)));
|
|
470
|
+
}
|
|
471
|
+
line.push(cell(ratioLabel(row.done, row.total), tone, 'r', row.bold));
|
|
472
|
+
return line;
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
const tasksBlock = (model, width, height, ctx, tile) => {
|
|
476
|
+
const specs = taskSpecs(model.tasks);
|
|
477
|
+
const nameW = widest(specs, (row) => row.title);
|
|
478
|
+
const ratioW = widest(specs, (row) => ratioLabel(row.done, row.total));
|
|
479
|
+
const barW = taskBarWidth(width, nameW, ratioW);
|
|
480
|
+
const rows = specs.map((row) => taskLine(row, barW));
|
|
481
|
+
const lines = tableLines(rows, width, TASK_GAP);
|
|
482
|
+
const shown = Math.max(0, height);
|
|
483
|
+
return {
|
|
484
|
+
titleLine: captionOver(tile, lines[0]),
|
|
485
|
+
lines: lines.slice(1, 1 + shown),
|
|
486
|
+
};
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
module.exports = {
|
|
490
|
+
filesBlock,
|
|
491
|
+
diffsBlock,
|
|
492
|
+
npmBlock,
|
|
493
|
+
npmContent,
|
|
494
|
+
npmStatus,
|
|
495
|
+
tasksBlock,
|
|
496
|
+
};
|