Add setting to support OSC 52 paste (#15336)

This commit is contained in:
Tommaso Sciortino
2026-01-05 16:11:50 -08:00
committed by GitHub
parent 2cb33b2f76
commit 384fb6a465
7 changed files with 216 additions and 10 deletions

View File

@@ -320,6 +320,111 @@ describe('KeypressContext', () => {
}),
);
});
it('should parse valid OSC 52 response', async () => {
const keyHandler = vi.fn();
const { result } = renderHook(() => useKeypressContext(), { wrapper });
act(() => result.current.subscribe(keyHandler));
const base64Data = Buffer.from('Hello OSC 52').toString('base64');
const sequence = `\x1b]52;c;${base64Data}\x07`;
act(() => stdin.write(sequence));
await waitFor(() => {
expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining({
name: 'paste',
paste: true,
sequence: 'Hello OSC 52',
}),
);
});
});
it('should handle split OSC 52 response', async () => {
const keyHandler = vi.fn();
const { result } = renderHook(() => useKeypressContext(), { wrapper });
act(() => result.current.subscribe(keyHandler));
const base64Data = Buffer.from('Split Paste').toString('base64');
const sequence = `\x1b]52;c;${base64Data}\x07`;
// Split the sequence
const part1 = sequence.slice(0, 5);
const part2 = sequence.slice(5);
act(() => stdin.write(part1));
act(() => stdin.write(part2));
await waitFor(() => {
expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining({
name: 'paste',
paste: true,
sequence: 'Split Paste',
}),
);
});
});
it('should handle OSC 52 response terminated by ESC \\', async () => {
const keyHandler = vi.fn();
const { result } = renderHook(() => useKeypressContext(), { wrapper });
act(() => result.current.subscribe(keyHandler));
const base64Data = Buffer.from('Terminated by ST').toString('base64');
const sequence = `\x1b]52;c;${base64Data}\x1b\\`;
act(() => stdin.write(sequence));
await waitFor(() => {
expect(keyHandler).toHaveBeenCalledWith(
expect.objectContaining({
name: 'paste',
paste: true,
sequence: 'Terminated by ST',
}),
);
});
});
it('should ignore unknown OSC sequences', async () => {
const keyHandler = vi.fn();
const { result } = renderHook(() => useKeypressContext(), { wrapper });
act(() => result.current.subscribe(keyHandler));
const sequence = `\x1b]1337;File=name=Zm9vCg==\x07`;
act(() => stdin.write(sequence));
await act(async () => {
vi.advanceTimersByTime(0);
});
expect(keyHandler).not.toHaveBeenCalled();
});
it('should ignore invalid OSC 52 format', async () => {
const keyHandler = vi.fn();
const { result } = renderHook(() => useKeypressContext(), { wrapper });
act(() => result.current.subscribe(keyHandler));
const sequence = `\x1b]52;x;notbase64\x07`;
act(() => stdin.write(sequence));
await act(async () => {
vi.advanceTimersByTime(0);
});
expect(keyHandler).not.toHaveBeenCalled();
});
});
describe('debug keystroke logging', () => {

View File

@@ -317,12 +317,56 @@ function* emitKeys(
}
}
if (escaped && (ch === 'O' || ch === '[')) {
if (escaped && (ch === 'O' || ch === '[' || ch === ']')) {
// ANSI escape sequence
code = ch;
let modifier = 0;
if (ch === 'O') {
if (ch === ']') {
// OSC sequence
// ESC ] <params> ; <data> BEL
// ESC ] <params> ; <data> ESC \
let buffer = '';
// Read until BEL, `ESC \`, or timeout (empty string)
while (true) {
const next = yield;
if (next === '' || next === '\u0007') {
break;
} else if (next === ESC) {
const afterEsc = yield;
if (afterEsc === '' || afterEsc === '\\') {
break;
}
buffer += next + afterEsc;
continue;
}
buffer += next;
}
// Check for OSC 52 (Clipboard) response
// Format: 52;c;<base64> or 52;p;<base64>
const match = /^52;[cp];(.*)$/.exec(buffer);
if (match) {
try {
const base64Data = match[1];
const decoded = Buffer.from(base64Data, 'base64').toString('utf-8');
keypressHandler({
name: 'paste',
ctrl: false,
meta: false,
shift: false,
paste: true,
insertable: true,
sequence: decoded,
});
} catch (_e) {
debugLogger.log('Failed to decode OSC 52 clipboard data');
}
}
continue; // resume main loop
} else if (ch === 'O') {
// ESC O letter
// ESC O modifier letter
ch = yield;