/** 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 */ } } } /** Hysteresis latch to avoid patch/full oscillation bursts. */ var bareTopPatchHysteresis = { preferFullUntil: 0 } /** * 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} ctx * @param {import('stream').Writable} stdout * @param {string} fullOut * @param {{ * prevLines: string[] | null, * rows: number, * cols: number, * incrementalFrameDiff: boolean, * useLineHash: boolean, * patchThresholdPct?: number, * fullscreenPanel: boolean, * cup: (r: number, c: number) => string * }} o * @returns {{ nextLines: string[], wrotePatch: boolean, patchLen: number, consideredPatch: boolean, fallbackReason: string }} */ function bareTopEmitFrame(ctx, stdout, fullOut, o) { const nextLines = bareTopFrameToLines(fullOut) let wrotePatch = false let patchLen = 0 let consideredPatch = false let fallbackReason = 'full_frame_initial' const pctThreshold = Math.max( 0.5, Math.min(0.99, Number(o.patchThresholdPct) || 0.92) ) const now = Date.now() const inHysteresisFull = bareTopPatchHysteresis.preferFullUntil > now if ( o.incrementalFrameDiff && o.prevLines && o.prevLines.length === nextLines.length && nextLines.length > 2 && !o.fullscreenPanel && !inHysteresisFull ) { consideredPatch = true const { patch, nch } = bareTopBuildLinePatch( o.prevLines, nextLines, o.cup, { // On tiny frames, hash setup is usually slower than direct compare. useLineHash: o.useLineHash && nextLines.length > 8, lineHashMinLen: 200 } ) if (nch > 0 && nch < nextLines.length * pctThreshold) { patchLen = patch.length bareTopSafeWrite(/** @type {Record} */ (ctx), stdout, patch) wrotePatch = true return { nextLines, wrotePatch, patchLen, consideredPatch, fallbackReason: 'patch_ok' } } fallbackReason = nch === 0 ? 'patch_no_changes' : 'patch_change_ratio_high' if (nch >= nextLines.length * 0.96) { bareTopPatchHysteresis.preferFullUntil = now + 1200 } } else if (!o.incrementalFrameDiff) { fallbackReason = 'incremental_disabled' } else if (inHysteresisFull) { fallbackReason = 'patch_hysteresis_full' } else if (!o.prevLines) { fallbackReason = 'no_prev_frame' } else if (o.prevLines.length !== nextLines.length) { fallbackReason = 'line_count_changed' } else if (nextLines.length <= 2) { fallbackReason = 'frame_too_small' } else if (o.fullscreenPanel) { fallbackReason = 'fullscreen_panel' } bareTopSafeWrite(/** @type {Record} */ (ctx), stdout, fullOut) return { nextLines, wrotePatch, patchLen, consideredPatch, fallbackReason } } /** * 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 | 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} */ (rows[0]) lines.push('head_pid=' + String(r.pid ?? '')) } return { lines, meta: { layoutVersion: 'baretop-compose-1', footerRow: lines.length + 1, procBodyStart: null } } }