Compare commits
212
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
253d1e9beb | ||
|
|
34e0179355 | ||
|
|
bd722e7b07 | ||
|
|
cccdd447a0 | ||
|
|
446bcb1f75 | ||
|
|
4316263457 | ||
|
|
4a35a4880d | ||
|
|
ad730f2d54 | ||
|
|
9c7c163dd4 | ||
|
|
8674bc5e82 | ||
|
|
d552be8c45 | ||
|
|
082a51127f | ||
|
|
89100f6a3d | ||
|
|
8149d4481f | ||
|
|
2d9b21f5c7 | ||
|
|
f43985c422 | ||
|
|
9f6c7d7ced | ||
|
|
aa7716a333 | ||
|
|
1432e20082 | ||
|
|
f1b0a96094 | ||
|
|
49096a21fd | ||
|
|
63035678ec | ||
|
|
503abee8ae | ||
|
|
4350536685 | ||
|
|
9feb963cc9 | ||
|
|
f4042876c6 | ||
|
|
c294695444 | ||
|
|
fe0952bb83 | ||
|
|
5c2b19f2c8 | ||
|
|
fb25eb8f79 | ||
|
|
e7a4578933 | ||
|
|
a05a3321d5 | ||
|
|
26ce9bd4d3 | ||
|
|
1c1aac0da8 | ||
|
|
beeebca1a4 | ||
|
|
fd1e53c63f | ||
|
|
18262c87d9 | ||
|
|
2285ac6ab8 | ||
|
|
5302e62c90 | ||
|
|
af6731b533 | ||
|
|
5b9b531012 | ||
|
|
b0118dfd3a | ||
|
|
6813fc175e | ||
|
|
409f049f39 | ||
|
|
9e1b89651a | ||
|
|
89980fc565 | ||
|
|
16b5da5c4c | ||
|
|
4812abb24f | ||
|
|
5f3a4646be | ||
|
|
d63445be94 | ||
|
|
1400398abb | ||
|
|
2331abe6bd | ||
|
|
b66128b239 | ||
|
|
c727240f16 | ||
|
|
649ce22081 | ||
|
|
73adfdfd56 | ||
|
|
1fc8a4687d | ||
|
|
c29e48c917 | ||
|
|
70fe22993b | ||
|
|
4f3a7331bd | ||
|
|
6f3ce7b0fe | ||
|
|
fdb7346f3a | ||
|
|
783c4a34d2 | ||
|
|
8a1760b924 | ||
|
|
211db9d067 | ||
|
|
ec6ad20e33 | ||
|
|
47235f47fe | ||
|
|
8d6ff3a27b | ||
|
|
55281022a0 | ||
|
|
ea50fbdd1e | ||
|
|
87bf81cf96 | ||
|
|
f155c65e5a | ||
|
|
413c87cbf9 | ||
|
|
1620f8f5a8 | ||
|
|
e769f80a14 | ||
|
|
6651c4ca23 | ||
|
|
cb78a53f57 | ||
|
|
1c7b9f6d79 | ||
|
|
0d535100e4 | ||
|
|
5e19624e71 | ||
|
|
671425bf5a | ||
|
|
df5393ec45 | ||
|
|
b2553c17b4 | ||
|
|
0cea95d3b3 | ||
|
|
163868ff60 | ||
|
|
f4166a87ba | ||
|
|
f7a353cd34 | ||
|
|
7ec41471da | ||
|
|
f5e81afba7 | ||
|
|
a12061c899 | ||
|
|
d6c872bb2c | ||
|
|
a2cb36e6dd | ||
|
|
114c66c86c | ||
|
|
fe9650ce16 | ||
|
|
dedf3b1595 | ||
|
|
2158827b25 | ||
|
|
b406797dc7 | ||
|
|
5c28e27062 | ||
|
|
6e35b44f02 | ||
|
|
30c97a225c | ||
|
|
1186d09e05 | ||
|
|
d99e6bb2bc | ||
|
|
579a0a530f | ||
|
|
0b6220e5ea | ||
|
|
0d119b30d6 | ||
|
|
79c94de968 | ||
|
|
e3980a27db | ||
|
|
898c0d91c3 | ||
|
|
83401e3769 | ||
|
|
6b756318f0 | ||
|
|
3a9bfa94bc | ||
|
|
209e5aa6b9 | ||
|
|
24e2eb9d7b | ||
|
|
d5ae437dda | ||
|
|
5efff43e93 | ||
|
|
ee85a1da9e | ||
|
|
c30fedff6b | ||
|
|
21f51564d1 | ||
|
|
27b01b22ef | ||
|
|
387ec93a90 | ||
|
|
4388a4ac13 | ||
|
|
08c97d12b0 | ||
|
|
fbf1f7bc5d | ||
|
|
c8704930e3 | ||
|
|
e60c96823d | ||
|
|
375be97615 | ||
|
|
74642ce5d5 | ||
|
|
eccbc60aa0 | ||
|
|
6b64660934 | ||
|
|
10182cc656 | ||
|
|
3c7f313c34 | ||
|
|
457542b7b3 | ||
|
|
36118d9d4c | ||
|
|
cf8a4e466d | ||
|
|
4de03aaa38 | ||
|
|
677e5275f9 | ||
|
|
c66b0ed168 | ||
|
|
d4c3b693bb | ||
|
|
2f66b5fdb6 | ||
|
|
008aa1bfb5 | ||
|
|
1d0cf32b28 | ||
|
|
c01e4b7be7 | ||
|
|
fafb195f0f | ||
|
|
2c4997f529 | ||
|
|
f0d291f645 | ||
|
|
f6b98291cf | ||
|
|
16ea2ec0ef | ||
|
|
f7118b19c6 | ||
|
|
a0574f4e9c | ||
|
|
d9b5d150ae | ||
|
|
a25db1c76d | ||
|
|
c6a572fe21 | ||
|
|
b9fa53169f | ||
|
|
eb0f5383c3 | ||
|
|
a4502a6d00 | ||
|
|
c11a8f43f4 | ||
|
|
d7ba56cd28 | ||
|
|
be44833f12 | ||
|
|
fcb3cc1a21 | ||
|
|
376268157c | ||
|
|
5ceb02b9c6 | ||
|
|
d320898231 | ||
|
|
cf3939af4a | ||
|
|
38913e4ae1 | ||
|
|
596ab845d0 | ||
|
|
a59d527d1a | ||
|
|
08f85cf826 | ||
|
|
e95ec8d4bc | ||
|
|
d15b426bd4 | ||
|
|
c907edd75f | ||
|
|
1029e19b57 | ||
|
|
d44900faa2 | ||
|
|
5f724c5924 | ||
|
|
ae58176bcb | ||
|
|
429b2882f1 | ||
|
|
ffac8c03ab | ||
|
|
a7c4a160a8 | ||
|
|
c9dc994090 | ||
|
|
e4710d3286 | ||
|
|
bae314f67c | ||
|
|
cb8159c4f2 | ||
|
|
492d950b64 | ||
|
|
a1b69e5ac8 | ||
|
|
d77711936f | ||
|
|
5a79b08661 | ||
|
|
3e8fc48b33 | ||
|
|
8c9afdb364 | ||
|
|
f89524e577 | ||
|
|
a1c1702508 | ||
|
|
15c71e28c8 | ||
|
|
fd641370ec | ||
|
|
259cc67f12 | ||
|
|
b0b0868e90 | ||
|
|
d58e8597cd | ||
|
|
17d439515f | ||
|
|
ae171acb79 | ||
|
|
e2dabc77fd | ||
|
|
40342ecd1e | ||
|
|
1442f84e4e | ||
|
|
5e95ef652d | ||
|
|
bc1d87dade | ||
|
|
849110d300 | ||
|
|
8b6f8f3172 | ||
|
|
27825f9ed8 | ||
|
|
a507afb388 | ||
|
|
ec5b421dfb | ||
|
|
8f0e711a79 | ||
|
|
fc0dc3fb07 | ||
|
|
bea1628015 | ||
|
|
447746314e | ||
|
|
6a02d7c9f5 | ||
|
|
304943658d |
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "npm run agent:setup-repo"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
[submodule "vendor/ghostty"]
|
||||
path = vendor/ghostty
|
||||
url = https://github.com/ghostty-org/ghostty
|
||||
|
||||
@@ -24,7 +24,36 @@ First, you need to install the module. We ship exclusively through [npm](https:/
|
||||
npm install --save @xterm/xterm
|
||||
```
|
||||
|
||||
To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to the head of your HTML page. Then create a `<div id="terminal"></div>` onto which xterm can attach itself. Finally, instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`.
|
||||
The recommended way to load xterm.js with the ES module syntax, either using TypeScript or a modern JS tooling. Note that both CommonJS and ES module files are shipped with the npm package.
|
||||
|
||||
```javascript
|
||||
import { Terminal } from '@xterm/xterm';
|
||||
```
|
||||
|
||||
Then instantiate a `Terminal` object, open it on an element and write to it:
|
||||
|
||||
```javascript
|
||||
const term = new Terminal();
|
||||
term.open(document.getElementById('terminal'));
|
||||
term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ')
|
||||
```
|
||||
|
||||
Most use cases will hook up input and output to a pseudoterminal API such as [node-pty](https://www.npmjs.com/package/node-pty) which will drive the terminal.
|
||||
|
||||
```javascript
|
||||
pty.onData(data => term.write(data));
|
||||
term.onData(data => pty.write(data));
|
||||
```
|
||||
|
||||
Then make sure to also include the `css/xterm.css` file, for example in HTML:
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="node_modules/@xterm/xterm/css/xterm.css" />
|
||||
```
|
||||
|
||||
### Standalone example
|
||||
|
||||
Here is a complete standalone example in a HTML file:
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
@@ -36,7 +65,7 @@ To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to t
|
||||
<body>
|
||||
<div id="terminal"></div>
|
||||
<script>
|
||||
var term = new Terminal();
|
||||
const term = new Terminal();
|
||||
term.open(document.getElementById('terminal'));
|
||||
term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ')
|
||||
</script>
|
||||
@@ -44,14 +73,6 @@ To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to t
|
||||
</html>
|
||||
```
|
||||
|
||||
### Importing
|
||||
|
||||
The recommended way to load xterm.js is via the ES6 module syntax:
|
||||
|
||||
```javascript
|
||||
import { Terminal } from '@xterm/xterm';
|
||||
```
|
||||
|
||||
### Addons
|
||||
|
||||
Addons are separate modules that extend the `Terminal` by building on the [xterm.js API](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts). To use an addon, you first need to install it in your project:
|
||||
@@ -94,7 +115,7 @@ Since xterm.js is typically implemented as a developer tool, generally only mode
|
||||
|
||||
### Node.js Support
|
||||
|
||||
We also publish [`xterm-headless`](https://www.npmjs.com/package/xterm-headless) which is a stripped down version of xterm.js that runs headless in Node.js. An example use case for this is to keep track of a terminal's state where the process is running and using the [serialize addon](https://www.npmjs.com/package/@xterm/addon-serialize) so it can get all state restored upon reconnection.
|
||||
We also publish [`@xterm/headless`](https://www.npmjs.com/package/@xterm/headless) which is a stripped down version of xterm.js that runs headless in Node.js. An example use case for this is to keep track of a terminal's state where the process is running and using the [serialize addon](https://www.npmjs.com/package/@xterm/addon-serialize) so it can get all state restored upon reconnection.
|
||||
|
||||
## API
|
||||
|
||||
@@ -229,6 +250,8 @@ Xterm.js is used in many world-class applications to provide great terminal expe
|
||||
- [**ecmaOS**](https://ecmaos.sh): A kernel and suite of applications tying modern web technologies into a browser-based operating system.
|
||||
- [**LabEx**](https://labex.io): Interactive learning platform with hands-on labs and xterm.js-based online terminals, focused on learn-by-doing approach.
|
||||
- [**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.
|
||||
- [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.
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"build": "../../node_modules/.bin/tsgo -p .",
|
||||
"prepackage": "npm run build",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package",
|
||||
|
||||
@@ -9,13 +9,15 @@
|
||||
"outDir": "../out-test",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
],
|
||||
"*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"strict": true,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"build": "../../node_modules/.bin/tsgo -p .",
|
||||
"prepackage": "npm run build",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package",
|
||||
|
||||
@@ -9,13 +9,15 @@
|
||||
"outDir": "../out-test",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
],
|
||||
"*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"strict": true,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"build": "../../node_modules/.bin/tsgo -p .",
|
||||
"prepackage": "npm run build",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package",
|
||||
|
||||
@@ -48,10 +48,7 @@ export class FitAddon implements ITerminalAddon, IFitApi {
|
||||
return;
|
||||
}
|
||||
|
||||
// Force a full render
|
||||
if (this._terminal.rows !== dims.rows || this._terminal.cols !== dims.cols) {
|
||||
this._terminal.resize(dims.cols, dims.rows);
|
||||
}
|
||||
this._terminal.resize(dims.cols, dims.rows);
|
||||
}
|
||||
|
||||
public proposeDimensions(): ITerminalDimensions | undefined {
|
||||
|
||||
@@ -119,6 +119,17 @@ test.describe('FitAddon', () => {
|
||||
strictEqual(await ctx.proxy.cols, 2);
|
||||
strictEqual(await ctx.proxy.rows, 1);
|
||||
});
|
||||
|
||||
test('same dimensions', async function(): Promise<any> {
|
||||
await setDimensions();
|
||||
await ctx.page.evaluate(`window.fit.fit()`);
|
||||
const cols: number = await ctx.proxy.cols;
|
||||
const rows: number = await ctx.proxy.rows;
|
||||
// Calling fit() again at the same container size should not throw
|
||||
await ctx.page.evaluate(`window.fit.fit()`);
|
||||
strictEqual(await ctx.proxy.cols, cols);
|
||||
strictEqual(await ctx.proxy.rows, rows);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -9,13 +9,15 @@
|
||||
"outDir": "../out-test",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
],
|
||||
"*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"strict": true,
|
||||
|
||||
@@ -30,7 +30,9 @@ const customSettings: IImageAddonOptions = {
|
||||
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: 20000000, // size limit of a single IIP sequence
|
||||
kittySupport: true, // enable Kitty graphics support
|
||||
kittySizeLimit: 20000000 // size limit of a single Kitty sequence
|
||||
}
|
||||
|
||||
// initialization
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 123 B |
Binary file not shown.
|
After Width: | Height: | Size: 449 B |
Binary file not shown.
|
After Width: | Height: | Size: 131 B |
@@ -18,13 +18,13 @@
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"prepackage": "../../node_modules/.bin/tsc -p .",
|
||||
"prepackage": "../../node_modules/.bin/tsgo -p .",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package",
|
||||
"start": "node ../../demo/start"
|
||||
},
|
||||
"devDependencies": {
|
||||
"sixel": "^0.16.0",
|
||||
"xterm-wasm-parts": "^0.1.0"
|
||||
"xterm-wasm-parts": "^0.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,21 @@
|
||||
*/
|
||||
import { IImageAddonOptions, IOscHandler, IResetHandler, ITerminalExt } from './Types';
|
||||
import { ImageRenderer } from './ImageRenderer';
|
||||
import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
|
||||
import { IIPImageStorage } from './IIPImageStorage';
|
||||
import { CELL_SIZE_DEFAULT } from './ImageStorage';
|
||||
import Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
|
||||
import { HeaderParser, IHeaderFields, HeaderState } from './IIPHeaderParser';
|
||||
import { imageType, UNSUPPORTED_TYPE } from './IIPMetrics';
|
||||
|
||||
// limit hold memory in base64 decoder
|
||||
const KEEP_DATA = 4194304;
|
||||
// Local const enum mirror - esbuild can't inline const enums from external packages
|
||||
const enum DecoderConst {
|
||||
// Limit held memory in base64 decoder (encoded bytes).
|
||||
KEEP_DATA = 4194304,
|
||||
// Initial buffer allocation for the decoder.
|
||||
INITIAL_DATA = 1048576,
|
||||
// Local mirror of const enum (esbuild can't inline const enums from external packages)
|
||||
OK = 0
|
||||
}
|
||||
|
||||
// default IIP header values
|
||||
const DEFAULT_HEADER: IHeaderFields = {
|
||||
@@ -27,15 +35,19 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
||||
private _aborted = false;
|
||||
private _hp = new HeaderParser();
|
||||
private _header: IHeaderFields = DEFAULT_HEADER;
|
||||
private _dec = new Base64Decoder(KEEP_DATA);
|
||||
private _dec: Base64Decoder;
|
||||
private _metrics = UNSUPPORTED_TYPE;
|
||||
|
||||
constructor(
|
||||
private readonly _opts: IImageAddonOptions,
|
||||
private readonly _renderer: ImageRenderer,
|
||||
private readonly _storage: ImageStorage,
|
||||
private readonly _storage: IIPImageStorage,
|
||||
private readonly _coreTerminal: ITerminalExt
|
||||
) {}
|
||||
) {
|
||||
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);
|
||||
}
|
||||
|
||||
public reset(): void {}
|
||||
|
||||
@@ -50,7 +62,7 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
||||
if (this._aborted) return;
|
||||
|
||||
if (this._hp.state === HeaderState.END) {
|
||||
if (this._dec.put(data, start, end)) {
|
||||
if ((this._dec.put(data.subarray(start, end)) as number) !== DecoderConst.OK) {
|
||||
this._dec.release();
|
||||
this._aborted = true;
|
||||
}
|
||||
@@ -66,8 +78,8 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
||||
this._aborted = true;
|
||||
return;
|
||||
}
|
||||
this._dec.init(this._header.size);
|
||||
if (this._dec.put(data, dataPos, end)) {
|
||||
this._dec.init();
|
||||
if ((this._dec.put(data.subarray(dataPos, end)) as number) !== DecoderConst.OK) {
|
||||
this._dec.release();
|
||||
this._aborted = true;
|
||||
}
|
||||
@@ -85,13 +97,15 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
||||
let cond: number | boolean = true;
|
||||
if (cond = success) {
|
||||
if (cond = !this._dec.end()) {
|
||||
this._metrics = imageType(this._dec.data8);
|
||||
if (cond = this._metrics.mime !== 'unsupported') {
|
||||
w = this._metrics.width;
|
||||
h = this._metrics.height;
|
||||
if (cond = w && h && w * h < this._opts.pixelLimit) {
|
||||
[w, h] = this._resize(w, h).map(Math.floor);
|
||||
cond = w && h && w * h < this._opts.pixelLimit;
|
||||
if (cond = this._dec.data8.length === this._header.size) {
|
||||
this._metrics = imageType(this._dec.data8);
|
||||
if (cond = this._metrics.mime !== 'unsupported') {
|
||||
w = this._metrics.width;
|
||||
h = this._metrics.height;
|
||||
if (cond = w && h && w * h < this._opts.pixelLimit) {
|
||||
[w, h] = this._resize(w, h).map(Math.floor);
|
||||
cond = w && h && w * h < this._opts.pixelLimit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ImageStorage } from './ImageStorage';
|
||||
|
||||
/**
|
||||
* IIP (iTerm Image Protocol) specific image storage controller.
|
||||
*
|
||||
* Wraps the shared ImageStorage with IIP protocol semantics:
|
||||
* - Always uses scrolling mode (cursor advances with image)
|
||||
*/
|
||||
export class IIPImageStorage {
|
||||
constructor(
|
||||
private readonly _storage: ImageStorage
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Add an IIP image to storage.
|
||||
* Always uses scrolling mode — cursor advances past the image.
|
||||
*/
|
||||
public addImage(img: HTMLCanvasElement | ImageBitmap): void {
|
||||
this._storage.addImage(img, true);
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,15 @@
|
||||
|
||||
import type { ITerminalAddon, IDisposable } from '@xterm/xterm';
|
||||
import type { ImageAddon as IImageApi } from '@xterm/addon-image';
|
||||
import { Emitter, type IEvent } from 'common/Event';
|
||||
import { IIPHandler } from './IIPHandler';
|
||||
import { ImageRenderer } from './ImageRenderer';
|
||||
import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
|
||||
import { KittyGraphicsHandler } from './kitty/KittyGraphicsHandler';
|
||||
import { KittyImageStorage } from './kitty/KittyImageStorage';
|
||||
import { SixelHandler } from './SixelHandler';
|
||||
import { SixelImageStorage } from './SixelImageStorage';
|
||||
import { IIPImageStorage } from './IIPImageStorage';
|
||||
import { ITerminalExt, IImageAddonOptions, IResetHandler } from './Types';
|
||||
|
||||
// default values of addon ctor options
|
||||
@@ -22,7 +27,9 @@ const DEFAULT_OPTIONS: IImageAddonOptions = {
|
||||
storageLimit: 128,
|
||||
showPlaceholder: true,
|
||||
iipSupport: true,
|
||||
iipSizeLimit: 20000000
|
||||
iipSizeLimit: 20000000,
|
||||
kittySupport: true,
|
||||
kittySizeLimit: 20000000
|
||||
};
|
||||
|
||||
// max palette size supported by the sixel lib (compile time setting)
|
||||
@@ -56,6 +63,8 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
|
||||
private _disposables: IDisposable[] = [];
|
||||
private _terminal: ITerminalExt | undefined;
|
||||
private _handlers: Map<String, IResetHandler> = new Map();
|
||||
private readonly _onImageAdded = new Emitter<void>();
|
||||
public readonly onImageAdded: IEvent<void> = this._onImageAdded.event;
|
||||
|
||||
constructor(opts?: Partial<IImageAddonOptions>) {
|
||||
this._opts = Object.assign({}, DEFAULT_OPTIONS, opts);
|
||||
@@ -68,6 +77,7 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
|
||||
}
|
||||
this._disposables.length = 0;
|
||||
this._handlers.clear();
|
||||
this._onImageAdded.dispose();
|
||||
}
|
||||
|
||||
private _disposeLater(...args: IDisposable[]): void {
|
||||
@@ -82,6 +92,7 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
|
||||
// internal data structures
|
||||
this._renderer = new ImageRenderer(terminal);
|
||||
this._storage = new ImageStorage(terminal, this._renderer, this._opts);
|
||||
this._storage.onImageAdded = () => this._onImageAdded.fire();
|
||||
|
||||
// enable size reports
|
||||
if (this._opts.enableSizeReports) {
|
||||
@@ -129,7 +140,8 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
|
||||
|
||||
// SIXEL handler
|
||||
if (this._opts.sixelSupport) {
|
||||
const sixelHandler = new SixelHandler(this._opts, this._storage!, terminal);
|
||||
const sixelStorage = new SixelImageStorage(this._storage!, this._opts, this._renderer!, terminal);
|
||||
const sixelHandler = new SixelHandler(this._opts, sixelStorage, terminal);
|
||||
this._handlers.set('sixel', sixelHandler);
|
||||
this._disposeLater(
|
||||
terminal._core._inputHandler._parser.registerDcsHandler({ final: 'q' }, sixelHandler)
|
||||
@@ -138,12 +150,25 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
|
||||
|
||||
// iTerm IIP handler
|
||||
if (this._opts.iipSupport) {
|
||||
const iipHandler = new IIPHandler(this._opts, this._renderer!, this._storage!, terminal);
|
||||
const iipStorage = new IIPImageStorage(this._storage!);
|
||||
const iipHandler = new IIPHandler(this._opts, this._renderer!, iipStorage, terminal);
|
||||
this._handlers.set('iip', iipHandler);
|
||||
this._disposeLater(
|
||||
terminal._core._inputHandler._parser.registerOscHandler(1337, iipHandler)
|
||||
);
|
||||
}
|
||||
|
||||
// Kitty graphics handler
|
||||
if (this._opts.kittySupport) {
|
||||
const kittyStorage = new KittyImageStorage(this._storage!);
|
||||
const kittyHandler = new KittyGraphicsHandler(this._opts, this._renderer!, kittyStorage, terminal);
|
||||
this._handlers.set('kitty', kittyHandler);
|
||||
this._disposeLater(
|
||||
kittyStorage,
|
||||
kittyHandler,
|
||||
terminal._core._inputHandler._parser.registerApcHandler(0x47, kittyHandler)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Note: storageLimit is skipped here to not intoduce a surprising side effect.
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { toRGBA8888 } from 'sixel/lib/Colors';
|
||||
import { IDisposable } from '@xterm/xterm';
|
||||
import { ICellSize, ITerminalExt, IImageSpec, IRenderDimensions, IRenderService } from './Types';
|
||||
import { ICellSize, ImageLayer, ITerminalExt, IImageSpec, IRenderDimensions, IRenderService } from './Types';
|
||||
import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
|
||||
|
||||
const PLACEHOLDER_LENGTH = 4096;
|
||||
@@ -18,8 +18,9 @@ const PLACEHOLDER_HEIGHT = 24;
|
||||
* - draw image tiles onRender
|
||||
*/
|
||||
export class ImageRenderer extends Disposable implements IDisposable {
|
||||
public canvas: HTMLCanvasElement | undefined;
|
||||
private _ctx: CanvasRenderingContext2D | null | undefined;
|
||||
/** @deprecated Kept for backward compat — points to top layer 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;
|
||||
private _optionsRefresh = this._register(new MutableDisposable());
|
||||
@@ -86,6 +87,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
||||
});
|
||||
this._register(toDisposable(() => {
|
||||
this.removeLayerFromDom();
|
||||
this.removeLayerFromDom('bottom');
|
||||
if (this._terminal._core && this._oldOpen) {
|
||||
this._terminal._core.open = this._oldOpen;
|
||||
this._oldOpen = undefined;
|
||||
@@ -95,8 +97,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
||||
this._oldSetRenderer = undefined;
|
||||
}
|
||||
this._renderService = undefined;
|
||||
this.canvas = undefined;
|
||||
this._ctx = undefined;
|
||||
this._layers.clear();
|
||||
this._placeholderBitmap?.close();
|
||||
this._placeholderBitmap = undefined;
|
||||
this._placeholder = undefined;
|
||||
@@ -140,27 +141,38 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
||||
/**
|
||||
* Clear a region of the image layer canvas.
|
||||
*/
|
||||
public clearLines(start: number, end: number): void {
|
||||
this._ctx?.clearRect(
|
||||
0,
|
||||
start * (this.dimensions?.css.cell.height || 0),
|
||||
this.dimensions?.css.canvas.width || 0,
|
||||
(++end - start) * (this.dimensions?.css.cell.height || 0)
|
||||
);
|
||||
public clearLines(start: number, end: number, layer?: ImageLayer): void {
|
||||
const y = start * (this.dimensions?.css.cell.height || 0);
|
||||
const w = this.dimensions?.css.canvas.width || 0;
|
||||
const h = (++end - start) * (this.dimensions?.css.cell.height || 0);
|
||||
if (!layer || layer === 'top') {
|
||||
this._layers.get('top')?.clearRect(0, y, w, h);
|
||||
}
|
||||
if (!layer || layer === 'bottom') {
|
||||
this._layers.get('bottom')?.clearRect(0, y, w, h);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear whole image canvas.
|
||||
*/
|
||||
public clearAll(): void {
|
||||
this._ctx?.clearRect(0, 0, this.canvas?.width || 0, this.canvas?.height || 0);
|
||||
public clearAll(layer?: ImageLayer): void {
|
||||
if (!layer || layer === 'top') {
|
||||
const ctx = this._layers.get('top');
|
||||
ctx?.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
}
|
||||
if (!layer || layer === 'bottom') {
|
||||
const ctx = this._layers.get('bottom');
|
||||
ctx?.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw neighboring tiles on the image layer canvas.
|
||||
*/
|
||||
public draw(imgSpec: IImageSpec, tileId: number, col: number, row: number, count: number = 1): void {
|
||||
if (!this._ctx) {
|
||||
const ctx = this._layers.get(imgSpec.layer);
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
const { width, height } = this.cellSize;
|
||||
@@ -187,7 +199,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
||||
// Note: For not pixel perfect aligned cells like in the DOM renderer
|
||||
// this will move a tile slightly to the top/left (subpixel range, thus ignore it).
|
||||
// FIX #34: avoid striping on displays with pixelDeviceRatio != 1 by ceiling height and width
|
||||
this._ctx.drawImage(
|
||||
ctx.drawImage(
|
||||
img,
|
||||
Math.floor(sx), Math.floor(sy), Math.ceil(finalWidth), Math.ceil(finalHeight),
|
||||
Math.floor(dx), Math.floor(dy), Math.ceil(finalWidth), Math.ceil(finalHeight)
|
||||
@@ -227,7 +239,8 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
||||
* Draw a line with placeholder on the image layer canvas.
|
||||
*/
|
||||
public drawPlaceholder(col: number, row: number, count: number = 1): void {
|
||||
if (this._ctx) {
|
||||
const ctx = this._layers.get('top');
|
||||
if (ctx) {
|
||||
const { width, height } = this.cellSize;
|
||||
|
||||
// Don't try to draw anything, if we cannot get valid renderer metrics.
|
||||
@@ -241,7 +254,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
||||
this._createPlaceHolder(height + 1);
|
||||
}
|
||||
if (!this._placeholder) return;
|
||||
this._ctx.drawImage(
|
||||
ctx.drawImage(
|
||||
this._placeholderBitmap ?? this._placeholder!,
|
||||
col * width,
|
||||
(row * height) % 2 ? 0 : 1, // needs %2 offset correction
|
||||
@@ -260,12 +273,13 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
||||
* Checked once from `ImageStorage.render`.
|
||||
*/
|
||||
public rescaleCanvas(): void {
|
||||
if (!this.canvas) {
|
||||
return;
|
||||
}
|
||||
if (this.canvas.width !== this.dimensions!.css.canvas.width || this.canvas.height !== this.dimensions!.css.canvas.height) {
|
||||
this.canvas.width = this.dimensions!.css.canvas.width || 0;
|
||||
this.canvas.height = this.dimensions!.css.canvas.height || 0;
|
||||
const w = this.dimensions?.css.canvas.width || 0;
|
||||
const h = this.dimensions?.css.canvas.height || 0;
|
||||
for (const ctx of this._layers.values()) {
|
||||
if (ctx.canvas.width !== w || ctx.canvas.height !== h) {
|
||||
ctx.canvas.width = w;
|
||||
ctx.canvas.height = h;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,35 +318,65 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
||||
this._renderService = this._terminal._core._renderService;
|
||||
this._oldSetRenderer = this._renderService.setRenderer.bind(this._renderService);
|
||||
this._renderService.setRenderer = (renderer: any) => {
|
||||
this.removeLayerFromDom();
|
||||
for (const key of [...this._layers.keys()]) {
|
||||
this.removeLayerFromDom(key);
|
||||
}
|
||||
this._oldSetRenderer?.call(this._renderService, renderer);
|
||||
};
|
||||
}
|
||||
|
||||
public insertLayerToDom(): void {
|
||||
public insertLayerToDom(layer: ImageLayer = 'top'): void {
|
||||
// make sure that the terminal is attached to a document and to DOM
|
||||
if (this.document && this._terminal._core.screenElement) {
|
||||
if (!this.canvas) {
|
||||
this.canvas = ImageRenderer.createCanvas(
|
||||
this.document, this.dimensions?.css.canvas.width || 0,
|
||||
this.dimensions?.css.canvas.height || 0
|
||||
);
|
||||
this.canvas.classList.add('xterm-image-layer');
|
||||
this._terminal._core.screenElement.appendChild(this.canvas);
|
||||
this._ctx = this.canvas.getContext('2d', { alpha: true, desynchronized: true });
|
||||
this.clearAll();
|
||||
}
|
||||
} else {
|
||||
if (!this.document || !this._terminal._core.screenElement) {
|
||||
console.warn('image addon: cannot insert output canvas to DOM, missing document or screenElement');
|
||||
return;
|
||||
}
|
||||
if (this._layers.has(layer)) {
|
||||
return;
|
||||
}
|
||||
const canvas = ImageRenderer.createCanvas(
|
||||
this.document, this.dimensions?.css.canvas.width || 0,
|
||||
this.dimensions?.css.canvas.height || 0
|
||||
);
|
||||
canvas.classList.add(`xterm-image-layer-${layer}`);
|
||||
const screenElement = this._terminal._core.screenElement;
|
||||
// Use isolation to create a stacking context without overriding z-index,
|
||||
// which would conflict with integrators (e.g. VS Code) that set their
|
||||
// own z-index on the screen element.
|
||||
screenElement.style.isolation = 'isolate';
|
||||
if (layer === 'bottom') {
|
||||
// Use z-index:-1 so it paints behind non-positioned text elements.
|
||||
// The screen element needs to be a stacking context (via isolation)
|
||||
// to contain the negative z-index, otherwise it would go behind the
|
||||
// entire terminal.
|
||||
canvas.style.zIndex = '-1';
|
||||
screenElement.insertBefore(canvas, screenElement.firstChild);
|
||||
} else {
|
||||
// Explicit z-index ensures the image canvas reliably stacks above
|
||||
// the text layer (DOM renderer rows). z-index: 0 is below the
|
||||
// selection overlay (z-index: 1).
|
||||
canvas.style.zIndex = '0';
|
||||
screenElement.appendChild(canvas);
|
||||
}
|
||||
const ctx = canvas.getContext('2d', { alpha: true, desynchronized: true });
|
||||
if (!ctx) {
|
||||
canvas.remove();
|
||||
return;
|
||||
}
|
||||
this._layers.set(layer, ctx);
|
||||
this.clearAll(layer);
|
||||
}
|
||||
|
||||
public removeLayerFromDom(layer: ImageLayer = 'top'): void {
|
||||
const ctx = this._layers.get(layer);
|
||||
if (ctx) {
|
||||
ctx.canvas.remove();
|
||||
this._layers.delete(layer);
|
||||
}
|
||||
}
|
||||
|
||||
public removeLayerFromDom(): void {
|
||||
if (this.canvas) {
|
||||
this._ctx = undefined;
|
||||
this.canvas.remove();
|
||||
this.canvas = undefined;
|
||||
}
|
||||
public hasLayer(layer: ImageLayer): boolean {
|
||||
return this._layers.has(layer);
|
||||
}
|
||||
|
||||
private _createPlaceHolder(height: number = PLACEHOLDER_HEIGHT): void {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { IDisposable } from '@xterm/xterm';
|
||||
import { ImageRenderer } from './ImageRenderer';
|
||||
import { ITerminalExt, IExtendedAttrsImage, IImageAddonOptions, IImageSpec, IBufferLineExt, BgFlags, Cell, Content, ICellSize, ExtFlags, Attributes, UnderlineStyle } from './Types';
|
||||
import { ITerminalExt, IExtendedAttrsImage, IImageAddonOptions, IImageSpec, IBufferLineExt, BgFlags, Cell, Content, ICellSize, ExtFlags, Attributes, UnderlineStyle, ImageLayer } from './Types';
|
||||
|
||||
|
||||
// fallback default cell size
|
||||
@@ -124,6 +124,8 @@ export class ImageStorage implements IDisposable {
|
||||
private _pixelLimit: number = 2500000;
|
||||
|
||||
private _viewportMetrics: { cols: number, rows: number };
|
||||
public onImageAdded: (() => void) | undefined;
|
||||
public onImageDeleted: ((storageId: number) => void) | undefined;
|
||||
|
||||
constructor(
|
||||
private _terminal: ITerminalExt,
|
||||
@@ -189,11 +191,13 @@ export class ImageStorage implements IDisposable {
|
||||
|
||||
private _delImg(id: number): void {
|
||||
const spec = this._images.get(id);
|
||||
if (!spec) return;
|
||||
this._images.delete(id);
|
||||
// FIXME: really ugly workaround to get bitmaps deallocated :(
|
||||
if (spec && window.ImageBitmap && spec.orig instanceof ImageBitmap) {
|
||||
if (window.ImageBitmap && spec.orig instanceof ImageBitmap) {
|
||||
spec.orig.close();
|
||||
}
|
||||
this.onImageDeleted?.(id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -217,27 +221,27 @@ export class ImageStorage implements IDisposable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Only advance text cursor.
|
||||
* This is an edge case from empty sixels carrying only a height but no pixels.
|
||||
* Partially fixes https://github.com/jerch/xterm-addon-image/issues/37.
|
||||
* Delete an image by its internal storage ID.
|
||||
* Used by protocols that support explicit deletion (e.g. Kitty a=d).
|
||||
*/
|
||||
public advanceCursor(height: number): void {
|
||||
if (this._opts.sixelScrolling) {
|
||||
let cellSize = this._renderer.cellSize;
|
||||
if (cellSize.width === -1 || cellSize.height === -1) {
|
||||
cellSize = CELL_SIZE_DEFAULT;
|
||||
}
|
||||
const rows = Math.ceil(height / cellSize.height);
|
||||
for (let i = 1; i < rows; ++i) {
|
||||
this._terminal._core._inputHandler.lineFeed();
|
||||
}
|
||||
public deleteImage(id: number): void {
|
||||
const spec = this._images.get(id);
|
||||
if (spec) {
|
||||
spec.marker?.dispose();
|
||||
this._delImg(id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to add an image to the storage.
|
||||
* @param img - The image to add (canvas or bitmap).
|
||||
* @param scrolling - When true, cursor advances with the image (lineFeed per row).
|
||||
* When false, image is placed at (0,0) and cursor is restored (DECSET 80 / sixel origin mode).
|
||||
* @param layer - Which canvas layer to render on ('top' or 'bottom').
|
||||
* @param zIndex - Z-index for image layering within the same layer.
|
||||
* @returns The internal image ID assigned to the stored image.
|
||||
*/
|
||||
public addImage(img: HTMLCanvasElement | ImageBitmap): void {
|
||||
public addImage(img: HTMLCanvasElement | ImageBitmap, scrolling: boolean, layer: ImageLayer = 'top', zIndex: number = 0): number {
|
||||
// never allow storage to exceed memory limit
|
||||
this._evictOldest(img.width * img.height);
|
||||
|
||||
@@ -259,7 +263,7 @@ export class ImageStorage implements IDisposable {
|
||||
let offset = originX;
|
||||
let tileCount = 0;
|
||||
|
||||
if (!this._opts.sixelScrolling) {
|
||||
if (!scrolling) {
|
||||
buffer.x = 0;
|
||||
buffer.y = 0;
|
||||
offset = 0;
|
||||
@@ -273,7 +277,7 @@ export class ImageStorage implements IDisposable {
|
||||
this._writeToCell(line as IBufferLineExt, offset + col, imageId, row * cols + col);
|
||||
tileCount++;
|
||||
}
|
||||
if (this._opts.sixelScrolling) {
|
||||
if (scrolling) {
|
||||
if (row < rows - 1) this._terminal._core._inputHandler.lineFeed();
|
||||
} else {
|
||||
if (++buffer.y >= termRows) break;
|
||||
@@ -283,7 +287,7 @@ export class ImageStorage implements IDisposable {
|
||||
this._terminal._core._inputHandler._dirtyRowTracker.markDirty(buffer.y);
|
||||
|
||||
// cursor positioning modes
|
||||
if (this._opts.sixelScrolling) {
|
||||
if (scrolling) {
|
||||
buffer.x = offset;
|
||||
} else {
|
||||
buffer.x = originX;
|
||||
@@ -326,11 +330,15 @@ export class ImageStorage implements IDisposable {
|
||||
actualCellSize: { ...cellSize }, // clone needed, since later modified
|
||||
marker: endMarker || undefined,
|
||||
tileCount,
|
||||
bufferType: this._terminal.buffer.active.type
|
||||
bufferType: this._terminal.buffer.active.type,
|
||||
layer,
|
||||
zIndex
|
||||
};
|
||||
|
||||
// finally add the image
|
||||
this._images.set(imageId, imgSpec);
|
||||
this.onImageAdded?.();
|
||||
return imageId;
|
||||
}
|
||||
|
||||
|
||||
@@ -340,16 +348,30 @@ export class ImageStorage implements IDisposable {
|
||||
*/
|
||||
// TODO: Should we move this to the ImageRenderer?
|
||||
public render(range: { start: number, end: number }): void {
|
||||
// setup image canvas in case we have none yet, but have images in store
|
||||
if (!this._renderer.canvas && this._images.size) {
|
||||
this._renderer.insertLayerToDom();
|
||||
// safety measure - in case we cannot spawn a canvas at all, just exit
|
||||
if (!this._renderer.canvas) {
|
||||
return;
|
||||
// Determine which layers have images
|
||||
let hasTopImages = false;
|
||||
let hasBottomImages = false;
|
||||
for (const spec of this._images.values()) {
|
||||
if (spec.layer === 'bottom') {
|
||||
hasBottomImages = true;
|
||||
} else {
|
||||
hasTopImages = true;
|
||||
}
|
||||
if (hasTopImages && hasBottomImages) break;
|
||||
}
|
||||
|
||||
// Lazily insert layers that are needed
|
||||
if (hasTopImages && !this._renderer.hasLayer('top')) {
|
||||
this._renderer.insertLayerToDom('top');
|
||||
if (!this._renderer.hasLayer('top')) return;
|
||||
}
|
||||
if (hasBottomImages && !this._renderer.hasLayer('bottom')) {
|
||||
this._renderer.insertLayerToDom('bottom');
|
||||
}
|
||||
|
||||
// rescale if needed
|
||||
this._renderer.rescaleCanvas();
|
||||
|
||||
// exit early if we dont have any images to test for
|
||||
if (!this._images.size) {
|
||||
if (!this._fullyCleared) {
|
||||
@@ -357,12 +379,25 @@ export class ImageStorage implements IDisposable {
|
||||
this._fullyCleared = true;
|
||||
this._needsFullClear = false;
|
||||
}
|
||||
if (this._renderer.canvas) {
|
||||
this._renderer.removeLayerFromDom();
|
||||
if (this._renderer.hasLayer('top')) {
|
||||
this._renderer.removeLayerFromDom('top');
|
||||
}
|
||||
if (this._renderer.hasLayer('bottom')) {
|
||||
this._renderer.removeLayerFromDom('bottom');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove layers no longer needed
|
||||
if (!hasTopImages && this._renderer.hasLayer('top')) {
|
||||
this._renderer.clearAll('top');
|
||||
this._renderer.removeLayerFromDom('top');
|
||||
}
|
||||
if (!hasBottomImages && this._renderer.hasLayer('bottom')) {
|
||||
this._renderer.clearAll('bottom');
|
||||
this._renderer.removeLayerFromDom('bottom');
|
||||
}
|
||||
|
||||
// buffer switches force a full clear
|
||||
if (this._needsFullClear) {
|
||||
this._renderer.clearAll();
|
||||
@@ -377,50 +412,77 @@ export class ImageStorage implements IDisposable {
|
||||
// clear drawing area
|
||||
this._renderer.clearLines(start, end);
|
||||
|
||||
// walk all cells in viewport and draw tiles found
|
||||
// Collect draw calls so we can sort by z-index (lower z drawn first).
|
||||
const drawCalls: { imgSpec: IImageSpec, tileId: number, col: number, row: number, count: number }[] = [];
|
||||
const placeholderCalls: { col: number, row: number, count: number }[] = [];
|
||||
|
||||
// walk all cells in viewport and collect tiles found
|
||||
// Note: We check _extendedAttrs directly (not just HAS_EXTENDED flag)
|
||||
// because text writes clear the BG flag but leave image tile data intact.
|
||||
// This lets top-layer images survive text overwrites (kitty C=1 behavior).
|
||||
for (let row = start; row <= end; ++row) {
|
||||
const line = buffer.lines.get(row + buffer.ydisp) as IBufferLineExt;
|
||||
if (!line) return;
|
||||
for (let col = 0; col < cols; ++col) {
|
||||
let e: IExtendedAttrsImage;
|
||||
if (line.getBg(col) & BgFlags.HAS_EXTENDED) {
|
||||
let e: IExtendedAttrsImage = line._extendedAttrs[col] ?? EMPTY_ATTRS;
|
||||
const imageId = e.imageId;
|
||||
if (imageId === undefined || imageId === -1) {
|
||||
e = line._extendedAttrs[col] ?? EMPTY_ATTRS;
|
||||
} else {
|
||||
const maybeImg = line._extendedAttrs[col] as IExtendedAttrsImage | undefined;
|
||||
if (!maybeImg || maybeImg.imageId === undefined || maybeImg.imageId === -1) {
|
||||
continue;
|
||||
}
|
||||
const imgSpec = this._images.get(imageId);
|
||||
if (e.tileId !== -1) {
|
||||
const startTile = e.tileId;
|
||||
const startCol = col;
|
||||
let count = 1;
|
||||
/**
|
||||
* merge tiles to the right into a single draw call, if:
|
||||
* - not at end of line
|
||||
* - cell has same image id
|
||||
* - cell has consecutive tile id
|
||||
*/
|
||||
while (
|
||||
++col < cols
|
||||
&& (line.getBg(col) & BgFlags.HAS_EXTENDED)
|
||||
&& (e = line._extendedAttrs[col] ?? EMPTY_ATTRS)
|
||||
&& (e.imageId === imageId)
|
||||
&& (e.tileId === startTile + count)
|
||||
) {
|
||||
count++;
|
||||
e = maybeImg;
|
||||
}
|
||||
const imageId = e.imageId;
|
||||
if (imageId === undefined || imageId === -1) {
|
||||
continue;
|
||||
}
|
||||
const imgSpec = this._images.get(imageId);
|
||||
if (e.tileId !== -1) {
|
||||
const startTile = e.tileId;
|
||||
const startCol = col;
|
||||
let count = 1;
|
||||
/**
|
||||
* merge tiles to the right into a single draw call, if:
|
||||
* - not at end of line
|
||||
* - cell has same image id
|
||||
* - cell has consecutive tile id
|
||||
* Also check _extendedAttrs directly for cells where text cleared HAS_EXTENDED.
|
||||
*/
|
||||
while (++col < cols) {
|
||||
const nextE = line._extendedAttrs[col] as IExtendedAttrsImage | undefined;
|
||||
if (!nextE || nextE.imageId !== imageId || nextE.tileId !== startTile + count) {
|
||||
break;
|
||||
}
|
||||
col--;
|
||||
if (imgSpec) {
|
||||
if (imgSpec.actual) {
|
||||
this._renderer.draw(imgSpec, startTile, startCol, row, count);
|
||||
}
|
||||
} else if (this._opts.showPlaceholder) {
|
||||
this._renderer.drawPlaceholder(startCol, row, count);
|
||||
}
|
||||
this._fullyCleared = false;
|
||||
e = nextE;
|
||||
count++;
|
||||
}
|
||||
col--;
|
||||
if (imgSpec) {
|
||||
if (imgSpec.actual) {
|
||||
drawCalls.push({ imgSpec, tileId: startTile, col: startCol, row, count });
|
||||
}
|
||||
} else if (this._opts.showPlaceholder) {
|
||||
placeholderCalls.push({ col: startCol, row, count });
|
||||
}
|
||||
this._fullyCleared = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by z-index so lower z draws first (higher z renders on top)
|
||||
drawCalls.sort((a, b) => a.imgSpec.zIndex - b.imgSpec.zIndex);
|
||||
|
||||
// Draw placeholders first (lowest priority)
|
||||
for (const call of placeholderCalls) {
|
||||
this._renderer.drawPlaceholder(call.col, call.row, call.count);
|
||||
}
|
||||
|
||||
// Draw images in z-index order
|
||||
for (const call of drawCalls) {
|
||||
this._renderer.draw(call.imgSpec, call.tileId, call.col, call.row, call.count);
|
||||
}
|
||||
}
|
||||
|
||||
public viewportResize(metrics: { cols: number, rows: number }): void {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ImageStorage } from './ImageStorage';
|
||||
import { SixelImageStorage } from './SixelImageStorage';
|
||||
import { IDcsHandler, IParams, IImageAddonOptions, ITerminalExt, AttributeData, IResetHandler, ReadonlyColorSet } from './Types';
|
||||
import { toRGBA8888, BIG_ENDIAN, PALETTE_ANSI_256, PALETTE_VT340_COLOR } from 'sixel/lib/Colors';
|
||||
import { RGBA8888 } from 'sixel/lib/Types';
|
||||
@@ -26,7 +26,7 @@ export class SixelHandler implements IDcsHandler, IResetHandler {
|
||||
|
||||
constructor(
|
||||
private readonly _opts: IImageAddonOptions,
|
||||
private readonly _storage: ImageStorage,
|
||||
private readonly _storage: SixelImageStorage,
|
||||
private readonly _coreTerminal: ITerminalExt
|
||||
) {
|
||||
DecoderAsync({
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Copyright (c) 2020 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
|
||||
import { IImageAddonOptions, ITerminalExt } from './Types';
|
||||
import { ImageRenderer } from './ImageRenderer';
|
||||
|
||||
/**
|
||||
* Sixel-specific image storage controller.
|
||||
*
|
||||
* Wraps the shared ImageStorage with sixel protocol semantics:
|
||||
* - Cursor behavior governed by DECSET 80 (sixelScrolling option)
|
||||
* - advanceCursor for empty sixels carrying only height
|
||||
*/
|
||||
export class SixelImageStorage {
|
||||
constructor(
|
||||
private readonly _storage: ImageStorage,
|
||||
private readonly _opts: IImageAddonOptions,
|
||||
private readonly _renderer: ImageRenderer,
|
||||
private readonly _terminal: ITerminalExt
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Add a sixel image to storage.
|
||||
* Cursor behavior depends on the sixelScrolling option (DECSET 80).
|
||||
*/
|
||||
public addImage(img: HTMLCanvasElement | ImageBitmap): void {
|
||||
this._storage.addImage(img, this._opts.sixelScrolling);
|
||||
}
|
||||
|
||||
/**
|
||||
* Only advance text cursor.
|
||||
* This is an edge case from empty sixels carrying only a height but no pixels.
|
||||
* Partially fixes https://github.com/jerch/xterm-addon-image/issues/37.
|
||||
*/
|
||||
public advanceCursor(height: number): void {
|
||||
if (this._opts.sixelScrolling) {
|
||||
let cellSize = this._renderer.cellSize;
|
||||
if (cellSize.width === -1 || cellSize.height === -1) {
|
||||
cellSize = CELL_SIZE_DEFAULT;
|
||||
}
|
||||
const rows = Math.ceil(height / cellSize.height);
|
||||
for (let i = 1; i < rows; ++i) {
|
||||
this._terminal._core._inputHandler.lineFeed();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import { IDisposable, IMarker, Terminal } from '@xterm/xterm';
|
||||
// private imports from base repo we build against
|
||||
import { Attributes, BgFlags, Content, ExtFlags, UnderlineStyle } from 'common/buffer/Constants';
|
||||
import type { AttributeData } from 'common/buffer/AttributeData';
|
||||
import type { IParams, IDcsHandler, IOscHandler, IEscapeSequenceParser } from 'common/parser/Types';
|
||||
import type { IParams, IDcsHandler, IOscHandler, IApcHandler, IEscapeSequenceParser } from 'common/parser/Types';
|
||||
import type { IBufferLine, IExtendedAttrs, IInputHandler } from 'common/Types';
|
||||
import type { ITerminal, ReadonlyColorSet } from 'browser/Types';
|
||||
import type { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
@@ -22,7 +22,7 @@ export const enum Cell {
|
||||
}
|
||||
|
||||
// export some privates for local usage
|
||||
export { AttributeData, IParams, IDcsHandler, IOscHandler, BgFlags, IRenderDimensions, IRenderService, Content, ExtFlags, Attributes, UnderlineStyle, ReadonlyColorSet };
|
||||
export { AttributeData, IParams, IDcsHandler, IOscHandler, IApcHandler, BgFlags, IRenderDimensions, IRenderService, Content, ExtFlags, Attributes, UnderlineStyle, ReadonlyColorSet };
|
||||
|
||||
/**
|
||||
* Plugin ctor options.
|
||||
@@ -38,6 +38,8 @@ export interface IImageAddonOptions {
|
||||
sixelSizeLimit: number;
|
||||
iipSupport: boolean;
|
||||
iipSizeLimit: number;
|
||||
kittySupport: boolean;
|
||||
kittySizeLimit: number;
|
||||
}
|
||||
|
||||
export interface IResetHandler {
|
||||
@@ -97,6 +99,8 @@ export interface ICellSize {
|
||||
height: number;
|
||||
}
|
||||
|
||||
export type ImageLayer = 'top' | 'bottom';
|
||||
|
||||
export interface IImageSpec {
|
||||
orig: HTMLCanvasElement | ImageBitmap | undefined;
|
||||
origCellSize: ICellSize;
|
||||
@@ -105,4 +109,6 @@ export interface IImageSpec {
|
||||
marker: IMarker | undefined;
|
||||
tileCount: number;
|
||||
bufferType: 'alternate' | 'normal';
|
||||
layer: ImageLayer;
|
||||
zIndex: number;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,820 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDisposable } from '@xterm/xterm';
|
||||
import { IApcHandler, IImageAddonOptions, IResetHandler, ITerminalExt, ImageLayer } from '../Types';
|
||||
import { ImageRenderer } from '../ImageRenderer';
|
||||
import { CELL_SIZE_DEFAULT } from '../ImageStorage';
|
||||
import { KittyImageStorage } from './KittyImageStorage';
|
||||
import Base64Decoder, { type DecodeStatus } from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
|
||||
import {
|
||||
KittyAction,
|
||||
KittyFormat,
|
||||
KittyCompression,
|
||||
IKittyCommand,
|
||||
IPendingTransmission,
|
||||
IKittyImageData,
|
||||
BYTES_PER_PIXEL_RGB,
|
||||
BYTES_PER_PIXEL_RGBA,
|
||||
ALPHA_OPAQUE,
|
||||
parseKittyCommand
|
||||
} from './KittyGraphicsTypes';
|
||||
|
||||
// Memory limit for base64 decoder (4MB, same as IIPHandler)
|
||||
const DECODER_KEEP_DATA = 4194304;
|
||||
const DECODER_INITIAL_DATA = 4194304; // 4MB
|
||||
|
||||
// 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;
|
||||
|
||||
// Kitty graphics protocol handler with streaming base64 decoding.
|
||||
export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDisposable {
|
||||
private _aborted = false;
|
||||
private _decodeError = false;
|
||||
|
||||
private _activeDecoder: Base64Decoder | null = null;
|
||||
private readonly _maxEncodedBytes: number;
|
||||
private readonly _initialEncodedBytes: number;
|
||||
|
||||
// Streaming related states
|
||||
|
||||
// True while receiving control data (before semicolon).
|
||||
private _inControlData = true;
|
||||
|
||||
// Buffer for control data.
|
||||
private _controlData = new Uint32Array(MAX_CONTROL_DATA_SIZE);
|
||||
private _controlLength = 0;
|
||||
|
||||
// Pre-calculated encoded size limit
|
||||
private _encodedSizeLimit = 0;
|
||||
private _totalEncodedSize = 0;
|
||||
|
||||
// Parsed command. These are the control data before semicolon.
|
||||
private _parsedCommand: IKittyCommand | null = null;
|
||||
|
||||
// Storage related states
|
||||
|
||||
private _pendingTransmissions: Map<number, IPendingTransmission> = new Map();
|
||||
// Tracks the pending key of the most recently started chunked upload.
|
||||
// Per spec, subsequent chunks only need m= (and optionally q=), without i=.
|
||||
// When a chunk arrives with no i=, this key is used to find the pending upload.
|
||||
private _lastPendingKey: number | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly _opts: IImageAddonOptions,
|
||||
private readonly _renderer: ImageRenderer,
|
||||
private readonly _kittyStorage: KittyImageStorage,
|
||||
private readonly _coreTerminal: ITerminalExt
|
||||
) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this._cleanupAllPending();
|
||||
if (this._activeDecoder) {
|
||||
this._activeDecoder.release();
|
||||
this._activeDecoder = null;
|
||||
}
|
||||
this._kittyStorage.reset();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
private _removePendingEntry(key: number): void {
|
||||
this._pendingTransmissions.delete(key);
|
||||
if (this._lastPendingKey === key) {
|
||||
this._lastPendingKey = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _cleanupAllPending(): void {
|
||||
for (const pending of this._pendingTransmissions.values()) {
|
||||
pending.decoder.release();
|
||||
}
|
||||
this._pendingTransmissions.clear();
|
||||
this._lastPendingKey = undefined;
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
this._aborted = false;
|
||||
this._decodeError = false;
|
||||
this._inControlData = true;
|
||||
this._controlLength = 0;
|
||||
this._parsedCommand = null;
|
||||
// Pre-calculate encoded limit once: base64 is 4 bytes encoded → 3 bytes decoded
|
||||
this._encodedSizeLimit = this._maxEncodedBytes;
|
||||
this._totalEncodedSize = 0;
|
||||
this._activeDecoder = null;
|
||||
}
|
||||
|
||||
public put(data: Uint32Array, start: number, end: number): void {
|
||||
if (this._aborted) return;
|
||||
|
||||
if (!this._inControlData) {
|
||||
this._streamPayload(data, start, end);
|
||||
} else {
|
||||
// Scan for semicolon
|
||||
let controlEnd = end;
|
||||
for (let i = start; i < end; i++) {
|
||||
if (data[i] === SEMICOLON) {
|
||||
this._inControlData = false;
|
||||
controlEnd = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Copy control data
|
||||
const copyLength = controlEnd - start;
|
||||
if (this._controlLength + copyLength > MAX_CONTROL_DATA_SIZE) {
|
||||
this._aborted = true;
|
||||
return;
|
||||
}
|
||||
this._controlData.set(data.subarray(start, controlEnd), this._controlLength);
|
||||
this._controlLength += copyLength;
|
||||
|
||||
if (!this._inControlData) {
|
||||
// Found semicolon - parse control data early for validation
|
||||
this._parsedCommand = parseKittyCommand(this._parseControlDataString());
|
||||
|
||||
// Early validation: i+I conflict
|
||||
if (this._parsedCommand.id !== undefined && this._parsedCommand.imageNumber !== undefined) {
|
||||
this._sendResponse(this._parsedCommand.id, 'EINVAL:cannot specify both i and I keys', this._parsedCommand.quiet ?? 0);
|
||||
this._aborted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete action doesn't need payload - skip streaming
|
||||
if (this._parsedCommand.action === KittyAction.DELETE) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Stream remaining as payload
|
||||
const payloadStart = controlEnd + 1;
|
||||
if (payloadStart < end) {
|
||||
this._streamPayload(data, payloadStart, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stream payload bytes into the base64 decoder.
|
||||
private _streamPayload(data: Uint32Array, start: number, end: number): void {
|
||||
if (this._aborted) return;
|
||||
|
||||
// Check size limit (compare encoded bytes against pre-calculated limit)
|
||||
// Include cumulative size from pending transmission for multi-chunk images.
|
||||
// Per spec, subsequent chunks may omit i=, so fall back to _lastPendingKey.
|
||||
const pendingKey = this._parsedCommand?.id ?? this._lastPendingKey ?? 0;
|
||||
const pending = this._pendingTransmissions.get(pendingKey);
|
||||
const previousEncodedSize = pending?.totalEncodedSize ?? 0;
|
||||
this._totalEncodedSize += end - start;
|
||||
const cumulativeEncodedSize = previousEncodedSize + this._totalEncodedSize;
|
||||
if (cumulativeEncodedSize > this._encodedSizeLimit) {
|
||||
const decoderToRelease = this._activeDecoder ?? pending?.decoder;
|
||||
if (decoderToRelease) {
|
||||
decoderToRelease.release();
|
||||
}
|
||||
this._activeDecoder = null;
|
||||
if (pending) {
|
||||
this._removePendingEntry(pendingKey);
|
||||
}
|
||||
this._aborted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._decodeError) return;
|
||||
|
||||
if (pending?.decoder && !this._activeDecoder) {
|
||||
this._activeDecoder = pending.decoder;
|
||||
}
|
||||
if (!this._activeDecoder) {
|
||||
this._activeDecoder = new Base64Decoder(DECODER_KEEP_DATA, this._maxEncodedBytes, this._initialEncodedBytes);
|
||||
this._activeDecoder.init();
|
||||
}
|
||||
|
||||
if (this._activeDecoder.put(data.subarray(start, end)) !== DECODER_OK) {
|
||||
this._activeDecoder.release();
|
||||
this._activeDecoder = null;
|
||||
this._decodeError = true;
|
||||
if (pending) {
|
||||
this._removePendingEntry(pendingKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public end(success: boolean): boolean | Promise<boolean> {
|
||||
if (this._aborted || !success) {
|
||||
if (this._activeDecoder) {
|
||||
this._activeDecoder.release();
|
||||
this._activeDecoder = null;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// No semicolon = no payload (delete, capability query)
|
||||
if (this._inControlData) {
|
||||
return this._handleNoPayloadCommand();
|
||||
}
|
||||
|
||||
// Use command parsed early in put() - i+I already validated there
|
||||
const cmd = this._parsedCommand!;
|
||||
|
||||
// Delete action was handled by skipping payload - just execute
|
||||
if (cmd.action === KittyAction.DELETE) {
|
||||
return this._handleDelete(cmd);
|
||||
}
|
||||
|
||||
// Per spec, subsequent chunks may omit i=, so fall back to _lastPendingKey.
|
||||
const pendingKey = cmd.id ?? this._lastPendingKey ?? 0;
|
||||
const isMoreComing = cmd.more === 1;
|
||||
const pending = this._pendingTransmissions.get(pendingKey);
|
||||
|
||||
if (isMoreComing) {
|
||||
if (this._activeDecoder) {
|
||||
if (pending) {
|
||||
pending.totalEncodedSize += this._totalEncodedSize;
|
||||
pending.decodeError = pending.decodeError || this._decodeError;
|
||||
} else {
|
||||
this._pendingTransmissions.set(pendingKey, {
|
||||
cmd: { ...cmd },
|
||||
decoder: this._activeDecoder,
|
||||
totalEncodedSize: this._totalEncodedSize,
|
||||
decodeError: this._decodeError
|
||||
});
|
||||
}
|
||||
this._lastPendingKey = pendingKey;
|
||||
this._activeDecoder = null;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Final chunk received — clear the last pending key
|
||||
if (pending) {
|
||||
this._lastPendingKey = undefined;
|
||||
}
|
||||
|
||||
let decodeError = this._decodeError;
|
||||
let finalCmd = cmd;
|
||||
let decoder = this._activeDecoder;
|
||||
|
||||
if (pending) {
|
||||
finalCmd = pending.cmd;
|
||||
decoder = pending.decoder;
|
||||
decodeError = decodeError || pending.decodeError;
|
||||
this._pendingTransmissions.delete(pendingKey);
|
||||
}
|
||||
|
||||
let imageBytes = new Uint8Array(0);
|
||||
if (decoder) {
|
||||
if (decoder.end() !== DECODER_OK) {
|
||||
decodeError = true;
|
||||
}
|
||||
imageBytes = decoder.data8;
|
||||
}
|
||||
this._activeDecoder = null;
|
||||
|
||||
// Handle command first — handlers create Blob/ImageData from imageBytes,
|
||||
// which copies the data. Only then is it safe to release the decoder's
|
||||
// wasm memory that imageBytes points into.
|
||||
const result = this._handleCommandWithBytesAndCmd(finalCmd, imageBytes, decodeError);
|
||||
if (decoder) {
|
||||
decoder.release();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Command handling
|
||||
|
||||
private _parseControlDataString(): string {
|
||||
let str = '';
|
||||
for (let i = 0; i < this._controlLength; i++) {
|
||||
str += String.fromCodePoint(this._controlData[i]);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
private _handleNoPayloadCommand(): boolean | Promise<boolean> {
|
||||
const cmd = parseKittyCommand(this._parseControlDataString());
|
||||
|
||||
// Per spec: specifying both i and I is an error
|
||||
if (cmd.id !== undefined && cmd.imageNumber !== undefined) {
|
||||
this._sendResponse(cmd.id, 'EINVAL:cannot specify both i and I keys', cmd.quiet ?? 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
const action = cmd.action ?? 't';
|
||||
|
||||
switch (action) {
|
||||
case KittyAction.DELETE:
|
||||
return this._handleDelete(cmd);
|
||||
case KittyAction.QUERY:
|
||||
this._sendResponse(cmd.id ?? 0, 'OK', cmd.quiet ?? 0);
|
||||
return true;
|
||||
case KittyAction.PLACEMENT:
|
||||
return this._handlePlacement(cmd);
|
||||
default:
|
||||
// TODO: Implement remaining actions when needed:
|
||||
// - a=f (frame): animation frame operations
|
||||
// - a=a (animation): animation control
|
||||
// - a=c (compose): compose images
|
||||
if (cmd.id !== undefined) {
|
||||
this._sendResponse(cmd.id, 'EINVAL:unsupported action', cmd.quiet ?? 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleCommandWithBytesAndCmd(cmd: IKittyCommand, bytes: Uint8Array, decodeError: boolean): boolean | Promise<boolean> {
|
||||
const action = cmd.action ?? 't';
|
||||
|
||||
switch (action) {
|
||||
case KittyAction.TRANSMIT: {
|
||||
const result = this._handleTransmit(cmd, bytes, decodeError);
|
||||
// Only send response when _handleTransmit didn't already respond
|
||||
// (it handles unsupported transmission medium responses internally)
|
||||
if ((cmd.transmission ?? 'd') === 'd' && cmd.id !== undefined) {
|
||||
if (decodeError) {
|
||||
this._sendResponse(cmd.id, 'EINVAL:invalid base64 data', cmd.quiet ?? 0);
|
||||
} else if (bytes.length > 0) {
|
||||
this._sendResponse(cmd.id, 'OK', cmd.quiet ?? 0);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
case KittyAction.TRANSMIT_DISPLAY:
|
||||
return this._handleTransmitDisplay(cmd, bytes, decodeError);
|
||||
case KittyAction.QUERY:
|
||||
return this._handleQuery(cmd, bytes, decodeError);
|
||||
case KittyAction.PLACEMENT:
|
||||
// a=p ignores any payload — image data was already transmitted
|
||||
return this._handlePlacement(cmd);
|
||||
default:
|
||||
// TODO: Implement remaining actions when needed:
|
||||
// - a=f (frame): animation frame operations
|
||||
// - a=a (animation): animation control
|
||||
// - a=c (compose): compose images
|
||||
if (cmd.id !== undefined) {
|
||||
this._sendResponse(cmd.id, 'EINVAL:unsupported action', cmd.quiet ?? 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private _handlePlacement(cmd: IKittyCommand): boolean | Promise<boolean> {
|
||||
if (cmd.id === undefined) {
|
||||
return true;
|
||||
}
|
||||
const id = cmd.id;
|
||||
const image = this._kittyStorage.getImage(id);
|
||||
if (!image) {
|
||||
this._sendResponse(id, 'ENOENT:image not found', cmd.quiet ?? 0, cmd.placementId);
|
||||
return true;
|
||||
}
|
||||
const result = this._displayImage(image, cmd);
|
||||
return result.then(success => {
|
||||
this._sendResponse(id, success ? 'OK' : 'EINVAL:image rendering failed', cmd.quiet ?? 0, cmd.placementId);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private _handleTransmit(cmd: IKittyCommand, bytes: Uint8Array, decodeError: boolean): boolean {
|
||||
// TODO: Support file-based transmission modes (t=f, t=t, t=s)
|
||||
// Currently only supports direct transmission (t=d, the default).
|
||||
// - t=f (file): Payload is base64-encoded file path. Terminal reads image from that path.
|
||||
// - t=t (temp file): Payload is base64-encoded path in temp directory. Terminal reads, deletes.
|
||||
// - t=s: Payload is base64-encoded POSIX shm name. Terminal reads from shared memory.
|
||||
// These modes require filesystem/IPC access not available in browsers. For Node.js/Electron:
|
||||
// 1. Check cmd.transmission (t key) before treating bytes as image data
|
||||
// 2. For t=f/t/s: decode bytes as UTF-8 string (the path/name), then read file contents
|
||||
// 3. For t=d: treat bytes as image data (current behavior)
|
||||
// When implementing, also update _handleQuery to accept these transmission mediums.
|
||||
const transmission = cmd.transmission ?? 'd';
|
||||
if (transmission !== 'd') {
|
||||
if (cmd.id !== undefined) {
|
||||
this._sendResponse(cmd.id, 'EINVAL:unsupported transmission medium', cmd.quiet ?? 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (decodeError || bytes.length === 0) return true;
|
||||
|
||||
this._kittyStorage.storeImage(cmd.id, {
|
||||
data: new Blob([bytes as BlobPart]),
|
||||
width: cmd.width ?? 0,
|
||||
height: cmd.height ?? 0,
|
||||
format: (cmd.format ?? KittyFormat.RGBA) as 24 | 32 | 100,
|
||||
compression: cmd.compression ?? ''
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private _handleTransmitDisplay(cmd: IKittyCommand, bytes: Uint8Array, decodeError: boolean): boolean | Promise<boolean> {
|
||||
if (decodeError) {
|
||||
if (cmd.id !== undefined) {
|
||||
this._sendResponse(cmd.id, 'EINVAL:invalid base64 data', cmd.quiet ?? 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
this._handleTransmit(cmd, bytes, decodeError);
|
||||
|
||||
const id = cmd.id ?? this._kittyStorage.lastImageId;
|
||||
const image = this._kittyStorage.getImage(id);
|
||||
if (image) {
|
||||
const result = this._displayImage(image, cmd);
|
||||
if (cmd.id !== undefined) {
|
||||
return result.then(success => {
|
||||
this._sendResponse(id, success ? 'OK' : 'EINVAL:image rendering failed', cmd.quiet ?? 0);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
return result.then(() => true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _handleQuery(cmd: IKittyCommand, bytes: Uint8Array, decodeError: boolean): boolean {
|
||||
const id = cmd.id ?? 0;
|
||||
const quiet = cmd.quiet ?? 0;
|
||||
|
||||
// Per spec: reject unsupported transmission mediums (only t=d is supported atm)
|
||||
// TODO: When filesystem support is added (Node.js/Electron), update this to accept
|
||||
// t=f (file), t=t (temp file), and t=s (shared memory) and respond OK for queries.
|
||||
const transmission = cmd.transmission ?? 'd';
|
||||
if (transmission !== 'd') {
|
||||
this._sendResponse(id, 'EINVAL:unsupported transmission medium', quiet);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check decode error first (invalid base64)
|
||||
if (decodeError) {
|
||||
this._sendResponse(id, 'EINVAL:invalid base64 data', quiet);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Capability query (no payload) - just respond OK
|
||||
if (bytes.length === 0) {
|
||||
this._sendResponse(id, 'OK', quiet);
|
||||
return true;
|
||||
}
|
||||
|
||||
const format = cmd.format ?? KittyFormat.RGBA;
|
||||
|
||||
if (format === KittyFormat.PNG) {
|
||||
this._sendResponse(id, 'OK', quiet);
|
||||
} else {
|
||||
const width = cmd.width ?? 0;
|
||||
const height = cmd.height ?? 0;
|
||||
|
||||
if (!width || !height) {
|
||||
this._sendResponse(id, 'EINVAL:width and height required for raw pixel data', quiet);
|
||||
return true;
|
||||
}
|
||||
|
||||
const bytesPerPixel = format === KittyFormat.RGBA ? BYTES_PER_PIXEL_RGBA : BYTES_PER_PIXEL_RGB;
|
||||
const expectedBytes = width * height * bytesPerPixel;
|
||||
|
||||
if (bytes.length < expectedBytes) {
|
||||
this._sendResponse(id, `EINVAL:insufficient pixel data`, quiet);
|
||||
return true;
|
||||
}
|
||||
|
||||
this._sendResponse(id, 'OK', quiet);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _handleDelete(cmd: IKittyCommand): boolean {
|
||||
// Per spec: default delete selector is 'a' (delete all visible placements)
|
||||
const selector = cmd.deleteSelector ?? 'a';
|
||||
|
||||
// TODO: Distinguish lowercase (delete placements only) from uppercase
|
||||
// (delete placements + free stored image data). Currently both variants
|
||||
// free everything since we don't separate stored data from placements.
|
||||
switch (selector) {
|
||||
case 'a':
|
||||
case 'A':
|
||||
this._cleanupAllPending();
|
||||
this._kittyStorage.deleteAll();
|
||||
break;
|
||||
case 'i':
|
||||
case 'I':
|
||||
// TODO: When placement id tracking is implemented (see TODO in
|
||||
// KittyImageStorage), d=i with p=<pid> should delete only that
|
||||
// specific placement, while d=i without p should delete all
|
||||
// placements for the image.
|
||||
if (cmd.id !== undefined) {
|
||||
const pending = this._pendingTransmissions.get(cmd.id);
|
||||
if (pending) {
|
||||
pending.decoder.release();
|
||||
}
|
||||
this._removePendingEntry(cmd.id);
|
||||
this._kittyStorage.deleteById(cmd.id);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Unsupported selectors (c, n, p, q, r, x, y, z, f) — ignore for now
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
const pPart = placementId ? `,p=${placementId}` : '';
|
||||
const response = `\x1b_Gi=${id}${pPart};${message}\x1b\\`;
|
||||
this._coreTerminal._core.coreService.triggerDataEvent(response);
|
||||
}
|
||||
|
||||
// Image display
|
||||
|
||||
private _displayImage(image: IKittyImageData, cmd: IKittyCommand): Promise<boolean> {
|
||||
return this._decodeAndDisplay(image, cmd)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
}
|
||||
|
||||
private async _decodeAndDisplay(image: IKittyImageData, cmd: IKittyCommand): Promise<void> {
|
||||
let bitmap: ImageBitmap | undefined = await this._createBitmap(image);
|
||||
|
||||
try {
|
||||
const cropX = Math.max(0, cmd.x ?? 0);
|
||||
const cropY = Math.max(0, cmd.y ?? 0);
|
||||
const cropW = cmd.sourceWidth || (bitmap.width - cropX);
|
||||
const cropH = cmd.sourceHeight || (bitmap.height - cropY);
|
||||
|
||||
const maxCropW = Math.max(0, bitmap.width - cropX);
|
||||
const maxCropH = Math.max(0, bitmap.height - cropY);
|
||||
const finalCropW = Math.max(0, Math.min(cropW, maxCropW));
|
||||
const finalCropH = Math.max(0, Math.min(cropH, maxCropH));
|
||||
|
||||
if (finalCropW === 0 || finalCropH === 0) {
|
||||
throw new Error('invalid source rectangle');
|
||||
}
|
||||
|
||||
if (cropX !== 0 || cropY !== 0 || finalCropW !== bitmap.width || finalCropH !== bitmap.height) {
|
||||
const cropped = await createImageBitmap(bitmap, cropX, cropY, finalCropW, finalCropH);
|
||||
bitmap.close();
|
||||
bitmap = cropped;
|
||||
}
|
||||
|
||||
const cw = this._renderer.dimensions?.css.cell.width || CELL_SIZE_DEFAULT.width;
|
||||
const ch = this._renderer.dimensions?.css.cell.height || CELL_SIZE_DEFAULT.height;
|
||||
|
||||
// Per spec: c/r default to image's natural cell dimensions.
|
||||
// If only one of c/r is specified, compute the other from image aspect ratio.
|
||||
let imgCols: number;
|
||||
let imgRows: number;
|
||||
if (cmd.columns !== undefined && cmd.rows !== undefined) {
|
||||
imgCols = cmd.columns;
|
||||
imgRows = cmd.rows;
|
||||
} else if (cmd.columns !== undefined) {
|
||||
imgCols = cmd.columns;
|
||||
imgRows = Math.max(1, Math.ceil((bitmap.height / bitmap.width) * (imgCols * cw) / ch));
|
||||
} else if (cmd.rows !== undefined) {
|
||||
imgRows = cmd.rows;
|
||||
imgCols = Math.max(1, Math.ceil((bitmap.width / bitmap.height) * (imgRows * ch) / cw));
|
||||
} else {
|
||||
imgCols = Math.ceil(bitmap.width / cw);
|
||||
imgRows = Math.ceil(bitmap.height / ch);
|
||||
}
|
||||
|
||||
let w = bitmap.width;
|
||||
let h = bitmap.height;
|
||||
|
||||
// Scale bitmap to fit placement rectangle when c/r are specified
|
||||
if (cmd.columns !== undefined || cmd.rows !== undefined) {
|
||||
w = Math.round(imgCols * cw);
|
||||
h = Math.round(imgRows * ch);
|
||||
}
|
||||
|
||||
if (w * h > this._opts.pixelLimit) {
|
||||
throw new Error('image exceeds pixel limit');
|
||||
}
|
||||
|
||||
// Save cursor position before addImage modifies it
|
||||
const buffer = this._coreTerminal._core.buffer;
|
||||
const savedX = buffer.x;
|
||||
const savedY = buffer.y;
|
||||
const savedYbase = buffer.ybase;
|
||||
|
||||
// Determine layer based on z-index: negative = behind text, 0+ = on top.
|
||||
// When z<0 we always use the bottom layer even without allowTransparency —
|
||||
// the image will simply be hidden behind the opaque text background, which
|
||||
// is the correct behavior (client asked for "behind text").
|
||||
const wantsBottom = cmd.zIndex !== undefined && cmd.zIndex < 0;
|
||||
const layer: ImageLayer = wantsBottom ? 'bottom' : 'top';
|
||||
|
||||
if (w !== bitmap.width || h !== bitmap.height) {
|
||||
const scaled = await createImageBitmap(bitmap, { resizeWidth: w, resizeHeight: h });
|
||||
bitmap.close();
|
||||
bitmap = scaled;
|
||||
}
|
||||
|
||||
// Per spec: X/Y are pixel offsets within the first cell, so clamp to cell dimensions
|
||||
const xOffset = Math.min(Math.max(0, cmd.xOffset ?? 0), cw - 1);
|
||||
const yOffset = Math.min(Math.max(0, cmd.yOffset ?? 0), ch - 1);
|
||||
if (xOffset !== 0 || yOffset !== 0) {
|
||||
// Per spec: X/Y is not added to c/r area. When c/r are explicit, the
|
||||
// total placement area remains c*cw × r*ch pixels and the offset image
|
||||
// is clipped to fit. When c/r are unset, the padded canvas determines
|
||||
// the natural cell dimensions.
|
||||
const canvasW = (cmd.columns !== undefined) ? Math.round(imgCols * cw) : bitmap.width + xOffset;
|
||||
const canvasH = (cmd.rows !== undefined) ? Math.round(imgRows * ch) : bitmap.height + yOffset;
|
||||
const offsetCanvas = ImageRenderer.createCanvas(window.document, canvasW, canvasH);
|
||||
const offsetCtx = offsetCanvas.getContext('2d');
|
||||
if (!offsetCtx) {
|
||||
throw new Error('Failed to create offset canvas context');
|
||||
}
|
||||
offsetCtx.drawImage(bitmap, xOffset, yOffset);
|
||||
|
||||
const offsetBitmap = await createImageBitmap(offsetCanvas);
|
||||
offsetCanvas.width = offsetCanvas.height = 0;
|
||||
bitmap.close();
|
||||
bitmap = offsetBitmap;
|
||||
w = bitmap.width;
|
||||
h = bitmap.height;
|
||||
if (w * h > this._opts.pixelLimit) {
|
||||
throw new Error('image exceeds pixel limit');
|
||||
}
|
||||
if (cmd.columns === undefined) {
|
||||
imgCols = Math.ceil(bitmap.width / cw);
|
||||
}
|
||||
if (cmd.rows === undefined) {
|
||||
imgRows = Math.ceil(bitmap.height / ch);
|
||||
}
|
||||
}
|
||||
|
||||
const zIndex = cmd.zIndex ?? 0;
|
||||
this._kittyStorage.addImage(image.id, bitmap, true, layer, zIndex);
|
||||
bitmap = undefined; // ownership transferred to storage
|
||||
|
||||
// Kitty cursor movement
|
||||
// Per spec: cursor placed at first column after last image column,
|
||||
// on the last row of the image. C=1 means don't move cursor.
|
||||
if (cmd.cursorMovement === 1) {
|
||||
// C=1: restore cursor to position before image was placed
|
||||
const scrolled = buffer.ybase - savedYbase;
|
||||
buffer.x = savedX;
|
||||
// Can't restore cursor to scrollback?
|
||||
buffer.y = Math.max(savedY - scrolled, 0);
|
||||
} else {
|
||||
// Default (C=0): advance cursor horizontally past the image
|
||||
// addImage already positioned cursor on the last row via lineFeeds
|
||||
buffer.x = Math.min(savedX + imgCols, this._coreTerminal.cols);
|
||||
}
|
||||
} catch (e) {
|
||||
bitmap?.close();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Create ImageBitmap from already-decoded image data.
|
||||
private async _createBitmap(image: IKittyImageData): Promise<ImageBitmap> {
|
||||
let bytes: Uint8Array = new Uint8Array(await image.data.arrayBuffer());
|
||||
|
||||
if (image.compression === KittyCompression.ZLIB) {
|
||||
bytes = await this._decompressZlib(bytes);
|
||||
}
|
||||
|
||||
if (image.format === KittyFormat.PNG) {
|
||||
const blob = new Blob([bytes as BlobPart], { type: 'image/png' });
|
||||
if (!window.createImageBitmap) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const img = new Image();
|
||||
return new Promise<ImageBitmap>((resolve, reject) => {
|
||||
img.addEventListener('load', () => {
|
||||
URL.revokeObjectURL(url);
|
||||
const canvas = ImageRenderer.createCanvas(window.document, img.width, img.height);
|
||||
canvas.getContext('2d')?.drawImage(img, 0, 0);
|
||||
createImageBitmap(canvas).then(resolve).catch(reject);
|
||||
});
|
||||
img.addEventListener('error', () => {
|
||||
URL.revokeObjectURL(url);
|
||||
reject(new Error('Failed to load image'));
|
||||
});
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
return createImageBitmap(blob);
|
||||
}
|
||||
|
||||
// Raw pixel data
|
||||
const width = image.width;
|
||||
const height = image.height;
|
||||
|
||||
if (!width || !height) {
|
||||
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 expectedBytes = width * height * bytesPerPixel;
|
||||
|
||||
if (bytes.length < expectedBytes) {
|
||||
throw new Error('Insufficient pixel data');
|
||||
}
|
||||
|
||||
const pixelCount = width * height;
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
// 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 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
|
||||
|
||||
let srcOffset = 0;
|
||||
let dstOffset = 0;
|
||||
for (let i = 0; i < alignedPixels; i += 4) {
|
||||
const b0 = src32[srcOffset++];
|
||||
const b1 = src32[srcOffset++];
|
||||
const b2 = src32[srcOffset++];
|
||||
// Little-endian: pixel bytes are [R,G,B] → uint32 ABGR layout
|
||||
dst32[dstOffset++] = 0xFF000000 | b0;
|
||||
dst32[dstOffset++] = 0xFF000000 | (b0 >>> 24) | (b1 << 8);
|
||||
dst32[dstOffset++] = 0xFF000000 | (b1 >>> 16) | (b2 << 16);
|
||||
dst32[dstOffset++] = 0xFF000000 | (b2 >>> 8);
|
||||
}
|
||||
|
||||
// Handle remaining 1–3 pixels
|
||||
let srcByte = alignedPixels * BYTES_PER_PIXEL_RGB;
|
||||
let dstByte = alignedPixels * 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;
|
||||
}
|
||||
|
||||
return createImageBitmap(new ImageData(data, width, height));
|
||||
}
|
||||
|
||||
private async _decompressZlib(compressed: Uint8Array): Promise<Uint8Array> {
|
||||
try {
|
||||
return await this._decompress(compressed, 'deflate');
|
||||
} catch {
|
||||
return await this._decompress(compressed, 'deflate-raw');
|
||||
}
|
||||
}
|
||||
|
||||
private async _decompress(compressed: Uint8Array, format: 'deflate' | 'deflate-raw'): Promise<Uint8Array> {
|
||||
const ds = new DecompressionStream(format);
|
||||
const writer = ds.writable.getWriter();
|
||||
writer.write(compressed as BufferSource);
|
||||
writer.close();
|
||||
|
||||
const chunks: Uint8Array[] = [];
|
||||
const reader = ds.readable.getReader();
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
chunks.push(value);
|
||||
}
|
||||
|
||||
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
||||
const result = new Uint8Array(totalLength);
|
||||
let offset = 0;
|
||||
for (const chunk of chunks) {
|
||||
result.set(chunk, offset);
|
||||
offset += chunk.length;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public get images(): ReadonlyMap<number, IKittyImageData> {
|
||||
return this._kittyStorage.images;
|
||||
}
|
||||
|
||||
public get _kittyIdToStorageId(): ReadonlyMap<number, number> {
|
||||
return this._kittyStorage.kittyIdToStorageId;
|
||||
}
|
||||
|
||||
public get pendingTransmissions(): ReadonlyMap<number, IPendingTransmission> {
|
||||
return this._pendingTransmissions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
import { assert } from 'chai';
|
||||
import { parseKittyCommand, KittyAction, KittyFormat } from './KittyGraphicsTypes';
|
||||
|
||||
describe('KittyGraphicsTypes', () => {
|
||||
describe('parseKittyCommand', () => {
|
||||
it('should parse control data with action and format', () => {
|
||||
const cmd = parseKittyCommand('a=T,f=100');
|
||||
assert.strictEqual(cmd.action, 'T');
|
||||
assert.strictEqual(cmd.format, 100);
|
||||
});
|
||||
|
||||
it('should parse control data with all options', () => {
|
||||
const cmd = parseKittyCommand('a=t,f=32,i=5,s=10,v=20,c=3,r=2,m=1,q=2');
|
||||
assert.strictEqual(cmd.action, 't');
|
||||
assert.strictEqual(cmd.format, 32);
|
||||
assert.strictEqual(cmd.id, 5);
|
||||
assert.strictEqual(cmd.width, 10);
|
||||
assert.strictEqual(cmd.height, 20);
|
||||
assert.strictEqual(cmd.columns, 3);
|
||||
assert.strictEqual(cmd.rows, 2);
|
||||
assert.strictEqual(cmd.more, 1);
|
||||
assert.strictEqual(cmd.quiet, 2);
|
||||
});
|
||||
|
||||
it('should handle empty control data', () => {
|
||||
const cmd = parseKittyCommand('');
|
||||
assert.strictEqual(cmd.action, undefined);
|
||||
assert.strictEqual(cmd.format, undefined);
|
||||
});
|
||||
|
||||
it('should parse transmit action', () => {
|
||||
const cmd = parseKittyCommand('a=t,f=100');
|
||||
assert.strictEqual(cmd.action, KittyAction.TRANSMIT);
|
||||
assert.strictEqual(cmd.format, KittyFormat.PNG);
|
||||
});
|
||||
|
||||
it('should parse delete action', () => {
|
||||
const cmd = parseKittyCommand('a=d,i=5');
|
||||
assert.strictEqual(cmd.action, KittyAction.DELETE);
|
||||
assert.strictEqual(cmd.id, 5);
|
||||
});
|
||||
|
||||
it('should parse empty action as empty string', () => {
|
||||
const cmd = parseKittyCommand('a=,f=100');
|
||||
assert.strictEqual(cmd.action, '');
|
||||
assert.strictEqual(cmd.format, 100);
|
||||
});
|
||||
|
||||
it('should leave action undefined when key is not present', () => {
|
||||
const cmd = parseKittyCommand('f=100,i=5');
|
||||
assert.strictEqual(cmd.action, undefined);
|
||||
assert.strictEqual(cmd.format, 100);
|
||||
assert.strictEqual(cmd.id, 5);
|
||||
});
|
||||
|
||||
it('should parse compression key', () => {
|
||||
const cmd = parseKittyCommand('a=t,f=32,o=z');
|
||||
assert.strictEqual(cmd.action, 't');
|
||||
assert.strictEqual(cmd.format, 32);
|
||||
assert.strictEqual(cmd.compression, 'z');
|
||||
});
|
||||
|
||||
it('should parse cursor movement key', () => {
|
||||
const cmd = parseKittyCommand('a=T,f=100,C=1');
|
||||
assert.strictEqual(cmd.cursorMovement, 1);
|
||||
});
|
||||
|
||||
it('should parse cursor movement key C=0', () => {
|
||||
const cmd = parseKittyCommand('a=T,f=100,C=0');
|
||||
assert.strictEqual(cmd.cursorMovement, 0);
|
||||
});
|
||||
|
||||
it('should parse x and y offset', () => {
|
||||
const cmd = parseKittyCommand('a=T,x=10,y=20');
|
||||
assert.strictEqual(cmd.x, 10);
|
||||
assert.strictEqual(cmd.y, 20);
|
||||
});
|
||||
|
||||
it('should handle keys without values', () => {
|
||||
const cmd = parseKittyCommand('a=t,f=,i=5');
|
||||
assert.strictEqual(cmd.action, 't');
|
||||
assert.ok(isNaN(cmd.format!));
|
||||
assert.strictEqual(cmd.id, 5);
|
||||
});
|
||||
|
||||
it('should parse z-index key with positive value', () => {
|
||||
const cmd = parseKittyCommand('a=T,f=100,z=10');
|
||||
assert.strictEqual(cmd.zIndex, 10);
|
||||
});
|
||||
|
||||
it('should parse z-index key with zero', () => {
|
||||
const cmd = parseKittyCommand('a=T,f=100,z=0');
|
||||
assert.strictEqual(cmd.zIndex, 0);
|
||||
});
|
||||
|
||||
it('should parse z-index key with negative value', () => {
|
||||
const cmd = parseKittyCommand('a=T,f=100,z=-1');
|
||||
assert.strictEqual(cmd.zIndex, -1);
|
||||
});
|
||||
|
||||
it('should leave zIndex undefined when not specified', () => {
|
||||
const cmd = parseKittyCommand('a=T,f=100');
|
||||
assert.strictEqual(cmd.zIndex, undefined);
|
||||
});
|
||||
|
||||
it('should parse delete selector key', () => {
|
||||
const cmd = parseKittyCommand('a=d,d=i,i=5');
|
||||
assert.strictEqual(cmd.action, 'd');
|
||||
assert.strictEqual(cmd.deleteSelector, 'i');
|
||||
assert.strictEqual(cmd.id, 5);
|
||||
});
|
||||
|
||||
it('should parse uppercase delete selector', () => {
|
||||
const cmd = parseKittyCommand('a=d,d=A');
|
||||
assert.strictEqual(cmd.deleteSelector, 'A');
|
||||
});
|
||||
|
||||
it('should parse delete selector d=a (all)', () => {
|
||||
const cmd = parseKittyCommand('a=d,d=a');
|
||||
assert.strictEqual(cmd.deleteSelector, 'a');
|
||||
});
|
||||
|
||||
it('should leave deleteSelector undefined when not specified', () => {
|
||||
const cmd = parseKittyCommand('a=d,i=5');
|
||||
assert.strictEqual(cmd.deleteSelector, undefined);
|
||||
});
|
||||
|
||||
it('should parse placement id key', () => {
|
||||
const cmd = parseKittyCommand('a=d,d=i,i=5,p=3');
|
||||
assert.strictEqual(cmd.placementId, 3);
|
||||
assert.strictEqual(cmd.deleteSelector, 'i');
|
||||
assert.strictEqual(cmd.id, 5);
|
||||
});
|
||||
|
||||
it('should leave placementId undefined when not specified', () => {
|
||||
const cmd = parseKittyCommand('a=d,d=i,i=5');
|
||||
assert.strictEqual(cmd.placementId, undefined);
|
||||
});
|
||||
|
||||
it('should parse image number key', () => {
|
||||
const cmd = parseKittyCommand('a=t,f=100,I=42');
|
||||
assert.strictEqual(cmd.imageNumber, 42);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*
|
||||
* Kitty graphics protocol types, constants, and parsing utilities.
|
||||
*/
|
||||
|
||||
import type Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
|
||||
|
||||
// Kitty graphics protocol action types.
|
||||
// See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference under key 'a'.
|
||||
export const enum KittyAction {
|
||||
TRANSMIT = 't',
|
||||
TRANSMIT_DISPLAY = 'T',
|
||||
QUERY = 'q',
|
||||
PLACEMENT = 'p',
|
||||
DELETE = 'd'
|
||||
}
|
||||
|
||||
// Kitty graphics protocol format types.
|
||||
// See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference
|
||||
export const enum KittyFormat {
|
||||
RGB = 24,
|
||||
RGBA = 32,
|
||||
PNG = 100
|
||||
}
|
||||
|
||||
// Kitty graphics protocol compression types.
|
||||
// See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference under key 'o'.
|
||||
export const enum KittyCompression {
|
||||
NONE = '',
|
||||
ZLIB = 'z'
|
||||
}
|
||||
|
||||
// Kitty graphics protocol control data keys.
|
||||
// See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference
|
||||
export const enum KittyKey {
|
||||
// Action to perform (t=transmit, T=transmit+display, q=query, p=placement, d=delete)
|
||||
ACTION = 'a',
|
||||
// Image format (24=RGB, 32=RGBA, 100=PNG)
|
||||
FORMAT = 'f',
|
||||
// Image ID for referencing stored images
|
||||
ID = 'i',
|
||||
// Image number (alternative to ID, terminal assigns ID)
|
||||
IMAGE_NUMBER = 'I',
|
||||
// Source image width in pixels
|
||||
WIDTH = 's',
|
||||
// Source image height in pixels
|
||||
HEIGHT = 'v',
|
||||
// The left edge (in pixels) of the image area to display
|
||||
X_OFFSET = 'x',
|
||||
// The top edge (in pixels) of the image area to display
|
||||
Y_OFFSET = 'y',
|
||||
// Width (in pixels) of the source rectangle to display
|
||||
SOURCE_WIDTH = 'w',
|
||||
// Height (in pixels) of the source rectangle to display
|
||||
SOURCE_HEIGHT = 'h',
|
||||
// Horizontal offset (in pixels) within the first cell
|
||||
X_PLACEMENT_OFFSET = 'X',
|
||||
// Vertical offset (in pixels) within the first cell
|
||||
Y_PLACEMENT_OFFSET = 'Y',
|
||||
// Number of terminal columns to display the image over
|
||||
COLUMNS = 'c',
|
||||
// Number of terminal rows to display the image over
|
||||
ROWS = 'r',
|
||||
// More data flag (1=more chunks coming, 0=final chunk)
|
||||
MORE = 'm',
|
||||
// Compression type (z=zlib). This is essential for chunking larger images.
|
||||
COMPRESSION = 'o',
|
||||
// Quiet mode (1=suppress OK responses, 2=suppress error responses)
|
||||
QUIET = 'q',
|
||||
// Cursor movement policy (0=move cursor after image, 1=don't move cursor)
|
||||
CURSOR_MOVEMENT = 'C',
|
||||
// Z-index for image layering (negative = behind text, 0+ = on top)
|
||||
Z_INDEX = 'z',
|
||||
// Transmission medium (d=direct, f=file, t=temp file, s=shared memory)
|
||||
TRANSMISSION = 't',
|
||||
// Delete selector (a/A=all, i/I=by id, c/C=at cursor, etc.) — only used when a=d
|
||||
DELETE_SELECTOR = 'd',
|
||||
// Placement ID for targeting specific placements
|
||||
PLACEMENT_ID = 'p'
|
||||
}
|
||||
|
||||
// Pixel format constants
|
||||
export const BYTES_PER_PIXEL_RGB = 3;
|
||||
export const BYTES_PER_PIXEL_RGBA = 4;
|
||||
export const ALPHA_OPAQUE = 255;
|
||||
|
||||
// Parsed Kitty graphics command.
|
||||
export interface IKittyCommand {
|
||||
action?: string;
|
||||
format?: number;
|
||||
id?: number;
|
||||
imageNumber?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
x?: number;
|
||||
y?: number;
|
||||
sourceWidth?: number;
|
||||
sourceHeight?: number;
|
||||
xOffset?: number;
|
||||
yOffset?: number;
|
||||
columns?: number;
|
||||
rows?: number;
|
||||
more?: number;
|
||||
quiet?: number;
|
||||
cursorMovement?: number;
|
||||
zIndex?: number;
|
||||
transmission?: string;
|
||||
deleteSelector?: string;
|
||||
placementId?: number;
|
||||
compression?: string;
|
||||
payload?: string;
|
||||
}
|
||||
|
||||
// Pending chunked transmission state.
|
||||
// Stores metadata from the first chunk while accumulating decoded payload data.
|
||||
export interface IPendingTransmission {
|
||||
// The parsed command from the first chunk (contains action, format, dimensions, etc.)
|
||||
cmd: IKittyCommand;
|
||||
// Decoder used across chunked payloads
|
||||
decoder: Base64Decoder;
|
||||
// Total encoded (base64) bytes received across all chunks - for size limit enforcement
|
||||
totalEncodedSize: number;
|
||||
// Whether any chunk has failed to decode
|
||||
decodeError: boolean;
|
||||
}
|
||||
|
||||
// Stored Kitty image data.
|
||||
export interface IKittyImageData {
|
||||
id: number;
|
||||
// Decoded image data stored as Blob (off JS heap) to avoid 2GB heap limit
|
||||
data: Blob;
|
||||
width: number;
|
||||
height: number;
|
||||
format: 24 | 32 | 100;
|
||||
compression?: string;
|
||||
}
|
||||
|
||||
// Parses Kitty graphics control data into a command object.
|
||||
export function parseKittyCommand(data: string): IKittyCommand {
|
||||
const cmd: IKittyCommand = {};
|
||||
const parts = data.split(',');
|
||||
|
||||
for (const part of parts) {
|
||||
const eqIdx = part.indexOf('=');
|
||||
if (eqIdx === -1) continue;
|
||||
|
||||
const key = part.substring(0, eqIdx);
|
||||
const value = part.substring(eqIdx + 1);
|
||||
|
||||
// Handle string keys first
|
||||
if (key === KittyKey.ACTION) {
|
||||
cmd.action = value;
|
||||
continue;
|
||||
}
|
||||
if (key === KittyKey.COMPRESSION) {
|
||||
cmd.compression = value;
|
||||
continue;
|
||||
}
|
||||
if (key === KittyKey.TRANSMISSION) {
|
||||
cmd.transmission = value;
|
||||
continue;
|
||||
}
|
||||
if (key === KittyKey.DELETE_SELECTOR) {
|
||||
cmd.deleteSelector = value;
|
||||
continue;
|
||||
}
|
||||
const numValue = parseInt(value);
|
||||
switch (key) {
|
||||
case KittyKey.FORMAT: cmd.format = numValue; break;
|
||||
case KittyKey.ID: cmd.id = numValue; break;
|
||||
case KittyKey.IMAGE_NUMBER: cmd.imageNumber = numValue; break;
|
||||
case KittyKey.WIDTH: cmd.width = numValue; break;
|
||||
case KittyKey.HEIGHT: cmd.height = numValue; break;
|
||||
case KittyKey.X_OFFSET: cmd.x = numValue; break;
|
||||
case KittyKey.Y_OFFSET: cmd.y = numValue; break;
|
||||
case KittyKey.SOURCE_WIDTH: cmd.sourceWidth = numValue; break;
|
||||
case KittyKey.SOURCE_HEIGHT: cmd.sourceHeight = numValue; break;
|
||||
case KittyKey.X_PLACEMENT_OFFSET: cmd.xOffset = numValue; break;
|
||||
case KittyKey.Y_PLACEMENT_OFFSET: cmd.yOffset = numValue; break;
|
||||
case KittyKey.COLUMNS: cmd.columns = numValue; break;
|
||||
case KittyKey.ROWS: cmd.rows = numValue; break;
|
||||
case KittyKey.MORE: cmd.more = numValue; break;
|
||||
case KittyKey.QUIET: cmd.quiet = numValue; break;
|
||||
case KittyKey.CURSOR_MOVEMENT: cmd.cursorMovement = numValue; break;
|
||||
case KittyKey.Z_INDEX: cmd.zIndex = numValue; break;
|
||||
case KittyKey.PLACEMENT_ID: cmd.placementId = numValue; break;
|
||||
}
|
||||
}
|
||||
|
||||
return cmd;
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDisposable } from '@xterm/xterm';
|
||||
import { ImageStorage } from '../ImageStorage';
|
||||
import { ImageLayer } from '../Types';
|
||||
import { IKittyImageData } from './KittyGraphicsTypes';
|
||||
|
||||
// Kitty-specific image storage controller.
|
||||
//
|
||||
// Wraps shared ImageStorage with kitty protocol semantics:
|
||||
// - tracks transmitted image payloads by kitty image id
|
||||
// - tracks kitty image id -> shared ImageStorage id mapping for displayed images
|
||||
// - mirrors shared-storage evictions into kitty maps
|
||||
// - applies protocol-level undisplayed-image eviction policy
|
||||
export class KittyImageStorage implements IDisposable {
|
||||
private static readonly _maxStoredImages = 256;
|
||||
|
||||
private _nextImageId = 1;
|
||||
private readonly _images: Map<number, IKittyImageData> = new Map();
|
||||
// TODO: Support multiple placements per image. The kitty spec identifies
|
||||
// placements by an (image id, placement id) pair — same i + different p
|
||||
// values should coexist, and same i + same p should replace the prior
|
||||
// placement. Currently we track only one storage entry per kitty image id,
|
||||
// so multiple placements of the same image overwrite each other. Fixing
|
||||
// this requires changing these maps to Map<number, Map<number, number>>
|
||||
// (kittyId → placementId → storageId) and updating addImage/deleteById
|
||||
// accordingly. The underlying shared ImageStorage would also need to
|
||||
// support multiple entries per logical image.
|
||||
private readonly _kittyIdToStorageId: Map<number, number> = new Map();
|
||||
private readonly _storageIdToKittyId: Map<number, number> = new Map();
|
||||
|
||||
private readonly _previousOnImageDeleted: ((storageId: number) => void) | undefined;
|
||||
private readonly _wrappedOnImageDeleted: (storageId: number) => void;
|
||||
private readonly _handleStorageImageDeleted = (storageId: number): void => {
|
||||
const kittyId = this._storageIdToKittyId.get(storageId);
|
||||
if (kittyId !== undefined) {
|
||||
this._kittyIdToStorageId.delete(kittyId);
|
||||
this._storageIdToKittyId.delete(storageId);
|
||||
this._images.delete(kittyId);
|
||||
}
|
||||
};
|
||||
|
||||
constructor(
|
||||
private readonly _storage: ImageStorage
|
||||
) {
|
||||
this._previousOnImageDeleted = this._storage.onImageDeleted;
|
||||
this._wrappedOnImageDeleted = (storageId: number) => {
|
||||
this._previousOnImageDeleted?.(storageId);
|
||||
this._handleStorageImageDeleted(storageId);
|
||||
};
|
||||
this._storage.onImageDeleted = this._wrappedOnImageDeleted;
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this._nextImageId = 1;
|
||||
this._images.clear();
|
||||
this._kittyIdToStorageId.clear();
|
||||
this._storageIdToKittyId.clear();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.reset();
|
||||
if (this._storage.onImageDeleted === this._wrappedOnImageDeleted) {
|
||||
this._storage.onImageDeleted = this._previousOnImageDeleted;
|
||||
}
|
||||
}
|
||||
|
||||
public storeImage(id: number | undefined, imageData: Omit<IKittyImageData, 'id'>): number {
|
||||
const imageId = id ?? this._nextImageId++;
|
||||
|
||||
const oldStorageId = this._kittyIdToStorageId.get(imageId);
|
||||
if (oldStorageId !== undefined) {
|
||||
this._storage.deleteImage(oldStorageId);
|
||||
this._kittyIdToStorageId.delete(imageId);
|
||||
this._storageIdToKittyId.delete(oldStorageId);
|
||||
}
|
||||
|
||||
if (!this._images.has(imageId) && this._images.size >= KittyImageStorage._maxStoredImages) {
|
||||
this._evictUndisplayedImages();
|
||||
}
|
||||
|
||||
this._images.set(imageId, {
|
||||
...imageData,
|
||||
id: imageId
|
||||
});
|
||||
return imageId;
|
||||
}
|
||||
|
||||
public addImage(kittyId: number, image: HTMLCanvasElement | ImageBitmap, scrolling: boolean, layer: ImageLayer, zIndex: number): void {
|
||||
// Clean up stale reverse-mapping from a previous placement of the same
|
||||
// kitty image. The old shared-storage entry is kept (it may still be
|
||||
// visible on screen) but its reverse mapping is removed so that eviction
|
||||
// of the old entry won't incorrectly delete the kitty image data.
|
||||
const oldStorageId = this._kittyIdToStorageId.get(kittyId);
|
||||
if (oldStorageId !== undefined) {
|
||||
this._storageIdToKittyId.delete(oldStorageId);
|
||||
}
|
||||
const storageId = this._storage.addImage(image, scrolling, layer, zIndex);
|
||||
this._kittyIdToStorageId.set(kittyId, storageId);
|
||||
this._storageIdToKittyId.set(storageId, kittyId);
|
||||
}
|
||||
|
||||
public getImage(kittyId: number): IKittyImageData | undefined {
|
||||
return this._images.get(kittyId);
|
||||
}
|
||||
|
||||
public deleteById(kittyId: number): void {
|
||||
this._images.delete(kittyId);
|
||||
const storageId = this._kittyIdToStorageId.get(kittyId);
|
||||
if (storageId !== undefined) {
|
||||
this._storage.deleteImage(storageId);
|
||||
this._kittyIdToStorageId.delete(kittyId);
|
||||
this._storageIdToKittyId.delete(storageId);
|
||||
}
|
||||
}
|
||||
|
||||
public deleteAll(): void {
|
||||
this._images.clear();
|
||||
for (const storageId of this._kittyIdToStorageId.values()) {
|
||||
this._storage.deleteImage(storageId);
|
||||
}
|
||||
this._kittyIdToStorageId.clear();
|
||||
this._storageIdToKittyId.clear();
|
||||
}
|
||||
|
||||
public get images(): ReadonlyMap<number, IKittyImageData> {
|
||||
return this._images;
|
||||
}
|
||||
|
||||
public get kittyIdToStorageId(): ReadonlyMap<number, number> {
|
||||
return this._kittyIdToStorageId;
|
||||
}
|
||||
|
||||
public get lastImageId(): number {
|
||||
return this._nextImageId - 1;
|
||||
}
|
||||
|
||||
private _evictUndisplayedImages(): void {
|
||||
for (const [kittyId] of this._images) {
|
||||
if (this._images.size <= KittyImageStorage._maxStoredImages / 2) {
|
||||
break;
|
||||
}
|
||||
if (!this._kittyIdToStorageId.has(kittyId)) {
|
||||
this._images.delete(kittyId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,11 +11,11 @@
|
||||
"types": [
|
||||
"../../../node_modules/@types/mocha"
|
||||
],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"browser/*": [ "../../../src/browser/*" ],
|
||||
"common/*": [ "../../../src/common/*" ],
|
||||
"@xterm/addon-image": [ "../typings/addon-image.d.ts" ]
|
||||
"@xterm/addon-image": [ "../typings/addon-image.d.ts" ],
|
||||
"*": [ "./*" ]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
|
||||
@@ -23,6 +23,8 @@ export interface IImageAddonOptions {
|
||||
sixelSizeLimit: number;
|
||||
iipSupport: boolean;
|
||||
iipSizeLimit: number;
|
||||
kittySupport: boolean;
|
||||
kittySizeLimit: number;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
@@ -134,7 +136,9 @@ test.describe('ImageAddon', () => {
|
||||
storageLimit: 128,
|
||||
showPlaceholder: true,
|
||||
iipSupport: true,
|
||||
iipSizeLimit: 20000000
|
||||
iipSizeLimit: 20000000,
|
||||
kittySupport: true,
|
||||
kittySizeLimit: 20000000
|
||||
};
|
||||
deepStrictEqual(await ctx.page.evaluate(`window.imageAddon._opts`), DEFAULT_OPTIONS);
|
||||
});
|
||||
@@ -149,7 +153,9 @@ test.describe('ImageAddon', () => {
|
||||
storageLimit: 10,
|
||||
showPlaceholder: false,
|
||||
iipSupport: false,
|
||||
iipSizeLimit: 1000
|
||||
iipSizeLimit: 1000,
|
||||
kittySupport: false,
|
||||
kittySizeLimit: 1000
|
||||
};
|
||||
await ctx.page.evaluate(opts => {
|
||||
(window as any).imageAddonCustom = new ImageAddon(opts.opts);
|
||||
@@ -190,6 +196,18 @@ test.describe('ImageAddon', () => {
|
||||
});
|
||||
|
||||
test.describe('image lifecycle & eviction', () => {
|
||||
test('onImageAdded fires for each image', async () => {
|
||||
await ctx.page.evaluate(`
|
||||
window._imageAddedCount = 0;
|
||||
window.imageAddon.onImageAdded(() => { window._imageAddedCount++; });
|
||||
`);
|
||||
await ctx.proxy.write(SIXEL_SEQ_0);
|
||||
await pollFor(ctx.page, 'window._imageAddedCount', 1);
|
||||
await ctx.proxy.write(SIXEL_SEQ_0);
|
||||
await pollFor(ctx.page, 'window._imageAddedCount', 2);
|
||||
await ctx.proxy.write(SIXEL_SEQ_0);
|
||||
await pollFor(ctx.page, 'window._imageAddedCount', 3);
|
||||
});
|
||||
test('delete image once scrolled off', async () => {
|
||||
await ctx.proxy.write(SIXEL_SEQ_0);
|
||||
pollFor(ctx.page, 'window.imageAddon._storage._images.size', 1);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,13 +9,15 @@
|
||||
"outDir": "../out-test",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
],
|
||||
"*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"strict": true,
|
||||
|
||||
+15
-1
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal, ITerminalAddon } from '@xterm/xterm';
|
||||
import { Terminal, ITerminalAddon, IEvent } from '@xterm/xterm';
|
||||
|
||||
declare module '@xterm/addon-image' {
|
||||
export interface IImageAddonOptions {
|
||||
@@ -76,6 +76,15 @@ declare module '@xterm/addon-image' {
|
||||
iipSupport?: boolean;
|
||||
/** IIP sequence size limit (default 20000000 bytes). */
|
||||
iipSizeLimit?: number;
|
||||
|
||||
/**
|
||||
* Kitty graphics protocol settings
|
||||
*/
|
||||
|
||||
/** Whether Kitty graphics protocol is enabled (default is true). */
|
||||
kittySupport?: boolean;
|
||||
/** Kitty image size limit in bytes (default 20000000 bytes). */
|
||||
kittySizeLimit?: number;
|
||||
}
|
||||
|
||||
export class ImageAddon implements ITerminalAddon {
|
||||
@@ -107,6 +116,11 @@ declare module '@xterm/addon-image' {
|
||||
*/
|
||||
public showPlaceholder: boolean;
|
||||
|
||||
/**
|
||||
* Event fired whenever a new image is added to storage.
|
||||
*/
|
||||
public readonly onImageAdded: IEvent<void>;
|
||||
|
||||
/**
|
||||
* Get original image canvas at buffer position.
|
||||
*/
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
"node": ">8.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -p src",
|
||||
"watch": "tsc -w -p src",
|
||||
"build": "tsgo -p src",
|
||||
"watch": "tsgo -w -p src",
|
||||
"prepackage": "npm run build",
|
||||
"package": "webpack",
|
||||
"pretest": "npm run build",
|
||||
@@ -40,6 +40,6 @@
|
||||
"axios": "^1.6.0",
|
||||
"font-finder": "^1.1.0",
|
||||
"mkdirp": "0.5.5",
|
||||
"yauzl": "^2.10.0"
|
||||
"yauzl": "^3.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,12 @@
|
||||
"outDir": "../out-esbuild-test",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"baseUrl": ".",
|
||||
"strict": true,
|
||||
"paths": {
|
||||
"*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"types": [
|
||||
"../../../node_modules/@types/mocha",
|
||||
"../../../node_modules/@types/node"
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"build": "../../node_modules/.bin/tsgo -p .",
|
||||
"prepackage": "npm run build",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package",
|
||||
|
||||
@@ -9,13 +9,15 @@
|
||||
"outDir": "../out-test",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
],
|
||||
"*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"strict": true,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"prepackage": "../../node_modules/.bin/tsc -p .",
|
||||
"prepackage": "../../node_modules/.bin/tsgo -p .",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package",
|
||||
"start": "node ../../demo/start"
|
||||
|
||||
@@ -64,9 +64,6 @@ export class SearchEngine {
|
||||
throw new Error(`Invalid col: ${startCol} to search in terminal of ${this._terminal.cols} cols`);
|
||||
}
|
||||
|
||||
startRow = Math.max(0, startRow);
|
||||
startCol = Math.max(0, startCol);
|
||||
|
||||
this._lineCache.initLinesCache();
|
||||
|
||||
const searchPosition: ISearchPosition = {
|
||||
|
||||
@@ -9,13 +9,15 @@
|
||||
"outDir": "../out-test",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
],
|
||||
"*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"strict": true,
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
"dom",
|
||||
"es2021"
|
||||
],
|
||||
"rootDir": "..",
|
||||
"outDir": "../out-benchmark",
|
||||
"types": ["../../../node_modules/@types/node"],
|
||||
"moduleResolution": "node",
|
||||
"strict": false,
|
||||
"target": "es2021",
|
||||
"module": "commonjs",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": ["../../../src/common/*"],
|
||||
"browser/*": ["../../../src/browser/*"],
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"build": "../../node_modules/.bin/tsgo -p .",
|
||||
"prepackage": "npm run build",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package",
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"outDir": "../out",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
@@ -23,6 +22,7 @@
|
||||
]
|
||||
},
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"types": [
|
||||
"../../../node_modules/@types/mocha"
|
||||
]
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
"outDir": "../out-test",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "es6"],
|
||||
"rootDir": "..",
|
||||
"outDir": "../out-benchmark",
|
||||
"types": ["../../../node_modules/@types/node"],
|
||||
"moduleResolution": "node",
|
||||
"strict": false,
|
||||
"target": "es2015",
|
||||
"module": "commonjs",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": ["../../../src/common/*"],
|
||||
"browser/*": ["../../../src/browser/*"],
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"build": "../../node_modules/.bin/tsgo -p .",
|
||||
"prepackage": "npm run build",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package",
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"strict": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
"outDir": "../out-test",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"build": "../../node_modules/.bin/tsgo -p .",
|
||||
"prepackage": "npm run build",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package",
|
||||
|
||||
@@ -11,13 +11,15 @@
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"strict": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"@xterm/addon-unicode11": [
|
||||
"../typings/addon-unicode11.d.ts"
|
||||
],
|
||||
"*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"types": [
|
||||
|
||||
@@ -9,13 +9,15 @@
|
||||
"outDir": "../out-test",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
],
|
||||
"*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"strict": true,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"build": "../../node_modules/.bin/tsgo -p .",
|
||||
"prepackage": "npm run build",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package",
|
||||
|
||||
@@ -10,13 +10,15 @@
|
||||
"outDir": "../out-test",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
],
|
||||
"*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"strict": true,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"build": "../../node_modules/.bin/tsgo -p .",
|
||||
"prepackage": "npm run build",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package",
|
||||
|
||||
@@ -10,13 +10,15 @@
|
||||
"outDir": "../out-test",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
],
|
||||
"*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"strict": true,
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"build": "../../node_modules/.bin/tsgo -p .",
|
||||
"prepackage": "npm run build",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package",
|
||||
|
||||
@@ -7,6 +7,35 @@ import { CustomGlyphDefinitionType, CustomGlyphScaleType, CustomGlyphVectorType,
|
||||
|
||||
/* eslint-disable max-len */
|
||||
|
||||
const enum Shapes {
|
||||
/** │ */ TOP_TO_BOTTOM = 'M.5,0 L.5,1',
|
||||
/** ─ */ LEFT_TO_RIGHT = 'M0,.5 L1,.5',
|
||||
|
||||
/** └ */ TOP_TO_RIGHT = 'M.5,0 L.5,.5 L1,.5',
|
||||
/** ┘ */ TOP_TO_LEFT = 'M.5,0 L.5,.5 L0,.5',
|
||||
/** ┐ */ LEFT_TO_BOTTOM = 'M0,.5 L.5,.5 L.5,1',
|
||||
/** ┌ */ RIGHT_TO_BOTTOM = 'M0.5,1 L.5,.5 L1,.5',
|
||||
|
||||
/** ╵ */ MIDDLE_TO_TOP = 'M.5,.5 L.5,0',
|
||||
/** ╴ */ MIDDLE_TO_LEFT = 'M.5,.5 L0,.5',
|
||||
/** ╶ */ MIDDLE_TO_RIGHT = 'M.5,.5 L1,.5',
|
||||
/** ╷ */ MIDDLE_TO_BOTTOM = 'M.5,.5 L.5,1',
|
||||
|
||||
/** ┴ */ T_TOP = 'M0,.5 L1,.5 M.5,.5 L.5,0',
|
||||
/** ┤ */ T_LEFT = 'M.5,0 L.5,1 M.5,.5 L0,.5',
|
||||
/** ├ */ T_RIGHT = 'M.5,0 L.5,1 M.5,.5 L1,.5',
|
||||
/** ┬ */ T_BOTTOM = 'M0,.5 L1,.5 M.5,.5 L.5,1',
|
||||
|
||||
/** ┼ */ CROSS = 'M0,.5 L1,.5 M.5,0 L.5,1',
|
||||
|
||||
/** ╌ */ TWO_DASHES_HORIZONTAL = 'M.1,.5 L.4,.5 M.6,.5 L.9,.5', // .2 empty, .3 filled
|
||||
/** ┄ */ THREE_DASHES_HORIZONTAL = 'M.0667,.5 L.2667,.5 M.4,.5 L.6,.5 M.7333,.5 L.9333,.5', // .1333 empty, .2 filled
|
||||
/** ┉ */ FOUR_DASHES_HORIZONTAL = 'M.05,.5 L.2,.5 M.3,.5 L.45,.5 M.55,.5 L.7,.5 M.8,.5 L.95,.5', // .1 empty, .15 filled
|
||||
/** ╎ */ TWO_DASHES_VERTICAL = 'M.5,.1 L.5,.4 M.5,.6 L.5,.9',
|
||||
/** ┆ */ THREE_DASHES_VERTICAL = 'M.5,.0667 L.5,.2667 M.5,.4 L.5,.6 M.5,.7333 L.5,.9333',
|
||||
/** ┊ */ FOUR_DASHES_VERTICAL = 'M.5,.05 L.5,.2 M.5,.3 L.5,.45 L.5,.55 M.5,.7 L.5,.95',
|
||||
}
|
||||
|
||||
namespace GitBranchSymbolsParts {
|
||||
// Lines
|
||||
export const LINE_H: CustomGlyphDefinitionPart = Object.freeze({ type: CustomGlyphDefinitionType.PATH_FUNCTION, data: Shapes.LEFT_TO_RIGHT, strokeWidth: 1 });
|
||||
@@ -988,31 +1017,3 @@ function segmentedDigit(pattern: number): string {
|
||||
return paths.join(' ');
|
||||
}
|
||||
|
||||
const enum Shapes {
|
||||
/** │ */ TOP_TO_BOTTOM = 'M.5,0 L.5,1',
|
||||
/** ─ */ LEFT_TO_RIGHT = 'M0,.5 L1,.5',
|
||||
|
||||
/** └ */ TOP_TO_RIGHT = 'M.5,0 L.5,.5 L1,.5',
|
||||
/** ┘ */ TOP_TO_LEFT = 'M.5,0 L.5,.5 L0,.5',
|
||||
/** ┐ */ LEFT_TO_BOTTOM = 'M0,.5 L.5,.5 L.5,1',
|
||||
/** ┌ */ RIGHT_TO_BOTTOM = 'M0.5,1 L.5,.5 L1,.5',
|
||||
|
||||
/** ╵ */ MIDDLE_TO_TOP = 'M.5,.5 L.5,0',
|
||||
/** ╴ */ MIDDLE_TO_LEFT = 'M.5,.5 L0,.5',
|
||||
/** ╶ */ MIDDLE_TO_RIGHT = 'M.5,.5 L1,.5',
|
||||
/** ╷ */ MIDDLE_TO_BOTTOM = 'M.5,.5 L.5,1',
|
||||
|
||||
/** ┴ */ T_TOP = 'M0,.5 L1,.5 M.5,.5 L.5,0',
|
||||
/** ┤ */ T_LEFT = 'M.5,0 L.5,1 M.5,.5 L0,.5',
|
||||
/** ├ */ T_RIGHT = 'M.5,0 L.5,1 M.5,.5 L1,.5',
|
||||
/** ┬ */ T_BOTTOM = 'M0,.5 L1,.5 M.5,.5 L.5,1',
|
||||
|
||||
/** ┼ */ CROSS = 'M0,.5 L1,.5 M.5,0 L.5,1',
|
||||
|
||||
/** ╌ */ TWO_DASHES_HORIZONTAL = 'M.1,.5 L.4,.5 M.6,.5 L.9,.5', // .2 empty, .3 filled
|
||||
/** ┄ */ THREE_DASHES_HORIZONTAL = 'M.0667,.5 L.2667,.5 M.4,.5 L.6,.5 M.7333,.5 L.9333,.5', // .1333 empty, .2 filled
|
||||
/** ┉ */ FOUR_DASHES_HORIZONTAL = 'M.05,.5 L.2,.5 M.3,.5 L.45,.5 M.55,.5 L.7,.5 M.8,.5 L.95,.5', // .1 empty, .15 filled
|
||||
/** ╎ */ TWO_DASHES_VERTICAL = 'M.5,.1 L.5,.4 M.5,.6 L.5,.9',
|
||||
/** ┆ */ THREE_DASHES_VERTICAL = 'M.5,.0667 L.5,.2667 M.5,.4 L.5,.6 M.5,.7333 L.5,.9333',
|
||||
/** ┊ */ FOUR_DASHES_VERTICAL = 'M.5,.05 L.5,.2 M.5,.3 L.5,.45 L.5,.55 M.5,.7 L.5,.95',
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"outDir": "../out",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
@@ -20,6 +19,9 @@
|
||||
],
|
||||
"@xterm/addon-webgl": [
|
||||
"../typings/addon-webgl.d.ts"
|
||||
],
|
||||
"*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"strict": true,
|
||||
|
||||
@@ -9,13 +9,15 @@
|
||||
"outDir": "../out-test",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
],
|
||||
"*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"strict": true,
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
// @ts-check
|
||||
|
||||
import { cpSync, existsSync, lstatSync, readFileSync } from 'node:fs';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { basename, dirname, resolve, sep } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
const nodeModulesPath = resolve(repoRoot, 'node_modules');
|
||||
const npmExecutable = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
||||
|
||||
/** @typedef {{ folder: string; reason: string }} Candidate */
|
||||
|
||||
/** @param {string} message */
|
||||
function log(message) {
|
||||
console.info(`[setup-fast] ${message}`);
|
||||
}
|
||||
|
||||
/** @param {string[]} args */
|
||||
function runNpm(args) {
|
||||
log(`Running: npm ${args.join(' ')}`);
|
||||
const result = spawnSync(npmExecutable, args, {
|
||||
cwd: repoRoot,
|
||||
stdio: 'inherit'
|
||||
});
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
if ((result.status ?? 1) !== 0) {
|
||||
process.exit(result.status ?? 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Candidate[]} candidates
|
||||
* @param {string | undefined} folder
|
||||
* @param {string} reason
|
||||
*/
|
||||
function addCandidate(candidates, folder, reason) {
|
||||
if (!folder) {
|
||||
return;
|
||||
}
|
||||
if (candidates.some(candidate => candidate.folder === folder)) {
|
||||
return;
|
||||
}
|
||||
candidates.push({ folder, reason });
|
||||
log(`Candidate found (${reason}): ${folder}`);
|
||||
}
|
||||
|
||||
function detectMainSiblingFolder() {
|
||||
const currentFolderName = basename(repoRoot);
|
||||
if (!currentFolderName.startsWith('xterm.js') || currentFolderName === 'xterm.js') {
|
||||
log(`Current folder is "${currentFolderName}", skipping xterm.js sibling lookup.`);
|
||||
return undefined;
|
||||
}
|
||||
const siblingFolder = resolve(dirname(repoRoot), 'xterm.js');
|
||||
log(`Current folder is "${currentFolderName}", sibling main folder candidate: ${siblingFolder}`);
|
||||
return siblingFolder;
|
||||
}
|
||||
|
||||
function detectWorktreeMainFolder() {
|
||||
const gitPath = resolve(repoRoot, '.git');
|
||||
if (!existsSync(gitPath)) {
|
||||
log('No .git entry found at repo root.');
|
||||
return undefined;
|
||||
}
|
||||
const gitStat = lstatSync(gitPath);
|
||||
if (!gitStat.isFile()) {
|
||||
log('.git is not a file, this repo does not appear to be a worktree checkout.');
|
||||
return undefined;
|
||||
}
|
||||
const gitFileContent = readFileSync(gitPath, 'utf8').trim();
|
||||
const gitDirMatch = /^gitdir:\s*(.+)$/m.exec(gitFileContent);
|
||||
if (!gitDirMatch) {
|
||||
log('Could not parse gitdir from .git file.');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const gitDirPath = resolve(repoRoot, gitDirMatch[1].trim());
|
||||
log(`Parsed gitdir from .git file: ${gitDirPath}`);
|
||||
|
||||
const normalizedGitDirPath = gitDirPath.replace(/\\/g, '/');
|
||||
const worktreeMarker = '/.git/worktrees/';
|
||||
const markerIndex = normalizedGitDirPath.indexOf(worktreeMarker);
|
||||
if (markerIndex === -1) {
|
||||
log('gitdir path does not contain /.git/worktrees/, skipping worktree main folder lookup.');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const normalizedMainFolder = normalizedGitDirPath.slice(0, markerIndex);
|
||||
const mainFolder = sep === '/' ? normalizedMainFolder : normalizedMainFolder.split('/').join(sep);
|
||||
log(`Worktree main folder candidate: ${mainFolder}`);
|
||||
return mainFolder;
|
||||
}
|
||||
|
||||
function resolveSourceFolder() {
|
||||
/** @type {Candidate[]} */
|
||||
const candidates = [];
|
||||
addCandidate(candidates, detectMainSiblingFolder(), 'xterm.js sibling');
|
||||
addCandidate(candidates, detectWorktreeMainFolder(), 'worktree main repo');
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const candidateNodeModulesPath = resolve(candidate.folder, 'node_modules');
|
||||
if (existsSync(candidateNodeModulesPath)) {
|
||||
log(`Using candidate (${candidate.reason}) with node_modules: ${candidate.folder}`);
|
||||
return candidate.folder;
|
||||
}
|
||||
log(`Candidate skipped (${candidate.reason}), node_modules missing: ${candidateNodeModulesPath}`);
|
||||
}
|
||||
|
||||
log('No candidate folder with node_modules was found.');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!existsSync(nodeModulesPath)) {
|
||||
log(`node_modules missing: ${nodeModulesPath}`);
|
||||
const sourceFolder = resolveSourceFolder();
|
||||
if (sourceFolder) {
|
||||
const sourceNodeModulesPath = resolve(sourceFolder, 'node_modules');
|
||||
log(`Copying node_modules from ${sourceNodeModulesPath} to ${nodeModulesPath}`);
|
||||
try {
|
||||
cpSync(sourceNodeModulesPath, nodeModulesPath, { recursive: true });
|
||||
log('node_modules copy completed.');
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
log(`node_modules copy failed: ${message}`);
|
||||
log('Falling back to npm ci.');
|
||||
runNpm(['ci']);
|
||||
}
|
||||
} else {
|
||||
log('No source folder available, running npm ci.');
|
||||
runNpm(['ci']);
|
||||
}
|
||||
} else {
|
||||
log(`node_modules already exists: ${nodeModulesPath}`);
|
||||
}
|
||||
|
||||
runNpm(['run', 'setup']);
|
||||
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { spawnSync } from 'child_process';
|
||||
import { copyFileSync, mkdirSync, renameSync } from 'fs';
|
||||
import path from 'path';
|
||||
import process from 'process';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const ghosttyDir = path.join(repoRoot, 'vendor', 'ghostty');
|
||||
const wasmOutDir = path.join(repoRoot, 'vendor', 'ghostty-wasm');
|
||||
const wasmBinDir = path.join(wasmOutDir, 'bin');
|
||||
const wasmFile = path.join(wasmOutDir, 'ghostty-vt.wasm');
|
||||
const wasmBinFile = path.join(wasmBinDir, 'ghostty-vt.wasm');
|
||||
|
||||
mkdirSync(wasmOutDir, { recursive: true });
|
||||
|
||||
const result = spawnSync('zig', [
|
||||
'build',
|
||||
'lib-vt',
|
||||
'-Dtarget=wasm32-freestanding',
|
||||
'-Doptimize=ReleaseSmall',
|
||||
'-p',
|
||||
path.relative(ghosttyDir, wasmOutDir)
|
||||
], {
|
||||
cwd: ghosttyDir,
|
||||
stdio: 'inherit',
|
||||
shell: true
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
process.exit(result.status ?? 1);
|
||||
}
|
||||
|
||||
try {
|
||||
renameSync(wasmBinFile, wasmFile);
|
||||
} catch {
|
||||
copyFileSync(wasmBinFile, wasmFile);
|
||||
}
|
||||
@@ -6,9 +6,6 @@
|
||||
const cp = require('child_process');
|
||||
const path = require('path');
|
||||
|
||||
globalThis.require = require;
|
||||
globalThis.XTERM_GHOSTTY_WASM_URL = path.resolve(__dirname, '../vendor/ghostty-wasm/ghostty-vt.wasm');
|
||||
|
||||
const COVERAGE_LINES_THRESHOLD = 40;
|
||||
|
||||
// Add `out` to the NODE_PATH so absolute paths can be resolved.
|
||||
|
||||
+12
-4
@@ -328,7 +328,9 @@ function createTerminal(): Terminal {
|
||||
}
|
||||
const cols = size.cols;
|
||||
const rows = size.rows;
|
||||
const url = '/terminals/' + pid + '/size?cols=' + cols + '&rows=' + rows;
|
||||
const pixelWidth = Math.round(term!.dimensions?.css?.canvas?.width ?? 0);
|
||||
const pixelHeight = Math.round(term!.dimensions?.css?.canvas?.height ?? 0);
|
||||
const url = '/terminals/' + pid + '/size?cols=' + cols + '&rows=' + rows + '&pixelWidth=' + pixelWidth + '&pixelHeight=' + pixelHeight;
|
||||
|
||||
fetch(url, { method: 'POST' });
|
||||
});
|
||||
@@ -379,7 +381,9 @@ function createTerminal(): Terminal {
|
||||
if (useRealTerminal instanceof HTMLInputElement && !useRealTerminal.checked) {
|
||||
runFakeTerminal();
|
||||
} else {
|
||||
const res = await fetch('/terminals?cols=' + term!.cols + '&rows=' + term!.rows, { method: 'POST' });
|
||||
const pixelWidth = Math.round(term!.dimensions?.css?.canvas?.width ?? 0);
|
||||
const pixelHeight = Math.round(term!.dimensions?.css?.canvas?.height ?? 0);
|
||||
const res = await fetch('/terminals?cols=' + term!.cols + '&rows=' + term!.rows + '&pixelWidth=' + pixelWidth + '&pixelHeight=' + pixelHeight, { method: 'POST' });
|
||||
const processId = await res.text();
|
||||
pid = processId;
|
||||
socketURL += processId;
|
||||
@@ -613,8 +617,12 @@ function addDomListener(element: HTMLElement, type: string, handler: (...args: a
|
||||
}
|
||||
|
||||
function updateTerminalSize(): void {
|
||||
const showScrollbar = term!.options.scrollbar?.showScrollbar ?? true;
|
||||
const scrollBarWidth = (term!.options.scrollback === 0 || !showScrollbar)
|
||||
? 0
|
||||
: (term!.options.scrollbar?.width ?? 14);
|
||||
const width = optionsWindow.autoResize ? '100%'
|
||||
: ((term as any).dimensions.css.canvas.width + (term as any)._core.viewport.scrollBarWidth).toString() + 'px';
|
||||
: ((term as any).dimensions.css.canvas.width + scrollBarWidth).toString() + 'px';
|
||||
const height = optionsWindow.autoResize ? '100%'
|
||||
: ((term as any).dimensions.css.canvas.height).toString() + 'px';
|
||||
terminalContainer!.style.width = width;
|
||||
@@ -626,7 +634,7 @@ function updateTerminalSize(): void {
|
||||
function getBox(width: number, height: number): any {
|
||||
return {
|
||||
string: '+',
|
||||
style: 'font-size: 1px; padding: ' + Math.floor(height/2) + 'px ' + Math.floor(width/2) + 'px; line-height: ' + height + 'px;'
|
||||
style: 'font-size: 1px; padding: ' + Math.floor(height / 2) + 'px ' + Math.floor(width / 2) + 'px; line-height: ' + height + 'px;'
|
||||
};
|
||||
}
|
||||
if (source instanceof HTMLCanvasElement) {
|
||||
|
||||
@@ -51,14 +51,30 @@ export class AddonImageWindow extends BaseWindow implements IControlWindow {
|
||||
container.appendChild(document.createElement('br'));
|
||||
container.appendChild(document.createElement('br'));
|
||||
|
||||
const dl = document.createElement('dl');
|
||||
const dt = document.createElement('dt');
|
||||
dt.textContent = 'Image Test';
|
||||
dl.appendChild(dt);
|
||||
this._addDdWithButton(dl, 'image-demo1', 'snake (sixel)');
|
||||
this._addDdWithButton(dl, 'image-demo2', 'oranges (sixel)');
|
||||
this._addDdWithButton(dl, 'image-demo3', 'palette (iip)');
|
||||
container.appendChild(dl);
|
||||
// Sixel demos
|
||||
const dlSixel = document.createElement('dl');
|
||||
const dtSixel = document.createElement('dt');
|
||||
dtSixel.textContent = 'Sixel';
|
||||
dlSixel.appendChild(dtSixel);
|
||||
this._addDdWithButton(dlSixel, 'image-demo1', 'snake');
|
||||
this._addDdWithButton(dlSixel, 'image-demo2', 'oranges');
|
||||
container.appendChild(dlSixel);
|
||||
|
||||
// IIP demos
|
||||
const dlIip = document.createElement('dl');
|
||||
const dtIip = document.createElement('dt');
|
||||
dtIip.textContent = 'IIP (iTerm)';
|
||||
dlIip.appendChild(dtIip);
|
||||
this._addDdWithButton(dlIip, 'image-demo3', 'palette');
|
||||
container.appendChild(dlIip);
|
||||
|
||||
// Kitty demos
|
||||
const dlKitty = document.createElement('dl');
|
||||
const dtKitty = document.createElement('dt');
|
||||
dtKitty.textContent = 'Kitty';
|
||||
dlKitty.appendChild(dtKitty);
|
||||
this._addDdWithButton(dlKitty, 'image-demo-kitty1', 'palette');
|
||||
container.appendChild(dlKitty);
|
||||
|
||||
this._initImageAddonExposed();
|
||||
}
|
||||
@@ -125,12 +141,25 @@ export class AddonImageWindow extends BaseWindow implements IControlWindow {
|
||||
this._terminal.write(`\x1b]1337;File=inline=1;size=${data.length}:${btoa(sdata)}\x1b\\`);
|
||||
});
|
||||
|
||||
const kittyDemo = (url: string) => () => fetch(url)
|
||||
.then(resp => resp.arrayBuffer())
|
||||
.then(buffer => {
|
||||
const data = new Uint8Array(buffer);
|
||||
let sdata = '';
|
||||
for (let i = 0; i < data.length; ++i) sdata += String.fromCharCode(data[i]);
|
||||
const payload = btoa(sdata);
|
||||
this._terminal.write('\r\n');
|
||||
this._terminal.write(`\x1b_Ga=T,f=100;${payload}\x1b\\`);
|
||||
});
|
||||
|
||||
document.getElementById('image-demo1')!.addEventListener('click',
|
||||
sixelDemo('https://raw.githubusercontent.com/saitoha/libsixel/master/images/snake.six'));
|
||||
document.getElementById('image-demo2')!.addEventListener('click',
|
||||
sixelDemo('https://raw.githubusercontent.com/jerch/node-sixel/master/testfiles/test2.sixel'));
|
||||
document.getElementById('image-demo3')!.addEventListener('click',
|
||||
iipDemo('https://raw.githubusercontent.com/jerch/node-sixel/master/palette.png'));
|
||||
document.getElementById('image-demo-kitty1')!.addEventListener('click',
|
||||
kittyDemo('https://raw.githubusercontent.com/jerch/node-sixel/master/palette.png'));
|
||||
|
||||
// demo for image retrieval API
|
||||
this._terminal.element!.addEventListener('click', (ev: MouseEvent) => {
|
||||
|
||||
@@ -85,7 +85,7 @@ export class CellInspectorWindow extends BaseWindow implements IControlWindow {
|
||||
|
||||
terminal.element.addEventListener('mousemove', (e: MouseEvent) => {
|
||||
const core = (terminal as any)._core;
|
||||
const coords = core._mouseService?.getCoords(e, core.screenElement, terminal.cols, terminal.rows);
|
||||
const coords = core._mouseCoordsService?.getCoords(e, core.screenElement, terminal.cols, terminal.rows);
|
||||
if (!coords) {
|
||||
this._clearDisplay();
|
||||
return;
|
||||
|
||||
@@ -116,6 +116,7 @@ export class OptionsWindow extends BaseWindow implements IControlWindow {
|
||||
'linkHandler',
|
||||
'logger',
|
||||
'quirks',
|
||||
'scrollbar',
|
||||
'theme',
|
||||
'vtExtensions',
|
||||
'windowOptions',
|
||||
|
||||
@@ -705,13 +705,13 @@ function loadTest(term: Terminal, addons: AddonCollection): void {
|
||||
term.write('', () => {
|
||||
const time = Math.round(performance.now() - start);
|
||||
const mbs = ((byteCount / 1024) * (1 / (time / 1000))).toFixed(2);
|
||||
term.write(`\n\r\nWrote ${byteCount}kB in ${time}ms (${mbs}MB/s) using the (${rendererName} renderer)`);
|
||||
term.write(`\n\r\nWrote ${byteCount}KiB in ${time}ms (${mbs}MiB/s) using the (${rendererName} renderer)`);
|
||||
// Send ^C to get a new prompt
|
||||
(term as any)._core._onData.fire('\x03');
|
||||
});
|
||||
}
|
||||
|
||||
function loadTestLongLines(term: Terminal, addons: AddonCollection): void {
|
||||
async function loadTestLongLines(term: Terminal, addons: AddonCollection): Promise<void> {
|
||||
const rendererName = addons.webgl.instance ? 'webgl' : 'dom';
|
||||
const testData = [];
|
||||
let byteCount = 0;
|
||||
@@ -729,16 +729,23 @@ function loadTestLongLines(term: Terminal, addons: AddonCollection): void {
|
||||
testData.push(data);
|
||||
}
|
||||
const start = performance.now();
|
||||
for (let i = 0; i < 1024 * 50; i++) {
|
||||
for (let i = 0; i < 1024; i++) {
|
||||
for (const d of testData) {
|
||||
term.write(d);
|
||||
try {
|
||||
term.write(d);
|
||||
} catch {
|
||||
// Flush events when cap is hit and try again(workaround for not having flow control in
|
||||
// demo)
|
||||
await new Promise<void>(r => term.write('', () => r()));
|
||||
term.write(d);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Wait for all data to be parsed before evaluating time
|
||||
term.write('', () => {
|
||||
const time = Math.round(performance.now() - start);
|
||||
const mbs = ((byteCount / 1024) * (1 / (time / 1000))).toFixed(2);
|
||||
term.write(`\n\r\nWrote ${byteCount}kB in ${time}ms (${mbs}MB/s) using the (${rendererName} renderer)`);
|
||||
term.write(`\n\r\nWrote ${byteCount}KiB in ${time}ms (${mbs}MiB/s) using the (${rendererName} renderer)`);
|
||||
// Send ^C to get a new prompt
|
||||
(term as any)._core._onData.fire('\x03');
|
||||
});
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
"outDir": "../out-demo/client",
|
||||
"rootDir": ".",
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"@xterm/addon-attach": ["../../addons/addon-attach"],
|
||||
"@xterm/addon-clipboard": ["../../addons/addon-clipboard"],
|
||||
@@ -20,7 +20,8 @@
|
||||
"@xterm/addon-webgl": ["../../addons/addon-webgl"],
|
||||
"@xterm/addon-unicode11": ["../../addons/addon-unicode11"],
|
||||
"@xterm/addon-unicode-graphemes": ["../../addons/addon-unicode-graphemes"],
|
||||
"@xterm/addon-ligatures": ["../../addons/addon-ligatures"]
|
||||
"@xterm/addon-ligatures": ["../../addons/addon-ligatures"],
|
||||
"*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
|
||||
@@ -19,7 +19,7 @@ import type { WebFontsAddon } from '@xterm/addon-web-fonts';
|
||||
import type { WebLinksAddon } from '@xterm/addon-web-links';
|
||||
import type { WebglAddon } from '@xterm/addon-webgl';
|
||||
|
||||
export type AddonType = 'attach' | 'clipboard' | 'fit' | 'image' | 'progress' | 'search' | 'serialize' | 'unicode11' | 'unicodeGraphemes' | 'webFonts' | 'webLinks' | 'webgl' | 'ligatures';
|
||||
export type AddonType = 'attach' | 'clipboard' | 'fit' | 'image' | 'ligatures' | 'progress' | 'search' | 'serialize' | 'unicode11' | 'unicodeGraphemes' | 'webFonts' | 'webLinks' | 'webgl';
|
||||
|
||||
export interface IDemoAddon<T extends AddonType> {
|
||||
name: T;
|
||||
|
||||
+18
-4
@@ -46,7 +46,6 @@ function startServer(): void {
|
||||
});
|
||||
|
||||
app.use('/fonts', express.static(demoRoot + '/fonts'));
|
||||
app.use('/vendor/ghostty-wasm', express.static(demoRoot + '/../vendor/ghostty-wasm'));
|
||||
|
||||
app.use('/dist', express.static(demoRoot + '/dist'));
|
||||
app.use('/src', express.static(demoRoot + '/src'));
|
||||
@@ -66,6 +65,8 @@ function startServer(): void {
|
||||
}
|
||||
const cols = parseInt(req.query.cols);
|
||||
const rows = parseInt(req.query.rows);
|
||||
const pixelWidth = typeof req.query.pixelWidth === 'string' ? parseInt(req.query.pixelWidth) : 0;
|
||||
const pixelHeight = typeof req.query.pixelHeight === 'string' ? parseInt(req.query.pixelHeight) : 0;
|
||||
const isWindows = process.platform === 'win32';
|
||||
const term = pty.spawn(isWindows ? 'powershell.exe' : 'bash', [], {
|
||||
name: 'xterm-256color',
|
||||
@@ -78,7 +79,13 @@ function startServer(): void {
|
||||
useConptyDll: isWindows,
|
||||
});
|
||||
|
||||
console.log('Created terminal with PID: ' + term.pid);
|
||||
// Set pixel dimensions immediately after spawn (pty.spawn doesn't support them)
|
||||
if (pixelWidth > 0 && pixelHeight > 0) {
|
||||
term.resize(cols, rows, { width: pixelWidth, height: pixelHeight });
|
||||
console.log('Created terminal with PID: ' + term.pid + ' (' + cols + 'x' + rows + ', ' + pixelWidth + 'px x ' + pixelHeight + 'px)');
|
||||
} else {
|
||||
console.log('Created terminal with PID: ' + term.pid);
|
||||
}
|
||||
terminals[term.pid] = term;
|
||||
unsentOutput[term.pid] = '';
|
||||
temporaryDisposable[term.pid] = term.onData(function(data) {
|
||||
@@ -96,10 +103,17 @@ function startServer(): void {
|
||||
const pid = parseInt(req.params.pid);
|
||||
const cols = parseInt(req.query.cols);
|
||||
const rows = parseInt(req.query.rows);
|
||||
const pixelWidth = typeof req.query.pixelWidth === 'string' ? parseInt(req.query.pixelWidth) : 0;
|
||||
const pixelHeight = typeof req.query.pixelHeight === 'string' ? parseInt(req.query.pixelHeight) : 0;
|
||||
const term = terminals[pid];
|
||||
|
||||
term.resize(cols, rows);
|
||||
console.log('Resized terminal ' + pid + ' to ' + cols + ' cols and ' + rows + ' rows.');
|
||||
if (pixelWidth > 0 && pixelHeight > 0) {
|
||||
term.resize(cols, rows, { width: pixelWidth, height: pixelHeight });
|
||||
console.log('Resized terminal ' + pid + ' to ' + cols + ' cols, ' + rows + ' rows, ' + pixelWidth + 'px x ' + pixelHeight + 'px');
|
||||
} else {
|
||||
term.resize(cols, rows);
|
||||
console.log('Resized terminal ' + pid + ' to ' + cols + ' cols and ' + rows + ' rows.');
|
||||
}
|
||||
res.end();
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"outDir": "../out-demo/server",
|
||||
"rootDir": ".",
|
||||
"sourceMap": true,
|
||||
"esModuleInterop": true
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"./**/*",
|
||||
|
||||
+5
-5
@@ -2,17 +2,17 @@
|
||||
|
||||
⚠ This package is experimental
|
||||
|
||||
`xterm-headless` is a headless terminal that can be run in node.js. This is useful in combination with the frontend [`xterm`](https://www.npmjs.com/package/xterm) for example to keep track of a terminal's state on a remote server where the process is hosted.
|
||||
`@xterm/headless` is a headless terminal that can be run in node.js. This is useful in combination with the frontend [`xterm`](https://www.npmjs.com/package/@xterm/xterm) for example to keep track of a terminal's state on a remote server where the process is hosted.
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, you need to install the module, we ship exclusively through npm, so you need that installed and then add xterm.js as a dependency by running:
|
||||
|
||||
```sh
|
||||
npm install xterm-headless
|
||||
npm install @xterm/headless
|
||||
```
|
||||
|
||||
Then import as you would a regular node package. The recommended way to load `xterm-headless` is with TypeScript and the ES6 module syntax:
|
||||
Then import as you would a regular node package. The recommended way to load `@xterm/headless` is with TypeScript and the ES6 module syntax:
|
||||
|
||||
```javascript
|
||||
import { Terminal } from '@xterm/headless';
|
||||
@@ -20,12 +20,12 @@ import { Terminal } from '@xterm/headless';
|
||||
|
||||
## API
|
||||
|
||||
The full API for `xterm-headless` is contained within the [TypeScript declaration file](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm-headless.d.ts), use the branch/tag picker in GitHub (`w`) to navigate to the correct version of the API.
|
||||
The full API for `@xterm/headless` is contained within the [TypeScript declaration file](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm-headless.d.ts), use the branch/tag picker in GitHub (`w`) to navigate to the correct version of the API.
|
||||
|
||||
Note that some APIs are marked *experimental*, these are added to enable experimentation with new ideas without committing to support it like a normal [semver](https://semver.org/) API. Note that these APIs can change radically between versions, so be sure to read release notes if you plan on using experimental APIs.
|
||||
|
||||
### Addons
|
||||
|
||||
Addons in `xterm-headless` work the [same as in `xterm`](https://github.com/xtermjs/xterm.js/blob/master/README.md#addons) with the one caveat being that the addon needs to be packaged for node.js and not use any DOM APIs.
|
||||
Addons in `@xterm/headless` work the [same as in `xterm`](https://github.com/xtermjs/xterm.js/blob/master/README.md#addons) with the one caveat being that the addon needs to be packaged for node.js and not use any DOM APIs.
|
||||
|
||||
Currently no official addons are packaged on npm.
|
||||
|
||||
Generated
+201
-341
@@ -29,6 +29,7 @@
|
||||
"@types/ws": "^8.2.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.50.1",
|
||||
"@typescript-eslint/parser": "^8.50.1",
|
||||
"@typescript/native-preview": "^7.0.0-dev.20260213.1",
|
||||
"chai": "^4.3.4",
|
||||
"concurrently": "^9.1.2",
|
||||
"cross-env": "^7.0.3",
|
||||
@@ -79,7 +80,7 @@
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"sixel": "^0.16.0",
|
||||
"xterm-wasm-parts": "^0.1.0"
|
||||
"xterm-wasm-parts": "^0.3.0"
|
||||
}
|
||||
},
|
||||
"addons/addon-ligatures": {
|
||||
@@ -96,7 +97,7 @@
|
||||
"axios": "^1.6.0",
|
||||
"font-finder": "^1.1.0",
|
||||
"mkdirp": "0.5.5",
|
||||
"yauzl": "^2.10.0"
|
||||
"yauzl": "^3.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">8.0.0"
|
||||
@@ -1107,10 +1108,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
@@ -1185,10 +1187,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
@@ -1278,102 +1281,6 @@
|
||||
"url": "https://github.com/sponsors/nzakas"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"string-width": "^5.1.2",
|
||||
"string-width-cjs": "npm:string-width@^4.2.0",
|
||||
"strip-ansi": "^7.0.1",
|
||||
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
|
||||
"wrap-ansi": "^8.1.0",
|
||||
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui/node_modules/ansi-regex": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui/node_modules/ansi-styles": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
||||
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui/node_modules/emoji-regex": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@isaacs/cliui/node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"eastasianwidth": "^0.2.0",
|
||||
"emoji-regex": "^9.2.2",
|
||||
"strip-ansi": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui/node_modules/strip-ansi": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
||||
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.1.0",
|
||||
"string-width": "^5.0.1",
|
||||
"strip-ansi": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@istanbuljs/load-nyc-config": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
|
||||
@@ -1546,16 +1453,6 @@
|
||||
"pako": "^2.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@pkgjs/parseargs": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz",
|
||||
@@ -2076,6 +1973,123 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/native-preview": {
|
||||
"version": "7.0.0-dev.20260213.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20260213.1.tgz",
|
||||
"integrity": "sha512-TZ/0Tv954jRpn5IHQJZQF0gaNmw//ZTvkgr3nC2H8u9t4GxUhIk5vfAHVFgdl8JnNGbl7gZks37FVsEwXjdHqg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsgo": "bin/tsgo.js"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260213.1",
|
||||
"@typescript/native-preview-darwin-x64": "7.0.0-dev.20260213.1",
|
||||
"@typescript/native-preview-linux-arm": "7.0.0-dev.20260213.1",
|
||||
"@typescript/native-preview-linux-arm64": "7.0.0-dev.20260213.1",
|
||||
"@typescript/native-preview-linux-x64": "7.0.0-dev.20260213.1",
|
||||
"@typescript/native-preview-win32-arm64": "7.0.0-dev.20260213.1",
|
||||
"@typescript/native-preview-win32-x64": "7.0.0-dev.20260213.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/native-preview-darwin-arm64": {
|
||||
"version": "7.0.0-dev.20260213.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20260213.1.tgz",
|
||||
"integrity": "sha512-nqfuHgFZ8MvaHeb1XOkAGIGc6cR91dwQdlHmlbH918n63JguKzkYjzzulpr48IVn1SVewGwjtfs1moCpaDpWcg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@typescript/native-preview-darwin-x64": {
|
||||
"version": "7.0.0-dev.20260213.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20260213.1.tgz",
|
||||
"integrity": "sha512-JDVDVLPIYhYZR12omTKwzKbdw+wnPEtLnkmM+nioqbISlV29fg7IBgFx0/mf1D+Gz1ztUQVHDTqw4GEgOZ9VTg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@typescript/native-preview-linux-arm": {
|
||||
"version": "7.0.0-dev.20260213.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20260213.1.tgz",
|
||||
"integrity": "sha512-O6iVB8tTnLZJVYrChH6VJuCwqAa2ObWVdMtrKQw5UtmLXBhsOhZ3isttSuGzQrPfTiGN091IdW1KuRi8Ft1lnQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@typescript/native-preview-linux-arm64": {
|
||||
"version": "7.0.0-dev.20260213.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20260213.1.tgz",
|
||||
"integrity": "sha512-d7Gwl8W4aics0KI4H0zz3TP1ngnP8L5COND0TPBZ7ifWVrxKDNSLhF2V9OuStW43RPfA4Gzik9iIv+l0Lg+eGw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@typescript/native-preview-linux-x64": {
|
||||
"version": "7.0.0-dev.20260213.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20260213.1.tgz",
|
||||
"integrity": "sha512-Aq55fID1fE5/8K4XvfVddgw8LKUgY3kp5IOb+QEklfyle4MuAV5gtat3MnEv9XMbiidk8iKVxYrYASlUQEQzDw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@typescript/native-preview-win32-arm64": {
|
||||
"version": "7.0.0-dev.20260213.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20260213.1.tgz",
|
||||
"integrity": "sha512-VlyULgXApmGMSqAANjskGx0AcjHchf3I8rhZ47AxF/XKAkZPe/5ewfleJ97iln7dg1UL3lq2cJY2CXxU+JPcFQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@typescript/native-preview-win32-x64": {
|
||||
"version": "7.0.0-dev.20260213.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20260213.1.tgz",
|
||||
"integrity": "sha512-lEtWvZDa2oCgqJ2gbP6BRLUeTyHLyrt5BfJmVlvmVSTZzdoBpFROJdp9yprsa2Zry2o5GK8HFAxd3RlvxkOU5Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@webassemblyjs/ast": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
|
||||
@@ -2499,10 +2513,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/anymatch/node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
@@ -3511,12 +3526,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
@@ -3863,10 +3872,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
@@ -4104,15 +4114,6 @@
|
||||
"node": ">= 4.9.1"
|
||||
}
|
||||
},
|
||||
"node_modules/fd-slicer": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
|
||||
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"pend": "~1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
@@ -4243,10 +4244,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/flatted": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
|
||||
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
|
||||
"dev": true
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
|
||||
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.11",
|
||||
@@ -4552,10 +4554,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/glob/node_modules/minimatch": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
||||
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
||||
"version": "5.1.9",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
|
||||
"integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
@@ -4868,54 +4871,12 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/inwasm": {
|
||||
"version": "0.0.13",
|
||||
"resolved": "https://registry.npmjs.org/inwasm/-/inwasm-0.0.13.tgz",
|
||||
"integrity": "sha512-gmULhw1wfF3tQ19y0TvcNH6A5jN7IuTD51kbZuy+ittUU59d+ZTQMb53wbGQuciMrledgagL3/ohnjUj5qJikQ==",
|
||||
"node_modules/inwasm-runtime": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/inwasm-runtime/-/inwasm-runtime-0.1.2.tgz",
|
||||
"integrity": "sha512-in+Lk4d7PGwQnG1zxRe6qHB2nvvc5LUOsI9ByoJRuaDONTY8xDlaF+CvapGoPiqreJ0cMHxRzKtS0SJBccpYfA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"acorn": "^8.8.2",
|
||||
"acorn-walk": "^8.2.0",
|
||||
"chokidar": "^3.5.3",
|
||||
"colorette": "^2.0.20",
|
||||
"glob": "^10.0.0",
|
||||
"wabt": "^1.0.32"
|
||||
},
|
||||
"bin": {
|
||||
"inwasm": "lib/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/inwasm/node_modules/acorn-walk": {
|
||||
"version": "8.3.4",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
|
||||
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"acorn": "^8.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/inwasm/node_modules/glob": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.1.0",
|
||||
"jackspeak": "^3.1.2",
|
||||
"minimatch": "^9.0.4",
|
||||
"minipass": "^7.1.2",
|
||||
"package-json-from-dist": "^1.0.0",
|
||||
"path-scurry": "^1.11.1"
|
||||
},
|
||||
"bin": {
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
@@ -5415,21 +5376,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/jackspeak": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
|
||||
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@isaacs/cliui": "^8.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@pkgjs/parseargs": "^0.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/javascript-natural-sort": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
|
||||
@@ -5810,10 +5756,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch/node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
@@ -5855,12 +5802,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"version": "9.0.9",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
||||
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
"brace-expansion": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
@@ -5878,15 +5826,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
|
||||
@@ -5935,10 +5874,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mocha/node_modules/minimatch": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
||||
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
||||
"version": "5.1.9",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
|
||||
"integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
@@ -6150,10 +6090,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nyc/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
@@ -6424,12 +6365,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/package-json-from-dist": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
|
||||
@@ -6517,28 +6452,6 @@
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/path-scurry": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
|
||||
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lru-cache": "^10.2.0",
|
||||
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/path-scurry/node_modules/lru-cache": {
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||
@@ -6558,7 +6471,8 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
@@ -6567,10 +6481,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -6755,9 +6670,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.14.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
|
||||
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
|
||||
"version": "6.14.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
|
||||
"integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
@@ -6816,10 +6731,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp/node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
@@ -6993,10 +6909,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rimraf/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
@@ -7532,21 +7449,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs": {
|
||||
"name": "string-width",
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
@@ -7559,19 +7461,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi-cjs": {
|
||||
"name": "strip-ansi",
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-bom": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz",
|
||||
@@ -7734,10 +7623,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/test-exclude/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
@@ -8090,23 +7980,6 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/wabt": {
|
||||
"version": "1.0.39",
|
||||
"resolved": "https://registry.npmjs.org/wabt/-/wabt-1.0.39.tgz",
|
||||
"integrity": "sha512-ba+dRL/75VQQY7RkU/CgriGbkoWAfS8TDyUlJfJhJ8KhtXgMl5dhNvoPNUcQ9IWRhW8u41glMSuZeTvsYq2rRg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"wasm-decompile": "bin/wasm-decompile",
|
||||
"wasm-interp": "bin/wasm-interp",
|
||||
"wasm-objdump": "bin/wasm-objdump",
|
||||
"wasm-stats": "bin/wasm-stats",
|
||||
"wasm-strip": "bin/wasm-strip",
|
||||
"wasm-validate": "bin/wasm-validate",
|
||||
"wasm2c": "bin/wasm2c",
|
||||
"wasm2wat": "bin/wasm2wat",
|
||||
"wat2wasm": "bin/wat2wasm"
|
||||
}
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.0.tgz",
|
||||
@@ -8450,24 +8323,6 @@
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs": {
|
||||
"name": "wrap-ansi",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
@@ -8580,12 +8435,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/xterm-wasm-parts": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/xterm-wasm-parts/-/xterm-wasm-parts-0.1.0.tgz",
|
||||
"integrity": "sha512-GFE8yNJfdkytGpcsOZhkL3B8XyUqkR/Du3SdxRyFbJg+BBCKm3raaaflgIM4TwNQ2AOLz01BEEuB5FZXx8aNTQ==",
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/xterm-wasm-parts/-/xterm-wasm-parts-0.3.0.tgz",
|
||||
"integrity": "sha512-V/lhvDv2Scov2ukhTNmmur6jMq2DSL8QOdQdXWgfqcAYbUf7bgixwl2sB1gYPGE21NTw2OvbAw+vANRncWzR4w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inwasm": "^0.0.13"
|
||||
"inwasm-runtime": "^0.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
@@ -8670,13 +8526,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/yauzl": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
|
||||
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.2.1.tgz",
|
||||
"integrity": "sha512-k1isifdbpNSFEHFJ1ZY4YDewv0IH9FR61lDetaRMD3j2ae3bIXGV+7c+LHCqtQGofSd8PIyV4X6+dHMAnSr60A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-crc32": "~0.2.3",
|
||||
"fd-slicer": "~1.1.0"
|
||||
"pend": "~1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
|
||||
+5
-4
@@ -34,8 +34,8 @@
|
||||
"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\"",
|
||||
"build": "npm run tsc",
|
||||
"watch": "npm run tsc-watch",
|
||||
"tsc": "tsc -b ./tsconfig.all.json",
|
||||
"tsc-watch": "tsc -b -w ./tsconfig.all.json --preserveWatchOutput",
|
||||
"tsc": "tsgo -b ./tsconfig.all.json",
|
||||
"tsc-watch": "tsgo -b -w ./tsconfig.all.json --preserveWatchOutput",
|
||||
"esbuild": "node bin/esbuild_all.mjs",
|
||||
"esbuild-watch": "node bin/esbuild_all.mjs --watch",
|
||||
"esbuild-package": "node bin/esbuild_all.mjs --prod",
|
||||
@@ -45,7 +45,6 @@
|
||||
"esbuild-demo-client-watch": "node bin/esbuild.mjs --demo-client --watch",
|
||||
"esbuild-demo-server": "node bin/esbuild.mjs --demo-server",
|
||||
"esbuild-demo-server-watch": "node bin/esbuild.mjs --demo-server --watch",
|
||||
"ghostty-wasm": "node bin/build_ghostty_wasm.mjs",
|
||||
"test": "npm run test-unit",
|
||||
"posttest": "npm run lint",
|
||||
"lint": "eslint --max-warnings 0 src/ addons/ demo/ test/",
|
||||
@@ -73,7 +72,8 @@
|
||||
"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"
|
||||
"prepublishOnly": "npm run package",
|
||||
"agent:setup-repo": "node bin/agent/setup-repo.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lunapaint/png-codec": "^0.2.0",
|
||||
@@ -93,6 +93,7 @@
|
||||
"@types/ws": "^8.2.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.50.1",
|
||||
"@typescript-eslint/parser": "^8.50.1",
|
||||
"@typescript/native-preview": "^7.0.0-dev.20260213.1",
|
||||
"chai": "^4.3.4",
|
||||
"concurrently": "^9.1.2",
|
||||
"cross-env": "^7.0.3",
|
||||
|
||||
@@ -29,4 +29,14 @@ describe('evaluatePastedTextProcessing', () => {
|
||||
assert.equal(unbracketedText, 'foo bar');
|
||||
assert.equal(bracketedText, '\x1b[200~foo bar\x1b[201~');
|
||||
});
|
||||
|
||||
it('should escape embedded escape sequences in pasted text only when bracketed', () => {
|
||||
const ESC_SYMBOL = '\u241b';
|
||||
const pastedText = '\x1b[201~foo\x1b[200~bar';
|
||||
const unbracketedText = Clipboard.bracketTextForPaste(pastedText, false);
|
||||
const bracketedText = Clipboard.bracketTextForPaste(pastedText, true);
|
||||
|
||||
assert.equal(unbracketedText, pastedText, 'non bracketed paste should remain unchanged');
|
||||
assert.equal(bracketedText, `\x1b[200~${ESC_SYMBOL}[201~foo${ESC_SYMBOL}[200~bar\x1b[201~`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,10 +19,13 @@ export function prepareTextForTerminal(text: string): string {
|
||||
* @param text The pasted text to bracket
|
||||
*/
|
||||
export function bracketTextForPaste(text: string, bracketedPasteMode: boolean): string {
|
||||
if (bracketedPasteMode) {
|
||||
return '\x1b[200~' + text + '\x1b[201~';
|
||||
if (!bracketedPasteMode) {
|
||||
return text;
|
||||
}
|
||||
return text;
|
||||
// Sanitize pasted text to prevent injected escape sequences (e.g. exiting bracketed paste)
|
||||
// by replacing ESC (\x1b) with its visible representation U+241B (␛).
|
||||
const sanitizedText = text.replace(/\x1b/g, '\u241b');
|
||||
return `\x1b[200~${sanitizedText}\x1b[201~`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -36,16 +36,17 @@ import { CharSizeService } from 'browser/services/CharSizeService';
|
||||
import { CharacterJoinerService } from 'browser/services/CharacterJoinerService';
|
||||
import { CoreBrowserService } from 'browser/services/CoreBrowserService';
|
||||
import { LinkProviderService } from 'browser/services/LinkProviderService';
|
||||
import { MouseCoordsService } from 'browser/services/MouseCoordsService';
|
||||
import { MouseService } from 'browser/services/MouseService';
|
||||
import { RenderService } from 'browser/services/RenderService';
|
||||
import { SelectionService } from 'browser/services/SelectionService';
|
||||
import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IKeyboardService, ILinkProviderService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
|
||||
import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IKeyboardService, ILinkProviderService, IMouseCoordsService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
|
||||
import { ThemeService } from 'browser/services/ThemeService';
|
||||
import { KeyboardService } from 'browser/services/KeyboardService';
|
||||
import { channels, color, rgb } from 'common/Color';
|
||||
import { CoreTerminal } from 'common/CoreTerminal';
|
||||
import * as Browser from 'common/Platform';
|
||||
import { ColorRequestType, CoreMouseAction, CoreMouseButton, CoreMouseEventType, IColorEvent, ITerminalOptions, KeyboardResultType, SpecialColorIndex } from 'common/Types';
|
||||
import { ColorRequestType, IColorEvent, ITerminalOptions, KeyboardResultType, SpecialColorIndex } from 'common/Types';
|
||||
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
|
||||
import { IBuffer } from 'common/buffer/Types';
|
||||
import { C0, C1ESCAPED } from 'common/data/EscapeSequences';
|
||||
@@ -77,7 +78,6 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
public browser: IBrowser = Browser as any;
|
||||
|
||||
private _customKeyEventHandler: CustomKeyEventHandler | undefined;
|
||||
private _customWheelEventHandler: CustomWheelEventHandler | undefined;
|
||||
|
||||
// Browser services
|
||||
private readonly _decorationService: DecorationService;
|
||||
@@ -87,6 +87,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
// Optional browser services
|
||||
private _charSizeService: ICharSizeService | undefined;
|
||||
private _coreBrowserService: ICoreBrowserService | undefined;
|
||||
private _mouseCoordsService: IMouseCoordsService | undefined;
|
||||
private _mouseService: IMouseService | undefined;
|
||||
private _renderService: IRenderService | undefined;
|
||||
private _themeService: IThemeService | undefined;
|
||||
@@ -413,7 +414,16 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
this._register(addDisposableListener(this.textarea!, 'keyup', (ev: KeyboardEvent) => this._keyUp(ev), true));
|
||||
this._register(addDisposableListener(this.textarea!, 'keydown', (ev: KeyboardEvent) => this._keyDown(ev), true));
|
||||
this._register(addDisposableListener(this.textarea!, 'keypress', (ev: KeyboardEvent) => this._keyPress(ev), true));
|
||||
this._register(addDisposableListener(this.textarea!, 'compositionstart', () => this._compositionHelper!.compositionstart()));
|
||||
this._register(addDisposableListener(this.textarea!, 'compositionstart', () => {
|
||||
// Ensure the textarea is synced to the latest cursor location before composition begins. This
|
||||
// is to workaround a problem where highly dynamic TUIs like agentic CLIs reprint agressively
|
||||
// would cause the IME to appear in the wrong position. The theory is that when the IME is
|
||||
// triggered during a partial render the textarea position becomes locked and will not move
|
||||
// until it is hidden and a custom move occurs.
|
||||
this._syncTextArea();
|
||||
this._compositionHelper!.compositionstart();
|
||||
this._compositionHelper!.updateCompositionElements();
|
||||
}));
|
||||
this._register(addDisposableListener(this.textarea!, 'compositionupdate', (e: CompositionEvent) => this._compositionHelper!.compositionupdate(e)));
|
||||
this._register(addDisposableListener(this.textarea!, 'compositionend', () => this._compositionHelper!.compositionend()));
|
||||
this._register(addDisposableListener(this.textarea!, 'input', (ev: InputEvent) => this._inputEvent(ev), true));
|
||||
@@ -543,8 +553,8 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
this._compositionHelper = this._instantiationService.createInstance(CompositionHelper, this.textarea, this._compositionView);
|
||||
this._helperContainer.appendChild(this._compositionView);
|
||||
|
||||
this._mouseService = this._instantiationService.createInstance(MouseService);
|
||||
this._instantiationService.setService(IMouseService, this._mouseService);
|
||||
this._mouseCoordsService = this._instantiationService.createInstance(MouseCoordsService);
|
||||
this._instantiationService.setService(IMouseCoordsService, this._mouseCoordsService);
|
||||
|
||||
const linkifier = this._linkifier.value = this._register(this._instantiationService.createInstance(Linkifier, this.screenElement));
|
||||
|
||||
@@ -563,7 +573,10 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
this._renderService!.handleCursorMove();
|
||||
this._syncTextArea();
|
||||
}));
|
||||
this._register(this.onResize(() => this._renderService!.handleResize(this.cols, this.rows)));
|
||||
this._register(this.onResize(() => {
|
||||
this._renderService!.handleResize(this.cols, this.rows);
|
||||
this._syncTextArea();
|
||||
}));
|
||||
this._register(this.onBlur(() => this._renderService!.handleBlur()));
|
||||
this._register(this.onFocus(() => this._renderService!.handleFocus()));
|
||||
|
||||
@@ -579,6 +592,8 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
linkifier
|
||||
));
|
||||
this._instantiationService.setService(ISelectionService, this._selectionService);
|
||||
this._mouseService = this._instantiationService.createInstance(MouseService);
|
||||
this._instantiationService.setService(IMouseService, this._mouseService);
|
||||
this._register(this._selectionService.onRequestScrollLines(e => this.scrollLines(e.amount, e.suppressScrollEvent)));
|
||||
this._register(this._selectionService.onSelectionChange(() => this._onSelectionChange.fire()));
|
||||
this._register(this._selectionService.onRequestRedraw(e => this._renderService!.handleSelectionChanged(e.start, e.end, e.columnSelectMode)));
|
||||
@@ -602,7 +617,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
this._register(addDisposableListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService!.handleMouseDown(e)));
|
||||
|
||||
// apply mouse event classes set by escape codes before terminal was attached
|
||||
if (this.coreMouseService.areMouseEventsActive) {
|
||||
if (this.mouseStateService.areMouseEventsActive) {
|
||||
this._selectionService.disable();
|
||||
this.element.classList.add('enable-mouse-events');
|
||||
} else {
|
||||
@@ -638,285 +653,18 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
|
||||
// Listen for mouse events and translate
|
||||
// them into terminal mouse protocols.
|
||||
this.bindMouse();
|
||||
this._mouseService.bindMouse({
|
||||
element: this.element!,
|
||||
screenElement: this.screenElement!,
|
||||
document: this._document!,
|
||||
handleTouchScroll: amount => this._viewport?.handleTouchScroll(amount)
|
||||
}, disposable => this._register(disposable), () => this.focus());
|
||||
}
|
||||
|
||||
private _createRenderer(): IRenderer {
|
||||
return this._instantiationService.createInstance(DomRenderer, this, this._document!, this.element!, this.screenElement!, this._viewportElement!, this._helperContainer!, this.linkifier!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind certain mouse events to the terminal.
|
||||
* By default only 3 button + wheel up/down is ativated. For higher buttons
|
||||
* no mouse report will be created. Typically the standard actions will be active.
|
||||
*
|
||||
* There are several reasons not to enable support for higher buttons/wheel:
|
||||
* - Button 4 and 5 are typically used for history back and forward navigation,
|
||||
* there is no straight forward way to supress/intercept those standard actions.
|
||||
* - Support for higher buttons does not work in some platform/browser combinations.
|
||||
* - Left/right wheel was not tested.
|
||||
* - Emulators vary in mouse button support, typically only 3 buttons and
|
||||
* wheel up/down work reliable.
|
||||
*
|
||||
* TODO: Move mouse event code into its own file.
|
||||
*/
|
||||
public bindMouse(): void {
|
||||
const self = this;
|
||||
const el = this.element!;
|
||||
|
||||
// send event to CoreMouseService
|
||||
function sendEvent(ev: MouseEvent | WheelEvent): boolean {
|
||||
// Get mouse coordinates
|
||||
const pos = self._mouseService?.getMouseReportCoords(ev, self.screenElement!);
|
||||
if (!pos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let but: CoreMouseButton;
|
||||
let action: CoreMouseAction | undefined;
|
||||
switch ((ev as any).overrideType || ev.type) {
|
||||
case 'mousemove':
|
||||
action = CoreMouseAction.MOVE;
|
||||
if (ev.buttons === undefined) {
|
||||
// buttons is not supported on macOS, try to get a value from button instead
|
||||
but = CoreMouseButton.NONE;
|
||||
if (ev.button !== undefined) {
|
||||
but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
|
||||
}
|
||||
} else {
|
||||
// according to MDN buttons only reports up to button 5 (AUX2)
|
||||
but = ev.buttons & 1 ? CoreMouseButton.LEFT :
|
||||
ev.buttons & 4 ? CoreMouseButton.MIDDLE :
|
||||
ev.buttons & 2 ? CoreMouseButton.RIGHT :
|
||||
CoreMouseButton.NONE; // fallback to NONE
|
||||
}
|
||||
break;
|
||||
case 'mouseup':
|
||||
action = CoreMouseAction.UP;
|
||||
but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
|
||||
break;
|
||||
case 'mousedown':
|
||||
action = CoreMouseAction.DOWN;
|
||||
but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
|
||||
break;
|
||||
case 'wheel':
|
||||
if (self._customWheelEventHandler && self._customWheelEventHandler(ev as WheelEvent) === false) {
|
||||
return false;
|
||||
}
|
||||
const deltaY = (ev as WheelEvent).deltaY;
|
||||
if (deltaY === 0) {
|
||||
return false;
|
||||
}
|
||||
const lines = self.coreMouseService.consumeWheelEvent(
|
||||
ev as WheelEvent,
|
||||
self._renderService?.dimensions?.device?.cell?.height,
|
||||
self._coreBrowserService?.dpr
|
||||
);
|
||||
if (lines === 0) {
|
||||
return false;
|
||||
}
|
||||
action = deltaY < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN;
|
||||
but = CoreMouseButton.WHEEL;
|
||||
break;
|
||||
default:
|
||||
// dont handle other event types by accident
|
||||
return false;
|
||||
}
|
||||
|
||||
// exit if we cannot determine valid button/action values
|
||||
// do nothing for higher buttons than wheel
|
||||
if (action === undefined || but === undefined || but > CoreMouseButton.WHEEL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self.coreMouseService.triggerMouseEvent({
|
||||
col: pos.col,
|
||||
row: pos.row,
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
button: but,
|
||||
action,
|
||||
ctrl: ev.ctrlKey,
|
||||
alt: ev.altKey,
|
||||
shift: ev.shiftKey
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener state handling.
|
||||
* We listen to the onProtocolChange event of CoreMouseService and put
|
||||
* requested listeners in `requestedEvents`. With this the listeners
|
||||
* have all bits to do the event listener juggling.
|
||||
* Note: 'mousedown' currently is "always on" and not managed
|
||||
* by onProtocolChange.
|
||||
*/
|
||||
const requestedEvents: { [key: string]: ((ev: MouseEvent | WheelEvent) => void) | null } = {
|
||||
mouseup: null,
|
||||
wheel: null,
|
||||
mousedrag: null,
|
||||
mousemove: null
|
||||
};
|
||||
const eventListeners: { [key: string]: (ev: any) => void | boolean } = {
|
||||
mouseup: (ev: MouseEvent) => {
|
||||
sendEvent(ev);
|
||||
if (!ev.buttons) {
|
||||
// if no other button is held remove global handlers
|
||||
this._document!.removeEventListener('mouseup', requestedEvents.mouseup!);
|
||||
if (requestedEvents.mousedrag) {
|
||||
this._document!.removeEventListener('mousemove', requestedEvents.mousedrag);
|
||||
}
|
||||
}
|
||||
},
|
||||
wheel: (ev: WheelEvent) => {
|
||||
sendEvent(ev);
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
return false;
|
||||
},
|
||||
mousedrag: (ev: MouseEvent) => {
|
||||
// deal only with move while a button is held
|
||||
if (ev.buttons) {
|
||||
sendEvent(ev);
|
||||
}
|
||||
},
|
||||
mousemove: (ev: MouseEvent) => {
|
||||
// deal only with move without any button
|
||||
if (!ev.buttons) {
|
||||
sendEvent(ev);
|
||||
}
|
||||
}
|
||||
};
|
||||
this._register(this.coreMouseService.onProtocolChange(events => {
|
||||
// apply global changes on events
|
||||
if (events) {
|
||||
if (this.optionsService.rawOptions.logLevel === 'debug') {
|
||||
this._logService.debug('Binding to mouse events:', this.coreMouseService.explainEvents(events));
|
||||
}
|
||||
this.element!.classList.add('enable-mouse-events');
|
||||
this._selectionService!.disable();
|
||||
} else {
|
||||
this._logService.debug('Unbinding from mouse events.');
|
||||
this.element!.classList.remove('enable-mouse-events');
|
||||
this._selectionService!.enable();
|
||||
}
|
||||
|
||||
// add/remove handlers from requestedEvents
|
||||
|
||||
if (!(events & CoreMouseEventType.MOVE)) {
|
||||
el.removeEventListener('mousemove', requestedEvents.mousemove!);
|
||||
requestedEvents.mousemove = null;
|
||||
} else if (!requestedEvents.mousemove) {
|
||||
el.addEventListener('mousemove', eventListeners.mousemove);
|
||||
requestedEvents.mousemove = eventListeners.mousemove;
|
||||
}
|
||||
|
||||
if (!(events & CoreMouseEventType.WHEEL)) {
|
||||
el.removeEventListener('wheel', requestedEvents.wheel!);
|
||||
requestedEvents.wheel = null;
|
||||
} else if (!requestedEvents.wheel) {
|
||||
el.addEventListener('wheel', eventListeners.wheel, { passive: false });
|
||||
requestedEvents.wheel = eventListeners.wheel;
|
||||
}
|
||||
|
||||
if (!(events & CoreMouseEventType.UP)) {
|
||||
this._document!.removeEventListener('mouseup', requestedEvents.mouseup!);
|
||||
requestedEvents.mouseup = null;
|
||||
} else {
|
||||
requestedEvents.mouseup ??= eventListeners.mouseup;
|
||||
}
|
||||
|
||||
if (!(events & CoreMouseEventType.DRAG)) {
|
||||
this._document!.removeEventListener('mousemove', requestedEvents.mousedrag!);
|
||||
requestedEvents.mousedrag = null;
|
||||
} else {
|
||||
requestedEvents.mousedrag ??= eventListeners.mousedrag;
|
||||
}
|
||||
}));
|
||||
// force initial onProtocolChange so we dont miss early mouse requests
|
||||
this.coreMouseService.activeProtocol = this.coreMouseService.activeProtocol;
|
||||
|
||||
// Ensure document-level listeners are removed on dispose
|
||||
this._register(toDisposable(() => {
|
||||
if (requestedEvents.mouseup) {
|
||||
this._document!.removeEventListener('mouseup', requestedEvents.mouseup);
|
||||
}
|
||||
if (requestedEvents.mousedrag) {
|
||||
this._document!.removeEventListener('mousemove', requestedEvents.mousedrag);
|
||||
}
|
||||
}));
|
||||
|
||||
/**
|
||||
* "Always on" event listeners.
|
||||
*/
|
||||
this._register(addDisposableListener(el, 'mousedown', (ev: MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
this.focus();
|
||||
|
||||
// Don't send the mouse button to the pty if mouse events are disabled or
|
||||
// if the selection manager is having selection forced (ie. a modifier is
|
||||
// held).
|
||||
if (!this.coreMouseService.areMouseEventsActive || this._selectionService!.shouldForceSelection(ev)) {
|
||||
return;
|
||||
}
|
||||
|
||||
sendEvent(ev);
|
||||
|
||||
// Register additional global handlers which should keep reporting outside
|
||||
// of the terminal element.
|
||||
// Note: Other emulators also do this for 'mousedown' while a button
|
||||
// is held, we currently limit 'mousedown' to the terminal only.
|
||||
if (requestedEvents.mouseup) {
|
||||
this._document!.addEventListener('mouseup', requestedEvents.mouseup);
|
||||
}
|
||||
if (requestedEvents.mousedrag) {
|
||||
this._document!.addEventListener('mousemove', requestedEvents.mousedrag);
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(addDisposableListener(el, 'wheel', (ev: WheelEvent) => {
|
||||
// do nothing, if app side handles wheel itself
|
||||
if (requestedEvents.wheel) return;
|
||||
|
||||
if (this._customWheelEventHandler && this._customWheelEventHandler(ev) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.buffer.hasScrollback) {
|
||||
// Convert wheel events into up/down events when the buffer does not have scrollback, this
|
||||
// enables scrolling in apps hosted in the alt buffer such as vim or tmux even when mouse
|
||||
// events are not enabled.
|
||||
// This used implementation used get the actual lines/partial lines scrolled from the
|
||||
// viewport but since moving to the new viewport implementation has been simplified to
|
||||
// simply send a single up or down sequence.
|
||||
|
||||
// Do nothing if there's no vertical scroll
|
||||
const deltaY = (ev as WheelEvent).deltaY;
|
||||
if (deltaY === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const lines = self.coreMouseService.consumeWheelEvent(
|
||||
ev as WheelEvent,
|
||||
self._renderService?.dimensions?.device?.cell?.height,
|
||||
self._coreBrowserService?.dpr
|
||||
);
|
||||
if (lines === 0) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Construct and send sequences
|
||||
const sequence = C0.ESC + (this.coreService.decPrivateModes.applicationCursorKeys ? 'O' : '[') + (ev.deltaY < 0 ? 'A' : 'B');
|
||||
this.coreService.triggerDataEvent(sequence, true);
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
}, { passive: false }));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tells the renderer to refresh terminal content between two rows (inclusive) at the next
|
||||
* opportunity.
|
||||
@@ -990,7 +738,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
}
|
||||
|
||||
public attachCustomWheelEventHandler(customWheelEventHandler: CustomWheelEventHandler): void {
|
||||
this._customWheelEventHandler = customWheelEventHandler;
|
||||
this.mouseStateService.setCustomWheelEventHandler(customWheelEventHandler);
|
||||
}
|
||||
|
||||
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
|
||||
@@ -1360,6 +1108,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
|
||||
this._setup();
|
||||
super.reset();
|
||||
this._mouseService?.reset();
|
||||
this._selectionService?.reset();
|
||||
this._decorationService.reset();
|
||||
|
||||
|
||||
@@ -43,6 +43,20 @@ describe('Linkifier2', () => {
|
||||
},
|
||||
activate: () => { }
|
||||
};
|
||||
const multiLineLink: ILink = {
|
||||
text: 'foo',
|
||||
range: {
|
||||
start: {
|
||||
x: 2,
|
||||
y: 1
|
||||
},
|
||||
end: {
|
||||
x: 4,
|
||||
y: 2
|
||||
}
|
||||
},
|
||||
activate: () => { }
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
const dom = new jsdom.JSDOM();
|
||||
@@ -86,4 +100,30 @@ describe('Linkifier2', () => {
|
||||
linkifier.linkLeave({ classList: { add: () => { } } } as any, link, {} as any);
|
||||
});
|
||||
|
||||
it('onShowLinkUnderline event range is correct for wrapped links', done => {
|
||||
linkifier.onShowLinkUnderline(e => {
|
||||
assert.equal(multiLineLink.range.start.x - 1, e.x1);
|
||||
assert.equal(multiLineLink.range.start.y - 1, e.y1);
|
||||
assert.equal(multiLineLink.range.end.x, e.x2);
|
||||
assert.equal(multiLineLink.range.end.y - 1, e.y2);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
linkifier.linkHover({ classList: { add: () => { } } } as any, multiLineLink, {} as any);
|
||||
});
|
||||
|
||||
it('onHideLinkUnderline event range is correct for wrapped links', done => {
|
||||
linkifier.onHideLinkUnderline(e => {
|
||||
assert.equal(multiLineLink.range.start.x - 1, e.x1);
|
||||
assert.equal(multiLineLink.range.start.y - 1, e.y1);
|
||||
assert.equal(multiLineLink.range.end.x, e.x2);
|
||||
assert.equal(multiLineLink.range.end.y - 1, e.y2);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
linkifier.linkLeave({ classList: { add: () => { } } } as any, multiLineLink, {} as any);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ import { IBufferCellPosition, ILink, ILinkDecorations, ILinkWithState, ILinkifie
|
||||
import { Disposable, dispose, toDisposable } from 'common/Lifecycle';
|
||||
import { IDisposable } from 'common/Types';
|
||||
import { IBufferService } from 'common/services/Services';
|
||||
import { ILinkProviderService, IMouseService, IRenderService } from './services/Services';
|
||||
import { ILinkProviderService, IMouseCoordsService, IRenderService } from './services/Services';
|
||||
import { Emitter } from 'common/Event';
|
||||
import { addDisposableListener } from 'browser/Dom';
|
||||
|
||||
@@ -30,7 +30,7 @@ export class Linkifier extends Disposable implements ILinkifier2 {
|
||||
|
||||
constructor(
|
||||
private readonly _element: HTMLElement,
|
||||
@IMouseService private readonly _mouseService: IMouseService,
|
||||
@IMouseCoordsService private readonly _mouseCoordsService: IMouseCoordsService,
|
||||
@IRenderService private readonly _renderService: IRenderService,
|
||||
@IBufferService private readonly _bufferService: IBufferService,
|
||||
@ILinkProviderService private readonly _linkProviderService: ILinkProviderService
|
||||
@@ -60,7 +60,7 @@ export class Linkifier extends Disposable implements ILinkifier2 {
|
||||
private _handleMouseMove(event: MouseEvent): void {
|
||||
this._lastMouseEvent = event;
|
||||
|
||||
const position = this._positionFromMouseEvent(event, this._element, this._mouseService);
|
||||
const position = this._positionFromMouseEvent(event, this._element);
|
||||
if (!position) {
|
||||
return;
|
||||
}
|
||||
@@ -222,7 +222,7 @@ export class Linkifier extends Disposable implements ILinkifier2 {
|
||||
return;
|
||||
}
|
||||
|
||||
const position = this._positionFromMouseEvent(event, this._element, this._mouseService);
|
||||
const position = this._positionFromMouseEvent(event, this._element);
|
||||
if (!position) {
|
||||
return;
|
||||
}
|
||||
@@ -251,7 +251,7 @@ export class Linkifier extends Disposable implements ILinkifier2 {
|
||||
return;
|
||||
}
|
||||
|
||||
const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element, this._mouseService);
|
||||
const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element);
|
||||
|
||||
if (!position) {
|
||||
return;
|
||||
@@ -312,7 +312,7 @@ export class Linkifier extends Disposable implements ILinkifier2 {
|
||||
this._clearCurrentLink(start, end);
|
||||
if (this._lastMouseEvent) {
|
||||
// re-eval previously active link after changes
|
||||
const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element, this._mouseService!);
|
||||
const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element);
|
||||
if (position) {
|
||||
this._askForLink(position, false);
|
||||
}
|
||||
@@ -378,8 +378,8 @@ export class Linkifier extends Disposable implements ILinkifier2 {
|
||||
* Get the buffer position from a mouse event
|
||||
* @param event
|
||||
*/
|
||||
private _positionFromMouseEvent(event: MouseEvent, element: HTMLElement, mouseService: IMouseService): IBufferCellPosition | undefined {
|
||||
const coords = mouseService.getCoords(event, element, this._bufferService.cols, this._bufferService.rows);
|
||||
private _positionFromMouseEvent(event: MouseEvent, element: HTMLElement): IBufferCellPosition | undefined {
|
||||
const coords = this._mouseCoordsService.getCoords(event, element, this._bufferService.cols, this._bufferService.rows);
|
||||
if (!coords) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { OscLinkProvider } from 'browser/OscLinkProvider';
|
||||
import { ILink } from 'browser/Types';
|
||||
import { createCellData, MockBufferService, MockOptionsService } from 'common/TestUtils.test';
|
||||
import { IBufferService, IOscLinkService } from 'common/services/Services';
|
||||
import { IBufferLine, IOscLinkData } from 'common/Types';
|
||||
|
||||
class TestOscLinkService implements IOscLinkService {
|
||||
public serviceBrand: any;
|
||||
public registerLink(_linkData: IOscLinkData): number { return 0; }
|
||||
public addLineToLink(_linkId: number, _y: number): void { }
|
||||
public getLinkData(linkId: number): IOscLinkData | undefined {
|
||||
return { uri: `https://example.com/${linkId}` };
|
||||
}
|
||||
}
|
||||
|
||||
function setText(line: IBufferLine | undefined, x: number, text: string): void {
|
||||
if (!line) {
|
||||
throw new Error('Missing buffer line');
|
||||
}
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
line.setCell(x + i, createCellData(0, text[i], 1));
|
||||
}
|
||||
}
|
||||
|
||||
function setUrl(line: IBufferLine | undefined, x: number, text: string, linkId: number): void {
|
||||
if (!line) {
|
||||
throw new Error('Missing buffer line');
|
||||
}
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const cell = createCellData(0, text[i], 1);
|
||||
cell.extended.urlId = linkId;
|
||||
cell.updateExtended();
|
||||
line.setCell(x + i, cell);
|
||||
}
|
||||
}
|
||||
|
||||
function getLinks(provider: OscLinkProvider, y: number): Promise<ILink[]> {
|
||||
return new Promise(resolve => provider.provideLinks(y, links => resolve(links ?? [])));
|
||||
}
|
||||
|
||||
describe('OscLinkProvider', () => {
|
||||
let bufferService: IBufferService;
|
||||
let provider: OscLinkProvider;
|
||||
|
||||
beforeEach(() => {
|
||||
const optionsService = new MockOptionsService();
|
||||
bufferService = new MockBufferService(5, 5, optionsService);
|
||||
provider = new OscLinkProvider(bufferService, optionsService, new TestOscLinkService());
|
||||
});
|
||||
|
||||
it('expands a wrapped link range backward to the previous line', async () => {
|
||||
const line1 = bufferService.buffer.lines.get(0);
|
||||
const line2 = bufferService.buffer.lines.get(1);
|
||||
setText(line1, 0, 'aa');
|
||||
setUrl(line1, 2, 'bbb', 1);
|
||||
setUrl(line2, 0, 'cccc', 1);
|
||||
setText(line2, 4, 'x');
|
||||
line2!.isWrapped = true;
|
||||
|
||||
const links = await getLinks(provider, 2);
|
||||
assert.lengthOf(links, 1);
|
||||
assert.deepEqual(links[0].range, {
|
||||
start: { x: 3, y: 1 },
|
||||
end: { x: 4, y: 2 }
|
||||
});
|
||||
});
|
||||
|
||||
it('expands a wrapped link range forward when a link ends at line boundary', async () => {
|
||||
const line1 = bufferService.buffer.lines.get(0);
|
||||
const line2 = bufferService.buffer.lines.get(1);
|
||||
setUrl(line1, 0, 'aaaaa', 1);
|
||||
setUrl(line2, 0, 'bb', 1);
|
||||
setText(line2, 2, 'ccc');
|
||||
line2!.isWrapped = true;
|
||||
|
||||
const links = await getLinks(provider, 1);
|
||||
assert.lengthOf(links, 1);
|
||||
assert.deepEqual(links[0].range, {
|
||||
start: { x: 1, y: 1 },
|
||||
end: { x: 2, y: 2 }
|
||||
});
|
||||
});
|
||||
|
||||
it('does not merge wrapped links with different url ids', async () => {
|
||||
const line1 = bufferService.buffer.lines.get(0);
|
||||
const line2 = bufferService.buffer.lines.get(1);
|
||||
setUrl(line1, 0, 'aaaaa', 1);
|
||||
setUrl(line2, 0, 'bbb', 2);
|
||||
setText(line2, 3, 'cc');
|
||||
line2!.isWrapped = true;
|
||||
|
||||
const links = await getLinks(provider, 1);
|
||||
assert.lengthOf(links, 1);
|
||||
assert.deepEqual(links[0].range, {
|
||||
start: { x: 1, y: 1 },
|
||||
end: { x: 5, y: 1 }
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -6,6 +6,7 @@
|
||||
import { IBufferRange, ILink } from 'browser/Types';
|
||||
import { ILinkProvider } from 'browser/services/Services';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { IBufferLine } from 'common/Types';
|
||||
import { IBufferService, IOptionsService, IOscLinkService } from 'common/services/Services';
|
||||
|
||||
export class OscLinkProvider implements ILinkProvider {
|
||||
@@ -57,19 +58,8 @@ export class OscLinkProvider implements ILinkProvider {
|
||||
if (finishLink || (currentStart !== -1 && x === lineLength - 1)) {
|
||||
const text = this._oscLinkService.getLinkData(currentLinkId)?.uri;
|
||||
if (text) {
|
||||
// These ranges are 1-based
|
||||
const range: IBufferRange = {
|
||||
start: {
|
||||
x: currentStart + 1,
|
||||
y
|
||||
},
|
||||
end: {
|
||||
// Offset end x if it's a link that ends on the last cell in the line
|
||||
x: x + (!finishLink && x === lineLength - 1 ? 1 : 0),
|
||||
y
|
||||
}
|
||||
};
|
||||
|
||||
const endX = x + (!finishLink && x === lineLength - 1 ? 1 : 0);
|
||||
const range = this._getRangeWithLineWrap(y, currentStart, endX, currentLinkId);
|
||||
let ignoreLink = false;
|
||||
if (!linkHandler?.allowNonHttpProtocols) {
|
||||
try {
|
||||
@@ -111,6 +101,82 @@ export class OscLinkProvider implements ILinkProvider {
|
||||
// id
|
||||
callback(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand a single-line OSC 8 range to a contiguous wrapped range for the same link id.
|
||||
*/
|
||||
private _getRangeWithLineWrap(y: number, startX: number, endX: number, linkId: number): IBufferRange {
|
||||
let startY = y;
|
||||
let finalStartX = startX;
|
||||
let endY = y;
|
||||
let finalEndX = endX;
|
||||
|
||||
// Expand upward only when this segment starts at column 0 and the current line is wrapped.
|
||||
while (finalStartX === 0) {
|
||||
const currentLine = this._bufferService.buffer.lines.get(startY - 1);
|
||||
if (!currentLine?.isWrapped) {
|
||||
break;
|
||||
}
|
||||
const previousLine = this._bufferService.buffer.lines.get(startY - 2);
|
||||
if (!previousLine) {
|
||||
break;
|
||||
}
|
||||
const previousLineLength = previousLine.getTrimmedLength();
|
||||
if (previousLineLength === 0 || !this._hasUrlId(previousLine, previousLineLength - 1, linkId)) {
|
||||
break;
|
||||
}
|
||||
let previousStartX = previousLineLength - 1;
|
||||
while (previousStartX > 0 && this._hasUrlId(previousLine, previousStartX - 1, linkId)) {
|
||||
previousStartX--;
|
||||
}
|
||||
startY--;
|
||||
finalStartX = previousStartX;
|
||||
}
|
||||
|
||||
// Expand downward only when this segment reaches trimmed EOL and the next line is wrapped.
|
||||
while (true) {
|
||||
const currentLine = this._bufferService.buffer.lines.get(endY - 1);
|
||||
if (!currentLine) {
|
||||
break;
|
||||
}
|
||||
const currentLineLength = currentLine.getTrimmedLength();
|
||||
if (finalEndX !== currentLineLength) {
|
||||
break;
|
||||
}
|
||||
const nextLine = this._bufferService.buffer.lines.get(endY);
|
||||
if (!nextLine?.isWrapped) {
|
||||
break;
|
||||
}
|
||||
const nextLineLength = nextLine.getTrimmedLength();
|
||||
if (nextLineLength === 0 || !this._hasUrlId(nextLine, 0, linkId)) {
|
||||
break;
|
||||
}
|
||||
let nextEndX = 1;
|
||||
while (nextEndX < nextLineLength && this._hasUrlId(nextLine, nextEndX, linkId)) {
|
||||
nextEndX++;
|
||||
}
|
||||
endY++;
|
||||
finalEndX = nextEndX;
|
||||
}
|
||||
|
||||
// IBufferRange uses 1-based coordinates.
|
||||
return {
|
||||
start: {
|
||||
x: finalStartX + 1,
|
||||
y: startY
|
||||
},
|
||||
end: {
|
||||
x: finalEndX,
|
||||
y: endY
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private _hasUrlId(line: IBufferLine, x: number, linkId: number): boolean {
|
||||
const cell = this._workCell;
|
||||
line.loadCell(x, cell);
|
||||
return !!cell.hasExtendedAttrs() && cell.extended.urlId === linkId;
|
||||
}
|
||||
}
|
||||
|
||||
function defaultActivate(e: MouseEvent, uri: string): void {
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { IBrowser } from 'browser/Types';
|
||||
import { assert } from 'chai';
|
||||
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { MockUnicodeService } from 'common/TestUtils.test';
|
||||
import { MockUnicodeService, createCellData } from 'common/TestUtils.test';
|
||||
import { IMarker } from 'common/Types';
|
||||
|
||||
const INIT_COLS = 80;
|
||||
@@ -17,21 +17,6 @@ const INIT_ROWS = 24;
|
||||
// grab wcwidth from mock unicode service (hardcoded to V6)
|
||||
const wcwidth = (new MockUnicodeService()).wcwidth;
|
||||
|
||||
function assertLineEqual(actual: any, expected: any): void {
|
||||
assert.isOk(actual);
|
||||
assert.isOk(expected);
|
||||
assert.equal(actual.length, expected.length);
|
||||
assert.equal(actual.isWrapped, expected.isWrapped);
|
||||
const actualCell = new CellData();
|
||||
const expectedCell = new CellData();
|
||||
for (let i = 0; i < actual.length; i++) {
|
||||
assert.deepEqual(
|
||||
actual.loadCell(i, actualCell).getAsCharData(),
|
||||
expected.loadCell(i, expectedCell).getAsCharData()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
describe('Terminal', () => {
|
||||
let term: TestTerminal;
|
||||
const termOptions = {
|
||||
@@ -198,9 +183,9 @@ describe('Terminal', () => {
|
||||
assert.equal(term.buffer.ybase, 0);
|
||||
assert.equal(term.buffer.ydisp, 0);
|
||||
assert.equal(term.buffer.lines.length, term.rows);
|
||||
assertLineEqual(term.buffer.lines.get(0), promptLine);
|
||||
assert.deepEqual(term.buffer.lines.get(0), promptLine);
|
||||
for (let i = 1; i < term.rows; i++) {
|
||||
assertLineEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR_DATA));
|
||||
assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR_DATA));
|
||||
}
|
||||
});
|
||||
it('should clear a buffer larger than rows', async () => {
|
||||
@@ -213,9 +198,9 @@ describe('Terminal', () => {
|
||||
assert.equal(term.buffer.ybase, 0);
|
||||
assert.equal(term.buffer.ydisp, 0);
|
||||
assert.equal(term.buffer.lines.length, term.rows);
|
||||
assertLineEqual(term.buffer.lines.get(0), promptLine);
|
||||
assert.deepEqual(term.buffer.lines.get(0), promptLine);
|
||||
for (let i = 1; i < term.rows; i++) {
|
||||
assertLineEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR_DATA));
|
||||
assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR_DATA));
|
||||
}
|
||||
});
|
||||
it('should not break the prompt when cleared twice', () => {
|
||||
@@ -226,9 +211,9 @@ describe('Terminal', () => {
|
||||
assert.equal(term.buffer.ybase, 0);
|
||||
assert.equal(term.buffer.ydisp, 0);
|
||||
assert.equal(term.buffer.lines.length, term.rows);
|
||||
assertLineEqual(term.buffer.lines.get(0), promptLine);
|
||||
assert.deepEqual(term.buffer.lines.get(0), promptLine);
|
||||
for (let i = 1; i < term.rows; i++) {
|
||||
assertLineEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR_DATA));
|
||||
assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR_DATA));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -455,8 +440,8 @@ describe('Terminal', () => {
|
||||
describe('scroll() function', () => {
|
||||
describe('when scrollback > 0', () => {
|
||||
it('should create a new line and scroll', () => {
|
||||
term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(INIT_ROWS - 1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(0)!.setCell(0, createCellData(0, 'a', 0));
|
||||
term.buffer.lines.get(INIT_ROWS - 1)!.setCell(0, createCellData(0, 'b', 0));
|
||||
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
||||
term.scroll(DEFAULT_ATTR_DATA.clone());
|
||||
assert.equal(term.buffer.lines.length, INIT_ROWS + 1);
|
||||
@@ -466,9 +451,9 @@ describe('Terminal', () => {
|
||||
});
|
||||
|
||||
it('should properly scroll inside a scroll region (scrollTop set)', () => {
|
||||
term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(2)!.setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(0)!.setCell(0, createCellData(0, 'a', 0));
|
||||
term.buffer.lines.get(1)!.setCell(0, createCellData(0, 'b', 0));
|
||||
term.buffer.lines.get(2)!.setCell(0, createCellData(0, 'c', 0));
|
||||
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
||||
term.buffer.scrollTop = 1;
|
||||
term.scroll(DEFAULT_ATTR_DATA.clone());
|
||||
@@ -478,11 +463,11 @@ describe('Terminal', () => {
|
||||
});
|
||||
|
||||
it('should properly scroll inside a scroll region (scrollBottom set)', () => {
|
||||
term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(2)!.setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(3)!.setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(4)!.setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(0)!.setCell(0, createCellData(0, 'a', 0));
|
||||
term.buffer.lines.get(1)!.setCell(0, createCellData(0, 'b', 0));
|
||||
term.buffer.lines.get(2)!.setCell(0, createCellData(0, 'c', 0));
|
||||
term.buffer.lines.get(3)!.setCell(0, createCellData(0, 'd', 0));
|
||||
term.buffer.lines.get(4)!.setCell(0, createCellData(0, 'e', 0));
|
||||
term.buffer.y = 3;
|
||||
term.buffer.scrollBottom = 3;
|
||||
term.scroll(DEFAULT_ATTR_DATA.clone());
|
||||
@@ -496,11 +481,11 @@ describe('Terminal', () => {
|
||||
});
|
||||
|
||||
it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => {
|
||||
term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(2)!.setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(3)!.setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(4)!.setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(0)!.setCell(0, createCellData(0, 'a', 0));
|
||||
term.buffer.lines.get(1)!.setCell(0, createCellData(0, 'b', 0));
|
||||
term.buffer.lines.get(2)!.setCell(0, createCellData(0, 'c', 0));
|
||||
term.buffer.lines.get(3)!.setCell(0, createCellData(0, 'd', 0));
|
||||
term.buffer.lines.get(4)!.setCell(0, createCellData(0, 'e', 0));
|
||||
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
||||
term.buffer.scrollTop = 1;
|
||||
term.buffer.scrollBottom = 3;
|
||||
@@ -521,9 +506,9 @@ describe('Terminal', () => {
|
||||
});
|
||||
|
||||
it('should create a new line and shift everything up', () => {
|
||||
term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(INIT_ROWS - 1)!.setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(0)!.setCell(0, createCellData(0, 'a', 0));
|
||||
term.buffer.lines.get(1)!.setCell(0, createCellData(0, 'b', 0));
|
||||
term.buffer.lines.get(INIT_ROWS - 1)!.setCell(0, createCellData(0, 'c', 0));
|
||||
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||
term.scroll(DEFAULT_ATTR_DATA.clone());
|
||||
@@ -536,9 +521,9 @@ describe('Terminal', () => {
|
||||
});
|
||||
|
||||
it('should properly scroll inside a scroll region (scrollTop set)', () => {
|
||||
term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(2)!.setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(0)!.setCell(0, createCellData(0, 'a', 0));
|
||||
term.buffer.lines.get(1)!.setCell(0, createCellData(0, 'b', 0));
|
||||
term.buffer.lines.get(2)!.setCell(0, createCellData(0, 'c', 0));
|
||||
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
||||
term.buffer.scrollTop = 1;
|
||||
term.scroll(DEFAULT_ATTR_DATA.clone());
|
||||
@@ -548,11 +533,11 @@ describe('Terminal', () => {
|
||||
});
|
||||
|
||||
it('should properly scroll inside a scroll region (scrollBottom set)', () => {
|
||||
term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(2)!.setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(3)!.setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(4)!.setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(0)!.setCell(0, createCellData(0, 'a', 0));
|
||||
term.buffer.lines.get(1)!.setCell(0, createCellData(0, 'b', 0));
|
||||
term.buffer.lines.get(2)!.setCell(0, createCellData(0, 'c', 0));
|
||||
term.buffer.lines.get(3)!.setCell(0, createCellData(0, 'd', 0));
|
||||
term.buffer.lines.get(4)!.setCell(0, createCellData(0, 'e', 0));
|
||||
term.buffer.y = 3;
|
||||
term.buffer.scrollBottom = 3;
|
||||
term.scroll(DEFAULT_ATTR_DATA.clone());
|
||||
@@ -565,11 +550,11 @@ describe('Terminal', () => {
|
||||
});
|
||||
|
||||
it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => {
|
||||
term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(2)!.setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(3)!.setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(4)!.setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(0)!.setCell(0, createCellData(0, 'a', 0));
|
||||
term.buffer.lines.get(1)!.setCell(0, createCellData(0, 'b', 0));
|
||||
term.buffer.lines.get(2)!.setCell(0, createCellData(0, 'c', 0));
|
||||
term.buffer.lines.get(3)!.setCell(0, createCellData(0, 'd', 0));
|
||||
term.buffer.lines.get(4)!.setCell(0, createCellData(0, 'e', 0));
|
||||
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
||||
term.buffer.scrollTop = 1;
|
||||
term.buffer.scrollBottom = 3;
|
||||
@@ -886,7 +871,7 @@ describe('Terminal', () => {
|
||||
assert.equal(cell.getWidth(), 1);
|
||||
});
|
||||
it('multiple combined é', async () => {
|
||||
await term.writeP(Array(100).join('e\u0301'));
|
||||
await term.writeP('e\u0301'.repeat(99));
|
||||
for (let i = 0; i < term.cols; ++i) {
|
||||
term.buffer.lines.get(0)!.loadCell(i, cell);
|
||||
assert.equal(cell.getChars(), 'e\u0301');
|
||||
@@ -899,7 +884,7 @@ describe('Terminal', () => {
|
||||
assert.equal(cell.getWidth(), 1);
|
||||
});
|
||||
it('multiple surrogate with combined', async () => {
|
||||
await term.writeP(Array(100).join('\uD800\uDC00\u0301'));
|
||||
await term.writeP('\uD800\uDC00\u0301'.repeat(99));
|
||||
for (let i = 0; i < term.cols; ++i) {
|
||||
term.buffer.lines.get(0)!.loadCell(i, cell);
|
||||
assert.equal(cell.getChars(), '\uD800\uDC00\u0301');
|
||||
@@ -927,7 +912,7 @@ describe('Terminal', () => {
|
||||
assert.equal(term.buffer.x, 3);
|
||||
});
|
||||
it('line of ¥ even', async () => {
|
||||
await term.writeP(Array(50).join('¥'));
|
||||
await term.writeP('¥'.repeat(49));
|
||||
for (let i = 0; i < term.cols; ++i) {
|
||||
term.buffer.lines.get(0)!.loadCell(i, cell);
|
||||
if (i % 2) {
|
||||
@@ -947,7 +932,7 @@ describe('Terminal', () => {
|
||||
});
|
||||
it('line of ¥ odd', async () => {
|
||||
term.buffer.x = 1;
|
||||
await term.writeP(Array(50).join('¥'));
|
||||
await term.writeP('¥'.repeat(49));
|
||||
for (let i = 1; i < term.cols - 1; ++i) {
|
||||
term.buffer.lines.get(0)!.loadCell(i, cell);
|
||||
if (!(i % 2)) {
|
||||
@@ -971,7 +956,7 @@ describe('Terminal', () => {
|
||||
});
|
||||
it('line of ¥ with combining odd', async () => {
|
||||
term.buffer.x = 1;
|
||||
await term.writeP(Array(50).join('¥\u0301'));
|
||||
await term.writeP('¥\u0301'.repeat(49));
|
||||
for (let i = 1; i < term.cols - 1; ++i) {
|
||||
term.buffer.lines.get(0)!.loadCell(i, cell);
|
||||
if (!(i % 2)) {
|
||||
@@ -994,7 +979,7 @@ describe('Terminal', () => {
|
||||
assert.equal(cell.getWidth(), 2);
|
||||
});
|
||||
it('line of ¥ with combining even', async () => {
|
||||
await term.writeP(Array(50).join('¥\u0301'));
|
||||
await term.writeP('¥\u0301'.repeat(49));
|
||||
for (let i = 0; i < term.cols; ++i) {
|
||||
term.buffer.lines.get(0)!.loadCell(i, cell);
|
||||
if (i % 2) {
|
||||
@@ -1014,7 +999,7 @@ describe('Terminal', () => {
|
||||
});
|
||||
it('line of surrogate fullwidth with combining odd', async () => {
|
||||
term.buffer.x = 1;
|
||||
await term.writeP(Array(50).join('\ud843\ude6d\u0301'));
|
||||
await term.writeP('\ud843\ude6d\u0301'.repeat(49));
|
||||
for (let i = 1; i < term.cols - 1; ++i) {
|
||||
term.buffer.lines.get(0)!.loadCell(i, cell);
|
||||
if (!(i % 2)) {
|
||||
@@ -1037,7 +1022,7 @@ describe('Terminal', () => {
|
||||
assert.equal(cell.getWidth(), 2);
|
||||
});
|
||||
it('line of surrogate fullwidth with combining even', async () => {
|
||||
await term.writeP(Array(50).join('\ud843\ude6d\u0301'));
|
||||
await term.writeP('\ud843\ude6d\u0301'.repeat(49));
|
||||
for (let i = 0; i < term.cols; ++i) {
|
||||
term.buffer.lines.get(0)!.loadCell(i, cell);
|
||||
if (i % 2) {
|
||||
@@ -1060,7 +1045,7 @@ describe('Terminal', () => {
|
||||
describe('insert mode', () => {
|
||||
const cell = new CellData();
|
||||
it('halfwidth - all', async () => {
|
||||
await term.writeP(Array(9).join('0123456789').slice(-80));
|
||||
await term.writeP('0123456789'.repeat(8).slice(-80));
|
||||
term.buffer.x = 10;
|
||||
term.buffer.y = 0;
|
||||
term.write('\x1b[4h');
|
||||
@@ -1072,7 +1057,7 @@ describe('Terminal', () => {
|
||||
assert.equal(term.buffer.lines.get(0)!.loadCell(79, cell).getChars(), '4');
|
||||
});
|
||||
it('fullwidth - insert', async () => {
|
||||
await term.writeP(Array(9).join('0123456789').slice(-80));
|
||||
await term.writeP('0123456789'.repeat(8).slice(-80));
|
||||
term.buffer.x = 10;
|
||||
term.buffer.y = 0;
|
||||
term.write('\x1b[4h');
|
||||
@@ -1085,7 +1070,7 @@ describe('Terminal', () => {
|
||||
assert.equal(term.buffer.lines.get(0)!.loadCell(79, cell).getChars(), '3');
|
||||
});
|
||||
it('fullwidth - right border', async () => {
|
||||
await term.writeP(Array(41).join('¥'));
|
||||
await term.writeP('¥'.repeat(40));
|
||||
term.buffer.x = 10;
|
||||
term.buffer.y = 0;
|
||||
term.write('\x1b[4h');
|
||||
|
||||
@@ -12,7 +12,7 @@ import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, I
|
||||
import { Buffer } from 'common/buffer/Buffer';
|
||||
import * as Browser from 'common/Platform';
|
||||
import { CoreBrowserTerminal } from 'browser/CoreBrowserTerminal';
|
||||
import { IUnicodeService, IOptionsService, ICoreService, ICoreMouseService } from 'common/services/Services';
|
||||
import { IUnicodeService, IOptionsService, ICoreService, IMouseStateService } from 'common/services/Services';
|
||||
import { IFunctionIdentifier, IParams } from 'common/parser/Types';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { ISelectionRedrawRequestEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
|
||||
@@ -51,7 +51,7 @@ export class MockTerminal implements ITerminal {
|
||||
public dimensions: IRenderDimensionsApi | undefined;
|
||||
public markers!: IMarker[];
|
||||
public linkifier: ILinkifier2 | undefined;
|
||||
public coreMouseService!: ICoreMouseService;
|
||||
public mouseStateService!: IMouseStateService;
|
||||
public coreService!: ICoreService;
|
||||
public optionsService!: IOptionsService;
|
||||
public unicodeService!: IUnicodeService;
|
||||
@@ -392,6 +392,9 @@ export class MockMouseService implements IMouseService {
|
||||
public getMouseReportCoords(event: MouseEvent, element: HTMLElement): { col: number, row: number, x: number, y: number } | undefined {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
public bindMouse(): void { }
|
||||
public reset(): void { }
|
||||
}
|
||||
|
||||
export class MockRenderService implements IRenderService {
|
||||
|
||||
+24
-12
@@ -6,14 +6,13 @@
|
||||
import { ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
|
||||
import { ViewportConstants } from 'browser/shared/Constants';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { IBufferService, ICoreMouseService, IOptionsService } from 'common/services/Services';
|
||||
import { IBufferService, ICoreService, IMouseStateService, IOptionsService } from 'common/services/Services';
|
||||
import { CoreMouseEventType } from 'common/Types';
|
||||
import { addDisposableListener, scheduleAtNextAnimationFrame } from 'browser/Dom';
|
||||
import { scheduleAtNextAnimationFrame } from 'browser/Dom';
|
||||
import { SmoothScrollableElement } from 'browser/scrollable/scrollableElement';
|
||||
import type { IScrollableElementChangeOptions } from 'browser/scrollable/scrollableElementOptions';
|
||||
import { Emitter, EventUtils } from 'common/Event';
|
||||
import { Scrollable, ScrollbarVisibility, type IScrollEvent } from 'browser/scrollable/scrollable';
|
||||
import { Gesture, EventType as GestureEventType, type IGestureEvent } from 'browser/scrollable/touch';
|
||||
|
||||
export class Viewport extends Disposable {
|
||||
|
||||
@@ -28,13 +27,15 @@ export class Viewport extends Disposable {
|
||||
private _isSyncing: boolean = false;
|
||||
private _isHandlingScroll: boolean = false;
|
||||
private _suppressOnScrollHandler: boolean = false;
|
||||
private _needsSyncOnRender: boolean = false;
|
||||
|
||||
constructor(
|
||||
element: HTMLElement,
|
||||
screenElement: HTMLElement,
|
||||
@IBufferService private readonly _bufferService: IBufferService,
|
||||
@ICoreBrowserService coreBrowserService: ICoreBrowserService,
|
||||
@ICoreMouseService coreMouseService: ICoreMouseService,
|
||||
@ICoreService private readonly _coreService: ICoreService,
|
||||
@IMouseStateService mouseStateService: IMouseStateService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IOptionsService private readonly _optionsService: IOptionsService,
|
||||
@IRenderService private readonly _renderService: IRenderService
|
||||
@@ -65,7 +66,7 @@ export class Viewport extends Disposable {
|
||||
'scrollbar'
|
||||
], () => this._scrollableElement.updateOptions(this._getChangeOptions())));
|
||||
// Don't handle mouse wheel if wheel events are supported by the current mouse prototcol
|
||||
this._register(coreMouseService.onProtocolChange(type => {
|
||||
this._register(mouseStateService.onProtocolChange(type => {
|
||||
this._scrollableElement.updateOptions({
|
||||
handleMouseWheel: !(type & CoreMouseEventType.WHEEL)
|
||||
});
|
||||
@@ -105,11 +106,18 @@ export class Viewport extends Disposable {
|
||||
}));
|
||||
this._register(this._bufferService.onScroll(() => this._sync()));
|
||||
|
||||
// Flush deferred viewport sync after a render completes (e.g. after ESU ends
|
||||
// synchronized output mode). This ensures DOM scroll position updates atomically
|
||||
// with the canvas render.
|
||||
this._register(this._renderService.onRender(() => {
|
||||
if (this._needsSyncOnRender) {
|
||||
this._needsSyncOnRender = false;
|
||||
this._sync();
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(this._scrollableElement.onScroll(e => this._handleScroll(e)));
|
||||
|
||||
// Touch/gesture scrolling support
|
||||
this._register(Gesture.addTarget(screenElement));
|
||||
this._register(addDisposableListener(screenElement, GestureEventType.CHANGE, (e: IGestureEvent) => this._handleGestureChange(e)));
|
||||
}
|
||||
|
||||
public scrollLines(disp: number): void {
|
||||
@@ -165,6 +173,12 @@ export class Viewport extends Disposable {
|
||||
if (!this._renderService || this._isSyncing) {
|
||||
return;
|
||||
}
|
||||
// Defer DOM scroll updates during synchronized output to prevent visible
|
||||
// scroll position flickering while the canvas content is frozen.
|
||||
if (this._coreService.decPrivateModes.synchronizedOutput) {
|
||||
this._needsSyncOnRender = true;
|
||||
return;
|
||||
}
|
||||
this._isSyncing = true;
|
||||
|
||||
// Ignore any onScroll event that happens as a result of dimensions changing as this should
|
||||
@@ -204,12 +218,10 @@ export class Viewport extends Disposable {
|
||||
this._isHandlingScroll = false;
|
||||
}
|
||||
|
||||
private _handleGestureChange(e: IGestureEvent): void {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
public handleTouchScroll(translationY: number): void {
|
||||
const pos = this._scrollableElement.getScrollPosition();
|
||||
this._scrollableElement.setScrollPosition({
|
||||
scrollTop: pos.scrollTop - e.translationY
|
||||
scrollTop: pos.scrollTop - translationY
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,8 +51,6 @@ export class OverviewRulerRenderer extends Disposable {
|
||||
private _shouldUpdateAnchor: boolean | undefined = true;
|
||||
private _lastKnownBufferLength: number = 0;
|
||||
|
||||
private _containerHeight: number | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly _viewportElement: HTMLElement,
|
||||
private readonly _screenElement: HTMLElement,
|
||||
@@ -91,13 +89,7 @@ export class OverviewRulerRenderer extends Disposable {
|
||||
}
|
||||
}));
|
||||
|
||||
// Container height changed
|
||||
this._register(this._renderService.onRender((): void => {
|
||||
if (!this._containerHeight || this._containerHeight !== this._screenElement.clientHeight) {
|
||||
this._queueRefresh(true);
|
||||
this._containerHeight = this._screenElement.clientHeight;
|
||||
}
|
||||
}));
|
||||
this._register(this._renderService.onDimensionsChange(() => this._queueRefresh(true)));
|
||||
|
||||
this._register(this._coreBrowserService.onDprChange(() => this._queueRefresh(true)));
|
||||
this._register(this._optionsService.onSpecificOptionChange('scrollbar', () => this._queueRefresh(true)));
|
||||
@@ -144,10 +136,12 @@ export class OverviewRulerRenderer extends Disposable {
|
||||
}
|
||||
|
||||
private _refreshCanvasDimensions(): void {
|
||||
const cssCanvasHeight = this._renderService.dimensions.css.canvas.height;
|
||||
const deviceCanvasHeight = this._renderService.dimensions.device.canvas.height;
|
||||
this._canvas.style.width = `${this._width}px`;
|
||||
this._canvas.width = Math.round(this._width * this._coreBrowserService.dpr);
|
||||
this._canvas.style.height = `${this._screenElement.clientHeight}px`;
|
||||
this._canvas.height = Math.round(this._screenElement.clientHeight * this._coreBrowserService.dpr);
|
||||
this._canvas.style.height = `${cssCanvasHeight}px`;
|
||||
this._canvas.height = deviceCanvasHeight;
|
||||
this._refreshDrawConstants();
|
||||
this._refreshColorZonePadding();
|
||||
}
|
||||
|
||||
@@ -233,5 +233,31 @@ describe('CompositionHelper', () => {
|
||||
}, 0);
|
||||
}, 0);
|
||||
});
|
||||
|
||||
it('Should insert middle composition and subsequent input without appending existing trailing text', (done) => {
|
||||
textarea.value = '一二';
|
||||
// screenReaderMode keeps textarea content/selection for assistive technologies (eg. screen
|
||||
// readers), so the caret can be moved within the textarea (eg. via arrow keys) before
|
||||
// starting composition.
|
||||
textarea.selectionStart = 1;
|
||||
textarea.selectionEnd = 1;
|
||||
|
||||
compositionHelper.compositionstart();
|
||||
compositionHelper.compositionupdate({ data: '一' });
|
||||
textarea.value = '一一二';
|
||||
// After the composed text is inserted, the caret typically moves to after it.
|
||||
textarea.selectionStart = 2;
|
||||
textarea.selectionEnd = 2;
|
||||
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionend();
|
||||
// Second character '1' (a non-composition character)
|
||||
textarea.value = '一一1二';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
assert.equal(handledText, '一1');
|
||||
done();
|
||||
}, 0);
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,6 +30,12 @@ export class CompositionHelper {
|
||||
*/
|
||||
private _compositionPosition: IPosition;
|
||||
|
||||
/**
|
||||
* Text that existed after the composing range when composition started.
|
||||
* This is used to avoid treating existing trailing text as new input.
|
||||
*/
|
||||
private _compositionSuffix: string;
|
||||
|
||||
/**
|
||||
* Whether a composition is in the process of being sent, setting this to false will cancel any
|
||||
* in-progress composition.
|
||||
@@ -57,6 +63,7 @@ export class CompositionHelper {
|
||||
this._isComposing = false;
|
||||
this._isSendingComposition = false;
|
||||
this._compositionPosition = { start: 0, end: 0 };
|
||||
this._compositionSuffix = '';
|
||||
this._dataAlreadySent = '';
|
||||
}
|
||||
|
||||
@@ -65,7 +72,13 @@ export class CompositionHelper {
|
||||
*/
|
||||
public compositionstart(): void {
|
||||
this._isComposing = true;
|
||||
this._compositionPosition.start = this._textarea.value.length;
|
||||
// It's important to use the selection here instead of textarea length to avoid conflicts with
|
||||
// screen reader mode
|
||||
const start = this._textarea.selectionStart ?? this._textarea.value.length;
|
||||
const end = this._textarea.selectionEnd ?? start;
|
||||
this._compositionPosition.start = Math.min(start, end);
|
||||
this._compositionPosition.end = Math.max(start, end);
|
||||
this._compositionSuffix = this._textarea.value.substring(this._compositionPosition.end);
|
||||
this._compositionView.textContent = '';
|
||||
this._dataAlreadySent = '';
|
||||
this._compositionView.classList.add('active');
|
||||
@@ -76,10 +89,13 @@ export class CompositionHelper {
|
||||
* @param ev The event.
|
||||
*/
|
||||
public compositionupdate(ev: Pick<CompositionEvent, 'data'>): void {
|
||||
this._compositionView.textContent = ev.data;
|
||||
// Mark text as LTR, direction=rtl is used in CSS so the end of the text is followed for long
|
||||
// compositions
|
||||
this._compositionView.textContent = `\u200E${ev.data}\u200E`;
|
||||
this.updateCompositionElements();
|
||||
setTimeout(() => {
|
||||
this._compositionPosition.end = this._textarea.value.length;
|
||||
const end = this._textarea.selectionEnd ?? this._textarea.value.length;
|
||||
this._compositionPosition.end = Math.max( this._compositionPosition.start, end);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
@@ -146,6 +162,7 @@ export class CompositionHelper {
|
||||
start: this._compositionPosition.start,
|
||||
end: this._compositionPosition.end
|
||||
};
|
||||
const currentCompositionSuffix = this._compositionSuffix;
|
||||
|
||||
// Since composition* events happen before the changes take place in the textarea on most
|
||||
// browsers, use a setTimeout with 0ms time to allow the native compositionend event to
|
||||
@@ -169,10 +186,14 @@ export class CompositionHelper {
|
||||
// if a new composition has started.
|
||||
input = this._textarea.value.substring(currentCompositionPosition.start, this._compositionPosition.start);
|
||||
} else {
|
||||
// Don't use the end position here in order to pick up any characters after the
|
||||
// composition has finished, for example when typing a non-composition character
|
||||
// (eg. 2) after a composition character.
|
||||
input = this._textarea.value.substring(currentCompositionPosition.start);
|
||||
// Keep support for non-composition characters typed immediately after composition end
|
||||
// while avoiding re-sending the trailing text that was already present
|
||||
// before composition started.
|
||||
const value = this._textarea.value;
|
||||
const valueEnd = currentCompositionSuffix.length > 0 && value.endsWith(currentCompositionSuffix)
|
||||
? value.length - currentCompositionSuffix.length
|
||||
: value.length;
|
||||
input = value.substring(currentCompositionPosition.start, Math.max(currentCompositionPosition.start, valueEnd));
|
||||
}
|
||||
if (input.length > 0) {
|
||||
this._coreService.triggerDataEvent(input, true);
|
||||
@@ -239,6 +260,12 @@ export class CompositionHelper {
|
||||
this._compositionView.style.lineHeight = cellHeight + 'px';
|
||||
this._compositionView.style.fontFamily = this._optionsService.rawOptions.fontFamily;
|
||||
this._compositionView.style.fontSize = this._optionsService.rawOptions.fontSize + 'px';
|
||||
// Limit the composition view width to the space between the cursor and
|
||||
// the terminal's right edge, preventing it from overflowing the terminal.
|
||||
const maxWidth = this._bufferService.cols * this._renderService.dimensions.css.cell.width - cursorLeft;
|
||||
this._compositionView.style.maxWidth = maxWidth + 'px';
|
||||
this._compositionView.style.overflow = 'hidden';
|
||||
this._compositionView.style.direction = 'rtl';
|
||||
// Sync the textarea to the exact position of the composition view so the IME knows where the
|
||||
// text is.
|
||||
const compositionViewBounds = this._compositionView.getBoundingClientRect();
|
||||
|
||||
@@ -103,7 +103,7 @@ export class Terminal extends Disposable implements ITerminalApi {
|
||||
public get modes(): IModes {
|
||||
const m = this._core.coreService.decPrivateModes;
|
||||
let mouseTrackingMode: 'none' | 'x10' | 'vt200' | 'drag' | 'any' = 'none';
|
||||
switch (this._core.coreMouseService.activeProtocol) {
|
||||
switch (this._core.mouseStateService.activeProtocol) {
|
||||
case 'X10': mouseTrackingMode = 'x10'; break;
|
||||
case 'VT200': mouseTrackingMode = 'vt200'; break;
|
||||
case 'DRAG': mouseTrackingMode = 'drag'; break;
|
||||
|
||||
@@ -45,6 +45,9 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
private _selectionContainer: HTMLElement;
|
||||
private _widthCache: WidthCache;
|
||||
private _selectionRenderModel: ISelectionRenderModel = createSelectionRenderModel();
|
||||
private _lastSelectionStart: [number, number] | undefined;
|
||||
private _lastSelectionEnd: [number, number] | undefined;
|
||||
private _lastSelectionColumnMode: boolean = false;
|
||||
private _cursorBlinkStateManager: CursorBlinkStateManager;
|
||||
private _textBlinkStateManager: TextBlinkStateManager;
|
||||
private _rowHasBlinkingCells: boolean[] = [];
|
||||
@@ -184,6 +187,9 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
// refresh() being called during the mousedown handler to start a selection.
|
||||
` pointer-events: none;` +
|
||||
` color: ${colors.foreground.css};` +
|
||||
`}`;
|
||||
styles +=
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}, ${this._terminalSelector} .${ROW_CONTAINER_CLASS} span {` +
|
||||
` font-family: ${this._optionsService.rawOptions.fontFamily};` +
|
||||
` font-size: ${this._optionsService.rawOptions.fontSize}px;` +
|
||||
` font-kerning: none;` +
|
||||
@@ -371,51 +377,89 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
}
|
||||
|
||||
public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
|
||||
const rows = this._bufferService.rows;
|
||||
|
||||
// Remove all selections
|
||||
this._selectionContainer.replaceChildren();
|
||||
this._rowFactory.handleSelectionChanged(start, end, columnSelectMode);
|
||||
this.renderRows(0, this._bufferService.rows - 1);
|
||||
|
||||
// Selection does not exist
|
||||
// Determine old selection viewport band
|
||||
let oldViewportStart = 0;
|
||||
let oldViewportEnd = -1;
|
||||
if (this._lastSelectionStart && this._lastSelectionEnd) {
|
||||
this._selectionRenderModel.update(this._terminal, this._lastSelectionStart, this._lastSelectionEnd, this._lastSelectionColumnMode);
|
||||
if (this._selectionRenderModel.hasSelection) {
|
||||
oldViewportStart = this._selectionRenderModel.viewportCappedStartRow;
|
||||
oldViewportEnd = this._selectionRenderModel.viewportCappedEndRow;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine new selection viewport band and create overlays
|
||||
let newViewportStart = 0;
|
||||
let newViewportEnd = -1;
|
||||
if (!start || !end) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._selectionRenderModel.update(this._terminal, start, end, columnSelectMode);
|
||||
if (!this._selectionRenderModel.hasSelection) {
|
||||
return;
|
||||
}
|
||||
if (this._selectionRenderModel.hasSelection) {
|
||||
const viewportStartRow = this._selectionRenderModel.viewportStartRow;
|
||||
const viewportEndRow = this._selectionRenderModel.viewportEndRow;
|
||||
const viewportCappedStartRow = this._selectionRenderModel.viewportCappedStartRow;
|
||||
const viewportCappedEndRow = this._selectionRenderModel.viewportCappedEndRow;
|
||||
|
||||
// Translate from buffer position to viewport position
|
||||
const viewportStartRow = this._selectionRenderModel.viewportStartRow;
|
||||
const viewportEndRow = this._selectionRenderModel.viewportEndRow;
|
||||
const viewportCappedStartRow = this._selectionRenderModel.viewportCappedStartRow;
|
||||
const viewportCappedEndRow = this._selectionRenderModel.viewportCappedEndRow;
|
||||
newViewportStart = viewportCappedStartRow;
|
||||
newViewportEnd = viewportCappedEndRow;
|
||||
|
||||
// Create the selections
|
||||
const documentFragment = this._document.createDocumentFragment();
|
||||
// Create the selections
|
||||
const documentFragment = this._document.createDocumentFragment();
|
||||
|
||||
if (columnSelectMode) {
|
||||
const isXFlipped = start[0] > end[0];
|
||||
documentFragment.appendChild(
|
||||
this._createSelectionElement(viewportCappedStartRow, isXFlipped ? end[0] : start[0], isXFlipped ? start[0] : end[0], viewportCappedEndRow - viewportCappedStartRow + 1)
|
||||
);
|
||||
} else {
|
||||
// Draw first row
|
||||
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
|
||||
const endCol = viewportCappedStartRow === viewportEndRow ? end[0] : this._bufferService.cols;
|
||||
documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow, startCol, endCol));
|
||||
// Draw middle rows
|
||||
const middleRowsCount = viewportCappedEndRow - viewportCappedStartRow - 1;
|
||||
documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow + 1, 0, this._bufferService.cols, middleRowsCount));
|
||||
// Draw final row
|
||||
if (viewportCappedStartRow !== viewportCappedEndRow) {
|
||||
// Only draw viewportEndRow if it's not the same as viewporttartRow
|
||||
const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._bufferService.cols;
|
||||
documentFragment.appendChild(this._createSelectionElement(viewportCappedEndRow, 0, endCol));
|
||||
if (columnSelectMode) {
|
||||
const isXFlipped = start[0] > end[0];
|
||||
documentFragment.appendChild(
|
||||
this._createSelectionElement(viewportCappedStartRow, isXFlipped ? end[0] : start[0], isXFlipped ? start[0] : end[0], viewportCappedEndRow - viewportCappedStartRow + 1)
|
||||
);
|
||||
} else {
|
||||
// Draw first row
|
||||
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
|
||||
const endCol = viewportCappedStartRow === viewportEndRow ? end[0] : this._bufferService.cols;
|
||||
documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow, startCol, endCol));
|
||||
// Draw middle rows
|
||||
const middleRowsCount = viewportCappedEndRow - viewportCappedStartRow - 1;
|
||||
documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow + 1, 0, this._bufferService.cols, middleRowsCount));
|
||||
// Draw final row
|
||||
if (viewportCappedStartRow !== viewportCappedEndRow) {
|
||||
// Only draw viewportEndRow if it's not the same as viewporttartRow
|
||||
const finalEndCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._bufferService.cols;
|
||||
documentFragment.appendChild(this._createSelectionElement(viewportCappedEndRow, 0, finalEndCol));
|
||||
}
|
||||
}
|
||||
this._selectionContainer.appendChild(documentFragment);
|
||||
}
|
||||
this._selectionContainer.appendChild(documentFragment);
|
||||
|
||||
// Compute minimal row range to redraw
|
||||
let renderStartRow = Math.min(oldViewportStart, newViewportStart);
|
||||
let renderEndRow = Math.max(oldViewportEnd, newViewportEnd);
|
||||
|
||||
if (renderEndRow >= 0) {
|
||||
// Clamp to viewport
|
||||
renderStartRow = Math.max(renderStartRow, 0);
|
||||
renderEndRow = Math.min(renderEndRow, rows - 1);
|
||||
|
||||
// Ensure cursor row is included when a selection is present
|
||||
const buffer = this._bufferService.buffer;
|
||||
const cursorViewportRow = buffer.y;
|
||||
if (this._selectionRenderModel.hasSelection && cursorViewportRow >= 0 && cursorViewportRow < rows) {
|
||||
renderStartRow = Math.min(renderStartRow, cursorViewportRow);
|
||||
renderEndRow = Math.max(renderEndRow, cursorViewportRow);
|
||||
}
|
||||
|
||||
this.renderRows(renderStartRow, renderEndRow);
|
||||
}
|
||||
|
||||
// Update last selection state
|
||||
this._lastSelectionStart = start;
|
||||
this._lastSelectionEnd = end;
|
||||
this._lastSelectionColumnMode = columnSelectMode;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,7 +10,7 @@ import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, DEFAULT_ATTR, FgFlags,
|
||||
import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
|
||||
import { IBufferLine } from 'common/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { MockCoreService, MockDecorationService, MockOptionsService } from 'common/TestUtils.test';
|
||||
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';
|
||||
|
||||
@@ -48,9 +48,9 @@ describe('DomRendererRowFactory', () => {
|
||||
|
||||
it('should set correct attributes for double width characters', () => {
|
||||
widthCache.setWidths({ '語': 10 });
|
||||
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, '語', 2, '語'.charCodeAt(0)]));
|
||||
lineData.setCell(0, createCellData(DEFAULT_ATTR, '語', 2));
|
||||
// There should be no element for the following "empty" cell
|
||||
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, '', 0, 0]));
|
||||
lineData.setCell(1, createCellData(DEFAULT_ATTR, '', 0));
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span>語</span>'
|
||||
@@ -117,7 +117,7 @@ describe('DomRendererRowFactory', () => {
|
||||
|
||||
describe('attributes', () => {
|
||||
it('should add class for bold', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
const cell = createCellData(0, 'a', 1);
|
||||
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.BOLD;
|
||||
lineData.setCell(0, cell);
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
|
||||
@@ -127,7 +127,7 @@ describe('DomRendererRowFactory', () => {
|
||||
});
|
||||
|
||||
it('should add class for italic', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
const cell = createCellData(0, 'a', 1);
|
||||
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.ITALIC;
|
||||
lineData.setCell(0, cell);
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
|
||||
@@ -137,7 +137,7 @@ describe('DomRendererRowFactory', () => {
|
||||
});
|
||||
|
||||
it('should add class for dim', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
const cell = createCellData(0, 'a', 1);
|
||||
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.DIM;
|
||||
lineData.setCell(0, cell);
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
|
||||
@@ -148,7 +148,7 @@ describe('DomRendererRowFactory', () => {
|
||||
|
||||
describe('underline', () => {
|
||||
it('should add class for straight underline style', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
const cell = createCellData(0, 'a', 1);
|
||||
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.UNDERLINE;
|
||||
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
|
||||
cell.extended.underlineStyle = UnderlineStyle.SINGLE;
|
||||
@@ -159,7 +159,7 @@ describe('DomRendererRowFactory', () => {
|
||||
);
|
||||
});
|
||||
it('should add class for double underline style', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
const cell = createCellData(0, 'a', 1);
|
||||
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.UNDERLINE;
|
||||
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
|
||||
cell.extended.underlineStyle = UnderlineStyle.DOUBLE;
|
||||
@@ -170,7 +170,7 @@ describe('DomRendererRowFactory', () => {
|
||||
);
|
||||
});
|
||||
it('should add class for curly underline style', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
const cell = createCellData(0, 'a', 1);
|
||||
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.UNDERLINE;
|
||||
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
|
||||
cell.extended.underlineStyle = UnderlineStyle.CURLY;
|
||||
@@ -181,7 +181,7 @@ describe('DomRendererRowFactory', () => {
|
||||
);
|
||||
});
|
||||
it('should add class for double dotted style', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
const cell = createCellData(0, 'a', 1);
|
||||
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.UNDERLINE;
|
||||
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
|
||||
cell.extended.underlineStyle = UnderlineStyle.DOTTED;
|
||||
@@ -192,7 +192,7 @@ describe('DomRendererRowFactory', () => {
|
||||
);
|
||||
});
|
||||
it('should add class for dashed underline style', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
const cell = createCellData(0, 'a', 1);
|
||||
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.UNDERLINE;
|
||||
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
|
||||
cell.extended.underlineStyle = UnderlineStyle.DASHED;
|
||||
@@ -205,7 +205,7 @@ describe('DomRendererRowFactory', () => {
|
||||
});
|
||||
|
||||
it('should add class for overline', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
const cell = createCellData(0, 'a', 1);
|
||||
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.OVERLINE;
|
||||
lineData.setCell(0, cell);
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
|
||||
@@ -215,7 +215,7 @@ describe('DomRendererRowFactory', () => {
|
||||
});
|
||||
|
||||
it('should add class for strikethrough', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
const cell = createCellData(0, 'a', 1);
|
||||
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.STRIKETHROUGH;
|
||||
lineData.setCell(0, cell);
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
|
||||
@@ -225,7 +225,7 @@ describe('DomRendererRowFactory', () => {
|
||||
});
|
||||
|
||||
it('should hide blinking text when blink is off', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
const cell = createCellData(0, 'a', 1);
|
||||
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.BLINK | FgFlags.UNDERLINE;
|
||||
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
|
||||
cell.extended.underlineStyle = UnderlineStyle.SINGLE;
|
||||
@@ -241,7 +241,7 @@ describe('DomRendererRowFactory', () => {
|
||||
});
|
||||
|
||||
it('should add classes for 256 foreground colors', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
const cell = createCellData(0, 'a', 1);
|
||||
cell.fg |= Attributes.CM_P256;
|
||||
for (let i = 0; i < 256; i++) {
|
||||
cell.fg &= ~Attributes.PCOLOR_MASK;
|
||||
@@ -255,7 +255,7 @@ describe('DomRendererRowFactory', () => {
|
||||
});
|
||||
|
||||
it('should add classes for 256 background colors', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
const cell = createCellData(0, 'a', 1);
|
||||
cell.bg |= Attributes.CM_P256;
|
||||
for (let i = 0; i < 256; i++) {
|
||||
cell.bg &= ~Attributes.PCOLOR_MASK;
|
||||
@@ -269,7 +269,7 @@ describe('DomRendererRowFactory', () => {
|
||||
});
|
||||
|
||||
it('should correctly invert colors', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
const cell = createCellData(0, 'a', 1);
|
||||
cell.fg |= Attributes.CM_P16 | 2 | FgFlags.INVERSE;
|
||||
cell.bg |= Attributes.CM_P16 | 1;
|
||||
lineData.setCell(0, cell);
|
||||
@@ -280,7 +280,7 @@ describe('DomRendererRowFactory', () => {
|
||||
});
|
||||
|
||||
it('should correctly invert default fg color', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
const cell = createCellData(0, 'a', 1);
|
||||
cell.fg |= FgFlags.INVERSE;
|
||||
cell.bg |= Attributes.CM_P16 | 1;
|
||||
lineData.setCell(0, cell);
|
||||
@@ -291,7 +291,7 @@ describe('DomRendererRowFactory', () => {
|
||||
});
|
||||
|
||||
it('should correctly invert default bg color', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
const cell = createCellData(0, 'a', 1);
|
||||
cell.fg |= Attributes.CM_P16 | 1 | FgFlags.INVERSE;
|
||||
lineData.setCell(0, cell);
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
|
||||
@@ -301,7 +301,7 @@ describe('DomRendererRowFactory', () => {
|
||||
});
|
||||
|
||||
it('should turn bold fg text bright', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
const cell = createCellData(0, 'a', 1);
|
||||
cell.fg |= FgFlags.BOLD | Attributes.CM_P16;
|
||||
for (let i = 0; i < 8; i++) {
|
||||
cell.fg &= ~Attributes.PCOLOR_MASK;
|
||||
@@ -315,7 +315,7 @@ describe('DomRendererRowFactory', () => {
|
||||
});
|
||||
|
||||
it('should set style attribute for RBG', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
const cell = createCellData(0, 'a', 1);
|
||||
cell.fg |= Attributes.CM_RGB | 1 << 16 | 2 << 8 | 3;
|
||||
cell.bg |= Attributes.CM_RGB | 4 << 16 | 5 << 8 | 6;
|
||||
lineData.setCell(0, cell);
|
||||
@@ -326,7 +326,7 @@ describe('DomRendererRowFactory', () => {
|
||||
});
|
||||
|
||||
it('should correctly invert RGB colors', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
const cell = createCellData(0, 'a', 1);
|
||||
cell.fg |= Attributes.CM_RGB | 1 << 16 | 2 << 8 | 3 | FgFlags.INVERSE;
|
||||
cell.bg |= Attributes.CM_RGB | 4 << 16 | 5 << 8 | 6;
|
||||
lineData.setCell(0, cell);
|
||||
@@ -339,8 +339,8 @@ describe('DomRendererRowFactory', () => {
|
||||
|
||||
describe('selectionForeground', () => {
|
||||
it('should force selected cells with content to be rendered above the background', () => {
|
||||
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]));
|
||||
lineData.setCell(0, createCellData(DEFAULT_ATTR, 'a', 1));
|
||||
lineData.setCell(1, createCellData(DEFAULT_ATTR, 'b', 1));
|
||||
rowFactory.handleSelectionChanged([1, 0], [2, 0], false);
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
@@ -348,7 +348,7 @@ describe('DomRendererRowFactory', () => {
|
||||
);
|
||||
});
|
||||
it('should force whitespace cells to be rendered above the background', () => {
|
||||
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
lineData.setCell(1, createCellData(DEFAULT_ATTR, 'a', 1));
|
||||
rowFactory.handleSelectionChanged([0, 0], [2, 0], false);
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
@@ -374,9 +374,9 @@ describe('DomRendererRowFactory', () => {
|
||||
});
|
||||
|
||||
it('can merge codepoints for equal spacing', () => {
|
||||
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]));
|
||||
lineData.setCell(2, CellData.fromCharData([DEFAULT_ATTR, 'c', 1, 'c'.charCodeAt(0)]));
|
||||
lineData.setCell(0, createCellData(DEFAULT_ATTR, 'a', 1));
|
||||
lineData.setCell(1, createCellData(DEFAULT_ATTR, 'b', 1));
|
||||
lineData.setCell(2, createCellData(DEFAULT_ATTR, 'c', 1));
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span>abc</span>'
|
||||
@@ -385,9 +385,9 @@ describe('DomRendererRowFactory', () => {
|
||||
|
||||
it('should not merge codepoints with different spacing', () => {
|
||||
widthCache.setWidths({ '€': 2 });
|
||||
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, '€', 1, '€'.charCodeAt(0)]));
|
||||
lineData.setCell(2, CellData.fromCharData([DEFAULT_ATTR, 'c', 1, 'c'.charCodeAt(0)]));
|
||||
lineData.setCell(0, createCellData(DEFAULT_ATTR, 'a', 1));
|
||||
lineData.setCell(1, createCellData(DEFAULT_ATTR, '€', 1));
|
||||
lineData.setCell(2, createCellData(DEFAULT_ATTR, 'c', 1));
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span>a</span><span style="letter-spacing: 3px;">€</span><span>c</span>'
|
||||
@@ -395,9 +395,9 @@ describe('DomRendererRowFactory', () => {
|
||||
});
|
||||
|
||||
it('should not merge on FG change', () => {
|
||||
const aColor1 = CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
const aColor1 = createCellData(DEFAULT_ATTR, 'a', 1);
|
||||
aColor1.fg |= Attributes.CM_P16 | 1;
|
||||
const bColor2 = CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]);
|
||||
const bColor2 = createCellData(DEFAULT_ATTR, 'b', 1);
|
||||
bColor2.fg |= Attributes.CM_P16 | 2;
|
||||
lineData.setCell(0, aColor1);
|
||||
lineData.setCell(1, aColor1);
|
||||
@@ -410,11 +410,11 @@ describe('DomRendererRowFactory', () => {
|
||||
});
|
||||
|
||||
it('should not merge cursor cell', () => {
|
||||
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
lineData.setCell(2, CellData.fromCharData([DEFAULT_ATTR, 'X', 1, 'X'.charCodeAt(0)]));
|
||||
lineData.setCell(3, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]));
|
||||
lineData.setCell(4, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]));
|
||||
lineData.setCell(0, createCellData(DEFAULT_ATTR, 'a', 1));
|
||||
lineData.setCell(1, createCellData(DEFAULT_ATTR, 'a', 1));
|
||||
lineData.setCell(2, createCellData(DEFAULT_ATTR, 'X', 1));
|
||||
lineData.setCell(3, createCellData(DEFAULT_ATTR, 'b', 1));
|
||||
lineData.setCell(4, createCellData(DEFAULT_ATTR, 'b', 1));
|
||||
const spans = rowFactory.createRow(lineData, 0, true, undefined, undefined, 2, false, true, 5, widthCache, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span>aa</span><span class="xterm-cursor xterm-cursor-block">X</span><span>bb</span>'
|
||||
@@ -453,7 +453,7 @@ describe('DomRendererRowFactory', () => {
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span class="xterm-bg-1"> </span>'
|
||||
);
|
||||
lineData.setCell(4, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
lineData.setCell(4, createCellData(DEFAULT_ATTR, 'a', 1));
|
||||
spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span class="xterm-bg-1"> </span><span>a</span>'
|
||||
@@ -462,9 +462,9 @@ describe('DomRendererRowFactory', () => {
|
||||
|
||||
it('should apply correct positive or negative spacing', () => {
|
||||
widthCache.setWidths({ '€': 2, '語': 10, '𝄞': 7 }); // €: too small (+3px), 語: exact, 𝄞: too wide (-2px)
|
||||
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, '€', 1, '€'.charCodeAt(0)]));
|
||||
lineData.setCell(2, CellData.fromCharData([DEFAULT_ATTR, 'c', 1, 'c'.charCodeAt(0)]));
|
||||
lineData.setCell(0, createCellData(DEFAULT_ATTR, 'a', 1));
|
||||
lineData.setCell(1, createCellData(DEFAULT_ATTR, '€', 1));
|
||||
lineData.setCell(2, createCellData(DEFAULT_ATTR, 'c', 1));
|
||||
lineData.setCell(3, CellData.fromCharData([DEFAULT_ATTR, '語', 2, 'c'.charCodeAt(0)]));
|
||||
lineData.setCell(4, CellData.fromCharData([DEFAULT_ATTR, '𝄞', 1, 'c'.charCodeAt(0)]));
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -1, -1);
|
||||
@@ -474,13 +474,13 @@ describe('DomRendererRowFactory', () => {
|
||||
});
|
||||
|
||||
it('should not merge across link borders', () => {
|
||||
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
lineData.setCell(2, CellData.fromCharData([DEFAULT_ATTR, 'x', 1, 'x'.charCodeAt(0)]));
|
||||
lineData.setCell(3, CellData.fromCharData([DEFAULT_ATTR, 'x', 1, 'x'.charCodeAt(0)]));
|
||||
lineData.setCell(4, CellData.fromCharData([DEFAULT_ATTR, 'x', 1, 'x'.charCodeAt(0)]));
|
||||
lineData.setCell(5, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]));
|
||||
lineData.setCell(6, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]));
|
||||
lineData.setCell(0, createCellData(DEFAULT_ATTR, 'a', 1));
|
||||
lineData.setCell(1, createCellData(DEFAULT_ATTR, 'a', 1));
|
||||
lineData.setCell(2, createCellData(DEFAULT_ATTR, 'x', 1));
|
||||
lineData.setCell(3, createCellData(DEFAULT_ATTR, 'x', 1));
|
||||
lineData.setCell(4, createCellData(DEFAULT_ATTR, 'x', 1));
|
||||
lineData.setCell(5, createCellData(DEFAULT_ATTR, 'b', 1));
|
||||
lineData.setCell(6, createCellData(DEFAULT_ATTR, 'b', 1));
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, 2, 4);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span>aa</span><span style="text-decoration: underline;">xxx</span><span>bb</span>'
|
||||
@@ -488,10 +488,10 @@ describe('DomRendererRowFactory', () => {
|
||||
});
|
||||
|
||||
it('empty cells included in link underline', () => {
|
||||
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
lineData.setCell(2, CellData.fromCharData([DEFAULT_ATTR, 'x', 1, 'x'.charCodeAt(0)]));
|
||||
lineData.setCell(4, CellData.fromCharData([DEFAULT_ATTR, 'x', 1, 'x'.charCodeAt(0)]));
|
||||
lineData.setCell(0, createCellData(DEFAULT_ATTR, 'a', 1));
|
||||
lineData.setCell(1, createCellData(DEFAULT_ATTR, 'a', 1));
|
||||
lineData.setCell(2, createCellData(DEFAULT_ATTR, 'x', 1));
|
||||
lineData.setCell(4, createCellData(DEFAULT_ATTR, 'x', 1));
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, 2, 4);
|
||||
assert.equal(extractHtml(spans),
|
||||
'<span>aa</span><span style="text-decoration: underline;">x x</span>'
|
||||
@@ -500,7 +500,7 @@ describe('DomRendererRowFactory', () => {
|
||||
|
||||
it('link range gets capped to actual line borders', () => {
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
lineData.setCell(i, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
lineData.setCell(i, createCellData(DEFAULT_ATTR, 'a', 1));
|
||||
}
|
||||
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, true, 5, widthCache, -100, 100);
|
||||
assert.equal(extractHtml(spans),
|
||||
@@ -519,7 +519,7 @@ describe('DomRendererRowFactory', () => {
|
||||
function createEmptyLineData(cols: number): IBufferLine {
|
||||
const lineData = new BufferLine(cols);
|
||||
for (let i = 0; i < cols; i++) {
|
||||
lineData.setCell(i, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]));
|
||||
lineData.setCell(i, NULL_CELL_DATA);
|
||||
}
|
||||
return lineData;
|
||||
}
|
||||
|
||||
@@ -153,6 +153,8 @@ export interface IGestureEvent extends MouseEvent {
|
||||
translationY: number;
|
||||
pageX: number;
|
||||
pageY: number;
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
tapCount: number;
|
||||
}
|
||||
|
||||
@@ -459,6 +461,8 @@ export class Gesture extends Disposable {
|
||||
evt.translationY = touch.pageY - tail(data.rollingPageY)!;
|
||||
evt.pageX = touch.pageX;
|
||||
evt.pageY = touch.pageY;
|
||||
evt.clientX = touch.clientX;
|
||||
evt.clientY = touch.clientY;
|
||||
this._dispatchEvent(evt);
|
||||
|
||||
if (data.rollingPageX.length > 3) {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { CharacterJoinerService } from 'browser/services/CharacterJoinerService'
|
||||
import { BufferLine } from 'common/buffer/BufferLine';
|
||||
import { IBufferLine } from 'common/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { MockBufferService } from 'common/TestUtils.test';
|
||||
import { MockBufferService, createCellData } from 'common/TestUtils.test';
|
||||
|
||||
describe('CharacterJoinerService', () => {
|
||||
let service: ICharacterJoinerService;
|
||||
@@ -25,17 +25,17 @@ describe('CharacterJoinerService', () => {
|
||||
lines.set(4, new BufferLine(0));
|
||||
lines.set(5, lineData([['a', 0x11111111], [' -> b -> c -> '], ['d', 0x22222222]]));
|
||||
const line6 = lineData([['wi']]);
|
||||
line6.resize(line6.length + 1, CellData.fromCharData([0, '¥', 2, '¥'.charCodeAt(0)]));
|
||||
line6.resize(line6.length + 1, CellData.fromCharData([0, '', 0, 0]));
|
||||
line6.resize(line6.length + 1, createCellData(0, '¥', 2));
|
||||
line6.resize(line6.length + 1, createCellData(0, '', 0));
|
||||
let sub = lineData([['deemo']]);
|
||||
let oldSize = line6.length;
|
||||
line6.resize(oldSize + sub.length, CellData.fromCharData([0, '', 0, 0]));
|
||||
line6.resize(oldSize + sub.length, createCellData(0, '', 0));
|
||||
for (let i = 0; i < sub.length; ++i) line6.setCell(i + oldSize, sub.loadCell(i, new CellData()));
|
||||
line6.resize(line6.length + 1, CellData.fromCharData([0, '\xf0\x9f\x98\x81', 1, 128513]));
|
||||
line6.resize(line6.length + 1, CellData.fromCharData([0, ' ', 1, ' '.charCodeAt(0)]));
|
||||
line6.resize(line6.length + 1, createCellData(0, ' ', 1));
|
||||
sub = lineData([['jiabc']]);
|
||||
oldSize = line6.length;
|
||||
line6.resize(oldSize + sub.length, CellData.fromCharData([0, '', 0, 0]));
|
||||
line6.resize(oldSize + sub.length, createCellData(0, '', 0));
|
||||
for (let i = 0; i < sub.length; ++i) line6.setCell(i + oldSize, sub.loadCell(i, new CellData()));
|
||||
lines.set(6, line6);
|
||||
|
||||
@@ -272,8 +272,8 @@ function lineData(data: IPartialLineData[]): IBufferLine {
|
||||
const line = data[i][0];
|
||||
const attr = (data[i][1] || 0) as number;
|
||||
const offset = tline.length;
|
||||
tline.resize(tline.length + line.split('').length, CellData.fromCharData([0, '', 0, 0]));
|
||||
line.split('').map((char, idx) => tline.setCell(idx + offset, CellData.fromCharData([attr, char, 1, char.charCodeAt(0)])));
|
||||
tline.resize(tline.length + line.split('').length, createCellData(0, '', 0));
|
||||
line.split('').map((char, idx) => tline.setCell(idx + offset, createCellData(attr, char, 1)));
|
||||
}
|
||||
return tline;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export class CoreBrowserService extends Disposable implements ICoreBrowserServic
|
||||
|
||||
private _isFocused = false;
|
||||
private _cachedIsFocused: boolean | undefined = undefined;
|
||||
private _screenDprMonitor = this._register(new ScreenDprMonitor(this._window));
|
||||
private _screenDprMonitor: ScreenDprMonitor;
|
||||
|
||||
private readonly _onDprChange = this._register(new Emitter<number>());
|
||||
public readonly onDprChange = this._onDprChange.event;
|
||||
@@ -27,6 +27,8 @@ export class CoreBrowserService extends Disposable implements ICoreBrowserServic
|
||||
) {
|
||||
super();
|
||||
|
||||
this._screenDprMonitor = this._register(new ScreenDprMonitor(this._window));
|
||||
|
||||
// Monitor device pixel ratio
|
||||
this._register(this.onWindowChange(w => this._screenDprMonitor.setWindow(w)));
|
||||
this._register(EventUtils.forward(this._screenDprMonitor.onDprChange, this._onDprChange));
|
||||
|
||||
@@ -40,7 +40,7 @@ export class KeyboardService implements IKeyboardService {
|
||||
}
|
||||
const kittyFlags = this._coreService.kittyKeyboard.flags;
|
||||
return this.useKitty
|
||||
? this._getKittyKeyboard().evaluate(event, kittyFlags, event.repeat ? KittyKeyboardEventType.REPEAT : KittyKeyboardEventType.PRESS)
|
||||
? this._getKittyKeyboard().evaluate(event, kittyFlags, event.repeat ? KittyKeyboardEventType.REPEAT : KittyKeyboardEventType.PRESS, isMac && this._optionsService.rawOptions.macOptionIsMeta)
|
||||
: evaluateKeyboardEvent(event, this._coreService.decPrivateModes.applicationCursorKeys, isMac, this._optionsService.rawOptions.macOptionIsMeta);
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ export class KeyboardService implements IKeyboardService {
|
||||
}
|
||||
const kittyFlags = this._coreService.kittyKeyboard.flags;
|
||||
if (this.useKitty && (kittyFlags & KittyKeyboardFlags.REPORT_EVENT_TYPES)) {
|
||||
return this._getKittyKeyboard().evaluate(event, kittyFlags, KittyKeyboardEventType.RELEASE);
|
||||
return this._getKittyKeyboard().evaluate(event, kittyFlags, KittyKeyboardEventType.RELEASE, isMac && this._optionsService.rawOptions.macOptionIsMeta);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { getWindow } from 'browser/Dom';
|
||||
import { getCoords, getCoordsRelativeToElement } from 'browser/input/Mouse';
|
||||
import { ICharSizeService, IMouseCoordsService, IRenderService } from 'browser/services/Services';
|
||||
|
||||
export class MouseCoordsService implements IMouseCoordsService {
|
||||
public serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@ICharSizeService private readonly _charSizeService: ICharSizeService,
|
||||
@IRenderService private readonly _renderService: IRenderService
|
||||
) {
|
||||
}
|
||||
|
||||
public getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined {
|
||||
return getCoords(
|
||||
window,
|
||||
event,
|
||||
element,
|
||||
colCount,
|
||||
rowCount,
|
||||
this._charSizeService.hasValidSize,
|
||||
this._renderService.dimensions.css.cell.width,
|
||||
this._renderService.dimensions.css.cell.height,
|
||||
isSelection
|
||||
);
|
||||
}
|
||||
|
||||
public getMouseReportCoords(event: MouseEvent, element: HTMLElement): { col: number, row: number, x: number, y: number } | undefined {
|
||||
const coords = getCoordsRelativeToElement(getWindow(element), event, element);
|
||||
if (!this._charSizeService.hasValidSize) {
|
||||
return undefined;
|
||||
}
|
||||
coords[0] = Math.min(Math.max(coords[0], 0), this._renderService.dimensions.css.canvas.width - 1);
|
||||
coords[1] = Math.min(Math.max(coords[1], 0), this._renderService.dimensions.css.canvas.height - 1);
|
||||
return {
|
||||
col: Math.floor(coords[0] / this._renderService.dimensions.css.cell.width),
|
||||
row: Math.floor(coords[1] / this._renderService.dimensions.css.cell.height),
|
||||
x: Math.floor(coords[0]),
|
||||
y: Math.floor(coords[1])
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
import { assert } from 'chai';
|
||||
import { MouseService } from 'browser/services/MouseService';
|
||||
import { MouseStateService } from 'common/services/MouseStateService';
|
||||
import { CoreMouseAction, CoreMouseButton } from 'common/Types';
|
||||
import { IBufferService, ICoreService, ILogService, IOptionsService } from 'common/services/Services';
|
||||
import { MockCoreBrowserService, MockRenderService, MockSelectionService } from 'browser/TestUtils.test';
|
||||
|
||||
function toBytes(s: string | undefined): number[] {
|
||||
if (!s) {
|
||||
return [];
|
||||
}
|
||||
const res: number[] = [];
|
||||
for (let i = 0; i < s.length; ++i) {
|
||||
res.push(s.charCodeAt(i));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
// Minimal mocks for deps that MouseService touches in these tests
|
||||
const bufferService: IBufferService = {
|
||||
buffer: { hasScrollback: true } as any,
|
||||
cols: 500,
|
||||
rows: 500
|
||||
} as any;
|
||||
|
||||
const optionsService: IOptionsService = {
|
||||
rawOptions: {
|
||||
logLevel: 'info',
|
||||
fastScrollSensitivity: 1,
|
||||
scrollSensitivity: 1
|
||||
}
|
||||
} as any;
|
||||
|
||||
const logService: ILogService = {
|
||||
debug: () => {},
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
error: () => {}
|
||||
} as any;
|
||||
|
||||
describe('MouseService _triggerMouseEvent', () => {
|
||||
let mouseService: MouseService;
|
||||
let mouseStateService: MouseStateService;
|
||||
let coreService: ICoreService;
|
||||
let reports: string[];
|
||||
|
||||
beforeEach(() => {
|
||||
reports = [];
|
||||
mouseStateService = new MouseStateService();
|
||||
coreService = {
|
||||
triggerDataEvent: (data: string) => reports.push(data),
|
||||
triggerBinaryEvent: (data: string) => reports.push(data),
|
||||
decPrivateModes: { applicationCursorKeys: false }
|
||||
} as any;
|
||||
|
||||
mouseService = new MouseService(
|
||||
new MockRenderService(),
|
||||
{
|
||||
getMouseReportCoords: (_ev: MouseEvent, _el: HTMLElement) => ({ col: 0, row: 0, x: 0, y: 0 })
|
||||
} as any,
|
||||
mouseStateService,
|
||||
coreService,
|
||||
bufferService,
|
||||
optionsService,
|
||||
new MockSelectionService(),
|
||||
logService,
|
||||
new MockCoreBrowserService()
|
||||
);
|
||||
});
|
||||
|
||||
function trigger(e: Parameters<any>[0]): boolean {
|
||||
return (mouseService as any)._triggerMouseEvent(e);
|
||||
}
|
||||
|
||||
it('NONE', () => {
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), false);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), false);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), false);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), false);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false);
|
||||
});
|
||||
|
||||
it('X10', () => {
|
||||
mouseStateService.activeProtocol = 'X10';
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), false);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), false);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false);
|
||||
});
|
||||
|
||||
it('VT200', () => {
|
||||
mouseStateService.activeProtocol = 'VT200';
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false);
|
||||
});
|
||||
|
||||
it('DRAG', () => {
|
||||
mouseStateService.activeProtocol = 'DRAG';
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), true);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true);
|
||||
});
|
||||
|
||||
it('ANY', () => {
|
||||
mouseStateService.activeProtocol = 'ANY';
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), true);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), true);
|
||||
// should not report in any case
|
||||
// invalid button + action combinations
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.MOVE }), false);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.DOWN }), false);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.UP }), false);
|
||||
// invalid coords
|
||||
assert.equal(trigger({ col: -1, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
|
||||
assert.equal(trigger({ col: 500, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
|
||||
assert.equal(trigger({ col: 0, row: -1, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
|
||||
assert.equal(trigger({ col: 0, row: 500, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
|
||||
});
|
||||
|
||||
describe('coords', () => {
|
||||
it('DEFAULT encoding', () => {
|
||||
mouseStateService.activeProtocol = 'ANY';
|
||||
for (let i = 0; i < bufferService.cols; ++i) {
|
||||
assert.equal(trigger({ col: i, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
|
||||
if (i > 222) {
|
||||
// supress mouse reports if we are out of addressible range (max. 222)
|
||||
assert.deepEqual(toBytes(reports.pop()), []);
|
||||
} else {
|
||||
assert.deepEqual(toBytes(reports.pop()), [0x1b, 0x5b, 0x4d, 0x20, i + 33, 0x21]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('SGR encoding', () => {
|
||||
mouseStateService.activeProtocol = 'ANY';
|
||||
mouseStateService.activeEncoding = 'SGR';
|
||||
for (let i = 0; i < bufferService.cols; ++i) {
|
||||
assert.equal(trigger({ col: i, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.deepEqual(reports.pop(), `\x1b[<0;${i + 1};1M`);
|
||||
}
|
||||
});
|
||||
|
||||
it('SGR_PIXELS encoding', () => {
|
||||
mouseStateService.activeProtocol = 'ANY';
|
||||
mouseStateService.activeEncoding = 'SGR_PIXELS';
|
||||
for (let i = 0; i < 500; ++i) {
|
||||
assert.equal(trigger({ col: 0, row: 0, x: i, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.deepEqual(reports.pop(), `\x1b[<0;${i};0M`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('eventCodes with modifiers (DEFAULT encoding)', () => {
|
||||
// TODO: implement AUX button tests
|
||||
mouseStateService.activeProtocol = 'ANY';
|
||||
mouseStateService.activeEncoding = 'DEFAULT';
|
||||
// all buttons + down + no modifer
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.deepEqual(reports, ['\x1b[M !!', '\x1b[M!!!', '\x1b[M"!!', '\x1b[Ma!!']);
|
||||
reports = [];
|
||||
|
||||
// all buttons + up + no modifier
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.deepEqual(reports, ['\x1b[M#!!', '\x1b[M#!!', '\x1b[M#!!', '\x1b[M`!!']);
|
||||
reports = [];
|
||||
|
||||
// all buttons + move + no modifier
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.deepEqual(reports, ['\x1b[M@!!', '\x1b[MA!!', '\x1b[MB!!', '\x1b[MC!!']);
|
||||
reports = [];
|
||||
|
||||
// button none + move + modifiers
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: false, shift: false }), true);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: true, shift: false }), true);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: true }), true);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: true, shift: false }), true);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: true, shift: true }), true);
|
||||
assert.equal(trigger({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: true, shift: true }), true);
|
||||
assert.deepEqual(reports, ['\x1b[MS!!', '\x1b[MK!!', '\x1b[MG!!', '\x1b[M[!!', '\x1b[MO!!', '\x1b[M_!!']);
|
||||
reports = [];
|
||||
});
|
||||
});
|
||||
@@ -3,45 +3,537 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { getWindow } from 'browser/Dom';
|
||||
import { ICharSizeService, IRenderService, IMouseService } from './Services';
|
||||
import { getCoords, getCoordsRelativeToElement } from 'browser/input/Mouse';
|
||||
import { addDisposableListener } from 'browser/Dom';
|
||||
import { IBufferService, IMouseStateService, ICoreService, ILogService, IOptionsService } from 'common/services/Services';
|
||||
import { CoreMouseAction, CoreMouseButton, CoreMouseEventType, ICoreMouseEvent, IDisposable } from 'common/Types';
|
||||
import { C0 } from 'common/data/EscapeSequences';
|
||||
import { toDisposable } from 'common/Lifecycle';
|
||||
import { ICoreBrowserService, IMouseCoordsService, IMouseService, IMouseServiceTarget, IRenderService, ISelectionService } from './Services';
|
||||
import { Gesture, EventType as GestureEventType, IGestureEvent } from 'browser/scrollable/touch';
|
||||
|
||||
type RequestedMouseEvents = Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener | null>;
|
||||
|
||||
interface IMouseBindContext {
|
||||
readonly target: IMouseServiceTarget;
|
||||
readonly focus: () => void;
|
||||
readonly requestedEvents: RequestedMouseEvents;
|
||||
}
|
||||
|
||||
export class MouseService implements IMouseService {
|
||||
public serviceBrand: undefined;
|
||||
|
||||
private _lastEvent: ICoreMouseEvent | null = null;
|
||||
private _wheelPartialScroll: number = 0;
|
||||
private _touchScrollAccumulator: number = 0;
|
||||
|
||||
constructor(
|
||||
@IRenderService private readonly _renderService: IRenderService,
|
||||
@ICharSizeService private readonly _charSizeService: ICharSizeService
|
||||
@IMouseCoordsService private readonly _mouseCoordsService: IMouseCoordsService,
|
||||
@IMouseStateService private readonly _mouseStateService: IMouseStateService,
|
||||
@ICoreService private readonly _coreService: ICoreService,
|
||||
@IBufferService private readonly _bufferService: IBufferService,
|
||||
@IOptionsService private readonly _optionsService: IOptionsService,
|
||||
@ISelectionService private readonly _selectionService: ISelectionService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService
|
||||
) {
|
||||
}
|
||||
|
||||
public getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined {
|
||||
return getCoords(
|
||||
window,
|
||||
event,
|
||||
element,
|
||||
colCount,
|
||||
rowCount,
|
||||
this._charSizeService.hasValidSize,
|
||||
this._renderService.dimensions.css.cell.width,
|
||||
this._renderService.dimensions.css.cell.height,
|
||||
isSelection
|
||||
);
|
||||
public bindMouse(target: IMouseServiceTarget, register: (disposable: IDisposable) => void, focus: () => void): void {
|
||||
const { element, document } = target;
|
||||
|
||||
/**
|
||||
* Event listener state handling.
|
||||
* We listen to the onProtocolChange event of MouseStateService and put
|
||||
* requested listeners in `requestedEvents`. With this the listeners
|
||||
* have all bits to do the event listener juggling.
|
||||
* Note: 'mousedown' currently is "always on" and not managed
|
||||
* by onProtocolChange.
|
||||
*/
|
||||
const requestedEvents: RequestedMouseEvents = {
|
||||
mouseup: null,
|
||||
wheel: null,
|
||||
mousedrag: null,
|
||||
mousemove: null
|
||||
};
|
||||
const ctx: IMouseBindContext = { target, focus, requestedEvents };
|
||||
const eventListeners: Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener> = {
|
||||
mouseup: (ev: Event) => this._handleMouseUp(ctx, ev as MouseEvent),
|
||||
wheel: (ev: Event) => this._handleWheel(ctx, ev as WheelEvent),
|
||||
mousedrag: (ev: Event) => this._handleMouseDrag(ctx, ev as MouseEvent),
|
||||
mousemove: (ev: Event) => this._handleMouseMove(ctx, ev as MouseEvent)
|
||||
};
|
||||
register(this._mouseStateService.onProtocolChange(events => {
|
||||
this._handleProtocolChange(ctx, eventListeners, events);
|
||||
}));
|
||||
// force initial onProtocolChange so we dont miss early mouse requests
|
||||
this._mouseStateService.activeProtocol = this._mouseStateService.activeProtocol;
|
||||
|
||||
// Ensure document-level listeners are removed on dispose
|
||||
register(toDisposable(() => {
|
||||
if (requestedEvents.mouseup) {
|
||||
document.removeEventListener('mouseup', requestedEvents.mouseup);
|
||||
}
|
||||
if (requestedEvents.mousedrag) {
|
||||
document.removeEventListener('mousemove', requestedEvents.mousedrag);
|
||||
}
|
||||
}));
|
||||
|
||||
/**
|
||||
* "Always on" event listeners.
|
||||
*/
|
||||
register(addDisposableListener(element, 'mousedown', (ev: MouseEvent) => this._handleMouseDown(ctx, ev)));
|
||||
register(addDisposableListener(element, 'wheel', (ev: WheelEvent) => this._handlePassiveWheel(ctx, ev), { passive: false }));
|
||||
register(Gesture.addTarget(target.screenElement));
|
||||
register(addDisposableListener(target.screenElement, GestureEventType.START, () => this._handleTouchStart()));
|
||||
register(addDisposableListener(target.screenElement, GestureEventType.CHANGE, (e: IGestureEvent) => this._handleTouchChange(ctx, e)));
|
||||
}
|
||||
|
||||
public getMouseReportCoords(event: MouseEvent, element: HTMLElement): { col: number, row: number, x: number, y: number } | undefined {
|
||||
const coords = getCoordsRelativeToElement(getWindow(element), event, element);
|
||||
if (!this._charSizeService.hasValidSize) {
|
||||
return undefined;
|
||||
private _sendEvent(ctx: IMouseBindContext, ev: MouseEvent | WheelEvent): boolean {
|
||||
// Get mouse coordinates
|
||||
const pos = this._mouseCoordsService.getMouseReportCoords(ev as MouseEvent, ctx.target.screenElement);
|
||||
if (!pos) {
|
||||
return false;
|
||||
}
|
||||
coords[0] = Math.min(Math.max(coords[0], 0), this._renderService.dimensions.css.canvas.width - 1);
|
||||
coords[1] = Math.min(Math.max(coords[1], 0), this._renderService.dimensions.css.canvas.height - 1);
|
||||
|
||||
let but: CoreMouseButton;
|
||||
let action: CoreMouseAction | undefined;
|
||||
switch ((ev as MouseEvent & { overrideType?: string }).overrideType || ev.type) {
|
||||
case 'mousemove':
|
||||
action = CoreMouseAction.MOVE;
|
||||
if (ev.buttons === undefined) {
|
||||
// buttons is not supported on macOS, try to get a value from button instead
|
||||
but = CoreMouseButton.NONE;
|
||||
if (ev.button !== undefined) {
|
||||
but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
|
||||
}
|
||||
} else {
|
||||
// according to MDN buttons only reports up to button 5 (AUX2)
|
||||
but = ev.buttons & 1 ? CoreMouseButton.LEFT :
|
||||
ev.buttons & 4 ? CoreMouseButton.MIDDLE :
|
||||
ev.buttons & 2 ? CoreMouseButton.RIGHT :
|
||||
CoreMouseButton.NONE; // fallback to NONE
|
||||
}
|
||||
break;
|
||||
case 'mouseup':
|
||||
action = CoreMouseAction.UP;
|
||||
but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
|
||||
break;
|
||||
case 'mousedown':
|
||||
action = CoreMouseAction.DOWN;
|
||||
but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
|
||||
break;
|
||||
case 'wheel':
|
||||
if (!this._mouseStateService.allowCustomWheelEvent(ev as WheelEvent)) {
|
||||
return false;
|
||||
}
|
||||
const deltaY = (ev as WheelEvent).deltaY;
|
||||
if (deltaY === 0) {
|
||||
return false;
|
||||
}
|
||||
const lines = this._consumeWheelEvent(
|
||||
ev as WheelEvent,
|
||||
this._renderService?.dimensions?.device?.cell?.height,
|
||||
this._coreBrowserService?.dpr
|
||||
);
|
||||
if (lines === 0) {
|
||||
return false;
|
||||
}
|
||||
action = deltaY < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN;
|
||||
but = CoreMouseButton.WHEEL;
|
||||
break;
|
||||
default:
|
||||
// dont handle other event types by accident
|
||||
return false;
|
||||
}
|
||||
|
||||
// exit if we cannot determine valid button/action values
|
||||
// do nothing for higher buttons than wheel
|
||||
if (action === undefined || but === undefined || but > CoreMouseButton.WHEEL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this._triggerMouseEvent({
|
||||
col: pos.col,
|
||||
row: pos.row,
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
button: but,
|
||||
action,
|
||||
ctrl: ev.ctrlKey,
|
||||
alt: ev.altKey,
|
||||
shift: ev.shiftKey
|
||||
});
|
||||
}
|
||||
|
||||
private _handleMouseUp(ctx: IMouseBindContext, ev: MouseEvent): void {
|
||||
this._sendEvent(ctx, ev);
|
||||
if (!ev.buttons) {
|
||||
// if no other button is held remove global handlers
|
||||
if (ctx.requestedEvents.mouseup) {
|
||||
ctx.target.document.removeEventListener('mouseup', ctx.requestedEvents.mouseup);
|
||||
}
|
||||
if (ctx.requestedEvents.mousedrag) {
|
||||
ctx.target.document.removeEventListener('mousemove', ctx.requestedEvents.mousedrag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _handleWheel(ctx: IMouseBindContext, ev: WheelEvent): false {
|
||||
this._sendEvent(ctx, ev);
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
private _handleMouseDrag(ctx: IMouseBindContext, ev: MouseEvent): void {
|
||||
// deal only with move while a button is held
|
||||
if (ev.buttons) {
|
||||
this._sendEvent(ctx, ev);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleMouseMove(ctx: IMouseBindContext, ev: MouseEvent): void {
|
||||
// deal only with move without any button
|
||||
if (!ev.buttons) {
|
||||
this._sendEvent(ctx, ev);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleMouseDown(ctx: IMouseBindContext, ev: MouseEvent): void {
|
||||
ev.preventDefault();
|
||||
ctx.focus();
|
||||
|
||||
// Don't send the mouse button to the pty if mouse events are disabled or
|
||||
// if the selection manager is having selection forced (ie. a modifier is
|
||||
// held).
|
||||
if (!this._mouseStateService.areMouseEventsActive || this._selectionService.shouldForceSelection(ev)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._sendEvent(ctx, ev);
|
||||
|
||||
// Register additional global handlers which should keep reporting outside
|
||||
// of the terminal element.
|
||||
// Note: Other emulators also do this for 'mousedown' while a button
|
||||
// is held, we currently limit 'mousedown' to the terminal only.
|
||||
if (ctx.requestedEvents.mouseup) {
|
||||
ctx.target.document.addEventListener('mouseup', ctx.requestedEvents.mouseup);
|
||||
}
|
||||
if (ctx.requestedEvents.mousedrag) {
|
||||
ctx.target.document.addEventListener('mousemove', ctx.requestedEvents.mousedrag);
|
||||
}
|
||||
}
|
||||
|
||||
private _handlePassiveWheel(ctx: IMouseBindContext, ev: WheelEvent): false | void {
|
||||
// do nothing, if app side handles wheel itself
|
||||
if (ctx.requestedEvents.wheel) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._mouseStateService.allowCustomWheelEvent(ev)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this._bufferService.buffer.hasScrollback) {
|
||||
// Convert wheel events into up/down events when the buffer does not have scrollback, this
|
||||
// enables scrolling in apps hosted in the alt buffer such as vim or tmux even when mouse
|
||||
// events are not enabled.
|
||||
// This used implementation used get the actual lines/partial lines scrolled from the
|
||||
// viewport but since moving to the new viewport implementation has been simplified to
|
||||
// simply send a single up or down sequence.
|
||||
|
||||
// Do nothing if there's no vertical scroll
|
||||
const deltaY = ev.deltaY;
|
||||
if (deltaY === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const lines = this._consumeWheelEvent(
|
||||
ev,
|
||||
this._renderService?.dimensions?.device?.cell?.height,
|
||||
this._coreBrowserService?.dpr
|
||||
);
|
||||
if (lines === 0) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Construct and send sequences
|
||||
const sequence = C0.ESC + (this._coreService.decPrivateModes.applicationCursorKeys ? 'O' : '[') + (ev.deltaY < 0 ? 'A' : 'B');
|
||||
this._coreService.triggerDataEvent(sequence, true);
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleTouchStart(): void {
|
||||
this._touchScrollAccumulator = 0;
|
||||
}
|
||||
|
||||
private _handleTouchChange(ctx: IMouseBindContext, e: IGestureEvent): void {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// When mouse protocol has wheel events active, send as mouse wheel events.
|
||||
if (ctx.requestedEvents.wheel) {
|
||||
this._handleTouchScrollAsWheel(ctx, e);
|
||||
return;
|
||||
}
|
||||
|
||||
// When in alt buffer (no scrollback), send up/down key sequences.
|
||||
if (!this._bufferService.buffer.hasScrollback) {
|
||||
this._handleTouchScrollAsKeys(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal scrollback: delegate to viewport scrolling when available.
|
||||
ctx.target.handleTouchScroll?.(e.translationY);
|
||||
}
|
||||
|
||||
private _handleTouchScrollAsKeys(e: IGestureEvent): void {
|
||||
const cellHeight = this._renderService?.dimensions.css.cell.height;
|
||||
if (!cellHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._touchScrollAccumulator -= e.translationY;
|
||||
const lines = Math.trunc(this._touchScrollAccumulator / cellHeight);
|
||||
if (lines === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._touchScrollAccumulator -= lines * cellHeight;
|
||||
const sequence = C0.ESC
|
||||
+ (this._coreService.decPrivateModes.applicationCursorKeys ? 'O' : '[')
|
||||
+ (lines < 0 ? 'A' : 'B');
|
||||
for (let i = 0; i < Math.abs(lines); i++) {
|
||||
this._coreService.triggerDataEvent(sequence, true);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleTouchScrollAsWheel(ctx: IMouseBindContext, e: IGestureEvent): void {
|
||||
const cellHeight = this._renderService?.dimensions.css.cell.height;
|
||||
if (!cellHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._touchScrollAccumulator -= e.translationY;
|
||||
const lines = Math.trunc(this._touchScrollAccumulator / cellHeight);
|
||||
if (lines === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._touchScrollAccumulator -= lines * cellHeight;
|
||||
const pos = this._mouseCoordsService.getMouseReportCoords(e, ctx.target.screenElement);
|
||||
if (!pos) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < Math.abs(lines); i++) {
|
||||
this._triggerMouseEvent({
|
||||
col: pos.col,
|
||||
row: pos.row,
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
button: CoreMouseButton.WHEEL,
|
||||
action: lines < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN,
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
shift: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this._lastEvent = null;
|
||||
this._wheelPartialScroll = 0;
|
||||
this._touchScrollAccumulator = 0;
|
||||
}
|
||||
|
||||
private _handleProtocolChange(ctx: IMouseBindContext, eventListeners: Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener>, events: CoreMouseEventType): void {
|
||||
const { element, document } = ctx.target;
|
||||
const { requestedEvents } = ctx;
|
||||
// apply global changes on events
|
||||
if (events) {
|
||||
if (this._optionsService.rawOptions.logLevel === 'debug') {
|
||||
this._logService.debug('Binding to mouse events:', this._explainEvents(events));
|
||||
}
|
||||
element.classList.add('enable-mouse-events');
|
||||
this._selectionService.disable();
|
||||
} else {
|
||||
this._logService.debug('Unbinding from mouse events.');
|
||||
element.classList.remove('enable-mouse-events');
|
||||
this._selectionService.enable();
|
||||
}
|
||||
|
||||
// add/remove handlers from requestedEvents
|
||||
if (!(events & CoreMouseEventType.MOVE)) {
|
||||
if (requestedEvents.mousemove) {
|
||||
element.removeEventListener('mousemove', requestedEvents.mousemove);
|
||||
}
|
||||
requestedEvents.mousemove = null;
|
||||
} else if (!requestedEvents.mousemove) {
|
||||
element.addEventListener('mousemove', eventListeners.mousemove);
|
||||
requestedEvents.mousemove = eventListeners.mousemove;
|
||||
}
|
||||
|
||||
if (!(events & CoreMouseEventType.WHEEL)) {
|
||||
if (requestedEvents.wheel) {
|
||||
element.removeEventListener('wheel', requestedEvents.wheel);
|
||||
}
|
||||
requestedEvents.wheel = null;
|
||||
} else if (!requestedEvents.wheel) {
|
||||
element.addEventListener('wheel', eventListeners.wheel, { passive: false });
|
||||
requestedEvents.wheel = eventListeners.wheel;
|
||||
}
|
||||
|
||||
if (!(events & CoreMouseEventType.UP)) {
|
||||
if (requestedEvents.mouseup) {
|
||||
document.removeEventListener('mouseup', requestedEvents.mouseup);
|
||||
}
|
||||
requestedEvents.mouseup = null;
|
||||
} else {
|
||||
requestedEvents.mouseup ??= eventListeners.mouseup;
|
||||
}
|
||||
|
||||
if (!(events & CoreMouseEventType.DRAG)) {
|
||||
if (requestedEvents.mousedrag) {
|
||||
document.removeEventListener('mousemove', requestedEvents.mousedrag);
|
||||
}
|
||||
requestedEvents.mousedrag = null;
|
||||
} else {
|
||||
requestedEvents.mousedrag ??= eventListeners.mousedrag;
|
||||
}
|
||||
}
|
||||
|
||||
private _applyScrollModifier(amount: number, ev: WheelEvent): number {
|
||||
// Multiply the scroll speed when the modifier key is pressed
|
||||
if (ev.altKey || ev.ctrlKey || ev.shiftKey) {
|
||||
return amount * this._optionsService.rawOptions.fastScrollSensitivity * this._optionsService.rawOptions.scrollSensitivity;
|
||||
}
|
||||
return amount * this._optionsService.rawOptions.scrollSensitivity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a wheel event, accounting for partial scrolls for trackpad, mouse scrolls.
|
||||
* This prevents hyper-sensitive scrolling in alt buffer.
|
||||
*/
|
||||
private _consumeWheelEvent(ev: WheelEvent, cellHeight?: number, dpr?: number): number {
|
||||
// Do nothing if it's not a vertical scroll event
|
||||
if (ev.deltaY === 0 || ev.shiftKey) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (cellHeight === undefined || dpr === undefined) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const targetWheelEventPixels = cellHeight / dpr;
|
||||
let amount = this._applyScrollModifier(ev.deltaY, ev);
|
||||
|
||||
if (ev.deltaMode === WheelEvent.DOM_DELTA_PIXEL) {
|
||||
amount /= (targetWheelEventPixels + 0.0); // Prevent integer division
|
||||
|
||||
const isLikelyTrackpad = Math.abs(ev.deltaY) < 50;
|
||||
if (isLikelyTrackpad) {
|
||||
amount *= 0.3;
|
||||
}
|
||||
|
||||
this._wheelPartialScroll += amount;
|
||||
amount = Math.floor(Math.abs(this._wheelPartialScroll)) * (this._wheelPartialScroll > 0 ? 1 : -1);
|
||||
this._wheelPartialScroll %= 1;
|
||||
} else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
|
||||
amount *= this._bufferService.rows;
|
||||
}
|
||||
return amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers a mouse event to be sent.
|
||||
*
|
||||
* Returns true if the event passed all protocol restrictions and a report
|
||||
* was sent, otherwise false. The return value may be used to decide whether
|
||||
* the default event action in the browser component should be omitted.
|
||||
*
|
||||
* Note: The method will change values of the given event object
|
||||
* to fulfill protocol and encoding restrictions.
|
||||
*/
|
||||
private _triggerMouseEvent(e: ICoreMouseEvent): boolean {
|
||||
// range check for col/row
|
||||
if (e.col < 0 || e.col >= this._bufferService.cols
|
||||
|| e.row < 0 || e.row >= this._bufferService.rows) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// filter nonsense combinations of button + action
|
||||
if (e.button === CoreMouseButton.WHEEL && e.action === CoreMouseAction.MOVE) {
|
||||
return false;
|
||||
}
|
||||
if (e.button === CoreMouseButton.NONE && e.action !== CoreMouseAction.MOVE) {
|
||||
return false;
|
||||
}
|
||||
if (e.button !== CoreMouseButton.WHEEL && (e.action === CoreMouseAction.LEFT || e.action === CoreMouseAction.RIGHT)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// report 1-based coords
|
||||
e.col++;
|
||||
e.row++;
|
||||
|
||||
// debounce move events at grid or pixel level
|
||||
if (e.action === CoreMouseAction.MOVE
|
||||
&& this._lastEvent
|
||||
&& this._equalEvents(this._lastEvent, e, this._mouseStateService.isPixelEncoding)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// apply protocol restrictions
|
||||
if (!this._mouseStateService.restrictMouseEvent(e)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// encode report and send
|
||||
const report = this._mouseStateService.encodeMouseEvent(e);
|
||||
if (report) {
|
||||
if (this._mouseStateService.isDefaultEncoding) {
|
||||
this._coreService.triggerBinaryEvent(report);
|
||||
} else {
|
||||
this._coreService.triggerDataEvent(report, true);
|
||||
}
|
||||
}
|
||||
|
||||
this._lastEvent = e;
|
||||
return true;
|
||||
}
|
||||
|
||||
private _explainEvents(events: CoreMouseEventType): { [event: string]: boolean } {
|
||||
return {
|
||||
col: Math.floor(coords[0] / this._renderService.dimensions.css.cell.width),
|
||||
row: Math.floor(coords[1] / this._renderService.dimensions.css.cell.height),
|
||||
x: Math.floor(coords[0]),
|
||||
y: Math.floor(coords[1])
|
||||
down: !!(events & CoreMouseEventType.DOWN),
|
||||
up: !!(events & CoreMouseEventType.UP),
|
||||
drag: !!(events & CoreMouseEventType.DRAG),
|
||||
move: !!(events & CoreMouseEventType.MOVE),
|
||||
wheel: !!(events & CoreMouseEventType.WHEEL)
|
||||
};
|
||||
}
|
||||
|
||||
private _equalEvents(e1: ICoreMouseEvent, e2: ICoreMouseEvent, pixels: boolean): boolean {
|
||||
if (pixels) {
|
||||
if (e1.x !== e2.x) return false;
|
||||
if (e1.y !== e2.y) return false;
|
||||
} else {
|
||||
if (e1.col !== e2.col) return false;
|
||||
if (e1.row !== e2.row) return false;
|
||||
}
|
||||
if (e1.button !== e2.button) return false;
|
||||
if (e1.action !== e2.action) return false;
|
||||
if (e1.ctrl !== e2.ctrl) return false;
|
||||
if (e1.alt !== e2.alt) return false;
|
||||
if (e1.shift !== e2.shift) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { assert } from 'chai';
|
||||
import { SelectionService, SelectionMode } from './SelectionService';
|
||||
import { SelectionModel } from 'browser/selection/SelectionModel';
|
||||
import { IBufferLine } from 'common/Types';
|
||||
import { MockBufferService, MockOptionsService, MockCoreService } from 'common/TestUtils.test';
|
||||
import { MockBufferService, MockOptionsService, MockCoreService, createCellData } from 'common/TestUtils.test';
|
||||
import { BufferLine } from 'common/buffer/BufferLine';
|
||||
import { IBufferService, IOptionsService } from 'common/services/Services';
|
||||
import { MockCoreBrowserService, MockMouseService, MockRenderService } from 'browser/TestUtils.test';
|
||||
@@ -57,14 +57,14 @@ describe('SelectionService', () => {
|
||||
function stringToRow(text: string): IBufferLine {
|
||||
const result = new BufferLine(text.length);
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
result.setCell(i, CellData.fromCharData([0, text.charAt(i), 1, text.charCodeAt(i)]));
|
||||
result.setCell(i, createCellData(0, text.charAt(i), 1));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function stringArrayToRow(chars: string[]): IBufferLine {
|
||||
const line = new BufferLine(chars.length);
|
||||
chars.map((c, idx) => line.setCell(idx, CellData.fromCharData([0, c, 1, c.charCodeAt(0)])));
|
||||
chars.map((c, idx) => line.setCell(idx, createCellData(0, c, 1)));
|
||||
return line;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { getCoordsRelativeToElement } from 'browser/input/Mouse';
|
||||
import { moveToCellSequence } from 'browser/input/MoveToCell';
|
||||
import { SelectionModel } from 'browser/selection/SelectionModel';
|
||||
import { ISelectionRedrawRequestEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
|
||||
import { ICoreBrowserService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services';
|
||||
import { ICoreBrowserService, IMouseCoordsService, IRenderService, ISelectionService } from 'browser/services/Services';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import * as Browser from 'common/Platform';
|
||||
import { IBufferLine, ICellData, IDisposable } from 'common/Types';
|
||||
@@ -126,7 +126,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
private readonly _linkifier: ILinkifier2,
|
||||
@IBufferService private readonly _bufferService: IBufferService,
|
||||
@ICoreService private readonly _coreService: ICoreService,
|
||||
@IMouseService private readonly _mouseService: IMouseService,
|
||||
@IMouseCoordsService private readonly _mouseCoordsService: IMouseCoordsService,
|
||||
@IOptionsService private readonly _optionsService: IOptionsService,
|
||||
@IRenderService private readonly _renderService: IRenderService,
|
||||
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService
|
||||
@@ -395,7 +395,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
private _getMouseBufferCoords(event: MouseEvent): [number, number] | undefined {
|
||||
const coords = this._mouseService.getCoords(event, this._screenElement, this._bufferService.cols, this._bufferService.rows, true);
|
||||
const coords = this._mouseCoordsService.getCoords(event, this._screenElement, this._bufferService.cols, this._bufferService.rows, true);
|
||||
if (!coords) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -715,7 +715,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
|
||||
if (this.selectionText.length <= 1 && timeElapsed < ALT_CLICK_MOVE_CURSOR_TIME && event.altKey && this._optionsService.rawOptions.altClickMovesCursor) {
|
||||
if (this._bufferService.buffer.ybase === this._bufferService.buffer.ydisp) {
|
||||
const coordinates = this._mouseService.getCoords(
|
||||
const coordinates = this._mouseCoordsService.getCoords(
|
||||
event,
|
||||
this._element,
|
||||
this._bufferService.cols,
|
||||
|
||||
@@ -49,14 +49,28 @@ export interface ICoreBrowserService {
|
||||
readonly dpr: number;
|
||||
}
|
||||
|
||||
export const IMouseService = createDecorator<IMouseService>('MouseService');
|
||||
export interface IMouseService {
|
||||
export const IMouseCoordsService = createDecorator<IMouseCoordsService>('MouseCoordsService');
|
||||
export interface IMouseCoordsService {
|
||||
serviceBrand: undefined;
|
||||
|
||||
getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined;
|
||||
getMouseReportCoords(event: MouseEvent, element: HTMLElement): { col: number, row: number, x: number, y: number } | undefined;
|
||||
}
|
||||
|
||||
export const IMouseService = createDecorator<IMouseService>('MouseService');
|
||||
export interface IMouseService {
|
||||
serviceBrand: undefined;
|
||||
|
||||
bindMouse(target: IMouseServiceTarget, register: (disposable: IDisposable) => void, focus: () => void): void;
|
||||
reset(): void;
|
||||
}
|
||||
export interface IMouseServiceTarget {
|
||||
element: HTMLElement;
|
||||
screenElement: HTMLElement;
|
||||
document: Document;
|
||||
handleTouchScroll?(amount: number): void;
|
||||
}
|
||||
|
||||
export const IRenderService = createDecorator<IRenderService>('RenderService');
|
||||
export interface IRenderService extends IDisposable {
|
||||
serviceBrand: undefined;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user