|
440 | 440 | const createClipboardAndContextController = ({term, sendInput}) => { |
441 | 441 | const isMacPlatform = /mac/i.test(navigator.userAgent); |
442 | 442 |
|
| 443 | + const writeClipboardText = (text, source) => { |
| 444 | + if (typeof text !== 'string' || text.length === 0) { |
| 445 | + return; |
| 446 | + } |
| 447 | + if (!navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') { |
| 448 | + logWarn('Clipboard write API is unavailable.', `source=${source}`); |
| 449 | + return; |
| 450 | + } |
| 451 | + |
| 452 | + navigator.clipboard.writeText(text).catch(error => { |
| 453 | + logWarn('Failed to write text to clipboard.', { |
| 454 | + source, |
| 455 | + error: stringifyLogDetails(error), |
| 456 | + }); |
| 457 | + }); |
| 458 | + }; |
| 459 | + |
| 460 | + const registerOsc52ClipboardHandler = () => { |
| 461 | + const parser = term.parser; |
| 462 | + if (!parser || typeof parser.registerOscHandler !== 'function') { |
| 463 | + logWarn('OSC 52 clipboard passthrough is unavailable.'); |
| 464 | + return undefined; |
| 465 | + } |
| 466 | + |
| 467 | + return parser.registerOscHandler(52, data => { |
| 468 | + const parts = String(data).split(';'); |
| 469 | + if (parts.length < 2) { |
| 470 | + return true; |
| 471 | + } |
| 472 | + |
| 473 | + const base64 = parts.slice(1).join(';').trim(); |
| 474 | + if (!base64 || base64 === '?') { |
| 475 | + return true; |
| 476 | + } |
| 477 | + |
| 478 | + try { |
| 479 | + const binary = atob(base64); |
| 480 | + const bytes = new Uint8Array(binary.length); |
| 481 | + for (let index = 0; index < binary.length; index += 1) { |
| 482 | + bytes[index] = binary.charCodeAt(index); |
| 483 | + } |
| 484 | + const text = new TextDecoder('utf-8', {fatal: false}).decode(bytes); |
| 485 | + writeClipboardText(text, 'osc52'); |
| 486 | + } catch (error) { |
| 487 | + logWarn('Failed to decode OSC 52 clipboard payload.', error); |
| 488 | + } |
| 489 | + |
| 490 | + return true; |
| 491 | + }); |
| 492 | + }; |
| 493 | + |
443 | 494 | const shouldUseCtrlSelectionCopy = event => { |
444 | 495 | if ( |
445 | 496 | isMacPlatform || |
|
470 | 521 | if (shouldUseCtrlSelectionCopy(event)) { |
471 | 522 | const selection = term.getSelection(); |
472 | 523 | if (selection) { |
473 | | - navigator.clipboard.writeText(selection).catch(() => { |
474 | | - // Ignore clipboard write failures. |
475 | | - }); |
| 524 | + writeClipboardText(selection, 'selection-keyboard'); |
476 | 525 | } |
477 | 526 | return false; |
478 | 527 | } |
|
483 | 532 | event.preventDefault(); |
484 | 533 | const selection = term.getSelection(); |
485 | 534 | if (selection) { |
486 | | - navigator.clipboard.writeText(selection).catch(() => { |
487 | | - // Ignore clipboard write failures. |
488 | | - }); |
| 535 | + writeClipboardText(selection, 'selection-context-menu'); |
489 | 536 | term.clearSelection(); |
490 | 537 | return; |
491 | 538 | } |
|
503 | 550 | return { |
504 | 551 | allowTerminalKeyEvent, |
505 | 552 | handleContextMenu, |
| 553 | + registerOsc52ClipboardHandler, |
506 | 554 | }; |
507 | 555 | }; |
508 | 556 |
|
|
1714 | 1762 | addManagedListener(container, 'pointerdown', unlockTerminalAudio); |
1715 | 1763 | addManagedListener(container, 'keydown', unlockTerminalAudio); |
1716 | 1764 |
|
1717 | | - const {allowTerminalKeyEvent, handleContextMenu} = |
1718 | | - createClipboardAndContextController({term, sendInput}); |
| 1765 | + const { |
| 1766 | + allowTerminalKeyEvent, |
| 1767 | + handleContextMenu, |
| 1768 | + registerOsc52ClipboardHandler, |
| 1769 | + } = createClipboardAndContextController({term, sendInput}); |
| 1770 | + registerDisposable(registerOsc52ClipboardHandler()); |
1719 | 1771 |
|
1720 | 1772 | // On macOS, Ctrl+V passes through to CLI which handles paste (including images). |
1721 | 1773 | // On Windows/Linux, Ctrl+V must be intercepted to suppress the raw \x16 that |
|
0 commit comments