fix(edit,nano): bracketed paste + bounded prompts so Save-as cannot flood

Enable xterm bracketed paste (?2004h/?2004l), parse ESC[200~…201~ as a single
paste event, insert paste in the buffer with one undo step, and replace
prompt input with first-line sanitized text capped by BARE_EDIT_MAX_PROMPT.
Cap typed prompt length for terminals without bracketed paste. Extend tests and
rebuild kernel /bin edit and nano bundles.
This commit is contained in:
Raven Scott
2026-04-21 18:45:55 -04:00
parent 36492de3d9
commit ef0301f435
19 changed files with 1511 additions and 40019 deletions
+95
View File
@@ -274,6 +274,101 @@ function bareEditSs3ToEvent(b3) {
return { type: 'unknown' }
}
/** ESC [ 200 ~ */
const BARE_EDIT_BRACKET_PASTE_START = [27, 91, 50, 48, 48, 126]
/** ESC [ 201 ~ */
const BARE_EDIT_BRACKET_PASTE_END = [27, 91, 50, 48, 49, 126]
/**
* @param {number[]} q
* @param {number[]} prefix
*/
function bareEditStartsWithBytes(q, prefix) {
if (q.length < prefix.length) return false
for (let i = 0; i < prefix.length; i++) {
if (q[i] !== prefix[i]) return false
}
return true
}
/**
* True if q could still become BARE_EDIT_BRACKET_PASTE_START with more bytes.
* @param {number[]} q
*/
function bareEditCouldBeBracketPastePrefix(q) {
const pre = BARE_EDIT_BRACKET_PASTE_START
const n = Math.min(q.length, pre.length)
for (let i = 0; i < n; i++) {
if (q[i] !== pre[i]) return false
}
return true
}
/**
* @param {number[]} bytes
*/
function bareEditDecodePasteInner(bytes) {
try {
if (typeof TextDecoder !== 'undefined') {
return new TextDecoder('utf-8', { fatal: false }).decode(
new Uint8Array(bytes)
)
}
} catch {
/* fall through */
}
let s = ''
for (const b of bytes) s += String.fromCharCode(b)
return s
}
/**
* If queue begins with ESC [ 200 ~ but no closing ESC [ 201 ~ (stdin closed), return inner bytes as text.
* @param {number[]} q
* @returns {string|undefined}
*/
function bareEditFinalizeBracketedPasteOnEof(q) {
const SL = BARE_EDIT_BRACKET_PASTE_START.length
if (q.length >= SL && bareEditStartsWithBytes(q, BARE_EDIT_BRACKET_PASTE_START)) {
const inner = q.slice(SL)
q.length = 0
return bareEditDecodePasteInner(inner)
}
return undefined
}
/**
* xterm bracketed paste: ESC [ 200 ~ … ESC [ 201 ~
* @param {number[]} q mutable queue
* @returns {string|null|undefined} string if consumed; null if incomplete; undefined if not bracketed paste at front
*/
function bareEditTryConsumeBracketedPaste(q) {
const SL = BARE_EDIT_BRACKET_PASTE_START.length
const EL = BARE_EDIT_BRACKET_PASTE_END.length
if (!q.length) return undefined
if (q.length < SL) {
return bareEditCouldBeBracketPastePrefix(q) ? null : undefined
}
if (!bareEditStartsWithBytes(q, BARE_EDIT_BRACKET_PASTE_START)) {
return undefined
}
for (let i = SL; i <= q.length - EL; i++) {
let ok = true
for (let j = 0; j < EL; j++) {
if (q[i + j] !== BARE_EDIT_BRACKET_PASTE_END[j]) {
ok = false
break
}
}
if (ok) {
const inner = q.slice(SL, i)
q.splice(0, i + EL)
return bareEditDecodePasteInner(inner)
}
}
return null
}
/**
* @param {number[]} q mutable queue (front = index 0)
* @returns {Record<string, unknown> | null} null if more bytes needed
+95
View File
@@ -274,6 +274,101 @@ function bareEditSs3ToEvent(b3) {
return { type: 'unknown' }
}
/** ESC [ 200 ~ */
const BARE_EDIT_BRACKET_PASTE_START = [27, 91, 50, 48, 48, 126]
/** ESC [ 201 ~ */
const BARE_EDIT_BRACKET_PASTE_END = [27, 91, 50, 48, 49, 126]
/**
* @param {number[]} q
* @param {number[]} prefix
*/
function bareEditStartsWithBytes(q, prefix) {
if (q.length < prefix.length) return false
for (let i = 0; i < prefix.length; i++) {
if (q[i] !== prefix[i]) return false
}
return true
}
/**
* True if q could still become BARE_EDIT_BRACKET_PASTE_START with more bytes.
* @param {number[]} q
*/
function bareEditCouldBeBracketPastePrefix(q) {
const pre = BARE_EDIT_BRACKET_PASTE_START
const n = Math.min(q.length, pre.length)
for (let i = 0; i < n; i++) {
if (q[i] !== pre[i]) return false
}
return true
}
/**
* @param {number[]} bytes
*/
function bareEditDecodePasteInner(bytes) {
try {
if (typeof TextDecoder !== 'undefined') {
return new TextDecoder('utf-8', { fatal: false }).decode(
new Uint8Array(bytes)
)
}
} catch {
/* fall through */
}
let s = ''
for (const b of bytes) s += String.fromCharCode(b)
return s
}
/**
* If queue begins with ESC [ 200 ~ but no closing ESC [ 201 ~ (stdin closed), return inner bytes as text.
* @param {number[]} q
* @returns {string|undefined}
*/
function bareEditFinalizeBracketedPasteOnEof(q) {
const SL = BARE_EDIT_BRACKET_PASTE_START.length
if (q.length >= SL && bareEditStartsWithBytes(q, BARE_EDIT_BRACKET_PASTE_START)) {
const inner = q.slice(SL)
q.length = 0
return bareEditDecodePasteInner(inner)
}
return undefined
}
/**
* xterm bracketed paste: ESC [ 200 ~ … ESC [ 201 ~
* @param {number[]} q mutable queue
* @returns {string|null|undefined} string if consumed; null if incomplete; undefined if not bracketed paste at front
*/
function bareEditTryConsumeBracketedPaste(q) {
const SL = BARE_EDIT_BRACKET_PASTE_START.length
const EL = BARE_EDIT_BRACKET_PASTE_END.length
if (!q.length) return undefined
if (q.length < SL) {
return bareEditCouldBeBracketPastePrefix(q) ? null : undefined
}
if (!bareEditStartsWithBytes(q, BARE_EDIT_BRACKET_PASTE_START)) {
return undefined
}
for (let i = SL; i <= q.length - EL; i++) {
let ok = true
for (let j = 0; j < EL; j++) {
if (q[i + j] !== BARE_EDIT_BRACKET_PASTE_END[j]) {
ok = false
break
}
}
if (ok) {
const inner = q.slice(SL, i)
q.splice(0, i + EL)
return bareEditDecodePasteInner(inner)
}
}
return null
}
/**
* @param {number[]} q mutable queue (front = index 0)
* @returns {Record<string, unknown> | null} null if more bytes needed
+217 -7
View File
@@ -580,6 +580,37 @@ function bareEditNewline(buf) {
buf.dirty = true
}
/**
* Insert pasted text as one undo step (normalized newlines; tab → two spaces).
* @param {{ lines: string[], row: number, col: number, dirty: boolean, undo: unknown[], redo: unknown[] }} buf
* @param {string} text
*/
function bareEditInsertPasteText(buf, text) {
const normalized = String(text).replace(/\r\n/g, '\n').replace(/\r/g, '\n')
if (!normalized) return
bareEditPushUndo(buf)
for (const ch of normalized) {
if (ch === '\n') {
const line = buf.lines[buf.row]
const rest = line.slice(buf.col)
buf.lines[buf.row] = line.slice(0, buf.col)
buf.lines.splice(buf.row + 1, 0, rest)
buf.row++
buf.col = 0
} else if (ch === '\t') {
const line = buf.lines[buf.row]
buf.lines[buf.row] = line.slice(0, buf.col) + ' ' + line.slice(buf.col)
buf.col += 2
} else {
const line = buf.lines[buf.row]
buf.lines[buf.row] = line.slice(0, buf.col) + ch + line.slice(buf.col)
buf.col += ch.length
}
}
buf.dirty = true
bareEditClampCursor(buf)
}
/**
* @param {{ lines: string[], row: number, col: number, dirty: boolean, undo: unknown[], redo: unknown[] }} buf
*/
@@ -797,6 +828,101 @@ function bareEditSs3ToEvent(b3) {
return { type: 'unknown' }
}
/** ESC [ 200 ~ */
const BARE_EDIT_BRACKET_PASTE_START = [27, 91, 50, 48, 48, 126]
/** ESC [ 201 ~ */
const BARE_EDIT_BRACKET_PASTE_END = [27, 91, 50, 48, 49, 126]
/**
* @param {number[]} q
* @param {number[]} prefix
*/
function bareEditStartsWithBytes(q, prefix) {
if (q.length < prefix.length) return false
for (let i = 0; i < prefix.length; i++) {
if (q[i] !== prefix[i]) return false
}
return true
}
/**
* True if q could still become BARE_EDIT_BRACKET_PASTE_START with more bytes.
* @param {number[]} q
*/
function bareEditCouldBeBracketPastePrefix(q) {
const pre = BARE_EDIT_BRACKET_PASTE_START
const n = Math.min(q.length, pre.length)
for (let i = 0; i < n; i++) {
if (q[i] !== pre[i]) return false
}
return true
}
/**
* @param {number[]} bytes
*/
function bareEditDecodePasteInner(bytes) {
try {
if (typeof TextDecoder !== 'undefined') {
return new TextDecoder('utf-8', { fatal: false }).decode(
new Uint8Array(bytes)
)
}
} catch {
/* fall through */
}
let s = ''
for (const b of bytes) s += String.fromCharCode(b)
return s
}
/**
* If queue begins with ESC [ 200 ~ but no closing ESC [ 201 ~ (stdin closed), return inner bytes as text.
* @param {number[]} q
* @returns {string|undefined}
*/
function bareEditFinalizeBracketedPasteOnEof(q) {
const SL = BARE_EDIT_BRACKET_PASTE_START.length
if (q.length >= SL && bareEditStartsWithBytes(q, BARE_EDIT_BRACKET_PASTE_START)) {
const inner = q.slice(SL)
q.length = 0
return bareEditDecodePasteInner(inner)
}
return undefined
}
/**
* xterm bracketed paste: ESC [ 200 ~ … ESC [ 201 ~
* @param {number[]} q mutable queue
* @returns {string|null|undefined} string if consumed; null if incomplete; undefined if not bracketed paste at front
*/
function bareEditTryConsumeBracketedPaste(q) {
const SL = BARE_EDIT_BRACKET_PASTE_START.length
const EL = BARE_EDIT_BRACKET_PASTE_END.length
if (!q.length) return undefined
if (q.length < SL) {
return bareEditCouldBeBracketPastePrefix(q) ? null : undefined
}
if (!bareEditStartsWithBytes(q, BARE_EDIT_BRACKET_PASTE_START)) {
return undefined
}
for (let i = SL; i <= q.length - EL; i++) {
let ok = true
for (let j = 0; j < EL; j++) {
if (q[i + j] !== BARE_EDIT_BRACKET_PASTE_END[j]) {
ok = false
break
}
}
if (ok) {
const inner = q.slice(SL, i)
q.splice(0, i + EL)
return bareEditDecodePasteInner(inner)
}
}
return null
}
/**
* @param {number[]} q mutable queue (front = index 0)
* @returns {Record<string, unknown> | null} null if more bytes needed
@@ -881,10 +1007,42 @@ async function bareEditReadKey(reader) {
reader._keyq = reader._keyq || []
const q = reader._keyq
for (;;) {
const pasteText = bareEditTryConsumeBracketedPaste(q)
if (pasteText !== undefined && pasteText !== null) {
return { type: 'paste', text: pasteText }
}
if (pasteText === null) {
const b = await reader.nextByte()
if (b === undefined) {
const eofPaste = bareEditFinalizeBracketedPasteOnEof(q)
if (eofPaste !== undefined) {
return { type: 'paste', text: eofPaste }
}
if (!q.length) return { type: 'eof' }
if (q.length === 1 && q[0] === 27) {
q.length = 0
return { type: 'key', ch: '\x1b' }
}
const lone = q.shift()
if (lone !== undefined && lone < 0x20) {
return { type: 'ctrl', code: lone }
}
if (lone !== undefined) {
return { type: 'key', ch: String.fromCharCode(lone) }
}
return { type: 'eof' }
}
q.push(b)
continue
}
const ev = bareEditTryConsumeKey(q)
if (ev) return ev
const b = await reader.nextByte()
if (b === undefined) {
const eofPaste = bareEditFinalizeBracketedPasteOnEof(q)
if (eofPaste !== undefined) {
return { type: 'paste', text: eofPaste }
}
if (!q.length) return { type: 'eof' }
if (q.length === 1 && q[0] === 27) {
q.length = 0
@@ -986,6 +1144,32 @@ async function bareOsRunEditTui(ctx, opts) {
let promptTitle = ''
let lastSearch = ''
const envEditEarly =
ctx.env && typeof ctx.env === 'object'
? /** @type {Record<string, string>} */ (ctx.env)
: {}
let maxPromptLen = 4096
const maxPromptRaw = envEditEarly.BARE_EDIT_MAX_PROMPT
if (maxPromptRaw != null && String(maxPromptRaw) !== '') {
const n = parseInt(String(maxPromptRaw), 10)
if (Number.isFinite(n) && n > 0) maxPromptLen = Math.min(n, 65536)
}
/**
* @param {string} text
* @param {'saveas'|'search'|'goto'} kind
*/
function promptLineFromPaste(text, kind) {
let line = String(text).split(/\r?\n/)[0] ?? ''
line = line.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, '')
if (line.length > maxPromptLen) line = line.slice(0, maxPromptLen)
if (kind === 'goto') {
const m = /^(\s*\d+)/.exec(line)
return m ? m[1].trim() : ''
}
return line
}
function termDims() {
const env =
ctx.env && typeof ctx.env === 'object'
@@ -1117,6 +1301,8 @@ async function bareOsRunEditTui(ctx, opts) {
let suspended = false
/** When true, alternate-screen mode was entered (?1049h); {@link bareEditWrite} must send ?1049l on exit. */
let useAltScreen = false
/** When true, bracketed paste mode was enabled (?2004h); must send ?2004l on exit. */
let useBracketPaste = false
try {
if (typeof ctx.suspendReplForSubprocess === 'function') {
ctx.suspendReplForSubprocess()
@@ -1125,18 +1311,22 @@ async function bareOsRunEditTui(ctx, opts) {
if (typeof stdin.setRawMode === 'function') stdin.setRawMode(true)
if (typeof stdin.resume === 'function') stdin.resume()
const envEdit =
ctx.env && typeof ctx.env === 'object'
? /** @type {Record<string, string>} */ (ctx.env)
: {}
const bareEditNoAltScreen =
envEdit.BARE_EDIT_NO_ALTSCREEN != null &&
String(envEdit.BARE_EDIT_NO_ALTSCREEN) !== ''
envEditEarly.BARE_EDIT_NO_ALTSCREEN != null &&
String(envEditEarly.BARE_EDIT_NO_ALTSCREEN) !== ''
if (!bareEditNoAltScreen) {
bareEditWrite(ctx, stdout, '\x1b[?1049h')
useAltScreen = true
}
const bareEditNoBracketPaste =
envEditEarly.BARE_EDIT_NO_BRACKETED_PASTE != null &&
String(envEditEarly.BARE_EDIT_NO_BRACKETED_PASTE) !== ''
if (!bareEditNoBracketPaste) {
bareEditWrite(ctx, stdout, '\x1b[?2004h')
useBracketPaste = true
}
draw()
for (;;) {
@@ -1176,6 +1366,17 @@ async function bareOsRunEditTui(ctx, opts) {
draw()
continue
}
if (ev.type === 'paste') {
if (mode === 'prompt_saveas') {
promptBuf = promptLineFromPaste(ev.text || '', 'saveas')
} else if (mode === 'prompt_search') {
promptBuf = promptLineFromPaste(ev.text || '', 'search')
} else if (mode === 'prompt_goto') {
promptBuf = promptLineFromPaste(ev.text || '', 'goto')
}
draw()
continue
}
if (ev.type === 'key' && ev.ch === '\n') {
if (mode === 'prompt_search') {
lastSearch = promptBuf
@@ -1217,13 +1418,19 @@ async function bareOsRunEditTui(ctx, opts) {
continue
}
if (ev.type === 'key' && ev.ch && ev.ch !== '\n' && ev.ch !== '\t') {
if (ev.ch >= ' ') promptBuf += ev.ch
if (ev.ch >= ' ' && promptBuf.length < maxPromptLen) promptBuf += ev.ch
draw()
continue
}
continue
}
if (ev.type === 'paste') {
bareEditInsertPasteText(buf, ev.text || '')
draw()
continue
}
if (ev.type === 'nav') {
if (ev.key === 'pageup') {
const { rows } = termDims()
@@ -1320,6 +1527,9 @@ async function bareOsRunEditTui(ctx, opts) {
}
} finally {
try {
if (useBracketPaste) {
bareEditWrite(ctx, stdout, '\x1b[?2004l')
}
if (useAltScreen) {
bareEditWrite(ctx, stdout, '\x1b[?1049l')
} else {
+217 -7
View File
@@ -580,6 +580,37 @@ function bareEditNewline(buf) {
buf.dirty = true
}
/**
* Insert pasted text as one undo step (normalized newlines; tab → two spaces).
* @param {{ lines: string[], row: number, col: number, dirty: boolean, undo: unknown[], redo: unknown[] }} buf
* @param {string} text
*/
function bareEditInsertPasteText(buf, text) {
const normalized = String(text).replace(/\r\n/g, '\n').replace(/\r/g, '\n')
if (!normalized) return
bareEditPushUndo(buf)
for (const ch of normalized) {
if (ch === '\n') {
const line = buf.lines[buf.row]
const rest = line.slice(buf.col)
buf.lines[buf.row] = line.slice(0, buf.col)
buf.lines.splice(buf.row + 1, 0, rest)
buf.row++
buf.col = 0
} else if (ch === '\t') {
const line = buf.lines[buf.row]
buf.lines[buf.row] = line.slice(0, buf.col) + ' ' + line.slice(buf.col)
buf.col += 2
} else {
const line = buf.lines[buf.row]
buf.lines[buf.row] = line.slice(0, buf.col) + ch + line.slice(buf.col)
buf.col += ch.length
}
}
buf.dirty = true
bareEditClampCursor(buf)
}
/**
* @param {{ lines: string[], row: number, col: number, dirty: boolean, undo: unknown[], redo: unknown[] }} buf
*/
@@ -797,6 +828,101 @@ function bareEditSs3ToEvent(b3) {
return { type: 'unknown' }
}
/** ESC [ 200 ~ */
const BARE_EDIT_BRACKET_PASTE_START = [27, 91, 50, 48, 48, 126]
/** ESC [ 201 ~ */
const BARE_EDIT_BRACKET_PASTE_END = [27, 91, 50, 48, 49, 126]
/**
* @param {number[]} q
* @param {number[]} prefix
*/
function bareEditStartsWithBytes(q, prefix) {
if (q.length < prefix.length) return false
for (let i = 0; i < prefix.length; i++) {
if (q[i] !== prefix[i]) return false
}
return true
}
/**
* True if q could still become BARE_EDIT_BRACKET_PASTE_START with more bytes.
* @param {number[]} q
*/
function bareEditCouldBeBracketPastePrefix(q) {
const pre = BARE_EDIT_BRACKET_PASTE_START
const n = Math.min(q.length, pre.length)
for (let i = 0; i < n; i++) {
if (q[i] !== pre[i]) return false
}
return true
}
/**
* @param {number[]} bytes
*/
function bareEditDecodePasteInner(bytes) {
try {
if (typeof TextDecoder !== 'undefined') {
return new TextDecoder('utf-8', { fatal: false }).decode(
new Uint8Array(bytes)
)
}
} catch {
/* fall through */
}
let s = ''
for (const b of bytes) s += String.fromCharCode(b)
return s
}
/**
* If queue begins with ESC [ 200 ~ but no closing ESC [ 201 ~ (stdin closed), return inner bytes as text.
* @param {number[]} q
* @returns {string|undefined}
*/
function bareEditFinalizeBracketedPasteOnEof(q) {
const SL = BARE_EDIT_BRACKET_PASTE_START.length
if (q.length >= SL && bareEditStartsWithBytes(q, BARE_EDIT_BRACKET_PASTE_START)) {
const inner = q.slice(SL)
q.length = 0
return bareEditDecodePasteInner(inner)
}
return undefined
}
/**
* xterm bracketed paste: ESC [ 200 ~ … ESC [ 201 ~
* @param {number[]} q mutable queue
* @returns {string|null|undefined} string if consumed; null if incomplete; undefined if not bracketed paste at front
*/
function bareEditTryConsumeBracketedPaste(q) {
const SL = BARE_EDIT_BRACKET_PASTE_START.length
const EL = BARE_EDIT_BRACKET_PASTE_END.length
if (!q.length) return undefined
if (q.length < SL) {
return bareEditCouldBeBracketPastePrefix(q) ? null : undefined
}
if (!bareEditStartsWithBytes(q, BARE_EDIT_BRACKET_PASTE_START)) {
return undefined
}
for (let i = SL; i <= q.length - EL; i++) {
let ok = true
for (let j = 0; j < EL; j++) {
if (q[i + j] !== BARE_EDIT_BRACKET_PASTE_END[j]) {
ok = false
break
}
}
if (ok) {
const inner = q.slice(SL, i)
q.splice(0, i + EL)
return bareEditDecodePasteInner(inner)
}
}
return null
}
/**
* @param {number[]} q mutable queue (front = index 0)
* @returns {Record<string, unknown> | null} null if more bytes needed
@@ -881,10 +1007,42 @@ async function bareEditReadKey(reader) {
reader._keyq = reader._keyq || []
const q = reader._keyq
for (;;) {
const pasteText = bareEditTryConsumeBracketedPaste(q)
if (pasteText !== undefined && pasteText !== null) {
return { type: 'paste', text: pasteText }
}
if (pasteText === null) {
const b = await reader.nextByte()
if (b === undefined) {
const eofPaste = bareEditFinalizeBracketedPasteOnEof(q)
if (eofPaste !== undefined) {
return { type: 'paste', text: eofPaste }
}
if (!q.length) return { type: 'eof' }
if (q.length === 1 && q[0] === 27) {
q.length = 0
return { type: 'key', ch: '\x1b' }
}
const lone = q.shift()
if (lone !== undefined && lone < 0x20) {
return { type: 'ctrl', code: lone }
}
if (lone !== undefined) {
return { type: 'key', ch: String.fromCharCode(lone) }
}
return { type: 'eof' }
}
q.push(b)
continue
}
const ev = bareEditTryConsumeKey(q)
if (ev) return ev
const b = await reader.nextByte()
if (b === undefined) {
const eofPaste = bareEditFinalizeBracketedPasteOnEof(q)
if (eofPaste !== undefined) {
return { type: 'paste', text: eofPaste }
}
if (!q.length) return { type: 'eof' }
if (q.length === 1 && q[0] === 27) {
q.length = 0
@@ -986,6 +1144,32 @@ async function bareOsRunEditTui(ctx, opts) {
let promptTitle = ''
let lastSearch = ''
const envEditEarly =
ctx.env && typeof ctx.env === 'object'
? /** @type {Record<string, string>} */ (ctx.env)
: {}
let maxPromptLen = 4096
const maxPromptRaw = envEditEarly.BARE_EDIT_MAX_PROMPT
if (maxPromptRaw != null && String(maxPromptRaw) !== '') {
const n = parseInt(String(maxPromptRaw), 10)
if (Number.isFinite(n) && n > 0) maxPromptLen = Math.min(n, 65536)
}
/**
* @param {string} text
* @param {'saveas'|'search'|'goto'} kind
*/
function promptLineFromPaste(text, kind) {
let line = String(text).split(/\r?\n/)[0] ?? ''
line = line.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, '')
if (line.length > maxPromptLen) line = line.slice(0, maxPromptLen)
if (kind === 'goto') {
const m = /^(\s*\d+)/.exec(line)
return m ? m[1].trim() : ''
}
return line
}
function termDims() {
const env =
ctx.env && typeof ctx.env === 'object'
@@ -1117,6 +1301,8 @@ async function bareOsRunEditTui(ctx, opts) {
let suspended = false
/** When true, alternate-screen mode was entered (?1049h); {@link bareEditWrite} must send ?1049l on exit. */
let useAltScreen = false
/** When true, bracketed paste mode was enabled (?2004h); must send ?2004l on exit. */
let useBracketPaste = false
try {
if (typeof ctx.suspendReplForSubprocess === 'function') {
ctx.suspendReplForSubprocess()
@@ -1125,18 +1311,22 @@ async function bareOsRunEditTui(ctx, opts) {
if (typeof stdin.setRawMode === 'function') stdin.setRawMode(true)
if (typeof stdin.resume === 'function') stdin.resume()
const envEdit =
ctx.env && typeof ctx.env === 'object'
? /** @type {Record<string, string>} */ (ctx.env)
: {}
const bareEditNoAltScreen =
envEdit.BARE_EDIT_NO_ALTSCREEN != null &&
String(envEdit.BARE_EDIT_NO_ALTSCREEN) !== ''
envEditEarly.BARE_EDIT_NO_ALTSCREEN != null &&
String(envEditEarly.BARE_EDIT_NO_ALTSCREEN) !== ''
if (!bareEditNoAltScreen) {
bareEditWrite(ctx, stdout, '\x1b[?1049h')
useAltScreen = true
}
const bareEditNoBracketPaste =
envEditEarly.BARE_EDIT_NO_BRACKETED_PASTE != null &&
String(envEditEarly.BARE_EDIT_NO_BRACKETED_PASTE) !== ''
if (!bareEditNoBracketPaste) {
bareEditWrite(ctx, stdout, '\x1b[?2004h')
useBracketPaste = true
}
draw()
for (;;) {
@@ -1176,6 +1366,17 @@ async function bareOsRunEditTui(ctx, opts) {
draw()
continue
}
if (ev.type === 'paste') {
if (mode === 'prompt_saveas') {
promptBuf = promptLineFromPaste(ev.text || '', 'saveas')
} else if (mode === 'prompt_search') {
promptBuf = promptLineFromPaste(ev.text || '', 'search')
} else if (mode === 'prompt_goto') {
promptBuf = promptLineFromPaste(ev.text || '', 'goto')
}
draw()
continue
}
if (ev.type === 'key' && ev.ch === '\n') {
if (mode === 'prompt_search') {
lastSearch = promptBuf
@@ -1217,13 +1418,19 @@ async function bareOsRunEditTui(ctx, opts) {
continue
}
if (ev.type === 'key' && ev.ch && ev.ch !== '\n' && ev.ch !== '\t') {
if (ev.ch >= ' ') promptBuf += ev.ch
if (ev.ch >= ' ' && promptBuf.length < maxPromptLen) promptBuf += ev.ch
draw()
continue
}
continue
}
if (ev.type === 'paste') {
bareEditInsertPasteText(buf, ev.text || '')
draw()
continue
}
if (ev.type === 'nav') {
if (ev.key === 'pageup') {
const { rows } = termDims()
@@ -1320,6 +1527,9 @@ async function bareOsRunEditTui(ctx, opts) {
}
} finally {
try {
if (useBracketPaste) {
bareEditWrite(ctx, stdout, '\x1b[?2004l')
}
if (useAltScreen) {
bareEditWrite(ctx, stdout, '\x1b[?1049l')
} else {