@@ -1170,10 +1170,891 @@ function bareTopLiveProcessList(
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} env
|
||||
*/
|
||||
function bareTopTabNames(env) {
|
||||
const e = env && typeof env === 'object' ? env : {}
|
||||
if (e.BARE_TOP_TAB_MERGE === 'netop') {
|
||||
return [
|
||||
'overview',
|
||||
'processes',
|
||||
'initd',
|
||||
'network',
|
||||
'features',
|
||||
'diagnostics',
|
||||
'pear',
|
||||
'catalog',
|
||||
'host',
|
||||
'cpu',
|
||||
'mem',
|
||||
'disk'
|
||||
]
|
||||
}
|
||||
return [
|
||||
'overview',
|
||||
'processes',
|
||||
'initd',
|
||||
'network',
|
||||
'features',
|
||||
'diagnostics',
|
||||
'operator',
|
||||
'pear',
|
||||
'catalog',
|
||||
'host',
|
||||
'cpu',
|
||||
'mem',
|
||||
'disk'
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} arr
|
||||
* @param {number} v
|
||||
* @param {number} cap
|
||||
*/
|
||||
function bareTopSdkPushRing(arr, v, cap) {
|
||||
arr.push(v)
|
||||
const n = cap > 0 ? cap : 72
|
||||
while (arr.length > n) arr.shift()
|
||||
}
|
||||
|
||||
/**
|
||||
* TEA dashboard. Fixed-size view so the terminal cannot scroll.
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {Record<string, string>} envTop
|
||||
*/
|
||||
function bareTopCreateTuiApp(ctx, envTop) {
|
||||
const tui = ctx.tui
|
||||
const env = envTop && typeof envTop === 'object' ? envTop : {}
|
||||
const tabNames = bareTopTabNames(env)
|
||||
const size0 = tui && typeof tui.size === 'function' ? tui.size() : {}
|
||||
const intervalRaw = parseInt(env.BARE_TOP_INTERVAL_MS || '1000', 10)
|
||||
const intervalMs = Number.isFinite(intervalRaw)
|
||||
? Math.min(10000, Math.max(250, intervalRaw))
|
||||
: 1000
|
||||
const sort0 = String(env.BARE_TOP_PROC_SORT || 'cpu').toLowerCase()
|
||||
const PROC_SORT = ['pid', 'name', 'state', 'time', 'nice', 'pri', 'cpu']
|
||||
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 ringCapRaw = parseInt(env.BARE_TOP_RING_CAP || '72', 10)
|
||||
const RING_CAP = Number.isFinite(ringCapRaw)
|
||||
? Math.min(240, Math.max(8, ringCapRaw))
|
||||
: 72
|
||||
const exportPath = String(
|
||||
env.BARE_TOP_EXPORT_PATH || '/tmp/baretop-snapshot.json'
|
||||
).trim()
|
||||
|
||||
return {
|
||||
displayTitle,
|
||||
tabNames,
|
||||
tab: 0,
|
||||
width: size0.width || 80,
|
||||
height: size0.height || 24,
|
||||
intervalMs,
|
||||
paused: false,
|
||||
deltaMode: false,
|
||||
help: false,
|
||||
filterOpen: false,
|
||||
filterLine: '',
|
||||
filters: {
|
||||
process: '',
|
||||
initd: '',
|
||||
network: '',
|
||||
features: '',
|
||||
overview: ''
|
||||
},
|
||||
snap: /** @type {Record<string, unknown> | null} */ (null),
|
||||
prevSnap: /** @type {Record<string, unknown> | null} */ (null),
|
||||
status: 'loading…',
|
||||
procSortKey: PROC_SORT.indexOf(sort0) >= 0 ? sort0 : 'cpu',
|
||||
procSortAsc: false,
|
||||
procDetailPid: /** @type {number | null} */ (null),
|
||||
viewport: tui.viewport.create({
|
||||
width: size0.width || 80,
|
||||
height: Math.max(6, (size0.height || 24) - 6)
|
||||
}),
|
||||
table: tui.table.create({
|
||||
columns: [
|
||||
{ key: 'pid', title: 'PID' },
|
||||
{ key: 'state', title: 'ST' },
|
||||
{ key: 'age', title: 'AGE' },
|
||||
{ key: 'name', title: 'NAME' }
|
||||
],
|
||||
rows: [],
|
||||
height: Math.max(6, (size0.height || 24) - 7)
|
||||
}),
|
||||
rings: {
|
||||
exec: [],
|
||||
pipe: [],
|
||||
wall: [],
|
||||
peers: [],
|
||||
net: [],
|
||||
cpu: [],
|
||||
mem: []
|
||||
},
|
||||
prevExec: -1,
|
||||
prevPipe: -1,
|
||||
prevWall: -1,
|
||||
prevNetSum: -1,
|
||||
init: function () {
|
||||
return tui.batch(this._fetch(), this._armTick())
|
||||
},
|
||||
_armTick: function () {
|
||||
if (this.paused) return null
|
||||
const ms = this.intervalMs
|
||||
return tui.tick(ms, function () {
|
||||
return { type: 'baretop.tick' }
|
||||
})
|
||||
},
|
||||
_fetch: function () {
|
||||
const self = this
|
||||
const name = this.tabNames[this.tab] || 'overview'
|
||||
return function () {
|
||||
return Promise.resolve(bareTopFetchSnapshot(ctx, { activeTab: name }))
|
||||
.then(function (snap) {
|
||||
return { type: 'baretop.snap', snap: snap }
|
||||
})
|
||||
.catch(function (error) {
|
||||
return { type: 'error', error: error }
|
||||
})
|
||||
}
|
||||
},
|
||||
_pushRings: function (snap) {
|
||||
const m = snap && snap.metricsLive
|
||||
const sess =
|
||||
m && m.session && typeof m.session === 'object' ? 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 (this.prevExec >= 0) dExec = Math.max(0, ec - this.prevExec)
|
||||
if (this.prevPipe >= 0) dPipe = Math.max(0, pb - this.prevPipe)
|
||||
if (this.prevWall >= 0) dWall = Math.max(0, wm - this.prevWall)
|
||||
this.prevExec = ec
|
||||
this.prevPipe = pb
|
||||
this.prevWall = wm
|
||||
bareTopSdkPushRing(this.rings.exec, dExec, RING_CAP)
|
||||
bareTopSdkPushRing(this.rings.pipe, dPipe, RING_CAP)
|
||||
bareTopSdkPushRing(this.rings.wall, dWall, RING_CAP)
|
||||
bareTopSdkPushRing(this.rings.peers, peers, RING_CAP)
|
||||
let netSum = 0
|
||||
const ns = snap && snap.netSummary
|
||||
if (ns && typeof ns === 'object' && Array.isArray(ns.interfaces)) {
|
||||
for (const iface of ns.interfaces) {
|
||||
if (!iface || typeof iface !== 'object') continue
|
||||
netSum += (Number(iface.rxBytes) || 0) + (Number(iface.txBytes) || 0)
|
||||
}
|
||||
}
|
||||
let dNet = 0
|
||||
if (this.prevNetSum >= 0) dNet = Math.max(0, netSum - this.prevNetSum)
|
||||
this.prevNetSum = netSum
|
||||
bareTopSdkPushRing(this.rings.net, dNet, RING_CAP)
|
||||
const hs = snap && snap.hostStats
|
||||
const cpuPct = Number(
|
||||
hs && (hs.cpuPct ?? hs.cpuPercent ?? hs.cpuUsagePct ?? hs.loadPct)
|
||||
)
|
||||
const memPct = Number(hs && (hs.memPct ?? hs.memPercent ?? hs.memoryPct))
|
||||
bareTopSdkPushRing(
|
||||
this.rings.cpu,
|
||||
Number.isFinite(cpuPct) ? Math.max(0, cpuPct) : 0,
|
||||
RING_CAP
|
||||
)
|
||||
bareTopSdkPushRing(
|
||||
this.rings.mem,
|
||||
Number.isFinite(memPct) ? Math.max(0, memPct) : 0,
|
||||
RING_CAP
|
||||
)
|
||||
},
|
||||
_layout: function () {
|
||||
const bodyH = Math.max(4, (this.height || 24) - 6)
|
||||
this.viewport.width = Math.max(20, this.width || 80)
|
||||
this.viewport.height = bodyH
|
||||
this.table.height = Math.max(3, bodyH - 1)
|
||||
},
|
||||
_tabName: function () {
|
||||
return this.tabNames[this.tab] || 'overview'
|
||||
},
|
||||
_setTab: function (i) {
|
||||
const n = this.tabNames.length
|
||||
if (n < 1) return
|
||||
this.tab = ((i % n) + n) % n
|
||||
this.procDetailPid = null
|
||||
this.viewport.gotoTop()
|
||||
this._syncBody()
|
||||
},
|
||||
_gotoTabName: function (name) {
|
||||
const i = this.tabNames.indexOf(name)
|
||||
if (i >= 0) this._setTab(i)
|
||||
},
|
||||
_activeFilter: function () {
|
||||
const n = this._tabName()
|
||||
if (n === 'processes') return this.filters.process
|
||||
if (n === 'initd') return this.filters.initd
|
||||
if (n === 'network') return this.filters.network
|
||||
if (n === 'features') return this.filters.features
|
||||
if (n === 'overview') return this.filters.overview
|
||||
return ''
|
||||
},
|
||||
_setActiveFilter: function (v) {
|
||||
const n = this._tabName()
|
||||
if (n === 'processes') this.filters.process = v
|
||||
else if (n === 'initd') this.filters.initd = v
|
||||
else if (n === 'network') this.filters.network = v
|
||||
else if (n === 'features') this.filters.features = v
|
||||
else if (n === 'overview') this.filters.overview = v
|
||||
},
|
||||
_procRows: function () {
|
||||
return bareTopLiveProcessList(
|
||||
this.snap,
|
||||
this.filters.process,
|
||||
this.procSortKey,
|
||||
this.procSortAsc,
|
||||
env,
|
||||
{}
|
||||
)
|
||||
},
|
||||
_syncBody: function () {
|
||||
this._layout()
|
||||
const name = this._tabName()
|
||||
if (name === 'processes' && this.procDetailPid == null) {
|
||||
const now = Date.now()
|
||||
const rows = this._procRows()
|
||||
this.table.rows = rows.map(function (r) {
|
||||
return {
|
||||
pid: String(bareTopProcessPid(r)),
|
||||
state: String(r.state || ''),
|
||||
age: bareTopFormatProcAge(bareTopProcessStartedMs(r), now),
|
||||
name: String(r.name || r.label || ''),
|
||||
_row: r
|
||||
}
|
||||
})
|
||||
if (this.table.selected >= this.table.rows.length) {
|
||||
this.table.selected = Math.max(0, this.table.rows.length - 1)
|
||||
}
|
||||
} else {
|
||||
this.viewport.setContent(this._tabLines().join('\n'))
|
||||
}
|
||||
},
|
||||
_tabLines: function () {
|
||||
const snap = this.snap
|
||||
const cols = Math.max(40, this.width || 80)
|
||||
const name = this._tabName()
|
||||
if (!snap) return ['loading…']
|
||||
const extra =
|
||||
snap.extra && typeof snap.extra === 'object' ? snap.extra : {}
|
||||
if (name === 'overview') {
|
||||
const sw = Math.min(36, Math.max(8, cols - 18))
|
||||
const head = [
|
||||
'Activity',
|
||||
' exec ' +
|
||||
bareTopSparkline(this.rings.exec, sw, false, false, false),
|
||||
' pipe ' +
|
||||
bareTopSparkline(this.rings.pipe, sw, false, false, false) +
|
||||
' ' +
|
||||
bareTopFormatBytes(
|
||||
Number(
|
||||
snap.metricsLive &&
|
||||
snap.metricsLive.session &&
|
||||
snap.metricsLive.session.pipelineBytesTotal
|
||||
) || 0
|
||||
),
|
||||
' peers ' +
|
||||
bareTopSparkline(this.rings.peers, sw, false, false, false) +
|
||||
' ' +
|
||||
(this.rings.peers.length
|
||||
? String(this.rings.peers[this.rings.peers.length - 1])
|
||||
: ''),
|
||||
' cpu ' +
|
||||
bareTopSparkline(this.rings.cpu, sw, false, false, false),
|
||||
' mem ' + bareTopSparkline(this.rings.mem, sw, false, false, false)
|
||||
]
|
||||
const ov = bareTopOverviewLines(snap, {
|
||||
cols: cols - 2,
|
||||
na: bareTopStrings.na,
|
||||
compact: env.BARE_TOP_DENSITY === 'compact',
|
||||
sectionsRaw: env.BARE_TOP_OVERVIEW_SECTIONS || '',
|
||||
sectionFilter: this.filters.overview,
|
||||
deltaMode: this.deltaMode,
|
||||
prevMetrics:
|
||||
this.prevSnap && this.prevSnap.metricsLive
|
||||
? this.prevSnap.metricsLive
|
||||
: null,
|
||||
prevSnap: this.deltaMode ? this.prevSnap : null,
|
||||
nowMs: Date.now(),
|
||||
asciiSep: false,
|
||||
flattenCap: function (v, maxL, maxK) {
|
||||
return bareTopFlattenLimited('', v, 1, 4, maxK, maxL)
|
||||
},
|
||||
sparkW: Math.min(32, sw),
|
||||
sparkAscii: false,
|
||||
healthDetail: env.BARE_TOP_HEALTH_DETAIL === '1',
|
||||
healthBreakdown: String(snap.healthBreakdown || ''),
|
||||
layoutVersion: bareTopStrings.layoutVersion,
|
||||
sessionWallRing: this.rings.wall
|
||||
})
|
||||
return head.concat(ov.lines || ov)
|
||||
}
|
||||
if (name === 'processes') {
|
||||
if (this.procDetailPid != null) {
|
||||
const rows = this._procRows()
|
||||
let found = null
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
if (bareTopProcessPid(rows[i]) === this.procDetailPid) {
|
||||
found = rows[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
const body = found
|
||||
? bareTopLinesFromSingle(
|
||||
'pid ' + this.procDetailPid,
|
||||
found,
|
||||
6,
|
||||
cols
|
||||
)
|
||||
: ['(process ' + this.procDetailPid + ' gone)']
|
||||
return ['Process detail Esc back'].concat(body)
|
||||
}
|
||||
return ['(table)']
|
||||
}
|
||||
if (name === 'initd') {
|
||||
const g = snap.initdGraph
|
||||
/** @type {string[]} */
|
||||
let initLines = ['Initd graph']
|
||||
const filt = String(this.filters.initd || '').toLowerCase()
|
||||
if (g && typeof g === 'object' && Array.isArray(g.nodes)) {
|
||||
const list = filt
|
||||
? g.nodes.filter(function (n) {
|
||||
return String(n).toLowerCase().includes(filt)
|
||||
})
|
||||
: g.nodes
|
||||
initLines.push('Units: ' + list.length)
|
||||
for (const n of list) initLines.push('- ' + String(n))
|
||||
if (
|
||||
Array.isArray(g.edges) &&
|
||||
g.edges.length &&
|
||||
g.edges.length <= 32
|
||||
) {
|
||||
initLines.push('Edges:')
|
||||
for (const e of g.edges) initLines.push(' ' + String(e))
|
||||
}
|
||||
} else {
|
||||
initLines = initLines.concat(bareTopInitdGraphLines(g))
|
||||
}
|
||||
return initLines
|
||||
}
|
||||
if (name === 'network') {
|
||||
const deep = bareTopNetworkDeepDiveLines(extra, cols)
|
||||
.concat(bareTopDhtScanPostureLines(extra))
|
||||
.concat(bareTopMeshdropLines(extra))
|
||||
.concat(bareTopPeerDetailsLines(extra, cols))
|
||||
const body = bareTopNetTabLines(snap.netSummary, cols, {
|
||||
maxLines: 400,
|
||||
filter: this.filters.network,
|
||||
wideTwoCol: cols >= 100
|
||||
})
|
||||
const spark =
|
||||
' iface dlt ' +
|
||||
bareTopSparkline(
|
||||
this.rings.net,
|
||||
Math.min(32, cols - 16),
|
||||
false,
|
||||
false,
|
||||
false
|
||||
)
|
||||
return ['Net summary', spark].concat(deep, body)
|
||||
}
|
||||
if (name === 'features') {
|
||||
let fl = bareTopFeaturesTableLines(snap.features, cols)
|
||||
const ff = String(this.filters.features || '').toLowerCase()
|
||||
if (ff)
|
||||
fl = fl.filter(function (ln) {
|
||||
return ln.toLowerCase().includes(ff)
|
||||
})
|
||||
return ['Features / capabilities'].concat(fl)
|
||||
}
|
||||
if (name === 'diagnostics') {
|
||||
const pack = {
|
||||
debug: extra.debug,
|
||||
delegateRed: extra.delegateRed,
|
||||
ipcBackpressure: extra.ipcBackpressure
|
||||
}
|
||||
let dg = bareTopLinesFromPack(pack, 6, cols)
|
||||
const prom = bareTopPromHeadlineLines(
|
||||
snap.fileTexts ? snap.fileTexts.metricsProm : ''
|
||||
)
|
||||
if (prom.length) dg = dg.concat(['', 'Prom counters:'], prom)
|
||||
return ['Diagnostics'].concat(dg)
|
||||
}
|
||||
if (name === 'operator') {
|
||||
const pack = {
|
||||
replication: extra.replication,
|
||||
replicationBackpressure: extra.replicationBackpressure,
|
||||
swarm: extra.swarm,
|
||||
syncWindow: extra.syncWindow,
|
||||
stagingSlot: extra.stagingSlot
|
||||
}
|
||||
return ['Operator']
|
||||
.concat(bareTopLinesFromPack(pack, 6, cols))
|
||||
.concat(bareTopNetworkDeepDiveLines(extra, cols))
|
||||
}
|
||||
if (name === 'pear') {
|
||||
const pack = {
|
||||
pearIpc: extra.pearIpc,
|
||||
pearIpcHealth: extra.pearIpcHealth,
|
||||
pearTrust: extra.pearTrust,
|
||||
peerHealth: extra.peerHealth
|
||||
}
|
||||
return ['Pear'].concat(bareTopLinesFromPack(pack, 8, cols))
|
||||
}
|
||||
if (name === 'catalog') {
|
||||
const pack = {
|
||||
index: extra.index,
|
||||
bootstrap: extra.bootstrap,
|
||||
provenance: extra.provenance,
|
||||
quotas: extra.quotas,
|
||||
rlimits: extra.rlimits,
|
||||
extensions: extra.extensions,
|
||||
clock: extra.clock,
|
||||
openssh: extra.openssh,
|
||||
kernelProgram: extra.kernelProgram
|
||||
}
|
||||
const ver =
|
||||
snap.fileTexts && snap.fileTexts.version
|
||||
? String(snap.fileTexts.version).trim().split('\n')[0]
|
||||
: ''
|
||||
return ['Catalog', ver ? ' version ' + ver : ''].concat(
|
||||
bareTopLinesFromPack(pack, 6, cols)
|
||||
)
|
||||
}
|
||||
if (name === 'host') {
|
||||
const pack = {
|
||||
hostOs: extra.hostOs || snap.hostOs,
|
||||
workerBudget: extra.workerBudget,
|
||||
sandboxProfile: extra.sandboxProfile,
|
||||
hdmsHealth: extra.hdmsHealth,
|
||||
dhtStatus: extra.dhtStatus
|
||||
}
|
||||
return ['Host / workers'].concat(bareTopLinesFromPack(pack, 6, cols))
|
||||
}
|
||||
if (name === 'cpu') {
|
||||
const lines = [
|
||||
'CPU',
|
||||
' logical cpus ' +
|
||||
String(snap.cpuCoreCount || 0) +
|
||||
' ' +
|
||||
String(snap.cpuLine || '')
|
||||
]
|
||||
const hs = snap.hostStats
|
||||
if (hs && typeof hs === 'object') {
|
||||
const per = hs.hostPerCpu || hs.perCpu || hs.cpus
|
||||
if (Array.isArray(per)) {
|
||||
const w = Math.min(40, Math.max(8, cols - 10))
|
||||
for (let i = 0; i < per.length && i < 32; i++) {
|
||||
const c = per[i]
|
||||
const v =
|
||||
typeof c === 'number'
|
||||
? c
|
||||
: c && typeof c === 'object'
|
||||
? Number(c.busy ?? c.pct) || 0
|
||||
: 0
|
||||
lines.push(
|
||||
' ' +
|
||||
String(i).padStart(2, '0') +
|
||||
' ' +
|
||||
bareTopSparkline([v], w, true, false, false)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (lines.length < 3)
|
||||
lines.push(' (no per-CPU samples — overview still tracks cpu %)')
|
||||
return lines
|
||||
}
|
||||
if (name === 'mem') {
|
||||
const pt = extra.processTable
|
||||
const byRss = bareTopProcessRowsFromTable(pt)
|
||||
.slice()
|
||||
.sort(function (a, b) {
|
||||
return (Number(b.memRssBytes) || 0) - (Number(a.memRssBytes) || 0)
|
||||
})
|
||||
const ml = ['Memory', ' ' + String(snap.meminfoLine || '')]
|
||||
if (snap.swapinfoLine) ml.push(' ' + String(snap.swapinfoLine))
|
||||
for (const r of byRss.slice(0, 16)) {
|
||||
const rb = Number(r.memRssBytes) || 0
|
||||
if (!rb) continue
|
||||
ml.push(
|
||||
' ' +
|
||||
bareTopProcessPid(r) +
|
||||
' ' +
|
||||
bareTopFormatBytes(rb) +
|
||||
' ' +
|
||||
String(r.name || '')
|
||||
)
|
||||
}
|
||||
if (ml.length < 4) ml.push(' (no memRssBytes on process rows)')
|
||||
return ml
|
||||
}
|
||||
if (name === 'disk') {
|
||||
const dl = [
|
||||
'Disk',
|
||||
' ' + String(snap.diskstatsLine || '(no diskstats)')
|
||||
]
|
||||
if (typeof snap.diskstatsRaw === 'string' && snap.diskstatsRaw) {
|
||||
const raw = snap.diskstatsRaw.split('\n').slice(0, 16)
|
||||
for (const ln of raw) dl.push(' ' + ln)
|
||||
}
|
||||
return dl
|
||||
}
|
||||
return ['(empty tab)']
|
||||
},
|
||||
update: function (msg) {
|
||||
if (msg && msg.type === 'resize') {
|
||||
this.width = msg.width || this.width
|
||||
this.height = msg.height || this.height
|
||||
this._syncBody()
|
||||
return [this, null]
|
||||
}
|
||||
if (msg && msg.type === 'baretop.snap') {
|
||||
this.prevSnap = this.snap
|
||||
this.snap = msg.snap || null
|
||||
this._pushRings(this.snap)
|
||||
this.status = this.paused
|
||||
? 'PAUSED'
|
||||
: this.snap && this.snap.readErr
|
||||
? 'read error'
|
||||
: 'ok'
|
||||
this._syncBody()
|
||||
return [this, null]
|
||||
}
|
||||
if (msg && msg.type === 'baretop.export') {
|
||||
this.status = msg.ok ? 'exported snapshot' : 'export failed'
|
||||
return [this, null]
|
||||
}
|
||||
if (msg && msg.type === 'error') {
|
||||
this.status =
|
||||
'error: ' +
|
||||
(msg.error && msg.error.message
|
||||
? msg.error.message
|
||||
: String(msg.error))
|
||||
return [this, null]
|
||||
}
|
||||
if (msg && msg.type === 'baretop.tick') {
|
||||
if (this.paused) return [this, this._armTick()]
|
||||
return [this, tui.batch(this._fetch(), this._armTick())]
|
||||
}
|
||||
if (this.help) {
|
||||
if (msg && msg.type === 'key') this.help = false
|
||||
return [this, null]
|
||||
}
|
||||
if (this.filterOpen) {
|
||||
if (tui.key.matches(msg, 'escape', 'ctrl+c')) {
|
||||
this.filterOpen = false
|
||||
this.filterLine = ''
|
||||
return [this, null]
|
||||
}
|
||||
if (tui.key.matches(msg, 'enter')) {
|
||||
this._setActiveFilter(this.filterLine)
|
||||
this.filterOpen = false
|
||||
this.filterLine = ''
|
||||
this.viewport.gotoTop()
|
||||
this._syncBody()
|
||||
return [this, null]
|
||||
}
|
||||
if (tui.key.matches(msg, 'backspace')) {
|
||||
this.filterLine = this.filterLine.slice(0, -1)
|
||||
return [this, null]
|
||||
}
|
||||
if (
|
||||
msg &&
|
||||
msg.type === 'key' &&
|
||||
!msg.ctrl &&
|
||||
!msg.meta &&
|
||||
typeof msg.sequence === 'string' &&
|
||||
msg.sequence.length === 1 &&
|
||||
msg.sequence >= ' '
|
||||
) {
|
||||
this.filterLine += msg.sequence
|
||||
}
|
||||
return [this, null]
|
||||
}
|
||||
if (tui.key.matches(msg, 'q', 'ctrl+c', 'f10')) return [this, tui.quit]
|
||||
if (tui.key.matches(msg, 'r', 'f5')) return [this, this._fetch()]
|
||||
if (tui.key.matches(msg, 'space')) {
|
||||
this.paused = !this.paused
|
||||
this.status = this.paused ? 'PAUSED' : 'ok'
|
||||
return [this, this.paused ? null : this._armTick()]
|
||||
}
|
||||
if (tui.key.matches(msg, 'd')) {
|
||||
this.deltaMode = !this.deltaMode
|
||||
this._syncBody()
|
||||
return [this, null]
|
||||
}
|
||||
if (tui.key.matches(msg, '?', 'h', 'f1')) {
|
||||
this.help = true
|
||||
return [this, null]
|
||||
}
|
||||
if (tui.key.matches(msg, '/')) {
|
||||
this.filterOpen = true
|
||||
this.filterLine = this._activeFilter()
|
||||
return [this, null]
|
||||
}
|
||||
if (tui.key.matches(msg, 'tab', ']')) {
|
||||
this._setTab(this.tab + 1)
|
||||
return [this, null]
|
||||
}
|
||||
if (tui.key.matches(msg, 'shift+tab', '[')) {
|
||||
this._setTab(this.tab - 1)
|
||||
return [this, null]
|
||||
}
|
||||
if (msg && msg.type === 'key' && msg.name && /^[1-9]$/.test(msg.name)) {
|
||||
this._setTab(parseInt(msg.name, 10) - 1)
|
||||
return [this, null]
|
||||
}
|
||||
if (tui.key.matches(msg, '0')) {
|
||||
this._gotoTabName('disk')
|
||||
return [this, null]
|
||||
}
|
||||
if (tui.key.matches(msg, 'H')) {
|
||||
this._gotoTabName('host')
|
||||
return [this, null]
|
||||
}
|
||||
if (tui.key.matches(msg, 'C')) {
|
||||
this._gotoTabName('cpu')
|
||||
return [this, null]
|
||||
}
|
||||
if (tui.key.matches(msg, 'M')) {
|
||||
this._gotoTabName('mem')
|
||||
return [this, null]
|
||||
}
|
||||
if (tui.key.matches(msg, 's', 'f6')) {
|
||||
const i = PROC_SORT.indexOf(this.procSortKey)
|
||||
this.procSortKey = PROC_SORT[(i + 1) % PROC_SORT.length]
|
||||
this._syncBody()
|
||||
return [this, null]
|
||||
}
|
||||
if (tui.key.matches(msg, 'R')) {
|
||||
this.procSortAsc = !this.procSortAsc
|
||||
this._syncBody()
|
||||
return [this, null]
|
||||
}
|
||||
if (tui.key.matches(msg, 'e')) {
|
||||
return [this, this._export()]
|
||||
}
|
||||
if (this.procDetailPid != null && tui.key.matches(msg, 'escape')) {
|
||||
this.procDetailPid = null
|
||||
this._syncBody()
|
||||
return [this, null]
|
||||
}
|
||||
if (this._tabName() === 'processes' && this.procDetailPid == null) {
|
||||
if (tui.key.matches(msg, 'enter')) {
|
||||
const row = this.table.selectedRow()
|
||||
if (row && row._row) this.procDetailPid = bareTopProcessPid(row._row)
|
||||
this._syncBody()
|
||||
return [this, null]
|
||||
}
|
||||
const pair = this.table.update(msg)
|
||||
this.table = pair[0]
|
||||
return [this, pair[1]]
|
||||
}
|
||||
const vp = this.viewport.update(msg)
|
||||
this.viewport = vp[0]
|
||||
return [this, vp[1]]
|
||||
},
|
||||
_export: function () {
|
||||
const self = this
|
||||
return function () {
|
||||
if (!ctx.vfs || typeof ctx.vfs.writeFile !== 'function') {
|
||||
return Promise.resolve({ type: 'baretop.export', ok: false })
|
||||
}
|
||||
const payload = JSON.stringify(
|
||||
{
|
||||
atMs: Date.now(),
|
||||
tab: self._tabName(),
|
||||
snap: self.snap
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
const buf =
|
||||
ctx.b4a && typeof ctx.b4a.from === 'function'
|
||||
? ctx.b4a.from(payload)
|
||||
: payload
|
||||
return Promise.resolve(ctx.vfs.writeFile(exportPath, buf))
|
||||
.then(function () {
|
||||
return { type: 'baretop.export', ok: true }
|
||||
})
|
||||
.catch(function () {
|
||||
return { type: 'baretop.export', ok: false }
|
||||
})
|
||||
}
|
||||
},
|
||||
view: function () {
|
||||
const cols = Math.max(40, this.width || 80)
|
||||
const rows = Math.max(12, this.height || 24)
|
||||
const st = tui.style
|
||||
const clock = new Date().toTimeString().slice(0, 8)
|
||||
const titleRaw =
|
||||
' ' +
|
||||
this.displayTitle +
|
||||
' — ' +
|
||||
this._tabName() +
|
||||
' ' +
|
||||
clock +
|
||||
(this.paused ? ' PAUSED' : '') +
|
||||
(this.deltaMode ? ' Δ' : '') +
|
||||
' ' +
|
||||
this.status +
|
||||
' '
|
||||
const title = st
|
||||
? st()
|
||||
.foreground('brightwhite')
|
||||
.background('blue')
|
||||
.width(cols)
|
||||
.render(titleRaw)
|
||||
: titleRaw
|
||||
const strip = this.tabNames
|
||||
.map(function (n, i) {
|
||||
return i === this.tab ? '[' + n + ']' : n
|
||||
}, this)
|
||||
.join(' ')
|
||||
const tabBar = st ? st().dim().width(cols).render(strip) : strip
|
||||
let body
|
||||
if (this._tabName() === 'processes' && this.procDetailPid == null) {
|
||||
body = this.table.view()
|
||||
} else {
|
||||
body = this.viewport.view()
|
||||
}
|
||||
const filt = this.filterOpen
|
||||
? '/' + this.filterLine + '█'
|
||||
: this._activeFilter()
|
||||
? 'filter:' + this._activeFilter()
|
||||
: ''
|
||||
const footRaw = this.filterOpen
|
||||
? filt + ' Enter apply Esc cancel'
|
||||
: 'q quit r refresh Tab/[ ] tabs / filter ? help space pause d Δ' +
|
||||
(this._tabName() === 'processes'
|
||||
? ' ↑↓ sel Enter detail s sort'
|
||||
: ' ↑↓ scroll') +
|
||||
(filt ? ' ' + filt : '')
|
||||
const foot = st ? st().dim().width(cols).render(footRaw) : footRaw
|
||||
const rule = st
|
||||
? st()
|
||||
.dim()
|
||||
.width(cols)
|
||||
.render('\u2500'.repeat(Math.min(cols, 120)))
|
||||
: ''
|
||||
const lines = [title, tabBar, rule]
|
||||
.concat(String(body).split('\n'))
|
||||
.concat([rule, foot])
|
||||
while (lines.length < rows) lines.push('')
|
||||
const out = []
|
||||
for (let i = 0; i < rows; i++) {
|
||||
const ln = lines[i] || ''
|
||||
out.push(st ? st.truncate(ln, cols) : ln.slice(0, cols))
|
||||
}
|
||||
return out.join('\n')
|
||||
},
|
||||
overlay: function (size) {
|
||||
if (!this.help) return null
|
||||
const cols = (size && size.width) || this.width || 80
|
||||
const rows = (size && size.height) || this.height || 24
|
||||
const st = tui.style
|
||||
const body =
|
||||
this.displayTitle +
|
||||
' — help\n\n' +
|
||||
'1-9 / 0 / H C M jump tab Tab [ ] next/prev\n' +
|
||||
'q F10 Ctrl+C quit r F5 refresh now\n' +
|
||||
'space pause d delta mode\n' +
|
||||
'/ filter e export JSON\n' +
|
||||
'↑↓ PgUp/PgDn scroll s F6 / R process sort\n' +
|
||||
'Enter process detail\n\n' +
|
||||
'Logical process table — not host OS processes.\n' +
|
||||
'Press any key to close.'
|
||||
const boxed = st
|
||||
? st()
|
||||
.border(st.borders.rounded)
|
||||
.padding(1, 2)
|
||||
.background('black')
|
||||
.render(body)
|
||||
: body
|
||||
const h = st ? st.height(boxed) : boxed.split('\n').length
|
||||
const w = st ? st.width(boxed) : 48
|
||||
return {
|
||||
row: Math.max(0, Math.floor((rows - h) / 2)),
|
||||
col: Math.max(0, Math.floor((cols - w) / 2)),
|
||||
text: boxed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
*/
|
||||
function bareTopTuiRunOpts(ctx) {
|
||||
const env =
|
||||
ctx.env && typeof ctx.env === 'object'
|
||||
? /** @type {Record<string, string>} */ (ctx.env)
|
||||
: {}
|
||||
/** @type {{ buffer: 'cell', altScreen?: boolean }} */
|
||||
const opts = { buffer: 'cell' }
|
||||
if (
|
||||
env.BARE_TOP_NO_ALTSCREEN != null &&
|
||||
String(env.BARE_TOP_NO_ALTSCREEN) !== ''
|
||||
) {
|
||||
opts.altScreen = false
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
*/
|
||||
async function bareOsRunBareTopTui(ctx) {
|
||||
if (ctx.tui && typeof ctx.tui.run === 'function') {
|
||||
const envTop = Object.assign(
|
||||
{},
|
||||
ctx.env && typeof ctx.env === 'object'
|
||||
? /** @type {Record<string, string>} */ (ctx.env)
|
||||
: {}
|
||||
)
|
||||
if (typeof bareTopLoadBareTopRc === 'function') {
|
||||
try {
|
||||
const rc = await bareTopLoadBareTopRc(ctx, envTop)
|
||||
if (rc && rc.flat) {
|
||||
for (const [k, v] of Object.entries(rc.flat)) {
|
||||
if (
|
||||
k.startsWith('BARE_TOP_') ||
|
||||
k === 'NO_COLOR' ||
|
||||
k === 'COLORTERM'
|
||||
) {
|
||||
envTop[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
await ctx.tui.run(bareTopCreateTuiApp(ctx, envTop), bareTopTuiRunOpts(ctx))
|
||||
return
|
||||
}
|
||||
await bareOsRunBareTopTuiLegacy(ctx)
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-SDK key loop (BARE_OS_TUI=0).
|
||||
* @param {Record<string, unknown>} ctx
|
||||
*/
|
||||
async function bareOsRunBareTopTuiLegacy(ctx) {
|
||||
const stdin = /** @type {import('stream').Readable | undefined} */ (
|
||||
ctx.replStdin
|
||||
)
|
||||
@@ -1650,40 +2531,19 @@ async function bareOsRunBareTopTui(ctx) {
|
||||
/** @type {(() => void) | null} */
|
||||
let hookOffResume = null
|
||||
|
||||
const useTuiSession = !!(
|
||||
ctx.tui &&
|
||||
typeof ctx.tui.acquire === 'function' &&
|
||||
typeof ctx.tui.release === 'function' &&
|
||||
ctx.tui.screen &&
|
||||
typeof ctx.tui.screen.enter === 'function'
|
||||
)
|
||||
/** @type {{ altScreen?: boolean, bracketedPaste: boolean, mouse?: string }} */
|
||||
const tuiSessionOpts = {
|
||||
altScreen: !noAlt,
|
||||
bracketedPaste: false,
|
||||
mouse: mouseOn ? 'drag' : undefined
|
||||
}
|
||||
|
||||
try {
|
||||
if (useTuiSession) {
|
||||
ctx.tui.acquire(tuiSessionOpts)
|
||||
ctx.tui.screen.enter(tuiSessionOpts)
|
||||
if (typeof ctx.suspendReplForSubprocess === 'function') {
|
||||
ctx.suspendReplForSubprocess()
|
||||
suspended = true
|
||||
useAltScreen = !noAlt
|
||||
} else {
|
||||
if (typeof ctx.suspendReplForSubprocess === 'function') {
|
||||
ctx.suspendReplForSubprocess()
|
||||
suspended = 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')
|
||||
}
|
||||
}
|
||||
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')
|
||||
}
|
||||
if (typeof ctx.bareOsRegisterSuspendHook === 'function') {
|
||||
hookOffSuspend = ctx.bareOsRegisterSuspendHook(() => {
|
||||
@@ -5056,31 +5916,26 @@ async function bareOsRunBareTopTui(ctx) {
|
||||
/* ignore */
|
||||
}
|
||||
try {
|
||||
if (useTuiSession) {
|
||||
ctx.tui.screen.leave(tuiSessionOpts)
|
||||
ctx.tui.release(tuiSessionOpts)
|
||||
} else {
|
||||
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')
|
||||
try {
|
||||
if (typeof stdin.setRawMode === 'function') stdin.setRawMode(false)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
if (suspended && typeof ctx.resumeReplAfterSubprocess === 'function') {
|
||||
ctx.resumeReplAfterSubprocess()
|
||||
}
|
||||
if (mouseOn) {
|
||||
bareTopWrite(ctx, stdout, '\x1b[?1000l\x1b[?1002l\x1b[?1006l')
|
||||
}
|
||||
if (useAltScreen) {
|
||||
bareTopWrite(ctx, stdout, '\x1b[?1049l')
|
||||
} else {
|
||||
bareTopWrite(ctx, stdout, '\x1b[2J\x1b[H')
|
||||
}
|
||||
bareTopWrite(ctx, stdout, '\x1b[?25h\x1b[0m')
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
try {
|
||||
if (typeof stdin.setRawMode === 'function') stdin.setRawMode(false)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
if (suspended && typeof ctx.resumeReplAfterSubprocess === 'function') {
|
||||
ctx.resumeReplAfterSubprocess()
|
||||
}
|
||||
if (
|
||||
(envTop.BARE_TOP_PERSIST === '1' || envTop.BARE_TOP_PERSIST === 'true') &&
|
||||
bareTopRcPath &&
|
||||
|
||||
Reference in New Issue
Block a user