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
package/lib/render/npm.js CHANGED
@@ -1,16 +1,19 @@
1
1
  'use strict';
2
2
 
3
3
  const ansi = require('../ansi.js');
4
+ const npmCommands = require('../npm-commands.js');
5
+ const wrap = require('../wrap.js');
4
6
  const chrome = require('./chrome.js');
5
7
  const primitives = require('./primitives.js');
6
- const npmCommands = require('../npm-commands.js');
7
8
 
8
- const { THEME, paint, visibleWidth, truncateVisible, seq, EL } = ansi;
9
- const { clipAnsi, stripAnsi, RESET, foregroundOn } = ansi;
10
- const { formatBusyStatus } = chrome;
9
+ const { THEME, CODE_FG, paint, visibleWidth, truncateVisible } = ansi;
10
+ const { trimVisible, seq, EL, RESET, fg } = ansi;
11
+ const { clipAnsi, stripAnsi, foregroundOn } = ansi;
11
12
  const { TABLE_KEY, TABLE_VAL } = npmCommands;
12
- const { fill, FILE_MARK, paneResult, paintCursorList } = primitives;
13
- const { paintListEdit } = primitives;
13
+ const { wrapPlain } = wrap;
14
+ const { formatBusyStatus } = chrome;
15
+ const { fill, MARK_W, markPrefix, editSpan } = primitives;
16
+ const { paneResult, paintCursorList, fieldWindow } = primitives;
14
17
 
15
18
  const EDGE = 2;
16
19
  const TAIL = 1;
@@ -18,98 +21,140 @@ const LOG_PAD_Y = 1;
18
21
  const LOG_PAD_X = 2;
19
22
  const NPM_NAME_MAX = 24;
20
23
 
24
+ const TABLE_INK = {
25
+ actual: THEME.errorFg,
26
+ expected: THEME.addLineFg,
27
+ code: THEME.shaFg,
28
+ type: CODE_FG.className,
29
+ count: THEME.warnFg,
30
+ omitted: THEME.warnFg,
31
+ };
32
+
33
+ const COUNT_INK = {
34
+ failed: THEME.errorFg,
35
+ errors: THEME.errorFg,
36
+ passed: THEME.addLineFg,
37
+ };
38
+
21
39
  const logViewRows = (bodyH) => {
22
40
  const height = Math.max(1, bodyH);
23
41
  if (height >= LOG_PAD_Y * 2 + 1) return height - LOG_PAD_Y * 2;
24
42
  return height;
25
43
  };
26
44
 
