@@ -665,12 +665,6 @@ export type AppShell = {
665665 * ./prompt-kill-ring.js).
666666 */
667667 promptKillRing : KillRing
668- /** `Date.now()` of the last keypress; detects an un-bracketed paste burst (see `PASTE_BURST_MS`). */
669- lastKeyAt : number
670- /** Whether that last keypress inserted a plain character (see `isPrintableInsertKey`). */
671- lastKeyWasPrintable : boolean
672- /** A converted CR is about to be followed by its CRLF partner LF; swallow that LF. */
673- suppressNextLinefeed : boolean
674668 /** Images attached with Ctrl+P, sent with the next prompt submit. */
675669 pendingAttachments : PendingImageAttachment [ ]
676670 /** Up/Down recall of messages already sent in this session. */
@@ -703,7 +697,13 @@ export type PrimaryOverlayKind =
703697
704698// Human keystrokes land tens of milliseconds apart at the fastest; a paste
705699// replayed onto stdin without bracketed-paste framing lands effectively all
706- // at once. Anything under this gap between keypresses is paste, not typing.
700+ // at once. 15ms is an empirical guess at a gap comfortably under normal
701+ // typing and comfortably over a replayed paste, not a measured figure --
702+ // too high false-positives on a very fast typist's real Enter (read as
703+ // paste, so it inserts a newline instead of sending); too low misses a
704+ // slow paste replay (read as typing, so a bare CR mid-paste still
705+ // submits). Only matters before this terminal's first real paste event;
706+ // see `sawBracketedPaste` below.
707707const PASTE_BURST_MS = 15
708708
709709/** A single unmodified character, as opposed to a control chord or named key. */
@@ -4542,6 +4542,21 @@ export function createAppShell(
45424542 session = enqueue ( session , `seed-${ i + 1 } ` )
45434543 }
45444544
4545+ // A real bracketed-paste event proves this terminal negotiates DEC 2004:
4546+ // every paste from here on arrives as one `paste` event, never as raw
4547+ // keystrokes, so the CRLF-submit fallback below has nothing left to guard
4548+ // against and turns itself off for the rest of the session. Terminals that
4549+ // never send one keep the guard, since they've never shown they can do
4550+ // better. Un-bracketed-paste bookkeeping only this key handler reads, so it
4551+ // lives in this closure rather than on the shared AppShell.
4552+ let sawBracketedPaste = false
4553+ let lastKeyAt = 0
4554+ let lastKeyWasPrintable = false
4555+ let suppressNextLinefeed = false
4556+ const onPaste = ( ) : void => {
4557+ sawBracketedPaste = true
4558+ }
4559+
45454560 const onKey = ( key : KeyEvent ) : void => {
45464561 if ( disposed ) return
45474562
@@ -4690,43 +4705,45 @@ export function createAppShell(
46904705 // Ctrl+Y/Alt+Y need somewhere to yank it back from.
46914706 const keyName = typeof key . name === "string" ? key . name . toLowerCase ( ) : ""
46924707
4693- // The LF half of a CRLF pair the block below just turned into a newline:
4694- // without this, "line one\r\nline two" would insert two newlines, one for
4695- // the converted CR and one for the LF arriving right behind it.
4696- const suppressLinefeed = shell . suppressNextLinefeed
4697- shell . suppressNextLinefeed = false
4698- if ( suppressLinefeed && keyName === "linefeed" && ! key . ctrl && ! key . meta && ! key . option ) {
4699- key . preventDefault ( )
4700- return
4701- }
4708+ // Everything below this line is the un-bracketed-paste fallback, and a
4709+ // terminal that has ever fired a real `paste` event has proven it never
4710+ // needs it: every future paste arrives as one `paste` event, not raw
4711+ // keystrokes, so re-running these checks on it would only risk a false
4712+ // positive for no benefit.
4713+ if ( ! sawBracketedPaste ) {
4714+ // The LF half of a CRLF pair the block below just turned into a
4715+ // newline: without this, "line one\r\nline two" would insert two
4716+ // newlines, one for the converted CR and one for the LF right behind it.
4717+ const suppressLinefeed = suppressNextLinefeed
4718+ suppressNextLinefeed = false
4719+ if ( suppressLinefeed && keyName === "linefeed" && ! key . ctrl && ! key . meta && ! key . option ) {
4720+ key . preventDefault ( )
4721+ return
4722+ }
47024723
4703- // A terminal that never negotiated bracketed paste (DEC 2004) hands a
4704- // multi-line paste to us as ordinary keystrokes, CR and all -- and a bare
4705- // CR is the same "return" that submits. Left alone, pasting three lines
4706- // sends three separate messages instead of composing one. Bracketed paste
4707- // delivers the whole blob as one `paste` event and never reaches here, so
4708- // this only fires on the raw-keystroke fallback.
4709- //
4710- // Detecting it needs two signals, not one: a lone fast Enter can happen
4711- // (key rollover, a scripted "send keys"), and a lone printable character
4712- // right before Enter is just typing. What never happens from a human is a
4713- // printable character landing, then Enter, both inside a keystroke burst
4714- // — that shape is unique to a paste being replayed byte-for-byte. Gating
4715- // on both keeps a deliberate Ctrl+J-then-Enter (newline, then send) safe,
4716- // since Ctrl+J is not "a printable character," while still catching
4717- // "...end of line one<CR><LF>line two..." arriving as raw keystrokes.
4718- const now = Date . now ( )
4719- const sincePreviousKey = now - shell . lastKeyAt
4720- const previousKeyWasPrintable = shell . lastKeyWasPrintable
4721- shell . lastKeyAt = now
4722- shell . lastKeyWasPrintable = isPrintableInsertKey ( key )
4723- const isBareReturn =
4724- ! key . ctrl && ! key . meta && ! key . option && ( keyName === "return" || keyName === "kpenter" )
4725- if ( isBareReturn && previousKeyWasPrintable && sincePreviousKey < PASTE_BURST_MS ) {
4726- key . preventDefault ( )
4727- shell . prompt . insertText ( "\n" )
4728- shell . suppressNextLinefeed = true
4729- return
4724+ // A bare CR is the same "return" that submits. Left alone, pasting
4725+ // three lines here sends three separate messages instead of composing
4726+ // one. Detecting it needs two signals, not one: a lone fast Enter can
4727+ // happen (key rollover, a scripted "send keys"), and a lone printable
4728+ // character right before Enter is just typing. What never happens from
4729+ // a human is a printable character landing, then Enter, both inside a
4730+ // keystroke burst -- that shape is unique to a paste being replayed
4731+ // byte-for-byte. Gating on both keeps a deliberate Ctrl+J-then-Enter
4732+ // (newline, then send) safe, since Ctrl+J is not "a printable
4733+ // character," while still catching "...line one<CR><LF>line two...".
4734+ const now = Date . now ( )
4735+ const sincePreviousKey = now - lastKeyAt
4736+ const previousKeyWasPrintable = lastKeyWasPrintable
4737+ lastKeyAt = now
4738+ lastKeyWasPrintable = isPrintableInsertKey ( key )
4739+ const isBareReturn =
4740+ ! key . ctrl && ! key . meta && ! key . option && ( keyName === "return" || keyName === "kpenter" )
4741+ if ( isBareReturn && previousKeyWasPrintable && sincePreviousKey < PASTE_BURST_MS ) {
4742+ key . preventDefault ( )
4743+ shell . prompt . insertText ( "\n" )
4744+ suppressNextLinefeed = true
4745+ return
4746+ }
47304747 }
47314748
47324749 const isCtrlKillYank =
@@ -4989,6 +5006,7 @@ export function createAppShell(
49895006
49905007 if ( wireKeys ) {
49915008 renderer . keyInput . on ( "keypress" , onKey )
5009+ renderer . keyInput . on ( "paste" , onPaste )
49925010 prompt . onSubmit = onEnter
49935011 }
49945012 renderer . on ( CliRenderEvents . FRAME , onFrame )
@@ -5047,9 +5065,6 @@ export function createAppShell(
50475065 parentStreamLog : null ,
50485066 parentStreamLogBase : null ,
50495067 promptKillRing : emptyKillRing ,
5050- lastKeyAt : 0 ,
5051- lastKeyWasPrintable : false ,
5052- suppressNextLinefeed : false ,
50535068 pendingAttachments : [ ] ,
50545069 sentHistory : createSentHistoryBrowse ( [ ] ) ,
50555070 disposed : false ,
@@ -5059,6 +5074,7 @@ export function createAppShell(
50595074 shell . disposed = true
50605075 if ( wireKeys ) {
50615076 renderer . keyInput . off ( "keypress" , onKey )
5077+ renderer . keyInput . off ( "paste" , onPaste )
50625078 prompt . onSubmit = undefined
50635079 }
50645080 renderer . off ( CliRenderEvents . FRAME , onFrame )
0 commit comments