reslop 0.2.7 → 0.3.1

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 (158) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/README.md +16 -14
  3. package/bin/reslop.js +8 -5
  4. package/lib/agents/agents.js +573 -0
  5. package/lib/agents/allow.js +71 -0
  6. package/lib/agents/allow.json +17 -0
  7. package/lib/agents/models.js +467 -0
  8. package/lib/api.js +15 -12
  9. package/lib/cli.js +43 -68
  10. package/lib/{files.js → common/files.js} +48 -55
  11. package/lib/{find.js → common/find.js} +1 -1
  12. package/lib/common/format.js +101 -0
  13. package/lib/{utilities.js → common/process.js} +7 -95
  14. package/lib/common/tasks.json +27 -0
  15. package/lib/common/utilities.js +112 -0
  16. package/lib/dashboard/git.js +9 -16
  17. package/lib/dashboard/model.js +90 -49
  18. package/lib/dashboard/npm.js +17 -13
  19. package/lib/dashboard/tree.js +27 -30
  20. package/lib/{session → dashboard}/watch.js +36 -31
  21. package/lib/{workspace.js → dashboard/workspace.js} +23 -38
  22. package/lib/{deps.js → deps/deps.js} +31 -45
  23. package/lib/{deps-diff.js → deps/diff.js} +35 -25
  24. package/lib/{manifest.js → deps/manifest.js} +47 -30
  25. package/lib/diff/align.js +1 -2
  26. package/lib/diff/diff.js +2 -2
  27. package/lib/diff/display.js +29 -5
  28. package/lib/diff/inline.js +10 -14
  29. package/lib/diff/items.js +6 -4
  30. package/lib/diff/patch.js +39 -3
  31. package/lib/diff/unit.js +45 -16
  32. package/lib/{git-branches.js → git/branches.js} +53 -48
  33. package/lib/{git-deps.js → git/deps.js} +151 -95
  34. package/lib/git/git.js +71 -0
  35. package/lib/git/worktree.js +504 -0
  36. package/lib/highlight/bash.js +1 -2
  37. package/lib/highlight/csv.js +1 -2
  38. package/lib/highlight/dot.js +1 -2
  39. package/lib/{highlight.js → highlight/highlight.js} +42 -11
  40. package/lib/highlight/html.js +20 -18
  41. package/lib/highlight/json.js +1 -2
  42. package/lib/highlight/log.js +1 -2
  43. package/lib/highlight/txt.js +1 -2
  44. package/lib/{session → input}/actions.js +114 -48
  45. package/lib/{editor.js → input/editor.js} +16 -8
  46. package/lib/{keys.js → input/keys.js} +24 -45
  47. package/lib/input/line-edit.js +78 -0
  48. package/lib/input/menu-pick.js +70 -0
  49. package/lib/render/agents.js +642 -0
  50. package/lib/render/branches.js +4 -5
  51. package/lib/render/commits.js +54 -64
  52. package/lib/render/{dash-activity.js → dashboard/activity.js} +25 -40
  53. package/lib/render/{dash-blocks.js → dashboard/blocks.js} +131 -137
  54. package/lib/render/{dashboard.js → dashboard/dashboard.js} +25 -22
  55. package/lib/render/diff.js +166 -45
  56. package/lib/render/field.js +131 -0
  57. package/lib/render/files.js +9 -14
  58. package/lib/render/footer.js +385 -0
  59. package/lib/render/frame.js +123 -25
  60. package/lib/render/header.js +176 -0
  61. package/lib/render/log.js +320 -0
  62. package/lib/render/menu.js +60 -0
  63. package/lib/render/notes.js +16 -11
  64. package/lib/render/npm.js +322 -196
  65. package/lib/render/packages.js +9 -13
  66. package/lib/render/primitives.js +101 -15
  67. package/lib/render/render.js +7 -11
  68. package/lib/render/repos.js +13 -25
  69. package/lib/render/status.js +370 -0
  70. package/lib/render/status.json +85 -0
  71. package/lib/render/{dash-table.js → table.js} +13 -60
  72. package/lib/render/tiles.js +109 -48
  73. package/lib/render/unit.js +32 -13
  74. package/lib/{report-model.js → report/model.js} +4 -6
  75. package/lib/{report-parse.js → report/parse.js} +9 -14
  76. package/lib/{report-render.js → report/render.js} +48 -19
  77. package/lib/{report-run.js → report/run.js} +42 -30
  78. package/lib/{report-test.js → report/test.js} +56 -55
  79. package/lib/{report-tools.js → report/tools.js} +24 -33
  80. package/lib/review/agent-instructions.md +14 -0
  81. package/lib/review/format.js +420 -0
  82. package/lib/{review-merge.js → review/merge.js} +9 -8
  83. package/lib/review/review.js +190 -0
  84. package/lib/review/store.js +281 -0
  85. package/lib/review/templates.js +83 -0
  86. package/lib/runs/commands.js +14 -0
  87. package/lib/runs/logs.js +299 -0
  88. package/lib/runs/output.js +174 -0
  89. package/lib/runs/process.js +113 -0
  90. package/lib/{runs.js → runs/runs.js} +84 -36
  91. package/lib/runs/scripts.js +150 -0
  92. package/lib/session/agents/agents.js +976 -0
  93. package/lib/session/agents/job.js +320 -0
  94. package/lib/session/agents/jobs.js +355 -0
  95. package/lib/session/agents/options.js +153 -0
  96. package/lib/session/agents/plans.js +83 -0
  97. package/lib/session/branches.js +33 -40
  98. package/lib/session/change-actions.js +34 -100
  99. package/lib/session/commit.js +91 -52
  100. package/lib/session/compose.js +189 -147
  101. package/lib/session/dashboard.js +23 -22
  102. package/lib/session/dispatch.js +86 -21
  103. package/lib/session/files-pane.js +47 -26
  104. package/lib/session/find.js +16 -29
  105. package/lib/session/import.js +93 -0
  106. package/lib/session/items.js +8 -17
  107. package/lib/session/lifecycle.js +78 -72
  108. package/lib/session/load.js +2 -4
  109. package/lib/session/navigation.js +6 -3
  110. package/lib/session/npm/history.js +250 -0
  111. package/lib/session/npm/npm.js +836 -0
  112. package/lib/session/{packages.js → npm/packages.js} +21 -14
  113. package/lib/session/ops.js +125 -34
  114. package/lib/session/pointer.js +202 -10
  115. package/lib/session/prefs.js +112 -0
  116. package/lib/session/reads.js +109 -0
  117. package/lib/session/review.js +28 -10
  118. package/lib/session/run-panel.js +40 -0
  119. package/lib/{session.js → session/session.js} +158 -63
  120. package/lib/session/tasks.js +321 -66
  121. package/lib/session/templates.js +1 -1
  122. package/lib/session/terminal.js +94 -12
  123. package/lib/session/unit.js +35 -30
  124. package/lib/session/update.js +194 -6
  125. package/lib/session/view.js +65 -65
  126. package/lib/session/workspace.js +39 -78
  127. package/lib/{capabilities.js → source/capabilities.js} +3 -12
  128. package/lib/{github.js → source/github.js} +102 -32
  129. package/lib/{gitlab.js → source/gitlab.js} +87 -10
  130. package/lib/{remote.js → source/remote.js} +144 -40
  131. package/lib/source/review-threads.graphql +20 -0
  132. package/lib/source/source.js +74 -0
  133. package/lib/term/ansi.js +396 -0
  134. package/lib/{clipboard.js → term/clipboard.js} +3 -6
  135. package/lib/{select.js → term/select.js} +4 -3
  136. package/lib/term/themes/dark.json +122 -0
  137. package/lib/term/themes/light.json +122 -0
  138. package/lib/term/wide.txt +21 -0
  139. package/lib/{wrap.js → term/wrap.js} +4 -4
  140. package/package.json +2 -2
  141. package/lib/ansi.js +0 -545
  142. package/lib/detect.js +0 -34
  143. package/lib/diff/edit.js +0 -33
  144. package/lib/git-worktree.js +0 -455
  145. package/lib/git.js +0 -79
  146. package/lib/npm-commands.js +0 -522
  147. package/lib/render/chrome.js +0 -806
  148. package/lib/render/present.js +0 -19
  149. package/lib/render/report.js +0 -43
  150. package/lib/render/search.js +0 -25
  151. package/lib/review.js +0 -745
  152. package/lib/session/accessors.js +0 -26
  153. package/lib/session/edit-click.js +0 -57
  154. package/lib/session/npm.js +0 -452
  155. package/lib/session/progress.js +0 -41
  156. package/lib/source.js +0 -36
  157. package/lib/update.js +0 -192
  158. /package/lib/{git-reword-todo.js → git/reword-todo.js} +0 -0
