Merge branch 'master' into package_upgrades

This commit is contained in:
jerch
2026-05-11 16:57:49 +02:00
committed by GitHub
7 changed files with 194 additions and 78 deletions
-3
View File
@@ -21,8 +21,5 @@
"package": "../../node_modules/.bin/webpack",
"prepublishOnly": "npm run package",
"start": "node ../../demo/start"
},
"dependencies": {
"js-base64": "^3.7.5"
}
}
+10 -18
View File
@@ -4,8 +4,7 @@
*/
import type { IDisposable, ITerminalAddon, Terminal } from '@xterm/xterm';
import { type IClipboardProvider, ClipboardSelectionType, type IBase64 } from '@xterm/addon-clipboard';
import { Base64 as JSBase64 } from 'js-base64';
import { type IClipboardProvider, type IBase64 } from '@xterm/addon-clipboard';
export class ClipboardAddon implements ITerminalAddon {
private _terminal?: Terminal;
@@ -25,7 +24,7 @@ export class ClipboardAddon implements ITerminalAddon {
return this._disposable?.dispose();
}
private _readText(sel: ClipboardSelectionType, data: string): void {
private _readText(sel: string, data: string): void {
const b64 = this._base64.encodeText(data);
this._terminal?.input(`\x1b]52;${sel};${b64}\x07`, false);
}
@@ -36,7 +35,7 @@ export class ClipboardAddon implements ITerminalAddon {
return true;
}
const pc = args[0] as ClipboardSelectionType;
const pc = args[0];
const pd = args[1];
if (pd === '?') {
const text = this._provider.readText(pc);
@@ -70,30 +69,23 @@ export class ClipboardAddon implements ITerminalAddon {
}
export class BrowserClipboardProvider implements IClipboardProvider {
public async readText(selection: ClipboardSelectionType): Promise<string> {
if (selection !== 'c') {
return Promise.resolve('');
}
public readText(selection: string): Promise<string> {
return navigator.clipboard.readText();
}
public async writeText(selection: ClipboardSelectionType, text: string): Promise<void> {
if (selection !== 'c') {
return Promise.resolve();
}
public writeText(selection: string, text: string): Promise<void> {
return navigator.clipboard.writeText(text);
}
}
export class Base64 implements IBase64 {
public encodeText(data: string): string {
return JSBase64.encode(data);
return btoa(data);
}
public decodeText(data: string): string {
const text = JSBase64.decode(data);
if (!JSBase64.isValid(data) || JSBase64.encode(text) !== data) {
return '';
}
return text;
try {
return atob(data);
} catch {}
return '';
}
}
@@ -20,7 +20,7 @@ test.describe('ClipboardAddon', () => {
test.beforeEach(async ({}, testInfo) => {
// DEBT: This test doesn't work since the migration to @playwright/test
if (ctx.browser.browserType().name() !== 'chromium') {
if (ctx.browser.browserType().name() === 'webkit') {
testInfo.skip();
return;
}
@@ -48,6 +48,14 @@ test.describe('ClipboardAddon', () => {
await ctx.proxy.write(`\x1b]52;c;${testDataEncoded}\x07`);
deepEqual(await ctx.page.evaluate(() => window.navigator.clipboard.readText()), testDataDecoded);
});
test('primary selection', async () => {
await ctx.proxy.write(`\x1b]52;p;${testDataEncoded}\x07`);
deepEqual(await ctx.page.evaluate(() => window.navigator.clipboard.readText()), testDataDecoded);
});
test('empty selection (default)', async () => {
await ctx.proxy.write(`\x1b]52;;${testDataEncoded}\x07`);
deepEqual(await ctx.page.evaluate(() => window.navigator.clipboard.readText()), testDataDecoded);
});
test('invalid base64 string', async () => {
await ctx.proxy.write(`\x1b]52;c;${testDataEncoded}invalid\x07`);
deepEqual(await ctx.page.evaluate(() => window.navigator.clipboard.readText()), '');
@@ -60,15 +68,24 @@ test.describe('ClipboardAddon', () => {
});
test.describe('read data', async function (): Promise<any> {
test('simple string', async () => {
test.beforeAll(async () => {
await ctx.page.evaluate(`
window.data = [];
window.term.onData(e => data.push(e));
`);
});
test('simple string', async () => {
await ctx.page.evaluate('window.data.length = 0;');
await ctx.page.evaluate(() => window.navigator.clipboard.writeText('hello world'));
await ctx.proxy.write(`\x1b]52;c;?\x07`);
deepEqual(await ctx.page.evaluate('window.data'), [`\x1b]52;c;${testDataEncoded}\x07`]);
});
test('primary selection', async () => {
await ctx.page.evaluate('window.data.length = 0;');
await ctx.page.evaluate(() => window.navigator.clipboard.writeText('hello world'));
await ctx.proxy.write(`\x1b]52;p;?\x07`);
deepEqual(await ctx.page.evaluate('window.data'), [`\x1b]52;p;${testDataEncoded}\x07`]);
});
test('clear clipboard', async () => {
await ctx.proxy.write(`\x1b]52;c;!\x07`);
await ctx.proxy.write(`\x1b]52;c;?\x07`);
+12 -21
View File
@@ -30,18 +30,6 @@ declare module '@xterm/addon-clipboard' {
public dispose(): void
}
/**
* Clipboard selection type. This is used to specify which selection buffer to
* read or write to.
* - SYSTEM `c`: The system clipboard.
* - PRIMARY `p`: The primary clipboard. This is provided for compatibility
* with Linux X11.
*/
export const enum ClipboardSelectionType {
SYSTEM = 'c',
PRIMARY = 'p',
}
export interface IBase64 {
/**
* Converts a utf-8 string to a base64 string.
@@ -78,36 +66,39 @@ declare module '@xterm/addon-clipboard' {
export interface IClipboardProvider {
/**
* Gets the clipboard content.
* @param selection The clipboard selection to read.
* @param selection The clipboard selection to read (see OSC 52 spec of xterm).
* @returns A promise that resolves with clipboard selection data.
*/
readText(selection: ClipboardSelectionType): string | Promise<string>;
readText(selection: string): string | Promise<string>;
/**
* Sets the clipboard content.
* @param selection The clipboard selection to set.
* @param selection The clipboard selection to set (see OSC 52 spec of xterm).
* @param data The clipboard text to write.
*/
writeText(selection: ClipboardSelectionType, text: string): void | Promise<void>;
writeText(selection: string, text: string): void | Promise<void>;
}
/**
* The clipboard provider interface that enables xterm.js to access the system clipboard.
* The clipboard provider interface that enables xterm.js to access the browser clipboard.
*
* Note this this clipboard provider does not implement own cut buffers as xterm does,
* but redirect the selection parameter always to navigator.clipboard.
*/
export class BrowserClipboardProvider implements IClipboardProvider{
/**
* Reads text from the clipboard.
* @param selection The selection type to read from.
* @param selection The selection type to read from (always refers to navigator.clipboard).
* @returns A promise that resolves with the text from the clipboard.
*/
public readText(selection: ClipboardSelectionType): Promise<string>;
public readText(selection: string): Promise<string>;
/**
* Writes text to the clipboard.
* @param selection The selection type to write to.
* @param selection The selection type to write to (always refers to navigator.clipboard).
* @param data The text to write to the clipboard.
* @returns A promise that resolves when the text has been written to the clipboard.
*/
public writeText(selection: ClipboardSelectionType, data: string): Promise<void>;
public writeText(selection: string, data: string): Promise<void>;
}
}
+15 -9
View File
@@ -65,10 +65,7 @@
"addons/addon-clipboard": {
"name": "@xterm/addon-clipboard",
"version": "0.2.0",
"license": "MIT",
"dependencies": {
"js-base64": "^3.7.5"
}
"license": "MIT"
},
"addons/addon-fit": {
"name": "@xterm/addon-fit",
@@ -5506,11 +5503,20 @@
"node": ">= 10.13.0"
}
},
"node_modules/js-base64": {
"version": "3.7.8",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz",
"integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==",
"license": "BSD-3-Clause"
"node_modules/jest-worker/node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
+113 -4
View File
@@ -453,9 +453,29 @@ describe('KittyKeyboard', () => {
describe('event types (press/repeat/release)', () => {
const flags = KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES | KittyKeyboardFlags.REPORT_EVENT_TYPES;
it('press event (default, no suffix)', () => {
it('UTF-8 text press event', () => {
const result = kitty.evaluate(createEvent({ key: 'a' }), flags, KittyKeyboardEventType.PRESS);
assert.strictEqual(result.key, '\x1b[97u');
assert.strictEqual(result.key, 'a');
});
it('Escape key press event (default, no suffix)', () => {
const result = kitty.evaluate(createEvent({ key: 'Escape' }), flags, KittyKeyboardEventType.PRESS);
assert.strictEqual(result.key, '\x1b[27u');
});
it('Enter key press event → legacy \\r', () => {
const result = kitty.evaluate(createEvent({ key: 'Enter' }), flags, KittyKeyboardEventType.PRESS);
assert.strictEqual(result.key, '\r');
});
it('Tab key press event → legacy \\t', () => {
const result = kitty.evaluate(createEvent({ key: 'Tab' }), flags, KittyKeyboardEventType.PRESS);
assert.strictEqual(result.key, '\t');
});
it('Backspace key press event → legacy \\x7f', () => {
const result = kitty.evaluate(createEvent({ key: 'Backspace' }), flags, KittyKeyboardEventType.PRESS);
assert.strictEqual(result.key, '\x7f');
});
it('press event explicit :1 when modifiers present', () => {
@@ -463,9 +483,29 @@ describe('KittyKeyboard', () => {
assert.strictEqual(result.key, '\x1b[97;5u');
});
it('repeat event → :2 suffix', () => {
it('UTF-8 text repeat event', () => {
const result = kitty.evaluate(createEvent({ key: 'a' }), flags, KittyKeyboardEventType.REPEAT);
assert.strictEqual(result.key, '\x1b[97;1:2u');
assert.strictEqual(result.key, 'a');
});
it('Escape key repeat event → :2 suffix', () => {
const result = kitty.evaluate(createEvent({ key: 'Escape' }), flags, KittyKeyboardEventType.REPEAT);
assert.strictEqual(result.key, '\x1b[27;1:2u');
});
it('Enter key repeat event → legacy \\r', () => {
const result = kitty.evaluate(createEvent({ key: 'Enter' }), flags, KittyKeyboardEventType.REPEAT);
assert.strictEqual(result.key, '\r');
});
it('Tab key repeat event → legacy \\t', () => {
const result = kitty.evaluate(createEvent({ key: 'Tab' }), flags, KittyKeyboardEventType.REPEAT);
assert.strictEqual(result.key, '\t');
});
it('Backspace key repeat event → legacy \\x7f', () => {
const result = kitty.evaluate(createEvent({ key: 'Backspace' }), flags, KittyKeyboardEventType.REPEAT);
assert.strictEqual(result.key, '\x7f');
});
it('release event → :3 suffix', () => {
@@ -473,6 +513,26 @@ describe('KittyKeyboard', () => {
assert.strictEqual(result.key, '\x1b[97;1:3u');
});
it('Escape key release event → :3 suffix', () => {
const result = kitty.evaluate(createEvent({ key: 'Escape' }), flags, KittyKeyboardEventType.RELEASE);
assert.strictEqual(result.key, '\x1b[27;1:3u');
});
it('Enter key release event is not reported', () => {
const result = kitty.evaluate(createEvent({ key: 'Enter' }), flags, KittyKeyboardEventType.RELEASE);
assert.strictEqual(result.key, undefined);
});
it('Tab key release event is not reported', () => {
const result = kitty.evaluate(createEvent({ key: 'Tab' }), flags, KittyKeyboardEventType.RELEASE);
assert.strictEqual(result.key, undefined);
});
it('Backspace key release event is not reported', () => {
const result = kitty.evaluate(createEvent({ key: 'Backspace' }), flags, KittyKeyboardEventType.RELEASE);
assert.strictEqual(result.key, undefined);
});
it('release with modifier → mod:3', () => {
const result = kitty.evaluate(createEvent({ key: 'a', ctrlKey: true }), flags, KittyKeyboardEventType.RELEASE);
assert.strictEqual(result.key, '\x1b[97;5:3u');
@@ -563,6 +623,55 @@ describe('KittyKeyboard', () => {
});
});
describe('REPORT_ALL_KEYS_AS_ESCAPE_CODES flag with REPORT_EVENT_TYPES', () => {
const flags = KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES | KittyKeyboardFlags.REPORT_EVENT_TYPES;
it('Enter key press event → CSI 13 u', () => {
const result = kitty.evaluate(createEvent({ key: 'Enter' }), flags, KittyKeyboardEventType.PRESS);
assert.strictEqual(result.key, '\x1b[13u');
});
it('Tab key press event → CSI 9 u', () => {
const result = kitty.evaluate(createEvent({ key: 'Tab' }), flags, KittyKeyboardEventType.PRESS);
assert.strictEqual(result.key, '\x1b[9u');
});
it('Backspace key press event → CSI 127 u', () => {
const result = kitty.evaluate(createEvent({ key: 'Backspace' }), flags, KittyKeyboardEventType.PRESS);
assert.strictEqual(result.key, '\x1b[127u');
});
it('Enter key repeat event → CSI 13;1:2 u', () => {
const result = kitty.evaluate(createEvent({ key: 'Enter' }), flags, KittyKeyboardEventType.REPEAT);
assert.strictEqual(result.key, '\x1b[13;1:2u');
});
it('Tab key repeat event → CSI 9;1:2 u', () => {
const result = kitty.evaluate(createEvent({ key: 'Tab' }), flags, KittyKeyboardEventType.REPEAT);
assert.strictEqual(result.key, '\x1b[9;1:2u');
});
it('Backspace key repeat event → CSI 127;1:2 u', () => {
const result = kitty.evaluate(createEvent({ key: 'Backspace' }), flags, KittyKeyboardEventType.REPEAT);
assert.strictEqual(result.key, '\x1b[127;1:2u');
});
it('Enter key release event → CSI 13;1:3 u', () => {
const result = kitty.evaluate(createEvent({ key: 'Enter' }), flags, KittyKeyboardEventType.RELEASE);
assert.strictEqual(result.key, '\x1b[13;1:3u');
});
it('Tab key release event → CSI 9;1:3 u', () => {
const result = kitty.evaluate(createEvent({ key: 'Tab' }), flags, KittyKeyboardEventType.RELEASE);
assert.strictEqual(result.key, '\x1b[9;1:3u');
});
it('Backspace key release event → CSI 127;1:3 u', () => {
const result = kitty.evaluate(createEvent({ key: 'Backspace' }), flags, KittyKeyboardEventType.RELEASE);
assert.strictEqual(result.key, '\x1b[127;1:3u');
});
});
describe('REPORT_ASSOCIATED_TEXT flag', () => {
const flags = KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES | KittyKeyboardFlags.REPORT_ASSOCIATED_TEXT;
+25 -21
View File
@@ -471,29 +471,33 @@ export class KittyKeyboard {
return result;
}
// Special handling for Enter/Tab/Backspace.
const specialKey = keyCode === 13 || keyCode === 9 || keyCode === 127;
// Per spec, Enter/Tab/Backspace will not have release events unless "Report all keys as escape
// codes" is also set.
if (specialKey && eventType === KittyKeyboardEventType.RELEASE && !(flags & KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES)) {
return result;
}
const isFunc = this._functionalKeyCodes[ev.key] !== undefined || this._getNumpadKeyCode(ev) !== undefined;
let useCsiU = false;
if (flags & KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES) {
useCsiU = true;
} else if (reportEventTypes) {
useCsiU = true;
} else if (flags & KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES) {
// Per spec, Enter/Tab/Backspace "still generate the same bytes as in legacy
// mode" and consider space to be a text-generating key, so these skip the isFunc fast-path
// and only get CSI u when modifiers are present (handled below).
const isDisambiguateLegacy = keyCode === 13 || keyCode === 9 || keyCode === 127;
if (isFunc && !isDisambiguateLegacy) {
useCsiU = true;
} else if (modifiers > 0) {
if (ev.shiftKey && !ev.ctrlKey && !ev.altKey && !ev.metaKey && ev.key.length === 1) {
useCsiU = false;
} else {
useCsiU = true;
}
}
}
const useCsiU = !!(
flags & KittyKeyboardFlags.REPORT_ALL_KEYS_AS_ESCAPE_CODES ||
(reportEventTypes && eventType === KittyKeyboardEventType.RELEASE) ||
(flags & KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES &&
(
// Per spec, Enter/Tab/Backspace "still generate the same bytes as in legacy mode" and
// consider space to be a text-generating key, so these skip the isFunc fast-path and only
// get CSI u when modifiers are present (handled below).
(isFunc && !specialKey) ||
(
(modifiers > 0 && ev.key.length !== 1) ||
modifiers - 1 > KittyKeyboardModifiers.SHIFT
)
)
)
);
if (useCsiU) {
result.key = this._buildCsiUSequence(ev, keyCode, modifiers, eventType, flags, isFunc, isMod);