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
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 { fieldWindow } = 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,152 +21,117 @@ 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
 
67
- const editNameSource = (view, compose, field) => {
68
- if (field === 'name') return compose.text;
69
- return view.npmDraftName ?? '';
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);
70
85
  };
71
86
 
72
- const editNameWidth = (commands, live) => {
73
- const typed = visibleWidth(`${live ?? ''}`);
74
- return Math.min(NPM_NAME_MAX, Math.max(npmNameWidth(commands), typed));
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 };
75
95
  };
76
96
 
77
- const paintNpmEdit = (view, width, color, nameW) => {
78
- const compose = view.compose;
79
- const field = view.npmEditField === 'command' ? 'command' : 'name';
80
- const nameSource = editNameSource(view, compose, field);
81
- const draft = view.npmDraftCommand ?? '';
82
- const commandSource = field === 'command' ? compose.text : draft;
83
- const inner = Math.max(1, width - EDGE - TAIL);
84
- const prefix = ` ${FILE_MARK} `;
85
- const prefixW = visibleWidth(prefix);
86
- const rest = Math.max(1, inner - prefixW);
87
- const colW = Math.min(Math.max(0, nameW), rest);
88
- const flatName = `${nameSource ?? ''}`.replaceAll('\n', ' ');
89
- const nameAt = field === 'name' ? compose.cursor : 0;
90
- const nameScroll = field === 'name' ? compose.scrollCol : 0;
91
- const nameWin = fieldWindow(flatName, nameAt, nameScroll, colW);
92
- const name = nameWin.text;
93
- const gap = rest > colW + 2 ? ' ' : '';
94
- const gapW = visibleWidth(gap);
95
- const detailW = Math.max(0, rest - colW - gapW);
96
- const flatCommand = `${commandSource ?? ''}`.replaceAll('\n', ' ');
97
- const commandAt = field === 'command' ? compose.cursor : 0;
98
- const commandScroll = field === 'command' ? compose.scrollCol : 0;
99
- const commandWin = fieldWindow(
100
- flatCommand,
101
- commandAt,
102
- commandScroll,
103
- detailW,
104
- );
105
- const command = commandWin.text;
106
- const namePad = ' '.repeat(Math.max(0, colW - visibleWidth(name)));
107
- const mid = `${name}${namePad}`;
108
- const after = `${gap}${command}`;
109
- const pad = Math.max(0, rest - visibleWidth(mid + after));
110
- const tail = ' '.repeat(TAIL);
111
- const left = `${prefix}${mid}${after}${' '.repeat(pad)}`;
112
- const fgRgb = THEME.chromeFg;
113
- const bgRgb = THEME.buttonBg;
114
- const edge = fill(EDGE, fgRgb, THEME.ctxBg, color);
115
- let row = `${left}${tail}${edge}`;
116
- if (color) {
117
- row = `${seq(fgRgb, bgRgb)}${EL}`;
118
- row += paint(prefix, fgRgb, bgRgb, color);
119
- row += paint(mid, THEME.buttonHotFg, bgRgb, color, true);
120
- row += paint(`${after}${' '.repeat(pad)}${tail}`, fgRgb, bgRgb, color);
121
- row += edge;
122
- }
123
- const active = field === 'name' ? nameWin : commandWin;
124
- const lead = field === 'name' ? 0 : colW + gapW;
125
- const nameX = prefixW + 1;
126
- const commandX = prefixW + colW + gapW + 1;
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
+ };
127
112
  const edits = [
128
113
  {
129
114
  field: 'name',
130
- x0: 1,
131
- textX: nameX,
132
- x1: commandX,
133
- start: 0,
134
- text: flatName,
135
- limit: flatName.length,
136
- scroll: nameWin.scroll,
137
- live: field === 'name',
115
+ ...editSpan(1, nameX, commandX, 0, name.value),
116
+ scroll: name.scroll,
117
+ live: isName,
138
118
  },
139
119
  {
140
120
  field: 'command',
141
- x0: commandX,
142
- textX: commandX,
143
- x1: width + 1,
144
- start: 0,
145
- text: flatCommand,
146
- limit: flatCommand.length,
147
- scroll: commandWin.scroll,
148
- live: field === 'command',
121
+ ...editSpan(commandX, commandX, width + 1, 0, command.value),
122
+ scroll: command.scroll,
123
+ live: !isName,
149
124
  },
150
125
  ];
151
- const cursor = {
152
- x: prefixW + lead + active.col + 1,
153
- scroll: active.scroll,
154
- };
155
126
  return { row, cursor, edits };
156
127
  };
157
128
 