@@ -0,0 +1,176 @@
1
+ 'use strict';
2
+
3
+ const ansi = require('../term/ansi.js');
4
+ const files = require('../common/files.js');
5
+ const primitives = require('./primitives.js');
6
+ const { isTaskView, paintPathText } = primitives;
7
+ const { itemPath, REPO_TASKS_LABEL, isTasksEntry } = files;
8
+ const { THEME, visibleWidth } = ansi;
9
+ const { truncateVisible, fg, RESET, EL, seq } = ansi;
10
+ const { activeHit, spansFor } = require('../common/find.js');
11
+
12
+ const LOGO = '👁️';
13
+
14
+ const HEADER_LEAD = ` ${LOGO} reslop: `;
15
+
16
+ const HEADER_ROLE_FG = {
17
+ chrome: THEME.headerChromeFg,
18
+ repo: THEME.headerRepoFg,
19
+ dir: THEME.headerDirFg,
20
+ slash: THEME.headerSlashFg,
21
+ file: THEME.headerFileFg,
22
+ };
23
+
24
+ const selectedFile = (view) => (view.files ?? [])[view.fileCursor ?? 0];
25
+
26
+ const selectedBranch = (view) => (view.branches ?? [])[view.branchCursor ?? 0];
27
+
28
+ const selectedCommit = (view) => (view.commits ?? [])[view.commitCursor ?? 0];
29
+
30
+ const piecesText = (pieces) => pieces.map((piece) => piece.text).join('');
31
+
32
+ const pathSegments = (rel) => {
33
+ const segs = [];
34
+ if (!rel) return segs;
35
+ const bits = rel.split('/');
36
+ for (let i = 0; i < bits.length; i++) {
37
+ if (i) segs.push({ text: '/', role: 'slash' });
38
+ const role = i === bits.length - 1 ? 'file' : 'dir';
39
+ if (bits[i]) segs.push({ text: bits[i], role });
40
+ }
41
+ return segs;
42
+ };
43
+
44
+ const agentsScreen = (view) => {
45
+ const live = view.agentView;
46
+ return live ? 'agents log' : 'agents';
47
+ };
48
+
49
+ const commitsScreen = (view) => {
50
+ const full = view.commitView === 'full';
51
+ return full ? 'commits full' : 'commits brief';
52
+ };
53
+
54
+ const filesScreen = (view) => {
55
+ const tasks = isTasksEntry(selectedFile(view));
56
+ return tasks ? REPO_TASKS_LABEL : '';
57
+ };
58
+
59
+ const paneScreen = (view) => view.pane;
60
+
61
+ const HEADER_SCREEN = {
62
+ agents: agentsScreen,
63
+ dashboard: paneScreen,
64
+ repos: paneScreen,
65
+ branches: paneScreen,
66
+ npm: paneScreen,
67
+ packages: paneScreen,
68
+ tasks: paneScreen,
69
+ commits: commitsScreen,
70
+ files: filesScreen,
71
+ };
72
+
73
+ const headerScreen = (view) => {
74
+ const pick = HEADER_SCREEN[view.pane];
75
+ if (pick) return pick(view);
76
+ const tasks = isTaskView(view);
77
+ return tasks ? REPO_TASKS_LABEL : '';
78
+ };
79
+
80
+ const headerTarget = (view) => {
81
+ if (view.pane === 'files') {
82
+ const entry = selectedFile(view);
83
+ if (!entry || isTasksEntry(entry)) return '';
84
+ return entry.path;
85
+ }
86
+ if (view.pane === 'unit') return view.reviewPath ?? '';
87
+ if (isTaskView(view)) return '';
88
+ return itemPath(view.item);
89
+ };
90
+
91
+ const headerLead = (view) => {
92
+ const screen = headerScreen(view);
93
+ const label = screen || headerTarget(view);
94
+ const repo = view.repoName || '';
95
+ const pieces = [{ text: HEADER_LEAD, role: 'chrome' }];
96
+ if (repo) {
97
+ pieces.push({ text: repo, role: 'repo' });
98
+ if (!label) pieces.push({ text: ' ', role: 'chrome' });
99
+ else if (screen) pieces.push({ text: ': ', role: 'chrome' });
100
+ else pieces.push({ text: '/', role: 'slash' });
101
+ }
102
+ return { pieces, label };
103
+ };
104
+
105
+ const headerText = (view) => {
106
+ const { pieces, label } = headerLead(view);
107
+ return `${piecesText(pieces)}${label}`;
108
+ };
109
+
110
+ const headerFind = (view, label, clipped) => {
111
+ const query = view.find && view.find.query ? view.find.query : '';
112
+ const target = headerTarget(view);
113
+ if (!query || label !== target || !label) return [];
114
+ const hit = activeHit(view.files ?? [], view.find);
115
+ return spansFor(clipped, label, query, hit);
116
+ };
117
+
118
+ const paintHeaderRoles = (pieces) => {
119
+ let out = `${seq(THEME.headerChromeFg, THEME.headerBg)}${EL}`;
120
+ for (const piece of pieces) {
121
+ out += `${fg(HEADER_ROLE_FG[piece.role])}${piece.text}`;
122
+ }
123
+ return out;
124
+ };
125
+
126
+ const paintHeaderHits = (clipped, spans, color) => {
127
+ let out = '';
128
+ let offset = 0;
129
+ for (const seg of pathSegments(clipped)) {
130
+ const local = [];
131
+ const end = offset + seg.text.length;
132
+ for (const span of spans) {
133
+ const start = Math.max(span.start, offset);
134
+ const stop = Math.min(span.end, end);
135
+ if (start >= stop) continue;
136
+ local.push({
137
+ start: start - offset,
138
+ end: stop - offset,
139
+ on: span.on === true,
140
+ });
141
+ }
142
+ const roleFg = HEADER_ROLE_FG[seg.role];
143
+ out += paintPathText(seg.text, roleFg, THEME.headerBg, color, false, local);
144
+ offset = end;
145
+ }
146
+ return out;
147
+ };
148
+
149
+ const paintHeader = (view, width, color) => {
150
+ const { pieces, label } = headerLead(view);
151
+ const right = ' ';
152
+ const used = visibleWidth(piecesText(pieces)) + visibleWidth(right);
153
+ const room = Math.max(0, width - used);
154
+ const clipped = truncateVisible(label, room);
155
+ const pad = ' '.repeat(Math.max(0, room - visibleWidth(clipped)));
156
+ const spans = headerFind(view, label, clipped);
157
+ if (!color || !spans.length) {
158
+ pieces.push(...pathSegments(clipped));
159
+ if (pad) pieces.push({ text: pad, role: 'chrome' });
160
+ pieces.push({ text: right, role: 'chrome' });
161
+ if (!color) return piecesText(pieces);
162
+ return `${paintHeaderRoles(pieces)}${RESET}`;
163
+ }
164
+ let out = paintHeaderRoles(pieces);
165
+ out += paintHeaderHits(clipped, spans, color);
166
+ out += `${fg(HEADER_ROLE_FG.chrome)}${pad}${right}`;
167
+ return `${out}${RESET}`;
168
+ };
169
+
170
+ module.exports = {
171
+ selectedFile,
172
+ selectedBranch,
173
+ selectedCommit,
174
+ headerText,
175
+ paintHeader,
176
+ };
@@ -0,0 +1,320 @@
1
+ 'use strict';
2
+
3
+ const { clamp } = require('../common/utilities.js');
4
+ const ansi = require('../term/ansi.js');
5
+ const { TABLE_KEY, TABLE_VAL } = require('../runs/output.js');
6
+ const { wrapPlain } = require('../term/wrap.js');
7
+ const { formatBusyStatus } = require('./status.js');
8
+ const primitives = require('./primitives.js');
9
+ const { scrollMark } = primitives;
10
+ const { THEME, CODE_FG, paint, visibleWidth, truncateVisible } = ansi;
11
+ const { trimVisible, seq, EL, RESET, fg, hyperlink } = ansi;
12
+ const { clipAnsi, stripAnsi, foregroundOn } = ansi;
13
+ const { fill } = primitives;
14
+ const { paneResult } = primitives;
15
+
16
+ const LOG_PAD_Y = 1;
17
+
18
+ const LOG_PAD_X = 2;
19
+
20
+ const LOG_URL = /https?:\/\/[^\s<>"']+/g;
21
+
22
+ const TABLE_INK = {
23
+ actual: THEME.errorFg,
24
+ expected: THEME.addLineFg,
25
+ code: THEME.shaFg,
26
+ type: CODE_FG.className,
27
+ count: THEME.warnFg,
28
+ omitted: THEME.warnFg,
29
+ };
30
+
31
+ const COUNT_INK = {
32
+ failed: THEME.errorFg,
33
+ errors: THEME.errorFg,
34
+ passed: THEME.addLineFg,
35
+ };
36
+
37
+ const logViewRows = (bodyH) => {
38
+ const height = Math.max(1, bodyH);
39
+ if (height >= LOG_PAD_Y * 2 + 1) return height - LOG_PAD_Y * 2;
40
+ return height;
41
+ };
42
+
43
+ const logStart = (view, count, rows) => {
44
+ const maxStart = Math.max(0, count - rows);
45
+ const start = view.npmFollow ? maxStart : view.npmScroll;
46
+ return clamp(start ?? 0, 0, maxStart);
47
+ };
48
+
49
+ const isTableLine = (line) => `${line ?? ''}`.startsWith(TABLE_KEY);
50
+
51
+ const mixRgb = (from, to, part) =>
52
+ from.map((value, i) => Math.round(value + (to[i] - value) * part));
53
+
54
+ const tableTone = (index) => {
55
+ const lift = index % 2 === 0 ? 0.035 : 0.07;
56
+ const valueBg = mixRgb(THEME.ctxBg, THEME.chromeFg, lift);
57
+ const keyBg = mixRgb(THEME.ctxBg, THEME.chromeFg, lift + 0.025);
58
+ const frame = mixRgb(THEME.ctxBg, THEME.chromeFg, lift * 0.4);
59
+ return { valueBg, keyBg, frame };
60
+ };
61
+
62
+ const tableIndex = (lines, index) => {
63
+ let row = 0;
64
+ while (index - row > 0 && isTableLine(lines[index - row - 1])) row += 1;
65
+ return row;
66
+ };
67
+
68
+ const tableInk = (key, value) => {
69
+ const name = key.trim();
70
+ if (Object.hasOwn(COUNT_INK, name)) {
71
+ return Number(value) > 0 ? COUNT_INK[name] : THEME.mutedFg;
72
+ }
73
+ return Object.hasOwn(TABLE_INK, name) ? TABLE_INK[name] : THEME.chromeFg;
74
+ };
75
+
76
+ const tableParts = (line, width) => {
77
+ const raw = line.slice(TABLE_KEY.length);
78
+ const at = raw.indexOf(TABLE_VAL);
79
+ const key = at < 0 ? raw : raw.slice(0, at);
80
+ const rest = at < 0 ? '' : raw.slice(at + TABLE_VAL.length);
81
+ const gap = rest.startsWith(' ') ? ' ' : '';
82
+ const value = rest.slice(gap.length);
83
+ const edge = Math.min(1, Math.floor(width / 2));
84
+ const inner = Math.max(0, width - edge * 2);
85
+ const keyW = Math.min(visibleWidth(key), inner);
86
+ const gapW = clamp(inner - keyW, 0, visibleWidth(gap));
87
+ const valueW = Math.max(0, inner - keyW - gapW);
88
+ return { key, gap, value, edge, keyW, gapW, valueW };
89
+ };
90
+
91
+ const wrapTableLine = (line, width) => {
92
+ const part = tableParts(line, width);
93
+ if (part.valueW < 1) return [line];
94
+ const trimmed = part.value.trimEnd();
95
+ const lead = trimmed.startsWith(' ') ? ' ' : '';
96
+ const body = trimmed.slice(lead.length);
97
+ const budget = Math.max(1, part.valueW - visibleWidth(lead));
98
+ if (visibleWidth(body) <= budget) return [line];
99
+ const blank = ' '.repeat(visibleWidth(part.key));
100
+ return wrapPlain(body, budget).map((piece, index) => {
101
+ const name = index === 0 ? part.key : blank;
102
+ return `${TABLE_KEY}${name}${TABLE_VAL}${part.gap}${lead}${piece}`;
103
+ });
104
+ };
105
+
106
+ const urlSpans = (text) => {
107
+ const spans = [];
108
+ for (const match of `${text ?? ''}`.matchAll(LOG_URL)) {
109
+ const start = match.index;
110
+ const end = start + match[0].length;
111
+ const href = match[0];
112
+ const span = { start, end, href };
113
+ spans.push(span);
114
+ }
115
+ return spans;
116
+ };
117
+
118
+ const linkSlice = (text, start, end) => {
119
+ let out = '';
120
+ let at = start;
121
+ for (const span of urlSpans(text)) {
122
+ if (span.end <= start || span.start >= end) continue;
123
+ const from = Math.max(start, span.start);
124
+ const to = Math.min(end, span.end);
125
+ out += text.slice(at, from);
126
+ out += hyperlink(span.href, text.slice(from, to));
127
+ at = to;
128
+ }
129
+ return out + text.slice(at, end);
130
+ };
131
+
132
+ const wrapLogText = (line, inner) => {
133
+ const mark = line.startsWith('│ ') ? '│ ' : '';
134
+ const rest = line.slice(mark.length);
135
+ const budget = Math.max(1, inner - visibleWidth(mark));
136
+ const rows = visibleWidth(rest) <= budget ? [rest] : wrapPlain(rest, budget);
137
+ const out = [];
138
+ let start = 0;
139
+ for (const piece of rows) {
140
+ const linked = linkSlice(rest, start, start + piece.length);
141
+ out.push(`${mark}${linked}`);
142
+ start += piece.length;
143
+ }
144
+ return out;
145
+ };
146
+
147
+ const logLines = (text) => {
148
+ const lines = `${text ?? ''}`.split('\n');
149
+ if (lines.at(-1) === '') lines.pop();
150
+ return lines;
151
+ };
152
+
153
+ let expanded = { text: '', width: 0, lines: [] };
154
+
155
+ const expandLogLines = (text, width) => {
156
+ const source = `${text ?? ''}`;
157
+ if (expanded.width === width && expanded.text === source) {
158
+ return expanded.lines;
159
+ }
160
+ const inner = Math.max(1, width - LOG_PAD_X * 2);
161
+ const lines = logLines(source).flatMap((line) => {
162
+ if (isTableLine(line)) return wrapTableLine(line, width);
163
+ return wrapLogText(line, inner);
164
+ });
165
+ expanded = { text: source, width, lines };
166
+ return lines;
167
+ };
168
+
169
+ const logRowCount = (text, width, running) => {
170
+ const lines = width > 0 ? expandLogLines(text, width) : logLines(text);
171
+ return lines.length + (running ? 1 : 0);
172
+ };
173
+
174
+ const logScrollMax = (text, size, frame, running) => {
175
+ const count = logRowCount(text, size?.width || 0, running);
176
+ return Math.max(0, count - logViewRows(frame?.bodyH || 1));
177
+ };
178
+
179
+ const paintPlainTable = (line, width) => {
180
+ const part = tableParts(line, width);
181
+ const name = trimVisible(part.key, part.keyW);
182
+ const namePad = ' '.repeat(Math.max(0, part.keyW - visibleWidth(name)));
183
+ const shown = trimVisible(part.value, part.valueW);
184
+ const pad = ' '.repeat(Math.max(0, part.valueW - visibleWidth(shown)));
185
+ const edge = ' '.repeat(part.edge);
186
+ const gap = part.gap.slice(0, part.gapW);
187
+ return `${edge}${name}${namePad}${gap}${shown}${pad}${edge}`;
188
+ };
189
+
190
+ const paintTableLine = (line, width, index) => {
191
+ const part = tableParts(line, width);
192
+ const shownKey = trimVisible(part.key, part.keyW);
193
+ const shown = trimVisible(part.value, part.valueW);
194
+ const pad = ' '.repeat(Math.max(0, part.valueW - visibleWidth(shown)));
195
+ const tone = tableTone(index);
196
+ const ink = THEME.ctxFg;
197
+ const valueFg = tableInk(part.key, part.value);
198
+ let out = fill(part.edge, ink, tone.frame, true);
199
+ out += paint(shownKey, ink, tone.keyBg, true);
200
+ out += paint(part.gap.slice(0, part.gapW), ink, tone.frame, true);
201
+ out += paint(`${shown}${pad}`, valueFg, tone.valueBg, true);
202
+ return out + fill(part.edge, ink, tone.frame, true);
203
+ };
204
+
205
+ const paintLogSource = (line) => {
206
+ if (line !== stripAnsi(line)) return line;
207
+ if (/^[✖✔△] /.test(line)) {
208
+ const ink = line[0] === '✔' ? THEME.addLineFg : THEME.errorFg;
209
+ const color = line[0] === '△' ? THEME.warnFg : ink;
210
+ return paint(line, color, THEME.ctxBg, true, true);
211
+ }
212
+ if (line.startsWith('│')) {
213
+ return `${fg(THEME.shaFg)}│${fg(THEME.ctxFg)}${line.slice(1)}`;
214
+ }
215
+ if (/^(Stack|Related failures|Stderr)$/.test(line)) {
216
+ return paint(line, THEME.chromeFg, THEME.ctxBg, true, true);
217
+ }
218
+ if (!/^\s+(at|↳)\s/.test(line)) return line;
219
+ const source = line.replace(
220
+ /([^\s()|]+)(:\d+:\d+)(?=[\s)|]|$)/g,
221
+ (match, file, position) => {
222
+ const path = `${fg(THEME.shaFg)}${file}`;
223
+ const point = `${fg(THEME.mutedFg)}${position}`;
224
+ return `${path}${point}${fg(THEME.ctxFg)}`;
225
+ },
226
+ );
227
+ const frame = /^(\s+at\s+)(.*?)(\s+\(.+\))$/.exec(source);
228
+ if (!frame) return source;
229
+ return (
230
+ `${fg(THEME.mutedFg)}${frame[1]}${fg(CODE_FG.function)}${frame[2]}` +
231
+ `${fg(THEME.ctxFg)}${frame[3]}`
232
+ );
233
+ };
234
+
235
+ const paintLogLine = (line, width, color, fgRgb = THEME.ctxFg) => {
236
+ const inner = Math.max(1, width - LOG_PAD_X * 2);
237
+ const lead = ' '.repeat(LOG_PAD_X);
238
+ if (!color) {
239
+ const body = truncateVisible(stripAnsi(line), inner);
240
+ const pad = Math.max(0, width - LOG_PAD_X - visibleWidth(body));
241
+ return `${lead}${body}${' '.repeat(pad)}`;
242
+ }
243
+ const source = paintLogSource(`${line ?? ''}`);
244
+ const body = foregroundOn(clipAnsi(source, inner), THEME.ctxBg);
245
+ const pad = Math.max(0, width - LOG_PAD_X - visibleWidth(body));
246
+ const base = seq(fgRgb, THEME.ctxBg);
247
+ let out = `${base}${EL}${lead}${body}`;
248
+ if (pad) out += `${RESET}${base}${' '.repeat(pad)}`;
249
+ return `${out}${RESET}`;
250
+ };
251
+
252
+ const paintLogRow = (view, lines, index, width, color) => {
253
+ if (index < lines.length) {
254
+ const line = lines[index];
255
+ if (!isTableLine(line)) return paintLogLine(line, width, color);
256
+ if (!color) return paintPlainTable(line, width);
257
+ return paintTableLine(line, width, tableIndex(lines, index));
258
+ }
259
+ if (view.npmRunning === true && index === lines.length) {
260
+ const label = formatBusyStatus('running', view.progressFrame ?? 0);
261
+ return paintLogLine(label, width, color, THEME.chromeFg);
262
+ }
263
+ return fill(width, THEME.ctxFg, THEME.ctxBg, color);
264
+ };
265
+
266
+ const paintScrollCell = (mark, color, rowBg) => {
267
+ let bgRgb = rowBg ?? THEME.buttonBg;
268
+ if (mark === 'thumb') bgRgb = THEME.taskHeadBg;
269
+ return paint(' ', THEME.chromeFg, bgRgb, color);
270
+ };
271
+
272
+ const withLogScroller = (line, mark, width, color, rowBg) => {
273
+ const text = clipAnsi(line, Math.max(0, width - 1));
274
+ return `${text}${paintScrollCell(mark, color, rowBg)}`;
275
+ };
276
+
277
+ const paintNpmLog = (view, width, color, bodyH) => {
278
+ const lines = expandLogLines(view.npmOutput ?? '', width);
279
+ const count = lines.length + (view.npmRunning === true ? 1 : 0);
280
+ const rows = logViewRows(bodyH);
281
+ const padY = bodyH - rows;
282
+ const top = padY ? 1 : 0;
283
+ const start = logStart(view, count, rows);
284
+ const blank = fill(width, THEME.ctxFg, THEME.ctxBg, color);
285
+ const body = [];
286
+ for (let i = 0; i < top; i++) body.push(blank);
287
+ for (let i = 0; i < rows; i++) {
288
+ body.push(paintLogRow(view, lines, start + i, width, color));
289
+ }
290
+ for (let i = 0; i < padY - top; i++) body.push(blank);
291
+ const overflow = view.logScroller === true && color && count > rows;
292
+ if (!overflow) return paneResult({ body });
293
+ const barred = body.map((line, index) => {
294
+ const at = index - top;
295
+ const onText = at >= 0 && at < rows;
296
+ const mark = onText ? scrollMark(at, count, rows, start) : 'track';
297
+ return withLogScroller(line, mark, width, color);
298
+ });
299
+ const scrollBar = {
300
+ col: width,
301
+ row: 0,
302
+ height: barred.length,
303
+ track: top,
304
+ rows,
305
+ count,
306
+ start,
307
+ };
308
+ return paneResult({ body: barred, scrollBar });
309
+ };
310
+
311
+ module.exports = {
312
+ LOG_PAD_X,
313
+ logViewRows,
314
+ mixRgb,
315
+ expandLogLines,
316
+ logRowCount,
317
+ logScrollMax,
318
+ withLogScroller,
319
+ paintNpmLog,
320
+ };
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+
3
+ const { clamp } = require('../common/utilities.js');
4
+ const ansi = require('../term/ansi.js');
5
+ const { listWindowStart, scrollMark } = require('./primitives.js');
6
+ const { THEME, paint, visibleWidth, trimVisible } = ansi;
7
+
8
+ const menuBoxWidth = (options, columnW, room) => {
9
+ let widest = Math.max(columnW, 4);
10
+ for (const label of options) {
11
+ widest = Math.max(widest, visibleWidth(`${label ?? ''}`));
12
+ }
13
+ return clamp(widest + 2, 4, room);
14
+ };
15
+
16
+ const menuItem = (label, boxW, color, selected, mark, disabled) => {
17
+ const barW = mark ? 1 : 0;
18
+ const faceW = Math.max(1, boxW - barW);
19
+ const room = Math.max(0, faceW - 1);
20
+ const shown = trimVisible(`${label ?? ''}`, room);
21
+ const pad = ' '.repeat(Math.max(0, room - visibleWidth(shown)));
22
+ const face = ` ${shown}${pad}`;
23
+ if (!color) return `${face}${' '.repeat(barW)}`;
24
+ const hot = selected && !disabled;
25
+ const fgRgb = disabled ? THEME.mutedFg : THEME.chromeFg;
26
+ const hotFg = hot ? THEME.buttonHotFg : fgRgb;
27
+ const bgRgb = hot ? THEME.checkBg : THEME.noteBg;
28
+ let row = paint(face, hotFg, bgRgb, color, hot);
29
+ if (!mark) return row;
30
+ const barBg = mark === 'thumb' ? THEME.taskHeadBg : THEME.buttonBg;
31
+ row += paint(' ', fgRgb, barBg, color);
32
+ return row;
33
+ };
34
+
35
+ const dropBox = (options, cursor, scroll, room) => {
36
+ const count = options.length;
37
+ const focus = count ? clamp(cursor ?? 0, 0, count - 1) : 0;
38
+ const inner = Math.max(1, Math.min(count || 1, room, 8));
39
+ const start = listWindowStart(focus, scroll, inner, count);
40
+ return { focus, inner, span: inner, start, count };
41
+ };
42
+
43
+ const menuFaces = (options, box, boxW, color, off = new Set()) => {
44
+ const faces = [];
45
+ for (let i = 0; i < box.inner; i++) {
46
+ const index = box.start + i;
47
+ const label = options[index];
48
+ const mark = scrollMark(i, box.count, box.inner, box.start);
49
+ const disabled = label !== undefined && off.has(label);
50
+ const focused = index === box.focus;
51
+ const face =
52
+ label === undefined
53
+ ? menuItem('none', boxW, color, false, mark, false)
54
+ : menuItem(label, boxW, color, focused, mark, disabled);
55
+ faces.push({ index, face });
56
+ }
57
+ return faces;
58
+ };
59
+
60
+ module.exports = { menuBoxWidth, dropBox, menuFaces };
@@ -1,11 +1,10 @@
1
1
  'use strict';
