reslop 0.2.4 → 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 (109) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/LICENSE +1 -1
  3. package/README.md +12 -13
  4. package/bin/reslop.js +3 -8
  5. package/lib/ansi.js +32 -19
  6. package/lib/capabilities.js +8 -12
  7. package/lib/cli.js +40 -74
  8. package/lib/clipboard.js +48 -18
  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 +196 -404
  14. package/lib/deps.js +240 -399
  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 +138 -141
  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 +90 -206
  25. package/lib/editor.js +98 -55
  26. package/lib/files.js +86 -103
  27. package/lib/git-branches.js +254 -416
  28. package/lib/git-deps.js +182 -344
  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 +157 -132
  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 +16 -26
  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 +22 -35
  44. package/lib/highlight/txt.js +9 -22
  45. package/lib/highlight.js +17 -46
  46. package/lib/keys.js +62 -85
  47. package/lib/manifest.js +67 -130
  48. package/lib/npm-commands.js +119 -189
  49. package/lib/remote.js +141 -197
  50. package/lib/render/branches.js +66 -73
  51. package/lib/render/chrome.js +264 -323
  52. package/lib/render/commits.js +187 -500
  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 -228
  58. package/lib/render/files.js +45 -99
  59. package/lib/render/frame.js +129 -183
  60. package/lib/render/notes.js +144 -160
  61. package/lib/render/npm.js +234 -112
  62. package/lib/render/present.js +4 -11
  63. package/lib/render/primitives.js +131 -170
  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 -28
  68. package/lib/report-model.js +110 -33
  69. package/lib/report-parse.js +56 -38
  70. package/lib/report-render.js +102 -59
  71. package/lib/report-run.js +87 -78
  72. package/lib/report-test.js +256 -141
  73. package/lib/report-tools.js +96 -161
  74. package/lib/review-merge.js +114 -152
  75. package/lib/review.js +115 -189
  76. package/lib/runs.js +225 -0
  77. package/lib/select.js +89 -75
  78. package/lib/session/accessors.js +14 -21
  79. package/lib/session/actions.js +82 -63
  80. package/lib/session/branches.js +193 -247
  81. package/lib/session/change-actions.js +171 -214
  82. package/lib/session/commit.js +280 -342
  83. package/lib/session/compose.js +559 -674
  84. package/lib/session/dashboard.js +385 -0
  85. package/lib/session/dispatch.js +164 -157
  86. package/lib/session/edit-click.js +57 -0
  87. package/lib/session/files-pane.js +85 -53
  88. package/lib/session/items.js +73 -104
  89. package/lib/session/lifecycle.js +403 -541
  90. package/lib/session/load.js +48 -68
  91. package/lib/session/navigation.js +96 -87
  92. package/lib/session/npm.js +409 -345
  93. package/lib/session/ops.js +196 -63
  94. package/lib/session/pointer.js +23 -15
  95. package/lib/session/progress.js +33 -21
  96. package/lib/session/review.js +33 -54
  97. package/lib/session/tasks.js +333 -0
  98. package/lib/session/templates.js +51 -59
  99. package/lib/session/terminal.js +85 -136
  100. package/lib/session/unit.js +3 -4
  101. package/lib/session/update.js +76 -97
  102. package/lib/session/view.js +52 -83
  103. package/lib/session/watch.js +208 -204
  104. package/lib/session.js +112 -237
  105. package/lib/update.js +45 -116
  106. package/lib/utilities.js +67 -89
  107. package/lib/wrap.js +33 -44
  108. package/package.json +1 -1
  109. package/lib/session/todos.js +0 -293
