reslop 0.2.6 → 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 +11 -1
- package/lib/ansi.js +42 -2
- package/lib/api.js +1 -1
- package/lib/cli.js +21 -1
- package/lib/dashboard/model.js +28 -3
- package/lib/dashboard/npm.js +116 -19
- package/lib/files.js +13 -7
- package/lib/find.js +61 -0
- package/lib/git-deps.js +1 -0
- package/lib/git-worktree.js +22 -4
- package/lib/keys.js +26 -1
- package/lib/render/chrome.js +257 -79
- package/lib/render/dash-blocks.js +297 -46
- package/lib/render/dash-table.js +6 -1
- package/lib/render/files.js +23 -4
- package/lib/render/frame.js +4 -0
- package/lib/render/notes.js +13 -2
- package/lib/render/packages.js +93 -0
- package/lib/render/primitives.js +13 -6
- package/lib/render/repos.js +226 -0
- package/lib/render/search.js +25 -0
- package/lib/render/tiles.js +107 -12
- package/lib/review.js +5 -4
- package/lib/session/actions.js +136 -30
- package/lib/session/branches.js +3 -5
- package/lib/session/commit.js +31 -4
- package/lib/session/compose.js +19 -4
- package/lib/session/dashboard.js +160 -15
- package/lib/session/dispatch.js +38 -4
- package/lib/session/files-pane.js +17 -2
- package/lib/session/find.js +94 -0
- package/lib/session/lifecycle.js +27 -0
- package/lib/session/navigation.js +2 -0
- package/lib/session/packages.js +143 -0
- package/lib/session/pointer.js +14 -0
- package/lib/session/tasks.js +3 -3
- package/lib/session/view.js +15 -2
- package/lib/session/watch.js +5 -0
- package/lib/session/workspace.js +455 -0
- package/lib/session.js +14 -0
- package/lib/workspace.js +166 -0
- package/package.json +1 -1
|
@@ -2,8 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
const files = require('../files.js');
|
|
4
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');
|
|
5
12
|
const tiles = require('./tiles.js');
|
|
6
|
-
const { seg, barSegments,
|
|
13
|
+
const { seg, barSegments, spinner } = tiles;
|
|
7
14
|
const table = require('./dash-table.js');
|
|
8
15
|
const { num, size, delta, waiting, cell, cellSegs, flexCell } = table;
|
|
9
16
|
const { tableLines, pickGroups, labelOf, stat, pairRows } = table;
|
|
@@ -12,7 +19,17 @@ const { titleAside, captionOver } = table;
|
|
|
12
19
|
const BAR_W = 6;
|
|
13
20
|
const FOLDER = '📁';
|
|
14
21
|
const MARK_GAP = 1;
|
|
15
|
-
const
|
|
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
|
+
];
|
|
16
33
|
const UNKNOWN = '…';
|
|
17
34
|
|
|
18
35
|
const share = (value, total) => (total > 0 ? value / total : 0);
|
|
@@ -113,11 +130,90 @@ const diffsHeader = (data, ctx) => {
|
|
|
113
130
|
return row;
|
|
114
131
|
};
|
|
115
132
|
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
|
|
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];
|
|
121
217
|
};
|
|
122
218
|
|
|
123
219
|
const diffsBlock = (model, width, height, ctx, tile) => {
|
|
@@ -126,7 +222,7 @@ const diffsBlock = (model, width, height, ctx, tile) => {
|
|
|
126
222
|
const rows = pairRows([stat('changes', 'working tree clean', 'muted')], 1);
|
|
127
223
|
return { badge: [], lines: tableLines(rows, width) };
|
|
128
224
|
}
|
|
129
|
-
const footer =
|
|
225
|
+
const footer = activityFooter(data, width, ctx);
|
|
130
226
|
const room = footer ? height - 1 : height;
|
|
131
227
|
const picked = pickGroups(data.dirs, data.exts, room);
|
|
132
228
|
const rows = [diffsHeader(data, ctx)];
|
|
@@ -137,6 +233,7 @@ const diffsBlock = (model, width, height, ctx, tile) => {
|
|
|
137
233
|
titleLine: captionOver(tile, lines[0]),
|
|
138
234
|
lines: lines.slice(1),
|
|
139
235
|
footer,
|
|
236
|
+
footerFlush: true,
|
|
140
237
|
};
|
|
141
238
|
};
|
|
142
239
|
|
|
@@ -166,20 +263,129 @@ const npmAside = (data) => {
|
|
|
166
263
|
];
|
|
167
264
|
};
|
|
168
265
|
|
|
169
|
-
const
|
|
170
|
-
|
|
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
|
+
};
|
|
171
362
|
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
return
|
|
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
|
+
};
|
|
176
379
|
};
|
|
177
380
|
|
|
381
|
+
const versionCell = (text, pkg) => cell(text, versionTone(text, pkg), 'r');
|
|
382
|
+
|
|
178
383
|
const packageHead = () => {
|
|
179
384
|
const title = flexCell('');
|
|
180
385
|
title.keep = true;
|
|
181
386
|
return [
|
|
182
387
|
title,
|
|
388
|
+
cell(''),
|
|
183
389
|
cell('current', 'muted', 'r'),
|
|
184
390
|
cell('wanted', 'muted', 'r'),
|
|
185
391
|
cell('latest', 'muted', 'r'),
|
|
@@ -187,54 +393,92 @@ const packageHead = () => {
|
|
|
187
393
|
];
|
|
188
394
|
};
|
|
189
395
|
|
|
190
|
-
const packageRow = (pkg) => {
|
|
191
|
-
const name = flexCell(pkg.name, pkg.dev ? 'sha' : 'add');
|
|
192
|
-
name.keep = true;
|
|
396
|
+
const packageRow = (pkg, marks) => {
|
|
193
397
|
const current = pkg.current || UNKNOWN;
|
|
194
398
|
const wanted = pkg.wanted || UNKNOWN;
|
|
195
399
|
const latest = pkg.latest || UNKNOWN;
|
|
196
400
|
return [
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
versionCell(
|
|
200
|
-
versionCell(
|
|
401
|
+
packageName(pkg),
|
|
402
|
+
packageMark(pkg, marks),
|
|
403
|
+
versionCell(current, pkg),
|
|
404
|
+
versionCell(wanted, pkg),
|
|
405
|
+
versionCell(latest, pkg),
|
|
201
406
|
cell(size(pkg.bytes), 'muted', 'r'),
|
|
202
407
|
];
|
|
203
408
|
};
|
|
204
409
|
|
|
205
|
-
const
|
|
206
|
-
const
|
|
207
|
-
|
|
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…');
|
|
208
422
|
if (!data.hasManifest) return waiting('no package.json');
|
|
209
423
|
const badge = data.running ? [seg(spinner(ctx.frame), 'warn', true)] : [];
|
|
210
424
|
const titleLine = titleAside(tile, npmAside(data), width, badge);
|
|
211
|
-
const
|
|
212
|
-
const
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
if (headed) rows.unshift(packageHead());
|
|
216
|
-
return { titleLine, lines: tableLines(rows, width) };
|
|
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) };
|
|
217
429
|
};
|
|
218
430
|
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
name.keep = true;
|
|
222
|
-
return name;
|
|
223
|
-
};
|
|
431
|
+
const npmBlock = (model, width, height, ctx, tile) =>
|
|
432
|
+
npmContent(model.npm, width, height, ctx, tile);
|
|
224
433
|
|
|
225
|
-
const
|
|
226
|
-
taskName(title),
|
|
227
|
-
cell(`${done}/${total}`, bold ? 'text' : 'muted', 'r', bold),
|
|
228
|
-
cellSegs(barSegments(share(done, total), BAR_W)),
|
|
229
|
-
];
|
|
434
|
+
const TASK_GAP = 2;
|
|
230
435
|
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
436
|
+
const ratioLabel = (done, total) => `${done}/${total}`;
|
|
437
|
+
|
|
438
|
+
const taskSpecs = (data) => {
|
|
439
|
+
const rows = [{ title: '', done: data.done, total: data.total, bold: true }];
|
|
234
440
|
for (const kind of data.kinds) {
|
|
235
|
-
rows.push(
|
|
441
|
+
rows.push({
|
|
442
|
+
title: kind.title,
|
|
443
|
+
done: kind.done,
|
|
444
|
+
total: kind.total,
|
|
445
|
+
bold: false,
|
|
446
|
+
});
|
|
236
447
|
}
|
|
237
|
-
|
|
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);
|
|
238
482
|
const shown = Math.max(0, height);
|
|
239
483
|
return {
|
|
240
484
|
titleLine: captionOver(tile, lines[0]),
|
|
@@ -242,4 +486,11 @@ const tasksBlock = (model, width, height, ctx, tile) => {
|
|
|
242
486
|
};
|
|
243
487
|
};
|
|
244
488
|
|
|
245
|
-
module.exports = {
|
|
489
|
+
module.exports = {
|
|
490
|
+
filesBlock,
|
|
491
|
+
diffsBlock,
|
|
492
|
+
npmBlock,
|
|
493
|
+
npmContent,
|
|
494
|
+
npmStatus,
|
|
495
|
+
tasksBlock,
|
|
496
|
+
};
|
package/lib/render/dash-table.js
CHANGED
|
@@ -67,13 +67,18 @@ const flexCell = (text, tone = 'muted') => ({
|
|
|
67
67
|
|
|
68
68
|
const blankCell = { segs: [], align: 'l', flex: false };
|
|
69
69
|
|
|
70
|
+
const measuredWidth = (item) => {
|
|
71
|
+
if (typeof item.measure === 'number') return item.measure;
|
|
72
|
+
return segmentsWidth(item.segs);
|
|
73
|
+
};
|
|
74
|
+
|
|
70
75
|
const columnWidths = (rows, count) => {
|
|
71
76
|
const widths = new Array(count).fill(0);
|
|
72
77
|
for (const row of rows) {
|
|
73
78
|
for (let c = 0; c < count; c++) {
|
|
74
79
|
const item = row[c];
|
|
75
80
|
if (!item || (item.flex && !item.keep)) continue;
|
|
76
|
-
widths[c] = Math.max(widths[c],
|
|
81
|
+
widths[c] = Math.max(widths[c], measuredWidth(item));
|
|
77
82
|
}
|
|
78
83
|
}
|
|
79
84
|
return widths;
|
package/lib/render/files.js
CHANGED
|
@@ -8,6 +8,10 @@ const { isTasksEntry, shortAge } = files;
|
|
|
8
8
|
const { THEME, paint, visibleWidth, trimVisible, seq, EL } = ansi;
|
|
9
9
|
const { fill, markPrefix, paintCursorList } = primitives;
|
|
10
10
|
const { measureColumns, padColumns } = primitives;
|
|
11
|
+
const searchPaint = require('./search.js');
|
|
12
|
+
const { paintPathText } = searchPaint;
|
|
13
|
+
const locate = require('../find.js');
|
|
14
|
+
const { activeHit, spansFor } = locate;
|
|
11
15
|
|
|
12
16
|
const FILE_STATUS_FG = {
|
|
13
17
|
unstaged: THEME.delLineFg,
|
|
@@ -56,7 +60,13 @@ const paintFileMeta = (parts, fgRgb, bgRgb, color) => {
|
|
|
56
60
|
return out + paint(parts.m, fgRgb, bgRgb, color);
|
|
57
61
|
};
|
|
58
62
|
|
|
59
|
-
const
|
|
63
|
+
const rowSpans = (entry, path, marks) => {
|
|
64
|
+
if (!marks || !marks.query) return [];
|
|
65
|
+
const hit = marks.hit && marks.hit.index === marks.index ? marks.hit : null;
|
|
66
|
+
return spansFor(path, entry.path ?? '', marks.query, hit);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const paintFileRow = (entry, width, color, selected, cols, marks) => {
|
|
60
70
|
const tasks = isTasksEntry(entry);
|
|
61
71
|
const inner = Math.max(1, width - 1);
|
|
62
72
|
const prefix = markPrefix(selected);
|
|
@@ -72,8 +82,11 @@ const paintFileRow = (entry, width, color, selected, cols) => {
|
|
|
72
82
|
const bgRgb = selected ? THEME.buttonBg : THEME.ctxBg;
|
|
73
83
|
const edge = fill(1, fgRgb, bgRgb, color);
|
|
74
84
|
if (!color) return `${left}${meta}${date}${edge}`;
|
|
85
|
+
const spans = rowSpans(entry, path, marks);
|
|
75
86
|
let out = `${seq(fgRgb, bgRgb)}${EL}`;
|
|
76
|
-
out += paint(
|
|
87
|
+
out += paint(prefix, fgRgb, bgRgb, color, tasks);
|
|
88
|
+
out += paintPathText(path, fgRgb, bgRgb, color, tasks, spans);
|
|
89
|
+
out += paint(pad, fgRgb, bgRgb, color, tasks);
|
|
77
90
|
if (tasks) out += paint(meta, fgRgb, bgRgb, color, true);
|
|
78
91
|
else out += paintFileMeta(parts, fgRgb, bgRgb, color);
|
|
79
92
|
if (date) {
|
|
@@ -88,8 +101,14 @@ const paintBodyFiles = (view, width, color, bodyH, headerLines) => {
|
|
|
88
101
|
const cols = measureColumns(entries, fileRowFields, FILE_COL_ALIGN);
|
|
89
102
|
const cursor = view.fileCursor ?? 0;
|
|
90
103
|
const pane = { width, color, bodyH, headerLines, offset: view.listScroll };
|
|
91
|
-
const
|
|
92
|
-
|
|
104
|
+
const hit = activeHit(entries, view.find);
|
|
105
|
+
const query = view.find && view.find.query ? view.find.query : '';
|
|
106
|
+
const paintRow = (entry, selected, index) =>
|
|
107
|
+
paintFileRow(entry, width, color, selected, cols, {
|
|
108
|
+
query,
|
|
109
|
+
hit,
|
|
110
|
+
index,
|
|
111
|
+
});
|
|
93
112
|
return paintCursorList(entries, cursor, pane, paintRow);
|
|
94
113
|
};
|
|
95
114
|
|
package/lib/render/frame.js
CHANGED
|
@@ -12,6 +12,8 @@ const npm = require('./npm.js');
|
|
|
12
12
|
const diff = require('./diff.js');
|
|
13
13
|
const unit = require('./unit.js');
|
|
14
14
|
const dashboard = require('./dashboard.js');
|
|
15
|
+
const packages = require('./packages.js');
|
|
16
|
+
const repos = require('./repos.js');
|
|
15
17
|
|
|
16
18
|
const { RESET } = ansi;
|
|
17
19
|
const { overlayRows, markEditorSelection } = select;
|
|
@@ -23,10 +25,12 @@ const { paintBodyDiff } = diff;
|
|
|
23
25
|
|
|
24
26
|
const PANE_BODY = {
|
|
25
27
|
dashboard: dashboard.paintBodyDashboard,
|
|
28
|
+
repos: repos.paintBodyRepos,
|
|
26
29
|
files: files.paintBodyFiles,
|
|
27
30
|
branches: branches.paintBodyBranches,
|
|
28
31
|
commits: commits.paintBodyCommits,
|
|
29
32
|
npm: npm.paintBodyNpm,
|
|
33
|
+
packages: packages.paintBodyPackages,
|
|
30
34
|
unit: unit.paintBodyUnit,
|
|
31
35
|
tasks: paintBodyTodo,
|
|
32
36
|
};
|
package/lib/render/notes.js
CHANGED
|
@@ -94,7 +94,10 @@ const taskLists = (view) => {
|
|
|
94
94
|
};
|
|
95
95
|
|
|
96
96
|
const paintSectionHead = (title, width, color) =>
|
|
97
|
-
paintBar(` ${title}`, width, THEME.
|
|
97
|
+
paintBar(` ${title}`, width, THEME.taskHeadFg, THEME.taskHeadBg, color);
|
|
98
|
+
|
|
99
|
+
const paintSectionGap = (width, color) =>
|
|
100
|
+
paintBar('', width, THEME.ctxFg, THEME.ctxBg, color);
|
|
98
101
|
|
|
99
102
|
const paintBodyTodo = (view, width, color) => {
|
|
100
103
|
const focus = view.tasksFocus ?? 0;
|
|
@@ -105,7 +108,9 @@ const paintBodyTodo = (view, width, color) => {
|
|
|
105
108
|
let editHits = [];
|
|
106
109
|
let cursor = null;
|
|
107
110
|
let index = 0;
|
|
108
|
-
|
|
111
|
+
const sections = taskLists(view);
|
|
112
|
+
for (let sectionAt = 0; sectionAt < sections.length; sectionAt++) {
|
|
113
|
+
const section = sections[sectionAt];
|
|
109
114
|
if (section.title) {
|
|
110
115
|
body.push(paintSectionHead(section.title, width, color));
|
|
111
116
|
taskOwners.push(null);
|
|
@@ -131,6 +136,12 @@ const paintBodyTodo = (view, width, color) => {
|
|
|
131
136
|
}
|
|
132
137
|
index += 1;
|
|
133
138
|
}
|
|
139
|
+
const last = sectionAt === sections.length - 1;
|
|
140
|
+
if (section.title && !last) {
|
|
141
|
+
body.push(paintSectionGap(width, color));
|
|
142
|
+
taskOwners.push(null);
|
|
143
|
+
taskChecks.push(false);
|
|
144
|
+
}
|
|
134
145
|
}
|
|
135
146
|
return paneResult({ body, editHits, taskOwners, taskChecks, cursor });
|
|
136
147
|
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ansi = require('../ansi.js');
|
|
4
|
+
const { THEME, paint, visibleWidth } = ansi;
|
|
5
|
+
const tiles = require('./tiles.js');
|
|
6
|
+
const { paintSegments } = tiles;
|
|
7
|
+
const blocks = require('./dash-blocks.js');
|
|
8
|
+
const { npmContent } = blocks;
|
|
9
|
+
const primitives = require('./primitives.js');
|
|
10
|
+
const { MARK_W, markPrefix, paintCursorList, fieldWindow, editSpan } =
|
|
11
|
+
primitives;
|
|
12
|
+
|
|
13
|
+
const TILE = { key: 'n', title: 'npm' };
|
|
14
|
+
const TAIL = 1;
|
|
15
|
+
const LIST_PAD = 1;
|
|
16
|
+
|
|
17
|
+
const paintMarginRow = (line, width, color, selected) => {
|
|
18
|
+
const bg = selected ? THEME.buttonBg : THEME.ctxBg;
|
|
19
|
+
const fg = selected ? THEME.chromeFg : THEME.mutedFg;
|
|
20
|
+
const inner = Math.max(1, width - MARK_W - TAIL);
|
|
21
|
+
const body = paintSegments(line, inner, bg, color);
|
|
22
|
+
const prefix = markPrefix(selected);
|
|
23
|
+
const tail = ' '.repeat(TAIL);
|
|
24
|
+
if (!color) return `${prefix}${body}${tail}`;
|
|
25
|
+
return paint(prefix, fg, bg, color) + body + paint(tail, fg, bg, color);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const paintPackageEdit = (compose, width, color) => {
|
|
29
|
+
const prefix = markPrefix(true);
|
|
30
|
+
const rest = Math.max(1, width - MARK_W - TAIL);
|
|
31
|
+
const text = `${compose.text ?? ''}`.replaceAll('\n', ' ');
|
|
32
|
+
const win = fieldWindow(text, compose.cursor, compose.scrollCol, rest);
|
|
33
|
+
const gap = ' '.repeat(Math.max(0, rest - visibleWidth(win.text)));
|
|
34
|
+
const tail = ' '.repeat(TAIL);
|
|
35
|
+
const fg = THEME.chromeFg;
|
|
36
|
+
const bg = THEME.buttonBg;
|
|
37
|
+
let row = `${prefix}${win.text}${gap}${tail}`;
|
|
38
|
+
if (color) {
|
|
39
|
+
row = paint(prefix, fg, bg, color);
|
|
40
|
+
row += paint(win.text, THEME.buttonHotFg, bg, color);
|
|
41
|
+
row += paint(`${gap}${tail}`, fg, bg, color);
|
|
42
|
+
}
|
|
43
|
+
const textX = MARK_W + 1;
|
|
44
|
+
const edit = editSpan(1, textX, textX + rest, 0, text);
|
|
45
|
+
const cursor = { x: textX + win.col, scroll: win.scroll };
|
|
46
|
+
return { row, cursor, edits: [{ ...edit, scroll: win.scroll }] };
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const paintBodyPackages = (view, width, color, bodyH, headerLines) => {
|
|
50
|
+
const ctx = { frame: view.progressFrame ?? 0 };
|
|
51
|
+
const inner = Math.max(1, width - MARK_W - TAIL);
|
|
52
|
+
const content = npmContent(view.packages, inner, null, ctx, TILE);
|
|
53
|
+
const headed = Boolean(content.titleLine) && content.lines.length > 0;
|
|
54
|
+
const rows = headed ? content.lines.slice(1) : content.lines;
|
|
55
|
+
const reserved = headed ? 1 : 0;
|
|
56
|
+
const last = Math.max(0, rows.length - 1);
|
|
57
|
+
const cursor = Math.min(view.packagesCursor ?? 0, last);
|
|
58
|
+
const pane = {
|
|
59
|
+
width,
|
|
60
|
+
color,
|
|
61
|
+
bodyH: Math.max(1, bodyH - reserved),
|
|
62
|
+
headerLines: headerLines + reserved,
|
|
63
|
+
offset: view.listScroll,
|
|
64
|
+
};
|
|
65
|
+
const paintRow = (line, selected) =>
|
|
66
|
+
paintMarginRow(line, width, color, selected);
|
|
67
|
+
const editing = view.compose && view.compose.kind === 'package';
|
|
68
|
+
const extra = editing
|
|
69
|
+
? {
|
|
70
|
+
...paintPackageEdit(view.compose, width, color),
|
|
71
|
+
at: 'index',
|
|
72
|
+
index: cursor,
|
|
73
|
+
}
|
|
74
|
+
: null;
|
|
75
|
+
const painted = paintCursorList(rows, cursor, pane, paintRow, extra);
|
|
76
|
+
if (!headed) return painted;
|
|
77
|
+
const head = paintMarginRow(content.lines[0], width, color, false);
|
|
78
|
+
const body = painted.body.slice();
|
|
79
|
+
const pad = pane.bodyH >= LIST_PAD * 2 + 1 ? LIST_PAD : 0;
|
|
80
|
+
body.splice(pad, 0, head);
|
|
81
|
+
const nudge = (point) => {
|
|
82
|
+
if (!point || point.row < pad) return point;
|
|
83
|
+
return { ...point, row: point.row + 1 };
|
|
84
|
+
};
|
|
85
|
+
return {
|
|
86
|
+
...painted,
|
|
87
|
+
body,
|
|
88
|
+
editHits: painted.editHits.map(nudge),
|
|
89
|
+
cursor: nudge(painted.cursor),
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
module.exports = { paintBodyPackages };
|