2
2
 
3
- const ansi = require('../ansi.js');
4
- const wrap = require('../wrap.js');
5
- const primitives = require('./primitives.js');
6
- const { wrapMultiline, wrapDoc, cursorInWrap } = wrap;
3
+ const { clamp } = require('../common/utilities.js');
4
+ const ansi = require('../term/ansi.js');
5
+ const { wrapMultiline, wrapDoc, cursorInWrap } = require('../term/wrap.js');
6
+ const { isListPane, paneResult, editSpan } = require('./primitives.js');
7
7
  const { THEME, paint, visibleWidth, truncateVisible, seq, EL, RESET } = ansi;
8
- const { isListPane, paneResult, editSpan } = primitives;
9
8
 
10
9
  const INPUT_MAX = 4;
11
10
  const NOTE_MAX = 4;
@@ -24,12 +23,18 @@ const paintBar = (text, width, fgRgb, bgRgb, color) => {
24
23
  return `${seq(fgRgb, bgRgb)}${EL}${filled}${RESET}`;
25
24
  };
26
25
 
27
- const checkChip = (mark, fgRgb, bgRgb, plain) => {
28
- if (plain) return { fgRgb, bgRgb };
29
- if (CHECK_DONE.includes(mark)) {
30
- return { fgRgb: THEME.checkDoneFg, bgRgb: THEME.checkDoneBg };
26
+ const checkChip = (mark, fg, bg, plain) => {
27
+ let fgRgb = fg;
28
+ let bgRgb = bg;
29
+ const done = CHECK_DONE.includes(mark);
30
+ if (!plain && done) {
31
+ fgRgb = THEME.checkDoneFg;
32
+ bgRgb = THEME.checkDoneBg;
33
+ } else if (!plain) {
34
+ fgRgb = THEME.checkFg;
35
+ bgRgb = THEME.checkBg;
31
36
  }
32
- return { fgRgb: THEME.checkFg, bgRgb: THEME.checkBg };
37
+ return { fgRgb, bgRgb };
33
38
  };
34
39
 
35
40
  const paintCheckBar = (text, width, fgRgb, bgRgb, color, plain = false) => {
@@ -49,7 +54,7 @@ const paintCheckBar = (text, width, fgRgb, bgRgb, color, plain = false) => {
49
54
 
50
55
  const windowRows = (rows, focus, cap) => {
51
56
  if (rows.length <= cap) return { rows, offset: 0 };
52
- const start = Math.min(Math.max(0, focus - cap + 1), rows.length - cap);
57
+ const start = clamp(focus - cap + 1, 0, rows.length - cap);
53
58
  return { rows: rows.slice(start, start + cap), offset: start };
54
59
  };
55
60