Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions frontend/src/features/terminal/components/TerminalModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
import { X, Terminal as TermIcon, BookOpen } from 'lucide-react';
import '@xterm/xterm/css/xterm.css';
import LinuxCheatSheet from './LinuxCheatSheet';
import { shouldProcessKeyInTerminal } from '../terminalKeyboard';
import { API_BASE } from '../../../shared/types';

interface TerminalModalProps {
Expand Down Expand Up @@ -42,9 +43,12 @@ export default function TerminalModal({ containerId, projectId, nodeName, onClos
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);

term.attachCustomKeyEventHandler(shouldProcessKeyInTerminal);

if (terminalRef.current) {
term.open(terminalRef.current);
fitAddon.fit();
term.focus();
}

socket.emit('join-terminal', { containerId, projectId });
Expand Down
26 changes: 26 additions & 0 deletions frontend/src/features/terminal/terminalKeyboard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { describe, expect, it } from 'vitest';
import { shouldProcessKeyInTerminal } from './terminalKeyboard';

function keydown(key: string, modifiers: KeyboardEventInit = {}): KeyboardEvent {
return new KeyboardEvent('keydown', { key, ...modifiers });
}

describe('shouldProcessKeyInTerminal', () => {
it.each([
['Ctrl+V', keydown('v', { ctrlKey: true })],
['Ctrl+Shift+V', keydown('V', { ctrlKey: true, shiftKey: true })],
['Cmd+V', keydown('v', { metaKey: true })],
['Shift+Insert', keydown('Insert', { shiftKey: true })],
])('leaves %s to the browser clipboard handler', (_shortcut, event) => {
expect(shouldProcessKeyInTerminal(event)).toBe(false);
});

it.each([
['ordinary input', keydown('v')],
['terminal interrupt', keydown('c', { ctrlKey: true })],
['Alt-modified input', keydown('v', { ctrlKey: true, altKey: true })],
['paste shortcut keyup', new KeyboardEvent('keyup', { key: 'v', ctrlKey: true })],
])('keeps processing %s in xterm', (_case, event) => {
expect(shouldProcessKeyInTerminal(event)).toBe(true);
});
});
19 changes: 19 additions & 0 deletions frontend/src/features/terminal/terminalKeyboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Returns whether xterm should process a key event itself.
*
* Browser-owned paste shortcuts must bypass xterm's key mapping so the browser
* can dispatch a `paste` event with clipboard data to xterm's hidden textarea.
* xterm already turns that event into terminal input, including newline and
* bracketed-paste handling.
*/
export function shouldProcessKeyInTerminal(event: KeyboardEvent): boolean {
if (event.type !== 'keydown' || event.altKey) {
return true;
}

const modifierPaste =
(event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'v';
const insertPaste = event.shiftKey && event.key === 'Insert';

return !modifierPaste && !insertPaste;
}
Loading