Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1040b799ce | ||
|
|
1c42ae9af4 | ||
|
|
e749cb6125 | ||
|
|
8df996f9e7 | ||
|
|
51f6186352 | ||
|
|
ae5c41c518 | ||
|
|
df9fb7e473 | ||
|
|
f100e93ba5 | ||
|
|
1d3825d341 | ||
|
|
57315e6cc9 | ||
|
|
2be4fc68e5 | ||
|
|
66fabfc232 | ||
|
|
3e2a42eb29 | ||
|
|
6a47d09b4c | ||
|
|
b36001ee7b | ||
|
|
e64f25a0dd | ||
|
|
3bcb57545e | ||
|
|
dc726a2ae6 | ||
|
|
06e5ac7ab4 | ||
|
|
c6e3492a4a | ||
|
|
86291b6e96 | ||
|
|
c8c8b3a7fe | ||
|
|
50e90ddf8a | ||
|
|
7059a07c09 | ||
|
|
c5c6fc2a64 | ||
|
|
e0add7fe4b | ||
|
|
b0f5bc2d28 | ||
|
|
c5a11220c1 | ||
|
|
6f55150485 | ||
|
|
c8dddddbbb | ||
|
|
6ba731d73b | ||
|
|
2d3909be2c | ||
|
|
1e189625fa | ||
|
|
80debafa11 | ||
|
|
1023e40428 | ||
|
|
5c47e6dd9a | ||
|
|
4ebb3fffe9 | ||
|
|
093e81b9a7 | ||
|
|
e04dd2501f | ||
|
|
d911b40a47 | ||
|
|
3dbfa7ce3e | ||
|
|
6287c6ddfe | ||
|
|
52d4b2719c | ||
|
|
7615cd210a | ||
|
|
4d4b994c35 | ||
|
|
7fa8b7a069 | ||
|
|
a2e51e8d36 | ||
|
|
f89253d24b | ||
|
|
7aeee95346 | ||
|
|
66fcdb2583 | ||
|
|
2a18600d18 | ||
|
|
5ef4f86543 | ||
|
|
874e767b40 | ||
|
|
605a7c6e30 | ||
|
|
802decb046 | ||
|
|
79a95dbfd8 | ||
|
|
75ba88c51b | ||
|
|
f9cea732f2 | ||
|
|
4dc934ce7c | ||
|
|
b2eff5ac67 | ||
|
|
31c53bc9c2 | ||
|
|
0d2975884f | ||
|
|
e8b5fd3c23 | ||
|
|
7abcca7e73 | ||
|
|
92339ac9dd | ||
|
|
ad4a6e8cb9 | ||
|
|
a094340afe | ||
|
|
cc7e3bbbf7 | ||
|
|
73d87b2c20 | ||
|
|
c5bf395b10 | ||
|
|
9d24099aa1 | ||
|
|
066c227436 | ||
|
|
32ad3a18bf | ||
|
|
d43b19c260 | ||
|
|
7dd0dac859 | ||
|
|
7e70d6ad78 | ||
|
|
893cdd494a | ||
|
|
2794e32b3f | ||
|
|
7d69e730ca | ||
|
|
da51d9608c | ||
|
|
3a84bcb7ae | ||
|
|
d9082215e7 | ||
|
|
41938b307e | ||
|
|
785c3cb336 | ||
|
|
ea7e958959 | ||
|
|
915b0b81a1 | ||
|
|
4e9abe7d96 | ||
|
|
84aca0b912 | ||
|
|
d82d42ef9a | ||
|
|
4d2f2e0d8e | ||
|
|
56986cb7eb | ||
|
|
4946436997 | ||
|
|
bf83ea06e4 | ||
|
|
b7e535e2ec | ||
|
|
8e12e69e4e | ||
|
|
849a9a53ca | ||
|
|
ee7d84bdd3 | ||
|
|
9c8de633e9 | ||
|
|
9c5420371b | ||
|
|
22778b5b49 | ||
|
|
cbe59a17ae | ||
|
|
76671cecb6 | ||
|
|
a11fcd2e0b | ||
|
|
b28b5d0c2b | ||
|
|
7fe89a1c1a | ||
|
|
eaebe2ab15 | ||
|
|
32553b4135 | ||
|
|
6c2465d745 | ||
|
|
5922123b9e | ||
|
|
eb65910c1b | ||
|
|
f0aeb70c92 | ||
|
|
9deec7aabf | ||
|
|
18cd9858ed | ||
|
|
6fd7ecc812 | ||
|
|
0911688295 | ||
|
|
62ce318a07 | ||
|
|
08ae141ef7 | ||
|
|
ba7e775a17 | ||
|
|
487b27f512 | ||
|
|
5d120de3d5 | ||
|
|
3ab299af7f | ||
|
|
6e375cb588 | ||
|
|
7a6e724bfe | ||
|
|
4b77e601cd | ||
|
|
5e9b2cbc14 | ||
|
|
3a59c8ec2b |
@@ -1,7 +1,11 @@
|
||||
---
|
||||
applyTo: '**/*.benchmark.ts'
|
||||
name: benchmark
|
||||
description: Run and author benchmark tests in xterm.js. Use when editing benchmark files, working in `*.benchmark.ts`, or when asked how to run the full benchmark suite, a single benchmark file, or a single benchmark case.
|
||||
---
|
||||
# Benchmark run instructions
|
||||
|
||||
# Benchmark Run Instructions
|
||||
|
||||
## Running Benchmarks
|
||||
|
||||
- Full suite: `npm run benchmark`
|
||||
- Single benchmark file:
|
||||
@@ -11,8 +15,14 @@ applyTo: '**/*.benchmark.ts'
|
||||
- Use `-t` to get the path, then:
|
||||
- `npm run benchmark -- -s "<path>" out-test/benchmark/Event.benchmark.js`
|
||||
|
||||
When writing instructions, use `RuntimeCase` to measure pure runtime in ms, use `ThroughputRuntimeCase` when measuring throughput in MB/s.
|
||||
## Benchmark Case Selection
|
||||
|
||||
When writing benchmark instructions:
|
||||
|
||||
- Use `RuntimeCase` to measure pure runtime in ms.
|
||||
- Use `ThroughputRuntimeCase` when measuring throughput in MB/s.
|
||||
|
||||
## Notes
|
||||
|
||||
Notes:
|
||||
- Benchmarks run from built JS in `out-test/benchmark/*.benchmark.js`.
|
||||
- Keep `NODE_PATH=./out` (handled by the npm script).
|
||||
@@ -1,7 +1,11 @@
|
||||
---
|
||||
applyTo: '**/*.test.ts'
|
||||
name: unit-test
|
||||
description: Write and review xterm.js unit tests with project conventions. Use when editing `*.test.ts` files or when asked for test authoring guidelines.
|
||||
---
|
||||
When writing unit tests follow these rules:
|
||||
|
||||
# Unit Test Instructions
|
||||
|
||||
When writing unit tests, follow these rules:
|
||||
|
||||
- When writing unit tests for addons, always create a real xterm.js instance instead of mocking it.
|
||||
- Prefer `assert.ok` over `assert.notStrictEqual` when checking something is undefined or not.
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "npm run agent:setup-repo"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+13
-13
@@ -207,31 +207,31 @@ jobs:
|
||||
- name: Build demo server
|
||||
run: npm run esbuild-demo-server
|
||||
- name: Integration tests (core) # Tests use 50% workers to reduce flakiness
|
||||
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=core
|
||||
run: npm run test-integration-${{ matrix.browser }} -- --workers=50% --forbid-only --suite=core
|
||||
- name: Integration tests (addon-attach)
|
||||
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-attach
|
||||
run: npm run test-integration-${{ matrix.browser }} -- --workers=50% --forbid-only --suite=addon-attach
|
||||
- name: Integration tests (addon-clipboard)
|
||||
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-clipboard
|
||||
run: npm run test-integration-${{ matrix.browser }} -- --workers=50% --forbid-only --suite=addon-clipboard
|
||||
- name: Integration tests (addon-fit)
|
||||
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-fit
|
||||
run: npm run test-integration-${{ matrix.browser }} -- --workers=50% --forbid-only --suite=addon-fit
|
||||
- name: Integration tests (addon-image)
|
||||
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-image
|
||||
run: npm run test-integration-${{ matrix.browser }} -- --workers=50% --forbid-only --suite=addon-image
|
||||
- name: Integration tests (addon-progress)
|
||||
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-progress
|
||||
run: npm run test-integration-${{ matrix.browser }} -- --workers=50% --forbid-only --suite=addon-progress
|
||||
- name: Integration tests (addon-search)
|
||||
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-search
|
||||
run: npm run test-integration-${{ matrix.browser }} -- --workers=50% --forbid-only --suite=addon-search
|
||||
- name: Integration tests (addon-serialize)
|
||||
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-serialize
|
||||
run: npm run test-integration-${{ matrix.browser }} -- --workers=50% --forbid-only --suite=addon-serialize
|
||||
- name: Integration tests (addon-unicode-graphemes)
|
||||
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-unicode-graphemes
|
||||
run: npm run test-integration-${{ matrix.browser }} -- --workers=50% --forbid-only --suite=addon-unicode-graphemes
|
||||
- name: Integration tests (addon-unicode11)
|
||||
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-unicode11
|
||||
run: npm run test-integration-${{ matrix.browser }} -- --workers=50% --forbid-only --suite=addon-unicode11
|
||||
- name: Integration tests (addon-web-fonts)
|
||||
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-web-fonts
|
||||
run: npm run test-integration-${{ matrix.browser }} -- --workers=50% --forbid-only --suite=addon-web-fonts
|
||||
- name: Integration tests (addon-web-links)
|
||||
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-web-links
|
||||
run: npm run test-integration-${{ matrix.browser }} -- --workers=50% --forbid-only --suite=addon-web-links
|
||||
- name: Integration tests (addon-webgl)
|
||||
run: npm run test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-webgl
|
||||
run: npm run test-integration-${{ matrix.browser }} -- --workers=50% --forbid-only --suite=addon-webgl
|
||||
|
||||
release-dry-run:
|
||||
needs: build
|
||||
|
||||
@@ -22,6 +22,9 @@ build/
|
||||
yarn.lock
|
||||
test-results/
|
||||
|
||||
# Tracing/debug files
|
||||
.cursor/nal-trace/
|
||||
|
||||
# Keep bundled code out of Git
|
||||
dist/
|
||||
demo/dist/
|
||||
|
||||
@@ -57,6 +57,13 @@ export class MyAddon implements ITerminalAddon {
|
||||
- Proposed APIs require `allowProposedApi: true` option
|
||||
- Constructor-only options (cols, rows) cannot be changed after instantiation
|
||||
|
||||
**Disposable Management**:
|
||||
- When a disposable object can be replaced over time, prefer a registered `MutableDisposable` over manual dispose/reassign logic.
|
||||
- Register it on the owning class (for example, `this._register(new MutableDisposable())`) and assign through `.value`; this automatically disposes the previous value and avoids accidentally leaking resources.
|
||||
|
||||
**TypeScript Constants**:
|
||||
- Prefer `const enum` over top-level `const` declarations for primitive constants when appropriate, since values are inlined and avoid runtime property lookups.
|
||||
|
||||
**Testing Utilities**: Use `TestUtils.ts` helpers:
|
||||
- `openTerminal(ctx, options)` for setup
|
||||
- `pollFor(page, fn, expectedValue)` for async assertions
|
||||
@@ -252,6 +252,8 @@ Xterm.js is used in many world-class applications to provide great terminal expe
|
||||
- [**EmuDevz**](https://afska.github.io/emudevz): A free coding game where players learn how to build an emulator from scratch.
|
||||
- [**SSH Connection Manager**](https://github.com/Amtrend/ssh-manager): Modern web-based SSH terminal and SFTP manager with AES-256 encryption, PWA support, and session management.
|
||||
- [**WooTTY**](https://github.com/icoretech/wootty): Flawless browser terminal for real operators.
|
||||
- [**Orca**](https://github.com/stablyai/orca): Agentic development environment for 100x builders.
|
||||
- [**4bit**](https://ciembor.github.io/4bit): Terminal Color Scheme Designer.
|
||||
- [And much more...](https://github.com/xtermjs/xterm.js/network/dependents?package_id=UGFja2FnZS0xNjYzMjc4OQ%3D%3D)
|
||||
|
||||
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it on our list. Please add any new contributions to the end of the list.
|
||||
|
||||
@@ -21,8 +21,5 @@
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package",
|
||||
"start": "node ../../demo/start"
|
||||
},
|
||||
"dependencies": {
|
||||
"js-base64": "^3.7.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,8 +19,10 @@ interface ITerminalDimensions {
|
||||
cols: number;
|
||||
}
|
||||
|
||||
const MINIMUM_COLS = 2;
|
||||
const MINIMUM_ROWS = 1;
|
||||
const enum Constants {
|
||||
MINIMUM_COLS = 2,
|
||||
MINIMUM_ROWS = 1
|
||||
}
|
||||
|
||||
function getWindow(e: Node): Window {
|
||||
if (e?.ownerDocument?.defaultView) {
|
||||
@@ -86,8 +88,8 @@ export class FitAddon implements ITerminalAddon, IFitApi {
|
||||
const availableHeight = parentElementHeight - elementPaddingVer;
|
||||
const availableWidth = parentElementWidth - elementPaddingHor - scrollbarWidth;
|
||||
const geometry = {
|
||||
cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / dims.css.cell.width)),
|
||||
rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / dims.css.cell.height))
|
||||
cols: Math.max(Constants.MINIMUM_COLS, Math.floor(availableWidth / dims.css.cell.width)),
|
||||
rows: Math.max(Constants.MINIMUM_ROWS, Math.floor(availableHeight / dims.css.cell.height))
|
||||
};
|
||||
return geometry;
|
||||
}
|
||||
|
||||
@@ -25,14 +25,14 @@ const customSettings: IImageAddonOptions = {
|
||||
pixelLimit: 16777216, // max. pixel size of a single image
|
||||
sixelSupport: true, // enable sixel support
|
||||
sixelScrolling: true, // whether to scroll on image output
|
||||
sixelPaletteLimit: 256, // initial sixel palette size
|
||||
sixelSizeLimit: 25000000, // size limit of a single sixel sequence
|
||||
sixelPaletteLimit: 4096, // initial sixel palette size
|
||||
sixelSizeLimit: 33554432, // size limit of a single sixel sequence
|
||||
storageLimit: 128, // FIFO storage limit in MB
|
||||
showPlaceholder: true, // whether to show a placeholder for evicted images
|
||||
iipSupport: true, // enable iTerm IIP support
|
||||
iipSizeLimit: 20000000, // size limit of a single IIP sequence
|
||||
iipSizeLimit: 33554432, // size limit of a single IIP sequence
|
||||
kittySupport: true, // enable Kitty graphics support
|
||||
kittySizeLimit: 20000000 // size limit of a single Kitty sequence
|
||||
kittySizeLimit: 33554432 // size limit of a single Kitty sequence
|
||||
}
|
||||
|
||||
// initialization
|
||||
|
||||
@@ -25,6 +25,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"sixel": "^0.16.0",
|
||||
"xterm-wasm-parts": "^0.3.0"
|
||||
"xterm-wasm-parts": "^0.4.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ImageRenderer } from './ImageRenderer';
|
||||
import { IIPImageStorage } from './IIPImageStorage';
|
||||
import { CELL_SIZE_DEFAULT } from './ImageStorage';
|
||||
import Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
|
||||
import QoiDecoder from 'xterm-wasm-parts/lib/qoi/QoiDecoder.wasm';
|
||||
import { HeaderParser, IHeaderFields, HeaderState } from './IIPHeaderParser';
|
||||
import { imageType, UNSUPPORTED_TYPE } from './IIPMetrics';
|
||||
|
||||
@@ -36,6 +37,7 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
||||
private _hp = new HeaderParser();
|
||||
private _header: IHeaderFields = DEFAULT_HEADER;
|
||||
private _dec: Base64Decoder;
|
||||
private _qoiDec: QoiDecoder;
|
||||
private _metrics = UNSUPPORTED_TYPE;
|
||||
|
||||
constructor(
|
||||
@@ -47,6 +49,7 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
||||
const maxEncodedBytes = Math.ceil(this._opts.iipSizeLimit * 4 / 3);
|
||||
const initialBytes = Math.min(DecoderConst.INITIAL_DATA, maxEncodedBytes);
|
||||
this._dec = new Base64Decoder(DecoderConst.KEEP_DATA, maxEncodedBytes, initialBytes);
|
||||
this._qoiDec = new QoiDecoder(DecoderConst.KEEP_DATA);
|
||||
}
|
||||
|
||||
public reset(): void {}
|
||||
@@ -115,27 +118,27 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
||||
return true;
|
||||
}
|
||||
|
||||
// HACK: The types on Blob are too restrictive, this is a Uint8Array so the browser accepts it
|
||||
const blob = new Blob([this._dec.data8 as Uint8Array<ArrayBuffer>], { type: this._metrics.mime });
|
||||
this._dec.release();
|
||||
|
||||
if (!window.createImageBitmap) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const img = new Image();
|
||||
return new Promise<boolean>(r => {
|
||||
img.addEventListener('load', () => {
|
||||
URL.revokeObjectURL(url);
|
||||
const canvas = ImageRenderer.createCanvas(window.document, w, h);
|
||||
canvas.getContext('2d')?.drawImage(img, 0, 0, w, h);
|
||||
this._storage.addImage(canvas);
|
||||
r(true);
|
||||
});
|
||||
img.src = url;
|
||||
// sanity measure to avoid terminal blocking from dangling promise
|
||||
// happens from corrupt data (onload never gets fired)
|
||||
setTimeout(() => r(true), 1000);
|
||||
});
|
||||
let blob: Blob | ImageData;
|
||||
if (this._metrics.mime === 'image/qoi') {
|
||||
const data = this._qoiDec.decode(this._dec.data8);
|
||||
blob = new ImageData(
|
||||
new Uint8ClampedArray(data.buffer, data.byteOffset, data.byteLength),
|
||||
this._qoiDec.width,
|
||||
this._qoiDec.height
|
||||
);
|
||||
this._qoiDec.release();
|
||||
if (w === this._qoiDec.width && h === this._qoiDec.height) {
|
||||
// use fast-path if we don't need to rescale
|
||||
this._dec.release();
|
||||
const canvas = ImageRenderer.createCanvas(undefined, this._qoiDec.width, this._qoiDec.height);
|
||||
canvas.getContext('2d')?.putImageData(blob, 0, 0);
|
||||
this._storage.addImage(canvas);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
blob = new Blob([this._dec.data8], { type: this._metrics.mime });
|
||||
}
|
||||
this._dec.release();
|
||||
return createImageBitmap(blob, { resizeWidth: w, resizeHeight: h })
|
||||
.then(bm => {
|
||||
this._storage.addImage(bm);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
|
||||
export type ImageType = 'image/png' | 'image/jpeg' | 'image/gif' | 'unsupported' | '';
|
||||
export type ImageType = 'image/png' | 'image/jpeg' | 'image/gif' | 'image/qoi' | 'unsupported' | '';
|
||||
|
||||
export interface IMetrics {
|
||||
mime: ImageType;
|
||||
@@ -45,6 +45,14 @@ export function imageType(d: Uint8Array): IMetrics {
|
||||
height: d[9] << 8 | d[8]
|
||||
};
|
||||
}
|
||||
// QOI: qoif
|
||||
if (d32[0] === 0x66696F71) {
|
||||
return {
|
||||
mime: 'image/qoi',
|
||||
width: d[4] << 24 | d[5] << 16 | d[6] << 8 | d[7],
|
||||
height: d[8] << 24 | d[9] << 16 | d[10] << 8 | d[11]
|
||||
};
|
||||
}
|
||||
return UNSUPPORTED_TYPE;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,20 +16,61 @@ import { SixelImageStorage } from './SixelImageStorage';
|
||||
import { IIPImageStorage } from './IIPImageStorage';
|
||||
import { ITerminalExt, IImageAddonOptions, IResetHandler } from './Types';
|
||||
|
||||
|
||||
/**
|
||||
* Document VT features provided by this addon.
|
||||
*
|
||||
* @vt: #E[Supported via @xterm/addon-image.] DCS SIXEL "SIXEL Graphics" "DCS Ps ; Ps ; Ps ; q Pt ST" "Draw SIXEL image."
|
||||
*
|
||||
* Sixel support is provided by the addon @xterm/addon-image with these limitations:
|
||||
* - immediate coloring (no shared palette, allows high color settings of `img2sixel`)
|
||||
* - max. palette size of 4096 colors
|
||||
* - max. pixel width of 16K
|
||||
* - max. 25 MB per sixel sequence
|
||||
* - VT340 cursor positioning (begin of last sixel data row)
|
||||
*
|
||||
* See [addon readme](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-image) for more details.
|
||||
*
|
||||
*
|
||||
* @vt: #E[Supported via @xterm/addon-image.] OSC 1337 "iTerm2 Commands" "OSC 1337 ; Pt BEL" "Custom iTerm2 commands."
|
||||
*
|
||||
* Only the inline image protocol (IIP) is supported by the addon @xterm/addon-image with
|
||||
* the following limitations:
|
||||
* - sequence:
|
||||
* - format: `OSC 1337 ; File=inline=1 ; size=<unencoded size> ; ... : <base64 payload> BEL`
|
||||
* - size param must be set and payload may not exceed CEIL(size * 4 / 3)
|
||||
* - strict base64 handling as of RFC4648 §4 (standard alphabet, optional padding,
|
||||
* no separator bytes allowed)
|
||||
* - supported params: size, name, width, height, preserveAspectRatio
|
||||
* - image formats: PNG, JPEG and GIF
|
||||
* - no animation support (renders first image of a GIF)
|
||||
* - no multipart support
|
||||
* - VT340 cursor positioning (begin of last sixel data row)
|
||||
*
|
||||
* See [addon readme](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-image)
|
||||
* and [iTerm2 IIP docs](https://iterm2.com/documentation-images.html) for more details.
|
||||
*
|
||||
*
|
||||
* @vt: #E[Supported via @xterm/addon-image.] APC KITTY_GRAPHICS "Kitty Graphics" "APC G Pt ST" "Kitty Graphics Protocol."
|
||||
*
|
||||
* Kitty graphics support is provided by the addon @xterm/addon-image.
|
||||
* Note that while basic image output already works, this is still work in progress.
|
||||
*/
|
||||
|
||||
// default values of addon ctor options
|
||||
const DEFAULT_OPTIONS: IImageAddonOptions = {
|
||||
enableSizeReports: true,
|
||||
pixelLimit: 16777216, // limit to 4096 * 4096 pixels
|
||||
sixelSupport: true,
|
||||
sixelScrolling: true,
|
||||
sixelPaletteLimit: 256,
|
||||
sixelSizeLimit: 25000000,
|
||||
sixelPaletteLimit: 4096,
|
||||
sixelSizeLimit: 33554432,
|
||||
storageLimit: 128,
|
||||
showPlaceholder: true,
|
||||
iipSupport: true,
|
||||
iipSizeLimit: 20000000,
|
||||
iipSizeLimit: 33554432,
|
||||
kittySupport: true,
|
||||
kittySizeLimit: 20000000
|
||||
kittySizeLimit: 33554432
|
||||
};
|
||||
|
||||
// max palette size supported by the sixel lib (compile time setting)
|
||||
@@ -166,7 +207,7 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
|
||||
this._disposeLater(
|
||||
kittyStorage,
|
||||
kittyHandler,
|
||||
terminal._core._inputHandler._parser.registerApcHandler(0x47, kittyHandler)
|
||||
terminal._core._inputHandler._parser.registerApcHandler({ final: 'G' }, kittyHandler)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,10 @@ import { IDisposable } from '@xterm/xterm';
|
||||
import { ICellSize, ImageLayer, ITerminalExt, IImageSpec, IRenderDimensions, IRenderService } from './Types';
|
||||
import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
|
||||
|
||||
const PLACEHOLDER_LENGTH = 4096;
|
||||
const PLACEHOLDER_HEIGHT = 24;
|
||||
const enum Constants {
|
||||
PLACEHOLDER_LENGTH = 4096,
|
||||
PLACEHOLDER_HEIGHT = 24
|
||||
}
|
||||
|
||||
/**
|
||||
* ImageRenderer - terminal frontend extension:
|
||||
@@ -19,7 +21,9 @@ const PLACEHOLDER_HEIGHT = 24;
|
||||
*/
|
||||
export class ImageRenderer extends Disposable implements IDisposable {
|
||||
/** @deprecated Kept for backward compat — points to top layer canvas. */
|
||||
public get canvas(): HTMLCanvasElement | undefined { return this._layers.get('top')?.canvas; }
|
||||
public get canvas(): HTMLCanvasElement | undefined {
|
||||
return this._layers.get('top')?.canvas;
|
||||
}
|
||||
private _layers = new Map<ImageLayer, CanvasRenderingContext2D>();
|
||||
private _placeholder: HTMLCanvasElement | undefined;
|
||||
private _placeholderBitmap: ImageBitmap | undefined;
|
||||
@@ -110,7 +114,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
||||
public showPlaceholder(value: boolean): void {
|
||||
if (value) {
|
||||
if (!this._placeholder && this.cellSize.height !== -1) {
|
||||
this._createPlaceHolder(Math.max(this.cellSize.height + 1, PLACEHOLDER_HEIGHT));
|
||||
this._createPlaceHolder(Math.max(this.cellSize.height + 1, Constants.PLACEHOLDER_HEIGHT));
|
||||
}
|
||||
} else {
|
||||
this._placeholderBitmap?.close();
|
||||
@@ -249,7 +253,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
||||
}
|
||||
|
||||
if (!this._placeholder) {
|
||||
this._createPlaceHolder(Math.max(height + 1, PLACEHOLDER_HEIGHT));
|
||||
this._createPlaceHolder(Math.max(height + 1, Constants.PLACEHOLDER_HEIGHT));
|
||||
} else if (height >= this._placeholder!.height) {
|
||||
this._createPlaceHolder(height + 1);
|
||||
}
|
||||
@@ -358,7 +362,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
||||
canvas.style.zIndex = '0';
|
||||
screenElement.appendChild(canvas);
|
||||
}
|
||||
const ctx = canvas.getContext('2d', { alpha: true, desynchronized: true });
|
||||
const ctx = canvas.getContext('2d', { alpha: true });
|
||||
if (!ctx) {
|
||||
canvas.remove();
|
||||
return;
|
||||
@@ -379,7 +383,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
||||
return this._layers.has(layer);
|
||||
}
|
||||
|
||||
private _createPlaceHolder(height: number = PLACEHOLDER_HEIGHT): void {
|
||||
private _createPlaceHolder(height: number = Constants.PLACEHOLDER_HEIGHT): void {
|
||||
this._placeholderBitmap?.close();
|
||||
this._placeholderBitmap = undefined;
|
||||
|
||||
@@ -403,7 +407,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
||||
ctx.putImageData(imgData, 0, 0);
|
||||
|
||||
// create placeholder line, width aligned to blueprint width
|
||||
const width = (screen.width + bWidth - 1) & ~(bWidth - 1) || PLACEHOLDER_LENGTH;
|
||||
const width = (screen.width + bWidth - 1) & ~(bWidth - 1) || Constants.PLACEHOLDER_LENGTH;
|
||||
this._placeholder = ImageRenderer.createCanvas(this.document, width, height);
|
||||
const ctx2 = this._placeholder.getContext('2d', { alpha: false });
|
||||
if (!ctx2) {
|
||||
|
||||
@@ -31,7 +31,9 @@ class ExtendedAttrsImage implements IExtendedAttrsImage {
|
||||
}
|
||||
return this._ext;
|
||||
}
|
||||
public set ext(value: number) { this._ext = value; }
|
||||
public set ext(value: number) {
|
||||
this._ext = value;
|
||||
}
|
||||
|
||||
public get underlineStyle(): UnderlineStyle {
|
||||
// Always return the URL style if it has one
|
||||
|
||||
@@ -16,24 +16,23 @@ import {
|
||||
IKittyCommand,
|
||||
IPendingTransmission,
|
||||
IKittyImageData,
|
||||
BYTES_PER_PIXEL_RGB,
|
||||
BYTES_PER_PIXEL_RGBA,
|
||||
ALPHA_OPAQUE,
|
||||
KittyPixelConstants,
|
||||
parseKittyCommand
|
||||
} from './KittyGraphicsTypes';
|
||||
|
||||
// Memory limit for base64 decoder (4MB, same as IIPHandler)
|
||||
const DECODER_KEEP_DATA = 4194304;
|
||||
const DECODER_INITIAL_DATA = 4194304; // 4MB
|
||||
const enum Constants {
|
||||
// Memory limit for base64 decoder (4MB, same as IIPHandler)
|
||||
DECODER_KEEP_DATA = 4194304,
|
||||
DECODER_INITIAL_DATA = 4194304, // 4MB
|
||||
// Local mirror of const enum (esbuild can't inline const enums from external packages)
|
||||
DECODER_OK = 0,
|
||||
// Maximum control data size
|
||||
MAX_CONTROL_DATA_SIZE = 512,
|
||||
// Semicolon codepoint
|
||||
SEMICOLON = 0x3B
|
||||
}
|
||||
|
||||
// Local mirror of const enum (esbuild can't inline const enums from external packages)
|
||||
const DECODER_OK: DecodeStatus.OK = 0;
|
||||
|
||||
// Maximum control data size
|
||||
const MAX_CONTROL_DATA_SIZE = 512;
|
||||
|
||||
// Semicolon codepoint
|
||||
const SEMICOLON = 0x3B;
|
||||
const DECODER_OK = Constants.DECODER_OK as unknown as DecodeStatus.OK;
|
||||
|
||||
// Kitty graphics protocol handler with streaming base64 decoding.
|
||||
export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDisposable {
|
||||
@@ -50,7 +49,7 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
|
||||
private _inControlData = true;
|
||||
|
||||
// Buffer for control data.
|
||||
private _controlData = new Uint32Array(MAX_CONTROL_DATA_SIZE);
|
||||
private _controlData = new Uint32Array(Constants.MAX_CONTROL_DATA_SIZE);
|
||||
private _controlLength = 0;
|
||||
|
||||
// Pre-calculated encoded size limit
|
||||
@@ -77,7 +76,7 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
|
||||
// Convert decoded size limit -> max encoded bytes.
|
||||
this._maxEncodedBytes = Math.ceil(this._opts.kittySizeLimit * 4 / 3);
|
||||
// ensure we preallocate more than configured limit while using 4mb initial size.
|
||||
this._initialEncodedBytes = Math.min(DECODER_INITIAL_DATA, this._maxEncodedBytes);
|
||||
this._initialEncodedBytes = Math.min(Constants.DECODER_INITIAL_DATA, this._maxEncodedBytes);
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
@@ -129,7 +128,7 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
|
||||
// Scan for semicolon
|
||||
let controlEnd = end;
|
||||
for (let i = start; i < end; i++) {
|
||||
if (data[i] === SEMICOLON) {
|
||||
if (data[i] === Constants.SEMICOLON) {
|
||||
this._inControlData = false;
|
||||
controlEnd = i;
|
||||
break;
|
||||
@@ -138,7 +137,7 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
|
||||
|
||||
// Copy control data
|
||||
const copyLength = controlEnd - start;
|
||||
if (this._controlLength + copyLength > MAX_CONTROL_DATA_SIZE) {
|
||||
if (this._controlLength + copyLength > Constants.MAX_CONTROL_DATA_SIZE) {
|
||||
this._aborted = true;
|
||||
return;
|
||||
}
|
||||
@@ -201,7 +200,7 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
|
||||
this._activeDecoder = pending.decoder;
|
||||
}
|
||||
if (!this._activeDecoder) {
|
||||
this._activeDecoder = new Base64Decoder(DECODER_KEEP_DATA, this._maxEncodedBytes, this._initialEncodedBytes);
|
||||
this._activeDecoder = new Base64Decoder(Constants.DECODER_KEEP_DATA, this._maxEncodedBytes, this._initialEncodedBytes);
|
||||
this._activeDecoder.init();
|
||||
}
|
||||
|
||||
@@ -484,7 +483,7 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
|
||||
return true;
|
||||
}
|
||||
|
||||
const bytesPerPixel = format === KittyFormat.RGBA ? BYTES_PER_PIXEL_RGBA : BYTES_PER_PIXEL_RGB;
|
||||
const bytesPerPixel = format === KittyFormat.RGBA ? KittyPixelConstants.BYTES_PER_PIXEL_RGBA : KittyPixelConstants.BYTES_PER_PIXEL_RGB;
|
||||
const expectedBytes = width * height * bytesPerPixel;
|
||||
|
||||
if (bytes.length < expectedBytes) {
|
||||
@@ -534,8 +533,8 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
|
||||
|
||||
private _sendResponse(id: number, message: string, quiet: number, placementId?: number): void {
|
||||
const isOk = message === 'OK';
|
||||
if (isOk && quiet === 1) return;
|
||||
if (!isOk && quiet === 2) return;
|
||||
if (isOk && quiet >= 1) return;
|
||||
if (!isOk && quiet >= 2) return;
|
||||
|
||||
const pPart = placementId ? `,p=${placementId}` : '';
|
||||
const response = `\x1b_Gi=${id}${pPart};${message}\x1b\\`;
|
||||
@@ -723,7 +722,7 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
|
||||
throw new Error('Width and height required for raw pixel data');
|
||||
}
|
||||
|
||||
const bytesPerPixel = image.format === KittyFormat.RGBA ? BYTES_PER_PIXEL_RGBA : BYTES_PER_PIXEL_RGB;
|
||||
const bytesPerPixel = image.format === KittyFormat.RGBA ? KittyPixelConstants.BYTES_PER_PIXEL_RGBA : KittyPixelConstants.BYTES_PER_PIXEL_RGB;
|
||||
const expectedBytes = width * height * bytesPerPixel;
|
||||
|
||||
if (bytes.length < expectedBytes) {
|
||||
@@ -734,13 +733,13 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
|
||||
|
||||
if (image.format === KittyFormat.RGBA) {
|
||||
// RGBA: use bytes directly — no copy needed
|
||||
return createImageBitmap(new ImageData(new Uint8ClampedArray(bytes.buffer as ArrayBuffer, bytes.byteOffset, pixelCount * BYTES_PER_PIXEL_RGBA), width, height));
|
||||
return createImageBitmap(new ImageData(new Uint8ClampedArray(bytes.buffer as ArrayBuffer, bytes.byteOffset, pixelCount * KittyPixelConstants.BYTES_PER_PIXEL_RGBA), width, height));
|
||||
}
|
||||
|
||||
// RGB→RGBA: interleave alpha using uint32 block processing (4 pixels per iteration).
|
||||
// 3 uint32 reads + 4 uint32 writes per 4 pixels vs 28 byte reads/writes — ~6x faster.
|
||||
// Assumes little-endian (all modern browsers/Node.js).
|
||||
const data = new Uint8ClampedArray(pixelCount * BYTES_PER_PIXEL_RGBA);
|
||||
const data = new Uint8ClampedArray(pixelCount * KittyPixelConstants.BYTES_PER_PIXEL_RGBA);
|
||||
const src32 = new Uint32Array(bytes.buffer, bytes.byteOffset, Math.floor(bytes.byteLength / 4));
|
||||
const dst32 = new Uint32Array(data.buffer);
|
||||
const alignedPixels = pixelCount & ~3; // round down to multiple of 4
|
||||
@@ -759,15 +758,15 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
|
||||
}
|
||||
|
||||
// Handle remaining 1–3 pixels
|
||||
let srcByte = alignedPixels * BYTES_PER_PIXEL_RGB;
|
||||
let dstByte = alignedPixels * BYTES_PER_PIXEL_RGBA;
|
||||
let srcByte = alignedPixels * KittyPixelConstants.BYTES_PER_PIXEL_RGB;
|
||||
let dstByte = alignedPixels * KittyPixelConstants.BYTES_PER_PIXEL_RGBA;
|
||||
for (let i = alignedPixels; i < pixelCount; i++) {
|
||||
data[dstByte] = bytes[srcByte];
|
||||
data[dstByte + 1] = bytes[srcByte + 1];
|
||||
data[dstByte + 2] = bytes[srcByte + 2];
|
||||
data[dstByte + 3] = ALPHA_OPAQUE;
|
||||
srcByte += BYTES_PER_PIXEL_RGB;
|
||||
dstByte += BYTES_PER_PIXEL_RGBA;
|
||||
data[dstByte + 3] = KittyPixelConstants.ALPHA_OPAQUE;
|
||||
srcByte += KittyPixelConstants.BYTES_PER_PIXEL_RGB;
|
||||
dstByte += KittyPixelConstants.BYTES_PER_PIXEL_RGBA;
|
||||
}
|
||||
|
||||
return createImageBitmap(new ImageData(data, width, height));
|
||||
|
||||
@@ -82,9 +82,11 @@ export const enum KittyKey {
|
||||
}
|
||||
|
||||
// Pixel format constants
|
||||
export const BYTES_PER_PIXEL_RGB = 3;
|
||||
export const BYTES_PER_PIXEL_RGBA = 4;
|
||||
export const ALPHA_OPAQUE = 255;
|
||||
export const enum KittyPixelConstants {
|
||||
BYTES_PER_PIXEL_RGB = 3,
|
||||
BYTES_PER_PIXEL_RGBA = 4,
|
||||
ALPHA_OPAQUE = 255
|
||||
}
|
||||
|
||||
// Parsed Kitty graphics command.
|
||||
export interface IKittyCommand {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { readFileSync } from 'fs';
|
||||
import { FINALIZER, introducer, sixelEncode } from 'sixel';
|
||||
import { ITestContext, createTestContext, openTerminal, pollFor, timeout } from '../../../test/playwright/TestUtils';
|
||||
import { deepStrictEqual, ok, strictEqual } from 'assert';
|
||||
import QoiEncoder from 'xterm-wasm-parts/lib/qoi/QoiEncoder.wasm';
|
||||
|
||||
/**
|
||||
* Plugin ctor options.
|
||||
@@ -74,6 +75,10 @@ const TESTDATA_IIP: [string, [number, number]][] = [
|
||||
[readFileSync('./addons/addon-image/fixture/iip/w3c_jpg.iip', { encoding: 'utf-8' }), [72, 48]],
|
||||
[readFileSync('./addons/addon-image/fixture/iip/w3c_png.iip', { encoding: 'utf-8' }), [72, 48]]
|
||||
];
|
||||
const PALETTE_PNG_BASE64 = readFileSync('./addons/addon-image/fixture/palette.png').toString('base64');
|
||||
const qoiEnc = new QoiEncoder(1024*1024);
|
||||
const qoiData = qoiEnc.encode(TESTDATA.bytes, 640, 80);
|
||||
const PALETTE_QOI_BASE64 = Buffer.from(qoiData).toString('base64');
|
||||
|
||||
let ctx: ITestContext;
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
@@ -85,11 +90,6 @@ test.afterAll(async () => await ctx.page.close());
|
||||
test.describe('ImageAddon', () => {
|
||||
|
||||
test.beforeEach(async ({}, testInfo) => {
|
||||
// DEBT: This test never worked on webkit
|
||||
if (ctx.browser.browserType().name() === 'webkit') {
|
||||
testInfo.skip();
|
||||
return;
|
||||
}
|
||||
await ctx.page.evaluate(`
|
||||
window.term.reset()
|
||||
window.imageAddon?.dispose();
|
||||
@@ -132,13 +132,13 @@ test.describe('ImageAddon', () => {
|
||||
sixelSupport: true,
|
||||
sixelScrolling: true,
|
||||
sixelPaletteLimit: 512, // set to 512 to get example image working
|
||||
sixelSizeLimit: 25000000,
|
||||
sixelSizeLimit: 33554432,
|
||||
storageLimit: 128,
|
||||
showPlaceholder: true,
|
||||
iipSupport: true,
|
||||
iipSizeLimit: 20000000,
|
||||
iipSizeLimit: 33554432,
|
||||
kittySupport: true,
|
||||
kittySizeLimit: 20000000
|
||||
kittySizeLimit: 33554432
|
||||
};
|
||||
deepStrictEqual(await ctx.page.evaluate(`window.imageAddon._opts`), DEFAULT_OPTIONS);
|
||||
});
|
||||
@@ -312,6 +312,68 @@ test.describe('ImageAddon', () => {
|
||||
deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[4][1]);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('IIP - QOI support', () => {
|
||||
test('palette should yield same bytes from PNG and QOI', async () => {
|
||||
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=525:${PALETTE_PNG_BASE64}\x07`);
|
||||
deepStrictEqual(await getOrigSize(1), [640, 80]);
|
||||
await ctx.proxy.write(`\x1b[10H\x1b]1337;File=inline=1;size=${qoiData.length}:${PALETTE_QOI_BASE64}\x07`);
|
||||
deepStrictEqual(await getOrigSize(2), [640, 80]);
|
||||
const pngScrape = await getImageAtBufferCell(0, 0);
|
||||
const qoiScrape = await getImageAtBufferCell(0, 11);
|
||||
deepStrictEqual(qoiScrape, pngScrape);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('IIP - resizing', () => {
|
||||
/**
|
||||
* The correct resize behavior is wonky and needs to be tested against iTerm2,
|
||||
* especially for missing params to derive correct default behavior.
|
||||
* We document the current behavior here w'o claiming to be fully in line with iTerm2.
|
||||
* ref: https://iterm2.com/documentation-images.html
|
||||
* imgcat: https://iterm2.com/utilities/imgcat
|
||||
*
|
||||
* NOTE: QOI has a slightly different parse path, thus we test resizing explicitly
|
||||
*/
|
||||
const images = [
|
||||
['palette.png', 525, PALETTE_PNG_BASE64],
|
||||
['palette.qoi', qoiData.length, PALETTE_QOI_BASE64]
|
||||
];
|
||||
for (const [name, size, payload] of images) {
|
||||
test(name + ': N --> width=20 height=5 preserveAspectRatio=0', async () => {
|
||||
// cell based resize
|
||||
const header = 'width=20;height=5;preserveAspectRatio=0';
|
||||
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header}:${payload}\x07`);
|
||||
const dim = await getDimensions();
|
||||
deepStrictEqual(await getOrigSize(1), [dim.cellWidth * 20, dim.cellHeight * 5]);
|
||||
});
|
||||
test(name + ': Npx --> width=320px height=160px preserveAspectRatio=0', async () => {
|
||||
// pixel based resize
|
||||
const header = 'width=320px;height=160px;preserveAspectRatio=0';
|
||||
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header}:${payload}\x07`);
|
||||
deepStrictEqual(await getOrigSize(1), [320, 160]);
|
||||
});
|
||||
test(name + ': N% --> width=50% height=30% preserveAspectRatio=0', async () => {
|
||||
// % of viewport resize
|
||||
const header = 'width=50%;height=30%;preserveAspectRatio=0';
|
||||
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header}:${payload}\x07`);
|
||||
const dim = await getDimensions();
|
||||
deepStrictEqual(await getOrigSize(1), [Math.floor(dim.width * 0.5), Math.floor(dim.height * 0.3)]);
|
||||
});
|
||||
test(name + ': ommitted dimension assumes preserveAspectRatio=1', async () => {
|
||||
// width provided in percent
|
||||
const header = 'width=50%';
|
||||
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header}:${payload}\x07`);
|
||||
const dim = await getDimensions();
|
||||
const width = Math.floor(dim.width * 0.5);
|
||||
deepStrictEqual(await getOrigSize(1), [width, Math.floor(width * 80 / 640)]);
|
||||
// height provided in pixel
|
||||
const header2 = 'height=200px';
|
||||
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header2}:${payload}\x07`);
|
||||
deepStrictEqual(await getOrigSize(2), [Math.floor(200 * 640 / 80), 200]);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -345,3 +407,7 @@ async function getOrigSize(id: number): Promise<[number, number]> {
|
||||
window.imageAddon._storage._images.get(${id}).orig.height
|
||||
]`);
|
||||
}
|
||||
|
||||
async function getImageAtBufferCell(x: number, y: number): Promise<string | undefined> {
|
||||
return ctx.page.evaluate<any>(`window.imageAddon.getImageAtBufferCell(${x}, ${y})?.toDataURL('image/png')`);
|
||||
}
|
||||
|
||||
@@ -109,11 +109,6 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
// TODO: Distinguish lowercase delete selectors (placement only) from uppercase (placement + free data)
|
||||
|
||||
test.beforeEach(async ({}, testInfo) => {
|
||||
// DEBT: This test never worked on webkit
|
||||
if (ctx.browser.browserType().name() === 'webkit') {
|
||||
testInfo.skip();
|
||||
return;
|
||||
}
|
||||
await ctx.page.evaluate(`
|
||||
window.term.reset()
|
||||
window.imageAddon?.dispose();
|
||||
@@ -327,7 +322,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
test('chunked transfer responds OK on final chunk when i= on first only', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
(window as any).term.onData((data: string) => {
|
||||
(window as any).kittyResponse = data;
|
||||
});
|
||||
});
|
||||
|
||||
const half = Math.floor(KITTY_BLACK_1X1_BASE64.length / 2);
|
||||
@@ -547,7 +544,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
let response = '';
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
(window as any).term.onData((data: string) => {
|
||||
(window as any).kittyResponse = data;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write('\x1b_Gi=31,a=q;\x1b\\');
|
||||
@@ -561,7 +560,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
let response = '';
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
(window as any).term.onData((data: string) => {
|
||||
(window as any).kittyResponse = data;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=42,a=q,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
@@ -586,7 +587,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
let response = '';
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
(window as any).term.onData((data: string) => {
|
||||
(window as any).kittyResponse = data;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write('\x1b_Gi=60,a=q,f=100;!!!invalid!!!\x1b\\');
|
||||
@@ -600,7 +603,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
let response = '';
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
(window as any).term.onData((data: string) => {
|
||||
(window as any).kittyResponse = data;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write('\x1b_Gi=70,a=q,f=24;AAAA\x1b\\');
|
||||
@@ -613,7 +618,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
test('suppresses OK response when q=1', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyGotResponse = false;
|
||||
(window as any).term.onData(() => { (window as any).kittyGotResponse = true; });
|
||||
(window as any).term.onData(() => {
|
||||
(window as any).kittyGotResponse = true;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=80,a=q,q=1,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
@@ -622,10 +629,26 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
strictEqual(await ctx.page.evaluate('window.kittyGotResponse'), false);
|
||||
});
|
||||
|
||||
test('suppresses OK response when q=2', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyGotResponse = false;
|
||||
(window as any).term.onData(() => {
|
||||
(window as any).kittyGotResponse = true;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=81,a=q,q=2,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await timeout(100);
|
||||
|
||||
strictEqual(await ctx.page.evaluate('window.kittyGotResponse'), false);
|
||||
});
|
||||
|
||||
test('suppresses error response when q=2', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyGotResponse = false;
|
||||
(window as any).term.onData(() => { (window as any).kittyGotResponse = true; });
|
||||
(window as any).term.onData(() => {
|
||||
(window as any).kittyGotResponse = true;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write('\x1b_Gi=90,a=q,q=2,f=100;!!!invalid!!!\x1b\\');
|
||||
@@ -638,7 +661,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
let response = '';
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
(window as any).term.onData((data: string) => {
|
||||
(window as any).kittyResponse = data;
|
||||
});
|
||||
});
|
||||
|
||||
// Per spec: "Specifying both i and I keys in any command is an error"
|
||||
@@ -653,7 +678,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
let response = '';
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
(window as any).term.onData((data: string) => {
|
||||
(window as any).kittyResponse = data;
|
||||
});
|
||||
});
|
||||
|
||||
// Delete command with both i and I (no payload case)
|
||||
@@ -669,7 +696,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
test('a=t sends EINVAL on decode error when id is specified', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
(window as any).term.onData((data: string) => {
|
||||
(window as any).kittyResponse = data;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write('\x1b_Gi=110,a=t,f=100;!!!invalid!!!\x1b\\');
|
||||
@@ -682,7 +711,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
test('a=t sends no response on decode error without id', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyGotResponse = false;
|
||||
(window as any).term.onData(() => { (window as any).kittyGotResponse = true; });
|
||||
(window as any).term.onData(() => {
|
||||
(window as any).kittyGotResponse = true;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write('\x1b_Ga=t,f=100;!!!invalid!!!\x1b\\');
|
||||
@@ -694,7 +725,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
test('a=T sends EINVAL on decode error when id is specified', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
(window as any).term.onData((data: string) => {
|
||||
(window as any).kittyResponse = data;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write('\x1b_Gi=120,a=T,f=100;!!!invalid!!!\x1b\\');
|
||||
@@ -707,7 +740,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
test('a=T sends no response on decode error without id', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyGotResponse = false;
|
||||
(window as any).term.onData(() => { (window as any).kittyGotResponse = true; });
|
||||
(window as any).term.onData(() => {
|
||||
(window as any).kittyGotResponse = true;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write('\x1b_Ga=T,f=100;!!!invalid!!!\x1b\\');
|
||||
@@ -719,7 +754,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
test('a=T sends EINVAL when raw pixel render fails (missing dimensions)', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
(window as any).term.onData((data: string) => {
|
||||
(window as any).kittyResponse = data;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=130,a=T,f=24;${RAW_RGB_1X1_BLACK}\x1b\\`);
|
||||
@@ -732,7 +769,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
test('a=T sends OK on successful render with id', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
(window as any).term.onData((data: string) => {
|
||||
(window as any).kittyResponse = data;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=140,a=T,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
@@ -745,7 +784,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
test('a=t sends OK on successful transmit with id', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
(window as any).term.onData((data: string) => {
|
||||
(window as any).kittyResponse = data;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=150,a=t,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
@@ -758,7 +799,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
test('a=t EINVAL suppressed by q=2', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyGotResponse = false;
|
||||
(window as any).term.onData(() => { (window as any).kittyGotResponse = true; });
|
||||
(window as any).term.onData(() => {
|
||||
(window as any).kittyGotResponse = true;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write('\x1b_Gi=160,a=t,q=2,f=100;!!!invalid!!!\x1b\\');
|
||||
@@ -770,7 +813,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
test('a=T EINVAL suppressed by q=2', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyGotResponse = false;
|
||||
(window as any).term.onData(() => { (window as any).kittyGotResponse = true; });
|
||||
(window as any).term.onData(() => {
|
||||
(window as any).kittyGotResponse = true;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write('\x1b_Gi=170,a=T,q=2,f=100;!!!invalid!!!\x1b\\');
|
||||
@@ -782,7 +827,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
test('a=t OK suppressed by q=1', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyGotResponse = false;
|
||||
(window as any).term.onData(() => { (window as any).kittyGotResponse = true; });
|
||||
(window as any).term.onData(() => {
|
||||
(window as any).kittyGotResponse = true;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=180,a=t,q=1,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
@@ -794,7 +841,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
test('a=T OK suppressed by q=1', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyGotResponse = false;
|
||||
(window as any).term.onData(() => { (window as any).kittyGotResponse = true; });
|
||||
(window as any).term.onData(() => {
|
||||
(window as any).kittyGotResponse = true;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=190,a=T,q=1,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
@@ -802,13 +851,43 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
|
||||
strictEqual(await ctx.page.evaluate('window.kittyGotResponse'), false);
|
||||
});
|
||||
|
||||
test('a=t OK suppressed by q=2', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyGotResponse = false;
|
||||
(window as any).term.onData(() => {
|
||||
(window as any).kittyGotResponse = true;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=181,a=t,q=2,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await timeout(100);
|
||||
|
||||
strictEqual(await ctx.page.evaluate('window.kittyGotResponse'), false);
|
||||
});
|
||||
|
||||
test('a=T OK suppressed by q=2', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyGotResponse = false;
|
||||
(window as any).term.onData(() => {
|
||||
(window as any).kittyGotResponse = true;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=191,a=T,q=2,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await timeout(100);
|
||||
|
||||
strictEqual(await ctx.page.evaluate('window.kittyGotResponse'), false);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Transmission medium rejection', () => {
|
||||
test('query rejects t=f (file transmission)', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
(window as any).term.onData((data: string) => {
|
||||
(window as any).kittyResponse = data;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=200,a=q,t=f,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
@@ -821,7 +900,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
test('query rejects t=s (shared memory)', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
(window as any).term.onData((data: string) => {
|
||||
(window as any).kittyResponse = data;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=201,a=q,t=s,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
@@ -834,7 +915,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
test('query rejects t=t (temp file)', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
(window as any).term.onData((data: string) => {
|
||||
(window as any).kittyResponse = data;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=202,a=q,t=t,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
@@ -847,7 +930,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
test('query accepts t=d (direct transmission)', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
(window as any).term.onData((data: string) => {
|
||||
(window as any).kittyResponse = data;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=203,a=q,t=d,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
@@ -860,7 +945,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
test('query without t key defaults to direct (OK)', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
(window as any).term.onData((data: string) => {
|
||||
(window as any).kittyResponse = data;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=204,a=q,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
@@ -873,7 +960,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
test('transmit rejects t=f with id (EINVAL response)', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
(window as any).term.onData((data: string) => {
|
||||
(window as any).kittyResponse = data;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=300,a=t,t=f,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
@@ -886,7 +975,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
test('transmit rejects t=s with id (EINVAL response)', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
(window as any).term.onData((data: string) => {
|
||||
(window as any).kittyResponse = data;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=301,a=t,t=s,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
@@ -899,7 +990,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
test('transmit rejects t=t with id (EINVAL response)', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
(window as any).term.onData((data: string) => {
|
||||
(window as any).kittyResponse = data;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=302,a=t,t=t,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
@@ -912,7 +1005,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
test('transmit rejects t=f without id (no response)', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
(window as any).term.onData((data: string) => {
|
||||
(window as any).kittyResponse = data;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Ga=t,t=f,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
@@ -925,7 +1020,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
test('transmit+display rejects t=f with id (EINVAL response)', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
(window as any).term.onData((data: string) => {
|
||||
(window as any).kittyResponse = data;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=310,a=T,t=f,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
@@ -938,7 +1035,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
test('transmit+display rejects t=s with id (EINVAL response)', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
(window as any).term.onData((data: string) => {
|
||||
(window as any).kittyResponse = data;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=311,a=T,t=s,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
@@ -951,7 +1050,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
test('transmit+display rejects t=t with id (EINVAL response)', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
(window as any).term.onData((data: string) => {
|
||||
(window as any).kittyResponse = data;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=312,a=T,t=t,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
@@ -964,7 +1065,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
test('transmit+display rejects t=f without id (no response)', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
(window as any).term.onData((data: string) => {
|
||||
(window as any).kittyResponse = data;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Ga=T,t=f,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
@@ -995,7 +1098,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
(window as any).term.onData((data: string) => {
|
||||
(window as any).kittyResponse = data;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Ga=p,i=211\x1b\\`);
|
||||
@@ -1008,7 +1113,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
test('responds ENOENT for non-existent image id', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
(window as any).term.onData((data: string) => {
|
||||
(window as any).kittyResponse = data;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Ga=p,i=9999\x1b\\`);
|
||||
@@ -1021,7 +1128,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
test('without id sends no response', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyGotResponse = false;
|
||||
(window as any).term.onData(() => { (window as any).kittyGotResponse = true; });
|
||||
(window as any).term.onData(() => {
|
||||
(window as any).kittyGotResponse = true;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Ga=p\x1b\\`);
|
||||
@@ -1141,7 +1250,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyGotResponse = false;
|
||||
(window as any).term.onData(() => { (window as any).kittyGotResponse = true; });
|
||||
(window as any).term.onData(() => {
|
||||
(window as any).kittyGotResponse = true;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Ga=p,i=220,q=1\x1b\\`);
|
||||
@@ -1151,10 +1262,30 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
strictEqual(await getImageStorageLength(), 1);
|
||||
});
|
||||
|
||||
test('OK response suppressed by q=2', async () => {
|
||||
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=221;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await timeout(100);
|
||||
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyGotResponse = false;
|
||||
(window as any).term.onData(() => {
|
||||
(window as any).kittyGotResponse = true;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Ga=p,i=221,q=2\x1b\\`);
|
||||
await timeout(100);
|
||||
|
||||
strictEqual(await ctx.page.evaluate('window.kittyGotResponse'), false);
|
||||
strictEqual(await getImageStorageLength(), 1);
|
||||
});
|
||||
|
||||
test('ENOENT error suppressed by q=2', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyGotResponse = false;
|
||||
(window as any).term.onData(() => { (window as any).kittyGotResponse = true; });
|
||||
(window as any).term.onData(() => {
|
||||
(window as any).kittyGotResponse = true;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Ga=p,i=9998,q=2\x1b\\`);
|
||||
@@ -1166,7 +1297,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
test('ENOENT still reported when q=1 (only suppresses OK)', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
(window as any).term.onData((data: string) => {
|
||||
(window as any).kittyResponse = data;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Ga=p,i=9997,q=1\x1b\\`);
|
||||
@@ -1221,7 +1354,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
(window as any).term.onData((data: string) => {
|
||||
(window as any).kittyResponse = data;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Ga=p,i=224,p=42\x1b\\`);
|
||||
@@ -1756,7 +1891,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
test('responds with OK for valid 200x100 PNG query', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
(window as any).term.onData((data: string) => {
|
||||
(window as any).kittyResponse = data;
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=600,a=q,f=100;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
|
||||
@@ -1905,7 +2042,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
test('query returns EINVAL without dimensions', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
(window as any).term.onData((data: string) => {
|
||||
(window as any).kittyResponse = data;
|
||||
});
|
||||
});
|
||||
await ctx.proxy.write(`\x1b_Gi=200,a=q,f=24;${RAW_RGB_1X1_BLACK}\x1b\\`);
|
||||
await timeout(100);
|
||||
@@ -1916,7 +2055,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
test('query returns EINVAL for insufficient pixel data', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
(window as any).term.onData((data: string) => {
|
||||
(window as any).kittyResponse = data;
|
||||
});
|
||||
});
|
||||
await ctx.proxy.write(`\x1b_Gi=201,a=q,f=24,s=2,v=2;${RAW_RGB_1X1_BLACK}\x1b\\`);
|
||||
await timeout(100);
|
||||
@@ -1927,7 +2068,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
test('query returns OK for valid RGB data with correct dimensions', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
(window as any).term.onData((data: string) => {
|
||||
(window as any).kittyResponse = data;
|
||||
});
|
||||
});
|
||||
await ctx.proxy.write(`\x1b_Gi=202,a=q,f=24,s=1,v=1;${RAW_RGB_1X1_RED}\x1b\\`);
|
||||
await timeout(100);
|
||||
@@ -2039,7 +2182,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
test('query returns EINVAL without dimensions', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
(window as any).term.onData((data: string) => {
|
||||
(window as any).kittyResponse = data;
|
||||
});
|
||||
});
|
||||
await ctx.proxy.write(`\x1b_Gi=300,a=q,f=32;${RAW_RGBA_1X1_RED}\x1b\\`);
|
||||
await timeout(100);
|
||||
@@ -2050,7 +2195,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
test('query returns EINVAL for insufficient pixel data', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
(window as any).term.onData((data: string) => {
|
||||
(window as any).kittyResponse = data;
|
||||
});
|
||||
});
|
||||
await ctx.proxy.write(`\x1b_Gi=301,a=q,f=32,s=2,v=2;${RAW_RGBA_1X1_RED}\x1b\\`);
|
||||
await timeout(100);
|
||||
@@ -2061,7 +2208,9 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
test('query returns OK for valid RGBA data with correct dimensions', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
(window as any).term.onData((data: string) => {
|
||||
(window as any).kittyResponse = data;
|
||||
});
|
||||
});
|
||||
await ctx.proxy.write(`\x1b_Gi=302,a=q,f=32,s=1,v=1;${RAW_RGBA_1X1_RED}\x1b\\`);
|
||||
await timeout(100);
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"devDependencies": {
|
||||
"@types/lru-cache": "^5.1.0",
|
||||
"@types/opentype.js": "^0.7.0",
|
||||
"axios": "^1.6.0",
|
||||
"axios": "^1.15.0",
|
||||
"font-finder": "^1.1.0",
|
||||
"mkdirp": "0.5.5",
|
||||
"yauzl": "^3.2.1"
|
||||
|
||||
@@ -65,9 +65,8 @@ export default async function load(fontFamily: string, cacheSize: number): Promi
|
||||
console.error(err.name, err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Latest proposal https://bugs.chromium.org/p/chromium/issues/detail?id=1312603
|
||||
else if (typeof window !== 'undefined' && 'queryLocalFonts' in window) {
|
||||
} else if (typeof window !== 'undefined' && 'queryLocalFonts' in window) {
|
||||
// Latest proposal https://bugs.chromium.org/p/chromium/issues/detail?id=1312603
|
||||
const fonts: Record<string, IFontMetadata[]> = {};
|
||||
try {
|
||||
const fontsIterator = await (window as any).queryLocalFonts();
|
||||
|
||||
@@ -46,7 +46,9 @@ describe('LigaturesAddon', () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
onRefresh = Object.assign((..._args: any[]) => { onRefresh.called = true; onRefresh.callCount++; }, { called: false, callCount: 0 });
|
||||
onRefresh = Object.assign((..._args: any[]) => {
|
||||
onRefresh.called = true; onRefresh.callCount++;
|
||||
}, { called: false, callCount: 0 });
|
||||
term = new MockTerminal(onRefresh);
|
||||
ligatureSupport.enableLigatures(term as any);
|
||||
});
|
||||
@@ -127,7 +129,9 @@ class MockTerminal {
|
||||
public deregisterCharacterJoiner(id: number): void {
|
||||
this.joiner = undefined;
|
||||
}
|
||||
public get options(): { [name: string]: string | number } { return this._options; }
|
||||
public get options(): { [name: string]: string | number } {
|
||||
return this._options;
|
||||
}
|
||||
public set options(options: { [name: string]: string | number }) {
|
||||
for (const key in this._options) {
|
||||
this._options[key] = options[key];
|
||||
|
||||
@@ -64,7 +64,9 @@ export class DecorationManager extends Disposable {
|
||||
public createActiveDecoration(result: ISearchResult, options: ISearchDecorationOptions): IMultiHighlight | undefined {
|
||||
const decorations = this._createResultDecorations(result, options, true);
|
||||
if (decorations) {
|
||||
return { decorations, match: result, dispose() { dispose(decorations); } };
|
||||
return { decorations, match: result, dispose() {
|
||||
dispose(decorations);
|
||||
} };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -85,7 +87,9 @@ export class DecorationManager extends Disposable {
|
||||
*/
|
||||
private _storeDecoration(decoration: IDecoration, match: ISearchResult): void {
|
||||
this._highlightedLines.add(decoration.marker.line);
|
||||
this._highlightDecorations.push({ decoration, match, dispose() { decoration.dispose(); } });
|
||||
this._highlightDecorations.push({ decoration, match, dispose() {
|
||||
decoration.dispose();
|
||||
} });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,7 +25,9 @@ export class SearchResultTracker extends Disposable {
|
||||
private _selectedDecoration: ISelectedDecoration | undefined;
|
||||
|
||||
private readonly _onDidChangeResults = this._register(new Emitter<ISearchResultChangeEvent>());
|
||||
public get onDidChangeResults(): IEvent<ISearchResultChangeEvent> { return this._onDidChangeResults.event; }
|
||||
public get onDidChangeResults(): IEvent<ISearchResultChangeEvent> {
|
||||
return this._onDidChangeResults.event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current search results.
|
||||
|
||||
@@ -29,12 +29,20 @@ class TestSelectionService {
|
||||
this._model = new SelectionModel(bufferService);
|
||||
}
|
||||
|
||||
public get model(): SelectionModel { return this._model; }
|
||||
public get model(): SelectionModel {
|
||||
return this._model;
|
||||
}
|
||||
|
||||
public get hasSelection(): boolean { return this._hasSelection; }
|
||||
public get hasSelection(): boolean {
|
||||
return this._hasSelection;
|
||||
}
|
||||
|
||||
public get selectionStart(): [number, number] | undefined { return this._model.finalSelectionStart; }
|
||||
public get selectionEnd(): [number, number] | undefined { return this._model.finalSelectionEnd; }
|
||||
public get selectionStart(): [number, number] | undefined {
|
||||
return this._model.finalSelectionStart;
|
||||
}
|
||||
public get selectionEnd(): [number, number] | undefined {
|
||||
return this._model.finalSelectionEnd;
|
||||
}
|
||||
|
||||
public setSelection(col: number, row: number, length: number): void {
|
||||
this._model.selectionStart = [col, row];
|
||||
|
||||
@@ -70,7 +70,9 @@ abstract class BaseSerializeHandler {
|
||||
protected _rowEnd(row: number, isLastRow: boolean): void { }
|
||||
protected _beforeSerialize(rows: number, startRow: number, endRow: number): void { }
|
||||
protected _afterSerialize(): void { }
|
||||
protected _serializeString(excludeFinalCursorPosition?: boolean): string { return ''; }
|
||||
protected _serializeString(excludeFinalCursorPosition?: boolean): string {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function equalFg(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
|
||||
@@ -291,25 +293,39 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
} else {
|
||||
if (fgChanged) {
|
||||
const color = cell.getFgColor();
|
||||
if (cell.isFgRGB()) { sgrSeq.push(38, 2, (color >>> 16) & 0xFF, (color >>> 8) & 0xFF, color & 0xFF); }
|
||||
else if (cell.isFgPalette()) {
|
||||
if (color >= 16) { sgrSeq.push(38, 5, color); }
|
||||
else { sgrSeq.push(color & 8 ? 90 + (color & 7) : 30 + (color & 7)); }
|
||||
if (cell.isFgRGB()) {
|
||||
sgrSeq.push(38, 2, (color >>> 16) & 0xFF, (color >>> 8) & 0xFF, color & 0xFF);
|
||||
} else if (cell.isFgPalette()) {
|
||||
if (color >= 16) {
|
||||
sgrSeq.push(38, 5, color);
|
||||
} else {
|
||||
sgrSeq.push(color & 8 ? 90 + (color & 7) : 30 + (color & 7));
|
||||
}
|
||||
} else {
|
||||
sgrSeq.push(39);
|
||||
}
|
||||
else { sgrSeq.push(39); }
|
||||
}
|
||||
if (bgChanged) {
|
||||
const color = cell.getBgColor();
|
||||
if (cell.isBgRGB()) { sgrSeq.push(48, 2, (color >>> 16) & 0xFF, (color >>> 8) & 0xFF, color & 0xFF); }
|
||||
else if (cell.isBgPalette()) {
|
||||
if (color >= 16) { sgrSeq.push(48, 5, color); }
|
||||
else { sgrSeq.push(color & 8 ? 100 + (color & 7) : 40 + (color & 7)); }
|
||||
if (cell.isBgRGB()) {
|
||||
sgrSeq.push(48, 2, (color >>> 16) & 0xFF, (color >>> 8) & 0xFF, color & 0xFF);
|
||||
} else if (cell.isBgPalette()) {
|
||||
if (color >= 16) {
|
||||
sgrSeq.push(48, 5, color);
|
||||
} else {
|
||||
sgrSeq.push(color & 8 ? 100 + (color & 7) : 40 + (color & 7));
|
||||
}
|
||||
} else {
|
||||
sgrSeq.push(49);
|
||||
}
|
||||
else { sgrSeq.push(49); }
|
||||
}
|
||||
if (flagsChanged) {
|
||||
if (cell.isInverse() !== oldCell.isInverse()) { sgrSeq.push(cell.isInverse() ? 7 : 27); }
|
||||
if (cell.isBold() !== oldCell.isBold()) { sgrSeq.push(cell.isBold() ? 1 : 22); }
|
||||
if (cell.isInverse() !== oldCell.isInverse()) {
|
||||
sgrSeq.push(cell.isInverse() ? 7 : 27);
|
||||
}
|
||||
if (cell.isBold() !== oldCell.isBold()) {
|
||||
sgrSeq.push(cell.isBold() ? 1 : 22);
|
||||
}
|
||||
if (!equalUnderline(cell, oldCell)) {
|
||||
const style = cell.getUnderlineStyle();
|
||||
if (style === UnderlineStyle.NONE) {
|
||||
@@ -332,12 +348,24 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
} else if (cell.isUnderline() !== oldCell.isUnderline()) {
|
||||
sgrSeq.push(cell.isUnderline() ? 4 : 24);
|
||||
}
|
||||
if (cell.isOverline() !== oldCell.isOverline()) { sgrSeq.push(cell.isOverline() ? 53 : 55); }
|
||||
if (cell.isBlink() !== oldCell.isBlink()) { sgrSeq.push(cell.isBlink() ? 5 : 25); }
|
||||
if (cell.isInvisible() !== oldCell.isInvisible()) { sgrSeq.push(cell.isInvisible() ? 8 : 28); }
|
||||
if (cell.isItalic() !== oldCell.isItalic()) { sgrSeq.push(cell.isItalic() ? 3 : 23); }
|
||||
if (cell.isDim() !== oldCell.isDim()) { sgrSeq.push(cell.isDim() ? 2 : 22); }
|
||||
if (cell.isStrikethrough() !== oldCell.isStrikethrough()) { sgrSeq.push(cell.isStrikethrough() ? 9 : 29); }
|
||||
if (cell.isOverline() !== oldCell.isOverline()) {
|
||||
sgrSeq.push(cell.isOverline() ? 53 : 55);
|
||||
}
|
||||
if (cell.isBlink() !== oldCell.isBlink()) {
|
||||
sgrSeq.push(cell.isBlink() ? 5 : 25);
|
||||
}
|
||||
if (cell.isInvisible() !== oldCell.isInvisible()) {
|
||||
sgrSeq.push(cell.isInvisible() ? 8 : 28);
|
||||
}
|
||||
if (cell.isItalic() !== oldCell.isItalic()) {
|
||||
sgrSeq.push(cell.isItalic() ? 3 : 23);
|
||||
}
|
||||
if (cell.isDim() !== oldCell.isDim()) {
|
||||
sgrSeq.push(cell.isDim() ? 2 : 22);
|
||||
}
|
||||
if (cell.isStrikethrough() !== oldCell.isStrikethrough()) {
|
||||
sgrSeq.push(cell.isStrikethrough() ? 9 : 29);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -646,8 +674,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
|
||||
// For xterm headless: fallback to ansi colors
|
||||
if ((_terminal as any)._core._themeService) {
|
||||
this._ansiColors = (_terminal as any)._core._themeService.colors.ansi;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
this._ansiColors = DEFAULT_ANSI_COLORS;
|
||||
}
|
||||
}
|
||||
@@ -752,8 +779,12 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
|
||||
content.push('background-color: ' + bgHexColor + ';');
|
||||
}
|
||||
|
||||
if (cell.isInverse()) { content.push('color: #000000; background-color: #BFBFBF;'); }
|
||||
if (cell.isBold()) { content.push('font-weight: bold;'); }
|
||||
if (cell.isInverse()) {
|
||||
content.push('color: #000000; background-color: #BFBFBF;');
|
||||
}
|
||||
if (cell.isBold()) {
|
||||
content.push('font-weight: bold;');
|
||||
}
|
||||
|
||||
// Handle text-decoration (underline, overline, strikethrough, blink)
|
||||
const decorations: string[] = [];
|
||||
@@ -781,9 +812,15 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (cell.isInvisible()) { content.push('visibility: hidden;'); }
|
||||
if (cell.isItalic()) { content.push('font-style: italic;'); }
|
||||
if (cell.isDim()) { content.push('opacity: 0.5;'); }
|
||||
if (cell.isInvisible()) {
|
||||
content.push('visibility: hidden;');
|
||||
}
|
||||
if (cell.isItalic()) {
|
||||
content.push('font-style: italic;');
|
||||
}
|
||||
if (cell.isDim()) {
|
||||
content.push('opacity: 0.5;');
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
@@ -49,7 +49,14 @@ test.describe('SerializeAddon', () => {
|
||||
for (let i = 0; i < buffer.length; i++) {
|
||||
// Do this intentionally to get content of underlining source
|
||||
const bufferLine = buffer.getLine(i)._line;
|
||||
lines.push(JSON.stringify(bufferLine));
|
||||
lines.push(JSON.stringify(bufferLine, (key, value) => {
|
||||
// BufferLine caches are internal/transient and can legitimately differ
|
||||
// across equivalent terminal states.
|
||||
if (key === '_stringCache' || key === '_stringCacheEntryRef') {
|
||||
return undefined;
|
||||
}
|
||||
return value;
|
||||
}));
|
||||
}
|
||||
return {
|
||||
x: buffer.cursorX,
|
||||
|
||||
@@ -33,16 +33,16 @@ test.describe('WebFontsAddon', () => {
|
||||
test.describe('font loading at runtime', () => {
|
||||
test('loadFonts (JS)', async () => {
|
||||
await ctx.page.evaluate(`
|
||||
const ff1 = new FontFace('Kongtext', "url(/kongtext.regular.ttf) format('truetype')");
|
||||
const ff2 = new FontFace('BPdots', "url(/bpdots.regular.otf) format('opentype')");
|
||||
const ff1 = new FontFace('Kongtext', "url(/fonts/kongtext.regular.ttf) format('truetype')");
|
||||
const ff2 = new FontFace('BPdots', "url(/fonts/bpdots.regular.otf) format('opentype')");
|
||||
loadFonts([ff1, ff2]);
|
||||
`);
|
||||
deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'loaded' }, { family: 'BPdots', status: 'loaded' }]);
|
||||
});
|
||||
test('loadFonts (CSS, unquoted)', async () => {
|
||||
await ctx.page.evaluate(`
|
||||
document.styleSheets[0].insertRule("@font-face {font-family: Kongtext; src: url(/kongtext.regular.ttf) format('truetype')}", 0);
|
||||
document.styleSheets[0].insertRule("@font-face {font-family: BPdots; src: url(/bpdots.regular.otf) format('opentype')}", 1);
|
||||
document.styleSheets[0].insertRule("@font-face {font-family: Kongtext; src: url(/fonts/kongtext.regular.ttf) format('truetype')}", 0);
|
||||
document.styleSheets[0].insertRule("@font-face {font-family: BPdots; src: url(/fonts/bpdots.regular.otf) format('opentype')}", 1);
|
||||
loadFonts(['Kongtext', 'BPdots']);
|
||||
`);
|
||||
deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'loaded' }, { family: 'BPdots', status: 'loaded' }]);
|
||||
@@ -50,8 +50,8 @@ test.describe('WebFontsAddon', () => {
|
||||
test('loadFonts (CSS, quoted)', async ({ browser }) => {
|
||||
// NOTE: firefox preserves family quotes from CSS rules in fontface, all other browsers unquote them
|
||||
await ctx.page.evaluate(`
|
||||
document.styleSheets[0].insertRule("@font-face {font-family: 'Kongtext'; src: url(/kongtext.regular.ttf) format('truetype')}", 0);
|
||||
document.styleSheets[0].insertRule("@font-face {font-family: 'BPdots'; src: url(/bpdots.regular.otf) format('opentype')}", 1);
|
||||
document.styleSheets[0].insertRule("@font-face {font-family: 'Kongtext'; src: url(/fonts/kongtext.regular.ttf) format('truetype')}", 0);
|
||||
document.styleSheets[0].insertRule("@font-face {font-family: 'BPdots'; src: url(/fonts/bpdots.regular.otf) format('opentype')}", 1);
|
||||
loadFonts(['Kongtext', 'BPdots']);
|
||||
`);
|
||||
if (browser.browserType().name() === 'firefox') {
|
||||
@@ -63,8 +63,8 @@ test.describe('WebFontsAddon', () => {
|
||||
test('FontFace hashing', async () => {
|
||||
// multiple calls of `loadFonts` with the same objects shall not bloat document.fonts
|
||||
await ctx.page.evaluate(`
|
||||
const ff1 = new FontFace('Kongtext', "url(/kongtext.regular.ttf) format('truetype')");
|
||||
const ff2 = new FontFace('BPdots', "url(/bpdots.regular.otf) format('opentype')");
|
||||
const ff1 = new FontFace('Kongtext', "url(/fonts/kongtext.regular.ttf) format('truetype')");
|
||||
const ff2 = new FontFace('BPdots', "url(/fonts/bpdots.regular.otf) format('opentype')");
|
||||
loadFonts([ff1, ff2]);
|
||||
loadFonts([ff1, ff2]);
|
||||
loadFonts([ff1, ff2]).then(() => loadFonts([ff1, ff2]));
|
||||
@@ -76,7 +76,7 @@ test.describe('WebFontsAddon', () => {
|
||||
// to make this test work, we exclude the default measurement char W (x57) by restricting unicode-range
|
||||
// now the browser will postpone font loading until codepoint is hit --> wrong glyph metrics on first usage
|
||||
const data = await ctx.page.evaluate(`
|
||||
document.styleSheets[0].insertRule("@font-face {font-family: Kongtext; src: url(/kongtext.regular.ttf) format('truetype'); unicode-range: U+00A0-00FF}", 0);
|
||||
document.styleSheets[0].insertRule("@font-face {font-family: Kongtext; src: url(/fonts/kongtext.regular.ttf) format('truetype'); unicode-range: U+00A0-00FF}", 0);
|
||||
`);
|
||||
deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'unloaded' }]);
|
||||
|
||||
@@ -88,7 +88,11 @@ test.describe('WebFontsAddon', () => {
|
||||
|
||||
// safari loads the font, firefox & chrome dont
|
||||
if (browser.browserType().name() === 'webkit') {
|
||||
deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'loaded' }]);
|
||||
try {
|
||||
deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'loading' }]);
|
||||
} catch (e) {
|
||||
deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'loaded' }]);
|
||||
}
|
||||
} else {
|
||||
deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'unloaded' }]);
|
||||
}
|
||||
@@ -103,7 +107,10 @@ test.describe('WebFontsAddon', () => {
|
||||
window.helperTerm.open(term.element);
|
||||
`);
|
||||
await timeout(100);
|
||||
deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'loaded' }]);
|
||||
if (browser.browserType().name() !== 'webkit') {
|
||||
// safari sometimes reports 'unloadeed'?
|
||||
deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'loaded' }]);
|
||||
}
|
||||
|
||||
// cleanup this messy test case
|
||||
await ctx.page.evaluate(`
|
||||
|
||||
@@ -40,7 +40,9 @@ export class CursorBlinkStateManager {
|
||||
}
|
||||
}
|
||||
|
||||
public get isPaused(): boolean { return !(this._blinkStartTimeout || this._blinkInterval); }
|
||||
public get isPaused(): boolean {
|
||||
return !(this._blinkStartTimeout || this._blinkInterval);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._blinkInterval) {
|
||||
|
||||
@@ -78,9 +78,11 @@ void main() {
|
||||
}`);
|
||||
}
|
||||
|
||||
const INDICES_PER_CELL = 11;
|
||||
const BYTES_PER_CELL = INDICES_PER_CELL * Float32Array.BYTES_PER_ELEMENT;
|
||||
const CELL_POSITION_INDICES = 2;
|
||||
const enum Constants {
|
||||
INDICES_PER_CELL = 11,
|
||||
BYTES_PER_CELL = INDICES_PER_CELL * 4/* Float32Array.BYTES_PER_ELEMENT */,
|
||||
CELL_POSITION_INDICES = 2
|
||||
}
|
||||
|
||||
// Work variables to avoid garbage collection
|
||||
let $i = 0;
|
||||
@@ -160,22 +162,22 @@ export class GlyphRenderer extends Disposable {
|
||||
this._register(toDisposable(() => gl.deleteBuffer(this._attributesBuffer)));
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
|
||||
gl.enableVertexAttribArray(VertexAttribLocations.OFFSET);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.OFFSET, 2, gl.FLOAT, false, BYTES_PER_CELL, 0);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.OFFSET, 2, gl.FLOAT, false, Constants.BYTES_PER_CELL, 0);
|
||||
gl.vertexAttribDivisor(VertexAttribLocations.OFFSET, 1);
|
||||
gl.enableVertexAttribArray(VertexAttribLocations.SIZE);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.SIZE, 2, gl.FLOAT, false, BYTES_PER_CELL, 2 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.SIZE, 2, gl.FLOAT, false, Constants.BYTES_PER_CELL, 2 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribDivisor(VertexAttribLocations.SIZE, 1);
|
||||
gl.enableVertexAttribArray(VertexAttribLocations.TEXPAGE);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.TEXPAGE, 1, gl.FLOAT, false, BYTES_PER_CELL, 4 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.TEXPAGE, 1, gl.FLOAT, false, Constants.BYTES_PER_CELL, 4 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribDivisor(VertexAttribLocations.TEXPAGE, 1);
|
||||
gl.enableVertexAttribArray(VertexAttribLocations.TEXCOORD);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.TEXCOORD, 2, gl.FLOAT, false, BYTES_PER_CELL, 5 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.TEXCOORD, 2, gl.FLOAT, false, Constants.BYTES_PER_CELL, 5 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribDivisor(VertexAttribLocations.TEXCOORD, 1);
|
||||
gl.enableVertexAttribArray(VertexAttribLocations.TEXSIZE);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.TEXSIZE, 2, gl.FLOAT, false, BYTES_PER_CELL, 7 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.TEXSIZE, 2, gl.FLOAT, false, Constants.BYTES_PER_CELL, 7 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribDivisor(VertexAttribLocations.TEXSIZE, 1);
|
||||
gl.enableVertexAttribArray(VertexAttribLocations.CELL_POSITION);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.CELL_POSITION, 2, gl.FLOAT, false, BYTES_PER_CELL, 9 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.CELL_POSITION, 2, gl.FLOAT, false, Constants.BYTES_PER_CELL, 9 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribDivisor(VertexAttribLocations.CELL_POSITION, 1);
|
||||
|
||||
// Setup static uniforms
|
||||
@@ -222,12 +224,12 @@ export class GlyphRenderer extends Disposable {
|
||||
}
|
||||
|
||||
private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, bg: number, fg: number, ext: number, chars: string, width: number, lastBg: number): void {
|
||||
$i = (y * this._terminal.cols + x) * INDICES_PER_CELL;
|
||||
$i = (y * this._terminal.cols + x) * Constants.INDICES_PER_CELL;
|
||||
|
||||
// Exit early if this is a null character, allow space character to continue as it may have
|
||||
// underline/strikethrough styles
|
||||
if (code === NULL_CELL_CODE || code === undefined/* This is used for the right side of wide chars */) {
|
||||
array.fill(0, $i, $i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES);
|
||||
array.fill(0, $i, $i + Constants.INDICES_PER_CELL - 1 - Constants.CELL_POSITION_INDICES);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -288,7 +290,7 @@ export class GlyphRenderer extends Disposable {
|
||||
|
||||
public clear(): void {
|
||||
const terminal = this._terminal;
|
||||
const newCount = terminal.cols * terminal.rows * INDICES_PER_CELL;
|
||||
const newCount = terminal.cols * terminal.rows * Constants.INDICES_PER_CELL;
|
||||
|
||||
// Clear vertices
|
||||
if (this._vertices.count !== newCount) {
|
||||
@@ -310,7 +312,7 @@ export class GlyphRenderer extends Disposable {
|
||||
for (let x = 0; x < terminal.cols; x++) {
|
||||
this._vertices.attributes[i + 9] = x / terminal.cols;
|
||||
this._vertices.attributes[i + 10] = y / terminal.rows;
|
||||
i += INDICES_PER_CELL;
|
||||
i += Constants.INDICES_PER_CELL;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -346,8 +348,8 @@ export class GlyphRenderer extends Disposable {
|
||||
// - So we don't send vertices for all the line-ending whitespace to the GPU
|
||||
let bufferLength = 0;
|
||||
for (let y = 0; y < renderModel.lineLengths.length; y++) {
|
||||
const si = y * this._terminal.cols * INDICES_PER_CELL;
|
||||
const sub = this._vertices.attributes.subarray(si, si + renderModel.lineLengths[y] * INDICES_PER_CELL);
|
||||
const si = y * this._terminal.cols * Constants.INDICES_PER_CELL;
|
||||
const sub = this._vertices.attributes.subarray(si, si + renderModel.lineLengths[y] * Constants.INDICES_PER_CELL);
|
||||
activeBuffer.set(sub, bufferLength);
|
||||
bufferLength += sub.length;
|
||||
}
|
||||
@@ -356,7 +358,9 @@ export class GlyphRenderer extends Disposable {
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, activeBuffer.subarray(0, bufferLength), gl.STREAM_DRAW);
|
||||
|
||||
// Bind the atlas page texture if they have changed
|
||||
// Bind the atlas page texture if they have changed. AtlasPage.version is globally
|
||||
// monotonic, so a page object swap at the same index (which happens after a page merge)
|
||||
// is detected by the same comparison.
|
||||
for (let i = 0; i < this._atlas.pages.length; i++) {
|
||||
if (this._atlas.pages[i].version !== this._atlasTextures[i].version) {
|
||||
this._bindAtlasPageTexture(gl, this._atlas, i);
|
||||
@@ -364,11 +368,15 @@ export class GlyphRenderer extends Disposable {
|
||||
}
|
||||
|
||||
// Draw the viewport
|
||||
gl.drawElementsInstanced(gl.TRIANGLE_STRIP, 4, gl.UNSIGNED_BYTE, 0, bufferLength / INDICES_PER_CELL);
|
||||
gl.drawElementsInstanced(gl.TRIANGLE_STRIP, 4, gl.UNSIGNED_BYTE, 0, bufferLength / Constants.INDICES_PER_CELL);
|
||||
}
|
||||
|
||||
public setAtlas(atlas: ITextureAtlas): void {
|
||||
this._atlas = atlas;
|
||||
this.invalidateAtlasTextures();
|
||||
}
|
||||
|
||||
public invalidateAtlasTextures(): void {
|
||||
for (const glTexture of this._atlasTextures) {
|
||||
glTexture.version = -1;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Attributes, FgFlags } from 'common/buffer/Constants';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { IColor } from 'common/Types';
|
||||
import { Terminal } from '@xterm/xterm';
|
||||
import { RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
|
||||
import { RenderModelConstants } from './RenderModel';
|
||||
import { IRenderModel, IWebGL2RenderingContext, IWebGLVertexArrayObject } from './Types';
|
||||
import { createProgram, expandFloat32Array, PROJECTION_MATRIX } from './WebglUtils';
|
||||
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
|
||||
@@ -217,9 +217,9 @@ export class RectangleRenderer extends Disposable {
|
||||
currentFg = 0;
|
||||
currentInverse = false;
|
||||
for (x = 0; x < terminal.cols; x++) {
|
||||
modelIndex = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
|
||||
bg = model.cells[modelIndex + RENDER_MODEL_BG_OFFSET];
|
||||
fg = model.cells[modelIndex + RENDER_MODEL_FG_OFFSET];
|
||||
modelIndex = ((y * terminal.cols) + x) * RenderModelConstants.INDICIES_PER_CELL;
|
||||
bg = model.cells[modelIndex + RenderModelConstants.BG_OFFSET];
|
||||
fg = model.cells[modelIndex + RenderModelConstants.FG_OFFSET];
|
||||
inverse = !!(fg & FgFlags.INVERSE);
|
||||
if (bg !== currentBg || (fg !== currentFg && (currentInverse || inverse))) {
|
||||
// A rectangle needs to be drawn if going from non-default to another color
|
||||
@@ -336,8 +336,9 @@ export class RectangleRenderer extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
if (vertices.attributes.length < offset + 4) {
|
||||
vertices.attributes = expandFloat32Array(vertices.attributes, this._terminal.rows * this._terminal.cols * INDICES_PER_RECTANGLE);
|
||||
if (vertices.attributes.length < offset + INDICES_PER_RECTANGLE) {
|
||||
// +1 for the viewport-clear rectangle at offset 0.
|
||||
vertices.attributes = expandFloat32Array(vertices.attributes, (this._terminal.rows * this._terminal.cols + 1) * INDICES_PER_RECTANGLE);
|
||||
}
|
||||
$x1 = startX * this._dimensions.device.cell.width;
|
||||
$y1 = y * this._dimensions.device.cell.height;
|
||||
|
||||
@@ -7,10 +7,12 @@ import { ICursorRenderModel, IRenderModel } from './Types';
|
||||
import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
|
||||
import { createSelectionRenderModel } from 'browser/renderer/shared/SelectionRenderModel';
|
||||
|
||||
export const RENDER_MODEL_INDICIES_PER_CELL = 4;
|
||||
export const RENDER_MODEL_BG_OFFSET = 1;
|
||||
export const RENDER_MODEL_FG_OFFSET = 2;
|
||||
export const RENDER_MODEL_EXT_OFFSET = 3;
|
||||
export const enum RenderModelConstants {
|
||||
INDICIES_PER_CELL = 4,
|
||||
BG_OFFSET = 1,
|
||||
FG_OFFSET = 2,
|
||||
EXT_OFFSET = 3
|
||||
}
|
||||
|
||||
export const COMBINED_CHAR_BIT_MASK = 0x80000000;
|
||||
|
||||
@@ -27,7 +29,7 @@ export class RenderModel implements IRenderModel {
|
||||
}
|
||||
|
||||
public resize(cols: number, rows: number): void {
|
||||
const indexCount = cols * rows * RENDER_MODEL_INDICIES_PER_CELL;
|
||||
const indexCount = cols * rows * RenderModelConstants.INDICIES_PER_CELL;
|
||||
if (indexCount !== this.cells.length) {
|
||||
this.cells = new Uint32Array(indexCount);
|
||||
this.lineLengths = new Uint32Array(rows);
|
||||
|
||||
@@ -62,7 +62,9 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
|
||||
// The texture that the atlas is drawn to
|
||||
private _pages: AtlasPage[] = [];
|
||||
public get pages(): { canvas: HTMLCanvasElement, version: number }[] { return this._pages; }
|
||||
public get pages(): { canvas: HTMLCanvasElement, version: number }[] {
|
||||
return this._pages;
|
||||
}
|
||||
|
||||
// The set of atlas pages that can be written to
|
||||
private _activePages: AtlasPage[] = [];
|
||||
@@ -133,7 +135,9 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
|
||||
private _requestClearModel = false;
|
||||
public beginFrame(): boolean {
|
||||
return this._requestClearModel;
|
||||
const result = this._requestClearModel;
|
||||
this._requestClearModel = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
public clearTexture(): void {
|
||||
@@ -193,7 +197,7 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
|
||||
// Merge into the new page
|
||||
const mergedPage = this._mergePages(mergingPages, mergedPageIndex);
|
||||
mergedPage.version++;
|
||||
mergedPage.version = ++AtlasPage.nextVersion;
|
||||
|
||||
// Delete the pages, shifting glyph texture pages as needed
|
||||
for (let i = sortedMergingPagesIndexes.length - 1; i >= 0; i--) {
|
||||
@@ -251,7 +255,7 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
for (const g of adjustingPage.glyphs) {
|
||||
g.texturePage--;
|
||||
}
|
||||
adjustingPage.version++;
|
||||
adjustingPage.version = ++AtlasPage.nextVersion;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -937,7 +941,7 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
rasterizedGlyph.size.y
|
||||
);
|
||||
activePage.addGlyph(rasterizedGlyph);
|
||||
activePage.version++;
|
||||
activePage.version = ++AtlasPage.nextVersion;
|
||||
|
||||
return rasterizedGlyph;
|
||||
}
|
||||
@@ -1037,19 +1041,27 @@ class AtlasPage {
|
||||
public readonly ctx: CanvasRenderingContext2D;
|
||||
|
||||
private _usedPixels: number = 0;
|
||||
public get percentageUsed(): number { return this._usedPixels / (this.canvas.width * this.canvas.height); }
|
||||
public get percentageUsed(): number {
|
||||
return this._usedPixels / (this.canvas.width * this.canvas.height);
|
||||
}
|
||||
|
||||
private readonly _glyphs: IRasterizedGlyph[] = [];
|
||||
public get glyphs(): ReadonlyArray<IRasterizedGlyph> { return this._glyphs; }
|
||||
public get glyphs(): ReadonlyArray<IRasterizedGlyph> {
|
||||
return this._glyphs;
|
||||
}
|
||||
public addGlyph(glyph: IRasterizedGlyph): void {
|
||||
this._glyphs.push(glyph);
|
||||
this._usedPixels += glyph.size.x * glyph.size.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to check whether the canvas of the atlas page has changed.
|
||||
* Monotonically increasing across all atlas pages globally. Used to detect when the texture
|
||||
* unit at a given index needs to be re-uploaded — both for content changes within the same
|
||||
* page and for a page object swap at the same index (which happens after a page merge,
|
||||
* where a per-page counter could coincide with the previously-bound page's value).
|
||||
*/
|
||||
public version = 0;
|
||||
public static nextVersion: number = 0;
|
||||
public version = ++AtlasPage.nextVersion;
|
||||
|
||||
// Texture atlas current positioning data. The texture packing strategy used is to fill from
|
||||
// left-to-right and top-to-bottom. When the glyph being written is less than half of the current
|
||||
@@ -1092,7 +1104,7 @@ class AtlasPage {
|
||||
this.currentRow.y = 0;
|
||||
this.currentRow.height = 0;
|
||||
this.fixedRows.length = 0;
|
||||
this.version++;
|
||||
this.version = ++AtlasPage.nextVersion;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import { ICoreService, IDecorationService, IOptionsService } from 'common/servic
|
||||
import { Terminal } from '@xterm/xterm';
|
||||
import { GlyphRenderer } from './GlyphRenderer';
|
||||
import { RectangleRenderer } from './RectangleRenderer';
|
||||
import { COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_EXT_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL, RenderModel } from './RenderModel';
|
||||
import { COMBINED_CHAR_BIT_MASK, RenderModel, RenderModelConstants } from './RenderModel';
|
||||
import { IWebGL2RenderingContext, type ITextureAtlas } from './Types';
|
||||
import { LinkRenderLayer } from './renderLayer/LinkRenderLayer';
|
||||
import { IRenderLayer } from './renderLayer/Types';
|
||||
@@ -28,9 +28,13 @@ import { addDisposableListener } from 'browser/Dom';
|
||||
import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
|
||||
import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
|
||||
|
||||
const enum Constants {
|
||||
MERGE_RETRY_LIMIT = 32
|
||||
}
|
||||
|
||||
export class WebglRenderer extends Disposable implements IRenderer {
|
||||
private _renderLayers: IRenderLayer[];
|
||||
private _cursorBlinkStateManager: MutableDisposable<CursorBlinkStateManager> = new MutableDisposable();
|
||||
private _cursorBlinkStateManager: MutableDisposable<CursorBlinkStateManager> = this._register(new MutableDisposable());
|
||||
private _textBlinkStateManager: TextBlinkStateManager;
|
||||
private _charAtlasDisposable = this._register(new MutableDisposable());
|
||||
private _charAtlas: ITextureAtlas | undefined;
|
||||
@@ -375,6 +379,19 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
this._updateModel(start, end);
|
||||
}
|
||||
|
||||
// A mid-update atlas page merge invalidates vertex data and may not bump the host
|
||||
// page's version, so re-run the update and force a full texture rebind.
|
||||
let merged = false;
|
||||
let mergeRetries = 0;
|
||||
while (this._charAtlas && this._glyphRenderer.value.beginFrame() && mergeRetries++ < Constants.MERGE_RETRY_LIMIT) {
|
||||
merged = true;
|
||||
this._clearModel(true);
|
||||
this._updateModel(0, this._terminal.rows - 1);
|
||||
}
|
||||
if (merged) {
|
||||
this._glyphRenderer.value.invalidateAtlasTextures();
|
||||
}
|
||||
|
||||
// Render
|
||||
this._rectangleRenderer.value.renderBackgrounds();
|
||||
this._glyphRenderer.value.render(this._model);
|
||||
@@ -493,7 +510,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
|
||||
chars = cell.getChars();
|
||||
code = cell.getCode();
|
||||
i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
|
||||
i = ((y * terminal.cols) + x) * RenderModelConstants.INDICIES_PER_CELL;
|
||||
|
||||
if (!rowHasBlinkingCells && cell.isBlink()) {
|
||||
rowHasBlinkingCells = true;
|
||||
@@ -538,9 +555,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
|
||||
// Nothing has changed, no updates needed
|
||||
if (this._model.cells[i] === code &&
|
||||
this._model.cells[i + RENDER_MODEL_BG_OFFSET] === this._cellColorResolver.result.bg &&
|
||||
this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._cellColorResolver.result.fg &&
|
||||
this._model.cells[i + RENDER_MODEL_EXT_OFFSET] === this._cellColorResolver.result.ext) {
|
||||
this._model.cells[i + RenderModelConstants.BG_OFFSET] === this._cellColorResolver.result.bg &&
|
||||
this._model.cells[i + RenderModelConstants.FG_OFFSET] === this._cellColorResolver.result.fg &&
|
||||
this._model.cells[i + RenderModelConstants.EXT_OFFSET] === this._cellColorResolver.result.ext) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -553,9 +570,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
|
||||
// Cache the results in the model
|
||||
this._model.cells[i] = code;
|
||||
this._model.cells[i + RENDER_MODEL_BG_OFFSET] = this._cellColorResolver.result.bg;
|
||||
this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg;
|
||||
this._model.cells[i + RENDER_MODEL_EXT_OFFSET] = this._cellColorResolver.result.ext;
|
||||
this._model.cells[i + RenderModelConstants.BG_OFFSET] = this._cellColorResolver.result.bg;
|
||||
this._model.cells[i + RenderModelConstants.FG_OFFSET] = this._cellColorResolver.result.fg;
|
||||
this._model.cells[i + RenderModelConstants.EXT_OFFSET] = this._cellColorResolver.result.ext;
|
||||
|
||||
width = cell.getWidth();
|
||||
this._glyphRenderer.value!.updateCell(x, y, code, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, chars, width, lastBg);
|
||||
@@ -566,14 +583,14 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
|
||||
// Null out non-first cells
|
||||
for (x++; x <= lastCharX; x++) {
|
||||
j = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
|
||||
j = ((y * terminal.cols) + x) * RenderModelConstants.INDICIES_PER_CELL;
|
||||
this._glyphRenderer.value!.updateCell(x, y, NULL_CELL_CODE, 0, 0, 0, NULL_CELL_CHAR, 0, 0);
|
||||
this._model.cells[j] = NULL_CELL_CODE;
|
||||
// Don't re-resolve the cell color since multi-colored ligature backgrounds are not
|
||||
// supported
|
||||
this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._cellColorResolver.result.bg;
|
||||
this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg;
|
||||
this._model.cells[j + RENDER_MODEL_EXT_OFFSET] = this._cellColorResolver.result.ext;
|
||||
this._model.cells[j + RenderModelConstants.BG_OFFSET] = this._cellColorResolver.result.bg;
|
||||
this._model.cells[j + RenderModelConstants.FG_OFFSET] = this._cellColorResolver.result.fg;
|
||||
this._model.cells[j + RenderModelConstants.EXT_OFFSET] = this._cellColorResolver.result.ext;
|
||||
}
|
||||
x--; // Go back to the previous update cell for next iteration
|
||||
}
|
||||
|
||||
@@ -15,8 +15,10 @@ test.beforeAll(async ({ browser }) => {
|
||||
await openTerminal(ctx);
|
||||
ctxWrapper.value = ctx;
|
||||
await ctx.page.evaluate(`
|
||||
window.addon = new window.WebglAddon(true);
|
||||
window.term.loadAddon(window.addon);
|
||||
try {
|
||||
window.addon = new window.WebglAddon(true);
|
||||
window.term.loadAddon(window.addon);
|
||||
} catch (e) {}
|
||||
`);
|
||||
});
|
||||
test.afterAll(async () => await ctx.page.close());
|
||||
@@ -24,15 +26,18 @@ test.afterAll(async () => await ctx.page.close());
|
||||
test.describe('WebGL Renderer Integration Tests', async () => {
|
||||
// HACK: webgl2 is often not supported in headless firefox on Linux
|
||||
// https://github.com/microsoft/playwright/issues/11566
|
||||
// also disable safari due to #5852
|
||||
if (platform() === 'linux') {
|
||||
test.skip(({ browserName }) => browserName === 'firefox');
|
||||
test.skip(({ browserName }) => browserName === 'firefox' || browserName === 'webkit');
|
||||
}
|
||||
|
||||
injectSharedRendererTests(ctxWrapper);
|
||||
injectSharedRendererTestsStandalone(ctxWrapper, async () => {
|
||||
await ctx.page.evaluate(`
|
||||
window.addon = new window.WebglAddon(true);
|
||||
window.term.loadAddon(window.addon);
|
||||
try {
|
||||
window.addon = new window.WebglAddon(true);
|
||||
window.term.loadAddon(window.addon);
|
||||
} catch (e) {}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,10 @@ const config: PlaywrightTestConfig = {
|
||||
{
|
||||
name: 'Chromium',
|
||||
use: {
|
||||
browserName: 'chromium'
|
||||
browserName: 'chromium',
|
||||
launchOptions: {
|
||||
args: ['--enable-unsafe-swiftshader']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -57,6 +57,9 @@ xterm.js version: {{version}}
|
||||
{{#CSI.length}}
|
||||
- [CSI](#csi)
|
||||
{{/CSI.length}}
|
||||
{{#APC.length}}
|
||||
- [APC](#apc)
|
||||
{{/APC.length}}
|
||||
{{#DCS.length}}
|
||||
- [DCS](#dcs)
|
||||
{{/DCS.length}}
|
||||
@@ -80,8 +83,9 @@ This document lists xterm.js' support of terminal sequences. The sequences are g
|
||||
- CSI - Control Sequence Introducer: sequence starting with \`ESC [\` (7bit) or CSI (\`\\x9B\`, 8bit)
|
||||
- DCS - Device Control String: sequence starting with \`ESC P\` (7bit) or DCS (\`\\x90\`, 8bit)
|
||||
- OSC - Operating System Command: sequence starting with \`ESC ]\` (7bit) or OSC (\`\\x9D\`, 8bit)
|
||||
- APC - Application Program Command: sequence starting with \`ESC _\` (7bit) or OSC (\`\\x9F\`, 8bit)
|
||||
|
||||
Application Program Command (APC), Privacy Message (PM) and Start of String (SOS) are recognized but not supported,
|
||||
Privacy Message (PM) and Start of String (SOS) are recognized but not supported,
|
||||
any sequence of these types will be silently ignored. They are also not hookable by the API.
|
||||
|
||||
Note that the list only marks sequences implemented in xterm.js' core codebase as supported. Missing sequences are either
|
||||
@@ -178,6 +182,33 @@ To denote the sequences the tables use the same abbreviations as xterm does:
|
||||
{{/CSI.length}}
|
||||
|
||||
|
||||
{{#APC.length}}
|
||||
## APC
|
||||
|
||||
| Mnemonic | Name | Sequence | Short Description | Support |
|
||||
| -------- | ---- | -------- | ----------------- | ------- |
|
||||
{{#APC}}
|
||||
| {{mnemonic}} | {{name}} | \`{{sequence}}\` | {{{shortDescription}}} {{#longDescription.length}}_[more](#{{longTarget}}){: .link-details}_{{/longDescription.length}} | {{{status}}} |
|
||||
{{/APC}}
|
||||
|
||||
{{#APC.hasLongDescriptions}}
|
||||
{{#APC}}
|
||||
{{#longDescription.length}}
|
||||
<section class="sequence-details">
|
||||
|
||||
### {{name}}
|
||||
{{#longDescription}}
|
||||
{{{.}}}
|
||||
{{/longDescription}}
|
||||
|
||||
</section>
|
||||
{{/longDescription.length}}
|
||||
{{/APC}}
|
||||
{{/APC.hasLongDescriptions}}
|
||||
|
||||
{{/APC.length}}
|
||||
|
||||
|
||||
{{#DCS.length}}
|
||||
## DCS
|
||||
|
||||
|
||||
@@ -521,8 +521,7 @@ function initAddons(term: Terminal): void {
|
||||
} else if (name === 'webLinks') {
|
||||
controlBar.setTabVisible('addon-web-links', true);
|
||||
}
|
||||
}
|
||||
catch {
|
||||
} catch {
|
||||
addon.instance = undefined;
|
||||
checkbox.checked = false;
|
||||
checkbox.disabled = true;
|
||||
|
||||
@@ -30,7 +30,9 @@ export class AddonWebFontsWindow extends BaseWindow implements IControlWindow {
|
||||
this._terminal.options.lineHeight = 1.3;
|
||||
this._addons.fit.instance?.fit();
|
||||
setTimeout(() => this._terminal.write('\x1b[?12h\x1b]12;#776CF9\x07\x1b[38;2;119;108;249;48;2;21;8;150m\x1b[2J\x1b[2;5H**** COMMODORE 64 BASIC V2 ****\r\n\r\n 64K RAM SYSTEM 38911 BASIC BYTES FREE\r\n\r\nREADY.\r\nLOAD '), 1000);
|
||||
setTimeout(() => { this._terminal.write('🤣\x1b[m\x1b[99;1H'); this._terminal.input('\r'); }, 5000);
|
||||
setTimeout(() => {
|
||||
this._terminal.write('🤣\x1b[m\x1b[99;1H'); this._terminal.input('\r');
|
||||
}, 5000);
|
||||
});
|
||||
ddKongtext.appendChild(btnKongtext);
|
||||
dl.appendChild(ddKongtext);
|
||||
|
||||
@@ -8,7 +8,9 @@ import type { IControlWindow } from '../controlBar';
|
||||
import type { Terminal } from '@xterm/xterm';
|
||||
|
||||
export abstract class BaseWindow implements IControlWindow {
|
||||
protected get _terminal(): Terminal { return this._terminalPrivate; }
|
||||
protected get _terminal(): Terminal {
|
||||
return this._terminalPrivate;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private _terminalPrivate: Terminal,
|
||||
|
||||
@@ -252,6 +252,11 @@ export class OptionsWindow extends BaseWindow implements IControlWindow {
|
||||
input.value = 'auto';
|
||||
this._handlers.updateTerminalSize();
|
||||
}
|
||||
const screenElement = document.querySelector('.xterm-screen');
|
||||
if (screenElement instanceof HTMLElement) {
|
||||
screenElement.style.outline = this._autoResize ? ''
|
||||
: 'solid thin red';
|
||||
}
|
||||
} else if (o === 'theme') {
|
||||
value = this._getTheme();
|
||||
}
|
||||
|
||||
@@ -128,6 +128,7 @@ export default tseslint.config(
|
||||
'no-fallthrough': 'off',
|
||||
'prefer-spread': 'off',
|
||||
'object-curly-spacing': ['warn', 'always'],
|
||||
'brace-style': ['warn', '1tbs', { allowSingleLine: false }],
|
||||
'prefer-const': 'warn',
|
||||
'spaced-comment': ['warn', 'always', { markers: ['/'], exceptions: ['-'] }]
|
||||
}
|
||||
|
||||
Generated
+25
-714
File diff suppressed because it is too large
Load Diff
+3
-11
@@ -29,6 +29,7 @@
|
||||
"scripts": {
|
||||
"presetup": "npm run build",
|
||||
"setup": "npm run esbuild",
|
||||
"setup-full": "node bin/setup-full.mjs",
|
||||
"postsetup": "npm run esbuild-demo-server",
|
||||
"start": "node demo/start",
|
||||
"dev": "concurrently -k -p [{name}] -n tsc,esbuild,esbuild-demo-client,esbuild-demo-server,server -c blue,yellow,cyan,green,magenta \"npm:tsc-watch\" \"npm:esbuild-watch\" \"npm:esbuild-demo-client-watch\" \"npm:esbuild-demo-server-watch\" \"npm:start\"",
|
||||
@@ -55,7 +56,6 @@
|
||||
"test-unit": "node ./bin/test_unit.js",
|
||||
"test-unit-slow-tests": "npm run test-unit | grep \"ms)\"",
|
||||
"test-unit-coverage": "node ./bin/test_unit.js --coverage",
|
||||
"test-unit-dev": "cross-env NODE_PATH='./out' mocha",
|
||||
"test-integration": "node ./bin/test_integration.js --workers=75%",
|
||||
"test-integration-chromium": "node ./bin/test_integration.js --workers=75% \"--project=Chromium\"",
|
||||
"test-integration-firefox": "node ./bin/test_integration.js --workers=75% \"--project=FirefoxStable\"",
|
||||
@@ -65,30 +65,25 @@
|
||||
"benchmark-baseline": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json --baseline out-test/benchmark/*benchmark.js",
|
||||
"benchmark-eval": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json --eval out-test/benchmark/*benchmark.js",
|
||||
"clean": "rm -rf lib out addons/*/lib addons/*/out",
|
||||
"vtfeatures": "node bin/extract_vtfeatures.js src/**/*.ts src/*.ts",
|
||||
"vtfeatures": "node bin/extract_vtfeatures.js src/*/*.ts src/*/*/*.ts src/*.ts addons/**/src/*.ts",
|
||||
"prepackage": "npm run build",
|
||||
"package": "webpack",
|
||||
"postpackage": "npm run esbuild-package",
|
||||
"prepackage-headless": "npm run esbuild-package-headless-only",
|
||||
"package-headless": "webpack --config ./webpack.config.headless.js",
|
||||
"postpackage-headless": "node ./bin/package_headless.js",
|
||||
"prepublishOnly": "npm run package",
|
||||
"agent:setup-repo": "node bin/agent/setup-repo.mjs"
|
||||
"prepublishOnly": "npm run package"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lunapaint/png-codec": "^0.2.0",
|
||||
"@playwright/test": "^1.57.0",
|
||||
"@stylistic/eslint-plugin": "^4.4.1",
|
||||
"@types/chai": "^4.2.22",
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/deep-equal": "^1.0.1",
|
||||
"@types/express": "4",
|
||||
"@types/express-ws": "^3.0.1",
|
||||
"@types/jsdom": "^27.0.0",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/node": "^22.19.3",
|
||||
"@types/trusted-types": "^1.0.6",
|
||||
"@types/utf8": "^3.0.0",
|
||||
"@types/webpack": "^5.28.0",
|
||||
"@types/ws": "^8.2.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.50.1",
|
||||
@@ -96,8 +91,6 @@
|
||||
"@typescript/native-preview": "^7.0.0-dev.20260213.1",
|
||||
"chai": "^4.3.4",
|
||||
"concurrently": "^9.1.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"deep-equal": "^2.0.5",
|
||||
"esbuild": "~0.25.2",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-plugin-jsdoc": "^50.8.0",
|
||||
@@ -113,7 +106,6 @@
|
||||
"ts-loader": "^9.3.1",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.50.1",
|
||||
"utf8": "^3.0.0",
|
||||
"webpack": "^5.61.0",
|
||||
"webpack-cli": "^4.9.1",
|
||||
"ws": "^8.2.3",
|
||||
|
||||
@@ -12,7 +12,9 @@ import { IBuffer } from 'common/buffer/Types';
|
||||
import { IInstantiationService } from 'common/services/Services';
|
||||
import { addDisposableListener } from 'browser/Dom';
|
||||
|
||||
const MAX_ROWS_TO_READ = 20;
|
||||
const enum Constants {
|
||||
MAX_ROWS_TO_READ = 20
|
||||
}
|
||||
|
||||
const enum BoundaryPosition {
|
||||
TOP,
|
||||
@@ -137,7 +139,7 @@ export class AccessibilityManager extends Disposable {
|
||||
}
|
||||
|
||||
private _handleChar(char: string): void {
|
||||
if (this._liveRegionLineCount < MAX_ROWS_TO_READ + 1) {
|
||||
if (this._liveRegionLineCount < Constants.MAX_ROWS_TO_READ + 1) {
|
||||
if (this._charsToConsume.length > 0) {
|
||||
// Have the screen reader ignore the char if it was just input
|
||||
const shiftedChar = this._charsToConsume.shift();
|
||||
@@ -150,7 +152,7 @@ export class AccessibilityManager extends Disposable {
|
||||
|
||||
if (char === '\n') {
|
||||
this._liveRegionLineCount++;
|
||||
if (this._liveRegionLineCount === MAX_ROWS_TO_READ + 1) {
|
||||
if (this._liveRegionLineCount === Constants.MAX_ROWS_TO_READ + 1) {
|
||||
this._liveRegion.textContent = Strings.tooMuchOutput.get();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,9 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
private _compositionView: HTMLElement | undefined;
|
||||
|
||||
private readonly _linkifier: MutableDisposable<ILinkifier2> = this._register(new MutableDisposable());
|
||||
public get linkifier(): ILinkifier2 | undefined { return this._linkifier.value; }
|
||||
public get linkifier(): ILinkifier2 | undefined {
|
||||
return this._linkifier.value;
|
||||
}
|
||||
private _overviewRulerRenderer: OverviewRulerRenderer | undefined;
|
||||
private _viewport: Viewport | undefined;
|
||||
|
||||
@@ -136,15 +138,25 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
public readonly onBell = this._onBell.event;
|
||||
|
||||
private _onFocus = this._register(new Emitter<void>());
|
||||
public get onFocus(): IEvent<void> { return this._onFocus.event; }
|
||||
public get onFocus(): IEvent<void> {
|
||||
return this._onFocus.event;
|
||||
}
|
||||
private _onBlur = this._register(new Emitter<void>());
|
||||
public get onBlur(): IEvent<void> { return this._onBlur.event; }
|
||||
public get onBlur(): IEvent<void> {
|
||||
return this._onBlur.event;
|
||||
}
|
||||
private _onA11yCharEmitter = this._register(new Emitter<string>());
|
||||
public get onA11yChar(): IEvent<string> { return this._onA11yCharEmitter.event; }
|
||||
public get onA11yChar(): IEvent<string> {
|
||||
return this._onA11yCharEmitter.event;
|
||||
}
|
||||
private _onA11yTabEmitter = this._register(new Emitter<number>());
|
||||
public get onA11yTab(): IEvent<number> { return this._onA11yTabEmitter.event; }
|
||||
public get onA11yTab(): IEvent<number> {
|
||||
return this._onA11yTabEmitter.event;
|
||||
}
|
||||
private _onWillOpen = this._register(new Emitter<HTMLElement>());
|
||||
public get onWillOpen(): IEvent<HTMLElement> { return this._onWillOpen.event; }
|
||||
public get onWillOpen(): IEvent<HTMLElement> {
|
||||
return this._onWillOpen.event;
|
||||
}
|
||||
private readonly _onDimensionsChange = this._register(new Emitter<IRenderDimensionsApi>());
|
||||
public readonly onDimensionsChange = this._onDimensionsChange.event;
|
||||
|
||||
@@ -563,8 +575,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
|
||||
try {
|
||||
this._onWillOpen.fire(this.element);
|
||||
}
|
||||
catch { /* fails to load addon for some reason */ }
|
||||
} catch { /* fails to load addon for some reason */ }
|
||||
if (!this._renderService.hasRenderer()) {
|
||||
this._renderService.setRenderer(this._createRenderer());
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ import { Emitter } from 'common/Event';
|
||||
import { addDisposableListener } from 'browser/Dom';
|
||||
|
||||
export class Linkifier extends Disposable implements ILinkifier2 {
|
||||
public get currentLink(): ILinkWithState | undefined { return this._currentLink; }
|
||||
public get currentLink(): ILinkWithState | undefined {
|
||||
return this._currentLink;
|
||||
}
|
||||
protected _currentLink: ILinkWithState | undefined;
|
||||
private _mouseDownLink: ILinkWithState | undefined;
|
||||
private _lastMouseEvent: MouseEvent | undefined;
|
||||
|
||||
@@ -21,9 +21,15 @@ import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
|
||||
import { Emitter, type IEvent } from 'common/Event';
|
||||
|
||||
export class TestTerminal extends CoreBrowserTerminal {
|
||||
public get curAttrData(): IAttributeData { return (this as any)._inputHandler._curAttrData; }
|
||||
public keyDown(ev: any): boolean | undefined { return this._keyDown(ev); }
|
||||
public keyPress(ev: any): boolean { return this._keyPress(ev); }
|
||||
public get curAttrData(): IAttributeData {
|
||||
return (this as any)._inputHandler._curAttrData;
|
||||
}
|
||||
public keyDown(ev: any): boolean | undefined {
|
||||
return this._keyDown(ev);
|
||||
}
|
||||
public keyPress(ev: any): boolean {
|
||||
return this._keyPress(ev);
|
||||
}
|
||||
public writeP(data: string | Uint8Array): Promise<void> {
|
||||
return new Promise(r => this.write(data, r));
|
||||
}
|
||||
@@ -107,7 +113,7 @@ export class MockTerminal implements ITerminal {
|
||||
public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public registerApcHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
|
||||
public registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
|
||||
@@ -213,7 +219,9 @@ export class MockTerminal implements ITerminal {
|
||||
public refresh(start: number, end: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public registerCharacterJoiner(handler: CharacterJoinerHandler): number { return 0; }
|
||||
public registerCharacterJoiner(handler: CharacterJoinerHandler): number {
|
||||
return 0;
|
||||
}
|
||||
public deregisterCharacterJoiner(joinerId: number): void { }
|
||||
}
|
||||
|
||||
@@ -377,7 +385,9 @@ export class MockCoreBrowserService implements ICoreBrowserService {
|
||||
|
||||
export class MockCharSizeService implements ICharSizeService {
|
||||
public serviceBrand: undefined;
|
||||
public get hasValidSize(): boolean { return this.width > 0 && this.height > 0; }
|
||||
public get hasValidSize(): boolean {
|
||||
return this.width > 0 && this.height > 0;
|
||||
}
|
||||
public onCharSizeChange: IEvent<void> = new Emitter<void>().event;
|
||||
constructor(public width: number, public height: number) {}
|
||||
public measure(): void {}
|
||||
|
||||
@@ -94,6 +94,12 @@ export class OverviewRulerRenderer extends Disposable {
|
||||
this._register(this._coreBrowserService.onDprChange(() => this._queueRefresh(true)));
|
||||
this._register(this._optionsService.onSpecificOptionChange('scrollbar', () => this._queueRefresh(true)));
|
||||
this._register(this._themeService.onChangeColors(() => this._queueRefresh()));
|
||||
this._register(toDisposable(() => {
|
||||
if (this._animationFrame !== undefined) {
|
||||
this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame);
|
||||
this._animationFrame = undefined;
|
||||
}
|
||||
}));
|
||||
this._queueRefresh(true);
|
||||
}
|
||||
|
||||
@@ -136,6 +142,9 @@ export class OverviewRulerRenderer extends Disposable {
|
||||
}
|
||||
|
||||
private _refreshCanvasDimensions(): void {
|
||||
if (this._store.isDisposed || !this._renderService.hasRenderer()) {
|
||||
return;
|
||||
}
|
||||
const cssCanvasHeight = this._renderService.dimensions.css.canvas.height;
|
||||
const deviceCanvasHeight = this._renderService.dimensions.device.canvas.height;
|
||||
this._canvas.style.width = `${this._width}px`;
|
||||
@@ -147,6 +156,9 @@ export class OverviewRulerRenderer extends Disposable {
|
||||
}
|
||||
|
||||
private _refreshDecorations(): void {
|
||||
if (this._store.isDisposed || !this._renderService.hasRenderer()) {
|
||||
return;
|
||||
}
|
||||
if (this._shouldUpdateDimensions) {
|
||||
this._refreshCanvasDimensions();
|
||||
}
|
||||
@@ -200,13 +212,18 @@ export class OverviewRulerRenderer extends Disposable {
|
||||
}
|
||||
|
||||
private _queueRefresh(updateCanvasDimensions?: boolean, updateAnchor?: boolean): void {
|
||||
if (this._store.isDisposed) {
|
||||
return;
|
||||
}
|
||||
this._shouldUpdateDimensions = updateCanvasDimensions || this._shouldUpdateDimensions;
|
||||
this._shouldUpdateAnchor = updateAnchor || this._shouldUpdateAnchor;
|
||||
if (this._animationFrame !== undefined) {
|
||||
return;
|
||||
}
|
||||
this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => {
|
||||
this._refreshDecorations();
|
||||
if (!this._store.isDisposed) {
|
||||
this._refreshDecorations();
|
||||
}
|
||||
this._animationFrame = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,7 +23,9 @@ export class CompositionHelper {
|
||||
* IME. This variable determines whether the compositionText should be displayed on the UI.
|
||||
*/
|
||||
private _isComposing: boolean;
|
||||
public get isComposing(): boolean { return this._isComposing; }
|
||||
public get isComposing(): boolean {
|
||||
return this._isComposing;
|
||||
}
|
||||
|
||||
/**
|
||||
* The position within the input textarea's value of the current composition.
|
||||
|
||||
@@ -68,22 +68,52 @@ export class Terminal extends Disposable implements ITerminalApi {
|
||||
}
|
||||
}
|
||||
|
||||
public get onBell(): IEvent<void> { return this._core.onBell; }
|
||||
public get onBinary(): IEvent<string> { return this._core.onBinary; }
|
||||
public get onCursorMove(): IEvent<void> { return this._core.onCursorMove; }
|
||||
public get onData(): IEvent<string> { return this._core.onData; }
|
||||
public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._core.onKey; }
|
||||
public get onLineFeed(): IEvent<void> { return this._core.onLineFeed; }
|
||||
public get onRender(): IEvent<{ start: number, end: number }> { return this._core.onRender; }
|
||||
public get onResize(): IEvent<{ cols: number, rows: number }> { return this._core.onResize; }
|
||||
public get onScroll(): IEvent<number> { return this._core.onScroll; }
|
||||
public get onSelectionChange(): IEvent<void> { return this._core.onSelectionChange; }
|
||||
public get onTitleChange(): IEvent<string> { return this._core.onTitleChange; }
|
||||
public get onWriteParsed(): IEvent<void> { return this._core.onWriteParsed; }
|
||||
public get onDimensionsChange(): IEvent<IRenderDimensions> { return this._core.onDimensionsChange; }
|
||||
public get onBell(): IEvent<void> {
|
||||
return this._core.onBell;
|
||||
}
|
||||
public get onBinary(): IEvent<string> {
|
||||
return this._core.onBinary;
|
||||
}
|
||||
public get onCursorMove(): IEvent<void> {
|
||||
return this._core.onCursorMove;
|
||||
}
|
||||
public get onData(): IEvent<string> {
|
||||
return this._core.onData;
|
||||
}
|
||||
public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> {
|
||||
return this._core.onKey;
|
||||
}
|
||||
public get onLineFeed(): IEvent<void> {
|
||||
return this._core.onLineFeed;
|
||||
}
|
||||
public get onRender(): IEvent<{ start: number, end: number }> {
|
||||
return this._core.onRender;
|
||||
}
|
||||
public get onResize(): IEvent<{ cols: number, rows: number }> {
|
||||
return this._core.onResize;
|
||||
}
|
||||
public get onScroll(): IEvent<number> {
|
||||
return this._core.onScroll;
|
||||
}
|
||||
public get onSelectionChange(): IEvent<void> {
|
||||
return this._core.onSelectionChange;
|
||||
}
|
||||
public get onTitleChange(): IEvent<string> {
|
||||
return this._core.onTitleChange;
|
||||
}
|
||||
public get onWriteParsed(): IEvent<void> {
|
||||
return this._core.onWriteParsed;
|
||||
}
|
||||
public get onDimensionsChange(): IEvent<IRenderDimensions> {
|
||||
return this._core.onDimensionsChange;
|
||||
}
|
||||
|
||||
public get element(): HTMLElement | undefined { return this._core.element; }
|
||||
public get screenElement(): HTMLElement | undefined { return this._core.screenElement; }
|
||||
public get element(): HTMLElement | undefined {
|
||||
return this._core.element;
|
||||
}
|
||||
public get screenElement(): HTMLElement | undefined {
|
||||
return this._core.screenElement;
|
||||
}
|
||||
public get parser(): IParser {
|
||||
return this._parser ??= new ParserApi(this._core);
|
||||
}
|
||||
@@ -91,9 +121,15 @@ export class Terminal extends Disposable implements ITerminalApi {
|
||||
this._checkProposedApi();
|
||||
return new UnicodeApi(this._core);
|
||||
}
|
||||
public get textarea(): HTMLTextAreaElement | undefined { return this._core.textarea; }
|
||||
public get rows(): number { return this._core.rows; }
|
||||
public get cols(): number { return this._core.cols; }
|
||||
public get textarea(): HTMLTextAreaElement | undefined {
|
||||
return this._core.textarea;
|
||||
}
|
||||
public get rows(): number {
|
||||
return this._core.rows;
|
||||
}
|
||||
public get cols(): number {
|
||||
return this._core.cols;
|
||||
}
|
||||
public get buffer(): IBufferNamespaceApi {
|
||||
return this._buffer ??= this._register(new BufferNamespaceApi(this._core));
|
||||
}
|
||||
@@ -247,10 +283,18 @@ export class Terminal extends Disposable implements ITerminalApi {
|
||||
public static get strings(): ILocalizableStrings {
|
||||
// A wrapper is required here because esbuild prevents setting an `export let`
|
||||
return {
|
||||
get promptLabel(): string { return Strings.promptLabel.get(); },
|
||||
set promptLabel(value: string) { Strings.promptLabel.set(value); },
|
||||
get tooMuchOutput(): string { return Strings.tooMuchOutput.get(); },
|
||||
set tooMuchOutput(value: string) { Strings.tooMuchOutput.set(value); }
|
||||
get promptLabel(): string {
|
||||
return Strings.promptLabel.get();
|
||||
},
|
||||
set promptLabel(value: string) {
|
||||
Strings.promptLabel.set(value);
|
||||
},
|
||||
get tooMuchOutput(): string {
|
||||
return Strings.tooMuchOutput.get();
|
||||
},
|
||||
set tooMuchOutput(value: string) {
|
||||
Strings.tooMuchOutput.set(value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -19,13 +19,15 @@ import { Emitter } from 'common/Event';
|
||||
import { addDisposableListener } from 'browser/Dom';
|
||||
|
||||
|
||||
const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-';
|
||||
const ROW_CONTAINER_CLASS = 'xterm-rows';
|
||||
const FG_CLASS_PREFIX = 'xterm-fg-';
|
||||
const BG_CLASS_PREFIX = 'xterm-bg-';
|
||||
const FOCUS_CLASS = 'xterm-focus';
|
||||
const SELECTION_CLASS = 'xterm-selection';
|
||||
const CURSOR_BLINK_IDLE_CLASS = 'xterm-cursor-blink-idle';
|
||||
const enum Constants {
|
||||
TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-',
|
||||
ROW_CONTAINER_CLASS = 'xterm-rows',
|
||||
FG_CLASS_PREFIX = 'xterm-fg-',
|
||||
BG_CLASS_PREFIX = 'xterm-bg-',
|
||||
FOCUS_CLASS = 'xterm-focus',
|
||||
SELECTION_CLASS = 'xterm-selection',
|
||||
CURSOR_BLINK_IDLE_CLASS = 'xterm-cursor-blink-idle'
|
||||
}
|
||||
|
||||
let nextTerminalId = 1;
|
||||
|
||||
@@ -76,12 +78,12 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
) {
|
||||
super();
|
||||
this._rowContainer = this._document.createElement('div');
|
||||
this._rowContainer.classList.add(ROW_CONTAINER_CLASS);
|
||||
this._rowContainer.classList.add(Constants.ROW_CONTAINER_CLASS);
|
||||
this._rowContainer.style.lineHeight = 'normal';
|
||||
this._rowContainer.setAttribute('aria-hidden', 'true');
|
||||
this._refreshRowElements(this._bufferService.cols, this._bufferService.rows);
|
||||
this._selectionContainer = this._document.createElement('div');
|
||||
this._selectionContainer.classList.add(SELECTION_CLASS);
|
||||
this._selectionContainer.classList.add(Constants.SELECTION_CLASS);
|
||||
this._selectionContainer.setAttribute('aria-hidden', 'true');
|
||||
|
||||
this.dimensions = createRenderDimensions();
|
||||
@@ -93,7 +95,7 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
|
||||
this._rowFactory = instantiationService.createInstance(DomRendererRowFactory, document);
|
||||
|
||||
this._element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass);
|
||||
this._element.classList.add(Constants.TERMINAL_CLASS_PREFIX + this._terminalClass);
|
||||
this._screenElement.appendChild(this._rowContainer);
|
||||
this._screenElement.appendChild(this._selectionContainer);
|
||||
|
||||
@@ -110,7 +112,7 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
));
|
||||
|
||||
this._register(toDisposable(() => {
|
||||
this._element.classList.remove(TERMINAL_CLASS_PREFIX + this._terminalClass);
|
||||
this._element.classList.remove(Constants.TERMINAL_CLASS_PREFIX + this._terminalClass);
|
||||
|
||||
// Outside influences such as React unmounts may manipulate the DOM before our disposal.
|
||||
// https://github.com/xtermjs/xterm.js/issues/2960
|
||||
@@ -160,7 +162,7 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
}
|
||||
|
||||
const styles =
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} span {` +
|
||||
`${this._terminalSelector} .${Constants.ROW_CONTAINER_CLASS} span {` +
|
||||
` display: inline-block;` + // TODO: find workaround for inline-block (creates ~20% render penalty)
|
||||
` height: 100%;` +
|
||||
` vertical-align: top;` +
|
||||
@@ -181,7 +183,7 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
|
||||
// Base CSS
|
||||
let styles =
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} {` +
|
||||
`${this._terminalSelector} .${Constants.ROW_CONTAINER_CLASS} {` +
|
||||
// Disabling pointer events circumvents a browser behavior that prevents `click` events from
|
||||
// being delivered if the target element is replaced during the click. This happened due to
|
||||
// refresh() being called during the mousedown handler to start a selection.
|
||||
@@ -189,14 +191,14 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
` color: ${colors.foreground.css};` +
|
||||
`}`;
|
||||
styles +=
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}, ${this._terminalSelector} .${ROW_CONTAINER_CLASS} span {` +
|
||||
`${this._terminalSelector} .${Constants.ROW_CONTAINER_CLASS}, ${this._terminalSelector} .${Constants.ROW_CONTAINER_CLASS} span {` +
|
||||
` font-family: ${this._optionsService.rawOptions.fontFamily};` +
|
||||
` font-size: ${this._optionsService.rawOptions.fontSize}px;` +
|
||||
` font-kerning: none;` +
|
||||
` white-space: pre` +
|
||||
`}`;
|
||||
styles +=
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} .xterm-dim {` +
|
||||
`${this._terminalSelector} .${Constants.ROW_CONTAINER_CLASS} .xterm-dim {` +
|
||||
` color: ${color.multiplyOpacity(colors.foreground, 0.5).css};` +
|
||||
`}`;
|
||||
// Text styles
|
||||
@@ -242,70 +244,70 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
`}`;
|
||||
// Cursor
|
||||
styles +=
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}.${RowCss.CURSOR_STYLE_UNDERLINE_CLASS} {` +
|
||||
`${this._terminalSelector} .${Constants.ROW_CONTAINER_CLASS}.${Constants.FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}.${RowCss.CURSOR_STYLE_UNDERLINE_CLASS} {` +
|
||||
` animation: ${blinkAnimationUnderlineId} 1s step-end infinite;` +
|
||||
`}` +
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}.${RowCss.CURSOR_STYLE_BAR_CLASS} {` +
|
||||
`${this._terminalSelector} .${Constants.ROW_CONTAINER_CLASS}.${Constants.FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}.${RowCss.CURSOR_STYLE_BAR_CLASS} {` +
|
||||
` animation: ${blinkAnimationBarId} 1s step-end infinite;` +
|
||||
`}` +
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS} {` +
|
||||
`${this._terminalSelector} .${Constants.ROW_CONTAINER_CLASS}.${Constants.FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS} {` +
|
||||
` animation: ${blinkAnimationBlockId} 1s step-end infinite;` +
|
||||
`}` +
|
||||
// Disable cursor blinking when idle
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${CURSOR_BLINK_IDLE_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS} {` +
|
||||
`${this._terminalSelector} .${Constants.ROW_CONTAINER_CLASS}.${Constants.CURSOR_BLINK_IDLE_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS} {` +
|
||||
` animation: none !important;` +
|
||||
`}` +
|
||||
// !important helps fix an issue where the cursor will not render on top of the selection,
|
||||
// however it's very hard to fix this issue and retain the blink animation without the use of
|
||||
// !important. So this edge case fails when cursor blink is on.
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS} {` +
|
||||
`${this._terminalSelector} .${Constants.ROW_CONTAINER_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS} {` +
|
||||
` background-color: ${colors.cursor.css};` +
|
||||
` color: ${colors.cursorAccent.css};` +
|
||||
`}` +
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS}:not(.${RowCss.CURSOR_BLINK_CLASS}) {` +
|
||||
`${this._terminalSelector} .${Constants.ROW_CONTAINER_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS}:not(.${RowCss.CURSOR_BLINK_CLASS}) {` +
|
||||
` background-color: ${colors.cursor.css} !important;` +
|
||||
` color: ${colors.cursorAccent.css} !important;` +
|
||||
`}` +
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_OUTLINE_CLASS} {` +
|
||||
`${this._terminalSelector} .${Constants.ROW_CONTAINER_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_OUTLINE_CLASS} {` +
|
||||
` outline: 1px solid ${colors.cursor.css};` +
|
||||
` outline-offset: -1px;` +
|
||||
`}` +
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_BAR_CLASS} {` +
|
||||
`${this._terminalSelector} .${Constants.ROW_CONTAINER_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_BAR_CLASS} {` +
|
||||
` box-shadow: ${this._optionsService.rawOptions.cursorWidth}px 0 0 ${colors.cursor.css} inset;` +
|
||||
`}` +
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_UNDERLINE_CLASS} {` +
|
||||
`${this._terminalSelector} .${Constants.ROW_CONTAINER_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_UNDERLINE_CLASS} {` +
|
||||
` border-bottom: 1px ${colors.cursor.css};` +
|
||||
` border-bottom-style: solid;` +
|
||||
` height: calc(100% - 1px);` +
|
||||
`}`;
|
||||
// Selection
|
||||
styles +=
|
||||
`${this._terminalSelector} .${SELECTION_CLASS} {` +
|
||||
`${this._terminalSelector} .${Constants.SELECTION_CLASS} {` +
|
||||
` position: absolute;` +
|
||||
` top: 0;` +
|
||||
` left: 0;` +
|
||||
` z-index: 1;` +
|
||||
` pointer-events: none;` +
|
||||
`}` +
|
||||
`${this._terminalSelector}.focus .${SELECTION_CLASS} div {` +
|
||||
`${this._terminalSelector}.focus .${Constants.SELECTION_CLASS} div {` +
|
||||
` position: absolute;` +
|
||||
` background-color: ${colors.selectionBackgroundOpaque.css};` +
|
||||
`}` +
|
||||
`${this._terminalSelector} .${SELECTION_CLASS} div {` +
|
||||
`${this._terminalSelector} .${Constants.SELECTION_CLASS} div {` +
|
||||
` position: absolute;` +
|
||||
` background-color: ${colors.selectionInactiveBackgroundOpaque.css};` +
|
||||
`}`;
|
||||
// Colors
|
||||
for (const [i, c] of colors.ansi.entries()) {
|
||||
styles +=
|
||||
`${this._terminalSelector} .${FG_CLASS_PREFIX}${i} { color: ${c.css}; }` +
|
||||
`${this._terminalSelector} .${FG_CLASS_PREFIX}${i}.${RowCss.DIM_CLASS} { color: ${color.multiplyOpacity(c, 0.5).css}; }` +
|
||||
`${this._terminalSelector} .${BG_CLASS_PREFIX}${i} { background-color: ${c.css}; }`;
|
||||
`${this._terminalSelector} .${Constants.FG_CLASS_PREFIX}${i} { color: ${c.css}; }` +
|
||||
`${this._terminalSelector} .${Constants.FG_CLASS_PREFIX}${i}.${RowCss.DIM_CLASS} { color: ${color.multiplyOpacity(c, 0.5).css}; }` +
|
||||
`${this._terminalSelector} .${Constants.BG_CLASS_PREFIX}${i} { background-color: ${c.css}; }`;
|
||||
}
|
||||
styles +=
|
||||
`${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { color: ${color.opaque(colors.background).css}; }` +
|
||||
`${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR}.${RowCss.DIM_CLASS} { color: ${color.multiplyOpacity(color.opaque(colors.background), 0.5).css}; }` +
|
||||
`${this._terminalSelector} .${BG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { background-color: ${colors.foreground.css}; }`;
|
||||
`${this._terminalSelector} .${Constants.FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { color: ${color.opaque(colors.background).css}; }` +
|
||||
`${this._terminalSelector} .${Constants.FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR}.${RowCss.DIM_CLASS} { color: ${color.multiplyOpacity(color.opaque(colors.background), 0.5).css}; }` +
|
||||
`${this._terminalSelector} .${Constants.BG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { background-color: ${colors.foreground.css}; }`;
|
||||
|
||||
this._themeStyleElement.textContent = styles;
|
||||
}
|
||||
@@ -361,13 +363,13 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
}
|
||||
|
||||
public handleBlur(): void {
|
||||
this._rowContainer.classList.remove(FOCUS_CLASS);
|
||||
this._rowContainer.classList.remove(Constants.FOCUS_CLASS);
|
||||
this._cursorBlinkStateManager.pause();
|
||||
this.renderRows(0, this._bufferService.rows - 1);
|
||||
}
|
||||
|
||||
public handleFocus(): void {
|
||||
this._rowContainer.classList.add(FOCUS_CLASS);
|
||||
this._rowContainer.classList.add(Constants.FOCUS_CLASS);
|
||||
this._cursorBlinkStateManager.resume();
|
||||
this.renderRows(this._bufferService.buffer.y, this._bufferService.buffer.y);
|
||||
}
|
||||
@@ -561,7 +563,7 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
}
|
||||
|
||||
private get _terminalSelector(): string {
|
||||
return `.${TERMINAL_CLASS_PREFIX}${this._terminalClass}`;
|
||||
return `.${Constants.TERMINAL_CLASS_PREFIX}${this._terminalClass}`;
|
||||
}
|
||||
|
||||
private _handleLinkHover(e: ILinkifierEvent): void {
|
||||
@@ -667,7 +669,7 @@ class CursorBlinkStateManager {
|
||||
|
||||
public restartBlinkAnimation(): void {
|
||||
if (this._isIdlePaused) {
|
||||
this._rowContainer.classList.remove(CURSOR_BLINK_IDLE_CLASS);
|
||||
this._rowContainer.classList.remove(Constants.CURSOR_BLINK_IDLE_CLASS);
|
||||
}
|
||||
this._resetIdleTimer();
|
||||
}
|
||||
@@ -679,7 +681,7 @@ class CursorBlinkStateManager {
|
||||
|
||||
public resume(): void {
|
||||
this._isIdlePaused = false;
|
||||
this._rowContainer.classList.remove(CURSOR_BLINK_IDLE_CLASS);
|
||||
this._rowContainer.classList.remove(Constants.CURSOR_BLINK_IDLE_CLASS);
|
||||
this._resetIdleTimer();
|
||||
}
|
||||
|
||||
@@ -699,7 +701,7 @@ class CursorBlinkStateManager {
|
||||
}
|
||||
|
||||
private _stopBlinkingDueToIdle(): void {
|
||||
this._rowContainer.classList.add(CURSOR_BLINK_IDLE_CLASS);
|
||||
this._rowContainer.classList.add(Constants.CURSOR_BLINK_IDLE_CLASS);
|
||||
this._isIdlePaused = true;
|
||||
this._idleTimeout = undefined;
|
||||
}
|
||||
|
||||
@@ -8,14 +8,14 @@ import { assert } from 'chai';
|
||||
import { DomRendererRowFactory } from 'browser/renderer/dom/DomRendererRowFactory';
|
||||
import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, DEFAULT_ATTR, FgFlags, BgFlags, Attributes, UnderlineStyle } from 'common/buffer/Constants';
|
||||
import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
|
||||
import { BufferLineStringCache } from 'common/buffer/BufferLineStringCache';
|
||||
import { IBufferLine } from 'common/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { MockCoreService, MockDecorationService, MockOptionsService, createCellData, NULL_CELL_DATA } from 'common/TestUtils.test';
|
||||
import { MockCharacterJoinerService, MockCoreBrowserService, MockThemeService } from 'browser/TestUtils.test';
|
||||
import { TestWidthCache } from 'browser/renderer/dom/WidthCache.test';
|
||||
|
||||
const dom = new jsdom.JSDOM('');
|
||||
|
||||
const TEST_STRING_CACHE = new BufferLineStringCache();
|
||||
|
||||
describe('DomRendererRowFactory', () => {
|
||||
let dom: jsdom.JSDOM;
|
||||
@@ -517,7 +517,7 @@ describe('DomRendererRowFactory', () => {
|
||||
}
|
||||
|
||||
function createEmptyLineData(cols: number): IBufferLine {
|
||||
const lineData = new BufferLine(cols);
|
||||
const lineData = new BufferLine(TEST_STRING_CACHE, cols);
|
||||
for (let i = 0; i < cols; i++) {
|
||||
lineData.setCell(i, NULL_CELL_DATA);
|
||||
}
|
||||
|
||||
@@ -18,8 +18,10 @@ import * as platform from 'common/Platform';
|
||||
import { INewScrollDimensions, INewScrollPosition, IScrollDimensions, IScrollPosition, IScrollEvent, Scrollable, ScrollbarVisibility } from './scrollable';
|
||||
// import 'vs/css!./media/scrollbars';
|
||||
|
||||
const HIDE_TIMEOUT = 500;
|
||||
const SCROLL_WHEEL_SENSITIVITY = 50;
|
||||
const enum Constants {
|
||||
HIDE_TIMEOUT = 500,
|
||||
SCROLL_WHEEL_SENSITIVITY = 50
|
||||
}
|
||||
|
||||
class MouseWheelClassifierItem {
|
||||
public timestamp: number;
|
||||
@@ -404,12 +406,12 @@ export class SmoothScrollableElement extends Widget {
|
||||
|
||||
let desiredScrollPosition: INewScrollPosition = {};
|
||||
if (deltaY) {
|
||||
const deltaScrollTop = SCROLL_WHEEL_SENSITIVITY * deltaY;
|
||||
const deltaScrollTop = Constants.SCROLL_WHEEL_SENSITIVITY * deltaY;
|
||||
const desiredScrollTop = futureScrollPosition.scrollTop - (deltaScrollTop < 0 ? Math.floor(deltaScrollTop) : Math.ceil(deltaScrollTop));
|
||||
this._verticalScrollbar.writeScrollPosition(desiredScrollPosition, desiredScrollTop);
|
||||
}
|
||||
if (deltaX) {
|
||||
const deltaScrollLeft = SCROLL_WHEEL_SENSITIVITY * deltaX;
|
||||
const deltaScrollLeft = Constants.SCROLL_WHEEL_SENSITIVITY * deltaX;
|
||||
const desiredScrollLeft = futureScrollPosition.scrollLeft - (deltaScrollLeft < 0 ? Math.floor(deltaScrollLeft) : Math.ceil(deltaScrollLeft));
|
||||
this._horizontalScrollbar.writeScrollPosition(desiredScrollPosition, desiredScrollLeft);
|
||||
}
|
||||
@@ -533,7 +535,7 @@ export class SmoothScrollableElement extends Widget {
|
||||
|
||||
private _scheduleHide(): void {
|
||||
if (!this._mouseIsOver && !this._isDragging) {
|
||||
this._hideTimeout.cancelAndSet(() => this._hide(), HIDE_TIMEOUT);
|
||||
this._hideTimeout.cancelAndSet(() => this._hide(), Constants.HIDE_TIMEOUT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@ export class CharSizeService extends Disposable implements ICharSizeService {
|
||||
public height: number = 0;
|
||||
private _measureStrategy: IMeasureStrategy;
|
||||
|
||||
public get hasValidSize(): boolean { return this.width > 0 && this.height > 0; }
|
||||
public get hasValidSize(): boolean {
|
||||
return this.width > 0 && this.height > 0;
|
||||
}
|
||||
|
||||
private readonly _onCharSizeChange = this._register(new Emitter<void>());
|
||||
public readonly onCharSizeChange = this._onCharSizeChange.event;
|
||||
|
||||
@@ -7,10 +7,13 @@ import { assert } from 'chai';
|
||||
import { ICharacterJoinerService } from 'browser/services/Services';
|
||||
import { CharacterJoinerService } from 'browser/services/CharacterJoinerService';
|
||||
import { BufferLine } from 'common/buffer/BufferLine';
|
||||
import { BufferLineStringCache } from 'common/buffer/BufferLineStringCache';
|
||||
import { IBufferLine } from 'common/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { MockBufferService, createCellData } from 'common/TestUtils.test';
|
||||
|
||||
const TEST_STRING_CACHE = new BufferLineStringCache();
|
||||
|
||||
describe('CharacterJoinerService', () => {
|
||||
let service: ICharacterJoinerService;
|
||||
|
||||
@@ -22,7 +25,7 @@ describe('CharacterJoinerService', () => {
|
||||
lines.set(2, lineData([['a -> b -', 0xFFFFFFFF], ['> c -> d', 0]]));
|
||||
|
||||
lines.set(3, lineData([['no joined ranges']]));
|
||||
lines.set(4, new BufferLine(0));
|
||||
lines.set(4, new BufferLine(TEST_STRING_CACHE, 0));
|
||||
lines.set(5, lineData([['a', 0x11111111], [' -> b -> c -> '], ['d', 0x22222222]]));
|
||||
const line6 = lineData([['wi']]);
|
||||
line6.resize(line6.length + 1, createCellData(0, '¥', 2));
|
||||
@@ -267,7 +270,7 @@ describe('CharacterJoinerService', () => {
|
||||
type IPartialLineData = ([string] | [string, number]);
|
||||
|
||||
function lineData(data: IPartialLineData[]): IBufferLine {
|
||||
const tline = new BufferLine(0);
|
||||
const tline = new BufferLine(TEST_STRING_CACHE, 0);
|
||||
for (let i = 0; i < data.length; ++i) {
|
||||
const line = data[i][0];
|
||||
const attr = (data[i][1] || 0) as number;
|
||||
|
||||
@@ -52,7 +52,9 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
private readonly _onRefreshRequest = this._register(new Emitter<{ start: number, end: number }>());
|
||||
public readonly onRefreshRequest = this._onRefreshRequest.event;
|
||||
|
||||
public get dimensions(): IRenderDimensions { return this._renderer.value!.dimensions; }
|
||||
public get dimensions(): IRenderDimensions {
|
||||
return this._renderer.value!.dimensions;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private _rowCount: number,
|
||||
@@ -68,7 +70,7 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
) {
|
||||
super();
|
||||
|
||||
this._pausedResizeTask = new DebouncedIdleTask(this._logService);
|
||||
this._pausedResizeTask = this._register(new DebouncedIdleTask(this._logService));
|
||||
|
||||
this._renderDebouncer = new RenderDebouncer((start, end) => this._renderRows(start, end), this._coreBrowserService);
|
||||
this._register(this._renderDebouncer);
|
||||
|
||||
@@ -9,12 +9,15 @@ import { SelectionModel } from 'browser/selection/SelectionModel';
|
||||
import { IBufferLine } from 'common/Types';
|
||||
import { MockBufferService, MockOptionsService, MockCoreService, createCellData } from 'common/TestUtils.test';
|
||||
import { BufferLine } from 'common/buffer/BufferLine';
|
||||
import { BufferLineStringCache } from 'common/buffer/BufferLineStringCache';
|
||||
import { IBufferService, IOptionsService } from 'common/services/Services';
|
||||
import { MockCoreBrowserService, MockMouseService, MockRenderService } from 'browser/TestUtils.test';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { IBuffer } from 'common/buffer/Types';
|
||||
import { IRenderService } from 'browser/services/Services';
|
||||
|
||||
const TEST_STRING_CACHE = new BufferLineStringCache();
|
||||
|
||||
class TestSelectionService extends SelectionService {
|
||||
constructor(
|
||||
bufferService: IBufferService,
|
||||
@@ -24,13 +27,23 @@ class TestSelectionService extends SelectionService {
|
||||
super(null!, null!, null!, bufferService, new MockCoreService(), new MockMouseService(), optionsService, renderService, new MockCoreBrowserService());
|
||||
}
|
||||
|
||||
public get model(): SelectionModel { return this._model; }
|
||||
public get model(): SelectionModel {
|
||||
return this._model;
|
||||
}
|
||||
|
||||
public set selectionMode(mode: SelectionMode) { this._activeSelectionMode = mode; }
|
||||
public set selectionMode(mode: SelectionMode) {
|
||||
this._activeSelectionMode = mode;
|
||||
}
|
||||
|
||||
public selectLineAt(line: number): void { this._selectLineAt(line); }
|
||||
public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords, true); }
|
||||
public areCoordsInSelection(coords: [number, number], start: [number, number], end: [number, number]): boolean { return this._areCoordsInSelection(coords, start, end); }
|
||||
public selectLineAt(line: number): void {
|
||||
this._selectLineAt(line);
|
||||
}
|
||||
public selectWordAt(coords: [number, number]): void {
|
||||
this._selectWordAt(coords, true);
|
||||
}
|
||||
public areCoordsInSelection(coords: [number, number], start: [number, number], end: [number, number]): boolean {
|
||||
return this._areCoordsInSelection(coords, start, end);
|
||||
}
|
||||
|
||||
// Disable DOM interaction
|
||||
public enable(): void {}
|
||||
@@ -55,7 +68,7 @@ describe('SelectionService', () => {
|
||||
});
|
||||
|
||||
function stringToRow(text: string): IBufferLine {
|
||||
const result = new BufferLine(text.length);
|
||||
const result = new BufferLine(TEST_STRING_CACHE, text.length);
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
result.setCell(i, createCellData(0, text.charAt(i), 1));
|
||||
}
|
||||
@@ -63,7 +76,7 @@ describe('SelectionService', () => {
|
||||
}
|
||||
|
||||
function stringArrayToRow(chars: string[]): IBufferLine {
|
||||
const line = new BufferLine(chars.length);
|
||||
const line = new BufferLine(TEST_STRING_CACHE, chars.length);
|
||||
chars.map((c, idx) => line.setCell(idx, createCellData(0, c, 1)));
|
||||
return line;
|
||||
}
|
||||
@@ -118,7 +131,7 @@ describe('SelectionService', () => {
|
||||
[0, 'o', 1, 'o'.charCodeAt(0)],
|
||||
[0, 'o', 1, 'o'.charCodeAt(0)]
|
||||
];
|
||||
const line = new BufferLine(data.length);
|
||||
const line = new BufferLine(TEST_STRING_CACHE, data.length);
|
||||
for (let i = 0; i < data.length; ++i) line.setCell(i, CellData.fromCharData(data[i]));
|
||||
buffer.lines.set(0, line);
|
||||
// Ensure wide characters take up 2 columns
|
||||
|
||||
@@ -18,27 +18,26 @@ import { IBuffer } from 'common/buffer/Types';
|
||||
import { IBufferService, ICoreService, IOptionsService } from 'common/services/Services';
|
||||
import { Emitter } from 'common/Event';
|
||||
|
||||
/**
|
||||
* The number of pixels the mouse needs to be above or below the viewport in
|
||||
* order to scroll at the maximum speed.
|
||||
*/
|
||||
const DRAG_SCROLL_MAX_THRESHOLD = 50;
|
||||
|
||||
/**
|
||||
* The maximum scrolling speed
|
||||
*/
|
||||
const DRAG_SCROLL_MAX_SPEED = 15;
|
||||
|
||||
/**
|
||||
* The number of milliseconds between drag scroll updates.
|
||||
*/
|
||||
const DRAG_SCROLL_INTERVAL = 50;
|
||||
|
||||
/**
|
||||
* The maximum amount of time that can have elapsed for an alt click to move the
|
||||
* cursor.
|
||||
*/
|
||||
const ALT_CLICK_MOVE_CURSOR_TIME = 500;
|
||||
const enum Constants {
|
||||
/**
|
||||
* The number of pixels the mouse needs to be above or below the viewport in
|
||||
* order to scroll at the maximum speed.
|
||||
*/
|
||||
DRAG_SCROLL_MAX_THRESHOLD = 50,
|
||||
/**
|
||||
* The maximum scrolling speed
|
||||
*/
|
||||
DRAG_SCROLL_MAX_SPEED = 15,
|
||||
/**
|
||||
* The number of milliseconds between drag scroll updates.
|
||||
*/
|
||||
DRAG_SCROLL_INTERVAL = 50,
|
||||
/**
|
||||
* The maximum amount of time that can have elapsed for an alt click to move the
|
||||
* cursor.
|
||||
*/
|
||||
ALT_CLICK_MOVE_CURSOR_TIME = 500
|
||||
}
|
||||
|
||||
const NON_BREAKING_SPACE_CHAR = String.fromCharCode(160);
|
||||
const ALL_NON_BREAKING_SPACE_REGEX = new RegExp(NON_BREAKING_SPACE_CHAR, 'g');
|
||||
@@ -182,8 +181,12 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
this._enabled = true;
|
||||
}
|
||||
|
||||
public get selectionStart(): [number, number] | undefined { return this._model.finalSelectionStart; }
|
||||
public get selectionEnd(): [number, number] | undefined { return this._model.finalSelectionEnd; }
|
||||
public get selectionStart(): [number, number] | undefined {
|
||||
return this._model.finalSelectionStart;
|
||||
}
|
||||
public get selectionEnd(): [number, number] | undefined {
|
||||
return this._model.finalSelectionEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether there is an active text selection.
|
||||
@@ -424,9 +427,9 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
offset -= terminalHeight;
|
||||
}
|
||||
|
||||
offset = Math.min(Math.max(offset, -DRAG_SCROLL_MAX_THRESHOLD), DRAG_SCROLL_MAX_THRESHOLD);
|
||||
offset /= DRAG_SCROLL_MAX_THRESHOLD;
|
||||
return (offset / Math.abs(offset)) + Math.round(offset * (DRAG_SCROLL_MAX_SPEED - 1));
|
||||
offset = Math.min(Math.max(offset, -Constants.DRAG_SCROLL_MAX_THRESHOLD), Constants.DRAG_SCROLL_MAX_THRESHOLD);
|
||||
offset /= Constants.DRAG_SCROLL_MAX_THRESHOLD;
|
||||
return (offset / Math.abs(offset)) + Math.round(offset * (Constants.DRAG_SCROLL_MAX_SPEED - 1));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -500,7 +503,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
this._screenElement.ownerDocument.addEventListener('mousemove', this._mouseMoveListener);
|
||||
this._screenElement.ownerDocument.addEventListener('mouseup', this._mouseUpListener);
|
||||
}
|
||||
this._dragScrollIntervalTimer = this._coreBrowserService.window.setInterval(() => this._dragScroll(), DRAG_SCROLL_INTERVAL);
|
||||
this._dragScrollIntervalTimer = this._coreBrowserService.window.setInterval(() => this._dragScroll(), Constants.DRAG_SCROLL_INTERVAL);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -675,7 +678,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
}
|
||||
|
||||
/**
|
||||
* The callback that occurs every DRAG_SCROLL_INTERVAL ms that does the
|
||||
* The callback that occurs every Constants.DRAG_SCROLL_INTERVAL ms that does the
|
||||
* scrolling of the viewport.
|
||||
*/
|
||||
private _dragScroll(): void {
|
||||
@@ -713,7 +716,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
|
||||
this._removeMouseDownListeners();
|
||||
|
||||
if (this.selectionText.length <= 1 && timeElapsed < ALT_CLICK_MOVE_CURSOR_TIME && event.altKey && this._optionsService.rawOptions.altClickMovesCursor) {
|
||||
if (this.selectionText.length <= 1 && timeElapsed < Constants.ALT_CLICK_MOVE_CURSOR_TIME && event.altKey && this._optionsService.rawOptions.altClickMovesCursor) {
|
||||
if (this._bufferService.buffer.ybase === this._bufferService.buffer.ydisp) {
|
||||
const coordinates = this._mouseCoordsService.getCoords(
|
||||
event,
|
||||
|
||||
@@ -38,7 +38,9 @@ export class ThemeService extends Disposable implements IThemeService {
|
||||
private _halfContrastCache: IColorContrastCache = new ColorContrastCache();
|
||||
private _restoreColors!: IRestoreColorSet;
|
||||
|
||||
public get colors(): ReadonlyColorSet { return this._colors; }
|
||||
public get colors(): ReadonlyColorSet {
|
||||
return this._colors;
|
||||
}
|
||||
|
||||
private readonly _onChangeColors = this._register(new Emitter<ReadonlyColorSet>());
|
||||
public readonly onChangeColors = this._onChangeColors.event;
|
||||
|
||||
+1
-2
@@ -132,8 +132,7 @@ export namespace css {
|
||||
$ctx.globalCompositeOperation = 'copy';
|
||||
$litmusColor = $ctx.createLinearGradient(0, 0, 1, 1);
|
||||
}
|
||||
}
|
||||
catch {
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, IMouseStateService, IUnicodeService, LogLevelEnum, ITerminalOptions, IOscLinkService } from 'common/services/Services';
|
||||
import { InstantiationService } from 'common/services/InstantiationService';
|
||||
import { LogService } from 'common/services/LogService';
|
||||
import { BufferService, MINIMUM_COLS, MINIMUM_ROWS } from 'common/services/BufferService';
|
||||
import { BufferService, BufferServiceConstants } from 'common/services/BufferService';
|
||||
import { OptionsService } from 'common/services/OptionsService';
|
||||
import { IDisposable, IAttributeData, ICoreTerminal, IScrollEvent } from 'common/Types';
|
||||
import { CoreService } from 'common/services/CoreService';
|
||||
@@ -88,10 +88,18 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
return this._onScrollApi.event;
|
||||
}
|
||||
|
||||
public get cols(): number { return this._bufferService.cols; }
|
||||
public get rows(): number { return this._bufferService.rows; }
|
||||
public get buffers(): IBufferSet { return this._bufferService.buffers; }
|
||||
public get options(): Required<ITerminalOptions> { return this.optionsService.options; }
|
||||
public get cols(): number {
|
||||
return this._bufferService.cols;
|
||||
}
|
||||
public get rows(): number {
|
||||
return this._bufferService.rows;
|
||||
}
|
||||
public get buffers(): IBufferSet {
|
||||
return this._bufferService.buffers;
|
||||
}
|
||||
public get options(): Required<ITerminalOptions> {
|
||||
return this.optionsService.options;
|
||||
}
|
||||
public set options(options: ITerminalOptions) {
|
||||
for (const key in options) {
|
||||
this.optionsService.options[key] = options[key];
|
||||
@@ -173,8 +181,8 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
return;
|
||||
}
|
||||
|
||||
x = Math.max(x, MINIMUM_COLS);
|
||||
y = Math.max(y, MINIMUM_ROWS);
|
||||
x = Math.max(x, BufferServiceConstants.MINIMUM_COLS);
|
||||
y = Math.max(y, BufferServiceConstants.MINIMUM_ROWS);
|
||||
|
||||
// Flush pending writes before resize to avoid race conditions where async
|
||||
// writes are processed with incorrect dimensions
|
||||
@@ -243,8 +251,8 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
}
|
||||
|
||||
/** Add handler for APC escape sequence. See xterm.d.ts for details. */
|
||||
public registerApcHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
|
||||
return this._inputHandler.registerApcHandler(ident, callback);
|
||||
public registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
|
||||
return this._inputHandler.registerApcHandler(id, callback);
|
||||
}
|
||||
|
||||
protected _setup(): void {
|
||||
|
||||
@@ -15,14 +15,18 @@ describe('Emitter', () => {
|
||||
it('should fire with 1 listener', () => {
|
||||
const emitter = new Emitter<number>();
|
||||
let received: number | undefined;
|
||||
emitter.event(e => { received = e; });
|
||||
emitter.event(e => {
|
||||
received = e;
|
||||
});
|
||||
emitter.fire(42);
|
||||
assert.strictEqual(received, 42);
|
||||
});
|
||||
|
||||
it('should fire with 1 listener using thisArgs', () => {
|
||||
const emitter = new Emitter<number>();
|
||||
const obj = { value: 0, handler(e: number) { this.value = e; } };
|
||||
const obj = { value: 0, handler(e: number) {
|
||||
this.value = e;
|
||||
} };
|
||||
emitter.event(obj.handler, obj);
|
||||
emitter.fire(42);
|
||||
assert.strictEqual(obj.value, 42);
|
||||
@@ -54,7 +58,9 @@ describe('Emitter', () => {
|
||||
it('should not fire after dispose', () => {
|
||||
const emitter = new Emitter<number>();
|
||||
let called = false;
|
||||
emitter.event(() => { called = true; });
|
||||
emitter.event(() => {
|
||||
called = true;
|
||||
});
|
||||
emitter.dispose();
|
||||
emitter.fire(42);
|
||||
assert.strictEqual(called, false);
|
||||
@@ -63,7 +69,9 @@ describe('Emitter', () => {
|
||||
it('should allow disposing a listener', () => {
|
||||
const emitter = new Emitter<number>();
|
||||
let count = 0;
|
||||
const disposable = emitter.event(() => { count++; });
|
||||
const disposable = emitter.event(() => {
|
||||
count++;
|
||||
});
|
||||
emitter.fire(1);
|
||||
disposable.dispose();
|
||||
emitter.fire(2);
|
||||
|
||||
@@ -39,9 +39,15 @@ function getLines(bufferService: IBufferService, limit: number = bufferService.r
|
||||
}
|
||||
|
||||
class TestInputHandler extends InputHandler {
|
||||
public get curAttrData(): IAttributeData { return (this as any)._curAttrData; }
|
||||
public get windowTitleStack(): string[] { return this._windowTitleStack; }
|
||||
public get iconNameStack(): string[] { return this._iconNameStack; }
|
||||
public get curAttrData(): IAttributeData {
|
||||
return (this as any)._curAttrData;
|
||||
}
|
||||
public get windowTitleStack(): string[] {
|
||||
return this._windowTitleStack;
|
||||
}
|
||||
public get iconNameStack(): string[] {
|
||||
return this._iconNameStack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Promise based parse call to await the full resolve of given input data.
|
||||
@@ -2511,7 +2517,9 @@ describe('InputHandler', () => {
|
||||
bufferService = new BufferService(optionsService, new MockLogService());
|
||||
bufferService.resize(80, 30);
|
||||
coreService = new CoreService(bufferService, new MockLogService(), optionsService);
|
||||
coreService.onData(data => { console.log(data); });
|
||||
coreService.onData(data => {
|
||||
console.log(data);
|
||||
});
|
||||
|
||||
inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockMouseStateService(), new MockUnicodeService());
|
||||
});
|
||||
|
||||
+37
-52
@@ -30,28 +30,9 @@ import { XTERM_VERSION } from 'common/Version';
|
||||
*/
|
||||
const GLEVEL: { [key: string]: number } = { '(': 0, ')': 1, '*': 2, '+': 3, '-': 1, '.': 2 };
|
||||
|
||||
/**
|
||||
* VT commands done by the parser - FIXME: move this to the parser?
|
||||
*/
|
||||
// @vt: #Y ESC CSI "Control Sequence Introducer" "ESC [" "Start of a CSI sequence."
|
||||
// @vt: #Y ESC OSC "Operating System Command" "ESC ]" "Start of an OSC sequence."
|
||||
// @vt: #Y ESC DCS "Device Control String" "ESC P" "Start of a DCS sequence."
|
||||
// @vt: #Y ESC ST "String Terminator" "ESC \" "Terminator used for string type sequences."
|
||||
// @vt: #Y ESC PM "Privacy Message" "ESC ^" "Start of a privacy message."
|
||||
// @vt: #Y ESC APC "Application Program Command" "ESC _" "Start of an APC sequence."
|
||||
// @vt: #Y C1 CSI "Control Sequence Introducer" "\x9B" "Start of a CSI sequence."
|
||||
// @vt: #Y C1 OSC "Operating System Command" "\x9D" "Start of an OSC sequence."
|
||||
// @vt: #Y C1 DCS "Device Control String" "\x90" "Start of a DCS sequence."
|
||||
// @vt: #Y C1 ST "String Terminator" "\x9C" "Terminator used for string type sequences."
|
||||
// @vt: #Y C1 PM "Privacy Message" "\x9E" "Start of a privacy message."
|
||||
// @vt: #Y C1 APC "Application Program Command" "\x9F" "Start of an APC sequence."
|
||||
// @vt: #Y C0 NUL "Null" "\0, \x00" "NUL is ignored."
|
||||
// @vt: #Y C0 ESC "Escape" "\e, \x1B" "Start of a sequence. Cancels any other sequence."
|
||||
|
||||
/**
|
||||
* Document xterm VT features here that are currently unsupported
|
||||
*/
|
||||
// @vt: #E[Supported via @xterm/addon-image.] DCS SIXEL "SIXEL Graphics" "DCS Ps ; Ps ; Ps ; q Pt ST" "Draw SIXEL image."
|
||||
// @vt: #N DCS DECUDK "User Defined Keys" "DCS Ps ; Ps \| Pt ST" "Definitions for user-defined keys."
|
||||
// @vt: #N DCS XTGETTCAP "Request Terminfo String" "DCS + q Pt ST" "Request Terminfo String."
|
||||
// @vt: #N DCS XTSETTCAP "Set Terminfo Data" "DCS + p Pt ST" "Set Terminfo Data."
|
||||
@@ -60,12 +41,13 @@ const GLEVEL: { [key: string]: number } = { '(': 0, ')': 1, '*': 2, '+': 3, '-':
|
||||
/**
|
||||
* Max length of the UTF32 input buffer. Real memory consumption is 4 times higher.
|
||||
*/
|
||||
const MAX_PARSEBUFFER_LENGTH = 131072;
|
||||
|
||||
/**
|
||||
* Limit length of title and icon name stacks.
|
||||
*/
|
||||
const STACK_LIMIT = 10;
|
||||
const enum Constants {
|
||||
MAX_PARSEBUFFER_LENGTH = 131072,
|
||||
/** Limit length of title and icon name stacks. */
|
||||
STACK_LIMIT = 10,
|
||||
// create a warning log if an async handler takes longer than the limit (in ms)
|
||||
SLOW_ASYNC_LIMIT = 5000
|
||||
}
|
||||
|
||||
// map params to window option
|
||||
function paramToWindowOption(n: number, opts: IWindowOptions): boolean {
|
||||
@@ -104,9 +86,6 @@ export enum WindowsOptionsReportType {
|
||||
GET_CELL_SIZE_PIXELS = 1
|
||||
}
|
||||
|
||||
// create a warning log if an async handler takes longer than the limit (in ms)
|
||||
const SLOW_ASYNC_LIMIT = 5000;
|
||||
|
||||
// Work variables to avoid garbage collection
|
||||
let $temp = 0;
|
||||
|
||||
@@ -128,7 +107,9 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
protected _iconNameStack: string[] = [];
|
||||
|
||||
private _curAttrData: IAttributeData = DEFAULT_ATTR_DATA.clone();
|
||||
public getAttrData(): IAttributeData { return this._curAttrData; }
|
||||
public getAttrData(): IAttributeData {
|
||||
return this._curAttrData;
|
||||
}
|
||||
private _eraseAttrDataInternal: IAttributeData = DEFAULT_ATTR_DATA.clone();
|
||||
|
||||
private _activeBuffer: IBuffer;
|
||||
@@ -211,6 +192,9 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
}
|
||||
this._logService.debug('Unknown DCS code: ', { identifier: this._parser.identToString(ident), action, payload });
|
||||
});
|
||||
this._parser.setApcHandlerFallback((ident, action, payload) => {
|
||||
this._logService.debug('Unknown APC code: ', { identifier: this._parser.identToString(ident), action, payload });
|
||||
});
|
||||
|
||||
/**
|
||||
* print handler
|
||||
@@ -301,7 +285,9 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
* OSC handler
|
||||
*/
|
||||
// 0 - icon name + title
|
||||
this._parser.registerOscHandler(0, new OscHandler(data => { this.setTitle(data); this.setIconName(data); return true; }));
|
||||
this._parser.registerOscHandler(0, new OscHandler(data => {
|
||||
this.setTitle(data); this.setIconName(data); return true;
|
||||
}));
|
||||
// 1 - icon name
|
||||
this._parser.registerOscHandler(1, new OscHandler(data => this.setIconName(data)));
|
||||
// 2 - title
|
||||
@@ -409,7 +395,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
if (this._logService.logLevel <= LogLevelEnum.WARN) {
|
||||
let slowTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||
const slowPromise = new Promise<never>((_res, rej) => {
|
||||
slowTimeout = setTimeout(() => rej('#SLOW_TIMEOUT'), SLOW_ASYNC_LIMIT);
|
||||
slowTimeout = setTimeout(() => rej('#SLOW_TIMEOUT'), Constants.SLOW_ASYNC_LIMIT);
|
||||
});
|
||||
Promise.race([p, slowPromise])
|
||||
.then(() => {
|
||||
@@ -423,7 +409,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
if (err !== '#SLOW_TIMEOUT') {
|
||||
throw err;
|
||||
}
|
||||
console.warn(`async parser handler taking longer than ${SLOW_ASYNC_LIMIT} ms`);
|
||||
console.warn(`async parser handler taking longer than ${Constants.SLOW_ASYNC_LIMIT} ms`);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -461,8 +447,8 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
cursorStartX = this._parseStack.cursorStartX;
|
||||
cursorStartY = this._parseStack.cursorStartY;
|
||||
this._parseStack.paused = false;
|
||||
if (data.length > MAX_PARSEBUFFER_LENGTH) {
|
||||
start = this._parseStack.position + MAX_PARSEBUFFER_LENGTH;
|
||||
if (data.length > Constants.MAX_PARSEBUFFER_LENGTH) {
|
||||
start = this._parseStack.position + Constants.MAX_PARSEBUFFER_LENGTH;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -479,8 +465,8 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
|
||||
// resize input buffer if needed
|
||||
if (this._parseBuffer.length < data.length) {
|
||||
if (this._parseBuffer.length < MAX_PARSEBUFFER_LENGTH) {
|
||||
this._parseBuffer = new Uint32Array(Math.min(data.length, MAX_PARSEBUFFER_LENGTH));
|
||||
if (this._parseBuffer.length < Constants.MAX_PARSEBUFFER_LENGTH) {
|
||||
this._parseBuffer = new Uint32Array(Math.min(data.length, Constants.MAX_PARSEBUFFER_LENGTH));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -491,9 +477,9 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
}
|
||||
|
||||
// process big data in smaller chunks
|
||||
if (data.length > MAX_PARSEBUFFER_LENGTH) {
|
||||
for (let i = start; i < data.length; i += MAX_PARSEBUFFER_LENGTH) {
|
||||
const end = i + MAX_PARSEBUFFER_LENGTH < data.length ? i + MAX_PARSEBUFFER_LENGTH : data.length;
|
||||
if (data.length > Constants.MAX_PARSEBUFFER_LENGTH) {
|
||||
for (let i = start; i < data.length; i += Constants.MAX_PARSEBUFFER_LENGTH) {
|
||||
const end = i + Constants.MAX_PARSEBUFFER_LENGTH < data.length ? i + Constants.MAX_PARSEBUFFER_LENGTH : data.length;
|
||||
const len = (typeof data === 'string')
|
||||
? this._stringDecoder.decode(data.substring(i, end), this._parseBuffer)
|
||||
: this._utf8Decoder.decode(data.subarray(i, end), this._parseBuffer);
|
||||
@@ -729,8 +715,8 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
/**
|
||||
* Forward registerApcHandler from parser.
|
||||
*/
|
||||
public registerApcHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
|
||||
return this._parser.registerApcHandler(ident, new ApcHandler(callback));
|
||||
public registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
|
||||
return this._parser.registerApcHandler(id, new ApcHandler(callback));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1286,8 +1272,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
for (; j >= 0; j--) {
|
||||
this._bufferService.scroll(this._eraseAttrData());
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
j = this._bufferService.rows;
|
||||
this._dirtyRowTracker.markDirty(j - 1);
|
||||
while (j--) {
|
||||
@@ -2415,7 +2400,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
color &= ~Attributes.RGB_MASK;
|
||||
color |= AttributeData.fromColorRGB([c1, c2, c3]);
|
||||
} else if (mode === 5) {
|
||||
color &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK);
|
||||
color &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);
|
||||
color |= Attributes.CM_P256 | (c1 & 0xff);
|
||||
}
|
||||
return color;
|
||||
@@ -2628,19 +2613,19 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
p = params.params[i];
|
||||
if (p >= 30 && p <= 37) {
|
||||
// fg color 8
|
||||
attr.fg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK);
|
||||
attr.fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);
|
||||
attr.fg |= Attributes.CM_P16 | (p - 30);
|
||||
} else if (p >= 40 && p <= 47) {
|
||||
// bg color 8
|
||||
attr.bg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK);
|
||||
attr.bg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);
|
||||
attr.bg |= Attributes.CM_P16 | (p - 40);
|
||||
} else if (p >= 90 && p <= 97) {
|
||||
// fg color 16
|
||||
attr.fg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK);
|
||||
attr.fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);
|
||||
attr.fg |= Attributes.CM_P16 | (p - 90) | 8;
|
||||
} else if (p >= 100 && p <= 107) {
|
||||
// bg color 16
|
||||
attr.bg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK);
|
||||
attr.bg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);
|
||||
attr.bg |= Attributes.CM_P16 | (p - 100) | 8;
|
||||
} else if (p === 0) {
|
||||
// default
|
||||
@@ -2700,11 +2685,11 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
} else if (p === 39) {
|
||||
// reset fg
|
||||
attr.fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);
|
||||
attr.fg |= DEFAULT_ATTR_DATA.fg & (Attributes.PCOLOR_MASK | Attributes.RGB_MASK);
|
||||
attr.fg |= DEFAULT_ATTR_DATA.fg & Attributes.RGB_MASK;
|
||||
} else if (p === 49) {
|
||||
// reset bg
|
||||
attr.bg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);
|
||||
attr.bg |= DEFAULT_ATTR_DATA.bg & (Attributes.PCOLOR_MASK | Attributes.RGB_MASK);
|
||||
attr.bg |= DEFAULT_ATTR_DATA.bg & Attributes.RGB_MASK;
|
||||
} else if (p === 38 || p === 48 || p === 58) {
|
||||
// fg color 256 and RGB
|
||||
i += this._extractColor(params, i, attr);
|
||||
@@ -2971,13 +2956,13 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
case 22: // PushTitle
|
||||
if (second === 0 || second === 2) {
|
||||
this._windowTitleStack.push(this._windowTitle);
|
||||
if (this._windowTitleStack.length > STACK_LIMIT) {
|
||||
if (this._windowTitleStack.length > Constants.STACK_LIMIT) {
|
||||
this._windowTitleStack.shift();
|
||||
}
|
||||
}
|
||||
if (second === 0 || second === 1) {
|
||||
this._iconNameStack.push(this._iconName);
|
||||
if (this._iconNameStack.length > STACK_LIMIT) {
|
||||
if (this._iconNameStack.length > Constants.STACK_LIMIT) {
|
||||
this._iconNameStack.shift();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,4 +168,8 @@ export class DebouncedIdleTask {
|
||||
public flush(): void {
|
||||
this._queue.flush();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._queue.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,9 @@ export const NULL_CELL_DATA = Object.freeze(createCellData(DEFAULT_ATTR, NULL_CE
|
||||
|
||||
export class MockBufferService implements IBufferService {
|
||||
public serviceBrand: any;
|
||||
public get buffer(): IBuffer { return this.buffers.active; }
|
||||
public get buffer(): IBuffer {
|
||||
return this.buffers.active;
|
||||
}
|
||||
public buffers: IBufferSet = {} as any;
|
||||
public onResize: IEvent<IBufferResizeEvent> = new Emitter<IBufferResizeEvent>().event;
|
||||
public onScroll: IEvent<number> = new Emitter<number>().event;
|
||||
@@ -83,10 +85,16 @@ export class MockMouseStateService implements IMouseStateService {
|
||||
public addProtocol(name: string): void { }
|
||||
public reset(): void { }
|
||||
public onProtocolChange: IEvent<CoreMouseEventType> = new Emitter<CoreMouseEventType>().event;
|
||||
public restrictMouseEvent(event: ICoreMouseEvent): boolean { return true; }
|
||||
public encodeMouseEvent(event: ICoreMouseEvent): string { return ''; }
|
||||
public restrictMouseEvent(event: ICoreMouseEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
public encodeMouseEvent(event: ICoreMouseEvent): string {
|
||||
return '';
|
||||
}
|
||||
public setCustomWheelEventHandler(customWheelEventHandler: ((event: WheelEvent) => boolean) | undefined): void { }
|
||||
public allowCustomWheelEvent(ev: WheelEvent): boolean { return true; }
|
||||
public allowCustomWheelEvent(ev: WheelEvent): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class MockCharsetService implements ICharsetService {
|
||||
@@ -233,10 +241,14 @@ export class MockUnicodeService implements IUnicodeService {
|
||||
|
||||
export class MockDecorationService implements IDecorationService {
|
||||
public serviceBrand: any;
|
||||
public get decorations(): IterableIterator<IInternalDecoration> { return [].values(); }
|
||||
public get decorations(): IterableIterator<IInternalDecoration> {
|
||||
return [].values();
|
||||
}
|
||||
public onDecorationRegistered = new Emitter<IInternalDecoration>().event;
|
||||
public onDecorationRemoved = new Emitter<IInternalDecoration>().event;
|
||||
public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined { return undefined; }
|
||||
public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined {
|
||||
return undefined;
|
||||
}
|
||||
public reset(): void { }
|
||||
public forEachDecorationAtCell(x: number, line: number, layer: 'bottom' | 'top' | undefined, callback: (decoration: IInternalDecoration) => void): void { }
|
||||
public dispose(): void { }
|
||||
|
||||
+2
-2
@@ -22,7 +22,7 @@ export interface ICoreTerminal {
|
||||
registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise<boolean>): IDisposable;
|
||||
registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise<boolean>): IDisposable;
|
||||
registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
|
||||
registerApcHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
|
||||
registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
|
||||
}
|
||||
|
||||
export interface IDisposable {
|
||||
@@ -476,7 +476,7 @@ export interface IInputHandler {
|
||||
registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise<boolean>): IDisposable;
|
||||
registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise<boolean>): IDisposable;
|
||||
registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
|
||||
registerApcHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
|
||||
registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
|
||||
|
||||
/** C0 BEL */ bell(): boolean;
|
||||
/** C0 LF */ lineFeed(): boolean;
|
||||
|
||||
@@ -33,32 +33,68 @@ export class AttributeData implements IAttributeData {
|
||||
public extended: IExtendedAttrs = new ExtendedAttrs();
|
||||
|
||||
// flags
|
||||
public isInverse(): number { return this.fg & FgFlags.INVERSE; }
|
||||
public isBold(): number { return this.fg & FgFlags.BOLD; }
|
||||
public isInverse(): number {
|
||||
return this.fg & FgFlags.INVERSE;
|
||||
}
|
||||
public isBold(): number {
|
||||
return this.fg & FgFlags.BOLD;
|
||||
}
|
||||
public isUnderline(): number {
|
||||
if (this.hasExtendedAttrs() && this.extended.underlineStyle !== UnderlineStyle.NONE) {
|
||||
return 1;
|
||||
}
|
||||
return this.fg & FgFlags.UNDERLINE;
|
||||
}
|
||||
public isBlink(): number { return this.fg & FgFlags.BLINK; }
|
||||
public isInvisible(): number { return this.fg & FgFlags.INVISIBLE; }
|
||||
public isItalic(): number { return this.bg & BgFlags.ITALIC; }
|
||||
public isDim(): number { return this.bg & BgFlags.DIM; }
|
||||
public isStrikethrough(): number { return this.fg & FgFlags.STRIKETHROUGH; }
|
||||
public isProtected(): number { return this.bg & BgFlags.PROTECTED; }
|
||||
public isOverline(): number { return this.bg & BgFlags.OVERLINE; }
|
||||
public isBlink(): number {
|
||||
return this.fg & FgFlags.BLINK;
|
||||
}
|
||||
public isInvisible(): number {
|
||||
return this.fg & FgFlags.INVISIBLE;
|
||||
}
|
||||
public isItalic(): number {
|
||||
return this.bg & BgFlags.ITALIC;
|
||||
}
|
||||
public isDim(): number {
|
||||
return this.bg & BgFlags.DIM;
|
||||
}
|
||||
public isStrikethrough(): number {
|
||||
return this.fg & FgFlags.STRIKETHROUGH;
|
||||
}
|
||||
public isProtected(): number {
|
||||
return this.bg & BgFlags.PROTECTED;
|
||||
}
|
||||
public isOverline(): number {
|
||||
return this.bg & BgFlags.OVERLINE;
|
||||
}
|
||||
|
||||
// color modes
|
||||
public getFgColorMode(): number { return this.fg & Attributes.CM_MASK; }
|
||||
public getBgColorMode(): number { return this.bg & Attributes.CM_MASK; }
|
||||
public isFgRGB(): boolean { return (this.fg & Attributes.CM_MASK) === Attributes.CM_RGB; }
|
||||
public isBgRGB(): boolean { return (this.bg & Attributes.CM_MASK) === Attributes.CM_RGB; }
|
||||
public isFgPalette(): boolean { return (this.fg & Attributes.CM_MASK) === Attributes.CM_P16 || (this.fg & Attributes.CM_MASK) === Attributes.CM_P256; }
|
||||
public isBgPalette(): boolean { return (this.bg & Attributes.CM_MASK) === Attributes.CM_P16 || (this.bg & Attributes.CM_MASK) === Attributes.CM_P256; }
|
||||
public isFgDefault(): boolean { return (this.fg & Attributes.CM_MASK) === 0; }
|
||||
public isBgDefault(): boolean { return (this.bg & Attributes.CM_MASK) === 0; }
|
||||
public isAttributeDefault(): boolean { return this.fg === 0 && this.bg === 0; }
|
||||
public getFgColorMode(): number {
|
||||
return this.fg & Attributes.CM_MASK;
|
||||
}
|
||||
public getBgColorMode(): number {
|
||||
return this.bg & Attributes.CM_MASK;
|
||||
}
|
||||
public isFgRGB(): boolean {
|
||||
return (this.fg & Attributes.CM_MASK) === Attributes.CM_RGB;
|
||||
}
|
||||
public isBgRGB(): boolean {
|
||||
return (this.bg & Attributes.CM_MASK) === Attributes.CM_RGB;
|
||||
}
|
||||
public isFgPalette(): boolean {
|
||||
return (this.fg & Attributes.CM_MASK) === Attributes.CM_P16 || (this.fg & Attributes.CM_MASK) === Attributes.CM_P256;
|
||||
}
|
||||
public isBgPalette(): boolean {
|
||||
return (this.bg & Attributes.CM_MASK) === Attributes.CM_P16 || (this.bg & Attributes.CM_MASK) === Attributes.CM_P256;
|
||||
}
|
||||
public isFgDefault(): boolean {
|
||||
return (this.fg & Attributes.CM_MASK) === 0;
|
||||
}
|
||||
public isBgDefault(): boolean {
|
||||
return (this.bg & Attributes.CM_MASK) === 0;
|
||||
}
|
||||
public isAttributeDefault(): boolean {
|
||||
return this.fg === 0 && this.bg === 0;
|
||||
}
|
||||
|
||||
// colors
|
||||
public getFgColor(): number {
|
||||
@@ -147,7 +183,9 @@ export class ExtendedAttrs implements IExtendedAttrs {
|
||||
}
|
||||
return this._ext;
|
||||
}
|
||||
public set ext(value: number) { this._ext = value; }
|
||||
public set ext(value: number) {
|
||||
this._ext = value;
|
||||
}
|
||||
|
||||
public get underlineStyle(): UnderlineStyle {
|
||||
// Always return the URL style if it has one
|
||||
|
||||
@@ -8,22 +8,34 @@ import { Buffer } from 'common/buffer/Buffer';
|
||||
import { CircularList } from 'common/CircularList';
|
||||
import { MockOptionsService, MockBufferService, MockLogService, createCellData } from 'common/TestUtils.test';
|
||||
import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
|
||||
import { BufferLineStringCache } from 'common/buffer/BufferLineStringCache';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { ExtendedAttrs } from 'common/buffer/AttributeData';
|
||||
|
||||
const INIT_COLS = 80;
|
||||
const INIT_ROWS = 24;
|
||||
const INIT_SCROLLBACK = 1000;
|
||||
const TEST_STRING_CACHE = new BufferLineStringCache();
|
||||
|
||||
class TestBuffer extends Buffer {
|
||||
public getStringCache(): BufferLineStringCache {
|
||||
return (this as unknown as { _stringCache: BufferLineStringCache })._stringCache;
|
||||
}
|
||||
|
||||
public getStringCacheClearTimeout(): unknown {
|
||||
return (this.getStringCache() as unknown as { _clearTimeout: { value: unknown } })._clearTimeout.value;
|
||||
}
|
||||
}
|
||||
|
||||
describe('Buffer', () => {
|
||||
let optionsService: MockOptionsService;
|
||||
let bufferService: MockBufferService;
|
||||
let buffer: Buffer;
|
||||
let buffer: TestBuffer;
|
||||
|
||||
beforeEach(() => {
|
||||
optionsService = new MockOptionsService({ scrollback: INIT_SCROLLBACK });
|
||||
bufferService = new MockBufferService(INIT_COLS, INIT_ROWS);
|
||||
buffer = new Buffer(true, optionsService, bufferService, new MockLogService());
|
||||
buffer = new TestBuffer(true, optionsService, bufferService, new MockLogService());
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
@@ -151,7 +163,7 @@ describe('Buffer', () => {
|
||||
|
||||
describe('no scrollback', () => {
|
||||
it('should trim from the top of the buffer when the cursor reaches the bottom', () => {
|
||||
buffer = new Buffer(true, new MockOptionsService({ scrollback: 0 }), bufferService, new MockLogService());
|
||||
buffer = new TestBuffer(true, new MockOptionsService({ scrollback: 0 }), bufferService, new MockLogService());
|
||||
assert.equal(buffer.lines.maxLength, INIT_ROWS);
|
||||
buffer.y = INIT_ROWS - 1;
|
||||
buffer.fillViewportRows();
|
||||
@@ -1054,7 +1066,7 @@ describe('Buffer', () => {
|
||||
describe('buffer marked to have no scrollback', () => {
|
||||
it('should always have a scrollback of 0', () => {
|
||||
// Test size on initialization
|
||||
buffer = new Buffer(false, new MockOptionsService({ scrollback: 1000 }), bufferService, new MockLogService());
|
||||
buffer = new TestBuffer(false, new MockOptionsService({ scrollback: 1000 }), bufferService, new MockLogService());
|
||||
buffer.fillViewportRows();
|
||||
assert.equal(buffer.lines.maxLength, INIT_ROWS);
|
||||
// Test size on buffer increase
|
||||
@@ -1068,7 +1080,7 @@ describe('Buffer', () => {
|
||||
|
||||
describe('addMarker', () => {
|
||||
it('should adjust a marker line when the buffer is trimmed', () => {
|
||||
buffer = new Buffer(true, new MockOptionsService({ scrollback: 0 }), bufferService, new MockLogService());
|
||||
buffer = new TestBuffer(true, new MockOptionsService({ scrollback: 0 }), bufferService, new MockLogService());
|
||||
buffer.fillViewportRows();
|
||||
const marker = buffer.addMarker(buffer.lines.length - 1);
|
||||
assert.equal(marker.line, buffer.lines.length - 1);
|
||||
@@ -1076,7 +1088,7 @@ describe('Buffer', () => {
|
||||
assert.equal(marker.line, buffer.lines.length - 2);
|
||||
});
|
||||
it('should dispose of a marker if it is trimmed off the buffer', () => {
|
||||
buffer = new Buffer(true, new MockOptionsService({ scrollback: 0 }), bufferService, new MockLogService());
|
||||
buffer = new TestBuffer(true, new MockOptionsService({ scrollback: 0 }), bufferService, new MockLogService());
|
||||
buffer.fillViewportRows();
|
||||
assert.equal(buffer.markers.length, 0);
|
||||
const marker = buffer.addMarker(0);
|
||||
@@ -1088,7 +1100,7 @@ describe('Buffer', () => {
|
||||
});
|
||||
it('should call onDispose', () => {
|
||||
const eventStack: string[] = [];
|
||||
buffer = new Buffer(true, new MockOptionsService({ scrollback: 0 }), bufferService, new MockLogService());
|
||||
buffer = new TestBuffer(true, new MockOptionsService({ scrollback: 0 }), bufferService, new MockLogService());
|
||||
buffer.fillViewportRows();
|
||||
assert.equal(buffer.markers.length, 0);
|
||||
const marker = buffer.addMarker(0);
|
||||
@@ -1104,7 +1116,7 @@ describe('Buffer', () => {
|
||||
|
||||
describe ('translateBufferLineToString', () => {
|
||||
it('should handle selecting a section of ascii text', () => {
|
||||
const line = new BufferLine(4);
|
||||
const line = new BufferLine(TEST_STRING_CACHE, 4);
|
||||
line.setCell(0, createCellData(0, 'a', 1));
|
||||
line.setCell(1, createCellData(0, 'b', 1));
|
||||
line.setCell(2, createCellData(0, 'c', 1));
|
||||
@@ -1116,7 +1128,7 @@ describe('Buffer', () => {
|
||||
});
|
||||
|
||||
it('should handle a cut-off double width character by including it', () => {
|
||||
const line = new BufferLine(3);
|
||||
const line = new BufferLine(TEST_STRING_CACHE, 3);
|
||||
line.setCell(0, createCellData(0, '語', 2));
|
||||
line.setCell(1, createCellData(0, '', 0));
|
||||
line.setCell(2, createCellData(0, 'a', 1));
|
||||
@@ -1127,7 +1139,7 @@ describe('Buffer', () => {
|
||||
});
|
||||
|
||||
it('should handle a zero width character in the middle of the string by not including it', () => {
|
||||
const line = new BufferLine(3);
|
||||
const line = new BufferLine(TEST_STRING_CACHE, 3);
|
||||
line.setCell(0, createCellData(0, '語', 2));
|
||||
line.setCell(1, createCellData(0, '', 0));
|
||||
line.setCell(2, createCellData(0, 'a', 1));
|
||||
@@ -1144,7 +1156,7 @@ describe('Buffer', () => {
|
||||
});
|
||||
|
||||
it('should handle single width emojis', () => {
|
||||
const line = new BufferLine(2);
|
||||
const line = new BufferLine(TEST_STRING_CACHE, 2);
|
||||
line.setCell(0, createCellData(0, '😁', 1));
|
||||
line.setCell(1, createCellData(0, 'a', 1));
|
||||
buffer.lines.set(0, line);
|
||||
@@ -1157,7 +1169,7 @@ describe('Buffer', () => {
|
||||
});
|
||||
|
||||
it('should handle double width emojis', () => {
|
||||
const line = new BufferLine(2);
|
||||
const line = new BufferLine(TEST_STRING_CACHE, 2);
|
||||
line.setCell(0, createCellData(0, '😁', 2));
|
||||
line.setCell(1, createCellData(0, '', 0));
|
||||
buffer.lines.set(0, line);
|
||||
@@ -1168,7 +1180,7 @@ describe('Buffer', () => {
|
||||
const str2 = buffer.translateBufferLineToString(0, true, 0, 2);
|
||||
assert.equal(str2, '😁');
|
||||
|
||||
const line2 = new BufferLine(3);
|
||||
const line2 = new BufferLine(TEST_STRING_CACHE, 3);
|
||||
line2.setCell(0, createCellData(0, '😁', 2));
|
||||
line2.setCell(1, createCellData(0, '', 0));
|
||||
line2.setCell(2, createCellData(0, 'a', 1));
|
||||
@@ -1179,6 +1191,99 @@ describe('Buffer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('line string cache cleanup', () => {
|
||||
it('should clear shared cache entries with a single timer', () => {
|
||||
const originalSetTimeout = globalThis.setTimeout;
|
||||
const originalClearTimeout = globalThis.clearTimeout;
|
||||
const originalDateNow = Date.now;
|
||||
let timeoutId = 0;
|
||||
let now = 0;
|
||||
const clearedTimeouts: number[] = [];
|
||||
const scheduledTimeouts = new Map<number, { delay: number, fire: () => void }>();
|
||||
(globalThis as any).setTimeout = ((handler: (...args: any[]) => void, timeout?: number) => {
|
||||
const id = ++timeoutId;
|
||||
scheduledTimeouts.set(id, {
|
||||
delay: timeout ?? 0,
|
||||
fire: () => {
|
||||
scheduledTimeouts.delete(id);
|
||||
handler();
|
||||
}
|
||||
});
|
||||
return id as ReturnType<typeof setTimeout>;
|
||||
}) as typeof setTimeout;
|
||||
(globalThis as any).clearTimeout = ((id: ReturnType<typeof setTimeout>) => {
|
||||
const numericId = id as unknown as number;
|
||||
clearedTimeouts.push(numericId);
|
||||
scheduledTimeouts.delete(numericId);
|
||||
}) as typeof clearTimeout;
|
||||
Date.now = () => now;
|
||||
try {
|
||||
buffer.fillViewportRows();
|
||||
buffer.lines.get(0)!.setCell(0, createCellData(0, 'a', 1));
|
||||
buffer.lines.get(1)!.setCell(0, createCellData(0, 'b', 1));
|
||||
|
||||
assert.equal(buffer.translateBufferLineToString(0, false), `a${' '.repeat(INIT_COLS - 1)}`);
|
||||
assert.equal(buffer.translateBufferLineToString(1, false), `b${' '.repeat(INIT_COLS - 1)}`);
|
||||
|
||||
const cache = buffer.getStringCache();
|
||||
assert.equal(cache.entries.size, 2);
|
||||
assert.ok(buffer.getStringCacheClearTimeout() !== undefined);
|
||||
assert.equal(scheduledTimeouts.size, 1);
|
||||
assert.equal([...scheduledTimeouts.values()][0].delay, 15000);
|
||||
const initialTimerCreationCount = timeoutId;
|
||||
|
||||
now = 5000;
|
||||
assert.equal(buffer.translateBufferLineToString(0, false), `a${' '.repeat(INIT_COLS - 1)}`);
|
||||
assert.equal(timeoutId, initialTimerCreationCount);
|
||||
assert.equal(scheduledTimeouts.size, 1);
|
||||
assert.deepEqual(clearedTimeouts, []);
|
||||
|
||||
now = 15000;
|
||||
[...scheduledTimeouts.values()][0].fire();
|
||||
assert.equal(timeoutId, initialTimerCreationCount + 1);
|
||||
assert.ok(buffer.getStringCacheClearTimeout() !== undefined);
|
||||
assert.equal(scheduledTimeouts.size, 1);
|
||||
assert.equal([...scheduledTimeouts.values()][0].delay, 5000);
|
||||
|
||||
now = 20000;
|
||||
[...scheduledTimeouts.values()][0].fire();
|
||||
|
||||
assert.equal(cache.entries.size, 0);
|
||||
assert.equal(buffer.getStringCacheClearTimeout(), undefined);
|
||||
|
||||
assert.equal(buffer.translateBufferLineToString(0, false), `a${' '.repeat(INIT_COLS - 1)}`);
|
||||
assert.equal(cache.entries.size, 1);
|
||||
} finally {
|
||||
Date.now = originalDateNow;
|
||||
globalThis.setTimeout = originalSetTimeout;
|
||||
globalThis.clearTimeout = originalClearTimeout;
|
||||
}
|
||||
});
|
||||
|
||||
it('should reset line string cache state on clear and resize', () => {
|
||||
buffer.fillViewportRows();
|
||||
buffer.lines.get(0)!.setCell(0, createCellData(0, 'a', 1));
|
||||
buffer.translateBufferLineToString(0, false);
|
||||
|
||||
const cache = buffer.getStringCache();
|
||||
assert.equal(cache.entries.size, 1);
|
||||
assert.ok(buffer.getStringCacheClearTimeout() !== undefined);
|
||||
|
||||
buffer.clear();
|
||||
assert.equal(cache.entries.size, 0);
|
||||
assert.equal(buffer.getStringCacheClearTimeout(), undefined);
|
||||
|
||||
buffer.fillViewportRows();
|
||||
buffer.lines.get(0)!.setCell(0, createCellData(0, 'b', 1));
|
||||
buffer.translateBufferLineToString(0, false);
|
||||
assert.equal(cache.entries.size, 1);
|
||||
|
||||
buffer.resize(INIT_COLS - 1, INIT_ROWS);
|
||||
assert.equal(cache.entries.size, 0);
|
||||
assert.equal(buffer.getStringCacheClearTimeout(), undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('memory cleanup after shrinking', () => {
|
||||
it('should realign memory from idle task execution', async () => {
|
||||
buffer.fillViewportRows();
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
*/
|
||||
|
||||
import { CircularList, IInsertEvent } from 'common/CircularList';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { IdleTaskQueue } from 'common/TaskQueue';
|
||||
import { IAttributeData, IBufferLine, ICellData, ICharset } from 'common/Types';
|
||||
import { ExtendedAttrs } from 'common/buffer/AttributeData';
|
||||
import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
|
||||
import { BufferLineStringCache } from 'common/buffer/BufferLineStringCache';
|
||||
import { getWrappedLineTrimmedLength, reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths } from 'common/buffer/BufferReflow';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE, WHITESPACE_CELL_WIDTH } from 'common/buffer/Constants';
|
||||
@@ -25,7 +27,7 @@ export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1
|
||||
* - cursor position
|
||||
* - scroll position
|
||||
*/
|
||||
export class Buffer implements IBuffer {
|
||||
export class Buffer extends Disposable implements IBuffer {
|
||||
public lines: CircularList<IBufferLine>;
|
||||
public ydisp: number = 0;
|
||||
public ybase: number = 0;
|
||||
@@ -50,6 +52,7 @@ export class Buffer implements IBuffer {
|
||||
private _isClearing: boolean = false;
|
||||
private _memoryCleanupQueue: InstanceType<typeof IdleTaskQueue>;
|
||||
private _memoryCleanupPosition = 0;
|
||||
private readonly _stringCache: BufferLineStringCache;
|
||||
|
||||
constructor(
|
||||
private _hasScrollback: boolean,
|
||||
@@ -57,6 +60,7 @@ export class Buffer implements IBuffer {
|
||||
private _bufferService: IBufferService,
|
||||
private readonly _logService: ILogService
|
||||
) {
|
||||
super();
|
||||
this._cols = this._bufferService.cols;
|
||||
this._rows = this._bufferService.rows;
|
||||
this.lines = new CircularList<IBufferLine>(this._getCorrectBufferLength(this._rows));
|
||||
@@ -64,6 +68,9 @@ export class Buffer implements IBuffer {
|
||||
this.scrollBottom = this._rows - 1;
|
||||
this.setupTabStops();
|
||||
this._memoryCleanupQueue = new IdleTaskQueue(this._logService);
|
||||
this._register(toDisposable(() => this._memoryCleanupQueue.clear()));
|
||||
this._register(toDisposable(() => this.clearAllMarkers()));
|
||||
this._stringCache = this._register(new BufferLineStringCache());
|
||||
}
|
||||
|
||||
public getNullCell(attr?: IAttributeData): ICellData {
|
||||
@@ -93,7 +100,7 @@ export class Buffer implements IBuffer {
|
||||
}
|
||||
|
||||
public getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine {
|
||||
return new BufferLine(this._bufferService.cols, this.getNullCell(attr), isWrapped);
|
||||
return new BufferLine(this._stringCache, this._bufferService.cols, this.getNullCell(attr), isWrapped);
|
||||
}
|
||||
|
||||
public get hasScrollback(): boolean {
|
||||
@@ -138,6 +145,7 @@ export class Buffer implements IBuffer {
|
||||
* Clears the buffer to it's initial state, discarding all previous data.
|
||||
*/
|
||||
public clear(): void {
|
||||
this._stringCache.clear();
|
||||
this.ydisp = 0;
|
||||
this.ybase = 0;
|
||||
this.y = 0;
|
||||
@@ -156,6 +164,7 @@ export class Buffer implements IBuffer {
|
||||
public resize(newCols: number, newRows: number): void {
|
||||
// store reference to null cell with default attrs
|
||||
const nullCell = this.getNullCell(DEFAULT_ATTR_DATA);
|
||||
this._stringCache.clear();
|
||||
|
||||
// count bufferlines with overly big memory to be cleaned afterwards
|
||||
let dirtyMemoryLines = 0;
|
||||
@@ -190,7 +199,7 @@ export class Buffer implements IBuffer {
|
||||
if (this._optionsService.rawOptions.windowsPty.backend !== undefined || this._optionsService.rawOptions.windowsPty.buildNumber !== undefined) {
|
||||
// Just add the new missing rows on Windows as conpty reprints the screen with it's
|
||||
// view of the world. Once a line enters scrollback for conpty it remains there
|
||||
this.lines.push(new BufferLine(newCols, nullCell));
|
||||
this.lines.push(new BufferLine(this._stringCache, newCols, nullCell, false));
|
||||
} else {
|
||||
if (this.ybase > 0 && this.lines.length <= this.ybase + this.y + addToY + 1) {
|
||||
// There is room above the buffer and there are no empty elements below the line,
|
||||
@@ -204,7 +213,7 @@ export class Buffer implements IBuffer {
|
||||
} else {
|
||||
// Add a blank line if there is no buffer left at the top to scroll to, or if there
|
||||
// are blank lines after the cursor
|
||||
this.lines.push(new BufferLine(newCols, nullCell));
|
||||
this.lines.push(new BufferLine(this._stringCache, newCols, nullCell, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -345,7 +354,7 @@ export class Buffer implements IBuffer {
|
||||
}
|
||||
if (this.lines.length < newRows) {
|
||||
// Add an extra row at the bottom of the viewport
|
||||
this.lines.push(new BufferLine(newCols, nullCell));
|
||||
this.lines.push(new BufferLine(this._stringCache, newCols, nullCell, false));
|
||||
}
|
||||
} else {
|
||||
if (this.ydisp === this.ybase) {
|
||||
|
||||
@@ -4,18 +4,41 @@
|
||||
*/
|
||||
import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR, Content, UnderlineStyle, BgFlags, Attributes, FgFlags } from 'common/buffer/Constants';
|
||||
import { BufferLine } from 'common/buffer//BufferLine';
|
||||
import { BufferLineStringCache } from 'common/buffer/BufferLineStringCache';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { CharData, IBufferLine } from '../Types';
|
||||
import { CharData, IBufferLine, ICellData } from '../Types';
|
||||
import { assert } from 'chai';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { createCellData, NULL_CELL_DATA, extendedAttributes } from 'common/TestUtils.test';
|
||||
|
||||
const TEST_STRING_CACHE = new BufferLineStringCache();
|
||||
|
||||
|
||||
class TestBufferLine extends BufferLine {
|
||||
constructor(cols: number, fillCellData?: ICellData, isWrapped: boolean = false) {
|
||||
super(TEST_STRING_CACHE, cols, fillCellData, isWrapped);
|
||||
}
|
||||
|
||||
public get combined(): {[index: number]: string} {
|
||||
return this._combined;
|
||||
}
|
||||
|
||||
public get cachedString(): string | undefined {
|
||||
return this._getStringCacheEntry(false)?.value;
|
||||
}
|
||||
|
||||
public set cachedString(value: string | undefined) {
|
||||
this._getStringCacheEntry(true)!.value = value;
|
||||
}
|
||||
|
||||
public get isCachedStringTrimmed(): boolean {
|
||||
return this._getStringCacheEntry(false)?.isTrimmed ?? false;
|
||||
}
|
||||
|
||||
public set isCachedStringTrimmed(value: boolean) {
|
||||
this._getStringCacheEntry(true)!.isTrimmed = value;
|
||||
}
|
||||
|
||||
public toArray(): CharData[] {
|
||||
const result = [];
|
||||
for (let i = 0; i < this.length; ++i) {
|
||||
@@ -807,5 +830,81 @@ describe('BufferLine', function(): void {
|
||||
assert.equal(extendedAttributes(line, 3), extendedAttributes(initial, 3));
|
||||
assert.equal(extendedAttributes(line, 4), extendedAttributes(initial, 4));
|
||||
});
|
||||
|
||||
it('should cache canonical string translations', () => {
|
||||
const line = new TestBufferLine(5);
|
||||
line.setCell(0, createCellData(1, 'a', 1));
|
||||
line.setCell(1, createCellData(1, 'b', 1));
|
||||
line.setCell(2, createCellData(1, 'c', 1));
|
||||
|
||||
// Trimmed-only canonical request should cache the trimmed value.
|
||||
const trimmed = line.translateToString(true, undefined, undefined, undefined);
|
||||
assert.equal(trimmed, 'abc');
|
||||
assert.equal(line.cachedString, 'abc');
|
||||
assert.equal(line.isCachedStringTrimmed, true);
|
||||
|
||||
// Non-trimmed canonical request should refresh cache with the full value.
|
||||
const translated = line.translateToString(false, undefined, undefined, undefined);
|
||||
assert.equal(translated, 'abc ');
|
||||
assert.equal(line.cachedString, 'abc ');
|
||||
assert.equal(line.isCachedStringTrimmed, false);
|
||||
|
||||
// Once non-trimmed is cached, trimmed should be derived via trimEnd().
|
||||
assert.equal(line.translateToString(true, undefined, undefined, undefined), 'abc');
|
||||
assert.equal(line.cachedString, 'abc ');
|
||||
assert.equal(line.isCachedStringTrimmed, false);
|
||||
|
||||
line.cachedString = 'cached-non-trimmed ';
|
||||
line.isCachedStringTrimmed = false;
|
||||
assert.equal(line.translateToString(false, undefined, undefined, undefined), 'cached-non-trimmed ');
|
||||
assert.equal(line.translateToString(true, undefined, undefined, undefined), 'cached-non-trimmed');
|
||||
|
||||
line.cachedString = 'cached-trimmed';
|
||||
line.isCachedStringTrimmed = true;
|
||||
assert.equal(line.translateToString(true, undefined, undefined, undefined), 'cached-trimmed');
|
||||
assert.equal(line.translateToString(false, undefined, undefined, undefined), 'abc ');
|
||||
assert.equal(line.cachedString, 'abc ');
|
||||
assert.equal(line.isCachedStringTrimmed, false);
|
||||
|
||||
// Any optional translation argument should bypass cache.
|
||||
assert.equal(line.translateToString(false, 0, 2, undefined), 'ab');
|
||||
assert.equal(line.translateToString(true, 0, 2, undefined), 'ab');
|
||||
});
|
||||
|
||||
it('should invalidate cached canonical strings on line mutations', () => {
|
||||
const assertCacheInvalidated = (mutate: (line: TestBufferLine) => void): void => {
|
||||
const line = new TestBufferLine(5);
|
||||
line.fill(createCellData(1, 'a', 1));
|
||||
line.translateToString(true, undefined, undefined, undefined);
|
||||
assert.equal(line.cachedString, 'aaaaa');
|
||||
assert.equal(line.isCachedStringTrimmed, true);
|
||||
line.translateToString(false, undefined, undefined, undefined);
|
||||
assert.equal(line.cachedString, 'aaaaa');
|
||||
assert.equal(line.isCachedStringTrimmed, false);
|
||||
mutate(line);
|
||||
assert.equal(line.cachedString, undefined);
|
||||
assert.equal(line.isCachedStringTrimmed, false);
|
||||
};
|
||||
|
||||
assertCacheInvalidated(line => line.set(0, [0, 'b', 1, 'b'.charCodeAt(0)]));
|
||||
assertCacheInvalidated(line => line.setCell(0, createCellData(1, 'b', 1)));
|
||||
assertCacheInvalidated(line => line.setCellFromCodepoint(0, 'b'.charCodeAt(0), 1, createCellData(1, 'b', 1)));
|
||||
assertCacheInvalidated(line => line.addCodepointToCell(0, 0x301, 0));
|
||||
assertCacheInvalidated(line => line.insertCells(1, 1, createCellData(1, 'b', 1)));
|
||||
assertCacheInvalidated(line => line.deleteCells(1, 1, createCellData(1, 'b', 1)));
|
||||
assertCacheInvalidated(line => line.replaceCells(1, 3, createCellData(1, 'b', 1)));
|
||||
assertCacheInvalidated(line => line.resize(6, createCellData(1, 'b', 1)));
|
||||
assertCacheInvalidated(line => line.fill(createCellData(1, 'b', 1)));
|
||||
assertCacheInvalidated(line => {
|
||||
const src = new TestBufferLine(5);
|
||||
src.fill(createCellData(1, 'x', 1));
|
||||
line.copyFrom(src);
|
||||
});
|
||||
assertCacheInvalidated(line => {
|
||||
const src = new TestBufferLine(5);
|
||||
src.fill(createCellData(1, 'x', 1));
|
||||
line.copyCellsFrom(src, 0, 0, 2, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+128
-59
@@ -9,25 +9,26 @@ import { CellData } from 'common/buffer/CellData';
|
||||
import { Attributes, BgFlags, CHAR_DATA_ATTR_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, Content, NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH, WHITESPACE_CELL_CHAR } from 'common/buffer/Constants';
|
||||
import { stringFromCodePoint } from 'common/input/TextDecoder';
|
||||
|
||||
/**
|
||||
* buffer memory layout:
|
||||
*
|
||||
* | uint32_t | uint32_t | uint32_t |
|
||||
* | `content` | `FG` | `BG` |
|
||||
* | wcwidth(2) comb(1) codepoint(21) | flags(8) R(8) G(8) B(8) | flags(8) R(8) G(8) B(8) |
|
||||
*/
|
||||
// Buffer memory layout:
|
||||
//
|
||||
// [0]: content `uint32_t` - wcwidth(2) comb(1) codepoint(21)
|
||||
// [1]: fg `uint32_t` - flags(8) r(8) g(8) b(8)
|
||||
// [2]: bg `uint32_t` - flags(8) r(8) g(8) b(8)
|
||||
|
||||
|
||||
/** typed array slots taken by one cell */
|
||||
const CELL_SIZE = 3;
|
||||
const enum Constants {
|
||||
/** The number of 32 bit array indices taken by one cell. */
|
||||
CELL_INDICIES = 3,
|
||||
/** Factor when to cleanup underlying array buffer after shrinking. */
|
||||
CLEANUP_THRESHOLD = 2
|
||||
}
|
||||
|
||||
/**
|
||||
* Cell member indices.
|
||||
*
|
||||
* Direct access:
|
||||
* `content = data[column * CELL_SIZE + Cell.CONTENT];`
|
||||
* `fg = data[column * CELL_SIZE + Cell.FG];`
|
||||
* `bg = data[column * CELL_SIZE + Cell.BG];`
|
||||
* `content = data[column * Constants.CELL_INDICIES + Cell.CONTENT];`
|
||||
* `fg = data[column * Constants.CELL_INDICIES + Cell.FG];`
|
||||
* `bg = data[column * Constants.CELL_INDICIES + Cell.BG];`
|
||||
*/
|
||||
const enum Cell {
|
||||
CONTENT = 0,
|
||||
@@ -41,8 +42,17 @@ export const DEFAULT_ATTR_DATA = Object.freeze(new AttributeData());
|
||||
let $startIndex = 0;
|
||||
const $workCell = new CellData();
|
||||
|
||||
/** Factor when to cleanup underlying array buffer after shrinking. */
|
||||
const CLEANUP_THRESHOLD = 2;
|
||||
export interface IBufferLineStringCacheEntry {
|
||||
value: string | undefined;
|
||||
isTrimmed: boolean;
|
||||
generation: number;
|
||||
}
|
||||
|
||||
export interface IBufferLineStringCache {
|
||||
generation: number;
|
||||
allocateEntry(): IBufferLineStringCacheEntry;
|
||||
touch?(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Typed array based bufferline implementation.
|
||||
@@ -63,10 +73,16 @@ export class BufferLine implements IBufferLine {
|
||||
protected _data: Uint32Array;
|
||||
protected _combined: {[index: number]: string} = {};
|
||||
protected _extendedAttrs: {[index: number]: IExtendedAttrs | undefined} = {};
|
||||
protected _stringCacheEntryRef: WeakRef<IBufferLineStringCacheEntry> | undefined;
|
||||
public length: number;
|
||||
|
||||
constructor(cols: number, fillCellData?: ICellData, public isWrapped: boolean = false) {
|
||||
this._data = new Uint32Array(cols * CELL_SIZE);
|
||||
constructor(
|
||||
protected readonly _stringCache: IBufferLineStringCache,
|
||||
cols: number,
|
||||
fillCellData?: ICellData,
|
||||
public isWrapped: boolean = false
|
||||
) {
|
||||
this._data = new Uint32Array(cols * Constants.CELL_INDICIES);
|
||||
const cell = fillCellData ?? CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
for (let i = 0; i < cols; ++i) {
|
||||
this.setCell(i, cell);
|
||||
@@ -79,10 +95,10 @@ export class BufferLine implements IBufferLine {
|
||||
* @deprecated
|
||||
*/
|
||||
public get(index: number): CharData {
|
||||
const content = this._data[index * CELL_SIZE + Cell.CONTENT];
|
||||
const content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];
|
||||
const cp = content & Content.CODEPOINT_MASK;
|
||||
return [
|
||||
this._data[index * CELL_SIZE + Cell.FG],
|
||||
this._data[index * Constants.CELL_INDICIES + Cell.FG],
|
||||
(content & Content.IS_COMBINED_MASK)
|
||||
? this._combined[index]
|
||||
: (cp) ? stringFromCodePoint(cp) : '',
|
||||
@@ -98,12 +114,13 @@ export class BufferLine implements IBufferLine {
|
||||
* @deprecated
|
||||
*/
|
||||
public set(index: number, value: CharData): void {
|
||||
this._data[index * CELL_SIZE + Cell.FG] = value[CHAR_DATA_ATTR_INDEX];
|
||||
this._invalidateStringCache();
|
||||
this._data[index * Constants.CELL_INDICIES + Cell.FG] = value[CHAR_DATA_ATTR_INDEX];
|
||||
if (value[CHAR_DATA_CHAR_INDEX].length > 1) {
|
||||
this._combined[index] = value[1];
|
||||
this._data[index * CELL_SIZE + Cell.CONTENT] = index | Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
|
||||
this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = index | Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
|
||||
} else {
|
||||
this._data[index * CELL_SIZE + Cell.CONTENT] = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
|
||||
this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,22 +129,22 @@ export class BufferLine implements IBufferLine {
|
||||
* use these when only one value is needed, otherwise use `loadCell`
|
||||
*/
|
||||
public getWidth(index: number): number {
|
||||
return this._data[index * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT;
|
||||
return this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] >> Content.WIDTH_SHIFT;
|
||||
}
|
||||
|
||||
/** Test whether content has width. */
|
||||
public hasWidth(index: number): number {
|
||||
return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.WIDTH_MASK;
|
||||
return this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] & Content.WIDTH_MASK;
|
||||
}
|
||||
|
||||
/** Get FG cell component. */
|
||||
public getFg(index: number): number {
|
||||
return this._data[index * CELL_SIZE + Cell.FG];
|
||||
return this._data[index * Constants.CELL_INDICIES + Cell.FG];
|
||||
}
|
||||
|
||||
/** Get BG cell component. */
|
||||
public getBg(index: number): number {
|
||||
return this._data[index * CELL_SIZE + Cell.BG];
|
||||
return this._data[index * Constants.CELL_INDICIES + Cell.BG];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -136,7 +153,7 @@ export class BufferLine implements IBufferLine {
|
||||
* from real empty cells.
|
||||
*/
|
||||
public hasContent(index: number): number {
|
||||
return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK;
|
||||
return this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] & Content.HAS_CONTENT_MASK;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -145,7 +162,7 @@ export class BufferLine implements IBufferLine {
|
||||
* a single UTF32 codepoint or the last codepoint of a combined string.
|
||||
*/
|
||||
public getCodePoint(index: number): number {
|
||||
const content = this._data[index * CELL_SIZE + Cell.CONTENT];
|
||||
const content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];
|
||||
if (content & Content.IS_COMBINED_MASK) {
|
||||
return this._combined[index].charCodeAt(this._combined[index].length - 1);
|
||||
}
|
||||
@@ -154,12 +171,12 @@ export class BufferLine implements IBufferLine {
|
||||
|
||||
/** Test whether the cell contains a combined string. */
|
||||
public isCombined(index: number): number {
|
||||
return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.IS_COMBINED_MASK;
|
||||
return this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] & Content.IS_COMBINED_MASK;
|
||||
}
|
||||
|
||||
/** Returns the string content of the cell. */
|
||||
public getString(index: number): string {
|
||||
const content = this._data[index * CELL_SIZE + Cell.CONTENT];
|
||||
const content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];
|
||||
if (content & Content.IS_COMBINED_MASK) {
|
||||
return this._combined[index];
|
||||
}
|
||||
@@ -172,7 +189,7 @@ export class BufferLine implements IBufferLine {
|
||||
|
||||
/** Get state of protected flag. */
|
||||
public isProtected(index: number): number {
|
||||
return this._data[index * CELL_SIZE + Cell.BG] & BgFlags.PROTECTED;
|
||||
return this._data[index * Constants.CELL_INDICIES + Cell.BG] & BgFlags.PROTECTED;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -180,7 +197,7 @@ export class BufferLine implements IBufferLine {
|
||||
* to GC as it significantly reduced the amount of new objects/references needed.
|
||||
*/
|
||||
public loadCell(index: number, cell: ICellData): ICellData {
|
||||
$startIndex = index * CELL_SIZE;
|
||||
$startIndex = index * Constants.CELL_INDICIES;
|
||||
cell.content = this._data[$startIndex + Cell.CONTENT];
|
||||
cell.fg = this._data[$startIndex + Cell.FG];
|
||||
cell.bg = this._data[$startIndex + Cell.BG];
|
||||
@@ -197,15 +214,16 @@ export class BufferLine implements IBufferLine {
|
||||
* Set data at `index` to `cell`.
|
||||
*/
|
||||
public setCell(index: number, cell: ICellData): void {
|
||||
this._invalidateStringCache();
|
||||
if (cell.content & Content.IS_COMBINED_MASK) {
|
||||
this._combined[index] = cell.combinedData;
|
||||
}
|
||||
if (cell.bg & BgFlags.HAS_EXTENDED) {
|
||||
this._extendedAttrs[index] = cell.extended;
|
||||
}
|
||||
this._data[index * CELL_SIZE + Cell.CONTENT] = cell.content;
|
||||
this._data[index * CELL_SIZE + Cell.FG] = cell.fg;
|
||||
this._data[index * CELL_SIZE + Cell.BG] = cell.bg;
|
||||
this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = cell.content;
|
||||
this._data[index * Constants.CELL_INDICIES + Cell.FG] = cell.fg;
|
||||
this._data[index * Constants.CELL_INDICIES + Cell.BG] = cell.bg;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -214,12 +232,13 @@ export class BufferLine implements IBufferLine {
|
||||
* it gets an optimized access method.
|
||||
*/
|
||||
public setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void {
|
||||
this._invalidateStringCache();
|
||||
if (attrs.bg & BgFlags.HAS_EXTENDED) {
|
||||
this._extendedAttrs[index] = attrs.extended;
|
||||
}
|
||||
this._data[index * CELL_SIZE + Cell.CONTENT] = codePoint | (width << Content.WIDTH_SHIFT);
|
||||
this._data[index * CELL_SIZE + Cell.FG] = attrs.fg;
|
||||
this._data[index * CELL_SIZE + Cell.BG] = attrs.bg;
|
||||
this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = codePoint | (width << Content.WIDTH_SHIFT);
|
||||
this._data[index * Constants.CELL_INDICIES + Cell.FG] = attrs.fg;
|
||||
this._data[index * Constants.CELL_INDICIES + Cell.BG] = attrs.bg;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -229,7 +248,8 @@ export class BufferLine implements IBufferLine {
|
||||
* by the previous `setDataFromCodePoint` call, we can omit it here.
|
||||
*/
|
||||
public addCodepointToCell(index: number, codePoint: number, width: number): void {
|
||||
let content = this._data[index * CELL_SIZE + Cell.CONTENT];
|
||||
this._invalidateStringCache();
|
||||
let content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];
|
||||
if (content & Content.IS_COMBINED_MASK) {
|
||||
// we already have a combined string, simply add
|
||||
this._combined[index] += stringFromCodePoint(codePoint);
|
||||
@@ -251,10 +271,11 @@ export class BufferLine implements IBufferLine {
|
||||
content &= ~Content.WIDTH_MASK;
|
||||
content |= width << Content.WIDTH_SHIFT;
|
||||
}
|
||||
this._data[index * CELL_SIZE + Cell.CONTENT] = content;
|
||||
this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = content;
|
||||
}
|
||||
|
||||
public insertCells(pos: number, n: number, fillCellData: ICellData): void {
|
||||
this._invalidateStringCache();
|
||||
pos %= this.length;
|
||||
|
||||
// handle fullwidth at pos: reset cell one to the left if pos is second cell of a wide char
|
||||
@@ -282,6 +303,7 @@ export class BufferLine implements IBufferLine {
|
||||
}
|
||||
|
||||
public deleteCells(pos: number, n: number, fillCellData: ICellData): void {
|
||||
this._invalidateStringCache();
|
||||
pos %= this.length;
|
||||
if (n < this.length - pos) {
|
||||
for (let i = 0; i < this.length - pos - n; ++i) {
|
||||
@@ -308,6 +330,7 @@ export class BufferLine implements IBufferLine {
|
||||
}
|
||||
|
||||
public replaceCells(start: number, end: number, fillCellData: ICellData, respectProtect: boolean = false): void {
|
||||
this._invalidateStringCache();
|
||||
// full branching on respectProtect==true, hopefully getting fast JIT for standard case
|
||||
if (respectProtect) {
|
||||
if (start && this.getWidth(start - 1) === 2 && !this.isProtected(start - 1)) {
|
||||
@@ -344,13 +367,14 @@ export class BufferLine implements IBufferLine {
|
||||
* The underlying array buffer will not change if there is still enough space
|
||||
* to hold the new buffer line data.
|
||||
* Returns a boolean indicating, whether a `cleanupMemory` call would free
|
||||
* excess memory (true after shrinking > CLEANUP_THRESHOLD).
|
||||
* excess memory (true after shrinking > Constants.CLEANUP_THRESHOLD).
|
||||
*/
|
||||
public resize(cols: number, fillCellData: ICellData): boolean {
|
||||
this._invalidateStringCache();
|
||||
if (cols === this.length) {
|
||||
return this._data.length * 4 * CLEANUP_THRESHOLD < this._data.buffer.byteLength;
|
||||
return this._data.length * 4 * Constants.CLEANUP_THRESHOLD < this._data.buffer.byteLength;
|
||||
}
|
||||
const uint32Cells = cols * CELL_SIZE;
|
||||
const uint32Cells = cols * Constants.CELL_INDICIES;
|
||||
if (cols > this.length) {
|
||||
if (this._data.buffer.byteLength >= uint32Cells * 4) {
|
||||
// optimization: avoid alloc and data copy if buffer has enough room
|
||||
@@ -385,17 +409,17 @@ export class BufferLine implements IBufferLine {
|
||||
}
|
||||
}
|
||||
this.length = cols;
|
||||
return uint32Cells * 4 * CLEANUP_THRESHOLD < this._data.buffer.byteLength;
|
||||
return uint32Cells * 4 * Constants.CLEANUP_THRESHOLD < this._data.buffer.byteLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup underlying array buffer.
|
||||
* A cleanup will be triggered if the array buffer exceeds the actual used
|
||||
* memory by a factor of CLEANUP_THRESHOLD.
|
||||
* memory by a factor of Constants.CLEANUP_THRESHOLD.
|
||||
* Returns 0 or 1 indicating whether a cleanup happened.
|
||||
*/
|
||||
public cleanupMemory(): number {
|
||||
if (this._data.length * 4 * CLEANUP_THRESHOLD < this._data.buffer.byteLength) {
|
||||
if (this._data.length * 4 * Constants.CLEANUP_THRESHOLD < this._data.buffer.byteLength) {
|
||||
const data = new Uint32Array(this._data.length);
|
||||
data.set(this._data);
|
||||
this._data = data;
|
||||
@@ -406,6 +430,7 @@ export class BufferLine implements IBufferLine {
|
||||
|
||||
/** fill a line with fillCharData */
|
||||
public fill(fillCellData: ICellData, respectProtect: boolean = false): void {
|
||||
this._invalidateStringCache();
|
||||
// full branching on respectProtect==true, hopefully getting fast JIT for standard case
|
||||
if (respectProtect) {
|
||||
for (let i = 0; i < this.length; ++i) {
|
||||
@@ -424,6 +449,7 @@ export class BufferLine implements IBufferLine {
|
||||
|
||||
/** alter to a full copy of line */
|
||||
public copyFrom(line: BufferLine): void {
|
||||
this._invalidateStringCache();
|
||||
if (this.length !== line.length) {
|
||||
this._data = new Uint32Array(line._data);
|
||||
} else {
|
||||
@@ -444,7 +470,7 @@ export class BufferLine implements IBufferLine {
|
||||
|
||||
/** create a new clone */
|
||||
public clone(): IBufferLine {
|
||||
const newLine = new BufferLine(0);
|
||||
const newLine = new BufferLine(this._stringCache, 0, undefined, false);
|
||||
newLine._data = new Uint32Array(this._data);
|
||||
newLine.length = this.length;
|
||||
for (const el in this._combined) {
|
||||
@@ -459,8 +485,8 @@ export class BufferLine implements IBufferLine {
|
||||
|
||||
public getTrimmedLength(): number {
|
||||
for (let i = this.length - 1; i >= 0; --i) {
|
||||
if ((this._data[i * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK)) {
|
||||
return i + (this._data[i * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT);
|
||||
if ((this._data[i * Constants.CELL_INDICIES + Cell.CONTENT] & Content.HAS_CONTENT_MASK)) {
|
||||
return i + (this._data[i * Constants.CELL_INDICIES + Cell.CONTENT] >> Content.WIDTH_SHIFT);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
@@ -468,30 +494,31 @@ export class BufferLine implements IBufferLine {
|
||||
|
||||
public getNoBgTrimmedLength(): number {
|
||||
for (let i = this.length - 1; i >= 0; --i) {
|
||||
if ((this._data[i * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK) || (this._data[i * CELL_SIZE + Cell.BG] & Attributes.CM_MASK)) {
|
||||
return i + (this._data[i * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT);
|
||||
if ((this._data[i * Constants.CELL_INDICIES + Cell.CONTENT] & Content.HAS_CONTENT_MASK) || (this._data[i * Constants.CELL_INDICIES + Cell.BG] & Attributes.CM_MASK)) {
|
||||
return i + (this._data[i * Constants.CELL_INDICIES + Cell.CONTENT] >> Content.WIDTH_SHIFT);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public copyCellsFrom(src: BufferLine, srcCol: number, destCol: number, length: number, applyInReverse: boolean): void {
|
||||
this._invalidateStringCache();
|
||||
const srcData = src._data;
|
||||
if (applyInReverse) {
|
||||
for (let cell = length - 1; cell >= 0; cell--) {
|
||||
for (let i = 0; i < CELL_SIZE; i++) {
|
||||
this._data[(destCol + cell) * CELL_SIZE + i] = srcData[(srcCol + cell) * CELL_SIZE + i];
|
||||
for (let i = 0; i < Constants.CELL_INDICIES; i++) {
|
||||
this._data[(destCol + cell) * Constants.CELL_INDICIES + i] = srcData[(srcCol + cell) * Constants.CELL_INDICIES + i];
|
||||
}
|
||||
if (srcData[(srcCol + cell) * CELL_SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) {
|
||||
if (srcData[(srcCol + cell) * Constants.CELL_INDICIES + Cell.BG] & BgFlags.HAS_EXTENDED) {
|
||||
this._extendedAttrs[destCol + cell] = src._extendedAttrs[srcCol + cell];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let cell = 0; cell < length; cell++) {
|
||||
for (let i = 0; i < CELL_SIZE; i++) {
|
||||
this._data[(destCol + cell) * CELL_SIZE + i] = srcData[(srcCol + cell) * CELL_SIZE + i];
|
||||
for (let i = 0; i < Constants.CELL_INDICIES; i++) {
|
||||
this._data[(destCol + cell) * Constants.CELL_INDICIES + i] = srcData[(srcCol + cell) * Constants.CELL_INDICIES + i];
|
||||
}
|
||||
if (srcData[(srcCol + cell) * CELL_SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) {
|
||||
if (srcData[(srcCol + cell) * Constants.CELL_INDICIES + Cell.BG] & BgFlags.HAS_EXTENDED) {
|
||||
this._extendedAttrs[destCol + cell] = src._extendedAttrs[srcCol + cell];
|
||||
}
|
||||
}
|
||||
@@ -508,7 +535,8 @@ export class BufferLine implements IBufferLine {
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates the buffer line to a string.
|
||||
* Translates the buffer line to a string. Caching only applies to canonical full-line translation
|
||||
* requests (regardless of `trimRight` value).
|
||||
*
|
||||
* @param trimRight Whether to trim any empty cells on the right.
|
||||
* @param startCol The column to start the string (0-based inclusive).
|
||||
@@ -521,6 +549,19 @@ export class BufferLine implements IBufferLine {
|
||||
* returned string, the corresponding entries in `outColumns` will have the same column number.
|
||||
*/
|
||||
public translateToString(trimRight?: boolean, startCol?: number, endCol?: number, outColumns?: number[]): string {
|
||||
const isCanonicalRequest = (startCol === undefined || startCol === 0) && endCol === undefined && outColumns === undefined;
|
||||
if (isCanonicalRequest) {
|
||||
this._stringCache.touch?.();
|
||||
}
|
||||
const stringCacheEntry = isCanonicalRequest ? this._getStringCacheEntry(false) : undefined;
|
||||
if (isCanonicalRequest && stringCacheEntry?.value !== undefined) {
|
||||
if (trimRight) {
|
||||
return stringCacheEntry.isTrimmed ? stringCacheEntry.value : stringCacheEntry.value.trimEnd();
|
||||
}
|
||||
if (!stringCacheEntry.isTrimmed) {
|
||||
return stringCacheEntry.value;
|
||||
}
|
||||
}
|
||||
startCol = startCol ?? 0;
|
||||
endCol = endCol ?? this.length;
|
||||
if (trimRight) {
|
||||
@@ -531,7 +572,7 @@ export class BufferLine implements IBufferLine {
|
||||
}
|
||||
let result = '';
|
||||
while (startCol < endCol) {
|
||||
const content = this._data[startCol * CELL_SIZE + Cell.CONTENT];
|
||||
const content = this._data[startCol * Constants.CELL_INDICIES + Cell.CONTENT];
|
||||
const cp = content & Content.CODEPOINT_MASK;
|
||||
const chars = (content & Content.IS_COMBINED_MASK) ? this._combined[startCol] : (cp) ? stringFromCodePoint(cp) : WHITESPACE_CELL_CHAR;
|
||||
result += chars;
|
||||
@@ -545,6 +586,34 @@ export class BufferLine implements IBufferLine {
|
||||
if (outColumns) {
|
||||
outColumns.push(startCol);
|
||||
}
|
||||
if (isCanonicalRequest) {
|
||||
const cacheEntry = this._getStringCacheEntry(true)!;
|
||||
cacheEntry.value = result;
|
||||
cacheEntry.isTrimmed = !!trimRight;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
protected _getStringCacheEntry(createIfNeeded: boolean): IBufferLineStringCacheEntry | undefined {
|
||||
const cachedEntry = this._stringCacheEntryRef?.deref();
|
||||
if (cachedEntry) {
|
||||
if (cachedEntry.generation === this._stringCache.generation) {
|
||||
return cachedEntry;
|
||||
}
|
||||
}
|
||||
if (!createIfNeeded) {
|
||||
return undefined;
|
||||
}
|
||||
const cacheEntry = this._stringCache.allocateEntry();
|
||||
this._stringCacheEntryRef = new WeakRef(cacheEntry);
|
||||
return cacheEntry;
|
||||
}
|
||||
|
||||
private _invalidateStringCache(): void {
|
||||
const cacheEntry = this._getStringCacheEntry(false);
|
||||
if (cacheEntry) {
|
||||
cacheEntry.value = undefined;
|
||||
cacheEntry.isTrimmed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import type { IBufferLineStringCache, IBufferLineStringCacheEntry } from 'common/buffer/BufferLine';
|
||||
import { disposableTimeout } from 'common/Async';
|
||||
import { Disposable, MutableDisposable, toDisposable, type IDisposable } from 'common/Lifecycle';
|
||||
|
||||
const enum Constants {
|
||||
CACHE_TTL_MS = 15000
|
||||
}
|
||||
|
||||
export class BufferLineStringCache extends Disposable implements IBufferLineStringCache {
|
||||
public generation: number = 0;
|
||||
public readonly entries: Set<IBufferLineStringCacheEntry> = new Set();
|
||||
private readonly _clearTimeout = this._register(new MutableDisposable<IDisposable>());
|
||||
private _lastAccessTimestamp: number = 0;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._register(toDisposable(() => this.entries.clear()));
|
||||
}
|
||||
|
||||
public touch(): void {
|
||||
this._scheduleClear();
|
||||
}
|
||||
|
||||
public allocateEntry(): IBufferLineStringCacheEntry {
|
||||
const entry: IBufferLineStringCacheEntry = {
|
||||
value: undefined,
|
||||
isTrimmed: false,
|
||||
generation: this.generation
|
||||
};
|
||||
this.entries.add(entry);
|
||||
this._scheduleClear();
|
||||
return entry;
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._clearTimeout.clear();
|
||||
this._lastAccessTimestamp = 0;
|
||||
this.generation++;
|
||||
for (const entry of this.entries) {
|
||||
entry.value = undefined;
|
||||
entry.isTrimmed = false;
|
||||
}
|
||||
this.entries.clear();
|
||||
}
|
||||
|
||||
private _scheduleClear(): void {
|
||||
this._lastAccessTimestamp = Date.now();
|
||||
if (this._clearTimeout.value) {
|
||||
return;
|
||||
}
|
||||
this._scheduleClearTimeout(Constants.CACHE_TTL_MS);
|
||||
}
|
||||
|
||||
private _scheduleClearTimeout(timeoutMs: number): void {
|
||||
this._clearTimeout.value = disposableTimeout(() => {
|
||||
const elapsed = Date.now() - this._lastAccessTimestamp;
|
||||
if (elapsed >= Constants.CACHE_TTL_MS) {
|
||||
this.clear();
|
||||
return;
|
||||
}
|
||||
this._scheduleClearTimeout(Constants.CACHE_TTL_MS - elapsed);
|
||||
}, timeoutMs);
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,16 @@
|
||||
*/
|
||||
import { assert } from 'chai';
|
||||
import { BufferLine } from 'common/buffer/BufferLine';
|
||||
import { BufferLineStringCache } from 'common/buffer/BufferLineStringCache';
|
||||
import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from 'common/buffer/Constants';
|
||||
import { reflowSmallerGetNewLineLengths } from 'common/buffer/BufferReflow';
|
||||
|
||||
const TEST_STRING_CACHE = new BufferLineStringCache();
|
||||
|
||||
describe('BufferReflow', () => {
|
||||
describe('reflowSmallerGetNewLineLengths', () => {
|
||||
it('should return correct line lengths for a small line with wide characters', () => {
|
||||
const line = new BufferLine(4);
|
||||
const line = new BufferLine(TEST_STRING_CACHE, 4);
|
||||
line.set(0, [0, '汉', 2, '汉'.charCodeAt(0)]);
|
||||
line.set(1, [0, '', 0, 0]);
|
||||
line.set(2, [0, '语', 2, '语'.charCodeAt(0)]);
|
||||
@@ -20,7 +23,7 @@ describe('BufferReflow', () => {
|
||||
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 4, 2), [2, 2], 'line: 汉, 语');
|
||||
});
|
||||
it('should return correct line lengths for a large line with wide characters', () => {
|
||||
const line = new BufferLine(12);
|
||||
const line = new BufferLine(TEST_STRING_CACHE, 12);
|
||||
for (let i = 0; i < 12; i += 4) {
|
||||
line.set(i, [0, '汉', 2, '汉'.charCodeAt(0)]);
|
||||
line.set(i + 2, [0, '语', 2, '语'.charCodeAt(0)]);
|
||||
@@ -42,7 +45,7 @@ describe('BufferReflow', () => {
|
||||
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 2), [2, 2, 2, 2, 2, 2], 'line: 汉, 语, 汉, 语, 汉, 语');
|
||||
});
|
||||
it('should return correct line lengths for a string with wide and single characters', () => {
|
||||
const line = new BufferLine(6);
|
||||
const line = new BufferLine(TEST_STRING_CACHE, 6);
|
||||
line.set(0, [0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
line.set(1, [0, '汉', 2, '汉'.charCodeAt(0)]);
|
||||
line.set(2, [0, '', 0, 0]);
|
||||
@@ -56,14 +59,14 @@ describe('BufferReflow', () => {
|
||||
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 6, 2), [1, 2, 2, 1], 'line: a, 汉, 语, b');
|
||||
});
|
||||
it('should return correct line lengths for a wrapped line with wide and single characters', () => {
|
||||
const line1 = new BufferLine(6);
|
||||
const line1 = new BufferLine(TEST_STRING_CACHE, 6);
|
||||
line1.set(0, [0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
line1.set(1, [0, '汉', 2, '汉'.charCodeAt(0)]);
|
||||
line1.set(2, [0, '', 0, 0]);
|
||||
line1.set(3, [0, '语', 2, '语'.charCodeAt(0)]);
|
||||
line1.set(4, [0, '', 0, 0]);
|
||||
line1.set(5, [0, 'b', 1, 'b'.charCodeAt(0)]);
|
||||
const line2 = new BufferLine(6, undefined, true);
|
||||
const line2 = new BufferLine(TEST_STRING_CACHE, 6, undefined, true);
|
||||
line2.set(0, [0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
line2.set(1, [0, '汉', 2, '汉'.charCodeAt(0)]);
|
||||
line2.set(2, [0, '', 0, 0]);
|
||||
@@ -78,7 +81,7 @@ describe('BufferReflow', () => {
|
||||
assert.deepEqual(reflowSmallerGetNewLineLengths([line1, line2], 6, 2), [1, 2, 2, 2, 2, 2, 1], 'lines: a, 汉, 语, ba, 汉, 语, b');
|
||||
});
|
||||
it('should work on lines ending in null space', () => {
|
||||
const line = new BufferLine(5);
|
||||
const line = new BufferLine(TEST_STRING_CACHE, 5);
|
||||
line.set(0, [0, '汉', 2, '汉'.charCodeAt(0)]);
|
||||
line.set(1, [0, '', 0, 0]);
|
||||
line.set(2, [0, '语', 2, '语'.charCodeAt(0)]);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { assert } from 'chai';
|
||||
import { BufferSet } from 'common/buffer/BufferSet';
|
||||
import { Buffer } from 'common/buffer/Buffer';
|
||||
import { MockOptionsService, MockBufferService, MockLogService } from 'common/TestUtils.test';
|
||||
import { MockOptionsService, MockBufferService, MockLogService, createCellData } from 'common/TestUtils.test';
|
||||
|
||||
describe('BufferSet', () => {
|
||||
let bufferSet: BufferSet;
|
||||
@@ -82,4 +82,45 @@ describe('BufferSet', () => {
|
||||
assert.equal(bufferSet.alt.markers.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('lifecycle', () => {
|
||||
it('should dispose previous buffers on reset', () => {
|
||||
const oldNormal = bufferSet.normal as any;
|
||||
oldNormal.lines.get(0)!.setCell(0, createCellData(0, 'a', 1));
|
||||
oldNormal.translateBufferLineToString(0, false);
|
||||
|
||||
const oldCache = oldNormal._stringCache;
|
||||
assert.equal(oldCache.entries.size, 1);
|
||||
assert.notEqual(oldCache._clearTimeout.value, undefined);
|
||||
|
||||
bufferSet.reset();
|
||||
|
||||
assert.notEqual(bufferSet.normal, oldNormal);
|
||||
assert.equal(oldCache.entries.size, 0);
|
||||
assert.equal(oldCache._clearTimeout.value, undefined);
|
||||
});
|
||||
|
||||
it('should dispose both buffers when disposed', () => {
|
||||
const normal = bufferSet.normal as any;
|
||||
normal.lines.get(0)!.setCell(0, createCellData(0, 'a', 1));
|
||||
normal.translateBufferLineToString(0, false);
|
||||
|
||||
bufferSet.activateAltBuffer();
|
||||
const alt = bufferSet.alt as any;
|
||||
alt.lines.get(0)!.setCell(0, createCellData(0, 'b', 1));
|
||||
alt.translateBufferLineToString(0, false);
|
||||
|
||||
const normalCache = normal._stringCache;
|
||||
const altCache = alt._stringCache;
|
||||
assert.notEqual(normalCache._clearTimeout.value, undefined);
|
||||
assert.notEqual(altCache._clearTimeout.value, undefined);
|
||||
|
||||
bufferSet.dispose();
|
||||
|
||||
assert.equal(normalCache.entries.size, 0);
|
||||
assert.equal(altCache.entries.size, 0);
|
||||
assert.equal(normalCache._clearTimeout.value, undefined);
|
||||
assert.equal(altCache._clearTimeout.value, undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { Disposable, MutableDisposable } from 'common/Lifecycle';
|
||||
import { IAttributeData } from 'common/Types';
|
||||
import { Buffer } from 'common/buffer/Buffer';
|
||||
import { IBuffer, IBufferSet } from 'common/buffer/Types';
|
||||
@@ -18,6 +18,8 @@ export class BufferSet extends Disposable implements IBufferSet {
|
||||
private _normal!: Buffer;
|
||||
private _alt!: Buffer;
|
||||
private _activeBuffer!: Buffer;
|
||||
private readonly _normalBuffer = this._register(new MutableDisposable<Buffer>());
|
||||
private readonly _altBuffer = this._register(new MutableDisposable<Buffer>());
|
||||
|
||||
private readonly _onBufferActivate = this._register(new Emitter<{ activeBuffer: IBuffer, inactiveBuffer: IBuffer }>());
|
||||
public readonly onBufferActivate = this._onBufferActivate.event;
|
||||
@@ -38,11 +40,13 @@ export class BufferSet extends Disposable implements IBufferSet {
|
||||
|
||||
public reset(): void {
|
||||
this._normal = new Buffer(true, this._optionsService, this._bufferService, this._logService);
|
||||
this._normalBuffer.value = this._normal;
|
||||
this._normal.fillViewportRows();
|
||||
|
||||
// The alt buffer should never have scrollback.
|
||||
// See http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer
|
||||
this._alt = new Buffer(false, this._optionsService, this._bufferService, this._logService);
|
||||
this._altBuffer.value = this._alt;
|
||||
this._activeBuffer = this._normal;
|
||||
this._onBufferActivate.fire({
|
||||
activeBuffer: this._normal,
|
||||
|
||||
@@ -62,8 +62,7 @@ export class CellData extends AttributeData implements ICellData {
|
||||
// surrogates and combined strings need special treatment
|
||||
if (value[CHAR_DATA_CHAR_INDEX].length > 2) {
|
||||
combined = true;
|
||||
}
|
||||
else if (value[CHAR_DATA_CHAR_INDEX].length === 2) {
|
||||
} else if (value[CHAR_DATA_CHAR_INDEX].length === 2) {
|
||||
const code = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0);
|
||||
// if the 2-char string is a surrogate create single codepoint
|
||||
// everything else is combined
|
||||
@@ -71,16 +70,13 @@ export class CellData extends AttributeData implements ICellData {
|
||||
const second = value[CHAR_DATA_CHAR_INDEX].charCodeAt(1);
|
||||
if (0xDC00 <= second && second <= 0xDFFF) {
|
||||
this.content = ((code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
combined = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
combined = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
this.content = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
|
||||
}
|
||||
if (combined) {
|
||||
|
||||
@@ -14,7 +14,9 @@ export class Marker implements IMarker {
|
||||
private readonly _disposables: IDisposable[] = [];
|
||||
|
||||
private readonly _id: number = Marker._nextId++;
|
||||
public get id(): number { return this._id; }
|
||||
public get id(): number {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
private readonly _onDispose = this.register(new Emitter<void>());
|
||||
public readonly onDispose = this._onDispose.event;
|
||||
|
||||
@@ -58,22 +58,19 @@ export function evaluateKeyboardEvent(
|
||||
} else {
|
||||
result.key = C0.ESC + '[A';
|
||||
}
|
||||
}
|
||||
else if (ev.key === 'UIKeyInputLeftArrow') {
|
||||
} else if (ev.key === 'UIKeyInputLeftArrow') {
|
||||
if (applicationCursorMode) {
|
||||
result.key = C0.ESC + 'OD';
|
||||
} else {
|
||||
result.key = C0.ESC + '[D';
|
||||
}
|
||||
}
|
||||
else if (ev.key === 'UIKeyInputRightArrow') {
|
||||
} else if (ev.key === 'UIKeyInputRightArrow') {
|
||||
if (applicationCursorMode) {
|
||||
result.key = C0.ESC + 'OC';
|
||||
} else {
|
||||
result.key = C0.ESC + '[C';
|
||||
}
|
||||
}
|
||||
else if (ev.key === 'UIKeyInputDownArrow') {
|
||||
} else if (ev.key === 'UIKeyInputDownArrow') {
|
||||
if (applicationCursorMode) {
|
||||
result.key = C0.ESC + 'OB';
|
||||
} else {
|
||||
|
||||
@@ -453,19 +453,59 @@ 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('press event explicit :1 when modifiers present', () => {
|
||||
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 when modifiers present', () => {
|
||||
const result = kitty.evaluate(createEvent({ key: 'a', ctrlKey: true }), flags, KittyKeyboardEventType.PRESS);
|
||||
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');
|
||||
@@ -494,6 +554,27 @@ describe('KittyKeyboard', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Enabling REPORT_EVENT_TYPES without DISAMBIGUATE_ESCAPE_CODES doesn't really make sense and
|
||||
// isn't specified in the spec, but press and repeat events shouldn't get swallowed.
|
||||
describe('REPORT_EVENT_TYPES flag without DISAMBIGUATE_ESCAPE_CODES', () => {
|
||||
const flags = KittyKeyboardFlags.REPORT_EVENT_TYPES;
|
||||
|
||||
it('press event is not swallowed when modifiers present', () => {
|
||||
const result = kitty.evaluate(createEvent({ key: 'a', ctrlKey: true }), flags, KittyKeyboardEventType.PRESS);
|
||||
assert.strictEqual(result.key, '\x1b[97;5u');
|
||||
});
|
||||
|
||||
it('repeat event is not swallowed when modifiers present', () => {
|
||||
const result = kitty.evaluate(createEvent({ key: 'a', ctrlKey: true }), flags, KittyKeyboardEventType.REPEAT);
|
||||
assert.strictEqual(result.key, '\x1b[97;5:2u');
|
||||
});
|
||||
|
||||
it('release event is reported as CSI u sequence', () => {
|
||||
const result = kitty.evaluate(createEvent({ key: 'a', ctrlKey: true }), flags, KittyKeyboardEventType.RELEASE);
|
||||
assert.strictEqual(result.key, '\x1b[97;5:3u');
|
||||
});
|
||||
});
|
||||
|
||||
describe('modifier-only reporting', () => {
|
||||
const flags = KittyKeyboardFlags.REPORT_EVENT_TYPES;
|
||||
|
||||
@@ -563,6 +644,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;
|
||||
|
||||
|
||||
@@ -471,29 +471,36 @@ 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) ||
|
||||
// Enabling REPORT_EVENT_TYPES without DISAMBIGUATE_ESCAPE_CODES doesn't really make sense, so
|
||||
// just make REPORT_EVENT_TYPES imply DISAMBIGUATE_ESCAPE_CODES here for simplicity.
|
||||
// See: https://github.com/kovidgoyal/kitty/issues/9999
|
||||
((flags & KittyKeyboardFlags.DISAMBIGUATE_ESCAPE_CODES || reportEventTypes) &&
|
||||
(
|
||||
// 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);
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { StringToUtf32, stringFromCodePoint, Utf8ToUtf32, utf32ToString } from 'common/input/TextDecoder';
|
||||
import { encode } from 'utf8';
|
||||
|
||||
|
||||
// convert UTF32 codepoints to string
|
||||
@@ -29,6 +28,35 @@ function fromByteString(s: string): Uint8Array {
|
||||
return result;
|
||||
}
|
||||
|
||||
function stringToUtf8Bytes(s: string): Uint8Array {
|
||||
const bytes: number[] = [];
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
let cp = s.charCodeAt(i);
|
||||
if (cp >= 0xD800 && cp <= 0xDBFF && i + 1 < s.length) {
|
||||
const next = s.charCodeAt(i + 1);
|
||||
if (next >= 0xDC00 && next <= 0xDFFF) {
|
||||
cp = 0x10000 + ((cp - 0xD800) << 10) + (next - 0xDC00);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
if (cp < 0x80) {
|
||||
bytes.push(cp);
|
||||
} else if (cp < 0x800) {
|
||||
bytes.push(0xC0 | (cp >> 6), 0x80 | (cp & 0x3F));
|
||||
} else if (cp < 0x10000) {
|
||||
bytes.push(0xE0 | (cp >> 12), 0x80 | ((cp >> 6) & 0x3F), 0x80 | (cp & 0x3F));
|
||||
} else {
|
||||
bytes.push(
|
||||
0xF0 | (cp >> 18),
|
||||
0x80 | ((cp >> 12) & 0x3F),
|
||||
0x80 | ((cp >> 6) & 0x3F),
|
||||
0x80 | (cp & 0x3F)
|
||||
);
|
||||
}
|
||||
}
|
||||
return new Uint8Array(bytes);
|
||||
}
|
||||
|
||||
function assertDecodedRange(
|
||||
min: number,
|
||||
max: number,
|
||||
@@ -172,7 +200,7 @@ describe('text encodings', () => {
|
||||
max,
|
||||
(i) => (i >= 0xD800 && i <= 0xDFFF) || i === 0xFEFF,
|
||||
(i) => String.fromCharCode(i),
|
||||
(input, target) => decoder.decode(fromByteString(encode(input)), target),
|
||||
(input, target) => decoder.decode(stringToUtf8Bytes(input), target),
|
||||
(data, length) => toString(data, length)
|
||||
);
|
||||
});
|
||||
@@ -187,7 +215,7 @@ describe('text encodings', () => {
|
||||
max,
|
||||
() => false,
|
||||
(i) => stringFromCodePoint(i),
|
||||
(input, target) => decoder.decode(fromByteString(encode(input)), target),
|
||||
(input, target) => decoder.decode(stringToUtf8Bytes(input), target),
|
||||
(data, length) => toString(data, length)
|
||||
);
|
||||
});
|
||||
@@ -196,7 +224,7 @@ describe('text encodings', () => {
|
||||
it('0xFEFF(BOM)', () => {
|
||||
const decoder = new Utf8ToUtf32();
|
||||
const target = new Uint32Array(5);
|
||||
const utf8Data = fromByteString(encode(String.fromCharCode(0xFEFF)));
|
||||
const utf8Data = stringToUtf8Bytes(String.fromCharCode(0xFEFF));
|
||||
const length = decoder.decode(utf8Data, target);
|
||||
assert.equal(length, 0);
|
||||
decoder.clear();
|
||||
@@ -207,7 +235,7 @@ describe('text encodings', () => {
|
||||
const decoder = new Utf8ToUtf32();
|
||||
const target = new Uint32Array(500);
|
||||
for (let i = 0; i < TEST_STRINGS.length; ++i) {
|
||||
const utf8Data = fromByteString(encode(TEST_STRINGS[i]));
|
||||
const utf8Data = stringToUtf8Bytes(TEST_STRINGS[i]);
|
||||
const length = decoder.decode(utf8Data, target);
|
||||
assert.equal(toString(target, length), TEST_STRINGS[i]);
|
||||
decoder.clear();
|
||||
|
||||
@@ -230,8 +230,7 @@ export class Utf8ToUtf32 {
|
||||
&& !((byte1 = input[i]) & 0x80)
|
||||
&& !((byte2 = input[i + 1]) & 0x80)
|
||||
&& !((byte3 = input[i + 2]) & 0x80)
|
||||
&& !((byte4 = input[i + 3]) & 0x80))
|
||||
{
|
||||
&& !((byte4 = input[i + 3]) & 0x80)) {
|
||||
target[size++] = byte1;
|
||||
target[size++] = byte2;
|
||||
target[size++] = byte3;
|
||||
|
||||
@@ -24,14 +24,20 @@ describe('WriteBuffer', () => {
|
||||
beforeEach(() => {
|
||||
stack = [];
|
||||
cbStack = [];
|
||||
wb = new WriteBuffer(data => { stack.push(data); });
|
||||
wb = new WriteBuffer(data => {
|
||||
stack.push(data);
|
||||
});
|
||||
});
|
||||
describe('write input', () => {
|
||||
it('string', done => {
|
||||
wb.write('a._');
|
||||
wb.write('b.x', () => { cbStack.push('b'); });
|
||||
wb.write('b.x', () => {
|
||||
cbStack.push('b');
|
||||
});
|
||||
wb.write('c._');
|
||||
wb.write('d.x', () => { cbStack.push('d'); });
|
||||
wb.write('d.x', () => {
|
||||
cbStack.push('d');
|
||||
});
|
||||
wb.write('e', () => {
|
||||
assert.deepEqual(stack, ['a._', 'b.x', 'c._', 'd.x', 'e']);
|
||||
assert.deepEqual(cbStack, ['b', 'd']);
|
||||
@@ -40,9 +46,13 @@ describe('WriteBuffer', () => {
|
||||
});
|
||||
it('bytes', done => {
|
||||
wb.write(toBytes('a._'));
|
||||
wb.write(toBytes('b.x'), () => { cbStack.push('b'); });
|
||||
wb.write(toBytes('b.x'), () => {
|
||||
cbStack.push('b');
|
||||
});
|
||||
wb.write(toBytes('c._'));
|
||||
wb.write(toBytes('d.x'), () => { cbStack.push('d'); });
|
||||
wb.write(toBytes('d.x'), () => {
|
||||
cbStack.push('d');
|
||||
});
|
||||
wb.write(toBytes('e'), () => {
|
||||
assert.deepEqual(stack.map(val => typeof val === 'string' ? '' : fromBytes(val)), ['a._', 'b.x', 'c._', 'd.x', 'e']);
|
||||
assert.deepEqual(cbStack, ['b', 'd']);
|
||||
@@ -51,9 +61,13 @@ describe('WriteBuffer', () => {
|
||||
});
|
||||
it('string/bytes mixed', done => {
|
||||
wb.write('a._');
|
||||
wb.write('b.x', () => { cbStack.push('b'); });
|
||||
wb.write('b.x', () => {
|
||||
cbStack.push('b');
|
||||
});
|
||||
wb.write(toBytes('c._'));
|
||||
wb.write(toBytes('d.x'), () => { cbStack.push('d'); });
|
||||
wb.write(toBytes('d.x'), () => {
|
||||
cbStack.push('d');
|
||||
});
|
||||
wb.write(toBytes('e'), () => {
|
||||
assert.deepEqual(stack.map(val => typeof val === 'string' ? val : fromBytes(val)), ['a._', 'b.x', 'c._', 'd.x', 'e']);
|
||||
assert.deepEqual(cbStack, ['b', 'd']);
|
||||
@@ -61,10 +75,18 @@ describe('WriteBuffer', () => {
|
||||
});
|
||||
});
|
||||
it('write callback works for empty chunks', done => {
|
||||
wb.write('a', () => { cbStack.push('a'); });
|
||||
wb.write('', () => { cbStack.push('b'); });
|
||||
wb.write(toBytes('c'), () => { cbStack.push('c'); });
|
||||
wb.write(new Uint8Array(0), () => { cbStack.push('d'); });
|
||||
wb.write('a', () => {
|
||||
cbStack.push('a');
|
||||
});
|
||||
wb.write('', () => {
|
||||
cbStack.push('b');
|
||||
});
|
||||
wb.write(toBytes('c'), () => {
|
||||
cbStack.push('c');
|
||||
});
|
||||
wb.write(new Uint8Array(0), () => {
|
||||
cbStack.push('d');
|
||||
});
|
||||
wb.write('e', () => {
|
||||
assert.deepEqual(stack.map(val => typeof val === 'string' ? val : fromBytes(val)), ['a', '', 'c', '', 'e']);
|
||||
assert.deepEqual(cbStack, ['a', 'b', 'c', 'd']);
|
||||
@@ -72,13 +94,21 @@ describe('WriteBuffer', () => {
|
||||
});
|
||||
});
|
||||
it('writeSync', done => {
|
||||
wb.write('a', () => { cbStack.push('a'); });
|
||||
wb.write('b', () => { cbStack.push('b'); });
|
||||
wb.write('c', () => { cbStack.push('c'); });
|
||||
wb.write('a', () => {
|
||||
cbStack.push('a');
|
||||
});
|
||||
wb.write('b', () => {
|
||||
cbStack.push('b');
|
||||
});
|
||||
wb.write('c', () => {
|
||||
cbStack.push('c');
|
||||
});
|
||||
wb.writeSync('d');
|
||||
assert.deepEqual(stack, ['a', 'b', 'c', 'd']);
|
||||
assert.deepEqual(cbStack, ['a', 'b', 'c']);
|
||||
wb.write('x', () => { cbStack.push('x'); });
|
||||
wb.write('x', () => {
|
||||
cbStack.push('x');
|
||||
});
|
||||
wb.write('', () => {
|
||||
assert.deepEqual(stack, ['a', 'b', 'c', 'd', 'x', '']);
|
||||
assert.deepEqual(cbStack, ['a', 'b', 'c', 'x']);
|
||||
@@ -107,13 +137,21 @@ describe('WriteBuffer', () => {
|
||||
assert.equal(last, '11'); // 1 + 10 sub calls = 11
|
||||
});
|
||||
it('flushSync processes all pending writes', done => {
|
||||
wb.write('a', () => { cbStack.push('a'); });
|
||||
wb.write('b', () => { cbStack.push('b'); });
|
||||
wb.write('c', () => { cbStack.push('c'); });
|
||||
wb.write('a', () => {
|
||||
cbStack.push('a');
|
||||
});
|
||||
wb.write('b', () => {
|
||||
cbStack.push('b');
|
||||
});
|
||||
wb.write('c', () => {
|
||||
cbStack.push('c');
|
||||
});
|
||||
wb.flushSync();
|
||||
assert.deepEqual(stack, ['a', 'b', 'c']);
|
||||
assert.deepEqual(cbStack, ['a', 'b', 'c']);
|
||||
wb.write('x', () => { cbStack.push('x'); });
|
||||
wb.write('x', () => {
|
||||
cbStack.push('x');
|
||||
});
|
||||
wb.write('', () => {
|
||||
assert.deepEqual(stack, ['a', 'b', 'c', 'x', '']);
|
||||
assert.deepEqual(cbStack, ['a', 'b', 'c', 'x']);
|
||||
|
||||
@@ -9,30 +9,30 @@ import { Emitter } from 'common/Event';
|
||||
|
||||
declare const setTimeout: (handler: () => void, timeout?: number) => void;
|
||||
|
||||
/**
|
||||
* Safety watermark to avoid memory exhaustion and browser engine crash on fast data input.
|
||||
* Enable flow control to avoid this limit and make sure that your backend correctly
|
||||
* propagates this to the underlying pty. (see docs for further instructions)
|
||||
* Since this limit is meant as a safety parachute to prevent browser crashs,
|
||||
* it is set to a very high number. Typically xterm.js gets unresponsive with
|
||||
* a 100 times lower number (>500 kB).
|
||||
*/
|
||||
const DISCARD_WATERMARK = 50000000; // ~50 MB
|
||||
|
||||
/**
|
||||
* The max number of ms to spend on writes before allowing the renderer to
|
||||
* catch up with a 0ms setTimeout. A value of < 33 to keep us close to
|
||||
* 30fps, and a value of < 16 to try to run at 60fps. Of course, the real FPS
|
||||
* depends on the time it takes for the renderer to draw the frame.
|
||||
*/
|
||||
const WRITE_TIMEOUT_MS = 12;
|
||||
|
||||
/**
|
||||
* Threshold of max held chunks in the write buffer, that were already processed.
|
||||
* This is a tradeoff between extensive write buffer shifts (bad runtime) and high
|
||||
* memory consumption by data thats not used anymore.
|
||||
*/
|
||||
const WRITE_BUFFER_LENGTH_THRESHOLD = 50;
|
||||
const enum Constants {
|
||||
/**
|
||||
* Safety watermark to avoid memory exhaustion and browser engine crash on fast data input.
|
||||
* Enable flow control to avoid this limit and make sure that your backend correctly
|
||||
* propagates this to the underlying pty. (see docs for further instructions)
|
||||
* Since this limit is meant as a safety parachute to prevent browser crashs,
|
||||
* it is set to a very high number. Typically xterm.js gets unresponsive with
|
||||
* a 100 times lower number (>500 kB).
|
||||
*/
|
||||
DISCARD_WATERMARK = 50000000, // ~50 MB
|
||||
/**
|
||||
* The max number of ms to spend on writes before allowing the renderer to
|
||||
* catch up with a 0ms setTimeout. A value of < 33 to keep us close to
|
||||
* 30fps, and a value of < 16 to try to run at 60fps. Of course, the real FPS
|
||||
* depends on the time it takes for the renderer to draw the frame.
|
||||
*/
|
||||
WRITE_TIMEOUT_MS = 12,
|
||||
/**
|
||||
* Threshold of max held chunks in the write buffer, that were already processed.
|
||||
* This is a tradeoff between extensive write buffer shifts (bad runtime) and high
|
||||
* memory consumption by data thats not used anymore.
|
||||
*/
|
||||
WRITE_BUFFER_LENGTH_THRESHOLD = 50
|
||||
}
|
||||
|
||||
export class WriteBuffer extends Disposable {
|
||||
private _writeBuffer: (string | Uint8Array)[] = [];
|
||||
@@ -133,7 +133,7 @@ export class WriteBuffer extends Disposable {
|
||||
}
|
||||
|
||||
public write(data: string | Uint8Array, callback?: () => void): void {
|
||||
if (this._pendingData > DISCARD_WATERMARK) {
|
||||
if (this._pendingData > Constants.DISCARD_WATERMARK) {
|
||||
throw new Error('write data discarded, use flow control to avoid losing data');
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ export class WriteBuffer extends Disposable {
|
||||
* effectively lowering the redrawing needs, schematically:
|
||||
*
|
||||
* macroTask _innerWrite:
|
||||
* if (performance.now() - (lastTime | 0) < WRITE_TIMEOUT_MS):
|
||||
* if (performance.now() - (lastTime | 0) < Constants.WRITE_TIMEOUT_MS):
|
||||
* schedule microTask _innerWrite(lastTime)
|
||||
* else:
|
||||
* schedule macroTask _innerWrite(0)
|
||||
@@ -218,7 +218,7 @@ export class WriteBuffer extends Disposable {
|
||||
* responsibility to slice hard work), but we can at least schedule a screen update as we
|
||||
* gain control.
|
||||
*/
|
||||
const continuation: (r: boolean) => void = (r: boolean) => performance.now() - startTime >= WRITE_TIMEOUT_MS
|
||||
const continuation: (r: boolean) => void = (r: boolean) => performance.now() - startTime >= Constants.WRITE_TIMEOUT_MS
|
||||
? setTimeout(() => this._innerWrite(0, r))
|
||||
: this._innerWrite(startTime, r);
|
||||
|
||||
@@ -235,7 +235,7 @@ export class WriteBuffer extends Disposable {
|
||||
* current microtask queue (executed before setTimeout).
|
||||
*/
|
||||
// const continuation: (r: boolean) => void = performance.now() - startTime >=
|
||||
// WRITE_TIMEOUT_MS
|
||||
// Constants.WRITE_TIMEOUT_MS
|
||||
// ? r => setTimeout(() => this._innerWrite(0, r))
|
||||
// : r => this._innerWrite(startTime, r);
|
||||
|
||||
@@ -244,7 +244,9 @@ export class WriteBuffer extends Disposable {
|
||||
// 2. spawn a promise immediately resolving to `true`
|
||||
// (executed on the same queue, thus properly aligned before continuation happens)
|
||||
result.catch(err => {
|
||||
queueMicrotask(() => {throw err;});
|
||||
queueMicrotask(() => {
|
||||
throw err;
|
||||
});
|
||||
return Promise.resolve(false);
|
||||
}).then(continuation);
|
||||
return;
|
||||
@@ -255,14 +257,14 @@ export class WriteBuffer extends Disposable {
|
||||
this._bufferOffset++;
|
||||
this._pendingData -= data.length;
|
||||
|
||||
if (performance.now() - startTime >= WRITE_TIMEOUT_MS) {
|
||||
if (performance.now() - startTime >= Constants.WRITE_TIMEOUT_MS) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (this._writeBuffer.length > this._bufferOffset) {
|
||||
// Allow renderer to catch up before processing the next batch
|
||||
// trim already processed chunks if we are above threshold
|
||||
if (this._bufferOffset > WRITE_BUFFER_LENGTH_THRESHOLD) {
|
||||
if (this._bufferOffset > Constants.WRITE_BUFFER_LENGTH_THRESHOLD) {
|
||||
this._writeBuffer = this._writeBuffer.slice(this._bufferOffset);
|
||||
this._callbacks = this._callbacks.slice(this._bufferOffset);
|
||||
this._bufferOffset = 0;
|
||||
|
||||
+378
-251
@@ -5,7 +5,7 @@
|
||||
import { assert } from 'chai';
|
||||
import { ApcParser, ApcHandler } from 'common/parser/ApcParser';
|
||||
import { StringToUtf32, utf32ToString } from 'common/input/TextDecoder';
|
||||
import { IApcHandler } from 'common/parser/Types';
|
||||
import { IApcHandler, IFunctionIdentifier } from 'common/parser/Types';
|
||||
|
||||
function toUtf32(s: string): Uint32Array {
|
||||
const utf32 = new Uint32Array(s.length);
|
||||
@@ -14,31 +14,53 @@ function toUtf32(s: string): Uint32Array {
|
||||
return utf32.subarray(0, length);
|
||||
}
|
||||
|
||||
class TestHandler implements IApcHandler {
|
||||
public id: number;
|
||||
public output: [string, number, string, (boolean | string)?][];
|
||||
public msg: string;
|
||||
public returnFalse: boolean;
|
||||
|
||||
constructor(
|
||||
id: number,
|
||||
output: [string, number, string, (boolean | string)?][],
|
||||
msg: string,
|
||||
returnFalse: boolean = false
|
||||
) {
|
||||
this.id = id;
|
||||
this.output = output;
|
||||
this.msg = msg;
|
||||
this.returnFalse = returnFalse;
|
||||
function identifier(id: IFunctionIdentifier): number {
|
||||
let res = 0;
|
||||
if (id.prefix) {
|
||||
if (id.prefix.length > 1) {
|
||||
throw new Error('only one byte as prefix supported');
|
||||
}
|
||||
res = id.prefix.charCodeAt(0);
|
||||
if (res && 0x3c > res || res > 0x3f) {
|
||||
throw new Error('prefix must be in range 0x3c .. 0x3f');
|
||||
}
|
||||
}
|
||||
if (id.intermediates) {
|
||||
if (id.intermediates.length > 2) {
|
||||
throw new Error('only two bytes as intermediates are supported');
|
||||
}
|
||||
for (let i = 0; i < id.intermediates.length; ++i) {
|
||||
const intermediate = id.intermediates.charCodeAt(i);
|
||||
if (0x20 > intermediate || intermediate > 0x2f) {
|
||||
throw new Error('intermediate must be in range 0x20 .. 0x2f');
|
||||
}
|
||||
res <<= 8;
|
||||
res |= intermediate;
|
||||
}
|
||||
}
|
||||
if (id.final.length !== 1) {
|
||||
throw new Error('final must be a single byte');
|
||||
}
|
||||
const finalCode = id.final.charCodeAt(0);
|
||||
if (0x40 > finalCode || finalCode > 0x7e) {
|
||||
throw new Error('final must be in range 0x40 .. 0x7e');
|
||||
}
|
||||
res <<= 8;
|
||||
res |= finalCode;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
class TestHandler implements IApcHandler {
|
||||
constructor(public output: any[], public msg: string, public returnFalse: boolean = false) {}
|
||||
public start(): void {
|
||||
this.output.push([this.msg, this.id, 'START']);
|
||||
this.output.push([this.msg, 'START']);
|
||||
}
|
||||
public put(data: Uint32Array, start: number, end: number): void {
|
||||
this.output.push([this.msg, this.id, 'PUT', utf32ToString(data, start, end)]);
|
||||
this.output.push([this.msg, 'PUT', utf32ToString(data, start, end)]);
|
||||
}
|
||||
public end(success: boolean): boolean {
|
||||
this.output.push([this.msg, this.id, 'END', success]);
|
||||
this.output.push([this.msg, 'END', success]);
|
||||
if (this.returnFalse) {
|
||||
return false;
|
||||
}
|
||||
@@ -48,173 +70,105 @@ class TestHandler implements IApcHandler {
|
||||
|
||||
describe('ApcParser', () => {
|
||||
let parser: ApcParser;
|
||||
let reports: [number, string, (boolean | string | undefined)?][] = [];
|
||||
|
||||
let reports: any[] = [];
|
||||
beforeEach(() => {
|
||||
reports = [];
|
||||
parser = new ApcParser();
|
||||
parser.setHandlerFallback((id: number, action: 'START' | 'PUT' | 'END', data?: string | boolean) => {
|
||||
reports.push([id, action, data]);
|
||||
});
|
||||
parser.setHandlerFallback((id, action, data) => reports.push([id, action, data]));
|
||||
});
|
||||
|
||||
describe('identifier parsing', () => {
|
||||
it('single character identifier', () => {
|
||||
parser.start();
|
||||
const data = toUtf32('Gf=100,a=T;payload');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, [
|
||||
[0x47, 'START', undefined], // 0x47 = 'G'
|
||||
[0x47, 'PUT', 'f=100,a=T;payload'],
|
||||
[0x47, 'END', true]
|
||||
]);
|
||||
});
|
||||
|
||||
it('identifier with no payload', () => {
|
||||
parser.start();
|
||||
const data = toUtf32('G');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, [
|
||||
[0x47, 'START', undefined],
|
||||
[0x47, 'END', true]
|
||||
]);
|
||||
});
|
||||
|
||||
it('identifier with chunked payload', () => {
|
||||
parser.start();
|
||||
let data = toUtf32('Gf=100');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(',a=T');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(';payload');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, [
|
||||
[0x47, 'START', undefined],
|
||||
[0x47, 'PUT', 'f=100'],
|
||||
[0x47, 'PUT', ',a=T'],
|
||||
[0x47, 'PUT', ';payload'],
|
||||
[0x47, 'END', true]
|
||||
]);
|
||||
});
|
||||
|
||||
it('empty APC sequence', () => {
|
||||
parser.start();
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, []);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handler registration', () => {
|
||||
let handlerReports: [string, number, string, (boolean | string)?][];
|
||||
|
||||
beforeEach(() => {
|
||||
handlerReports = [];
|
||||
});
|
||||
|
||||
it('registerHandler for specific identifier', () => {
|
||||
const G_CODE = 0x47; // 'G'
|
||||
parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'kitty'));
|
||||
parser.start();
|
||||
const data = toUtf32('Gf=100,a=T;imagedata');
|
||||
it('setApcHandler', () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th'));
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(handlerReports, [
|
||||
['kitty', G_CODE, 'START'],
|
||||
['kitty', G_CODE, 'PUT', 'f=100,a=T;imagedata'],
|
||||
['kitty', G_CODE, 'END', true]
|
||||
]);
|
||||
assert.deepEqual(reports, []);
|
||||
});
|
||||
|
||||
it('unregistered identifier falls back', () => {
|
||||
const G_CODE = 0x47; // 'G'
|
||||
const X_CODE = 0x58; // 'X'
|
||||
parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'kitty'));
|
||||
parser.start();
|
||||
const data = toUtf32('Xsome data');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(handlerReports, []);
|
||||
assert.deepEqual(reports, [
|
||||
[X_CODE, 'START', undefined],
|
||||
[X_CODE, 'PUT', 'some data'],
|
||||
[X_CODE, 'END', true]
|
||||
// messages from TestHandler
|
||||
['th', 'START'],
|
||||
['th', 'PUT', 'Here comes'],
|
||||
['th', 'PUT', 'the mouse!'],
|
||||
['th', 'END', true]
|
||||
]);
|
||||
});
|
||||
|
||||
it('clearHandler removes handler', () => {
|
||||
const G_CODE = 0x47;
|
||||
parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'kitty'));
|
||||
parser.clearHandler(G_CODE);
|
||||
parser.start();
|
||||
const data = toUtf32('Gf=100');
|
||||
it('clearApcHandler', () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th'));
|
||||
parser.clearHandler(identifier({intermediates: '+', final: 'p'}));
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(handlerReports, []);
|
||||
assert.deepEqual(reports, [
|
||||
[G_CODE, 'START', undefined],
|
||||
[G_CODE, 'PUT', 'f=100'],
|
||||
[G_CODE, 'END', true]
|
||||
// messages from fallback handler
|
||||
[identifier({intermediates: '+', final: 'p'}), 'START', undefined],
|
||||
[identifier({intermediates: '+', final: 'p'}), 'PUT', 'Here comes'],
|
||||
[identifier({intermediates: '+', final: 'p'}), 'PUT', 'the mouse!'],
|
||||
[identifier({intermediates: '+', final: 'p'}), 'END', true]
|
||||
]);
|
||||
});
|
||||
|
||||
it('multiple handlers for same identifier', () => {
|
||||
const G_CODE = 0x47;
|
||||
parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'handler1'));
|
||||
parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'handler2'));
|
||||
parser.start();
|
||||
const data = toUtf32('Gdata');
|
||||
it('addApcHandler', () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1'));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2'));
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(handlerReports, [
|
||||
['handler2', G_CODE, 'START'],
|
||||
['handler1', G_CODE, 'START'],
|
||||
['handler2', G_CODE, 'PUT', 'data'],
|
||||
['handler1', G_CODE, 'PUT', 'data'],
|
||||
['handler2', G_CODE, 'END', true],
|
||||
['handler1', G_CODE, 'END', false]
|
||||
assert.deepEqual(reports, [
|
||||
['th2', 'START'],
|
||||
['th1', 'START'],
|
||||
['th2', 'PUT', 'Here comes'],
|
||||
['th1', 'PUT', 'Here comes'],
|
||||
['th2', 'PUT', 'the mouse!'],
|
||||
['th1', 'PUT', 'the mouse!'],
|
||||
['th2', 'END', true],
|
||||
['th1', 'END', false] // false due being already handled by th2!
|
||||
]);
|
||||
});
|
||||
|
||||
it('handler returning false allows fallthrough', () => {
|
||||
const G_CODE = 0x47;
|
||||
parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'handler1'));
|
||||
parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'handler2', true));
|
||||
parser.start();
|
||||
const data = toUtf32('Gdata');
|
||||
it('addApcHandler with return false', () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1'));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2', true));
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(handlerReports, [
|
||||
['handler2', G_CODE, 'START'],
|
||||
['handler1', G_CODE, 'START'],
|
||||
['handler2', G_CODE, 'PUT', 'data'],
|
||||
['handler1', G_CODE, 'PUT', 'data'],
|
||||
['handler2', G_CODE, 'END', true],
|
||||
['handler1', G_CODE, 'END', true]
|
||||
assert.deepEqual(reports, [
|
||||
['th2', 'START'],
|
||||
['th1', 'START'],
|
||||
['th2', 'PUT', 'Here comes'],
|
||||
['th1', 'PUT', 'Here comes'],
|
||||
['th2', 'PUT', 'the mouse!'],
|
||||
['th1', 'PUT', 'the mouse!'],
|
||||
['th2', 'END', true],
|
||||
['th1', 'END', true] // true since th2 indicated to keep bubbling
|
||||
]);
|
||||
});
|
||||
|
||||
it('dispose removes handler', () => {
|
||||
const G_CODE = 0x47;
|
||||
parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'handler1'));
|
||||
const disposable = parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'handler2'));
|
||||
disposable.dispose();
|
||||
parser.start();
|
||||
const data = toUtf32('Gdata');
|
||||
it('dispose handlers', () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1'));
|
||||
const dispo = parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2', true));
|
||||
dispo.dispose();
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(handlerReports, [
|
||||
['handler1', G_CODE, 'START'],
|
||||
['handler1', G_CODE, 'PUT', 'data'],
|
||||
['handler1', G_CODE, 'END', true]
|
||||
assert.deepEqual(reports, [
|
||||
['th1', 'START'],
|
||||
['th1', 'PUT', 'Here comes'],
|
||||
['th1', 'PUT', 'the mouse!'],
|
||||
['th1', 'END', true]
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ApcHandler convenience class', () => {
|
||||
describe('ApcHandlerFactory', () => {
|
||||
const TEST_PAYLOAD_LIMIT = 100;
|
||||
const CHUNK_SIZE = 10;
|
||||
let originalPayloadLimit: number;
|
||||
@@ -231,133 +185,306 @@ describe('ApcParser', () => {
|
||||
});
|
||||
|
||||
it('should be called once on end(true)', () => {
|
||||
const G_CODE = 0x47;
|
||||
const results: [number, string][] = [];
|
||||
parser.registerHandler(G_CODE, new ApcHandler((data: string) => {
|
||||
results.push([G_CODE, data]);
|
||||
return true;
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(data => {
|
||||
reports.push(data); return true;
|
||||
}));
|
||||
parser.start();
|
||||
let data = toUtf32('Gf=100');
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(',a=T;payload');
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(results, [[G_CODE, 'f=100,a=T;payload']]);
|
||||
assert.deepEqual(reports, ['Here comes the mouse!']);
|
||||
});
|
||||
|
||||
it('should not be called on end(false)', () => {
|
||||
const G_CODE = 0x47;
|
||||
const results: [number, string][] = [];
|
||||
parser.registerHandler(G_CODE, new ApcHandler((data: string) => {
|
||||
results.push([G_CODE, data]);
|
||||
return true;
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(data => {
|
||||
reports.push(data); return true;
|
||||
}));
|
||||
parser.start();
|
||||
const data = toUtf32('Gf=100,a=T;payload');
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(false);
|
||||
assert.deepEqual(results, []);
|
||||
assert.deepEqual(reports, []);
|
||||
});
|
||||
|
||||
it('should handle payload up to limit', function(): void {
|
||||
this.timeout(30000);
|
||||
const G_CODE = 0x47;
|
||||
const results: [number, string][] = [];
|
||||
parser.registerHandler(G_CODE, new ApcHandler((data: string) => {
|
||||
results.push([G_CODE, data]);
|
||||
return true;
|
||||
it('should be disposable', () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(data => {
|
||||
reports.push(['one', data]); return true;
|
||||
}));
|
||||
parser.start();
|
||||
let data = toUtf32('G');
|
||||
const dispo = parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(data => {
|
||||
reports.push(['two', data]); return true;
|
||||
}));
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('A'.repeat(CHUNK_SIZE));
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, [['two', 'Here comes the mouse!']]);
|
||||
dispo.dispose();
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
data = toUtf32('some other');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' data');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'some other data']]);
|
||||
});
|
||||
it('should respect return false', () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(data => {
|
||||
reports.push(['one', data]); return true;
|
||||
}));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(data => {
|
||||
reports.push(['two', data]); return false;
|
||||
}));
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'Here comes the mouse!']]);
|
||||
});
|
||||
it('should work up to payload limit', function(): void {
|
||||
this.timeout(30000);
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(data => {
|
||||
reports.push(data); return true;
|
||||
}));
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
const data = toUtf32('A'.repeat(CHUNK_SIZE));
|
||||
for (let i = 0; i < TEST_PAYLOAD_LIMIT; i += CHUNK_SIZE) {
|
||||
parser.put(data, 0, data.length);
|
||||
}
|
||||
parser.end(true);
|
||||
assert.deepEqual(results, [[G_CODE, 'A'.repeat(TEST_PAYLOAD_LIMIT)]]);
|
||||
assert.deepEqual(reports, ['A'.repeat(TEST_PAYLOAD_LIMIT)]);
|
||||
});
|
||||
|
||||
it('should abort for payload over limit', function(): void {
|
||||
it('should abort for payload limit +1', function(): void {
|
||||
this.timeout(30000);
|
||||
const G_CODE = 0x47;
|
||||
const results: [number, string][] = [];
|
||||
parser.registerHandler(G_CODE, new ApcHandler((data: string) => {
|
||||
results.push([G_CODE, data]);
|
||||
return true;
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(data => {
|
||||
reports.push(data); return true;
|
||||
}));
|
||||
parser.start();
|
||||
let data = toUtf32('G');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('A'.repeat(CHUNK_SIZE));
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
let data = toUtf32('A'.repeat(CHUNK_SIZE));
|
||||
for (let i = 0; i < TEST_PAYLOAD_LIMIT; i += CHUNK_SIZE) {
|
||||
parser.put(data, 0, data.length);
|
||||
}
|
||||
data = toUtf32('A');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(results, []);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reset behavior', () => {
|
||||
let handlerReports: [string, number, string, (boolean | string)?][];
|
||||
|
||||
beforeEach(() => {
|
||||
handlerReports = [];
|
||||
});
|
||||
|
||||
it('reset during payload cleans up handlers', () => {
|
||||
const G_CODE = 0x47;
|
||||
parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'kitty'));
|
||||
parser.start();
|
||||
const data = toUtf32('Gf=100');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.reset();
|
||||
assert.deepEqual(handlerReports, [
|
||||
['kitty', G_CODE, 'START'],
|
||||
['kitty', G_CODE, 'PUT', 'f=100'],
|
||||
['kitty', G_CODE, 'END', false]
|
||||
]);
|
||||
assert.deepEqual(reports, []);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
class TestHandlerAsync implements IApcHandler {
|
||||
constructor(public output: any[], public msg: string, public returnFalse: boolean = false) {}
|
||||
public start(): void {
|
||||
this.output.push([this.msg, 'START']);
|
||||
}
|
||||
public put(data: Uint32Array, start: number, end: number): void {
|
||||
this.output.push([this.msg, 'PUT', utf32ToString(data, start, end)]);
|
||||
}
|
||||
public async end(success: boolean): Promise<boolean> {
|
||||
// simple sleep to check in tests whether ordering gets messed up
|
||||
await Promise.resolve();
|
||||
this.output.push([this.msg, 'END', success]);
|
||||
if (this.returnFalse) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
async function unhookP(parser: ApcParser, success: boolean): Promise<void> {
|
||||
let result: void | Promise<boolean>;
|
||||
let prev: boolean | undefined;
|
||||
while (result = parser.end(success, prev)) {
|
||||
prev = await result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
describe('ApcParser - async tests', () => {
|
||||
let parser: ApcParser;
|
||||
let reports: [number, string, (boolean | string | undefined)?][] = [];
|
||||
|
||||
let reports: any[] = [];
|
||||
beforeEach(() => {
|
||||
reports = [];
|
||||
parser = new ApcParser();
|
||||
parser.setHandlerFallback((id: number, action: 'START' | 'PUT' | 'END', data?: string | boolean) => {
|
||||
reports.push([id, action, data]);
|
||||
});
|
||||
parser.setHandlerFallback((id, action, data) => reports.push([id, action, data]));
|
||||
});
|
||||
|
||||
async function endP(parser: ApcParser, success: boolean): Promise<void> {
|
||||
let result: void | Promise<boolean>;
|
||||
let prev: boolean | undefined;
|
||||
while (result = parser.end(success, prev)) {
|
||||
prev = await result;
|
||||
}
|
||||
}
|
||||
|
||||
describe('async ApcHandler', () => {
|
||||
it('should handle async handler', async () => {
|
||||
const G_CODE = 0x47;
|
||||
const results: [number, string][] = [];
|
||||
parser.registerHandler(G_CODE, new ApcHandler(async (data: string) => {
|
||||
await new Promise(res => setTimeout(res, 10));
|
||||
results.push([G_CODE, data]);
|
||||
return true;
|
||||
}));
|
||||
parser.start();
|
||||
const data = toUtf32('Gf=100,a=T');
|
||||
parser.put(data, 0, data.length);
|
||||
await endP(parser, true);
|
||||
assert.deepEqual(results, [[G_CODE, 'f=100,a=T']]);
|
||||
describe('sync and async mixed', () => {
|
||||
describe('sync | async | sync', () => {
|
||||
it('first should run, cleanup action for others', async () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', false));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', false));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's2', false));
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await unhookP(parser, true);
|
||||
assert.deepEqual(reports, [
|
||||
// messages from TestHandler
|
||||
['s2', 'START'],
|
||||
['a1', 'START'],
|
||||
['s1', 'START'],
|
||||
['s2', 'PUT', 'Here comes'],
|
||||
['a1', 'PUT', 'Here comes'],
|
||||
['s1', 'PUT', 'Here comes'],
|
||||
['s2', 'PUT', 'the mouse!'],
|
||||
['a1', 'PUT', 'the mouse!'],
|
||||
['s1', 'PUT', 'the mouse!'],
|
||||
['s2', 'END', true],
|
||||
['a1', 'END', false], // important: a1 before s1
|
||||
['s1', 'END', false]
|
||||
]);
|
||||
});
|
||||
it('all should run', async () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', true));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', true));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's2', true));
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await unhookP(parser, true);
|
||||
assert.deepEqual(reports, [
|
||||
// messages from TestHandler
|
||||
['s2', 'START'],
|
||||
['a1', 'START'],
|
||||
['s1', 'START'],
|
||||
['s2', 'PUT', 'Here comes'],
|
||||
['a1', 'PUT', 'Here comes'],
|
||||
['s1', 'PUT', 'Here comes'],
|
||||
['s2', 'PUT', 'the mouse!'],
|
||||
['a1', 'PUT', 'the mouse!'],
|
||||
['s1', 'PUT', 'the mouse!'],
|
||||
['s2', 'END', true],
|
||||
['a1', 'END', true], // important: a1 before s1
|
||||
['s1', 'END', true]
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('async | sync | async', () => {
|
||||
it('first should run, cleanup action for others', async () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', false));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', false));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a2', false));
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await unhookP(parser, true);
|
||||
assert.deepEqual(reports, [
|
||||
// messages from TestHandler
|
||||
['a2', 'START'],
|
||||
['s1', 'START'],
|
||||
['a1', 'START'],
|
||||
['a2', 'PUT', 'Here comes'],
|
||||
['s1', 'PUT', 'Here comes'],
|
||||
['a1', 'PUT', 'Here comes'],
|
||||
['a2', 'PUT', 'the mouse!'],
|
||||
['s1', 'PUT', 'the mouse!'],
|
||||
['a1', 'PUT', 'the mouse!'],
|
||||
['a2', 'END', true],
|
||||
['s1', 'END', false], // important: s1 between a2 .. a1
|
||||
['a1', 'END', false]
|
||||
]);
|
||||
});
|
||||
it('all should run', async () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', true));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', true));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a2', true));
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await unhookP(parser, true);
|
||||
assert.deepEqual(reports, [
|
||||
// messages from TestHandler
|
||||
['a2', 'START'],
|
||||
['s1', 'START'],
|
||||
['a1', 'START'],
|
||||
['a2', 'PUT', 'Here comes'],
|
||||
['s1', 'PUT', 'Here comes'],
|
||||
['a1', 'PUT', 'Here comes'],
|
||||
['a2', 'PUT', 'the mouse!'],
|
||||
['s1', 'PUT', 'the mouse!'],
|
||||
['a1', 'PUT', 'the mouse!'],
|
||||
['a2', 'END', true],
|
||||
['s1', 'END', true], // important: s1 between a2 .. a1
|
||||
['a1', 'END', true]
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('ApcHandlerFactory', () => {
|
||||
it('should be called once on end(true)', async () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(async data => {
|
||||
reports.push(data); return true;
|
||||
}));
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await unhookP(parser, true);
|
||||
assert.deepEqual(reports, ['Here comes the mouse!']);
|
||||
});
|
||||
it('should not be called on end(false)', async () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(async data => {
|
||||
reports.push(data); return true;
|
||||
}));
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await unhookP(parser, false);
|
||||
assert.deepEqual(reports, []);
|
||||
});
|
||||
it('should be disposable', async () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(async data => {
|
||||
reports.push(['one', data]); return true;
|
||||
}));
|
||||
const dispo = parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(async data => {
|
||||
reports.push(['two', data]); return true;
|
||||
}));
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await unhookP(parser, true);
|
||||
assert.deepEqual(reports, [['two', 'Here comes the mouse!']]);
|
||||
dispo.dispose();
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
data = toUtf32('some other');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' data');
|
||||
parser.put(data, 0, data.length);
|
||||
await unhookP(parser, true);
|
||||
assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'some other data']]);
|
||||
});
|
||||
it('should respect return false', async () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(async data => {
|
||||
reports.push(['one', data]); return true;
|
||||
}));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new ApcHandler(async data => {
|
||||
reports.push(['two', data]); return false;
|
||||
}));
|
||||
parser.start(identifier({intermediates: '+', final: 'p'}));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await unhookP(parser, true);
|
||||
assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'Here comes the mouse!']]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { IApcHandler, IHandlerCollection, ApcFallbackHandlerType, IApcParser, ISubParserStackState } from 'common/parser/Types';
|
||||
import { ApcState, ParserConstants } from 'common/parser/Constants';
|
||||
import { ParserConstants } from 'common/parser/Constants';
|
||||
import { utf32ToString } from 'common/input/TextDecoder';
|
||||
import { IDisposable } from 'common/Types';
|
||||
|
||||
@@ -19,10 +19,9 @@ const EMPTY_HANDLERS: IApcHandler[] = [];
|
||||
* The identifier is the character code of the first byte after ESC _.
|
||||
*/
|
||||
export class ApcParser implements IApcParser {
|
||||
private _state = ApcState.START;
|
||||
private _active = EMPTY_HANDLERS;
|
||||
private _id = -1;
|
||||
private _handlers: IHandlerCollection<IApcHandler> = Object.create(null);
|
||||
private _active = EMPTY_HANDLERS;
|
||||
private _ident: number = 0;
|
||||
private _handlerFb: ApcFallbackHandlerType = () => { };
|
||||
private _stack: ISubParserStackState = {
|
||||
paused: false,
|
||||
@@ -64,22 +63,24 @@ export class ApcParser implements IApcParser {
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
// force cleanup handlers if payload was already sent
|
||||
if (this._state === ApcState.PAYLOAD) {
|
||||
// force cleanup handlers
|
||||
if (this._active.length) {
|
||||
for (let j = this._stack.paused ? this._stack.loopPosition - 1 : this._active.length - 1; j >= 0; --j) {
|
||||
this._active[j].end(false);
|
||||
}
|
||||
}
|
||||
this._stack.paused = false;
|
||||
this._active = EMPTY_HANDLERS;
|
||||
this._id = -1;
|
||||
this._state = ApcState.START;
|
||||
this._ident = 0;
|
||||
}
|
||||
|
||||
private _start(): void {
|
||||
this._active = this._handlers[this._id] || EMPTY_HANDLERS;
|
||||
public start(ident: number): void {
|
||||
// always reset leftover handlers
|
||||
this.reset();
|
||||
this._ident = ident;
|
||||
this._active = this._handlers[ident] || EMPTY_HANDLERS;
|
||||
if (!this._active.length) {
|
||||
this._handlerFb(this._id, 'START');
|
||||
this._handlerFb(this._ident, 'START');
|
||||
} else {
|
||||
for (let j = this._active.length - 1; j >= 0; j--) {
|
||||
this._active[j].start();
|
||||
@@ -87,9 +88,9 @@ export class ApcParser implements IApcParser {
|
||||
}
|
||||
}
|
||||
|
||||
private _put(data: Uint32Array, start: number, end: number): void {
|
||||
public put(data: Uint32Array, start: number, end: number): void {
|
||||
if (!this._active.length) {
|
||||
this._handlerFb(this._id, 'PUT', utf32ToString(data, start, end));
|
||||
this._handlerFb(this._ident, 'PUT', utf32ToString(data, start, end));
|
||||
} else {
|
||||
for (let j = this._active.length - 1; j >= 0; j--) {
|
||||
this._active[j].put(data, start, end);
|
||||
@@ -97,100 +98,51 @@ export class ApcParser implements IApcParser {
|
||||
}
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
// always reset leftover handlers
|
||||
this.reset();
|
||||
this._state = ApcState.ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Put data to current APC command.
|
||||
* For APC, the first character is used as the identifier.
|
||||
* Format: ESC _ <identifier><payload> ESC \
|
||||
* Example: ESC _ G f=100,a=T;... ESC \ (Kitty graphics, identifier='G')
|
||||
*/
|
||||
public put(data: Uint32Array, start: number, end: number): void {
|
||||
if (this._state === ApcState.ABORT) {
|
||||
return;
|
||||
}
|
||||
if (this._state === ApcState.ID) {
|
||||
// The first character is the identifier
|
||||
if (start < end) {
|
||||
this._id = data[start++];
|
||||
this._state = ApcState.PAYLOAD;
|
||||
this._start();
|
||||
}
|
||||
}
|
||||
if (this._state === ApcState.PAYLOAD && end - start > 0) {
|
||||
this._put(data, start, end);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates end of an APC command.
|
||||
* Whether the APC got aborted or finished normally
|
||||
* is indicated by `success`.
|
||||
*/
|
||||
public end(success: boolean, promiseResult: boolean = true): void | Promise<boolean> {
|
||||
if (this._state === ApcState.START) {
|
||||
return;
|
||||
}
|
||||
// do nothing if command was faulty
|
||||
if (this._state !== ApcState.ABORT) {
|
||||
// if we are still in ID state and get an early end
|
||||
// means we got an empty APC sequence with no identifier,
|
||||
// which is invalid - just reset and return
|
||||
if (this._state === ApcState.ID) {
|
||||
this._active = EMPTY_HANDLERS;
|
||||
this._id = -1;
|
||||
this._state = ApcState.START;
|
||||
return;
|
||||
if (!this._active.length) {
|
||||
this._handlerFb(this._ident, 'END', success);
|
||||
} else {
|
||||
let handlerResult: boolean | Promise<boolean> = false;
|
||||
let j = this._active.length - 1;
|
||||
let fallThrough = false;
|
||||
if (this._stack.paused) {
|
||||
j = this._stack.loopPosition - 1;
|
||||
handlerResult = promiseResult;
|
||||
fallThrough = this._stack.fallThrough;
|
||||
this._stack.paused = false;
|
||||
}
|
||||
|
||||
if (!this._active.length) {
|
||||
this._handlerFb(this._id, 'END', success);
|
||||
} else {
|
||||
let handlerResult: boolean | Promise<boolean> = false;
|
||||
let j = this._active.length - 1;
|
||||
let fallThrough = false;
|
||||
if (this._stack.paused) {
|
||||
j = this._stack.loopPosition - 1;
|
||||
handlerResult = promiseResult;
|
||||
fallThrough = this._stack.fallThrough;
|
||||
this._stack.paused = false;
|
||||
}
|
||||
if (!fallThrough && handlerResult === false) {
|
||||
for (; j >= 0; j--) {
|
||||
handlerResult = this._active[j].end(success);
|
||||
if (handlerResult === true) {
|
||||
break;
|
||||
} else if (handlerResult instanceof Promise) {
|
||||
this._stack.paused = true;
|
||||
this._stack.loopPosition = j;
|
||||
this._stack.fallThrough = false;
|
||||
return handlerResult;
|
||||
}
|
||||
}
|
||||
j--;
|
||||
}
|
||||
// cleanup left over handlers
|
||||
// we always have to call .end for proper cleanup,
|
||||
// here we use `success` to indicate whether a handler should execute
|
||||
if (!fallThrough && handlerResult === false) {
|
||||
for (; j >= 0; j--) {
|
||||
handlerResult = this._active[j].end(false);
|
||||
if (handlerResult instanceof Promise) {
|
||||
handlerResult = this._active[j].end(success);
|
||||
if (handlerResult === true) {
|
||||
break;
|
||||
} else if (handlerResult instanceof Promise) {
|
||||
this._stack.paused = true;
|
||||
this._stack.loopPosition = j;
|
||||
this._stack.fallThrough = true;
|
||||
this._stack.fallThrough = false;
|
||||
return handlerResult;
|
||||
}
|
||||
}
|
||||
j--;
|
||||
}
|
||||
// cleanup left over handlers (fallThrough for async)
|
||||
for (; j >= 0; j--) {
|
||||
handlerResult = this._active[j].end(false);
|
||||
if (handlerResult instanceof Promise) {
|
||||
this._stack.paused = true;
|
||||
this._stack.loopPosition = j;
|
||||
this._stack.fallThrough = true;
|
||||
return handlerResult;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
this._active = EMPTY_HANDLERS;
|
||||
this._id = -1;
|
||||
this._state = ApcState.START;
|
||||
this._ident = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,9 +21,11 @@ export const enum ParserState {
|
||||
DCS_IGNORE = 11,
|
||||
DCS_INTERMEDIATE = 12,
|
||||
DCS_PASSTHROUGH = 13,
|
||||
APC_STRING = 14,
|
||||
APC_ENTRY = 14,
|
||||
APC_INTERMEDIATE = 15,
|
||||
APC_PASSTHROUGH = 16,
|
||||
// Number of states, meaning LAST_STATE + 1.
|
||||
STATE_LENGTH = 15
|
||||
STATE_LENGTH = 17
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,16 +62,6 @@ export const enum OscState {
|
||||
ABORT = 3
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal states of ApcParser.
|
||||
*/
|
||||
export const enum ApcState {
|
||||
START = 0,
|
||||
ID = 1,
|
||||
PAYLOAD = 2,
|
||||
ABORT = 3
|
||||
}
|
||||
|
||||
// payload limit for OSC and DCS
|
||||
export const enum ParserConstants {
|
||||
PAYLOAD_LIMIT = 10000000
|
||||
|
||||
@@ -191,7 +191,9 @@ describe('DcsParser', () => {
|
||||
});
|
||||
|
||||
it('should be called once on end(true)', () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push([params.toArray(), data]); return true; }));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => {
|
||||
reports.push([params.toArray(), data]); return true;
|
||||
}));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
@@ -201,7 +203,9 @@ describe('DcsParser', () => {
|
||||
assert.deepEqual(reports, [[[1, 2, 3], 'Here comes the mouse!']]);
|
||||
});
|
||||
it('should not be called on end(false)', () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push([params.toArray(), data]); return true; }));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => {
|
||||
reports.push([params.toArray(), data]); return true;
|
||||
}));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
@@ -211,8 +215,12 @@ describe('DcsParser', () => {
|
||||
assert.deepEqual(reports, []);
|
||||
});
|
||||
it('should be disposable', () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push(['one', params.toArray(), data]); return true; }));
|
||||
const dispo = parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push(['two', params.toArray(), data]); return true; }));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => {
|
||||
reports.push(['one', params.toArray(), data]); return true;
|
||||
}));
|
||||
const dispo = parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => {
|
||||
reports.push(['two', params.toArray(), data]); return true;
|
||||
}));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
@@ -230,8 +238,12 @@ describe('DcsParser', () => {
|
||||
assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'some other data']]);
|
||||
});
|
||||
it('should respect return false', () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push(['one', params.toArray(), data]); return true; }));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push(['two', params.toArray(), data]); return false; }));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => {
|
||||
reports.push(['one', params.toArray(), data]); return true;
|
||||
}));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => {
|
||||
reports.push(['two', params.toArray(), data]); return false;
|
||||
}));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
@@ -242,7 +254,9 @@ describe('DcsParser', () => {
|
||||
});
|
||||
it('should work up to payload limit', function(): void {
|
||||
this.timeout(30000);
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push([params.toArray(), data]); return true; }));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => {
|
||||
reports.push([params.toArray(), data]); return true;
|
||||
}));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
const data = toUtf32('A'.repeat(CHUNK_SIZE));
|
||||
for (let i = 0; i < TEST_PAYLOAD_LIMIT; i += CHUNK_SIZE) {
|
||||
@@ -253,7 +267,9 @@ describe('DcsParser', () => {
|
||||
});
|
||||
it('should abort for payload limit +1', function(): void {
|
||||
this.timeout(30000);
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push([params.toArray(), data]); return true; }));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => {
|
||||
reports.push([params.toArray(), data]); return true;
|
||||
}));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
let data = toUtf32('A'.repeat(CHUNK_SIZE));
|
||||
for (let i = 0; i < TEST_PAYLOAD_LIMIT; i += CHUNK_SIZE) {
|
||||
@@ -419,7 +435,9 @@ describe('DcsParser - async tests', () => {
|
||||
});
|
||||
describe('DcsHandlerFactory', () => {
|
||||
it('should be called once on end(true)', async () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push([params.toArray(), data]); return true; }));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => {
|
||||
reports.push([params.toArray(), data]); return true;
|
||||
}));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
@@ -429,7 +447,9 @@ describe('DcsParser - async tests', () => {
|
||||
assert.deepEqual(reports, [[[1, 2, 3], 'Here comes the mouse!']]);
|
||||
});
|
||||
it('should not be called on end(false)', async () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push([params.toArray(), data]); return true; }));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => {
|
||||
reports.push([params.toArray(), data]); return true;
|
||||
}));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
@@ -439,8 +459,12 @@ describe('DcsParser - async tests', () => {
|
||||
assert.deepEqual(reports, []);
|
||||
});
|
||||
it('should be disposable', async () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push(['one', params.toArray(), data]); return true; }));
|
||||
const dispo = parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push(['two', params.toArray(), data]); return true; }));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => {
|
||||
reports.push(['one', params.toArray(), data]); return true;
|
||||
}));
|
||||
const dispo = parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => {
|
||||
reports.push(['two', params.toArray(), data]); return true;
|
||||
}));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
@@ -458,8 +482,12 @@ describe('DcsParser - async tests', () => {
|
||||
assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'some other data']]);
|
||||
});
|
||||
it('should respect return false', async () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push(['one', params.toArray(), data]); return true; }));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push(['two', params.toArray(), data]); return false; }));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => {
|
||||
reports.push(['one', params.toArray(), data]); return true;
|
||||
}));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => {
|
||||
reports.push(['two', params.toArray(), data]); return false;
|
||||
}));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,24 @@ import { OscParser } from 'common/parser/OscParser';
|
||||
import { DcsParser } from 'common/parser/DcsParser';
|
||||
import { ApcParser } from 'common/parser/ApcParser';
|
||||
|
||||
/**
|
||||
* VT commands done by the parser
|
||||
*/
|
||||
// @vt: #Y ESC CSI "Control Sequence Introducer" "ESC [" "Start of a CSI sequence."
|
||||
// @vt: #Y ESC OSC "Operating System Command" "ESC ]" "Start of an OSC sequence."
|
||||
// @vt: #Y ESC DCS "Device Control String" "ESC P" "Start of a DCS sequence."
|
||||
// @vt: #Y ESC ST "String Terminator" "ESC \" "Terminator used for string type sequences."
|
||||
// @vt: #Y ESC PM "Privacy Message" "ESC ^" "Start of a privacy message."
|
||||
// @vt: #Y ESC APC "Application Program Command" "ESC _" "Start of an APC sequence."
|
||||
// @vt: #Y C1 CSI "Control Sequence Introducer" "\x9B" "Start of a CSI sequence."
|
||||
// @vt: #Y C1 OSC "Operating System Command" "\x9D" "Start of an OSC sequence."
|
||||
// @vt: #Y C1 DCS "Device Control String" "\x90" "Start of a DCS sequence."
|
||||
// @vt: #Y C1 ST "String Terminator" "\x9C" "Terminator used for string type sequences."
|
||||
// @vt: #Y C1 PM "Privacy Message" "\x9E" "Start of a privacy message."
|
||||
// @vt: #Y C1 APC "Application Program Command" "\x9F" "Start of an APC sequence."
|
||||
// @vt: #Y C0 NUL "Null" "\0, \x00" "NUL is ignored."
|
||||
// @vt: #Y C0 ESC "Escape" "\e, \x1B" "Start of a sequence. Cancels any other sequence."
|
||||
|
||||
/**
|
||||
* Table values are generated like this:
|
||||
* index: currentState << TableValue.INDEX_STATE_SHIFT | charCode
|
||||
@@ -77,7 +95,9 @@ const NON_ASCII_PRINTABLE = 0xA0;
|
||||
* Taken from https://vt100.net/emu/dec_ansi_parser.
|
||||
*/
|
||||
export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
|
||||
const table: TransitionTable = new TransitionTable(4095);
|
||||
// table size:
|
||||
// (ParserState.STATE_LENGTH - 1) << TableAccess.INDEX_STATE_SHIFT | NON_ASCII_PRINTABLE + 1
|
||||
const table: TransitionTable = new TransitionTable(4257);
|
||||
|
||||
// range macro for byte
|
||||
const BYTE_VALUES = 256;
|
||||
@@ -105,7 +125,7 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
|
||||
table.add(0x1b, state, ParserAction.CLEAR, ParserState.ESCAPE); // ESC
|
||||
table.add(0x9d, state, ParserAction.OSC_START, ParserState.OSC_STRING); // OSC
|
||||
table.addMany([0x98, 0x9e], state, ParserAction.IGNORE, ParserState.SOS_PM_STRING); // SOS, PM
|
||||
table.add(0x9f, state, ParserAction.APC_START, ParserState.APC_STRING); // APC
|
||||
table.add(0x9f, state, ParserAction.CLEAR, ParserState.APC_ENTRY); // APC
|
||||
table.add(0x9b, state, ParserAction.CLEAR, ParserState.CSI_ENTRY); // CSI
|
||||
table.add(0x90, state, ParserAction.CLEAR, ParserState.DCS_ENTRY); // DCS
|
||||
}
|
||||
@@ -136,11 +156,20 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
|
||||
table.add(0x9c, ParserState.SOS_PM_STRING, ParserAction.IGNORE, ParserState.GROUND);
|
||||
table.add(0x7f, ParserState.SOS_PM_STRING, ParserAction.IGNORE, ParserState.SOS_PM_STRING);
|
||||
// apc
|
||||
table.add(0x5f, ParserState.ESCAPE, ParserAction.APC_START, ParserState.APC_STRING);
|
||||
table.addMany(PRINTABLES, ParserState.APC_STRING, ParserAction.APC_PUT, ParserState.APC_STRING);
|
||||
table.addMany(EXECUTABLES, ParserState.APC_STRING, ParserAction.IGNORE, ParserState.APC_STRING);
|
||||
table.add(0x7f, ParserState.APC_STRING, ParserAction.IGNORE, ParserState.APC_STRING);
|
||||
table.addMany([0x1b, 0x9c, 0x18, 0x1a], ParserState.APC_STRING, ParserAction.APC_END, ParserState.GROUND);
|
||||
table.add(0x5f, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.APC_ENTRY);
|
||||
table.addMany(EXECUTABLES, ParserState.APC_ENTRY, ParserAction.IGNORE, ParserState.APC_ENTRY);
|
||||
table.add(0x7f, ParserState.APC_ENTRY, ParserAction.IGNORE, ParserState.APC_ENTRY);
|
||||
table.addMany(r(0x20, 0x30), ParserState.APC_ENTRY, ParserAction.COLLECT, ParserState.APC_INTERMEDIATE);
|
||||
table.addMany(r(0x30, 0x7f), ParserState.APC_ENTRY, ParserAction.APC_START, ParserState.APC_PASSTHROUGH);
|
||||
table.addMany(r(0x30, 0x7f), ParserState.APC_INTERMEDIATE, ParserAction.APC_START, ParserState.APC_PASSTHROUGH);
|
||||
table.addMany(EXECUTABLES, ParserState.APC_INTERMEDIATE, ParserAction.IGNORE, ParserState.APC_INTERMEDIATE);
|
||||
table.addMany(r(0x20, 0x30), ParserState.APC_INTERMEDIATE, ParserAction.COLLECT, ParserState.APC_INTERMEDIATE);
|
||||
table.add(0x7f, ParserState.APC_INTERMEDIATE, ParserAction.IGNORE, ParserState.APC_INTERMEDIATE);
|
||||
table.addMany(PRINTABLES, ParserState.APC_PASSTHROUGH, ParserAction.APC_PUT, ParserState.APC_PASSTHROUGH);
|
||||
table.addMany(EXECUTABLES, ParserState.APC_PASSTHROUGH, ParserAction.IGNORE, ParserState.APC_PASSTHROUGH);
|
||||
table.addMany(r(0x08, 0x0e), ParserState.APC_PASSTHROUGH, ParserAction.APC_PUT, ParserState.APC_PASSTHROUGH);
|
||||
table.add(0x7f, ParserState.APC_PASSTHROUGH, ParserAction.IGNORE, ParserState.APC_PASSTHROUGH);
|
||||
table.addMany([0x1b, 0x9c, 0x18, 0x1a], ParserState.APC_PASSTHROUGH, ParserAction.APC_END, ParserState.GROUND);
|
||||
// csi entries
|
||||
table.add(0x5b, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.CSI_ENTRY);
|
||||
table.addMany(r(0x40, 0x7f), ParserState.CSI_ENTRY, ParserAction.CSI_DISPATCH, ParserState.GROUND);
|
||||
@@ -169,22 +198,18 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
|
||||
table.add(0x50, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.DCS_ENTRY);
|
||||
table.addMany(EXECUTABLES, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY);
|
||||
table.add(0x7f, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY);
|
||||
table.addMany(r(0x1c, 0x20), ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY);
|
||||
table.addMany(r(0x20, 0x30), ParserState.DCS_ENTRY, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE);
|
||||
table.addMany(r(0x30, 0x3c), ParserState.DCS_ENTRY, ParserAction.PARAM, ParserState.DCS_PARAM);
|
||||
table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.DCS_ENTRY, ParserAction.COLLECT, ParserState.DCS_PARAM);
|
||||
table.addMany(EXECUTABLES, ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE);
|
||||
table.addMany(r(0x20, 0x80), ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE);
|
||||
table.addMany(r(0x1c, 0x20), ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE);
|
||||
table.addMany(EXECUTABLES, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM);
|
||||
table.add(0x7f, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM);
|
||||
table.addMany(r(0x1c, 0x20), ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM);
|
||||
table.addMany(r(0x30, 0x3c), ParserState.DCS_PARAM, ParserAction.PARAM, ParserState.DCS_PARAM);
|
||||
table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_IGNORE);
|
||||
table.addMany(r(0x20, 0x30), ParserState.DCS_PARAM, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE);
|
||||
table.addMany(EXECUTABLES, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE);
|
||||
table.add(0x7f, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE);
|
||||
table.addMany(r(0x1c, 0x20), ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE);
|
||||
table.addMany(r(0x20, 0x30), ParserState.DCS_INTERMEDIATE, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE);
|
||||
table.addMany(r(0x30, 0x40), ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_IGNORE);
|
||||
table.addMany(r(0x40, 0x7f), ParserState.DCS_INTERMEDIATE, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH);
|
||||
@@ -200,7 +225,7 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
|
||||
table.add(NON_ASCII_PRINTABLE, ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE);
|
||||
table.add(NON_ASCII_PRINTABLE, ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE);
|
||||
table.add(NON_ASCII_PRINTABLE, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH);
|
||||
table.add(NON_ASCII_PRINTABLE, ParserState.APC_STRING, ParserAction.APC_PUT, ParserState.APC_STRING);
|
||||
table.add(NON_ASCII_PRINTABLE, ParserState.APC_PASSTHROUGH, ParserAction.APC_PUT, ParserState.APC_PASSTHROUGH);
|
||||
return table;
|
||||
})();
|
||||
|
||||
@@ -247,6 +272,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
// handler lookup containers
|
||||
protected _printHandler: PrintHandlerType;
|
||||
protected _executeHandlers: { [flag: number]: ExecuteHandlerType };
|
||||
// fast path for EXE bytes < 0x18
|
||||
protected _executeHandlersArr: (ExecuteHandlerType | undefined)[];
|
||||
protected _csiHandlers: IHandlerCollection<CsiHandlerType>;
|
||||
protected _escHandlers: IHandlerCollection<EscHandlerType>;
|
||||
protected readonly _oscParser: IOscParser;
|
||||
@@ -290,11 +317,13 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
this._errorHandlerFb = (state: IParsingState): IParsingState => state;
|
||||
this._printHandler = this._printHandlerFb;
|
||||
this._executeHandlers = Object.create(null);
|
||||
this._executeHandlersArr = new Array(0x18).fill(undefined);
|
||||
this._csiHandlers = Object.create(null);
|
||||
this._escHandlers = Object.create(null);
|
||||
this._register(toDisposable(() => {
|
||||
this._csiHandlers = Object.create(null);
|
||||
this._executeHandlers = Object.create(null);
|
||||
this._executeHandlersArr = new Array(0x18).fill(undefined);
|
||||
this._escHandlers = Object.create(null);
|
||||
}));
|
||||
this._oscParser = this._register(new OscParser());
|
||||
@@ -381,10 +410,14 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
}
|
||||
|
||||
public setExecuteHandler(flag: string, handler: ExecuteHandlerType): void {
|
||||
this._executeHandlers[flag.charCodeAt(0)] = handler;
|
||||
const code = flag.charCodeAt(0);
|
||||
this._executeHandlers[code] = handler;
|
||||
if (code < 0x18) this._executeHandlersArr[code] = handler;
|
||||
}
|
||||
public clearExecuteHandler(flag: string): void {
|
||||
if (this._executeHandlers[flag.charCodeAt(0)]) delete this._executeHandlers[flag.charCodeAt(0)];
|
||||
const code = flag.charCodeAt(0);
|
||||
if (this._executeHandlers[code]) delete this._executeHandlers[code];
|
||||
if (code < 0x18) this._executeHandlersArr[code] = undefined;
|
||||
}
|
||||
public setExecuteHandlerFallback(handler: ExecuteFallbackHandlerType): void {
|
||||
this._executeHandlerFb = handler;
|
||||
@@ -431,11 +464,13 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
this._oscParser.setHandlerFallback(handler);
|
||||
}
|
||||
|
||||
public registerApcHandler(ident: number, handler: IApcHandler): IDisposable {
|
||||
return this._apcParser.registerHandler(ident, handler);
|
||||
public registerApcHandler(id: IFunctionIdentifier, handler: IApcHandler): IDisposable {
|
||||
id.prefix = undefined; // APC does not support prefix byte
|
||||
return this._apcParser.registerHandler(this._identifier(id, [0x30, 0x7e]), handler);
|
||||
}
|
||||
public clearApcHandler(ident: number): void {
|
||||
this._apcParser.clearHandler(ident);
|
||||
public clearApcHandler(id: IFunctionIdentifier): void {
|
||||
id.prefix = undefined; // APC does not support prefix byte
|
||||
this._apcParser.clearHandler(this._identifier(id, [0x30, 0x7e]));
|
||||
}
|
||||
public setApcHandlerFallback(handler: ApcFallbackHandlerType): void {
|
||||
this._apcParser.setHandlerFallback(handler);
|
||||
@@ -462,8 +497,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
this._oscParser.reset();
|
||||
this._dcsParser.reset();
|
||||
this._apcParser.reset();
|
||||
this._params.reset();
|
||||
this._params.addParam(0); // ZDM
|
||||
this._params.resetZdm();
|
||||
this._collect = 0;
|
||||
this.precedingJoinState = 0;
|
||||
// abort pending continuation from async handler
|
||||
@@ -506,6 +540,10 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
* - OSC_STRING:OSC_PUT
|
||||
* - DCS_PASSTHROUGH:DCS_PUT
|
||||
*
|
||||
* Additionally the following fast paths exist before the table lookup:
|
||||
* - EXE bytes < 0x18 in non-payload states (avoids table lookup entirely)
|
||||
* - 7-bit CSI sequences without intermediates (ESC [ params final)
|
||||
*
|
||||
* Note on asynchronous handler support:
|
||||
* Any handler returning a promise will be treated as asynchronous.
|
||||
* To keep the in-band blocking working for async handlers, `parse` pauses execution,
|
||||
@@ -608,8 +646,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
return handlerResult;
|
||||
}
|
||||
if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE;
|
||||
this._params.reset();
|
||||
this._params.addParam(0); // ZDM
|
||||
this._params.resetZdm();
|
||||
this._collect = 0;
|
||||
break;
|
||||
case ParserStackType.OSC:
|
||||
@@ -619,8 +656,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
return handlerResult;
|
||||
}
|
||||
if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE;
|
||||
this._params.reset();
|
||||
this._params.addParam(0); // ZDM
|
||||
this._params.resetZdm();
|
||||
this._collect = 0;
|
||||
break;
|
||||
case ParserStackType.APC:
|
||||
@@ -630,8 +666,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
return handlerResult;
|
||||
}
|
||||
if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE;
|
||||
this._params.reset();
|
||||
this._params.addParam(0); // ZDM
|
||||
this._params.resetZdm();
|
||||
this._collect = 0;
|
||||
break;
|
||||
}
|
||||
@@ -649,34 +684,90 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
for (let i = start; i < length; ++i) {
|
||||
code = data[i];
|
||||
|
||||
// EXE fast-path: common control bytes (0x00-0x17) in non-payload states
|
||||
if (code < 0x18 && this.currentState <= ParserState.CSI_IGNORE) {
|
||||
(this._executeHandlersArr[code] ?? this._executeHandlerFb)(code);
|
||||
this.precedingJoinState = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
// CSI fast-path: collapse ESC [ into a single entry, parse params+final in a tight loop
|
||||
if (code === 0x1b
|
||||
&& this.currentState < ParserState.OSC_STRING
|
||||
&& i + 2 < length && data[i + 1] === 0x5b
|
||||
) {
|
||||
this._params.resetZdm();
|
||||
this._collect = 0;
|
||||
let k = i + 2;
|
||||
let ch = data[k];
|
||||
if (ch >= 0x3c && ch <= 0x3f) {
|
||||
this._collect = ch;
|
||||
k++;
|
||||
}
|
||||
let csiDone = false;
|
||||
for (; k < length; k++) {
|
||||
ch = data[k];
|
||||
if (ch >= 0x30 && ch <= 0x39) {
|
||||
this._params.addDigit(ch - 48);
|
||||
} else if (ch === 0x3b) {
|
||||
this._params.addParam(0);
|
||||
} else if (ch === 0x3a) {
|
||||
this._params.addSubParam(-1);
|
||||
} else if (ch >= 0x40 && ch <= 0x7e) {
|
||||
const handlers = this._csiHandlers[this._collect << 8 | ch];
|
||||
let j = handlers ? handlers.length - 1 : -1;
|
||||
for (; j >= 0; j--) {
|
||||
handlerResult = handlers[j](this._params);
|
||||
if (handlerResult === true) {
|
||||
break;
|
||||
} else if (handlerResult instanceof Promise) {
|
||||
transition = ParserAction.CSI_DISPATCH << TableAccess.TRANSITION_ACTION_SHIFT | ParserState.GROUND;
|
||||
this._preserveStack(ParserStackType.CSI, handlers, j, transition, k);
|
||||
return handlerResult;
|
||||
}
|
||||
}
|
||||
if (j < 0) {
|
||||
this._csiHandlerFb(this._collect << 8 | ch, this._params);
|
||||
}
|
||||
this.precedingJoinState = 0;
|
||||
i = k;
|
||||
this.currentState = ParserState.GROUND;
|
||||
csiDone = true;
|
||||
break;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!csiDone) {
|
||||
i = k - 1;
|
||||
this.currentState = ParserState.CSI_PARAM;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// normal transition & action lookup
|
||||
transition = this._transitions.table[this.currentState << TableAccess.INDEX_STATE_SHIFT | (code < 0xa0 ? code : NON_ASCII_PRINTABLE)];
|
||||
transition = this._transitions.table[
|
||||
this.currentState << TableAccess.INDEX_STATE_SHIFT |
|
||||
(code < NON_ASCII_PRINTABLE ? code : NON_ASCII_PRINTABLE)
|
||||
];
|
||||
switch (transition >> TableAccess.TRANSITION_ACTION_SHIFT) {
|
||||
case ParserAction.PRINT:
|
||||
// read ahead with loop unrolling
|
||||
// Note: 0x20 (SP) is included, 0x7F (DEL) is excluded
|
||||
for (let j = i + 1; ; ++j) {
|
||||
if (j >= length || (code = data[j]) < 0x20 || (code > 0x7e && code < NON_ASCII_PRINTABLE)) {
|
||||
this._printHandler(data, i, j);
|
||||
i = j - 1;
|
||||
break;
|
||||
}
|
||||
if (++j >= length || (code = data[j]) < 0x20 || (code > 0x7e && code < NON_ASCII_PRINTABLE)) {
|
||||
this._printHandler(data, i, j);
|
||||
i = j - 1;
|
||||
break;
|
||||
}
|
||||
if (++j >= length || (code = data[j]) < 0x20 || (code > 0x7e && code < NON_ASCII_PRINTABLE)) {
|
||||
this._printHandler(data, i, j);
|
||||
i = j - 1;
|
||||
break;
|
||||
}
|
||||
if (++j >= length || (code = data[j]) < 0x20 || (code > 0x7e && code < NON_ASCII_PRINTABLE)) {
|
||||
this._printHandler(data, i, j);
|
||||
i = j - 1;
|
||||
break;
|
||||
let c = i;
|
||||
const l4 = length - 4;
|
||||
while (c < l4
|
||||
&& data[++c] >= 0x20 && (data[c] <= 0x7e || data[c] >= NON_ASCII_PRINTABLE)
|
||||
&& data[++c] >= 0x20 && (data[c] <= 0x7e || data[c] >= NON_ASCII_PRINTABLE)
|
||||
&& data[++c] >= 0x20 && (data[c] <= 0x7e || data[c] >= NON_ASCII_PRINTABLE)
|
||||
&& data[++c] >= 0x20 && (data[c] <= 0x7e || data[c] >= NON_ASCII_PRINTABLE)
|
||||
) {}
|
||||
if (c >= l4) {
|
||||
while (c < length && data[c] >= 0x20 && (data[c] <= 0x7e || data[c] >= NON_ASCII_PRINTABLE)) {
|
||||
c++;
|
||||
}
|
||||
}
|
||||
this._printHandler(data, i, c);
|
||||
i = c - 1;
|
||||
break;
|
||||
case ParserAction.EXECUTE:
|
||||
if (this._executeHandlers[code]) this._executeHandlers[code]();
|
||||
@@ -758,8 +849,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
this.precedingJoinState = 0;
|
||||
break;
|
||||
case ParserAction.CLEAR:
|
||||
this._params.reset();
|
||||
this._params.addParam(0); // ZDM
|
||||
this._params.resetZdm();
|
||||
this._collect = 0;
|
||||
break;
|
||||
case ParserAction.DCS_HOOK:
|
||||
@@ -783,8 +873,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
return handlerResult;
|
||||
}
|
||||
if (code === 0x1b) transition |= ParserState.ESCAPE;
|
||||
this._params.reset();
|
||||
this._params.addParam(0); // ZDM
|
||||
this._params.resetZdm();
|
||||
this._collect = 0;
|
||||
this.precedingJoinState = 0;
|
||||
break;
|
||||
@@ -808,22 +897,23 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
return handlerResult;
|
||||
}
|
||||
if (code === 0x1b) transition |= ParserState.ESCAPE;
|
||||
this._params.reset();
|
||||
this._params.addParam(0); // ZDM
|
||||
this._params.resetZdm();
|
||||
this._collect = 0;
|
||||
this.precedingJoinState = 0;
|
||||
break;
|
||||
case ParserAction.APC_START:
|
||||
this._apcParser.start();
|
||||
this._apcParser.start(this._collect << 8 | code);
|
||||
break;
|
||||
case ParserAction.APC_PUT:
|
||||
// inner loop - exit APC_PUT: 0x18, 0x1a, 0x1b, 0x9c
|
||||
// allowed: 00/08 .. 00/13, 02/00 .. 07/14 + NON_ASCII_PRINTABLE
|
||||
for (let j = i + 1; ; ++j) {
|
||||
if (j >= length || (code = data[j]) === 0x18 || code === 0x1a || code === 0x1b || code === 0x9c || (code > 0x7f && code < NON_ASCII_PRINTABLE)) {
|
||||
this._apcParser.put(data, i, j);
|
||||
i = j - 1;
|
||||
break;
|
||||
}
|
||||
if (j < length && (
|
||||
(data[j] >= 0x20 && data[j] < 0x7f) || (data[j] >= 0x08 && data[j] < 0x0e) || data[j] >= NON_ASCII_PRINTABLE
|
||||
)) continue;
|
||||
this._apcParser.put(data, i, j);
|
||||
i = j - 1;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case ParserAction.APC_END:
|
||||
@@ -833,8 +923,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
return handlerResult;
|
||||
}
|
||||
if (code === 0x1b) transition |= ParserState.ESCAPE;
|
||||
this._params.reset();
|
||||
this._params.addParam(0); // ZDM
|
||||
this._params.resetZdm();
|
||||
this._collect = 0;
|
||||
this.precedingJoinState = 0;
|
||||
break;
|
||||
|
||||
@@ -185,7 +185,9 @@ describe('OscParser', () => {
|
||||
});
|
||||
|
||||
it('should be called once on end(true)', () => {
|
||||
parser.registerHandler(1234, new OscHandler(data => { reports.push([1234, data]); return true; }));
|
||||
parser.registerHandler(1234, new OscHandler(data => {
|
||||
reports.push([1234, data]); return true;
|
||||
}));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
@@ -195,7 +197,9 @@ describe('OscParser', () => {
|
||||
assert.deepEqual(reports, [[1234, 'Here comes the mouse!']]);
|
||||
});
|
||||
it('should not be called on end(false)', () => {
|
||||
parser.registerHandler(1234, new OscHandler(data => { reports.push([1234, data]); return true; }));
|
||||
parser.registerHandler(1234, new OscHandler(data => {
|
||||
reports.push([1234, data]); return true;
|
||||
}));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
@@ -205,8 +209,12 @@ describe('OscParser', () => {
|
||||
assert.deepEqual(reports, []);
|
||||
});
|
||||
it('should be disposable', () => {
|
||||
parser.registerHandler(1234, new OscHandler(data => { reports.push(['one', data]); return true; }));
|
||||
const dispo = parser.registerHandler(1234, new OscHandler(data => { reports.push(['two', data]); return true; }));
|
||||
parser.registerHandler(1234, new OscHandler(data => {
|
||||
reports.push(['one', data]); return true;
|
||||
}));
|
||||
const dispo = parser.registerHandler(1234, new OscHandler(data => {
|
||||
reports.push(['two', data]); return true;
|
||||
}));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
@@ -224,8 +232,12 @@ describe('OscParser', () => {
|
||||
assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'some other data']]);
|
||||
});
|
||||
it('should respect return false', () => {
|
||||
parser.registerHandler(1234, new OscHandler(data => { reports.push(['one', data]); return true; }));
|
||||
parser.registerHandler(1234, new OscHandler(data => { reports.push(['two', data]); return false; }));
|
||||
parser.registerHandler(1234, new OscHandler(data => {
|
||||
reports.push(['one', data]); return true;
|
||||
}));
|
||||
parser.registerHandler(1234, new OscHandler(data => {
|
||||
reports.push(['two', data]); return false;
|
||||
}));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
@@ -236,7 +248,9 @@ describe('OscParser', () => {
|
||||
});
|
||||
it('should work up to payload limit', function(): void {
|
||||
this.timeout(30000);
|
||||
parser.registerHandler(1234, new OscHandler(data => { reports.push([1234, data]); return true; }));
|
||||
parser.registerHandler(1234, new OscHandler(data => {
|
||||
reports.push([1234, data]); return true;
|
||||
}));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;');
|
||||
parser.put(data, 0, data.length);
|
||||
@@ -249,7 +263,9 @@ describe('OscParser', () => {
|
||||
});
|
||||
it('should abort for payload limit +1', function(): void {
|
||||
this.timeout(30000);
|
||||
parser.registerHandler(1234, new OscHandler(data => { reports.push([1234, data]); return true; }));
|
||||
parser.registerHandler(1234, new OscHandler(data => {
|
||||
reports.push([1234, data]); return true;
|
||||
}));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;');
|
||||
parser.put(data, 0, data.length);
|
||||
@@ -412,7 +428,9 @@ describe('OscParser - async tests', () => {
|
||||
});
|
||||
describe('OscHandlerFactory', () => {
|
||||
it('should be called once on end(true)', async () => {
|
||||
parser.registerHandler(1234, new OscHandler(async data => { reports.push([1234, data]); return true; }));
|
||||
parser.registerHandler(1234, new OscHandler(async data => {
|
||||
reports.push([1234, data]); return true;
|
||||
}));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
@@ -423,7 +441,9 @@ describe('OscParser - async tests', () => {
|
||||
assert.deepEqual(reports, [[1234, 'Here comes the mouse!']]);
|
||||
});
|
||||
it('should not be called on end(false)', async () => {
|
||||
parser.registerHandler(1234, new OscHandler(async data => { reports.push([1234, data]); return true; }));
|
||||
parser.registerHandler(1234, new OscHandler(async data => {
|
||||
reports.push([1234, data]); return true;
|
||||
}));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
@@ -433,8 +453,12 @@ describe('OscParser - async tests', () => {
|
||||
assert.deepEqual(reports, []);
|
||||
});
|
||||
it('should be disposable', async () => {
|
||||
parser.registerHandler(1234, new OscHandler(async data => { reports.push(['one', data]); return true; }));
|
||||
const dispo = parser.registerHandler(1234, new OscHandler(async data => { reports.push(['two', data]); return true; }));
|
||||
parser.registerHandler(1234, new OscHandler(async data => {
|
||||
reports.push(['one', data]); return true;
|
||||
}));
|
||||
const dispo = parser.registerHandler(1234, new OscHandler(async data => {
|
||||
reports.push(['two', data]); return true;
|
||||
}));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
@@ -452,8 +476,12 @@ describe('OscParser - async tests', () => {
|
||||
assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'some other data']]);
|
||||
});
|
||||
it('should respect return false', async () => {
|
||||
parser.registerHandler(1234, new OscHandler(async data => { reports.push(['one', data]); return true; }));
|
||||
parser.registerHandler(1234, new OscHandler(async data => { reports.push(['two', data]); return false; }));
|
||||
parser.registerHandler(1234, new OscHandler(async data => {
|
||||
reports.push(['one', data]); return true;
|
||||
}));
|
||||
parser.registerHandler(1234, new OscHandler(async data => {
|
||||
reports.push(['two', data]); return false;
|
||||
}));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
|
||||
@@ -4,10 +4,16 @@
|
||||
*/
|
||||
import { IParams, ParamsArray } from 'common/parser/Types';
|
||||
|
||||
// max value supported for a single param/subparam (clamped to positive int32 range)
|
||||
const MAX_VALUE = 0x7FFFFFFF;
|
||||
// max allowed subparams for a single sequence (hardcoded limitation)
|
||||
const MAX_SUBPARAMS = 256;
|
||||
const enum Constants {
|
||||
/**
|
||||
* Max value supported for a single param/subparam (clamped to positive int32 range)
|
||||
*/
|
||||
MAX_VALUE = 0x7FFFFFFF,
|
||||
/**
|
||||
* Max allowed subparams for a single sequence (hardcoded limitation)
|
||||
*/
|
||||
MAX_SUBPARAMS = 256
|
||||
}
|
||||
|
||||
/**
|
||||
* Params storage class.
|
||||
@@ -70,7 +76,7 @@ export class Params implements IParams {
|
||||
* @param maxSubParamsLength max length of storable sub parameters
|
||||
*/
|
||||
constructor(public maxLength: number = 32, public maxSubParamsLength: number = 32) {
|
||||
if (maxSubParamsLength > MAX_SUBPARAMS) {
|
||||
if (maxSubParamsLength > Constants.MAX_SUBPARAMS) {
|
||||
throw new Error('maxSubParamsLength must not be greater than 256');
|
||||
}
|
||||
this.params = new Int32Array(maxLength);
|
||||
@@ -129,6 +135,19 @@ export class Params implements IParams {
|
||||
this._digitIsSub = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset and add 0 as first param (ZDM).
|
||||
*/
|
||||
public resetZdm(): void {
|
||||
this.length = 1;
|
||||
this._subParamsLength = 0;
|
||||
this._rejectDigits = false;
|
||||
this._rejectSubDigits = false;
|
||||
this._digitIsSub = false;
|
||||
this._subParamsIdx[0] = 0;
|
||||
this.params[0] = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a parameter value.
|
||||
* `Params` only stores up to `maxLength` parameters, any later
|
||||
@@ -146,7 +165,7 @@ export class Params implements IParams {
|
||||
throw new Error('values lesser than -1 are not allowed');
|
||||
}
|
||||
this._subParamsIdx[this.length] = this._subParamsLength << 8 | this._subParamsLength;
|
||||
this.params[this.length++] = value > MAX_VALUE ? MAX_VALUE : value;
|
||||
this.params[this.length++] = value > Constants.MAX_VALUE ? Constants.MAX_VALUE : value;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,7 +187,7 @@ export class Params implements IParams {
|
||||
if (value < -1) {
|
||||
throw new Error('values lesser than -1 are not allowed');
|
||||
}
|
||||
this._subParams[this._subParamsLength++] = value > MAX_VALUE ? MAX_VALUE : value;
|
||||
this._subParams[this._subParamsLength++] = value > Constants.MAX_VALUE ? Constants.MAX_VALUE : value;
|
||||
this._subParamsIdx[this.length - 1]++;
|
||||
}
|
||||
|
||||
@@ -224,6 +243,6 @@ export class Params implements IParams {
|
||||
|
||||
const store = this._digitIsSub ? this._subParams : this.params;
|
||||
const cur = store[length - 1];
|
||||
store[length - 1] = ~cur ? Math.min(cur * 10 + value, MAX_VALUE) : value;
|
||||
store[length - 1] = ~cur ? Math.min(cur * 10 + value, Constants.MAX_VALUE) : value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface IParams {
|
||||
clone(): IParams;
|
||||
toArray(): ParamsArray;
|
||||
reset(): void;
|
||||
resetZdm(): void;
|
||||
addParam(value: number): void;
|
||||
addSubParam(value: number): void;
|
||||
hasSubParams(idx: number): boolean;
|
||||
@@ -107,7 +108,7 @@ export type EscFallbackHandlerType = (identifier: number) => void;
|
||||
/**
|
||||
* EXECUTE handler types.
|
||||
*/
|
||||
export type ExecuteHandlerType = () => boolean;
|
||||
export type ExecuteHandlerType = (ident?: number) => boolean;
|
||||
export type ExecuteFallbackHandlerType = (ident: number) => void;
|
||||
|
||||
/**
|
||||
@@ -219,8 +220,8 @@ export interface IEscapeSequenceParser extends IDisposable {
|
||||
clearOscHandler(ident: number): void;
|
||||
setOscHandlerFallback(handler: OscFallbackHandlerType): void;
|
||||
|
||||
registerApcHandler(ident: number, handler: IApcHandler): IDisposable;
|
||||
clearApcHandler(ident: number): void;
|
||||
registerApcHandler(id: IFunctionIdentifier, handler: IApcHandler): IDisposable;
|
||||
clearApcHandler(id: IFunctionIdentifier): void;
|
||||
setApcHandlerFallback(handler: ApcFallbackHandlerType): void;
|
||||
|
||||
setErrorHandler(handler: (state: IParsingState) => IParsingState): void;
|
||||
@@ -251,7 +252,7 @@ export interface IDcsParser extends ISubParser<IDcsHandler, DcsFallbackHandlerTy
|
||||
}
|
||||
|
||||
export interface IApcParser extends ISubParser<IApcHandler, ApcFallbackHandlerType> {
|
||||
start(): void;
|
||||
start(ident: number): void;
|
||||
end(success: boolean, promiseResult?: boolean): void | Promise<boolean>;
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user