recomposable 1.1.2 → 1.1.3
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.
- package/dist/index.d.ts +4 -0
- package/dist/index.js +343 -58
- package/dist/index.js.map +1 -1
- package/dist/lib/docker.d.ts +2 -1
- package/dist/lib/docker.js +37 -10
- package/dist/lib/docker.js.map +1 -1
- package/dist/lib/renderer.d.ts +4 -1
- package/dist/lib/renderer.js +231 -129
- package/dist/lib/renderer.js.map +1 -1
- package/dist/lib/state.js +11 -0
- package/dist/lib/state.js.map +1 -1
- package/dist/lib/types.d.ts +15 -1
- package/package.json +1 -1
package/dist/lib/renderer.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CLEAR_EOS = exports.CLEAR_EOL = void 0;
|
|
3
4
|
exports.visLen = visLen;
|
|
4
5
|
exports.padVisible = padVisible;
|
|
5
6
|
exports.padVisibleStart = padVisibleStart;
|
|
@@ -13,6 +14,7 @@ exports.renderLegend = renderLegend;
|
|
|
13
14
|
exports.renderListView = renderListView;
|
|
14
15
|
exports.truncateLine = truncateLine;
|
|
15
16
|
exports.highlightSearchInLine = highlightSearchInLine;
|
|
17
|
+
exports.wrapPlainLine = wrapPlainLine;
|
|
16
18
|
exports.renderLogView = renderLogView;
|
|
17
19
|
exports.renderExecView = renderExecView;
|
|
18
20
|
const state_1 = require("./state");
|
|
@@ -46,7 +48,21 @@ function padVisibleStart(str, width) {
|
|
|
46
48
|
const pad = Math.max(0, width - visLen(str));
|
|
47
49
|
return ' '.repeat(pad) + str;
|
|
48
50
|
}
|
|
51
|
+
exports.CLEAR_EOL = `${ESC}K`;
|
|
52
|
+
exports.CLEAR_EOS = `${ESC}J`;
|
|
49
53
|
const PATTERN_COLORS = [FG_YELLOW, FG_RED, FG_CYAN, FG_WHITE];
|
|
54
|
+
function logLineColor(line, patterns) {
|
|
55
|
+
let color = null;
|
|
56
|
+
for (let pi = 0; pi < patterns.length; pi++) {
|
|
57
|
+
if (line.includes(patterns[pi])) {
|
|
58
|
+
color = PATTERN_COLORS[pi % PATTERN_COLORS.length];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return color;
|
|
62
|
+
}
|
|
63
|
+
// Cached separator line — recomputed only when terminal width changes
|
|
64
|
+
let cachedSepColumns = 0;
|
|
65
|
+
let cachedSepLine = '';
|
|
50
66
|
function patternLabel(pattern) {
|
|
51
67
|
return pattern.replace(/^[\[\(\{<]/, '').replace(/[\]\)\}>]$/, '');
|
|
52
68
|
}
|
|
@@ -76,7 +92,14 @@ function relativeTime(ts) {
|
|
|
76
92
|
return `${DIM}${days}d ago${RESET}`;
|
|
77
93
|
}
|
|
78
94
|
function clearScreen() {
|
|
79
|
-
return `${ESC}
|
|
95
|
+
return `${ESC}H${ESC}?25l`;
|
|
96
|
+
}
|
|
97
|
+
function separatorLine(columns) {
|
|
98
|
+
if (columns !== cachedSepColumns) {
|
|
99
|
+
cachedSepColumns = columns;
|
|
100
|
+
cachedSepLine = ` ${FG_GRAY}${'\u2500'.repeat(Math.max(0, columns - 2))}${RESET}`;
|
|
101
|
+
}
|
|
102
|
+
return cachedSepLine;
|
|
80
103
|
}
|
|
81
104
|
function showCursor() {
|
|
82
105
|
return `${ESC}?25h`;
|
|
@@ -133,7 +156,7 @@ function formatMem(bytes) {
|
|
|
133
156
|
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}G`;
|
|
134
157
|
}
|
|
135
158
|
function renderLegend(opts = {}) {
|
|
136
|
-
const { logPanelActive = false, logsScrollMode = false, noCacheActive = false, watchActive = false, execMode = false, execInline = false } = opts;
|
|
159
|
+
const { logPanelActive = false, logsScrollMode = false, noCacheActive = false, noDepsActive = false, watchActive = false, execMode = false, execInline = false } = opts;
|
|
137
160
|
const item = (text, active) => {
|
|
138
161
|
if (active)
|
|
139
162
|
return `${BG_HIGHLIGHT} ${text} ${RESET}`;
|
|
@@ -159,8 +182,9 @@ function renderLegend(opts = {}) {
|
|
|
159
182
|
].join(' ');
|
|
160
183
|
}
|
|
161
184
|
if (logsScrollMode) {
|
|
185
|
+
const hasSearch = opts.hasLogSearch || false;
|
|
162
186
|
return [
|
|
163
|
-
item('[Esc] back', false),
|
|
187
|
+
item(hasSearch ? '[Esc] clear search' : '[Esc] back', false),
|
|
164
188
|
item('[j/k] scroll', false),
|
|
165
189
|
item('[G] bottom', false),
|
|
166
190
|
item('[gg] top', false),
|
|
@@ -176,6 +200,7 @@ function renderLegend(opts = {}) {
|
|
|
176
200
|
item('Sto[P]', false),
|
|
177
201
|
item('[W]atch', watchActive),
|
|
178
202
|
item('[N]o cache', noCacheActive),
|
|
203
|
+
item('n[O] deps', noDepsActive),
|
|
179
204
|
item('[e]xec', false),
|
|
180
205
|
item('[F]ull logs', false),
|
|
181
206
|
item('[L]og panel', logPanelActive),
|
|
@@ -186,6 +211,7 @@ function renderListView(state) {
|
|
|
186
211
|
const columns = process.stdout.columns ?? 80;
|
|
187
212
|
const rows = process.stdout.rows ?? 24;
|
|
188
213
|
const patterns = state.config.logScanPatterns || [];
|
|
214
|
+
const sep = separatorLine(columns);
|
|
189
215
|
const buf = [];
|
|
190
216
|
for (const line of LOGO) {
|
|
191
217
|
buf.push(line);
|
|
@@ -193,13 +219,13 @@ function renderListView(state) {
|
|
|
193
219
|
const watchActive = state.watching.size > 0;
|
|
194
220
|
const help = state.execActive
|
|
195
221
|
? renderLegend({ execInline: true })
|
|
196
|
-
: renderLegend({ logPanelActive: state.showBottomLogs, noCacheActive: state.noCache, watchActive });
|
|
197
|
-
buf.push(
|
|
222
|
+
: renderLegend({ logPanelActive: state.showBottomLogs, noCacheActive: state.noCache, noDepsActive: state.noDeps, watchActive });
|
|
223
|
+
buf.push(sep);
|
|
198
224
|
buf.push(` ${help}`);
|
|
199
225
|
const headerHeight = buf.length;
|
|
200
226
|
const bottomBuf = [];
|
|
201
227
|
if (state.execActive && state.execService) {
|
|
202
|
-
bottomBuf.push(
|
|
228
|
+
bottomBuf.push(sep);
|
|
203
229
|
const runningIndicator = state.execChild ? `${FG_YELLOW}running${RESET}` : `${FG_GREEN}ready${RESET}`;
|
|
204
230
|
const cwdInfo = state.execCwd ? ` ${DIM}${state.execCwd}${RESET}` : '';
|
|
205
231
|
bottomBuf.push(` ${FG_CYAN}exec ${BOLD}${state.execService}${RESET} ${runningIndicator}${cwdInfo}`);
|
|
@@ -217,7 +243,7 @@ function renderListView(state) {
|
|
|
217
243
|
// Check for cascade progress
|
|
218
244
|
const cascade = state.cascading.get(sk);
|
|
219
245
|
if (cascade) {
|
|
220
|
-
bottomBuf.push(
|
|
246
|
+
bottomBuf.push(sep);
|
|
221
247
|
bottomBuf.push(` ${FG_YELLOW}cascading ${BOLD}${selEntry.service}${RESET}`);
|
|
222
248
|
for (let si = 0; si < cascade.steps.length; si++) {
|
|
223
249
|
const step = cascade.steps[si];
|
|
@@ -240,22 +266,36 @@ function renderListView(state) {
|
|
|
240
266
|
const info = state.bottomLogLines.get(sk);
|
|
241
267
|
if (info) {
|
|
242
268
|
if (!cascade) {
|
|
243
|
-
bottomBuf.push(
|
|
269
|
+
bottomBuf.push(sep);
|
|
244
270
|
}
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
271
|
+
const isFailed = info.action === 'build_failed' || info.action === 'restart_failed' || info.action === 'stop_failed' || info.action === 'start_failed';
|
|
272
|
+
const actionColor = isFailed ? FG_RED
|
|
273
|
+
: info.action === 'rebuilding' || info.action === 'restarting' || info.action === 'stopping' || info.action === 'starting' || info.action === 'cascading' ? FG_YELLOW
|
|
274
|
+
: info.action === 'watching' ? FG_CYAN : FG_GREEN;
|
|
275
|
+
const actionLabel = isFailed ? info.action.replace('_', ' ').toUpperCase() : info.action;
|
|
276
|
+
let headerLine = ` ${actionColor}${actionLabel} ${BOLD}${info.service}${RESET}`;
|
|
248
277
|
const bq = state.bottomSearchQuery || '';
|
|
249
278
|
if (bq && !state.bottomSearchActive) {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
279
|
+
if (state.bottomSearchLoading) {
|
|
280
|
+
headerLine += ` ${FG_YELLOW}searching "${bq}"...${RESET}`;
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
const totalMatches = state.bottomSearchTotalMatches;
|
|
284
|
+
headerLine += totalMatches > 0
|
|
285
|
+
? ` ${DIM}search: "${bq}" (${totalMatches} match${totalMatches !== 1 ? 'es' : ''} in full log)${RESET}`
|
|
286
|
+
: ` ${FG_RED}search: "${bq}" (no matches)${RESET}`;
|
|
287
|
+
}
|
|
254
288
|
}
|
|
255
289
|
bottomBuf.push(headerLine);
|
|
290
|
+
if (info.lines.length === 0 && info.action === 'logs') {
|
|
291
|
+
bottomBuf.push(` ${DIM}loading...${RESET}`);
|
|
292
|
+
}
|
|
256
293
|
const searchQuery = bq && !state.bottomSearchActive ? bq : '';
|
|
257
|
-
|
|
294
|
+
const maxBottomLines = state.config.bottomLogCount || 10;
|
|
295
|
+
const visibleLines = info.lines.slice(-maxBottomLines);
|
|
296
|
+
for (const line of visibleLines) {
|
|
258
297
|
let coloredLine = line.substring(0, columns - 4);
|
|
298
|
+
const lineColor = logLineColor(coloredLine, patterns) || FG_GRAY;
|
|
259
299
|
if (searchQuery) {
|
|
260
300
|
const lowerLine = coloredLine.toLowerCase();
|
|
261
301
|
const lowerQ = searchQuery.toLowerCase();
|
|
@@ -269,20 +309,13 @@ function renderListView(state) {
|
|
|
269
309
|
break;
|
|
270
310
|
}
|
|
271
311
|
result += coloredLine.substring(pos, idx);
|
|
272
|
-
result += `${REVERSE}${FG_YELLOW}${coloredLine.substring(idx, idx + searchQuery.length)}${RESET}${
|
|
312
|
+
result += `${REVERSE}${FG_YELLOW}${coloredLine.substring(idx, idx + searchQuery.length)}${RESET}${lineColor}`;
|
|
273
313
|
pos = idx + searchQuery.length;
|
|
274
314
|
}
|
|
275
315
|
coloredLine = result;
|
|
276
316
|
}
|
|
277
317
|
}
|
|
278
|
-
|
|
279
|
-
const p = patterns[pi];
|
|
280
|
-
if (coloredLine.includes(p)) {
|
|
281
|
-
const color = PATTERN_COLORS[pi % PATTERN_COLORS.length];
|
|
282
|
-
coloredLine = coloredLine.split(p).join(`${color}${p}${RESET}${FG_GRAY}`);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
bottomBuf.push(` ${FG_GRAY}${coloredLine}${RESET}`);
|
|
318
|
+
bottomBuf.push(` ${lineColor}${coloredLine}${RESET}`);
|
|
286
319
|
}
|
|
287
320
|
if (state.bottomSearchActive) {
|
|
288
321
|
bottomBuf.push(`${BOLD}/${RESET}${state.bottomSearchQuery}${BOLD}_${RESET}`);
|
|
@@ -291,100 +324,113 @@ function renderListView(state) {
|
|
|
291
324
|
}
|
|
292
325
|
}
|
|
293
326
|
const bottomHeight = bottomBuf.length;
|
|
294
|
-
|
|
327
|
+
// Pass 1: build lightweight stubs (type + index only, no text computation)
|
|
328
|
+
const stubs = [];
|
|
295
329
|
let currentGroup = -1;
|
|
296
330
|
for (let i = 0; i < state.flatList.length; i++) {
|
|
297
331
|
const entry = state.flatList[i];
|
|
298
332
|
if (entry.groupIdx !== currentGroup) {
|
|
299
333
|
currentGroup = entry.groupIdx;
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
if (group.error) {
|
|
305
|
-
lines.push({ type: 'header', text: `${label} ${FG_RED}(${group.error})${RESET}` });
|
|
306
|
-
}
|
|
307
|
-
else {
|
|
308
|
-
lines.push({ type: 'header', text: label });
|
|
309
|
-
}
|
|
310
|
-
let colHeader = `${DIM} ${'SERVICE'.padEnd(24)} ${'STATUS'.padEnd(22)} ${'BUILT'.padEnd(12)} ${'RESTARTED'.padEnd(12)}`;
|
|
311
|
-
for (const p of patterns)
|
|
312
|
-
colHeader += patternLabel(p).padStart(5) + ' ';
|
|
313
|
-
colHeader += ` ${'CPU/MEM'.padStart(16)} ${'PORTS'.padEnd(14)}`;
|
|
314
|
-
lines.push({ type: 'colheader', text: colHeader + RESET });
|
|
315
|
-
}
|
|
316
|
-
const sk = (0, state_1.statusKey)(entry.file, entry.service);
|
|
317
|
-
const st = state.statuses.get(sk);
|
|
318
|
-
const rebuilding = state.rebuilding.has(sk);
|
|
319
|
-
const restarting = state.restarting.has(sk);
|
|
320
|
-
const stopping = state.stopping.has(sk);
|
|
321
|
-
const starting = state.starting.has(sk);
|
|
322
|
-
const isWatching = state.watching.has(sk);
|
|
323
|
-
const isCascading = state.cascading.has(sk);
|
|
324
|
-
const icon = statusIcon(st, rebuilding || isCascading, restarting, stopping, starting);
|
|
325
|
-
const stext = statusText(st, rebuilding || isCascading, restarting, stopping, starting);
|
|
326
|
-
const watchIndicator = isWatching ? `${FG_CYAN}W${RESET}` : ' ';
|
|
327
|
-
const name = entry.service.padEnd(24);
|
|
328
|
-
const statusPadded = padVisible(stext, 22);
|
|
329
|
-
let cpuMemStr;
|
|
330
|
-
const stats = state.containerStats ? state.containerStats.get(sk) : null;
|
|
331
|
-
if (stats && st && st.state === 'running') {
|
|
332
|
-
const cpu = stats.cpuPercent;
|
|
333
|
-
const mem = stats.memUsageBytes;
|
|
334
|
-
const cpuWarn = state.config.cpuWarnThreshold || 50;
|
|
335
|
-
const cpuDanger = state.config.cpuDangerThreshold || 100;
|
|
336
|
-
const memWarn = (state.config.memWarnThreshold || 512) * 1024 * 1024;
|
|
337
|
-
const memDanger = (state.config.memDangerThreshold || 1024) * 1024 * 1024;
|
|
338
|
-
let color = DIM;
|
|
339
|
-
if (cpu > cpuDanger || mem > memDanger)
|
|
340
|
-
color = FG_RED;
|
|
341
|
-
else if (cpu > cpuWarn || mem > memWarn)
|
|
342
|
-
color = FG_YELLOW;
|
|
343
|
-
const cpuText = cpu.toFixed(1) + '%';
|
|
344
|
-
const memText = formatMem(mem);
|
|
345
|
-
cpuMemStr = padVisible(`${color}${cpuText} / ${memText}${RESET}`, 16);
|
|
346
|
-
}
|
|
347
|
-
else {
|
|
348
|
-
cpuMemStr = padVisible(`${DIM}-${RESET}`, 16);
|
|
349
|
-
}
|
|
350
|
-
let portsStr;
|
|
351
|
-
if (st && st.ports && st.ports.length > 0) {
|
|
352
|
-
const portsText = st.ports.map(p => p.published).join(' ');
|
|
353
|
-
portsStr = padVisible(`${DIM}${portsText}${RESET}`, 14);
|
|
354
|
-
}
|
|
355
|
-
else {
|
|
356
|
-
portsStr = padVisible(`${DIM}-${RESET}`, 14);
|
|
357
|
-
}
|
|
358
|
-
const built = padVisible(relativeTime(st ? st.createdAt : null), 12);
|
|
359
|
-
const restarted = padVisible(relativeTime(st ? st.startedAt : null), 12);
|
|
360
|
-
const pointer = i === state.cursor ? `${REVERSE}` : '';
|
|
361
|
-
const endPointer = i === state.cursor ? `${RESET}` : '';
|
|
362
|
-
let countsStr = '';
|
|
363
|
-
const logCounts = state.logCounts.get(sk);
|
|
364
|
-
for (let pi = 0; pi < patterns.length; pi++) {
|
|
365
|
-
const count = logCounts ? (logCounts.get(patterns[pi]) || 0) : 0;
|
|
366
|
-
const color = count > 0 ? PATTERN_COLORS[pi % PATTERN_COLORS.length] : DIM;
|
|
367
|
-
const countText = count > 0 ? `${color}${count}${RESET}` : `${color}-${RESET}`;
|
|
368
|
-
countsStr += padVisibleStart(countText, 5) + ' ';
|
|
334
|
+
if (stubs.length > 0)
|
|
335
|
+
stubs.push({ type: 'blank', flatIdx: -1, groupIdx: entry.groupIdx });
|
|
336
|
+
stubs.push({ type: 'header', flatIdx: -1, groupIdx: entry.groupIdx });
|
|
337
|
+
stubs.push({ type: 'colheader', flatIdx: -1, groupIdx: entry.groupIdx });
|
|
369
338
|
}
|
|
370
|
-
|
|
371
|
-
type: 'service',
|
|
372
|
-
text: `${pointer} ${watchIndicator}${icon} ${FG_WHITE}${name}${RESET} ${statusPadded} ${built} ${restarted}${countsStr} ${cpuMemStr} ${portsStr}${endPointer}`,
|
|
373
|
-
flatIdx: i,
|
|
374
|
-
});
|
|
339
|
+
stubs.push({ type: 'service', flatIdx: i, groupIdx: entry.groupIdx });
|
|
375
340
|
}
|
|
376
341
|
const availableRows = Math.max(3, rows - headerHeight - bottomHeight);
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
342
|
+
// Find cursor position in stubs
|
|
343
|
+
const cursorStubIdx = stubs.findIndex(s => s.type === 'service' && s.flatIdx === state.cursor);
|
|
344
|
+
if (cursorStubIdx < state.scrollOffset) {
|
|
345
|
+
state.scrollOffset = cursorStubIdx;
|
|
380
346
|
}
|
|
381
|
-
else if (
|
|
382
|
-
state.scrollOffset =
|
|
347
|
+
else if (cursorStubIdx >= state.scrollOffset + availableRows) {
|
|
348
|
+
state.scrollOffset = cursorStubIdx - availableRows + 1;
|
|
383
349
|
}
|
|
384
|
-
state.scrollOffset = Math.max(0, Math.min(
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
350
|
+
state.scrollOffset = Math.max(0, Math.min(stubs.length - availableRows, state.scrollOffset));
|
|
351
|
+
// Pass 2: render text only for visible stubs
|
|
352
|
+
const visEnd = Math.min(stubs.length, state.scrollOffset + availableRows);
|
|
353
|
+
for (let si = state.scrollOffset; si < visEnd; si++) {
|
|
354
|
+
const stub = stubs[si];
|
|
355
|
+
switch (stub.type) {
|
|
356
|
+
case 'blank':
|
|
357
|
+
buf.push('');
|
|
358
|
+
break;
|
|
359
|
+
case 'header': {
|
|
360
|
+
const group = state.groups[stub.groupIdx];
|
|
361
|
+
const label = ` ${BOLD}${group.label}${RESET}`;
|
|
362
|
+
buf.push(group.error ? `${label} ${FG_RED}(${group.error})${RESET}` : label);
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
case 'colheader': {
|
|
366
|
+
let colHeader = `${DIM} ${'SERVICE'.padEnd(24)} ${'STATUS'.padEnd(22)} ${'BUILT'.padEnd(12)} ${'RESTARTED'.padEnd(12)}`;
|
|
367
|
+
for (const p of patterns)
|
|
368
|
+
colHeader += patternLabel(p).padStart(5) + ' ';
|
|
369
|
+
colHeader += ` ${'CPU/MEM'.padStart(16)} ${'PORTS'.padEnd(14)}`;
|
|
370
|
+
buf.push(colHeader + RESET);
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
case 'service': {
|
|
374
|
+
const i = stub.flatIdx;
|
|
375
|
+
const entry = state.flatList[i];
|
|
376
|
+
const sk = (0, state_1.statusKey)(entry.file, entry.service);
|
|
377
|
+
const st = state.statuses.get(sk);
|
|
378
|
+
const rebuilding = state.rebuilding.has(sk);
|
|
379
|
+
const restarting = state.restarting.has(sk);
|
|
380
|
+
const stopping = state.stopping.has(sk);
|
|
381
|
+
const starting = state.starting.has(sk);
|
|
382
|
+
const isWatching = state.watching.has(sk);
|
|
383
|
+
const isCascading = state.cascading.has(sk);
|
|
384
|
+
const icon = statusIcon(st, rebuilding || isCascading, restarting, stopping, starting);
|
|
385
|
+
const stext = statusText(st, rebuilding || isCascading, restarting, stopping, starting);
|
|
386
|
+
const watchIndicator = isWatching ? `${FG_CYAN}W${RESET}` : ' ';
|
|
387
|
+
const name = entry.service.padEnd(24);
|
|
388
|
+
const statusPadded = padVisible(stext, 22);
|
|
389
|
+
let cpuMemStr;
|
|
390
|
+
const stats = state.containerStats ? state.containerStats.get(sk) : null;
|
|
391
|
+
if (stats && st && st.state === 'running') {
|
|
392
|
+
const cpu = stats.cpuPercent;
|
|
393
|
+
const mem = stats.memUsageBytes;
|
|
394
|
+
const cpuWarn = state.config.cpuWarnThreshold || 50;
|
|
395
|
+
const cpuDanger = state.config.cpuDangerThreshold || 100;
|
|
396
|
+
const memWarn = (state.config.memWarnThreshold || 512) * 1024 * 1024;
|
|
397
|
+
const memDanger = (state.config.memDangerThreshold || 1024) * 1024 * 1024;
|
|
398
|
+
let color = DIM;
|
|
399
|
+
if (cpu > cpuDanger || mem > memDanger)
|
|
400
|
+
color = FG_RED;
|
|
401
|
+
else if (cpu > cpuWarn || mem > memWarn)
|
|
402
|
+
color = FG_YELLOW;
|
|
403
|
+
const cpuText = cpu.toFixed(1) + '%';
|
|
404
|
+
const memText = formatMem(mem);
|
|
405
|
+
cpuMemStr = padVisible(`${color}${cpuText} / ${memText}${RESET}`, 16);
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
cpuMemStr = padVisible(`${DIM}-${RESET}`, 16);
|
|
409
|
+
}
|
|
410
|
+
let portsStr;
|
|
411
|
+
if (st && st.ports && st.ports.length > 0) {
|
|
412
|
+
const portsText = st.ports.map(p => p.published).join(' ');
|
|
413
|
+
portsStr = padVisible(`${DIM}${portsText}${RESET}`, 14);
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
portsStr = padVisible(`${DIM}-${RESET}`, 14);
|
|
417
|
+
}
|
|
418
|
+
const built = padVisible(relativeTime(st ? st.createdAt : null), 12);
|
|
419
|
+
const restarted = padVisible(relativeTime(st ? st.startedAt : null), 12);
|
|
420
|
+
const pointer = i === state.cursor ? `${REVERSE}` : '';
|
|
421
|
+
const endPointer = i === state.cursor ? `${RESET}` : '';
|
|
422
|
+
let countsStr = '';
|
|
423
|
+
const logCounts = state.logCounts.get(sk);
|
|
424
|
+
for (let pi = 0; pi < patterns.length; pi++) {
|
|
425
|
+
const count = logCounts ? (logCounts.get(patterns[pi]) || 0) : 0;
|
|
426
|
+
const color = count > 0 ? PATTERN_COLORS[pi % PATTERN_COLORS.length] : DIM;
|
|
427
|
+
const countText = count > 0 ? `${color}${count}${RESET}` : `${color}-${RESET}`;
|
|
428
|
+
countsStr += padVisibleStart(countText, 5) + ' ';
|
|
429
|
+
}
|
|
430
|
+
buf.push(`${pointer} ${watchIndicator}${icon} ${FG_WHITE}${name}${RESET} ${statusPadded} ${built} ${restarted}${countsStr} ${cpuMemStr} ${portsStr}${endPointer}`);
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
388
434
|
}
|
|
389
435
|
const usedLines = buf.length + bottomHeight;
|
|
390
436
|
const paddingNeeded = Math.max(0, rows - usedLines);
|
|
@@ -392,7 +438,7 @@ function renderListView(state) {
|
|
|
392
438
|
buf.push('');
|
|
393
439
|
}
|
|
394
440
|
buf.push(...bottomBuf);
|
|
395
|
-
return buf.join('\n');
|
|
441
|
+
return buf.join(exports.CLEAR_EOL + '\n');
|
|
396
442
|
}
|
|
397
443
|
function truncateLine(str, maxWidth) {
|
|
398
444
|
let visPos = 0;
|
|
@@ -418,11 +464,12 @@ function truncateLine(str, maxWidth) {
|
|
|
418
464
|
}
|
|
419
465
|
return str;
|
|
420
466
|
}
|
|
421
|
-
function highlightSearchInLine(line, query) {
|
|
467
|
+
function highlightSearchInLine(line, query, baseColor) {
|
|
422
468
|
if (!query)
|
|
423
469
|
return line;
|
|
424
470
|
const lowerLine = line.toLowerCase();
|
|
425
471
|
const lowerQuery = query.toLowerCase();
|
|
472
|
+
const restore = baseColor || '';
|
|
426
473
|
let result = '';
|
|
427
474
|
let pos = 0;
|
|
428
475
|
while (pos < line.length) {
|
|
@@ -432,11 +479,20 @@ function highlightSearchInLine(line, query) {
|
|
|
432
479
|
break;
|
|
433
480
|
}
|
|
434
481
|
result += line.substring(pos, idx);
|
|
435
|
-
result += `${REVERSE}${FG_YELLOW}${line.substring(idx, idx + query.length)}${RESET}`;
|
|
482
|
+
result += `${REVERSE}${FG_YELLOW}${line.substring(idx, idx + query.length)}${RESET}${restore}`;
|
|
436
483
|
pos = idx + query.length;
|
|
437
484
|
}
|
|
438
485
|
return result;
|
|
439
486
|
}
|
|
487
|
+
function wrapPlainLine(line, width) {
|
|
488
|
+
if (width <= 0 || line.length <= width)
|
|
489
|
+
return [line];
|
|
490
|
+
const result = [];
|
|
491
|
+
for (let i = 0; i < line.length; i += width) {
|
|
492
|
+
result.push(line.substring(i, i + width));
|
|
493
|
+
}
|
|
494
|
+
return result;
|
|
495
|
+
}
|
|
440
496
|
function renderLogView(state) {
|
|
441
497
|
const columns = process.stdout.columns ?? 80;
|
|
442
498
|
const rows = process.stdout.rows ?? 24;
|
|
@@ -444,17 +500,43 @@ function renderLogView(state) {
|
|
|
444
500
|
for (const line of LOGO) {
|
|
445
501
|
buf.push(line);
|
|
446
502
|
}
|
|
447
|
-
buf.push(
|
|
448
|
-
|
|
503
|
+
buf.push(separatorLine(columns));
|
|
504
|
+
const hasLogSearch = !!state.logSearchQuery && !state.logSearchActive;
|
|
505
|
+
buf.push(` ${renderLegend({ logsScrollMode: true, hasLogSearch })}`);
|
|
449
506
|
const entry = state.flatList[state.cursor];
|
|
450
507
|
const serviceName = entry ? entry.service : '???';
|
|
451
508
|
const totalLines = state.logLines.length;
|
|
452
|
-
let statusLine
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
509
|
+
let statusLine;
|
|
510
|
+
if (state.logBuildKey) {
|
|
511
|
+
const buildInfo = state.bottomLogLines.get(state.logBuildKey);
|
|
512
|
+
const isBuilding = state.rebuilding.has(state.logBuildKey) || state.cascading.has(state.logBuildKey);
|
|
513
|
+
if (buildInfo && buildInfo.action === 'build_failed') {
|
|
514
|
+
statusLine = ` ${FG_RED}build failed ${BOLD}${serviceName}${RESET}`;
|
|
515
|
+
}
|
|
516
|
+
else if (isBuilding) {
|
|
517
|
+
statusLine = ` ${FG_YELLOW}rebuilding ${BOLD}${serviceName}${RESET}`;
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
statusLine = ` ${FG_GREEN}build logs ${BOLD}${serviceName}${RESET}`;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
statusLine = ` ${FG_GREEN}full logs ${BOLD}${serviceName}${RESET}`;
|
|
525
|
+
}
|
|
526
|
+
let scrollStatus;
|
|
527
|
+
if (state.logAutoScroll) {
|
|
528
|
+
scrollStatus = `${FG_GREEN}live${RESET}`;
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
const currentLine = Math.max(1, totalLines - state.logScrollOffset);
|
|
532
|
+
const pct = totalLines > 0 ? Math.round((currentLine / totalLines) * 100) : 100;
|
|
533
|
+
scrollStatus = `${FG_YELLOW}paused ${DIM}line ${currentLine} / ${totalLines} (${pct}%)${RESET}`;
|
|
534
|
+
}
|
|
456
535
|
statusLine += ` ${scrollStatus}`;
|
|
457
|
-
if (state.
|
|
536
|
+
if (state.logSearchPending || state.logHistoryLoading) {
|
|
537
|
+
statusLine += ` ${FG_YELLOW}loading history...${RESET}`;
|
|
538
|
+
}
|
|
539
|
+
else if (state.logSearchQuery && state.logSearchMatches.length > 0) {
|
|
458
540
|
statusLine += ` ${DIM}match ${state.logSearchMatchIdx + 1}/${state.logSearchMatches.length}${RESET}`;
|
|
459
541
|
}
|
|
460
542
|
else if (state.logSearchQuery && state.logSearchMatches.length === 0) {
|
|
@@ -469,18 +551,38 @@ function renderLogView(state) {
|
|
|
469
551
|
endLine = totalLines;
|
|
470
552
|
}
|
|
471
553
|
else {
|
|
472
|
-
endLine = Math.max(
|
|
554
|
+
endLine = Math.max(Math.min(availableRows, totalLines), totalLines - state.logScrollOffset);
|
|
555
|
+
}
|
|
556
|
+
if (totalLines === 0) {
|
|
557
|
+
buf.push(` ${DIM}loading...${RESET}`);
|
|
473
558
|
}
|
|
474
|
-
const startLine = Math.max(0, endLine - availableRows);
|
|
475
559
|
const searchQuery = state.logSearchQuery || '';
|
|
476
560
|
const matchSet = searchQuery ? new Set(state.logSearchMatches) : null;
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
561
|
+
const patterns = state.config.logScanPatterns || [];
|
|
562
|
+
// Build display lines by wrapping log lines, working backwards from endLine
|
|
563
|
+
const displayLines = [];
|
|
564
|
+
for (let i = endLine - 1; i >= 0 && displayLines.length < availableRows; i--) {
|
|
565
|
+
const line = state.logLines[i];
|
|
566
|
+
const wrapped = wrapPlainLine(line, columns);
|
|
567
|
+
const isMatch = matchSet && matchSet.has(i);
|
|
568
|
+
const lineColor = logLineColor(line, patterns);
|
|
569
|
+
for (let w = wrapped.length - 1; w >= 0; w--) {
|
|
570
|
+
let segment = wrapped[w];
|
|
571
|
+
if (isMatch) {
|
|
572
|
+
segment = highlightSearchInLine(segment, searchQuery, lineColor || undefined);
|
|
573
|
+
}
|
|
574
|
+
if (lineColor) {
|
|
575
|
+
segment = `${lineColor}${segment}${RESET}`;
|
|
576
|
+
}
|
|
577
|
+
displayLines.push(segment);
|
|
481
578
|
}
|
|
482
|
-
buf.push(truncateLine(line, columns));
|
|
483
579
|
}
|
|
580
|
+
displayLines.reverse();
|
|
581
|
+
// Trim to fit available rows (keep the bottom portion)
|
|
582
|
+
const trimmed = displayLines.length > availableRows
|
|
583
|
+
? displayLines.slice(displayLines.length - availableRows)
|
|
584
|
+
: displayLines;
|
|
585
|
+
buf.push(...trimmed);
|
|
484
586
|
const targetRows = rows - bottomReserved;
|
|
485
587
|
for (let i = buf.length; i < targetRows; i++) {
|
|
486
588
|
buf.push('');
|
|
@@ -488,7 +590,7 @@ function renderLogView(state) {
|
|
|
488
590
|
if (state.logSearchActive) {
|
|
489
591
|
buf.push(`${BOLD}/${RESET}${state.logSearchQuery}${BOLD}_${RESET}`);
|
|
490
592
|
}
|
|
491
|
-
return buf.join('\n');
|
|
593
|
+
return buf.join(exports.CLEAR_EOL + '\n');
|
|
492
594
|
}
|
|
493
595
|
function renderExecView(state) {
|
|
494
596
|
const columns = process.stdout.columns ?? 80;
|
|
@@ -497,7 +599,7 @@ function renderExecView(state) {
|
|
|
497
599
|
for (const line of LOGO) {
|
|
498
600
|
buf.push(line);
|
|
499
601
|
}
|
|
500
|
-
buf.push(
|
|
602
|
+
buf.push(separatorLine(columns));
|
|
501
603
|
buf.push(` ${renderLegend({ execMode: true })}`);
|
|
502
604
|
const serviceName = state.execService || '???';
|
|
503
605
|
const runningIndicator = state.execChild ? `${FG_YELLOW}running${RESET}` : `${FG_GREEN}ready${RESET}`;
|
|
@@ -519,6 +621,6 @@ function renderExecView(state) {
|
|
|
519
621
|
}
|
|
520
622
|
// Command prompt
|
|
521
623
|
buf.push(`${FG_GREEN}$ ${RESET}${state.execInput}${BOLD}_${RESET}`);
|
|
522
|
-
return buf.join('\n');
|
|
624
|
+
return buf.join(exports.CLEAR_EOL + '\n');
|
|
523
625
|
}
|
|
524
626
|
//# sourceMappingURL=renderer.js.map
|