reslop 0.2.5 → 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 (114) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/LICENSE +1 -1
  3. package/README.md +11 -11
  4. package/bin/reslop.js +3 -8
  5. package/lib/ansi.js +72 -19
  6. package/lib/api.js +1 -1
  7. package/lib/capabilities.js +8 -8
  8. package/lib/cli.js +61 -75
  9. package/lib/clipboard.js +39 -47
  10. package/lib/dashboard/git.js +128 -0
  11. package/lib/dashboard/model.js +357 -0
  12. package/lib/dashboard/npm.js +290 -0
  13. package/lib/dashboard/tree.js +278 -0
  14. package/lib/deps-diff.js +197 -402
  15. package/lib/deps.js +113 -240
  16. package/lib/detect.js +8 -18
  17. package/lib/diff/align.js +41 -113
  18. package/lib/diff/diff.js +12 -43
  19. package/lib/diff/display.js +25 -36
  20. package/lib/diff/edit.js +6 -20
  21. package/lib/diff/inline.js +76 -144
  22. package/lib/diff/items.js +33 -47
  23. package/lib/diff/parse.js +28 -56
  24. package/lib/diff/patch.js +12 -33
  25. package/lib/diff/unit.js +79 -178
  26. package/lib/editor.js +52 -69
  27. package/lib/files.js +79 -91
  28. package/lib/find.js +61 -0
  29. package/lib/git-branches.js +254 -416
  30. package/lib/git-deps.js +148 -272
  31. package/lib/git-reword-todo.js +5 -13
  32. package/lib/git-worktree.js +161 -214
  33. package/lib/git.js +21 -49
  34. package/lib/github.js +67 -181
  35. package/lib/gitlab.js +40 -90
  36. package/lib/highlight/bash.js +55 -177
  37. package/lib/highlight/core.js +155 -141
  38. package/lib/highlight/css.js +25 -58
  39. package/lib/highlight/csv.js +19 -37
  40. package/lib/highlight/dart.js +72 -213
  41. package/lib/highlight/dot.js +14 -14
  42. package/lib/highlight/html.js +42 -63
  43. package/lib/highlight/js.js +177 -506
  44. package/lib/highlight/json.js +17 -37
  45. package/lib/highlight/log.js +19 -25
  46. package/lib/highlight/txt.js +7 -10
  47. package/lib/highlight.js +17 -46
  48. package/lib/keys.js +75 -83
  49. package/lib/manifest.js +67 -126
  50. package/lib/npm-commands.js +120 -169
  51. package/lib/remote.js +140 -190
  52. package/lib/render/branches.js +63 -61
  53. package/lib/render/chrome.js +412 -347
  54. package/lib/render/commits.js +160 -361
  55. package/lib/render/dash-activity.js +212 -0
  56. package/lib/render/dash-blocks.js +496 -0
  57. package/lib/render/dash-table.js +295 -0
  58. package/lib/render/dashboard.js +62 -0
  59. package/lib/render/diff.js +123 -258
  60. package/lib/render/files.js +60 -86
  61. package/lib/render/frame.js +82 -117
  62. package/lib/render/notes.js +150 -210
  63. package/lib/render/npm.js +213 -177
  64. package/lib/render/packages.js +93 -0
  65. package/lib/render/present.js +4 -11
  66. package/lib/render/primitives.js +77 -172
  67. package/lib/render/render.js +5 -14
  68. package/lib/render/report.js +43 -0
  69. package/lib/render/repos.js +226 -0
  70. package/lib/render/search.js +25 -0
  71. package/lib/render/tiles.js +355 -0
  72. package/lib/render/unit.js +15 -31
  73. package/lib/report-model.js +110 -33
  74. package/lib/report-parse.js +56 -37
  75. package/lib/report-render.js +102 -59
  76. package/lib/report-run.js +87 -77
  77. package/lib/report-test.js +221 -99
  78. package/lib/report-tools.js +96 -154
  79. package/lib/review-merge.js +33 -41
  80. package/lib/review.js +69 -129
  81. package/lib/runs.js +225 -0
  82. package/lib/select.js +53 -91
  83. package/lib/session/accessors.js +14 -21
  84. package/lib/session/actions.js +189 -65
  85. package/lib/session/branches.js +164 -222
  86. package/lib/session/change-actions.js +49 -119
  87. package/lib/session/commit.js +298 -352
  88. package/lib/session/compose.js +572 -715
  89. package/lib/session/dashboard.js +530 -0
  90. package/lib/session/dispatch.js +197 -171
  91. package/lib/session/files-pane.js +92 -45
  92. package/lib/session/find.js +94 -0
  93. package/lib/session/items.js +6 -17
  94. package/lib/session/lifecycle.js +420 -466
  95. package/lib/session/load.js +1 -14
  96. package/lib/session/navigation.js +4 -2
  97. package/lib/session/npm.js +403 -420
  98. package/lib/session/ops.js +5 -13
  99. package/lib/session/packages.js +143 -0
  100. package/lib/session/pointer.js +30 -14
  101. package/lib/session/progress.js +39 -30
  102. package/lib/session/review.js +1 -3
  103. package/lib/session/tasks.js +277 -236
  104. package/lib/session/templates.js +51 -59
  105. package/lib/session/terminal.js +85 -132
  106. package/lib/session/view.js +59 -80
  107. package/lib/session/watch.js +201 -212
  108. package/lib/session/workspace.js +455 -0
  109. package/lib/session.js +95 -161
  110. package/lib/update.js +44 -111
  111. package/lib/utilities.js +46 -88
  112. package/lib/workspace.js +166 -0
  113. package/lib/wrap.js +29 -55
  114. 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 };
@@ -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 };
@@ -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 };