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
@@ -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 };