162 lines
4.6 KiB
JavaScript
162 lines
4.6 KiB
JavaScript
/** Frame lines, incremental patch helpers, smoke compose for tests (preamble; no import). */
|
|
|
|
function bareTopSafeWrite(ctx, stdout, s) {
|
|
if (!stdout || typeof stdout.write !== 'function') return
|
|
try {
|
|
stdout.write(s)
|
|
} catch {
|
|
try {
|
|
ctx.console?.error?.('baretop: stdout write failed')
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* FNV-1a 32-bit for line dirty detection (item 4).
|
|
* @param {string} s
|
|
*/
|
|
function bareTopLineHash32(s) {
|
|
const str = String(s)
|
|
let h = 0x811c9dc5
|
|
for (let i = 0; i < str.length; i++) {
|
|
h ^= str.charCodeAt(i)
|
|
h = Math.imul(h, 0x01000193)
|
|
}
|
|
return h >>> 0
|
|
}
|
|
|
|
/**
|
|
* @param {string} out
|
|
* @returns {string[]}
|
|
*/
|
|
function bareTopFrameToLines(out) {
|
|
return out.split(/\r\n/)
|
|
}
|
|
|
|
/**
|
|
* Join lines to frame string (no trailing requirement).
|
|
* @param {string[]} lines
|
|
*/
|
|
function bareTopLinesToFrame(lines) {
|
|
return lines.join('\r\n')
|
|
}
|
|
|
|
/**
|
|
* Build incremental ANSI patch (CUP + EL + line per change).
|
|
* @param {string[] | null} prevLines
|
|
* @param {string[]} nextLines
|
|
* @param {(row: number, col: number) => string} cup
|
|
* @param {{ useLineHash?: boolean, lineHashMinLen?: number }} opts
|
|
* @returns {{ patch: string, nch: number }}
|
|
*/
|
|
function bareTopBuildLinePatch(prevLines, nextLines, cup, opts) {
|
|
const o = opts && typeof opts === 'object' ? opts : {}
|
|
const useHash = o.useLineHash === true
|
|
const minLen = Number(o.lineHashMinLen) > 0 ? Number(o.lineHashMinLen) : 200
|
|
let patch = '\x1b[?25l'
|
|
let nch = 0
|
|
for (let i = 0; i < nextLines.length; i++) {
|
|
const prev = prevLines && i < prevLines.length ? prevLines[i] : undefined
|
|
const next = nextLines[i]
|
|
let changed = false
|
|
if (prev === undefined) changed = true
|
|
else if (useHash && next.length >= minLen) {
|
|
changed = bareTopLineHash32(prev) !== bareTopLineHash32(next)
|
|
} else changed = prev !== next
|
|
if (changed) {
|
|
nch++
|
|
const r = Math.max(1, Math.min(i + 1, 9999))
|
|
patch += cup(r, 1) + '\x1b[K' + next + '\r\n'
|
|
}
|
|
}
|
|
patch += '\x1b[?25h'
|
|
return { patch, nch }
|
|
}
|
|
|
|
/**
|
|
* Emit full frame or patch (item 1/10): single write.
|
|
* @param {Record<string, unknown>} ctx
|
|
* @param {import('stream').Writable} stdout
|
|
* @param {string} fullOut
|
|
* @param {{
|
|
* prevLines: string[] | null,
|
|
* rows: number,
|
|
* cols: number,
|
|
* incrementalFrameDiff: boolean,
|
|
* useLineHash: boolean,
|
|
* fullscreenPanel: boolean,
|
|
* cup: (r: number, c: number) => string
|
|
* }} o
|
|
* @returns {{ nextLines: string[], wrotePatch: boolean, patchLen: number }}
|
|
*/
|
|
function bareTopEmitFrame(ctx, stdout, fullOut, o) {
|
|
const nextLines = bareTopFrameToLines(fullOut)
|
|
let wrotePatch = false
|
|
let patchLen = 0
|
|
if (
|
|
o.incrementalFrameDiff &&
|
|
o.prevLines &&
|
|
o.prevLines.length === nextLines.length &&
|
|
nextLines.length > 2 &&
|
|
!o.fullscreenPanel
|
|
) {
|
|
const { patch, nch } = bareTopBuildLinePatch(
|
|
o.prevLines,
|
|
nextLines,
|
|
o.cup,
|
|
{ useLineHash: o.useLineHash, lineHashMinLen: 200 }
|
|
)
|
|
if (nch > 0 && nch < nextLines.length * 0.92) {
|
|
patchLen = patch.length
|
|
bareTopSafeWrite(/** @type {Record<string, unknown>} */ (ctx), stdout, patch)
|
|
wrotePatch = true
|
|
return { nextLines, wrotePatch, patchLen }
|
|
}
|
|
}
|
|
bareTopSafeWrite(/** @type {Record<string, unknown>} */ (ctx), stdout, fullOut)
|
|
return { nextLines, wrotePatch, patchLen }
|
|
}
|
|
|
|
/**
|
|
* Minimal deterministic compose for unit tests (item 96) — no TTY, stable output.
|
|
* Uses bareTopProcessRowsFromTable from preamble (baretop-ui-helpers.js before this file in bundle order).
|
|
* @param {Record<string, unknown> | null} snap
|
|
* @param {number} cols
|
|
*/
|
|
function bareTopComposeSmokeTestFrame(snap, cols) {
|
|
const c = Math.max(40, cols | 0)
|
|
const lines = []
|
|
lines.push('baretop-compose-smoke cols=' + c)
|
|
const pt =
|
|
snap &&
|
|
snap.extra &&
|
|
snap.extra.processTable &&
|
|
typeof snap.extra.processTable === 'object'
|
|
? snap.extra.processTable
|
|
: snap &&
|
|
snap.metricsLive &&
|
|
snap.metricsLive.processTable &&
|
|
typeof snap.metricsLive.processTable === 'object'
|
|
? snap.metricsLive.processTable
|
|
: null
|
|
const rows =
|
|
typeof bareTopProcessRowsFromTable === 'function'
|
|
? bareTopProcessRowsFromTable(pt)
|
|
: []
|
|
lines.push('process_rows=' + rows.length)
|
|
if (rows[0] && typeof rows[0] === 'object') {
|
|
const r = /** @type {Record<string, unknown>} */ (rows[0])
|
|
lines.push('head_pid=' + String(r.pid ?? ''))
|
|
}
|
|
return {
|
|
lines,
|
|
meta: {
|
|
layoutVersion: 'baretop-compose-1',
|
|
footerRow: lines.length + 1,
|
|
procBodyStart: null
|
|
}
|
|
}
|
|
}
|