@@ -625,10 +625,12 @@ export class GhosttyTerminalSurface {
625625 private theme : GhosttyTheme ;
626626 private readonly suppressedKeyCodes = new Set < string > ( ) ;
627627 private pasteShortcutToken = 0 ;
628- /** Whether the paste shortcut is still held, so its native paste can still follow. */
629- private pasteShortcutHeld = false ;
630- /** Text the held paste shortcut's clipboard read already sent. */
631- private shortcutPasteReadText : string | null = null ;
628+ // The paste shortcut gesture whose native paste may still arrive, and the
629+ // text its clipboard read already sent (empty until then; onPaste ignores
630+ // empty data). The browser may deliver that paste after the read and after
631+ // other keys move, but before the shortcut key's own keyup, so only that
632+ // keyup, a new shortcut, or blur ends the gesture.
633+ private pasteShortcutGesture : { code : string ; readText : string } | null = null ;
632634 private copyShortcutToken = 0 ;
633635 private clearSelectionAfterCopy = false ;
634636 private primedCopySelection = "" ;
@@ -1057,8 +1059,6 @@ export class GhosttyTerminalSurface {
10571059 }
10581060
10591061 private readonly onKeyDown = ( event : KeyboardEvent ) => {
1060- this . pasteShortcutHeld = false ;
1061- this . shortcutPasteReadText = null ;
10621062 // Presses handled outside the terminal must also swallow their release:
10631063 // beforeKey runs side effects (keybindings, navigation sends), so it cannot
10641064 // be consulted again on keyup, and Kitty report-event-types sessions would
@@ -1124,23 +1124,22 @@ export class GhosttyTerminalSurface {
11241124 }
11251125 if ( isTerminalPasteShortcut ( event ) ) {
11261126 this . suppressedKeyCodes . add ( event . code ) ;
1127- this . pasteShortcutHeld = true ;
1127+ const gesture = { code : event . code , readText : "" } ;
1128+ this . pasteShortcutGesture = gesture ;
11281129 const clipboard = navigator . clipboard ;
11291130 if ( typeof clipboard ?. readText === "function" ) {
11301131 // Race the async clipboard read against the browser's own paste event;
11311132 // the read covers browsers whose paste shortcut produces no paste
11321133 // event. Not preventing the default keeps the native path alive when
1133- // the read is denied. Either side can land first: a native paste
1134- // claims the token, and a read delivered while the shortcut is held
1135- // records its text so the native paste that follows it is dropped in
1136- // onPaste. The keyboard paste fires before keyup, which ends the window.
1134+ // the read is denied. A native paste that lands first claims the
1135+ // token; a read that lands first records its text on the gesture.
11371136 const token = ++ this . pasteShortcutToken ;
11381137 void clipboard . readText ( ) . then (
11391138 ( text ) => {
11401139 if ( this . disposed || this . pasteShortcutToken !== token ) return ;
11411140 this . pasteShortcutToken += 1 ;
11421141 if ( text . length === 0 ) return ;
1143- if ( this . pasteShortcutHeld ) this . shortcutPasteReadText = text ;
1142+ if ( this . pasteShortcutGesture === gesture ) gesture . readText = text ;
11441143 this . options . onData ( this . core . encodePaste ( text ) ) ;
11451144 } ,
11461145 ( ) => {
@@ -1166,10 +1165,7 @@ export class GhosttyTerminalSurface {
11661165 } ;
11671166
11681167 private readonly onKeyUp = ( event : KeyboardEvent ) => {
1169- // Any release ends the paste gesture: macOS never delivers V's keyup while
1170- // Cmd is held, so the Cmd release is often the only one.
1171- this . pasteShortcutHeld = false ;
1172- this . shortcutPasteReadText = null ;
1168+ if ( event . code === this . pasteShortcutGesture ?. code ) this . pasteShortcutGesture = null ;
11731169 if ( this . suppressedKeyCodes . delete ( event . code ) ) return ;
11741170 if ( isTerminalCompositionKey ( event , this . composing ) ) {
11751171 return ;
@@ -1191,6 +1187,7 @@ export class GhosttyTerminalSurface {
11911187
11921188 private readonly onBlur = ( ) => {
11931189 this . focused = false ;
1190+ this . pasteShortcutGesture = null ;
11941191 this . refreshHoveredLink ( ) ;
11951192 // Suppressions survive blur deliberately: a shortcut that moves focus (for
11961193 // example terminal-toggle) must still swallow its own keyup if focus comes
@@ -1250,8 +1247,8 @@ export class GhosttyTerminalSurface {
12501247 event . preventDefault ( ) ;
12511248 const data = event . clipboardData ?. getData ( "text/plain" ) ?? "" ;
12521249 if ( data . length === 0 ) return ;
1253- if ( data === this . shortcutPasteReadText ) {
1254- this . shortcutPasteReadText = null ;
1250+ if ( data === this . pasteShortcutGesture ?. readText ) {
1251+ this . pasteShortcutGesture = null ;
12551252 return ;
12561253 }
12571254 // The native paste won the race with actual text; a pending clipboard read
0 commit comments