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} ctx * @param {{ title: string, subtitle?: string, renderLines: () => string[] | Promise, onRefresh?: () => void | Promise }} 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} ctx * @param {{ title: string, subtitle?: string, renderLines: () => string[] | Promise, onRefresh?: () => void | Promise }} 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} */ (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() } } }