250 lines
7.5 KiB
JavaScript
250 lines
7.5 KiB
JavaScript
function bareP2pFmtClock(ms) {
|
|
const d = new Date(typeof ms === 'number' ? ms : Date.now())
|
|
const z = (n) => (n < 10 ? '0' : '') + n
|
|
return z(d.getHours()) + ':' + z(d.getMinutes()) + ':' + z(d.getSeconds())
|
|
}
|
|
|
|
function bareP2pTruncate(s, cols) {
|
|
const t = String(s || '')
|
|
if (t.length <= cols) return t
|
|
return t.slice(0, Math.max(0, cols - 1)) + '\u2026'
|
|
}
|
|
|
|
/**
|
|
* TEA model for the read-only p2p dashboards (dhttop, swarmmap, …).
|
|
* @param {Record<string, unknown>} ctx
|
|
* @param {{ title: string, subtitle?: string, renderLines: () => string[] | Promise<string[]>, onRefresh?: () => void | Promise<void> }} opts
|
|
*/
|
|
function bareP2pCreateSimpleTuiApp(ctx, opts) {
|
|
const tui = ctx.tui
|
|
const size0 = tui && typeof tui.size === 'function' ? tui.size() : {}
|
|
return {
|
|
title: String(opts.title || 'p2p'),
|
|
subtitle: opts.subtitle || 'q quit · r refresh',
|
|
lines: ['loading\u2026'],
|
|
error: null,
|
|
width: size0.width || 80,
|
|
height: size0.height || 24,
|
|
init: function () {
|
|
return this._load()
|
|
},
|
|
_load: function () {
|
|
return function () {
|
|
return Promise.resolve()
|
|
.then(function () {
|
|
return opts.renderLines()
|
|
})
|
|
.then(function (lines) {
|
|
return {
|
|
type: 'p2p.lines',
|
|
lines: Array.isArray(lines) ? lines : []
|
|
}
|
|
})
|
|
.catch(function (error) {
|
|
return { type: 'error', error: error }
|
|
})
|
|
}
|
|
},
|
|
_refresh: function () {
|
|
const self = this
|
|
if (typeof opts.onRefresh !== 'function') return this._load()
|
|
return function () {
|
|
return Promise.resolve()
|
|
.then(function () {
|
|
return opts.onRefresh()
|
|
})
|
|
.then(function () {
|
|
return self._load()()
|
|
})
|
|
}
|
|
},
|
|
update: function (msg) {
|
|
if (
|
|
tui &&
|
|
tui.key &&
|
|
tui.key.matches(msg, 'q', 'Q', 'ctrl+c', 'ctrl+q', 'ctrl+x')
|
|
) {
|
|
return [this, tui.quit]
|
|
}
|
|
if (tui && tui.key && tui.key.matches(msg, 'r', 'R')) {
|
|
return [this, this._refresh()]
|
|
}
|
|
if (msg && msg.type === 'p2p.lines') {
|
|
this.lines = Array.isArray(msg.lines) ? msg.lines.map(String) : []
|
|
this.error = null
|
|
return [this, null]
|
|
}
|
|
if (msg && msg.type === 'error') {
|
|
const err = msg.error
|
|
this.error =
|
|
err && err.message ? String(err.message) : String(err || 'error')
|
|
return [this, null]
|
|
}
|
|
if (msg && msg.type === 'resize') {
|
|
this.width = msg.width || this.width
|
|
this.height = msg.height || this.height
|
|
}
|
|
return [this, null]
|
|
},
|
|
view: function () {
|
|
const cols = Math.max(48, this.width || 80)
|
|
const rows = Math.max(12, this.height || 24)
|
|
const st = tui && tui.style
|
|
const titleText = ' ' + this.title + ' '
|
|
const subText = this.subtitle + ' · ' + bareP2pFmtClock(Date.now())
|
|
let title
|
|
let sub
|
|
if (st) {
|
|
title = st()
|
|
.foreground('brightwhite')
|
|
.background('blue')
|
|
.width(cols)
|
|
.render(titleText)
|
|
sub = st().dim().width(cols).render(subText)
|
|
} else {
|
|
title = bareP2pTruncate(titleText, cols)
|
|
sub = bareP2pTruncate(subText, cols)
|
|
}
|
|
const maxBody = Math.max(1, rows - 3)
|
|
const body = []
|
|
if (this.error) {
|
|
const errLine = 'error: ' + this.error
|
|
body.push(
|
|
st ? st.truncate(errLine, cols) : bareP2pTruncate(errLine, cols)
|
|
)
|
|
}
|
|
for (let i = 0; body.length < maxBody; i++) {
|
|
const raw = i < this.lines.length ? String(this.lines[i]) : ''
|
|
body.push(st ? st.truncate(raw, cols) : bareP2pTruncate(raw, cols))
|
|
}
|
|
return title + '\n' + sub + '\n' + body.join('\n')
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read-only TUI shell: q quits, r refreshes.
|
|
* Prefers ctx.tui when attached; otherwise the pre-SDK key loop.
|
|
* @param {Record<string, unknown>} ctx
|
|
* @param {{ title: string, subtitle?: string, renderLines: () => string[] | Promise<string[]>, onRefresh?: () => void | Promise<void> }} opts
|
|
*/
|
|
async function bareP2pRunSimpleTui(ctx, opts) {
|
|
if (ctx.tui && typeof ctx.tui.run === 'function') {
|
|
await ctx.tui.run(bareP2pCreateSimpleTuiApp(ctx, opts))
|
|
return
|
|
}
|
|
await bareP2pRunSimpleTuiLegacy(ctx, opts)
|
|
}
|
|
|
|
/** Pre-SDK key loop (BARE_OS_TUI=0). */
|
|
async function bareP2pRunSimpleTuiLegacy(ctx, opts) {
|
|
const stdin = ctx.replStdin
|
|
const stdout = bareEditResolveStdout(ctx)
|
|
if (!stdin || !stdout) {
|
|
ctx.console.error('p2p tui: missing stdin/stdout')
|
|
ctx.exitCode = 1
|
|
return
|
|
}
|
|
const useColor = bareEditUseColor(ctx)
|
|
const envEarly =
|
|
ctx.env && typeof ctx.env === 'object'
|
|
? /** @type {Record<string, string>} */ (ctx.env)
|
|
: {}
|
|
const cols0 = parseInt(envEarly.COLUMNS || '80', 10) || 80
|
|
const rows0 = parseInt(envEarly.LINES || '24', 10) || 24
|
|
const dims = () => ({
|
|
cols: Math.max(
|
|
48,
|
|
/** @type {{ columns?: number }} */ (stdout).columns || cols0
|
|
),
|
|
rows: Math.max(12, /** @type {{ rows?: number }} */ (stdout).rows || rows0)
|
|
})
|
|
|
|
const reader = bareEditCreateStdinReader(stdin)
|
|
let suspended = false
|
|
let alt = false
|
|
let loop = true
|
|
|
|
const draw = async () => {
|
|
const { cols, rows } = dims()
|
|
const lines = await opts.renderLines()
|
|
let out = '\x1b[?25l\x1b[2J\x1b[H'
|
|
const top =
|
|
bareEditSgr('status', useColor) +
|
|
bareP2pTruncate(' ' + opts.title + ' ', cols) +
|
|
EDIT_ANSI_RESET
|
|
out += bareEditCup(1, 1) + '\x1b[K' + top
|
|
const sub = bareP2pTruncate(
|
|
(opts.subtitle || 'q quit · r refresh') +
|
|
' · ' +
|
|
bareP2pFmtClock(Date.now()),
|
|
cols
|
|
)
|
|
out +=
|
|
bareEditCup(2, 1) +
|
|
'\x1b[K' +
|
|
bareEditSgr('dim', useColor) +
|
|
sub +
|
|
EDIT_ANSI_RESET
|
|
out += bareEditCup(3, 1) + '\x1b[K'
|
|
const maxBody = Math.max(1, rows - 3)
|
|
for (let i = 0; i < maxBody; i++) {
|
|
const raw = i < lines.length ? String(lines[i]) : ''
|
|
out += bareEditCup(4 + i, 1) + '\x1b[K' + bareP2pTruncate(raw, cols)
|
|
}
|
|
bareEditWrite(ctx, stdout, out + '\x1b[?25h')
|
|
}
|
|
|
|
try {
|
|
if (typeof ctx.suspendReplForSubprocess === 'function') {
|
|
ctx.suspendReplForSubprocess()
|
|
suspended = true
|
|
}
|
|
if (typeof stdin.setRawMode === 'function') stdin.setRawMode(true)
|
|
if (typeof stdin.resume === 'function') stdin.resume()
|
|
bareEditWrite(ctx, stdout, '\x1b[?1049h')
|
|
alt = true
|
|
await draw()
|
|
while (loop) {
|
|
const ev = await bareEditReadKey(reader)
|
|
if (ev.type === 'eof') break
|
|
if (ev.type === 'ctrl') {
|
|
const code = typeof ev.code === 'number' ? ev.code : 0
|
|
if (
|
|
ev.code === 'interrupt' ||
|
|
code === 3 ||
|
|
code === 17 ||
|
|
code === 24
|
|
) {
|
|
break
|
|
}
|
|
}
|
|
if (ev.type === 'key' && ev.ch) {
|
|
if (ev.ch === 'q' || ev.ch === 'Q') break
|
|
if (ev.ch === 'r' || ev.ch === 'R') {
|
|
if (typeof opts.onRefresh === 'function') await opts.onRefresh()
|
|
await draw()
|
|
continue
|
|
}
|
|
}
|
|
await draw()
|
|
}
|
|
} finally {
|
|
try {
|
|
if (alt) bareEditWrite(ctx, stdout, '\x1b[?1049l')
|
|
bareEditWrite(ctx, stdout, '\x1b[?25h\x1b[0m')
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
reader.dispose()
|
|
try {
|
|
if (typeof stdin.setRawMode === 'function') stdin.setRawMode(false)
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
if (suspended && typeof ctx.resumeReplAfterSubprocess === 'function') {
|
|
ctx.resumeReplAfterSubprocess()
|
|
}
|
|
}
|
|
}
|