27
- const rowFg = (selected) => (selected ? THEME.chromeFg : THEME.mutedFg);
28
-
29
- const npmNameWidth = (commands) => {
30
- let width = 0;
45
+ const npmNameWidth = (commands, typed = '') => {
46
+ let width = visibleWidth(typed);
31
47
  for (const entry of commands) {
32
48
  width = Math.max(width, visibleWidth(`${entry.name ?? ''}`));
33
49
  }
34
50
  return Math.min(NPM_NAME_MAX, width);
35
51
  };
36
52
 
37
- const paintNpmRow = (entry, width, color, selected, nameCol = 0) => {
38
- const inner = Math.max(1, width - EDGE - TAIL);
39
- const mark = selected ? FILE_MARK : ' ';
40
- const prefix = ` ${mark} `;
41
- const prefixW = visibleWidth(prefix);
42
- const rest = Math.max(1, inner - prefixW);
43
- const detail = entry.kind === 'bin' ? 'bin' : entry.command;
44
- const nameW = Math.min(Math.max(0, nameCol), rest);
45
- const name = truncateVisible(entry.name, nameW);
53
+ const npmColumns = (width, nameCol) => {
54
+ const rest = Math.max(1, width - EDGE - TAIL - MARK_W);
55
+ const nameW = Math.min(nameCol, rest);
46
56
  const gap = rest > nameW + 2 ? ' ' : '';
47
- const detailW = Math.max(0, rest - nameW - visibleWidth(gap));
48
- const shown = truncateVisible(detail, detailW);
49
- const mid = `${name}${' '.repeat(Math.max(0, nameW - visibleWidth(name)))}`;
50
- const after = `${gap}${shown}`;
51
- const pad = Math.max(0, rest - visibleWidth(mid + after));
52
- const left = `${prefix}${mid}${after}${' '.repeat(pad)}`;
53
- const fgRgb = rowFg(selected);
54
- const bgRgb = selected ? THEME.buttonBg : THEME.ctxBg;
57
+ return { rest, nameW, gap, detailW: rest - nameW - gap.length };
58
+ };
59
+
60
+ const paintNpmLine = (cols, name, detail, color, selected, bin) => {
61
+ const prefix = markPrefix(selected);
62
+ const namePad = ' '.repeat(Math.max(0, cols.nameW - visibleWidth(name)));
63
+ const mid = `${name}${namePad}`;
64
+ const after = `${cols.gap}${detail}`;
65
+ const pad = ' '.repeat(Math.max(0, cols.rest - visibleWidth(mid + after)));
55
66
  const tail = ' '.repeat(TAIL);
67
+ const fgRgb = selected ? THEME.chromeFg : THEME.mutedFg;
68
+ const bgRgb = selected ? THEME.buttonBg : THEME.ctxBg;
56
69
  const edge = fill(EDGE, fgRgb, THEME.ctxBg, color);
57
- if (!color) return `${left}${tail}${edge}`;
70
+ if (!color) return `${prefix}${mid}${after}${pad}${tail}${edge}`;
71
+ const nameFg = bin ? THEME.shaFg : THEME.buttonHotFg;
58
72
  let out = `${seq(fgRgb, bgRgb)}${EL}`;
59
73
  out += paint(prefix, fgRgb, bgRgb, color);
60
- const bin = entry.kind === 'bin';
61
- const nameFg = bin ? THEME.shaFg : THEME.buttonHotFg;
62
74
  out += paint(mid, nameFg, bgRgb, color, !bin);
63
- out += paint(`${after}${' '.repeat(pad)}${tail}`, fgRgb, bgRgb, color);
75
+ out += paint(`${after}${pad}${tail}`, fgRgb, bgRgb, color);
64
76
  return out + edge;
65
77
  };
66
78
 
79
+ const paintNpmRow = (entry, width, color, selected, nameCol) => {
80
+ const cols = npmColumns(width, nameCol);
81
+ const bin = entry.kind === 'bin';
82
+ const name = trimVisible(entry.name, cols.nameW);
83
+ const detail = trimVisible(bin ? 'bin' : entry.command, cols.detailW);
84
+ return paintNpmLine(cols, name, detail, color, selected, bin);
85
+ };
86
+
87
+ const editingName = (view) => view.npmEditField !== 'command';
88
+
89
+ const editWindow = (compose, live, draft, width) => {
90
+ const value = `${(live ? compose.text : draft) ?? ''}`.replaceAll('\n', ' ');
91
+ const win = live
92
+ ? fieldWindow(value, compose.cursor, compose.scrollCol, width)
93
+ : fieldWindow(value, 0, 0, width);
94
+ return { ...win, value };
95
+ };
96
+
97
+ const paintNpmEdit = (view, width, color, nameCol) => {
98
+ const { compose } = view;
99
+ const isName = editingName(view);
100
+ const cols = npmColumns(width, nameCol);
101
+ const { npmDraftName: draftName, npmDraftCommand: draftCommand } = view;
102
+ const name = editWindow(compose, isName, draftName, cols.nameW);
103
+ const command = editWindow(compose, !isName, draftCommand, cols.detailW);
104
+ const row = paintNpmLine(cols, name.text, command.text, color, true, false);
105
+ const nameX = MARK_W + 1;
106
+ const commandX = nameX + cols.nameW + cols.gap.length;
107
+ const active = isName ? name : command;
108
+ const cursor = {
109
+ x: (isName ? nameX : commandX) + active.col,
110
+ scroll: active.scroll,
111
+ };
112
+ const edits = [
113
+ {
114
+ field: 'name',
115
+ ...editSpan(1, nameX, commandX, 0, name.value),
116
+ scroll: name.scroll,
117
+ live: isName,
118
+ },
119
+ {
120
+ field: 'command',
121
+ ...editSpan(commandX, commandX, width + 1, 0, command.value),
122
+ scroll: command.scroll,
123
+ live: !isName,
124
+ },
125
+ ];
126
+ return { row, cursor, edits };
127
+ };
128
+
67
129
  const paintNpmList = (view, width, color, bodyH, headerLines) => {
68
130
  const commands = view.npmCommands ?? [];
69
- const nameW = npmNameWidth(commands);
131
+ const { compose } = view;
132
+ const editing = compose?.kind === 'npm';
133
+ const typed = editingName(view) ? compose?.text : view.npmDraftName;
134
+ const nameW = npmNameWidth(commands, editing ? `${typed ?? ''}` : '');
70
135
  const cursor = view.npmCursor ?? 0;
71
- const compose = view.compose;
72
- const editing = compose && compose.kind === 'npm';
73
- const paintRow = (entry, rowWidth, rowColor, selected) =>
74
- paintNpmRow(entry, rowWidth, rowColor, selected, nameW);
136
+ const pane = { width, color, bodyH, headerLines, offset: view.listScroll };
137
+ const paintRow = (entry, selected) =>
138
+ paintNpmRow(entry, width, color, selected, nameW);
75
139
  let extra = null;
76
140
  if (editing) {
77
- const painted = paintListEdit(compose, width, color, EDGE);
141
+ const painted = paintNpmEdit(view, width, color, nameW);
78
142
  const at = view.npmEditName ? 'replace' : 'end';
79
- extra = { row: painted.row, cursor: painted.cursor, at, index: cursor };
143
+ extra = { ...painted, at, index: cursor };
80
144
  }
81
- return paintCursorList(
82
- commands,
83
- cursor,
84
- width,
85
- color,
86
- bodyH,
87
- headerLines,
88
- paintRow,
89
- extra,
90
- view.listScroll,
91
- );
145
+ return paintCursorList(commands, cursor, pane, paintRow, extra);
92
146
  };
93
147
 
94
148
  const logStart = (view, count, rows) => {
95
149
  const maxStart = Math.max(0, count - rows);
96
- let start = maxStart;
97
- if (!view.npmFollow) start = view.npmScroll ?? 0;
98
- if (start > maxStart) start = maxStart;
99
- if (start < 0) start = 0;
100
- return start;
150
+ const start = view.npmFollow ? maxStart : view.npmScroll;
151
+ return Math.max(0, Math.min(start ?? 0, maxStart));
101
152
  };
102
153
 
103
154
  const isTableLine = (line) => `${line ?? ''}`.startsWith(TABLE_KEY);
104
155
 
105
- const mixRgb = (from, to, part) => {
106
- const out = [0, 0, 0];
107
- for (let i = 0; i < 3; i++) {
108
- const next = from[i] + (to[i] - from[i]) * part;
109
- out[i] = Math.round(next);
110
- }
111
- return out;
112
- };
156
+ const mixRgb = (from, to, part) =>
157
+ from.map((value, i) => Math.round(value + (to[i] - value) * part));
113
158
 
114
159
  const tableTone = (index) => {
115
160
  const lift = index % 2 === 0 ? 0.035 : 0.07;
@@ -120,65 +165,157 @@ const tableTone = (index) => {
120
165
  };
121
166
 
122
167
  const tableIndex = (lines, index) => {
123
- let at = index;
124
168
  let row = 0;
125
- while (at > 0 && isTableLine(lines[at - 1])) {
126
- at -= 1;
127
- row += 1;
128
- }
169
+ while (index - row > 0 && isTableLine(lines[index - row - 1])) row += 1;
129
170
  return row;
130
171
  };
131
172
 
132
- const paintTableLine = (line, width, index) => {
173
+ const tableInk = (key, value) => {
174
+ const name = key.trim();
175
+ if (Object.hasOwn(COUNT_INK, name)) {
176
+ return Number(value) > 0 ? COUNT_INK[name] : THEME.mutedFg;
177
+ }
178
+ return Object.hasOwn(TABLE_INK, name) ? TABLE_INK[name] : THEME.chromeFg;
179
+ };
180
+
181
+ const tableParts = (line, width) => {
133
182
  const raw = line.slice(TABLE_KEY.length);
134
183
  const at = raw.indexOf(TABLE_VAL);
135
184
  const key = at < 0 ? raw : raw.slice(0, at);
136
185
  const rest = at < 0 ? '' : raw.slice(at + TABLE_VAL.length);
137
186
  const gap = rest.startsWith(' ') ? ' ' : '';
138
187
  const value = rest.slice(gap.length);
139
- const edge = 1;
140
- const inner = Math.max(1, width - edge * 2);
141
- const keyW = visibleWidth(key);
142
- const gapW = visibleWidth(gap);
188
+ const edge = Math.min(1, Math.floor(width / 2));
189
+ const inner = Math.max(0, width - edge * 2);
190
+ const keyW = Math.min(visibleWidth(key), inner);
191
+ const gapW = Math.min(visibleWidth(gap), Math.max(0, inner - keyW));
143
192
  const valueW = Math.max(0, inner - keyW - gapW);
144
- const shown = truncateVisible(value, valueW);
145
- const pad = ' '.repeat(Math.max(0, valueW - visibleWidth(shown)));
193
+ return { key, gap, value, edge, keyW, gapW, valueW };
194
+ };
195
+
196
+ const wrapTableLine = (line, width) => {
197
+ const part = tableParts(line, width);
198
+ if (part.valueW < 1) return [line];
199
+ const trimmed = part.value.trimEnd();
200
+ const lead = trimmed.startsWith(' ') ? ' ' : '';
201
+ const body = trimmed.slice(lead.length);
202
+ const budget = Math.max(1, part.valueW - visibleWidth(lead));
203
+ if (visibleWidth(body) <= budget) return [line];
204
+ const blank = ' '.repeat(visibleWidth(part.key));
205
+ return wrapPlain(body, budget).map((piece, index) => {
206
+ const name = index === 0 ? part.key : blank;
207
+ return `${TABLE_KEY}${name}${TABLE_VAL}${part.gap}${lead}${piece}`;
208
+ });
209
+ };
210
+
211
+ const wrapLogText = (line, inner) => {
212
+ const mark = line.startsWith('│ ') ? '│ ' : '';
213
+ const rest = line.slice(mark.length);
214
+ const budget = Math.max(1, inner - visibleWidth(mark));
215
+ if (visibleWidth(rest) <= budget) return [line];
216
+ return wrapPlain(rest, budget).map((piece) => `${mark}${piece}`);
217
+ };
218
+
219
+ const expandLogLines = (text, width) => {
220
+ const lines = `${text ?? ''}`.split('\n');
221
+ if (lines.at(-1) === '') lines.pop();
222
+ const inner = Math.max(1, width - LOG_PAD_X * 2);
223
+ return lines.flatMap((line) => {
224
+ if (isTableLine(line)) return wrapTableLine(line, width);
225
+ return wrapLogText(line, inner);
226
+ });
227
+ };
228
+
229
+ const paintPlainTable = (line, width) => {
230
+ const part = tableParts(line, width);
231
+ const name = trimVisible(part.key, part.keyW);
232
+ const namePad = ' '.repeat(Math.max(0, part.keyW - visibleWidth(name)));
233
+ const shown = trimVisible(part.value, part.valueW);
234
+ const pad = ' '.repeat(Math.max(0, part.valueW - visibleWidth(shown)));
235
+ const edge = ' '.repeat(part.edge);
236
+ const gap = part.gap.slice(0, part.gapW);
237
+ return `${edge}${name}${namePad}${gap}${shown}${pad}${edge}`;
238
+ };
239
+
240
+ const paintTableLine = (line, width, index) => {
241
+ const part = tableParts(line, width);
242
+ const shownKey = trimVisible(part.key, part.keyW);
243
+ const shown = trimVisible(part.value, part.valueW);
244
+ const pad = ' '.repeat(Math.max(0, part.valueW - visibleWidth(shown)));
146
245
  const tone = tableTone(index);
147
246
  const ink = THEME.ctxFg;
148
- let out = fill(edge, ink, tone.frame, true);
149
- out += paint(key, ink, tone.keyBg, true);
150
- out += paint(gap, ink, tone.frame, true);
151
- out += paint(`${shown}${pad}`, ink, tone.valueBg, true);
152
- return `${out}${fill(edge, ink, tone.frame, true)}`;
247
+ const valueFg = tableInk(part.key, part.value);
248
+ let out = fill(part.edge, ink, tone.frame, true);
249
+ out += paint(shownKey, ink, tone.keyBg, true);
250
+ out += paint(part.gap.slice(0, part.gapW), ink, tone.frame, true);
251
+ out += paint(`${shown}${pad}`, valueFg, tone.valueBg, true);
252
+ return out + fill(part.edge, ink, tone.frame, true);
253
+ };
254
+
255
+ const paintLogSource = (line) => {
256
+ if (line !== stripAnsi(line)) return line;
257
+ if (/^[✖✔△] /.test(line)) {
258
+ const ink = line[0] === '✔' ? THEME.addLineFg : THEME.errorFg;
259
+ const color = line[0] === '△' ? THEME.warnFg : ink;
260
+ return paint(line, color, THEME.ctxBg, true, true);
261
+ }
262
+ if (line.startsWith('│')) {
263
+ return `${fg(THEME.shaFg)}│${fg(THEME.ctxFg)}${line.slice(1)}`;
264
+ }
265
+ if (/^(Stack|Related failures|Stderr)$/.test(line)) {
266
+ return paint(line, THEME.chromeFg, THEME.ctxBg, true, true);
267
+ }
268
+ if (!/^\s+(at|↳)\s/.test(line)) return line;
269
+ const source = line.replace(
270
+ /([^\s()|]+)(:\d+:\d+)(?=[\s)|]|$)/g,
271
+ (match, file, position) => {
272
+ const path = `${fg(THEME.shaFg)}${file}`;
273
+ const point = `${fg(THEME.mutedFg)}${position}`;
274
+ return `${path}${point}${fg(THEME.ctxFg)}`;
275
+ },
276
+ );
277
+ const frame = /^(\s+at\s+)(.*?)(\s+\(.+\))$/.exec(source);
278
+ if (!frame) return source;
279
+ return (
280
+ `${fg(THEME.mutedFg)}${frame[1]}${fg(CODE_FG.function)}${frame[2]}` +
281
+ `${fg(THEME.ctxFg)}${frame[3]}`
282
+ );
153
283
  };
154
284
 
155
285
  const paintLogLine = (line, width, color, fgRgb = THEME.ctxFg) => {
156
286
  const inner = Math.max(1, width - LOG_PAD_X * 2);
157
- const source = color ? `${line ?? ''}` : stripAnsi(line);
158
- const clip = (value) =>
159
- color ? clipAnsi(value, inner) : truncateVisible(value, inner);
160
- const body = color ? foregroundOn(clip(source), THEME.ctxBg) : clip(source);
161
287
  const lead = ' '.repeat(LOG_PAD_X);
288
+ if (!color) {
289
+ const body = truncateVisible(stripAnsi(line), inner);
290
+ const pad = Math.max(0, width - LOG_PAD_X - visibleWidth(body));
291
+ return `${lead}${body}${' '.repeat(pad)}`;
292
+ }
293
+ const source = paintLogSource(`${line ?? ''}`);
294
+ const body = foregroundOn(clipAnsi(source, inner), THEME.ctxBg);
162
295
  const pad = Math.max(0, width - LOG_PAD_X - visibleWidth(body));
163
- const tail = ' '.repeat(pad);
164
- if (!color) return `${lead}${body}${tail}`;
165
296
  const base = seq(fgRgb, THEME.ctxBg);
166
297
  let out = `${base}${EL}${lead}${body}`;
167
- if (pad) out += `${RESET}${base}${tail}`;
298
+ if (pad) out += `${RESET}${base}${' '.repeat(pad)}`;
168
299
  return `${out}${RESET}`;
169
300
  };
170
301
 
171
- const paintRunBar = (width, color, frame) => {
172
- const label = formatBusyStatus('running', frame);
173
- return paintLogLine(label, width, color, THEME.chromeFg);
302
+ const paintLogRow = (view, lines, index, width, color) => {
303
+ if (index < lines.length) {
304
+ const line = lines[index];
305
+ if (!isTableLine(line)) return paintLogLine(line, width, color);
306
+ if (!color) return paintPlainTable(line, width);
307
+ return paintTableLine(line, width, tableIndex(lines, index));
308
+ }
309
+ if (view.npmRunning === true && index === lines.length) {
310
+ const label = formatBusyStatus('running', view.progressFrame ?? 0);
311
+ return paintLogLine(label, width, color, THEME.chromeFg);
312
+ }
313
+ return fill(width, THEME.ctxFg, THEME.ctxBg, color);
174
314
  };
175
315
 
176
316
  const paintNpmLog = (view, width, color, bodyH) => {
177
- const text = view.npmOutput ?? '';
178
- const lines = text.split('\n');
179
- if (lines.length && lines.at(-1) === '') lines.pop();
180
- const running = view.npmRunning === true;
181
- const count = lines.length + (running ? 1 : 0);
317
+ const lines = expandLogLines(view.npmOutput ?? '', width);
318
+ const count = lines.length + (view.npmRunning === true ? 1 : 0);
182
319
  const rows = logViewRows(bodyH);
183
320
  const padY = bodyH - rows;
184
321
  const top = padY ? 1 : 0;
@@ -187,25 +324,10 @@ const paintNpmLog = (view, width, color, bodyH) => {
187
324
  const body = [];
188
325
  for (let i = 0; i < top; i++) body.push(blank);
189
326
  for (let i = 0; i < rows; i++) {
190
- const index = start + i;
191
- if (index < lines.length) {
192
- const line = lines[index];
193
- if (color && isTableLine(line)) {
194
- const row = tableIndex(lines, index);
195
- body.push(paintTableLine(line, width, row));
196
- } else {
197
- body.push(paintLogLine(line, width, color));
198
- }
199
- } else if (running && index === lines.length) {
200
- body.push(paintRunBar(width, color, view.progressFrame ?? 0));
201
- } else {
202
- body.push(blank);
203
- }
327
+ body.push(paintLogRow(view, lines, start + i, width, color));
204
328
  }
205
329
  for (let i = 0; i < padY - top; i++) body.push(blank);
206
- return paneResult({
207
- body,
208
- });
330
+ return paneResult({ body });
209
331
  };
210
332
 
211
333
  const paintBodyNpm = (view, width, color, bodyH, headerLines) => {
@@ -213,4 +335,4 @@ const paintBodyNpm = (view, width, color, bodyH, headerLines) => {
213
335
  return paintNpmList(view, width, color, bodyH, headerLines);
214
336
  };
215
337
 
216
- module.exports = { paintBodyNpm, logViewRows };
338
+ module.exports = { paintBodyNpm, logViewRows, expandLogLines };
@@ -10,17 +10,10 @@ const presentCursor = (cursor) => {
10
10
  };
11
11
 
12
12
  const presentRows = (rows, options = {}) => {
13
- const clear = options.clear === true;
14
- const cursor = options.cursor;
15
- const parts = [`${ESC}[?25l`, `${ESC}[?2026h`];
16
- if (clear) parts.push(`${ESC}[H${ESC}[2J`);
17
- for (let i = 0; i < rows.length; i++) {
18
- parts.push(`${ESC}[${i + 1};1H`);
19
- parts.push(rows[i]);
20
- }
21
- parts.push(`${ESC}[?2026l`);
22
- parts.push(presentCursor(cursor));
23
- return parts.join('');
13
+ let out = `${ESC}[?25l${ESC}[?2026h`;
14
+ if (options.clear === true) out += `${ESC}[H${ESC}[2J`;
15
+ for (let i = 0; i < rows.length; i++) out += `${ESC}[${i + 1};1H${rows[i]}`;
16
+ return `${out}${ESC}[?2026l${presentCursor(options.cursor)}`;
24
17
  };
25
18
 
26
19
  module.exports = { presentCursor, presentRows };