/* BARE_OS_BIN_API 1.0.0 — bump when staged /bin script semantics change (see developer guide). */ /** Shared helpers for drive-resident /bin scripts (prepended before each command). */ function bareStdin(ctx) { return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : '' } /** @param {number} mode @param {'file' | 'directory' | 'symlink'} type */ function bareFormatModeString(mode, type) { const typeChar = type === 'directory' ? 'd' : type === 'symlink' ? 'l' : '-' const perm = mode & 0o777 const r = (bit) => (perm & bit ? 'r' : '-') const w = (bit) => (perm & bit ? 'w' : '-') const x = (bit) => (perm & bit ? 'x' : '-') return ( typeChar + r(0o400) + w(0o200) + x(0o100) + r(0o040) + w(0o020) + x(0o010) + r(0o004) + w(0o002) + x(0o001) ) } /** @param {number} mtimeMs @param {number} [nowMs] */ function bareFormatLsMtime(mtimeMs, nowMs) { const now = nowMs != null ? nowMs : Date.now() const d = new Date(mtimeMs) const months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ] const mon = months[d.getMonth()] const day = String(d.getDate()).padStart(2, ' ') const sixMo = 180 * 24 * 3600 * 1000 if (Math.abs(now - mtimeMs) > sixMo) { const yr = String(d.getFullYear()).padStart(4, ' ') return mon + ' ' + day + ' ' + yr } const hh = String(d.getHours()).padStart(2, '0') const mm = String(d.getMinutes()).padStart(2, '0') return mon + ' ' + day + ' ' + hh + ':' + mm } /** @param {number} size */ function barePosixBlocks(size) { return Math.ceil(Number(size) / 512) || 0 } /** * Raw stdout for NUL/binary when **`process.stdout.write`** is missing. * If **`ctx.bareOsBinWrite(Uint8Array|string)`** is set (tests / host), use it. * @param {Record} ctx * @param {string | Uint8Array} chunk * @returns {boolean} */ function bareOsEmitRaw(ctx, chunk) { if (typeof ctx.bareOsBinWrite === 'function') { const b4 = ctx.b4a const u8 = typeof chunk === 'string' ? b4 && typeof b4.from === 'function' ? b4.from(chunk) : new TextEncoder().encode(chunk) : chunk ctx.bareOsBinWrite(u8 instanceof Uint8Array ? u8 : new Uint8Array(u8)) return true } const w = globalThis.process?.stdout?.write if (typeof w === 'function') { w.call(globalThis.process.stdout, chunk) return true } return false } /** ANSI helpers for /bin/edit (preamble; no import in src). */ const EDIT_ANSI_RESET = '\x1b[0m' /** * @param {Record} [ctx] * @returns {import('stream').Writable | undefined} */ function bareEditResolveStdout(ctx) { if (!ctx || typeof ctx !== 'object') return globalThis.process?.stdout const c = /** @type {{ replStdout?: unknown, stdout?: unknown }} */ (ctx) const out = c.replStdout || c.stdout || globalThis.process?.stdout return /** @type {import('stream').Writable | undefined} */ (out) } /** * @param {Record} [ctx] */ function bareEditUseColor(ctx) { const env = ctx && typeof ctx === 'object' && ctx.env && typeof ctx.env === 'object' ? /** @type {Record} */ (ctx.env) : {} if (env.NO_COLOR != null && String(env.NO_COLOR) !== '') return false const out = bareEditResolveStdout(ctx) return Boolean(out && /** @type {{ isTTY?: boolean }} */ (out).isTTY) } /** * @param {'keyword'|'string'|'comment'|'number'|'status'|'inverse'|'dim'} cls * @param {boolean} on */ function bareEditSgr(cls, on) { if (!on) return '' switch (cls) { case 'keyword': return '\x1b[36m' case 'string': return '\x1b[32m' case 'comment': return '\x1b[90m' case 'number': return '\x1b[33m' case 'status': return '\x1b[44m\x1b[97m' case 'inverse': return '\x1b[7m' case 'dim': return '\x1b[2m' default: return '' } } /** * @param {string} fullLine * @param {Array<{ start: number, end: number, cls: string }>} spans offsets into fullLine * @param {number} visStart first column (0-based) * @param {number} maxLen max code units to show * @param {boolean} useColor */ function bareEditPaintLineWindow(fullLine, spans, visStart, maxLen, useColor) { const slice = fullLine.slice(visStart, visStart + maxLen) if (!useColor) return slice const n = slice.length const relSpans = spans .map((s) => ({ start: Math.max(0, s.start - visStart), end: Math.min(n, s.end - visStart), cls: s.cls })) .filter((s) => s.end > 0 && s.start < n) .sort((a, b) => a.start - b.start) let out = '' let pos = 0 for (const sp of relSpans) { if (sp.start > pos) out += slice.slice(pos, sp.start) out += bareEditSgr(/** @type {'keyword'} */ (sp.cls), true) + slice.slice(sp.start, sp.end) + EDIT_ANSI_RESET pos = sp.end } if (pos < n) out += slice.slice(pos) return out } /** * Move cursor (1-based row/col, DEC origin). Clamp to sane bounds for escape parsing. * @param {number} row1 * @param {number} col1 */ function bareEditCup(row1, col1) { const r = Math.max(1, Math.min(Math.floor(row1), 9999)) const c = Math.max(1, Math.min(Math.floor(col1), 9999)) return '\x1b[' + r + ';' + c + 'H' } /** TTY key parsing for /bin/edit: consume one logical key from a mutable byte queue. */ /** * @param {number} b1 */ function bareEditUtf8TrailCount(b1) { if (b1 >= 0xc0 && b1 < 0xe0) return 1 if (b1 >= 0xe0 && b1 < 0xf0) return 2 if (b1 >= 0xf0) return 3 return 0 } /** * @param {number[]} bytes */ function bareEditUtf8DecodeKey(bytes) { try { const u = new Uint8Array(bytes) if (typeof TextDecoder !== 'undefined') { return new TextDecoder('utf-8', { fatal: false }).decode(u) } } catch { /* fall through */ } let s = '' for (const b of bytes) s += String.fromCharCode(b) return s } /** * @param {string} seq CSI payload after ESC [, including final byte (e.g. "A", "1;5A", "3~") */ function bareEditCsiToEvent(seq) { if (seq === '3~') return { type: 'ctrl', code: 'delete' } if (seq === '5~') return { type: 'nav', key: 'pageup' } if (seq === '6~') return { type: 'nav', key: 'pagedown' } const last = seq.charAt(seq.length - 1) if (last === '~') { if (seq === '1~' || seq === '7~') return { type: 'nav', key: 'home' } if (seq === '4~' || seq === '8~') return { type: 'nav', key: 'end' } const tildeNum = /^(\d+)~$/.exec(seq) if (tildeNum) { const n = parseInt(tildeNum[1], 10) const fnMap = { 11: 1, 12: 2, 13: 3, 14: 4, 15: 5, 17: 6, 18: 7, 19: 8, 20: 9, 21: 10, 23: 11, 24: 12 } if (fnMap[n] != null) return { type: 'fn', n: fnMap[n] } } return { type: 'unknown' } } if (last === 'A' || last === 'B' || last === 'C' || last === 'D') { const map = { A: 'up', B: 'down', C: 'right', D: 'left' } return { type: 'nav', key: map[last] } } if (last === 'H') return { type: 'nav', key: 'home' } if (last === 'F') return { type: 'nav', key: 'end' } return { type: 'unknown' } } /** * @param {number} b3 byte after ESC O */ function bareEditSs3ToEvent(b3) { if (b3 === 72) return { type: 'nav', key: 'home' } if (b3 === 70) return { type: 'nav', key: 'end' } if (b3 === 65) return { type: 'nav', key: 'up' } if (b3 === 66) return { type: 'nav', key: 'down' } if (b3 === 67) return { type: 'nav', key: 'right' } if (b3 === 68) return { type: 'nav', key: 'left' } if (b3 === 80) return { type: 'fn', n: 1 } if (b3 === 81) return { type: 'fn', n: 2 } if (b3 === 82) return { type: 'fn', n: 3 } if (b3 === 83) return { type: 'fn', n: 4 } return { type: 'unknown' } } /** * @param {number[]} q mutable queue (front = index 0) * @returns {Record | null} null if more bytes needed */ function bareEditTryConsumeKey(q) { if (!q.length) return null const b1 = q[0] if (b1 === 3) { q.shift() return { type: 'ctrl', code: 'interrupt' } } if (b1 === 8 || b1 === 127) { q.shift() return { type: 'ctrl', code: 'backspace' } } if (b1 === 13 || b1 === 10) { q.shift() return { type: 'key', ch: '\n' } } if (b1 === 9) { q.shift() return { type: 'key', ch: '\t' } } if (b1 === 27) { if (q.length < 2) return null const b2 = q[1] if (b2 === 91) { let i = 2 while (i < q.length) { const b = q[i] if (b >= 0x40 && b <= 0x7e) { const seq = String.fromCharCode.apply(null, q.slice(2, i + 1)) q.splice(0, i + 1) return bareEditCsiToEvent(seq) } i++ } return null } if (b2 === 79) { if (q.length < 3) return null const b3 = q[2] q.splice(0, 3) return bareEditSs3ToEvent(b3) } q.splice(0, 2) return { type: 'key', ch: String.fromCharCode(b2) } } if (b1 < 0x20) { q.shift() return { type: 'ctrl', code: b1 } } const need = bareEditUtf8TrailCount(b1) if (q.length < 1 + need) return null const chunk = q.splice(0, 1 + need) return { type: 'key', ch: bareEditUtf8DecodeKey(chunk) } } /** Fetch /proc and ctx snapshots for /bin/baretop (preamble; no import). */ /** * Keep in sync with `bareOsReadBareTopSnapshot` path list in packages/bare-os-booter/index.js * @type {readonly [string, string][]} */ var BARE_TOP_SNAPSHOT_PROC_ENTRIES = [ ['index', '/proc/bare_os/index.json'], ['version', '/proc/bare_os/version'], ['hostOs', '/proc/bare_os/host_os.json'], ['debug', '/proc/bare_os/debug.json'], ['sessionStats', '/proc/bare_os/session_stats'], ['quotas', '/proc/bare_os/quotas'], ['capabilitiesJson', '/proc/bare_os/capabilities.json'], ['capabilitiesNode', '/proc/bare_os/capabilities'], ['seedHandshake', '/proc/bare_os/seed_handshake'], ['bootstrap', '/proc/bare_os/bootstrap'], ['provenance', '/proc/bare_os/provenance'], ['snapshotHints', '/proc/bare_os/snapshot_hints.json'], ['stagingSlot', '/proc/bare_os/staging_slot'], ['peerHealth', '/proc/bare_os/peer_health'], ['pearIpc', '/proc/bare_os/pear_ipc.json'], ['pearIpcHealth', '/proc/bare_os/pear_ipc_health.json'], ['pearTrust', '/proc/bare_os/pear_trust.json'], ['replication', '/proc/bare_os/replication'], ['replicationBackpressure', '/proc/bare_os/replication_backpressure.json'], ['swarm', '/proc/bare_os/swarm'], ['syncWindow', '/proc/bare_os/sync_window.json'], ['clock', '/proc/bare_os/clock.json'], ['hdmsHealth', '/proc/bare_os/hdms_health.json'], ['hdmsHints', '/proc/bare_os/hdms_hints.json'], ['dhtStatus', '/proc/bare_os/dht_status.json'], ['udxExtended', '/proc/bare_os/udx_extended.json'], ['ipcBackpressure', '/proc/bare_os/ipc_backpressure.json'], ['delegateRed', '/proc/bare_os/delegate_red.json'], ['gitDelegateStats', '/proc/bare_os/git_delegate_stats.json'], ['kernelProgram', '/proc/bare_os/kernel_program.json'], ['gitLfsPointerStats', '/proc/bare_os/git_lfs_pointer_stats.json'], ['workerBudget', '/proc/bare_os/worker_budget.json'], ['sandboxProfile', '/proc/bare_os/sandbox_profile.json'], ['rlimits', '/proc/bare_os/rlimits.json'], ['extensions', '/proc/bare_os/extensions.json'], ['initdDag', '/proc/bare_os/initd_dag.json'], ['bootGraph', '/proc/bare_os/boot_graph.json'], ['processTable', '/proc/bare_os/process_table.json'], ['syscalls', '/proc/bare_os/syscalls.json'], ['metricsProm', '/proc/bare_os/metrics.prom'], ['protomuxWire', '/proc/bare_os/protomux.json'] ] /** @param {unknown} buf @param {unknown} b4a @returns {string} */ function bareTopBufToString(buf, b4a) { if (buf == null) return '' if (typeof buf === 'string') return buf try { if (b4a && typeof b4a.toString === 'function') return b4a.toString(buf) } catch { /* fall through */ } try { return new TextDecoder().decode( buf instanceof Uint8Array ? buf : new Uint8Array(/** @type {ArrayLike} */ (buf)) ) } catch { return '' } } /** @param {string} s @returns {unknown} */ function bareTopJsonParse(s) { const t = String(s || '').trim() if (!t) return null try { return JSON.parse(t) } catch { return null } } /** * @param {Record} ctx * @param {string} path * @returns {Promise} */ async function bareTopReadProc(ctx, path) { const vfs = ctx.vfs if (!vfs || typeof vfs.readFile !== 'function') return '' try { const buf = await vfs.readFile(path) return bareTopBufToString(buf, ctx.b4a) } catch { return '' } } /** * @param {Record} ctx * @param {readonly [string, string][]} entries * @param {number} concurrency * @returns {Promise>} */ async function bareTopReadProcBatch(ctx, entries, concurrency) { /** @type {Record} */ const out = {} const conc = Math.max(1, Math.min(16, concurrency | 0) || 6) for (let i = 0; i < entries.length; i += conc) { const slice = entries.slice(i, i + conc) await Promise.all( slice.map(async ([key, path]) => { out[key] = await bareTopReadProc(ctx, path) }) ) } return out } /** * @param {unknown} o * @param {number} maxChars * @returns {string} */ function bareTopTruncateJsonPretty(o, maxChars) { const raw = JSON.stringify(o, null, 2) if (raw.length <= maxChars) return raw return raw.slice(0, Math.max(0, maxChars - 24)) + '\n… (truncated) …\n' } /** @param {number} n @returns {string} */ function bareTopFormatBytes(n) { const x = Number(n) if (!Number.isFinite(x) || x < 0) return '—' if (x < 1024) return String(Math.floor(x)) + ' B' if (x < 1048576) return (x / 1024).toFixed(1) + ' KiB' if (x < 1073741824) return (x / 1048576).toFixed(1) + ' MiB' return (x / 1073741824).toFixed(2) + ' GiB' } /** @param {number} ms @returns {string} */ function bareTopFormatDuration(ms) { const x = Number(ms) if (!Number.isFinite(x) || x < 0) return '—' if (x < 1000) return String(Math.floor(x)) + ' ms' if (x < 60000) return (x / 1000).toFixed(1) + ' s' if (x < 3600000) return Math.floor(x / 60000) + ' m' return (x / 3600000).toFixed(1) + ' h' } /** * @param {Record | null} m metrics live * @param {number} peers * @returns {number} 0–100 */ function bareTopHealthScore(m, peers) { let score = 72 if (m && typeof m.delegateInflight === 'object' && m.delegateInflight) { const o = /** @type {Record} */ (m.delegateInflight) const n = Object.keys(o).length score -= Math.min(25, n * 3) } if (peers < 0) score -= 5 if (score < 0) score = 0 if (score > 100) score = 100 return score } /** * @param {number} prev * @param {number} value * @param {number} alpha 0–1 * @returns {number} */ function bareTopEma(prev, value, alpha) { const a = Math.min(1, Math.max(0, alpha)) return prev * (1 - a) + value * a } /** * @param {Record} ctx * @param {Record} files * @param {string} key * @returns {Record | null} */ function bareTopParsedFile(files, key) { const t = files[key] if (!t) return null const p = bareTopJsonParse(t) return p && typeof p === 'object' ? /** @type {Record} */ (p) : null } /** * @param {Record} ctx * @returns {Promise>} */ /** One coalesced operator frame; keep VFS read fan-out bounded (batch size + concurrency). */ async function bareTopFetchSnapshot(ctx) { const fetchStart = Date.now() let readErr = null const env = ctx.env && typeof ctx.env === 'object' ? /** @type {Record} */ (ctx.env) : {} const concRaw = parseInt(env.BARE_TOP_FETCH_CONCURRENCY || '6', 10) const concurrency = Number.isFinite(concRaw) ? concRaw : 6 /** @type {Record} */ let fileTexts = {} let fastAtMs = 0 try { if (typeof ctx.bareOsReadBareTopSnapshot === 'function') { const r = await ctx.bareOsReadBareTopSnapshot() if (r && typeof r === 'object') { const o = /** @type {Record} */ (r) const files = o.files if (files && typeof files === 'object') { for (const k of Object.keys(files)) { const v = files[k] if (typeof v === 'string') fileTexts[k] = v } } const am = o.atMs if (typeof am === 'number' && Number.isFinite(am)) fastAtMs = am } } } catch (e) { readErr = (e && /** @type {{ message?: string }} */ (e).message) || String(e) } if (Object.keys(fileTexts).length === 0) { fileTexts = await bareTopReadProcBatch(ctx, BARE_TOP_SNAPSHOT_PROC_ENTRIES, concurrency) } const atMs = fastAtMs || Date.now() /** @type {Record | null} */ let metricsLive = null /** @type {Record | null} */ let resources = null /** @type {Record | null} */ let features = null /** @type {Record | null} */ let netSummary = null /** @type {unknown} */ let initdGraph = null /** @type {Record | null} */ let fairnessSnapshot = null /** @type {unknown} */ let subprocessBridge = null /** @type {Record | null} */ let hostStats = null try { if (typeof ctx.bareOsReadProcMetricsLive === 'function') { const o = ctx.bareOsReadProcMetricsLive() if (o && typeof o === 'object') metricsLive = /** @type {Record} */ (o) } } catch (e) { if (!readErr) readErr = (e && /** @type {{ message?: string }} */ (e).message) || String(e) } if (!metricsLive) { const t = await bareTopReadProc(ctx, '/proc/bare_os/metrics_live.json') const p = bareTopJsonParse(t) if (p && typeof p === 'object') metricsLive = /** @type {Record} */ (p) } try { if (typeof ctx.bareOsGetResourceStatus === 'function') { const o = ctx.bareOsGetResourceStatus() if (o && typeof o === 'object') resources = /** @type {Record} */ (o) } } catch { /* ignore */ } if (!resources) { const t = await bareTopReadProc(ctx, '/proc/bare_os_resources') const p = bareTopJsonParse(t) if (p && typeof p === 'object') resources = /** @type {Record} */ (p) } let fp = bareTopJsonParse(await bareTopReadProc(ctx, '/proc/bare_os/features')) if (!fp) fp = bareTopJsonParse(await bareTopReadProc(ctx, '/proc/bare_os_features')) if (fp && typeof fp === 'object') features = /** @type {Record} */ (fp) const netT = await bareTopReadProc(ctx, '/proc/bare_os/net_summary.json') const np = bareTopJsonParse(netT) if (np && typeof np === 'object') netSummary = /** @type {Record} */ (np) const initT = await bareTopReadProc(ctx, '/proc/bare_os/initd_graph.json') initdGraph = bareTopJsonParse(initT) if (initdGraph === null) { const alt = bareTopJsonParse(fileTexts.initdDag || '') initdGraph = alt } try { if (typeof ctx.bareOsReadDelegateFairnessSnapshot === 'function') { const o = ctx.bareOsReadDelegateFairnessSnapshot() if (o && typeof o === 'object') fairnessSnapshot = /** @type {Record} */ (o) } } catch { /* ignore */ } try { if (typeof ctx.bareOsReadSubprocessBridgeSnapshot === 'function') { subprocessBridge = ctx.bareOsReadSubprocessBridgeSnapshot() } } catch { /* ignore */ } try { const hs = ctx.bareOsHostStats if (hs && typeof hs === 'object') hostStats = /** @type {Record} */ (hs) } catch { /* ignore */ } const memRaw = await bareTopReadProc(ctx, '/proc/meminfo') let meminfoLine = '' for (const line of memRaw.split('\n')) { const L = line.trim() if (L.startsWith('MemTotal:') || L.startsWith('MemAvailable:')) { meminfoLine = L break } } if (!meminfoLine) meminfoLine = memRaw.split('\n')[0]?.trim() || '' const loadavgLine = (await bareTopReadProc(ctx, '/proc/loadavg')).split('\n')[0]?.trim() || '' let cpuLine = '' const cpuRaw = await bareTopReadProc(ctx, '/proc/cpuinfo') for (const line of cpuRaw.split('\n')) { if (line.startsWith('model name') || line.startsWith('Model')) { cpuLine = line.replace(/^[^:]+:\s*/, '').trim().slice(0, 72) break } } const hostOs = bareTopParsedFile(fileTexts, 'hostOs') const metaAt = metricsLive && typeof metricsLive.atMs === 'number' ? metricsLive.atMs : atMs /** @type {Record | null>} */ const extra = {} for (const [key] of BARE_TOP_SNAPSHOT_PROC_ENTRIES) { extra[key] = bareTopParsedFile(fileTexts, key) } const fetchWallMs = Date.now() - fetchStart return { metricsLive, resources, features, initdGraph, netSummary, meminfoLine, loadavgLine, cpuLine, hostOs, atMs, metaAtMs: metaAt, readErr, fileTexts, extra, fairnessSnapshot, subprocessBridge, hostStats, fetchWallMs, healthScore: bareTopHealthScore( metricsLive, Number(metricsLive && metricsLive.peers) || 0 ) } } /** Pure UI helpers for /bin/baretop (preamble; no import). */ /** * @param {string} line * @returns {{ label: string, value: string }[]} */ function bareTopParseMeminfoPairs(line) { const s = String(line || '').trim() const out = [] if (!s) return out const re = /(\w+):\s*(\d+)\s*(\w+)?/g let m while ((m = re.exec(s)) !== null) { out.push({ label: m[1], value: m[2] + (m[3] ? ' ' + m[3] : '') }) } return out } /** * @param {string} line * @returns {{ pairs: { label: string, value: string }[], memTotal?: number, memAvail?: number }} */ function bareTopParseMeminfoMetrics(line) { const pairs = bareTopParseMeminfoPairs(line) let memTotal let memAvail for (const p of pairs) { if (p.label === 'MemTotal') memTotal = parseInt(p.value, 10) if (p.label === 'MemAvailable') memAvail = parseInt(p.value, 10) } return { pairs, memTotal, memAvail } } /** * @param {string} line * @returns {{ one: number, five: number, fifteen: number, running?: number, total?: number } | null} */ function bareTopParseLoadavg(line) { const s = String(line || '').trim() if (!s) return null const parts = s.split(/\s+/) if (parts.length < 3) return null const one = parseFloat(parts[0]) const five = parseFloat(parts[1]) const fifteen = parseFloat(parts[2]) if (!Number.isFinite(one)) return null const slash = parts[3] && parts[3].includes('/') ? parts[3].split('/') : null return { one, five: Number.isFinite(five) ? five : one, fifteen: Number.isFinite(fifteen) ? fifteen : one, running: slash ? parseInt(slash[0], 10) : undefined, total: slash ? parseInt(slash[1], 10) : undefined } } /** * @param {number} pct 0–100 * @param {number} width * @param {boolean} ascii * @param {{ low?: string, mid?: string, hi?: string, reset: string }} pal * @param {boolean} useColor */ function bareTopFormatMeterBar(pct, width, ascii, pal, useColor) { const w = Math.max(2, width | 0) const p = Math.min(100, Math.max(0, Number(pct) || 0)) const fill = Math.round((p / 100) * w) const full = ascii ? '#' : '\u2588' const empty = ascii ? '-' : '\u2591' let s = '' for (let i = 0; i < w; i++) { const on = i < fill if (!useColor) { s += on ? full : empty continue } const c = p >= 95 ? pal.hi || '' : p >= 80 ? pal.mid || '' : on ? pal.low || '' : '' s += c + (on ? full : empty) + (c ? pal.reset : '') } return s } /** * @param {string} text * @param {number} maxLen * @param {boolean} wordBoundary */ function bareTopTruncateCell(text, maxLen, wordBoundary) { const t = String(text ?? '') const m = Math.max(1, maxLen | 0) if (t.length <= m) return t if (!wordBoundary || m < 5) return t.slice(0, m - 1) + '\u2026' let cut = t.lastIndexOf(' ', m - 1) if (cut < m >> 1) cut = m - 1 return t.slice(0, cut).trimEnd() + '\u2026' } /** * @param {unknown} pt process_table.json root * @returns {Array>} */ function bareTopProcessRowsFromTable(pt) { if (!pt || typeof pt !== 'object') return [] const o = /** @type {Record} */ (pt) const arr = o.processes if (!Array.isArray(arr)) return [] return arr.filter((x) => x && typeof x === 'object') } /** * @param {Record} row */ function bareTopProcessPid(row) { const n = Number(row.pid) return Number.isFinite(n) ? n : 0 } /** * @param {Record} row */ function bareTopProcessSortKey(row, key) { if (key === 'name') return String(row.name || row.label || '').toLowerCase() if (key === 'state') return String(row.state || '').toLowerCase() return bareTopProcessPid(row) } /** * @param {Record[]} rows * @param {'pid'|'name'|'state'} sortKey * @param {boolean} asc */ function bareTopSortProcessRows(rows, sortKey, asc) { const dir = asc ? 1 : -1 const out = rows.slice() out.sort((a, b) => { const ka = bareTopProcessSortKey(a, sortKey) const kb = bareTopProcessSortKey(b, sortKey) let c = 0 if (typeof ka === 'number' && typeof kb === 'number') c = ka - kb else c = String(ka).localeCompare(String(kb)) if (c !== 0) return c * dir return bareTopProcessPid(a) - bareTopProcessPid(b) }) return out } /** * @param {string[]} cells * @param {number[]} widths * @param {('l'|'r')[]} align * @param {number} maxLineLen */ function bareTopFormatFixedColumns(cells, widths, align, maxLineLen) { const parts = [] let total = 0 for (let i = 0; i < cells.length && i < widths.length; i++) { const w = Math.max(1, widths[i] | 0) let c = bareTopTruncateCell(cells[i], w, true) if (c.length > w) c = c.slice(0, w) const pad = w - c.length if (align[i] === 'r') { parts.push(' '.repeat(Math.max(0, pad)) + c) } else { parts.push(c + ' '.repeat(Math.max(0, pad))) } total += w + (i < cells.length - 1 ? 1 : 0) } let line = parts.join(' ') if (line.length > maxLineLen) line = line.slice(0, maxLineLen) return line } /** * @param {number} score 0–100 * @param {number} width * @param {boolean} ascii */ function bareTopHealthBarLine(score, width, ascii) { const w = Math.max(4, width | 0) const p = Math.min(100, Math.max(0, Number(score) || 0)) const fill = Math.round((p / 100) * w) const full = ascii ? '#' : '\u2588' const empty = ascii ? '.' : '\u2591' let bar = '' for (let i = 0; i < w; i++) bar += i < fill ? full : empty return bar + ' ' + p + '/100' } /** * @param {string} title * @param {number} cols */ function bareTopSectionRule(title, cols) { const t = String(title || '').trim() const c = Math.max(8, cols | 0) const inner = c - 4 if (inner < 4) return t const pad = inner - t.length if (pad <= 0) return '\u2500 ' + t.slice(0, inner - 2) + ' \u2500' const left = Math.floor(pad / 2) const right = pad - left return ( '\u2500'.repeat(Math.max(1, left)) + ' ' + t + ' ' + '\u2500'.repeat(Math.max(1, right)) ) } /** Full-screen TUI for /bin/baretop — session / kernel dashboard (preamble; no import). */ /** User-visible strings (i18n-ready single object). */ var bareTopStrings = { title: 'baretop', helpKeys: 'Keys', quitHint: '^C also quits', noHtop: 'Logical process table (schema 7) lists synthetic PIDs/jobs — not host OS processes.', noHtop2: ' Session metrics, delegates, /proc/bare_os mirrors, and ctx snapshots fill other tabs.', pressCloseHelp: 'Esc or any other key closes help.', activity: 'Activity (per refresh)', session: 'Session', delegates: 'Delegates', pipeline: 'Pipeline limits', exportOk: 'exported snapshot', exportFail: 'export failed', paused: 'PAUSED', na: 'N/A' } /** * @param {unknown} chunk * @returns {number[]} */ function bareTopChunkBytes(chunk) { if (chunk == null) return [] if (typeof chunk === 'string') { const out = [] for (let i = 0; i < chunk.length; i++) out.push(chunk.charCodeAt(i) & 0xff) return out } const len = /** @type {{ length: number, [k: number]: number }} */ (chunk).length const out = [] for (let i = 0; i < len; i++) out.push(Number(chunk[i]) & 0xff) return out } /** * Strip bracketed paste wrappers from raw bytes before key parse (best-effort). * @param {number[]} q */ function bareTopStripBracketedPaste(q) { const esc = 0x1b let i = 0 while (i < q.length) { if (q[i] !== esc || i + 1 >= q.length || q[i + 1] !== 0x5b) { i++ continue } let j = i + 2 let acc = '' while (j < q.length && q[j] !== 0x7e) { acc += String.fromCharCode(q[j]) j++ } if (j < q.length && acc.startsWith('200')) { j++ let k = j while (k < q.length) { if ( k + 5 < q.length && q[k] === esc && q[k + 1] === 0x5b && q[k + 2] === 0x32 && q[k + 3] === 0x30 && q[k + 4] === 0x31 && q[k + 5] === 0x7e ) { q.splice(i, k + 6 - i) i = Math.max(0, i - 1) break } k++ } if (k >= q.length) break continue } i++ } } /** * @param {Record} ctx * @param {import('stream').Writable} stdout * @param {string} s */ function bareTopWrite(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 */ } } } /** * @param {string} theme * @param {boolean} useColor */ function bareTopTheme(theme, useColor, highContrast) { if (!useColor) return { kw: '', dim: '', warn: '', bad: '', good: '', barHi: '', border: '', header: '', sel: '', zebraBg: '', meterLow: '', meterMid: '', meterHi: '', reset: '' } if (highContrast) { return { kw: '\x1b[1;36m', dim: '\x1b[4m', warn: '\x1b[1;33m', bad: '\x1b[1;31m', good: '\x1b[1;32m', barHi: '\x1b[1;35m', border: '\x1b[1;37m', header: '\x1b[44m\x1b[1;97m', sel: '\x1b[7;1m', zebraBg: '', meterLow: '\x1b[1;32m', meterMid: '\x1b[1;33m', meterHi: '\x1b[1;31m', reset: '\x1b[0m' } } if (theme === 'light') { return { kw: '\x1b[34m', dim: '\x1b[90m', warn: '\x1b[33;1m', bad: '\x1b[31;1m', good: '\x1b[32;1m', barHi: '\x1b[35;1m', border: '\x1b[90m', header: '\x1b[44m\x1b[97m', sel: '\x1b[30;47m', zebraBg: '\x1b[48;5;252m', meterLow: '\x1b[32m', meterMid: '\x1b[33m', meterHi: '\x1b[31m', reset: '\x1b[0m' } } if (theme === 'none') { return { kw: '', dim: '', warn: '', bad: '', good: '', barHi: '', border: '', header: '', sel: '', zebraBg: '', meterLow: '', meterMid: '', meterHi: '', reset: '' } } return { kw: '\x1b[36m', dim: '\x1b[2m', warn: '\x1b[33m', bad: '\x1b[31m', good: '\x1b[32m', barHi: '\x1b[35m', border: '\x1b[90m', header: '\x1b[44m\x1b[97m', sel: '\x1b[7m', zebraBg: '\x1b[48;5;235m', meterLow: '\x1b[32m', meterMid: '\x1b[33m', meterHi: '\x1b[31m', reset: '\x1b[0m' } } /** @param {number[]} q @param {number} max @param {{ vi: boolean, inFilter?: boolean }} o */ function bareTopDrainKeys(q, max, o) { let n = 0 for (;;) { if (n >= max) break const ev = bareEditTryConsumeKey(q) if (!ev) break n++ if (ev.type === 'eof') return { type: 'quit' } if (ev.type === 'ctrl' && ev.code === 'interrupt') return { type: 'quit' } if (ev.type === 'fn') { const fn = /** @type {{ n?: number }} */ (ev).n if (fn === 1) return { type: 'help_open' } if (fn === 5) return { type: 'refresh' } if (fn === 9) return { type: 'signal_menu' } } if (ev.type === 'ctrl' && ev.code === 'backspace') return { type: 'filter_bs' } if (ev.type === 'key' && ev.ch === '\n') return o.inFilter ? { type: 'filter_enter' } : { type: 'enter' } if (ev.type === 'nav') { if (ev.key === 'pageup') return { type: 'scroll', dir: -1, amt: 5 } if (ev.key === 'pagedown') return { type: 'scroll', dir: 1, amt: 5 } if (ev.key === 'home') return { type: 'scroll', dir: 'home' } if (ev.key === 'end') return { type: 'scroll', dir: 'end' } if (ev.key === 'up') return { type: 'scroll', dir: -1, amt: 1 } if (ev.key === 'down') return { type: 'scroll', dir: 1, amt: 1 } if (o.vi) { if (ev.key === 'left') return { type: 'focus', dir: -1 } if (ev.key === 'right') return { type: 'focus', dir: 1 } } } if (ev.type === 'key' && ev.ch) { const ch = ev.ch if (ch === 'q' || ch === 'Q') return { type: 'quit' } if (ch === 'r' || ch === 'R') return { type: 'refresh' } if (ch === 'h' || ch === '?' || ch === 'H') return { type: 'help_open' } if (ch === ' ') return { type: 'pause_toggle' } if (ch === '.') return { type: 'step' } if (ch === 'd' || ch === 'D') return { type: 'delta_toggle' } if (ch === 'e' || ch === 'E') return { type: 'export' } if (ch === 'f' || ch === 'F') return { type: 'fullscreen_toggle' } if (ch === 't' || ch === 'T') return { type: 'clock_toggle' } if (ch === '[') return { type: 'tab_prev' } if (ch === ']') return { type: 'tab_next' } if (ch === '/') return { type: 'filter_open' } if (ch === 'k' || ch === 'K') return { type: 'signal_menu' } if (ch === 'g') return { type: 'list_top' } if (ch === 'G') return { type: 'list_end' } if (ch === '<') return { type: 'sort_prev' } if (ch === '>') return { type: 'sort_next' } if (ch === 'n' && o.vi) return { type: 'filter_next' } if (ch === 'N' && o.vi) return { type: 'filter_prev' } if (ch >= '1' && ch <= '9') return { type: 'tab', n: ch.charCodeAt(0) - 0x31 } if (ch === '0') return { type: 'tab', n: -1 } if (ch.length === 1 && ch >= ' ' && ch.charCodeAt(0) > 32) return { type: 'filter_char', ch } } } return null } /** * @param {number[]} values * @param {number} width * @param {boolean} ascii * @param {boolean} logScale * @param {boolean} braille */ function bareTopSparkline(values, width, ascii, logScale, braille) { if (width <= 0) return '' const slice = values.length > width ? values.slice(values.length - width) : values.slice() if (slice.length === 0) return ' '.repeat(width) const vmap = logScale ? slice.map((v) => Math.log1p(Math.max(0, v))) : slice.slice() let min = vmap[0] let max = vmap[0] for (let i = 1; i < vmap.length; i++) { if (vmap[i] < min) min = vmap[i] if (vmap[i] > max) max = vmap[i] } const span = max - min || 1 const uni = '\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588' const br = '\u2800\u2840\u28c0\u28e0\u28f0\u28f4\u28f6\u28ff' const asc = ' .:-=+*#' let out = '' const pad = width - slice.length for (let i = 0; i < pad; i++) out += ascii ? ' ' : '\u2581' for (let i = 0; i < vmap.length; i++) { const v = vmap[i] const t = (v - min) / span let idx = Math.min(7, Math.floor(t * 8)) if (idx < 0) idx = 0 if (ascii) out += asc.charAt(idx) else if (braille) out += br.charAt(idx) else out += uni.charAt(idx) } return out.slice(0, width) } /** * @param {unknown} inflight * @param {number} maxWidth * @param {boolean} use256 * @returns {string} */ function bareTopDelegateHistogram(inflight, maxWidth, use256) { if (!inflight || typeof inflight !== 'object') return '' const o = /** @type {Record} */ (inflight) const keys = Object.keys(o).slice(0, 12) if (!keys.length) return '' let maxV = 1 for (const k of keys) { const v = Number(o[k]) if (Number.isFinite(v) && v > maxV) maxV = v } let line = '' const per = Math.max(4, Math.floor(maxWidth / Math.max(1, keys.length)) - 1) for (const k of keys) { const v = Math.min(maxV, Math.max(0, Number(o[k]) || 0)) const fill = Math.round((v / maxV) * per) let bar = '' for (let i = 0; i < per; i++) { const on = i < fill if (use256 && on) bar += '\x1b[38;5;39m\u2588\x1b[0m' else if (on) bar += '\u2588' else bar += '\u2581' } line += k.slice(0, 3) + bar + ' ' } return line.slice(0, maxWidth) } /** * @param {unknown} pl * @param {number} cols * @param {{ * dim: string, * warn: string, * reset: string, * meterLow?: string, * meterMid?: string, * meterHi?: string * }} pal */ function bareTopPipelineGauges(pl, cols, pal, useColor) { if (!pl || typeof pl !== 'object') return '' const o = /** @type {Record} */ (pl) const parts = [] for (const k of Object.keys(o).slice(0, 8)) { const v = o[k] if (typeof v === 'number' && Number.isFinite(v)) { const w = Math.min(12, Math.max(4, Math.floor(cols / 10))) const cap = v > 1000 ? v : 100 const pct = Math.min(100, Math.round((v / cap) * 100)) const fill = Math.round((pct / 100) * w) let bar = '' for (let i = 0; i < w; i++) bar += i < fill ? '\u2588' : '\u2591' let meter = pal.dim if (useColor) { if (pct >= 95) meter = pal.meterHi || pal.warn else if (pct >= 80) meter = pal.meterMid || pal.warn else if (pct >= 50) meter = pal.warn else meter = pal.meterLow || pal.dim } const s = k.slice(0, 10) + ' ' + meter + bar + pal.reset + pct + '%' parts.push(s) } } return parts.join(' ').slice(0, cols - 1) } /** * Break one logical line into wrapped physical lines (word-aware when possible). * @param {string} s * @param {number} width * @returns {string[]} */ function bareTopWrapLine(s, width) { const t = String(s) if (width < 8) return t ? [t] : [''] if (t.length <= width) return [t] const out = [] let i = 0 while (i < t.length) { let take = Math.min(width, t.length - i) if (i + take < t.length) { const chunk = t.slice(i, i + take) const sp = chunk.lastIndexOf(' ') if (sp > (width >> 1)) take = sp else { const tab = chunk.lastIndexOf('\t') if (tab > (width >> 1)) take = tab + 1 } } const piece = t.slice(i, i + take).replace(/\s+$/g, '') if (piece.length) out.push(piece) i += take while (i < t.length && (t[i] === ' ' || t[i] === '\t')) i++ } return out.length ? out : [''] } /** * @param {string} k */ function bareTopHumanKey(k) { return String(k || '') .replace(/([A-Z])/g, ' $1') .replace(/_/g, ' ') .replace(/\s+/g, ' ') .trim() .toLowerCase() .replace(/^\w/, (c) => c.toUpperCase()) } /** * Human-readable lines (no JSON). * @param {string} prefix * @param {unknown} v * @param {number} depth * @param {number} maxD * @param {string[]} lines * @param {number} maxKeys */ function bareTopFlatten(prefix, v, depth, maxD, lines, maxKeys) { const pad = ' '.repeat(depth) if (depth > maxD) { lines.push(pad + (prefix ? bareTopHumanKey(prefix) + ': ' : '') + '\u2026') return } if (v == null) { lines.push(pad + (prefix ? bareTopHumanKey(prefix) + ': ' : '') + bareTopStrings.na) return } const t = typeof v if (t === 'string' || t === 'number' || t === 'boolean') { let s = String(v) if (t === 'string' && s.length > 800) s = s.slice(0, 797) + '\u2026' lines.push(pad + (prefix ? bareTopHumanKey(prefix) + ': ' : '') + s) return } if (Array.isArray(v)) { const label = prefix ? bareTopHumanKey(prefix) + ': ' : '' if (v.length === 0) { lines.push(pad + label + '(empty)') return } lines.push(pad + label + '(' + v.length + ' items)') const lim = Math.min(v.length, 120) for (let i = 0; i < lim; i++) { const it = v[i] if (it != null && typeof it === 'object' && !Array.isArray(it)) { lines.push(pad + ' #' + i) bareTopFlatten('', it, depth + 2, maxD, lines, maxKeys) } else if (Array.isArray(it)) { lines.push(pad + ' #' + i) bareTopFlatten('', it, depth + 2, maxD, lines, maxKeys) } else { lines.push( pad + ' #' + i + ': ' + (it == null ? bareTopStrings.na : String(it)) ) } } if (v.length > lim) lines.push(pad + ' \u2026 +' + (v.length - lim) + ' more') return } if (t === 'object') { const o = /** @type {Record} */ (v) const keys = Object.keys(o) if (keys.length === 0) { lines.push(pad + (prefix ? bareTopHumanKey(prefix) + ': ' : '') + '(empty)') return } const slice = keys.slice(0, maxKeys) if (prefix) { lines.push(pad + bareTopHumanKey(prefix) + ':') for (const k of slice) bareTopFlatten(k, o[k], depth + 1, maxD, lines, maxKeys) } else { for (const k of slice) bareTopFlatten(k, o[k], depth, maxD, lines, maxKeys) } if (keys.length > maxKeys) lines.push(pad + '\u2026 +' + (keys.length - maxKeys) + ' keys') } } /** * @param {unknown} val * @param {number} cols * @returns {string[] | null} */ function bareTopShallowObjectLines(val, cols) { if (val == null || typeof val !== 'object' || Array.isArray(val)) return null const o = /** @type {Record} */ (val) const keys = Object.keys(o) if (keys.length === 0 || keys.length > 48) return null for (const k of keys) { const v = o[k] if (v != null && typeof v === 'object') return null } const keyW = Math.min(28, Math.max(8, Math.floor(cols * 0.38))) /** @type {string[]} */ const out = [] for (const k of keys.slice(0, 80)) { const label = bareTopHumanKey(k).slice(0, keyW) const pad = ' '.repeat(Math.max(0, keyW - label.length)) let sv = o[k] == null ? bareTopStrings.na : String(o[k]) sv = bareTopTruncateCell(sv, Math.max(8, cols - keyW - 4), true) out.push(' ' + label + pad + ' ' + sv) } return out } /** * @param {Record} pack * @param {number} maxDepth * @param {number} [wrapCols] * @returns {string[]} */ function bareTopLinesFromPack(pack, maxDepth, wrapCols) { const wc = wrapCols && wrapCols > 40 ? wrapCols : 120 /** @type {string[]} */ const lines = [] for (const key of Object.keys(pack)) { const val = pack[key] lines.push('') lines.push(bareTopSectionRule(bareTopHumanKey(key), wc)) if (val == null) lines.push(' ' + bareTopStrings.na) else { const shallow = bareTopShallowObjectLines(val, wc) if (shallow) lines.push(...shallow) else bareTopFlatten('', val, 1, maxDepth, lines, 220) } } return lines } /** * @param {string} title * @param {unknown} o * @param {number} maxDepth * @returns {string[]} */ function bareTopLinesFromSingle(title, o, maxDepth, wrapCols) { const wc = wrapCols && wrapCols > 40 ? wrapCols : 120 /** @type {string[]} */ const lines = [] lines.push('') lines.push(bareTopSectionRule(title, wc)) if (o == null) lines.push(' ' + bareTopStrings.na) else { const shallow = bareTopShallowObjectLines(o, wc) if (shallow) lines.push(...shallow) else bareTopFlatten('', o, 1, maxDepth, lines, 220) } return lines } /** * @param {unknown} graph * @returns {string[]} */ function bareTopInitdGraphLines(graph) { /** @type {string[]} */ const lines = [] if (graph == null) { lines.push(bareTopStrings.na) return lines } if (typeof graph === 'object' && graph && Array.isArray(graph.nodes)) { lines.push('Units (' + graph.nodes.length + '):') for (const n of graph.nodes) lines.push(' \u2022 ' + String(n)) return lines } bareTopFlatten('', graph, 0, 6, lines, 200) return lines } /** * @param {unknown} hints * @returns {string} */ function bareTopHintsFootLine(hints) { if (!hints || typeof hints !== 'object') return '' /** @type {string[]} */ const parts = [] bareTopFlatten('', hints, 0, 2, parts, 20) return parts.filter(Boolean).join(' \u00b7 ').slice(0, 380) } /** * @param {Awaited> | null} snap * @param {string} filterProcess * @param {'pid' | 'name' | 'state'} sortKey * @param {boolean} sortAsc */ function bareTopLiveProcessList(snap, filterProcess, sortKey, sortAsc) { const pt = snap && snap.extra && snap.extra.processTable && typeof snap.extra.processTable === 'object' ? snap.extra.processTable : null const raw = bareTopProcessRowsFromTable(pt) const pf = String(filterProcess || '').toLowerCase() const filtered = pf.length ? raw.filter((r) => String(r.name || r.label || '') .toLowerCase() .includes(pf) ) : raw.slice() return bareTopSortProcessRows(filtered, sortKey, sortAsc) } /** * @param {Record} ctx */ async function bareOsRunBareTopTui(ctx) { const stdin = /** @type {import('stream').Readable | undefined} */ ( ctx.replStdin ) const stdout = bareEditResolveStdout(ctx) if (!stdin || !stdout) { ctx.console.error('baretop: missing stdin/stdout') ctx.exitCode = 1 return } const useColor = bareEditUseColor(ctx) const envTop = ctx.env && typeof ctx.env === 'object' ? /** @type {Record} */ (ctx.env) : {} const noAlt = envTop.BARE_TOP_NO_ALTSCREEN != null && String(envTop.BARE_TOP_NO_ALTSCREEN) !== '' const intervalRaw = parseInt(envTop.BARE_TOP_INTERVAL_MS || '1000', 10) const intervalMs = Number.isFinite(intervalRaw) ? Math.min(10000, Math.max(250, intervalRaw)) : 1000 const uiMinRaw = parseInt(envTop.BARE_TOP_UI_MIN_MS || '0', 10) const uiMinMs = Number.isFinite(uiMinRaw) ? Math.max(0, uiMinRaw) : 0 const emaRaw = parseFloat(envTop.BARE_TOP_EMA_ALPHA || '0') const emaAlpha = Number.isFinite(emaRaw) ? Math.min(1, Math.max(0, emaRaw)) : 0 const theme = String(envTop.BARE_TOP_THEME || 'dark').toLowerCase() const asciiUi = envTop.BARE_TOP_ASCII_UI === '1' || (envTop.LANG && String(envTop.LANG).toLowerCase().includes('ascii')) const asciiGraph = envTop.BARE_TOP_ASCII_GRAPH === '1' || asciiUi const braille = envTop.BARE_TOP_BRAILLE_SPARK === '1' const logSpark = envTop.BARE_TOP_LOG_SPARK === '1' const use256 = useColor && (String(envTop.COLORTERM || '').toLowerCase() === 'truecolor' || String(envTop.COLORTERM || '').includes('256') || envTop.BARE_TOP_COLOR256 === '1') const viKeys = envTop.BARE_TOP_VI_KEYS === '1' || envTop.BARE_TOP_VI_KEYS === 'true' const highContrast = envTop.BARE_TOP_HIGH_CONTRAST === '1' || envTop.BARE_TOP_HIGH_CONTRAST === 'true' const layout = String(envTop.BARE_TOP_LAYOUT || 'stacked').toLowerCase() const exportPath = String( envTop.BARE_TOP_EXPORT_PATH || '/tmp/baretop-snapshot.json' ).trim() const icons = envTop.BARE_TOP_ICONS === '1' const monoUi = envTop.BARE_TOP_MONO === '1' || asciiUi const useFrames = envTop.BARE_TOP_FRAMES === '1' const incrementalClear = envTop.BARE_TOP_INCREMENTAL === '1' const hideFsTabs = envTop.BARE_TOP_FULLSCREEN_HIDE_TABS === '1' || envTop.BARE_TOP_FULLSCREEN_HIDE_TABS === 'true' const ringCapRaw = parseInt(envTop.BARE_TOP_RING_CAP || '72', 10) const RING_CAP = Number.isFinite(ringCapRaw) ? Math.min(240, Math.max(8, ringCapRaw)) : 72 const argvTop = Array.isArray(/** @type {unknown} */ (ctx).bareTopArgv) ? /** @type {string[]} */ (/** @type {unknown} */ (ctx).bareTopArgv) : null const invName = argvTop && argvTop[0] ? String(argvTop[0]).replace(/^.*\//, '') : 'baretop' const displayTitle = invName === 'btop' ? 'btop' : bareTopStrings.title const pal = bareTopTheme(theme, useColor, highContrast) const TAB_NAMES = [ 'overview', 'processes', 'initd', 'network', 'features', 'diagnostics', 'operator', 'pear', 'catalog', 'host', 'keys' ] const NTABS = TAB_NAMES.length const TAB_PROC = 1 /** @type {number[]} */ const keyq = [] function onData(chunk) { keyq.push(...bareTopChunkBytes(chunk)) bareTopStripBracketedPaste(keyq) } stdin.on('data', onData) let needsRedraw = true function onResize() { needsRedraw = true } if (typeof stdout.on === 'function') { try { stdout.on('resize', onResize) } catch { /* ignore */ } } try { if (typeof process !== 'undefined' && typeof process.on === 'function') { process.on('SIGWINCH', onResize) } } catch { /* ignore */ } function termDims() { const env = envTop const rawCols = /** @type {{ columns?: number }} */ (stdout).columns || parseInt(env.COLUMNS || '80', 10) || 80 const rows = /** @type {{ rows?: number }} */ (stdout).rows || parseInt(env.LINES || '24', 10) || 24 const emergencyNarrow = rawCols < 40 return { cols: Math.max(20, rawCols), rows: Math.max(12, rows), emergencyNarrow } } function headerLine(title) { const bar = (useColor ? bareEditSgr('status', true) : '') + title + (useColor ? EDIT_ANSI_RESET : '') return bar } /** @type {number[]} */ const ringExecDelta = [] /** @type {number[]} */ const ringPipeDelta = [] /** @type {number[]} */ const ringWallDelta = [] /** @type {number[]} */ const ringPeers = [] let prevExec = -1 let prevPipe = -1 let prevWall = -1 let emaExec = -1 let emaPipe = -1 let emaWall = -1 function pushRing(arr, v) { arr.push(v) while (arr.length > RING_CAP) arr.shift() } let tab = 0 let helpMode = false let quit = false /** @type {Awaited> | null} */ let lastSnap = null /** @type {Awaited> | null} */ let prevSnap = null let paused = false let deltaMode = false let fullscreenPanel = false let utcClock = false /** @type {number[]} */ const scrollRows = Array(NTABS).fill(0) let filterOpen = false /** @type {string} */ let filterLine = '' /** @type {string} */ let filterInitd = '' /** @type {string} */ let filterProcess = '' /** @type {'initd' | 'process'} */ let filterWhich = 'initd' const procSortEnv = String(envTop.BARE_TOP_PROC_SORT || 'pid').toLowerCase() /** @type {'pid' | 'name' | 'state'} */ let procSortKey = procSortEnv === 'name' ? 'name' : procSortEnv === 'state' ? 'state' : 'pid' let procSortAsc = true let procCursor = 0 /** @type {number | null} */ let procDetailPid = null /** @type {{ pid: number, sig: string, phase: 'pick' | 'confirm' } | null} */ let signalPrompt = null let burstUntil = 0 let lastDrawAt = 0 let tipRot = 0 let suspended = false let useAltScreen = false /** @type {(() => void) | null} */ let hookOffSuspend = null /** @type {(() => void) | null} */ let hookOffResume = null try { if (typeof ctx.suspendReplForSubprocess === 'function') { ctx.suspendReplForSubprocess() suspended = true } if (typeof ctx.bareOsRegisterSuspendHook === 'function') { hookOffSuspend = ctx.bareOsRegisterSuspendHook(() => { paused = true }) } if (typeof ctx.bareOsRegisterResumeHook === 'function') { hookOffResume = ctx.bareOsRegisterResumeHook(() => { paused = false needsRedraw = true }) } if (typeof stdin.setRawMode === 'function') stdin.setRawMode(true) if (typeof stdin.resume === 'function') stdin.resume() if (!noAlt) { bareTopWrite(ctx, stdout, '\x1b[?1049h') useAltScreen = true } async function tick() { lastSnap = await bareTopFetchSnapshot(ctx) const m = lastSnap.metricsLive const sess = m && typeof m.session === 'object' && m.session ? /** @type {Record} */ (m.session) : {} const ec = Number(sess.execLineCount) || 0 const pb = Number(sess.pipelineBytesTotal) || 0 const wm = Number(sess.execLineWallMsTotal) || 0 const peers = Number(m && m.peers) || 0 let dExec = 0 let dPipe = 0 let dWall = 0 if (prevExec >= 0) dExec = Math.max(0, ec - prevExec) if (prevPipe >= 0) dPipe = Math.max(0, pb - prevPipe) if (prevWall >= 0) dWall = Math.max(0, wm - prevWall) prevExec = ec prevPipe = pb prevWall = wm if (emaAlpha > 0 && emaExec >= 0) { dExec = bareTopEma(emaExec, dExec, emaAlpha) dPipe = bareTopEma(emaPipe, dPipe, emaAlpha) dWall = bareTopEma(emaWall, dWall, emaAlpha) } emaExec = dExec emaPipe = dPipe emaWall = dWall pushRing(ringExecDelta, dExec) pushRing(ringPipeDelta, dPipe) pushRing(ringWallDelta, dWall) pushRing(ringPeers, peers) } /** @type {string} */ let out = '' function draw() { const now = Date.now() if (uiMinMs > 0 && now - lastDrawAt < uiMinMs && !helpMode) return lastDrawAt = now const { cols, rows, emergencyNarrow } = termDims() const narrow = cols < 80 let scrollHint = '' out = incrementalClear ? '\x1b[?25l\x1b[H' : '\x1b[?25l\x1b[2J\x1b[H' const sparkAscii = asciiGraph || monoUi const clock = utcClock && typeof Date.prototype.toISOString === 'function' ? new Date().toISOString().slice(11, 19) + 'Z' : new Date().toTimeString().slice(0, 8) const tabTitle = TAB_NAMES[tab] || 'tab' const icon = icons ? '\u25cf ' : '' let title = icon + displayTitle + ' ' + (helpMode ? '— help ' : '— ' + tabTitle + ' ') + '| ' + clock + ' | ' + intervalMs + 'ms' if (paused) title += ' | ' + bareTopStrings.paused if (deltaMode) title += ' | DELTA' const pad = Math.max(0, cols - title.length) out += headerLine(title + ' '.repeat(pad)) + '\r\n' let hdrRow = 2 if (helpMode) { const dim = useColor ? pal.dim : '' const kw = useColor ? pal.kw : '' const rst = useColor ? pal.reset : '' out += '\r\n' + kw + bareTopStrings.helpKeys + rst + '\r\n' + dim + 'Navigation' + rst + '\r\n' + ' arrows Move list / process selection (Up/Down; PgUp/Dn page)\r\n' + ' g G Jump list top / bottom\r\n' + ' [ ] Prev / next tab\r\n' + ' 1-9 Jump tabs 1–9 (overview\u2026catalog); 0 = keys/help tab\r\n' + ' Esc Close help or cancel filter\r\n' + '\r\n' + dim + 'Tabs' + rst + '\r\n' + ' 1 overview 2 processes 3 initd 4 network 5 features 6 diagnostics\r\n' + ' 7 operator 8 pear 9 catalog (host: use [ ]) 0 keys\r\n' + '\r\n' + dim + 'Actions' + rst + '\r\n' + ' q Quit r F5 refresh+burst sp pause . step (paused)\r\n' + ' d delta e export f fullscreen t UTC clock h F1 help\r\n' + ' / Filter (initd or processes tab)\r\n' + ' < > Sort process column (processes tab)\r\n' + ' Enter Toggle process JSON detail (processes tab)\r\n' + ' k F9 Signal menu (selected logical PID)\r\n' + '\r\n' + dim + 'Notes' + rst + '\r\n' + bareTopStrings.noHtop + '\r\n' + bareTopStrings.noHtop2 + '\r\n' + 'Pear IPC: bare_os_*, pear:* (booter pear_ipc registry).\r\n' + '\r\n' + bareTopStrings.pressCloseHelp + '\r\n' const used = 28 for (let r = used; r < rows; r++) { out += bareEditCup(r, 1) + '\x1b[K' } out += bareEditCup(rows, 1) + '\x1b[K' out += dim + bareTopStrings.quitHint + rst out += '\x1b[?25h' bareTopWrite(ctx, stdout, out) return } const snap = lastSnap const mem = snap ? snap.meminfoLine : '' const load = snap ? snap.loadavgLine : '' const cpu = snap ? snap.cpuLine : '' const sparkW = Math.min(48, Math.max(8, cols - (narrow ? 22 : 28))) const staleMs = snap && snap.metaAtMs ? Math.max(0, Date.now() - snap.metaAtMs) : 0 const staleStr = staleMs > 2000 ? (useColor ? pal.warn : '') + ' stale ' + bareTopFormatDuration(staleMs) + (useColor ? pal.reset : '') : '' const fetchB = snap && snap.fetchWallMs != null ? (useColor ? pal.dim : '') + ' fetch ' + String(snap.fetchWallMs) + 'ms' + (useColor ? pal.reset : '') : '' function emitHdrLn(line) { out += line + '\r\n\x1b[K' hdrRow++ } { const mm = bareTopParseMeminfoMetrics(mem) const parts = mm.pairs.map((p) => p.label + ':' + p.value) let memMain = parts.length ? parts.join(' ') : mem || '(no meminfo)' memMain += staleStr + fetchB const mw = bareTopWrapLine(memMain, cols - 2) for (let mi = 0; mi < mw.length; mi++) emitHdrLn(mw[mi]) if ( mm.memTotal && mm.memAvail != null && Number.isFinite(mm.memTotal) && mm.memTotal > 0 ) { const pctAv = Math.round((mm.memAvail / mm.memTotal) * 100) const mw2 = Math.min(28, Math.max(8, Math.floor(cols / 3))) const bar = bareTopFormatMeterBar( pctAv, mw2, sparkAscii, { low: pal.meterLow, mid: pal.meterMid, hi: pal.meterHi, reset: pal.reset }, useColor ) emitHdrLn( (useColor ? pal.dim : '') + ' MemAvail ' + bar + ' ' + pctAv + '%' + (useColor ? pal.reset : '') ) } } { const la = bareTopParseLoadavg(load) let loadCol = load || '' if (la) { const hi = la.one >= 8 ? pal.bad : la.one >= 4 ? pal.warn : pal.dim loadCol = (useColor ? hi : '') + 'load ' + la.one.toFixed(2) + ' ' + la.five.toFixed(2) + ' ' + la.fifteen.toFixed(2) + (la.running != null && la.total != null ? ' tasks ' + la.running + '/' + la.total : '') + (useColor ? pal.reset : '') } let cpuExtra = cpu ? 'cpu: ' + cpu : '' if (snap && snap.hostStats && typeof snap.hostStats === 'object') { const hs = /** @type {Record} */ (snap.hostStats) const c0 = hs.cpuBusyPct ?? hs.cpuPct ?? hs.cpu if (typeof c0 === 'number' && Number.isFinite(c0)) { const cPct = Math.min(100, Math.max(0, c0)) const cw = Math.min(16, Math.max(6, Math.floor(cols / 5))) const cbar = bareTopFormatMeterBar( cPct, cw, sparkAscii, { low: pal.meterLow, mid: pal.meterMid, hi: pal.meterHi, reset: pal.reset }, useColor ) cpuExtra = (useColor ? pal.dim : '') + 'host cpu ' + cbar + ' ' + Math.round(cPct) + '%' + (useColor ? pal.reset : '') } } const row = (loadCol ? loadCol + ' ' : '') + (cpuExtra || '') const first = bareTopWrapLine(row, cols - 2) for (let li = 0; li < first.length; li++) emitHdrLn(first[li]) } if (!emergencyNarrow && snap && snap.hostOs && typeof snap.hostOs === 'object') { /** @type {string[]} */ const hl = [] bareTopFlatten('Host OS', snap.hostOs, 0, 3, hl, 24) for (const ln of hl) { for (const w of bareTopWrapLine(ln, cols - 2)) { emitHdrLn((useColor ? pal.dim : '') + w + (useColor ? pal.reset : '')) } } } const showTabStrip = !(fullscreenPanel && hideFsTabs) if (showTabStrip) { const stripParts = [] const short = emergencyNarrow for (let ti = 0; ti < NTABS; ti++) { const nm = TAB_NAMES[ti] || 't' let lab if (short) { if (ti === 10) lab = '0' else if (ti === 9) lab = 'H' else lab = String(ti + 1) } else if (ti === 10) lab = '0:' + nm.slice(0, 2) else if (ti === 9) lab = 'H:' + nm.slice(0, 2) else lab = String(ti + 1) + ':' + nm.slice(0, 3) const on = ti === tab stripParts.push( (useColor && on ? pal.sel : useColor ? pal.border : '') + (on ? '[' : '') + lab + (on ? ']' : '') + (useColor ? pal.reset : '') ) } const stripLine = stripParts.join(short ? '' : ' ') emitHdrLn((useColor ? pal.header : '') + stripLine.slice(0, cols - 1) + pal.reset) } if (snap && snap.healthScore != null) { const hbw = Math.min(36, Math.max(8, cols - 24)) const hb = bareTopHealthBarLine(snap.healthScore, hbw, sparkAscii) emitHdrLn( (useColor ? pal.good : '') + 'health ' + hb + (useColor ? pal.reset : '') ) } const mainEnd = fullscreenPanel ? rows - 1 : rows - 2 /** * @param {string[]} rawLines * @param {number} tabIdx * @param {number} startRow */ function drawScrollableLines(rawLines, tabIdx, startRow) { const wrapW = Math.max(16, cols - 2) /** @type {string[]} */ const wrapped = [] for (const ln of rawLines) { for (const w of bareTopWrapLine(ln, wrapW)) wrapped.push(w) } const mStart = startRow const vis = Math.max(1, mainEnd - mStart) const maxScroll = Math.max(0, wrapped.length - vis) if (scrollRows[tabIdx] > maxScroll) scrollRows[tabIdx] = maxScroll const s = scrollRows[tabIdx] let r = mStart const framePad = useFrames && !monoUi ? 1 : 0 const innerW = Math.max(8, cols - 1 - framePad * 2) for (let i = s; i < wrapped.length && r < mainEnd; i++) { let zeb = '' if (use256 && pal.zebraBg && i % 2 === 1) zeb = pal.zebraBg else if (i % 2 === 1) zeb = useColor ? pal.dim : '' const rst = useColor ? pal.reset : '' const left = useFrames && !monoUi ? (useColor ? pal.border : '') + '\u2502' + rst + ' ' : '' const text = wrapped[i].slice(0, innerW) out += bareEditCup(r, 1) + '\x1b[K' + zeb + left + text + rst + '\r\n' r++ } if (wrapped.length > vis) { scrollHint = ' | ' + (s + 1) + '\u2013' + Math.min(wrapped.length, s + vis) + '/' + wrapped.length } } const split = layout === 'even' && !fullscreenPanel && cols >= 100 && (tab === 0 || tab === TAB_PROC) if (tab === 0 && snap) { const m = snap.metricsLive const sess = m && typeof m.session === 'object' && m.session ? /** @type {Record} */ (m.session) : {} const ec = Number(sess.execLineCount) || 0 const pb = Number(sess.pipelineBytesTotal) || 0 const wm = Number(sess.execLineWallMsTotal) || 0 out += '\r\n' + (useColor ? pal.kw : '') + (icons ? '\u2022 ' : '') + bareTopStrings.activity + (useColor ? pal.reset : '') + '\r\n' out += ' exec dlt ' + bareTopSparkline( ringExecDelta, split ? Math.floor(sparkW / 2) : sparkW, sparkAscii, logSpark, braille ) + '\r\n' { const pipSpark = bareTopSparkline( ringPipeDelta, split ? Math.floor(sparkW / 2) : sparkW, sparkAscii, logSpark, braille ) const pipSuf = bareTopFormatBytes(pb) const pipPad = narrow && pipSpark.length < sparkW ? ' '.repeat(Math.max(0, sparkW - pipSpark.length)) : ' ' out += ' pipe dlt ' + pipSpark + pipPad + pipSuf + '\r\n' } { const wSpark = bareTopSparkline( ringWallDelta, split ? Math.floor(sparkW / 2) : sparkW, sparkAscii, logSpark, braille ) const wSuf = bareTopFormatDuration(wm) const wPad = narrow && wSpark.length < sparkW ? ' '.repeat(Math.max(0, sparkW - wSpark.length)) : ' ' out += ' wall dlt ' + wSpark + wPad + wSuf + '\r\n' } out += ' peers ' + bareTopSparkline( ringPeers, split ? Math.floor(sparkW / 2) : sparkW, sparkAscii, logSpark, braille ) + ' ' + (ringPeers.length ? String(ringPeers[ringPeers.length - 1]) : '') + '\r\n' if (deltaMode && prevSnap && prevSnap.metricsLive) { const ps = /** @type {Record} */ ( prevSnap.metricsLive.session || {} ) const pec = Number(ps.execLineCount) || 0 const ppb = Number(ps.pipelineBytesTotal) || 0 const pwm = Number(ps.execLineWallMsTotal) || 0 out += (useColor ? pal.dim : '') + ' d exec ' + (ec - pec) + ' d pipe ' + (pb - ppb) + ' d wall ' + (wm - pwm) + (useColor ? pal.reset : '') + '\r\n\x1b[K' } out += '\r\n' + (useColor ? pal.kw : '') + bareTopStrings.session + (useColor ? pal.reset : '') + '\r\n' out += ' execLineCount=' + String(sess.execLineCount ?? bareTopStrings.na) + ' pipelineBytesTotal=' + String(sess.pipelineBytesTotal ?? bareTopStrings.na) + '\r\n\x1b[K' out += ' execLineWallMsTotal=' + String(sess.execLineWallMsTotal ?? bareTopStrings.na) + '\r\n\x1b[K' const pg = bareTopPipelineGauges(m && m.pipeline, cols, pal, useColor) if (pg) { out += '\r\n' + (useColor ? pal.kw : '') + bareTopStrings.pipeline + (useColor ? pal.reset : '') + '\r\n' + pg + '\r\n\x1b[K' } const delI = m && m.delegateInflight out += '\r\n' + (useColor ? pal.kw : '') + bareTopStrings.delegates + (useColor ? pal.reset : '') + '\r\n' const hist = bareTopDelegateHistogram(delI, cols - 4, use256) if (hist) out += ' ' + hist + '\r\n\x1b[K' if (hist && delI != null && typeof delI === 'object') { const dk = Object.keys(/** @type {Record} */ (delI)).slice( 0, 10 ) const leg = dk.map((k) => k + '=' + String((/** @type {Record} */ (delI))[k])).join(' ') out += (useColor ? pal.dim : '') + ' hist keys: ' + leg.slice(0, cols - 4) + (useColor ? pal.reset : '') + '\r\n\x1b[K' } if (delI != null && typeof delI === 'object') { /** @type {string[]} */ const d1 = [] bareTopFlatten('Inflight', delI, 0, 4, d1, 48) for (const ln of d1) { for (const w of bareTopWrapLine(ln, cols - 2)) out += w + '\r\n\x1b[K' } } if (m && m.delegateRateBuckets != null) { /** @type {string[]} */ const d2 = [] bareTopFlatten('Rate buckets', m.delegateRateBuckets, 0, 4, d2, 48) for (const ln of d2) { for (const w of bareTopWrapLine(ln, cols - 2)) out += w + '\r\n\x1b[K' } } if (snap.fairnessSnapshot) { /** @type {string[]} */ const f1 = [] bareTopFlatten('Fairness', snap.fairnessSnapshot, 0, 4, f1, 48) for (const ln of f1) { for (const w of bareTopWrapLine(ln, cols - 2)) { out += (useColor ? pal.dim : '') + w + (useColor ? pal.reset : '') + '\r\n\x1b[K' } } } if (snap.subprocessBridge) { /** @type {string[]} */ const b1 = [] bareTopFlatten('Subprocess bridge', snap.subprocessBridge, 0, 4, b1, 64) for (const ln of b1) { for (const w of bareTopWrapLine(ln, cols - 2)) { out += (useColor ? pal.dim : '') + w + (useColor ? pal.reset : '') + '\r\n\x1b[K' } } } if (snap.hostStats) { /** @type {string[]} */ const h1 = [] bareTopFlatten('Host stats', snap.hostStats, 0, 4, h1, 48) for (const ln of h1) { for (const w of bareTopWrapLine(ln, cols - 2)) { out += (useColor ? pal.dim : '') + w + (useColor ? pal.reset : '') + '\r\n\x1b[K' } } } if (snap.resources) { out += '\r\n' + (useColor ? pal.kw : '') + 'resources' + (useColor ? pal.reset : '') + '\r\n' /** @type {string[]} */ const r1 = [] bareTopFlatten('', snap.resources, 0, 4, r1, 80) for (const ln of r1) { for (const w of bareTopWrapLine(' ' + ln, cols - 2)) out += w + '\r\n\x1b[K' } } if (snap.readErr) { for (const w of bareTopWrapLine('warn: ' + snap.readErr, cols - 2)) { out += (useColor ? pal.bad : '') + w + (useColor ? pal.reset : '') + '\r\n\x1b[K' } } } else if (tab === TAB_PROC && snap) { const rst = useColor ? pal.reset : '' emitHdrLn('') if (split && ringPeers.length) { emitHdrLn( (useColor ? pal.dim : '') + ' activity peers ' + bareTopSparkline( ringPeers, Math.min(32, sparkW), sparkAscii, logSpark, braille ) + ' ' + String(ringPeers[ringPeers.length - 1]) + rst ) } const pt = snap.extra && snap.extra.processTable && typeof snap.extra.processTable === 'object' ? snap.extra.processTable : null let pTitle = (useColor ? pal.kw : '') + (icons ? '\u2022 ' : '') + 'Processes (logical)' + rst + ' sort:' + procSortKey + (procSortAsc ? '+' : '-') + (filterProcess ? ' filter:' + filterProcess : '') if (signalPrompt) { pTitle += ' ' + (useColor ? pal.warn : '') + '[sig ' + signalPrompt.pid + ' ' + signalPrompt.sig + ' ' + (signalPrompt.phase === 'pick' ? 'Enter arm n cycle]' : 'y/n]') + rst } emitHdrLn(pTitle) /** @type {Record[]} */ const rawRows = bareTopProcessRowsFromTable(pt) const pf = filterProcess.toLowerCase() const filtered = pf.length ? rawRows.filter((r) => String(r.name || r.label || '') .toLowerCase() .includes(pf) ) : rawRows.slice() const sorted = bareTopSortProcessRows(filtered, procSortKey, procSortAsc) /** @type {Record} */ const states = {} for (const r of rawRows) { const st = String(r.state || '?') states[st] = (states[st] || 0) + 1 } const cnt = Object.keys(states) .sort() .map((k) => k + ':' + states[k]) .join(' ') emitHdrLn( (useColor ? pal.dim : '') + ' rows ' + sorted.length + (pf ? ' (filtered)' : '') + ' ' + cnt + rst ) if (procCursor >= sorted.length) procCursor = Math.max(0, sorted.length - 1) const mVis = Math.max(1, mainEnd - hdrRow) let scrollTop = scrollRows[TAB_PROC] const maxScr = Math.max(0, sorted.length - mVis) if (scrollTop > maxScr) scrollTop = maxScr if (procCursor < scrollTop) scrollTop = procCursor if (procCursor >= scrollTop + mVis) scrollTop = procCursor - mVis + 1 scrollRows[TAB_PROC] = scrollTop if (procDetailPid != null) { const row = sorted.find((x) => bareTopProcessPid(x) === procDetailPid) const raw = row || { note: 'pid not in list (check filter)' } const js = bareTopTruncateJsonPretty( raw, Math.min(16000, Math.max(400, cols * Math.max(8, rows - hdrRow - 2) * 2)) ) const jl = js.split('\n') emitHdrLn((useColor ? pal.dim : '') + ' (Enter closes detail)' + rst) drawScrollableLines(jl, TAB_PROC, hdrRow) } else { const wPid = 5 const wPpid = 5 const wSt = 9 const wPg = 5 const wSid = 3 const wName = Math.max(8, cols - (wPid + wPpid + wSt + wPg + wSid + 8)) const hdrLn = ' ' + bareTopFormatFixedColumns( ['PID', 'PPID', 'PGID', 'ST', 'SID', 'NAME'], [wPid, wPpid, wPg, wSt, wSid, wName], ['r', 'r', 'r', 'l', 'r', 'l'], cols - 2 ) emitHdrLn((useColor ? pal.header : '') + hdrLn + rst) const rowStart = hdrRow let r = rowStart for (let j = 0; j < mVis && scrollTop + j < sorted.length; j++) { const row = /** @type {Record} */ ( sorted[scrollTop + j] ) const pid = bareTopProcessPid(row) const cells = [ String(pid), String(row.ppid ?? ''), String(row.pgid ?? ''), String(row.state ?? '').slice(0, wSt), String(row.sid ?? ''), bareTopTruncateCell(String(row.name ?? ''), wName, true) ] const line = ' ' + bareTopFormatFixedColumns( cells, [wPid, wPpid, wPg, wSt, wSid, wName], ['r', 'r', 'r', 'l', 'r', 'l'], cols - 2 ) const sel = scrollTop + j === procCursor const framePad = useFrames && !monoUi ? 1 : 0 const left = useFrames && !monoUi ? (useColor ? pal.border : '') + '\u2502' + rst + ' ' : '' const prefix = sel && useColor ? pal.sel : '' const suf = sel && useColor ? rst : '' out += bareEditCup(r, 1) + '\x1b[K' + prefix + left + line.slice(0, Math.max(8, cols - 1 - framePad * 2)) + suf + '\r\n' r++ } if (sorted.length > mVis) { scrollHint = ' | ' + (scrollTop + 1) + '\u2013' + Math.min(sorted.length, scrollTop + mVis) + '/' + sorted.length + ' @' + (procCursor + 1) } } } else if (tab === 2 && snap && snap.initdGraph) { emitHdrLn('') emitHdrLn( (useColor ? pal.kw : '') + (icons ? '\u2022 ' : '') + 'Initd graph' + (useColor ? pal.reset : '') + (filterInitd ? ' filter:' + filterInitd : '') ) const g = snap.initdGraph /** @type {string[]} */ let initLines = [] if (typeof g === 'object' && g && Array.isArray(g.nodes)) { const nodes = /** @type {string[]} */ (g.nodes) const filt = filterInitd.toLowerCase() const list = filt ? nodes.filter((n) => String(n).toLowerCase().includes(filt)) : nodes initLines.push('Units: ' + list.length + (filterInitd ? ' (filtered)' : '')) for (const n of list) initLines.push((icons ? '\u2022 ' : '- ') + String(n)) } else { initLines = bareTopInitdGraphLines(g) } drawScrollableLines(initLines, 2, hdrRow) } else if (tab === 3 && snap) { emitHdrLn('') emitHdrLn( (useColor ? pal.kw : '') + (icons ? '\u2022 ' : '') + 'Net summary' + (useColor ? pal.reset : '') ) drawScrollableLines( bareTopLinesFromSingle('Network', snap.netSummary, 6, cols), 3, hdrRow ) } else if (tab === 4 && snap) { emitHdrLn('') emitHdrLn( (useColor ? pal.kw : '') + (icons ? '\u2022 ' : '') + 'Features / capabilities' + (useColor ? pal.reset : '') ) drawScrollableLines( bareTopLinesFromSingle('Features', snap.features, 6, cols), 4, hdrRow ) } else if (tab === 5 && snap) { emitHdrLn('') emitHdrLn( (useColor ? pal.kw : '') + (icons ? '\u2022 ' : '') + 'Diagnostics (debug, delegate_red, ipc backpressure)' + (useColor ? pal.reset : '') ) const pack = { debug: snap.extra.debug, delegateRed: snap.extra.delegateRed, ipcBackpressure: snap.extra.ipcBackpressure } drawScrollableLines(bareTopLinesFromPack(pack, 6, cols), 5, hdrRow) } else if (tab === 6 && snap) { emitHdrLn('') emitHdrLn( (useColor ? pal.kw : '') + (icons ? '\u2022 ' : '') + 'Operator (replication, swarm, sync, staging)' + (useColor ? pal.reset : '') ) const pack = { replication: snap.extra.replication, replicationBackpressure: snap.extra.replicationBackpressure, swarm: snap.extra.swarm, syncWindow: snap.extra.syncWindow, stagingSlot: snap.extra.stagingSlot, snapshotHints: snap.extra.snapshotHints } drawScrollableLines(bareTopLinesFromPack(pack, 6, cols), 6, hdrRow) } else if (tab === 7 && snap) { emitHdrLn('') emitHdrLn( (useColor ? pal.kw : '') + (icons ? '\u2022 ' : '') + 'Pear (ipc, health, trust, peer_health)' + (useColor ? pal.reset : '') ) const pack = { pearIpc: snap.extra.pearIpc, pearIpcHealth: snap.extra.pearIpcHealth, pearTrust: snap.extra.pearTrust, peerHealth: snap.extra.peerHealth } drawScrollableLines(bareTopLinesFromPack(pack, 6, cols), 7, hdrRow) } else if (tab === 8 && snap) { emitHdrLn('') emitHdrLn( (useColor ? pal.kw : '') + (icons ? '\u2022 ' : '') + 'Catalog (index, version, bootstrap, provenance, quotas, rlimits, extensions)' + (useColor ? pal.reset : '') ) const pack = { index: snap.extra.index, version: snap.fileTexts && snap.fileTexts.version ? bareTopJsonParse(snap.fileTexts.version) : null, bootstrap: snap.extra.bootstrap, provenance: snap.extra.provenance, quotas: snap.extra.quotas, rlimits: snap.extra.rlimits, extensions: snap.extra.extensions, capabilitiesJson: snap.extra.capabilitiesJson, capabilitiesNode: snap.extra.capabilitiesNode, seedHandshake: snap.extra.seedHandshake } drawScrollableLines(bareTopLinesFromPack(pack, 6, cols), 8, hdrRow) } else if (tab === 9 && snap) { emitHdrLn('') emitHdrLn( (useColor ? pal.kw : '') + (icons ? '\u2022 ' : '') + 'Host / workers (host_os, worker_budget, sandbox, git stats, HDMS, DHT)' + (useColor ? pal.reset : '') ) const pack = { hostOs: snap.hostOs, workerBudget: snap.extra.workerBudget, sandboxProfile: snap.extra.sandboxProfile, gitDelegateStats: snap.extra.gitDelegateStats, gitLfsPointerStats: snap.extra.gitLfsPointerStats, hdmsHealth: snap.extra.hdmsHealth, hdmsHints: snap.extra.hdmsHints, dhtStatus: snap.extra.dhtStatus, udxExtended: snap.extra.udxExtended, sessionStats: snap.extra.sessionStats } drawScrollableLines(bareTopLinesFromPack(pack, 6, cols), 9, hdrRow) } else if (tab === 10) { out += '\r\n' + (useColor ? pal.kw : '') + 'Key reference' + (useColor ? pal.reset : '') + '\r\n' + ' Same as h / F1 help — press 0 or tab to keys.\r\n' } const footProc = '^v sel <> sort Enter detail k/F9 sig / filter gG top/btm [ ] tab' const footInitd = 'PgUp/Dn scroll / filter units h help' const footDefault = 'PgUp/Dn scroll [ ] tabs h help e export f full' let footBase = tab === TAB_PROC ? footProc : tab === 2 ? footInitd : footDefault let foot = (useColor ? pal.dim : '') + footBase + (useColor ? pal.reset : '') + scrollHint if (filterOpen) { foot = (useColor ? pal.warn : '') + 'FILTER> ' + filterLine + '_' + (useColor ? pal.reset : '') } else { const hints = snap && snap.extra.snapshotHints const hintLine = bareTopHintsFootLine(hints) if (hintLine) { tipRot = (tipRot + 1) % Math.max(1, hintLine.length) const rot = hintLine.slice(tipRot) + ' · ' + hintLine.slice(0, tipRot) const room = Math.max(0, cols - foot.length - 4) if (room > 12) foot += ' | ' + rot.slice(0, room) } } out += bareEditCup(rows, 1) + '\x1b[K' + foot.slice(0, cols) out += '\x1b[?25h' bareTopWrite(ctx, stdout, out) } await tick() draw() while (!quit) { if (signalPrompt) { if (keyq.length === 1 && keyq[0] === 0x1b) { keyq.shift() signalPrompt = null draw() continue } const evs = bareTopDrainKeys(keyq, 16, { vi: viKeys, inFilter: false }) if (evs) { if (evs.type === 'quit') { quit = true break } const SIGS = ['TERM', 'INT', 'KILL'] if (signalPrompt.phase === 'pick') { if (evs.type === 'key' && evs.ch === 'n') { const i = SIGS.indexOf(signalPrompt.sig) signalPrompt.sig = SIGS[(i + 1) % SIGS.length] draw() } else if (evs.type === 'enter') { signalPrompt.phase = 'confirm' draw() } else if (evs.type === 'key' && (evs.ch === 'q' || evs.ch === 'Q')) { signalPrompt = null draw() } } else if (signalPrompt.phase === 'confirm') { if (evs.type === 'key' && (evs.ch === 'y' || evs.ch === 'Y')) { try { if (typeof ctx.bareOsSendSignal === 'function') ctx.bareOsSendSignal(signalPrompt.pid, signalPrompt.sig) } catch (e) { ctx.console.error( 'baretop: signal failed ' + ((e && /** @type {{ message?: string }} */ (e).message) || String(e)) ) } signalPrompt = null draw() } else if ( evs.type === 'key' && (evs.ch === 'n' || evs.ch === 'N') ) { signalPrompt = null draw() } } } await new Promise((r) => setTimeout(r, 50)) continue } if (helpMode) { if (keyq.length === 1 && keyq[0] === 0x1b) { keyq.shift() helpMode = false draw() continue } const evh = bareTopDrainKeys(keyq, 32, { vi: viKeys, inFilter: false }) if (evh) { if (evh.type === 'quit') { quit = true break } helpMode = false draw() } await new Promise((r) => setTimeout(r, 50)) continue } if (filterOpen) { if (keyq.length === 1 && keyq[0] === 0x1b) { keyq.shift() filterOpen = false draw() continue } const evf = bareTopDrainKeys(keyq, 64, { vi: viKeys, inFilter: true }) if (evf) { if (evf.type === 'quit') { quit = true break } if (evf.type === 'filter_bs') { filterLine = filterLine.slice(0, -1) draw() continue } if (evf.type === 'filter_enter') { if (filterWhich === 'process') filterProcess = filterLine else filterInitd = filterLine filterOpen = false scrollRows[tab] = 0 procCursor = 0 draw() continue } if (evf.type === 'filter_char' && evf.ch) { if (evf.ch.length === 1 && filterLine.length < 64) filterLine += evf.ch draw() continue } } await new Promise((r) => setTimeout(r, 50)) continue } const ev = bareTopDrainKeys(keyq, 32, { vi: viKeys, inFilter: false }) if (ev) { if (ev.type === 'quit') { quit = true break } if (ev.type === 'refresh') { burstUntil = Date.now() + Math.min(5000, intervalMs * 3) const beforeDelta = deltaMode ? lastSnap : null await tick() if (beforeDelta) prevSnap = beforeDelta draw() continue } if (ev.type === 'help_open') { helpMode = true draw() continue } if (ev.type === 'pause_toggle') { paused = !paused draw() continue } if (ev.type === 'step') { if (paused) { const beforeDelta = deltaMode ? lastSnap : null await tick() if (beforeDelta) prevSnap = beforeDelta draw() } continue } if (ev.type === 'delta_toggle') { deltaMode = !deltaMode if (deltaMode) prevSnap = lastSnap draw() continue } if (ev.type === 'fullscreen_toggle') { fullscreenPanel = !fullscreenPanel draw() continue } if (ev.type === 'clock_toggle') { utcClock = !utcClock draw() continue } if (ev.type === 'tab_prev') { tab = (tab + NTABS - 1) % NTABS draw() continue } if (ev.type === 'tab_next') { tab = (tab + 1) % NTABS draw() continue } if (ev.type === 'tab') { tab = ev.n < 0 ? NTABS - 1 : Math.max(0, Math.min(NTABS - 1, ev.n)) draw() continue } if (ev.type === 'list_top') { if (tab === TAB_PROC && !procDetailPid) { procCursor = 0 scrollRows[TAB_PROC] = 0 } else scrollRows[tab] = 0 draw() continue } if (ev.type === 'list_end') { if (tab === TAB_PROC && !procDetailPid) { const pl = bareTopLiveProcessList( lastSnap, filterProcess, procSortKey, procSortAsc ) procCursor = Math.max(0, pl.length - 1) scrollRows[TAB_PROC] = Math.max(0, pl.length - 1) } else scrollRows[tab] = 99999 draw() continue } if (ev.type === 'sort_prev' || ev.type === 'sort_next') { if (tab === TAB_PROC) { const ORDER = ['pid', 'name', 'state'] const idx = ORDER.indexOf(procSortKey) const n = ORDER.length procSortKey = /** @type {'pid' | 'name' | 'state'} */ ( ORDER[ ev.type === 'sort_next' ? (idx + 1 + n) % n : (idx - 1 + n) % n ] ) procSortAsc = true procCursor = 0 scrollRows[TAB_PROC] = 0 draw() } continue } if (ev.type === 'enter') { if (tab === TAB_PROC) { const pl = bareTopLiveProcessList( lastSnap, filterProcess, procSortKey, procSortAsc ) const row = pl[procCursor] const pid = row ? bareTopProcessPid(row) : 0 procDetailPid = procDetailPid != null ? null : pid > 0 ? pid : null draw() } continue } if (ev.type === 'signal_menu') { if (tab === TAB_PROC && !procDetailPid) { const pl = bareTopLiveProcessList( lastSnap, filterProcess, procSortKey, procSortAsc ) const row = pl[procCursor] const pid = row ? bareTopProcessPid(row) : 0 if (pid >= 1) signalPrompt = { pid, sig: 'TERM', phase: 'pick' } draw() } continue } if (ev.type === 'scroll') { const amt = typeof ev.amt === 'number' ? ev.amt : 5 if (tab === TAB_PROC && procDetailPid) { const sr = scrollRows[TAB_PROC] || 0 if (ev.dir === 'home') scrollRows[TAB_PROC] = 0 else if (ev.dir === 'end') scrollRows[TAB_PROC] = 99999 else if (typeof ev.dir === 'number') scrollRows[TAB_PROC] = Math.max(0, sr + ev.dir * amt) draw() continue } if (tab === TAB_PROC && !procDetailPid) { const pl = bareTopLiveProcessList( lastSnap, filterProcess, procSortKey, procSortAsc ) const maxI = Math.max(0, pl.length - 1) if (amt === 1) { if (ev.dir === 'home') { procCursor = 0 scrollRows[TAB_PROC] = 0 } else if (ev.dir === 'end') { procCursor = maxI scrollRows[TAB_PROC] = maxI } else if (typeof ev.dir === 'number' && ev.dir < 0) { procCursor = Math.max(0, procCursor - 1) } else if (typeof ev.dir === 'number' && ev.dir > 0) { procCursor = Math.min(maxI, procCursor + 1) } } else { const sr = scrollRows[TAB_PROC] || 0 if (ev.dir === 'home') scrollRows[TAB_PROC] = 0 else if (ev.dir === 'end') scrollRows[TAB_PROC] = 99999 else if (typeof ev.dir === 'number') scrollRows[TAB_PROC] = Math.max(0, sr + ev.dir * amt) } draw() continue } const sr = scrollRows[tab] || 0 if (ev.dir === 'home') scrollRows[tab] = 0 else if (ev.dir === 'end') scrollRows[tab] = 99999 else if (typeof ev.dir === 'number') scrollRows[tab] = Math.max(0, sr + ev.dir * amt) draw() continue } if (ev.type === 'filter_open') { filterOpen = true filterWhich = tab === TAB_PROC ? 'process' : 'initd' filterLine = filterWhich === 'process' ? filterProcess : filterInitd draw() continue } if (ev.type === 'export') { if (lastSnap && ctx.vfs && typeof ctx.vfs.writeFile === 'function') { try { const payload = JSON.stringify(lastSnap, null, 2) await ctx.vfs.writeFile(exportPath, payload) ctx.console.log(bareTopStrings.exportOk + ' ' + exportPath) } catch (e) { ctx.console.error( bareTopStrings.exportFail + ': ' + ((e && /** @type {{ message?: string }} */ (e).message) || String(e)) ) } } continue } } if (needsRedraw) { needsRedraw = false draw() } const waitMs = Date.now() < burstUntil ? Math.min(250, intervalMs) : intervalMs await new Promise((r) => setTimeout(r, paused ? 200 : waitMs)) if (!paused) { const beforeDelta = deltaMode ? lastSnap : null await tick() if (beforeDelta) prevSnap = beforeDelta } draw() } } finally { try { hookOffSuspend?.() } catch { /* ignore */ } try { hookOffResume?.() } catch { /* ignore */ } stdin.removeListener('data', onData) try { if (typeof stdout.removeListener === 'function') { stdout.removeListener('resize', onResize) } } catch { /* ignore */ } try { if (typeof process !== 'undefined' && typeof process.off === 'function') { process.off('SIGWINCH', onResize) } } catch { /* ignore */ } try { if (useAltScreen) { bareTopWrite(ctx, stdout, '\x1b[?1049l') } else { bareTopWrite(ctx, stdout, '\x1b[2J\x1b[H') } bareTopWrite(ctx, stdout, '\x1b[?25h\x1b[0m') } catch { /* ignore */ } try { if (typeof stdin.setRawMode === 'function') stdin.setRawMode(false) } catch { /* ignore */ } if (suspended && typeof ctx.resumeReplAfterSubprocess === 'function') { ctx.resumeReplAfterSubprocess() } } } async function run(ctx, argv) { const args = argv.slice(1) if (args.includes('-h') || args.includes('--help')) { ctx.console.log( 'usage: ' + (argv[0] || 'baretop') + '\n' + 'Full-screen session / kernel dashboard (htop-inspired).\n' + 'Shows metrics_live, a logical process table (process_table.json), peers, delegates,\n' + 'pipeline limits, many /proc/bare_os JSON mirrors, and optional ctx fast paths.\n' + 'Requires a TTY.\n' + '\n' + 'Tabs: 1 overview 2 processes 3 initd 4 network 5 features 6 diagnostics\n' + ' 7 operator 8 pear 9 catalog [ ] host 0 keys (F1 help, F5 refresh, F9 signal)\n' + '\n' + 'Environment:\n' + ' BARE_TOP_INTERVAL_MS Refresh period (default 1000; clamped 250–10000)\n' + ' BARE_TOP_UI_MIN_MS Minimum ms between full redraws (0 = off)\n' + ' BARE_TOP_FETCH_CONCURRENCY Parallel /proc reads per batch (default 6)\n' + ' BARE_TOP_NO_ALTSCREEN Any non-empty value skips alternate-screen mode\n' + ' BARE_TOP_INCREMENTAL Set to 1 to skip full 2J clear (may flicker)\n' + ' BARE_TOP_ASCII_GRAPH Set to 1 for ASCII sparklines\n' + ' BARE_TOP_ASCII_UI Set to 1 for ASCII box/spark fallbacks\n' + ' BARE_TOP_MONO Set to 1 to avoid Unicode box/spark glyphs\n' + ' BARE_TOP_BRAILLE_SPARK Set to 1 for braille sparkline glyphs\n' + ' BARE_TOP_LOG_SPARK Set to 1 for log-scaled sparklines\n' + ' BARE_TOP_EMA_ALPHA 0–1 smoothing for delta samples (0 = off)\n' + ' BARE_TOP_THEME dark | light | none\n' + ' BARE_TOP_HIGH_CONTRAST Set to 1 for bold/underline emphasis\n' + ' BARE_TOP_COLOR256 Set to 1 to force 256-color bar hints\n' + ' BARE_TOP_VI_KEYS Set to 1 for hjkl focus in vi-style mode\n' + ' BARE_TOP_LAYOUT stacked | even (wide overview / process hint)\n' + ' BARE_TOP_FRAMES Set to 1 for left rule on scroll regions\n' + ' BARE_TOP_FULLSCREEN_HIDE_TABS Set to 1 to hide tab strip in fullscreen (f)\n' + ' BARE_TOP_EXPORT_PATH JSON export target for the e key\n' + ' BARE_TOP_ICONS Set to 1 for bullet prefixes on sections\n' + ' BARE_TOP_PROC_SORT pid | name | state (default pid)\n' + ' BARE_TOP_RING_CAP Sparkline history length (default 72, max 240)\n' + ' NO_COLOR Disable ANSI colors\n' + '\n' + 'Keys: q quit r F5 refresh+burst sp pause . step d delta e export f fullscreen\n' + ' t UTC [ ] tabs arrows scroll / move selection g G top/bottom h F1 help\n' + ' / filter (initd or processes) <> sort (processes) Enter detail k F9 signal\n' + ' Esc closes help or filter\n' + '\n' + 'Contributors: /bin bundles cannot use node:crypto or node:module; use ctx.vfs and\n' + 'JSON.parse only. See holepunch-repos/docs/05-BARE-RUNTIME.md (Bare module stack).' ) return } const stdin = ctx.replStdin const stdout = ctx.replStdout || ctx.stdout if (!stdin || !/** @type {{ isTTY?: boolean }} */ (stdin).isTTY) { ctx.console.error('baretop: a terminal (TTY) is required') ctx.exitCode = 1 return } if (!stdout) { ctx.console.error('baretop: missing stdout') ctx.exitCode = 1 return } /** @type {Record} */ (ctx).bareTopArgv = argv await bareOsRunBareTopTui(ctx) }