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.
Files changed (42) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/lib/ansi.js +42 -2
  3. package/lib/api.js +1 -1
  4. package/lib/cli.js +21 -1
  5. package/lib/dashboard/model.js +28 -3
  6. package/lib/dashboard/npm.js +116 -19
  7. package/lib/files.js +13 -7
  8. package/lib/find.js +61 -0
  9. package/lib/git-deps.js +1 -0
  10. package/lib/git-worktree.js +22 -4
  11. package/lib/keys.js +26 -1
  12. package/lib/render/chrome.js +257 -79
  13. package/lib/render/dash-blocks.js +297 -46
  14. package/lib/render/dash-table.js +6 -1
  15. package/lib/render/files.js +23 -4
  16. package/lib/render/frame.js +4 -0
  17. package/lib/render/notes.js +13 -2
  18. package/lib/render/packages.js +93 -0
  19. package/lib/render/primitives.js +13 -6
  20. package/lib/render/repos.js +226 -0
  21. package/lib/render/search.js +25 -0
  22. package/lib/render/tiles.js +107 -12
  23. package/lib/review.js +5 -4
  24. package/lib/session/actions.js +136 -30
  25. package/lib/session/branches.js +3 -5
  26. package/lib/session/commit.js +31 -4
  27. package/lib/session/compose.js +19 -4
  28. package/lib/session/dashboard.js +160 -15
  29. package/lib/session/dispatch.js +38 -4
  30. package/lib/session/files-pane.js +17 -2
  31. package/lib/session/find.js +94 -0
  32. package/lib/session/lifecycle.js +27 -0
  33. package/lib/session/navigation.js +2 -0
  34. package/lib/session/packages.js +143 -0
  35. package/lib/session/pointer.js +14 -0
  36. package/lib/session/tasks.js +3 -3
  37. package/lib/session/view.js +15 -2
  38. package/lib/session/watch.js +5 -0
  39. package/lib/session/workspace.js +455 -0
  40. package/lib/session.js +14 -0
  41. package/lib/workspace.js +166 -0
  42. 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, sparkline, spinner } = tiles;
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 TREND_LABEL = 'trend ';
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 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')];
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 = trendFooter(data, width);
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 differs = (left, right) =>
170
- left !== UNKNOWN && right !== UNKNOWN && left !== right;
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 versionCell = (text, changed, audited) => {
173
- const base = changed ? 'warn' : 'muted';
174
- const tone = audited && text !== UNKNOWN ? 'error' : base;
175
- return cell(text, tone, 'r');
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
- 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),
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 npmBlock = (model, width, height, ctx, tile) => {
206
- const data = model.npm;
207
- if (!data.ready) return waiting('reading package.json…');
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 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) };
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 taskName = (title) => {
220
- const name = flexCell(title, 'text');
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 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
- ];
434
+ const TASK_GAP = 2;
230
435
 
231
- const tasksBlock = (model, width, height, ctx, tile) => {
232
- const data = model.tasks;
233
- const rows = [taskLine('', data.done, data.total, true)];
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(taskLine(kind.title, kind.done, kind.total));
441
+ rows.push({
442
+ title: kind.title,
443
+ done: kind.done,
444
+ total: kind.total,
445
+ bold: false,
446
+ });
236
447
  }
237
- const lines = tableLines(rows, width);
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 = { filesBlock, diffsBlock, npmBlock, tasksBlock };
489
+ module.exports = {
490
+ filesBlock,
491
+ diffsBlock,
492
+ npmBlock,
493
+ npmContent,
494
+ npmStatus,
495
+ tasksBlock,
496
+ };
@@ -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], segmentsWidth(item.segs));
81
+ widths[c] = Math.max(widths[c], measuredWidth(item));
77
82
  }
78
83
  }
79
84
  return widths;
@@ -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 paintFileRow = (entry, width, color, selected, cols) => {
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(left, fgRgb, bgRgb, color, tasks);
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 paintRow = (entry, selected) =>
92
- paintFileRow(entry, width, color, selected, cols);
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
 
@@ -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
  };
@@ -94,7 +94,10 @@ const taskLists = (view) => {
94
94
  };
95
95
 
96
96
  const paintSectionHead = (title, width, color) =>
97
- paintBar(` ${title}`, width, THEME.mutedFg, THEME.ctxBg, color);
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
- for (const section of taskLists(view)) {
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 };