diff --git a/src/client/wetty/term/confiruragtion.spec.ts b/src/client/wetty/term/confiruragtion.spec.ts new file mode 100644 index 000000000..8e0789b6a --- /dev/null +++ b/src/client/wetty/term/confiruragtion.spec.ts @@ -0,0 +1,107 @@ +import { expect } from 'chai'; +import 'mocha'; +import * as sinon from 'sinon'; +import { modifierHandler } from './confiruragtion'; + +describe('modifierHandler', () => { + let inputStub: sinon.SinonStub; + + beforeEach(() => { + inputStub = sinon.stub(); + (window as any).wetty_term = { + input: inputStub + }; + }); + + afterEach(() => { + delete (window as any).wetty_term; + sinon.restore(); + }); + + it('should allow normal Enter to pass through', () => { + const event = { + type: 'keydown', + key: 'Enter', + shiftKey: false, + altKey: false, + ctrlKey: false, + metaKey: false + } as KeyboardEvent; + + const result = modifierHandler(event); + expect(result).to.be.true; + expect(inputStub.called).to.be.false; + }); + + it('should intercept Shift+Enter and send CSI u sequence', () => { + const event = { + type: 'keydown', + key: 'Enter', + shiftKey: true, + altKey: false, + ctrlKey: false, + metaKey: false + } as KeyboardEvent; + + const result = modifierHandler(event); + expect(result).to.be.false; + expect(inputStub.calledOnceWith('\x1b[13;2u', false)).to.be.true; + }); + + it('should intercept Ctrl+Tab and send CSI u sequence', () => { + const event = { + type: 'keydown', + key: 'Tab', + shiftKey: false, + altKey: false, + ctrlKey: true, + metaKey: false + } as KeyboardEvent; + + const result = modifierHandler(event); + expect(result).to.be.false; + expect(inputStub.calledOnceWith('\x1b[9;5u', false)).to.be.true; + }); + + it('should intercept Ctrl+Shift+Backspace and send CSI u sequence', () => { + const event = { + type: 'keydown', + key: 'Backspace', + shiftKey: true, + altKey: false, + ctrlKey: true, + metaKey: false + } as KeyboardEvent; + + const result = modifierHandler(event); + expect(result).to.be.false; + expect(inputStub.calledOnceWith('\x1b[127;6u', false)).to.be.true; + }); + + it('should ignore non-keydown events', () => { + const event = { + type: 'keyup', + key: 'Enter', + shiftKey: true + } as KeyboardEvent; + + const result = modifierHandler(event); + expect(result).to.be.true; + expect(inputStub.called).to.be.false; + }); + + it('should ignore modified keys that are not in the special list', () => { + const event = { + type: 'keydown', + key: 'a', + shiftKey: true, + altKey: false, + ctrlKey: false, + metaKey: false + } as KeyboardEvent; + + const result = modifierHandler(event); + expect(result).to.be.true; + expect(inputStub.called).to.be.false; + }); +}); diff --git a/src/client/wetty/term/confiruragtion.ts b/src/client/wetty/term/confiruragtion.ts index 37b785aaf..d59aa8a09 100644 --- a/src/client/wetty/term/confiruragtion.ts +++ b/src/client/wetty/term/confiruragtion.ts @@ -5,6 +5,38 @@ import { loadOptions } from './load'; import type { Options } from './options'; import type { Term } from '../term'; +export function modifierHandler(e: KeyboardEvent): boolean { + // We only care about keydown events with modifiers + if (e.type !== 'keydown') return true; + + const modifiers = + (e.shiftKey ? 1 : 0) | + (e.altKey ? 2 : 0) | + (e.ctrlKey ? 4 : 0) | + (e.metaKey ? 8 : 0); + + // If no modifiers, let xterm handle it normally + if (modifiers === 0) return true; + + // Key codes for special keys we want to support generically + const specialKeys: Record = { + Enter: 13, + Tab: 9, + Backspace: 127, + Escape: 27, + }; + + if (specialKeys[e.key]) { + const code = specialKeys[e.key]; + const mod = modifiers + 1; // CSI u uses 1-based modifier mapping + // Send the CSI u sequence: ESC [ code ; mod u + window.wetty_term?.input(`\x1b[${code};${mod}u`, false); + return false; // Intercepted + } + + return true; +} + export function configureTerm(term: Term): void { const options = loadOptions(); try { @@ -42,14 +74,10 @@ export function configureTerm(term: Term): void { } editor.addEventListener('load', editorOnLoad); - toggle.addEventListener('click', e => { - editor?.contentWindow?.loadOptions(loadOptions()); - optionsElem.classList.toggle('opened'); - e.preventDefault(); + term.attachCustomKeyEventHandler(e => { + return copyShortcut(e) && modifierHandler(e); }); - term.attachCustomKeyEventHandler(copyShortcut); - document.addEventListener( 'mouseup', () => { @@ -57,4 +85,10 @@ export function configureTerm(term: Term): void { }, false, ); + + toggle.addEventListener('click', e => { + editor?.contentWindow?.loadOptions(loadOptions()); + optionsElem.classList.toggle('opened'); + e.preventDefault(); + }); }