bareTopFlattenLimited (multi-line, wider budget) for object/array values. Narrow key column slightly to give more width to values; regenerate bundles.
8071 lines
251 KiB
Plaintext
8071 lines
251 KiB
Plaintext
/* 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<string, unknown>} 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<string, unknown>} [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<string, unknown>} [ctx]
|
||
*/
|
||
function bareEditUseColor(ctx) {
|
||
const env =
|
||
ctx && typeof ctx === 'object' && ctx.env && typeof ctx.env === 'object'
|
||
? /** @type {Record<string, string>} */ (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 === 'Z') return { type: 'nav', key: 'stab' }
|
||
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' }
|
||
}
|
||
|
||
/** ESC [ 200 ~ */
|
||
const BARE_EDIT_BRACKET_PASTE_START = [27, 91, 50, 48, 48, 126]
|
||
/** ESC [ 201 ~ */
|
||
const BARE_EDIT_BRACKET_PASTE_END = [27, 91, 50, 48, 49, 126]
|
||
|
||
/**
|
||
* @param {number[]} q
|
||
* @param {number[]} prefix
|
||
*/
|
||
function bareEditStartsWithBytes(q, prefix) {
|
||
if (q.length < prefix.length) return false
|
||
for (let i = 0; i < prefix.length; i++) {
|
||
if (q[i] !== prefix[i]) return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* True if q could still become BARE_EDIT_BRACKET_PASTE_START with more bytes.
|
||
* @param {number[]} q
|
||
*/
|
||
function bareEditCouldBeBracketPastePrefix(q) {
|
||
const pre = BARE_EDIT_BRACKET_PASTE_START
|
||
const n = Math.min(q.length, pre.length)
|
||
for (let i = 0; i < n; i++) {
|
||
if (q[i] !== pre[i]) return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
/**
|
||
* @param {number[]} bytes
|
||
*/
|
||
function bareEditDecodePasteInner(bytes) {
|
||
try {
|
||
if (typeof TextDecoder !== 'undefined') {
|
||
return new TextDecoder('utf-8', { fatal: false }).decode(
|
||
new Uint8Array(bytes)
|
||
)
|
||
}
|
||
} catch {
|
||
/* fall through */
|
||
}
|
||
let s = ''
|
||
for (const b of bytes) s += String.fromCharCode(b)
|
||
return s
|
||
}
|
||
|
||
/**
|
||
* If queue begins with ESC [ 200 ~ but no closing ESC [ 201 ~ (stdin closed), return inner bytes as text.
|
||
* @param {number[]} q
|
||
* @returns {string|undefined}
|
||
*/
|
||
function bareEditFinalizeBracketedPasteOnEof(q) {
|
||
const SL = BARE_EDIT_BRACKET_PASTE_START.length
|
||
if (q.length >= SL && bareEditStartsWithBytes(q, BARE_EDIT_BRACKET_PASTE_START)) {
|
||
const inner = q.slice(SL)
|
||
q.length = 0
|
||
return bareEditDecodePasteInner(inner)
|
||
}
|
||
return undefined
|
||
}
|
||
|
||
/**
|
||
* xterm bracketed paste: ESC [ 200 ~ … ESC [ 201 ~
|
||
* @param {number[]} q mutable queue
|
||
* @returns {string|null|undefined} string if consumed; null if incomplete; undefined if not bracketed paste at front
|
||
*/
|
||
function bareEditTryConsumeBracketedPaste(q) {
|
||
const SL = BARE_EDIT_BRACKET_PASTE_START.length
|
||
const EL = BARE_EDIT_BRACKET_PASTE_END.length
|
||
if (!q.length) return undefined
|
||
if (q.length < SL) {
|
||
return bareEditCouldBeBracketPastePrefix(q) ? null : undefined
|
||
}
|
||
if (!bareEditStartsWithBytes(q, BARE_EDIT_BRACKET_PASTE_START)) {
|
||
return undefined
|
||
}
|
||
for (let i = SL; i <= q.length - EL; i++) {
|
||
let ok = true
|
||
for (let j = 0; j < EL; j++) {
|
||
if (q[i + j] !== BARE_EDIT_BRACKET_PASTE_END[j]) {
|
||
ok = false
|
||
break
|
||
}
|
||
}
|
||
if (ok) {
|
||
const inner = q.slice(SL, i)
|
||
q.splice(0, i + EL)
|
||
return bareEditDecodePasteInner(inner)
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
|
||
/**
|
||
* @param {number[]} q mutable queue (front = index 0)
|
||
* @returns {Record<string, unknown> | 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). */
|
||
|
||
/** EWMA of last fetch wall time (ms); used when `BARE_TOP_FETCH_EWMA=1`. */
|
||
var bareTopFetchEwmaMs = -1
|
||
|
||
/**
|
||
* 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'],
|
||
['openssh', '/proc/bare_os/openssh.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'],
|
||
[
|
||
'bootBudgetSummary',
|
||
'/proc/bare_os/boot_budget_summary.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'],
|
||
['securityPosture', '/proc/bare_os/security_posture.json'],
|
||
['processIo', '/proc/bare_os/process_io.json'],
|
||
['processThreads', '/proc/bare_os/process_threads.json'],
|
||
['processMaps', '/proc/bare_os/process_maps.json']
|
||
]
|
||
|
||
/** Subset for BARE_TOP_SNAPSHOT_LITE=1 — keep keys subset of full list above. */
|
||
var BARE_TOP_SNAPSHOT_LITE_ENTRIES = [
|
||
['index', '/proc/bare_os/index.json'],
|
||
['version', '/proc/bare_os/version'],
|
||
['hostOs', '/proc/bare_os/host_os.json'],
|
||
['replication', '/proc/bare_os/replication'],
|
||
['swarm', '/proc/bare_os/swarm'],
|
||
['syncWindow', '/proc/bare_os/sync_window.json'],
|
||
['processTable', '/proc/bare_os/process_table.json'],
|
||
['snapshotHints', '/proc/bare_os/snapshot_hints.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<number>} */ (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<string, unknown>} ctx
|
||
* @param {string} path
|
||
* @returns {Promise<string>}
|
||
*/
|
||
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<string, unknown>} ctx
|
||
* @param {readonly [string, string][]} entries
|
||
* @param {number} concurrency
|
||
* @returns {Promise<Record<string, string>>}
|
||
*/
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {readonly [string, string][]} entries
|
||
* @param {number} concurrency
|
||
* @param {string[] | null} [errPaths] push failed paths (optional)
|
||
*/
|
||
async function bareTopReadProcBatch(ctx, entries, concurrency, errPaths) {
|
||
/** @type {Record<string, string>} */
|
||
const out = {}
|
||
const vfs = ctx.vfs
|
||
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]) => {
|
||
if (!vfs || typeof vfs.readFile !== 'function') {
|
||
out[key] = ''
|
||
return
|
||
}
|
||
try {
|
||
const buf = await vfs.readFile(path)
|
||
out[key] = bareTopBufToString(buf, ctx.b4a)
|
||
} catch {
|
||
out[key] = ''
|
||
if (errPaths) errPaths.push(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<string, unknown> | null} m metrics live
|
||
* @param {number} peers
|
||
* @param {string | null | undefined} readErr optional snapshot read failure
|
||
* @returns {number} 0–100
|
||
*/
|
||
function bareTopHealthScore(m, peers, readErr) {
|
||
return bareTopHealthScoreDetail(m, peers, readErr).score
|
||
}
|
||
|
||
/**
|
||
* Subtractive score from 100 (penalty table):
|
||
* - read_err: -28
|
||
* - no_metrics: -22
|
||
* - delegate_inflight: capped combined from active keys (count>0) + total inflight sum; max -12
|
||
* - replication stall: -20 when stallHint is a fault token OR replicationStall===true;
|
||
* benign hints no_peers | length_unavailable | ok do not penalize (operational, not fault)
|
||
* - boot_cold exceeded: -12; boot_stdlib exceeded: -12
|
||
* - initd_readiness.failed: up to -18; kernel initd.unit_failed_final: up to -10
|
||
* - peer_mismatch (replication.peerCount vs metrics peers): -8 when both finite and |Δ|>2
|
||
* - peers_invalid (peers<0): -5
|
||
* @param {Record<string, unknown> | null} m
|
||
* @param {number} peers
|
||
* @param {string | null | undefined} readErr
|
||
* @returns {{ score: number, breakdown: string }}
|
||
*/
|
||
function bareTopHealthScoreDetail(m, peers, readErr) {
|
||
let score = 100
|
||
/** @type {string[]} */
|
||
const parts = ['base=100']
|
||
|
||
const errS = readErr != null ? String(readErr).trim() : ''
|
||
if (errS) {
|
||
const pen = 28
|
||
score -= pen
|
||
parts.push('read_err=-' + pen)
|
||
}
|
||
|
||
const live =
|
||
m && typeof m === 'object'
|
||
? /** @type {Record<string, unknown>} */ (m)
|
||
: null
|
||
if (!live) {
|
||
const pen = 22
|
||
score -= pen
|
||
parts.push('no_metrics=-' + pen)
|
||
} else {
|
||
const delI = live.delegateInflight
|
||
if (delI != null && typeof delI === 'object') {
|
||
const o = /** @type {Record<string, unknown>} */ (delI)
|
||
const keys = Object.keys(o)
|
||
let nActive = 0
|
||
let sum = 0
|
||
for (let i = 0; i < keys.length; i++) {
|
||
const v = Math.max(0, Number(o[keys[i]]) || 0)
|
||
sum += v
|
||
if (v > 0) nActive++
|
||
}
|
||
const penK = Math.min(10, nActive * 2)
|
||
const penS = Math.min(20, sum * 2)
|
||
const inflightCap = 12
|
||
const inflightTotal = Math.min(inflightCap, penK + penS)
|
||
if (inflightTotal > 0) {
|
||
score -= inflightTotal
|
||
parts.push(
|
||
'inflight_-' +
|
||
inflightTotal +
|
||
'(activeKinds=' +
|
||
nActive +
|
||
',sum=' +
|
||
sum +
|
||
')'
|
||
)
|
||
}
|
||
}
|
||
|
||
const rep =
|
||
live.replicationLive && typeof live.replicationLive === 'object'
|
||
? /** @type {Record<string, unknown>} */ (live.replicationLive)
|
||
: null
|
||
const stallExplicit =
|
||
rep && rep.replicationStall === true ? true : false
|
||
const hintRaw = rep && rep.stallHint
|
||
const benignStallHints = new Set([
|
||
'',
|
||
'ok',
|
||
'no_peers',
|
||
'length_unavailable',
|
||
'0',
|
||
'false',
|
||
'none',
|
||
'off',
|
||
'null',
|
||
'undefined'
|
||
])
|
||
if (stallExplicit) {
|
||
const pen = 20
|
||
score -= pen
|
||
parts.push('replication_stall_flag=-' + pen)
|
||
} else if (hintRaw != null) {
|
||
const hs = String(hintRaw).trim().toLowerCase()
|
||
if (hs && !benignStallHints.has(hs)) {
|
||
const pen = 20
|
||
score -= pen
|
||
parts.push('stall_hint=-' + pen + '(' + hs + ')')
|
||
}
|
||
}
|
||
|
||
const cold =
|
||
live.bootBudgetCold && typeof live.bootBudgetCold === 'object'
|
||
? /** @type {Record<string, unknown>} */ (live.bootBudgetCold)
|
||
: null
|
||
if (cold && cold.exceeded === true) {
|
||
score -= 12
|
||
parts.push('boot_cold=-12')
|
||
}
|
||
const std =
|
||
live.bootBudgetBareStdlib &&
|
||
typeof live.bootBudgetBareStdlib === 'object'
|
||
? /** @type {Record<string, unknown>} */ (live.bootBudgetBareStdlib)
|
||
: null
|
||
if (std && std.exceeded === true) {
|
||
score -= 12
|
||
parts.push('boot_stdlib=-12')
|
||
}
|
||
|
||
const ir = live.initdReadiness
|
||
if (ir && typeof ir === 'object') {
|
||
const failed = Number(/** @type {Record<string, unknown>} */ (ir).failed)
|
||
if (Number.isFinite(failed) && failed > 0) {
|
||
const pen = Math.min(18, failed * 6)
|
||
score -= pen
|
||
parts.push('initd_failed_' + failed + '=-' + pen)
|
||
}
|
||
}
|
||
|
||
const kc = live.kernelCounters
|
||
if (kc && typeof kc === 'object') {
|
||
const kf = Number(
|
||
/** @type {Record<string, unknown>} */ (kc)['initd.unit_failed_final']
|
||
)
|
||
if (Number.isFinite(kf) && kf > 0) {
|
||
const pen = Math.min(10, kf * 2)
|
||
score -= pen
|
||
parts.push('initd_kc_' + kf + '=-' + pen)
|
||
}
|
||
}
|
||
|
||
if (
|
||
rep &&
|
||
typeof rep.peerCount === 'number' &&
|
||
Number.isFinite(rep.peerCount) &&
|
||
rep.peerCount >= 0 &&
|
||
Number.isFinite(peers) &&
|
||
peers >= 0
|
||
) {
|
||
if (Math.abs(rep.peerCount - peers) > 2) {
|
||
score -= 8
|
||
parts.push('peer_mismatch=-8')
|
||
}
|
||
}
|
||
}
|
||
|
||
if (peers < 0) {
|
||
score -= 5
|
||
parts.push('peers_invalid=-5')
|
||
}
|
||
|
||
if (score < 0) score = 0
|
||
if (score > 100) score = 100
|
||
parts.push('=>' + score)
|
||
return { score, breakdown: parts.join(' ') }
|
||
}
|
||
|
||
/**
|
||
* @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<string, unknown>} ctx
|
||
* @param {Record<string, string>} files
|
||
* @param {string} key
|
||
* @returns {Record<string, unknown> | null}
|
||
*/
|
||
function bareTopParsedFile(files, key) {
|
||
const t = files[key]
|
||
if (!t) return null
|
||
const p = bareTopJsonParse(t)
|
||
return p && typeof p === 'object'
|
||
? /** @type {Record<string, unknown>} */ (p)
|
||
: null
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @returns {Promise<Record<string, unknown>>}
|
||
*/
|
||
/** One coalesced operator frame; keep VFS read fan-out bounded (batch size + concurrency). */
|
||
async function bareTopFetchSnapshot(ctx, fetchOpts) {
|
||
const fetchStart = Date.now()
|
||
let readErr = null
|
||
const env =
|
||
ctx.env && typeof ctx.env === 'object'
|
||
? /** @type {Record<string, string>} */ (ctx.env)
|
||
: {}
|
||
const fo =
|
||
fetchOpts && typeof fetchOpts === 'object'
|
||
? /** @type {{ forceLite?: boolean }} */ (fetchOpts)
|
||
: null
|
||
const concRaw = parseInt(env.BARE_TOP_FETCH_CONCURRENCY || '8', 10)
|
||
const concurrency = Number.isFinite(concRaw) ? concRaw : 8
|
||
const fetchEwmaOn =
|
||
env.BARE_TOP_FETCH_EWMA === '1' || env.BARE_TOP_FETCH_EWMA === 'true'
|
||
const snapshotLite =
|
||
fo && fo.forceLite === true
|
||
? true
|
||
: env.BARE_TOP_SNAPSHOT_LITE === '1' ||
|
||
env.BARE_TOP_SNAPSHOT_LITE === 'true'
|
||
const procEntryList = snapshotLite
|
||
? BARE_TOP_SNAPSHOT_LITE_ENTRIES
|
||
: BARE_TOP_SNAPSHOT_PROC_ENTRIES
|
||
|
||
/** @type {Record<string, string>} */
|
||
let fileTexts = {}
|
||
let fastAtMs = 0
|
||
/** @type {Record<string, unknown> | null} */
|
||
let metricsLiveFromSnap = null
|
||
|
||
try {
|
||
if (typeof ctx.bareOsReadBareTopSnapshot === 'function') {
|
||
const r = await ctx.bareOsReadBareTopSnapshot(
|
||
snapshotLite ? { lite: true } : undefined
|
||
)
|
||
if (r && typeof r === 'object') {
|
||
const o = /** @type {Record<string, unknown>} */ (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
|
||
const mlRaw = o.metricsLiveText
|
||
if (typeof mlRaw === 'string' && mlRaw.trim()) {
|
||
const mp = bareTopJsonParse(mlRaw)
|
||
if (mp && typeof mp === 'object') {
|
||
metricsLiveFromSnap = /** @type {Record<string, unknown>} */ (mp)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} catch (e) {
|
||
readErr = (e && /** @type {{ message?: string }} */ (e).message) || String(e)
|
||
}
|
||
|
||
/** @type {string[]} */
|
||
const readErrPaths = []
|
||
if (Object.keys(fileTexts).length === 0) {
|
||
let useConc = concurrency
|
||
if (fetchEwmaOn && bareTopFetchEwmaMs >= 0) {
|
||
const e = bareTopFetchEwmaMs
|
||
if (e > 900) useConc = Math.max(2, concurrency - 3)
|
||
else if (e > 500) useConc = Math.max(2, concurrency - 2)
|
||
else if (e > 280) useConc = Math.max(2, concurrency - 1)
|
||
}
|
||
fileTexts = await bareTopReadProcBatch(
|
||
ctx,
|
||
procEntryList,
|
||
useConc,
|
||
readErrPaths
|
||
)
|
||
}
|
||
|
||
const atMs = fastAtMs || Date.now()
|
||
|
||
/** @type {Record<string, unknown> | null} */
|
||
let metricsLive = metricsLiveFromSnap
|
||
/** @type {Record<string, unknown> | null} */
|
||
let resources = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
let features = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
let netSummary = null
|
||
/** @type {unknown} */
|
||
let initdGraph = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
let fairnessSnapshot = null
|
||
/** @type {unknown} */
|
||
let subprocessBridge = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
let hostStats = null
|
||
|
||
try {
|
||
if (typeof ctx.bareOsReadProcMetricsLive === 'function') {
|
||
const o = ctx.bareOsReadProcMetricsLive()
|
||
if (o && typeof o === 'object') metricsLive = /** @type {Record<string, unknown>} */ (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<string, unknown>} */ (p)
|
||
}
|
||
|
||
try {
|
||
if (typeof ctx.bareOsGetResourceStatus === 'function') {
|
||
const o = ctx.bareOsGetResourceStatus()
|
||
if (o && typeof o === 'object') resources = /** @type {Record<string, unknown>} */ (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<string, unknown>} */ (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<string, unknown>} */ (fp)
|
||
|
||
const netT = await bareTopReadProc(ctx, '/proc/bare_os/net_summary.json')
|
||
const np = bareTopJsonParse(netT)
|
||
if (np && typeof np === 'object') netSummary = /** @type {Record<string, unknown>} */ (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<string, unknown>} */ (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<string, unknown>} */ (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 = ''
|
||
let cpuCoreCount = 0
|
||
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)
|
||
}
|
||
const L = line.trim().toLowerCase()
|
||
if (L.startsWith('processor') && L.includes(':')) cpuCoreCount++
|
||
}
|
||
if (!cpuCoreCount) {
|
||
const m = cpuRaw.match(/^processor\s*:/gim)
|
||
if (m) cpuCoreCount = m.length
|
||
}
|
||
|
||
let swapinfoLine = ''
|
||
for (const line of memRaw.split('\n')) {
|
||
const L = line.trim()
|
||
if (L.startsWith('SwapTotal:') || L.startsWith('SwapFree:')) {
|
||
swapinfoLine = swapinfoLine ? swapinfoLine + ' ' + L : L
|
||
}
|
||
}
|
||
|
||
const diskstatsRaw = await bareTopReadProc(ctx, '/proc/diskstats')
|
||
const diskstatsLine = diskstatsRaw.split('\n')[0]?.trim() || ''
|
||
|
||
const hostOs = bareTopParsedFile(fileTexts, 'hostOs')
|
||
const metaAt =
|
||
metricsLive && typeof metricsLive.atMs === 'number' ? metricsLive.atMs : atMs
|
||
|
||
/** @type {Record<string, Record<string, unknown> | null>} */
|
||
const extra = {}
|
||
for (const [key] of procEntryList) {
|
||
if (
|
||
key === 'processTable' &&
|
||
metricsLive &&
|
||
metricsLive.processTable &&
|
||
typeof metricsLive.processTable === 'object'
|
||
) {
|
||
extra[key] = /** @type {Record<string, unknown>} */ (
|
||
metricsLive.processTable
|
||
)
|
||
continue
|
||
}
|
||
extra[key] = bareTopParsedFile(fileTexts, key)
|
||
}
|
||
|
||
const fetchWallMs = Date.now() - fetchStart
|
||
bareTopFetchEwmaMs =
|
||
bareTopFetchEwmaMs < 0
|
||
? fetchWallMs
|
||
: bareTopFetchEwmaMs * 0.82 + fetchWallMs * 0.18
|
||
const peersN = Number(metricsLive && metricsLive.peers) || 0
|
||
const healthD = bareTopHealthScoreDetail(metricsLive, peersN, readErr)
|
||
|
||
return {
|
||
metricsLive,
|
||
resources,
|
||
features,
|
||
initdGraph,
|
||
netSummary,
|
||
meminfoLine,
|
||
swapinfoLine,
|
||
loadavgLine,
|
||
cpuLine,
|
||
cpuCoreCount,
|
||
diskstatsLine,
|
||
diskstatsRaw,
|
||
hostOs,
|
||
atMs,
|
||
metaAtMs: metaAt,
|
||
readErr,
|
||
readErrPaths,
|
||
fileTexts,
|
||
extra,
|
||
fairnessSnapshot,
|
||
subprocessBridge,
|
||
hostStats,
|
||
fetchWallMs,
|
||
healthScore: healthD.score,
|
||
healthBreakdown: healthD.breakdown,
|
||
snapshotLite
|
||
}
|
||
}
|
||
|
||
/** 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<Record<string, unknown>>}
|
||
*/
|
||
function bareTopProcessRowsFromTable(pt) {
|
||
if (!pt || typeof pt !== 'object') return []
|
||
const o = /** @type {Record<string, unknown>} */ (pt)
|
||
const arr = o.processes
|
||
if (!Array.isArray(arr)) return []
|
||
return arr.filter((x) => x && typeof x === 'object')
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} pt
|
||
* @param {number} nowMs
|
||
*/
|
||
function bareTopSessionUptimeLine(pt, nowMs) {
|
||
const rows = bareTopProcessRowsFromTable(pt)
|
||
let boot = 0
|
||
for (const r of rows) {
|
||
if (bareTopProcessPid(r) === 1) {
|
||
boot = bareTopProcessStartedMs(r)
|
||
break
|
||
}
|
||
}
|
||
if (!boot) return ''
|
||
return ' uptime ' + bareTopFormatProcAge(boot, nowMs) + ' (since bare-os-kernel row)'
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} row
|
||
*/
|
||
function bareTopProcessPid(row) {
|
||
const n = Number(row.pid)
|
||
return Number.isFinite(n) ? n : 0
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} row
|
||
* @param {'pid'|'name'|'state'|'time'|'nice'|'pri'|'cpu'} key
|
||
*/
|
||
/**
|
||
* @param {Record<string, unknown>} row
|
||
* @param {number} wallMs
|
||
*/
|
||
function bareTopEffectiveCpuPct(row, wallMs) {
|
||
const c = row.cpuPct ?? row.cpu
|
||
if (typeof c === 'number' && Number.isFinite(c)) return c
|
||
const delta = row.cpuMsDelta
|
||
const w = Number(wallMs) || 0
|
||
if (typeof delta === 'number' && Number.isFinite(delta) && w > 0)
|
||
return Math.min(100, Math.max(0, (delta / w) * 100))
|
||
return 0
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} row
|
||
* @param {'pid'|'name'|'state'|'time'|'nice'|'pri'|'cpu'} key
|
||
* @param {number} [wallMs]
|
||
*/
|
||
function bareTopProcessSortKey(row, key, wallMs) {
|
||
if (key === 'name') return String(row.name || row.label || '').toLowerCase()
|
||
if (key === 'state') return String(row.state || '').toLowerCase()
|
||
if (key === 'time') return bareTopProcessStartedMs(row)
|
||
if (key === 'nice') return Number(row.nice ?? row.ni) || 0
|
||
if (key === 'pri') return Number(row.pri ?? row.priority) || 0
|
||
if (key === 'cpu') return bareTopEffectiveCpuPct(row, wallMs || 0)
|
||
return bareTopProcessPid(row)
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} row
|
||
* @param {string} pfRaw
|
||
* @param {Record<string, string>} env
|
||
* @param {{ taggedPids?: Set<number>, taggedOnly?: boolean }} [opts]
|
||
*/
|
||
function bareTopProcessRowMatchesFilter(row, pfRaw, env, opts) {
|
||
const o = opts && typeof opts === 'object' ? opts : {}
|
||
const tagged = o.taggedPids
|
||
const taggedOnly = o.taggedOnly === true
|
||
const pid = bareTopProcessPid(row)
|
||
if (taggedOnly && tagged && tagged.size && !tagged.has(pid)) return false
|
||
|
||
const invertBase =
|
||
env.BARE_TOP_FILTER_INVERT === '1' || env.BARE_TOP_FILTER_INVERT === 'true'
|
||
let pf = String(pfRaw || '').trim()
|
||
let invert = invertBase
|
||
if (pf.startsWith('!')) {
|
||
pf = pf.slice(1).trim()
|
||
invert = !invert
|
||
}
|
||
if (!pf) return true
|
||
|
||
const regexEnv =
|
||
env.BARE_TOP_FILTER_REGEX === '1' || env.BARE_TOP_FILTER_REGEX === 'true'
|
||
const name = String(row.name || row.label || '')
|
||
const user = String(row.user || row.identity || '')
|
||
/** @type {RegExp | null} */
|
||
let re = null
|
||
if (regexEnv && pf.length) {
|
||
try {
|
||
re = new RegExp(pf, 'i')
|
||
} catch {
|
||
re = null
|
||
}
|
||
} else if (pf.length >= 2 && pf.charAt(0) === '/') {
|
||
const end = pf.lastIndexOf('/')
|
||
if (end > 0) {
|
||
try {
|
||
const flags = pf.slice(end + 1) || 'i'
|
||
re = new RegExp(pf.slice(1, end), flags)
|
||
} catch {
|
||
re = null
|
||
}
|
||
}
|
||
}
|
||
let match = false
|
||
if (re) match = re.test(name) || re.test(user)
|
||
else match = name.toLowerCase().includes(pf.toLowerCase())
|
||
return invert ? !match : match
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>[]} rows
|
||
* @param {'pid'|'name'|'state'|'time'|'nice'|'pri'|'cpu'} sortKey
|
||
* @param {boolean} asc
|
||
* @param {{ tertiaryPpid?: boolean, sortWallMs?: number }} [opts]
|
||
*/
|
||
function bareTopSortProcessRows(rows, sortKey, asc, opts) {
|
||
const o = opts && typeof opts === 'object' ? opts : {}
|
||
const tertiaryPpid = o.tertiaryPpid === true
|
||
const wallMs = Number(o.sortWallMs) || 0
|
||
const dir = asc ? 1 : -1
|
||
const out = rows.slice()
|
||
out.sort((a, b) => {
|
||
const ka = bareTopProcessSortKey(a, sortKey, wallMs)
|
||
const kb = bareTopProcessSortKey(b, sortKey, wallMs)
|
||
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
|
||
if (tertiaryPpid) {
|
||
const pa = Number(a.ppid) || 0
|
||
const pb = Number(b.ppid) || 0
|
||
if (pa !== pb) return (pa - pb) * 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, Math.round(Number(score) || 0)))
|
||
// Floor fill so the bar never reads a higher % than the integer score (round caused ~73% for 72/100).
|
||
const fill = Math.min(w, Math.floor((p * w) / 100))
|
||
const full = ascii ? '#' : '\u2588'
|
||
const empty = ascii ? '.' : '\u2591'
|
||
let bar = ''
|
||
for (let i = 0; i < w; i++) bar += i < fill ? full : empty
|
||
return String(p) + '/100 ' + bar
|
||
}
|
||
|
||
/**
|
||
* @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))
|
||
)
|
||
}
|
||
|
||
/**
|
||
* Strip C0 controls except tab; keep printable + UTF-8 continuation for display safety.
|
||
* @param {string} s
|
||
*/
|
||
function bareTopSanitizeVisible(s) {
|
||
let t = String(s)
|
||
let o = ''
|
||
for (let i = 0; i < t.length; i++) {
|
||
const c = t.charCodeAt(i)
|
||
if (c === 9 || c === 10 || c === 13) o += t[i]
|
||
else if (c < 32) o += ' '
|
||
else o += t[i]
|
||
}
|
||
return o
|
||
}
|
||
|
||
/**
|
||
* @param {number} scrollTop
|
||
* @param {number} vis
|
||
* @param {string[]} wrapped
|
||
* @returns {string[]} visible slice (copy)
|
||
*/
|
||
function bareTopScrollSliceLines(wrapped, scrollTop, vis) {
|
||
const v = Math.max(1, vis | 0)
|
||
const s = Math.max(0, scrollTop | 0)
|
||
return wrapped.slice(s, s + v)
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} row
|
||
* @returns {number}
|
||
*/
|
||
function bareTopProcessStartedMs(row) {
|
||
if (!row || typeof row !== 'object') return 0
|
||
const n = Number(/** @type {Record<string, unknown>} */ (row).startedAtMs)
|
||
return Number.isFinite(n) ? n : 0
|
||
}
|
||
|
||
/**
|
||
* @param {number} startedAtMs
|
||
* @param {number} nowMs
|
||
*/
|
||
function bareTopPad2(n) {
|
||
const x = n | 0
|
||
return x < 10 ? '0' + x : String(x)
|
||
}
|
||
|
||
function bareTopFormatProcAge(startedAtMs, nowMs) {
|
||
const t0 = Number(startedAtMs)
|
||
const now = Number(nowMs)
|
||
if (!Number.isFinite(t0) || t0 <= 0 || !Number.isFinite(now)) return ''
|
||
let sec = Math.max(0, Math.floor((now - t0) / 1000))
|
||
if (sec < 3600) {
|
||
const m = Math.floor(sec / 60)
|
||
const s = sec % 60
|
||
return m > 0 ? m + ':' + bareTopPad2(s) : String(s) + 's'
|
||
}
|
||
const h = Math.floor(sec / 3600)
|
||
sec %= 3600
|
||
const m = Math.floor(sec / 60)
|
||
const s = sec % 60
|
||
return h + ':' + bareTopPad2(m) + ':' + bareTopPad2(s)
|
||
}
|
||
|
||
/**
|
||
* Tree order: depth-first by ppid, stable by pid.
|
||
* @param {Record<string, unknown>[]} rows
|
||
*/
|
||
function bareTopProcessTreeOrder(rows) {
|
||
const byPid = new Map()
|
||
for (const r of rows) {
|
||
const p = bareTopProcessPid(r)
|
||
if (p) byPid.set(p, r)
|
||
}
|
||
const children = new Map()
|
||
for (const r of rows) {
|
||
const ppid = Number(r.ppid) || 0
|
||
if (!children.has(ppid)) children.set(ppid, [])
|
||
children.get(ppid).push(r)
|
||
}
|
||
for (const arr of children.values()) {
|
||
arr.sort((a, b) => bareTopProcessPid(a) - bareTopProcessPid(b))
|
||
}
|
||
/** @type {Record<string, unknown>[]} */
|
||
const out = []
|
||
function walk(pid, depth) {
|
||
const ch = children.get(pid)
|
||
if (!ch) return
|
||
for (const r of ch) {
|
||
out.push(Object.assign({}, r, { _treeDepth: depth }))
|
||
walk(bareTopProcessPid(r), depth + 1)
|
||
}
|
||
}
|
||
walk(0, 0)
|
||
for (const r of rows) {
|
||
const p = bareTopProcessPid(r)
|
||
const pp = Number(r.ppid) || 0
|
||
if (!byPid.has(pp) && pp === 0 && p) {
|
||
/* orphan roots not reached */
|
||
}
|
||
}
|
||
if (out.length === 0) return rows.map((r) => Object.assign({}, r, { _treeDepth: 0 }))
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {string} title
|
||
* @param {number} cols
|
||
* @param {boolean} ascii
|
||
*/
|
||
function bareTopSectionSeparator(title, cols, ascii) {
|
||
const rule = bareTopSectionRule(title, cols)
|
||
if (ascii) return '- ' + String(title || '') + ' ' + '-'.repeat(Math.max(4, cols - title.length - 4))
|
||
return rule
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} o
|
||
* @param {number} maxRows
|
||
* @param {number} keyW
|
||
*/
|
||
function bareTopDelegateInflightTableLines(o, maxRows, keyW) {
|
||
if (!o || typeof o !== 'object') return []
|
||
const rec = /** @type {Record<string, unknown>} */ (o)
|
||
const pairs = Object.keys(rec)
|
||
.map((k) => ({ k, v: Number(rec[k]) || 0 }))
|
||
.sort((a, b) => b.v - a.v || a.k.localeCompare(b.k))
|
||
const lim = Math.min(maxRows, pairs.length)
|
||
/** @type {string[]} */
|
||
const out = []
|
||
const kw = Math.max(6, keyW | 0)
|
||
const vw = 8
|
||
for (let i = 0; i < lim; i++) {
|
||
const { k, v } = pairs[i]
|
||
const ks = bareTopTruncateCell(k, kw, true)
|
||
const padK = ' '.repeat(Math.max(0, kw - ks.length))
|
||
const vs = String(v).padStart(vw, ' ')
|
||
out.push(' ' + ks + padK + ' ' + vs)
|
||
}
|
||
if (pairs.length > lim) out.push(' … +' + (pairs.length - lim) + ' more keys')
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} o
|
||
* @param {number} cols
|
||
*/
|
||
/**
|
||
* @param {Record<string, unknown>} snap
|
||
*/
|
||
function bareTopLimitsMergedLines(snap) {
|
||
/** @type {string[]} */
|
||
const lines = []
|
||
const q =
|
||
snap &&
|
||
snap.extra &&
|
||
snap.extra.quotas &&
|
||
typeof snap.extra.quotas === 'object'
|
||
? /** @type {Record<string, unknown>} */ (snap.extra.quotas)
|
||
: null
|
||
const r =
|
||
snap &&
|
||
snap.extra &&
|
||
snap.extra.rlimits &&
|
||
typeof snap.extra.rlimits === 'object'
|
||
? /** @type {Record<string, unknown>} */ (snap.extra.rlimits)
|
||
: null
|
||
function row(prefix, o2, max) {
|
||
const keys = Object.keys(o2).slice(0, max)
|
||
for (const k of keys) {
|
||
const v = o2[k]
|
||
const s =
|
||
v != null && typeof v !== 'object'
|
||
? String(v)
|
||
: v && typeof v === 'object'
|
||
? '{…}'
|
||
: ''
|
||
lines.push(prefix + k + '=' + bareTopTruncateCell(s, 56, false))
|
||
}
|
||
}
|
||
if (q) {
|
||
lines.push(' quotas (operator caps — full detail on catalog tab)')
|
||
row(' ', q, 8)
|
||
}
|
||
if (r) {
|
||
lines.push(' rlimits (process limits mirror)')
|
||
row(' ', r, 8)
|
||
}
|
||
return lines
|
||
}
|
||
|
||
/**
|
||
* Human-readable key labels (matches baretop-tui bareTopHumanKey).
|
||
* @param {string} k
|
||
*/
|
||
function bareTopUiHumanKey(k) {
|
||
return String(k || '')
|
||
.replace(/([A-Z])/g, ' $1')
|
||
.replace(/_/g, ' ')
|
||
.replace(/\s+/g, ' ')
|
||
.trim()
|
||
.toLowerCase()
|
||
.replace(/^\w/, (c) => c.toUpperCase())
|
||
}
|
||
|
||
/**
|
||
* Flatten nested values to lines (no JSON); uses N/A for nulls.
|
||
* @param {string} prefix
|
||
* @param {unknown} v
|
||
* @param {number} depth
|
||
* @param {number} maxD
|
||
* @param {string[]} lines
|
||
* @param {number} maxKeys
|
||
*/
|
||
function bareTopUiFlatten(prefix, v, depth, maxD, lines, maxKeys) {
|
||
const na = 'N/A'
|
||
const pad = ' '.repeat(depth)
|
||
if (depth > maxD) {
|
||
lines.push(pad + (prefix ? bareTopUiHumanKey(prefix) + ': ' : '') + '\u2026')
|
||
return
|
||
}
|
||
if (v == null) {
|
||
lines.push(pad + (prefix ? bareTopUiHumanKey(prefix) + ': ' : '') + 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 ? bareTopUiHumanKey(prefix) + ': ' : '') + s)
|
||
return
|
||
}
|
||
if (Array.isArray(v)) {
|
||
const label = prefix ? bareTopUiHumanKey(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)
|
||
bareTopUiFlatten('', it, depth + 2, maxD, lines, maxKeys)
|
||
} else if (Array.isArray(it)) {
|
||
lines.push(pad + ' #' + i)
|
||
bareTopUiFlatten('', it, depth + 2, maxD, lines, maxKeys)
|
||
} else {
|
||
lines.push(
|
||
pad + ' #' + i + ': ' + (it == null ? na : String(it))
|
||
)
|
||
}
|
||
}
|
||
if (v.length > lim) lines.push(pad + ' \u2026 +' + (v.length - lim) + ' more')
|
||
return
|
||
}
|
||
if (t === 'object') {
|
||
const o = /** @type {Record<string, unknown>} */ (v)
|
||
const keys = Object.keys(o)
|
||
if (keys.length === 0) {
|
||
lines.push(pad + (prefix ? bareTopUiHumanKey(prefix) + ': ' : '') + '(empty)')
|
||
return
|
||
}
|
||
const slice = keys.slice(0, maxKeys)
|
||
if (prefix) {
|
||
lines.push(pad + bareTopUiHumanKey(prefix) + ':')
|
||
for (const k of slice) bareTopUiFlatten(k, o[k], depth + 1, maxD, lines, maxKeys)
|
||
} else {
|
||
for (const k of slice) bareTopUiFlatten(k, o[k], depth, maxD, lines, maxKeys)
|
||
}
|
||
if (keys.length > maxKeys)
|
||
lines.push(pad + '\u2026 +' + (keys.length - maxKeys) + ' keys')
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {string} prefix
|
||
* @param {unknown} v
|
||
* @param {number} depth
|
||
* @param {number} maxD
|
||
* @param {number} maxKeys
|
||
* @param {number} maxLines
|
||
*/
|
||
function bareTopUiFlattenLimited(prefix, v, depth, maxD, maxKeys, maxLines) {
|
||
const lines = []
|
||
bareTopUiFlatten(prefix, v, depth, maxD, lines, maxKeys)
|
||
const cap = Math.max(4, maxLines | 0)
|
||
if (lines.length > cap) {
|
||
return lines.slice(0, cap - 1).concat([' … (truncated)'])
|
||
}
|
||
return lines
|
||
}
|
||
|
||
/**
|
||
* One-line-ish summary for nested objects (no JSON braces in the TUI).
|
||
* @param {unknown} v
|
||
* @param {number} maxChars
|
||
*/
|
||
function bareTopFormatNestedBrief(v, maxChars) {
|
||
const m = Math.max(8, maxChars | 0)
|
||
if (v == null) return ''
|
||
const t = typeof v
|
||
if (t === 'string' || t === 'number' || t === 'boolean')
|
||
return bareTopTruncateCell(String(v), m, false)
|
||
if (t === 'bigint') return String(v)
|
||
if (t !== 'object') return bareTopTruncateCell(String(v), m, false)
|
||
const lines = bareTopUiFlattenLimited('', v, 0, 6, 80, 24)
|
||
const s = lines.join(' · ')
|
||
if (s.length <= m) return s
|
||
return s.slice(0, Math.max(4, m - 18)) + '\u2026(truncated)'
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} v
|
||
* @param {number} maxChars
|
||
*/
|
||
function bareTopJsonSnippet(v, maxChars) {
|
||
const m = Math.max(8, maxChars | 0)
|
||
try {
|
||
if (v != null && typeof v === 'object')
|
||
return bareTopFormatNestedBrief(v, m)
|
||
if (typeof v === 'bigint') return String(v)
|
||
const s = JSON.stringify(v)
|
||
if (s.length <= m) return s
|
||
return s.slice(0, Math.max(4, m - 18)) + '\u2026(truncated)'
|
||
} catch {
|
||
return '(unserializable)'
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} v
|
||
* @param {number} maxLen
|
||
*/
|
||
function bareTopFormatScalarTerminal(v, maxLen) {
|
||
const m = Math.max(4, maxLen | 0)
|
||
if (v == null) return ''
|
||
const t = typeof v
|
||
if (t === 'string' || t === 'number' || t === 'boolean' || t === 'bigint')
|
||
return bareTopTruncateCell(String(v), m, false)
|
||
if (t === 'object') return bareTopJsonSnippet(v, m)
|
||
return bareTopTruncateCell(String(v), m, false)
|
||
}
|
||
|
||
/**
|
||
* Format integers with grouped thousands (en-US style).
|
||
* @param {unknown} n
|
||
*/
|
||
function bareTopFormatIntGrouped(n) {
|
||
const x = Number(n)
|
||
if (!Number.isFinite(x)) return String(n)
|
||
const neg = x < 0
|
||
const i = Math.floor(Math.abs(x))
|
||
const s = String(i)
|
||
const g = []
|
||
for (let k = s.length; k > 0; k -= 3) g.push(s.slice(Math.max(0, k - 3), k))
|
||
return (neg ? '-' : '') + g.reverse().join(',')
|
||
}
|
||
|
||
/**
|
||
* Human duration for millisecond-ish counters (shared UI).
|
||
* @param {unknown} ms
|
||
*/
|
||
function bareTopFormatDurationMs(ms) {
|
||
const t = Number(ms)
|
||
if (!Number.isFinite(t) || t < 0) return ''
|
||
if (t < 1000) return Math.round(t) + 'ms'
|
||
if (t < 60000) return (t / 1000).toFixed(1) + 's'
|
||
if (t < 3600000) return Math.floor(t / 60000) + 'm'
|
||
return Math.floor(t / 3600000) + 'h'
|
||
}
|
||
|
||
/**
|
||
* Rx/tx bytes: use global bareTopFormatBytes from baretop-snapshot preamble when bundled.
|
||
* @param {unknown} n
|
||
*/
|
||
function bareTopNetFormatBytes(n) {
|
||
if (typeof bareTopFormatBytes === 'function') return bareTopFormatBytes(n)
|
||
const x = Number(n)
|
||
if (!Number.isFinite(x) || x < 0) return '\u2014'
|
||
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 {Record<string, unknown>} o
|
||
*/
|
||
function bareTopNetOrderedTopKeys(o) {
|
||
const priority = [
|
||
'schema',
|
||
'schemaVersion',
|
||
'topicHex',
|
||
'peerCount',
|
||
'replicationQueueDepth',
|
||
'peerFirewallAcceptedTotal',
|
||
'peerFirewallRejectedTotal',
|
||
'peerFirewallInboundTotal',
|
||
'peerFirewallOutboundTotal',
|
||
'seedHandshakeError',
|
||
'seedRole',
|
||
'atMs'
|
||
]
|
||
const seen = new Set()
|
||
/** @type {string[]} */
|
||
const out = []
|
||
for (const k of priority) {
|
||
if (Object.prototype.hasOwnProperty.call(o, k) && o[k] != null) {
|
||
out.push(k)
|
||
seen.add(k)
|
||
}
|
||
}
|
||
const rest = Object.keys(o)
|
||
.filter((k) => !seen.has(k))
|
||
.sort((a, b) => a.localeCompare(b))
|
||
return out.concat(rest)
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} q
|
||
* @param {number} cols
|
||
*/
|
||
function bareTopNetReplicationQueueLines(q, cols) {
|
||
if (!q || typeof q !== 'object') return [' (null)']
|
||
const o = /** @type {Record<string, unknown>} */ (q)
|
||
/** @type {string[]} */
|
||
const lines = []
|
||
const scalars = [
|
||
'depth',
|
||
'pending',
|
||
'pendingCount',
|
||
'length',
|
||
'size',
|
||
'backlog',
|
||
'queued',
|
||
'inFlight',
|
||
'lastError',
|
||
'error',
|
||
'note',
|
||
'atMs'
|
||
]
|
||
for (const k of scalars) {
|
||
if (!Object.prototype.hasOwnProperty.call(o, k)) continue
|
||
const v = o[k]
|
||
if (v != null && typeof v !== 'object')
|
||
lines.push(' ' + k + ': ' + String(v))
|
||
}
|
||
for (const ak of ['items', 'queue', 'entries', 'pendingJobs']) {
|
||
if (Array.isArray(o[ak]))
|
||
lines.push(' ' + ak + ': ' + o[ak].length + ' entries')
|
||
}
|
||
const nestedKeys = Object.keys(o).filter((k) => {
|
||
const v = o[k]
|
||
return v != null && typeof v === 'object'
|
||
})
|
||
for (const nk of nestedKeys.slice(0, 6)) {
|
||
lines.push(' ' + nk + ': ' + bareTopJsonSnippet(o[nk], Math.max(24, cols - 14)))
|
||
}
|
||
if (!lines.length)
|
||
lines.push(' ' + bareTopJsonSnippet(o, Math.max(40, cols - 6)))
|
||
return lines
|
||
}
|
||
|
||
/**
|
||
* peer_firewall_stats / channel.js firewall shape: acceptedSessionCount, rejectedSessionCount, etc.
|
||
* @param {unknown} fw
|
||
* @param {number} cols
|
||
*/
|
||
function bareTopNetPeerFirewallLines(fw, cols) {
|
||
if (!fw || typeof fw !== 'object') return [' (null)']
|
||
const o = /** @type {Record<string, unknown>} */ (fw)
|
||
/** @type {string[]} */
|
||
const lines = []
|
||
const counts = [
|
||
'acceptedSessionCount',
|
||
'rejectedSessionCount',
|
||
'inboundSessionCount',
|
||
'outboundSessionCount'
|
||
]
|
||
for (const k of counts) {
|
||
if (typeof o[k] === 'number') lines.push(' ' + k + ': ' + bareTopFormatIntGrouped(o[k]))
|
||
}
|
||
if (typeof o.error === 'string' && o.error.trim())
|
||
lines.push(' error: ' + bareTopTruncateCell(o.error, cols - 8, true))
|
||
if (typeof o.note === 'string' && o.note.trim())
|
||
lines.push(' note: ' + bareTopTruncateCell(o.note, cols - 8, true))
|
||
for (const extra of ['transportBreakdown', 'saturationClass', 'wave9', 'wave10']) {
|
||
if (o[extra] != null && typeof o[extra] === 'object')
|
||
lines.push(
|
||
' ' +
|
||
extra +
|
||
': ' +
|
||
bareTopJsonSnippet(o[extra], Math.max(32, cols - 14))
|
||
)
|
||
}
|
||
if (!lines.length)
|
||
lines.push(' ' + bareTopJsonSnippet(o, Math.max(40, cols - 6)))
|
||
return lines
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} b
|
||
* @param {number} cols
|
||
*/
|
||
function bareTopNetBsdBridgeLines(b, cols) {
|
||
if (!b || typeof b !== 'object') return [' (null)']
|
||
const o = /** @type {Record<string, unknown>} */ (b)
|
||
/** @type {string[]} */
|
||
const lines = []
|
||
for (const k of [
|
||
'schema',
|
||
'policyEnv',
|
||
'activeBridgedFds',
|
||
'bridgeErrors',
|
||
'guestSocketCount'
|
||
]) {
|
||
if (!Object.prototype.hasOwnProperty.call(o, k)) continue
|
||
const v = o[k]
|
||
if (v != null && typeof v !== 'object')
|
||
lines.push(' ' + k + ': ' + String(v))
|
||
}
|
||
if (typeof o.note === 'string' && o.note.trim())
|
||
lines.push(
|
||
' note: ' + bareTopTruncateCell(String(o.note), Math.max(20, cols - 10), true)
|
||
)
|
||
const rest = Object.keys(o).filter(
|
||
(k) => !['schema', 'policyEnv', 'note', 'activeBridgedFds', 'bridgeErrors', 'guestSocketCount'].includes(k)
|
||
)
|
||
for (const k of rest.slice(0, 8)) {
|
||
const v = o[k]
|
||
lines.push(
|
||
' ' +
|
||
k +
|
||
': ' +
|
||
bareTopFormatScalarTerminal(v, Math.max(16, cols - 12))
|
||
)
|
||
}
|
||
return lines.length ? lines : [' (empty)']
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} net
|
||
* @param {unknown} metricsLive
|
||
*/
|
||
function bareTopNetHealthOverviewLines(net, metricsLive) {
|
||
/** @type {string[]} */
|
||
const parts = []
|
||
const peersMl =
|
||
metricsLive && typeof metricsLive === 'object'
|
||
? Number(/** @type {Record<string, unknown>} */ (metricsLive).peers)
|
||
: NaN
|
||
if (Number.isFinite(peersMl)) parts.push('peers_ml=' + bareTopFormatIntGrouped(peersMl))
|
||
if (net && typeof net === 'object') {
|
||
const o = /** @type {Record<string, unknown>} */ (net)
|
||
if (o.topicHex != null) parts.push('topic=' + String(o.topicHex).slice(0, 16))
|
||
if (typeof o.peerCount === 'number')
|
||
parts.push('peers_net=' + bareTopFormatIntGrouped(o.peerCount))
|
||
const err = o.seedHandshakeError
|
||
if (err != null && String(err).trim())
|
||
parts.push('seedErr=' + bareTopTruncateCell(String(err), 36, true))
|
||
const rd = o.replicationQueueDepth
|
||
if (typeof rd === 'number') parts.push('replQ=' + bareTopFormatIntGrouped(rd))
|
||
}
|
||
if (!parts.length) return []
|
||
return [' net ' + parts.join(' ') + ' (tab: network)']
|
||
}
|
||
|
||
/**
|
||
* Consolidated HDMS + DHT line for overview (traffic-light style).
|
||
* @param {Record<string, unknown>} snap
|
||
*/
|
||
function bareTopHdmsDhtTrafficLine(snap) {
|
||
const hd =
|
||
snap &&
|
||
snap.extra &&
|
||
snap.extra.hdmsHealth &&
|
||
typeof snap.extra.hdmsHealth === 'object'
|
||
? /** @type {Record<string, unknown>} */ (snap.extra.hdmsHealth)
|
||
: null
|
||
const dh =
|
||
snap &&
|
||
snap.extra &&
|
||
snap.extra.dhtStatus &&
|
||
typeof snap.extra.dhtStatus === 'object'
|
||
? /** @type {Record<string, unknown>} */ (snap.extra.dhtStatus)
|
||
: null
|
||
if (!hd && !dh) return ''
|
||
function grade(o) {
|
||
if (!o) return '?'
|
||
const e = o.error ?? o.err
|
||
if (e != null && String(e).trim()) return 'R'
|
||
const ok = o.ok ?? o.healthy ?? o.ready
|
||
if (ok === false) return 'R'
|
||
if (ok === true) return 'G'
|
||
return 'Y'
|
||
}
|
||
const bits = []
|
||
if (hd) {
|
||
bits.push('hdms[' + grade(hd) + ']=' + bareTopP2PKvString(hd, 5))
|
||
}
|
||
if (dh) {
|
||
bits.push('dht[' + grade(dh) + ']=' + bareTopP2PKvString(dh, 5))
|
||
}
|
||
return ' p2p ' + bits.join(' ')
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} o
|
||
* @param {number} maxKeys
|
||
*/
|
||
function bareTopP2PKvString(o, maxKeys) {
|
||
return Object.keys(o)
|
||
.slice(0, maxKeys)
|
||
.map((k) => {
|
||
const v = o[k]
|
||
return k + '=' + (v != null && typeof v !== 'object' ? String(v) : bareTopJsonSnippet(v, 32))
|
||
})
|
||
.join(' ')
|
||
}
|
||
|
||
function bareTopP2PStackLines(snap) {
|
||
/** @type {string[]} */
|
||
const out = []
|
||
const traffic = bareTopHdmsDhtTrafficLine(/** @type {Record<string, unknown>} */ (snap))
|
||
if (traffic) {
|
||
out.push(traffic)
|
||
return out
|
||
}
|
||
const hd =
|
||
snap &&
|
||
snap.extra &&
|
||
snap.extra.hdmsHealth &&
|
||
typeof snap.extra.hdmsHealth === 'object'
|
||
? /** @type {Record<string, unknown>} */ (snap.extra.hdmsHealth)
|
||
: null
|
||
const dh =
|
||
snap &&
|
||
snap.extra &&
|
||
snap.extra.dhtStatus &&
|
||
typeof snap.extra.dhtStatus === 'object'
|
||
? /** @type {Record<string, unknown>} */ (snap.extra.dhtStatus)
|
||
: null
|
||
if (hd)
|
||
out.push(' hdms ' + bareTopTruncateCell(bareTopP2PKvString(hd, 8), 100, false))
|
||
if (dh)
|
||
out.push(' dht ' + bareTopTruncateCell(bareTopP2PKvString(dh, 8), 100, false))
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @typedef {{ maxLines?: number, filter?: string, wideTwoCol?: boolean }} BareTopNetTabOpts
|
||
* @param {unknown} net
|
||
* @param {number} cols
|
||
* @param {BareTopNetTabOpts | null} [opts]
|
||
*/
|
||
function bareTopNetTabLines(net, cols, opts) {
|
||
const oopts = opts && typeof opts === 'object' ? opts : {}
|
||
const maxTotal = Math.max(8, (oopts.maxLines | 0) || 512)
|
||
const filt = String(oopts.filter || '')
|
||
.trim()
|
||
.toLowerCase()
|
||
const wide = oopts.wideTwoCol === true && cols >= 100
|
||
if (!net || typeof net !== 'object') return [' N/A']
|
||
const o = /** @type {Record<string, unknown>} */ (net)
|
||
/** @type {string[]} */
|
||
const lines = []
|
||
|
||
function pushFiltered(s) {
|
||
if (filt && !String(s).toLowerCase().includes(filt)) return
|
||
lines.push(s)
|
||
}
|
||
|
||
function pushSection(title) {
|
||
pushFiltered('')
|
||
pushFiltered(' --- ' + title + ' ---')
|
||
}
|
||
|
||
if (Array.isArray(o.interfaces)) {
|
||
pushSection('interfaces (rx+tx)')
|
||
const arr = o.interfaces.slice().sort((a, b) => {
|
||
const ta =
|
||
a && typeof a === 'object'
|
||
? /** @type {Record<string, unknown>} */ (a)
|
||
: null
|
||
const tb =
|
||
b && typeof b === 'object'
|
||
? /** @type {Record<string, unknown>} */ (b)
|
||
: null
|
||
const sa =
|
||
(Number(ta && ta.rxBytes) || 0) + (Number(ta && ta.txBytes) || 0)
|
||
const sb =
|
||
(Number(tb && tb.rxBytes) || 0) + (Number(tb && tb.txBytes) || 0)
|
||
return sb - sa
|
||
})
|
||
const w = Math.max(40, cols - 4)
|
||
for (const iface of arr.slice(0, 32)) {
|
||
if (!iface || typeof iface !== 'object') continue
|
||
const i = /** @type {Record<string, unknown>} */ (iface)
|
||
const nm = String(i.name ?? i.if ?? '?')
|
||
const rxB = i.rxBytes ?? i.rx
|
||
const txB = i.txBytes ?? i.tx
|
||
const rxS =
|
||
rxB != null && rxB !== '' && Number.isFinite(Number(rxB))
|
||
? bareTopNetFormatBytes(rxB)
|
||
: String(rxB ?? '\u2014')
|
||
const txS =
|
||
txB != null && txB !== '' && Number.isFinite(Number(txB))
|
||
? bareTopNetFormatBytes(txB)
|
||
: String(txB ?? '\u2014')
|
||
const extra = Object.keys(i)
|
||
.filter((k) => !['name', 'if', 'rxBytes', 'txBytes', 'rx', 'tx'].includes(k))
|
||
.slice(0, 4)
|
||
.map((k) => {
|
||
const v = i[k]
|
||
return (
|
||
k +
|
||
'=' +
|
||
(v != null && typeof v !== 'object'
|
||
? String(v)
|
||
: bareTopJsonSnippet(v, 36))
|
||
)
|
||
})
|
||
.join(' ')
|
||
pushFiltered(
|
||
' ' +
|
||
bareTopTruncateCell(nm, 16, true) +
|
||
' rx=' +
|
||
rxS +
|
||
' tx=' +
|
||
txS +
|
||
' ' +
|
||
bareTopTruncateCell(extra, w - 24, false)
|
||
)
|
||
}
|
||
}
|
||
|
||
const sectionKeys = new Set([
|
||
'interfaces',
|
||
'replicationQueue',
|
||
'peerFirewallStats',
|
||
'bsdSocketGuestBridge',
|
||
'transport',
|
||
'udxTuning',
|
||
'hyperswarmTuning',
|
||
'peerFirewallE2e',
|
||
'stagingSlot',
|
||
'snapshotHints'
|
||
])
|
||
|
||
pushSection('summary')
|
||
const ordered = bareTopNetOrderedTopKeys(o)
|
||
for (const k of ordered) {
|
||
if (sectionKeys.has(k)) continue
|
||
const v = o[k]
|
||
if (v != null && typeof v === 'object') continue
|
||
pushFiltered(
|
||
' ' +
|
||
k +
|
||
': ' +
|
||
bareTopTruncateCell(bareTopFormatScalarTerminal(v, Math.max(24, cols - 8)), Math.max(20, cols - 6), false)
|
||
)
|
||
}
|
||
|
||
if (o.replicationQueue != null) {
|
||
pushSection('replicationQueue')
|
||
for (const ln of bareTopNetReplicationQueueLines(o.replicationQueue, cols)) pushFiltered(ln)
|
||
}
|
||
if (o.peerFirewallStats != null) {
|
||
pushSection('peerFirewallStats')
|
||
for (const ln of bareTopNetPeerFirewallLines(o.peerFirewallStats, cols)) pushFiltered(ln)
|
||
}
|
||
if (o.bsdSocketGuestBridge != null) {
|
||
pushSection('bsdSocketGuestBridge')
|
||
for (const ln of bareTopNetBsdBridgeLines(o.bsdSocketGuestBridge, cols)) pushFiltered(ln)
|
||
}
|
||
|
||
const tun = ['transport', 'udxTuning', 'hyperswarmTuning', 'peerFirewallE2e']
|
||
for (const tk of tun) {
|
||
if (o[tk] == null) continue
|
||
pushSection(tk)
|
||
const v = o[tk]
|
||
if (v != null && typeof v === 'object') {
|
||
const jo = /** @type {Record<string, unknown>} */ (v)
|
||
const keys = Object.keys(jo).slice(0, 24)
|
||
for (const kk of keys) {
|
||
const vv = jo[kk]
|
||
pushFiltered(
|
||
' ' +
|
||
kk +
|
||
': ' +
|
||
bareTopFormatScalarTerminal(vv, Math.max(16, cols - 10))
|
||
)
|
||
}
|
||
} else pushFiltered(' ' + String(v))
|
||
}
|
||
|
||
for (const ek of ['stagingSlot', 'snapshotHints']) {
|
||
if (o[ek] == null) continue
|
||
pushSection(ek)
|
||
const v = o[ek]
|
||
if (v != null && typeof v === 'object') {
|
||
const jo = /** @type {Record<string, unknown>} */ (v)
|
||
for (const kk of Object.keys(jo).slice(0, 20)) {
|
||
const vv = jo[kk]
|
||
pushFiltered(
|
||
' ' +
|
||
kk +
|
||
': ' +
|
||
bareTopFormatScalarTerminal(vv, Math.max(12, cols - 12))
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
if (wide && lines.length > 6) {
|
||
/** @type {string[]} */
|
||
const merged = []
|
||
for (let i = 0; i < lines.length; i += 2) {
|
||
const a = lines[i] || ''
|
||
const b = lines[i + 1] || ''
|
||
const half = Math.floor(cols / 2) - 1
|
||
merged.push(
|
||
bareTopTruncateCell(a, half, false) +
|
||
' | ' +
|
||
bareTopTruncateCell(b, half, false)
|
||
)
|
||
}
|
||
lines.length = 0
|
||
lines.push(...merged)
|
||
}
|
||
|
||
if (lines.length > maxTotal) {
|
||
const head = lines.slice(0, maxTotal - 1)
|
||
head.push(' \u2026 (truncated, ' + (lines.length - maxTotal + 1) + ' more lines)')
|
||
return head
|
||
}
|
||
return lines.length ? lines : [' (empty net summary)']
|
||
}
|
||
|
||
function bareTopDelegateRateBucketLines(o, cols) {
|
||
if (!o || typeof o !== 'object') return []
|
||
const rec = /** @type {Record<string, unknown>} */ (o)
|
||
const keys = Object.keys(rec).sort()
|
||
if (!keys.length) return []
|
||
/** @type {string[]} */
|
||
const out = []
|
||
const w = Math.max(40, Math.min(cols - 2, cols))
|
||
for (const k of keys) {
|
||
const v = rec[k]
|
||
if (Array.isArray(v)) {
|
||
const nums = v.map((x) => String(x)).join(' ')
|
||
out.push(
|
||
' ' +
|
||
bareTopTruncateCell(k, Math.min(20, Math.floor(w * 0.35)), true).padEnd(20) +
|
||
' ' +
|
||
bareTopTruncateCell(nums, w - 24, false)
|
||
)
|
||
} else if (v != null && typeof v === 'object') {
|
||
out.push(
|
||
' ' +
|
||
k +
|
||
': ' +
|
||
bareTopJsonSnippet(v, Math.min(120, w - k.length - 4))
|
||
)
|
||
} else {
|
||
out.push(' ' + k + ': ' + String(v))
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* Kernel counter keys emitted by the booter (inventory for dashboards):
|
||
* initd.unit_failed_final, vfs.readfile.samples, vfs.replication_warm_full_invalidate,
|
||
* vfs.warm_read_cache_*, security.key_handle_*, audit.append*, operator.corestore_snapshot_hint,
|
||
* vfs.hyperblobs_dedup_* — see bareOsKernelMetricInc call sites in packages/bare-os-booter.
|
||
* @param {unknown} counters
|
||
* @param {unknown} prev
|
||
* @param {boolean} deltaMode
|
||
* @param {number} topN
|
||
* @param {number} colW
|
||
*/
|
||
function bareTopKernelCounterLines(counters, prev, deltaMode, topN, colW) {
|
||
if (!counters || typeof counters !== 'object') return []
|
||
const o = /** @type {Record<string, number>} */ (counters)
|
||
const po =
|
||
prev && typeof prev === 'object'
|
||
? /** @type {Record<string, number>} */ (prev)
|
||
: null
|
||
const pairs = Object.keys(o)
|
||
.map((k) => ({ k, v: Number(o[k]) || 0 }))
|
||
.sort((a, b) => b.v - a.v || a.k.localeCompare(b.k))
|
||
const lim = Math.min(Math.max(1, topN | 0), pairs.length)
|
||
/** @type {string[]} */
|
||
const out = []
|
||
const wn = Math.max(6, colW | 0)
|
||
for (let i = 0; i < lim; i++) {
|
||
const { k, v } = pairs[i]
|
||
let suf = ''
|
||
if (deltaMode && po && Object.prototype.hasOwnProperty.call(po, k)) {
|
||
const d = v - (Number(po[k]) || 0)
|
||
suf = ' (d ' + (d >= 0 ? '+' : '') + d + ')'
|
||
}
|
||
const kt = bareTopTruncateCell(k, wn - 1, true)
|
||
const kPad = kt + ' '.repeat(Math.max(0, wn - kt.length))
|
||
out.push(' ' + kPad + ' ' + String(v) + suf)
|
||
}
|
||
if (pairs.length > lim) out.push(' … +' + (pairs.length - lim) + ' counters')
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} w
|
||
*/
|
||
function bareTopWarmReadCacheSummaryLines(w) {
|
||
if (!w || typeof w !== 'object') return []
|
||
const o = /** @type {Record<string, unknown>} */ (w)
|
||
const parts = []
|
||
for (const key of ['entries', 'bytes', 'hits', 'misses', 'evictions']) {
|
||
if (o[key] != null) parts.push(key + '=' + String(o[key]))
|
||
}
|
||
if (!parts.length) {
|
||
const keys = Object.keys(o).slice(0, 8)
|
||
for (const k of keys) {
|
||
const v = o[k]
|
||
if (v != null && typeof v !== 'object') parts.push(k + '=' + String(v))
|
||
}
|
||
}
|
||
if (!parts.length) return []
|
||
return [' ' + parts.join(' ')]
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} ipc
|
||
* @param {number} maxLines
|
||
*/
|
||
function bareTopIpcTelemetrySummaryLines(ipc, maxLines) {
|
||
if (!ipc || typeof ipc !== 'object') return []
|
||
const o = /** @type {Record<string, unknown>} */ (ipc)
|
||
/** @type {string[]} */
|
||
const lines = []
|
||
for (const k of Object.keys(o).slice(0, 24)) {
|
||
const v = o[k]
|
||
if (v != null && typeof v !== 'object') {
|
||
lines.push(' ' + k + ': ' + String(v))
|
||
} else if (v && typeof v === 'object' && !Array.isArray(v)) {
|
||
const sub = /** @type {Record<string, unknown>} */ (v)
|
||
const inner = Object.keys(sub)
|
||
.slice(0, 6)
|
||
.map(
|
||
(sk) =>
|
||
sk +
|
||
'=' +
|
||
(sub[sk] != null && typeof sub[sk] !== 'object'
|
||
? String(sub[sk])
|
||
: bareTopJsonSnippet(sub[sk], 24))
|
||
)
|
||
.join(' ')
|
||
lines.push(' ' + k + ': ' + bareTopTruncateCell(inner, 72, true))
|
||
}
|
||
if (lines.length >= maxLines) break
|
||
}
|
||
if (lines.length >= maxLines && Object.keys(o).length > maxLines)
|
||
lines.push(' … (truncated)')
|
||
return lines
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} rep
|
||
* @param {string} na
|
||
*/
|
||
function bareTopReplicationOverviewLines(rep, na) {
|
||
if (!rep || typeof rep !== 'object') return []
|
||
const o = /** @type {Record<string, unknown>} */ (rep)
|
||
/** @type {string[]} */
|
||
const lines = []
|
||
const sys = o.systemCoreLength
|
||
const per = o.personalCoreLength
|
||
const aux = o.auxiliaryDriveCount
|
||
const pc = o.peerCount
|
||
if (typeof sys === 'number') lines.push(' sysCoreLen ' + sys)
|
||
if (typeof per === 'number') lines.push(' perCoreLen ' + per)
|
||
if (typeof aux === 'number') lines.push(' auxDrives ' + aux)
|
||
if (typeof pc === 'number') lines.push(' peerCount ' + pc)
|
||
const sh = o.stallHint
|
||
if (sh != null && String(sh).trim()) {
|
||
lines.push(' stallHint ' + String(sh))
|
||
const sl = String(sh).trim().toLowerCase()
|
||
if (sl === 'no_peers' || sl === 'length_unavailable' || sl === 'ok')
|
||
lines.push(
|
||
' (hint is operational — health score treats these as non-fault)'
|
||
)
|
||
}
|
||
if (!lines.length) lines.push(' ' + na)
|
||
return lines
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} cold
|
||
* @param {unknown} stdlib
|
||
* @param {unknown} tel
|
||
*/
|
||
function bareTopBootBudgetLines(cold, stdlib, tel) {
|
||
/** @type {string[]} */
|
||
const lines = []
|
||
const c = cold && typeof cold === 'object' ? /** @type {Record<string, unknown>} */ (cold) : null
|
||
const s = stdlib && typeof stdlib === 'object' ? /** @type {Record<string, unknown>} */ (stdlib) : null
|
||
if (c) {
|
||
const ex = c.exceeded === true
|
||
lines.push(
|
||
' coldBoot exceeded=' +
|
||
(ex ? 'yes' : 'no') +
|
||
' wall=' +
|
||
String(c.wallMs ?? '—') +
|
||
' limit=' +
|
||
String(c.limitMs ?? '—')
|
||
)
|
||
}
|
||
if (s) {
|
||
const ex = s.exceeded === true
|
||
lines.push(
|
||
' bareStdlib exceeded=' +
|
||
(ex ? 'yes' : 'no') +
|
||
' wall=' +
|
||
String(s.wallMs ?? '—') +
|
||
' limit=' +
|
||
String(s.limitMs ?? '—')
|
||
)
|
||
}
|
||
if (tel && typeof tel === 'object') {
|
||
const t = /** @type {Record<string, unknown>} */ (tel)
|
||
lines.push(
|
||
' policy strict=' +
|
||
String(!!t.strict) +
|
||
' bootPerf=' +
|
||
String(t.bootPerfJsonPath || '/run/bare-os/boot-perf.json')
|
||
)
|
||
}
|
||
return lines
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} sl
|
||
*/
|
||
function bareTopSwarmLifecycleLine(sl) {
|
||
if (!sl || typeof sl !== 'object') return ''
|
||
const o = /** @type {Record<string, unknown>} */ (sl)
|
||
const parts = []
|
||
for (const k of Object.keys(o).slice(0, 12)) {
|
||
const v = o[k]
|
||
if (v != null && typeof v !== 'object') parts.push(k + '=' + v)
|
||
}
|
||
if (!parts.length) return ''
|
||
return ' swarm ' + parts.join(' ') + ' (tab: operator)'
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} pt
|
||
*/
|
||
function bareTopProcessStateHistogramLines(pt) {
|
||
const rows = bareTopProcessRowsFromTable(pt)
|
||
/** @type {Record<string, number>} */
|
||
const h = {}
|
||
for (const r of rows) {
|
||
const st = String(r.state || '?')
|
||
h[st] = (h[st] || 0) + 1
|
||
}
|
||
const parts = Object.keys(h)
|
||
.sort()
|
||
.map((k) => k + ':' + h[k])
|
||
if (!parts.length) return []
|
||
return [' states ' + parts.join(' ')]
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} ir
|
||
*/
|
||
function bareTopInitdReadinessSummaryLines(ir) {
|
||
if (!ir || typeof ir !== 'object') return []
|
||
const o = /** @type {Record<string, unknown>} */ (ir)
|
||
const units = Array.isArray(o.units) ? o.units : []
|
||
let active = 0
|
||
let starting = 0
|
||
let failed = 0
|
||
let other = 0
|
||
for (const u of units) {
|
||
if (!u || typeof u !== 'object') continue
|
||
const ph = String(/** @type {Record<string, unknown>} */ (u).phase || '')
|
||
if (ph === 'active') active++
|
||
else if (ph === 'starting') starting++
|
||
else if (ph === 'failed') failed++
|
||
else other++
|
||
}
|
||
if (!units.length) return []
|
||
return [
|
||
' initd units=' +
|
||
units.length +
|
||
' active=' +
|
||
active +
|
||
' starting=' +
|
||
starting +
|
||
' failed=' +
|
||
failed +
|
||
(other ? ' other=' + other : '')
|
||
]
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} m metrics live
|
||
* @param {number} nowMs
|
||
* @param {string} na
|
||
*/
|
||
function bareTopMetaCoalesceLine(m, nowMs, na) {
|
||
if (!m || typeof m !== 'object') return ''
|
||
const o = /** @type {Record<string, unknown>} */ (m)
|
||
const co = o.coalesceMs
|
||
const at = o.atMs
|
||
let age = ''
|
||
if (typeof at === 'number' && Number.isFinite(at)) {
|
||
age = ' age=' + Math.max(0, Math.floor(nowMs - at)) + 'ms'
|
||
}
|
||
return (
|
||
' metrics coalesceMs=' +
|
||
(typeof co === 'number' ? co : na) +
|
||
' atMs=' +
|
||
(typeof at === 'number' ? at : na) +
|
||
age
|
||
)
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} wbh
|
||
*/
|
||
function bareTopWorkerBudgetCompactLine(wbh) {
|
||
if (!wbh || typeof wbh !== 'object') return ''
|
||
const o = /** @type {Record<string, unknown>} */ (wbh)
|
||
const parts = []
|
||
if (o.wallMsMax != null) parts.push('wallMax=' + String(o.wallMsMax))
|
||
if (o.cpuMsMax != null) parts.push('cpuMax=' + String(o.cpuMsMax))
|
||
if (!parts.length) return ''
|
||
return ' workerBudget ' + parts.join(' ')
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} cs collaboration session object
|
||
*/
|
||
function bareTopProtomuxCollabLine(cs) {
|
||
if (!cs || typeof cs !== 'object') return ''
|
||
const o = /** @type {Record<string, unknown>} */ (cs)
|
||
const rx = o.protomuxAppChannelRxTotal
|
||
const cap = o.protomuxCapChannelRxTotal
|
||
const parts = []
|
||
if (typeof rx === 'number') parts.push('appRx=' + rx)
|
||
if (typeof cap === 'number') parts.push('capRx=' + cap)
|
||
if (!parts.length) return ''
|
||
return ' protomux ' + parts.join(' ')
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} pt
|
||
*/
|
||
function bareTopLogicalFdAggregateLine(pt) {
|
||
const rows = bareTopProcessRowsFromTable(pt)
|
||
let n = 0
|
||
for (const r of rows) {
|
||
const ft = r.fdTable
|
||
if (Array.isArray(ft)) n += ft.length
|
||
}
|
||
if (!n) return ''
|
||
return ' logicalFds rows=' + n + ' (fdTable entries)'
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} hostStats
|
||
*/
|
||
function bareTopHostStatsSummaryLine(hostStats) {
|
||
if (!hostStats || typeof hostStats !== 'object') return ''
|
||
const o = /** @type {Record<string, unknown>} */ (hostStats)
|
||
const pick = [
|
||
'cpuBusyPct',
|
||
'cpuPct',
|
||
'memUsedPct',
|
||
'rssBytes',
|
||
'hostname',
|
||
'eventLoopLagMs',
|
||
'loopLagMs',
|
||
'lagMs'
|
||
]
|
||
const parts = []
|
||
for (const k of pick) {
|
||
if (o[k] != null && typeof o[k] !== 'object')
|
||
parts.push(k + '=' + String(o[k]))
|
||
}
|
||
if (!parts.length) return ''
|
||
return ' host ' + parts.join(' ') + ' (detail: host tab)'
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} resources
|
||
* @param {number} maxKeys
|
||
*/
|
||
function bareTopResourcesSummaryLines(resources, maxKeys) {
|
||
if (!resources || typeof resources !== 'object') return []
|
||
const o = /** @type {Record<string, unknown>} */ (resources)
|
||
const keys = Object.keys(o).slice(0, Math.max(1, maxKeys | 0))
|
||
/** @type {string[]} */
|
||
const lines = []
|
||
for (const k of keys) {
|
||
const v = o[k]
|
||
if (v != null && typeof v !== 'object')
|
||
lines.push(' ' + k + ': ' + String(v).slice(0, 120))
|
||
}
|
||
if (Object.keys(o).length > keys.length) lines.push(' … +' + (Object.keys(o).length - keys.length) + ' keys (full: diagnostics)')
|
||
return lines
|
||
}
|
||
|
||
/**
|
||
* @param {number} peersMl peers from metrics_live
|
||
* @param {unknown} repLive replicationLive object
|
||
*/
|
||
function bareTopPeerMismatchWarningLine(peersMl, repLive) {
|
||
if (!repLive || typeof repLive !== 'object') return ''
|
||
const o = /** @type {Record<string, unknown>} */ (repLive)
|
||
const pc = o.peerCount
|
||
if (typeof pc !== 'number' || !Number.isFinite(peersMl)) return ''
|
||
if (Math.abs(pc - peersMl) > 2)
|
||
return ' warn peerCount mismatch metrics peers=' + peersMl + ' replication.peerCount=' + pc
|
||
return ''
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} extraIpc from /proc/bare_os/ipc_backpressure.json
|
||
*/
|
||
function bareTopIpcBackpressureProcLine(extraIpc) {
|
||
if (!extraIpc || typeof extraIpc !== 'object') return ''
|
||
const o = /** @type {Record<string, unknown>} */ (extraIpc)
|
||
const parts = []
|
||
for (const k of Object.keys(o).slice(0, 14)) {
|
||
const v = o[k]
|
||
if (typeof v === 'number' && v !== 0)
|
||
parts.push(k + '=' + bareTopFormatIntGrouped(v))
|
||
else if (v === true) parts.push(k)
|
||
}
|
||
if (!parts.length) return ''
|
||
return ' ipcBackpressure(proc) ' + parts.join(' ')
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} syscallsParsed
|
||
*/
|
||
function bareTopSyscallsOverviewLines(syscallsParsed) {
|
||
if (!syscallsParsed || typeof syscallsParsed !== 'object') return []
|
||
const o = /** @type {Record<string, unknown>} */ (syscallsParsed)
|
||
const ops = o.ops
|
||
const n = Array.isArray(ops) ? ops.length : 0
|
||
return [
|
||
' syscalls schemaVersion=' +
|
||
String(o.schemaVersion ?? '?') +
|
||
' stockOps~' +
|
||
n
|
||
]
|
||
}
|
||
|
||
/**
|
||
* @param {string} diskRaw
|
||
*/
|
||
function bareTopDiskstatsOverviewLines(diskRaw) {
|
||
const s = String(diskRaw || '').trim()
|
||
if (!s) return []
|
||
const first = s.split('\n')[0] || ''
|
||
if (first.length < 6) return []
|
||
return [' diskstats ' + bareTopTruncateCell(first, 110, true)]
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} dr delegate_red.json
|
||
*/
|
||
function bareTopDelegateRedOverviewLines(dr) {
|
||
if (!dr || typeof dr !== 'object') return []
|
||
if (!Object.keys(dr).length) return []
|
||
return [
|
||
' delegate_red ' +
|
||
bareTopTruncateCell(bareTopJsonSnippet(dr, 220), 118, true)
|
||
]
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} sp security_posture.json
|
||
*/
|
||
function bareTopSecurityPostureOverviewLines(sp) {
|
||
if (!sp || typeof sp !== 'object') return []
|
||
const o = /** @type {Record<string, unknown>} */ (sp)
|
||
const parts = []
|
||
if (typeof o.schema === 'number') parts.push('schema=' + o.schema)
|
||
if (o.bootPolicyStrict != null) parts.push('bootStrict=' + String(o.bootPolicyStrict))
|
||
if (o.bootPolicyMerged != null) parts.push('bootPol=' + String(o.bootPolicyMerged))
|
||
if (o.ctxApiVersion != null) parts.push('ctxApi=' + String(o.ctxApiVersion).slice(0, 20))
|
||
return parts.length ? [' security_posture ' + parts.join(' ')] : []
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} bb boot_budget_summary.json
|
||
*/
|
||
function bareTopBootBudgetSummaryOverviewLines(bb) {
|
||
if (!bb || typeof bb !== 'object') return []
|
||
const o = /** @type {Record<string, unknown>} */ (bb)
|
||
const parts = []
|
||
const crit = o.criticalPathMs ?? o.criticalPathWallMs ?? o.totalWallMs
|
||
if (typeof crit === 'number') parts.push('critical~' + bareTopFormatDurationMs(crit))
|
||
const top = o.slowestUnit ?? o.criticalUnit ?? o.topUnit ?? o.bottleneck
|
||
if (top != null) parts.push('top=' + String(top).slice(0, 28))
|
||
if (o.exceeded === true) parts.push('EXCEEDED')
|
||
return parts.length ? [' boot_budget_summary ' + parts.join(' ')] : []
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} ph peer_health.json
|
||
*/
|
||
function bareTopWorstPeerOverviewLines(ph) {
|
||
if (!ph || typeof ph !== 'object') return []
|
||
const peers = /** @type {Record<string, unknown>} */ (ph).peers
|
||
if (!Array.isArray(peers) || !peers.length) return []
|
||
let worst = /** @type {Record<string, unknown> | null} */ (null)
|
||
let worstScore = -1
|
||
for (const p of peers) {
|
||
if (!p || typeof p !== 'object') continue
|
||
const r = /** @type {Record<string, unknown>} */ (p)
|
||
const err = r.error ?? r.lastError ?? r.status
|
||
const lat = Number(r.rttMs ?? r.latencyMs ?? r.rtt ?? 0)
|
||
const sc =
|
||
err != null && String(err).trim() ? 1e12 : Number.isFinite(lat) ? lat : 0
|
||
if (sc > worstScore) {
|
||
worstScore = sc
|
||
worst = r
|
||
}
|
||
}
|
||
if (!worst) return []
|
||
const id = String(
|
||
worst.publicKeyHex?.slice?.(0, 14) ||
|
||
worst.publicKey?.slice?.(0, 14) ||
|
||
worst.peerId ||
|
||
worst.id ||
|
||
'?'
|
||
)
|
||
const tail = bareTopJsonSnippet(worst, 72)
|
||
return [' worstPeer ' + id + ' ' + tail]
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} ext extensions.json
|
||
*/
|
||
function bareTopExtensionsOverviewLines(ext) {
|
||
if (!ext || typeof ext !== 'object') return []
|
||
const root = /** @type {Record<string, unknown>} */ (ext)
|
||
const list = root.extensions ?? root.names ?? root.list
|
||
const names = Array.isArray(list)
|
||
? list
|
||
.map((x) => {
|
||
if (x && typeof x === 'object')
|
||
return String(
|
||
/** @type {Record<string, unknown>} */ (x).name ??
|
||
/** @type {Record<string, unknown>} */ (x).id ??
|
||
''
|
||
)
|
||
return String(x)
|
||
})
|
||
.filter(Boolean)
|
||
: []
|
||
const head = names.slice(0, 5).join(', ')
|
||
return [
|
||
' extensions count=' +
|
||
names.length +
|
||
(head ? ' e.g. ' + bareTopTruncateCell(head, 56, true) : '')
|
||
]
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} cj capabilities.json
|
||
* @param {unknown} cn capabilities node JSON
|
||
*/
|
||
function bareTopCapabilitiesDiffOverviewLines(cj, cn) {
|
||
if (!cj || typeof cj !== 'object' || !cn || typeof cn !== 'object') return []
|
||
const a = /** @type {Record<string, unknown>} */ (cj)
|
||
const b = /** @type {Record<string, unknown>} */ (cn)
|
||
const ka = new Set(Object.keys(a))
|
||
const kb = new Set(Object.keys(b))
|
||
const onlyA = [...ka].filter((k) => !kb.has(k)).slice(0, 8)
|
||
const onlyB = [...kb].filter((k) => !ka.has(k)).slice(0, 8)
|
||
/** @type {string[]} */
|
||
const out = []
|
||
if (onlyA.length) out.push(' cap onlyInJson: ' + onlyA.join(', '))
|
||
if (onlyB.length) out.push(' cap onlyInNode: ' + onlyB.join(', '))
|
||
if (!out.length)
|
||
out.push(' cap seed JSON vs /capabilities: same top-level keys (sampled)')
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} gitDel
|
||
* @param {unknown} gitLfs
|
||
* @param {unknown} prevDel
|
||
* @param {unknown} prevLfs
|
||
*/
|
||
function bareTopGitStatsDeltaOverviewLines(gitDel, gitLfs, prevDel, prevLfs) {
|
||
function pickNum(o, keys) {
|
||
if (!o || typeof o !== 'object') return null
|
||
const r = /** @type {Record<string, unknown>} */ (o)
|
||
for (const k of keys) {
|
||
const v = r[k]
|
||
if (typeof v === 'number' && Number.isFinite(v)) return v
|
||
}
|
||
return null
|
||
}
|
||
/** @type {string[]} */
|
||
const out = []
|
||
const c1 = pickNum(gitDel, ['delegatesStarted', 'total', 'invocations'])
|
||
const p1 = pickNum(prevDel, ['delegatesStarted', 'total', 'invocations'])
|
||
if (c1 != null && p1 != null) out.push(' gitDelegate d=' + (c1 - p1))
|
||
const c2 = pickNum(gitLfs, ['pointersResolved', 'resolved', 'total'])
|
||
const p2 = pickNum(prevLfs, ['pointersResolved', 'resolved', 'total'])
|
||
if (c2 != null && p2 != null) out.push(' gitLfs d=' + (c2 - p2))
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} wb worker_budget.json
|
||
* @param {unknown} sb sandbox_profile.json
|
||
*/
|
||
function bareTopWorkerSandboxOverviewLines(wb, sb) {
|
||
const parts = []
|
||
if (wb && typeof wb === 'object') {
|
||
const o = /** @type {Record<string, unknown>} */ (wb)
|
||
const wm = Number(o.wallMsMax)
|
||
const wu = Number(o.wallMsUsed ?? o.wallMsTotal ?? o.usedWallMs)
|
||
if (Number.isFinite(wm) && wm > 0 && Number.isFinite(wu)) {
|
||
const pct = Math.min(100, Math.round((wu / wm) * 100))
|
||
if (pct >= 85) parts.push('workerBudget~' + pct + '%')
|
||
}
|
||
}
|
||
if (sb && typeof sb === 'object') {
|
||
const o = /** @type {Record<string, unknown>} */ (sb)
|
||
if (o.denied === true || o.violationCount === true || o.violations)
|
||
parts.push('sandbox:check')
|
||
}
|
||
return parts.length ? [' worker/sandbox ' + parts.join(' ')] : []
|
||
}
|
||
|
||
/** Default overview section ids in render order */
|
||
var BARE_TOP_OVERVIEW_DEFAULT_SECTIONS = [
|
||
'session',
|
||
'pipeline',
|
||
'delegates',
|
||
'replication',
|
||
'nethealth',
|
||
'kernel',
|
||
'warm',
|
||
'ipc',
|
||
'boot',
|
||
'swarm',
|
||
'prochist',
|
||
'protomux',
|
||
'meta',
|
||
'worker',
|
||
'initd',
|
||
'fdagg',
|
||
'delegate_rates',
|
||
'fairness',
|
||
'subprocess',
|
||
'host',
|
||
'limits',
|
||
'p2p',
|
||
'diskio',
|
||
'syscallsum',
|
||
'delegate_red',
|
||
'security',
|
||
'bootsum',
|
||
'badpeer',
|
||
'capdiff',
|
||
'extsum',
|
||
'gitcounters',
|
||
'resources',
|
||
'readerr'
|
||
]
|
||
|
||
/**
|
||
* @param {string} raw
|
||
* @param {boolean} compact
|
||
* @returns {Set<string> | null} null = use default list
|
||
*/
|
||
function bareTopOverviewSectionSet(raw, compact) {
|
||
const s = String(raw || '').trim()
|
||
if (!s) {
|
||
const base = BARE_TOP_OVERVIEW_DEFAULT_SECTIONS.slice()
|
||
if (compact) {
|
||
const drop = new Set([
|
||
'fairness',
|
||
'subprocess',
|
||
'host',
|
||
'resources',
|
||
'limits',
|
||
'p2p',
|
||
'diskio',
|
||
'syscallsum',
|
||
'security',
|
||
'bootsum',
|
||
'capdiff',
|
||
'gitcounters',
|
||
'extsum'
|
||
])
|
||
return new Set(base.filter((x) => !drop.has(x)))
|
||
}
|
||
return new Set(base)
|
||
}
|
||
const parts = s.split(/[,;]+/).map((x) => x.trim().toLowerCase()).filter(Boolean)
|
||
return new Set(parts)
|
||
}
|
||
|
||
/**
|
||
* @typedef {{
|
||
* cols: number,
|
||
* na: string,
|
||
* compact: boolean,
|
||
* sectionFilter: string,
|
||
* deltaMode: boolean,
|
||
* prevMetrics: Record<string, unknown> | null,
|
||
* nowMs: number,
|
||
* asciiSep: boolean,
|
||
* flattenCap: ((v: unknown, maxLines: number, maxKeys: number) => string[]) | null,
|
||
* ringProto: number[],
|
||
* protomuxSpark: boolean,
|
||
* sparkW: number,
|
||
* sparkAscii: boolean,
|
||
* logSpark: boolean,
|
||
* braille: boolean,
|
||
* healthDetail: boolean,
|
||
* healthBreakdown: string,
|
||
* splitLeftCol: number,
|
||
* splitMiniProc: boolean,
|
||
* layoutVersion: string,
|
||
* sectionsRaw?: string,
|
||
* sessionWallRing?: number[],
|
||
* prevSnap?: Record<string, unknown> | null
|
||
* }} BareTopOverviewOpts
|
||
*/
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} snap
|
||
* @param {BareTopOverviewOpts} opts
|
||
* @returns {{ lines: string[], activeSections: string[], sectionRawLineIndex: Record<string, number> }}
|
||
*/
|
||
function bareTopOverviewLines(snap, opts) {
|
||
const na = opts.na || 'N/A'
|
||
const cols = Math.max(40, opts.cols | 0)
|
||
const fc = opts.flattenCap
|
||
const sectionSet =
|
||
opts.sectionsRaw != null && String(opts.sectionsRaw).trim()
|
||
? bareTopOverviewSectionSet(String(opts.sectionsRaw), false)
|
||
: bareTopOverviewSectionSet('', opts.compact)
|
||
|
||
const filter = String(opts.sectionFilter || '').trim().toLowerCase()
|
||
/** @type {string[]} */
|
||
const lines = []
|
||
/** @type {string[]} */
|
||
const active = []
|
||
/** @type {Record<string, number>} */
|
||
const sectionRawLineIndex = {}
|
||
|
||
const m = snap.metricsLive
|
||
const sess =
|
||
m && typeof m.session === 'object' && m.session
|
||
? /** @type {Record<string, unknown>} */ (m.session)
|
||
: {}
|
||
|
||
function want(id) {
|
||
if (!sectionSet.has(id)) return false
|
||
if (filter && !id.includes(filter) && !bareTopSectionTitleFor(id).toLowerCase().includes(filter))
|
||
return false
|
||
return true
|
||
}
|
||
|
||
function pushSec(title, id, bodyLines) {
|
||
if (!want(id)) return
|
||
sectionRawLineIndex[id] = lines.length
|
||
active.push(id)
|
||
lines.push('')
|
||
lines.push(bareTopSectionSeparator(title, cols, opts.asciiSep))
|
||
for (const ln of bodyLines) lines.push(bareTopSanitizeVisible(ln))
|
||
}
|
||
|
||
const ec = Number(sess.execLineCount) || 0
|
||
const pb = Number(sess.pipelineBytesTotal) || 0
|
||
const wm = Number(sess.execLineWallMsTotal) || 0
|
||
|
||
if (want('session')) {
|
||
sectionRawLineIndex.session = lines.length
|
||
active.push('session')
|
||
lines.push('')
|
||
lines.push(bareTopSectionSeparator('Session', cols, opts.asciiSep))
|
||
lines.push(
|
||
bareTopSanitizeVisible(
|
||
' execLineCount=' + (sess.execLineCount ?? na) + ' pipelineBytesTotal=' + (sess.pipelineBytesTotal ?? na)
|
||
)
|
||
)
|
||
lines.push(
|
||
bareTopSanitizeVisible(' execLineWallMsTotal=' + (sess.execLineWallMsTotal ?? na))
|
||
)
|
||
const ringW = opts.sessionWallRing
|
||
if (ringW && ringW.length && opts.sparkW > 0) {
|
||
const sp = bareTopSparklineOverview(
|
||
ringW,
|
||
Math.min(32, opts.sparkW),
|
||
opts.sparkAscii,
|
||
opts.logSpark,
|
||
opts.braille
|
||
)
|
||
lines.push(bareTopSanitizeVisible(' execLineWallMsTotal spark ' + sp))
|
||
}
|
||
}
|
||
|
||
if (want('pipeline')) {
|
||
const pg = bareTopPipelineGaugesFromMetrics(m && m.pipeline, cols)
|
||
pushSec('Pipeline limits', 'pipeline', pg ? [pg] : [' ' + na])
|
||
}
|
||
|
||
if (want('delegates')) {
|
||
const delI = m && m.delegateInflight
|
||
/** @type {string[]} */
|
||
const dl = []
|
||
if (delI != null && typeof delI === 'object') {
|
||
dl.push(...bareTopDelegateInflightTableLines(delI, 20, Math.min(24, Math.floor(cols * 0.35))))
|
||
} else dl.push(' ' + na)
|
||
pushSec('Delegates (inflight)', 'delegates', dl)
|
||
}
|
||
|
||
const repLive = m && m.replicationLive
|
||
if (want('replication')) {
|
||
pushSec('Replication live', 'replication', bareTopReplicationOverviewLines(repLive, na))
|
||
}
|
||
|
||
if (want('nethealth')) {
|
||
const nl = bareTopNetHealthOverviewLines(snap.netSummary, m)
|
||
if (nl.length) pushSec('Network summary', 'nethealth', nl)
|
||
}
|
||
|
||
if (want('kernel')) {
|
||
const prevK =
|
||
opts.prevMetrics && opts.prevMetrics.kernelCounters
|
||
? opts.prevMetrics.kernelCounters
|
||
: null
|
||
const kTop = opts.compact ? 12 : 22
|
||
const kl = bareTopKernelCounterLines(
|
||
m && m.kernelCounters,
|
||
prevK,
|
||
opts.deltaMode,
|
||
kTop,
|
||
Math.min(28, Math.floor(cols * 0.38))
|
||
)
|
||
pushSec('Kernel counters', 'kernel', kl.length ? kl : [' ' + na])
|
||
}
|
||
|
||
if (want('warm')) {
|
||
const wl = bareTopWarmReadCacheSummaryLines(m && m.warmReadCache)
|
||
pushSec('Warm read cache', 'warm', wl.length ? wl : [' ' + na])
|
||
}
|
||
|
||
if (want('ipc')) {
|
||
/** @type {string[]} */
|
||
const ibody = bareTopIpcTelemetrySummaryLines(m && m.ipcTelemetry, 8).slice()
|
||
const ipL = bareTopIpcBackpressureProcLine(
|
||
snap.extra && snap.extra.ipcBackpressure
|
||
)
|
||
if (ipL) ibody.push(ipL)
|
||
pushSec('IPC telemetry', 'ipc', ibody.length ? ibody : [' ' + na])
|
||
}
|
||
|
||
if (want('boot')) {
|
||
const bl = bareTopBootBudgetLines(
|
||
m && m.bootBudgetCold,
|
||
m && m.bootBudgetBareStdlib,
|
||
m && m.bootBudgetTelemetry
|
||
)
|
||
pushSec('Boot budget', 'boot', bl.length ? bl : [' ' + na])
|
||
}
|
||
|
||
if (want('swarm') && m && m.swarmLifecycle) {
|
||
const sl = bareTopSwarmLifecycleLine(m.swarmLifecycle)
|
||
pushSec('Swarm lifecycle', 'swarm', sl ? [sl] : [' ' + na])
|
||
}
|
||
|
||
const pt =
|
||
(m && m.processTable && typeof m.processTable === 'object'
|
||
? m.processTable
|
||
: null) ||
|
||
(snap.extra &&
|
||
snap.extra.processTable &&
|
||
typeof snap.extra.processTable === 'object'
|
||
? snap.extra.processTable
|
||
: null)
|
||
|
||
if (want('prochist')) {
|
||
pushSec('Process states', 'prochist', bareTopProcessStateHistogramLines(pt))
|
||
}
|
||
|
||
if (want('protomux')) {
|
||
const cs =
|
||
repLive &&
|
||
typeof repLive === 'object' &&
|
||
repLive.collaborationSession &&
|
||
typeof repLive.collaborationSession === 'object'
|
||
? repLive.collaborationSession
|
||
: null
|
||
/** @type {string[]} */
|
||
const pl = []
|
||
const cl = bareTopProtomuxCollabLine(cs)
|
||
if (cl) pl.push(cl)
|
||
if (opts.protomuxSpark && opts.ringProto && opts.ringProto.length && opts.sparkW > 0) {
|
||
const sp = bareTopSparklineOverview(
|
||
opts.ringProto,
|
||
opts.sparkW,
|
||
opts.sparkAscii,
|
||
opts.logSpark,
|
||
opts.braille
|
||
)
|
||
pl.push(' appRx spark ' + sp)
|
||
}
|
||
if (pl.length) pushSec('Protomux / collab', 'protomux', pl)
|
||
}
|
||
|
||
if (want('meta')) {
|
||
const ml = bareTopMetaCoalesceLine(m, opts.nowMs, na)
|
||
if (ml) pushSec('Refresh meta', 'meta', [ml])
|
||
}
|
||
|
||
if (want('worker')) {
|
||
/** @type {string[]} */
|
||
const wbod = []
|
||
const wl = bareTopWorkerBudgetCompactLine(m && m.workerBudgetHints)
|
||
if (wl) wbod.push(wl)
|
||
const ws = bareTopWorkerSandboxOverviewLines(
|
||
snap.extra && snap.extra.workerBudget,
|
||
snap.extra && snap.extra.sandboxProfile
|
||
)
|
||
for (const x of ws) wbod.push(x)
|
||
if (wbod.length) pushSec('Worker budget', 'worker', wbod)
|
||
}
|
||
|
||
if (want('initd')) {
|
||
pushSec('Initd readiness', 'initd', bareTopInitdReadinessSummaryLines(m && m.initdReadiness))
|
||
}
|
||
|
||
if (want('fdagg')) {
|
||
const fl = bareTopLogicalFdAggregateLine(pt)
|
||
if (fl) pushSec('FD summary', 'fdagg', [fl])
|
||
}
|
||
|
||
if (want('delegate_rates') && m && m.delegateRateBuckets != null) {
|
||
pushSec('Delegate rate buckets', 'delegate_rates', bareTopDelegateRateBucketLines(m.delegateRateBuckets, cols))
|
||
}
|
||
|
||
if (want('fairness') && snap.fairnessSnapshot && fc) {
|
||
const fl = fc(snap.fairnessSnapshot, 14, 32)
|
||
const expl = [
|
||
' Fairness = delegate / job scheduling hints (not host CPU share).'
|
||
]
|
||
pushSec('Fairness', 'fairness', expl.concat(fl))
|
||
}
|
||
|
||
if (want('subprocess') && snap.subprocessBridge && fc) {
|
||
pushSec('Subprocess bridge', 'subprocess', fc(snap.subprocessBridge, 12, 24))
|
||
}
|
||
|
||
if (want('limits')) {
|
||
const ll = bareTopLimitsMergedLines(
|
||
/** @type {Record<string, unknown>} */ (snap)
|
||
)
|
||
if (ll.length) {
|
||
ll.push(
|
||
' note: quotas = operator pipeline caps; rlimits = per-process limits mirror.'
|
||
)
|
||
pushSec('Limits (quotas + rlimits)', 'limits', ll)
|
||
}
|
||
}
|
||
|
||
if (want('p2p')) {
|
||
const pl = bareTopP2PStackLines(/** @type {Record<string, unknown>} */ (snap))
|
||
if (pl.length) pushSec('P2P / HDMS / DHT', 'p2p', pl)
|
||
}
|
||
|
||
if (want('diskio') && snap.diskstatsLine) {
|
||
const dl = bareTopDiskstatsOverviewLines(String(snap.diskstatsLine))
|
||
if (dl.length) pushSec('Disk', 'diskio', dl)
|
||
}
|
||
|
||
if (want('syscallsum')) {
|
||
const sy = snap.extra && snap.extra.syscalls
|
||
const sl = bareTopSyscallsOverviewLines(
|
||
sy && typeof sy === 'object' ? sy : null
|
||
)
|
||
if (sl.length) pushSec('Syscalls proc', 'syscallsum', sl)
|
||
}
|
||
|
||
if (want('delegate_red')) {
|
||
const dr = snap.extra && snap.extra.delegateRed
|
||
const dl = bareTopDelegateRedOverviewLines(dr)
|
||
if (dl.length) pushSec('Delegate red', 'delegate_red', dl)
|
||
}
|
||
|
||
if (want('security')) {
|
||
const sp = snap.extra && snap.extra.securityPosture
|
||
const sl = bareTopSecurityPostureOverviewLines(sp)
|
||
if (sl.length) pushSec('Security posture', 'security', sl)
|
||
}
|
||
|
||
if (want('bootsum')) {
|
||
const bb = snap.extra && snap.extra.bootBudgetSummary
|
||
const bl = bareTopBootBudgetSummaryOverviewLines(bb)
|
||
if (bl.length) pushSec('Boot budget file', 'bootsum', bl)
|
||
}
|
||
|
||
if (want('badpeer')) {
|
||
const ph = snap.extra && snap.extra.peerHealth
|
||
const wl = bareTopWorstPeerOverviewLines(ph)
|
||
if (wl.length) pushSec('Peer health', 'badpeer', wl)
|
||
}
|
||
|
||
if (want('capdiff')) {
|
||
const cj = snap.extra && snap.extra.capabilitiesJson
|
||
const cn = snap.extra && snap.extra.capabilitiesNode
|
||
const cl = bareTopCapabilitiesDiffOverviewLines(cj, cn)
|
||
if (cl.length) pushSec('Capabilities diff', 'capdiff', cl)
|
||
}
|
||
|
||
if (want('extsum')) {
|
||
const ex = snap.extra && snap.extra.extensions
|
||
const el = bareTopExtensionsOverviewLines(ex)
|
||
if (el.length) pushSec('Extensions', 'extsum', el)
|
||
}
|
||
|
||
if (want('gitcounters') && opts.deltaMode && opts.prevSnap) {
|
||
const gl = bareTopGitStatsDeltaOverviewLines(
|
||
snap.extra && snap.extra.gitDelegateStats,
|
||
snap.extra && snap.extra.gitLfsPointerStats,
|
||
opts.prevSnap.extra && opts.prevSnap.extra.gitDelegateStats,
|
||
opts.prevSnap.extra && opts.prevSnap.extra.gitLfsPointerStats
|
||
)
|
||
if (gl.length) pushSec('Git stats delta', 'gitcounters', gl)
|
||
}
|
||
|
||
if (want('host') && snap.hostStats) {
|
||
const hl = bareTopHostStatsSummaryLine(snap.hostStats)
|
||
if (hl) pushSec('Host stats', 'host', [hl])
|
||
}
|
||
|
||
if (want('resources') && snap.resources) {
|
||
const full = !opts.compact && fc
|
||
if (full) {
|
||
pushSec('Resources', 'resources', fc(snap.resources, 20, 40))
|
||
} else {
|
||
pushSec('Resources', 'resources', bareTopResourcesSummaryLines(snap.resources, 10))
|
||
}
|
||
}
|
||
|
||
if (
|
||
want('readerr') &&
|
||
(snap.readErr ||
|
||
(Array.isArray(snap.readErrPaths) && snap.readErrPaths.length))
|
||
) {
|
||
/** @type {string[]} */
|
||
const erl = []
|
||
if (snap.readErr) {
|
||
const err = String(snap.readErr)
|
||
const wrapped = err.split('\n').slice(0, 3)
|
||
for (const w of wrapped) erl.push(' warn ' + w)
|
||
if (err.split('\n').length > 3) erl.push(' … (truncated)')
|
||
}
|
||
const paths = Array.isArray(snap.readErrPaths) ? snap.readErrPaths : []
|
||
for (const p of paths.slice(0, 16)) {
|
||
erl.push(' read fail ' + bareTopSanitizeVisible(String(p)))
|
||
}
|
||
if (erl.length) pushSec('Read errors', 'readerr', erl)
|
||
}
|
||
|
||
const mismatch = bareTopPeerMismatchWarningLine(Number(m && m.peers) || 0, repLive)
|
||
if (mismatch && want('replication')) {
|
||
lines.push(bareTopSanitizeVisible(mismatch))
|
||
}
|
||
|
||
if (opts.healthDetail && opts.healthBreakdown) {
|
||
sectionRawLineIndex.health_detail = lines.length
|
||
lines.push('')
|
||
lines.push(bareTopSanitizeVisible(' health detail ' + opts.healthBreakdown))
|
||
active.push('health_detail')
|
||
}
|
||
|
||
return { lines, activeSections: active, sectionRawLineIndex }
|
||
}
|
||
|
||
function bareTopSectionTitleFor(id) {
|
||
const map = {
|
||
session: 'session',
|
||
pipeline: 'pipeline',
|
||
delegates: 'delegates',
|
||
replication: 'replication',
|
||
kernel: 'kernel',
|
||
warm: 'warm',
|
||
ipc: 'ipc',
|
||
boot: 'boot',
|
||
swarm: 'swarm',
|
||
prochist: 'process',
|
||
protomux: 'protomux',
|
||
meta: 'meta',
|
||
worker: 'worker',
|
||
initd: 'initd',
|
||
initd_phases: 'initd',
|
||
fdagg: 'fd',
|
||
delegate_rates: 'delegate',
|
||
fairness: 'fairness',
|
||
subprocess: 'subprocess',
|
||
host: 'host',
|
||
resources: 'resources',
|
||
readerr: 'error',
|
||
limits: 'limits',
|
||
p2p: 'p2p',
|
||
nethealth: 'network',
|
||
diskio: 'disk',
|
||
syscallsum: 'syscall',
|
||
delegate_red: 'delegate',
|
||
security: 'security',
|
||
bootsum: 'boot',
|
||
badpeer: 'peer',
|
||
capdiff: 'cap',
|
||
extsum: 'ext',
|
||
gitcounters: 'git'
|
||
}
|
||
return map[id] || id
|
||
}
|
||
|
||
/**
|
||
* Pipeline gauges string (shared with TUI).
|
||
* Duplicates bareTopPipelineGauges from tui when only helpers are tested; TUI keeps full color version.
|
||
*/
|
||
function bareTopPipelineGaugesFromMetrics(pl, cols) {
|
||
if (!pl || typeof pl !== 'object') return ''
|
||
const o = /** @type {Record<string, unknown>} */ (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 cap = v > 1000 ? v : 100
|
||
const pct = Math.min(100, Math.round((v / cap) * 100))
|
||
parts.push(k.slice(0, 10) + '=' + pct + '%')
|
||
}
|
||
}
|
||
return parts.join(' ').slice(0, Math.max(20, cols - 2))
|
||
}
|
||
|
||
/**
|
||
* @param {number[]} values
|
||
* @param {number} width
|
||
* @param {boolean} ascii
|
||
* @param {boolean} logScale
|
||
* @param {boolean} braille
|
||
*/
|
||
function bareTopSparklineOverview(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)
|
||
}
|
||
|
||
/** 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
|
||
}
|
||
}
|
||
}
|
||
|
||
/** 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 8) 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.',
|
||
pressCloseSetup: 'Esc or any other key closes setup.',
|
||
setupTitle: 'Runtime setup (hints only — set env before launch to persist)',
|
||
activity: 'Activity (per refresh)',
|
||
session: 'Session',
|
||
delegates: 'Delegates',
|
||
pipeline: 'Pipeline limits',
|
||
exportOk: 'exported snapshot',
|
||
exportFail: 'export failed',
|
||
paused: 'PAUSED',
|
||
na: 'N/A',
|
||
overviewPinned:
|
||
'Overview: activity + sparklines stay visible; PgUp/Dn scrolls detail below.',
|
||
sortHint:
|
||
'F6/S sort R asc/desc V tree z collapse F follow n renice cols: pid ppid pgid st sid [ni pri cpu% thr wchan user] time name',
|
||
layoutVersion: 'baretop-ui-4'
|
||
}
|
||
|
||
/**
|
||
* @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<string, unknown>} 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: '',
|
||
stateRun: '',
|
||
stateStop: '',
|
||
stateZombie: '',
|
||
roleShell: '',
|
||
roleJob: '',
|
||
roleInitd: ''
|
||
}
|
||
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',
|
||
stateRun: '\x1b[1;32m',
|
||
stateStop: '\x1b[1;33m',
|
||
stateZombie: '\x1b[1;31m',
|
||
roleShell: '\x1b[1;36m',
|
||
roleJob: '\x1b[1;35m',
|
||
roleInitd: '\x1b[1;34m'
|
||
}
|
||
}
|
||
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',
|
||
stateRun: '\x1b[32;1m',
|
||
stateStop: '\x1b[33;1m',
|
||
stateZombie: '\x1b[31;1m',
|
||
roleShell: '\x1b[34m',
|
||
roleJob: '\x1b[35;1m',
|
||
roleInitd: '\x1b[34;1m'
|
||
}
|
||
}
|
||
if (theme === 'none') {
|
||
return {
|
||
kw: '',
|
||
dim: '',
|
||
warn: '',
|
||
bad: '',
|
||
good: '',
|
||
barHi: '',
|
||
border: '',
|
||
header: '',
|
||
sel: '',
|
||
zebraBg: '',
|
||
meterLow: '',
|
||
meterMid: '',
|
||
meterHi: '',
|
||
reset: '',
|
||
stateRun: '',
|
||
stateStop: '',
|
||
stateZombie: '',
|
||
roleShell: '',
|
||
roleJob: '',
|
||
roleInitd: ''
|
||
}
|
||
}
|
||
if (theme === 'gruvbox') {
|
||
return {
|
||
kw: '\x1b[38;5;214m',
|
||
dim: '\x1b[38;5;245m',
|
||
warn: '\x1b[38;5;214m',
|
||
bad: '\x1b[38;5;167m',
|
||
good: '\x1b[38;5;142m',
|
||
barHi: '\x1b[38;5;175m',
|
||
border: '\x1b[38;5;237m',
|
||
header: '\x1b[48;5;235m\x1b[38;5;223m',
|
||
sel: '\x1b[48;5;237m\x1b[38;5;229m',
|
||
zebraBg: '\x1b[48;5;236m',
|
||
meterLow: '\x1b[38;5;142m',
|
||
meterMid: '\x1b[38;5;214m',
|
||
meterHi: '\x1b[38;5;167m',
|
||
reset: '\x1b[0m',
|
||
stateRun: '\x1b[38;5;142m',
|
||
stateStop: '\x1b[38;5;214m',
|
||
stateZombie: '\x1b[38;5;167m',
|
||
roleShell: '\x1b[38;5;109m',
|
||
roleJob: '\x1b[38;5;175m',
|
||
roleInitd: '\x1b[38;5;66m'
|
||
}
|
||
}
|
||
if (theme === 'nord') {
|
||
return {
|
||
kw: '\x1b[38;5;81m',
|
||
dim: '\x1b[38;5;245m',
|
||
warn: '\x1b[38;5;216m',
|
||
bad: '\x1b[38;5;167m',
|
||
good: '\x1b[38;5;114m',
|
||
barHi: '\x1b[38;5;139m',
|
||
border: '\x1b[38;5;238m',
|
||
header: '\x1b[48;5;237m\x1b[38;5;231m',
|
||
sel: '\x1b[48;5;238m\x1b[38;5;252m',
|
||
zebraBg: '\x1b[48;5;236m',
|
||
meterLow: '\x1b[38;5;114m',
|
||
meterMid: '\x1b[38;5;216m',
|
||
meterHi: '\x1b[38;5;167m',
|
||
reset: '\x1b[0m',
|
||
stateRun: '\x1b[38;5;114m',
|
||
stateStop: '\x1b[38;5;216m',
|
||
stateZombie: '\x1b[38;5;167m',
|
||
roleShell: '\x1b[38;5;75m',
|
||
roleJob: '\x1b[38;5;139m',
|
||
roleInitd: '\x1b[38;5;68m'
|
||
}
|
||
}
|
||
if (theme === 'solarized') {
|
||
return {
|
||
kw: '\x1b[36m',
|
||
dim: '\x1b[38;5;245m',
|
||
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',
|
||
stateRun: '\x1b[32m',
|
||
stateStop: '\x1b[33m',
|
||
stateZombie: '\x1b[31m',
|
||
roleShell: '\x1b[36m',
|
||
roleJob: '\x1b[35m',
|
||
roleInitd: '\x1b[34m'
|
||
}
|
||
}
|
||
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',
|
||
stateRun: '\x1b[32m',
|
||
stateStop: '\x1b[33m',
|
||
stateZombie: '\x1b[31m',
|
||
roleShell: '\x1b[36m',
|
||
roleJob: '\x1b[35m',
|
||
roleInitd: '\x1b[34m'
|
||
}
|
||
}
|
||
|
||
/**
|
||
* SGR 1006 mouse: ESC [ < btn ; x ; y M|m
|
||
* @param {number[]} q
|
||
*/
|
||
function bareTopTryConsumeMouse(q) {
|
||
if (q.length < 9) return null
|
||
if (q[0] !== 0x1b || q[1] !== 0x5b || q[2] !== 0x3c) return null
|
||
let i = 3
|
||
const acc = []
|
||
while (i < q.length) {
|
||
const b = q[i]
|
||
if (b === 0x4d || b === 0x6d) break
|
||
acc.push(b)
|
||
i++
|
||
}
|
||
if (i >= q.length) return null
|
||
const release = q[i] === 0x6d
|
||
i++
|
||
const payload = String.fromCharCode.apply(null, acc)
|
||
const parts = payload.split(';')
|
||
const btn = parseInt(parts[0], 10) || 0
|
||
const x = parseInt(parts[1], 10) || 0
|
||
const y = parseInt(parts[2], 10) || 0
|
||
q.splice(0, i)
|
||
return { type: 'mouse', btn, x, y, release }
|
||
}
|
||
|
||
/**
|
||
* @param {number[]} q
|
||
* @param {number} max
|
||
* @param {{
|
||
* vi: boolean,
|
||
* inFilter?: boolean,
|
||
* mouseEnabled?: boolean,
|
||
* procListActive?: boolean,
|
||
* procDetailOpen?: boolean,
|
||
* helpPaging?: boolean,
|
||
* keyQuit?: string,
|
||
* keyRefresh?: string,
|
||
* tab?: number,
|
||
* overviewJumpPick?: boolean,
|
||
* tabCpuIdx?: number,
|
||
* tabMemIdx?: number
|
||
* }} o
|
||
*/
|
||
function bareTopDrainKeys(q, max, o) {
|
||
if (o.overviewJumpPick) {
|
||
const evPick = bareEditTryConsumeKey(q)
|
||
if (!evPick) return null
|
||
if (evPick.type === 'key' && evPick.ch && evPick.ch.length === 1) {
|
||
const ch0 = evPick.ch
|
||
if (ch0 >= '0' && ch0 <= '9') {
|
||
const idx = ch0 === '0' ? 9 : ch0.charCodeAt(0) - 0x31
|
||
return { type: 'overview_section_digit', idx }
|
||
}
|
||
}
|
||
return { type: 'overview_jump_cancel' }
|
||
}
|
||
const kq = (o.keyQuit && o.keyQuit.length === 1 ? o.keyQuit : 'q').toLowerCase()
|
||
const kr = (o.keyRefresh && o.keyRefresh.length === 1 ? o.keyRefresh : 'r').toLowerCase()
|
||
let n = 0
|
||
for (;;) {
|
||
if (n >= max) break
|
||
if (o.mouseEnabled) {
|
||
const me = bareTopTryConsumeMouse(q)
|
||
if (me) {
|
||
n++
|
||
return me
|
||
}
|
||
}
|
||
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 === 2) return { type: 'setup_open' }
|
||
if (fn === 3) return { type: 'search_next' }
|
||
if (fn === 4) return { type: 'search_prev' }
|
||
if (fn === 5) return { type: 'refresh' }
|
||
if (fn === 6) return { type: 'sort_menu' }
|
||
if (fn === 7) return { type: 'renice_open' }
|
||
if (fn === 9) return { type: 'signal_menu' }
|
||
if (fn === 10) return { type: 'quit' }
|
||
}
|
||
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 === 'key' && ev.ch === '\t')
|
||
return o.inFilter ? { type: 'filter_char', ch: '\t' } : { type: 'tab_next' }
|
||
if (ev.type === 'nav') {
|
||
if (o.helpPaging) {
|
||
if (ev.key === 'pageup') return { type: 'help_page', dir: -1 }
|
||
if (ev.key === 'pagedown') return { type: 'help_page', dir: 1 }
|
||
}
|
||
if (ev.key === 'stab' && !o.inFilter) return { type: 'tab_prev' }
|
||
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
|
||
const chl = ch.toLowerCase()
|
||
if (chl === kq || (kq === 'q' && ch === 'Q')) return { type: 'quit' }
|
||
if (ch === 'r' || (kr !== 'r' && chl === kr)) return { type: 'refresh' }
|
||
if (ch === 'R') return { type: 'sort_dir_toggle' }
|
||
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') return { type: 'export_tab' }
|
||
if (ch === 'e') {
|
||
if (o.procDetailOpen) return { type: 'detail_sub', sub: 'env' }
|
||
return { type: 'export' }
|
||
}
|
||
if (ch === 'f') return { type: 'fullscreen_toggle' }
|
||
if (ch === 'F') {
|
||
if (o.procListActive) return { type: 'follow_toggle' }
|
||
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 === 'n' && !o.vi && !o.inFilter) return { type: 'renice_open' }
|
||
if (ch === 'S') return { type: 'sort_menu' }
|
||
if (ch === 'V') return { type: 'tree_toggle' }
|
||
if (ch === 'z') return { type: 'tree_collapse' }
|
||
if (ch === 'x' || ch === 'X') return { type: 'initd_view_toggle' }
|
||
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 (o.procDetailOpen && ch === 'i') return { type: 'detail_sub', sub: 'summary' }
|
||
if (o.procDetailOpen && ch === 'o') return { type: 'detail_sub', sub: 'fds' }
|
||
if (ch === 'a' && !o.inFilter) return { type: 'action_menu' }
|
||
if (ch === 'c' && !o.inFilter) return { type: 'copy_pid' }
|
||
if (ch === '=') return { type: 'tag_toggle' }
|
||
if (ch === '%') return { type: 'tagged_only_toggle' }
|
||
if (ch === 'n' && o.vi) return { type: 'filter_next' }
|
||
if (ch === 'N' && o.vi) return { type: 'filter_prev' }
|
||
if (
|
||
ch === "'" &&
|
||
(o.tab | 0) === 0 &&
|
||
!o.procDetailOpen &&
|
||
!o.inFilter
|
||
)
|
||
return { type: 'overview_jump_arm' }
|
||
{
|
||
const tCpu =
|
||
typeof o.tabCpuIdx === 'number' ? o.tabCpuIdx : -1
|
||
const tMem =
|
||
typeof o.tabMemIdx === 'number' ? o.tabMemIdx : -1
|
||
if (!o.inFilter && ch === 'C' && tCpu >= 0)
|
||
return { type: 'tab', n: tCpu }
|
||
if (!o.inFilter && ch === 'M' && tMem >= 0)
|
||
return { type: 'tab', n: tMem }
|
||
}
|
||
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
|
||
* @param {{ meterLow?: string, meterMid?: string, meterHi?: string, reset: string }} [pal]
|
||
* @param {boolean} [useColor]
|
||
* @returns {string}
|
||
*/
|
||
function bareTopDelegateHistogram(inflight, maxWidth, use256, pal, useColor) {
|
||
if (!inflight || typeof inflight !== 'object') return ''
|
||
const o = /** @type {Record<string, unknown>} */ (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)
|
||
const rst = pal && useColor ? pal.reset : ''
|
||
for (const k of keys) {
|
||
const v = Math.min(maxV, Math.max(0, Number(o[k]) || 0))
|
||
const fill = Math.round((v / maxV) * per)
|
||
const pct = maxV ? v / maxV : 0
|
||
let bar = ''
|
||
for (let i = 0; i < per; i++) {
|
||
const on = i < fill
|
||
if (use256 && on && pal && useColor) {
|
||
const c =
|
||
pct >= 0.85 ? pal.meterHi || ''
|
||
: pct >= 0.5 ? pal.meterMid || ''
|
||
: pal.meterLow || ''
|
||
bar += c + '\u2588' + rst
|
||
} 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<string, unknown>} */ (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 : ['']
|
||
}
|
||
|
||
/**
|
||
* Wrapped-line scroll offset for overview jump: raw `lines` index → first visible wrapped row.
|
||
* @param {string[]} rawLines
|
||
* @param {number} wrapW
|
||
* @param {number} rawLineStart
|
||
*/
|
||
function bareTopScrollTopForRawLine(rawLines, wrapW, rawLineStart) {
|
||
const w = Math.max(8, wrapW | 0)
|
||
let count = 0
|
||
const lim = Math.min(Math.max(0, rawLineStart | 0), rawLines.length)
|
||
for (let i = 0; i < lim; i++) {
|
||
for (const piece of bareTopWrapLine(rawLines[i] || '', w)) {
|
||
void piece
|
||
count++
|
||
}
|
||
}
|
||
return count
|
||
}
|
||
|
||
/**
|
||
* @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<string, unknown>} */ (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 {string} prefix
|
||
* @param {unknown} v
|
||
* @param {number} depth
|
||
* @param {number} maxD
|
||
* @param {number} maxKeys
|
||
* @param {number} maxLines
|
||
*/
|
||
function bareTopFlattenLimited(prefix, v, depth, maxD, maxKeys, maxLines) {
|
||
const lines = []
|
||
bareTopFlatten(prefix, v, depth, maxD, lines, maxKeys)
|
||
const cap = Math.max(4, maxLines | 0)
|
||
if (lines.length > cap) {
|
||
return lines.slice(0, cap - 1).concat([' … (truncated)'])
|
||
}
|
||
return lines
|
||
}
|
||
|
||
/**
|
||
* @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<string, unknown>} */ (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))
|
||
const rawV = o[k]
|
||
let sv = ''
|
||
if (rawV == null) sv = bareTopStrings.na
|
||
else if (
|
||
typeof rawV === 'number' &&
|
||
Number.isFinite(rawV) &&
|
||
/Bytes$/i.test(k) &&
|
||
typeof bareTopFormatBytes === 'function'
|
||
)
|
||
sv = bareTopFormatBytes(rawV)
|
||
else if (
|
||
typeof rawV === 'number' &&
|
||
Number.isFinite(rawV) &&
|
||
/Ms$/i.test(k) &&
|
||
typeof bareTopFormatDurationMs === 'function'
|
||
)
|
||
sv = bareTopFormatDurationMs(rawV)
|
||
else sv = String(rawV)
|
||
sv = bareTopTruncateCell(sv, Math.max(8, cols - keyW - 4), true)
|
||
out.push(' ' + label + pad + ' ' + sv)
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown | null | undefined>} 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
|
||
lines.push(
|
||
...bareTopFlattenLimited('', val, 1, maxDepth, 120, 80)
|
||
)
|
||
}
|
||
}
|
||
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 lines.push(...bareTopFlattenLimited('', o, 1, maxDepth, 200, 80))
|
||
}
|
||
return lines
|
||
}
|
||
|
||
/**
|
||
* Scroll line budget for process detail panel.
|
||
* @param {number} rows
|
||
* @param {number} hdrRow
|
||
*/
|
||
function bareTopProcDetailMaxLines(rows, hdrRow) {
|
||
return Math.max(12, Math.min(200, Math.max(1, rows - hdrRow - 2)))
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} o
|
||
* @param {number} cols
|
||
*/
|
||
function bareTopProcDetailEnvLines(o, cols) {
|
||
const c = Math.max(24, cols | 0)
|
||
/** @type {string[]} */
|
||
const lines = []
|
||
const keys = Object.keys(o).sort((a, b) => a.localeCompare(b))
|
||
for (const k of keys) {
|
||
const v = o[k]
|
||
let val = ''
|
||
if (v == null) val = ''
|
||
else if (typeof v === 'object')
|
||
val = bareTopFlattenLimited('', v, 0, 4, 48, 8).join(' ')
|
||
else val = String(v)
|
||
const piece = k + '=' + val
|
||
lines.push(' ' + bareTopTruncateCell(piece, Math.max(12, c - 2), false))
|
||
}
|
||
return lines.length ? lines : [' (empty env)']
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} o
|
||
* @param {number} cols
|
||
*/
|
||
function bareTopProcDetailFdLines(o, cols) {
|
||
const c = Math.max(24, cols | 0)
|
||
/** @type {string[]} */
|
||
const lines = []
|
||
const keys = Object.keys(o)
|
||
keys.sort((a, b) => {
|
||
const na = Number(a)
|
||
const nb = Number(b)
|
||
if (Number.isFinite(na) && Number.isFinite(nb)) return na - nb
|
||
return String(a).localeCompare(String(b))
|
||
})
|
||
for (const k of keys) {
|
||
const v = o[k]
|
||
let cell = ''
|
||
if (v == null) cell = ''
|
||
else if (typeof v === 'object' && !Array.isArray(v)) {
|
||
cell = bareTopFlattenLimited('', v, 0, 5, 48, 12).join(' · ')
|
||
} else if (Array.isArray(v)) {
|
||
cell = v.map((x) => String(x)).join(', ')
|
||
} else {
|
||
cell = String(v)
|
||
}
|
||
const ks = String(k)
|
||
const fdLab =
|
||
ks.length <= 8 ? ks.padStart(4) : bareTopTruncateCell(ks, 8, false)
|
||
lines.push(
|
||
' ' +
|
||
fdLab +
|
||
' ' +
|
||
bareTopTruncateCell(cell, Math.max(12, c - 12), false)
|
||
)
|
||
}
|
||
return lines.length ? lines : [' (empty fd table)']
|
||
}
|
||
|
||
/**
|
||
* @param {'all' | 'summary' | 'fds' | 'env'} sub
|
||
* @param {Record<string, unknown>} detailObj
|
||
* @param {number} cols
|
||
* @param {number} maxLines
|
||
* @param {Record<string, string>} envTop
|
||
*/
|
||
function bareTopProcDetailBodyLines(sub, detailObj, cols, maxLines, envTop) {
|
||
const wantJson =
|
||
envTop.BARE_TOP_PROC_DETAIL_JSON === '1' ||
|
||
envTop.BARE_TOP_PROC_DETAIL_JSON === 'true'
|
||
const maxChars = Math.min(
|
||
16000,
|
||
Math.max(400, cols * Math.max(8, maxLines) * 2)
|
||
)
|
||
if (wantJson)
|
||
return bareTopTruncateJsonPretty(detailObj, maxChars).split('\n')
|
||
const onlyNote =
|
||
detailObj &&
|
||
typeof detailObj === 'object' &&
|
||
!Array.isArray(detailObj) &&
|
||
Object.keys(detailObj).length === 1 &&
|
||
typeof detailObj.note === 'string'
|
||
if (onlyNote) return [' ' + String(detailObj.note)]
|
||
if (sub === 'env')
|
||
return bareTopProcDetailEnvLines(
|
||
/** @type {Record<string, unknown>} */ (detailObj),
|
||
cols
|
||
)
|
||
if (sub === 'fds')
|
||
return bareTopProcDetailFdLines(
|
||
/** @type {Record<string, unknown>} */ (detailObj),
|
||
cols
|
||
)
|
||
if (sub === 'summary') {
|
||
const shallow = bareTopShallowObjectLines(detailObj, cols)
|
||
if (shallow) return shallow
|
||
return bareTopFlattenLimited('', detailObj, 1, 6, 120, maxLines)
|
||
}
|
||
return bareTopFlattenLimited('', detailObj, 1, 10, 120, maxLines)
|
||
}
|
||
|
||
/**
|
||
* @param {string} text
|
||
* @param {number} maxLen
|
||
*/
|
||
function bareTopTruncateMiddle(text, maxLen) {
|
||
const t = String(text)
|
||
const m = maxLen | 0
|
||
if (m < 5 || t.length <= m) return t
|
||
const inner = m - 1
|
||
const left = Math.ceil(inner / 2)
|
||
const right = inner - left
|
||
return t.slice(0, left) + '\u2026' + t.slice(t.length - right)
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} features
|
||
* @param {number} cols
|
||
*/
|
||
function bareTopFeaturesTableLines(features, cols) {
|
||
if (!features || typeof features !== 'object') return [' N/A']
|
||
const o = /** @type {Record<string, unknown>} */ (features)
|
||
const c = Math.max(40, cols | 0)
|
||
const kw = Math.min(30, Math.max(8, Math.floor(c * 0.26)))
|
||
const rowPrefix = ' '
|
||
const gap = ' '
|
||
const valWidth = Math.max(16, c - rowPrefix.length - kw - gap.length)
|
||
const contPad = ' '.repeat(rowPrefix.length + kw + gap.length)
|
||
|
||
/** @type {string[]} */
|
||
const out = [' Features (flattened booleans and scalars)']
|
||
|
||
/**
|
||
* @param {string} keyCol
|
||
* @param {string} text
|
||
*/
|
||
function pushWrappedScalar(keyCol, text) {
|
||
const wrapped = bareTopWrapLine(text, valWidth)
|
||
for (let i = 0; i < wrapped.length; i++) {
|
||
const pad = i === 0 ? rowPrefix + keyCol + gap : contPad
|
||
const maxCell = Math.max(4, c - pad.length)
|
||
out.push(pad + bareTopTruncateCell(wrapped[i], maxCell, false))
|
||
}
|
||
}
|
||
|
||
for (const k of Object.keys(o).sort().slice(0, 200)) {
|
||
const v = o[k]
|
||
const keyCol = bareTopTruncateCell(k, kw, true).padEnd(kw)
|
||
if (v === true) pushWrappedScalar(keyCol, 'on')
|
||
else if (v === false) pushWrappedScalar(keyCol, 'off')
|
||
else if (v == null) pushWrappedScalar(keyCol, '')
|
||
else if (typeof v !== 'object') pushWrappedScalar(keyCol, String(v))
|
||
else {
|
||
const nested = bareTopFlattenLimited('', v, 1, 10, 120, 80)
|
||
nested.forEach((nl, i) => {
|
||
const pad = i === 0 ? rowPrefix + keyCol + gap : contPad
|
||
const maxCell = Math.max(8, c - pad.length)
|
||
out.push(pad + bareTopTruncateCell(nl, maxCell, false))
|
||
})
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @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
|
||
}
|
||
lines.push(...bareTopFlattenLimited('', graph, 0, 6, 200, 40))
|
||
return lines
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} hints
|
||
* @returns {string}
|
||
*/
|
||
function bareTopHintsFootLine(hints) {
|
||
if (!hints || typeof hints !== 'object') return ''
|
||
const parts = bareTopFlattenLimited('', hints, 0, 2, 20, 12)
|
||
return parts.filter(Boolean).join(' \u00b7 ').slice(0, 380)
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {Record<string, string>} env
|
||
* @returns {Promise<{ path: string, flat: Record<string, string>, keys: Record<string, string> }>}
|
||
*/
|
||
async function bareTopLoadBareTopRc(ctx, env) {
|
||
const path =
|
||
String(env.BARE_TOP_CONFIG_PATH || '').trim() ||
|
||
(env.HOME ? env.HOME + '/.config/baretoprc' : '')
|
||
const out = {
|
||
path,
|
||
flat: /** @type {Record<string, string>} */ ({}),
|
||
keys: /** @type {Record<string, string>} */ ({})
|
||
}
|
||
if (!path || !ctx.vfs || typeof ctx.vfs.readFile !== 'function') return out
|
||
try {
|
||
const buf = await ctx.vfs.readFile(path)
|
||
const t = typeof buf === 'string' ? buf : String(buf)
|
||
const p = bareTopJsonParse(t.trim())
|
||
if (p && typeof p === 'object') {
|
||
for (const [k, v] of Object.entries(p)) {
|
||
if (k === 'keys' && v && typeof v === 'object') {
|
||
for (const [a, b] of Object.entries(
|
||
/** @type {Record<string, unknown>} */ (v)
|
||
))
|
||
if (typeof b === 'string') out.keys[String(a)] = b
|
||
} else if (typeof v === 'string' || typeof v === 'number')
|
||
out.flat[String(k)] = String(v)
|
||
}
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {Awaited<ReturnType<typeof bareTopFetchSnapshot>> | null} snap
|
||
* @param {string} filterProcess
|
||
* @param {'pid' | 'name' | 'state' | 'time' | 'nice' | 'pri' | 'cpu'} sortKey
|
||
* @param {boolean} sortAsc
|
||
* @param {Record<string, string>} env
|
||
* @param {{ taggedPids?: Set<number>, taggedOnly?: boolean, tertiaryPpid?: boolean }} [fopts]
|
||
*/
|
||
function bareTopLiveProcessList(snap, filterProcess, sortKey, sortAsc, env, fopts) {
|
||
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 || '')
|
||
const fo = fopts && typeof fopts === 'object' ? fopts : {}
|
||
const tagged = fo.taggedPids
|
||
const taggedOnly = fo.taggedOnly === true
|
||
const filtered = raw.filter((r) =>
|
||
typeof bareTopProcessRowMatchesFilter === 'function'
|
||
? bareTopProcessRowMatchesFilter(
|
||
/** @type {Record<string, unknown>} */ (r),
|
||
pf,
|
||
env,
|
||
{ taggedPids: tagged, taggedOnly }
|
||
)
|
||
: !pf.length ||
|
||
String(
|
||
/** @type {Record<string, unknown>} */ (r).name ||
|
||
/** @type {Record<string, unknown>} */ (r).label ||
|
||
''
|
||
)
|
||
.toLowerCase()
|
||
.includes(pf.toLowerCase())
|
||
)
|
||
const wallMs = snap && snap.fetchWallMs != null ? Number(snap.fetchWallMs) : 1000
|
||
return bareTopSortProcessRows(filtered, sortKey, sortAsc, {
|
||
tertiaryPpid: fo.tertiaryPpid === true,
|
||
sortWallMs: wallMs
|
||
})
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} 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 = Object.assign(
|
||
{},
|
||
ctx.env && typeof ctx.env === 'object'
|
||
? /** @type {Record<string, string>} */ (ctx.env)
|
||
: {}
|
||
)
|
||
const rcBareTop = await bareTopLoadBareTopRc(ctx, envTop)
|
||
for (const [k, v] of Object.entries(rcBareTop.flat)) {
|
||
if (
|
||
k.startsWith('BARE_TOP_') ||
|
||
k === 'NO_COLOR' ||
|
||
k === 'COLORTERM'
|
||
)
|
||
envTop[k] = v
|
||
}
|
||
const bareTopKeyMap = rcBareTop.keys
|
||
const bareTopRcPath = rcBareTop.path
|
||
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 graphMode = String(envTop.BARE_TOP_GRAPH_MODE || '').toLowerCase()
|
||
const asciiGraphEff =
|
||
graphMode === 'ascii' ||
|
||
envTop.BARE_TOP_ASCII_GRAPH === '1' ||
|
||
asciiUi
|
||
const sparkBrailleEff =
|
||
graphMode === 'braille' ||
|
||
(graphMode !== 'ascii' &&
|
||
graphMode !== 'unicode' &&
|
||
envTop.BARE_TOP_BRAILLE_SPARK === '1')
|
||
const logSpark = envTop.BARE_TOP_LOG_SPARK === '1'
|
||
const noColorEnv =
|
||
envTop.NO_COLOR != null && String(envTop.NO_COLOR) !== ''
|
||
const use256 =
|
||
useColor &&
|
||
!noColorEnv &&
|
||
(String(envTop.COLORTERM || '').toLowerCase() === 'truecolor' ||
|
||
String(envTop.COLORTERM || '').includes('256') ||
|
||
envTop.BARE_TOP_COLOR256 === '1')
|
||
const truecolorCpuOn =
|
||
use256 &&
|
||
!noColorEnv &&
|
||
String(envTop.COLORTERM || '').toLowerCase() === 'truecolor' &&
|
||
(envTop.BARE_TOP_TRUECOLOR_CPU === '1' ||
|
||
envTop.BARE_TOP_TRUECOLOR_CPU === 'true')
|
||
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 layoutRaw = String(envTop.BARE_TOP_LAYOUT || 'stacked').toLowerCase()
|
||
const layout =
|
||
layoutRaw === 'htop'
|
||
? 'stacked'
|
||
: layoutRaw === 'btop'
|
||
? 'even'
|
||
: layoutRaw
|
||
const layoutAutoWide =
|
||
envTop.BARE_TOP_LAYOUT_AUTO === '1' ||
|
||
envTop.BARE_TOP_LAYOUT_AUTO === 'true'
|
||
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 incrementalModeRaw = String(envTop.BARE_TOP_INCREMENTAL || '')
|
||
.trim()
|
||
.toLowerCase()
|
||
const fullRedrawForced =
|
||
envTop.BARE_TOP_FULL_REDRAW === '1' ||
|
||
envTop.BARE_TOP_FULL_REDRAW === 'true'
|
||
const termRaw = String(envTop.TERM || '').toLowerCase()
|
||
const incrementalDefaultOk =
|
||
!fullRedrawForced &&
|
||
termRaw !== 'dumb' &&
|
||
termRaw !== '' &&
|
||
incrementalModeRaw !== '0' &&
|
||
incrementalModeRaw !== 'false' &&
|
||
incrementalModeRaw !== 'off'
|
||
const incrementalFrameDiff =
|
||
incrementalModeRaw === '2' ||
|
||
incrementalModeRaw === 'true' ||
|
||
(incrementalDefaultOk && incrementalModeRaw !== '1')
|
||
const incrementalClear =
|
||
incrementalModeRaw === '1' ||
|
||
incrementalModeRaw === 'true' ||
|
||
incrementalModeRaw === '2' ||
|
||
incrementalFrameDiff
|
||
const lineHashDiff =
|
||
envTop.BARE_TOP_LINE_HASH === '1' ||
|
||
envTop.BARE_TOP_LINE_HASH === 'true' ||
|
||
incrementalFrameDiff
|
||
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 overviewCompact =
|
||
envTop.BARE_TOP_OVERVIEW_COMPACT === '1' ||
|
||
envTop.BARE_TOP_OVERVIEW_COMPACT === 'true'
|
||
const overviewSectionsRaw = String(envTop.BARE_TOP_OVERVIEW_SECTIONS || '').trim()
|
||
const healthDetailOn =
|
||
envTop.BARE_TOP_HEALTH_DETAIL === '1' ||
|
||
envTop.BARE_TOP_HEALTH_DETAIL === 'true'
|
||
const protomuxSparkOn = envTop.BARE_TOP_OVERVIEW_PROTOMUX_SPARK === '1'
|
||
const mouseOn =
|
||
envTop.BARE_TOP_MOUSE === '1' || envTop.BARE_TOP_MOUSE === 'true'
|
||
const no2jAfterFirst =
|
||
envTop.BARE_TOP_NO_2J_AFTER_FIRST === '1' ||
|
||
envTop.BARE_TOP_NO_2J_AFTER_FIRST === 'true'
|
||
const debugTimings =
|
||
envTop.BARE_TOP_DEBUG_TIMINGS === '1' ||
|
||
envTop.BARE_TOP_DEBUG_TIMINGS === 'true'
|
||
const quietFooter =
|
||
envTop.BARE_TOP_QUIET_FOOTER === '1' ||
|
||
envTop.BARE_TOP_QUIET_FOOTER === 'true'
|
||
const exportRedact =
|
||
envTop.BARE_TOP_EXPORT_REDACT === '1' ||
|
||
envTop.BARE_TOP_EXPORT_REDACT === 'true'
|
||
const titleVersionOn =
|
||
envTop.BARE_TOP_TITLE_VERSION === '1' ||
|
||
envTop.BARE_TOP_TITLE_VERSION === 'true'
|
||
const scrollRegionOn =
|
||
envTop.BARE_TOP_SCROLL_REGION === '1' ||
|
||
envTop.BARE_TOP_SCROLL_REGION === 'true'
|
||
const profileOn =
|
||
envTop.BARE_TOP_PROFILE === '1' || envTop.BARE_TOP_PROFILE === 'true'
|
||
const unicodeColSep =
|
||
envTop.BARE_TOP_PROC_COL_SEP === '1' ||
|
||
envTop.BARE_TOP_PROC_COL_SEP === 'true'
|
||
const minimapOn =
|
||
envTop.BARE_TOP_MINIMAP === '1' || envTop.BARE_TOP_MINIMAP === 'true'
|
||
const procNameWidthAdjRaw = parseInt(
|
||
String(envTop.BARE_TOP_PROC_NAME_WIDTH || '0').trim(),
|
||
10
|
||
)
|
||
const procNameWidthAdj = Number.isFinite(procNameWidthAdjRaw)
|
||
? Math.min(120, Math.max(-120, procNameWidthAdjRaw))
|
||
: 0
|
||
|
||
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 netopMerge = envTop.BARE_TOP_TAB_MERGE === 'netop'
|
||
const TAB_NAMES = netopMerge
|
||
? [
|
||
'overview',
|
||
'processes',
|
||
'initd',
|
||
'network',
|
||
'features',
|
||
'diagnostics',
|
||
'pear',
|
||
'catalog',
|
||
'host',
|
||
'cpu',
|
||
'mem',
|
||
'disk'
|
||
]
|
||
: [
|
||
'overview',
|
||
'processes',
|
||
'initd',
|
||
'network',
|
||
'features',
|
||
'diagnostics',
|
||
'operator',
|
||
'pear',
|
||
'catalog',
|
||
'host',
|
||
'cpu',
|
||
'mem',
|
||
'disk'
|
||
]
|
||
const NTABS = TAB_NAMES.length
|
||
const TAB_PROC = TAB_NAMES.indexOf('processes')
|
||
const TAB_INITD = TAB_NAMES.indexOf('initd')
|
||
const liteAutoOn =
|
||
envTop.BARE_TOP_SNAPSHOT_LITE_AUTO === '1' ||
|
||
envTop.BARE_TOP_SNAPSHOT_LITE_AUTO === 'true'
|
||
|
||
/** @type {number[]} */
|
||
const keyq = []
|
||
function onData(chunk) {
|
||
keyq.push(...bareTopChunkBytes(chunk))
|
||
bareTopStripBracketedPaste(keyq)
|
||
}
|
||
stdin.on('data', onData)
|
||
|
||
let needsRedraw = true
|
||
let resizePulse = false
|
||
let resizeDebounceUntil = 0
|
||
function onResize() {
|
||
resizeDebounceUntil = Date.now() + 50
|
||
needsRedraw = true
|
||
resizePulse = true
|
||
lastLineFrame = null
|
||
}
|
||
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 = []
|
||
/** @type {number[]} */
|
||
const ringReplSkip = []
|
||
/** @type {number[]} */
|
||
const ringProtoMux = []
|
||
/** @type {number[]} */
|
||
const ringSessionWall = []
|
||
/** @type {number[]} */
|
||
const ringNetDelta = []
|
||
let prevNetSum = -1
|
||
/** @type {number[]} */
|
||
const ringSwarmChurn = []
|
||
/** @type {number[]} */
|
||
const ringPeerDeriv = []
|
||
/** @type {number[]} */
|
||
const ringHdms = []
|
||
/** @type {number[]} */
|
||
const ringDht = []
|
||
/** @type {number[]} */
|
||
const ringBridge = []
|
||
let prevPeerCount = -1
|
||
const PROC_SORT_ORDER = [
|
||
'pid',
|
||
'name',
|
||
'state',
|
||
'time',
|
||
'nice',
|
||
'pri',
|
||
'cpu'
|
||
]
|
||
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 setupMode = false
|
||
let quit = false
|
||
/** @type {Awaited<ReturnType<typeof bareTopFetchSnapshot>> | null} */
|
||
let lastSnap = null
|
||
/** @type {Awaited<ReturnType<typeof bareTopFetchSnapshot>> | 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 filterNetwork = ''
|
||
/** @type {string} */
|
||
let filterProcess = ''
|
||
/** @type {string} */
|
||
let filterFeatures = ''
|
||
/** @type {'initd' | 'process' | 'overview' | 'network' | 'features'} */
|
||
let filterWhich = 'initd'
|
||
/** @type {string} */
|
||
let overviewSectionFilter = ''
|
||
/** @type {string[]} */
|
||
let activeOverviewSections = []
|
||
let overviewJumpArmed = false
|
||
/** @type {string[]} */
|
||
let lastOvLines = []
|
||
/** @type {Record<string, number>} */
|
||
let lastOvSectionRaw = {}
|
||
let procTreeMode = false
|
||
let slowFetchStreak = 0
|
||
/** @type {Set<number>} */
|
||
const taggedPids = new Set()
|
||
let procTaggedOnly = false
|
||
let procFollowOn = false
|
||
/** @type {Set<number>} */
|
||
const treeCollapsedPPids = new Set()
|
||
/** @type {'all' | 'summary' | 'fds' | 'env'} */
|
||
let procDetailSub = 'all'
|
||
let helpPageIdx = 0
|
||
let reniceOpen = false
|
||
/** @type {string} */
|
||
let reniceBuf = ''
|
||
/** @type {string} */
|
||
let toastMsg = ''
|
||
let toastUntil = 0
|
||
let prevCatalogIdxHash = ''
|
||
let lastMouseDownMs = 0
|
||
let lastMouseDownRow = -1
|
||
let lastStaleBucket = 0
|
||
let followPid = 0
|
||
let initdRawFallback = false
|
||
let firstDrawDone = false
|
||
let lastDbgLog = 0
|
||
/** @type {string[] | null} */
|
||
let lastLineFrame = null
|
||
let lastLineFrameRows = 0
|
||
let lastLineFrameCols = 0
|
||
let lastDbgPatchLen = 0
|
||
const procSortEnv = String(envTop.BARE_TOP_PROC_SORT || 'pid').toLowerCase()
|
||
/** @type {'pid' | 'name' | 'state' | 'time' | 'nice' | 'pri' | 'cpu'} */
|
||
let procSortKey =
|
||
procSortEnv === 'name'
|
||
? 'name'
|
||
: procSortEnv === 'state'
|
||
? 'state'
|
||
: procSortEnv === 'time'
|
||
? 'time'
|
||
: procSortEnv === 'nice'
|
||
? 'nice'
|
||
: procSortEnv === 'pri'
|
||
? 'pri'
|
||
: procSortEnv === 'cpu'
|
||
? 'cpu'
|
||
: '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
|
||
}
|
||
if (mouseOn) {
|
||
bareTopWrite(ctx, stdout, '\x1b[?1000h\x1b[?1002h\x1b[?1006h')
|
||
}
|
||
|
||
async function tick() {
|
||
if (
|
||
liteAutoOn &&
|
||
typeof bareTopFetchEwmaMs === 'number' &&
|
||
bareTopFetchEwmaMs >= 800
|
||
)
|
||
slowFetchStreak++
|
||
else slowFetchStreak = 0
|
||
const fetchOpts =
|
||
liteAutoOn && slowFetchStreak >= 3 ? { forceLite: true } : undefined
|
||
lastSnap = await bareTopFetchSnapshot(ctx, fetchOpts)
|
||
const m = lastSnap.metricsLive
|
||
const sess =
|
||
m && typeof m.session === 'object' && m.session
|
||
? /** @type {Record<string, unknown>} */ (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)
|
||
|
||
const rl = m && m.replicationLive && typeof m.replicationLive === 'object'
|
||
? /** @type {Record<string, unknown>} */ (m.replicationLive)
|
||
: null
|
||
const wa =
|
||
rl && rl.warmReplAdaptive && typeof rl.warmReplAdaptive === 'object'
|
||
? /** @type {Record<string, unknown>} */ (rl.warmReplAdaptive)
|
||
: null
|
||
const skipN =
|
||
wa && typeof wa.skippedFullFlushCount === 'number'
|
||
? wa.skippedFullFlushCount
|
||
: 0
|
||
pushRing(ringReplSkip, skipN)
|
||
const cs =
|
||
rl &&
|
||
rl.collaborationSession &&
|
||
typeof rl.collaborationSession === 'object'
|
||
? /** @type {Record<string, unknown>} */ (rl.collaborationSession)
|
||
: null
|
||
const pRx =
|
||
cs && typeof cs.protomuxAppChannelRxTotal === 'number'
|
||
? cs.protomuxAppChannelRxTotal
|
||
: 0
|
||
pushRing(ringProtoMux, pRx)
|
||
pushRing(ringSessionWall, wm)
|
||
let netSum = 0
|
||
const ns = lastSnap.netSummary
|
||
if (ns && typeof ns === 'object' && Array.isArray(ns.interfaces)) {
|
||
for (const iface of ns.interfaces) {
|
||
if (!iface || typeof iface !== 'object') continue
|
||
const ir = /** @type {Record<string, unknown>} */ (iface)
|
||
netSum += (Number(ir.rxBytes) || 0) + (Number(ir.txBytes) || 0)
|
||
}
|
||
}
|
||
let dNet = 0
|
||
if (prevNetSum >= 0) dNet = Math.max(0, netSum - prevNetSum)
|
||
prevNetSum = netSum
|
||
pushRing(ringNetDelta, dNet)
|
||
|
||
const sw0 = lastSnap.extra && lastSnap.extra.swarm
|
||
let churn = 0
|
||
if (sw0 && typeof sw0 === 'object') {
|
||
const s = /** @type {Record<string, unknown>} */ (sw0)
|
||
churn = Number(s.connectionChurn) || Number(s.churn) || 0
|
||
}
|
||
pushRing(ringSwarmChurn, churn)
|
||
if (prevPeerCount >= 0)
|
||
pushRing(ringPeerDeriv, peers - prevPeerCount)
|
||
else pushRing(ringPeerDeriv, 0)
|
||
prevPeerCount = peers
|
||
|
||
let hdN = 0
|
||
const hh = lastSnap.extra && lastSnap.extra.hdmsHealth
|
||
if (hh && typeof hh === 'object') {
|
||
const o = /** @type {Record<string, unknown>} */ (hh)
|
||
const sc = o.score
|
||
hdN =
|
||
typeof sc === 'number'
|
||
? sc
|
||
: typeof o.ok === 'boolean'
|
||
? o.ok
|
||
? 100
|
||
: 0
|
||
: 0
|
||
}
|
||
pushRing(ringHdms, hdN)
|
||
let dhtN = 0
|
||
const ds = lastSnap.extra && lastSnap.extra.dhtStatus
|
||
if (ds && typeof ds === 'object') {
|
||
const o = /** @type {Record<string, unknown>} */ (ds)
|
||
dhtN = Number(o.peers) || Number(o.nodes) || 0
|
||
}
|
||
pushRing(ringDht, dhtN)
|
||
let bdg = 0
|
||
const br = lastSnap.subprocessBridge
|
||
if (br && typeof br === 'object') {
|
||
const o = /** @type {Record<string, unknown>} */ (br)
|
||
bdg = Number(o.hostPidMapEntryCount) || 0
|
||
}
|
||
pushRing(ringBridge, bdg)
|
||
}
|
||
|
||
function currentProcRows() {
|
||
const sn = lastSnap
|
||
const pt =
|
||
sn &&
|
||
sn.extra &&
|
||
sn.extra.processTable &&
|
||
typeof sn.extra.processTable === 'object'
|
||
? sn.extra.processTable
|
||
: null
|
||
const rawRows = bareTopProcessRowsFromTable(pt)
|
||
const tertiaryPpid =
|
||
envTop.BARE_TOP_SORT_PPID_TIE === '1' ||
|
||
envTop.BARE_TOP_SORT_PPID_TIE === 'true'
|
||
const filtered = rawRows.filter((r) =>
|
||
typeof bareTopProcessRowMatchesFilter === 'function'
|
||
? bareTopProcessRowMatchesFilter(
|
||
/** @type {Record<string, unknown>} */ (r),
|
||
filterProcess,
|
||
envTop,
|
||
{ taggedPids, taggedOnly: procTaggedOnly }
|
||
)
|
||
: !filterProcess.length ||
|
||
String(
|
||
/** @type {Record<string, unknown>} */ (r).name ||
|
||
/** @type {Record<string, unknown>} */ (r).label ||
|
||
''
|
||
)
|
||
.toLowerCase()
|
||
.includes(filterProcess.toLowerCase())
|
||
)
|
||
const wallMs = sn && sn.fetchWallMs != null ? Number(sn.fetchWallMs) : 1000
|
||
const sorted = bareTopSortProcessRows(filtered, procSortKey, procSortAsc, {
|
||
tertiaryPpid,
|
||
sortWallMs: wallMs
|
||
})
|
||
return procTreeMode ? bareTopProcessTreeOrder(sorted) : sorted
|
||
}
|
||
|
||
/** @type {string} */
|
||
let out = ''
|
||
let hdrRow = 2
|
||
|
||
function draw() {
|
||
const now = Date.now()
|
||
if (resizePulse) {
|
||
resizePulse = false
|
||
lastDrawAt = 0
|
||
}
|
||
if (
|
||
uiMinMs > 0 &&
|
||
now - lastDrawAt < uiMinMs &&
|
||
!helpMode &&
|
||
!setupMode
|
||
)
|
||
return
|
||
if (
|
||
Date.now() < resizeDebounceUntil &&
|
||
!helpMode &&
|
||
!setupMode
|
||
)
|
||
return
|
||
const tDrawStart = profileOn ? Date.now() : 0
|
||
lastDrawAt = now
|
||
const { cols, rows, emergencyNarrow } = termDims()
|
||
const narrow = cols < 80
|
||
const layoutEffective =
|
||
layout === 'stacked' && layoutAutoWide && cols >= 100 ? 'even' : layout
|
||
let scrollHint = ''
|
||
const softIncr =
|
||
incrementalClear || (no2jAfterFirst && firstDrawDone && !helpMode)
|
||
out = softIncr ? '\x1b[?25l\x1b[H' : '\x1b[?25l\x1b[2J\x1b[H'
|
||
firstDrawDone = true
|
||
const sparkAscii = asciiGraphEff || 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'
|
||
if (
|
||
titleVersionOn &&
|
||
lastSnap &&
|
||
lastSnap.fileTexts &&
|
||
lastSnap.fileTexts.version
|
||
) {
|
||
const vx = String(lastSnap.fileTexts.version)
|
||
.trim()
|
||
.split('\n')[0]
|
||
.slice(0, 24)
|
||
if (vx) title += ' v:' + vx
|
||
}
|
||
const pad = Math.max(0, cols - title.length)
|
||
out += headerLine(title + ' '.repeat(pad)) + '\r\n'
|
||
hdrRow = 2
|
||
if (scrollRegionOn) out += '\x1b[r'
|
||
|
||
if (helpMode) {
|
||
const dim = useColor ? pal.dim : ''
|
||
const kw = useColor ? pal.kw : ''
|
||
const rst = useColor ? pal.reset : ''
|
||
const hp = Math.min(1, Math.max(0, helpPageIdx))
|
||
out +=
|
||
'\r\n' +
|
||
kw +
|
||
bareTopStrings.helpKeys +
|
||
(hp ? ' (page 2/2)' : ' (page 1/2)') +
|
||
rst +
|
||
'\r\n' +
|
||
dim +
|
||
'PgUp / PgDn more help pages' +
|
||
rst +
|
||
'\r\n\r\n'
|
||
if (hp === 0) {
|
||
out +=
|
||
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' +
|
||
' Tab Next tab Shift+Tab Prev tab\r\n' +
|
||
' [ ] Prev / next tab\r\n' +
|
||
' 1-9 Jump tabs 1–9; 0 = disk; C / M Cpu / Mem tabs\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 H host C M cpu mem 0 disk\r\n' +
|
||
'\r\n' +
|
||
dim +
|
||
'Actions (1/2)' +
|
||
rst +
|
||
'\r\n' +
|
||
' q Quit r refresh R sort asc/desc sp pause . step (paused)\r\n' +
|
||
' d delta e export E tab export f full F follow pid t clock\r\n' +
|
||
' / Filter (features tab too) % tagged-only = tag pid c copy pid\r\n' +
|
||
' n F7 Renice prompt a action menu z tree collapse\r\n' +
|
||
' i o e Process detail subviews (summary / fds / env)\r\n'
|
||
} else {
|
||
out +=
|
||
dim +
|
||
'Actions (2/2)' +
|
||
rst +
|
||
'\r\n' +
|
||
' < > Sort processes F6 S sort menu V tree x initd alternate view\r\n' +
|
||
' Enter Process detail panel k F9 signal (HUP USR1 USR2 TERM INT KILL)\r\n' +
|
||
' F2 Setup F3 F4 row jump (matches / filter substring when set)\r\n' +
|
||
' F10 Quit h ? help\r\n' +
|
||
'\r\n' +
|
||
dim +
|
||
'Notes' +
|
||
rst +
|
||
'\r\n' +
|
||
bareTopStrings.overviewPinned +
|
||
'\r\n' +
|
||
bareTopStrings.noHtop +
|
||
'\r\n' +
|
||
bareTopStrings.noHtop2 +
|
||
'\r\n' +
|
||
'Pear IPC: bare_os_*, pear:* (booter pear_ipc registry).\r\n' +
|
||
"Overview: ' then 1-9,0 jumps to Nth visible section (wrap-aware scroll).\r\n" +
|
||
'Config: ~/.config/baretoprc or BARE_TOP_CONFIG_PATH (JSON; keys.quit etc.).\r\n' +
|
||
(scrollRegionOn
|
||
? 'BARE_TOP_SCROLL_REGION=1: DECSTBM on overview detail or process list (experimental).\r\n'
|
||
: '') +
|
||
'BARE_TOP_TAB_MERGE=netop merges operator summary into network tab.\r\n' +
|
||
'\r\n' +
|
||
bareTopStrings.pressCloseHelp +
|
||
'\r\n'
|
||
}
|
||
const used = 30
|
||
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)
|
||
lastLineFrame = null
|
||
return
|
||
}
|
||
|
||
if (setupMode) {
|
||
const dim = useColor ? pal.dim : ''
|
||
const kw = useColor ? pal.kw : ''
|
||
const rst = useColor ? pal.reset : ''
|
||
out +=
|
||
'\r\n' +
|
||
kw +
|
||
bareTopStrings.setupTitle +
|
||
rst +
|
||
'\r\n' +
|
||
dim +
|
||
'Display' +
|
||
rst +
|
||
'\r\n' +
|
||
' BARE_TOP_THEME dark | light | none\r\n' +
|
||
' BARE_TOP_HIGH_CONTRAST 1\r\n' +
|
||
' BARE_TOP_ASCII_UI 1 (boxes + ASCII sparks)\r\n' +
|
||
'\r\n' +
|
||
dim +
|
||
'Refresh' +
|
||
rst +
|
||
'\r\n' +
|
||
' BARE_TOP_INTERVAL_MS 250–10000 (default 1000)\r\n' +
|
||
' BARE_TOP_UI_MIN_MS throttle redraws\r\n' +
|
||
'\r\n' +
|
||
dim +
|
||
'Incremental terminal updates' +
|
||
rst +
|
||
'\r\n' +
|
||
' BARE_TOP_INCREMENTAL 1 = soft home 2 = line-diff (default ~2 when TERM ok)\r\n' +
|
||
' BARE_TOP_FULL_REDRAW 1 = disable default incremental patches\r\n' +
|
||
' BARE_TOP_NO_2J_AFTER_FIRST 1\r\n' +
|
||
' BARE_TOP_LAYOUT_AUTO 1 = use even split when cols>=100\r\n' +
|
||
' BARE_TOP_SNAPSHOT_LITE 1 = smaller /proc batch (see baretop -h)\r\n' +
|
||
' BARE_TOP_NET_FILTER 1 = / filter applies on network tab\r\n' +
|
||
' BARE_TOP_CMD_ELLIPSIS_MIDDLE 1 = middle-ellipsis long process names\r\n' +
|
||
' BARE_TOP_PROC_DETAIL_JSON 1 = pretty-print JSON in process detail (Enter)\r\n' +
|
||
' BARE_TOP_DEBUG_TIMINGS 1 = throttled fetch/draw ms on stderr\r\n' +
|
||
' BARE_TOP_FETCH_EWMA 1 = lower /proc batch concurrency when fetches are slow\r\n' +
|
||
' BARE_TOP_SNAPSHOT_LITE_AUTO 1 = auto lite batch after sustained slow fetches\r\n' +
|
||
' BARE_TOP_GRAPH_MODE unicode | ascii | braille\r\n' +
|
||
'\r\n' +
|
||
dim +
|
||
'Health' +
|
||
rst +
|
||
'\r\n' +
|
||
' BARE_TOP_HEALTH_DETAIL 1 = penalty breakdown on overview\r\n' +
|
||
'\r\n' +
|
||
dim +
|
||
'Sample meter (current theme)' +
|
||
rst +
|
||
'\r\n ' +
|
||
bareTopFormatMeterBar(
|
||
62,
|
||
14,
|
||
asciiGraphEff || monoUi,
|
||
{
|
||
low: pal.meterLow,
|
||
mid: pal.meterMid,
|
||
hi: pal.meterHi,
|
||
reset: pal.reset
|
||
},
|
||
useColor
|
||
) +
|
||
' 62%\r\n' +
|
||
'\r\n' +
|
||
bareTopStrings.pressCloseSetup +
|
||
'\r\n'
|
||
const usedS = 48
|
||
for (let r = usedS; 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)
|
||
lastLineFrame = null
|
||
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 staleBucket = staleMs > 2000 ? 1 : 0
|
||
const stalePulse = useColor && staleBucket > lastStaleBucket
|
||
lastStaleBucket = staleBucket
|
||
const staleStr =
|
||
staleMs > 2000
|
||
? (useColor ? (stalePulse ? '\x1b[1m' : '') + 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 : '')
|
||
: ''
|
||
const readErrS =
|
||
snap && snap.readErr
|
||
? (useColor ? pal.warn : '') +
|
||
' readErr ' +
|
||
bareTopSanitizeVisible(String(snap.readErr)).slice(0, cols - 14) +
|
||
(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 used = mm.memTotal - mm.memAvail
|
||
if (used >= 0) {
|
||
const pctUsed = Math.round((used / mm.memTotal) * 100)
|
||
const barU = bareTopFormatMeterBar(
|
||
pctUsed,
|
||
mw2,
|
||
sparkAscii,
|
||
{
|
||
low: pal.meterLow,
|
||
mid: pal.meterMid,
|
||
hi: pal.meterHi,
|
||
reset: pal.reset
|
||
},
|
||
useColor
|
||
)
|
||
emitHdrLn(
|
||
(useColor ? pal.dim : '') +
|
||
' MemUsed ' +
|
||
barU +
|
||
' ' +
|
||
pctUsed +
|
||
'%' +
|
||
(useColor ? pal.reset : '')
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
if (snap && snap.swapinfoLine) {
|
||
let lowSwap = false
|
||
const swT = snap.swapinfoLine.match(/SwapTotal:\s*(\d+)/i)
|
||
const swF = snap.swapinfoLine.match(/SwapFree:\s*(\d+)/i)
|
||
if (swT && swF && Number(swT[1]) > 0)
|
||
lowSwap = Number(swF[1]) / Number(swT[1]) < 0.12
|
||
const pre =
|
||
useColor && lowSwap ? pal.warn : useColor ? pal.dim : ''
|
||
const rst = useColor ? pal.reset : ''
|
||
emitHdrLn(pre + ' ' + snap.swapinfoLine + rst)
|
||
}
|
||
|
||
if (snap && snap.cpuCoreCount > 0) {
|
||
emitHdrLn(
|
||
(useColor ? pal.dim : '') +
|
||
' logical cpus ' +
|
||
snap.cpuCoreCount +
|
||
(useColor ? pal.reset : '')
|
||
)
|
||
}
|
||
|
||
if (readErrS) emitHdrLn(readErrS)
|
||
|
||
{
|
||
const la = bareTopParseLoadavg(load)
|
||
let loadCol = load || ''
|
||
if (la) {
|
||
const hi =
|
||
la.one >= 8
|
||
? pal.bad
|
||
: la.one >= 4
|
||
? pal.warn
|
||
: la.one >= 1
|
||
? pal.dim
|
||
: pal.good
|
||
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<string, unknown>} */ (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])
|
||
}
|
||
|
||
{
|
||
const ptU =
|
||
(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 upt = bareTopSessionUptimeLine(ptU, Date.now())
|
||
if (upt) {
|
||
emitHdrLn(
|
||
(useColor ? pal.dim : '') + upt + (useColor ? pal.reset : '')
|
||
)
|
||
}
|
||
}
|
||
|
||
if (!emergencyNarrow && snap && snap.hostOs && typeof snap.hostOs === 'object') {
|
||
const hl = bareTopFlattenLimited('Host OS', snap.hostOs, 0, 3, 24, 16)
|
||
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
|
||
const T_HOST = TAB_NAMES.indexOf('host')
|
||
const T_CPU = TAB_NAMES.indexOf('cpu')
|
||
const T_MEM = TAB_NAMES.indexOf('mem')
|
||
const T_DISK = TAB_NAMES.indexOf('disk')
|
||
for (let ti = 0; ti < NTABS; ti++) {
|
||
const nm = TAB_NAMES[ti] || 't'
|
||
let lab
|
||
if (short) {
|
||
if (ti === NTABS - 1) lab = '0'
|
||
else if (ti === T_HOST) lab = 'H'
|
||
else if (ti === T_CPU) lab = 'C'
|
||
else if (ti === T_MEM) lab = 'M'
|
||
else if (ti === T_DISK) lab = 'D'
|
||
else if (ti < 9) lab = String(ti + 1)
|
||
else lab = nm.charAt(0).toUpperCase()
|
||
} else if (ti === NTABS - 1) lab = '0:' + nm.slice(0, 2)
|
||
else if (ti === T_HOST) lab = 'H:' + nm.slice(0, 2)
|
||
else if (ti === T_CPU) lab = 'C:' + nm.slice(0, 2)
|
||
else if (ti === T_MEM) lab = 'M:' + 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 && !highContrast)
|
||
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 =
|
||
layoutEffective === '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<string, unknown>} */ (m.session)
|
||
: {}
|
||
const ec = Number(sess.execLineCount) || 0
|
||
const pb = Number(sess.pipelineBytesTotal) || 0
|
||
const wm = Number(sess.execLineWallMsTotal) || 0
|
||
const sw = split ? Math.floor(sparkW / 2) : sparkW
|
||
const rstOv = useColor ? pal.reset : ''
|
||
|
||
emitHdrLn('')
|
||
emitHdrLn(
|
||
(useColor ? pal.kw : '') +
|
||
(icons ? '\u2022 ' : '') +
|
||
bareTopStrings.activity +
|
||
rstOv
|
||
)
|
||
emitHdrLn(
|
||
' exec dlt ' +
|
||
bareTopSparkline(ringExecDelta, sw, sparkAscii, logSpark, sparkBrailleEff)
|
||
)
|
||
{
|
||
const pipSpark = bareTopSparkline(
|
||
ringPipeDelta,
|
||
sw,
|
||
sparkAscii,
|
||
logSpark,
|
||
sparkBrailleEff
|
||
)
|
||
const pipSuf = bareTopFormatBytes(pb)
|
||
const pipPad =
|
||
narrow && pipSpark.length < sparkW
|
||
? ' '.repeat(Math.max(0, sparkW - pipSpark.length))
|
||
: ' '
|
||
emitHdrLn(' pipe dlt ' + pipSpark + pipPad + pipSuf)
|
||
}
|
||
{
|
||
const wSpark = bareTopSparkline(
|
||
ringWallDelta,
|
||
sw,
|
||
sparkAscii,
|
||
logSpark,
|
||
sparkBrailleEff
|
||
)
|
||
const wSuf = bareTopFormatDuration(wm)
|
||
const wPad =
|
||
narrow && wSpark.length < sparkW
|
||
? ' '.repeat(Math.max(0, sparkW - wSpark.length))
|
||
: ' '
|
||
emitHdrLn(' wall dlt ' + wSpark + wPad + wSuf)
|
||
}
|
||
emitHdrLn(
|
||
' peers ' +
|
||
bareTopSparkline(ringPeers, sw, sparkAscii, logSpark, sparkBrailleEff) +
|
||
' ' +
|
||
(ringPeers.length ? String(ringPeers[ringPeers.length - 1]) : '')
|
||
)
|
||
if (ringReplSkip.length) {
|
||
emitHdrLn(
|
||
(useColor ? pal.dim : '') +
|
||
' repl skip ' +
|
||
bareTopSparkline(
|
||
ringReplSkip,
|
||
Math.min(24, sw),
|
||
sparkAscii,
|
||
logSpark,
|
||
sparkBrailleEff
|
||
) +
|
||
' ' +
|
||
String(ringReplSkip[ringReplSkip.length - 1]) +
|
||
rstOv
|
||
)
|
||
}
|
||
|
||
if (deltaMode && prevSnap && prevSnap.metricsLive) {
|
||
const ps = /** @type {Record<string, unknown>} */ (
|
||
prevSnap.metricsLive.session || {}
|
||
)
|
||
const pec = Number(ps.execLineCount) || 0
|
||
const ppb = Number(ps.pipelineBytesTotal) || 0
|
||
const pwm = Number(ps.execLineWallMsTotal) || 0
|
||
emitHdrLn(
|
||
(useColor ? pal.dim : '') +
|
||
' d exec ' +
|
||
(ec - pec) +
|
||
' d pipe ' +
|
||
(pb - ppb) +
|
||
' d wall ' +
|
||
(wm - pwm) +
|
||
rstOv
|
||
)
|
||
}
|
||
|
||
emitHdrLn((useColor ? pal.kw : '') + bareTopStrings.session + rstOv)
|
||
emitHdrLn(
|
||
' execLineCount=' +
|
||
String(sess.execLineCount ?? bareTopStrings.na) +
|
||
' pipelineBytesTotal=' +
|
||
String(sess.pipelineBytesTotal ?? bareTopStrings.na)
|
||
)
|
||
emitHdrLn(
|
||
' execLineWallMsTotal=' +
|
||
String(sess.execLineWallMsTotal ?? bareTopStrings.na)
|
||
)
|
||
|
||
const pg = bareTopPipelineGauges(m && m.pipeline, cols, pal, useColor)
|
||
if (pg) {
|
||
emitHdrLn((useColor ? pal.kw : '') + bareTopStrings.pipeline + rstOv)
|
||
emitHdrLn(pg)
|
||
}
|
||
|
||
const delI = m && m.delegateInflight
|
||
emitHdrLn((useColor ? pal.kw : '') + bareTopStrings.delegates + rstOv)
|
||
const hist = bareTopDelegateHistogram(
|
||
delI,
|
||
cols - 4,
|
||
use256,
|
||
pal,
|
||
useColor
|
||
)
|
||
if (hist) emitHdrLn(' ' + hist)
|
||
|
||
if (split) {
|
||
const ptMini =
|
||
snap.extra &&
|
||
snap.extra.processTable &&
|
||
typeof snap.extra.processTable === 'object'
|
||
? snap.extra.processTable
|
||
: null
|
||
const mini = bareTopSortProcessRows(
|
||
bareTopProcessRowsFromTable(ptMini),
|
||
'time',
|
||
false
|
||
).slice(0, 6)
|
||
const nowM = Date.now()
|
||
const bits = mini.map((r) => {
|
||
const age = bareTopFormatProcAge(
|
||
bareTopProcessStartedMs(r),
|
||
nowM
|
||
)
|
||
return (
|
||
bareTopTruncateCell(String(r.name || ''), 18, true) +
|
||
(age ? '(' + age + ')' : '')
|
||
)
|
||
})
|
||
emitHdrLn(
|
||
(useColor ? pal.dim : '') +
|
||
' top jobs ' +
|
||
bits.join(' ') +
|
||
rstOv
|
||
)
|
||
}
|
||
|
||
const ov = bareTopOverviewLines(snap, {
|
||
cols: cols - 2,
|
||
na: bareTopStrings.na,
|
||
compact: overviewCompact,
|
||
sectionsRaw: overviewSectionsRaw,
|
||
sectionFilter: overviewSectionFilter,
|
||
deltaMode,
|
||
prevMetrics:
|
||
prevSnap && prevSnap.metricsLive
|
||
? /** @type {Record<string, unknown>} */ (prevSnap.metricsLive)
|
||
: null,
|
||
prevSnap: deltaMode
|
||
? /** @type {Record<string, unknown>} */ (prevSnap)
|
||
: null,
|
||
nowMs: Date.now(),
|
||
asciiSep: monoUi,
|
||
flattenCap: (v, maxL, maxK) =>
|
||
bareTopFlattenLimited('', v, 1, 4, maxK, maxL),
|
||
ringProto: ringProtoMux,
|
||
protomuxSpark: protomuxSparkOn,
|
||
sparkW: Math.min(32, sw),
|
||
sparkAscii,
|
||
logSpark,
|
||
braille: sparkBrailleEff,
|
||
healthDetail: healthDetailOn,
|
||
healthBreakdown: String(snap.healthBreakdown || ''),
|
||
splitLeftCol: 0,
|
||
splitMiniProc: false,
|
||
layoutVersion: bareTopStrings.layoutVersion,
|
||
sessionWallRing: ringSessionWall
|
||
})
|
||
activeOverviewSections = ov.activeSections
|
||
lastOvLines = ov.lines
|
||
lastOvSectionRaw =
|
||
ov.sectionRawLineIndex && typeof ov.sectionRawLineIndex === 'object'
|
||
? ov.sectionRawLineIndex
|
||
: {}
|
||
if (scrollRegionOn && hdrRow < mainEnd - 1) {
|
||
out += '\x1b[' + hdrRow + ';' + (mainEnd - 1) + 'r'
|
||
}
|
||
drawScrollableLines(ov.lines, 0, hdrRow)
|
||
if (scrollRegionOn && tab === 0) out += '\x1b[r'
|
||
} 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,
|
||
sparkBrailleEff
|
||
) +
|
||
' ' +
|
||
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 ? '+' : '-') +
|
||
(procTreeMode ? ' tree' : '') +
|
||
(filterProcess ? ' filter:' + filterProcess : '') +
|
||
(procTaggedOnly ? ' tagged' : '') +
|
||
(taggedPids.size ? ' #' + taggedPids.size : '')
|
||
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<string, unknown>[]} */
|
||
const rawRows = bareTopProcessRowsFromTable(pt)
|
||
/** @type {Record<string, unknown>[]} */
|
||
let displayList = currentProcRows()
|
||
if (procTreeMode && treeCollapsedPPids.size) {
|
||
displayList = displayList.filter(
|
||
(r) => !treeCollapsedPPids.has(Number(r.ppid) || 0)
|
||
)
|
||
}
|
||
if (procFollowOn && followPid > 0) {
|
||
const ix = displayList.findIndex((r) => bareTopProcessPid(r) === followPid)
|
||
if (ix >= 0) procCursor = ix
|
||
}
|
||
if (displayList[procCursor])
|
||
followPid = bareTopProcessPid(
|
||
/** @type {Record<string, unknown>} */ (displayList[procCursor])
|
||
)
|
||
const pf = filterProcess.toLowerCase()
|
||
/** @type {Record<string, number>} */
|
||
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 ' +
|
||
displayList.length +
|
||
(pf ? ' (filtered)' : '') +
|
||
(procTreeMode ? ' tree' : '') +
|
||
' ' +
|
||
cnt +
|
||
rst
|
||
)
|
||
if (procCursor >= displayList.length)
|
||
procCursor = Math.max(0, displayList.length - 1)
|
||
const mVis = Math.max(1, mainEnd - hdrRow)
|
||
let scrollTop = scrollRows[TAB_PROC]
|
||
const maxScr = Math.max(0, displayList.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 = displayList.find(
|
||
(x) => bareTopProcessPid(x) === procDetailPid
|
||
)
|
||
const raw =
|
||
row && typeof row === 'object'
|
||
? /** @type {Record<string, unknown>} */ (row)
|
||
: { note: 'pid not in list (check filter)' }
|
||
/** @type {Record<string, unknown>} */
|
||
let detailObj = raw
|
||
if (procDetailSub === 'summary') {
|
||
detailObj = {
|
||
pid: raw.pid,
|
||
ppid: raw.ppid,
|
||
name: raw.name,
|
||
state: raw.state,
|
||
role: raw.role,
|
||
replicationHint: raw.replicationHint
|
||
}
|
||
} else if (procDetailSub === 'fds') {
|
||
detailObj =
|
||
raw.fdTable && typeof raw.fdTable === 'object'
|
||
? /** @type {Record<string, unknown>} */ (raw.fdTable)
|
||
: { note: 'no fdTable on row' }
|
||
} else if (procDetailSub === 'env') {
|
||
detailObj =
|
||
raw.env && typeof raw.env === 'object'
|
||
? /** @type {Record<string, unknown>} */ (raw.env)
|
||
: { note: 'no env slice on row' }
|
||
}
|
||
const jl = bareTopProcDetailBodyLines(
|
||
/** @type {'all' | 'summary' | 'fds' | 'env'} */ (procDetailSub),
|
||
detailObj,
|
||
cols,
|
||
bareTopProcDetailMaxLines(rows, hdrRow),
|
||
envTop
|
||
)
|
||
emitHdrLn(
|
||
(useColor ? pal.dim : '') +
|
||
' (Enter closes) i summary o fds e env sub:' +
|
||
procDetailSub +
|
||
rst
|
||
)
|
||
drawScrollableLines(jl, TAB_PROC, hdrRow)
|
||
} else {
|
||
const cmdMid =
|
||
envTop.BARE_TOP_CMD_ELLIPSIS_MIDDLE === '1' ||
|
||
envTop.BARE_TOP_CMD_ELLIPSIS_MIDDLE === 'true'
|
||
let showNi = false
|
||
let showPri = false
|
||
let showCpu = false
|
||
for (const rr of displayList) {
|
||
if (!rr || typeof rr !== 'object') continue
|
||
if (rr.nice != null || rr.ni != null) showNi = true
|
||
if (rr.pri != null || rr.priority != null) showPri = true
|
||
if (rr.cpuPct != null || rr.cpu != null) showCpu = true
|
||
}
|
||
const wNi = showNi ? 4 : 0
|
||
const wPri = showPri ? 4 : 0
|
||
const wCpu = showCpu ? 5 : 0
|
||
const wPid = 5
|
||
const wPpid = 5
|
||
const wSt = 9
|
||
const wPg = 5
|
||
const wSid = 3
|
||
const wTime = 7
|
||
const fixed =
|
||
wPid +
|
||
wPpid +
|
||
wSt +
|
||
wPg +
|
||
wSid +
|
||
wTime +
|
||
wNi +
|
||
wPri +
|
||
wCpu +
|
||
7
|
||
const wName = Math.max(6, cols - fixed + procNameWidthAdj)
|
||
const hdrCells = ['PID', 'PPID', 'PGID', 'ST', 'SID']
|
||
/** @type {number[]} */
|
||
const hdrWidths = [wPid, wPpid, wPg, wSt, wSid]
|
||
/** @type {('l'|'r')[]} */
|
||
const hdrAlign = ['r', 'r', 'r', 'l', 'r']
|
||
if (showNi) {
|
||
hdrCells.push('NI')
|
||
hdrWidths.push(wNi)
|
||
hdrAlign.push('r')
|
||
}
|
||
if (showPri) {
|
||
hdrCells.push('PRI')
|
||
hdrWidths.push(wPri)
|
||
hdrAlign.push('r')
|
||
}
|
||
if (showCpu) {
|
||
hdrCells.push('CPU%')
|
||
hdrWidths.push(wCpu)
|
||
hdrAlign.push('r')
|
||
}
|
||
hdrCells.push(deltaMode ? 'D.TIME' : 'TIME', 'NAME')
|
||
hdrWidths.push(wTime, wName)
|
||
hdrAlign.push('r', 'l')
|
||
const nowP = Date.now()
|
||
const hdrLn =
|
||
' ' +
|
||
bareTopFormatFixedColumns(
|
||
hdrCells,
|
||
hdrWidths,
|
||
hdrAlign,
|
||
cols - 2
|
||
)
|
||
emitHdrLn((useColor ? pal.header : '') + hdrLn + rst)
|
||
const rowStart = hdrRow
|
||
if (
|
||
scrollRegionOn &&
|
||
rowStart <= mainEnd - 1 &&
|
||
mainEnd > rowStart
|
||
) {
|
||
out += '\x1b[' + rowStart + ';' + (mainEnd - 1) + 'r'
|
||
}
|
||
let r = rowStart
|
||
/** @type {Set<number>} */
|
||
const ancestorPids = new Set()
|
||
if (procTreeMode && displayList[procCursor]) {
|
||
const byPid = new Map(
|
||
displayList.map((x) => [bareTopProcessPid(x), x])
|
||
)
|
||
let walk = displayList[procCursor]
|
||
for (let g = 0; g < 64 && walk; g++) {
|
||
const p = bareTopProcessPid(walk)
|
||
if (p) ancestorPids.add(p)
|
||
const pp = Number(walk.ppid) || 0
|
||
walk = pp ? /** @type {Record<string, unknown>} */ (byPid.get(pp)) : null
|
||
}
|
||
}
|
||
for (let j = 0; j < mVis && scrollTop + j < displayList.length; j++) {
|
||
const row = /** @type {Record<string, unknown>} */ (
|
||
displayList[scrollTop + j]
|
||
)
|
||
const pid = bareTopProcessPid(row)
|
||
const depth = Number(row._treeDepth) || 0
|
||
const ind = procTreeMode
|
||
? monoUi
|
||
? '+-- '.repeat(Math.min(5, depth))
|
||
: '\u251c\u2500 '.repeat(Math.min(5, depth))
|
||
: ''
|
||
const tStr = bareTopFormatProcAge(
|
||
bareTopProcessStartedMs(row),
|
||
nowP
|
||
)
|
||
const rawNm = ind + String(row.name ?? '')
|
||
const nm = cmdMid
|
||
? bareTopTruncateMiddle(rawNm, wName)
|
||
: bareTopTruncateCell(rawNm, wName, true)
|
||
/** @type {string[]} */
|
||
const cells = [
|
||
String(pid),
|
||
String(row.ppid ?? ''),
|
||
String(row.pgid ?? ''),
|
||
String(row.state ?? '').slice(0, wSt),
|
||
String(row.sid ?? '')
|
||
]
|
||
if (showNi)
|
||
cells.push(
|
||
String(
|
||
row.nice != null ? row.nice : row.ni != null ? row.ni : ''
|
||
)
|
||
)
|
||
if (showPri)
|
||
cells.push(
|
||
String(
|
||
row.pri != null
|
||
? row.pri
|
||
: row.priority != null
|
||
? row.priority
|
||
: ''
|
||
)
|
||
)
|
||
if (showCpu) {
|
||
const wallP =
|
||
snap && snap.fetchWallMs != null ? Number(snap.fetchWallMs) : 1000
|
||
const c0 =
|
||
typeof bareTopEffectiveCpuPct === 'function'
|
||
? bareTopEffectiveCpuPct(
|
||
/** @type {Record<string, unknown>} */ (row),
|
||
wallP
|
||
)
|
||
: row.cpuPct ?? row.cpu
|
||
cells.push(
|
||
typeof c0 === 'number' && Number.isFinite(c0)
|
||
? String(Math.round(c0))
|
||
: String(c0 ?? '')
|
||
)
|
||
}
|
||
cells.push(tStr || '—', nm)
|
||
/** @type {number[]} */
|
||
const cw = [wPid, wPpid, wPg, wSt, wSid]
|
||
if (showNi) cw.push(wNi)
|
||
if (showPri) cw.push(wPri)
|
||
if (showCpu) cw.push(wCpu)
|
||
cw.push(wTime, wName)
|
||
/** @type {('l'|'r')[]} */
|
||
const ca = ['r', 'r', 'r', 'l', 'r']
|
||
if (showNi) ca.push('r')
|
||
if (showPri) ca.push('r')
|
||
if (showCpu) ca.push('r')
|
||
ca.push('r', 'l')
|
||
const line =
|
||
' ' +
|
||
bareTopFormatFixedColumns(cells, cw, ca, 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 : ''
|
||
const stl = String(row.state || '').toLowerCase()
|
||
let rowTone = ''
|
||
let rowToneX = ''
|
||
if (useColor && !sel) {
|
||
const nmLc = String(row.name || '').toLowerCase()
|
||
if (pf && nmLc.includes(pf)) {
|
||
rowTone = pal.barHi
|
||
rowToneX = rst
|
||
} else if (stl === 'zombie') {
|
||
rowTone = pal.bad
|
||
rowToneX = rst
|
||
} else if (stl === 'stopped' || stl === 'signaled') {
|
||
rowTone = pal.warn
|
||
rowToneX = rst
|
||
} else if (stl === 'running') {
|
||
rowTone = pal.good
|
||
rowToneX = rst
|
||
}
|
||
const role = String(row.role || '')
|
||
if (!rowTone && role.includes('shell'))
|
||
rowTone = pal.roleShell || pal.kw
|
||
else if (!rowTone && role.includes('job'))
|
||
rowTone = pal.roleJob || pal.barHi
|
||
else if (!rowTone && role.includes('initd'))
|
||
rowTone = pal.roleInitd || pal.dim
|
||
if (rowTone && !rowToneX) rowToneX = rst
|
||
if (
|
||
!rowTone &&
|
||
procTreeMode &&
|
||
ancestorPids.size &&
|
||
!ancestorPids.has(pid)
|
||
) {
|
||
rowTone = pal.dim
|
||
rowToneX = rst
|
||
}
|
||
}
|
||
out +=
|
||
bareEditCup(r, 1) +
|
||
'\x1b[K' +
|
||
prefix +
|
||
left +
|
||
rowTone +
|
||
line.slice(0, Math.max(8, cols - 1 - framePad * 2)) +
|
||
rowToneX +
|
||
suf +
|
||
'\r\n'
|
||
r++
|
||
}
|
||
if (displayList.length > mVis) {
|
||
scrollHint =
|
||
' | ' +
|
||
(scrollTop + 1) +
|
||
'\u2013' +
|
||
Math.min(displayList.length, scrollTop + mVis) +
|
||
'/' +
|
||
displayList.length +
|
||
' @' +
|
||
(procCursor + 1)
|
||
}
|
||
}
|
||
} else if (TAB_NAMES[tab] === 'initd' && snap && snap.initdGraph) {
|
||
emitHdrLn('')
|
||
emitHdrLn(
|
||
(useColor ? pal.kw : '') +
|
||
(icons ? '\u2022 ' : '') +
|
||
'Initd graph' +
|
||
(useColor ? pal.reset : '') +
|
||
(filterInitd ? ' filter:' + filterInitd : '') +
|
||
(initdRawFallback ? ' [alternate layout]' : '')
|
||
)
|
||
emitHdrLn(
|
||
(useColor ? pal.dim : '') +
|
||
'x toggles alternate layout when a nodes list exists (edges / graph path)' +
|
||
(useColor ? pal.reset : '')
|
||
)
|
||
const g = snap.initdGraph
|
||
/** @type {string[]} */
|
||
let initLines = []
|
||
if (
|
||
!initdRawFallback &&
|
||
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)
|
||
}
|
||
if (
|
||
!initdRawFallback &&
|
||
typeof g === 'object' &&
|
||
g &&
|
||
Array.isArray(g.edges) &&
|
||
g.edges.length > 0 &&
|
||
g.edges.length <= 32
|
||
) {
|
||
initLines.push('Edges (DAG sketch):')
|
||
for (const e of g.edges) initLines.push(' ' + String(e))
|
||
}
|
||
drawScrollableLines(initLines, tab, hdrRow)
|
||
} else if (TAB_NAMES[tab] === 'network' && snap) {
|
||
emitHdrLn('')
|
||
emitHdrLn(
|
||
(useColor ? pal.kw : '') +
|
||
(icons ? '\u2022 ' : '') +
|
||
'Net summary' +
|
||
(useColor ? pal.reset : '') +
|
||
(filterNetwork ? ' filter:' + filterNetwork : '')
|
||
)
|
||
emitHdrLn(
|
||
(useColor ? pal.dim : '') +
|
||
'Logical network mirror from /proc/bare_os/net_summary.json' +
|
||
(useColor ? pal.reset : '')
|
||
)
|
||
if (ringNetDelta.length) {
|
||
const nsw = Math.min(32, sparkW)
|
||
const dt = Math.max(1, snap.fetchWallMs || intervalMs) / 1000
|
||
const rate = ringNetDelta.length
|
||
? ringNetDelta[ringNetDelta.length - 1] / dt
|
||
: 0
|
||
emitHdrLn(
|
||
(useColor ? pal.dim : '') +
|
||
' iface bytes d (sum) ' +
|
||
bareTopSparkline(
|
||
ringNetDelta,
|
||
nsw,
|
||
sparkAscii,
|
||
logSpark,
|
||
sparkBrailleEff
|
||
) +
|
||
' ~' +
|
||
bareTopFormatBytes(Math.round(rate)) +
|
||
'/s' +
|
||
(useColor ? pal.reset : '')
|
||
)
|
||
}
|
||
const maxNet = Math.min(480, Math.max(24, (mainEnd - hdrRow) * 4))
|
||
/** @type {string[]} */
|
||
let netBody = bareTopNetTabLines(snap.netSummary, cols, {
|
||
maxLines: maxNet,
|
||
filter: filterNetwork,
|
||
wideTwoCol: layoutEffective === 'even' && cols >= 100
|
||
})
|
||
if (netopMerge) {
|
||
const opPack = {
|
||
replication: snap.extra.replication,
|
||
replicationBackpressure: snap.extra.replicationBackpressure,
|
||
swarm: snap.extra.swarm,
|
||
syncWindow: snap.extra.syncWindow
|
||
}
|
||
const opLines = bareTopLinesFromPack(opPack, 6, cols)
|
||
netBody = [
|
||
(useColor ? pal.kw : '') + '— operator (merged) —' + (useColor ? pal.reset : ''),
|
||
...opLines,
|
||
(useColor ? pal.kw : '') + '— net_summary —' + (useColor ? pal.reset : ''),
|
||
...netBody
|
||
]
|
||
}
|
||
drawScrollableLines(netBody, tab, hdrRow)
|
||
} else if (TAB_NAMES[tab] === 'features' && snap) {
|
||
emitHdrLn('')
|
||
emitHdrLn(
|
||
(useColor ? pal.kw : '') +
|
||
(icons ? '\u2022 ' : '') +
|
||
'Features / capabilities' +
|
||
(useColor ? pal.reset : '') +
|
||
(filterFeatures ? ' filter:' + filterFeatures : '')
|
||
)
|
||
let fl = bareTopFeaturesTableLines(snap.features, cols)
|
||
const ff = filterFeatures.toLowerCase()
|
||
if (ff.length)
|
||
fl = fl.filter((ln) => ln.toLowerCase().includes(ff))
|
||
drawScrollableLines(fl, tab, hdrRow)
|
||
} else if (TAB_NAMES[tab] === 'diagnostics' && 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), tab, hdrRow)
|
||
} else if (TAB_NAMES[tab] === 'operator' && snap) {
|
||
emitHdrLn('')
|
||
emitHdrLn(
|
||
(useColor ? pal.kw : '') +
|
||
(icons ? '\u2022 ' : '') +
|
||
'Operator (replication, swarm, sync, staging)' +
|
||
(useColor ? pal.reset : '')
|
||
)
|
||
emitHdrLn(
|
||
(useColor ? pal.dim : '') +
|
||
'Operator workflow: P2P replication state, sync windows, staging — not shell jobs.' +
|
||
(useColor ? pal.reset : '')
|
||
)
|
||
const pack = {
|
||
replication: snap.extra.replication,
|
||
replicationBackpressure: snap.extra.replicationBackpressure,
|
||
swarm: snap.extra.swarm,
|
||
syncWindow: snap.extra.syncWindow
|
||
}
|
||
drawScrollableLines(bareTopLinesFromPack(pack, 6, cols), tab, hdrRow)
|
||
} else if (TAB_NAMES[tab] === 'pear' && snap) {
|
||
emitHdrLn('')
|
||
emitHdrLn(
|
||
(useColor ? pal.kw : '') +
|
||
(icons ? '\u2022 ' : '') +
|
||
'Pear (ipc, health, trust, peer_health)' +
|
||
(useColor ? pal.reset : '')
|
||
)
|
||
emitHdrLn(
|
||
(useColor ? pal.dim : '') +
|
||
'Pear IPC registry: bare_os_* and pear:* channels (see handbook).' +
|
||
(useColor ? pal.reset : '')
|
||
)
|
||
const pih = snap.extra.pearIpcHealth
|
||
if (pih && typeof pih === 'object') {
|
||
const o = /** @type {Record<string, unknown>} */ (pih)
|
||
const p50 = o.p50Ms ?? o.p50
|
||
const p95 = o.p95Ms ?? o.p95
|
||
if (p50 != null || p95 != null)
|
||
emitHdrLn(
|
||
(useColor ? pal.dim : '') +
|
||
' ipc RTT p50=' +
|
||
String(p50 ?? '—') +
|
||
' p95=' +
|
||
String(p95 ?? '—') +
|
||
(useColor ? pal.reset : '')
|
||
)
|
||
}
|
||
if (snap.extra.swarm && typeof snap.extra.swarm === 'object') {
|
||
const sw = /** @type {Record<string, unknown>} */ (snap.extra.swarm)
|
||
const churnN =
|
||
Number(sw.connectionChurn) || Number(sw.churn) || 0
|
||
emitHdrLn(
|
||
(useColor ? pal.dim : '') +
|
||
' swarm churn ' +
|
||
bareTopSparkline(
|
||
ringSwarmChurn,
|
||
Math.min(28, sparkW),
|
||
sparkAscii,
|
||
logSpark,
|
||
sparkBrailleEff
|
||
) +
|
||
' ' +
|
||
String(churnN) +
|
||
(useColor ? pal.reset : '')
|
||
)
|
||
}
|
||
const pack = {
|
||
pearIpc: snap.extra.pearIpc,
|
||
pearIpcHealth: snap.extra.pearIpcHealth,
|
||
pearTrust: snap.extra.pearTrust,
|
||
peerHealth: snap.extra.peerHealth
|
||
}
|
||
drawScrollableLines(bareTopLinesFromPack(pack, 8, cols), tab, hdrRow)
|
||
} else if (TAB_NAMES[tab] === 'catalog' && snap) {
|
||
emitHdrLn('')
|
||
emitHdrLn(
|
||
(useColor ? pal.kw : '') +
|
||
(icons ? '\u2022 ' : '') +
|
||
'Catalog (index, version, bootstrap, provenance, quotas, rlimits, extensions)' +
|
||
(useColor ? pal.reset : '')
|
||
)
|
||
emitHdrLn(
|
||
(useColor ? pal.dim : '') +
|
||
'Static / semi-static kernel catalog entries and caps.' +
|
||
(useColor ? pal.reset : '')
|
||
)
|
||
if (snap.fileTexts && snap.fileTexts.version) {
|
||
const v1 = String(snap.fileTexts.version).trim().split('\n')[0]
|
||
emitHdrLn(
|
||
(useColor ? pal.kw : '') +
|
||
' version (line 1) ' +
|
||
(useColor ? pal.reset : '') +
|
||
bareTopSanitizeVisible(v1.slice(0, Math.max(20, cols - 22)))
|
||
)
|
||
}
|
||
const idx = snap.extra.index
|
||
let idxHash = ''
|
||
if (idx && typeof idx === 'object') {
|
||
const raw = JSON.stringify(idx)
|
||
let h = 0
|
||
for (let i = 0; i < raw.length; i++) {
|
||
h = (Math.imul(h, 31) + raw.charCodeAt(i)) >>> 0
|
||
}
|
||
idxHash = h.toString(16)
|
||
if (prevCatalogIdxHash && prevCatalogIdxHash !== idxHash)
|
||
emitHdrLn(
|
||
(useColor ? pal.warn : '') +
|
||
' index.json hash changed ' +
|
||
idxHash +
|
||
(useColor ? pal.reset : '')
|
||
)
|
||
prevCatalogIdxHash = idxHash
|
||
}
|
||
const pack = {
|
||
index: snap.extra.index,
|
||
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), tab, hdrRow)
|
||
} else if (TAB_NAMES[tab] === 'host' && snap) {
|
||
emitHdrLn('')
|
||
emitHdrLn(
|
||
(useColor ? pal.kw : '') +
|
||
(icons ? '\u2022 ' : '') +
|
||
'Host / workers (host_os, worker_budget, sandbox, git stats, HDMS, DHT)' +
|
||
(useColor ? pal.reset : '')
|
||
)
|
||
const wbLine = bareTopWorkerBudgetCompactLine(
|
||
snap.metricsLive && snap.metricsLive.workerBudgetHints
|
||
)
|
||
if (wbLine)
|
||
emitHdrLn((useColor ? pal.dim : '') + wbLine + (useColor ? pal.reset : ''))
|
||
if (ringPeerDeriv.length && ringPeers.length) {
|
||
const pr = ringPeers[ringPeers.length - 1]
|
||
const d = ringPeerDeriv[ringPeerDeriv.length - 1]
|
||
emitHdrLn(
|
||
(useColor ? pal.dim : '') +
|
||
' peers ' +
|
||
pr +
|
||
' d ' +
|
||
(d >= 0 ? '+' : '') +
|
||
d +
|
||
'/tick' +
|
||
(useColor ? pal.reset : '')
|
||
)
|
||
}
|
||
if (ringHdms.length || ringDht.length) {
|
||
emitHdrLn(
|
||
(useColor ? pal.dim : '') +
|
||
' HDMS ' +
|
||
bareTopSparkline(
|
||
ringHdms,
|
||
Math.min(22, sparkW),
|
||
sparkAscii,
|
||
logSpark,
|
||
sparkBrailleEff
|
||
) +
|
||
' DHT ' +
|
||
bareTopSparkline(
|
||
ringDht,
|
||
Math.min(22, sparkW),
|
||
sparkAscii,
|
||
logSpark,
|
||
sparkBrailleEff
|
||
) +
|
||
(useColor ? pal.reset : '')
|
||
)
|
||
}
|
||
if (ringBridge.length) {
|
||
emitHdrLn(
|
||
(useColor ? pal.dim : '') +
|
||
' subprocessBridge map ' +
|
||
bareTopSparkline(
|
||
ringBridge,
|
||
Math.min(24, sparkW),
|
||
sparkAscii,
|
||
logSpark,
|
||
sparkBrailleEff
|
||
) +
|
||
(useColor ? pal.reset : '')
|
||
)
|
||
}
|
||
/** @type {string[]} */
|
||
const hostFairLines = []
|
||
if (snap.fairnessSnapshot && typeof snap.fairnessSnapshot === 'object') {
|
||
hostFairLines.push('')
|
||
hostFairLines.push(bareTopSectionRule('Fairness snapshot', cols - 2))
|
||
hostFairLines.push(
|
||
...bareTopFlattenLimited('', snap.fairnessSnapshot, 0, 4, 80, 36)
|
||
)
|
||
}
|
||
const ss = snap.extra && snap.extra.sessionStats
|
||
const m0 = snap.metricsLive && snap.metricsLive.session
|
||
if (ss && m0 && typeof ss === 'object' && typeof m0 === 'object') {
|
||
const sx = /** @type {Record<string, unknown>} */ (ss)
|
||
const mx = /** @type {Record<string, unknown>} */ (m0)
|
||
const a = Number(sx.execLineCount)
|
||
const b = Number(mx.execLineCount)
|
||
if (Number.isFinite(a) && Number.isFinite(b) && a > 0) {
|
||
const drift = Math.abs(a - b) / a
|
||
if (drift > 0.01)
|
||
emitHdrLn(
|
||
(useColor ? pal.warn : '') +
|
||
' session_stats vs metrics_live execLineCount drift ' +
|
||
(drift * 100).toFixed(1) +
|
||
'%' +
|
||
(useColor ? pal.reset : '')
|
||
)
|
||
}
|
||
}
|
||
const sb = snap.extra && snap.extra.sandboxProfile
|
||
if (sb && typeof sb === 'object') {
|
||
const keys = Object.keys(sb).slice(0, 6)
|
||
.map((k) => k + '=' + String(/** @type {Record<string, unknown>} */ (sb)[k]))
|
||
.join(' ')
|
||
if (keys)
|
||
emitHdrLn(
|
||
(useColor ? pal.dim : '') +
|
||
' sandboxProfile ' +
|
||
bareTopTruncateCell(keys, cols - 4, false) +
|
||
(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(
|
||
hostFairLines.concat(bareTopLinesFromPack(pack, 6, cols)),
|
||
tab,
|
||
hdrRow
|
||
)
|
||
} else if (TAB_NAMES[tab] === 'cpu' && snap) {
|
||
emitHdrLn('')
|
||
emitHdrLn(
|
||
(useColor ? pal.kw : '') +
|
||
(icons ? '\u2022 ' : '') +
|
||
'CPU (host + logical cores)' +
|
||
(useColor ? pal.reset : '')
|
||
)
|
||
emitHdrLn(
|
||
(useColor ? pal.dim : '') +
|
||
' logical cpus ' +
|
||
snap.cpuCoreCount +
|
||
' ' +
|
||
(snap.cpuLine || '') +
|
||
(useColor ? pal.reset : '')
|
||
)
|
||
/** @type {string[]} */
|
||
const cpuLines = []
|
||
const hs = snap.hostStats
|
||
if (hs && typeof hs === 'object') {
|
||
const o = /** @type {Record<string, unknown>} */ (hs)
|
||
const per = o.hostPerCpu || o.perCpu || o.cpus
|
||
if (Array.isArray(per) && per.length) {
|
||
const w = Math.min(48, Math.max(8, cols - 10))
|
||
for (let i = 0; i < per.length && i < 32; i++) {
|
||
const c = per[i]
|
||
const v =
|
||
typeof c === 'number'
|
||
? c
|
||
: c && typeof c === 'object'
|
||
? Number(
|
||
/** @type {Record<string, unknown>} */ (c).busy ??
|
||
/** @type {Record<string, unknown>} */ (c).pct
|
||
) || 0
|
||
: 0
|
||
const seg = []
|
||
for (let b = 0; b < w; b++) {
|
||
const th = (b / w) * 100
|
||
seg.push(th < v ? (sparkAscii ? '#' : '\u280b') : ' ')
|
||
}
|
||
cpuLines.push(
|
||
' ' +
|
||
String(i).padStart(2, '0') +
|
||
' ' +
|
||
seg.join('').slice(0, w)
|
||
)
|
||
}
|
||
}
|
||
}
|
||
if (!cpuLines.length)
|
||
cpuLines.push(
|
||
' (no hostPerCpu in hostStats — set metrics or use overview host bar)'
|
||
)
|
||
drawScrollableLines(cpuLines, tab, hdrRow)
|
||
} else if (TAB_NAMES[tab] === 'mem' && snap) {
|
||
emitHdrLn('')
|
||
emitHdrLn(
|
||
(useColor ? pal.kw : '') +
|
||
(icons ? '\u2022 ' : '') +
|
||
'Memory (meminfo + logical RSS when present)' +
|
||
(useColor ? pal.reset : '')
|
||
)
|
||
emitHdrLn(
|
||
(useColor ? pal.dim : '') + (snap.meminfoLine || '') + (useColor ? pal.reset : '')
|
||
)
|
||
const ptM =
|
||
snap.extra &&
|
||
snap.extra.processTable &&
|
||
typeof snap.extra.processTable === 'object'
|
||
? snap.extra.processTable
|
||
: null
|
||
const byRss = bareTopSortProcessRows(
|
||
bareTopProcessRowsFromTable(ptM),
|
||
'pid',
|
||
false
|
||
).sort(
|
||
(a, b) =>
|
||
(Number(b.memRssBytes) || 0) - (Number(a.memRssBytes) || 0)
|
||
)
|
||
/** @type {string[]} */
|
||
const ml = [' top memRssBytes (when kernel fills field)']
|
||
for (const r of byRss.slice(0, Math.max(12, rows - hdrRow - 4))) {
|
||
const rb = Number(r.memRssBytes) || 0
|
||
if (!rb) continue
|
||
ml.push(
|
||
' ' +
|
||
bareTopProcessPid(r) +
|
||
' ' +
|
||
bareTopFormatBytes(rb) +
|
||
' ' +
|
||
String(r.name || '').slice(0, Math.max(8, cols - 28))
|
||
)
|
||
}
|
||
if (ml.length < 2)
|
||
ml.push(' (no memRssBytes on rows — schema v8 optional field)')
|
||
drawScrollableLines(ml, tab, hdrRow)
|
||
} else if (TAB_NAMES[tab] === 'disk' && snap) {
|
||
emitHdrLn('')
|
||
emitHdrLn(
|
||
(useColor ? pal.kw : '') +
|
||
(icons ? '\u2022 ' : '') +
|
||
'Disk (proc diskstats line + trend)' +
|
||
(useColor ? pal.reset : '')
|
||
)
|
||
emitHdrLn(
|
||
(useColor ? pal.dim : '') +
|
||
(snap.diskstatsLine || '(no diskstats)') +
|
||
(useColor ? pal.reset : '')
|
||
)
|
||
/** @type {string[]} */
|
||
const dl = []
|
||
if (typeof snap.diskstatsRaw === 'string' && snap.diskstatsRaw.length) {
|
||
const lines = snap.diskstatsRaw.split('\n').slice(0, 12)
|
||
for (const ln of lines) dl.push(' ' + ln.slice(0, cols - 2))
|
||
}
|
||
drawScrollableLines(dl, tab, hdrRow)
|
||
}
|
||
|
||
const footProc =
|
||
'^v sel F6/S sort V tree Enter detail k/F9 / filter gG [ ] tab'
|
||
const footOverview =
|
||
'PgUp/Dn scroll detail / filter h help Tab/[ ] tab C M e export'
|
||
const footInitd = 'PgUp/Dn scroll / filter units h help'
|
||
const footDefault =
|
||
'PgUp/Dn scroll Tab/[ ] tabs C M cpu/mem h help e export f full'
|
||
let footBase = quietFooter
|
||
? tab === 0
|
||
? 'ov'
|
||
: tab === TAB_PROC
|
||
? 'proc'
|
||
: '…'
|
||
: tab === TAB_PROC
|
||
? footProc
|
||
: tab === 0
|
||
? footOverview
|
||
: TAB_NAMES[tab] === 'initd'
|
||
? footInitd
|
||
: footDefault
|
||
const tzFoot = (() => {
|
||
if (utcClock) return ' | UTC'
|
||
const off = new Date().getTimezoneOffset()
|
||
const sign = off > 0 ? '-' : '+'
|
||
const h = Math.floor(Math.abs(off) / 60)
|
||
return ' | TZ GMT' + sign + h
|
||
})()
|
||
const burstFoot =
|
||
Date.now() < burstUntil ? (quietFooter ? ' |B' : ' | BURST') : ''
|
||
let foot =
|
||
(useColor ? pal.dim : '') +
|
||
footBase +
|
||
tzFoot +
|
||
burstFoot +
|
||
(useColor ? pal.reset : '') +
|
||
scrollHint
|
||
if (reniceOpen) {
|
||
foot =
|
||
(useColor ? pal.warn : '') +
|
||
'RENICE+/> ' +
|
||
reniceBuf +
|
||
'_' +
|
||
(useColor ? pal.reset : '')
|
||
} else 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)
|
||
}
|
||
}
|
||
if (scrollRegionOn && tab === TAB_PROC && procDetailPid == null) {
|
||
out += '\x1b[r'
|
||
}
|
||
if (tDrawStart) {
|
||
foot =
|
||
foot.slice(0, Math.max(0, cols - 14)) +
|
||
(useColor ? pal.dim : '') +
|
||
' ~' +
|
||
(Date.now() - tDrawStart) +
|
||
'ms' +
|
||
(useColor ? pal.reset : '')
|
||
}
|
||
if (toastMsg && Date.now() < toastUntil) {
|
||
foot =
|
||
(useColor ? pal.warn : '') +
|
||
toastMsg.slice(0, cols - 2) +
|
||
(useColor ? pal.reset : '')
|
||
} else toastMsg = ''
|
||
out += bareEditCup(rows, 1) + '\x1b[K' + foot.slice(0, cols)
|
||
out += '\x1b[?25h'
|
||
if (debugTimings) {
|
||
const nw = Date.now()
|
||
if (nw - lastDbgLog > 4000) {
|
||
lastDbgLog = nw
|
||
try {
|
||
ctx.console.error(
|
||
'baretop: fetchMs=' +
|
||
String(lastSnap && lastSnap.fetchWallMs) +
|
||
' outLen=' +
|
||
out.length +
|
||
(lastDbgPatchLen
|
||
? ' patchLen=' + String(lastDbgPatchLen)
|
||
: '')
|
||
)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
}
|
||
lastDbgPatchLen = 0
|
||
const emitFr = bareTopEmitFrame(
|
||
/** @type {Record<string, unknown>} */ (ctx),
|
||
stdout,
|
||
out,
|
||
{
|
||
prevLines: lastLineFrame,
|
||
rows,
|
||
cols,
|
||
incrementalFrameDiff:
|
||
incrementalFrameDiff &&
|
||
!helpMode &&
|
||
!setupMode &&
|
||
!filterOpen &&
|
||
!signalPrompt &&
|
||
!reniceOpen,
|
||
useLineHash: lineHashDiff,
|
||
fullscreenPanel,
|
||
cup: bareEditCup
|
||
}
|
||
)
|
||
lastLineFrame = emitFr.nextLines
|
||
lastLineFrameRows = rows
|
||
lastLineFrameCols = cols
|
||
if (emitFr.wrotePatch) lastDbgPatchLen = emitFr.patchLen
|
||
}
|
||
|
||
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,
|
||
mouseEnabled: mouseOn,
|
||
keyQuit: bareTopKeyMap.quit,
|
||
keyRefresh: bareTopKeyMap.refresh
|
||
})
|
||
if (evs) {
|
||
if (evs.type === 'quit') {
|
||
quit = true
|
||
break
|
||
}
|
||
const SIGS = ['TERM', 'INT', 'KILL', 'HUP', 'USR1', 'USR2']
|
||
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')) {
|
||
const targets =
|
||
taggedPids.size > 0
|
||
? [...taggedPids]
|
||
: [signalPrompt.pid]
|
||
for (const p of targets) {
|
||
try {
|
||
if (typeof ctx.bareOsSendSignal === 'function')
|
||
ctx.bareOsSendSignal(p, 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,
|
||
mouseEnabled: false,
|
||
helpPaging: true,
|
||
keyQuit: bareTopKeyMap.quit,
|
||
keyRefresh: bareTopKeyMap.refresh
|
||
})
|
||
if (evh) {
|
||
if (evh.type === 'quit') {
|
||
quit = true
|
||
break
|
||
}
|
||
if (evh.type === 'help_page') {
|
||
const maxP = 1
|
||
helpPageIdx =
|
||
evh.dir === 1
|
||
? Math.min(maxP, helpPageIdx + 1)
|
||
: Math.max(0, helpPageIdx - 1)
|
||
draw()
|
||
continue
|
||
}
|
||
helpMode = false
|
||
helpPageIdx = 0
|
||
draw()
|
||
}
|
||
await new Promise((r) => setTimeout(r, 50))
|
||
continue
|
||
}
|
||
|
||
if (setupMode) {
|
||
if (keyq.length === 1 && keyq[0] === 0x1b) {
|
||
keyq.shift()
|
||
setupMode = false
|
||
draw()
|
||
continue
|
||
}
|
||
const evs = bareTopDrainKeys(keyq, 32, {
|
||
vi: viKeys,
|
||
inFilter: false,
|
||
mouseEnabled: false
|
||
})
|
||
if (evs) {
|
||
if (evs.type === 'quit') {
|
||
quit = true
|
||
break
|
||
}
|
||
setupMode = 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,
|
||
mouseEnabled: false
|
||
})
|
||
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 if (filterWhich === 'overview')
|
||
overviewSectionFilter = filterLine
|
||
else if (filterWhich === 'network') filterNetwork = filterLine
|
||
else if (filterWhich === 'features') filterFeatures = 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
|
||
}
|
||
|
||
if (reniceOpen) {
|
||
if (keyq.length === 1 && keyq[0] === 0x1b) {
|
||
keyq.shift()
|
||
reniceOpen = false
|
||
reniceBuf = ''
|
||
draw()
|
||
continue
|
||
}
|
||
const evz = bareTopDrainKeys(keyq, 32, {
|
||
vi: false,
|
||
inFilter: true,
|
||
mouseEnabled: false,
|
||
keyQuit: bareTopKeyMap.quit,
|
||
keyRefresh: bareTopKeyMap.refresh
|
||
})
|
||
if (evz) {
|
||
if (evz.type === 'quit') {
|
||
quit = true
|
||
break
|
||
}
|
||
if (evz.type === 'filter_bs') {
|
||
reniceBuf = reniceBuf.slice(0, -1)
|
||
draw()
|
||
continue
|
||
}
|
||
if (evz.type === 'filter_enter') {
|
||
const d = parseInt(reniceBuf.trim(), 10)
|
||
const pl = currentProcRows()
|
||
const row = pl[procCursor]
|
||
const pid = row ? bareTopProcessPid(row) : 0
|
||
reniceOpen = false
|
||
reniceBuf = ''
|
||
if (pid >= 1 && Number.isFinite(d)) {
|
||
try {
|
||
if (typeof ctx.bareOsRenice === 'function') {
|
||
const r = ctx.bareOsRenice(pid, d)
|
||
const ro = r && typeof r === 'object' ? r : {}
|
||
const ok = /** @type {{ ok?: boolean }} */ (ro).ok
|
||
toastMsg = ok ? 'renice ok' : 'renice: unsupported'
|
||
} else toastMsg = 'renice: no ctx.bareOsRenice'
|
||
} catch {
|
||
toastMsg = 'renice error'
|
||
}
|
||
} else toastMsg = 'renice: bad pid/delta'
|
||
toastUntil = Date.now() + 4000
|
||
draw()
|
||
continue
|
||
}
|
||
if (evz.type === 'filter_char' && evz.ch && reniceBuf.length < 8)
|
||
reniceBuf += evz.ch
|
||
draw()
|
||
continue
|
||
}
|
||
await new Promise((r) => setTimeout(r, 50))
|
||
continue
|
||
}
|
||
|
||
if (overviewJumpArmed) {
|
||
if (keyq.length === 1 && keyq[0] === 0x1b) {
|
||
keyq.shift()
|
||
overviewJumpArmed = false
|
||
draw()
|
||
continue
|
||
}
|
||
const evj = bareTopDrainKeys(keyq, 8, {
|
||
overviewJumpPick: true,
|
||
vi: false,
|
||
inFilter: true,
|
||
mouseEnabled: false
|
||
})
|
||
if (evj) {
|
||
if (evj.type === 'quit') {
|
||
quit = true
|
||
break
|
||
}
|
||
overviewJumpArmed = false
|
||
if (evj.type === 'overview_section_digit') {
|
||
const evjd = /** @type {{ idx?: number }} */ (evj)
|
||
const idx = typeof evjd.idx === 'number' ? evjd.idx : 0
|
||
const ids = activeOverviewSections
|
||
if (
|
||
idx >= 0 &&
|
||
idx < ids.length &&
|
||
lastOvLines.length &&
|
||
tab === 0
|
||
) {
|
||
const sid = ids[idx]
|
||
const rawIx =
|
||
sid != null && lastOvSectionRaw[String(sid)] != null
|
||
? lastOvSectionRaw[String(sid)]
|
||
: -1
|
||
if (rawIx >= 0) {
|
||
const wrapW = Math.max(16, cols - 2)
|
||
scrollRows[0] = bareTopScrollTopForRawLine(
|
||
lastOvLines,
|
||
wrapW,
|
||
rawIx
|
||
)
|
||
} else {
|
||
toastMsg = 'section jump: n/a'
|
||
toastUntil = Date.now() + 2500
|
||
}
|
||
} else {
|
||
toastMsg = 'section jump: n/a'
|
||
toastUntil = Date.now() + 2500
|
||
}
|
||
draw()
|
||
continue
|
||
}
|
||
draw()
|
||
continue
|
||
}
|
||
await new Promise((r) => setTimeout(r, 50))
|
||
continue
|
||
}
|
||
|
||
const ev = bareTopDrainKeys(keyq, 32, {
|
||
vi: viKeys,
|
||
inFilter: false,
|
||
tab,
|
||
tabCpuIdx: TAB_NAMES.indexOf('cpu'),
|
||
tabMemIdx: TAB_NAMES.indexOf('mem'),
|
||
mouseEnabled:
|
||
mouseOn &&
|
||
!helpMode &&
|
||
!setupMode &&
|
||
!filterOpen &&
|
||
!signalPrompt &&
|
||
!reniceOpen,
|
||
procListActive: tab === TAB_PROC && !procDetailPid,
|
||
procDetailOpen: procDetailPid != null,
|
||
keyQuit: bareTopKeyMap.quit,
|
||
keyRefresh: bareTopKeyMap.refresh
|
||
})
|
||
if (ev) {
|
||
if (ev.type === 'mouse' && mouseOn) {
|
||
const mx = /** @type {{ btn: number, x: number, y: number, release: boolean }} */ (
|
||
ev
|
||
)
|
||
if (!mx.release && tab === TAB_PROC && !procDetailPid) {
|
||
const { rows: termR } = termDims()
|
||
const my = mx.y - 1
|
||
const mxBody = hdrRow <= my && my < termR - 2
|
||
if (mxBody) {
|
||
const plen = currentProcRows().length
|
||
const mVis = Math.max(1, termR - 2 - hdrRow)
|
||
const st = scrollRows[TAB_PROC] || 0
|
||
const rowIdx = my - hdrRow + st
|
||
if (rowIdx >= 0 && rowIdx < plen) {
|
||
const nowM = Date.now()
|
||
if (
|
||
!mx.release &&
|
||
mx.btn === 0 &&
|
||
nowM - lastMouseDownMs < 450 &&
|
||
rowIdx === lastMouseDownRow
|
||
) {
|
||
const pl = currentProcRows()
|
||
const row = pl[rowIdx]
|
||
const p = row ? bareTopProcessPid(row) : 0
|
||
if (p >= 1) procDetailPid = p
|
||
}
|
||
lastMouseDownMs = nowM
|
||
lastMouseDownRow = rowIdx
|
||
procCursor = rowIdx
|
||
scrollRows[TAB_PROC] = Math.min(
|
||
Math.max(0, plen - mVis),
|
||
Math.max(0, rowIdx - Math.floor(mVis / 2))
|
||
)
|
||
draw()
|
||
}
|
||
}
|
||
} else if (
|
||
!mx.release &&
|
||
mx.btn === 0 &&
|
||
tab !== TAB_PROC &&
|
||
tab >= 2 &&
|
||
tab < NTABS
|
||
) {
|
||
const { rows: termR } = termDims()
|
||
const my = mx.y - 1
|
||
const mainEnd2 = fullscreenPanel ? termR - 1 : termR - 2
|
||
if (my >= hdrRow && my < mainEnd2) {
|
||
const vis = Math.max(1, mainEnd2 - hdrRow)
|
||
const delta = my - hdrRow - Math.floor(vis / 2)
|
||
scrollRows[tab] = Math.max(0, (scrollRows[tab] || 0) + delta)
|
||
draw()
|
||
}
|
||
}
|
||
if ((mx.btn === 64 || mx.btn === 65) && !mx.release) {
|
||
const dir = mx.btn === 64 ? -1 : 1
|
||
if (tab === TAB_PROC && !procDetailPid) {
|
||
const pl = currentProcRows()
|
||
const maxI = Math.max(0, pl.length - 1)
|
||
procCursor = Math.max(0, Math.min(maxI, procCursor + dir * 3))
|
||
} else {
|
||
const sr = scrollRows[tab] || 0
|
||
scrollRows[tab] = Math.max(0, sr + dir * 3)
|
||
}
|
||
draw()
|
||
}
|
||
continue
|
||
}
|
||
if (ev.type === 'sort_menu') {
|
||
if (tab === TAB_PROC) {
|
||
const idx = PROC_SORT_ORDER.indexOf(procSortKey)
|
||
const n = PROC_SORT_ORDER.length
|
||
procSortKey =
|
||
/** @type {'pid' | 'name' | 'state' | 'time' | 'nice' | 'pri' | 'cpu'} */ (
|
||
PROC_SORT_ORDER[(Math.max(0, idx) + 1 + n) % n]
|
||
)
|
||
procSortAsc = true
|
||
procCursor = 0
|
||
scrollRows[TAB_PROC] = 0
|
||
draw()
|
||
}
|
||
continue
|
||
}
|
||
if (ev.type === 'tree_toggle') {
|
||
if (tab === TAB_PROC) {
|
||
procTreeMode = !procTreeMode
|
||
procCursor = 0
|
||
scrollRows[TAB_PROC] = 0
|
||
draw()
|
||
}
|
||
continue
|
||
}
|
||
if (ev.type === 'initd_view_toggle') {
|
||
if (TAB_NAMES[tab] === 'initd') {
|
||
initdRawFallback = !initdRawFallback
|
||
scrollRows[tab] = 0
|
||
draw()
|
||
}
|
||
continue
|
||
}
|
||
if (ev.type === 'sort_dir_toggle') {
|
||
if (tab === TAB_PROC) procSortAsc = !procSortAsc
|
||
draw()
|
||
continue
|
||
}
|
||
if (ev.type === 'follow_toggle') {
|
||
procFollowOn = !procFollowOn
|
||
draw()
|
||
continue
|
||
}
|
||
if (ev.type === 'tree_collapse') {
|
||
if (tab === TAB_PROC && procTreeMode) {
|
||
const pl = currentProcRows()
|
||
const row = pl[procCursor]
|
||
if (row) {
|
||
const pid = bareTopProcessPid(row)
|
||
if (treeCollapsedPPids.has(pid)) treeCollapsedPPids.delete(pid)
|
||
else treeCollapsedPPids.add(pid)
|
||
}
|
||
}
|
||
draw()
|
||
continue
|
||
}
|
||
if (ev.type === 'tag_toggle') {
|
||
if (tab === TAB_PROC && !procDetailPid) {
|
||
const pl = currentProcRows()
|
||
const row = pl[procCursor]
|
||
const p = row ? bareTopProcessPid(row) : 0
|
||
if (p >= 1) {
|
||
if (taggedPids.has(p)) taggedPids.delete(p)
|
||
else taggedPids.add(p)
|
||
}
|
||
}
|
||
draw()
|
||
continue
|
||
}
|
||
if (ev.type === 'tagged_only_toggle') {
|
||
procTaggedOnly = !procTaggedOnly
|
||
draw()
|
||
continue
|
||
}
|
||
if (ev.type === 'renice_open') {
|
||
if (tab === TAB_PROC && !procDetailPid) {
|
||
reniceOpen = true
|
||
reniceBuf = ''
|
||
}
|
||
draw()
|
||
continue
|
||
}
|
||
if (ev.type === 'copy_pid') {
|
||
if (tab === TAB_PROC && !procDetailPid) {
|
||
const pl = currentProcRows()
|
||
const row = pl[procCursor]
|
||
const p = row ? bareTopProcessPid(row) : 0
|
||
if (p >= 1 && typeof ctx.bareOsClipboardWrite === 'function') {
|
||
try {
|
||
ctx.bareOsClipboardWrite(String(p))
|
||
toastMsg = 'pid ' + p
|
||
} catch {
|
||
toastMsg = 'clipboard failed'
|
||
}
|
||
} else toastMsg = 'no ctx.bareOsClipboardWrite'
|
||
toastUntil = Date.now() + 3500
|
||
}
|
||
draw()
|
||
continue
|
||
}
|
||
if (ev.type === 'action_menu') {
|
||
toastMsg = 'a: use k signal c copy pid e export'
|
||
toastUntil = Date.now() + 4500
|
||
draw()
|
||
continue
|
||
}
|
||
if (ev.type === 'detail_sub') {
|
||
if (procDetailPid != null) {
|
||
const s = /** @type {{ sub?: string }} */ (ev).sub
|
||
if (s === 'summary' || s === 'fds' || s === 'env') procDetailSub = s
|
||
else procDetailSub = 'all'
|
||
}
|
||
draw()
|
||
continue
|
||
}
|
||
if (ev.type === 'export_tab') {
|
||
if (lastSnap && ctx.vfs && typeof ctx.vfs.writeFile === 'function') {
|
||
try {
|
||
const tabName = TAB_NAMES[tab] || 'tab'
|
||
const slice = { tab: tabName, atMs: Date.now() }
|
||
if (tabName === 'processes')
|
||
slice.processes = currentProcRows().slice(0, 400)
|
||
else if (tabName === 'features') slice.features = lastSnap.features
|
||
else if (tabName === 'host') {
|
||
slice.hostOs = lastSnap.hostOs
|
||
slice.hostStats = lastSnap.hostStats
|
||
} else slice.note = 'tab slice minimal; use full export for more'
|
||
const ep = exportPath.replace(/\.json$/i, '') + '-' + tabName + '.json'
|
||
await ctx.vfs.writeFile(ep, JSON.stringify(slice, null, 2))
|
||
ctx.console.log(bareTopStrings.exportOk + ' ' + ep)
|
||
} catch (e) {
|
||
ctx.console.error(
|
||
bareTopStrings.exportFail +
|
||
': ' +
|
||
((e && /** @type {{ message?: string }} */ (e).message) ||
|
||
String(e))
|
||
)
|
||
}
|
||
}
|
||
continue
|
||
}
|
||
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 === 'setup_open') {
|
||
setupMode = true
|
||
lastLineFrame = null
|
||
draw()
|
||
continue
|
||
}
|
||
if (ev.type === 'search_next') {
|
||
if (tab === TAB_PROC && !procDetailPid) {
|
||
const pl = currentProcRows()
|
||
const f = filterProcess.toLowerCase()
|
||
if (pl.length) {
|
||
if (f.length) {
|
||
let found = -1
|
||
for (let k = 1; k <= pl.length; k++) {
|
||
const ix = (procCursor + k) % pl.length
|
||
const nm = String(pl[ix].name || '').toLowerCase()
|
||
if (nm.includes(f)) {
|
||
found = ix
|
||
break
|
||
}
|
||
}
|
||
if (found >= 0) procCursor = found
|
||
} else procCursor = (procCursor + 1) % pl.length
|
||
}
|
||
draw()
|
||
}
|
||
continue
|
||
}
|
||
if (ev.type === 'search_prev') {
|
||
if (tab === TAB_PROC && !procDetailPid) {
|
||
const pl = currentProcRows()
|
||
const f = filterProcess.toLowerCase()
|
||
if (pl.length) {
|
||
if (f.length) {
|
||
let found = -1
|
||
for (let k = 1; k <= pl.length; k++) {
|
||
const ix = (procCursor - k + pl.length * 9) % pl.length
|
||
const nm = String(pl[ix].name || '').toLowerCase()
|
||
if (nm.includes(f)) {
|
||
found = ix
|
||
break
|
||
}
|
||
}
|
||
if (found >= 0) procCursor = found
|
||
} else procCursor = (procCursor - 1 + pl.length) % pl.length
|
||
}
|
||
draw()
|
||
}
|
||
continue
|
||
}
|
||
if (ev.type === 'help_open') {
|
||
helpMode = true
|
||
lastLineFrame = null
|
||
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 === 'overview_jump_arm') {
|
||
if (tab === 0) {
|
||
overviewJumpArmed = true
|
||
toastMsg = "section: ' then 1-9,0 (10th)"
|
||
toastUntil = Date.now() + 3500
|
||
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 = currentProcRows()
|
||
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 idx = Math.max(0, PROC_SORT_ORDER.indexOf(procSortKey))
|
||
const n = PROC_SORT_ORDER.length
|
||
procSortKey =
|
||
/** @type {'pid' | 'name' | 'state' | 'time' | 'nice' | 'pri' | 'cpu'} */ (
|
||
PROC_SORT_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 = currentProcRows()
|
||
const row = pl[procCursor]
|
||
const pid = row ? bareTopProcessPid(row) : 0
|
||
if (procDetailPid != null) {
|
||
procDetailPid = null
|
||
procDetailSub = 'all'
|
||
} else if (pid > 0) {
|
||
procDetailPid = pid
|
||
procDetailSub = 'all'
|
||
}
|
||
draw()
|
||
}
|
||
continue
|
||
}
|
||
if (ev.type === 'signal_menu') {
|
||
if (tab === TAB_PROC && !procDetailPid) {
|
||
const pl = currentProcRows()
|
||
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 = currentProcRows()
|
||
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
|
||
const netFiltOn =
|
||
envTop.BARE_TOP_NET_FILTER === '1' ||
|
||
envTop.BARE_TOP_NET_FILTER === 'true'
|
||
filterWhich =
|
||
tab === TAB_PROC
|
||
? 'process'
|
||
: tab === 0
|
||
? 'overview'
|
||
: TAB_NAMES[tab] === 'network' && netFiltOn
|
||
? 'network'
|
||
: TAB_NAMES[tab] === 'features'
|
||
? 'features'
|
||
: 'initd'
|
||
filterLine =
|
||
filterWhich === 'process'
|
||
? filterProcess
|
||
: filterWhich === 'overview'
|
||
? overviewSectionFilter
|
||
: filterWhich === 'network'
|
||
? filterNetwork
|
||
: filterWhich === 'features'
|
||
? filterFeatures
|
||
: filterInitd
|
||
draw()
|
||
continue
|
||
}
|
||
if (ev.type === 'export') {
|
||
if (lastSnap && ctx.vfs && typeof ctx.vfs.writeFile === 'function') {
|
||
try {
|
||
const toSave = Object.assign({}, lastSnap, {
|
||
bareTopExportMeta: {
|
||
layoutVersion: bareTopStrings.layoutVersion,
|
||
overviewSections: activeOverviewSections.slice()
|
||
}
|
||
})
|
||
if (exportRedact && toSave.fileTexts) toSave.fileTexts = {}
|
||
const payload = JSON.stringify(toSave, 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()
|
||
}
|
||
|
||
let waitMs =
|
||
Date.now() < burstUntil ? Math.min(250, intervalMs) : intervalMs
|
||
const ewTick =
|
||
typeof bareTopFetchEwmaMs === 'number' ? bareTopFetchEwmaMs : -1
|
||
if (
|
||
ewTick > 0 &&
|
||
ewTick > intervalMs &&
|
||
Date.now() >= burstUntil
|
||
)
|
||
waitMs = Math.min(intervalMs * 2, Math.round(ewTick))
|
||
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 (mouseOn) {
|
||
bareTopWrite(ctx, stdout, '\x1b[?1000l\x1b[?1002l\x1b[?1006l')
|
||
}
|
||
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()
|
||
}
|
||
if (
|
||
(envTop.BARE_TOP_PERSIST === '1' || envTop.BARE_TOP_PERSIST === 'true') &&
|
||
bareTopRcPath &&
|
||
ctx.vfs &&
|
||
typeof ctx.vfs.writeFile === 'function'
|
||
) {
|
||
try {
|
||
void ctx.vfs.writeFile(
|
||
bareTopRcPath,
|
||
JSON.stringify(
|
||
{
|
||
BARE_TOP_THEME: String(envTop.BARE_TOP_THEME || 'dark'),
|
||
BARE_TOP_PROC_SORT: procSortKey,
|
||
keys: bareTopKeyMap
|
||
},
|
||
null,
|
||
2
|
||
)
|
||
)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
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). Same binary as btop.\n' +
|
||
'Shows metrics_live, logical process_table.json, peers, delegates, pipeline limits,\n' +
|
||
'many /proc/bare_os JSON mirrors (incl. process_io / process_threads / process_maps when present),\n' +
|
||
'and optional ctx fast paths. Requires a TTY.\n' +
|
||
'\n' +
|
||
'Tabs (default): 1 overview 2 processes 3 initd 4 network 5 features 6 diagnostics\n' +
|
||
' 7 operator 8 pear 9 catalog H host C cpu M mem 0 disk Tab next tab Shift+Tab prev\n' +
|
||
' BARE_TOP_TAB_MERGE=netop drops the operator tab (operator lines fold into network).\n' +
|
||
' F1 help F2 setup F3/F4 proc row F5 refresh F7 renice F9 signal F10 quit\n' +
|
||
'\n' +
|
||
'Environment:\n' +
|
||
' BARE_TOP_CONFIG_PATH / ~/.config/baretoprc JSON: theme, interval, keys.{quit,refresh}, …\n' +
|
||
' BARE_TOP_PERSIST=1 Write non-security prefs back to baretoprc on quit (when supported)\n' +
|
||
' BARE_TOP_INTERVAL_MS Refresh (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 8)\n' +
|
||
' BARE_TOP_FETCH_EWMA=1 Lower batch concurrency when recent fetches are slow\n' +
|
||
' BARE_TOP_SNAPSHOT_LITE=1 Smaller bareOsReadBareTopSnapshot batch\n' +
|
||
' BARE_TOP_SNAPSHOT_LITE_AUTO=1 Auto lite mode after sustained slow fetches\n' +
|
||
' BARE_TOP_FULL_REDRAW=1 Disable default line-diff patches (incremental still selectable)\n' +
|
||
' BARE_TOP_INCREMENTAL 0 off 1 soft home 2 line-diff (default ~2 when TERM ok)\n' +
|
||
' BARE_TOP_LINE_HASH=1 Hash long lines before strcmp in incremental diff\n' +
|
||
' BARE_TOP_PROFILE=1 Throttled compose/emit/fetch ms in footer\n' +
|
||
' BARE_TOP_GRAPH_MODE unicode | ascii | braille (sparkline glyphs)\n' +
|
||
' BARE_TOP_TRUECOLOR_CPU=1 24-bit CPU bar hints when COLORTERM=truecolor (NO_COLOR wins)\n' +
|
||
' BARE_TOP_NET_FILTER=1 / filter on network tab\n' +
|
||
' BARE_TOP_FILTER_REGEX=1 Allow /pattern/ process filter; else leading /…/ is regex when matched\n' +
|
||
' BARE_TOP_FILTER_INVERT=1 Invert process name filter\n' +
|
||
' BARE_TOP_SORT_PPID_TIE=1 Tertiary sort by ppid when tree is off\n' +
|
||
' BARE_TOP_PROC_NAME_WIDTH Delta columns added to NAME width (clamped)\n' +
|
||
' BARE_TOP_PROC_COL_SEP=1 Unicode column separators in process table\n' +
|
||
' BARE_TOP_MINIMAP=1 Scroll minimap on long lists\n' +
|
||
' BARE_TOP_CMD_ELLIPSIS_MIDDLE Middle-ellipsis long process names\n' +
|
||
' BARE_TOP_PROC_DETAIL_JSON 1 = pretty-print JSON in process detail (Enter)\n' +
|
||
' BARE_TOP_NO_ALTSCREEN Non-empty skips alternate-screen mode\n' +
|
||
' BARE_TOP_NO_2J_AFTER_FIRST Soft clear after first full paint\n' +
|
||
' BARE_TOP_ASCII_GRAPH / BARE_TOP_ASCII_UI / BARE_TOP_MONO / BARE_TOP_BRAILLE_SPARK\n' +
|
||
' BARE_TOP_LOG_SPARK Log-scaled sparklines\n' +
|
||
' BARE_TOP_EMA_ALPHA 0–1 delta smoothing (0 = off)\n' +
|
||
' BARE_TOP_THEME dark | light | none | gruvbox | nord | solarized\n' +
|
||
' BARE_TOP_LAYOUT stacked | even | htop (=stacked) | btop (=even)\n' +
|
||
' BARE_TOP_LAYOUT_AUTO Even layout when cols >= 100\n' +
|
||
' BARE_TOP_HIGH_CONTRAST 1\n' +
|
||
' BARE_TOP_COLOR256 Force 256-color bar hints\n' +
|
||
' BARE_TOP_VI_KEYS hjkl + n/N search in vi-style mode\n' +
|
||
' BARE_TOP_FRAMES Left rule on scroll regions\n' +
|
||
' BARE_TOP_FULLSCREEN_HIDE_TABS Hide tab strip in fullscreen (f)\n' +
|
||
' BARE_TOP_OVERVIEW_COMPACT / BARE_TOP_OVERVIEW_SECTIONS / BARE_TOP_OVERVIEW_PROTOMUX_SPARK\n' +
|
||
' BARE_TOP_HEALTH_DETAIL Health penalty breakdown on overview\n' +
|
||
' BARE_TOP_TITLE_VERSION Append truncated /proc/bare_os/version\n' +
|
||
' BARE_TOP_MOUSE=1 SGR mouse (wheel; proc row click; double-click detail)\n' +
|
||
' BARE_TOP_SCROLL_REGION=1 DECSTBM on overview detail or process list (experimental)\n' +
|
||
' BARE_TOP_QUIET_FOOTER Shorter footer on small terminals\n' +
|
||
' BARE_TOP_DEBUG_TIMINGS Throttled fetch/draw on stderr\n' +
|
||
' BARE_TOP_EXPORT_PATH JSON export for e; E exports current tab slice\n' +
|
||
' BARE_TOP_EXPORT_REDACT Omit fileTexts from export\n' +
|
||
' BARE_TOP_ICONS Bullet prefixes on sections\n' +
|
||
' BARE_TOP_PROC_SORT pid | name | state | time | nice | pri | cpu\n' +
|
||
' BARE_TOP_RING_CAP Sparkline depth (default 72, max 240)\n' +
|
||
' NO_COLOR / COLORTERM See https://no-color.org/ ; truecolor gated on COLORTERM\n' +
|
||
'\n' +
|
||
'Keys: q F10 quit r F5 refresh R sort +/- sp pause . step d delta e export E tab export\n' +
|
||
' f fullscreen F follow pid t clock Tab/[ ] tab next/prev h ? F1 help F2 setup\n' +
|
||
' / filter (overview, initd, processes, features) V tree z collapse a action k F9 signal\n' +
|
||
' n F7 renice c copy pid = tag % tagged-only i o e detail subviews\n' +
|
||
" ' then 1-9,0 Jump to Nth overview section (on overview tab)\n" +
|
||
' Enter detail F3 F4 row F6 S sort <> sort cycle g G top/bottom\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<string, unknown>} */ (ctx).bareTopArgv = argv
|
||
await bareOsRunBareTopTui(ctx)
|
||
}
|