4561 lines
139 KiB
Plaintext
4561 lines
139 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 === 'A' || last === 'B' || last === 'C' || last === 'D') {
|
||
const map = { A: 'up', B: 'down', C: 'right', D: 'left' }
|
||
return { type: 'nav', key: map[last] }
|
||
}
|
||
if (last === 'H') return { type: 'nav', key: 'home' }
|
||
if (last === 'F') return { type: 'nav', key: 'end' }
|
||
return { type: 'unknown' }
|
||
}
|
||
|
||
/**
|
||
* @param {number} b3 byte after ESC O
|
||
*/
|
||
function bareEditSs3ToEvent(b3) {
|
||
if (b3 === 72) return { type: 'nav', key: 'home' }
|
||
if (b3 === 70) return { type: 'nav', key: 'end' }
|
||
if (b3 === 65) return { type: 'nav', key: 'up' }
|
||
if (b3 === 66) return { type: 'nav', key: 'down' }
|
||
if (b3 === 67) return { type: 'nav', key: 'right' }
|
||
if (b3 === 68) return { type: 'nav', key: 'left' }
|
||
if (b3 === 80) return { type: 'fn', n: 1 }
|
||
if (b3 === 81) return { type: 'fn', n: 2 }
|
||
if (b3 === 82) return { type: 'fn', n: 3 }
|
||
if (b3 === 83) return { type: 'fn', n: 4 }
|
||
return { type: 'unknown' }
|
||
}
|
||
|
||
/**
|
||
* @param {number[]} q mutable queue (front = index 0)
|
||
* @returns {Record<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). */
|
||
|
||
/**
|
||
* Keep in sync with `bareOsReadBareTopSnapshot` path list in packages/bare-os-booter/index.js
|
||
* @type {readonly [string, string][]}
|
||
*/
|
||
var BARE_TOP_SNAPSHOT_PROC_ENTRIES = [
|
||
['index', '/proc/bare_os/index.json'],
|
||
['version', '/proc/bare_os/version'],
|
||
['hostOs', '/proc/bare_os/host_os.json'],
|
||
['debug', '/proc/bare_os/debug.json'],
|
||
['sessionStats', '/proc/bare_os/session_stats'],
|
||
['quotas', '/proc/bare_os/quotas'],
|
||
['capabilitiesJson', '/proc/bare_os/capabilities.json'],
|
||
['capabilitiesNode', '/proc/bare_os/capabilities'],
|
||
['seedHandshake', '/proc/bare_os/seed_handshake'],
|
||
['bootstrap', '/proc/bare_os/bootstrap'],
|
||
['provenance', '/proc/bare_os/provenance'],
|
||
['snapshotHints', '/proc/bare_os/snapshot_hints.json'],
|
||
['stagingSlot', '/proc/bare_os/staging_slot'],
|
||
['peerHealth', '/proc/bare_os/peer_health'],
|
||
['pearIpc', '/proc/bare_os/pear_ipc.json'],
|
||
['pearIpcHealth', '/proc/bare_os/pear_ipc_health.json'],
|
||
['pearTrust', '/proc/bare_os/pear_trust.json'],
|
||
['replication', '/proc/bare_os/replication'],
|
||
['replicationBackpressure', '/proc/bare_os/replication_backpressure.json'],
|
||
['swarm', '/proc/bare_os/swarm'],
|
||
['syncWindow', '/proc/bare_os/sync_window.json'],
|
||
['clock', '/proc/bare_os/clock.json'],
|
||
['hdmsHealth', '/proc/bare_os/hdms_health.json'],
|
||
['hdmsHints', '/proc/bare_os/hdms_hints.json'],
|
||
['dhtStatus', '/proc/bare_os/dht_status.json'],
|
||
['udxExtended', '/proc/bare_os/udx_extended.json'],
|
||
['ipcBackpressure', '/proc/bare_os/ipc_backpressure.json'],
|
||
['delegateRed', '/proc/bare_os/delegate_red.json'],
|
||
['gitDelegateStats', '/proc/bare_os/git_delegate_stats.json'],
|
||
['kernelProgram', '/proc/bare_os/kernel_program.json'],
|
||
['gitLfsPointerStats', '/proc/bare_os/git_lfs_pointer_stats.json'],
|
||
['workerBudget', '/proc/bare_os/worker_budget.json'],
|
||
['sandboxProfile', '/proc/bare_os/sandbox_profile.json'],
|
||
['rlimits', '/proc/bare_os/rlimits.json'],
|
||
['extensions', '/proc/bare_os/extensions.json'],
|
||
['initdDag', '/proc/bare_os/initd_dag.json'],
|
||
['bootGraph', '/proc/bare_os/boot_graph.json'],
|
||
[
|
||
'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']
|
||
]
|
||
|
||
/** 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>>}
|
||
*/
|
||
async function bareTopReadProcBatch(ctx, entries, concurrency) {
|
||
/** @type {Record<string, string>} */
|
||
const out = {}
|
||
const conc = Math.max(1, Math.min(16, concurrency | 0) || 6)
|
||
for (let i = 0; i < entries.length; i += conc) {
|
||
const slice = entries.slice(i, i + conc)
|
||
await Promise.all(
|
||
slice.map(async ([key, path]) => {
|
||
out[key] = await bareTopReadProc(ctx, path)
|
||
})
|
||
)
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} o
|
||
* @param {number} maxChars
|
||
* @returns {string}
|
||
*/
|
||
function bareTopTruncateJsonPretty(o, maxChars) {
|
||
const raw = JSON.stringify(o, null, 2)
|
||
if (raw.length <= maxChars) return raw
|
||
return raw.slice(0, Math.max(0, maxChars - 24)) + '\n… (truncated) …\n'
|
||
}
|
||
|
||
/** @param {number} n @returns {string} */
|
||
function bareTopFormatBytes(n) {
|
||
const x = Number(n)
|
||
if (!Number.isFinite(x) || x < 0) return '—'
|
||
if (x < 1024) return String(Math.floor(x)) + ' B'
|
||
if (x < 1048576) return (x / 1024).toFixed(1) + ' KiB'
|
||
if (x < 1073741824) return (x / 1048576).toFixed(1) + ' MiB'
|
||
return (x / 1073741824).toFixed(2) + ' GiB'
|
||
}
|
||
|
||
/** @param {number} ms @returns {string} */
|
||
function bareTopFormatDuration(ms) {
|
||
const x = Number(ms)
|
||
if (!Number.isFinite(x) || x < 0) return '—'
|
||
if (x < 1000) return String(Math.floor(x)) + ' ms'
|
||
if (x < 60000) return (x / 1000).toFixed(1) + ' s'
|
||
if (x < 3600000) return Math.floor(x / 60000) + ' m'
|
||
return (x / 3600000).toFixed(1) + ' h'
|
||
}
|
||
|
||
/**
|
||
* @param {Record<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: healthy sessions read ~95–100; issues pull it down.
|
||
* Weights: read/metrics gaps, delegate inflight keys + sum, replication stall,
|
||
* boot budgets, initd failures, peer sanity / mismatch.
|
||
* @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)
|
||
const n = keys.length
|
||
const penK = Math.min(14, n * 2)
|
||
if (penK > 0) {
|
||
score -= penK
|
||
parts.push('inflight_k_' + n + '=-' + penK)
|
||
}
|
||
let sum = 0
|
||
for (let i = 0; i < keys.length; i++) {
|
||
sum += Math.max(0, Number(o[keys[i]]) || 0)
|
||
}
|
||
const penS = Math.min(28, sum * 3)
|
||
if (penS > 0) {
|
||
score -= penS
|
||
parts.push('inflight_sum_' + sum + '=-' + penS)
|
||
}
|
||
}
|
||
|
||
const rep =
|
||
live.replicationLive && typeof live.replicationLive === 'object'
|
||
? /** @type {Record<string, unknown>} */ (live.replicationLive)
|
||
: null
|
||
const hintRaw = rep && rep.stallHint
|
||
if (hintRaw != null) {
|
||
const hs = String(hintRaw).trim().toLowerCase()
|
||
if (
|
||
hs &&
|
||
hs !== '0' &&
|
||
hs !== 'false' &&
|
||
hs !== 'none' &&
|
||
hs !== 'off' &&
|
||
hs !== 'null' &&
|
||
hs !== 'undefined'
|
||
) {
|
||
const pen = 20
|
||
score -= pen
|
||
parts.push('stall=-' + pen)
|
||
}
|
||
}
|
||
|
||
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(peers)) {
|
||
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) {
|
||
const fetchStart = Date.now()
|
||
let readErr = null
|
||
const env =
|
||
ctx.env && typeof ctx.env === 'object'
|
||
? /** @type {Record<string, string>} */ (ctx.env)
|
||
: {}
|
||
const concRaw = parseInt(env.BARE_TOP_FETCH_CONCURRENCY || '6', 10)
|
||
const concurrency = Number.isFinite(concRaw) ? concRaw : 6
|
||
const snapshotLite =
|
||
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
|
||
|
||
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
|
||
}
|
||
}
|
||
} catch (e) {
|
||
readErr = (e && /** @type {{ message?: string }} */ (e).message) || String(e)
|
||
}
|
||
|
||
if (Object.keys(fileTexts).length === 0) {
|
||
fileTexts = await bareTopReadProcBatch(ctx, procEntryList, concurrency)
|
||
}
|
||
|
||
const atMs = fastAtMs || Date.now()
|
||
|
||
/** @type {Record<string, unknown> | null} */
|
||
let metricsLive = null
|
||
/** @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 = ''
|
||
const cpuRaw = await bareTopReadProc(ctx, '/proc/cpuinfo')
|
||
for (const line of cpuRaw.split('\n')) {
|
||
if (line.startsWith('model name') || line.startsWith('Model')) {
|
||
cpuLine = line.replace(/^[^:]+:\s*/, '').trim().slice(0, 72)
|
||
break
|
||
}
|
||
}
|
||
|
||
const hostOs = bareTopParsedFile(fileTexts, 'hostOs')
|
||
const metaAt =
|
||
metricsLive && typeof metricsLive.atMs === 'number' ? metricsLive.atMs : atMs
|
||
|
||
/** @type {Record<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
|
||
const peersN = Number(metricsLive && metricsLive.peers) || 0
|
||
const healthD = bareTopHealthScoreDetail(metricsLive, peersN, readErr)
|
||
|
||
return {
|
||
metricsLive,
|
||
resources,
|
||
features,
|
||
initdGraph,
|
||
netSummary,
|
||
meminfoLine,
|
||
loadavgLine,
|
||
cpuLine,
|
||
hostOs,
|
||
atMs,
|
||
metaAtMs: metaAt,
|
||
readErr,
|
||
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'} key
|
||
*/
|
||
function bareTopProcessSortKey(row, key) {
|
||
if (key === 'name') return String(row.name || row.label || '').toLowerCase()
|
||
if (key === 'state') return String(row.state || '').toLowerCase()
|
||
if (key === 'time') return bareTopProcessStartedMs(row)
|
||
return bareTopProcessPid(row)
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>[]} rows
|
||
* @param {'pid'|'name'|'state'|'time'} sortKey
|
||
* @param {boolean} asc
|
||
*/
|
||
function bareTopSortProcessRows(rows, sortKey, asc) {
|
||
const dir = asc ? 1 : -1
|
||
const out = rows.slice()
|
||
out.sort((a, b) => {
|
||
const ka = bareTopProcessSortKey(a, sortKey)
|
||
const kb = bareTopProcessSortKey(b, sortKey)
|
||
let c = 0
|
||
if (typeof ka === 'number' && typeof kb === 'number') c = ka - kb
|
||
else c = String(ka).localeCompare(String(kb))
|
||
if (c !== 0) return c * dir
|
||
return bareTopProcessPid(a) - bareTopProcessPid(b)
|
||
})
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {string[]} cells
|
||
* @param {number[]} widths
|
||
* @param {('l'|'r')[]} align
|
||
* @param {number} maxLineLen
|
||
*/
|
||
function bareTopFormatFixedColumns(cells, widths, align, maxLineLen) {
|
||
const parts = []
|
||
let total = 0
|
||
for (let i = 0; i < cells.length && i < widths.length; i++) {
|
||
const w = Math.max(1, widths[i] | 0)
|
||
let c = bareTopTruncateCell(cells[i], w, true)
|
||
if (c.length > w) c = c.slice(0, w)
|
||
const pad = w - c.length
|
||
if (align[i] === 'r') {
|
||
parts.push(' '.repeat(Math.max(0, pad)) + c)
|
||
} else {
|
||
parts.push(c + ' '.repeat(Math.max(0, pad)))
|
||
}
|
||
total += w + (i < cells.length - 1 ? 1 : 0)
|
||
}
|
||
let line = parts.join(' ')
|
||
if (line.length > maxLineLen) line = line.slice(0, maxLineLen)
|
||
return line
|
||
}
|
||
|
||
/**
|
||
* @param {number} score 0–100
|
||
* @param {number} width
|
||
* @param {boolean} ascii
|
||
*/
|
||
function bareTopHealthBarLine(score, width, ascii) {
|
||
const w = Math.max(4, width | 0)
|
||
const p = Math.min(100, Math.max(0, 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)
|
||
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)
|
||
out.push(' ' + ks + padK + ' ' + vs)
|
||
}
|
||
if (pairs.length > lim) out.push(' … +' + (pairs.length - lim) + ' more keys')
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} o
|
||
* @param {number} cols
|
||
*/
|
||
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 {
|
||
out.push(' ' + k + ': ' + String(v))
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @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 + '=' + String(sub[sk]))
|
||
.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))
|
||
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']
|
||
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 ''
|
||
}
|
||
|
||
/** Default overview section ids in render order */
|
||
var BARE_TOP_OVERVIEW_DEFAULT_SECTIONS = [
|
||
'session',
|
||
'pipeline',
|
||
'delegates',
|
||
'replication',
|
||
'kernel',
|
||
'warm',
|
||
'ipc',
|
||
'boot',
|
||
'swarm',
|
||
'prochist',
|
||
'protomux',
|
||
'meta',
|
||
'worker',
|
||
'initd',
|
||
'fdagg',
|
||
'delegate_rates',
|
||
'fairness',
|
||
'subprocess',
|
||
'host',
|
||
'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'])
|
||
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
|
||
* }} BareTopOverviewOpts
|
||
*/
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} snap
|
||
* @param {BareTopOverviewOpts} opts
|
||
* @returns {{ lines: string[], activeSections: string[] }}
|
||
*/
|
||
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 = []
|
||
|
||
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
|
||
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')) {
|
||
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))
|
||
)
|
||
}
|
||
|
||
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('kernel')) {
|
||
const prevK =
|
||
opts.prevMetrics && opts.prevMetrics.kernelCounters
|
||
? opts.prevMetrics.kernelCounters
|
||
: null
|
||
const kl = bareTopKernelCounterLines(
|
||
m && m.kernelCounters,
|
||
prevK,
|
||
opts.deltaMode,
|
||
12,
|
||
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')) {
|
||
pushSec('IPC telemetry', 'ipc', bareTopIpcTelemetrySummaryLines(m && m.ipcTelemetry, 8))
|
||
}
|
||
|
||
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')) {
|
||
const wl = bareTopWorkerBudgetCompactLine(m && m.workerBudgetHints)
|
||
if (wl) pushSec('Worker budget', 'worker', [wl])
|
||
}
|
||
|
||
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) {
|
||
pushSec('Fairness', 'fairness', fc(snap.fairnessSnapshot, 14, 32))
|
||
}
|
||
|
||
if (want('subprocess') && snap.subprocessBridge && fc) {
|
||
pushSec('Subprocess bridge', 'subprocess', fc(snap.subprocessBridge, 12, 24))
|
||
}
|
||
|
||
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) {
|
||
const err = String(snap.readErr)
|
||
const wrapped = err.split('\n').slice(0, 3)
|
||
/** @type {string[]} */
|
||
const erl = []
|
||
for (const w of wrapped) erl.push(' warn ' + w)
|
||
if (err.split('\n').length > 3) erl.push(' … (truncated)')
|
||
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) {
|
||
lines.push('')
|
||
lines.push(bareTopSanitizeVisible(' health detail ' + opts.healthBreakdown))
|
||
active.push('health_detail')
|
||
}
|
||
|
||
return { lines, activeSections: active }
|
||
}
|
||
|
||
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'
|
||
}
|
||
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)
|
||
}
|
||
|
||
/** Full-screen TUI for /bin/baretop — session / kernel dashboard (preamble; no import). */
|
||
|
||
/** User-visible strings (i18n-ready single object). */
|
||
var bareTopStrings = {
|
||
title: 'baretop',
|
||
helpKeys: 'Keys',
|
||
quitHint: '^C also quits',
|
||
noHtop:
|
||
'Logical process table (schema 7) lists synthetic PIDs/jobs — not host OS processes.',
|
||
noHtop2:
|
||
' Session metrics, delegates, /proc/bare_os mirrors, and ctx snapshots fill other tabs.',
|
||
pressCloseHelp: 'Esc or any other key closes help.',
|
||
activity: 'Activity (per refresh)',
|
||
session: 'Session',
|
||
delegates: 'Delegates',
|
||
pipeline: 'Pipeline limits',
|
||
exportOk: 'exported snapshot',
|
||
exportFail: 'export failed',
|
||
paused: 'PAUSED',
|
||
na: 'N/A',
|
||
overviewPinned:
|
||
'Overview: activity + sparklines stay visible; PgUp/Dn scrolls detail below.',
|
||
sortHint: 'F6 or S sort V tree columns: pid name state time',
|
||
layoutVersion: 'baretop-ui-2'
|
||
}
|
||
|
||
/**
|
||
* @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: ''
|
||
}
|
||
if (highContrast) {
|
||
return {
|
||
kw: '\x1b[1;36m',
|
||
dim: '\x1b[4m',
|
||
warn: '\x1b[1;33m',
|
||
bad: '\x1b[1;31m',
|
||
good: '\x1b[1;32m',
|
||
barHi: '\x1b[1;35m',
|
||
border: '\x1b[1;37m',
|
||
header: '\x1b[44m\x1b[1;97m',
|
||
sel: '\x1b[7;1m',
|
||
zebraBg: '',
|
||
meterLow: '\x1b[1;32m',
|
||
meterMid: '\x1b[1;33m',
|
||
meterHi: '\x1b[1;31m',
|
||
reset: '\x1b[0m'
|
||
}
|
||
}
|
||
if (theme === 'light') {
|
||
return {
|
||
kw: '\x1b[34m',
|
||
dim: '\x1b[90m',
|
||
warn: '\x1b[33;1m',
|
||
bad: '\x1b[31;1m',
|
||
good: '\x1b[32;1m',
|
||
barHi: '\x1b[35;1m',
|
||
border: '\x1b[90m',
|
||
header: '\x1b[44m\x1b[97m',
|
||
sel: '\x1b[30;47m',
|
||
zebraBg: '\x1b[48;5;252m',
|
||
meterLow: '\x1b[32m',
|
||
meterMid: '\x1b[33m',
|
||
meterHi: '\x1b[31m',
|
||
reset: '\x1b[0m'
|
||
}
|
||
}
|
||
if (theme === 'none') {
|
||
return {
|
||
kw: '',
|
||
dim: '',
|
||
warn: '',
|
||
bad: '',
|
||
good: '',
|
||
barHi: '',
|
||
border: '',
|
||
header: '',
|
||
sel: '',
|
||
zebraBg: '',
|
||
meterLow: '',
|
||
meterMid: '',
|
||
meterHi: '',
|
||
reset: ''
|
||
}
|
||
}
|
||
return {
|
||
kw: '\x1b[36m',
|
||
dim: '\x1b[2m',
|
||
warn: '\x1b[33m',
|
||
bad: '\x1b[31m',
|
||
good: '\x1b[32m',
|
||
barHi: '\x1b[35m',
|
||
border: '\x1b[90m',
|
||
header: '\x1b[44m\x1b[97m',
|
||
sel: '\x1b[7m',
|
||
zebraBg: '\x1b[48;5;235m',
|
||
meterLow: '\x1b[32m',
|
||
meterMid: '\x1b[33m',
|
||
meterHi: '\x1b[31m',
|
||
reset: '\x1b[0m'
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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 }} o */
|
||
function bareTopDrainKeys(q, max, o) {
|
||
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 === 5) return { type: 'refresh' }
|
||
if (fn === 6) return { type: 'sort_menu' }
|
||
if (fn === 9) return { type: 'signal_menu' }
|
||
}
|
||
if (ev.type === 'ctrl' && ev.code === 'backspace')
|
||
return { type: 'filter_bs' }
|
||
if (ev.type === 'key' && ev.ch === '\n')
|
||
return o.inFilter ? { type: 'filter_enter' } : { type: 'enter' }
|
||
if (ev.type === 'nav') {
|
||
if (ev.key === 'pageup') return { type: 'scroll', dir: -1, amt: 5 }
|
||
if (ev.key === 'pagedown') return { type: 'scroll', dir: 1, amt: 5 }
|
||
if (ev.key === 'home') return { type: 'scroll', dir: 'home' }
|
||
if (ev.key === 'end') return { type: 'scroll', dir: 'end' }
|
||
if (ev.key === 'up') return { type: 'scroll', dir: -1, amt: 1 }
|
||
if (ev.key === 'down') return { type: 'scroll', dir: 1, amt: 1 }
|
||
if (o.vi) {
|
||
if (ev.key === 'left') return { type: 'focus', dir: -1 }
|
||
if (ev.key === 'right') return { type: 'focus', dir: 1 }
|
||
}
|
||
}
|
||
if (ev.type === 'key' && ev.ch) {
|
||
const ch = ev.ch
|
||
if (ch === 'q' || ch === 'Q') return { type: 'quit' }
|
||
if (ch === 'r' || ch === 'R') return { type: 'refresh' }
|
||
if (ch === 'h' || ch === '?' || ch === 'H') return { type: 'help_open' }
|
||
if (ch === ' ') return { type: 'pause_toggle' }
|
||
if (ch === '.') return { type: 'step' }
|
||
if (ch === 'd' || ch === 'D') return { type: 'delta_toggle' }
|
||
if (ch === 'e' || ch === 'E') return { type: 'export' }
|
||
if (ch === 'f' || ch === 'F') return { type: 'fullscreen_toggle' }
|
||
if (ch === 't' || ch === 'T') return { type: 'clock_toggle' }
|
||
if (ch === '[') return { type: 'tab_prev' }
|
||
if (ch === ']') return { type: 'tab_next' }
|
||
if (ch === '/') return { type: 'filter_open' }
|
||
if (ch === 'k' || ch === 'K') return { type: 'signal_menu' }
|
||
if (ch === 'S') return { type: 'sort_menu' }
|
||
if (ch === 'V') return { type: 'tree_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 (ch === 'n' && o.vi) return { type: 'filter_next' }
|
||
if (ch === 'N' && o.vi) return { type: 'filter_prev' }
|
||
if (ch >= '1' && ch <= '9') return { type: 'tab', n: ch.charCodeAt(0) - 0x31 }
|
||
if (ch === '0') return { type: 'tab', n: -1 }
|
||
if (ch.length === 1 && ch >= ' ' && ch.charCodeAt(0) > 32)
|
||
return { type: 'filter_char', ch }
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
|
||
/**
|
||
* @param {number[]} values
|
||
* @param {number} width
|
||
* @param {boolean} ascii
|
||
* @param {boolean} logScale
|
||
* @param {boolean} braille
|
||
*/
|
||
function bareTopSparkline(values, width, ascii, logScale, braille) {
|
||
if (width <= 0) return ''
|
||
const slice =
|
||
values.length > width ? values.slice(values.length - width) : values.slice()
|
||
if (slice.length === 0) return ' '.repeat(width)
|
||
const vmap = logScale
|
||
? slice.map((v) => Math.log1p(Math.max(0, v)))
|
||
: slice.slice()
|
||
let min = vmap[0]
|
||
let max = vmap[0]
|
||
for (let i = 1; i < vmap.length; i++) {
|
||
if (vmap[i] < min) min = vmap[i]
|
||
if (vmap[i] > max) max = vmap[i]
|
||
}
|
||
const span = max - min || 1
|
||
const uni = '\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588'
|
||
const br = '\u2800\u2840\u28c0\u28e0\u28f0\u28f4\u28f6\u28ff'
|
||
const asc = ' .:-=+*#'
|
||
let out = ''
|
||
const pad = width - slice.length
|
||
for (let i = 0; i < pad; i++) out += ascii ? ' ' : '\u2581'
|
||
for (let i = 0; i < vmap.length; i++) {
|
||
const v = vmap[i]
|
||
const t = (v - min) / span
|
||
let idx = Math.min(7, Math.floor(t * 8))
|
||
if (idx < 0) idx = 0
|
||
if (ascii) out += asc.charAt(idx)
|
||
else if (braille) out += br.charAt(idx)
|
||
else out += uni.charAt(idx)
|
||
}
|
||
return out.slice(0, width)
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} inflight
|
||
* @param {number} maxWidth
|
||
* @param {boolean} use256
|
||
* @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 : ['']
|
||
}
|
||
|
||
/**
|
||
* @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))
|
||
let sv = o[k] == null ? bareTopStrings.na : String(o[k])
|
||
sv = bareTopTruncateCell(sv, Math.max(8, cols - keyW - 4), true)
|
||
out.push(' ' + label + pad + ' ' + sv)
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {Record<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
|
||
}
|
||
|
||
/**
|
||
* @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 {Awaited<ReturnType<typeof bareTopFetchSnapshot>> | null} snap
|
||
* @param {string} filterProcess
|
||
* @param {'pid' | 'name' | 'state' | 'time'} sortKey
|
||
* @param {boolean} sortAsc
|
||
*/
|
||
function bareTopLiveProcessList(snap, filterProcess, sortKey, sortAsc) {
|
||
const pt =
|
||
snap &&
|
||
snap.extra &&
|
||
snap.extra.processTable &&
|
||
typeof snap.extra.processTable === 'object'
|
||
? snap.extra.processTable
|
||
: null
|
||
const raw = bareTopProcessRowsFromTable(pt)
|
||
const pf = String(filterProcess || '').toLowerCase()
|
||
const filtered = pf.length
|
||
? raw.filter((r) =>
|
||
String(r.name || r.label || '')
|
||
.toLowerCase()
|
||
.includes(pf)
|
||
)
|
||
: raw.slice()
|
||
return bareTopSortProcessRows(filtered, sortKey, sortAsc)
|
||
}
|
||
|
||
/**
|
||
* @param {Record<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 =
|
||
ctx.env && typeof ctx.env === 'object'
|
||
? /** @type {Record<string, string>} */ (ctx.env)
|
||
: {}
|
||
const noAlt =
|
||
envTop.BARE_TOP_NO_ALTSCREEN != null &&
|
||
String(envTop.BARE_TOP_NO_ALTSCREEN) !== ''
|
||
const intervalRaw = parseInt(envTop.BARE_TOP_INTERVAL_MS || '1000', 10)
|
||
const intervalMs = Number.isFinite(intervalRaw)
|
||
? Math.min(10000, Math.max(250, intervalRaw))
|
||
: 1000
|
||
const uiMinRaw = parseInt(envTop.BARE_TOP_UI_MIN_MS || '0', 10)
|
||
const uiMinMs = Number.isFinite(uiMinRaw) ? Math.max(0, uiMinRaw) : 0
|
||
const emaRaw = parseFloat(envTop.BARE_TOP_EMA_ALPHA || '0')
|
||
const emaAlpha = Number.isFinite(emaRaw) ? Math.min(1, Math.max(0, emaRaw)) : 0
|
||
const theme = String(envTop.BARE_TOP_THEME || 'dark').toLowerCase()
|
||
const asciiUi =
|
||
envTop.BARE_TOP_ASCII_UI === '1' ||
|
||
(envTop.LANG && String(envTop.LANG).toLowerCase().includes('ascii'))
|
||
const asciiGraph = envTop.BARE_TOP_ASCII_GRAPH === '1' || asciiUi
|
||
const braille = envTop.BARE_TOP_BRAILLE_SPARK === '1'
|
||
const logSpark = envTop.BARE_TOP_LOG_SPARK === '1'
|
||
const use256 =
|
||
useColor &&
|
||
(String(envTop.COLORTERM || '').toLowerCase() === 'truecolor' ||
|
||
String(envTop.COLORTERM || '').includes('256') ||
|
||
envTop.BARE_TOP_COLOR256 === '1')
|
||
const viKeys =
|
||
envTop.BARE_TOP_VI_KEYS === '1' || envTop.BARE_TOP_VI_KEYS === 'true'
|
||
const highContrast =
|
||
envTop.BARE_TOP_HIGH_CONTRAST === '1' ||
|
||
envTop.BARE_TOP_HIGH_CONTRAST === 'true'
|
||
const layout = String(envTop.BARE_TOP_LAYOUT || 'stacked').toLowerCase()
|
||
const exportPath = String(
|
||
envTop.BARE_TOP_EXPORT_PATH || '/tmp/baretop-snapshot.json'
|
||
).trim()
|
||
const icons = envTop.BARE_TOP_ICONS === '1'
|
||
const monoUi = envTop.BARE_TOP_MONO === '1' || asciiUi
|
||
const useFrames = envTop.BARE_TOP_FRAMES === '1'
|
||
const incrementalClear = envTop.BARE_TOP_INCREMENTAL === '1'
|
||
const hideFsTabs =
|
||
envTop.BARE_TOP_FULLSCREEN_HIDE_TABS === '1' ||
|
||
envTop.BARE_TOP_FULLSCREEN_HIDE_TABS === 'true'
|
||
const ringCapRaw = parseInt(envTop.BARE_TOP_RING_CAP || '72', 10)
|
||
const RING_CAP = Number.isFinite(ringCapRaw)
|
||
? Math.min(240, Math.max(8, ringCapRaw))
|
||
: 72
|
||
const 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 argvTop =
|
||
Array.isArray(/** @type {unknown} */ (ctx).bareTopArgv)
|
||
? /** @type {string[]} */ (/** @type {unknown} */ (ctx).bareTopArgv)
|
||
: null
|
||
const invName =
|
||
argvTop && argvTop[0]
|
||
? String(argvTop[0]).replace(/^.*\//, '')
|
||
: 'baretop'
|
||
const displayTitle = invName === 'btop' ? 'btop' : bareTopStrings.title
|
||
|
||
const pal = bareTopTheme(theme, useColor, highContrast)
|
||
|
||
const TAB_NAMES = [
|
||
'overview',
|
||
'processes',
|
||
'initd',
|
||
'network',
|
||
'features',
|
||
'diagnostics',
|
||
'operator',
|
||
'pear',
|
||
'catalog',
|
||
'host',
|
||
'keys'
|
||
]
|
||
const NTABS = TAB_NAMES.length
|
||
const TAB_PROC = 1
|
||
|
||
/** @type {number[]} */
|
||
const keyq = []
|
||
function onData(chunk) {
|
||
keyq.push(...bareTopChunkBytes(chunk))
|
||
bareTopStripBracketedPaste(keyq)
|
||
}
|
||
stdin.on('data', onData)
|
||
|
||
let needsRedraw = true
|
||
let resizePulse = false
|
||
function onResize() {
|
||
needsRedraw = true
|
||
resizePulse = true
|
||
}
|
||
if (typeof stdout.on === 'function') {
|
||
try {
|
||
stdout.on('resize', onResize)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
try {
|
||
if (typeof process !== 'undefined' && typeof process.on === 'function') {
|
||
process.on('SIGWINCH', onResize)
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
|
||
function termDims() {
|
||
const env = envTop
|
||
const rawCols =
|
||
/** @type {{ columns?: number }} */ (stdout).columns ||
|
||
parseInt(env.COLUMNS || '80', 10) ||
|
||
80
|
||
const rows = /** @type {{ rows?: number }} */ (stdout).rows ||
|
||
parseInt(env.LINES || '24', 10) ||
|
||
24
|
||
const emergencyNarrow = rawCols < 40
|
||
return {
|
||
cols: Math.max(20, rawCols),
|
||
rows: Math.max(12, rows),
|
||
emergencyNarrow
|
||
}
|
||
}
|
||
|
||
function headerLine(title) {
|
||
const bar =
|
||
(useColor ? bareEditSgr('status', true) : '') +
|
||
title +
|
||
(useColor ? EDIT_ANSI_RESET : '')
|
||
return bar
|
||
}
|
||
|
||
/** @type {number[]} */
|
||
const ringExecDelta = []
|
||
/** @type {number[]} */
|
||
const ringPipeDelta = []
|
||
/** @type {number[]} */
|
||
const ringWallDelta = []
|
||
/** @type {number[]} */
|
||
const ringPeers = []
|
||
/** @type {number[]} */
|
||
const ringReplSkip = []
|
||
/** @type {number[]} */
|
||
const ringProtoMux = []
|
||
let prevExec = -1
|
||
let prevPipe = -1
|
||
let prevWall = -1
|
||
let emaExec = -1
|
||
let emaPipe = -1
|
||
let emaWall = -1
|
||
|
||
function pushRing(arr, v) {
|
||
arr.push(v)
|
||
while (arr.length > RING_CAP) arr.shift()
|
||
}
|
||
|
||
let tab = 0
|
||
let helpMode = false
|
||
let quit = false
|
||
/** @type {Awaited<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 filterProcess = ''
|
||
/** @type {'initd' | 'process' | 'overview'} */
|
||
let filterWhich = 'initd'
|
||
/** @type {string} */
|
||
let overviewSectionFilter = ''
|
||
/** @type {string[]} */
|
||
let activeOverviewSections = []
|
||
let procTreeMode = false
|
||
let firstDrawDone = false
|
||
let lastDbgLog = 0
|
||
const procSortEnv = String(envTop.BARE_TOP_PROC_SORT || 'pid').toLowerCase()
|
||
/** @type {'pid' | 'name' | 'state' | 'time'} */
|
||
let procSortKey =
|
||
procSortEnv === 'name'
|
||
? 'name'
|
||
: procSortEnv === 'state'
|
||
? 'state'
|
||
: procSortEnv === 'time'
|
||
? 'time'
|
||
: '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() {
|
||
lastSnap = await bareTopFetchSnapshot(ctx)
|
||
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)
|
||
}
|
||
|
||
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 pf = filterProcess.toLowerCase()
|
||
const filtered = pf.length
|
||
? rawRows.filter((r) =>
|
||
String(r.name || r.label || '')
|
||
.toLowerCase()
|
||
.includes(pf)
|
||
)
|
||
: rawRows.slice()
|
||
return procTreeMode
|
||
? bareTopProcessTreeOrder(filtered)
|
||
: bareTopSortProcessRows(filtered, procSortKey, procSortAsc)
|
||
}
|
||
|
||
/** @type {string} */
|
||
let out = ''
|
||
|
||
function draw() {
|
||
const now = Date.now()
|
||
if (resizePulse) {
|
||
resizePulse = false
|
||
lastDrawAt = 0
|
||
}
|
||
if (uiMinMs > 0 && now - lastDrawAt < uiMinMs && !helpMode) return
|
||
lastDrawAt = now
|
||
const { cols, rows, emergencyNarrow } = termDims()
|
||
const narrow = cols < 80
|
||
let scrollHint = ''
|
||
const softIncr =
|
||
incrementalClear || (no2jAfterFirst && firstDrawDone && !helpMode)
|
||
out = softIncr ? '\x1b[?25l\x1b[H' : '\x1b[?25l\x1b[2J\x1b[H'
|
||
firstDrawDone = true
|
||
const sparkAscii = asciiGraph || monoUi
|
||
|
||
const clock =
|
||
utcClock && typeof Date.prototype.toISOString === 'function'
|
||
? new Date().toISOString().slice(11, 19) + 'Z'
|
||
: new Date().toTimeString().slice(0, 8)
|
||
|
||
const tabTitle = TAB_NAMES[tab] || 'tab'
|
||
const icon = icons ? '\u25cf ' : ''
|
||
let title =
|
||
icon +
|
||
displayTitle +
|
||
' ' +
|
||
(helpMode
|
||
? '— help '
|
||
: '— ' + tabTitle + ' ') +
|
||
'| ' +
|
||
clock +
|
||
' | ' +
|
||
intervalMs +
|
||
'ms'
|
||
if (paused) title += ' | ' + bareTopStrings.paused
|
||
if (deltaMode) title += ' | DELTA'
|
||
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'
|
||
let hdrRow = 2
|
||
|
||
if (helpMode) {
|
||
const dim = useColor ? pal.dim : ''
|
||
const kw = useColor ? pal.kw : ''
|
||
const rst = useColor ? pal.reset : ''
|
||
out +=
|
||
'\r\n' +
|
||
kw +
|
||
bareTopStrings.helpKeys +
|
||
rst +
|
||
'\r\n' +
|
||
dim +
|
||
'Navigation' +
|
||
rst +
|
||
'\r\n' +
|
||
' arrows Move list / process selection (Up/Down; PgUp/Dn page)\r\n' +
|
||
' g G Jump list top / bottom\r\n' +
|
||
' [ ] Prev / next tab\r\n' +
|
||
' 1-9 Jump tabs 1–9 (overview\u2026catalog); 0 = keys/help tab\r\n' +
|
||
' Esc Close help or cancel filter\r\n' +
|
||
'\r\n' +
|
||
dim +
|
||
'Tabs' +
|
||
rst +
|
||
'\r\n' +
|
||
' 1 overview 2 processes 3 initd 4 network 5 features 6 diagnostics\r\n' +
|
||
' 7 operator 8 pear 9 catalog (host: use [ ]) 0 keys\r\n' +
|
||
'\r\n' +
|
||
dim +
|
||
'Actions' +
|
||
rst +
|
||
'\r\n' +
|
||
' q Quit r F5 refresh+burst sp pause . step (paused)\r\n' +
|
||
' d delta e export f fullscreen t UTC clock h F1 help\r\n' +
|
||
' / Filter overview sections / initd / processes\r\n' +
|
||
' < > Sort processes (pid name state time)\r\n' +
|
||
' F6 S Cycle process sort\r\n' +
|
||
' V Toggle process tree order\r\n' +
|
||
' Enter Toggle process JSON detail (processes tab)\r\n' +
|
||
' k F9 Signal menu (selected logical PID)\r\n' +
|
||
'\r\n' +
|
||
dim +
|
||
'Notes' +
|
||
rst +
|
||
'\r\n' +
|
||
bareTopStrings.overviewPinned +
|
||
'\r\n' +
|
||
bareTopStrings.noHtop +
|
||
'\r\n' +
|
||
bareTopStrings.noHtop2 +
|
||
'\r\n' +
|
||
'Pear IPC: bare_os_*, pear:* (booter pear_ipc registry).\r\n' +
|
||
(scrollRegionOn
|
||
? 'BARE_TOP_SCROLL_REGION: DECSTBM scrolling is experimental.\r\n'
|
||
: '') +
|
||
'\r\n' +
|
||
bareTopStrings.pressCloseHelp +
|
||
'\r\n'
|
||
const used = 28
|
||
for (let r = used; r < rows; r++) {
|
||
out += bareEditCup(r, 1) + '\x1b[K'
|
||
}
|
||
out += bareEditCup(rows, 1) + '\x1b[K'
|
||
out += dim + bareTopStrings.quitHint + rst
|
||
out += '\x1b[?25h'
|
||
bareTopWrite(ctx, stdout, out)
|
||
return
|
||
}
|
||
|
||
const snap = lastSnap
|
||
const mem = snap ? snap.meminfoLine : ''
|
||
const load = snap ? snap.loadavgLine : ''
|
||
const cpu = snap ? snap.cpuLine : ''
|
||
const sparkW = Math.min(48, Math.max(8, cols - (narrow ? 22 : 28)))
|
||
|
||
const staleMs =
|
||
snap && snap.metaAtMs ? Math.max(0, Date.now() - snap.metaAtMs) : 0
|
||
const staleStr =
|
||
staleMs > 2000
|
||
? (useColor ? pal.warn : '') +
|
||
' stale ' +
|
||
bareTopFormatDuration(staleMs) +
|
||
(useColor ? pal.reset : '')
|
||
: ''
|
||
const fetchB =
|
||
snap && snap.fetchWallMs != null
|
||
? (useColor ? pal.dim : '') +
|
||
' fetch ' +
|
||
String(snap.fetchWallMs) +
|
||
'ms' +
|
||
(useColor ? pal.reset : '')
|
||
: ''
|
||
|
||
function emitHdrLn(line) {
|
||
out += line + '\r\n\x1b[K'
|
||
hdrRow++
|
||
}
|
||
|
||
{
|
||
const mm = bareTopParseMeminfoMetrics(mem)
|
||
const parts = mm.pairs.map((p) => p.label + ':' + p.value)
|
||
let memMain = parts.length ? parts.join(' ') : mem || '(no meminfo)'
|
||
memMain += staleStr + fetchB
|
||
const mw = bareTopWrapLine(memMain, cols - 2)
|
||
for (let mi = 0; mi < mw.length; mi++) emitHdrLn(mw[mi])
|
||
if (
|
||
mm.memTotal &&
|
||
mm.memAvail != null &&
|
||
Number.isFinite(mm.memTotal) &&
|
||
mm.memTotal > 0
|
||
) {
|
||
const pctAv = Math.round((mm.memAvail / mm.memTotal) * 100)
|
||
const mw2 = Math.min(28, Math.max(8, Math.floor(cols / 3)))
|
||
const bar = bareTopFormatMeterBar(
|
||
pctAv,
|
||
mw2,
|
||
sparkAscii,
|
||
{
|
||
low: pal.meterLow,
|
||
mid: pal.meterMid,
|
||
hi: pal.meterHi,
|
||
reset: pal.reset
|
||
},
|
||
useColor
|
||
)
|
||
emitHdrLn(
|
||
(useColor ? pal.dim : '') +
|
||
' MemAvail ' +
|
||
bar +
|
||
' ' +
|
||
pctAv +
|
||
'%' +
|
||
(useColor ? pal.reset : '')
|
||
)
|
||
}
|
||
}
|
||
|
||
{
|
||
const la = bareTopParseLoadavg(load)
|
||
let loadCol = load || ''
|
||
if (la) {
|
||
const hi = la.one >= 8 ? pal.bad : la.one >= 4 ? pal.warn : pal.dim
|
||
loadCol =
|
||
(useColor ? hi : '') +
|
||
'load ' +
|
||
la.one.toFixed(2) +
|
||
' ' +
|
||
la.five.toFixed(2) +
|
||
' ' +
|
||
la.fifteen.toFixed(2) +
|
||
(la.running != null && la.total != null
|
||
? ' tasks ' + la.running + '/' + la.total
|
||
: '') +
|
||
(useColor ? pal.reset : '')
|
||
}
|
||
let cpuExtra = cpu ? 'cpu: ' + cpu : ''
|
||
if (snap && snap.hostStats && typeof snap.hostStats === 'object') {
|
||
const hs = /** @type {Record<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
|
||
for (let ti = 0; ti < NTABS; ti++) {
|
||
const nm = TAB_NAMES[ti] || 't'
|
||
let lab
|
||
if (short) {
|
||
if (ti === 10) lab = '0'
|
||
else if (ti === 9) lab = 'H'
|
||
else lab = String(ti + 1)
|
||
} else if (ti === 10) lab = '0:' + nm.slice(0, 2)
|
||
else if (ti === 9) lab = 'H:' + nm.slice(0, 2)
|
||
else lab = String(ti + 1) + ':' + nm.slice(0, 3)
|
||
const on = ti === tab
|
||
stripParts.push(
|
||
(useColor && on ? pal.sel : useColor ? pal.border : '') +
|
||
(on ? '[' : '') +
|
||
lab +
|
||
(on ? ']' : '') +
|
||
(useColor ? pal.reset : '')
|
||
)
|
||
}
|
||
const stripLine = stripParts.join(short ? '' : ' ')
|
||
emitHdrLn((useColor ? pal.header : '') + stripLine.slice(0, cols - 1) + pal.reset)
|
||
}
|
||
|
||
if (snap && snap.healthScore != null) {
|
||
const hbw = Math.min(36, Math.max(8, cols - 24))
|
||
const hb = bareTopHealthBarLine(snap.healthScore, hbw, sparkAscii)
|
||
emitHdrLn(
|
||
(useColor ? pal.good : '') +
|
||
'health ' +
|
||
hb +
|
||
(useColor ? pal.reset : '')
|
||
)
|
||
}
|
||
|
||
const mainEnd = fullscreenPanel ? rows - 1 : rows - 2
|
||
|
||
/**
|
||
* @param {string[]} rawLines
|
||
* @param {number} tabIdx
|
||
* @param {number} startRow
|
||
*/
|
||
function drawScrollableLines(rawLines, tabIdx, startRow) {
|
||
const wrapW = Math.max(16, cols - 2)
|
||
/** @type {string[]} */
|
||
const wrapped = []
|
||
for (const ln of rawLines) {
|
||
for (const w of bareTopWrapLine(ln, wrapW)) wrapped.push(w)
|
||
}
|
||
const mStart = startRow
|
||
const vis = Math.max(1, mainEnd - mStart)
|
||
const maxScroll = Math.max(0, wrapped.length - vis)
|
||
if (scrollRows[tabIdx] > maxScroll) scrollRows[tabIdx] = maxScroll
|
||
const s = scrollRows[tabIdx]
|
||
let r = mStart
|
||
const framePad = useFrames && !monoUi ? 1 : 0
|
||
const innerW = Math.max(8, cols - 1 - framePad * 2)
|
||
for (let i = s; i < wrapped.length && r < mainEnd; i++) {
|
||
let zeb = ''
|
||
if (use256 && pal.zebraBg && i % 2 === 1) zeb = pal.zebraBg
|
||
else if (i % 2 === 1) zeb = useColor ? pal.dim : ''
|
||
const rst = useColor ? pal.reset : ''
|
||
const left =
|
||
useFrames && !monoUi
|
||
? (useColor ? pal.border : '') + '\u2502' + rst + ' '
|
||
: ''
|
||
const text = wrapped[i].slice(0, innerW)
|
||
out +=
|
||
bareEditCup(r, 1) +
|
||
'\x1b[K' +
|
||
zeb +
|
||
left +
|
||
text +
|
||
rst +
|
||
'\r\n'
|
||
r++
|
||
}
|
||
if (wrapped.length > vis) {
|
||
scrollHint =
|
||
' | ' +
|
||
(s + 1) +
|
||
'\u2013' +
|
||
Math.min(wrapped.length, s + vis) +
|
||
'/' +
|
||
wrapped.length
|
||
}
|
||
}
|
||
|
||
const split =
|
||
layout === 'even' &&
|
||
!fullscreenPanel &&
|
||
cols >= 100 &&
|
||
(tab === 0 || tab === TAB_PROC)
|
||
|
||
if (tab === 0 && snap) {
|
||
const m = snap.metricsLive
|
||
const sess =
|
||
m && typeof m.session === 'object' && m.session
|
||
? /** @type {Record<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, braille)
|
||
)
|
||
{
|
||
const pipSpark = bareTopSparkline(
|
||
ringPipeDelta,
|
||
sw,
|
||
sparkAscii,
|
||
logSpark,
|
||
braille
|
||
)
|
||
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,
|
||
braille
|
||
)
|
||
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, braille) +
|
||
' ' +
|
||
(ringPeers.length ? String(ringPeers[ringPeers.length - 1]) : '')
|
||
)
|
||
if (ringReplSkip.length) {
|
||
emitHdrLn(
|
||
(useColor ? pal.dim : '') +
|
||
' repl skip ' +
|
||
bareTopSparkline(
|
||
ringReplSkip,
|
||
Math.min(24, sw),
|
||
sparkAscii,
|
||
logSpark,
|
||
braille
|
||
) +
|
||
' ' +
|
||
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,
|
||
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,
|
||
healthDetail: healthDetailOn,
|
||
healthBreakdown: String(snap.healthBreakdown || ''),
|
||
splitLeftCol: 0,
|
||
splitMiniProc: false,
|
||
layoutVersion: bareTopStrings.layoutVersion
|
||
})
|
||
activeOverviewSections = ov.activeSections
|
||
drawScrollableLines(ov.lines, 0, hdrRow)
|
||
} else if (tab === TAB_PROC && snap) {
|
||
const rst = useColor ? pal.reset : ''
|
||
emitHdrLn('')
|
||
if (split && ringPeers.length) {
|
||
emitHdrLn(
|
||
(useColor ? pal.dim : '') +
|
||
' activity peers ' +
|
||
bareTopSparkline(
|
||
ringPeers,
|
||
Math.min(32, sparkW),
|
||
sparkAscii,
|
||
logSpark,
|
||
braille
|
||
) +
|
||
' ' +
|
||
String(ringPeers[ringPeers.length - 1]) +
|
||
rst
|
||
)
|
||
}
|
||
const pt =
|
||
snap.extra &&
|
||
snap.extra.processTable &&
|
||
typeof snap.extra.processTable === 'object'
|
||
? snap.extra.processTable
|
||
: null
|
||
let pTitle =
|
||
(useColor ? pal.kw : '') +
|
||
(icons ? '\u2022 ' : '') +
|
||
'Processes (logical)' +
|
||
rst +
|
||
' sort:' +
|
||
procSortKey +
|
||
(procSortAsc ? '+' : '-') +
|
||
(procTreeMode ? ' tree' : '') +
|
||
(filterProcess ? ' filter:' + filterProcess : '')
|
||
if (signalPrompt) {
|
||
pTitle +=
|
||
' ' +
|
||
(useColor ? pal.warn : '') +
|
||
'[sig ' +
|
||
signalPrompt.pid +
|
||
' ' +
|
||
signalPrompt.sig +
|
||
' ' +
|
||
(signalPrompt.phase === 'pick' ? 'Enter arm n cycle]' : 'y/n]') +
|
||
rst
|
||
}
|
||
emitHdrLn(pTitle)
|
||
/** @type {Record<string, unknown>[]} */
|
||
const rawRows = bareTopProcessRowsFromTable(pt)
|
||
const pf = filterProcess.toLowerCase()
|
||
const filtered = pf.length
|
||
? rawRows.filter((r) =>
|
||
String(r.name || r.label || '')
|
||
.toLowerCase()
|
||
.includes(pf)
|
||
)
|
||
: rawRows.slice()
|
||
const displayList = procTreeMode
|
||
? bareTopProcessTreeOrder(filtered)
|
||
: bareTopSortProcessRows(filtered, procSortKey, procSortAsc)
|
||
/** @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 || { note: 'pid not in list (check filter)' }
|
||
const js = bareTopTruncateJsonPretty(
|
||
raw,
|
||
Math.min(16000, Math.max(400, cols * Math.max(8, rows - hdrRow - 2) * 2))
|
||
)
|
||
const jl = js.split('\n')
|
||
emitHdrLn((useColor ? pal.dim : '') + ' (Enter closes detail)' + rst)
|
||
drawScrollableLines(jl, TAB_PROC, hdrRow)
|
||
} else {
|
||
const wPid = 5
|
||
const wPpid = 5
|
||
const wSt = 9
|
||
const wPg = 5
|
||
const wSid = 3
|
||
const wTime = 7
|
||
const wName = Math.max(
|
||
6,
|
||
cols -
|
||
(wPid + wPpid + wSt + wPg + wSid + wTime + 7)
|
||
)
|
||
const nowP = Date.now()
|
||
const hdrLn =
|
||
' ' +
|
||
bareTopFormatFixedColumns(
|
||
['PID', 'PPID', 'PGID', 'ST', 'SID', 'TIME', 'NAME'],
|
||
[wPid, wPpid, wPg, wSt, wSid, wTime, wName],
|
||
['r', 'r', 'r', 'l', 'r', 'r', 'l'],
|
||
cols - 2
|
||
)
|
||
emitHdrLn((useColor ? pal.header : '') + hdrLn + rst)
|
||
const rowStart = hdrRow
|
||
let r = rowStart
|
||
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 ? ' '.repeat(Math.min(4, depth)) : ''
|
||
const tStr = bareTopFormatProcAge(
|
||
bareTopProcessStartedMs(row),
|
||
nowP
|
||
)
|
||
const nm = bareTopTruncateCell(
|
||
ind + String(row.name ?? ''),
|
||
wName,
|
||
true
|
||
)
|
||
const cells = [
|
||
String(pid),
|
||
String(row.ppid ?? ''),
|
||
String(row.pgid ?? ''),
|
||
String(row.state ?? '').slice(0, wSt),
|
||
String(row.sid ?? ''),
|
||
tStr || '—',
|
||
nm
|
||
]
|
||
const line =
|
||
' ' +
|
||
bareTopFormatFixedColumns(
|
||
cells,
|
||
[wPid, wPpid, wPg, wSt, wSid, wTime, wName],
|
||
['r', 'r', 'r', 'l', 'r', 'r', 'l'],
|
||
cols - 2
|
||
)
|
||
const sel = scrollTop + j === procCursor
|
||
const framePad = useFrames && !monoUi ? 1 : 0
|
||
const left =
|
||
useFrames && !monoUi
|
||
? (useColor ? pal.border : '') + '\u2502' + rst + ' '
|
||
: ''
|
||
const prefix = sel && useColor ? pal.sel : ''
|
||
const suf = sel && useColor ? rst : ''
|
||
const stl = String(row.state || '').toLowerCase()
|
||
let rowTone = ''
|
||
let rowToneX = ''
|
||
if (useColor && !sel) {
|
||
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
|
||
}
|
||
}
|
||
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 === 2 && snap && snap.initdGraph) {
|
||
emitHdrLn('')
|
||
emitHdrLn(
|
||
(useColor ? pal.kw : '') +
|
||
(icons ? '\u2022 ' : '') +
|
||
'Initd graph' +
|
||
(useColor ? pal.reset : '') +
|
||
(filterInitd ? ' filter:' + filterInitd : '')
|
||
)
|
||
const g = snap.initdGraph
|
||
/** @type {string[]} */
|
||
let initLines = []
|
||
if (typeof g === 'object' && g && Array.isArray(g.nodes)) {
|
||
const nodes = /** @type {string[]} */ (g.nodes)
|
||
const filt = filterInitd.toLowerCase()
|
||
const list = filt
|
||
? nodes.filter((n) => String(n).toLowerCase().includes(filt))
|
||
: nodes
|
||
initLines.push('Units: ' + list.length + (filterInitd ? ' (filtered)' : ''))
|
||
for (const n of list) initLines.push((icons ? '\u2022 ' : '- ') + String(n))
|
||
} else {
|
||
initLines = bareTopInitdGraphLines(g)
|
||
}
|
||
drawScrollableLines(initLines, 2, hdrRow)
|
||
} else if (tab === 3 && snap) {
|
||
emitHdrLn('')
|
||
emitHdrLn(
|
||
(useColor ? pal.kw : '') +
|
||
(icons ? '\u2022 ' : '') +
|
||
'Net summary' +
|
||
(useColor ? pal.reset : '')
|
||
)
|
||
drawScrollableLines(
|
||
bareTopLinesFromSingle('Network', snap.netSummary, 6, cols),
|
||
3,
|
||
hdrRow
|
||
)
|
||
} else if (tab === 4 && snap) {
|
||
emitHdrLn('')
|
||
emitHdrLn(
|
||
(useColor ? pal.kw : '') +
|
||
(icons ? '\u2022 ' : '') +
|
||
'Features / capabilities' +
|
||
(useColor ? pal.reset : '')
|
||
)
|
||
drawScrollableLines(
|
||
bareTopLinesFromSingle('Features', snap.features, 6, cols),
|
||
4,
|
||
hdrRow
|
||
)
|
||
} else if (tab === 5 && snap) {
|
||
emitHdrLn('')
|
||
emitHdrLn(
|
||
(useColor ? pal.kw : '') +
|
||
(icons ? '\u2022 ' : '') +
|
||
'Diagnostics (debug, delegate_red, ipc backpressure)' +
|
||
(useColor ? pal.reset : '')
|
||
)
|
||
const pack = {
|
||
debug: snap.extra.debug,
|
||
delegateRed: snap.extra.delegateRed,
|
||
ipcBackpressure: snap.extra.ipcBackpressure
|
||
}
|
||
drawScrollableLines(bareTopLinesFromPack(pack, 6, cols), 5, hdrRow)
|
||
} else if (tab === 6 && snap) {
|
||
emitHdrLn('')
|
||
emitHdrLn(
|
||
(useColor ? pal.kw : '') +
|
||
(icons ? '\u2022 ' : '') +
|
||
'Operator (replication, swarm, sync, staging)' +
|
||
(useColor ? pal.reset : '')
|
||
)
|
||
const pack = {
|
||
replication: snap.extra.replication,
|
||
replicationBackpressure: snap.extra.replicationBackpressure,
|
||
swarm: snap.extra.swarm,
|
||
syncWindow: snap.extra.syncWindow,
|
||
stagingSlot: snap.extra.stagingSlot,
|
||
snapshotHints: snap.extra.snapshotHints
|
||
}
|
||
drawScrollableLines(bareTopLinesFromPack(pack, 6, cols), 6, hdrRow)
|
||
} else if (tab === 7 && snap) {
|
||
emitHdrLn('')
|
||
emitHdrLn(
|
||
(useColor ? pal.kw : '') +
|
||
(icons ? '\u2022 ' : '') +
|
||
'Pear (ipc, health, trust, peer_health)' +
|
||
(useColor ? pal.reset : '')
|
||
)
|
||
const pack = {
|
||
pearIpc: snap.extra.pearIpc,
|
||
pearIpcHealth: snap.extra.pearIpcHealth,
|
||
pearTrust: snap.extra.pearTrust,
|
||
peerHealth: snap.extra.peerHealth
|
||
}
|
||
drawScrollableLines(bareTopLinesFromPack(pack, 6, cols), 7, hdrRow)
|
||
} else if (tab === 8 && snap) {
|
||
emitHdrLn('')
|
||
emitHdrLn(
|
||
(useColor ? pal.kw : '') +
|
||
(icons ? '\u2022 ' : '') +
|
||
'Catalog (index, version, bootstrap, provenance, quotas, rlimits, extensions)' +
|
||
(useColor ? pal.reset : '')
|
||
)
|
||
const pack = {
|
||
index: snap.extra.index,
|
||
version:
|
||
snap.fileTexts && snap.fileTexts.version
|
||
? bareTopJsonParse(snap.fileTexts.version)
|
||
: null,
|
||
bootstrap: snap.extra.bootstrap,
|
||
provenance: snap.extra.provenance,
|
||
quotas: snap.extra.quotas,
|
||
rlimits: snap.extra.rlimits,
|
||
extensions: snap.extra.extensions,
|
||
capabilitiesJson: snap.extra.capabilitiesJson,
|
||
capabilitiesNode: snap.extra.capabilitiesNode,
|
||
seedHandshake: snap.extra.seedHandshake
|
||
}
|
||
drawScrollableLines(bareTopLinesFromPack(pack, 6, cols), 8, hdrRow)
|
||
} else if (tab === 9 && snap) {
|
||
emitHdrLn('')
|
||
emitHdrLn(
|
||
(useColor ? pal.kw : '') +
|
||
(icons ? '\u2022 ' : '') +
|
||
'Host / workers (host_os, worker_budget, sandbox, git stats, HDMS, DHT)' +
|
||
(useColor ? pal.reset : '')
|
||
)
|
||
const pack = {
|
||
hostOs: snap.hostOs,
|
||
workerBudget: snap.extra.workerBudget,
|
||
sandboxProfile: snap.extra.sandboxProfile,
|
||
gitDelegateStats: snap.extra.gitDelegateStats,
|
||
gitLfsPointerStats: snap.extra.gitLfsPointerStats,
|
||
hdmsHealth: snap.extra.hdmsHealth,
|
||
hdmsHints: snap.extra.hdmsHints,
|
||
dhtStatus: snap.extra.dhtStatus,
|
||
udxExtended: snap.extra.udxExtended,
|
||
sessionStats: snap.extra.sessionStats
|
||
}
|
||
drawScrollableLines(bareTopLinesFromPack(pack, 6, cols), 9, hdrRow)
|
||
} else if (tab === 10) {
|
||
out +=
|
||
'\r\n' +
|
||
(useColor ? pal.kw : '') +
|
||
'Key reference' +
|
||
(useColor ? pal.reset : '') +
|
||
'\r\n' +
|
||
' Same as h / F1 — overview scrolls; F6/S/V on processes; see help.\r\n'
|
||
}
|
||
|
||
const footProc =
|
||
'^v sel F6/S sort V tree Enter detail k/F9 / filter gG [ ] tab'
|
||
const footOverview =
|
||
'PgUp/Dn scroll detail / section filter h help [ ] tab e export'
|
||
const footInitd = 'PgUp/Dn scroll / filter units h help'
|
||
const footDefault = 'PgUp/Dn scroll [ ] tabs h help e export f full'
|
||
let footBase = quietFooter
|
||
? tab === 0
|
||
? 'ov'
|
||
: tab === TAB_PROC
|
||
? 'proc'
|
||
: '…'
|
||
: tab === TAB_PROC
|
||
? footProc
|
||
: tab === 0
|
||
? footOverview
|
||
: tab === 2
|
||
? footInitd
|
||
: footDefault
|
||
let foot =
|
||
(useColor ? pal.dim : '') +
|
||
footBase +
|
||
(useColor ? pal.reset : '') +
|
||
scrollHint
|
||
if (filterOpen) {
|
||
foot =
|
||
(useColor ? pal.warn : '') +
|
||
'FILTER> ' +
|
||
filterLine +
|
||
'_' +
|
||
(useColor ? pal.reset : '')
|
||
} else {
|
||
const hints = snap && snap.extra.snapshotHints
|
||
const hintLine = bareTopHintsFootLine(hints)
|
||
if (hintLine) {
|
||
tipRot = (tipRot + 1) % Math.max(1, hintLine.length)
|
||
const rot = hintLine.slice(tipRot) + ' · ' + hintLine.slice(0, tipRot)
|
||
const room = Math.max(0, cols - foot.length - 4)
|
||
if (room > 12) foot += ' | ' + rot.slice(0, room)
|
||
}
|
||
}
|
||
out += bareEditCup(rows, 1) + '\x1b[K' + foot.slice(0, cols)
|
||
out += '\x1b[?25h'
|
||
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
|
||
)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
}
|
||
bareTopWrite(ctx, stdout, out)
|
||
}
|
||
|
||
await tick()
|
||
draw()
|
||
|
||
while (!quit) {
|
||
if (signalPrompt) {
|
||
if (keyq.length === 1 && keyq[0] === 0x1b) {
|
||
keyq.shift()
|
||
signalPrompt = null
|
||
draw()
|
||
continue
|
||
}
|
||
const evs = bareTopDrainKeys(keyq, 16, {
|
||
vi: viKeys,
|
||
inFilter: false,
|
||
mouseEnabled: mouseOn
|
||
})
|
||
if (evs) {
|
||
if (evs.type === 'quit') {
|
||
quit = true
|
||
break
|
||
}
|
||
const SIGS = ['TERM', 'INT', 'KILL']
|
||
if (signalPrompt.phase === 'pick') {
|
||
if (evs.type === 'key' && evs.ch === 'n') {
|
||
const i = SIGS.indexOf(signalPrompt.sig)
|
||
signalPrompt.sig = SIGS[(i + 1) % SIGS.length]
|
||
draw()
|
||
} else if (evs.type === 'enter') {
|
||
signalPrompt.phase = 'confirm'
|
||
draw()
|
||
} else if (evs.type === 'key' && (evs.ch === 'q' || evs.ch === 'Q')) {
|
||
signalPrompt = null
|
||
draw()
|
||
}
|
||
} else if (signalPrompt.phase === 'confirm') {
|
||
if (evs.type === 'key' && (evs.ch === 'y' || evs.ch === 'Y')) {
|
||
try {
|
||
if (typeof ctx.bareOsSendSignal === 'function')
|
||
ctx.bareOsSendSignal(signalPrompt.pid, signalPrompt.sig)
|
||
} catch (e) {
|
||
ctx.console.error(
|
||
'baretop: signal failed ' +
|
||
((e && /** @type {{ message?: string }} */ (e).message) ||
|
||
String(e))
|
||
)
|
||
}
|
||
signalPrompt = null
|
||
draw()
|
||
} else if (
|
||
evs.type === 'key' &&
|
||
(evs.ch === 'n' || evs.ch === 'N')
|
||
) {
|
||
signalPrompt = null
|
||
draw()
|
||
}
|
||
}
|
||
}
|
||
await new Promise((r) => setTimeout(r, 50))
|
||
continue
|
||
}
|
||
|
||
if (helpMode) {
|
||
if (keyq.length === 1 && keyq[0] === 0x1b) {
|
||
keyq.shift()
|
||
helpMode = false
|
||
draw()
|
||
continue
|
||
}
|
||
const evh = bareTopDrainKeys(keyq, 32, {
|
||
vi: viKeys,
|
||
inFilter: false,
|
||
mouseEnabled: false
|
||
})
|
||
if (evh) {
|
||
if (evh.type === 'quit') {
|
||
quit = true
|
||
break
|
||
}
|
||
helpMode = false
|
||
draw()
|
||
}
|
||
await new Promise((r) => setTimeout(r, 50))
|
||
continue
|
||
}
|
||
|
||
if (filterOpen) {
|
||
if (keyq.length === 1 && keyq[0] === 0x1b) {
|
||
keyq.shift()
|
||
filterOpen = false
|
||
draw()
|
||
continue
|
||
}
|
||
const evf = bareTopDrainKeys(keyq, 64, {
|
||
vi: viKeys,
|
||
inFilter: true,
|
||
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 filterInitd = filterLine
|
||
filterOpen = false
|
||
scrollRows[tab] = 0
|
||
procCursor = 0
|
||
draw()
|
||
continue
|
||
}
|
||
if (evf.type === 'filter_char' && evf.ch) {
|
||
if (evf.ch.length === 1 && filterLine.length < 64)
|
||
filterLine += evf.ch
|
||
draw()
|
||
continue
|
||
}
|
||
}
|
||
await new Promise((r) => setTimeout(r, 50))
|
||
continue
|
||
}
|
||
|
||
const ev = bareTopDrainKeys(keyq, 32, {
|
||
vi: viKeys,
|
||
inFilter: false,
|
||
mouseEnabled:
|
||
mouseOn && !helpMode && !filterOpen && !signalPrompt
|
||
})
|
||
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) {
|
||
procCursor = rowIdx
|
||
scrollRows[TAB_PROC] = Math.min(
|
||
Math.max(0, plen - mVis),
|
||
Math.max(0, rowIdx - Math.floor(mVis / 2))
|
||
)
|
||
draw()
|
||
}
|
||
}
|
||
}
|
||
if ((mx.btn === 64 || mx.btn === 65) && !mx.release) {
|
||
const sr = scrollRows[tab] || 0
|
||
const dir = mx.btn === 64 ? -1 : 1
|
||
scrollRows[tab] = Math.max(0, sr + dir * 3)
|
||
draw()
|
||
}
|
||
continue
|
||
}
|
||
if (ev.type === 'sort_menu') {
|
||
if (tab === TAB_PROC) {
|
||
const ORDER = ['pid', 'name', 'state', 'time']
|
||
const idx = ORDER.indexOf(procSortKey)
|
||
const n = ORDER.length
|
||
procSortKey =
|
||
/** @type {'pid' | 'name' | 'state' | 'time'} */ (
|
||
ORDER[(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 === 'quit') {
|
||
quit = true
|
||
break
|
||
}
|
||
if (ev.type === 'refresh') {
|
||
burstUntil = Date.now() + Math.min(5000, intervalMs * 3)
|
||
const beforeDelta = deltaMode ? lastSnap : null
|
||
await tick()
|
||
if (beforeDelta) prevSnap = beforeDelta
|
||
draw()
|
||
continue
|
||
}
|
||
if (ev.type === 'help_open') {
|
||
helpMode = true
|
||
draw()
|
||
continue
|
||
}
|
||
if (ev.type === 'pause_toggle') {
|
||
paused = !paused
|
||
draw()
|
||
continue
|
||
}
|
||
if (ev.type === 'step') {
|
||
if (paused) {
|
||
const beforeDelta = deltaMode ? lastSnap : null
|
||
await tick()
|
||
if (beforeDelta) prevSnap = beforeDelta
|
||
draw()
|
||
}
|
||
continue
|
||
}
|
||
if (ev.type === 'delta_toggle') {
|
||
deltaMode = !deltaMode
|
||
if (deltaMode) prevSnap = lastSnap
|
||
draw()
|
||
continue
|
||
}
|
||
if (ev.type === 'fullscreen_toggle') {
|
||
fullscreenPanel = !fullscreenPanel
|
||
draw()
|
||
continue
|
||
}
|
||
if (ev.type === 'clock_toggle') {
|
||
utcClock = !utcClock
|
||
draw()
|
||
continue
|
||
}
|
||
if (ev.type === 'tab_prev') {
|
||
tab = (tab + NTABS - 1) % NTABS
|
||
draw()
|
||
continue
|
||
}
|
||
if (ev.type === 'tab_next') {
|
||
tab = (tab + 1) % NTABS
|
||
draw()
|
||
continue
|
||
}
|
||
if (ev.type === 'tab') {
|
||
tab =
|
||
ev.n < 0 ? NTABS - 1 : Math.max(0, Math.min(NTABS - 1, ev.n))
|
||
draw()
|
||
continue
|
||
}
|
||
if (ev.type === 'list_top') {
|
||
if (tab === TAB_PROC && !procDetailPid) {
|
||
procCursor = 0
|
||
scrollRows[TAB_PROC] = 0
|
||
} else scrollRows[tab] = 0
|
||
draw()
|
||
continue
|
||
}
|
||
if (ev.type === 'list_end') {
|
||
if (tab === TAB_PROC && !procDetailPid) {
|
||
const pl = 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 ORDER = ['pid', 'name', 'state', 'time']
|
||
const idx = Math.max(0, ORDER.indexOf(procSortKey))
|
||
const n = ORDER.length
|
||
procSortKey =
|
||
/** @type {'pid' | 'name' | 'state' | 'time'} */ (
|
||
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
|
||
procDetailPid = procDetailPid != null ? null : pid > 0 ? pid : null
|
||
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
|
||
filterWhich =
|
||
tab === TAB_PROC ? 'process' : tab === 0 ? 'overview' : 'initd'
|
||
filterLine =
|
||
filterWhich === 'process'
|
||
? filterProcess
|
||
: filterWhich === 'overview'
|
||
? overviewSectionFilter
|
||
: 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()
|
||
}
|
||
|
||
const waitMs =
|
||
Date.now() < burstUntil ? Math.min(250, intervalMs) : intervalMs
|
||
await new Promise((r) => setTimeout(r, paused ? 200 : waitMs))
|
||
if (!paused) {
|
||
const beforeDelta = deltaMode ? lastSnap : null
|
||
await tick()
|
||
if (beforeDelta) prevSnap = beforeDelta
|
||
}
|
||
draw()
|
||
}
|
||
} finally {
|
||
try {
|
||
hookOffSuspend?.()
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
try {
|
||
hookOffResume?.()
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
stdin.removeListener('data', onData)
|
||
try {
|
||
if (typeof stdout.removeListener === 'function') {
|
||
stdout.removeListener('resize', onResize)
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
try {
|
||
if (typeof process !== 'undefined' && typeof process.off === 'function') {
|
||
process.off('SIGWINCH', onResize)
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
try {
|
||
if (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()
|
||
}
|
||
}
|
||
}
|
||
|
||
async function run(ctx, argv) {
|
||
const args = argv.slice(1)
|
||
if (args.includes('-h') || args.includes('--help')) {
|
||
ctx.console.log(
|
||
'usage: ' +
|
||
(argv[0] || 'baretop') +
|
||
'\n' +
|
||
'Full-screen session / kernel dashboard (htop-inspired).\n' +
|
||
'Shows metrics_live, a logical process table (process_table.json), peers, delegates,\n' +
|
||
'pipeline limits, many /proc/bare_os JSON mirrors, and optional ctx fast paths.\n' +
|
||
'Requires a TTY.\n' +
|
||
'\n' +
|
||
'Tabs: 1 overview 2 processes 3 initd 4 network 5 features 6 diagnostics\n' +
|
||
' 7 operator 8 pear 9 catalog [ ] host 0 keys (F1 help, F5 refresh, F9 signal)\n' +
|
||
'\n' +
|
||
'Environment:\n' +
|
||
' BARE_TOP_INTERVAL_MS Refresh period (default 1000; clamped 250–10000)\n' +
|
||
' BARE_TOP_UI_MIN_MS Minimum ms between full redraws (0 = off)\n' +
|
||
' BARE_TOP_FETCH_CONCURRENCY Parallel /proc reads per batch (default 6)\n' +
|
||
' BARE_TOP_SNAPSHOT_LITE Set to 1 for smaller bareOsReadBareTopSnapshot batch\n' +
|
||
' BARE_TOP_NO_ALTSCREEN Any non-empty value skips alternate-screen mode\n' +
|
||
' BARE_TOP_INCREMENTAL Set to 1 to skip full 2J clear (may flicker)\n' +
|
||
' BARE_TOP_NO_2J_AFTER_FIRST Set to 1 to use soft clear after first full paint\n' +
|
||
' BARE_TOP_ASCII_GRAPH Set to 1 for ASCII sparklines\n' +
|
||
' BARE_TOP_ASCII_UI Set to 1 for ASCII box/spark fallbacks\n' +
|
||
' BARE_TOP_MONO Set to 1 to avoid Unicode box/spark glyphs\n' +
|
||
' BARE_TOP_BRAILLE_SPARK Set to 1 for braille sparkline glyphs\n' +
|
||
' BARE_TOP_LOG_SPARK Set to 1 for log-scaled sparklines\n' +
|
||
' BARE_TOP_EMA_ALPHA 0–1 smoothing for delta samples (0 = off)\n' +
|
||
' BARE_TOP_THEME dark | light | none\n' +
|
||
' BARE_TOP_HIGH_CONTRAST Set to 1 for bold/underline emphasis\n' +
|
||
' BARE_TOP_COLOR256 Set to 1 to force 256-color bar hints\n' +
|
||
' BARE_TOP_VI_KEYS Set to 1 for hjkl focus in vi-style mode\n' +
|
||
' BARE_TOP_LAYOUT stacked | even (wide overview split + mini jobs)\n' +
|
||
' BARE_TOP_FRAMES Set to 1 for left rule on scroll regions\n' +
|
||
' BARE_TOP_FULLSCREEN_HIDE_TABS Set to 1 to hide tab strip in fullscreen (f)\n' +
|
||
' BARE_TOP_OVERVIEW_COMPACT Set to 1 to hide low-priority overview sections\n' +
|
||
' BARE_TOP_OVERVIEW_SECTIONS Comma tokens to include/exclude overview sections\n' +
|
||
' BARE_TOP_OVERVIEW_PROTOMUX_SPARK Set to 1 for protomux RX sparkline on overview\n' +
|
||
' BARE_TOP_HEALTH_DETAIL Set to 1 for health score penalty breakdown line\n' +
|
||
' BARE_TOP_TITLE_VERSION Set to 1 to append truncated /proc/bare_os/version\n' +
|
||
' BARE_TOP_MOUSE Set to 1 for SGR mouse (wheel + row click on processes)\n' +
|
||
' BARE_TOP_QUIET_FOOTER Set to 1 for shorter footer hints on small terminals\n' +
|
||
' BARE_TOP_DEBUG_TIMINGS Set to 1 for throttled fetch/draw timing on stderr\n' +
|
||
' BARE_TOP_EXPORT_PATH JSON export target for the e key\n' +
|
||
' BARE_TOP_EXPORT_REDACT Set to 1 to omit fileTexts from export payload\n' +
|
||
' BARE_TOP_ICONS Set to 1 for bullet prefixes on sections\n' +
|
||
' BARE_TOP_PROC_SORT pid | name | state | time (default pid)\n' +
|
||
' BARE_TOP_RING_CAP Sparkline history length (default 72, max 240)\n' +
|
||
' BARE_TOP_SCROLL_REGION Set to 1 only with compatible terminals (see help)\n' +
|
||
' NO_COLOR Disable ANSI colors\n' +
|
||
'\n' +
|
||
'Keys: q quit r F5 refresh+burst sp pause . step d delta e export f fullscreen\n' +
|
||
' t UTC [ ] tabs arrows scroll / move selection g G top/bottom h F1 help\n' +
|
||
' / filter (overview sections, initd, or processes) V tree (processes)\n' +
|
||
' F6 S sort menu (processes) <> cycle sort Enter detail k F9 signal\n' +
|
||
' Esc closes help or filter\n' +
|
||
'\n' +
|
||
'Contributors: /bin bundles cannot use node:crypto or node:module; use ctx.vfs and\n' +
|
||
'JSON.parse only. See holepunch-repos/docs/05-BARE-RUNTIME.md (Bare module stack).'
|
||
)
|
||
return
|
||
}
|
||
const stdin = ctx.replStdin
|
||
const stdout = ctx.replStdout || ctx.stdout
|
||
if (!stdin || !/** @type {{ isTTY?: boolean }} */ (stdin).isTTY) {
|
||
ctx.console.error('baretop: a terminal (TTY) is required')
|
||
ctx.exitCode = 1
|
||
return
|
||
}
|
||
if (!stdout) {
|
||
ctx.console.error('baretop: missing stdout')
|
||
ctx.exitCode = 1
|
||
return
|
||
}
|
||
/** @type {Record<string, unknown>} */ (ctx).bareTopArgv = argv
|
||
await bareOsRunBareTopTui(ctx)
|
||
}
|