158
129
  const paintNpmList = (view, width, color, bodyH, headerLines) => {
159
130
  const commands = view.npmCommands ?? [];
160
- const compose = view.compose;
161
- const editing = compose && compose.kind === 'npm';
162
- const field = view.npmEditField === 'command' ? 'command' : 'name';
163
- const live = editing ? editNameSource(view, compose, field) : '';
164
- const nameW = editing
165
- ? editNameWidth(commands, live)
166
- : 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 ?? ''}` : '');
167
135
  const cursor = view.npmCursor ?? 0;
168
136
  const pane = { width, color, bodyH, headerLines, offset: view.listScroll };
169
137
  const paintRow = (entry, selected) =>
@@ -179,23 +147,14 @@ const paintNpmList = (view, width, color, bodyH, headerLines) => {
179
147
 
180
148
  const logStart = (view, count, rows) => {
181
149
  const maxStart = Math.max(0, count - rows);
182
- let start = maxStart;
183
- if (!view.npmFollow) start = view.npmScroll ?? 0;
184
- if (start > maxStart) start = maxStart;
185
- if (start < 0) start = 0;
186
- return start;
150
+ const start = view.npmFollow ? maxStart : view.npmScroll;
151
+ return Math.max(0, Math.min(start ?? 0, maxStart));
187
152
  };
188
153
 
189
154
  const isTableLine = (line) => `${line ?? ''}`.startsWith(TABLE_KEY);
190
155
 
191
- const mixRgb = (from, to, part) => {
192
- const out = [0, 0, 0];
193
- for (let i = 0; i < 3; i++) {
194
- const next = from[i] + (to[i] - from[i]) * part;
195
- out[i] = Math.round(next);
196
- }
197
- return out;
198
- };
156
+ const mixRgb = (from, to, part) =>
157
+ from.map((value, i) => Math.round(value + (to[i] - value) * part));
199
158
 
200
159
  const tableTone = (index) => {
201
160
  const lift = index % 2 === 0 ? 0.035 : 0.07;
@@ -206,65 +165,157 @@ const tableTone = (index) => {
206
165
  };
207
166
 
208
167
  const tableIndex = (lines, index) => {
209
- let at = index;
210
168
  let row = 0;
211
- while (at > 0 && isTableLine(lines[at - 1])) {
212
- at -= 1;
213
- row += 1;
214
- }
169
+ while (index - row > 0 && isTableLine(lines[index - row - 1])) row += 1;
215
170
  return row;
216
171
  };
217
172
 
218
- 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) => {
219
182
  const raw = line.slice(TABLE_KEY.length);
220
183
  const at = raw.indexOf(TABLE_VAL);
221
184
  const key = at < 0 ? raw : raw.slice(0, at);
222
185
  const rest = at < 0 ? '' : raw.slice(at + TABLE_VAL.length);
223
186
  const gap = rest.startsWith(' ') ? ' ' : '';
224
187
  const value = rest.slice(gap.length);
225
- const edge = 1;
226
- const inner = Math.max(1, width - edge * 2);
227
- const keyW = visibleWidth(key);
228
- 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));
229
192
  const valueW = Math.max(0, inner - keyW - gapW);
230
- const shown = truncateVisible(value, valueW);
231
- 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)));
232
245
  const tone = tableTone(index);
233
246
  const ink = THEME.ctxFg;
234
- let out = fill(edge, ink, tone.frame, true);
235
- out += paint(key, ink, tone.keyBg, true);
236
- out += paint(gap, ink, tone.frame, true);
237
- out += paint(`${shown}${pad}`, ink, tone.valueBg, true);
238
- 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
+ );
239
283
  };
240
284
 
241
285
  const paintLogLine = (line, width, color, fgRgb = THEME.ctxFg) => {
242
286
  const inner = Math.max(1, width - LOG_PAD_X * 2);
243
- const source = color ? `${line ?? ''}` : stripAnsi(line);
244
- const clip = (value) =>
245
- color ? clipAnsi(value, inner) : truncateVisible(value, inner);
246
- const body = color ? foregroundOn(clip(source), THEME.ctxBg) : clip(source);
247
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);
248
295
  const pad = Math.max(0, width - LOG_PAD_X - visibleWidth(body));
249
- const tail = ' '.repeat(pad);
250
- if (!color) return `${lead}${body}${tail}`;
251
296
  const base = seq(fgRgb, THEME.ctxBg);
252
297
  let out = `${base}${EL}${lead}${body}`;
253
- if (pad) out += `${RESET}${base}${tail}`;
298
+ if (pad) out += `${RESET}${base}${' '.repeat(pad)}`;
254
299
  return `${out}${RESET}`;
255
300
  };
256
301
 
257
- const paintRunBar = (width, color, frame) => {
258
- const label = formatBusyStatus('running', frame);
259
- 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);
260
314
  };
261
315
 
262
316
  const paintNpmLog = (view, width, color, bodyH) => {
263
- const text = view.npmOutput ?? '';
264
- const lines = text.split('\n');
265
- if (lines.length && lines.at(-1) === '') lines.pop();
266
- const running = view.npmRunning === true;
267
- const count = lines.length + (running ? 1 : 0);
317
+ const lines = expandLogLines(view.npmOutput ?? '', width);
318
+ const count = lines.length + (view.npmRunning === true ? 1 : 0);
268
319
  const rows = logViewRows(bodyH);
269
320
  const padY = bodyH - rows;
270
321
  const top = padY ? 1 : 0;
@@ -273,25 +324,10 @@ const paintNpmLog = (view, width, color, bodyH) => {
273
324
  const body = [];
274
325
  for (let i = 0; i < top; i++) body.push(blank);
275
326
  for (let i = 0; i < rows; i++) {
276
- const index = start + i;
277
- if (index < lines.length) {
278
- const line = lines[index];
279
- if (color && isTableLine(line)) {
280
- const row = tableIndex(lines, index);
281
- body.push(paintTableLine(line, width, row));
282
- } else {
283
- body.push(paintLogLine(line, width, color));
284
- }
285
- } else if (running && index === lines.length) {
286
- body.push(paintRunBar(width, color, view.progressFrame ?? 0));
287
- } else {
288
- body.push(blank);
289
- }
327
+ body.push(paintLogRow(view, lines, start + i, width, color));
290
328
  }
291
329
  for (let i = 0; i < padY - top; i++) body.push(blank);
292
- return paneResult({
293
- body,
294
- });
330
+ return paneResult({ body });
295
331
  };
296
332
 
297
333
  const paintBodyNpm = (view, width, color, bodyH, headerLines) => {
@@ -299,4 +335,4 @@ const paintBodyNpm = (view, width, color, bodyH, headerLines) => {
299
335
  return paintNpmList(view, width, color, bodyH, headerLines);
300
336
  };
301
337
 
302
- 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 };