@@ -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,245 @@
1
+ 'use strict';
2
+
3
+ const files = require('../files.js');
4
+ const { shortAge } = files;
5
+ const tiles = require('./tiles.js');
6
+ const { seg, barSegments, sparkline, spinner } = tiles;
7
+ const table = require('./dash-table.js');
8
+ const { num, size, delta, waiting, cell, cellSegs, flexCell } = table;
9
+ const { tableLines, pickGroups, labelOf, stat, pairRows } = table;
10
+ const { titleAside, captionOver } = table;
11
+
12
+ const BAR_W = 6;
13
+ const FOLDER = '📁';
14
+ const MARK_GAP = 1;
15
+ const TREND_LABEL = 'trend ';
16
+ const UNKNOWN = '…';
17
+
18
+ const share = (value, total) => (total > 0 ? value / total : 0);
19
+
20
+ const markCell = (isDir) => cell(isDir ? FOLDER : '');
21
+
22
+ const nameCell = (key, isDir, hot = false) => {
23
+ const name = cell(labelOf(key, isDir), hot ? 'warn' : 'sha', 'l', hot);
24
+ name.gap = MARK_GAP;
25
+ return name;
26
+ };
27
+
28
+ const fileName = (key, isDir, hot) => {
29
+ const name = nameCell(key, isDir, hot);
30
+ name.flex = true;
31
+ name.keep = true;
32
+ return name;
33
+ };
34
+
35
+ const fileRow = (group, isDir, data) => {
36
+ const hot = (isDir ? data.hot.dirs : data.hot.exts).has(group.key);
37
+ const ratio = share(group.bytes, data.total.bytes);
38
+ return [
39
+ markCell(isDir),
40
+ fileName(group.key, isDir, hot),
41
+ cell(`${group.files}`, 'muted', 'r'),
42
+ cell(size(group.bytes), 'text', 'r'),
43
+ cell(num(group.lines), 'muted', 'r'),
44
+ cellSegs(barSegments(ratio, BAR_W, 'sha')),
45
+ ];
46
+ };
47
+
48
+ const filesTotal = (data, ctx) => {
49
+ const { total } = data;
50
+ const name = cell('');
51
+ name.flex = true;
52
+ name.gap = MARK_GAP;
53
+ const row = [
54
+ cell(''),
55
+ name,
56
+ cell(`${total.files}`, 'text', 'r', true),
57
+ cell(size(total.bytes), 'text', 'r', true),
58
+ cell(num(total.lines), 'text', 'r', true),
59
+ cell(''),
60
+ ];
61
+ const moved = data.delta ? data.delta.lines : 0;
62
+ const change = delta(moved, ctx.now, data.delta?.at);
63
+ if (change) row.push(cellSegs([change]));
64
+ return row;
65
+ };
66
+
67
+ const filesBlock = (model, width, height, ctx, tile) => {
68
+ const data = model.files;
69
+ if (!data.ready) return waiting('scanning…');
70
+ const picked = pickGroups(data.dirs, data.exts, height - 1);
71
+ const labels = [
72
+ cell(''),
73
+ cell(''),
74
+ cell('files', 'muted', 'r'),
75
+ cell('size', 'muted', 'r'),
76
+ cell('lines', 'muted', 'r'),
77
+ cell(''),
78
+ ];
79
+ const rows = [labels];
80
+ for (const group of picked.dirs) rows.push(fileRow(group, true, data));
81
+ for (const group of picked.exts) rows.push(fileRow(group, false, data));
82
+ rows.push(filesTotal(data, ctx));
83
+ const lines = tableLines(rows, width);
84
+ return {
85
+ titleLine: captionOver(tile, lines[0]),
86
+ lines: lines.slice(1, -1),
87
+ footer: lines.at(-1),
88
+ };
89
+ };
90
+
91
+ const ratio = (sign, part, total) => `${sign}${part}/${total}`;
92
+
93
+ const changeCells = (group, bold = false) => [
94
+ cell(ratio('+', group.stagedAdded, group.added), 'add', 'r', bold),
95
+ cell(ratio('-', group.stagedRemoved, group.removed), 'del', 'r', bold),
96
+ cell(`${group.staged}/${group.remaining}`, 'text', 'r', bold),
97
+ cell(shortAge(group.date), 'muted', 'r'),
98
+ ];
99
+
100
+ const diffRow = (group, isDir) => [
101
+ markCell(isDir),
102
+ nameCell(group.key, isDir),
103
+ ...changeCells(group),
104
+ ];
105
+
106
+ const diffsHeader = (data, ctx) => {
107
+ const name = cell('');
108
+ name.gap = MARK_GAP;
109
+ const row = [cell(''), name, ...changeCells(data.totals, true)];
110
+ const moved = data.delta ? data.delta.added + data.delta.removed : 0;
111
+ const change = delta(moved, ctx.now, data.delta?.at);
112
+ if (change) row[row.length - 1] = cellSegs([change], 'r');
113
+ return row;
114
+ };
115
+
116
+ const trendFooter = (data, width) => {
117
+ const room = width - TREND_LABEL.length;
118
+ if (data.samples.length < 2 || room < 2) return null;
119
+ const trend = sparkline(data.samples, room);
120
+ return [seg(TREND_LABEL, 'muted'), seg(trend, 'trend')];
121
+ };
122
+
123
+ const diffsBlock = (model, width, height, ctx, tile) => {
124
+ const data = model.diffs;
125
+ if (!data.files) {
126
+ const rows = pairRows([stat('changes', 'working tree clean', 'muted')], 1);
127
+ return { badge: [], lines: tableLines(rows, width) };
128
+ }
129
+ const footer = trendFooter(data, width);
130
+ const room = footer ? height - 1 : height;
131
+ const picked = pickGroups(data.dirs, data.exts, room);
132
+ const rows = [diffsHeader(data, ctx)];
133
+ for (const group of picked.dirs) rows.push(diffRow(group, true));
134
+ for (const group of picked.exts) rows.push(diffRow(group, false));
135
+ const lines = tableLines(rows, width, 2, true);
136
+ return {
137
+ titleLine: captionOver(tile, lines[0]),
138
+ lines: lines.slice(1),
139
+ footer,
140
+ };
141
+ };
142
+
143
+ const countSegs = (label, value) => [
144
+ seg(`${label}: `, 'muted'),
145
+ seg(`${value}`, 'text'),
146
+ ];
147
+
148
+ const markSegs = (value, tone, mark) => {
149
+ if (!value) return [];
150
+ return [seg(' '), seg(mark), seg(`${value}`, tone, true)];
151
+ };
152
+
153
+ const npmAside = (data) => {
154
+ const modules = data.modules;
155
+ return [
156
+ ...countSegs('deps', data.deps),
157
+ seg(' '),
158
+ ...countSegs('dev', data.dev),
159
+ seg(' '),
160
+ ...countSegs('all', modules.count),
161
+ seg(' (', 'muted'),
162
+ seg(size(modules.bytes), 'text'),
163
+ seg(')', 'muted'),
164
+ ...markSegs(data.audit, 'error', '🚨 '),
165
+ ...markSegs(data.outdated, 'warn', '⚠️ '),
166
+ ];
167
+ };
168
+
169
+ const differs = (left, right) =>
170
+ left !== UNKNOWN && right !== UNKNOWN && left !== right;
171
+
172
+ const versionCell = (text, changed, audited) => {
173
+ const base = changed ? 'warn' : 'muted';
174
+ const tone = audited && text !== UNKNOWN ? 'error' : base;
175
+ return cell(text, tone, 'r');
176
+ };
177
+
178
+ const packageHead = () => {
179
+ const title = flexCell('');
180
+ title.keep = true;
181
+ return [
182
+ title,
183
+ cell('current', 'muted', 'r'),
184
+ cell('wanted', 'muted', 'r'),
185
+ cell('latest', 'muted', 'r'),
186
+ cell('size', 'muted', 'r'),
187
+ ];
188
+ };
189
+
190
+ const packageRow = (pkg) => {
191
+ const name = flexCell(pkg.name, pkg.dev ? 'sha' : 'add');
192
+ name.keep = true;
193
+ const current = pkg.current || UNKNOWN;
194
+ const wanted = pkg.wanted || UNKNOWN;
195
+ const latest = pkg.latest || UNKNOWN;
196
+ return [
197
+ name,
198
+ versionCell(current, differs(current, wanted), pkg.audit),
199
+ versionCell(wanted, differs(wanted, current), pkg.audit),
200
+ versionCell(latest, differs(latest, wanted), pkg.audit),
201
+ cell(size(pkg.bytes), 'muted', 'r'),
202
+ ];
203
+ };
204
+
205
+ const npmBlock = (model, width, height, ctx, tile) => {
206
+ const data = model.npm;
207
+ if (!data.ready) return waiting('reading package.json…');
208
+ if (!data.hasManifest) return waiting('no package.json');
209
+ const badge = data.running ? [seg(spinner(ctx.frame), 'warn', true)] : [];
210
+ const titleLine = titleAside(tile, npmAside(data), width, badge);
211
+ const packages = data.modules.packages;
212
+ const headed = packages.length > 0 && height > 1;
213
+ const shown = packages.slice(0, Math.max(0, height - (headed ? 1 : 0)));
214
+ const rows = shown.map(packageRow);
215
+ if (headed) rows.unshift(packageHead());
216
+ return { titleLine, lines: tableLines(rows, width) };
217
+ };
218
+
219
+ const taskName = (title) => {
220
+ const name = flexCell(title, 'text');
221
+ name.keep = true;
222
+ return name;
223
+ };
224
+
225
+ const taskLine = (title, done, total, bold = false) => [
226
+ taskName(title),
227
+ cell(`${done}/${total}`, bold ? 'text' : 'muted', 'r', bold),
228
+ cellSegs(barSegments(share(done, total), BAR_W)),
229
+ ];
230
+
231
+ const tasksBlock = (model, width, height, ctx, tile) => {
232
+ const data = model.tasks;
233
+ const rows = [taskLine('', data.done, data.total, true)];
234
+ for (const kind of data.kinds) {
235
+ rows.push(taskLine(kind.title, kind.done, kind.total));
236
+ }
237
+ const lines = tableLines(rows, width);
238
+ const shown = Math.max(0, height);
239
+ return {
240
+ titleLine: captionOver(tile, lines[0]),
241
+ lines: lines.slice(1, 1 + shown),
242
+ };
243
+ };
244
+
245
+ module.exports = { filesBlock, diffsBlock, npmBlock, tasksBlock };