feat: add persistent PTY sessions
This commit is contained in:
99
packages/pty/pty-local/src/sanitize.ts
Normal file
99
packages/pty/pty-local/src/sanitize.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
/** Streaming terminal-control sanitizer for the line-oriented first release. */
|
||||
|
||||
/** OSC marker emitted by the controlled bash before each prompt. */
|
||||
export const PROMPT_MARKER_PREFIX = '133;D;'
|
||||
|
||||
/** One sanitized chunk plus whether it contained the owned prompt marker. */
|
||||
export interface SanitizedChunk {
|
||||
text: string
|
||||
prompt: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove CSI/OSC/short escape sequences while preserving split-sequence carry.
|
||||
* Full terminal emulation is deliberately deferred; ordinary line output and
|
||||
* the private prompt marker are the supported contract.
|
||||
*/
|
||||
export class TerminalSanitizer {
|
||||
private pending = ''
|
||||
|
||||
/**
|
||||
* Consume one decoded `node-pty` data chunk.
|
||||
* @param chunk - decoded terminal data.
|
||||
* @returns Printable text and whether the private prompt marker completed.
|
||||
*/
|
||||
push(chunk: string): SanitizedChunk {
|
||||
this.pending += chunk
|
||||
let text = ''
|
||||
let prompt = false
|
||||
let index = 0
|
||||
while (index < this.pending.length) {
|
||||
const escape = this.pending.indexOf('\x1b', index)
|
||||
if (escape < 0) {
|
||||
text += this.pending.slice(index)
|
||||
index = this.pending.length
|
||||
break
|
||||
}
|
||||
text += this.pending.slice(index, escape)
|
||||
if (escape + 1 >= this.pending.length) {
|
||||
index = escape
|
||||
break
|
||||
}
|
||||
const kind = this.pending[escape + 1]
|
||||
if (kind === ']') {
|
||||
const bel = this.pending.indexOf('\x07', escape + 2)
|
||||
const stringTerminator = this.pending.indexOf('\x1b\\', escape + 2)
|
||||
let end = -1
|
||||
if (bel >= 0 && stringTerminator >= 0) end = Math.min(bel + 1, stringTerminator + 2)
|
||||
else if (bel >= 0) end = bel + 1
|
||||
else if (stringTerminator >= 0) end = stringTerminator + 2
|
||||
if (end < 0) {
|
||||
index = escape
|
||||
break
|
||||
}
|
||||
const terminatorBytes = this.pending[end - 1] === '\x07' ? 1 : 2
|
||||
const content = this.pending.slice(escape + 2, end - terminatorBytes)
|
||||
if (content.startsWith(PROMPT_MARKER_PREFIX)) prompt = true
|
||||
index = end
|
||||
continue
|
||||
}
|
||||
if (kind === '[') {
|
||||
let end = escape + 2
|
||||
while (end < this.pending.length) {
|
||||
const code = this.pending.charCodeAt(end)
|
||||
if (code >= 0x40 && code <= 0x7e) break
|
||||
end += 1
|
||||
}
|
||||
if (end >= this.pending.length) {
|
||||
index = escape
|
||||
break
|
||||
}
|
||||
index = end + 1
|
||||
continue
|
||||
}
|
||||
// Two-byte escape family (save/restore cursor and similar).
|
||||
index = escape + 2
|
||||
}
|
||||
this.pending = this.pending.slice(index)
|
||||
return { text: normalizeTerminalText(text), prompt }
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush a trailing printable fragment when the PTY exits.
|
||||
* @returns Remaining printable text; incomplete escapes are discarded.
|
||||
*/
|
||||
flush(): string {
|
||||
const text = this.pending.startsWith('\x1b') ? '' : this.pending
|
||||
this.pending = ''
|
||||
return normalizeTerminalText(text)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize CRLF and standalone carriage returns for line-oriented rendering.
|
||||
* @param text - sanitized terminal text.
|
||||
* @returns Line-normalized text with BEL removed.
|
||||
*/
|
||||
export function normalizeTerminalText(text: string): string {
|
||||
return text.replaceAll('\r\n', '\n').replaceAll('\r', '\n').replaceAll('\x07', '')
|
||||
}
|
||||
Reference in New Issue
Block a user