/** Full-screen swarm chat TUI — preamble: edit-ansi → edit-key-parse → edit-stream-read. */ const BARE_CHAT_MAX_TRANSCRIPT = 4000 const BARE_CHAT_INPUT_MAX = 8192 /** * @param {string} s * @param {number} maxCols */ function bareChatTruncateVis(s, maxCols) { const t = String(s || '') if (maxCols < 8) return '' if (t.length <= maxCols) return t return t.slice(0, Math.max(0, maxCols - 1)) + '\u2026' } /** * @param {unknown} ms */ function bareChatFmtClock(ms) { if (typeof ms !== 'number' || !Number.isFinite(ms)) return '--:--:--' const d = new Date(ms) const z = (n) => (n < 10 ? '0' : '') + n return z(d.getHours()) + ':' + z(d.getMinutes()) + ':' + z(d.getSeconds()) } /** * @param {Record} ev */ function bareChatFmtEventLine(ev) { const nickRaw = String(ev.displayName ?? '').trim() const pkHint = typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey.slice(0, 10) : '' const nick = nickRaw || pkHint || 'peer' const body = String(ev.body ?? '') .replace(/\r\n/g, '\n') .split('\n') .join('\u2423 ') const tag = ev.local ? '*' : ' ' const ms = typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : typeof ev.tsMs === 'number' ? ev.tsMs : Date.now() return '[' + bareChatFmtClock(ms) + ']' + tag + nick + ': ' + body } /** * @param {Record} ctx */ async function bareChatReadProcSnapshot(ctx) { try { const vfs = ctx.vfs if (!vfs || typeof vfs.readFile !== 'function') return null const b = await vfs.readFile('/proc/bare_os/chat.json') if (!b) return null const t = ctx.b4a.toString(b).trim() if (!t) return null return /** @type {Record} */ (JSON.parse(t)) } catch { return null } } /** * @param {Record | null} snap * @param {{ * swarmPeers: { current: number | null }, * muxRx: { current: number | null }, * muxTx: { current: number | null }, * wireRxTotal: { current: number | null }, * dropRate: { current: number | null }, * dropVerify: { current: number | null } * }} out */ function bareChatApplyProcSnap(snap, out) { if (!snap || typeof snap !== 'object') return const sp = snap.swarmPeers out.swarmPeers.current = typeof sp === 'number' && Number.isFinite(sp) ? sp : null const met = snap.metrics if (met && typeof met === 'object') { const m = /** @type {{ rxEvent?: number, txEvent?: number, droppedRate?: number, droppedVerify?: number }} */ ( met ) out.muxRx.current = typeof m.rxEvent === 'number' && Number.isFinite(m.rxEvent) ? m.rxEvent : null out.muxTx.current = typeof m.txEvent === 'number' && Number.isFinite(m.txEvent) ? m.txEvent : null out.dropRate.current = typeof m.droppedRate === 'number' && Number.isFinite(m.droppedRate) ? m.droppedRate : null out.dropVerify.current = typeof m.droppedVerify === 'number' && Number.isFinite(m.droppedVerify) ? m.droppedVerify : null } else { out.muxRx.current = null out.muxTx.current = null out.dropRate.current = null out.dropVerify.current = null } const wire = snap.protomuxChatRxTotal out.wireRxTotal.current = typeof wire === 'number' && Number.isFinite(wire) ? wire : null } /** * @param {Record} ctx */ function bareChatTuiRunOpts(ctx) { const env = ctx.env && typeof ctx.env === 'object' ? /** @type {Record} */ (ctx.env) : {} /** @type {{ altScreen?: boolean, bracketedPaste?: boolean }} */ const opts = {} if ( (env.BARE_EDIT_NO_ALTSCREEN != null && String(env.BARE_EDIT_NO_ALTSCREEN) !== '') || (env.BARE_OS_TUI_NO_ALTSCREEN != null && String(env.BARE_OS_TUI_NO_ALTSCREEN) !== '') ) { opts.altScreen = false } if ( env.BARE_EDIT_NO_BRACKETED_PASTE != null && String(env.BARE_EDIT_NO_BRACKETED_PASTE) !== '' ) { opts.bracketedPaste = false } return opts } /** * @param {Record} ctx */ function bareChatTickMs(ctx) { const env = ctx.env && typeof ctx.env === 'object' ? /** @type {Record} */ (ctx.env) : {} let tickMs = 1000 const tickRaw = env.BARE_CHAT_STATUS_MS if (tickRaw != null && String(tickRaw) !== '') { const n = Number.parseInt(String(tickRaw), 10) if (Number.isFinite(n) && n >= 0) tickMs = Math.min(Math.max(0, n), 3_600_000) } return tickMs } /** * Windowed input line (inverse cursor). Matches the pre-SDK painter. * @param {{ value: string, cursor: number, prompt?: string }} input * @param {number} cols */ function bareChatViewInput(input, cols) { const prompt = input.prompt || '> ' const budget = Math.max(8, cols - prompt.length - 2) const ib = String(input.value || '') const ic = Math.min(Math.max(0, input.cursor | 0), ib.length) let winStart = 0 if (ib.length > budget) { winStart = ic - Math.floor(budget / 2) if (winStart < 0) winStart = 0 if (winStart > ib.length - budget) { winStart = Math.max(0, ib.length - budget) } } const slice = ib.slice(winStart, winStart + budget) const rel = ic - winStart return { prompt, before: slice.slice(0, rel), curCh: rel < slice.length ? slice.charAt(rel) : ' ', after: slice.slice(rel + 1) } } /** * TEA model for the swarm chat TUI. * @param {Record} ctx * @param {string} argv0 */ function bareChatCreateTuiApp(ctx, argv0) { const tui = ctx.tui const size0 = tui && typeof tui.size === 'function' ? tui.size() : {} const tickMs = bareChatTickMs(ctx) return { argv0: argv0 || 'chat', mode: /** @type {'main'|'help'} */ ('main'), transcript: /** @type {string[]} */ ([]), scrollTop: 0, stickToBottom: true, input: tui.textinput.create({ prompt: '> ', charLimit: BARE_CHAT_INPUT_MAX, focused: true }), tickMs, width: size0.width || 80, height: size0.height || 24, metrics: { swarmPeers: /** @type {{ current: number | null }} */ ({ current: null }), muxRx: /** @type {{ current: number | null }} */ ({ current: null }), muxTx: /** @type {{ current: number | null }} */ ({ current: null }), wireRxTotal: /** @type {{ current: number | null }} */ ({ current: null }), dropRate: /** @type {{ current: number | null }} */ ({ current: null }), dropVerify: /** @type {{ current: number | null }} */ ({ current: null }) }, _unsub: /** @type {(() => void) | null} */ (null), init: function () { const self = this if (typeof ctx.bareOsChatSubscribe === 'function') { this._unsub = ctx.bareOsChatSubscribe(function (ev) { if (tui && typeof tui.send === 'function') { tui.send({ type: 'chat.event', ev: ev && typeof ev === 'object' ? ev : {} }) } }) } if (typeof ctx.bareOsChatHistory === 'function') { try { const hist = ctx.bareOsChatHistory(BARE_CHAT_MAX_TRANSCRIPT) if (Array.isArray(hist)) { for (const h of hist) { if (h && typeof h === 'object') { self._push( bareChatFmtEventLine( /** @type {Record} */ (h) ) ) } } self.stickToBottom = true } } catch { /* ignore */ } } return tui.batch(this._refreshProc(), this._tickCmd()) }, dispose: function () { if (typeof this._unsub === 'function') { try { this._unsub() } catch { /* ignore */ } this._unsub = null } }, _push: function (text) { this.transcript.push(text) while (this.transcript.length > BARE_CHAT_MAX_TRANSCRIPT) { this.transcript.shift() if (this.scrollTop > 0) this.scrollTop-- } }, _msgH: function () { return Math.max(1, (this.height || 24) - 6) }, _clamp: function () { const maxTop = Math.max(0, this.transcript.length - this._msgH()) if (this.stickToBottom || this.scrollTop > maxTop) this.scrollTop = maxTop if (this.scrollTop < 0) this.scrollTop = 0 }, _refreshProc: function () { return function () { return Promise.resolve() .then(function () { let snap = null if (typeof ctx.bareOsChatProcSnapshot === 'function') { try { snap = ctx.bareOsChatProcSnapshot() } catch { snap = null } } if (!snap || typeof snap !== 'object') { return bareChatReadProcSnapshot(ctx) } return snap }) .then(function (snap) { return { type: 'chat.proc', snap: snap } }) } }, _tickCmd: function () { if (this.tickMs <= 0) return null const ms = this.tickMs return tui.tick(ms, function () { return { type: 'chat.tick' } }) }, _insertPaste: function (text) { const first = String(text || '') .split(/\r?\n/)[0] .slice(0, BARE_CHAT_INPUT_MAX) if (!first) return const field = this.input const room = Math.max(0, BARE_CHAT_INPUT_MAX - field.value.length) const chunk = first.slice(0, room) const c = field.cursor field.setValue(field.value.slice(0, c) + chunk + field.value.slice(c)) field.cursor = c + chunk.length }, update: function (msg) { if (this.mode === 'help') { if (msg && msg.type === 'key') this.mode = 'main' return [this, null] } if (tui.key.matches(msg, 'ctrl+c', 'ctrl+q', 'ctrl+x')) { return [this, tui.quit] } if (msg && msg.type === 'resize') { this.width = msg.width || this.width this.height = msg.height || this.height this._clamp() return [this, null] } if (msg && msg.type === 'chat.proc') { bareChatApplyProcSnap( /** @type {Record | null} */ (msg.snap), this.metrics ) return [this, null] } if (msg && msg.type === 'chat.event') { this._push( bareChatFmtEventLine( /** @type {Record} */ (msg.ev || {}) ) ) this._clamp() return [this, this._refreshProc()] } if (msg && msg.type === 'chat.tick') { return [this, tui.batch(this._refreshProc(), this._tickCmd())] } if (msg && msg.type === 'paste') { this._insertPaste(msg.text) return [this, null] } if (tui.key.matches(msg, '?')) { this.mode = 'help' return [this, null] } if (tui.key.matches(msg, 'enter')) { const line = String(this.input.value || '').trim() this.input.reset() if (line && typeof ctx.bareOsChatSend === 'function') { ctx.bareOsChatSend(line) } this.stickToBottom = true this._clamp() return [this, this._refreshProc()] } if (tui.key.matches(msg, 'ctrl+r')) { return [this, this._refreshProc()] } if (tui.key.matches(msg, 'ctrl+l')) { return [this, null] } if (tui.key.matches(msg, 'ctrl+a')) { this.input.cursor = 0 return [this, null] } if (tui.key.matches(msg, 'ctrl+e')) { this.input.cursor = this.input.value.length return [this, null] } if (tui.key.matches(msg, 'ctrl+u')) { this.input.setValue(this.input.value.slice(this.input.cursor)) this.input.cursor = 0 return [this, null] } if (tui.key.matches(msg, 'ctrl+k')) { this.input.setValue(this.input.value.slice(0, this.input.cursor)) return [this, null] } if (tui.key.matches(msg, 'up')) { this.stickToBottom = false if (this.scrollTop > 0) this.scrollTop-- return [this, null] } if (tui.key.matches(msg, 'down')) { const maxTop = Math.max(0, this.transcript.length - this._msgH()) if (this.scrollTop < maxTop) this.scrollTop++ if (this.scrollTop >= maxTop) this.stickToBottom = true return [this, null] } if (tui.key.matches(msg, 'pageup')) { this.stickToBottom = false this.scrollTop = Math.max(0, this.scrollTop - this._msgH()) return [this, null] } if (tui.key.matches(msg, 'pagedown')) { const maxTop = Math.max(0, this.transcript.length - this._msgH()) this.scrollTop = Math.min(maxTop, this.scrollTop + this._msgH()) if (this.scrollTop >= maxTop) this.stickToBottom = true return [this, null] } const pair = this.input.update(msg) this.input = pair[0] return [this, pair[1]] }, view: function () { const cols = Math.max(40, this.width || 80) const st = tui.style if (this.mode === 'help') { return ( st().bold().foreground('cyan').render('Bare OS — swarm chat') + '\n\n' + 'Send with Enter. Scroll transcript with \u2191/\u2193 or PgUp/PgDn.\n' + 'Input: Backspace/Delete, \u2190/\u2192, Home/End, ^A/^E line start/end, ^U kill to start, ^K kill to end.\n' + '^R refresh status ^L redraw ^Q / ^X / Ctrl+C exit\n' + '`chat send TEXT` / `chat who` / `chat history` remain for scripts.\n' + 'Env: BARE_CHAT_STATUS_MS — status refresh interval in ms (default 1000; 0 disables timer).\n\n' + 'Press any key.\n' ) } this._clamp() const rooms = typeof ctx.bareOsChatRooms === 'function' ? ctx.bareOsChatRooms() : [] const roomStr = Array.isArray(rooms) && rooms.length ? rooms.join(', ') : 'general' const m = this.metrics const peerStr = m.swarmPeers.current != null ? String(m.swarmPeers.current) : '?' const rxStr = m.muxRx.current != null ? String(m.muxRx.current) : '?' const txStr = m.muxTx.current != null ? String(m.muxTx.current) : '?' const wireStr = m.wireRxTotal.current != null ? String(m.wireRxTotal.current) : '?' const titleParts = [ this.argv0 || 'chat', roomStr, 'peers ' + peerStr, 'rx ' + rxStr, 'tx ' + txStr, 'wire ' + wireStr ] const dr = m.dropRate.current ?? 0 const dv = m.dropVerify.current ?? 0 if (dr > 0 || dv > 0) { titleParts.push('drops r' + dr + '/v' + dv) } const title = st() .foreground('brightwhite') .background('blue') .width(cols) .render(' \u250c ' + titleParts.join(' \u00b7 ') + ' ') const nowClock = bareChatFmtClock(Date.now()) const hint = st() .dim() .width(cols) .render( nowClock + ' bare-os-chat-v1 ? help ^Q quit ^R refresh' + (this.tickMs > 0 ? ' tick ' + this.tickMs + 'ms' : ' tick off') ) const rule = st() .dim() .width(cols) .render('\u2500'.repeat(Math.min(cols, 120))) const msgH = this._msgH() const body = [] for (let i = 0; i < msgH; i++) { const idx = this.scrollTop + i const raw = idx >= 0 && idx < this.transcript.length ? this.transcript[idx] : '' const line = st.truncate(raw, cols) if (idx === this.transcript.length - 1 && this.stickToBottom && raw) { body.push(st().foreground('green').width(cols).render(line)) } else { body.push(line) } } const linesBelow = Math.max( 0, this.transcript.length - this.scrollTop - msgH ) let scrollHint = '' if (!this.stickToBottom && (this.scrollTop > 0 || linesBelow > 0)) { const parts = [] if (this.scrollTop > 0) parts.push('\u2191 ' + this.scrollTop + ' older') if (linesBelow > 0) parts.push('\u2193 ' + linesBelow + ' newer') scrollHint = parts.join(' ') } const hint2 = st() .dim() .width(cols) .render( (scrollHint ? scrollHint + ' ' : '') + 'Enter send \u2191\u2193 transcript Backspace / ^A ^E ^U ^K' ) const win = bareChatViewInput(this.input, cols) const inputLine = st().dim().render(win.prompt) + win.before + st() .reverse() .render(win.curCh || ' ') + win.after return ( title + '\n' + hint + '\n' + rule + '\n' + body.join('\n') + '\n' + rule + '\n' + hint2 + '\n' + inputLine ) } } } /** * @param {Record} ctx * @param {string} argv0 */ async function bareOsRunChatTui(ctx, argv0) { if (ctx.tui && typeof ctx.tui.run === 'function') { const app = bareChatCreateTuiApp(ctx, argv0) try { await ctx.tui.run(app, bareChatTuiRunOpts(ctx)) } finally { if (app && typeof app.dispose === 'function') app.dispose() } return } await bareOsRunChatTuiLegacy(ctx, argv0) } /** * Pre-SDK key loop (BARE_OS_TUI=0). * @param {Record} ctx * @param {string} argv0 */ async function bareOsRunChatTuiLegacy(ctx, argv0) { const stdin = /** @type {import('stream').Readable | undefined} */ ( ctx.replStdin ) const stdout = bareEditResolveStdout(ctx) if (!stdin || !stdout) { ctx.console.error('chat: missing stdin/stdout') ctx.exitCode = 1 return } const useColor = bareEditUseColor(ctx) const envEarly = ctx.env && typeof ctx.env === 'object' ? /** @type {Record} */ (ctx.env) : {} let tickMs = 1000 const tickRaw = envEarly.BARE_CHAT_STATUS_MS if (tickRaw != null && String(tickRaw) !== '') { const n = Number.parseInt(String(tickRaw), 10) if (Number.isFinite(n) && n >= 0) tickMs = Math.min(Math.max(0, n), 3_600_000) } /** @type {'main'|'help'} */ let mode = 'main' /** @type {string[]} */ const transcript = [] /** First visible transcript index */ let scrollTop = 0 /** When true, new messages snap scroll to bottom */ let stickToBottom = true let inputBuf = '' let inputCursor = 0 const metricRef = { swarmPeers: /** @type {{ current: number | null }} */ ({ current: null }), muxRx: /** @type {{ current: number | null }} */ ({ current: null }), muxTx: /** @type {{ current: number | null }} */ ({ current: null }), wireRxTotal: /** @type {{ current: number | null }} */ ({ current: null }), dropRate: /** @type {{ current: number | null }} */ ({ current: null }), dropVerify: /** @type {{ current: number | null }} */ ({ current: null }) } async function refreshProcStrip() { let snap = null if (typeof ctx.bareOsChatProcSnapshot === 'function') { try { snap = ctx.bareOsChatProcSnapshot() } catch { snap = null } } if (!snap || typeof snap !== 'object') { snap = await bareChatReadProcSnapshot(ctx) } bareChatApplyProcSnap(snap, metricRef) } function clampScroll(viewH) { const maxTop = Math.max(0, transcript.length - viewH) if (stickToBottom || scrollTop > maxTop) scrollTop = maxTop if (scrollTop < 0) scrollTop = 0 } function appendEvent(rec) { bareChatPushTranscript(transcript, bareChatFmtEventLine(rec)) } function bareChatPushTranscript(lines, text) { lines.push(text) while (lines.length > BARE_CHAT_MAX_TRANSCRIPT) { lines.shift() if (scrollTop > 0) scrollTop-- } } function termDims() { const cols = /** @type {{ columns?: number }} */ (stdout).columns || parseInt(envEarly.COLUMNS || '80', 10) || 80 const rows = /** @type {{ rows?: number }} */ (stdout).rows || parseInt(envEarly.LINES || '24', 10) || 24 return { cols: Math.max(40, cols), rows: Math.max(10, rows) } } let paintBusy = false let paintAgain = false function draw() { const { cols, rows } = termDims() const headerRows = 3 const footerRows = 3 const msgH = Math.max(1, rows - headerRows - footerRows) clampScroll(msgH) let out = '\x1b[?25l\x1b[2J\x1b[H' if (mode === 'help') { out += bareEditSgr('keyword', useColor) + 'Bare OS — swarm chat' + EDIT_ANSI_RESET + '\r\n\r\n' + 'Send with Enter. Scroll transcript with \u2191/\u2193 or PgUp/PgDn.\r\n' + 'Input: Backspace/Delete, \u2190/\u2192, Home/End, ^A/^E line start/end, ^U kill to start, ^K kill to end.\r\n' + '^R refresh status ^L redraw ^Q / ^X / Ctrl+C exit\r\n' + '`chat send TEXT` / `chat who` / `chat history` remain for scripts.\r\n' + 'Env: BARE_CHAT_STATUS_MS — status refresh interval in ms (default 1000; 0 disables timer).\r\n\r\n' + 'Press any key.\r\n' bareEditWrite(ctx, stdout, out + '\x1b[?25h') return } const rooms = typeof ctx.bareOsChatRooms === 'function' ? ctx.bareOsChatRooms() : [] const roomStr = Array.isArray(rooms) && rooms.length ? rooms.join(', ') : 'general' const peerStr = metricRef.swarmPeers.current != null ? String(metricRef.swarmPeers.current) : '?' const rxStr = metricRef.muxRx.current != null ? String(metricRef.muxRx.current) : '?' const txStr = metricRef.muxTx.current != null ? String(metricRef.muxTx.current) : '?' const wireStr = metricRef.wireRxTotal.current != null ? String(metricRef.wireRxTotal.current) : '?' /** @type {string[]} */ const titleParts = [ argv0 || 'chat', roomStr, 'peers ' + peerStr, 'rx ' + rxStr, 'tx ' + txStr, 'wire ' + wireStr ] const dr = metricRef.dropRate.current ?? 0 const dv = metricRef.dropVerify.current ?? 0 if (dr > 0 || dv > 0) { titleParts.push('drops r' + dr + '/v' + dv) } const title = bareEditSgr('status', useColor) + bareChatTruncateVis( ' \u250c ' + titleParts.join(' \u00b7 ') + ' ', cols ) + EDIT_ANSI_RESET const nowClock = bareChatFmtClock(Date.now()) const hint = bareEditSgr('dim', useColor) + bareChatTruncateVis( nowClock + ' bare-os-chat-v1 ? help ^Q quit ^R refresh' + (tickMs > 0 ? ' tick ' + tickMs + 'ms' : ' tick off'), cols ) + EDIT_ANSI_RESET out += bareEditCup(1, 1) + '\x1b[K' + title out += bareEditCup(2, 1) + '\x1b[K' + hint out += bareEditCup(3, 1) + '\x1b[K' + bareEditSgr('dim', useColor) + '\u2500'.repeat(Math.min(cols, 120)) + EDIT_ANSI_RESET for (let i = 0; i < msgH; i++) { const idx = scrollTop + i const raw = idx >= 0 && idx < transcript.length ? transcript[idx] : '' const line = bareChatTruncateVis(raw, cols) const row = headerRows + 1 + i const dim = idx === transcript.length - 1 && stickToBottom ? bareEditSgr('string', useColor) : '' out += bareEditCup(row, 1) + '\x1b[K' + dim + line + EDIT_ANSI_RESET } const sepRow = rows - footerRows + 1 out += bareEditCup(sepRow, 1) + '\x1b[K' + bareEditSgr('dim', useColor) + '\u2500'.repeat(Math.min(cols, 120)) + EDIT_ANSI_RESET const hintRow = sepRow + 1 const linesBelow = Math.max(0, transcript.length - scrollTop - msgH) let scrollHint = '' if (!stickToBottom && (scrollTop > 0 || linesBelow > 0)) { const parts = [] if (scrollTop > 0) parts.push('\u2191 ' + scrollTop + ' older') if (linesBelow > 0) parts.push('\u2193 ' + linesBelow + ' newer') scrollHint = parts.join(' ') } const hint2 = bareEditSgr('dim', useColor) + bareChatTruncateVis( (scrollHint ? scrollHint + ' ' : '') + 'Enter send \u2191\u2193 transcript Backspace / ^A ^E ^U ^K', cols ) + EDIT_ANSI_RESET out += bareEditCup(hintRow, 1) + '\x1b[K' + hint2 const prompt = '> ' const promptLen = prompt.length const budget = Math.max(8, cols - promptLen - 2) const ib = inputBuf const ic = Math.min(Math.max(0, inputCursor), ib.length) let winStart = 0 if (ib.length > budget) { winStart = ic - Math.floor(budget / 2) if (winStart < 0) winStart = 0 if (winStart > ib.length - budget) { winStart = Math.max(0, ib.length - budget) } } const slice = ib.slice(winStart, winStart + budget) const rel = ic - winStart const before = slice.slice(0, rel) const curCh = rel < slice.length ? slice.charAt(rel) : ' ' const after = slice.slice(rel + 1) const inputRow = hintRow + 1 const inputLine = bareEditSgr('dim', useColor) + prompt + EDIT_ANSI_RESET + before + bareEditSgr('inverse', useColor) + (curCh || ' ') + EDIT_ANSI_RESET + after out += bareEditCup(inputRow, 1) + '\x1b[K' + inputLine bareEditWrite(ctx, stdout, out + '\x1b[?25h') } function paint() { if (paintBusy) { paintAgain = true return } paintBusy = true try { do { paintAgain = false draw() } while (paintAgain) } finally { paintBusy = false } } /** @type {(() => void) | null} */ let unsub = null if (typeof ctx.bareOsChatSubscribe === 'function') { unsub = ctx.bareOsChatSubscribe((ev) => { appendEvent( /** @type {Record} */ ( ev && typeof ev === 'object' ? ev : {} ) ) void refreshProcStrip().then(() => paint()) }) } if (typeof ctx.bareOsChatHistory === 'function') { try { const hist = ctx.bareOsChatHistory(BARE_CHAT_MAX_TRANSCRIPT) if (Array.isArray(hist)) { for (const h of hist) { if (h && typeof h === 'object') { bareChatPushTranscript( transcript, bareChatFmtEventLine(/** @type {Record} */ (h)) ) } } stickToBottom = true } } catch { /* ignore */ } } const reader = bareEditCreateStdinReader(stdin) let suspended = false let useAltScreen = false let useBracketPaste = false /** @type {ReturnType | null} */ let tickTimer = null function onResize() { void refreshProcStrip().then(() => paint()) } await refreshProcStrip() 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() const bareChatNoAltScreen = envEarly.BARE_EDIT_NO_ALTSCREEN != null && String(envEarly.BARE_EDIT_NO_ALTSCREEN) !== '' if (!bareChatNoAltScreen) { bareEditWrite(ctx, stdout, '\x1b[?1049h') useAltScreen = true } const bareChatNoBracketPaste = envEarly.BARE_EDIT_NO_BRACKETED_PASTE != null && String(envEarly.BARE_EDIT_NO_BRACKETED_PASTE) !== '' if (!bareChatNoBracketPaste) { bareEditWrite(ctx, stdout, '\x1b[?2004h') useBracketPaste = 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 */ } if (tickMs > 0 && typeof setInterval === 'function') { tickTimer = setInterval(() => { void refreshProcStrip().then(() => paint()) }, tickMs) } paint() for (;;) { const ev = await bareEditReadKey(reader) if (ev.type === 'eof') break if (mode === 'help') { mode = 'main' paint() continue } if (ev.type === 'ctrl') { if (ev.code === 'interrupt') break /** Backspace — key-parse uses `code: 'backspace'` for bytes 8 and 127 (not numeric). */ if (ev.code === 'backspace') { if (inputCursor > 0) { inputBuf = inputBuf.slice(0, inputCursor - 1) + inputBuf.slice(inputCursor) inputCursor-- } paint() continue } if (ev.code === 'delete') { if (inputCursor < inputBuf.length) { inputBuf = inputBuf.slice(0, inputCursor) + inputBuf.slice(inputCursor + 1) } paint() continue } const code = typeof ev.code === 'number' ? ev.code : 0 if (code === 3) break if (code === 12) { paint() continue } if (code === 17 || code === 24) break if (code === 18) { await refreshProcStrip() paint() continue } /** Readline-style shortcuts (ASCII control chars). */ if (code === 1) { inputCursor = 0 paint() continue } if (code === 5) { inputCursor = inputBuf.length paint() continue } if (code === 21) { inputBuf = inputBuf.slice(inputCursor) inputCursor = 0 paint() continue } if (code === 11) { inputBuf = inputBuf.slice(0, inputCursor) paint() continue } if (code === 8 || code === 127) { if (inputCursor > 0) { inputBuf = inputBuf.slice(0, inputCursor - 1) + inputBuf.slice(inputCursor) inputCursor-- } paint() continue } continue } if (ev.type === 'nav') { const k = /** @type {{ key?: string }} */ (ev).key if (k === 'up') { stickToBottom = false if (scrollTop > 0) scrollTop-- paint() continue } if (k === 'down') { const { rows: rr } = termDims() const msgH = Math.max(1, rr - 6) const maxTop = Math.max(0, transcript.length - msgH) if (scrollTop < maxTop) scrollTop++ if (scrollTop >= maxTop) stickToBottom = true paint() continue } if (k === 'left') { if (inputCursor > 0) inputCursor-- paint() continue } if (k === 'right') { if (inputCursor < inputBuf.length) inputCursor++ paint() continue } if (k === 'home') { inputCursor = 0 paint() continue } if (k === 'end') { inputCursor = inputBuf.length paint() continue } if (k === 'pageup') { stickToBottom = false const { rows: rr } = termDims() const msgH = Math.max(1, rr - 6) scrollTop = Math.max(0, scrollTop - msgH) paint() continue } if (k === 'pagedown') { const { rows: rr } = termDims() const msgH = Math.max(1, rr - 6) const maxTop = Math.max(0, transcript.length - msgH) scrollTop = Math.min(maxTop, scrollTop + msgH) if (scrollTop >= maxTop) stickToBottom = true paint() continue } continue } if (ev.type === 'key' && ev.ch) { if (ev.ch === '?') { mode = 'help' paint() continue } const ch = ev.ch if (ch === '\n') { const line = inputBuf.trim() inputBuf = '' inputCursor = 0 if (line && typeof ctx.bareOsChatSend === 'function') { ctx.bareOsChatSend(line) } stickToBottom = true await refreshProcStrip() paint() continue } if (inputBuf.length < BARE_CHAT_INPUT_MAX) { inputBuf = inputBuf.slice(0, inputCursor) + ch + inputBuf.slice(inputCursor) inputCursor += ch.length } paint() continue } if (ev.type === 'paste') { const first = String(ev.text || '') .split(/\r?\n/)[0] ?.slice(0, BARE_CHAT_INPUT_MAX) ?? '' if (first) { const room = Math.max( 0, BARE_CHAT_INPUT_MAX - inputBuf.length + (inputBuf.length - inputCursor) ) const chunk = first.slice(0, room) inputBuf = inputBuf.slice(0, inputCursor) + chunk + inputBuf.slice(inputCursor) inputCursor += chunk.length } paint() continue } } } finally { if (tickTimer != null && typeof clearInterval === 'function') { try { clearInterval(tickTimer) } catch { /* ignore */ } tickTimer = null } if (typeof stdout.removeListener === 'function') { try { stdout.removeListener('resize', onResize) } catch { /* ignore */ } } try { if (typeof process !== 'undefined' && typeof process.off === 'function') { process.off('SIGWINCH', onResize) } } catch { /* ignore */ } try { if (useBracketPaste) bareEditWrite(ctx, stdout, '\x1b[?2004l') if (useAltScreen) bareEditWrite(ctx, stdout, '\x1b[?1049l') else bareEditWrite(ctx, stdout, '\x1b[2J\x1b[H') bareEditWrite(ctx, stdout, '\x1b[?25h\x1b[0m') } catch { /* ignore */ } try { if (typeof unsub === 'function') unsub() } catch { /* ignore */ } reader.dispose() try { if (typeof stdin.setRawMode === 'function') stdin.setRawMode(false) } catch { /* ignore */ } if (suspended && typeof ctx.resumeReplAfterSubprocess === 'function') { ctx.resumeReplAfterSubprocess() } } }