125 lines
3.5 KiB
JavaScript
125 lines
3.5 KiB
JavaScript
/**
|
|
* Verbose REPL/fish diagnostics when `DEBUG=1`.
|
|
* Writes to the **session TTY** when bound (see `bindReplDebugStream`) so Pear/bare-stdio
|
|
* logs show in the same terminal pane as the shell. Falls back to `process.stdout`, then
|
|
* `stderr`, then `console.log`. Lines are prefixed with reset + newline so they stay
|
|
* readable if the prompt left SGR/cursor state; expect the screen to scroll while debugging.
|
|
*/
|
|
|
|
/** @type {import('stream').Writable | null} */
|
|
let _debugStream = null
|
|
|
|
/** Bind REPL/fish stdout (e.g. bare-stdio `out`) so DEBUG lines appear in the visible terminal. */
|
|
export function bindReplDebugStream(stream) {
|
|
_debugStream = stream && typeof stream.write === 'function' ? stream : null
|
|
}
|
|
|
|
export function unbindReplDebugStream() {
|
|
_debugStream = null
|
|
}
|
|
|
|
/** @returns {boolean} */
|
|
export function isReplDebug() {
|
|
const p = globalThis.process?.env
|
|
return p?.DEBUG === '1'
|
|
}
|
|
|
|
function ts() {
|
|
if (typeof globalThis.performance?.now === 'function') {
|
|
return `${globalThis.performance.now().toFixed(2)}ms`
|
|
}
|
|
return `${Date.now()}`
|
|
}
|
|
|
|
function serialize(part) {
|
|
if (typeof part === 'string') return part
|
|
if (part === undefined) return 'undefined'
|
|
if (part === null) return 'null'
|
|
if (typeof part === 'number' || typeof part === 'boolean') return String(part)
|
|
try {
|
|
return JSON.stringify(part)
|
|
} catch {
|
|
return String(part)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} scope Short tag (e.g. fish-render, repl, booter)
|
|
* @param {...unknown} parts Message pieces
|
|
*/
|
|
function writeDebugLine(text) {
|
|
const payload = `\x1b[0m\n${text}\n`
|
|
try {
|
|
if (_debugStream && typeof _debugStream.write === 'function') {
|
|
_debugStream.write(payload)
|
|
return
|
|
}
|
|
const p = globalThis.process
|
|
if (p?.stdout && typeof p.stdout.write === 'function') {
|
|
p.stdout.write(payload)
|
|
return
|
|
}
|
|
if (p?.stderr && typeof p.stderr.write === 'function') {
|
|
p.stderr.write(payload)
|
|
return
|
|
}
|
|
globalThis.console?.log?.(text)
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
|
|
export function replDbg(scope, ...parts) {
|
|
if (!isReplDebug()) return
|
|
const line = `[${ts()}] [bare-os DEBUG:${scope}] ${parts.map(serialize).join(' ')}`
|
|
writeDebugLine(line)
|
|
}
|
|
|
|
/**
|
|
* Human-readable key / escape sequence for logs.
|
|
* @param {string} key
|
|
*/
|
|
export function replDbgKeyRepr(key) {
|
|
if (key === '\n') return '\\n'
|
|
if (key === '\r') return '\\r'
|
|
if (key === '\t') return '\\t'
|
|
if (key === '\u007f') return 'DEL'
|
|
if (key === '\u0003') return '^C'
|
|
if (key === '\u0004') return '^D'
|
|
if (key === '\u000c') return '^L'
|
|
if (key === '\u0012' || (key.length === 1 && key.charCodeAt(0) === 18))
|
|
return '^R'
|
|
if (key.startsWith('\u001b')) {
|
|
return `CSI(${key.length}ch)=${JSON.stringify(key)}`
|
|
}
|
|
if (key.length === 1) {
|
|
const c = key.charCodeAt(0)
|
|
if (c < 32) return `^${String.fromCharCode(c + 64)}(u${c})`
|
|
return key
|
|
}
|
|
return JSON.stringify(key.length > 120 ? key.slice(0, 120) + '…' : key)
|
|
}
|
|
|
|
/**
|
|
* @param {string} label
|
|
* @param {string} chunk
|
|
* @param {number} [maxLen]
|
|
*/
|
|
export function replDbgChunk(label, chunk, maxLen = 256) {
|
|
if (!isReplDebug()) return
|
|
const s = typeof chunk === 'string' ? chunk : String(chunk)
|
|
const t =
|
|
s.length > maxLen ? s.slice(0, maxLen) + `…(+${s.length - maxLen})` : s
|
|
const codes = [...t].slice(0, 80).map((ch) => {
|
|
const u = ch.charCodeAt(0)
|
|
if (u < 32 || u === 127) return `\\x${u.toString(16).padStart(2, '0')}`
|
|
return ch
|
|
})
|
|
replDbg(
|
|
'fish-stdin',
|
|
label,
|
|
`len=${s.length}`,
|
|
`head=${JSON.stringify(codes.join(''))}`
|
|
)
|
|
}
|