Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f49f321c1 | ||
|
|
c0007301c1 | ||
|
|
0d8996395c | ||
|
|
e24b043ab2 | ||
|
|
6f15b0f916 | ||
|
|
33eb1dd45e | ||
|
|
0ca8107055 | ||
|
|
d844070855 |
@@ -22,9 +22,6 @@ build/
|
||||
yarn.lock
|
||||
test-results/
|
||||
|
||||
# Tracing/debug files
|
||||
.cursor/nal-trace/
|
||||
|
||||
# Keep bundled code out of Git
|
||||
dist/
|
||||
demo/dist/
|
||||
|
||||
@@ -253,7 +253,6 @@ Xterm.js is used in many world-class applications to provide great terminal expe
|
||||
- [**SSH Connection Manager**](https://github.com/Amtrend/ssh-manager): Modern web-based SSH terminal and SFTP manager with AES-256 encryption, PWA support, and session management.
|
||||
- [**WooTTY**](https://github.com/icoretech/wootty): Flawless browser terminal for real operators.
|
||||
- [**Orca**](https://github.com/stablyai/orca): Agentic development environment for 100x builders.
|
||||
- [**4bit**](https://ciembor.github.io/4bit): Terminal Color Scheme Designer.
|
||||
- [And much more...](https://github.com/xtermjs/xterm.js/network/dependents?package_id=UGFja2FnZS0xNjYzMjc4OQ%3D%3D)
|
||||
|
||||
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it on our list. Please add any new contributions to the end of the list.
|
||||
|
||||
@@ -91,7 +91,7 @@ export class SixelHandler implements IDcsHandler, IResetHandler {
|
||||
const height = this._dec.height;
|
||||
|
||||
// partial fix for https://github.com/jerch/xterm-addon-image/issues/37
|
||||
if (!width || !height) {
|
||||
if (!width || ! height) {
|
||||
if (height) {
|
||||
this._storage.advanceCursor(height);
|
||||
}
|
||||
|
||||
@@ -533,8 +533,8 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
|
||||
|
||||
private _sendResponse(id: number, message: string, quiet: number, placementId?: number): void {
|
||||
const isOk = message === 'OK';
|
||||
if (isOk && quiet >= 1) return;
|
||||
if (!isOk && quiet >= 2) return;
|
||||
if (isOk && quiet === 1) return;
|
||||
if (!isOk && quiet === 2) return;
|
||||
|
||||
const pPart = placementId ? `,p=${placementId}` : '';
|
||||
const response = `\x1b_Gi=${id}${pPart};${message}\x1b\\`;
|
||||
|
||||
@@ -617,18 +617,6 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
strictEqual(await ctx.page.evaluate('window.kittyGotResponse'), false);
|
||||
});
|
||||
|
||||
test('suppresses OK response when q=2', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyGotResponse = false;
|
||||
(window as any).term.onData(() => { (window as any).kittyGotResponse = true; });
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=81,a=q,q=2,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await timeout(100);
|
||||
|
||||
strictEqual(await ctx.page.evaluate('window.kittyGotResponse'), false);
|
||||
});
|
||||
|
||||
test('suppresses error response when q=2', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyGotResponse = false;
|
||||
@@ -809,30 +797,6 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
|
||||
strictEqual(await ctx.page.evaluate('window.kittyGotResponse'), false);
|
||||
});
|
||||
|
||||
test('a=t OK suppressed by q=2', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyGotResponse = false;
|
||||
(window as any).term.onData(() => { (window as any).kittyGotResponse = true; });
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=181,a=t,q=2,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await timeout(100);
|
||||
|
||||
strictEqual(await ctx.page.evaluate('window.kittyGotResponse'), false);
|
||||
});
|
||||
|
||||
test('a=T OK suppressed by q=2', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyGotResponse = false;
|
||||
(window as any).term.onData(() => { (window as any).kittyGotResponse = true; });
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=191,a=T,q=2,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await timeout(100);
|
||||
|
||||
strictEqual(await ctx.page.evaluate('window.kittyGotResponse'), false);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Transmission medium rejection', () => {
|
||||
@@ -1182,22 +1146,6 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
strictEqual(await getImageStorageLength(), 1);
|
||||
});
|
||||
|
||||
test('OK response suppressed by q=2', async () => {
|
||||
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=221;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await timeout(100);
|
||||
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyGotResponse = false;
|
||||
(window as any).term.onData(() => { (window as any).kittyGotResponse = true; });
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Ga=p,i=221,q=2\x1b\\`);
|
||||
await timeout(100);
|
||||
|
||||
strictEqual(await ctx.page.evaluate('window.kittyGotResponse'), false);
|
||||
strictEqual(await getImageStorageLength(), 1);
|
||||
});
|
||||
|
||||
test('ENOENT error suppressed by q=2', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyGotResponse = false;
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"package": "webpack",
|
||||
"pretest": "npm run build",
|
||||
"test": "nyc mocha out/**/*.test.js",
|
||||
"prepublishOnly": "npm run package"
|
||||
"prepublish": "npm run package"
|
||||
},
|
||||
"keywords": [
|
||||
"font",
|
||||
@@ -37,7 +37,7 @@
|
||||
"devDependencies": {
|
||||
"@types/lru-cache": "^5.1.0",
|
||||
"@types/opentype.js": "^0.7.0",
|
||||
"axios": "^1.15.2",
|
||||
"axios": "^1.15.0",
|
||||
"font-finder": "^1.1.0",
|
||||
"mkdirp": "0.5.5",
|
||||
"yauzl": "^3.2.1"
|
||||
|
||||
@@ -358,9 +358,7 @@ export class GlyphRenderer extends Disposable {
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, activeBuffer.subarray(0, bufferLength), gl.STREAM_DRAW);
|
||||
|
||||
// Bind the atlas page texture if they have changed. AtlasPage.version is globally
|
||||
// monotonic, so a page object swap at the same index (which happens after a page merge)
|
||||
// is detected by the same comparison.
|
||||
// Bind the atlas page texture if they have changed
|
||||
for (let i = 0; i < this._atlas.pages.length; i++) {
|
||||
if (this._atlas.pages[i].version !== this._atlasTextures[i].version) {
|
||||
this._bindAtlasPageTexture(gl, this._atlas, i);
|
||||
@@ -373,10 +371,6 @@ export class GlyphRenderer extends Disposable {
|
||||
|
||||
public setAtlas(atlas: ITextureAtlas): void {
|
||||
this._atlas = atlas;
|
||||
this.invalidateAtlasTextures();
|
||||
}
|
||||
|
||||
public invalidateAtlasTextures(): void {
|
||||
for (const glTexture of this._atlasTextures) {
|
||||
glTexture.version = -1;
|
||||
}
|
||||
|
||||
@@ -336,9 +336,8 @@ export class RectangleRenderer extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
if (vertices.attributes.length < offset + INDICES_PER_RECTANGLE) {
|
||||
// +1 for the viewport-clear rectangle at offset 0.
|
||||
vertices.attributes = expandFloat32Array(vertices.attributes, (this._terminal.rows * this._terminal.cols + 1) * INDICES_PER_RECTANGLE);
|
||||
if (vertices.attributes.length < offset + 4) {
|
||||
vertices.attributes = expandFloat32Array(vertices.attributes, this._terminal.rows * this._terminal.cols * INDICES_PER_RECTANGLE);
|
||||
}
|
||||
$x1 = startX * this._dimensions.device.cell.width;
|
||||
$y1 = y * this._dimensions.device.cell.height;
|
||||
|
||||
@@ -133,9 +133,7 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
|
||||
private _requestClearModel = false;
|
||||
public beginFrame(): boolean {
|
||||
const result = this._requestClearModel;
|
||||
this._requestClearModel = false;
|
||||
return result;
|
||||
return this._requestClearModel;
|
||||
}
|
||||
|
||||
public clearTexture(): void {
|
||||
@@ -195,7 +193,7 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
|
||||
// Merge into the new page
|
||||
const mergedPage = this._mergePages(mergingPages, mergedPageIndex);
|
||||
mergedPage.version = ++AtlasPage.nextVersion;
|
||||
mergedPage.version++;
|
||||
|
||||
// Delete the pages, shifting glyph texture pages as needed
|
||||
for (let i = sortedMergingPagesIndexes.length - 1; i >= 0; i--) {
|
||||
@@ -253,7 +251,7 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
for (const g of adjustingPage.glyphs) {
|
||||
g.texturePage--;
|
||||
}
|
||||
adjustingPage.version = ++AtlasPage.nextVersion;
|
||||
adjustingPage.version++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -939,7 +937,7 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
rasterizedGlyph.size.y
|
||||
);
|
||||
activePage.addGlyph(rasterizedGlyph);
|
||||
activePage.version = ++AtlasPage.nextVersion;
|
||||
activePage.version++;
|
||||
|
||||
return rasterizedGlyph;
|
||||
}
|
||||
@@ -1049,13 +1047,9 @@ class AtlasPage {
|
||||
}
|
||||
|
||||
/**
|
||||
* Monotonically increasing across all atlas pages globally. Used to detect when the texture
|
||||
* unit at a given index needs to be re-uploaded — both for content changes within the same
|
||||
* page and for a page object swap at the same index (which happens after a page merge,
|
||||
* where a per-page counter could coincide with the previously-bound page's value).
|
||||
* Used to check whether the canvas of the atlas page has changed.
|
||||
*/
|
||||
public static nextVersion: number = 0;
|
||||
public version = ++AtlasPage.nextVersion;
|
||||
public version = 0;
|
||||
|
||||
// Texture atlas current positioning data. The texture packing strategy used is to fill from
|
||||
// left-to-right and top-to-bottom. When the glyph being written is less than half of the current
|
||||
@@ -1098,7 +1092,7 @@ class AtlasPage {
|
||||
this.currentRow.y = 0;
|
||||
this.currentRow.height = 0;
|
||||
this.fixedRows.length = 0;
|
||||
this.version = ++AtlasPage.nextVersion;
|
||||
this.version++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,10 +28,6 @@ import { addDisposableListener } from 'browser/Dom';
|
||||
import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
|
||||
import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
|
||||
|
||||
const enum Constants {
|
||||
MERGE_RETRY_LIMIT = 32
|
||||
}
|
||||
|
||||
export class WebglRenderer extends Disposable implements IRenderer {
|
||||
private _renderLayers: IRenderLayer[];
|
||||
private _cursorBlinkStateManager: MutableDisposable<CursorBlinkStateManager> = this._register(new MutableDisposable());
|
||||
@@ -379,19 +375,6 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
this._updateModel(start, end);
|
||||
}
|
||||
|
||||
// A mid-update atlas page merge invalidates vertex data and may not bump the host
|
||||
// page's version, so re-run the update and force a full texture rebind.
|
||||
let merged = false;
|
||||
let mergeRetries = 0;
|
||||
while (this._charAtlas && this._glyphRenderer.value.beginFrame() && mergeRetries++ < Constants.MERGE_RETRY_LIMIT) {
|
||||
merged = true;
|
||||
this._clearModel(true);
|
||||
this._updateModel(0, this._terminal.rows - 1);
|
||||
}
|
||||
if (merged) {
|
||||
this._glyphRenderer.value.invalidateAtlasTextures();
|
||||
}
|
||||
|
||||
// Render
|
||||
this._rectangleRenderer.value.renderBackgrounds();
|
||||
this._glyphRenderer.value.render(this._model);
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
from termios import tcgetattr, tcsetattr, TCSADRAIN, TIOCGWINSZ, TCSAFLUSH
|
||||
from tty import setcbreak, setraw
|
||||
import os
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
from select import select
|
||||
from time import sleep
|
||||
from json import dumps, loads
|
||||
from typing import Optional, Tuple, Union
|
||||
|
||||
|
||||
class TerminalContext:
|
||||
def __init__(self, fd: int, close_fd=False) -> None:
|
||||
if not os.isatty(fd):
|
||||
raise TypeError('fd is not a terminal')
|
||||
self.close_fd = close_fd
|
||||
self.fd = fd
|
||||
self.is_cbreak = False
|
||||
self.is_raw = False
|
||||
self._initial_attr = self.termios_attributes
|
||||
|
||||
@classmethod
|
||||
def from_cterm(cls):
|
||||
fd = os.open(os.ctermid(), os.O_RDWR)
|
||||
return TerminalContext(fd, True)
|
||||
|
||||
def close(self) -> None:
|
||||
tcsetattr(self.fd, TCSADRAIN, self._initial_attr)
|
||||
if self.close_fd:
|
||||
os.close(self.fd)
|
||||
|
||||
@property
|
||||
def termios_attributes(self) -> list:
|
||||
return tcgetattr(self.fd)
|
||||
|
||||
@property
|
||||
def ttyname(self) -> str:
|
||||
return os.ttyname(self.fd)
|
||||
|
||||
@contextmanager
|
||||
def cbreak_mode(self):
|
||||
"""
|
||||
Enter cbreak mode context.
|
||||
"""
|
||||
if self.is_cbreak:
|
||||
yield
|
||||
return
|
||||
tattr = self.termios_attributes
|
||||
try:
|
||||
setcbreak(self.fd, TCSADRAIN)
|
||||
self.is_cbreak = True
|
||||
yield
|
||||
finally:
|
||||
tcsetattr(self.fd, TCSADRAIN, tattr)
|
||||
self.is_cbreak = False
|
||||
|
||||
@contextmanager
|
||||
def raw_mode(self):
|
||||
"""
|
||||
Enter cbreak mode context.
|
||||
"""
|
||||
if self.is_raw:
|
||||
yield
|
||||
return
|
||||
tattr = self.termios_attributes
|
||||
try:
|
||||
setraw(self.fd, TCSADRAIN)
|
||||
self.is_raw = True
|
||||
yield
|
||||
finally:
|
||||
tcsetattr(self.fd, TCSADRAIN, tattr)
|
||||
self.is_raw = False
|
||||
|
||||
@contextmanager
|
||||
def custom_state(self, undo=None):
|
||||
"""
|
||||
Enter custom terminal state, that needs to to be undone by ``undo``.
|
||||
Useful, if you want to apply a custom terminal state and
|
||||
have to make sure, that it gets properly reset to previous state.
|
||||
"""
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if undo:
|
||||
undo()
|
||||
|
||||
def write(self, data: Union[str, bytes]) -> None:
|
||||
"""
|
||||
Write string or bytes directly to the terminal.
|
||||
"""
|
||||
if isinstance(data, str):
|
||||
data = data.encode('utf-8')
|
||||
sent = os.write(self.fd, data)
|
||||
while sent:
|
||||
data = data[sent:]
|
||||
sent = os.write(self.fd, data)
|
||||
|
||||
def read(self, amount: int = 1024, timeout: Optional[float] = None) -> bytes:
|
||||
"""
|
||||
Blocking read from the terminal.
|
||||
If nothing was sent from the terminal within ``timeout``,
|
||||
empty bytes are returned.
|
||||
"""
|
||||
can_read, _, _ = select([self.fd], [], [], timeout)
|
||||
if can_read:
|
||||
return os.read(self.fd, amount)
|
||||
return b''
|
||||
|
||||
|
||||
@contextmanager
|
||||
def cterminal_context():
|
||||
t = TerminalContext.from_cterm()
|
||||
try:
|
||||
yield t
|
||||
finally:
|
||||
t.close()
|
||||
|
||||
|
||||
def extract_events(data: list[str]):
|
||||
if len(data) < 5:
|
||||
raise Exception('not enough reports')
|
||||
types = set(data)
|
||||
if len(types) == 1:
|
||||
return {'PRESS': data[0], 'REPEAT': data[0], 'RELEASE': None}
|
||||
if len(types) == 2:
|
||||
last = data.pop()
|
||||
if last != data[0] and len(set(data)) == 1:
|
||||
return {'PRESS': data[0], 'REPEAT': data[0], 'RELEASE': last}
|
||||
raise Exception('weird reports, 2 types')
|
||||
if len(types) == 3:
|
||||
first = data.pop(0)
|
||||
last = data.pop()
|
||||
if first != last and first != data[0] and last != data[0]:
|
||||
return {'PRESS': first, 'REPEAT': data[0], 'RELEASE': last}
|
||||
raise Exception('weird reports, 3 types')
|
||||
raise Exception('more than 3 types')
|
||||
|
||||
|
||||
def query(term: TerminalContext, mode: int):
|
||||
with term.custom_state(undo=lambda:term.write('\x1b[<u')):
|
||||
term.write(f'\x1b[>{mode}u')
|
||||
term.read(timeout=.1)
|
||||
print('PRESS (within 5s) and HOLD (for 5s)\r')
|
||||
data: list[bytes] = []
|
||||
cur = term.read(timeout=5)
|
||||
while cur:
|
||||
data.append(cur)
|
||||
cur = term.read(timeout=.5)
|
||||
if len(data) > 5:
|
||||
print('RELEASE\r', end='')
|
||||
try:
|
||||
return extract_events([b.decode('utf-8') for b in data])
|
||||
except Exception as e:
|
||||
print('Error:', e, '\r')
|
||||
print(data, '\r')
|
||||
term.read(timeout=.1)
|
||||
|
||||
|
||||
def save(filedata: dict[str, dict[str, str]], filename, entry, mode, events):
|
||||
try:
|
||||
filedata[entry][mode] = events
|
||||
except KeyError:
|
||||
filedata[entry] = {mode: events}
|
||||
with open(filename, 'w') as f:
|
||||
f.write(dumps(filedata, indent=2))
|
||||
|
||||
|
||||
def main():
|
||||
mode = 1
|
||||
filedata = {}
|
||||
print(sys.argv)
|
||||
if len(sys.argv) != 3:
|
||||
print('ERROR: not enough arguments')
|
||||
print('Usage: python kitty_keytester.py <mode> <json-file>')
|
||||
return
|
||||
mode = int(sys.argv[1])
|
||||
filename = sys.argv[2]
|
||||
if os.path.exists(filename):
|
||||
with open(filename) as f:
|
||||
filedata = loads(f.read())
|
||||
with cterminal_context() as term:
|
||||
while True:
|
||||
with term.raw_mode():
|
||||
events = query(term, mode)
|
||||
if events:
|
||||
print(events)
|
||||
entry = input('Entry: ')
|
||||
if entry:
|
||||
save(filedata, filename, entry, str(mode), events)
|
||||
cont = input('Continue? (y)')
|
||||
if cont not in ['y', '']:
|
||||
break
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -252,11 +252,6 @@ export class OptionsWindow extends BaseWindow implements IControlWindow {
|
||||
input.value = 'auto';
|
||||
this._handlers.updateTerminalSize();
|
||||
}
|
||||
const screenElement = document.querySelector('.xterm-screen');
|
||||
if (screenElement instanceof HTMLElement) {
|
||||
screenElement.style.outline = this._autoResize ? ''
|
||||
: 'solid thin red';
|
||||
}
|
||||
} else if (o === 'theme') {
|
||||
value = this._getTheme();
|
||||
}
|
||||
|
||||
@@ -43,7 +43,6 @@ export default tseslint.config(
|
||||
singleline: { delimiter: 'comma', requireLast: false }
|
||||
}],
|
||||
'@stylistic/type-annotation-spacing': 'warn',
|
||||
'@stylistic/space-unary-ops': 'warn',
|
||||
|
||||
'@typescript-eslint/array-type': ['warn', { default: 'array', readonly: 'generic' }],
|
||||
'@typescript-eslint/consistent-type-assertions': 'warn',
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
{
|
||||
"Escape +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[27;65u",
|
||||
"REPEAT": "\u001b[27;65u",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"F1 +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[1;65P",
|
||||
"REPEAT": "\u001b[1;65P",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Insert +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[2;65~",
|
||||
"REPEAT": "\u001b[2;65~",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Delete +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[3;65~",
|
||||
"REPEAT": "\u001b[3;65~",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"PageUp +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[5;65~",
|
||||
"REPEAT": "\u001b[5;65~",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Dead (Backquote) +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": "^",
|
||||
"REPEAT": "^",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"1 (Digit1) +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": "1",
|
||||
"REPEAT": "1",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"\u00df (Minus) +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": "\u1e9e",
|
||||
"REPEAT": "\u1e9e",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Dead (Equal) +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": "\u00b4",
|
||||
"REPEAT": "\u00b4",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Backspace +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": "\u007f",
|
||||
"REPEAT": "\u007f",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Tab +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": "\t",
|
||||
"REPEAT": "\t",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Q (KeyQ) +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": "Q",
|
||||
"REPEAT": "Q",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"\u00dc (BracketLeft) +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": "\u00dc",
|
||||
"REPEAT": "\u00dc",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"+ (BracketRight) +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": "+",
|
||||
"REPEAT": "+",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Enter +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": "\r",
|
||||
"REPEAT": "\r",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"\u00d6 (Semicolon) +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": "\u00d6",
|
||||
"REPEAT": "\u00d6",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"\u00c4 (Quote) +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": "\u00c4",
|
||||
"REPEAT": "\u00c4",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"# (Bashslash) +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": "#",
|
||||
"REPEAT": "#",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"< (IntlBackslash) +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": "<",
|
||||
"REPEAT": "<",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
", (Comma) +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": ",",
|
||||
"REPEAT": ",",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
". (Period) +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": ".",
|
||||
"REPEAT": ".",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"- (Slash) +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": "-",
|
||||
"REPEAT": "-",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"ArrowUp +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[1;65A",
|
||||
"REPEAT": "\u001b[1;65A",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"PageDown +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[6;65~",
|
||||
"REPEAT": "\u001b[6;65~",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Home +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[1;65H",
|
||||
"REPEAT": "\u001b[1;65H",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"End +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[1;65F",
|
||||
"REPEAT": "\u001b[1;65F",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"+ (NumpadAdd) +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": "+",
|
||||
"REPEAT": "+",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Enter (NumpadEnter) +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[57414;65u",
|
||||
"REPEAT": "\u001b[57414;65u",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Insert (Numpad0) +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[57425;65u",
|
||||
"REPEAT": "\u001b[57425;65u",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"\u0000 (NumpadDecimal) +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[57426;65u",
|
||||
"REPEAT": "\u001b[57426;65u",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Numlock +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": null,
|
||||
"REPEAT": null,
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"CapsLock +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": null,
|
||||
"REPEAT": null,
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Shift (ShiftLeft) +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": null,
|
||||
"REPEAT": null,
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Shift (ShiftRight) +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": null,
|
||||
"REPEAT": null,
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Control (ControlLeft) +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": null,
|
||||
"REPEAT": null,
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Meta (MetaLeft) +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": null,
|
||||
"REPEAT": null,
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Alt (AltLeft) +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": null,
|
||||
"REPEAT": null,
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"AltGraph (AltRight) +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": null,
|
||||
"REPEAT": null,
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Control (ControlRight) +CapsLock": {
|
||||
"1": {
|
||||
"PRESS": null,
|
||||
"REPEAT": null,
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"/ (NumpadDivide) +NumLock": {
|
||||
"1": {
|
||||
"PRESS": "/",
|
||||
"REPEAT": "/",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"* (NumpadMultiply) +NumLock": {
|
||||
"1": {
|
||||
"PRESS": "*",
|
||||
"REPEAT": "*",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"- (NumpadSubtract) +NumLock": {
|
||||
"1": {
|
||||
"PRESS": "-",
|
||||
"REPEAT": "-",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"0 (Numpad0) +NumLock": {
|
||||
"1": {
|
||||
"PRESS": "0",
|
||||
"REPEAT": "0",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"1 (Numpad1) +NumLock": {
|
||||
"1": {
|
||||
"PRESS": "1",
|
||||
"REPEAT": "1",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"2 (Numpad2) +NumLock": {
|
||||
"1": {
|
||||
"PRESS": "2",
|
||||
"REPEAT": "2",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"3 (Numpad3) +NumLock": {
|
||||
"1": {
|
||||
"PRESS": "3",
|
||||
"REPEAT": "3",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"4 (Numpad4) +NumLock": {
|
||||
"1": {
|
||||
"PRESS": "4",
|
||||
"REPEAT": "4",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"5 (Numpad5) +NumLock": {
|
||||
"1": {
|
||||
"PRESS": "5",
|
||||
"REPEAT": "5",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"6 (Numpad6) +NumLock": {
|
||||
"1": {
|
||||
"PRESS": "6",
|
||||
"REPEAT": "6",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"7 (Numpad7) +NumLock": {
|
||||
"1": {
|
||||
"PRESS": "7",
|
||||
"REPEAT": "7",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"8 (Numpad8) +NumLock": {
|
||||
"1": {
|
||||
"PRESS": "8",
|
||||
"REPEAT": "8",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"9 (Numpad9) +NumLock": {
|
||||
"1": {
|
||||
"PRESS": "9",
|
||||
"REPEAT": "9",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
", (NumpadDecimal) +NumLock": {
|
||||
"1": {
|
||||
"PRESS": ",",
|
||||
"REPEAT": ",",
|
||||
"RELEASE": null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,702 @@
|
||||
{
|
||||
"Escape": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[27u",
|
||||
"REPEAT": "\u001b[27u",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"F1": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[P",
|
||||
"REPEAT": "\u001b[P",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"F2": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[Q",
|
||||
"REPEAT": "\u001b[Q",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"F3": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[13~",
|
||||
"REPEAT": "\u001b[13~",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"F4": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[S",
|
||||
"REPEAT": "\u001b[S",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"F5": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[15~",
|
||||
"REPEAT": "\u001b[15~",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"F6": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[17~",
|
||||
"REPEAT": "\u001b[17~",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"F7": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[18~",
|
||||
"REPEAT": "\u001b[18~",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"F8": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[19~",
|
||||
"REPEAT": "\u001b[19~",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"F9": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[20~",
|
||||
"REPEAT": "\u001b[20~",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"F10": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[21~",
|
||||
"REPEAT": "\u001b[21~",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"F11": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[23~",
|
||||
"REPEAT": "\u001b[23~",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"F12": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[24~",
|
||||
"REPEAT": "\u001b[24~",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Insert": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[2~",
|
||||
"REPEAT": "\u001b[2~",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Delete": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[3~",
|
||||
"REPEAT": "\u001b[3~",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"PageUp": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[5~",
|
||||
"REPEAT": "\u001b[5~",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"PageDown": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[6~",
|
||||
"REPEAT": "\u001b[6~",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Home": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[H",
|
||||
"REPEAT": "\u001b[H",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"End": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[F",
|
||||
"REPEAT": "\u001b[F",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Dead (Backquote)": {
|
||||
"1": {
|
||||
"PRESS": "^",
|
||||
"REPEAT": "^",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"1 (Digit1)": {
|
||||
"1": {
|
||||
"PRESS": "1",
|
||||
"REPEAT": "1",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"2 (Digit2)": {
|
||||
"1": {
|
||||
"PRESS": "2",
|
||||
"REPEAT": "2",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"3 (Digit3)": {
|
||||
"1": {
|
||||
"PRESS": "3",
|
||||
"REPEAT": "3",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"4 (Digit4)": {
|
||||
"1": {
|
||||
"PRESS": "4",
|
||||
"REPEAT": "4",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"5 (Digit5)": {
|
||||
"1": {
|
||||
"PRESS": "5",
|
||||
"REPEAT": "5",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"6 (Digit6)": {
|
||||
"1": {
|
||||
"PRESS": "6",
|
||||
"REPEAT": "6",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"7 (Digit7)": {
|
||||
"1": {
|
||||
"PRESS": "7",
|
||||
"REPEAT": "7",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"8 (Digit8)": {
|
||||
"1": {
|
||||
"PRESS": "8",
|
||||
"REPEAT": "8",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"9 (Digit9)": {
|
||||
"1": {
|
||||
"PRESS": "9",
|
||||
"REPEAT": "9",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"0 (Digit0)": {
|
||||
"1": {
|
||||
"PRESS": "0",
|
||||
"REPEAT": "0",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"\u00df (Minus)": {
|
||||
"1": {
|
||||
"PRESS": "\u00df",
|
||||
"REPEAT": "\u00df",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Dead (Equal)": {
|
||||
"1": {
|
||||
"PRESS": "\u00b4",
|
||||
"REPEAT": "\u00b4",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Backspace": {
|
||||
"1": {
|
||||
"PRESS": "\u007f",
|
||||
"REPEAT": "\u007f",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Tab": {
|
||||
"1": {
|
||||
"PRESS": "\t",
|
||||
"REPEAT": "\t",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"q (KeyQ)": {
|
||||
"1": {
|
||||
"PRESS": "q",
|
||||
"REPEAT": "q",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"w (KeyW)": {
|
||||
"1": {
|
||||
"PRESS": "w",
|
||||
"REPEAT": "w",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"e (KeyE)": {
|
||||
"1": {
|
||||
"PRESS": "e",
|
||||
"REPEAT": "e",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"r (KeyR)": {
|
||||
"1": {
|
||||
"PRESS": "r",
|
||||
"REPEAT": "r",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"t (KeyT)": {
|
||||
"1": {
|
||||
"PRESS": "t",
|
||||
"REPEAT": "t",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"z (KeyY)": {
|
||||
"1": {
|
||||
"PRESS": "z",
|
||||
"REPEAT": "z",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"u (KeyU)": {
|
||||
"1": {
|
||||
"PRESS": "u",
|
||||
"REPEAT": "u",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"i (KeyI)": {
|
||||
"1": {
|
||||
"PRESS": "i",
|
||||
"REPEAT": "i",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"o (KeyO)": {
|
||||
"1": {
|
||||
"PRESS": "o",
|
||||
"REPEAT": "o",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"p (KeyP)": {
|
||||
"1": {
|
||||
"PRESS": "p",
|
||||
"REPEAT": "p",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"\u00fc (BracketLeft)": {
|
||||
"1": {
|
||||
"PRESS": "\u00fc",
|
||||
"REPEAT": "\u00fc",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"+ (BracketRight)": {
|
||||
"1": {
|
||||
"PRESS": "+",
|
||||
"REPEAT": "+",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Enter": {
|
||||
"1": {
|
||||
"PRESS": "\r",
|
||||
"REPEAT": "\r",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"a (KeyA)": {
|
||||
"1": {
|
||||
"PRESS": "a",
|
||||
"REPEAT": "a",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"s (KeyS)": {
|
||||
"1": {
|
||||
"PRESS": "s",
|
||||
"REPEAT": "s",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"d (KeyD)": {
|
||||
"1": {
|
||||
"PRESS": "d",
|
||||
"REPEAT": "d",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"f (KeyF)": {
|
||||
"1": {
|
||||
"PRESS": "f",
|
||||
"REPEAT": "f",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"g (KeyG)": {
|
||||
"1": {
|
||||
"PRESS": "g",
|
||||
"REPEAT": "g",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"h (KeyH)": {
|
||||
"1": {
|
||||
"PRESS": "h",
|
||||
"REPEAT": "h",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"j (KeyJ)": {
|
||||
"1": {
|
||||
"PRESS": "j",
|
||||
"REPEAT": "j",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"k (KeyK)": {
|
||||
"1": {
|
||||
"PRESS": "k",
|
||||
"REPEAT": "k",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"l (KeyL)": {
|
||||
"1": {
|
||||
"PRESS": "l",
|
||||
"REPEAT": "l",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"\u00f6 (Semicolon)": {
|
||||
"1": {
|
||||
"PRESS": "\u00f6",
|
||||
"REPEAT": "\u00f6",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"\u00e4 (Quote)": {
|
||||
"1": {
|
||||
"PRESS": "\u00e4",
|
||||
"REPEAT": "\u00e4",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"# (Backslash)": {
|
||||
"1": {
|
||||
"PRESS": "#",
|
||||
"REPEAT": "#",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"< (IntlBackslash)": {
|
||||
"1": {
|
||||
"PRESS": "<",
|
||||
"REPEAT": "<",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"y (KeyZ)": {
|
||||
"1": {
|
||||
"PRESS": "y",
|
||||
"REPEAT": "y",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"x (KeyX)": {
|
||||
"1": {
|
||||
"PRESS": "x",
|
||||
"REPEAT": "x",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"c (KeyC)": {
|
||||
"1": {
|
||||
"PRESS": "c",
|
||||
"REPEAT": "c",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"v (KeyV)": {
|
||||
"1": {
|
||||
"PRESS": "v",
|
||||
"REPEAT": "v",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"b (KeyB)": {
|
||||
"1": {
|
||||
"PRESS": "b",
|
||||
"REPEAT": "b",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"n (KeyN)": {
|
||||
"1": {
|
||||
"PRESS": "n",
|
||||
"REPEAT": "n",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"m (KeyM)": {
|
||||
"1": {
|
||||
"PRESS": "m",
|
||||
"REPEAT": "m",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
", (Comma)": {
|
||||
"1": {
|
||||
"PRESS": ",",
|
||||
"REPEAT": ",",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
". (Period)": {
|
||||
"1": {
|
||||
"PRESS": ".",
|
||||
"REPEAT": ".",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"- (Slash)": {
|
||||
"1": {
|
||||
"PRESS": "-",
|
||||
"REPEAT": "-",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"ArrowLeft": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[D",
|
||||
"REPEAT": "\u001b[D",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"ArrowUp": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[A",
|
||||
"REPEAT": "\u001b[A",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"ArrowRight": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[C",
|
||||
"REPEAT": "\u001b[C",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"ArrowDown": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[B",
|
||||
"REPEAT": "\u001b[B",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"/ (NumpadDivide)": {
|
||||
"1": {
|
||||
"PRESS": "/",
|
||||
"REPEAT": "/",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"* (NumpadMultiply)": {
|
||||
"1": {
|
||||
"PRESS": "*",
|
||||
"REPEAT": "*",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"- (NumpadSubtract)": {
|
||||
"1": {
|
||||
"PRESS": "-",
|
||||
"REPEAT": "-",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Home (Numpad7)": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[57423u",
|
||||
"REPEAT": "\u001b[57423u",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"ArrowUp (Numpad8)": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[57419u",
|
||||
"REPEAT": "\u001b[57419u",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"PageUp (Numpad9)": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[57421u",
|
||||
"REPEAT": "\u001b[57421u",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"+ (NumpadAdd)": {
|
||||
"1": {
|
||||
"PRESS": "+",
|
||||
"REPEAT": "+",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"ArrowLeft (Numpad4)": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[57417u",
|
||||
"REPEAT": "\u001b[57417u",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Clear (Numpad5)": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[E",
|
||||
"REPEAT": "\u001b[E",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"ArrowRight (Numpad6)": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[57418u",
|
||||
"REPEAT": "\u001b[57418u",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"End (Numpad1)": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[57424u",
|
||||
"REPEAT": "\u001b[57424u",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"ArrowDown (Numpad2)": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[57420u",
|
||||
"REPEAT": "\u001b[57420u",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"PageDown (Numpad3)": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[57422u",
|
||||
"REPEAT": "\u001b[57422u",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Enter (NumpadEnter)": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[57414u",
|
||||
"REPEAT": "\u001b[57414u",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Insert (Numpad0)": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[57425u",
|
||||
"REPEAT": "\u001b[57425u",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"\u0000 (NumpadDecimal)": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[57426u",
|
||||
"REPEAT": "\u001b[57426u",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Numlock": {
|
||||
"1": {
|
||||
"PRESS": null,
|
||||
"REPEAT": null,
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"CapsLock": {
|
||||
"1": {
|
||||
"PRESS": null,
|
||||
"REPEAT": null,
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Shift (ShiftLeft)": {
|
||||
"1": {
|
||||
"PRESS": null,
|
||||
"REPEAT": null,
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Shift (ShiftRight)": {
|
||||
"1": {
|
||||
"PRESS": null,
|
||||
"REPEAT": null,
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Control (ControlLeft)": {
|
||||
"1": {
|
||||
"PRESS": null,
|
||||
"REPEAT": null,
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Meta (MetaLeft)": {
|
||||
"1": {
|
||||
"PRESS": null,
|
||||
"REPEAT": null,
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Alt (AltLeft)": {
|
||||
"1": {
|
||||
"PRESS": null,
|
||||
"REPEAT": null,
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
" (Space)": {
|
||||
"1": {
|
||||
"PRESS": " ",
|
||||
"REPEAT": " ",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"AltGraph (AltRight)": {
|
||||
"1": {
|
||||
"PRESS": null,
|
||||
"REPEAT": null,
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Control (ControlRight)": {
|
||||
"1": {
|
||||
"PRESS": null,
|
||||
"REPEAT": null,
|
||||
"RELEASE": null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
{
|
||||
"Escape +Shift": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[27;2u",
|
||||
"REPEAT": "\u001b[27;2u",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"F1 +Shift": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[1;2P",
|
||||
"REPEAT": "\u001b[1;2P",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Delete +Shift": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[3;2~",
|
||||
"REPEAT": "\u001b[3;2~",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"PageUp +Shift": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[5;2~",
|
||||
"REPEAT": "\u001b[5;2~",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Dead (Backquote) +Shift": {
|
||||
"1": {
|
||||
"PRESS": "\u00b0",
|
||||
"REPEAT": "\u00b0",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"! (Digit1) +Shift": {
|
||||
"1": {
|
||||
"PRESS": "!",
|
||||
"REPEAT": "!",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"\" (Digit2) +Shift": {
|
||||
"1": {
|
||||
"PRESS": "\"",
|
||||
"REPEAT": "\"",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"\u00a7 (Digit3) +Shift": {
|
||||
"1": {
|
||||
"PRESS": "\u00a7",
|
||||
"REPEAT": "\u00a7",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"$ (Digit4) +Shifft": {
|
||||
"1": {
|
||||
"PRESS": "$",
|
||||
"REPEAT": "$",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"% (Digit5) +Shift": {
|
||||
"1": {
|
||||
"PRESS": "%",
|
||||
"REPEAT": "%",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"& (Digit6) +Shift": {
|
||||
"1": {
|
||||
"PRESS": "&",
|
||||
"REPEAT": "&",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"/ (Digit7) +Shift": {
|
||||
"1": {
|
||||
"PRESS": "/",
|
||||
"REPEAT": "/",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"( (Digit8) +Shift": {
|
||||
"1": {
|
||||
"PRESS": "(",
|
||||
"REPEAT": "(",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
") (Digit9) +Shift": {
|
||||
"1": {
|
||||
"PRESS": ")",
|
||||
"REPEAT": ")",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"= (Digit0) +Shift": {
|
||||
"1": {
|
||||
"PRESS": "=",
|
||||
"REPEAT": "=",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"? (Minus) +Shift": {
|
||||
"1": {
|
||||
"PRESS": "?",
|
||||
"REPEAT": "?",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Backspace +Shift": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[127;2u",
|
||||
"REPEAT": "\u001b[127;2u",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Tab +Shift": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[9;2u",
|
||||
"REPEAT": "\u001b[9;2u",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Q (KeyQ) +Shift": {
|
||||
"1": {
|
||||
"PRESS": "Q",
|
||||
"REPEAT": "Q",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"\u00dc (BracketLeft) +Shift": {
|
||||
"1": {
|
||||
"PRESS": "\u00dc",
|
||||
"REPEAT": "\u00dc",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"* (BracketRight) +Shift": {
|
||||
"1": {
|
||||
"PRESS": "*",
|
||||
"REPEAT": "*",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Enter +Shift": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[13;2u",
|
||||
"REPEAT": "\u001b[13;2u",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"\u00d6 (Semicolon) +Shift": {
|
||||
"1": {
|
||||
"PRESS": "\u00d6",
|
||||
"REPEAT": "\u00d6",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"\u00c4 (Quote) +Shift": {
|
||||
"1": {
|
||||
"PRESS": "\u00c4",
|
||||
"REPEAT": "\u00c4",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"' (Backslash) +Shift": {
|
||||
"1": {
|
||||
"PRESS": "'",
|
||||
"REPEAT": "'",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"> (IntlBackslash) +Shift": {
|
||||
"1": {
|
||||
"PRESS": ">",
|
||||
"REPEAT": ">",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"; (Comma) +Shift": {
|
||||
"1": {
|
||||
"PRESS": ";",
|
||||
"REPEAT": ";",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
": (Period) +Shift": {
|
||||
"1": {
|
||||
"PRESS": ":",
|
||||
"REPEAT": ":",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"_ (Slash) +Shift": {
|
||||
"1": {
|
||||
"PRESS": "_",
|
||||
"REPEAT": "_",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"ArrowUp +Shift": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[1;2A",
|
||||
"REPEAT": "\u001b[1;2A",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"PageDown +Shift": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[6;2~",
|
||||
"REPEAT": "\u001b[6;2~",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Home +Shift": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[1;2H",
|
||||
"REPEAT": "\u001b[1;2H",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Home (Numpad7) +Shift": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[57423;2u",
|
||||
"REPEAT": "\u001b[57423;2u",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"ArrowUp (Numapd8) +Shift": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[57419;2u",
|
||||
"REPEAT": "\u001b[57419;2u",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Clear (Numpad5) +Shift": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[1;2E",
|
||||
"REPEAT": "\u001b[1;2E",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Enter (NumpadEnter) +Shift": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[57414;2u",
|
||||
"REPEAT": "\u001b[57414;2u",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Insert (Numpad0) +Shift": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[57425;2u",
|
||||
"REPEAT": "\u001b[57425;2u",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"\u0000 (NumpadDecimal) +Shift": {
|
||||
"1": {
|
||||
"PRESS": "\u001b[57426;2u",
|
||||
"REPEAT": "\u001b[57426;2u",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Numlock +Shift": {
|
||||
"1": {
|
||||
"PRESS": null,
|
||||
"REPEAT": null,
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"CapsLock +Shift": {
|
||||
"1": {
|
||||
"PRESS": null,
|
||||
"REPEAT": null,
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Shift (ShiftRight) +Shift": {
|
||||
"1": {
|
||||
"PRESS": null,
|
||||
"REPEAT": null,
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Control (ControlLeft) +Shift": {
|
||||
"1": {
|
||||
"PRESS": null,
|
||||
"REPEAT": null,
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Meta (MetaLeft) +Shift": {
|
||||
"1": {
|
||||
"PRESS": null,
|
||||
"REPEAT": null,
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Alt (AltLeft) +Shift": {
|
||||
"1": {
|
||||
"PRESS": null,
|
||||
"REPEAT": null,
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
" (Space) +Shift": {
|
||||
"1": {
|
||||
"PRESS": " ",
|
||||
"REPEAT": " ",
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"AltGraph (AltRight) +Shift": {
|
||||
"1": {
|
||||
"PRESS": null,
|
||||
"REPEAT": null,
|
||||
"RELEASE": null
|
||||
}
|
||||
},
|
||||
"Control (ControlRight) +Shift": {
|
||||
"1": {
|
||||
"PRESS": null,
|
||||
"REPEAT": null,
|
||||
"RELEASE": null
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+706
-9
File diff suppressed because it is too large
Load Diff
@@ -56,6 +56,7 @@
|
||||
"test-unit": "node ./bin/test_unit.js",
|
||||
"test-unit-slow-tests": "npm run test-unit | grep \"ms)\"",
|
||||
"test-unit-coverage": "node ./bin/test_unit.js --coverage",
|
||||
"test-unit-dev": "cross-env NODE_PATH='./out' mocha",
|
||||
"test-integration": "node ./bin/test_integration.js --workers=75%",
|
||||
"test-integration-chromium": "node ./bin/test_integration.js --workers=75% \"--project=Chromium\"",
|
||||
"test-integration-firefox": "node ./bin/test_integration.js --workers=75% \"--project=FirefoxStable\"",
|
||||
@@ -79,11 +80,15 @@
|
||||
"@playwright/test": "^1.57.0",
|
||||
"@stylistic/eslint-plugin": "^4.4.1",
|
||||
"@types/chai": "^4.2.22",
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/deep-equal": "^1.0.1",
|
||||
"@types/express": "4",
|
||||
"@types/express-ws": "^3.0.1",
|
||||
"@types/jsdom": "^27.0.0",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/node": "^22.19.3",
|
||||
"@types/trusted-types": "^1.0.6",
|
||||
"@types/utf8": "^3.0.0",
|
||||
"@types/webpack": "^5.28.0",
|
||||
"@types/ws": "^8.2.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.50.1",
|
||||
@@ -91,6 +96,8 @@
|
||||
"@typescript/native-preview": "^7.0.0-dev.20260213.1",
|
||||
"chai": "^4.3.4",
|
||||
"concurrently": "^9.1.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"deep-equal": "^2.0.5",
|
||||
"esbuild": "~0.25.2",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-plugin-jsdoc": "^50.8.0",
|
||||
@@ -106,6 +113,7 @@
|
||||
"ts-loader": "^9.3.1",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.50.1",
|
||||
"utf8": "^3.0.0",
|
||||
"webpack": "^5.61.0",
|
||||
"webpack-cli": "^4.9.1",
|
||||
"ws": "^8.2.3",
|
||||
|
||||
@@ -18,7 +18,7 @@ export class MouseCoordsService implements IMouseCoordsService {
|
||||
|
||||
public getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined {
|
||||
return getCoords(
|
||||
getWindow(element),
|
||||
window,
|
||||
event,
|
||||
element,
|
||||
colCount,
|
||||
|
||||
@@ -29,7 +29,6 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
private _renderDebouncer: IRenderDebouncerWithCallback;
|
||||
private _pausedResizeTask: DebouncedIdleTask;
|
||||
private _observerDisposable = this._register(new MutableDisposable());
|
||||
private _intersectionObserver: IntersectionObserver | undefined;
|
||||
|
||||
private _isPaused: boolean = false;
|
||||
private _needsFullRefresh: boolean = false;
|
||||
@@ -128,12 +127,8 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
// and resume based on terminal visibility if so
|
||||
if ('IntersectionObserver' in w) {
|
||||
const observer = new w.IntersectionObserver(e => this._handleIntersectionChange(e[e.length - 1]), { threshold: 0 });
|
||||
this._observerDisposable.value = toDisposable(() => {
|
||||
this._intersectionObserver?.disconnect();
|
||||
this._intersectionObserver = undefined;
|
||||
});
|
||||
this._intersectionObserver = observer;
|
||||
observer.observe(screenElement);
|
||||
this._observerDisposable.value = toDisposable(() => observer.disconnect());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -73,42 +73,6 @@ export class TimeoutTimer implements IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules a single runner on the microtask queue. Unlike {@link TimeoutTimer}, a scheduled
|
||||
* microtask cannot be unqueued; {@link cancel} prevents the runner from executing if it has not
|
||||
* run yet.
|
||||
*/
|
||||
export class MicrotaskTimer implements IDisposable {
|
||||
private _isScheduled = false;
|
||||
private _isDisposed = false;
|
||||
|
||||
public dispose(): void {
|
||||
this.cancel();
|
||||
this._isDisposed = true;
|
||||
}
|
||||
|
||||
public cancel(): void {
|
||||
this._isScheduled = false;
|
||||
}
|
||||
|
||||
public set(runner: () => void): void {
|
||||
if (this._isDisposed) {
|
||||
throw new Error('Calling set on a disposed MicrotaskTimer');
|
||||
}
|
||||
if (this._isScheduled) {
|
||||
return;
|
||||
}
|
||||
this._isScheduled = true;
|
||||
queueMicrotask(() => {
|
||||
if (!this._isScheduled) {
|
||||
return;
|
||||
}
|
||||
this._isScheduled = false;
|
||||
runner();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class IntervalTimer implements IDisposable {
|
||||
private _disposable: IDisposable | undefined;
|
||||
private _isDisposed = false;
|
||||
|
||||
+25
-32
@@ -2904,39 +2904,32 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
/**
|
||||
* CSI Ps ; Ps ; Ps t - Various window manipulations and reports (xterm)
|
||||
*
|
||||
* @vt: #P[Guarded by windowOptions; disabled by default for security. See below.] CSI WINMAN "Window Manipulation" "CSI Ps ; Ps t" "Various window manipulations and reports (xterm)."
|
||||
* Note: Only those listed below are supported. All others are left to integrators and
|
||||
* need special treatment based on the embedding environment.
|
||||
*
|
||||
* All `Ps` values are guarded by the corresponding `windowOptions` setting and disabled by
|
||||
* default for security (see `ITerminalOptions.windowOptions`). Custom `CSI t` handlers registered
|
||||
* via `registerCsiHandler` are subject to the same guard.
|
||||
*
|
||||
* | Ps | Action | windowOptions | Support |
|
||||
* | --- | --- | --- | --- |
|
||||
* | 1 | De-iconify window | restoreWin | #P[No default implementation.] |
|
||||
* | 2 | Iconify window | minimizeWin | #P[No default implementation.] |
|
||||
* | 3 | Move window to [x, y] | setWinPosition | #P[No default implementation.] |
|
||||
* | 4 | Resize window in pixels | setWinSizePixels | #P[No default implementation.] |
|
||||
* | 5 | Raise window | raiseWin | #P[No default implementation.] |
|
||||
* | 6 | Lower window | lowerWin | #P[No default implementation.] |
|
||||
* | 7 | Refresh window | refreshWin | #P[No default implementation.] |
|
||||
* | 8 | Resize text area in characters | setWinSizeChars | #P[No default implementation.] |
|
||||
* | 9 | Maximize window | maximizeWin | #P[No default implementation.] |
|
||||
* | 10 | Full-screen mode | fullscreenWin | #P[No default implementation.] |
|
||||
* | 11 | Report window state | getWinState | #P[No default implementation.] |
|
||||
* | 13 | Report window position | getWinPosition | #P[No default implementation.] |
|
||||
* | 14 | Report text area size in pixels (`CSI 4 ; height ; width t`) | getWinSizePixels | #P[Browser only; requires render service.] |
|
||||
* | 14 ; 2 | Report window size in pixels | getWinSizePixels | #N |
|
||||
* | 15 | Report screen size in pixels | getScreenSizePixels | #P[No default implementation.] |
|
||||
* | 16 | Report cell size in pixels (`CSI 6 ; height ; width t`) | getCellSizePixels | #P[Browser only; requires render service.] |
|
||||
* | 18 | Report text area size in characters (`CSI 8 ; height ; width t`) | getWinSizeChars | #Y |
|
||||
* | 19 | Report screen size in characters | getScreenSizeChars | #P[No default implementation.] |
|
||||
* | 20 | Report icon title (OSC) | getIconTitle | #P[No default implementation.] |
|
||||
* | 21 | Report window title (OSC) | getWinTitle | #P[No default implementation.] |
|
||||
* | 22 | Push title(s) on stack | pushTitle | #Y |
|
||||
* | 23 | Pop title(s) from stack | popTitle | #Y |
|
||||
* | >= 24 | Resize to Ps lines (DECSLPP) | setWinLines | #N |
|
||||
*
|
||||
* Note: `setWinLines` also gates DECCOLM (`CSI ? 3 h/l`), see DECSET mode 3.
|
||||
* Ps = 1 4 supported
|
||||
* Report xterm text area size in pixels.
|
||||
* Result is CSI 4 ; height ; width t
|
||||
* Ps = 14 ; 2 not implemented
|
||||
* Ps = 16 supported
|
||||
* Report xterm character cell size in pixels.
|
||||
* Result is CSI 6 ; height ; width t
|
||||
* Ps = 18 supported
|
||||
* Report the size of the text area in characters.
|
||||
* Result is CSI 8 ; height ; width t
|
||||
* Ps = 20 supported
|
||||
* Report xterm window's icon label.
|
||||
* Result is OSC L label ST
|
||||
* Ps = 21 supported
|
||||
* Report xterm window's title.
|
||||
* Result is OSC l label ST
|
||||
* Ps = 22 ; 0 -> Save xterm icon and window title on stack. supported
|
||||
* Ps = 22 ; 1 -> Save xterm icon title on stack. supported
|
||||
* Ps = 22 ; 2 -> Save xterm window title on stack. supported
|
||||
* Ps = 23 ; 0 -> Restore xterm icon and window title from stack. supported
|
||||
* Ps = 23 ; 1 -> Restore xterm icon title from stack. supported
|
||||
* Ps = 23 ; 2 -> Restore xterm window title from stack. supported
|
||||
* Ps >= 24 not implemented
|
||||
*/
|
||||
public windowOptions(params: IParams): boolean {
|
||||
if (!paramToWindowOption(params.params[0], this._optionsService.rawOptions.windowOptions)) {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { StringToUtf32, stringFromCodePoint, Utf8ToUtf32, utf32ToString } from 'common/input/TextDecoder';
|
||||
import { encode } from 'utf8';
|
||||
|
||||
|
||||
// convert UTF32 codepoints to string
|
||||
@@ -28,35 +29,6 @@ function fromByteString(s: string): Uint8Array {
|
||||
return result;
|
||||
}
|
||||
|
||||
function stringToUtf8Bytes(s: string): Uint8Array {
|
||||
const bytes: number[] = [];
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
let cp = s.charCodeAt(i);
|
||||
if (cp >= 0xD800 && cp <= 0xDBFF && i + 1 < s.length) {
|
||||
const next = s.charCodeAt(i + 1);
|
||||
if (next >= 0xDC00 && next <= 0xDFFF) {
|
||||
cp = 0x10000 + ((cp - 0xD800) << 10) + (next - 0xDC00);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
if (cp < 0x80) {
|
||||
bytes.push(cp);
|
||||
} else if (cp < 0x800) {
|
||||
bytes.push(0xC0 | (cp >> 6), 0x80 | (cp & 0x3F));
|
||||
} else if (cp < 0x10000) {
|
||||
bytes.push(0xE0 | (cp >> 12), 0x80 | ((cp >> 6) & 0x3F), 0x80 | (cp & 0x3F));
|
||||
} else {
|
||||
bytes.push(
|
||||
0xF0 | (cp >> 18),
|
||||
0x80 | ((cp >> 12) & 0x3F),
|
||||
0x80 | ((cp >> 6) & 0x3F),
|
||||
0x80 | (cp & 0x3F)
|
||||
);
|
||||
}
|
||||
}
|
||||
return new Uint8Array(bytes);
|
||||
}
|
||||
|
||||
function assertDecodedRange(
|
||||
min: number,
|
||||
max: number,
|
||||
@@ -200,7 +172,7 @@ describe('text encodings', () => {
|
||||
max,
|
||||
(i) => (i >= 0xD800 && i <= 0xDFFF) || i === 0xFEFF,
|
||||
(i) => String.fromCharCode(i),
|
||||
(input, target) => decoder.decode(stringToUtf8Bytes(input), target),
|
||||
(input, target) => decoder.decode(fromByteString(encode(input)), target),
|
||||
(data, length) => toString(data, length)
|
||||
);
|
||||
});
|
||||
@@ -215,7 +187,7 @@ describe('text encodings', () => {
|
||||
max,
|
||||
() => false,
|
||||
(i) => stringFromCodePoint(i),
|
||||
(input, target) => decoder.decode(stringToUtf8Bytes(input), target),
|
||||
(input, target) => decoder.decode(fromByteString(encode(input)), target),
|
||||
(data, length) => toString(data, length)
|
||||
);
|
||||
});
|
||||
@@ -224,7 +196,7 @@ describe('text encodings', () => {
|
||||
it('0xFEFF(BOM)', () => {
|
||||
const decoder = new Utf8ToUtf32();
|
||||
const target = new Uint32Array(5);
|
||||
const utf8Data = stringToUtf8Bytes(String.fromCharCode(0xFEFF));
|
||||
const utf8Data = fromByteString(encode(String.fromCharCode(0xFEFF)));
|
||||
const length = decoder.decode(utf8Data, target);
|
||||
assert.equal(length, 0);
|
||||
decoder.clear();
|
||||
@@ -235,7 +207,7 @@ describe('text encodings', () => {
|
||||
const decoder = new Utf8ToUtf32();
|
||||
const target = new Uint32Array(500);
|
||||
for (let i = 0; i < TEST_STRINGS.length; ++i) {
|
||||
const utf8Data = stringToUtf8Bytes(TEST_STRINGS[i]);
|
||||
const utf8Data = fromByteString(encode(TEST_STRINGS[i]));
|
||||
const length = decoder.decode(utf8Data, target);
|
||||
assert.equal(toString(target, length), TEST_STRINGS[i]);
|
||||
decoder.clear();
|
||||
|
||||
@@ -4,13 +4,11 @@
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { DecorationLineCache, DecorationService } from './DecorationService';
|
||||
import { DecorationService } from './DecorationService';
|
||||
import { IMarker } from 'common/Types';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { Emitter } from 'common/Event';
|
||||
import { MockLogService, MockBufferService, MockOptionsService } from 'common/TestUtils.test';
|
||||
import { Buffer } from 'common/buffer/Buffer';
|
||||
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
|
||||
import { MockLogService } from 'common/TestUtils.test';
|
||||
|
||||
function createFakeMarker(line: number): IMarker {
|
||||
return Object.freeze(new class extends Disposable {
|
||||
@@ -21,16 +19,11 @@ function createFakeMarker(line: number): IMarker {
|
||||
}());
|
||||
}
|
||||
|
||||
function createDecorationService(): DecorationService {
|
||||
const bufferService = new MockBufferService(80, 24, new MockOptionsService());
|
||||
return new DecorationService(new MockLogService(), bufferService);
|
||||
}
|
||||
|
||||
const fakeMarker: IMarker = createFakeMarker(1);
|
||||
|
||||
describe('DecorationService', () => {
|
||||
it('should set isDisposed to true after dispose', () => {
|
||||
const service = createDecorationService();
|
||||
const service = new DecorationService(new MockLogService());
|
||||
const decoration = service.registerDecoration({
|
||||
marker: fakeMarker
|
||||
});
|
||||
@@ -42,7 +35,7 @@ describe('DecorationService', () => {
|
||||
|
||||
describe('forEachDecorationAtCell', () => {
|
||||
it('should find decoration at its marker line', () => {
|
||||
const service = createDecorationService();
|
||||
const service = new DecorationService(new MockLogService());
|
||||
const decoration = service.registerDecoration({
|
||||
marker: createFakeMarker(5),
|
||||
width: 10
|
||||
@@ -55,7 +48,7 @@ describe('DecorationService', () => {
|
||||
});
|
||||
|
||||
it('should find decoration with height > 1 on subsequent lines', () => {
|
||||
const service = createDecorationService();
|
||||
const service = new DecorationService(new MockLogService());
|
||||
const decoration = service.registerDecoration({
|
||||
marker: createFakeMarker(5),
|
||||
width: 10,
|
||||
@@ -81,7 +74,7 @@ describe('DecorationService', () => {
|
||||
});
|
||||
|
||||
it('should not find decoration outside its x range', () => {
|
||||
const service = createDecorationService();
|
||||
const service = new DecorationService(new MockLogService());
|
||||
const decoration = service.registerDecoration({
|
||||
marker: createFakeMarker(5),
|
||||
x: 5,
|
||||
@@ -106,35 +99,11 @@ describe('DecorationService', () => {
|
||||
service.forEachDecorationAtCell(8, 5, undefined, d => foundAtX8.push(d));
|
||||
assert.strictEqual(foundAtX8.length, 0);
|
||||
});
|
||||
|
||||
it('should find multi-line decoration when single-line decorations exist on other lines', () => {
|
||||
const bufferService = new MockBufferService(80, 24, new MockOptionsService());
|
||||
const serviceWithBuffer = new DecorationService(new MockLogService(), bufferService);
|
||||
const buffer = bufferService.buffer;
|
||||
(buffer as Buffer).fillViewportRows();
|
||||
|
||||
for (let i = 0; i < 100; i++) {
|
||||
serviceWithBuffer.registerDecoration({
|
||||
marker: buffer.addMarker(i),
|
||||
width: 5
|
||||
});
|
||||
}
|
||||
const multiLine = serviceWithBuffer.registerDecoration({
|
||||
marker: buffer.addMarker(10),
|
||||
width: 10,
|
||||
height: 3
|
||||
});
|
||||
assert.ok(multiLine);
|
||||
|
||||
const found: typeof multiLine[] = [];
|
||||
serviceWithBuffer.forEachDecorationAtCell(0, 11, undefined, d => found.push(d));
|
||||
assert.include(found, multiLine);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDecorationsAtCell', () => {
|
||||
it('should find decoration with height > 1 on subsequent lines', () => {
|
||||
const service = createDecorationService();
|
||||
const service = new DecorationService(new MockLogService());
|
||||
const decoration = service.registerDecoration({
|
||||
marker: createFakeMarker(5),
|
||||
width: 10,
|
||||
@@ -148,73 +117,4 @@ describe('DecorationService', () => {
|
||||
assert.strictEqual([...service.getDecorationsAtCell(0, 8)].length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DecorationLineCache', () => {
|
||||
it('should return undefined for lines with no indexed decorations', () => {
|
||||
const cache = new DecorationLineCache();
|
||||
assert.isUndefined(cache.getDecorationsOnLine(0));
|
||||
});
|
||||
});
|
||||
|
||||
describe('line index maintenance', () => {
|
||||
it('should keep lookups correct after buffer trim', () => {
|
||||
const bufferService = new MockBufferService(80, 5, new MockOptionsService({ scrollback: 0 }));
|
||||
const service = new DecorationService(new MockLogService(), bufferService);
|
||||
const buffer = bufferService.buffer;
|
||||
(buffer as Buffer).fillViewportRows();
|
||||
|
||||
const marker = buffer.addMarker(buffer.lines.length - 1);
|
||||
const decoration = service.registerDecoration({ marker, width: 10 });
|
||||
assert.ok(decoration);
|
||||
|
||||
buffer.lines.onTrimEmitter.fire(1);
|
||||
|
||||
const found: typeof decoration[] = [];
|
||||
service.forEachDecorationAtCell(0, marker.line, undefined, d => found.push(d));
|
||||
assert.strictEqual(found.length, 1);
|
||||
});
|
||||
|
||||
it('should remove decoration from line index when marker is trimmed off buffer', () => {
|
||||
const bufferService = new MockBufferService(80, 5, new MockOptionsService({ scrollback: 0 }));
|
||||
const service = new DecorationService(new MockLogService(), bufferService);
|
||||
const buffer = bufferService.buffer;
|
||||
(buffer as Buffer).fillViewportRows();
|
||||
|
||||
const marker = buffer.addMarker(0);
|
||||
const decoration = service.registerDecoration({ marker, width: 10 });
|
||||
assert.ok(decoration);
|
||||
|
||||
buffer.lines.onTrimEmitter.fire(1);
|
||||
assert.isTrue(marker.isDisposed);
|
||||
assert.isTrue(decoration!.isDisposed);
|
||||
|
||||
const found: typeof decoration[] = [];
|
||||
service.forEachDecorationAtCell(0, 0, undefined, d => found.push(d));
|
||||
assert.strictEqual(found.length, 0);
|
||||
});
|
||||
|
||||
it('should keep multi-line decoration indexed after line insert', async () => {
|
||||
const bufferService = new MockBufferService(80, 10, new MockOptionsService({ scrollback: 100 }));
|
||||
const service = new DecorationService(new MockLogService(), bufferService);
|
||||
const buffer = bufferService.buffer;
|
||||
(buffer as Buffer).fillViewportRows();
|
||||
|
||||
const marker = buffer.addMarker(3);
|
||||
const decoration = service.registerDecoration({ marker, width: 10, height: 3 });
|
||||
assert.ok(decoration);
|
||||
|
||||
buffer.lines.splice(5, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA));
|
||||
await new Promise<void>(resolve => queueMicrotask(resolve));
|
||||
|
||||
const foundOnSpan: typeof decoration[] = [];
|
||||
for (let line = marker.line; line < marker.line + 3; line++) {
|
||||
service.forEachDecorationAtCell(0, line, undefined, d => foundOnSpan.push(d));
|
||||
}
|
||||
assert.include(foundOnSpan, decoration);
|
||||
|
||||
const foundOutsideSpan: typeof decoration[] = [];
|
||||
service.forEachDecorationAtCell(0, marker.line + 3, undefined, d => foundOutsideSpan.push(d));
|
||||
assert.strictEqual(foundOutsideSpan.length, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,19 +3,19 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import type { IDeleteEvent, IInsertEvent } from 'common/CircularList';
|
||||
import { MicrotaskTimer } from 'common/Async';
|
||||
import { css } from 'common/Color';
|
||||
import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'common/Lifecycle';
|
||||
import { IBufferService, IDecorationService, IInternalDecoration, ILogService } from 'common/services/Services';
|
||||
import { Disposable, DisposableStore, toDisposable } from 'common/Lifecycle';
|
||||
import { IDecorationService, IInternalDecoration, ILogService } from 'common/services/Services';
|
||||
import { SortedList } from 'common/SortedList';
|
||||
import { IColor, ICircularList } from 'common/Types';
|
||||
import { IColor } from 'common/Types';
|
||||
import { IDecoration, IDecorationOptions, IMarker } from '@xterm/xterm';
|
||||
import { Emitter } from 'common/Event';
|
||||
|
||||
// Work variables to avoid garbage collection
|
||||
let $xmin = 0;
|
||||
let $xmax = 0;
|
||||
let $ymin = 0;
|
||||
let $ymax = 0;
|
||||
|
||||
export class DecorationService extends Disposable implements IDecorationService {
|
||||
public serviceBrand: any;
|
||||
@@ -27,8 +27,6 @@ export class DecorationService extends Disposable implements IDecorationService
|
||||
*/
|
||||
private readonly _decorations: SortedList<IInternalDecoration>;
|
||||
|
||||
private readonly _lineCache = this._register(new DecorationLineCache());
|
||||
|
||||
private readonly _onDecorationRegistered = this._register(new Emitter<IInternalDecoration>());
|
||||
public readonly onDecorationRegistered = this._onDecorationRegistered.event;
|
||||
private readonly _onDecorationRemoved = this._register(new Emitter<IInternalDecoration>());
|
||||
@@ -36,19 +34,12 @@ export class DecorationService extends Disposable implements IDecorationService
|
||||
|
||||
public get decorations(): IterableIterator<IInternalDecoration> { return this._decorations.values(); }
|
||||
|
||||
constructor(
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@IBufferService private readonly _bufferService: IBufferService
|
||||
) {
|
||||
constructor(@ILogService private readonly _logService: ILogService) {
|
||||
super();
|
||||
|
||||
this._decorations = new SortedList(e => e?.marker.line, this._logService);
|
||||
|
||||
this._register(toDisposable(() => this.reset()));
|
||||
this._register(this._bufferService.buffers.onBufferActivate(() => {
|
||||
this._lineCache.attachToBufferLines(this._bufferService.buffer.lines);
|
||||
}));
|
||||
this._lineCache.attachToBufferLines(this._bufferService.buffer.lines);
|
||||
}
|
||||
|
||||
public registerDecoration(options: IDecorationOptions): IDecoration | undefined {
|
||||
@@ -62,14 +53,12 @@ export class DecorationService extends Disposable implements IDecorationService
|
||||
listener.dispose();
|
||||
if (decoration) {
|
||||
if (this._decorations.delete(decoration)) {
|
||||
this._lineCache.remove(decoration);
|
||||
this._onDecorationRemoved.fire(decoration);
|
||||
}
|
||||
markerDispose.dispose();
|
||||
}
|
||||
});
|
||||
this._decorations.insert(decoration);
|
||||
this._lineCache.add(decoration);
|
||||
this._onDecorationRegistered.fire(decoration);
|
||||
}
|
||||
return decoration;
|
||||
@@ -80,17 +69,19 @@ export class DecorationService extends Disposable implements IDecorationService
|
||||
d.dispose();
|
||||
}
|
||||
this._decorations.clear();
|
||||
this._lineCache.clear();
|
||||
}
|
||||
|
||||
public *getDecorationsAtCell(x: number, line: number, layer?: 'bottom' | 'top'): IterableIterator<IInternalDecoration> {
|
||||
const bucket = this._lineCache.getDecorationsOnLine(line);
|
||||
if (!bucket) {
|
||||
return;
|
||||
}
|
||||
let xmin = 0;
|
||||
let xmax = 0;
|
||||
for (const d of bucket) {
|
||||
let ymin = 0;
|
||||
let ymax = 0;
|
||||
for (const d of this._decorations.values()) {
|
||||
ymin = d.marker.line;
|
||||
ymax = ymin + (d.options.height ?? 1);
|
||||
if (line < ymin || line >= ymax) {
|
||||
continue;
|
||||
}
|
||||
xmin = d.options.x ?? 0;
|
||||
xmax = xmin + (d.options.width ?? 1);
|
||||
if (x >= xmin && x < xmax && (!layer || (d.options.layer ?? 'bottom') === layer)) {
|
||||
@@ -100,11 +91,12 @@ export class DecorationService extends Disposable implements IDecorationService
|
||||
}
|
||||
|
||||
public forEachDecorationAtCell(x: number, line: number, layer: 'bottom' | 'top' | undefined, callback: (decoration: IInternalDecoration) => void): void {
|
||||
const bucket = this._lineCache.getDecorationsOnLine(line);
|
||||
if (!bucket) {
|
||||
return;
|
||||
}
|
||||
for (const d of bucket) {
|
||||
for (const d of this._decorations.values()) {
|
||||
$ymin = d.marker.line;
|
||||
$ymax = $ymin + (d.options.height ?? 1);
|
||||
if (line < $ymin || line >= $ymax) {
|
||||
continue;
|
||||
}
|
||||
$xmin = d.options.x ?? 0;
|
||||
$xmax = $xmin + (d.options.width ?? 1);
|
||||
if (x >= $xmin && x < $xmax && (!layer || (d.options.layer ?? 'bottom') === layer)) {
|
||||
@@ -114,235 +106,10 @@ export class DecorationService extends Disposable implements IDecorationService
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-logical-line index of decorations for fast cell lookup.
|
||||
*
|
||||
* Keys are marker.line coordinates (logical buffer lines), not CircularList ring slots.
|
||||
* Multi-line decorations appear in every line bucket they span. The index is kept aligned
|
||||
* with marker.line updates via buffer line trim/insert/delete events.
|
||||
*/
|
||||
export class DecorationLineCache extends Disposable {
|
||||
private readonly _decorationsByLine: Map<number, IInternalDecoration[]> = new Map();
|
||||
private readonly _decorations = new Set<IInternalDecoration>();
|
||||
private readonly _bufferLineListeners = this._register(new MutableDisposable<DisposableStore>());
|
||||
private readonly _lineIndexSyncTimer = this._register(new MicrotaskTimer());
|
||||
private _lineIndexSyncCallbacks: (() => void)[] = [];
|
||||
|
||||
public clear(): void {
|
||||
this._lineIndexSyncCallbacks.length = 0;
|
||||
this._lineIndexSyncTimer.cancel();
|
||||
this._decorationsByLine.clear();
|
||||
this._decorations.clear();
|
||||
}
|
||||
|
||||
public add(decoration: IInternalDecoration): void {
|
||||
this._decorations.add(decoration);
|
||||
this._addToLineBuckets(decoration);
|
||||
}
|
||||
|
||||
public remove(decoration: IInternalDecoration): void {
|
||||
this._decorations.delete(decoration);
|
||||
this._removeFromLineBuckets(decoration);
|
||||
}
|
||||
|
||||
public getDecorationsOnLine(line: number): ReadonlyArray<IInternalDecoration> | undefined {
|
||||
return this._decorationsByLine.get(line);
|
||||
}
|
||||
|
||||
public attachToBufferLines(lines: ICircularList<unknown>): void {
|
||||
const store = new DisposableStore();
|
||||
this._bufferLineListeners.value = store;
|
||||
store.add(lines.onTrim(amount => this._handleBufferLinesTrim(amount)));
|
||||
store.add(lines.onInsert(event => this._handleBufferLinesInsert(event)));
|
||||
store.add(lines.onDelete(event => this._handleBufferLinesDelete(event)));
|
||||
}
|
||||
|
||||
private _getDecorationHeight(decoration: IInternalDecoration): number {
|
||||
return decoration.options.height ?? 1;
|
||||
}
|
||||
|
||||
private _addToLineBuckets(decoration: IInternalDecoration): void {
|
||||
const start = decoration.marker.line;
|
||||
if (start < 0) {
|
||||
return;
|
||||
}
|
||||
decoration._indexedStartLine = start;
|
||||
const height = this._getDecorationHeight(decoration);
|
||||
for (let line = start; line < start + height; line++) {
|
||||
let bucket = this._decorationsByLine.get(line);
|
||||
if (!bucket) {
|
||||
bucket = [];
|
||||
this._decorationsByLine.set(line, bucket);
|
||||
}
|
||||
bucket.push(decoration);
|
||||
}
|
||||
}
|
||||
|
||||
private _removeFromLineBuckets(decoration: IInternalDecoration): void {
|
||||
const start = decoration._indexedStartLine;
|
||||
const height = this._getDecorationHeight(decoration);
|
||||
for (let line = start; line < start + height; line++) {
|
||||
const bucket = this._decorationsByLine.get(line);
|
||||
if (!bucket) {
|
||||
continue;
|
||||
}
|
||||
const index = bucket.indexOf(decoration);
|
||||
if (index !== -1) {
|
||||
bucket.splice(index, 1);
|
||||
}
|
||||
if (bucket.length === 0) {
|
||||
this._decorationsByLine.delete(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _reindexDecoration(decoration: IInternalDecoration): void {
|
||||
this._removeFromLineBuckets(decoration);
|
||||
if (!decoration.marker.isDisposed && decoration.marker.line >= 0) {
|
||||
this._addToLineBuckets(decoration);
|
||||
}
|
||||
}
|
||||
|
||||
/** Re-index after marker line updates (buffer listeners may run before markers). */
|
||||
private _scheduleLineIndexSync(callback: () => void): void {
|
||||
this._lineIndexSyncCallbacks.push(callback);
|
||||
this._lineIndexSyncTimer.set(() => {
|
||||
const callbacks = this._lineIndexSyncCallbacks;
|
||||
this._lineIndexSyncCallbacks = [];
|
||||
for (const cb of callbacks) {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _handleBufferLinesTrim(amount: number): void {
|
||||
if (amount <= 0) {
|
||||
return;
|
||||
}
|
||||
const newMap = new Map<number, IInternalDecoration[]>();
|
||||
for (const [line, bucket] of this._decorationsByLine) {
|
||||
const newLine = line - amount;
|
||||
if (newLine < 0) {
|
||||
continue;
|
||||
}
|
||||
const existing = newMap.get(newLine);
|
||||
if (existing) {
|
||||
existing.push(...bucket);
|
||||
} else {
|
||||
newMap.set(newLine, bucket.slice());
|
||||
}
|
||||
}
|
||||
this._decorationsByLine.clear();
|
||||
for (const [line, bucket] of newMap) {
|
||||
this._decorationsByLine.set(line, bucket);
|
||||
}
|
||||
for (const d of this._decorations) {
|
||||
if (!d.marker.isDisposed) {
|
||||
d._indexedStartLine -= amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _handleBufferLinesInsert(event: IInsertEvent): void {
|
||||
this._scheduleLineIndexSync(() => this._applyBufferLinesInsert(event));
|
||||
}
|
||||
|
||||
private _handleBufferLinesDelete(event: IDeleteEvent): void {
|
||||
this._scheduleLineIndexSync(() => this._applyBufferLinesDelete(event));
|
||||
}
|
||||
|
||||
private _mergeLineBucket(newMap: Map<number, IInternalDecoration[]>, line: number, bucket: IInternalDecoration[]): void {
|
||||
const existing = newMap.get(line);
|
||||
if (existing) {
|
||||
existing.push(...bucket);
|
||||
} else {
|
||||
newMap.set(line, bucket.slice());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shift indexed line keys and sync start lines. O(unique indexed lines), not O(decoration count).
|
||||
* Decorations that span the insert point are re-indexed individually (rare vs single-line hits).
|
||||
*/
|
||||
private _applyBufferLinesInsert(event: IInsertEvent): void {
|
||||
const { index, amount } = event;
|
||||
const spanCrossers: IInternalDecoration[] = [];
|
||||
for (const d of this._decorations) {
|
||||
if (d.marker.isDisposed) {
|
||||
continue;
|
||||
}
|
||||
const start = d._indexedStartLine;
|
||||
if (start < index && start + this._getDecorationHeight(d) > index) {
|
||||
spanCrossers.push(d);
|
||||
this._removeFromLineBuckets(d);
|
||||
}
|
||||
}
|
||||
const newMap = new Map<number, IInternalDecoration[]>();
|
||||
for (const [line, bucket] of this._decorationsByLine) {
|
||||
const newLine = line >= index ? line + amount : line;
|
||||
this._mergeLineBucket(newMap, newLine, bucket);
|
||||
}
|
||||
this._decorationsByLine.clear();
|
||||
for (const [line, bucket] of newMap) {
|
||||
this._decorationsByLine.set(line, bucket);
|
||||
}
|
||||
for (const d of this._decorations) {
|
||||
if (d.marker.isDisposed) {
|
||||
continue;
|
||||
}
|
||||
if (d._indexedStartLine >= index) {
|
||||
d._indexedStartLine = d.marker.line;
|
||||
}
|
||||
}
|
||||
for (const d of spanCrossers) {
|
||||
this._addToLineBuckets(d);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop deleted line keys, shift keys below, sync start lines. Full re-index only when a
|
||||
* multi-line decoration spans across the deleted range but survives.
|
||||
*/
|
||||
private _applyBufferLinesDelete(event: IDeleteEvent): void {
|
||||
const deleteEnd = event.index + event.amount;
|
||||
const newMap = new Map<number, IInternalDecoration[]>();
|
||||
for (const [line, bucket] of this._decorationsByLine) {
|
||||
if (line >= event.index && line < deleteEnd) {
|
||||
continue;
|
||||
}
|
||||
const newLine = line >= deleteEnd ? line - event.amount : line;
|
||||
this._mergeLineBucket(newMap, newLine, bucket);
|
||||
}
|
||||
this._decorationsByLine.clear();
|
||||
for (const [line, bucket] of newMap) {
|
||||
this._decorationsByLine.set(line, bucket);
|
||||
}
|
||||
const toReindex: IInternalDecoration[] = [];
|
||||
for (const d of this._decorations) {
|
||||
if (d.marker.isDisposed) {
|
||||
continue;
|
||||
}
|
||||
const start = d._indexedStartLine;
|
||||
const height = this._getDecorationHeight(d);
|
||||
if (start >= deleteEnd) {
|
||||
d._indexedStartLine = d.marker.line;
|
||||
} else if (start < event.index && start + height > deleteEnd) {
|
||||
toReindex.push(d);
|
||||
}
|
||||
}
|
||||
for (const d of toReindex) {
|
||||
this._reindexDecoration(d);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Decoration extends DisposableStore implements IInternalDecoration {
|
||||
public readonly marker: IMarker;
|
||||
public element: HTMLElement | undefined;
|
||||
|
||||
/** Start line used for line-index removal when marker.line is cleared on dispose. */
|
||||
public _indexedStartLine: number;
|
||||
|
||||
public readonly onRenderEmitter = this.add(new Emitter<HTMLElement>());
|
||||
public readonly onRender = this.onRenderEmitter.event;
|
||||
private readonly _onDispose = this.add(new Emitter<void>());
|
||||
@@ -377,7 +144,6 @@ class Decoration extends DisposableStore implements IInternalDecoration {
|
||||
) {
|
||||
super();
|
||||
this.marker = options.marker;
|
||||
this._indexedStartLine = options.marker.line;
|
||||
if (this.options.overviewRulerOptions && !this.options.overviewRulerOptions.position) {
|
||||
this.options.overviewRulerOptions.position = 'full';
|
||||
}
|
||||
|
||||
@@ -396,6 +396,4 @@ export interface IInternalDecoration extends IDecoration {
|
||||
readonly backgroundColorRGB: IColor | undefined;
|
||||
readonly foregroundColorRGB: IColor | undefined;
|
||||
readonly onRenderEmitter: Emitter<HTMLElement>;
|
||||
/** @internal Start line for line-index removal; kept in sync on buffer line shifts. */
|
||||
_indexedStartLine: number;
|
||||
}
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { perfContext, before, RuntimeCase } from 'xterm-benchmark';
|
||||
import { DecorationService } from 'common/services/DecorationService';
|
||||
import { MockLogService, MockBufferService, MockOptionsService } from 'common/TestUtils.test';
|
||||
const enum Constants {
|
||||
COLS = 80,
|
||||
ROWS = 30,
|
||||
SINGLE_LINE_DECORATION_COUNT = 20_000,
|
||||
MULTI_LINE_DECORATION_COUNT = 19_999,
|
||||
MULTI_LINE_HEIGHT = 2,
|
||||
VIEWPORT_SCAN_ITERATIONS = 20
|
||||
}
|
||||
|
||||
function registerSingleLineDecorations(service: DecorationService, bufferService: MockBufferService, count: number): void {
|
||||
const buffer = bufferService.buffer;
|
||||
for (let i = 0; i < count; i++) {
|
||||
const line = i % 5000;
|
||||
const marker = buffer.addMarker(line);
|
||||
service.registerDecoration({ marker, width: Constants.COLS });
|
||||
}
|
||||
}
|
||||
|
||||
function registerMixedDecorations(service: DecorationService, bufferService: MockBufferService): void {
|
||||
const buffer = bufferService.buffer;
|
||||
for (let i = 0; i < Constants.MULTI_LINE_DECORATION_COUNT; i++) {
|
||||
const line = i % 5000;
|
||||
const marker = buffer.addMarker(line);
|
||||
service.registerDecoration({ marker, width: Constants.COLS });
|
||||
}
|
||||
const multiLineMarker = buffer.addMarker(10);
|
||||
service.registerDecoration({
|
||||
marker: multiLineMarker,
|
||||
width: Constants.COLS,
|
||||
height: Constants.MULTI_LINE_HEIGHT
|
||||
});
|
||||
}
|
||||
|
||||
function scanVisibleGrid(service: DecorationService, rows: number, cols: number): number {
|
||||
let hitCount = 0;
|
||||
for (let row = 0; row < rows; row++) {
|
||||
for (let col = 0; col < cols; col++) {
|
||||
service.forEachDecorationAtCell(col, row, undefined, () => {
|
||||
hitCount++;
|
||||
});
|
||||
}
|
||||
}
|
||||
return hitCount;
|
||||
}
|
||||
|
||||
perfContext('DecorationService.forEachDecorationAtCell', () => {
|
||||
perfContext('single-line dense / sparse line hit', () => {
|
||||
let service: DecorationService;
|
||||
let bufferService: MockBufferService;
|
||||
before(() => {
|
||||
bufferService = new MockBufferService(Constants.COLS, Constants.ROWS, new MockOptionsService());
|
||||
service = new DecorationService(new MockLogService(), bufferService);
|
||||
registerSingleLineDecorations(service, bufferService, Constants.SINGLE_LINE_DECORATION_COUNT);
|
||||
});
|
||||
new RuntimeCase('', () => {
|
||||
let hitCount = 0;
|
||||
for (let i = 0; i < Constants.VIEWPORT_SCAN_ITERATIONS; i++) {
|
||||
service.forEachDecorationAtCell(0, 0, undefined, () => {
|
||||
hitCount++;
|
||||
});
|
||||
}
|
||||
return { payloadSize: Constants.VIEWPORT_SCAN_ITERATIONS, hitCount };
|
||||
}, { fork: false }).showAverageRuntime();
|
||||
});
|
||||
|
||||
perfContext('mixed single-line + multi-line / sparse line hit', () => {
|
||||
let service: DecorationService;
|
||||
let bufferService: MockBufferService;
|
||||
before(() => {
|
||||
bufferService = new MockBufferService(Constants.COLS, Constants.ROWS, new MockOptionsService());
|
||||
service = new DecorationService(new MockLogService(), bufferService);
|
||||
registerMixedDecorations(service, bufferService);
|
||||
});
|
||||
new RuntimeCase('', () => {
|
||||
let hitCount = 0;
|
||||
for (let i = 0; i < Constants.VIEWPORT_SCAN_ITERATIONS; i++) {
|
||||
service.forEachDecorationAtCell(0, 10, undefined, () => {
|
||||
hitCount++;
|
||||
});
|
||||
}
|
||||
return { payloadSize: Constants.VIEWPORT_SCAN_ITERATIONS, hitCount };
|
||||
}, { fork: false }).showAverageRuntime();
|
||||
});
|
||||
|
||||
perfContext('viewport grid scan', () => {
|
||||
let service: DecorationService;
|
||||
let bufferService: MockBufferService;
|
||||
before(() => {
|
||||
bufferService = new MockBufferService(Constants.COLS, Constants.ROWS, new MockOptionsService());
|
||||
service = new DecorationService(new MockLogService(), bufferService);
|
||||
registerSingleLineDecorations(service, bufferService, Constants.SINGLE_LINE_DECORATION_COUNT);
|
||||
});
|
||||
new RuntimeCase('', () => {
|
||||
let totalHits = 0;
|
||||
for (let i = 0; i < Constants.VIEWPORT_SCAN_ITERATIONS; i++) {
|
||||
totalHits += scanVisibleGrid(service, Constants.ROWS, Constants.COLS);
|
||||
}
|
||||
return { payloadSize: Constants.VIEWPORT_SCAN_ITERATIONS * Constants.ROWS * Constants.COLS, totalHits };
|
||||
}, { fork: false }).showAverageRuntime();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user