Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
369ca9b525 | ||
|
|
a5422d1b11 | ||
|
|
c2890d8a4d | ||
|
|
7a7abddc45 | ||
|
|
8c71bfd65f | ||
|
|
3c2b911439 | ||
|
|
5ee8760418 |
@@ -22,10 +22,10 @@ npm run build && npm run esbuild # Build all TypeScript and bundle
|
|||||||
**Testing**:
|
**Testing**:
|
||||||
- Unit tests: `npm run test-unit` (Mocha)
|
- Unit tests: `npm run test-unit` (Mocha)
|
||||||
- Unit tests filtering to file: `npm run test-unit -- **/fileName.ts
|
- Unit tests filtering to file: `npm run test-unit -- **/fileName.ts
|
||||||
- Per-addon unit tests: `npm run test-unit -- addons/addon-image/out-esbuild/*.test.js`
|
- Per-addon unit tests: `npm run test-unit addons/addon-image/out-esbuild/*.test.js`
|
||||||
- Integration tests: `npm run test-integration` (Playwright across Chrome/Firefox/WebKit)
|
- Integration tests: `npm run test-integration` (Playwright across Chrome/Firefox/WebKit)
|
||||||
- Integration tests by file: `npm run test-integration -- test/playwright/InputHandler.test.ts`. Never use grep to filter tests, it doesn't work
|
- Integration tests by file: `npm run test-integration -- test/playwright/InputHandler.test.ts`. Never use grep to filter tests, it doesn't work
|
||||||
- Integration tests by addon: `npm run test-integration -- --suite=addon-search`. Suites always follow the format `addon-<something>`
|
- Integration tests by addon: `npm run test-integration --suite=addon-search`. Suites always follow the format `addon-<something>`
|
||||||
- Lint changes: `npm run lint-changes` to lint only changed files, `npm run lint-changes-fix` to fix them
|
- Lint changes: `npm run lint-changes` to lint only changed files, `npm run lint-changes-fix` to fix them
|
||||||
|
|
||||||
## Addon Development Pattern
|
## Addon Development Pattern
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
---
|
|
||||||
applyTo: '**/*.benchmark.ts'
|
|
||||||
---
|
|
||||||
# Benchmark run instructions
|
|
||||||
|
|
||||||
- Full suite: `npm run benchmark`
|
|
||||||
- Single benchmark file:
|
|
||||||
- Tree: `npm run benchmark -- -t out-test/benchmark/Event.benchmark.js`
|
|
||||||
- Run file: `npm run benchmark -- -s "out-test/benchmark/Event.benchmark.js" out-test/benchmark/Event.benchmark.js`
|
|
||||||
- Single context/case:
|
|
||||||
- Use `-t` to get the path, then:
|
|
||||||
- `npm run benchmark -- -s "<path>" out-test/benchmark/Event.benchmark.js`
|
|
||||||
|
|
||||||
When writing instructions, use `RuntimeCase` to measure pure runtime in ms, use `ThroughputRuntimeCase` when measuring throughput in MB/s.
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- Benchmarks run from built JS in `out-test/benchmark/*.benchmark.js`.
|
|
||||||
- Keep `NODE_PATH=./out` (handled by the npm script).
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
[submodule "vendor/ghostty"]
|
|
||||||
path = vendor/ghostty
|
|
||||||
url = https://github.com/ghostty-org/ghostty
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ interface IAttachOptions {
|
|||||||
bidirectional?: boolean;
|
bidirectional?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AttachAddon implements ITerminalAddon, IAttachApi {
|
export class AttachAddon implements ITerminalAddon , IAttachApi {
|
||||||
private _socket: WebSocket;
|
private _socket: WebSocket;
|
||||||
private _bidirectional: boolean;
|
private _bidirectional: boolean;
|
||||||
private _disposables: IDisposable[] = [];
|
private _disposables: IDisposable[] = [];
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ const config: PlaywrightTestConfig = {
|
|||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
name: 'Chromium',
|
name: 'ChromeStable',
|
||||||
use: {
|
use: {
|
||||||
browserName: 'chromium'
|
browserName: 'chromium',
|
||||||
|
channel: 'chrome'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ const config: PlaywrightTestConfig = {
|
|||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
name: 'Chromium',
|
name: 'ChromeStable',
|
||||||
use: {
|
use: {
|
||||||
browserName: 'chromium'
|
browserName: 'chromium',
|
||||||
|
channel: 'chrome'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ function _getComputedStyle(el: HTMLElement): CSSStyleDeclaration {
|
|||||||
return getWindow(el).getComputedStyle(el, null);
|
return getWindow(el).getComputedStyle(el, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FitAddon implements ITerminalAddon, IFitApi {
|
export class FitAddon implements ITerminalAddon , IFitApi {
|
||||||
private _terminal: Terminal | undefined;
|
private _terminal: Terminal | undefined;
|
||||||
|
|
||||||
public activate(terminal: Terminal): void {
|
public activate(terminal: Terminal): void {
|
||||||
@@ -69,10 +69,9 @@ export class FitAddon implements ITerminalAddon, IFitApi {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const showScrollbar = this._terminal.options.scrollbar?.showScrollbar ?? true;
|
const scrollbarWidth = (this._terminal.options.scrollback === 0
|
||||||
const scrollbarWidth = (this._terminal.options.scrollback === 0 || !showScrollbar
|
|
||||||
? 0
|
? 0
|
||||||
: (this._terminal.options.scrollbar?.width ?? ViewportConstants.DEFAULT_SCROLL_BAR_WIDTH));
|
: (this._terminal.options.overviewRuler?.width || ViewportConstants.DEFAULT_SCROLL_BAR_WIDTH));
|
||||||
|
|
||||||
const parentElementStyle = _getComputedStyle(this._terminal.element.parentElement);
|
const parentElementStyle = _getComputedStyle(this._terminal.element.parentElement);
|
||||||
const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
|
const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ const config: PlaywrightTestConfig = {
|
|||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
name: 'Chromium',
|
name: 'ChromeStable',
|
||||||
use: {
|
use: {
|
||||||
browserName: 'chromium'
|
browserName: 'chromium',
|
||||||
|
channel: 'chrome'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ import Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
|
|||||||
import { HeaderParser, IHeaderFields, HeaderState } from './IIPHeaderParser';
|
import { HeaderParser, IHeaderFields, HeaderState } from './IIPHeaderParser';
|
||||||
import { imageType, UNSUPPORTED_TYPE } from './IIPMetrics';
|
import { imageType, UNSUPPORTED_TYPE } from './IIPMetrics';
|
||||||
|
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
declare const Buffer: any;
|
||||||
|
|
||||||
// limit hold memory in base64 decoder
|
// limit hold memory in base64 decoder
|
||||||
const KEEP_DATA = 4194304;
|
const KEEP_DATA = 4194304;
|
||||||
|
|
||||||
@@ -101,8 +105,7 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// HACK: The types on Blob are too restrictive, this is a Uint8Array so the browser accepts it
|
const blob = new Blob([new Uint8Array(this._dec.data8)], { type: this._metrics.mime });
|
||||||
const blob = new Blob([this._dec.data8 as Uint8Array<ArrayBuffer>], { type: this._metrics.mime });
|
|
||||||
this._dec.release();
|
this._dec.release();
|
||||||
|
|
||||||
if (!window.createImageBitmap) {
|
if (!window.createImageBitmap) {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ declare const Buffer: any;
|
|||||||
|
|
||||||
|
|
||||||
export interface IHeaderFields {
|
export interface IHeaderFields {
|
||||||
[key: string]: number | string | Uint32Array | null | undefined;
|
|
||||||
// base-64 encoded filename. Defaults to "Unnamed file".
|
// base-64 encoded filename. Defaults to "Unnamed file".
|
||||||
name: string;
|
name: string;
|
||||||
// File size in bytes. The file transfer will be canceled if this size is exceeded.
|
// File size in bytes. The file transfer will be canceled if this size is exceeded.
|
||||||
@@ -82,7 +81,7 @@ function toName(data: Uint32Array): string {
|
|||||||
return new TextDecoder().decode(b);
|
return new TextDecoder().decode(b);
|
||||||
}
|
}
|
||||||
|
|
||||||
const DECODERS: {[key: string]: (v: Uint32Array) => number | string} = {
|
const DECODERS: {[key: string]: (v: Uint32Array) => any} = {
|
||||||
inline: toInt,
|
inline: toInt,
|
||||||
size: toInt,
|
size: toInt,
|
||||||
name: toName,
|
name: toName,
|
||||||
@@ -101,7 +100,7 @@ export class HeaderParser {
|
|||||||
private _buffer = new Uint32Array(MAX_FIELDCHARS);
|
private _buffer = new Uint32Array(MAX_FIELDCHARS);
|
||||||
private _position = 0;
|
private _position = 0;
|
||||||
private _key = '';
|
private _key = '';
|
||||||
public fields: {[key: string]: number | string | Uint32Array | null | undefined} = {};
|
public fields: {[key: string]: any} = {};
|
||||||
|
|
||||||
public reset(): void {
|
public reset(): void {
|
||||||
this._buffer.fill(0);
|
this._buffer.fill(0);
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ const enum GaStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class ImageAddon implements ITerminalAddon, IImageApi {
|
export class ImageAddon implements ITerminalAddon , IImageApi {
|
||||||
private _opts: IImageAddonOptions;
|
private _opts: IImageAddonOptions;
|
||||||
private _defaultOpts: IImageAddonOptions;
|
private _defaultOpts: IImageAddonOptions;
|
||||||
private _storage: ImageStorage | undefined;
|
private _storage: ImageStorage | undefined;
|
||||||
@@ -90,7 +90,7 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
|
|||||||
// windowOptions.getCellSizePixels = true;
|
// windowOptions.getCellSizePixels = true;
|
||||||
// windowOptions.getWinSizeChars = true;
|
// windowOptions.getWinSizeChars = true;
|
||||||
// terminal.setOption('windowOptions', windowOptions);
|
// terminal.setOption('windowOptions', windowOptions);
|
||||||
const windowOps = terminal.options.windowOptions ?? {};
|
const windowOps = terminal.options.windowOptions || {};
|
||||||
windowOps.getWinSizePixels = true;
|
windowOps.getWinSizePixels = true;
|
||||||
windowOps.getCellSizePixels = true;
|
windowOps.getCellSizePixels = true;
|
||||||
windowOps.getWinSizeChars = true;
|
windowOps.getWinSizeChars = true;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import { toRGBA8888 } from 'sixel/lib/Colors';
|
import { toRGBA8888 } from 'sixel/lib/Colors';
|
||||||
import { IDisposable } from '@xterm/xterm';
|
import { IDisposable } from '@xterm/xterm';
|
||||||
import { ICellSize, ITerminalExt, IImageSpec, IRenderDimensions, IRenderService } from './Types';
|
import { ICellSize, ITerminalExt, IImageSpec, IRenderDimensions, IRenderService } from './Types';
|
||||||
import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
|
import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||||
|
|
||||||
const PLACEHOLDER_LENGTH = 4096;
|
const PLACEHOLDER_LENGTH = 4096;
|
||||||
const PLACEHOLDER_HEIGHT = 24;
|
const PLACEHOLDER_HEIGHT = 24;
|
||||||
@@ -38,7 +38,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|||||||
* Only the DOM output canvas should be on the terminal's document,
|
* Only the DOM output canvas should be on the terminal's document,
|
||||||
* which gets explicitly checked in `insertLayerToDom`.
|
* which gets explicitly checked in `insertLayerToDom`.
|
||||||
*/
|
*/
|
||||||
const canvas = (localDocument ?? document).createElement('canvas');
|
const canvas = (localDocument || document).createElement('canvas');
|
||||||
canvas.width = width | 0;
|
canvas.width = width | 0;
|
||||||
canvas.height = height | 0;
|
canvas.height = height | 0;
|
||||||
return canvas;
|
return canvas;
|
||||||
@@ -242,7 +242,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|||||||
}
|
}
|
||||||
if (!this._placeholder) return;
|
if (!this._placeholder) return;
|
||||||
this._ctx.drawImage(
|
this._ctx.drawImage(
|
||||||
this._placeholderBitmap ?? this._placeholder!,
|
this._placeholderBitmap || this._placeholder!,
|
||||||
col * width,
|
col * width,
|
||||||
(row * height) % 2 ? 0 : 1, // needs %2 offset correction
|
(row * height) % 2 ? 0 : 1, // needs %2 offset correction
|
||||||
width * count,
|
width * count,
|
||||||
|
|||||||
@@ -132,10 +132,8 @@ export class ImageStorage implements IDisposable {
|
|||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
this.setLimit(this._opts.storageLimit);
|
this.setLimit(this._opts.storageLimit);
|
||||||
} catch (e: unknown) {
|
} catch (e: any) {
|
||||||
if (e instanceof Error) {
|
console.error(e.message);
|
||||||
console.error(e.message);
|
|
||||||
}
|
|
||||||
console.warn(`storageLimit is set to ${this.getLimit()} MB`);
|
console.warn(`storageLimit is set to ${this.getLimit()} MB`);
|
||||||
}
|
}
|
||||||
this._viewportMetrics = {
|
this._viewportMetrics = {
|
||||||
@@ -383,7 +381,7 @@ export class ImageStorage implements IDisposable {
|
|||||||
if (!line) return;
|
if (!line) return;
|
||||||
for (let col = 0; col < cols; ++col) {
|
for (let col = 0; col < cols; ++col) {
|
||||||
if (line.getBg(col) & BgFlags.HAS_EXTENDED) {
|
if (line.getBg(col) & BgFlags.HAS_EXTENDED) {
|
||||||
let e: IExtendedAttrsImage = line._extendedAttrs[col] ?? EMPTY_ATTRS;
|
let e: IExtendedAttrsImage = line._extendedAttrs[col] || EMPTY_ATTRS;
|
||||||
const imageId = e.imageId;
|
const imageId = e.imageId;
|
||||||
if (imageId === undefined || imageId === -1) {
|
if (imageId === undefined || imageId === -1) {
|
||||||
continue;
|
continue;
|
||||||
@@ -402,7 +400,7 @@ export class ImageStorage implements IDisposable {
|
|||||||
while (
|
while (
|
||||||
++col < cols
|
++col < cols
|
||||||
&& (line.getBg(col) & BgFlags.HAS_EXTENDED)
|
&& (line.getBg(col) & BgFlags.HAS_EXTENDED)
|
||||||
&& (e = line._extendedAttrs[col] ?? EMPTY_ATTRS)
|
&& (e = line._extendedAttrs[col] || EMPTY_ATTRS)
|
||||||
&& (e.imageId === imageId)
|
&& (e.imageId === imageId)
|
||||||
&& (e.tileId === startTile + count)
|
&& (e.tileId === startTile + count)
|
||||||
) {
|
) {
|
||||||
@@ -444,7 +442,7 @@ export class ImageStorage implements IDisposable {
|
|||||||
for (let row = 0; row < rows; ++row) {
|
for (let row = 0; row < rows; ++row) {
|
||||||
const line = buffer.lines.get(row) as IBufferLineExt;
|
const line = buffer.lines.get(row) as IBufferLineExt;
|
||||||
if (line.getBg(oldCol) & BgFlags.HAS_EXTENDED) {
|
if (line.getBg(oldCol) & BgFlags.HAS_EXTENDED) {
|
||||||
const e: IExtendedAttrsImage = line._extendedAttrs[oldCol] ?? EMPTY_ATTRS;
|
const e: IExtendedAttrsImage = line._extendedAttrs[oldCol] || EMPTY_ATTRS;
|
||||||
const imageId = e.imageId;
|
const imageId = e.imageId;
|
||||||
if (imageId === undefined || imageId === -1) {
|
if (imageId === undefined || imageId === -1) {
|
||||||
continue;
|
continue;
|
||||||
@@ -489,7 +487,7 @@ export class ImageStorage implements IDisposable {
|
|||||||
const buffer = this._terminal._core.buffer;
|
const buffer = this._terminal._core.buffer;
|
||||||
const line = buffer.lines.get(y) as IBufferLineExt;
|
const line = buffer.lines.get(y) as IBufferLineExt;
|
||||||
if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) {
|
if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) {
|
||||||
const e: IExtendedAttrsImage = line._extendedAttrs[x] ?? EMPTY_ATTRS;
|
const e: IExtendedAttrsImage = line._extendedAttrs[x] || EMPTY_ATTRS;
|
||||||
if (e.imageId && e.imageId !== -1) {
|
if (e.imageId && e.imageId !== -1) {
|
||||||
const orig = this._images.get(e.imageId)?.orig;
|
const orig = this._images.get(e.imageId)?.orig;
|
||||||
if (window.ImageBitmap && orig instanceof ImageBitmap) {
|
if (window.ImageBitmap && orig instanceof ImageBitmap) {
|
||||||
@@ -509,7 +507,7 @@ export class ImageStorage implements IDisposable {
|
|||||||
const buffer = this._terminal._core.buffer;
|
const buffer = this._terminal._core.buffer;
|
||||||
const line = buffer.lines.get(y) as IBufferLineExt;
|
const line = buffer.lines.get(y) as IBufferLineExt;
|
||||||
if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) {
|
if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) {
|
||||||
const e: IExtendedAttrsImage = line._extendedAttrs[x] ?? EMPTY_ATTRS;
|
const e: IExtendedAttrsImage = line._extendedAttrs[x] || EMPTY_ATTRS;
|
||||||
if (e.imageId && e.imageId !== -1 && e.tileId !== -1) {
|
if (e.imageId && e.imageId !== -1 && e.tileId !== -1) {
|
||||||
const spec = this._images.get(e.imageId);
|
const spec = this._images.get(e.imageId);
|
||||||
if (spec) {
|
if (spec) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"paths": {
|
"paths": {
|
||||||
"browser/*": [ "../../../src/browser/*" ],
|
"browser/*": [ "../../../src/browser/*" ],
|
||||||
"common/*": [ "../../../src/common/*" ],
|
"common/*": [ "../../../src/common/*" ],
|
||||||
|
"vs/*": [ "../../../src/vs/*" ],
|
||||||
"@xterm/addon-image": [ "../typings/addon-image.d.ts" ]
|
"@xterm/addon-image": [ "../typings/addon-image.d.ts" ]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -24,6 +25,7 @@
|
|||||||
],
|
],
|
||||||
"references": [
|
"references": [
|
||||||
{ "path": "../../../src/browser" },
|
{ "path": "../../../src/browser" },
|
||||||
{ "path": "../../../src/common" }
|
{ "path": "../../../src/common" },
|
||||||
|
{ "path": "../../../src/vs" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ const config: PlaywrightTestConfig = {
|
|||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
name: 'Chromium',
|
name: 'ChromeStable',
|
||||||
use: {
|
use: {
|
||||||
browserName: 'chromium'
|
browserName: 'chromium',
|
||||||
|
channel: 'chrome'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export interface ITerminalAddon {
|
|||||||
dispose(): void;
|
dispose(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LigaturesAddon implements ITerminalAddon, ILigaturesApi {
|
export class LigaturesAddon implements ITerminalAddon , ILigaturesApi {
|
||||||
private readonly _fallbackLigatures: string[];
|
private readonly _fallbackLigatures: string[];
|
||||||
private readonly _fontFeatureSettings?: string;
|
private readonly _fontFeatureSettings?: string;
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ export class LigaturesAddon implements ITerminalAddon, ILigaturesApi {
|
|||||||
|
|
||||||
constructor(options?: Partial<ILigatureOptions>) {
|
constructor(options?: Partial<ILigatureOptions>) {
|
||||||
// Source: calt set from https://github.com/be5invis/Iosevka?tab=readme-ov-file#ligations
|
// Source: calt set from https://github.com/be5invis/Iosevka?tab=readme-ov-file#ligations
|
||||||
this._fallbackLigatures = (options?.fallbackLigatures ?? [
|
this._fallbackLigatures = (options?.fallbackLigatures || [
|
||||||
'<--', '<---', '<<-', '<-', '->', '->>', '-->', '--->',
|
'<--', '<---', '<<-', '<-', '->', '->>', '-->', '--->',
|
||||||
'<==', '<===', '<<=', '<=', '=>', '=>>', '==>', '===>', '>=', '>>=',
|
'<==', '<===', '<<=', '<=', '=>', '=>>', '==>', '===>', '>=', '>>=',
|
||||||
'<->', '<-->', '<--->', '<---->', '<=>', '<==>', '<===>', '<====>', '::', ':::',
|
'<->', '<-->', '<--->', '<---->', '<=>', '<==>', '<===>', '<====>', '::', ':::',
|
||||||
|
|||||||
@@ -42,11 +42,11 @@ export default async function load(fontFamily: string, cacheSize: number): Promi
|
|||||||
if (status && status.state !== 'granted') {
|
if (status && status.state !== 'granted') {
|
||||||
throw new Error('Permission to access local fonts not granted.');
|
throw new Error('Permission to access local fonts not granted.');
|
||||||
}
|
}
|
||||||
} catch (err: unknown) {
|
} catch (err: any) {
|
||||||
// A `TypeError` indicates the 'local-fonts'
|
// A `TypeError` indicates the 'local-fonts'
|
||||||
// permission is not yet implemented, so
|
// permission is not yet implemented, so
|
||||||
// only `throw` if this is _not_ the problem.
|
// only `throw` if this is _not_ the problem.
|
||||||
if (err instanceof Error && err.name !== 'TypeError') {
|
if (err.name !== 'TypeError') {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,10 +60,8 @@ export default async function load(fontFamily: string, cacheSize: number): Promi
|
|||||||
fonts[metadata.family].push(metadata);
|
fonts[metadata.family].push(metadata);
|
||||||
}
|
}
|
||||||
fontsPromise = Promise.resolve(fonts);
|
fontsPromise = Promise.resolve(fonts);
|
||||||
} catch (err: unknown) {
|
} catch (err: any) {
|
||||||
if (err instanceof Error) {
|
console.error(err.name, err.message);
|
||||||
console.error(err.name, err.message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Latest proposal https://bugs.chromium.org/p/chromium/issues/detail?id=1312603
|
// Latest proposal https://bugs.chromium.org/p/chromium/issues/detail?id=1312603
|
||||||
@@ -78,13 +76,13 @@ export default async function load(fontFamily: string, cacheSize: number): Promi
|
|||||||
fonts[metadata.family].push(metadata);
|
fonts[metadata.family].push(metadata);
|
||||||
}
|
}
|
||||||
fontsPromise = Promise.resolve(fonts);
|
fontsPromise = Promise.resolve(fonts);
|
||||||
} catch (err: unknown) {
|
} catch (err: any) {
|
||||||
if (err instanceof Error) {
|
console.error(err.name, err.message);
|
||||||
console.error(err.name, err.message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fontsPromise ??= Promise.resolve({});
|
if (!fontsPromise) {
|
||||||
|
fontsPromise = Promise.resolve({});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fonts = await fontsPromise;
|
const fonts = await fontsPromise;
|
||||||
|
|||||||
@@ -52,10 +52,12 @@ export function processLookaheadPosition(
|
|||||||
}
|
}
|
||||||
processedEntries.add(currentEntry.entry);
|
processedEntries.add(currentEntry.entry);
|
||||||
|
|
||||||
currentEntry.entry.forward ??= {
|
if (!currentEntry.entry.forward) {
|
||||||
individual: {},
|
currentEntry.entry.forward = {
|
||||||
range: []
|
individual: {},
|
||||||
};
|
range: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// All glyphs at this position share ONE entry - lookahead just needs to match,
|
// All glyphs at this position share ONE entry - lookahead just needs to match,
|
||||||
// all paths lead to the same result
|
// all paths lead to the same result
|
||||||
@@ -95,10 +97,12 @@ export function processBacktrackPosition(
|
|||||||
}
|
}
|
||||||
processedEntries.add(currentEntry.entry);
|
processedEntries.add(currentEntry.entry);
|
||||||
|
|
||||||
currentEntry.entry.reverse ??= {
|
if (!currentEntry.entry.reverse) {
|
||||||
individual: {},
|
currentEntry.entry.reverse = {
|
||||||
range: []
|
individual: {},
|
||||||
};
|
range: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// All glyphs at this position share ONE entry - backtrack just needs to match,
|
// All glyphs at this position share ONE entry - backtrack just needs to match,
|
||||||
// all paths lead to the same result
|
// all paths lead to the same result
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ export function getIndividualSubstitutionGlyph(table: SubstitutionTable, glyphId
|
|||||||
return (glyphId + table.deltaGlyphId) % (2 ** 16);
|
return (glyphId + table.deltaGlyphId) % (2 ** 16);
|
||||||
// https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#12-single-substitution-format-2
|
// https://docs.microsoft.com/en-us/typography/opentype/spec/gsub#12-single-substitution-format-2
|
||||||
case 2:
|
case 2:
|
||||||
return table.substitute[coverageIndex] ?? null;
|
// eslint-disable-next-line eqeqeq
|
||||||
|
return table.substitute[coverageIndex] != null
|
||||||
|
? table.substitute[coverageIndex]
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export function enableLigatures(term: Terminal, fallbackLigatures: string[] = []
|
|||||||
let currentFontName: string | undefined = undefined;
|
let currentFontName: string | undefined = undefined;
|
||||||
let font: Font | undefined = undefined;
|
let font: Font | undefined = undefined;
|
||||||
let loadingState: LoadingState = LoadingState.UNLOADED;
|
let loadingState: LoadingState = LoadingState.UNLOADED;
|
||||||
let loadError: unknown = undefined;
|
let loadError: any | undefined = undefined;
|
||||||
|
|
||||||
return term.registerCharacterJoiner((text: string): [number, number][] => {
|
return term.registerCharacterJoiner((text: string): [number, number][] => {
|
||||||
// If the font hasn't been loaded yet, load it and return an empty result
|
// If the font hasn't been loaded yet, load it and return an empty result
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import type { Terminal, ITerminalAddon, IDisposable } from '@xterm/xterm';
|
import type { Terminal, ITerminalAddon, IDisposable } from '@xterm/xterm';
|
||||||
import type { ProgressAddon as IProgressApi, IProgressState } from '@xterm/addon-progress';
|
import type { ProgressAddon as IProgressApi, IProgressState } from '@xterm/addon-progress';
|
||||||
import type { Emitter, IEvent } from 'common/Event';
|
import type { Emitter, Event } from 'vs/base/common/event';
|
||||||
|
|
||||||
|
|
||||||
const enum ProgressType {
|
const enum ProgressType {
|
||||||
@@ -39,7 +39,7 @@ export class ProgressAddon implements ITerminalAddon, IProgressApi {
|
|||||||
private _pr = 0;
|
private _pr = 0;
|
||||||
// HACK: This uses ! to align with the API, this should be fixed when 5283 is resolved
|
// HACK: This uses ! to align with the API, this should be fixed when 5283 is resolved
|
||||||
private _onChange!: Emitter<IProgressState>;
|
private _onChange!: Emitter<IProgressState>;
|
||||||
public onChange!: IEvent<IProgressState>;
|
public onChange!: Event<IProgressState>;
|
||||||
|
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
this._seqHandler?.dispose();
|
this._seqHandler?.dispose();
|
||||||
|
|||||||
@@ -15,12 +15,12 @@
|
|||||||
"../../../node_modules/@types/mocha"
|
"../../../node_modules/@types/mocha"
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"common/*": [
|
|
||||||
"../../../src/common/*"
|
|
||||||
],
|
|
||||||
"browser/*": [
|
"browser/*": [
|
||||||
"../../../src/browser/*"
|
"../../../src/browser/*"
|
||||||
],
|
],
|
||||||
|
"vs/*": [
|
||||||
|
"../../../src/vs/*"
|
||||||
|
],
|
||||||
"@xterm/addon-progress": [
|
"@xterm/addon-progress": [
|
||||||
"../typings/addon-progress.d.ts"
|
"../typings/addon-progress.d.ts"
|
||||||
]
|
]
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
"path": "../../../src/browser"
|
"path": "../../../src/browser"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../../../src/common"
|
"path": "../../../src/vs"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ const config: PlaywrightTestConfig = {
|
|||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
name: 'Chromium',
|
name: 'ChromeStable',
|
||||||
use: {
|
use: {
|
||||||
browserName: 'chromium'
|
browserName: 'chromium',
|
||||||
|
channel: 'chrome'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import type { Terminal, IDisposable, IDecoration } from '@xterm/xterm';
|
import type { Terminal, IDisposable, IDecoration } from '@xterm/xterm';
|
||||||
import type { ISearchDecorationOptions } from '@xterm/addon-search';
|
import type { ISearchDecorationOptions } from '@xterm/addon-search';
|
||||||
import { dispose, Disposable, toDisposable } from 'common/Lifecycle';
|
import { dispose, Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||||
import type { ISearchResult } from './SearchEngine';
|
import type { ISearchResult } from './SearchEngine';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Terminal, IDisposable, ITerminalAddon } from '@xterm/xterm';
|
import type { Terminal, IDisposable, ITerminalAddon } from '@xterm/xterm';
|
||||||
import type { SearchAddon as ISearchApi, ISearchOptions, ISearchAddonOptions, ISearchResultChangeEvent, ISearchDecorationOptions } from '@xterm/addon-search';
|
import type { SearchAddon as ISearchApi, ISearchOptions, ISearchAddonOptions, ISearchResultChangeEvent } from '@xterm/addon-search';
|
||||||
import { Emitter, type IEvent } from 'common/Event';
|
import { Emitter, Event } from 'vs/base/common/event';
|
||||||
import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
|
import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||||
import { disposableTimeout } from 'common/Async';
|
import { disposableTimeout } from 'vs/base/common/async';
|
||||||
import { SearchLineCache } from './SearchLineCache';
|
import { SearchLineCache } from './SearchLineCache';
|
||||||
import { SearchState } from './SearchState';
|
import { SearchState } from './SearchState';
|
||||||
import { SearchEngine, type ISearchResult } from './SearchEngine';
|
import { SearchEngine, type ISearchResult } from './SearchEngine';
|
||||||
@@ -47,7 +47,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
|
|||||||
private readonly _onBeforeSearch = this._register(new Emitter<void>());
|
private readonly _onBeforeSearch = this._register(new Emitter<void>());
|
||||||
public readonly onBeforeSearch = this._onBeforeSearch.event;
|
public readonly onBeforeSearch = this._onBeforeSearch.event;
|
||||||
|
|
||||||
public get onDidChangeResults(): IEvent<ISearchResultChangeEvent> {
|
public get onDidChangeResults(): Event<ISearchResultChangeEvent> {
|
||||||
return this._resultTracker.onDidChangeResults;
|
return this._resultTracker.onDidChangeResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,7 +222,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
|
|||||||
* @param result The result to select.
|
* @param result The result to select.
|
||||||
* @returns Whether a result was selected.
|
* @returns Whether a result was selected.
|
||||||
*/
|
*/
|
||||||
private _selectResult(result: ISearchResult | undefined, options?: ISearchDecorationOptions, noScroll?: boolean): boolean {
|
private _selectResult(result: ISearchResult | undefined, options?: any, noScroll?: boolean): boolean {
|
||||||
if (!this._terminal || !this._decorationManager) {
|
if (!this._terminal || !this._decorationManager) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { SearchEngine } from './SearchEngine';
|
|||||||
import { SearchLineCache } from './SearchLineCache';
|
import { SearchLineCache } from './SearchLineCache';
|
||||||
import { Terminal } from 'browser/public/Terminal';
|
import { Terminal } from 'browser/public/Terminal';
|
||||||
import type { ISearchOptions } from '@xterm/addon-search';
|
import type { ISearchOptions } from '@xterm/addon-search';
|
||||||
import { DisposableStore } from 'common/Lifecycle';
|
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||||
|
|
||||||
function writeP(terminal: Terminal, data: string): Promise<void> {
|
function writeP(terminal: Terminal, data: string): Promise<void> {
|
||||||
return new Promise(r => terminal.write(data, r));
|
return new Promise(r => terminal.write(data, r));
|
||||||
|
|||||||
@@ -64,9 +64,6 @@ export class SearchEngine {
|
|||||||
throw new Error(`Invalid col: ${startCol} to search in terminal of ${this._terminal.cols} cols`);
|
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();
|
this._lineCache.initLinesCache();
|
||||||
|
|
||||||
const searchPosition: ISearchPosition = {
|
const searchPosition: ISearchPosition = {
|
||||||
@@ -201,7 +198,9 @@ export class SearchEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result ??= this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
|
if (!result) {
|
||||||
|
result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
|
||||||
|
}
|
||||||
|
|
||||||
// Search from startRow - 1 to top
|
// Search from startRow - 1 to top
|
||||||
if (!result) {
|
if (!result) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import { SearchLineCache, LineCacheEntry } from './SearchLineCache';
|
import { SearchLineCache, LineCacheEntry } from './SearchLineCache';
|
||||||
import { Terminal } from 'browser/public/Terminal';
|
import { Terminal } from 'browser/public/Terminal';
|
||||||
import { timeout } from 'common/Async';
|
import { timeout } from 'vs/base/common/async';
|
||||||
|
|
||||||
function writeP(terminal: Terminal, data: string): Promise<void> {
|
function writeP(terminal: Terminal, data: string): Promise<void> {
|
||||||
return new Promise(r => terminal.write(data, r));
|
return new Promise(r => terminal.write(data, r));
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Terminal } from '@xterm/xterm';
|
import type { Terminal } from '@xterm/xterm';
|
||||||
import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
|
import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||||
import { disposableTimeout } from 'common/Async';
|
import { disposableTimeout } from 'vs/base/common/async';
|
||||||
|
|
||||||
export type LineCacheEntry = [
|
export type LineCacheEntry = [
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
|
|
||||||
import type { ISearchResultChangeEvent } from '@xterm/addon-search';
|
import type { ISearchResultChangeEvent } from '@xterm/addon-search';
|
||||||
import type { IDisposable } from '@xterm/xterm';
|
import type { IDisposable } from '@xterm/xterm';
|
||||||
import { Emitter, type IEvent } from 'common/Event';
|
import { Emitter, Event } from 'vs/base/common/event';
|
||||||
import { Disposable } from 'common/Lifecycle';
|
import { Disposable } from 'vs/base/common/lifecycle';
|
||||||
import type { ISearchResult } from './SearchEngine';
|
import type { ISearchResult } from './SearchEngine';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,7 +25,7 @@ export class SearchResultTracker extends Disposable {
|
|||||||
private _selectedDecoration: ISelectedDecoration | undefined;
|
private _selectedDecoration: ISelectedDecoration | undefined;
|
||||||
|
|
||||||
private readonly _onDidChangeResults = this._register(new Emitter<ISearchResultChangeEvent>());
|
private readonly _onDidChangeResults = this._register(new Emitter<ISearchResultChangeEvent>());
|
||||||
public get onDidChangeResults(): IEvent<ISearchResultChangeEvent> { return this._onDidChangeResults.event; }
|
public get onDidChangeResults(): Event<ISearchResultChangeEvent> { return this._onDidChangeResults.event; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the current search results.
|
* Gets the current search results.
|
||||||
|
|||||||
@@ -21,6 +21,9 @@
|
|||||||
"browser/*": [
|
"browser/*": [
|
||||||
"../../../src/browser/*"
|
"../../../src/browser/*"
|
||||||
],
|
],
|
||||||
|
"vs/*": [
|
||||||
|
"../../../src/vs/*"
|
||||||
|
],
|
||||||
"@xterm/addon-search" : [
|
"@xterm/addon-search" : [
|
||||||
"../typings/addon-search.d.ts"
|
"../typings/addon-search.d.ts"
|
||||||
]
|
]
|
||||||
@@ -36,6 +39,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../../../src/browser"
|
"path": "../../../src/browser"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../../src/vs"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ const config: PlaywrightTestConfig = {
|
|||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
name: 'Chromium',
|
name: 'ChromeStable',
|
||||||
use: {
|
use: {
|
||||||
browserName: 'chromium'
|
browserName: 'chromium',
|
||||||
|
channel: 'chrome'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ describe('SerializeAddon', () => {
|
|||||||
describe('underline styles', () => {
|
describe('underline styles', () => {
|
||||||
it('should serialize single underline with style', async () => {
|
it('should serialize single underline with style', async () => {
|
||||||
await writeP(terminal, sgr('4:1') + 'test' + sgr('24'));
|
await writeP(terminal, sgr('4:1') + 'test' + sgr('24'));
|
||||||
assert.equal(serializeAddon.serialize(), '\u001b[4mtest\u001b[0m');
|
assert.equal(serializeAddon.serialize(), '\u001b[4:1mtest\u001b[0m');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should serialize double underline', async () => {
|
it('should serialize double underline', async () => {
|
||||||
@@ -228,7 +228,7 @@ describe('SerializeAddon', () => {
|
|||||||
it('empty terminal with selection turned off', () => {
|
it('empty terminal with selection turned off', () => {
|
||||||
const output = serializeAddon.serializeAsHTML();
|
const output = serializeAddon.serializeAsHTML();
|
||||||
assert.notEqual(output, '');
|
assert.notEqual(output, '');
|
||||||
assert.equal((output.match(/<div><span> {10}<\/span><\/div>/g) ?? []).length, 2);
|
assert.equal((output.match(/<div><span> {10}<\/span><\/div>/g) || []).length, 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('empty terminal with no selection', () => {
|
it('empty terminal with no selection', () => {
|
||||||
@@ -245,7 +245,7 @@ describe('SerializeAddon', () => {
|
|||||||
const output = serializeAddon.serializeAsHTML({
|
const output = serializeAddon.serializeAsHTML({
|
||||||
onlySelection: true
|
onlySelection: true
|
||||||
});
|
});
|
||||||
assert.equal((output.match(/<div><span>terminal<\/span><\/div>/g) ?? []).length, 1, output);
|
assert.equal((output.match(/<div><span>terminal<\/span><\/div>/g) || []).length, 1, output);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('basic terminal with html unsafe chars', async () => {
|
it('basic terminal with html unsafe chars', async () => {
|
||||||
@@ -255,7 +255,7 @@ describe('SerializeAddon', () => {
|
|||||||
const output = serializeAddon.serializeAsHTML({
|
const output = serializeAddon.serializeAsHTML({
|
||||||
onlySelection: true
|
onlySelection: true
|
||||||
});
|
});
|
||||||
assert.equal((output.match(/<div><span><a>&pi;<\/span><\/div>/g) ?? []).length, 1, output);
|
assert.equal((output.match(/<div><span><a>&pi;<\/span><\/div>/g) || []).length, 1, output);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('serializes rows within a provided range', async () => {
|
it('serializes rows within a provided range', async () => {
|
||||||
@@ -267,7 +267,7 @@ describe('SerializeAddon', () => {
|
|||||||
startCol: 4
|
startCol: 4
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const rowMatches = output.match(/<div><span>.*?<\/span><\/div>/g) ?? [];
|
const rowMatches = output.match(/<div><span>.*?<\/span><\/div>/g) || [];
|
||||||
assert.equal(rowMatches.length, 1, output);
|
assert.equal(rowMatches.length, 1, output);
|
||||||
assert.ok(rowMatches[0]?.includes('hello'));
|
assert.ok(rowMatches[0]?.includes('hello'));
|
||||||
assert.ok(!output.includes('bye'));
|
assert.ok(!output.includes('bye'));
|
||||||
@@ -278,56 +278,56 @@ describe('SerializeAddon', () => {
|
|||||||
await writeP(terminal, ' ' + sgr('1') + 'terminal' + sgr('22') + ' ');
|
await writeP(terminal, ' ' + sgr('1') + 'terminal' + sgr('22') + ' ');
|
||||||
|
|
||||||
const output = serializeAddon.serializeAsHTML();
|
const output = serializeAddon.serializeAsHTML();
|
||||||
assert.equal((output.match(/<span style='font-weight: bold;'>terminal<\/span>/g) ?? []).length, 1, output);
|
assert.equal((output.match(/<span style='font-weight: bold;'>terminal<\/span>/g) || []).length, 1, output);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cells with italic styling', async () => {
|
it('cells with italic styling', async () => {
|
||||||
await writeP(terminal, ' ' + sgr('3') + 'terminal' + sgr('23') + ' ');
|
await writeP(terminal, ' ' + sgr('3') + 'terminal' + sgr('23') + ' ');
|
||||||
|
|
||||||
const output = serializeAddon.serializeAsHTML();
|
const output = serializeAddon.serializeAsHTML();
|
||||||
assert.equal((output.match(/<span style='font-style: italic;'>terminal<\/span>/g) ?? []).length, 1, output);
|
assert.equal((output.match(/<span style='font-style: italic;'>terminal<\/span>/g) || []).length, 1, output);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cells with inverse styling', async () => {
|
it('cells with inverse styling', async () => {
|
||||||
await writeP(terminal, ' ' + sgr('7') + 'terminal' + sgr('27') + ' ');
|
await writeP(terminal, ' ' + sgr('7') + 'terminal' + sgr('27') + ' ');
|
||||||
|
|
||||||
const output = serializeAddon.serializeAsHTML();
|
const output = serializeAddon.serializeAsHTML();
|
||||||
assert.equal((output.match(/<span style='color: #000000; background-color: #BFBFBF;'>terminal<\/span>/g) ?? []).length, 1, output);
|
assert.equal((output.match(/<span style='color: #000000; background-color: #BFBFBF;'>terminal<\/span>/g) || []).length, 1, output);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cells with underline styling', async () => {
|
it('cells with underline styling', async () => {
|
||||||
await writeP(terminal, ' ' + sgr('4') + 'terminal' + sgr('24') + ' ');
|
await writeP(terminal, ' ' + sgr('4') + 'terminal' + sgr('24') + ' ');
|
||||||
|
|
||||||
const output = serializeAddon.serializeAsHTML();
|
const output = serializeAddon.serializeAsHTML();
|
||||||
assert.equal((output.match(/<span style='text-decoration: underline;'>terminal<\/span>/g) ?? []).length, 1, output);
|
assert.equal((output.match(/<span style='text-decoration: underline;'>terminal<\/span>/g) || []).length, 1, output);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cells with double underline styling', async () => {
|
it('cells with double underline styling', async () => {
|
||||||
await writeP(terminal, ' ' + sgr('4:2') + 'terminal' + sgr('24') + ' ');
|
await writeP(terminal, ' ' + sgr('4:2') + 'terminal' + sgr('24') + ' ');
|
||||||
|
|
||||||
const output = serializeAddon.serializeAsHTML();
|
const output = serializeAddon.serializeAsHTML();
|
||||||
assert.equal((output.match(/<span style='text-decoration: underline double;'>terminal<\/span>/g) ?? []).length, 1, output);
|
assert.equal((output.match(/<span style='text-decoration: underline double;'>terminal<\/span>/g) || []).length, 1, output);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cells with curly underline styling', async () => {
|
it('cells with curly underline styling', async () => {
|
||||||
await writeP(terminal, ' ' + sgr('4:3') + 'terminal' + sgr('24') + ' ');
|
await writeP(terminal, ' ' + sgr('4:3') + 'terminal' + sgr('24') + ' ');
|
||||||
|
|
||||||
const output = serializeAddon.serializeAsHTML();
|
const output = serializeAddon.serializeAsHTML();
|
||||||
assert.equal((output.match(/<span style='text-decoration: underline wavy;'>terminal<\/span>/g) ?? []).length, 1, output);
|
assert.equal((output.match(/<span style='text-decoration: underline wavy;'>terminal<\/span>/g) || []).length, 1, output);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cells with dotted underline styling', async () => {
|
it('cells with dotted underline styling', async () => {
|
||||||
await writeP(terminal, ' ' + sgr('4:4') + 'terminal' + sgr('24') + ' ');
|
await writeP(terminal, ' ' + sgr('4:4') + 'terminal' + sgr('24') + ' ');
|
||||||
|
|
||||||
const output = serializeAddon.serializeAsHTML();
|
const output = serializeAddon.serializeAsHTML();
|
||||||
assert.equal((output.match(/<span style='text-decoration: underline dotted;'>terminal<\/span>/g) ?? []).length, 1, output);
|
assert.equal((output.match(/<span style='text-decoration: underline dotted;'>terminal<\/span>/g) || []).length, 1, output);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cells with dashed underline styling', async () => {
|
it('cells with dashed underline styling', async () => {
|
||||||
await writeP(terminal, ' ' + sgr('4:5') + 'terminal' + sgr('24') + ' ');
|
await writeP(terminal, ' ' + sgr('4:5') + 'terminal' + sgr('24') + ' ');
|
||||||
|
|
||||||
const output = serializeAddon.serializeAsHTML();
|
const output = serializeAddon.serializeAsHTML();
|
||||||
assert.equal((output.match(/<span style='text-decoration: underline dashed;'>terminal<\/span>/g) ?? []).length, 1, output);
|
assert.equal((output.match(/<span style='text-decoration: underline dashed;'>terminal<\/span>/g) || []).length, 1, output);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cells with underline color (palette)', async () => {
|
it('cells with underline color (palette)', async () => {
|
||||||
@@ -350,49 +350,49 @@ describe('SerializeAddon', () => {
|
|||||||
await writeP(terminal, ' ' + sgr('8') + 'terminal' + sgr('28') + ' ');
|
await writeP(terminal, ' ' + sgr('8') + 'terminal' + sgr('28') + ' ');
|
||||||
|
|
||||||
const output = serializeAddon.serializeAsHTML();
|
const output = serializeAddon.serializeAsHTML();
|
||||||
assert.equal((output.match(/<span style='visibility: hidden;'>terminal<\/span>/g) ?? []).length, 1, output);
|
assert.equal((output.match(/<span style='visibility: hidden;'>terminal<\/span>/g) || []).length, 1, output);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cells with dim styling', async () => {
|
it('cells with dim styling', async () => {
|
||||||
await writeP(terminal, ' ' + sgr('2') + 'terminal' + sgr('22') + ' ');
|
await writeP(terminal, ' ' + sgr('2') + 'terminal' + sgr('22') + ' ');
|
||||||
|
|
||||||
const output = serializeAddon.serializeAsHTML();
|
const output = serializeAddon.serializeAsHTML();
|
||||||
assert.equal((output.match(/<span style='opacity: 0.5;'>terminal<\/span>/g) ?? []).length, 1, output);
|
assert.equal((output.match(/<span style='opacity: 0.5;'>terminal<\/span>/g) || []).length, 1, output);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cells with strikethrough styling', async () => {
|
it('cells with strikethrough styling', async () => {
|
||||||
await writeP(terminal, ' ' + sgr('9') + 'terminal' + sgr('29') + ' ');
|
await writeP(terminal, ' ' + sgr('9') + 'terminal' + sgr('29') + ' ');
|
||||||
|
|
||||||
const output = serializeAddon.serializeAsHTML();
|
const output = serializeAddon.serializeAsHTML();
|
||||||
assert.equal((output.match(/<span style='text-decoration: line-through;'>terminal<\/span>/g) ?? []).length, 1, output);
|
assert.equal((output.match(/<span style='text-decoration: line-through;'>terminal<\/span>/g) || []).length, 1, output);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cells with combined styling', async () => {
|
it('cells with combined styling', async () => {
|
||||||
await writeP(terminal, sgr('1') + ' ' + sgr('9') + 'termi' + sgr('22') + 'nal' + sgr('29') + ' ');
|
await writeP(terminal, sgr('1') + ' ' + sgr('9') + 'termi' + sgr('22') + 'nal' + sgr('29') + ' ');
|
||||||
|
|
||||||
const output = serializeAddon.serializeAsHTML();
|
const output = serializeAddon.serializeAsHTML();
|
||||||
assert.equal((output.match(/<span style='font-weight: bold;'> <\/span>/g) ?? []).length, 1, output);
|
assert.equal((output.match(/<span style='font-weight: bold;'> <\/span>/g) || []).length, 1, output);
|
||||||
assert.equal((output.match(/<span style='font-weight: bold; text-decoration: line-through;'>termi<\/span>/g) ?? []).length, 1, output);
|
assert.equal((output.match(/<span style='font-weight: bold; text-decoration: line-through;'>termi<\/span>/g) || []).length, 1, output);
|
||||||
assert.equal((output.match(/<span style='text-decoration: line-through;'>nal<\/span>/g) ?? []).length, 1, output);
|
assert.equal((output.match(/<span style='text-decoration: line-through;'>nal<\/span>/g) || []).length, 1, output);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cells with color styling', async () => {
|
it('cells with color styling', async () => {
|
||||||
await writeP(terminal, ' ' + sgr('38;5;46') + 'terminal' + sgr('39') + ' ');
|
await writeP(terminal, ' ' + sgr('38;5;46') + 'terminal' + sgr('39') + ' ');
|
||||||
|
|
||||||
const output = serializeAddon.serializeAsHTML();
|
const output = serializeAddon.serializeAsHTML();
|
||||||
assert.equal((output.match(/<span style='color: #00ff00;'>terminal<\/span>/g) ?? []).length, 1, output);
|
assert.equal((output.match(/<span style='color: #00ff00;'>terminal<\/span>/g) || []).length, 1, output);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cells with background styling', async () => {
|
it('cells with background styling', async () => {
|
||||||
await writeP(terminal, ' ' + sgr('48;5;46') + 'terminal' + sgr('49') + ' ');
|
await writeP(terminal, ' ' + sgr('48;5;46') + 'terminal' + sgr('49') + ' ');
|
||||||
|
|
||||||
const output = serializeAddon.serializeAsHTML();
|
const output = serializeAddon.serializeAsHTML();
|
||||||
assert.equal((output.match(/<span style='background-color: #00ff00;'>terminal<\/span>/g) ?? []).length, 1, output);
|
assert.equal((output.match(/<span style='background-color: #00ff00;'>terminal<\/span>/g) || []).length, 1, output);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('empty terminal with default options', async () => {
|
it('empty terminal with default options', async () => {
|
||||||
const output = serializeAddon.serializeAsHTML();
|
const output = serializeAddon.serializeAsHTML();
|
||||||
assert.equal((output.match(/color: #000000; background-color: #ffffff; font-family: monospace; font-size: 15px;/g) ?? []).length, 1, output);
|
assert.equal((output.match(/color: #000000; background-color: #ffffff; font-family: monospace; font-size: 15px;/g) || []).length, 1, output);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('empty terminal with custom options', async () => {
|
it('empty terminal with custom options', async () => {
|
||||||
@@ -405,14 +405,14 @@ describe('SerializeAddon', () => {
|
|||||||
const output = serializeAddon.serializeAsHTML({
|
const output = serializeAddon.serializeAsHTML({
|
||||||
includeGlobalBackground: true
|
includeGlobalBackground: true
|
||||||
});
|
});
|
||||||
assert.equal((output.match(/color: #ff00ff; background-color: #00ff00; font-family: verdana; font-size: 20px;/g) ?? []).length, 1, output);
|
assert.equal((output.match(/color: #ff00ff; background-color: #00ff00; font-family: verdana; font-size: 20px;/g) || []).length, 1, output);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('empty terminal with background included', async () => {
|
it('empty terminal with background included', async () => {
|
||||||
const output = serializeAddon.serializeAsHTML({
|
const output = serializeAddon.serializeAsHTML({
|
||||||
includeGlobalBackground: true
|
includeGlobalBackground: true
|
||||||
});
|
});
|
||||||
assert.equal((output.match(/color: #ffffff; background-color: #000000; font-family: monospace; font-size: 15px;/g) ?? []).length, 1, output);
|
assert.equal((output.match(/color: #ffffff; background-color: #000000; font-family: monospace; font-size: 15px;/g) || []).length, 1, output);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cells with custom color styling', async () => {
|
it('cells with custom color styling', async () => {
|
||||||
@@ -422,7 +422,7 @@ describe('SerializeAddon', () => {
|
|||||||
await writeP(terminal, ' ' + sgr('38;5;0') + 'terminal' + sgr('39') + ' ');
|
await writeP(terminal, ' ' + sgr('38;5;0') + 'terminal' + sgr('39') + ' ');
|
||||||
|
|
||||||
const output = serializeAddon.serializeAsHTML();
|
const output = serializeAddon.serializeAsHTML();
|
||||||
assert.equal((output.match(/<span style='color: #ffa500;'>terminal<\/span>/g) ?? []).length, 1, output);
|
assert.equal((output.match(/<span style='color: #ffa500;'>terminal<\/span>/g) || []).length, 1, output);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cells with color styling - xterm headless', async () => {
|
it('cells with color styling - xterm headless', async () => {
|
||||||
@@ -432,7 +432,7 @@ describe('SerializeAddon', () => {
|
|||||||
await writeP(terminal, ' ' + sgr('38;5;46') + 'terminal' + sgr('39') + ' ');
|
await writeP(terminal, ' ' + sgr('38;5;46') + 'terminal' + sgr('39') + ' ');
|
||||||
|
|
||||||
const output = serializeAddon.serializeAsHTML();
|
const output = serializeAddon.serializeAsHTML();
|
||||||
assert.equal((output.match(/<span style='color: #00ff00;'>terminal<\/span>/g) ?? []).length, 1, output);
|
assert.equal((output.match(/<span style='color: #00ff00;'>terminal<\/span>/g) || []).length, 1, output);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -89,19 +89,11 @@ function equalUnderline(cell1: IBufferCell | IAttributeData, cell2: IBufferCell)
|
|||||||
if (!cell1.isUnderline() && !cell2.isUnderline()) {
|
if (!cell1.isUnderline() && !cell2.isUnderline()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (cell1.getUnderlineStyle() !== cell2.getUnderlineStyle()) {
|
const cell1Data = cell1 as unknown as IAttributeData;
|
||||||
return false;
|
const cell2Data = cell2 as unknown as IAttributeData;
|
||||||
}
|
return cell1Data.getUnderlineStyle() === cell2Data.getUnderlineStyle()
|
||||||
const cell1Default = cell1.isUnderlineColorDefault();
|
&& cell1Data.getUnderlineColor() === cell2Data.getUnderlineColor()
|
||||||
const cell2Default = cell2.isUnderlineColorDefault();
|
&& cell1Data.getUnderlineColorMode() === cell2Data.getUnderlineColorMode();
|
||||||
if (cell1Default && cell2Default) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (cell1Default !== cell2Default) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return cell1.getUnderlineColor() === cell2.getUnderlineColor()
|
|
||||||
&& cell1.getUnderlineColorMode() === cell2.getUnderlineColorMode();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function equalFlags(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
|
function equalFlags(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
|
||||||
@@ -117,16 +109,6 @@ function equalFlags(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): bo
|
|||||||
&& cell1.isStrikethrough() === cell2.isStrikethrough();
|
&& cell1.isStrikethrough() === cell2.isStrikethrough();
|
||||||
}
|
}
|
||||||
|
|
||||||
function attributesEquals(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
|
|
||||||
const cell1AsBufferCell = cell1 as IBufferCell;
|
|
||||||
if (typeof cell1AsBufferCell.attributesEquals === 'function') {
|
|
||||||
return cell1AsBufferCell.attributesEquals(cell2);
|
|
||||||
}
|
|
||||||
return equalFg(cell1, cell2)
|
|
||||||
&& equalBg(cell1, cell2)
|
|
||||||
&& equalFlags(cell1, cell2);
|
|
||||||
}
|
|
||||||
|
|
||||||
class StringSerializeHandler extends BaseSerializeHandler {
|
class StringSerializeHandler extends BaseSerializeHandler {
|
||||||
private _rowIndex: number = 0;
|
private _rowIndex: number = 0;
|
||||||
private _allRows: string[] = new Array<string>();
|
private _allRows: string[] = new Array<string>();
|
||||||
@@ -276,9 +258,6 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
|||||||
|
|
||||||
private _diffStyle(cell: IBufferCell | IAttributeData, oldCell: IBufferCell): number[] {
|
private _diffStyle(cell: IBufferCell | IAttributeData, oldCell: IBufferCell): number[] {
|
||||||
const sgrSeq: number[] = [];
|
const sgrSeq: number[] = [];
|
||||||
if (attributesEquals(cell, oldCell)) {
|
|
||||||
return sgrSeq;
|
|
||||||
}
|
|
||||||
const fgChanged = !equalFg(cell, oldCell);
|
const fgChanged = !equalFg(cell, oldCell);
|
||||||
const bgChanged = !equalBg(cell, oldCell);
|
const bgChanged = !equalBg(cell, oldCell);
|
||||||
const flagsChanged = !equalFlags(cell, oldCell);
|
const flagsChanged = !equalFlags(cell, oldCell);
|
||||||
@@ -311,18 +290,17 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
|||||||
if (cell.isInverse() !== oldCell.isInverse()) { sgrSeq.push(cell.isInverse() ? 7 : 27); }
|
if (cell.isInverse() !== oldCell.isInverse()) { sgrSeq.push(cell.isInverse() ? 7 : 27); }
|
||||||
if (cell.isBold() !== oldCell.isBold()) { sgrSeq.push(cell.isBold() ? 1 : 22); }
|
if (cell.isBold() !== oldCell.isBold()) { sgrSeq.push(cell.isBold() ? 1 : 22); }
|
||||||
if (!equalUnderline(cell, oldCell)) {
|
if (!equalUnderline(cell, oldCell)) {
|
||||||
const style = cell.getUnderlineStyle();
|
const cellData = cell as unknown as IAttributeData;
|
||||||
|
const style = cellData.getUnderlineStyle();
|
||||||
if (style === UnderlineStyle.NONE) {
|
if (style === UnderlineStyle.NONE) {
|
||||||
sgrSeq.push(24);
|
sgrSeq.push(24);
|
||||||
} else if (style === UnderlineStyle.SINGLE && cell.isUnderlineColorDefault()) {
|
|
||||||
sgrSeq.push(4);
|
|
||||||
} else {
|
} else {
|
||||||
// Use SGR 4:X format for underline styles
|
// Use SGR 4:X format for underline styles
|
||||||
sgrSeq.push('4:' + style as unknown as number);
|
sgrSeq.push('4:' + style as unknown as number);
|
||||||
// Handle underline color
|
// Handle underline color
|
||||||
if (!cell.isUnderlineColorDefault()) {
|
if (!cellData.isUnderlineColorDefault()) {
|
||||||
const color = cell.getUnderlineColor();
|
const color = cellData.getUnderlineColor();
|
||||||
if (cell.isUnderlineColorRGB()) {
|
if (cellData.isUnderlineColorRGB()) {
|
||||||
sgrSeq.push('58:2::' + ((color >>> 16) & 0xFF) + ':' + ((color >>> 8) & 0xFF) + ':' + (color & 0xFF) as unknown as number);
|
sgrSeq.push('58:2::' + ((color >>> 16) & 0xFF) + ':' + ((color >>> 8) & 0xFF) + ':' + (color & 0xFF) as unknown as number);
|
||||||
} else {
|
} else {
|
||||||
sgrSeq.push('58:5:' + color as unknown as number);
|
sgrSeq.push('58:5:' + color as unknown as number);
|
||||||
@@ -479,7 +457,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SerializeAddon implements ITerminalAddon, ISerializeApi {
|
export class SerializeAddon implements ITerminalAddon , ISerializeApi {
|
||||||
private _terminal: Terminal | undefined;
|
private _terminal: Terminal | undefined;
|
||||||
|
|
||||||
public activate(terminal: Terminal): void {
|
public activate(terminal: Terminal): void {
|
||||||
@@ -623,7 +601,7 @@ export class SerializeAddon implements ITerminalAddon, ISerializeApi {
|
|||||||
throw new Error('Cannot use addon until it has been loaded');
|
throw new Error('Cannot use addon until it has been loaded');
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._serializeBufferAsHTML(this._terminal, options ?? {});
|
return this._serializeBufferAsHTML(this._terminal, options || {});
|
||||||
}
|
}
|
||||||
|
|
||||||
public dispose(): void { }
|
public dispose(): void { }
|
||||||
@@ -652,6 +630,20 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _padStart(target: string, targetLength: number, padString: string): string {
|
||||||
|
targetLength = targetLength >> 0;
|
||||||
|
padString = padString ?? ' ';
|
||||||
|
if (target.length > targetLength) {
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
targetLength -= target.length;
|
||||||
|
if (targetLength > padString.length) {
|
||||||
|
padString += padString.repeat(targetLength / padString.length);
|
||||||
|
}
|
||||||
|
return padString.slice(0, targetLength) + target;
|
||||||
|
}
|
||||||
|
|
||||||
protected _beforeSerialize(rows: number, start: number, end: number): void {
|
protected _beforeSerialize(rows: number, start: number, end: number): void {
|
||||||
this._htmlContent += '<html><body><!--StartFragment--><pre>';
|
this._htmlContent += '<html><body><!--StartFragment--><pre>';
|
||||||
|
|
||||||
@@ -688,7 +680,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
|
|||||||
(color >> 8) & 255,
|
(color >> 8) & 255,
|
||||||
(color ) & 255
|
(color ) & 255
|
||||||
];
|
];
|
||||||
return '#' + rgb.map(x => x.toString(16).padStart(2, '0')).join('');
|
return '#' + rgb.map(x => this._padStart(x.toString(16), 2, '0')).join('');
|
||||||
}
|
}
|
||||||
if (isFg ? cell.isFgPalette() : cell.isBgPalette()) {
|
if (isFg ? cell.isFgPalette() : cell.isBgPalette()) {
|
||||||
return this._ansiColors[color].css;
|
return this._ansiColors[color].css;
|
||||||
@@ -697,24 +689,26 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _getUnderlineColor(cell: IBufferCell): string | undefined {
|
private _getUnderlineColor(cell: IBufferCell): string | undefined {
|
||||||
if (cell.isUnderlineColorDefault()) {
|
const cellData = cell as unknown as IAttributeData;
|
||||||
|
if (cellData.isUnderlineColorDefault()) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const color = cell.getUnderlineColor();
|
const color = cellData.getUnderlineColor();
|
||||||
if (cell.isUnderlineColorRGB()) {
|
if (cellData.isUnderlineColorRGB()) {
|
||||||
const rgb = [
|
const rgb = [
|
||||||
(color >> 16) & 255,
|
(color >> 16) & 255,
|
||||||
(color >> 8) & 255,
|
(color >> 8) & 255,
|
||||||
(color ) & 255
|
(color ) & 255
|
||||||
];
|
];
|
||||||
return '#' + rgb.map(x => x.toString(16).padStart(2, '0')).join('');
|
return '#' + rgb.map(x => this._padStart(x.toString(16), 2, '0')).join('');
|
||||||
}
|
}
|
||||||
// Palette color
|
// Palette color
|
||||||
return this._ansiColors[color].css;
|
return this._ansiColors[color].css;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getUnderlineStyle(cell: IBufferCell): string {
|
private _getUnderlineStyle(cell: IBufferCell): string {
|
||||||
switch (cell.getUnderlineStyle()) {
|
const cellData = cell as unknown as IAttributeData;
|
||||||
|
switch (cellData.getUnderlineStyle()) {
|
||||||
case UnderlineStyle.SINGLE:
|
case UnderlineStyle.SINGLE:
|
||||||
return 'underline';
|
return 'underline';
|
||||||
case UnderlineStyle.DOUBLE:
|
case UnderlineStyle.DOUBLE:
|
||||||
@@ -733,10 +727,6 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
|
|||||||
private _diffStyle(cell: IBufferCell, oldCell: IBufferCell): string[] | undefined {
|
private _diffStyle(cell: IBufferCell, oldCell: IBufferCell): string[] | undefined {
|
||||||
const content: string[] = [];
|
const content: string[] = [];
|
||||||
|
|
||||||
if (attributesEquals(cell, oldCell)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fgChanged = !equalFg(cell, oldCell);
|
const fgChanged = !equalFg(cell, oldCell);
|
||||||
const bgChanged = !equalBg(cell, oldCell);
|
const bgChanged = !equalBg(cell, oldCell);
|
||||||
const flagsChanged = !equalFlags(cell, oldCell);
|
const flagsChanged = !equalFlags(cell, oldCell);
|
||||||
|
|||||||
@@ -203,46 +203,6 @@ test.describe('SerializeAddon', () => {
|
|||||||
strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), lines.join('\r\n'));
|
strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), lines.join('\r\n'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('buffer cell attributesEquals compares underline style and color', async () => {
|
|
||||||
await ctx.proxy.write(`${sgr(UNDERLINE_DOUBLE, UNDERLINE_COLOR_RED)}A${sgr(UNDERLINE_DOUBLE, UNDERLINE_COLOR_RED)}B${sgr(NORMAL)}`);
|
|
||||||
const sameAttributes = await ctx.page.evaluate(`(() => {
|
|
||||||
const line = window.term.buffer.active.getLine(0);
|
|
||||||
const cellA = line?.getCell(0);
|
|
||||||
const cellB = line?.getCell(1);
|
|
||||||
if (!cellA || !cellB) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return cellA.attributesEquals(cellB);
|
|
||||||
})()`);
|
|
||||||
strictEqual(sameAttributes, true);
|
|
||||||
|
|
||||||
await ctx.page.evaluate(`window.term.reset()`);
|
|
||||||
await ctx.proxy.write(`${sgr(UNDERLINE_DOUBLE, UNDERLINE_COLOR_RED)}A${sgr(UNDERLINE_DOUBLE, UNDERLINE_COLOR_GREEN)}B${sgr(NORMAL)}`);
|
|
||||||
const differentColor = await ctx.page.evaluate(`(() => {
|
|
||||||
const line = window.term.buffer.active.getLine(0);
|
|
||||||
const cellA = line?.getCell(0);
|
|
||||||
const cellB = line?.getCell(1);
|
|
||||||
if (!cellA || !cellB) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return cellA.attributesEquals(cellB);
|
|
||||||
})()`);
|
|
||||||
strictEqual(differentColor, false);
|
|
||||||
|
|
||||||
await ctx.page.evaluate(`window.term.reset()`);
|
|
||||||
await ctx.proxy.write(`${sgr(UNDERLINE_DOUBLE, UNDERLINE_COLOR_RED)}A${sgr(UNDERLINED, UNDERLINE_COLOR_RED)}B${sgr(NORMAL)}`);
|
|
||||||
const differentStyle = await ctx.page.evaluate(`(() => {
|
|
||||||
const line = window.term.buffer.active.getLine(0);
|
|
||||||
const cellA = line?.getCell(0);
|
|
||||||
const cellB = line?.getCell(1);
|
|
||||||
if (!cellA || !cellB) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return cellA.attributesEquals(cellB);
|
|
||||||
})()`);
|
|
||||||
strictEqual(differentStyle, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('serialize all rows of content with color256', async function(): Promise<any> {
|
test('serialize all rows of content with color256', async function(): Promise<any> {
|
||||||
const rows = 32;
|
const rows = 32;
|
||||||
const cols = 10;
|
const cols = 10;
|
||||||
@@ -642,9 +602,6 @@ const BOLD = '1';
|
|||||||
const DIM = '2';
|
const DIM = '2';
|
||||||
const ITALIC = '3';
|
const ITALIC = '3';
|
||||||
const UNDERLINED = '4';
|
const UNDERLINED = '4';
|
||||||
const UNDERLINE_DOUBLE = '4:2';
|
|
||||||
const UNDERLINE_COLOR_RED = '58;5;196';
|
|
||||||
const UNDERLINE_COLOR_GREEN = '58;5;46';
|
|
||||||
const BLINK = '5';
|
const BLINK = '5';
|
||||||
const INVERSE = '7';
|
const INVERSE = '7';
|
||||||
const INVISIBLE = '8';
|
const INVISIBLE = '8';
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ const config: PlaywrightTestConfig = {
|
|||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
name: 'Chromium',
|
name: 'ChromeStable',
|
||||||
use: {
|
use: {
|
||||||
browserName: 'chromium'
|
browserName: 'chromium',
|
||||||
|
channel: 'chrome'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,15 +9,19 @@ import type { Terminal, ITerminalAddon, IUnicodeHandling } from '@xterm/xterm';
|
|||||||
import type { UnicodeGraphemesAddon as IUnicodeGraphemesApi } from '@xterm/addon-unicode-graphemes';
|
import type { UnicodeGraphemesAddon as IUnicodeGraphemesApi } from '@xterm/addon-unicode-graphemes';
|
||||||
import { UnicodeGraphemeProvider } from './UnicodeGraphemeProvider';
|
import { UnicodeGraphemeProvider } from './UnicodeGraphemeProvider';
|
||||||
|
|
||||||
export class UnicodeGraphemesAddon implements ITerminalAddon, IUnicodeGraphemesApi {
|
export class UnicodeGraphemesAddon implements ITerminalAddon , IUnicodeGraphemesApi {
|
||||||
private _provider15Graphemes?: UnicodeGraphemeProvider;
|
private _provider15Graphemes?: UnicodeGraphemeProvider;
|
||||||
private _provider15?: UnicodeGraphemeProvider;
|
private _provider15?: UnicodeGraphemeProvider;
|
||||||
private _unicode?: IUnicodeHandling;
|
private _unicode?: IUnicodeHandling;
|
||||||
private _oldVersion: string = '';
|
private _oldVersion: string = '';
|
||||||
|
|
||||||
public activate(terminal: Terminal): void {
|
public activate(terminal: Terminal): void {
|
||||||
this._provider15 ??= new UnicodeGraphemeProvider(false);
|
if (! this._provider15) {
|
||||||
this._provider15Graphemes ??= new UnicodeGraphemeProvider(true);
|
this._provider15 = new UnicodeGraphemeProvider(false);
|
||||||
|
}
|
||||||
|
if (! this._provider15Graphemes) {
|
||||||
|
this._provider15Graphemes = new UnicodeGraphemeProvider(true);
|
||||||
|
}
|
||||||
const unicode = terminal.unicode;
|
const unicode = terminal.unicode;
|
||||||
this._unicode = unicode;
|
this._unicode = unicode;
|
||||||
unicode.register(this._provider15);
|
unicode.register(this._provider15);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import UnicodeTrie from './unicode-trie';
|
import UnicodeTrie from './unicode-trie';
|
||||||
const trieRaw = "AAARAAAAAABwxwAAAb4LQfTtmw+sVmUdx58LL/ffe/kjzNBV80gW1F3yR+6CvbJiypoZa0paWmAWSluErSBbFtYkkuZykq6QamGJ4WRqo2kFGy6dYWtEq6G1MFAJbRbOVTQr+x7f5+x97q/n/3me87wXzm/3s+f/7/d7/p7znnvOlvGMbQM7wIPgEbAPHABPgcPgefAS+BfYwuv/F/Q2OulBxKcK6TMRPxu8FcwFbwcjYCFYDC4Cl4ArwNXgGvBJsA58UdBDwy+jbBO4La8DtoEd4H7wkNBuN+KPgn3gADgIngaHwFHwF/AyeAWMm4C+TGi3LdiJ/EnIex04A2RgFpgD5oKFYDG4CLwHXAo+IKSvAqt4/evA9bz9jWA6+Cq3dyvCP8HWNwX93wF38/ROcD94SCjP2+1B+BiPP4HwgOD/7xD/I08fRniMx48jPAFeBeuF+n29jE0G08FZvaPHYWZvh9mcEfAOjlhXx/qGfd2QvLO3zccmtMnzliC9lPt+GenD1nyMiK/LNf1cycs+gfAzPJ6vtxe4jhuQtx5sBLeA28G3eb3v8/Beif4HkPewxu5G6N/rMP4qfgEdvwZPgj+AZ8Cx3nYfxiE8Dk6AV0FfH/YEOB28AbwJDIPzQAtcAC4Gl/Z19F+J+NVCehWPr0b46b7RvixvdPg8yr7U10l/BfFN4La8DdgGdoAHwU/AI2AfOACeAofB8+AlcAKwfvyBKeCM/o7NrF9PXmdWv9/Ynot2I7ztIg8dF5I2a8i63CjZU+9Fm2Wcy4U4ZQVYyeOrwVoev57UuxHcJKRvFuJXgnU8/nUebtbYrKmpCUOx31P7UVNTU1NTU1NTU1OGLTz8Xr/77+W7+9vP0or0MxPMbXaizY8FW3sQ3wseB/t5/kGEh8DR/vbzwL8i/Af4Dy8fP8BYE0weaKenI/wV/DhrQG97JspngzlgLpgHzgPzwUhdVpfVZXVZXRa87HxwAVgQ4Pn5WEd85l5TUzOasvezFw/E3b/LoP9D4CpwrcTWWsGXNQOj748/G9k3G56d1KYxmbELwQbwKFiJvBM8nDWlHa5E+AOwCzwLzjkNeeB28NvTeB1OYyr0gQ1g99R23nGE50xj7MPgc+A+8K5Bxj4FHgB/G2z/T9XEzCZjd/S0WYX4Pc3/r/Nn5I0f6qQXIP5x8ENwBMyYyNhHJ3b0pOCuLrBvM941NTU1JyNHEp+BrC8dMyalt1/m3uWfhmeULzRGp9d3wf0WZSN8+prCr60Wz09tuNmx35sl9Y825HXvRN39KNveaL8flb9f913kbec67kHeTsR3gYcH2uV7ED4m2HhCYi/X9ZuBzvuXv0f8iKIfx5B/XCg7gTgbVPdvAsomCuWnD45eK28UyvL3Jt+s0fU2TVnOXJQvJHUWIb0ELAWXgCt4+UcMumSsEtpch/g6ouMGpG/ieZsc9N/q4YsLd3D9WyPbsWEbfNgO7hN82TWY/n8xKbmsC3xQsYKf+7sjrx2TH+u4H3vhx+OO6+X9hmtXN7C/4r15EPaeBs9J7L7YBeeED/k7wn8fbIf/Rji+yVizmd4vW6bB19cb/PU9w7MxMA60bzPHgM8+zG623+OnzOf55yNc3Gw/k303wveBy3nZcoTXgNVgLfiCRNcG5N3SbIebwZ08fhe4l8d/BH7K4yI/4+HPwS/BAfBks+PzIaHuc3x+ivSL4GUyZ68I6fwZYRNMG2qnz+Th2QjfMtTx/1zE5w61nyN+Q7C3aKgdin1dgrylYBn4INdhGn/Z2FfFiqH01/SUXMvnPD+jC+j85N/RqRhR/DYaS6T+P09K1mD+vzW+5zVqqeVUl0wTz2lK8odJHRGXfBufdGLSoSo3+ZFJ6sl0qvJVNmhI4z4i06mrZ6uT1le1z5h5HE3tMiHPtQ5javu+ItMXUr/MXpmwmyRL3D6U7UwIMyYfczGu0qdqb2pbhcw4xQkhWQBMerrZ/liXrGTbsQwTwrEu4zSczKLrd7fCSKiKn+zSo8BWXMe8myXWOivrUxWi60OPoQ7VIasbQ0S/Ukk3rZVullNhHEL1rYoxUF0PTfm6elWJzq54ZsU4z11ohOy0oxT2izFqCNj4TesXcWZo6+Jfqr1O+1O1beqDagypj2J9F1u2daucj3Eknmq/6PaHrK7Mb1o35DiW1a/a76LuhlDXZX25SOz11S33ErKxDb2/fc/bFKI6axskn+4/W90u9mOtbRf7smsoTdvOfwoRz0t6DaP9k81v6P7Re5aUQudTd303rX+bZzBl97/KR7E+Xbux9lLI+aNr1PfaYLpPDiW2/vrYTX1drMIeXbMye6HXlw8292Jl7ZXxLxRlxXbcaH9drjFlxfa3Qozx8NWRi834lPVZbD+SmN7EJPzc9TVCSVXXDps9L+513b2J7fMu176V2YOhx1A3JrJ8KrLxUumpcu5j/lYT+2tzLRVDZmhjO442a1Clu0ox9VPVXzE/lcS4V0k1D6LI1pJsz8fct9SGbO5l/rmKzTlvsxdj3IvRtC2uv0t1fotltvd2VaCy5Sp5m0EhnZG4CCNxXZrWp/VUIrOjapfnNw11ZNI0V/GWzKNuxtzGKKTEtJeR0NVmpojbtBuW5On0u0is9ZMxvU8ZM+8vEyadtu10oqtP9Q4rcJEm85+Two/QkpGwjI6YkgkhtUfzZOW6fFVexuRri+qj9TJJHZkdmW5abiu0rs6uj2TMfmx06bISUj9tZ9Lja8dVQtox6WpxTJKfW3M4MSTmvU4sWy1CU6BF4jIfdNeDjHWuO1lCWIm2Jr2ixNZvklD2fP0Q6+vsmO4hqN1hJvfDtV5G8mTlsvau4qPP1a64L1skT6QYEzEtq0PzGZOfCbSdSmcKTP7Qs86Ej/1hEpelaV6IMdT5ayu2+nT9tmnnO746XbLxE8t0qOrYtJWhmk9bvaLfsrotRVw1PnR+bcafSUKZ6Mps7smobybJLH2R6WqRkJa1DHV0UmbfUcksiSF0HExSpp+uY0zbTklMaCm7blzEtg8h1rNMXNaYi05ZXsbC75sQ/4+aUxFV2jL50Q3jE0rK2rVtN09By8OHoo1vH2LPSdE323mr2sdu0pUZiDkWLRKWnfeQY6taKzHF9n/GPv8jd/0/egiRvYMR24fU79iY3s9Qva9RlYR8n8HHtq9fMcT1HRWfdZXiHd9YInt/iI4PTaf+BimXKvdXYU+3hlRpHzs2dVK/cxhDn+xs0I2jzxjL5kpXz1VU72aLtkK/97sALKyQqu25SshvG6h08/cLrlKswRklKXvvXfa+pZt+y8nah5YUv2Oo/ap/X2URdRfico9K69hcp6r6XaCz5Wo/hs/iNTGF6N6tV92/9ZS0Wba9SlT3pKF/e6W674+x9ly+VRL73cPU8ygb31D3eSqfVd+iqET0y3YMYojoO11XqrTt2nPxmeq1HYeqxkmUMt8DiesjpoTSr+qDrD+qPZDiOZxMdH0pRPX8MFUfQtv0Xbs+a1a1NnRryNZ/2+tsaPG5ZoX0RXZei88yZGdo4UMPj/cwv/kMJboxLISuQbE+1VW12Mx7FWOrW3M9Hv7Y+uxyraPSo8B2TGPuLdOeZha+hBKf8Sjsm/oR+7pmsx/oeOraFWdXleeV6oyl41zm+mgSuq9C6ox1TsU8D+m4dwMmf8v2nz7Tm+fYfj7HV1K/x1HWjquvY+2dllxM64ue87Su772zzbXIVC+WxLZTRR9MdkMTypZNH1z6G0tUvoccwxA+hfLNdV+a7MaQqscztMi+7QnxDZXvd1dldWQOyMbApb1Jd2h91Ffx+y9Xfb7tClokboOvrRhrbVpFFO8z+65t2/u4su9MUx028znH01/TGVDmHAj13W1o+1USw+eUfYtpO+b82rRNsb6oPpV+1fdBqddB6n3WDXvdJDZrJ0QfQp6bsc/kqq4BIddHWXGdN1pmWveh58F1zYUW1zmOITHXWOg1XrZvZSWUf77tq1ofqear6muaT1lIQp3bofabSafJVlnfYo9B6LGr8uzz2Xchvzfw+T9PlgiV/A8=";
|
const trieRaw = "AAARAAAAAABwxwAAAb4LQfTtmw+sVmUdx58LL/ffe/kjzNBV80gW1F3yR+6CvbJiypoZa0paWmAWSluErSBbFtYkkuZykq6QamGJ4WRqo2kFGy6dYWtEq6G1MFAJbRbOVTQr+x7f5+x97q/n/3me87wXzm/3s+f/7/d7/p7znnvOlvGMbQM7wIPgEbAPHABPgcPgefAS+BfYwuv/F/Q2OulBxKcK6TMRPxu8FcwFbwcjYCFYDC4Cl4ArwNXgGvBJsA58UdBDwy+jbBO4La8DtoEd4H7wkNBuN+KPgn3gADgIngaHwFHwF/AyeAWMm4C+TGi3LdiJ/EnIex04A2RgFpgD5oKFYDG4CLwHXAo+IKSvAqt4/evA9bz9jWA6+Cq3dyvCP8HWNwX93wF38/ROcD94SCjP2+1B+BiPP4HwgOD/7xD/I08fRniMx48jPAFeBeuF+n29jE0G08FZvaPHYWZvh9mcEfAOjlhXx/qGfd2QvLO3zccmtMnzliC9lPt+GenD1nyMiK/LNf1cycs+gfAzPJ6vtxe4jhuQtx5sBLeA28G3eb3v8/Beif4HkPewxu5G6N/rMP4qfgEdvwZPgj+AZ8Cx3nYfxiE8Dk6AV0FfH/YEOB28AbwJDIPzQAtcAC4Gl/Z19F+J+NVCehWPr0b46b7RvixvdPg8yr7U10l/BfFN4La8DdgGdoAHwU/AI2AfOACeAofB8+AlcAKwfvyBKeCM/o7NrF9PXmdWv9/Ynot2I7ztIg8dF5I2a8i63CjZU+9Fm2Wcy4U4ZQVYyeOrwVoev57UuxHcJKRvFuJXgnU8/nUebtbYrKmpCUOx31P7UVNTU1NTU1NTU1OGLTz8Xr/77+W7+9vP0or0MxPMbXaizY8FW3sQ3wseB/t5/kGEh8DR/vbzwL8i/Af4Dy8fP8BYE0weaKenI/wV/DhrQG97JspngzlgLpgHzgPzwUhdVpfVZXVZXRa87HxwAVgQ4Pn5WEd85l5TUzOasvezFw/E3b/LoP9D4CpwrcTWWsGXNQOj748/G9k3G56d1KYxmbELwQbwKFiJvBM8nDWlHa5E+AOwCzwLzjkNeeB28NvTeB1OYyr0gQ1g99R23nGE50xj7MPgc+A+8K5Bxj4FHgB/G2z/T9XEzCZjd/S0WYX4Pc3/r/Nn5I0f6qQXIP5x8ENwBMyYyNhHJ3b0pOCuLrBvM941NTU1JyNHEp+BrC8dMyalt1/m3uWfhmeULzRGp9d3wf0WZSN8+prCr60Wz09tuNmx35sl9Y825HXvRN39KNveaL8flb9f913kbec67kHeTsR3gYcH2uV7ED4m2HhCYi/X9ZuBzvuXv0f8iKIfx5B/XCg7gTgbVPdvAsomCuWnD45eK28UyvL3Jt+s0fU2TVnOXJQvJHUWIb0ELAWXgCt4+UcMumSsEtpch/g6ouMGpG/ieZsc9N/q4YsLd3D9WyPbsWEbfNgO7hN82TWY/n8xKbmsC3xQsYKf+7sjrx2TH+u4H3vhx+OO6+X9hmtXN7C/4r15EPaeBs9J7L7YBeeED/k7wn8fbIf/Rji+yVizmd4vW6bB19cb/PU9w7MxMA60bzPHgM8+zG623+OnzOf55yNc3Gw/k303wveBy3nZcoTXgNVgLfiCRNcG5N3SbIebwZ08fhe4l8d/BH7K4yI/4+HPwS/BAfBks+PzIaHuc3x+ivSL4GUyZ68I6fwZYRNMG2qnz+Th2QjfMtTx/1zE5w61nyN+Q7C3aKgdin1dgrylYBn4INdhGn/Z2FfFiqH01/SUXMvnPD+jC+j85N/RqRhR/DYaS6T+P09K1mD+vzW+5zVqqeVUl0wTz2lK8odJHRGXfBufdGLSoSo3+ZFJ6sl0qvJVNmhI4z4i06mrZ6uT1le1z5h5HE3tMiHPtQ5javu+ItMXUr/MXpmwmyRL3D6U7UwIMyYfczGu0qdqb2pbhcw4xQkhWQBMerrZ/liXrGTbsQwTwrEu4zSczKLrd7fCSKiKn+zSo8BWXMe8myXWOivrUxWi60OPoQ7VIasbQ0S/Ukk3rZVullNhHEL1rYoxUF0PTfm6elWJzq54ZsU4z11ohOy0oxT2izFqCNj4TesXcWZo6+Jfqr1O+1O1beqDagypj2J9F1u2daucj3Eknmq/6PaHrK7Mb1o35DiW1a/a76LuhlDXZX25SOz11S33ErKxDb2/fc/bFKI6axskn+4/W90u9mOtbRf7smsoTdvOfwoRz0t6DaP9k81v6P7Re5aUQudTd303rX+bZzBl97/KR7E+Xbux9lLI+aNr1PfaYLpPDiW2/vrYTX1drMIeXbMye6HXlw8292Jl7ZXxLxRlxXbcaH9drjFlxfa3Qozx8NWRi834lPVZbD+SmN7EJPzc9TVCSVXXDps9L+513b2J7fMu176V2YOhx1A3JrJ8KrLxUumpcu5j/lYT+2tzLRVDZmhjO442a1Clu0ox9VPVXzE/lcS4V0k1D6LI1pJsz8fct9SGbO5l/rmKzTlvsxdj3IvRtC2uv0t1fotltvd2VaCy5Sp5m0EhnZG4CCNxXZrWp/VUIrOjapfnNw11ZNI0V/GWzKNuxtzGKKTEtJeR0NVmpojbtBuW5On0u0is9ZMxvU8ZM+8vEyadtu10oqtP9Q4rcJEm85+Two/QkpGwjI6YkgkhtUfzZOW6fFVexuRri+qj9TJJHZkdmW5abiu0rs6uj2TMfmx06bISUj9tZ9Lja8dVQtox6WpxTJKfW3M4MSTmvU4sWy1CU6BF4jIfdNeDjHWuO1lCWIm2Jr2ixNZvklD2fP0Q6+vsmO4hqN1hJvfDtV5G8mTlsvau4qPP1a64L1skT6QYEzEtq0PzGZOfCbSdSmcKTP7Qs86Ej/1hEpelaV6IMdT5ayu2+nT9tmnnO746XbLxE8t0qOrYtJWhmk9bvaLfsrotRVw1PnR+bcafSUKZ6Mps7smobybJLH2R6WqRkJa1DHV0UmbfUcksiSF0HExSpp+uY0zbTklMaCm7blzEtg8h1rNMXNaYi05ZXsbC75sQ/4+aUxFV2jL50Q3jE0rK2rVtN09By8OHoo1vH2LPSdE323mr2sdu0pUZiDkWLRKWnfeQY6taKzHF9n/GPv8jd/0/egiRvYMR24fU79iY3s9Qva9RlYR8n8HHtq9fMcT1HRWfdZXiHd9YInt/iI4PTaf+BimXKvdXYU+3hlRpHzs2dVK/cxhDn+xs0I2jzxjL5kpXz1VU72aLtkK/97sALKyQqu25SshvG6h08/cLrlKswRklKXvvXfa+pZt+y8nah5YUv2Oo/ap/X2URdRfico9K69hcp6r6XaCz5Wo/hs/iNTGF6N6tV92/9ZS0Wba9SlT3pKF/e6W674+x9ly+VRL73cPU8ygb31D3eSqfVd+iqET0y3YMYojoO11XqrTt2nPxmeq1HYeqxkmUMt8DiesjpoTSr+qDrD+qPZDiOZxMdH0pRPX8MFUfQtv0Xbs+a1a1NnRryNZ/2+tsaPG5ZoX0RXZei88yZGdo4UMPj/cwv/kMJboxLISuQbE+1VW12Mx7FWOrW3M9Hv7Y+uxyraPSo8B2TGPuLdOeZha+hBKf8Sjsm/oR+7pmsx/oeOraFWdXleeV6oyl41zm+mgSuq9C6ox1TsU8D+m4dwMmf8v2nz7Tm+fYfj7HV1K/x1HWjquvY+2dllxM64ue87Su772zzbXIVC+WxLZTRR9MdkMTypZNH1z6G0tUvoccwxA+hfLNdV+a7MaQqscztMi+7QnxDZXvd1dldWQOyMbApb1Jd2h91Ffx+y9Xfb7tClokboOvrRhrbVpFFO8z+65t2/u4su9MUx028znH01/TGVDmHAj13W1o+1USw+eUfYtpO+b82rRNsb6oPpV+1fdBqddB6n3WDXvdJDZrJ0QfQp6bsc/kqq4BIddHWXGdN1pmWveh58F1zYUW1zmOITHXWOg1XrZvZSWUf77tq1ofqear6muaT1lIQp3bofabSafJVlnfYo9B6LGr8uzz2Xchvzfw+T9PlgiV/A8=";
|
||||||
|
|
||||||
declare const Buffer: { from(s: string, encoding: string): Uint8Array } | undefined;
|
declare const Buffer: any;
|
||||||
function _dec(s: string): Uint8Array {
|
function _dec(s: string): Uint8Array {
|
||||||
if (typeof Buffer !== 'undefined') return Buffer.from(s, 'base64');
|
if (typeof Buffer !== 'undefined') return Buffer.from(s, 'base64');
|
||||||
const bs = atob(s);
|
const bs = atob(s);
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ const config: PlaywrightTestConfig = {
|
|||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
name: 'Chromium',
|
name: 'ChromeStable',
|
||||||
use: {
|
use: {
|
||||||
browserName: 'chromium'
|
browserName: 'chromium',
|
||||||
|
channel: 'chrome'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import type { Terminal, ITerminalAddon } from '@xterm/xterm';
|
|||||||
import type { Unicode11Addon as IUnicode11Api } from '@xterm/addon-unicode11';
|
import type { Unicode11Addon as IUnicode11Api } from '@xterm/addon-unicode11';
|
||||||
import { UnicodeV11 } from './UnicodeV11';
|
import { UnicodeV11 } from './UnicodeV11';
|
||||||
|
|
||||||
export class Unicode11Addon implements ITerminalAddon, IUnicode11Api {
|
export class Unicode11Addon implements ITerminalAddon , IUnicode11Api {
|
||||||
public activate(terminal: Terminal): void {
|
public activate(terminal: Terminal): void {
|
||||||
terminal.unicode.register(new UnicodeV11());
|
terminal.unicode.register(new UnicodeV11());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ const config: PlaywrightTestConfig = {
|
|||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
name: 'Chromium',
|
name: 'ChromeStable',
|
||||||
use: {
|
use: {
|
||||||
browserName: 'chromium'
|
browserName: 'chromium',
|
||||||
|
channel: 'chrome'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ const config: PlaywrightTestConfig = {
|
|||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
name: 'Chromium',
|
name: 'ChromeStable',
|
||||||
use: {
|
use: {
|
||||||
browserName: 'chromium'
|
browserName: 'chromium',
|
||||||
|
channel: 'chrome'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ function handleLink(event: MouseEvent, uri: string): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WebLinksAddon implements ITerminalAddon, IWebLinksApi {
|
export class WebLinksAddon implements ITerminalAddon , IWebLinksApi {
|
||||||
private _terminal: Terminal | undefined;
|
private _terminal: Terminal | undefined;
|
||||||
private _linkProvider: IDisposable | undefined;
|
private _linkProvider: IDisposable | undefined;
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ export class WebLinksAddon implements ITerminalAddon, IWebLinksApi {
|
|||||||
public activate(terminal: Terminal): void {
|
public activate(terminal: Terminal): void {
|
||||||
this._terminal = terminal;
|
this._terminal = terminal;
|
||||||
const options = this._options as ILinkProviderOptions;
|
const options = this._options as ILinkProviderOptions;
|
||||||
const regex = options.urlRegex ?? strictUrlRegex;
|
const regex = options.urlRegex || strictUrlRegex;
|
||||||
this._linkProvider = this._terminal.registerLinkProvider(new WebLinkProvider(this._terminal, regex, this._handler, options));
|
this._linkProvider = this._terminal.registerLinkProvider(new WebLinkProvider(this._terminal, regex, this._handler, options));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ const config: PlaywrightTestConfig = {
|
|||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
name: 'Chromium',
|
name: 'ChromeStable',
|
||||||
use: {
|
use: {
|
||||||
browserName: 'chromium'
|
browserName: 'chromium',
|
||||||
|
channel: 'chrome'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { ICellData } from 'common/Types';
|
|||||||
import { Terminal } from '@xterm/xterm';
|
import { Terminal } from '@xterm/xterm';
|
||||||
import { rgba } from 'common/Color';
|
import { rgba } from 'common/Color';
|
||||||
import { treatGlyphAsBackgroundColor } from 'browser/renderer/shared/RendererUtils';
|
import { treatGlyphAsBackgroundColor } from 'browser/renderer/shared/RendererUtils';
|
||||||
import { blockPatternCodepoints } from './customGlyphs/CustomGlyphDefinitions';
|
|
||||||
|
|
||||||
// Work variables to avoid garbage collection
|
// Work variables to avoid garbage collection
|
||||||
let $fg = 0;
|
let $fg = 0;
|
||||||
@@ -43,7 +42,7 @@ export class CellColorResolver {
|
|||||||
* Resolves colors for the cell, putting the result into the shared {@link result}. This resolves
|
* Resolves colors for the cell, putting the result into the shared {@link result}. This resolves
|
||||||
* overrides, inverse and selection for the cell which can then be used to feed into the renderer.
|
* overrides, inverse and selection for the cell which can then be used to feed into the renderer.
|
||||||
*/
|
*/
|
||||||
public resolve(cell: ICellData, x: number, y: number, deviceCellWidth: number, deviceCellHeight: number): void {
|
public resolve(cell: ICellData, x: number, y: number, deviceCellWidth: number): void {
|
||||||
this.result.bg = cell.bg;
|
this.result.bg = cell.bg;
|
||||||
this.result.fg = cell.fg;
|
this.result.fg = cell.fg;
|
||||||
this.result.ext = cell.bg & BgFlags.HAS_EXTENDED ? cell.extended.ext : 0;
|
this.result.ext = cell.bg & BgFlags.HAS_EXTENDED ? cell.extended.ext : 0;
|
||||||
@@ -64,9 +63,7 @@ export class CellColorResolver {
|
|||||||
const lineWidth = Math.max(1, Math.floor(this._optionService.rawOptions.fontSize * this._coreBrowserService.dpr / 15));
|
const lineWidth = Math.max(1, Math.floor(this._optionService.rawOptions.fontSize * this._coreBrowserService.dpr / 15));
|
||||||
$variantOffset = x * deviceCellWidth % (Math.round(lineWidth) * 2);
|
$variantOffset = x * deviceCellWidth % (Math.round(lineWidth) * 2);
|
||||||
}
|
}
|
||||||
if ($variantOffset === 0 && blockPatternCodepoints.has(code)) {
|
|
||||||
$variantOffset = ((x * deviceCellWidth) % 2) * 2 + ((y * deviceCellHeight) % 2);
|
|
||||||
}
|
|
||||||
// Apply decorations on the bottom layer
|
// Apply decorations on the bottom layer
|
||||||
this._decorationService.forEachDecorationAtCell(x, y, 'bottom', d => {
|
this._decorationService.forEachDecorationAtCell(x, y, 'bottom', d => {
|
||||||
if (d.backgroundColorRGB) {
|
if (d.backgroundColorRGB) {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { ITerminalOptions, Terminal } from '@xterm/xterm';
|
|||||||
import { ITerminal, ReadonlyColorSet } from 'browser/Types';
|
import { ITerminal, ReadonlyColorSet } from 'browser/Types';
|
||||||
import { ICharAtlasConfig, ITextureAtlas } from './Types';
|
import { ICharAtlasConfig, ITextureAtlas } from './Types';
|
||||||
import { generateConfig, configEquals } from './CharAtlasUtils';
|
import { generateConfig, configEquals } from './CharAtlasUtils';
|
||||||
import type { ILogService } from 'common/services/Services';
|
|
||||||
|
|
||||||
interface ITextureAtlasCacheEntry {
|
interface ITextureAtlasCacheEntry {
|
||||||
atlas: ITextureAtlas;
|
atlas: ITextureAtlas;
|
||||||
@@ -68,9 +67,8 @@ export function acquireTextureAtlas(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const core: ITerminal = (terminal as any)._core;
|
const core: ITerminal = (terminal as any)._core;
|
||||||
const logService = (core as any)._logService as ILogService;
|
|
||||||
const newEntry: ITextureAtlasCacheEntry = {
|
const newEntry: ITextureAtlasCacheEntry = {
|
||||||
atlas: new TextureAtlas(document, newConfig, core.unicodeService, logService),
|
atlas: new TextureAtlas(document, newConfig, core.unicodeService),
|
||||||
config: newConfig,
|
config: newConfig,
|
||||||
ownedBy: [terminal]
|
ownedBy: [terminal]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { toDisposable, IDisposable } from 'common/Lifecycle';
|
import { toDisposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||||
|
|
||||||
export function observeDevicePixelDimensions(element: HTMLElement, parentWindow: Window & typeof globalThis, callback: (deviceWidth: number, deviceHeight: number) => void): IDisposable {
|
export function observeDevicePixelDimensions(element: HTMLElement, parentWindow: Window & typeof globalThis, callback: (deviceWidth: number, deviceHeight: number) => void): IDisposable {
|
||||||
// Observe any resizes to the element and extract the actual pixel size of the element if the
|
// Observe any resizes to the element and extract the actual pixel size of the element if the
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import { TextureAtlas } from './TextureAtlas';
|
import { TextureAtlas } from './TextureAtlas';
|
||||||
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||||
import { NULL_CELL_CODE } from 'common/buffer/Constants';
|
import { NULL_CELL_CODE } from 'common/buffer/Constants';
|
||||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||||
import { Terminal } from '@xterm/xterm';
|
import { Terminal } from '@xterm/xterm';
|
||||||
import { IRenderModel, IWebGL2RenderingContext, IWebGLVertexArrayObject, type IRasterizedGlyph, type ITextureAtlas } from './Types';
|
import { IRenderModel, IWebGL2RenderingContext, IWebGLVertexArrayObject, type IRasterizedGlyph, type ITextureAtlas } from './Types';
|
||||||
import { createProgram, GLTexture, PROJECTION_MATRIX } from './WebglUtils';
|
import { createProgram, GLTexture, PROJECTION_MATRIX } from './WebglUtils';
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
|||||||
import { IThemeService } from 'browser/services/Services';
|
import { IThemeService } from 'browser/services/Services';
|
||||||
import { ReadonlyColorSet } from 'browser/Types';
|
import { ReadonlyColorSet } from 'browser/Types';
|
||||||
import { Attributes, FgFlags } from 'common/buffer/Constants';
|
import { Attributes, FgFlags } from 'common/buffer/Constants';
|
||||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||||
import { IColor } from 'common/Types';
|
import { IColor } from 'common/Types';
|
||||||
import { Terminal } from '@xterm/xterm';
|
import { Terminal } from '@xterm/xterm';
|
||||||
import { RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
|
import { RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { IColorContrastCache } from 'browser/Types';
|
import { IColorContrastCache } from 'browser/Types';
|
||||||
import { DIM_OPACITY, TEXT_BASELINE } from './Constants';
|
import { DIM_OPACITY, TEXT_BASELINE } from './Constants';
|
||||||
import { tryDrawCustomGlyph } from './customGlyphs/CustomGlyphRasterizer';
|
import { loadCustomGlyphDefinitions, tryDrawCustomGlyph } from './customGlyphs/CustomGlyphRasterizer';
|
||||||
import { computeNextVariantOffset, treatGlyphAsBackgroundColor, isPowerlineGlyph, isRestrictedPowerlineGlyph, throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
|
import { computeNextVariantOffset, treatGlyphAsBackgroundColor, isPowerlineGlyph, isRestrictedPowerlineGlyph, throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
|
||||||
import { IBoundingBox, ICharAtlasConfig, IRasterizedGlyph, ITextureAtlas } from './Types';
|
import { IBoundingBox, ICharAtlasConfig, IRasterizedGlyph, ITextureAtlas } from './Types';
|
||||||
import { NULL_COLOR, channels, color, rgba } from 'common/Color';
|
import { NULL_COLOR, channels, color, rgba } from 'common/Color';
|
||||||
@@ -14,8 +14,8 @@ import { IdleTaskQueue } from 'common/TaskQueue';
|
|||||||
import { IColor } from 'common/Types';
|
import { IColor } from 'common/Types';
|
||||||
import { AttributeData } from 'common/buffer/AttributeData';
|
import { AttributeData } from 'common/buffer/AttributeData';
|
||||||
import { Attributes, DEFAULT_COLOR, DEFAULT_EXT, UnderlineStyle } from 'common/buffer/Constants';
|
import { Attributes, DEFAULT_COLOR, DEFAULT_EXT, UnderlineStyle } from 'common/buffer/Constants';
|
||||||
import { ILogService, IUnicodeService } from 'common/services/Services';
|
import { IUnicodeService } from 'common/services/Services';
|
||||||
import { Emitter } from 'common/Event';
|
import { Emitter } from 'vs/base/common/event';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A shared object which is used to draw nothing for a particular cell.
|
* A shared object which is used to draw nothing for a particular cell.
|
||||||
@@ -88,8 +88,7 @@ export class TextureAtlas implements ITextureAtlas {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly _document: Document,
|
private readonly _document: Document,
|
||||||
private readonly _config: ICharAtlasConfig,
|
private readonly _config: ICharAtlasConfig,
|
||||||
private readonly _unicodeService: IUnicodeService,
|
private readonly _unicodeService: IUnicodeService
|
||||||
private readonly _logService: ILogService
|
|
||||||
) {
|
) {
|
||||||
this._createNewPage();
|
this._createNewPage();
|
||||||
this._tmpCanvas = createCanvas(
|
this._tmpCanvas = createCanvas(
|
||||||
@@ -101,6 +100,10 @@ export class TextureAtlas implements ITextureAtlas {
|
|||||||
alpha: this._config.allowTransparency,
|
alpha: this._config.allowTransparency,
|
||||||
willReadFrequently: true
|
willReadFrequently: true
|
||||||
}));
|
}));
|
||||||
|
// Start loading custom glyph definitions asynchronously if custom glyphs are enabled
|
||||||
|
if (this._config.customGlyphs !== false) {
|
||||||
|
loadCustomGlyphDefinitions();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
@@ -120,7 +123,7 @@ export class TextureAtlas implements ITextureAtlas {
|
|||||||
|
|
||||||
private _doWarmUp(): void {
|
private _doWarmUp(): void {
|
||||||
// Pre-fill with ASCII 33-126, this is not urgent and done in idle callbacks
|
// Pre-fill with ASCII 33-126, this is not urgent and done in idle callbacks
|
||||||
const queue = new IdleTaskQueue(this._logService);
|
const queue = new IdleTaskQueue();
|
||||||
for (let i = 33; i < 126; i++) {
|
for (let i = 33; i < 126; i++) {
|
||||||
queue.enqueue(() => {
|
queue.enqueue(() => {
|
||||||
if (!this._cacheMap.get(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT)) {
|
if (!this._cacheMap.get(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT)) {
|
||||||
@@ -177,17 +180,6 @@ export class TextureAtlas implements ITextureAtlas {
|
|||||||
|
|
||||||
// Gather details of the merge
|
// Gather details of the merge
|
||||||
const mergingPages = pagesBySize.slice(sameSizeI, sameSizeI + 4);
|
const mergingPages = pagesBySize.slice(sameSizeI, sameSizeI + 4);
|
||||||
|
|
||||||
// Only proceed with merge if we have exactly 4 same-sized pages. If not, we cannot
|
|
||||||
// effectively reduce page count and merging would cause issues.
|
|
||||||
if (mergingPages.length < 4 || mergingPages.some(p => p.canvas.width !== mergingPages[0].canvas.width)) {
|
|
||||||
const newPage = new AtlasPage(this._document, this._textureSize);
|
|
||||||
this._pages.push(newPage);
|
|
||||||
this._activePages.push(newPage);
|
|
||||||
this._onAddTextureAtlasCanvas.fire(newPage.canvas);
|
|
||||||
return newPage;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortedMergingPagesIndexes = mergingPages.map(e => e.glyphs[0].texturePage).sort((a, b) => a > b ? 1 : -1);
|
const sortedMergingPagesIndexes = mergingPages.map(e => e.glyphs[0].texturePage).sort((a, b) => a > b ? 1 : -1);
|
||||||
const mergedPageIndex = this.pages.length - mergingPages.length;
|
const mergedPageIndex = this.pages.length - mergingPages.length;
|
||||||
|
|
||||||
@@ -411,7 +403,7 @@ export class TextureAtlas implements ITextureAtlas {
|
|||||||
const cache = this._getContrastCache(dim);
|
const cache = this._getContrastCache(dim);
|
||||||
const adjustedColor = cache.getColor(bg, fg);
|
const adjustedColor = cache.getColor(bg, fg);
|
||||||
if (adjustedColor !== undefined) {
|
if (adjustedColor !== undefined) {
|
||||||
return adjustedColor ?? undefined;
|
return adjustedColor || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, inverse);
|
const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, inverse);
|
||||||
@@ -526,8 +518,7 @@ export class TextureAtlas implements ITextureAtlas {
|
|||||||
// Draw custom characters if applicable
|
// Draw custom characters if applicable
|
||||||
let customGlyph = false;
|
let customGlyph = false;
|
||||||
if (this._config.customGlyphs !== false) {
|
if (this._config.customGlyphs !== false) {
|
||||||
const variantOffset = this._workAttributeData.getUnderlineVariantOffset();
|
customGlyph = tryDrawCustomGlyph(this._tmpCtx, chars, padding, padding, this._config.deviceCellWidth, this._config.deviceCellHeight, this._config.deviceCharWidth, this._config.deviceCharHeight, this._config.fontSize, this._config.devicePixelRatio, backgroundColor.css);
|
||||||
customGlyph = tryDrawCustomGlyph(this._tmpCtx, chars, padding, padding, this._config.deviceCellWidth, this._config.deviceCellHeight, this._config.deviceCharWidth, this._config.deviceCharHeight, this._config.fontSize, this._config.devicePixelRatio, backgroundColor.css, variantOffset);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Whether to clear pixels based on a threshold difference between the glyph color and the
|
// Whether to clear pixels based on a threshold difference between the glyph color and the
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { FontWeight } from '@xterm/xterm';
|
|||||||
import { IColorSet } from 'browser/Types';
|
import { IColorSet } from 'browser/Types';
|
||||||
import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
|
import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
|
||||||
import { CursorInactiveStyle, CursorStyle, type IDisposable } from 'common/Types';
|
import { CursorInactiveStyle, CursorStyle, type IDisposable } from 'common/Types';
|
||||||
import type { IEvent } from 'common/Event';
|
import type { Event } from 'vs/base/common/event';
|
||||||
|
|
||||||
export interface IRenderModel {
|
export interface IRenderModel {
|
||||||
cells: Uint32Array;
|
cells: Uint32Array;
|
||||||
@@ -58,8 +58,8 @@ export interface ICharAtlasConfig {
|
|||||||
export interface ITextureAtlas extends IDisposable {
|
export interface ITextureAtlas extends IDisposable {
|
||||||
readonly pages: { canvas: HTMLCanvasElement, version: number }[];
|
readonly pages: { canvas: HTMLCanvasElement, version: number }[];
|
||||||
|
|
||||||
onAddTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
|
onAddTextureAtlasCanvas: Event<HTMLCanvasElement>;
|
||||||
onRemoveTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
|
onRemoveTextureAtlasCanvas: Event<HTMLCanvasElement>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Warm up the texture atlas, adding common glyphs to avoid slowing early frame.
|
* Warm up the texture atlas, adding common glyphs to avoid slowing early frame.
|
||||||
|
|||||||
@@ -7,14 +7,15 @@ import type { ITerminalAddon, Terminal } from '@xterm/xterm';
|
|||||||
import type { IWebglAddonOptions, WebglAddon as IWebglApi } from '@xterm/addon-webgl';
|
import type { IWebglAddonOptions, WebglAddon as IWebglApi } from '@xterm/addon-webgl';
|
||||||
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
|
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
|
||||||
import { ITerminal } from 'browser/Types';
|
import { ITerminal } from 'browser/Types';
|
||||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||||
import { getSafariVersion, isSafari } from 'common/Platform';
|
import { getSafariVersion, isSafari } from 'common/Platform';
|
||||||
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
|
import { ICoreService, IDecorationService, ILogService, IOptionsService } from 'common/services/Services';
|
||||||
import { IWebGL2RenderingContext } from './Types';
|
import { IWebGL2RenderingContext } from './Types';
|
||||||
import { WebglRenderer } from './WebglRenderer';
|
import { WebglRenderer } from './WebglRenderer';
|
||||||
import { Emitter, EventUtils } from 'common/Event';
|
import { setTraceLogger } from 'common/services/LogService';
|
||||||
|
import { Emitter, Event } from 'vs/base/common/event';
|
||||||
|
|
||||||
export class WebglAddon extends Disposable implements ITerminalAddon, IWebglApi {
|
export class WebglAddon extends Disposable implements ITerminalAddon , IWebglApi {
|
||||||
private _terminal?: Terminal;
|
private _terminal?: Terminal;
|
||||||
private _renderer?: WebglRenderer;
|
private _renderer?: WebglRenderer;
|
||||||
|
|
||||||
@@ -65,8 +66,13 @@ export class WebglAddon extends Disposable implements ITerminalAddon, IWebglApi
|
|||||||
const charSizeService: ICharSizeService = unsafeCore._charSizeService;
|
const charSizeService: ICharSizeService = unsafeCore._charSizeService;
|
||||||
const coreBrowserService: ICoreBrowserService = unsafeCore._coreBrowserService;
|
const coreBrowserService: ICoreBrowserService = unsafeCore._coreBrowserService;
|
||||||
const decorationService: IDecorationService = unsafeCore._decorationService;
|
const decorationService: IDecorationService = unsafeCore._decorationService;
|
||||||
|
const logService: ILogService = unsafeCore._logService;
|
||||||
const themeService: IThemeService = unsafeCore._themeService;
|
const themeService: IThemeService = unsafeCore._themeService;
|
||||||
|
|
||||||
|
// Set trace logger just in case it hasn't been yet which could happen when the addon is
|
||||||
|
// bundled separately to the core module
|
||||||
|
setTraceLogger(logService);
|
||||||
|
|
||||||
this._renderer = this._register(new WebglRenderer(
|
this._renderer = this._register(new WebglRenderer(
|
||||||
terminal,
|
terminal,
|
||||||
characterJoinerService,
|
characterJoinerService,
|
||||||
@@ -79,10 +85,10 @@ export class WebglAddon extends Disposable implements ITerminalAddon, IWebglApi
|
|||||||
this._customGlyphs,
|
this._customGlyphs,
|
||||||
this._preserveDrawingBuffer
|
this._preserveDrawingBuffer
|
||||||
));
|
));
|
||||||
this._register(EventUtils.forward(this._renderer.onContextLoss, this._onContextLoss));
|
this._register(Event.forward(this._renderer.onContextLoss, this._onContextLoss));
|
||||||
this._register(EventUtils.forward(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas));
|
this._register(Event.forward(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas));
|
||||||
this._register(EventUtils.forward(this._renderer.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas));
|
this._register(Event.forward(this._renderer.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas));
|
||||||
this._register(EventUtils.forward(this._renderer.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas));
|
this._register(Event.forward(this._renderer.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas));
|
||||||
renderService.setRenderer(this._renderer);
|
renderService.setRenderer(this._renderer);
|
||||||
|
|
||||||
this._register(toDisposable(() => {
|
this._register(toDisposable(() => {
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IThemeS
|
|||||||
import { CharData, IBufferLine, ICellData } from 'common/Types';
|
import { CharData, IBufferLine, ICellData } from 'common/Types';
|
||||||
import { AttributeData } from 'common/buffer/AttributeData';
|
import { AttributeData } from 'common/buffer/AttributeData';
|
||||||
import { CellData } from 'common/buffer/CellData';
|
import { CellData } from 'common/buffer/CellData';
|
||||||
import { Attributes, Content, FgFlags, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
|
import { Attributes, Content, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
|
||||||
import { TextBlinkStateManager } from 'browser/renderer/shared/TextBlinkStateManager';
|
|
||||||
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
|
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||||
import { Terminal } from '@xterm/xterm';
|
import { Terminal } from '@xterm/xterm';
|
||||||
import { GlyphRenderer } from './GlyphRenderer';
|
import { GlyphRenderer } from './GlyphRenderer';
|
||||||
@@ -23,15 +22,14 @@ import { COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_EXT_OFFSET
|
|||||||
import { IWebGL2RenderingContext, type ITextureAtlas } from './Types';
|
import { IWebGL2RenderingContext, type ITextureAtlas } from './Types';
|
||||||
import { LinkRenderLayer } from './renderLayer/LinkRenderLayer';
|
import { LinkRenderLayer } from './renderLayer/LinkRenderLayer';
|
||||||
import { IRenderLayer } from './renderLayer/Types';
|
import { IRenderLayer } from './renderLayer/Types';
|
||||||
import { Emitter, EventUtils } from 'common/Event';
|
import { Emitter, Event } from 'vs/base/common/event';
|
||||||
import { addDisposableListener } from 'browser/Dom';
|
import { addDisposableListener } from 'vs/base/browser/dom';
|
||||||
import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
|
import { combinedDisposable, Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||||
import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
|
import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
|
||||||
|
|
||||||
export class WebglRenderer extends Disposable implements IRenderer {
|
export class WebglRenderer extends Disposable implements IRenderer {
|
||||||
private _renderLayers: IRenderLayer[];
|
private _renderLayers: IRenderLayer[];
|
||||||
private _cursorBlinkStateManager: MutableDisposable<CursorBlinkStateManager> = new MutableDisposable();
|
private _cursorBlinkStateManager: MutableDisposable<CursorBlinkStateManager> = new MutableDisposable();
|
||||||
private _textBlinkStateManager: TextBlinkStateManager;
|
|
||||||
private _charAtlasDisposable = this._register(new MutableDisposable());
|
private _charAtlasDisposable = this._register(new MutableDisposable());
|
||||||
private _charAtlas: ITextureAtlas | undefined;
|
private _charAtlas: ITextureAtlas | undefined;
|
||||||
private _devicePixelRatio: number;
|
private _devicePixelRatio: number;
|
||||||
@@ -39,9 +37,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
private _observerDisposable = this._register(new MutableDisposable());
|
private _observerDisposable = this._register(new MutableDisposable());
|
||||||
|
|
||||||
private _model: RenderModel = new RenderModel();
|
private _model: RenderModel = new RenderModel();
|
||||||
private _rowHasBlinkingCells: boolean[] = [];
|
|
||||||
private _rowHasBlinkingCellsCount: number = 0;
|
|
||||||
private _workCell: ICellData = new CellData();
|
private _workCell: ICellData = new CellData();
|
||||||
|
private _workCell2: ICellData = new CellData();
|
||||||
private _cellColorResolver: CellColorResolver;
|
private _cellColorResolver: CellColorResolver;
|
||||||
|
|
||||||
private _canvas: HTMLCanvasElement;
|
private _canvas: HTMLCanvasElement;
|
||||||
@@ -108,12 +105,6 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
this._updateDimensions();
|
this._updateDimensions();
|
||||||
this._updateCursorBlink();
|
this._updateCursorBlink();
|
||||||
this._register(_optionsService.onOptionChange(() => this._handleOptionsChanged()));
|
this._register(_optionsService.onOptionChange(() => this._handleOptionsChanged()));
|
||||||
this._textBlinkStateManager = this._register(new TextBlinkStateManager(
|
|
||||||
() => this._requestRedrawViewport(),
|
|
||||||
this._coreBrowserService,
|
|
||||||
this._optionsService
|
|
||||||
));
|
|
||||||
this._resetBlinkingRowState();
|
|
||||||
|
|
||||||
this._deviceMaxTextureSize = this._gl.getParameter(this._gl.MAX_TEXTURE_SIZE);
|
this._deviceMaxTextureSize = this._gl.getParameter(this._gl.MAX_TEXTURE_SIZE);
|
||||||
|
|
||||||
@@ -187,7 +178,6 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
this._updateDimensions();
|
this._updateDimensions();
|
||||||
|
|
||||||
this._model.resize(this._terminal.cols, this._terminal.rows);
|
this._model.resize(this._terminal.cols, this._terminal.rows);
|
||||||
this._resetBlinkingRowState();
|
|
||||||
|
|
||||||
// Resize all render layers
|
// Resize all render layers
|
||||||
for (const l of this._renderLayers) {
|
for (const l of this._renderLayers) {
|
||||||
@@ -241,10 +231,6 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
this._requestRedrawViewport();
|
this._requestRedrawViewport();
|
||||||
}
|
}
|
||||||
|
|
||||||
public handleViewportVisibilityChange(isVisible: boolean): void {
|
|
||||||
this._textBlinkStateManager.setViewportVisible(isVisible);
|
|
||||||
}
|
|
||||||
|
|
||||||
public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
|
public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
|
||||||
for (const l of this._renderLayers) {
|
for (const l of this._renderLayers) {
|
||||||
l.handleSelectionChanged(this._terminal, start, end, columnSelectMode);
|
l.handleSelectionChanged(this._terminal, start, end, columnSelectMode);
|
||||||
@@ -304,8 +290,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
if (this._charAtlas !== atlas) {
|
if (this._charAtlas !== atlas) {
|
||||||
this._onChangeTextureAtlas.fire(atlas.pages[0].canvas);
|
this._onChangeTextureAtlas.fire(atlas.pages[0].canvas);
|
||||||
this._charAtlasDisposable.value = combinedDisposable(
|
this._charAtlasDisposable.value = combinedDisposable(
|
||||||
EventUtils.forward(atlas.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas),
|
Event.forward(atlas.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas),
|
||||||
EventUtils.forward(atlas.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas)
|
Event.forward(atlas.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this._charAtlas = atlas;
|
this._charAtlas = atlas;
|
||||||
@@ -337,9 +323,6 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
l.reset(this._terminal);
|
l.reset(this._terminal);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._resetBlinkingRowState();
|
|
||||||
this._textBlinkStateManager.setNeedsBlinkInViewport(false);
|
|
||||||
|
|
||||||
this._cursorBlinkStateManager.value?.restartBlinkAnimation();
|
this._cursorBlinkStateManager.value?.restartBlinkAnimation();
|
||||||
this._updateCursorBlink();
|
this._updateCursorBlink();
|
||||||
}
|
}
|
||||||
@@ -437,7 +420,6 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
for (y = start; y <= end; y++) {
|
for (y = start; y <= end; y++) {
|
||||||
row = y + terminal.buffer.ydisp;
|
row = y + terminal.buffer.ydisp;
|
||||||
line = terminal.buffer.lines.get(row)!;
|
line = terminal.buffer.lines.get(row)!;
|
||||||
let rowHasBlinkingCells = false;
|
|
||||||
this._model.lineLengths[y] = 0;
|
this._model.lineLengths[y] = 0;
|
||||||
isCursorRow = cursorY === row;
|
isCursorRow = cursorY === row;
|
||||||
skipJoinedCheckUntilX = 0;
|
skipJoinedCheckUntilX = 0;
|
||||||
@@ -495,12 +477,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
code = cell.getCode();
|
code = cell.getCode();
|
||||||
i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
|
i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
|
||||||
|
|
||||||
if (!rowHasBlinkingCells && cell.isBlink()) {
|
|
||||||
rowHasBlinkingCells = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load colors/resolve overrides into work colors
|
// Load colors/resolve overrides into work colors
|
||||||
this._cellColorResolver.resolve(cell, x, row, this.dimensions.device.cell.width, this.dimensions.device.cell.height);
|
this._cellColorResolver.resolve(cell, x, row, this.dimensions.device.cell.width);
|
||||||
|
|
||||||
// Override colors for cursor cell
|
// Override colors for cursor cell
|
||||||
if (isCursorVisible && row === cursorY) {
|
if (isCursorVisible && row === cursorY) {
|
||||||
@@ -528,10 +506,6 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._textBlinkStateManager.isEnabled && !this._textBlinkStateManager.isBlinkOn && cell.isBlink()) {
|
|
||||||
this._cellColorResolver.result.fg |= FgFlags.INVISIBLE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (code !== NULL_CELL_CODE) {
|
if (code !== NULL_CELL_CODE) {
|
||||||
this._model.lineLengths[y] = x + 1;
|
this._model.lineLengths[y] = x + 1;
|
||||||
}
|
}
|
||||||
@@ -578,31 +552,11 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
x--; // Go back to the previous update cell for next iteration
|
x--; // Go back to the previous update cell for next iteration
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._setRowBlinkState(y, rowHasBlinkingCells);
|
|
||||||
}
|
}
|
||||||
if (modelUpdated) {
|
if (modelUpdated) {
|
||||||
this._rectangleRenderer.value!.updateBackgrounds(this._model);
|
this._rectangleRenderer.value!.updateBackgrounds(this._model);
|
||||||
}
|
}
|
||||||
this._rectangleRenderer.value!.updateCursor(this._model);
|
this._rectangleRenderer.value!.updateCursor(this._model);
|
||||||
this._updateTextBlinkState();
|
|
||||||
}
|
|
||||||
|
|
||||||
private _resetBlinkingRowState(): void {
|
|
||||||
this._rowHasBlinkingCells = new Array(this._terminal.rows).fill(false);
|
|
||||||
this._rowHasBlinkingCellsCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _setRowBlinkState(row: number, hasBlinkingCells: boolean): void {
|
|
||||||
const previous = this._rowHasBlinkingCells[row];
|
|
||||||
if (previous === hasBlinkingCells) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._rowHasBlinkingCells[row] = hasBlinkingCells;
|
|
||||||
this._rowHasBlinkingCellsCount += hasBlinkingCells ? 1 : -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _updateTextBlinkState(): void {
|
|
||||||
this._textBlinkStateManager.setNeedsBlinkInViewport(this._rowHasBlinkingCellsCount > 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -644,8 +644,8 @@ export const customGlyphDefinitions: { [index: string]: CustomGlyphCharacterDefi
|
|||||||
] },
|
] },
|
||||||
|
|
||||||
// Diagonal fill characters (1FB98-1FB99)
|
// Diagonal fill characters (1FB98-1FB99)
|
||||||
'\u{1FB98}': { type: CustomGlyphDefinitionType.PATH_FUNCTION, data: 'M-0.25,-0.25 L1.25,1.25 M-0.25,0 L1,1.25 M-0.25,0.25 L0.75,1.25 M-0.25,0.5 L0.5,1.25 M0,-0.25 L1.25,1 M0.25,-0.25 L1.25,0.75 M0.5,-0.25 L1.25,0.5 M-0.25,0.75 L0.25,1.25 M0.75,-0.25 L1.25,0.25', strokeWidth: 1 }, // UPPER LEFT TO LOWER RIGHT FILL
|
'\u{1FB98}': { type: CustomGlyphDefinitionType.PATH_FUNCTION, data: 'M0,0 L1,1 M0,.25 L.75,1 M0,.5 L.5,1 M0,.75 L.25,1 M.25,0 L1,.75 M.5,0 L1,.5 M.75,0 L1,.25', strokeWidth: 1 }, // UPPER LEFT TO LOWER RIGHT FILL
|
||||||
'\u{1FB99}': { type: CustomGlyphDefinitionType.PATH_FUNCTION, data: 'M-0.25,0.5 L0.5,-0.25 M-0.25,0.75 L0.75,-0.25 M-0.25,1 L1,-0.25 M-0.25,1.25 L1.25,-0.25 M0,1.25 L1.25,0 M0.25,1.25 L1.25,0.25 M0.5,1.25 L1.25,0.5 M-0.25,0.25 L0.25,-0.25 M0.75,1.25 L1.25,0.75', strokeWidth: 1 }, // UPPER RIGHT TO LOWER LEFT FILL
|
'\u{1FB99}': { type: CustomGlyphDefinitionType.PATH_FUNCTION, data: 'M0,.25 L.25,0 M0,.5 L.5,0 M0,.75 L.75,0 M0,1 L1,0 M.25,1 L1,.25 M.5,1 L1,.5 M.75,1 L1,.75', strokeWidth: 1 }, // UPPER RIGHT TO LOWER LEFT FILL
|
||||||
|
|
||||||
// Smooth mosaic terminal graphic characters (1FB9A-1FB9B)
|
// Smooth mosaic terminal graphic characters (1FB9A-1FB9B)
|
||||||
'\u{1FB9A}': { type: CustomGlyphDefinitionType.VECTOR_SHAPE, data: { d: 'M0,0 L.5,.5 L0,1 L1,1 L.5,.5 L1,0', type: CustomGlyphVectorType.FILL } }, // UPPER AND LOWER TRIANGULAR HALF BLOCK
|
'\u{1FB9A}': { type: CustomGlyphDefinitionType.VECTOR_SHAPE, data: { d: 'M0,0 L.5,.5 L0,1 L1,1 L.5,.5 L1,0', type: CustomGlyphVectorType.FILL } }, // UPPER AND LOWER TRIANGULAR HALF BLOCK
|
||||||
@@ -820,27 +820,6 @@ export const customGlyphDefinitions: { [index: string]: CustomGlyphCharacterDefi
|
|||||||
// #endregion
|
// #endregion
|
||||||
};
|
};
|
||||||
|
|
||||||
export const blockPatternCodepoints = new Set<number>([
|
|
||||||
// Shade characters (2591-2593)
|
|
||||||
0x2591,
|
|
||||||
0x2592,
|
|
||||||
0x2593,
|
|
||||||
// Rectangular shade characters (1FB8C-1FB94)
|
|
||||||
0x1FB8C,
|
|
||||||
0x1FB8D,
|
|
||||||
0x1FB8E,
|
|
||||||
0x1FB8F,
|
|
||||||
0x1FB90,
|
|
||||||
0x1FB91,
|
|
||||||
0x1FB92,
|
|
||||||
0x1FB94,
|
|
||||||
// Triangular shade characters (1FB9C-1FB9F)
|
|
||||||
0x1FB9C,
|
|
||||||
0x1FB9D,
|
|
||||||
0x1FB9E,
|
|
||||||
0x1FB9F
|
|
||||||
]);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a drawing function for sextant characters. Sextants are a 2x3 grid where each cell
|
* Generates a drawing function for sextant characters. Sextants are a 2x3 grid where each cell
|
||||||
* can be on or off.
|
* can be on or off.
|
||||||
|
|||||||
@@ -4,8 +4,38 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
|
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
|
||||||
import { customGlyphDefinitions } from './CustomGlyphDefinitions';
|
import { CustomGlyphDefinitionType, CustomGlyphScaleType, CustomGlyphVectorType, type CustomGlyphCharacterDefinition, type CustomGlyphDefinitionPart, type CustomGlyphPathDrawFunctionDefinition, type CustomGlyphPatternDefinition, type ICustomGlyphSolidOctantBlockVector, type ICustomGlyphVectorShape } from './Types';
|
||||||
import { CustomGlyphDefinitionType, CustomGlyphScaleType, CustomGlyphVectorType, type CustomGlyphDefinitionPart, type CustomGlyphPathDrawFunctionDefinition, type CustomGlyphPatternDefinition, type ICustomGlyphSolidOctantBlockVector, type ICustomGlyphVectorShape } from './Types';
|
|
||||||
|
/**
|
||||||
|
* Cached custom glyph definitions. This is loaded dynamically to support code splitting.
|
||||||
|
*/
|
||||||
|
let customGlyphDefinitions: { [index: string]: CustomGlyphCharacterDefinition | undefined } | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Promise that resolves when custom glyph definitions are loaded.
|
||||||
|
*/
|
||||||
|
let loadingPromise: Promise<void> | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Begins loading the custom glyph definitions. This should be called as early as possible
|
||||||
|
* to ensure the definitions are ready when needed.
|
||||||
|
* @returns A promise that resolves when the definitions are loaded.
|
||||||
|
*/
|
||||||
|
export function loadCustomGlyphDefinitions(): Promise<void> {
|
||||||
|
if (customGlyphDefinitions) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
if (!loadingPromise) {
|
||||||
|
loadingPromise = import('./CustomGlyphDefinitions').then(module => {
|
||||||
|
customGlyphDefinitions = module.customGlyphDefinitions;
|
||||||
|
}).catch(err => {
|
||||||
|
// Reset the loading promise on failure to allow retry
|
||||||
|
loadingPromise = undefined;
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return loadingPromise;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Try drawing a custom block element or box drawing character, returning whether it was
|
* Try drawing a custom block element or box drawing character, returning whether it was
|
||||||
@@ -22,15 +52,19 @@ export function tryDrawCustomGlyph(
|
|||||||
deviceCharHeight: number,
|
deviceCharHeight: number,
|
||||||
fontSize: number,
|
fontSize: number,
|
||||||
devicePixelRatio: number,
|
devicePixelRatio: number,
|
||||||
backgroundColor?: string,
|
backgroundColor?: string
|
||||||
variantOffset: number = 0
|
|
||||||
): boolean {
|
): boolean {
|
||||||
|
// If definitions are not loaded yet, return false to fall back to font rendering
|
||||||
|
if (!customGlyphDefinitions) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const unifiedCharDefinition = customGlyphDefinitions[c];
|
const unifiedCharDefinition = customGlyphDefinitions[c];
|
||||||
if (unifiedCharDefinition) {
|
if (unifiedCharDefinition) {
|
||||||
// Normalize to array for uniform handling
|
// Normalize to array for uniform handling
|
||||||
const parts = Array.isArray(unifiedCharDefinition) ? unifiedCharDefinition : [unifiedCharDefinition];
|
const parts = Array.isArray(unifiedCharDefinition) ? unifiedCharDefinition : [unifiedCharDefinition];
|
||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
drawDefinitionPart(ctx, part, xOffset, yOffset, deviceCellWidth, deviceCellHeight, deviceCharWidth, deviceCharHeight, fontSize, devicePixelRatio, backgroundColor, variantOffset);
|
drawDefinitionPart(ctx, part, xOffset, yOffset, deviceCellWidth, deviceCellHeight, deviceCharWidth, deviceCharHeight, fontSize, devicePixelRatio, backgroundColor);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -49,8 +83,7 @@ function drawDefinitionPart(
|
|||||||
deviceCharHeight: number,
|
deviceCharHeight: number,
|
||||||
fontSize: number,
|
fontSize: number,
|
||||||
devicePixelRatio: number,
|
devicePixelRatio: number,
|
||||||
backgroundColor?: string,
|
backgroundColor?: string
|
||||||
variantOffset: number = 0
|
|
||||||
): void {
|
): void {
|
||||||
// Handle scaleType - adjust dimensions and offset when scaling to character area
|
// Handle scaleType - adjust dimensions and offset when scaling to character area
|
||||||
let drawWidth = deviceCellWidth;
|
let drawWidth = deviceCellWidth;
|
||||||
@@ -76,7 +109,7 @@ function drawDefinitionPart(
|
|||||||
drawBlockVectorChar(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight);
|
drawBlockVectorChar(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight);
|
||||||
break;
|
break;
|
||||||
case CustomGlyphDefinitionType.BLOCK_PATTERN:
|
case CustomGlyphDefinitionType.BLOCK_PATTERN:
|
||||||
drawPatternChar(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight, variantOffset);
|
drawPatternChar(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight);
|
||||||
break;
|
break;
|
||||||
case CustomGlyphDefinitionType.PATH_FUNCTION:
|
case CustomGlyphDefinitionType.PATH_FUNCTION:
|
||||||
drawPathFunctionCharacter(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight, devicePixelRatio, part.strokeWidth);
|
drawPathFunctionCharacter(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight, devicePixelRatio, part.strokeWidth);
|
||||||
@@ -439,8 +472,7 @@ function drawPatternChar(
|
|||||||
xOffset: number,
|
xOffset: number,
|
||||||
yOffset: number,
|
yOffset: number,
|
||||||
deviceCellWidth: number,
|
deviceCellWidth: number,
|
||||||
deviceCellHeight: number,
|
deviceCellHeight: number
|
||||||
variantOffset: number = 0
|
|
||||||
): void {
|
): void {
|
||||||
let patternSet = cachedPatterns.get(charDefinition);
|
let patternSet = cachedPatterns.get(charDefinition);
|
||||||
if (!patternSet) {
|
if (!patternSet) {
|
||||||
@@ -489,15 +521,6 @@ function drawPatternChar(
|
|||||||
pattern = throwIfFalsy(ctx.createPattern(tmpCanvas, null));
|
pattern = throwIfFalsy(ctx.createPattern(tmpCanvas, null));
|
||||||
patternSet.set(fillStyle, pattern);
|
patternSet.set(fillStyle, pattern);
|
||||||
}
|
}
|
||||||
// Apply pattern offset to ensure seamless tiling across cells when cell dimensions are odd.
|
|
||||||
// variantOffset encodes: bit 1 = x pixel shift, bit 0 = y pixel shift.
|
|
||||||
const dx = (variantOffset >> 1) & 1;
|
|
||||||
const dy = variantOffset & 1;
|
|
||||||
if (dx !== 0 || dy !== 0) {
|
|
||||||
pattern.setTransform(new DOMMatrix().translateSelf(-dx, -dy));
|
|
||||||
} else {
|
|
||||||
pattern.setTransform(new DOMMatrix());
|
|
||||||
}
|
|
||||||
ctx.fillStyle = pattern;
|
ctx.fillStyle = pattern;
|
||||||
ctx.fillRect(xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
ctx.fillRect(xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
||||||
}
|
}
|
||||||
@@ -512,11 +535,6 @@ function drawPathFunctionCharacter(
|
|||||||
devicePixelRatio: number,
|
devicePixelRatio: number,
|
||||||
strokeWidth?: number
|
strokeWidth?: number
|
||||||
): void {
|
): void {
|
||||||
ctx.save();
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.rect(xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
|
||||||
ctx.clip();
|
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
let actualInstructions: string;
|
let actualInstructions: string;
|
||||||
if (typeof charDefinition === 'function') {
|
if (typeof charDefinition === 'function') {
|
||||||
@@ -543,7 +561,7 @@ function drawPathFunctionCharacter(
|
|||||||
if (!args[0] || !args[1]) {
|
if (!args[0] || !args[1]) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
f(ctx, translateArgs(args, deviceCellWidth, deviceCellHeight, xOffset, yOffset, true, devicePixelRatio, 0, 0, false), state);
|
f(ctx, translateArgs(args, deviceCellWidth, deviceCellHeight, xOffset, yOffset, true, devicePixelRatio), state);
|
||||||
state.lastCommand = type;
|
state.lastCommand = type;
|
||||||
}
|
}
|
||||||
if (strokeWidth !== undefined) {
|
if (strokeWidth !== undefined) {
|
||||||
@@ -554,7 +572,6 @@ function drawPathFunctionCharacter(
|
|||||||
ctx.fill();
|
ctx.fill();
|
||||||
}
|
}
|
||||||
ctx.closePath();
|
ctx.closePath();
|
||||||
ctx.restore();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -703,7 +720,7 @@ const svgToCanvasInstructionMap: { [index: string]: (ctx: CanvasRenderingContext
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function translateArgs(args: string[], cellWidth: number, cellHeight: number, xOffset: number, yOffset: number, doClamp: boolean, devicePixelRatio: number, leftPadding: number = 0, rightPadding: number = 0, clampToCell: boolean = true): number[] {
|
function translateArgs(args: string[], cellWidth: number, cellHeight: number, xOffset: number, yOffset: number, doClamp: boolean, devicePixelRatio: number, leftPadding: number = 0, rightPadding: number = 0): number[] {
|
||||||
const result = args.map(e => parseFloat(e) || parseInt(e));
|
const result = args.map(e => parseFloat(e) || parseInt(e));
|
||||||
|
|
||||||
if (result.length < 2) {
|
if (result.length < 2) {
|
||||||
@@ -713,11 +730,10 @@ function translateArgs(args: string[], cellWidth: number, cellHeight: number, xO
|
|||||||
for (let x = 0; x < result.length; x += 2) {
|
for (let x = 0; x < result.length; x += 2) {
|
||||||
// Translate from 0-1 to 0-cellWidth
|
// Translate from 0-1 to 0-cellWidth
|
||||||
result[x] *= cellWidth - (leftPadding * devicePixelRatio) - (rightPadding * devicePixelRatio);
|
result[x] *= cellWidth - (leftPadding * devicePixelRatio) - (rightPadding * devicePixelRatio);
|
||||||
// Round to the nearest 0.5 to ensure a crisp line at 100% devicePixelRatio, and optionally
|
// Ensure coordinate doesn't escape cell bounds and round to the nearest 0.5 to ensure a crisp
|
||||||
// clamp to the cell bounds.
|
// line at 100% devicePixelRatio
|
||||||
if (doClamp && result[x] !== 0) {
|
if (doClamp && result[x] !== 0) {
|
||||||
const rounded = Math.round(result[x] + 0.5) - 0.5;
|
result[x] = clamp(Math.round(result[x] + 0.5) - 0.5, cellWidth, 0);
|
||||||
result[x] = clampToCell ? clamp(rounded, cellWidth, 0) : rounded;
|
|
||||||
}
|
}
|
||||||
// Apply the cell's offset (ie. x*cellWidth)
|
// Apply the cell's offset (ie. x*cellWidth)
|
||||||
result[x] += xOffset + (leftPadding * devicePixelRatio);
|
result[x] += xOffset + (leftPadding * devicePixelRatio);
|
||||||
@@ -726,11 +742,10 @@ function translateArgs(args: string[], cellWidth: number, cellHeight: number, xO
|
|||||||
for (let y = 1; y < result.length; y += 2) {
|
for (let y = 1; y < result.length; y += 2) {
|
||||||
// Translate from 0-1 to 0-cellHeight
|
// Translate from 0-1 to 0-cellHeight
|
||||||
result[y] *= cellHeight;
|
result[y] *= cellHeight;
|
||||||
// Round to the nearest 0.5 to ensure a crisp line at 100% devicePixelRatio, and optionally
|
// Ensure coordinate doesn't escape cell bounds and round to the nearest 0.5 to ensure a crisp
|
||||||
// clamp to the cell bounds.
|
// line at 100% devicePixelRatio
|
||||||
if (doClamp && result[y] !== 0) {
|
if (doClamp && result[y] !== 0) {
|
||||||
const rounded = Math.round(result[y] + 0.5) - 0.5;
|
result[y] = clamp(Math.round(result[y] + 0.5) - 0.5, cellHeight, 0);
|
||||||
result[y] = clampToCell ? clamp(rounded, cellHeight, 0) : rounded;
|
|
||||||
}
|
}
|
||||||
// Apply the cell's offset (ie. x*cellHeight)
|
// Apply the cell's offset (ie. x*cellHeight)
|
||||||
result[y] += yOffset;
|
result[y] += yOffset;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { ReadonlyColorSet } from 'browser/Types';
|
|||||||
import { acquireTextureAtlas } from '../CharAtlasCache';
|
import { acquireTextureAtlas } from '../CharAtlasCache';
|
||||||
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||||
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||||
import { CellData } from 'common/buffer/CellData';
|
import { CellData } from 'common/buffer/CellData';
|
||||||
import { IOptionsService } from 'common/services/Services';
|
import { IOptionsService } from 'common/services/Services';
|
||||||
import { Terminal } from '@xterm/xterm';
|
import { Terminal } from '@xterm/xterm';
|
||||||
|
|||||||
@@ -18,6 +18,9 @@
|
|||||||
"browser/*": [
|
"browser/*": [
|
||||||
"../../../src/browser/*"
|
"../../../src/browser/*"
|
||||||
],
|
],
|
||||||
|
"vs/*": [
|
||||||
|
"../../../src/vs/*"
|
||||||
|
],
|
||||||
"@xterm/addon-webgl": [
|
"@xterm/addon-webgl": [
|
||||||
"../typings/addon-webgl.d.ts"
|
"../typings/addon-webgl.d.ts"
|
||||||
]
|
]
|
||||||
@@ -39,6 +42,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../../../src/browser"
|
"path": "../../../src/browser"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../../src/vs"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ const config: PlaywrightTestConfig = {
|
|||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
name: 'Chromium',
|
name: 'ChromeStable',
|
||||||
use: {
|
use: {
|
||||||
browserName: 'chromium'
|
browserName: 'chromium',
|
||||||
|
channel: 'chrome'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
+18
-6
@@ -26,7 +26,10 @@ const commonOptions = {
|
|||||||
target: 'es2021',
|
target: 'es2021',
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
treeShaking: true,
|
treeShaking: true,
|
||||||
|
splitting: true,
|
||||||
|
chunkNames: 'chunks/[name]-[hash]',
|
||||||
logLevel: 'warning',
|
logLevel: 'warning',
|
||||||
|
outExtension: { '.js': '.mjs' },
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @type {esbuild.BuildOptions} */
|
/** @type {esbuild.BuildOptions} */
|
||||||
@@ -103,7 +106,8 @@ if (config.addon) {
|
|||||||
bundleConfig = {
|
bundleConfig = {
|
||||||
...bundleConfig,
|
...bundleConfig,
|
||||||
entryPoints: [`addons/addon-${config.addon}/src/${getAddonEntryPoint(config.addon)}.ts`],
|
entryPoints: [`addons/addon-${config.addon}/src/${getAddonEntryPoint(config.addon)}.ts`],
|
||||||
outfile: `addons/addon-${config.addon}/lib/addon-${config.addon}.mjs`,
|
outdir: `addons/addon-${config.addon}/lib`,
|
||||||
|
entryNames: `addon-${config.addon}`,
|
||||||
};
|
};
|
||||||
outConfig = {
|
outConfig = {
|
||||||
...outConfig,
|
...outConfig,
|
||||||
@@ -127,9 +131,9 @@ if (config.addon) {
|
|||||||
} else if (config.isDemoClient) {
|
} else if (config.isDemoClient) {
|
||||||
bundleConfig = {
|
bundleConfig = {
|
||||||
...bundleConfig,
|
...bundleConfig,
|
||||||
sourcemap: false,
|
|
||||||
entryPoints: [`demo/client/client.ts`],
|
entryPoints: [`demo/client/client.ts`],
|
||||||
outfile: 'demo/dist/client-bundle.js',
|
outdir: 'demo/dist',
|
||||||
|
entryNames: 'client-bundle',
|
||||||
external: ['util', 'os', 'fs', 'path', 'stream', 'Terminal'],
|
external: ['util', 'os', 'fs', 'path', 'stream', 'Terminal'],
|
||||||
alias: {
|
alias: {
|
||||||
// Library ESM imports
|
// Library ESM imports
|
||||||
@@ -165,6 +169,10 @@ if (config.addon) {
|
|||||||
format: 'cjs',
|
format: 'cjs',
|
||||||
platform: 'node',
|
platform: 'node',
|
||||||
external: ['node-pty'],
|
external: ['node-pty'],
|
||||||
|
// Splitting is not compatible with cjs format
|
||||||
|
splitting: false,
|
||||||
|
// Override the .mjs extension from commonOptions
|
||||||
|
outExtension: {},
|
||||||
}
|
}
|
||||||
skipOut = true;
|
skipOut = true;
|
||||||
skipOutTest = true;
|
skipOutTest = true;
|
||||||
@@ -172,7 +180,8 @@ if (config.addon) {
|
|||||||
bundleConfig = {
|
bundleConfig = {
|
||||||
...bundleConfig,
|
...bundleConfig,
|
||||||
entryPoints: [`src/headless/public/Terminal.ts`],
|
entryPoints: [`src/headless/public/Terminal.ts`],
|
||||||
outfile: `headless/lib-headless/xterm-headless.mjs`
|
outdir: `headless/lib-headless`,
|
||||||
|
entryNames: 'xterm-headless',
|
||||||
};
|
};
|
||||||
outConfig = {
|
outConfig = {
|
||||||
...outConfig,
|
...outConfig,
|
||||||
@@ -184,14 +193,17 @@ if (config.addon) {
|
|||||||
bundleConfig = {
|
bundleConfig = {
|
||||||
...bundleConfig,
|
...bundleConfig,
|
||||||
entryPoints: [`src/browser/public/Terminal.ts`],
|
entryPoints: [`src/browser/public/Terminal.ts`],
|
||||||
outfile: `lib/xterm.mjs`
|
outdir: `lib`,
|
||||||
|
entryNames: 'xterm',
|
||||||
};
|
};
|
||||||
outConfig = {
|
outConfig = {
|
||||||
...outConfig,
|
...outConfig,
|
||||||
entryPoints: [
|
entryPoints: [
|
||||||
'src/browser/**/*.ts',
|
'src/browser/**/*.ts',
|
||||||
'src/common/**/*.ts',
|
'src/common/**/*.ts',
|
||||||
'src/headless/**/*.ts'
|
'src/headless/**/*.ts',
|
||||||
|
'src/vs/base/**/*.ts',
|
||||||
|
'src/vs/patches/**/*.ts'
|
||||||
],
|
],
|
||||||
outdir: 'out-esbuild/'
|
outdir: 'out-esbuild/'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,9 +6,6 @@
|
|||||||
const cp = require('child_process');
|
const cp = require('child_process');
|
||||||
const path = require('path');
|
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;
|
const COVERAGE_LINES_THRESHOLD = 40;
|
||||||
|
|
||||||
// Add `out` to the NODE_PATH so absolute paths can be resolved.
|
// Add `out` to the NODE_PATH so absolute paths can be resolved.
|
||||||
|
|||||||
+11
-24
@@ -218,30 +218,17 @@
|
|||||||
/* Derived from vs/base/browser/ui/scrollbar/media/scrollbar.css */
|
/* Derived from vs/base/browser/ui/scrollbar/media/scrollbar.css */
|
||||||
|
|
||||||
/* xterm.js customization: Override xterm's cursor style */
|
/* xterm.js customization: Override xterm's cursor style */
|
||||||
.xterm .xterm-scrollable-element > .xterm-scrollbar {
|
.xterm .xterm-scrollable-element > .scrollbar {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Arrows */
|
/* Arrows */
|
||||||
|
.xterm .xterm-scrollable-element > .scrollbar > .scra {
|
||||||
.xterm .xterm-scrollable-element > .xterm-scrollbar > .xterm-scra {
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: var(--vscode-scrollbarSliderBackground, rgba(100, 100, 100, 0.4));
|
font-size: 11px !important;
|
||||||
mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 11 11'><path d='M2.5 8.5 L5.5 2.5 L8.5 8.5 Z' fill='black'/></svg>");
|
|
||||||
-webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 11 11'><path d='M2.5 8.5 L5.5 2.5 L8.5 8.5 Z' fill='black'/></svg>");
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
-webkit-mask-repeat: no-repeat;
|
|
||||||
mask-position: center center;
|
|
||||||
-webkit-mask-position: center center;
|
|
||||||
mask-size: 100% 100%;
|
|
||||||
-webkit-mask-size: 100% 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.xterm .xterm-scrollable-element > .xterm-scrollbar > .xterm-scra.xterm-arrow-down {
|
.xterm .xterm-scrollable-element > .visible {
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm .xterm-scrollable-element > .xterm-visible {
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
||||||
/* Background rule added for IE9 - to allow clicks on dom node */
|
/* Background rule added for IE9 - to allow clicks on dom node */
|
||||||
@@ -251,20 +238,20 @@
|
|||||||
/* In front of peek view */
|
/* In front of peek view */
|
||||||
z-index: 11;
|
z-index: 11;
|
||||||
}
|
}
|
||||||
.xterm .xterm-scrollable-element > .xterm-invisible {
|
.xterm .xterm-scrollable-element > .invisible {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
.xterm .xterm-scrollable-element > .xterm-invisible.xterm-fade {
|
.xterm .xterm-scrollable-element > .invisible.fade {
|
||||||
transition: opacity 800ms linear;
|
transition: opacity 800ms linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Scrollable Content Inset Shadow */
|
/* Scrollable Content Inset Shadow */
|
||||||
.xterm .xterm-scrollable-element > .xterm-shadow {
|
.xterm .xterm-scrollable-element > .shadow {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.xterm .xterm-scrollable-element > .xterm-shadow.xterm-shadow-top {
|
.xterm .xterm-scrollable-element > .shadow.top {
|
||||||
display: block;
|
display: block;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 3px;
|
left: 3px;
|
||||||
@@ -272,7 +259,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
box-shadow: var(--vscode-scrollbar-shadow, #000) 0 6px 6px -6px inset;
|
box-shadow: var(--vscode-scrollbar-shadow, #000) 0 6px 6px -6px inset;
|
||||||
}
|
}
|
||||||
.xterm .xterm-scrollable-element > .xterm-shadow.xterm-shadow-left {
|
.xterm .xterm-scrollable-element > .shadow.left {
|
||||||
display: block;
|
display: block;
|
||||||
top: 3px;
|
top: 3px;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -280,13 +267,13 @@
|
|||||||
width: 3px;
|
width: 3px;
|
||||||
box-shadow: var(--vscode-scrollbar-shadow, #000) 6px 0 6px -6px inset;
|
box-shadow: var(--vscode-scrollbar-shadow, #000) 6px 0 6px -6px inset;
|
||||||
}
|
}
|
||||||
.xterm .xterm-scrollable-element > .xterm-shadow.xterm-shadow-top-left-corner {
|
.xterm .xterm-scrollable-element > .shadow.top-left-corner {
|
||||||
display: block;
|
display: block;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
height: 3px;
|
height: 3px;
|
||||||
width: 3px;
|
width: 3px;
|
||||||
}
|
}
|
||||||
.xterm .xterm-scrollable-element > .xterm-shadow.xterm-shadow-top.xterm-shadow-left {
|
.xterm .xterm-scrollable-element > .shadow.top.left {
|
||||||
box-shadow: var(--vscode-scrollbar-shadow, #000) 6px 0 6px -6px inset;
|
box-shadow: var(--vscode-scrollbar-shadow, #000) 6px 0 6px -6px inset;
|
||||||
}
|
}
|
||||||
|
|||||||
+80
-99
@@ -16,12 +16,9 @@ if ('WebAssembly' in window) {
|
|||||||
import { Terminal, ITerminalOptions, type ITheme } from '@xterm/xterm';
|
import { Terminal, ITerminalOptions, type ITheme } from '@xterm/xterm';
|
||||||
import { AttachAddon } from '@xterm/addon-attach';
|
import { AttachAddon } from '@xterm/addon-attach';
|
||||||
import { AddonImageWindow } from './components/window/addonImageWindow';
|
import { AddonImageWindow } from './components/window/addonImageWindow';
|
||||||
import { AddonLigaturesWindow } from './components/window/addonLigaturesWindow';
|
|
||||||
import { AddonProgressWindow } from './components/window/addonProgressWindow';
|
|
||||||
import { AddonSearchWindow } from './components/window/addonSearchWindow';
|
import { AddonSearchWindow } from './components/window/addonSearchWindow';
|
||||||
import { AddonSerializeWindow } from './components/window/addonSerializeWindow';
|
import { AddonSerializeWindow } from './components/window/addonSerializeWindow';
|
||||||
import { AddonWebFontsWindow } from './components/window/addonWebFontsWindow';
|
import { AddonWebFontsWindow } from './components/window/addonWebFontsWindow';
|
||||||
import { AddonWebLinksWindow } from './components/window/addonWebLinksWindow';
|
|
||||||
import { AddonsWindow } from './components/window/addonsWindow';
|
import { AddonsWindow } from './components/window/addonsWindow';
|
||||||
import { CellInspectorWindow } from './components/window/cellInspectorWindow';
|
import { CellInspectorWindow } from './components/window/cellInspectorWindow';
|
||||||
import { ControlBar } from './components/controlBar';
|
import { ControlBar } from './components/controlBar';
|
||||||
@@ -61,11 +58,11 @@ export interface IWindowWithTerminal extends Window {
|
|||||||
}
|
}
|
||||||
declare let window: IWindowWithTerminal;
|
declare let window: IWindowWithTerminal;
|
||||||
|
|
||||||
let term: Terminal | null;
|
let term;
|
||||||
let protocol: string;
|
let protocol;
|
||||||
let socketURL: string;
|
let socketURL;
|
||||||
let socket: WebSocket | null;
|
let socket;
|
||||||
let pid: string;
|
let pid;
|
||||||
let controlBar: ControlBar;
|
let controlBar: ControlBar;
|
||||||
let addonsWindow: AddonsWindow;
|
let addonsWindow: AddonsWindow;
|
||||||
let addonSearchWindow: AddonSearchWindow;
|
let addonSearchWindow: AddonSearchWindow;
|
||||||
@@ -76,7 +73,7 @@ const addons: AddonCollection = {
|
|||||||
attach: { name: 'attach', ctor: AttachAddon, canChange: false },
|
attach: { name: 'attach', ctor: AttachAddon, canChange: false },
|
||||||
clipboard: { name: 'clipboard', ctor: ClipboardAddon, canChange: true },
|
clipboard: { name: 'clipboard', ctor: ClipboardAddon, canChange: true },
|
||||||
fit: { name: 'fit', ctor: FitAddon, canChange: false },
|
fit: { name: 'fit', ctor: FitAddon, canChange: false },
|
||||||
image: { name: 'image', ctor: ImageAddon!, canChange: true },
|
image: { name: 'image', ctor: ImageAddon, canChange: true },
|
||||||
progress: { name: 'progress', ctor: ProgressAddon, canChange: true },
|
progress: { name: 'progress', ctor: ProgressAddon, canChange: true },
|
||||||
search: { name: 'search', ctor: SearchAddon, canChange: true },
|
search: { name: 'search', ctor: SearchAddon, canChange: true },
|
||||||
serialize: { name: 'serialize', ctor: SerializeAddon, canChange: true },
|
serialize: { name: 'serialize', ctor: SerializeAddon, canChange: true },
|
||||||
@@ -119,8 +116,8 @@ const xtermjsTheme = {
|
|||||||
brightWhite: '#FFFFFF'
|
brightWhite: '#FFFFFF'
|
||||||
} satisfies ITheme;
|
} satisfies ITheme;
|
||||||
function setPadding(): void {
|
function setPadding(): void {
|
||||||
term!.element!.style.padding = parseInt(paddingElement.value, 10).toString() + 'px';
|
term.element.style.padding = parseInt(paddingElement.value, 10).toString() + 'px';
|
||||||
addons.fit.instance!.fit();
|
addons.fit.instance.fit();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSearchOptions(): ISearchOptions {
|
function getSearchOptions(): ISearchOptions {
|
||||||
@@ -144,7 +141,7 @@ const disposeRecreateButtonHandler: () => void = () => {
|
|||||||
if (term) {
|
if (term) {
|
||||||
term.dispose();
|
term.dispose();
|
||||||
term = null;
|
term = null;
|
||||||
(window as any).term = null;
|
window.term = null;
|
||||||
socket = null;
|
socket = null;
|
||||||
addons.attach.instance = undefined;
|
addons.attach.instance = undefined;
|
||||||
addons.clipboard.instance = undefined;
|
addons.clipboard.instance = undefined;
|
||||||
@@ -157,10 +154,10 @@ const disposeRecreateButtonHandler: () => void = () => {
|
|||||||
addons.ligatures.instance = undefined;
|
addons.ligatures.instance = undefined;
|
||||||
addons.webLinks.instance = undefined;
|
addons.webLinks.instance = undefined;
|
||||||
addons.webgl.instance = undefined;
|
addons.webgl.instance = undefined;
|
||||||
document.getElementById('dispose')!.innerHTML = 'Recreate Terminal';
|
document.getElementById('dispose').innerHTML = 'Recreate Terminal';
|
||||||
} else {
|
} else {
|
||||||
createTerminal();
|
createTerminal();
|
||||||
document.getElementById('dispose')!.innerHTML = 'Dispose terminal';
|
document.getElementById('dispose').innerHTML = 'Dispose terminal';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -168,7 +165,7 @@ const createNewWindowButtonHandler: () => void = () => {
|
|||||||
if (term) {
|
if (term) {
|
||||||
disposeRecreateButtonHandler();
|
disposeRecreateButtonHandler();
|
||||||
}
|
}
|
||||||
const win = window.open()!;
|
const win = window.open();
|
||||||
terminalContainer = win.document.createElement('div');
|
terminalContainer = win.document.createElement('div');
|
||||||
terminalContainer.id = 'terminal-container';
|
terminalContainer.id = 'terminal-container';
|
||||||
win.document.body.appendChild(terminalContainer);
|
win.document.body.appendChild(terminalContainer);
|
||||||
@@ -210,20 +207,17 @@ if (document.location.pathname === '/test') {
|
|||||||
} else {
|
} else {
|
||||||
const typedTerm = createTerminal();
|
const typedTerm = createTerminal();
|
||||||
|
|
||||||
controlBar = new ControlBar(document.getElementById('sidebar')!, document.querySelector('.banner-tabs')!, []);
|
controlBar = new ControlBar(document.getElementById('sidebar'), document.querySelector('.banner-tabs'), []);
|
||||||
optionsWindow = controlBar.registerWindow(new OptionsWindow(typedTerm, addons, { updateTerminalSize, updateTerminalContainerBackground }));
|
optionsWindow = controlBar.registerWindow(new OptionsWindow(typedTerm, addons, { updateTerminalSize, updateTerminalContainerBackground }));
|
||||||
const styleWindow = controlBar.registerWindow(new StyleWindow(typedTerm, addons));
|
const styleWindow = controlBar.registerWindow(new StyleWindow(typedTerm, addons));
|
||||||
controlBar.registerWindow(new CellInspectorWindow(typedTerm, addons));
|
controlBar.registerWindow(new CellInspectorWindow(typedTerm, addons));
|
||||||
controlBar.registerWindow(new VtWindow(typedTerm, addons));
|
controlBar.registerWindow(new VtWindow(typedTerm, addons));
|
||||||
addonsWindow = controlBar.registerWindow(new AddonsWindow(typedTerm, addons));
|
addonsWindow = controlBar.registerWindow(new AddonsWindow(typedTerm, addons));
|
||||||
controlBar.registerWindow(new AddonImageWindow(typedTerm, addons), { afterId: 'addons', hidden: true, italics: true });
|
addonSearchWindow = controlBar.registerWindow(new AddonSearchWindow(typedTerm, addons), { afterId: 'addons', hidden: true, italics: true });
|
||||||
controlBar.registerWindow(new AddonLigaturesWindow(typedTerm, addons), { afterId: 'addon-image', hidden: true, italics: true });
|
|
||||||
controlBar.registerWindow(new AddonProgressWindow(typedTerm, addons), { afterId: 'addon-ligatures', hidden: true, italics: true });
|
|
||||||
addonSearchWindow = controlBar.registerWindow(new AddonSearchWindow(typedTerm, addons), { afterId: 'addon-progress', hidden: true, italics: true });
|
|
||||||
controlBar.registerWindow(new AddonSerializeWindow(typedTerm, addons), { afterId: 'addon-search', hidden: true, italics: true });
|
controlBar.registerWindow(new AddonSerializeWindow(typedTerm, addons), { afterId: 'addon-search', hidden: true, italics: true });
|
||||||
controlBar.registerWindow(new AddonWebFontsWindow(typedTerm, addons), { afterId: 'addon-serialize', hidden: true, italics: true });
|
controlBar.registerWindow(new AddonImageWindow(typedTerm, addons), { afterId: 'addon-serialize', hidden: true, italics: true });
|
||||||
controlBar.registerWindow(new AddonWebLinksWindow(typedTerm, addons), { afterId: 'addon-web-fonts', hidden: true, italics: true });
|
controlBar.registerWindow(new AddonWebFontsWindow(typedTerm, addons), { afterId: 'addon-image', hidden: true, italics: true });
|
||||||
addonWebglWindow = controlBar.registerWindow(new WebglWindow(typedTerm, addons), { afterId: 'addon-web-links', hidden: true, italics: true });
|
addonWebglWindow = controlBar.registerWindow(new WebglWindow(typedTerm, addons), { afterId: 'addon-web-fonts', hidden: true, italics: true });
|
||||||
controlBar.registerWindow(new TestWindow(typedTerm, addons, { disposeRecreateButtonHandler, createNewWindowButtonHandler }), { afterId: 'options' });
|
controlBar.registerWindow(new TestWindow(typedTerm, addons, { disposeRecreateButtonHandler, createNewWindowButtonHandler }), { afterId: 'options' });
|
||||||
actionElements = {
|
actionElements = {
|
||||||
findNext: addonSearchWindow.findNextInput,
|
findNext: addonSearchWindow.findNextInput,
|
||||||
@@ -235,56 +229,52 @@ if (document.location.pathname === '/test') {
|
|||||||
// TODO: Most of below should be encapsulated within windows
|
// TODO: Most of below should be encapsulated within windows
|
||||||
paddingElement = styleWindow.paddingElement;
|
paddingElement = styleWindow.paddingElement;
|
||||||
|
|
||||||
controlBar.setTabVisible('addon-image', !!addons.image.instance);
|
controlBar.setTabVisible('addon-webgl', true);
|
||||||
controlBar.setTabVisible('addon-ligatures', !!addons.ligatures.instance);
|
|
||||||
controlBar.setTabVisible('addon-progress', !!addons.progress.instance);
|
|
||||||
controlBar.setTabVisible('addon-search', true);
|
controlBar.setTabVisible('addon-search', true);
|
||||||
controlBar.setTabVisible('addon-serialize', true);
|
controlBar.setTabVisible('addon-serialize', true);
|
||||||
|
controlBar.setTabVisible('addon-image', true);
|
||||||
controlBar.setTabVisible('addon-web-fonts', true);
|
controlBar.setTabVisible('addon-web-fonts', true);
|
||||||
controlBar.setTabVisible('addon-web-links', !!addons.webLinks.instance);
|
addonWebglWindow.setTextureAtlas(addons.webgl.instance.textureAtlas);
|
||||||
controlBar.setTabVisible('addon-webgl', true);
|
addons.webgl.instance.onChangeTextureAtlas(e => addonWebglWindow.setTextureAtlas(e));
|
||||||
addonWebglWindow.setTextureAtlas(addons.webgl.instance!.textureAtlas!);
|
addons.webgl.instance.onAddTextureAtlasCanvas(e => addonWebglWindow.appendTextureAtlas(e));
|
||||||
addons.webgl.instance!.onChangeTextureAtlas(e => addonWebglWindow.setTextureAtlas(e));
|
addons.webgl.instance.onRemoveTextureAtlasCanvas(e => addonWebglWindow.removeTextureAtlas(e));
|
||||||
addons.webgl.instance!.onAddTextureAtlasCanvas(e => addonWebglWindow.appendTextureAtlas(e));
|
|
||||||
addons.webgl.instance!.onRemoveTextureAtlasCanvas(e => addonWebglWindow.removeTextureAtlas(e));
|
|
||||||
|
|
||||||
paddingElement.value = '0';
|
paddingElement.value = '0';
|
||||||
addDomListener(paddingElement, 'change', setPadding);
|
addDomListener(paddingElement, 'change', setPadding);
|
||||||
addDomListener(actionElements.findNext, 'keydown', (e) => {
|
addDomListener(actionElements.findNext, 'keydown', (e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
addons.search.instance!.findNext(actionElements.findNext.value, getSearchOptions());
|
addons.search.instance.findNext(actionElements.findNext.value, getSearchOptions());
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
addDomListener(actionElements.findNext, 'input', (e) => {
|
addDomListener(actionElements.findNext, 'input', (e) => {
|
||||||
addons.search.instance!.findNext(actionElements.findNext.value, getSearchOptions());
|
addons.search.instance.findNext(actionElements.findNext.value, getSearchOptions());
|
||||||
});
|
});
|
||||||
addDomListener(actionElements.findPrevious, 'keydown', (e) => {
|
addDomListener(actionElements.findPrevious, 'keydown', (e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
addons.search.instance!.findPrevious(actionElements.findPrevious.value, getSearchOptions());
|
addons.search.instance.findPrevious(actionElements.findPrevious.value, getSearchOptions());
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
addDomListener(actionElements.findPrevious, 'input', (e) => {
|
addDomListener(actionElements.findPrevious, 'input', (e) => {
|
||||||
addons.search.instance!.findPrevious(actionElements.findPrevious.value, getSearchOptions());
|
addons.search.instance.findPrevious(actionElements.findPrevious.value, getSearchOptions());
|
||||||
});
|
});
|
||||||
addDomListener(actionElements.findNext, 'blur', (e) => {
|
addDomListener(actionElements.findNext, 'blur', (e) => {
|
||||||
addons.search.instance!.clearActiveDecoration();
|
addons.search.instance.clearActiveDecoration();
|
||||||
});
|
});
|
||||||
addDomListener(actionElements.findPrevious, 'blur', (e) => {
|
addDomListener(actionElements.findPrevious, 'blur', (e) => {
|
||||||
addons.search.instance!.clearActiveDecoration();
|
addons.search.instance.clearActiveDecoration();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTerminal(): Terminal {
|
function createTerminal(): Terminal {
|
||||||
// Clean terminal
|
// Clean terminal
|
||||||
while (terminalContainer!.children.length) {
|
while (terminalContainer.children.length) {
|
||||||
terminalContainer!.removeChild(terminalContainer!.children[0]);
|
terminalContainer.removeChild(terminalContainer.children[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0;
|
const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0;
|
||||||
term = new Terminal({
|
term = new Terminal({
|
||||||
scrollbar: { showScrollbar: true },
|
|
||||||
allowProposedApi: true,
|
allowProposedApi: true,
|
||||||
windowsPty: isWindows ? {
|
windowsPty: isWindows ? {
|
||||||
// In a real scenario, these values should be verified on the backend
|
// In a real scenario, these values should be verified on the backend
|
||||||
@@ -292,7 +282,10 @@ function createTerminal(): Terminal {
|
|||||||
buildNumber: 22621
|
buildNumber: 22621
|
||||||
} : undefined,
|
} : undefined,
|
||||||
fontFamily: '"Fira Code", monospace, "Powerline Extra Symbols"',
|
fontFamily: '"Fira Code", monospace, "Powerline Extra Symbols"',
|
||||||
theme: { ...xtermjsTheme }
|
theme: { ...xtermjsTheme },
|
||||||
|
vtExtensions: {
|
||||||
|
win32InputMode: isWindows
|
||||||
|
}
|
||||||
} as ITerminalOptions);
|
} as ITerminalOptions);
|
||||||
|
|
||||||
// Load addons
|
// Load addons
|
||||||
@@ -300,7 +293,7 @@ function createTerminal(): Terminal {
|
|||||||
addons.search.instance = new SearchAddon();
|
addons.search.instance = new SearchAddon();
|
||||||
addons.serialize.instance = new SerializeAddon();
|
addons.serialize.instance = new SerializeAddon();
|
||||||
addons.fit.instance = new FitAddon();
|
addons.fit.instance = new FitAddon();
|
||||||
addons.image.instance = new ImageAddon!();
|
addons.image.instance = new ImageAddon();
|
||||||
addons.progress.instance = new ProgressAddon();
|
addons.progress.instance = new ProgressAddon();
|
||||||
addons.unicodeGraphemes.instance = new UnicodeGraphemesAddon();
|
addons.unicodeGraphemes.instance = new UnicodeGraphemesAddon();
|
||||||
addons.clipboard.instance = new ClipboardAddon();
|
addons.clipboard.instance = new ClipboardAddon();
|
||||||
@@ -321,7 +314,7 @@ function createTerminal(): Terminal {
|
|||||||
typedTerm.loadAddon(addons.webFonts.instance);
|
typedTerm.loadAddon(addons.webFonts.instance);
|
||||||
typedTerm.loadAddon(addons.clipboard.instance);
|
typedTerm.loadAddon(addons.clipboard.instance);
|
||||||
|
|
||||||
(window as any).term = term; // Expose `term` to window for debugging purposes
|
window.term = term; // Expose `term` to window for debugging purposes
|
||||||
term.onResize((size: { cols: number, rows: number }) => {
|
term.onResize((size: { cols: number, rows: number }) => {
|
||||||
if (!pid) {
|
if (!pid) {
|
||||||
return;
|
return;
|
||||||
@@ -340,7 +333,7 @@ function createTerminal(): Terminal {
|
|||||||
if (addons.webgl.instance) {
|
if (addons.webgl.instance) {
|
||||||
try {
|
try {
|
||||||
typedTerm.loadAddon(addons.webgl.instance);
|
typedTerm.loadAddon(addons.webgl.instance);
|
||||||
term.open(terminalContainer!);
|
term.open(terminalContainer);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('error during loading webgl addon:', e);
|
console.warn('error during loading webgl addon:', e);
|
||||||
addons.webgl.instance.dispose();
|
addons.webgl.instance.dispose();
|
||||||
@@ -349,7 +342,7 @@ function createTerminal(): Terminal {
|
|||||||
}
|
}
|
||||||
if (!typedTerm.element) {
|
if (!typedTerm.element) {
|
||||||
// webgl loading failed for some reason, attach with DOM renderer
|
// webgl loading failed for some reason, attach with DOM renderer
|
||||||
term.open(terminalContainer!);
|
term.open(terminalContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
term.focus();
|
term.focus();
|
||||||
@@ -359,13 +352,13 @@ function createTerminal(): Terminal {
|
|||||||
if (optionsWindow.autoResize) {
|
if (optionsWindow.autoResize) {
|
||||||
// In general this should be debounced to avoid excessive work on the main
|
// In general this should be debounced to avoid excessive work on the main
|
||||||
// thread by firing the expensive resize action repeatedly
|
// thread by firing the expensive resize action repeatedly
|
||||||
addons.fit.instance!.fit();
|
addons.fit.instance.fit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
resizeObserver.observe(terminalContainer!);
|
resizeObserver.observe(terminalContainer);
|
||||||
|
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
terminalContainer!.style.width = document.body.clientWidth + 'px';
|
terminalContainer.style.width = document.body.clientWidth + 'px';
|
||||||
});
|
});
|
||||||
|
|
||||||
// fit is called within a setTimeout, cols and rows need this.
|
// fit is called within a setTimeout, cols and rows need this.
|
||||||
@@ -379,7 +372,7 @@ function createTerminal(): Terminal {
|
|||||||
if (useRealTerminal instanceof HTMLInputElement && !useRealTerminal.checked) {
|
if (useRealTerminal instanceof HTMLInputElement && !useRealTerminal.checked) {
|
||||||
runFakeTerminal();
|
runFakeTerminal();
|
||||||
} else {
|
} else {
|
||||||
const res = await fetch('/terminals?cols=' + term!.cols + '&rows=' + term!.rows, { method: 'POST' });
|
const res = await fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, { method: 'POST' });
|
||||||
const processId = await res.text();
|
const processId = await res.text();
|
||||||
pid = processId;
|
pid = processId;
|
||||||
socketURL += processId;
|
socketURL += processId;
|
||||||
@@ -394,52 +387,52 @@ function createTerminal(): Terminal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function runRealTerminal(): void {
|
function runRealTerminal(): void {
|
||||||
addons.attach.instance = new AttachAddon(socket!);
|
addons.attach.instance = new AttachAddon(socket);
|
||||||
term!.loadAddon(addons.attach.instance);
|
term.loadAddon(addons.attach.instance);
|
||||||
(term as any)._initialized = true;
|
term._initialized = true;
|
||||||
initAddons(term!);
|
initAddons(term);
|
||||||
}
|
}
|
||||||
|
|
||||||
function runFakeTerminal(): void {
|
function runFakeTerminal(): void {
|
||||||
if ((term as any)._initialized) {
|
if (term._initialized) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
(term as any)._initialized = true;
|
term._initialized = true;
|
||||||
initAddons(term!);
|
initAddons(term);
|
||||||
|
|
||||||
(term as any).prompt = () => {
|
term.prompt = () => {
|
||||||
term!.write('\r\n$ ');
|
term.write('\r\n$ ');
|
||||||
};
|
};
|
||||||
|
|
||||||
term!.writeln('Welcome to xterm.js');
|
term.writeln('Welcome to xterm.js');
|
||||||
term!.writeln('This is a local terminal emulation, without a real terminal in the back-end.');
|
term.writeln('This is a local terminal emulation, without a real terminal in the back-end.');
|
||||||
term!.writeln('Type some keys and commands to play around.');
|
term.writeln('Type some keys and commands to play around.');
|
||||||
term!.writeln('');
|
term.writeln('');
|
||||||
(term as any).prompt();
|
term.prompt();
|
||||||
|
|
||||||
term!.onKey((e: { key: string, domEvent: KeyboardEvent }) => {
|
term.onKey((e: { key: string, domEvent: KeyboardEvent }) => {
|
||||||
const ev = e.domEvent;
|
const ev = e.domEvent;
|
||||||
const printable = !ev.altKey && !ev.ctrlKey && !ev.metaKey;
|
const printable = !ev.altKey && !ev.ctrlKey && !ev.metaKey;
|
||||||
|
|
||||||
if (ev.keyCode === 13) {
|
if (ev.keyCode === 13) {
|
||||||
(term as any).prompt();
|
term.prompt();
|
||||||
} else if (ev.keyCode === 8) {
|
} else if (ev.keyCode === 8) {
|
||||||
// Do not delete the prompt
|
// Do not delete the prompt
|
||||||
if ((term as any)._core.buffer.x > 2) {
|
if (term._core.buffer.x > 2) {
|
||||||
term!.write('\b \b');
|
term.write('\b \b');
|
||||||
}
|
}
|
||||||
} else if (printable) {
|
} else if (printable) {
|
||||||
term!.write(e.key);
|
term.write(e.key);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTerminalContainerBackground(): void {
|
function updateTerminalContainerBackground(): void {
|
||||||
if (term!.options.allowTransparency) {
|
if (term.options.allowTransparency) {
|
||||||
terminalContainer!.style.background = 'repeating-conic-gradient(#000000 0% 25%, #101010 0% 50%) 50% / 20px 20px';
|
terminalContainer.style.background = 'repeating-conic-gradient(#000000 0% 25%, #101010 0% 50%) 50% / 20px 20px';
|
||||||
} else {
|
} else {
|
||||||
terminalContainer!.style.background = term!.options.theme?.background ?? '#000000';
|
terminalContainer.style.background = term.options.theme?.background ?? '#000000';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,19 +442,19 @@ function initAddons(term: Terminal): void {
|
|||||||
function postInitWebgl(): void {
|
function postInitWebgl(): void {
|
||||||
controlBar.setTabVisible('addon-webgl', true);
|
controlBar.setTabVisible('addon-webgl', true);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
addonWebglWindow.setTextureAtlas(addons.webgl.instance!.textureAtlas!);
|
addonWebglWindow.setTextureAtlas(addons.webgl.instance.textureAtlas);
|
||||||
addons.webgl.instance!.onChangeTextureAtlas(e => addonWebglWindow.setTextureAtlas(e));
|
addons.webgl.instance.onChangeTextureAtlas(e => addonWebglWindow.setTextureAtlas(e));
|
||||||
addons.webgl.instance!.onAddTextureAtlasCanvas(e => addonWebglWindow.appendTextureAtlas(e));
|
addons.webgl.instance.onAddTextureAtlasCanvas(e => addonWebglWindow.appendTextureAtlas(e));
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
function preDisposeWebgl(): void {
|
function preDisposeWebgl(): void {
|
||||||
controlBar.setTabVisible('addon-webgl', false);
|
controlBar.setTabVisible('addon-webgl', false);
|
||||||
if (addons.webgl.instance!.textureAtlas) {
|
if (addons.webgl.instance.textureAtlas) {
|
||||||
addons.webgl.instance!.textureAtlas.remove();
|
addons.webgl.instance.textureAtlas.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(Object.keys(addons) as AddonType[]).forEach(name => {
|
Object.keys(addons).forEach((name: AddonType) => {
|
||||||
const addon = addons[name];
|
const addon = addons[name];
|
||||||
const checkbox = document.createElement('input') as HTMLInputElement;
|
const checkbox = document.createElement('input') as HTMLInputElement;
|
||||||
checkbox.type = 'checkbox';
|
checkbox.type = 'checkbox';
|
||||||
@@ -476,12 +469,12 @@ function initAddons(term: Terminal): void {
|
|||||||
term.unicode.activeVersion = '15-graphemes';
|
term.unicode.activeVersion = '15-graphemes';
|
||||||
}
|
}
|
||||||
if (name === 'search' && checkbox.checked) {
|
if (name === 'search' && checkbox.checked) {
|
||||||
addons[name].instance!.onDidChangeResults(e => updateFindResults(e));
|
addons[name].instance.onDidChangeResults(e => updateFindResults(e));
|
||||||
}
|
}
|
||||||
addDomListener(checkbox, 'change', () => {
|
addDomListener(checkbox, 'change', () => {
|
||||||
if (name === 'image') {
|
if (name === 'image') {
|
||||||
if (checkbox.checked) {
|
if (checkbox.checked) {
|
||||||
const ctorOptionsJson = document.querySelector<HTMLTextAreaElement>('#image-options')!.value;
|
const ctorOptionsJson = document.querySelector<HTMLTextAreaElement>('#image-options').value;
|
||||||
addon.instance = ctorOptionsJson
|
addon.instance = ctorOptionsJson
|
||||||
? new addons[name].ctor(JSON.parse(ctorOptionsJson))
|
? new addons[name].ctor(JSON.parse(ctorOptionsJson))
|
||||||
: new addons[name].ctor();
|
: new addons[name].ctor();
|
||||||
@@ -507,15 +500,9 @@ function initAddons(term: Terminal): void {
|
|||||||
term.unicode.activeVersion = '15-graphemes';
|
term.unicode.activeVersion = '15-graphemes';
|
||||||
} else if (name === 'search') {
|
} else if (name === 'search') {
|
||||||
controlBar.setTabVisible('addon-search', true);
|
controlBar.setTabVisible('addon-search', true);
|
||||||
addons[name].instance!.onDidChangeResults(e => updateFindResults(e));
|
addons[name].instance.onDidChangeResults(e => updateFindResults(e));
|
||||||
} else if (name === 'serialize') {
|
} else if (name === 'serialize') {
|
||||||
controlBar.setTabVisible('addon-serialize', true);
|
controlBar.setTabVisible('addon-serialize', true);
|
||||||
} else if (name === 'ligatures') {
|
|
||||||
controlBar.setTabVisible('addon-ligatures', true);
|
|
||||||
} else if (name === 'progress') {
|
|
||||||
controlBar.setTabVisible('addon-progress', true);
|
|
||||||
} else if (name === 'webLinks') {
|
|
||||||
controlBar.setTabVisible('addon-web-links', true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
@@ -532,12 +519,6 @@ function initAddons(term: Terminal): void {
|
|||||||
controlBar.setTabVisible('addon-search', false);
|
controlBar.setTabVisible('addon-search', false);
|
||||||
} else if (name === 'serialize') {
|
} else if (name === 'serialize') {
|
||||||
controlBar.setTabVisible('addon-serialize', false);
|
controlBar.setTabVisible('addon-serialize', false);
|
||||||
} else if (name === 'ligatures') {
|
|
||||||
controlBar.setTabVisible('addon-ligatures', false);
|
|
||||||
} else if (name === 'progress') {
|
|
||||||
controlBar.setTabVisible('addon-progress', false);
|
|
||||||
} else if (name === 'webLinks') {
|
|
||||||
controlBar.setTabVisible('addon-web-links', false);
|
|
||||||
}
|
}
|
||||||
addon.instance!.dispose();
|
addon.instance!.dispose();
|
||||||
addon.instance = undefined;
|
addon.instance = undefined;
|
||||||
@@ -609,17 +590,17 @@ function updateFindResults(e: { resultIndex: number, resultCount: number } | und
|
|||||||
|
|
||||||
function addDomListener(element: HTMLElement, type: string, handler: (...args: any[]) => any): void {
|
function addDomListener(element: HTMLElement, type: string, handler: (...args: any[]) => any): void {
|
||||||
element.addEventListener(type, handler);
|
element.addEventListener(type, handler);
|
||||||
(term as any)._core._register({ dispose: () => element.removeEventListener(type, handler) });
|
term._core._register({ dispose: () => element.removeEventListener(type, handler) });
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTerminalSize(): void {
|
function updateTerminalSize(): void {
|
||||||
const width = optionsWindow.autoResize ? '100%'
|
const width = optionsWindow.autoResize ? '100%'
|
||||||
: ((term as any).dimensions.css.canvas.width + (term as any)._core.viewport.scrollBarWidth).toString() + 'px';
|
: (term.dimensions.css.canvas.width + term._core.viewport.scrollBarWidth).toString() + 'px';
|
||||||
const height = optionsWindow.autoResize ? '100%'
|
const height = optionsWindow.autoResize ? '100%'
|
||||||
: ((term as any).dimensions.css.canvas.height).toString() + 'px';
|
: (term.dimensions.css.canvas.height).toString() + 'px';
|
||||||
terminalContainer!.style.width = width;
|
terminalContainer.style.width = width;
|
||||||
terminalContainer!.style.height = height;
|
terminalContainer.style.height = height;
|
||||||
addons.fit.instance!.fit();
|
addons.fit.instance.fit();
|
||||||
}
|
}
|
||||||
|
|
||||||
(console as any).image = (source: ImageData | HTMLCanvasElement, scale: number = 1) => {
|
(console as any).image = (source: ImageData | HTMLCanvasElement, scale: number = 1) => {
|
||||||
|
|||||||
@@ -5,15 +5,14 @@
|
|||||||
|
|
||||||
import { BaseWindow } from './baseWindow';
|
import { BaseWindow } from './baseWindow';
|
||||||
import type { IControlWindow } from '../controlBar';
|
import type { IControlWindow } from '../controlBar';
|
||||||
import type { IImageAddonOptions } from '@xterm/addon-image';
|
|
||||||
|
|
||||||
export class AddonImageWindow extends BaseWindow implements IControlWindow {
|
export class AddonImageWindow extends BaseWindow implements IControlWindow {
|
||||||
public readonly id = 'addon-image';
|
public readonly id = 'addon-image';
|
||||||
public readonly label = 'image';
|
public readonly label = 'image';
|
||||||
|
|
||||||
private _imageStorageLimitInput!: HTMLInputElement;
|
private _imageStorageLimitInput: HTMLInputElement;
|
||||||
private _imageShowPlaceholderCheckbox!: HTMLInputElement;
|
private _imageShowPlaceholderCheckbox: HTMLInputElement;
|
||||||
private _imageOptionsTextarea!: HTMLTextAreaElement;
|
private _imageOptionsTextarea: HTMLTextAreaElement;
|
||||||
|
|
||||||
public build(container: HTMLElement): void {
|
public build(container: HTMLElement): void {
|
||||||
// Storage limit
|
// Storage limit
|
||||||
@@ -47,20 +46,6 @@ export class AddonImageWindow extends BaseWindow implements IControlWindow {
|
|||||||
this._imageOptionsTextarea.rows = 12;
|
this._imageOptionsTextarea.rows = 12;
|
||||||
optionsLabel.appendChild(this._imageOptionsTextarea);
|
optionsLabel.appendChild(this._imageOptionsTextarea);
|
||||||
container.appendChild(optionsLabel);
|
container.appendChild(optionsLabel);
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
this._initImageAddonExposed();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public get imageStorageLimitInput(): HTMLInputElement {
|
public get imageStorageLimitInput(): HTMLInputElement {
|
||||||
@@ -74,93 +59,4 @@ export class AddonImageWindow extends BaseWindow implements IControlWindow {
|
|||||||
public get imageOptionsTextarea(): HTMLTextAreaElement {
|
public get imageOptionsTextarea(): HTMLTextAreaElement {
|
||||||
return this._imageOptionsTextarea;
|
return this._imageOptionsTextarea;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _addDdWithButton(dl: HTMLElement, id: string, label: string): void {
|
|
||||||
const dd = document.createElement('dd');
|
|
||||||
const button = document.createElement('button');
|
|
||||||
button.id = id;
|
|
||||||
button.textContent = label;
|
|
||||||
dd.appendChild(button);
|
|
||||||
dl.appendChild(dd);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _initImageAddonExposed(): void {
|
|
||||||
const imageAddon = this._addons.image.instance!;
|
|
||||||
const defaultOptions: IImageAddonOptions = (imageAddon as any)._defaultOpts;
|
|
||||||
const limitStorageElement = document.querySelector<HTMLInputElement>('#image-storagelimit')!;
|
|
||||||
limitStorageElement.valueAsNumber = imageAddon.storageLimit;
|
|
||||||
this._addDomListener(limitStorageElement, 'change', () => {
|
|
||||||
try {
|
|
||||||
imageAddon.storageLimit = limitStorageElement.valueAsNumber;
|
|
||||||
limitStorageElement.valueAsNumber = imageAddon.storageLimit;
|
|
||||||
console.log('changed storageLimit to', imageAddon.storageLimit);
|
|
||||||
} catch (e) {
|
|
||||||
limitStorageElement.valueAsNumber = imageAddon.storageLimit;
|
|
||||||
console.log('storageLimit at', imageAddon.storageLimit);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const showPlaceholderElement = document.querySelector<HTMLInputElement>('#image-showplaceholder')!;
|
|
||||||
showPlaceholderElement.checked = imageAddon.showPlaceholder;
|
|
||||||
this._addDomListener(showPlaceholderElement, 'change', () => {
|
|
||||||
imageAddon.showPlaceholder = showPlaceholderElement.checked;
|
|
||||||
});
|
|
||||||
const ctorOptionsElement = document.querySelector<HTMLTextAreaElement>('#image-options')!;
|
|
||||||
ctorOptionsElement.value = JSON.stringify(defaultOptions, null, 2);
|
|
||||||
|
|
||||||
const sixelDemo = (url: string) => () => fetch(url)
|
|
||||||
.then(resp => resp.arrayBuffer())
|
|
||||||
.then(buffer => {
|
|
||||||
this._terminal.write('\r\n');
|
|
||||||
this._terminal.write(new Uint8Array(buffer));
|
|
||||||
});
|
|
||||||
|
|
||||||
const iipDemo = (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]);
|
|
||||||
this._terminal.write('\r\n');
|
|
||||||
this._terminal.write(`\x1b]1337;File=inline=1;size=${data.length}:${btoa(sdata)}\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'));
|
|
||||||
|
|
||||||
// demo for image retrieval API
|
|
||||||
this._terminal.element!.addEventListener('click', (ev: MouseEvent) => {
|
|
||||||
if (!ev.ctrlKey || !imageAddon) return;
|
|
||||||
|
|
||||||
// TODO...
|
|
||||||
// if (ev.altKey) {
|
|
||||||
// const sel = term.getSelectionPosition();
|
|
||||||
// if (sel) {
|
|
||||||
// addons.image.instance
|
|
||||||
// .extractCanvasAtBufferRange(term.getSelectionPosition())
|
|
||||||
// ?.toBlob(data => window.open(URL.createObjectURL(data), '_blank'));
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
const pos = (this._terminal as any)._core._mouseService!.getCoords(ev, (this._terminal as any)._core.screenElement!, this._terminal.cols, this._terminal.rows);
|
|
||||||
const x = pos[0] - 1;
|
|
||||||
const y = pos[1] - 1;
|
|
||||||
const canvas = ev.shiftKey
|
|
||||||
// ctrl+shift+click: get single tile
|
|
||||||
? imageAddon.extractTileAtBufferCell(x, this._terminal.buffer.active.viewportY + y)
|
|
||||||
// ctrl+click: get original image
|
|
||||||
: imageAddon.getImageAtBufferCell(x, this._terminal.buffer.active.viewportY + y);
|
|
||||||
canvas?.toBlob(data => data && window.open(URL.createObjectURL(data), '_blank'));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _addDomListener(element: HTMLElement, type: string, handler: (...args: any[]) => any): void {
|
|
||||||
element.addEventListener(type, handler);
|
|
||||||
(this._terminal as any)._core._register({ dispose: () => element.removeEventListener(type, handler) });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
|
||||||
* @license MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { BaseWindow } from './baseWindow';
|
|
||||||
import type { IControlWindow } from '../controlBar';
|
|
||||||
|
|
||||||
export class AddonLigaturesWindow extends BaseWindow implements IControlWindow {
|
|
||||||
public readonly id = 'addon-ligatures';
|
|
||||||
public readonly label = 'ligatures';
|
|
||||||
|
|
||||||
public build(container: HTMLElement): void {
|
|
||||||
const dl = document.createElement('dl');
|
|
||||||
const dt = document.createElement('dt');
|
|
||||||
dt.textContent = 'Ligatures Addon';
|
|
||||||
dl.appendChild(dt);
|
|
||||||
|
|
||||||
const dd = document.createElement('dd');
|
|
||||||
const button = document.createElement('button');
|
|
||||||
button.id = 'ligatures-test';
|
|
||||||
button.textContent = 'Common ligatures';
|
|
||||||
button.title = 'Write common ligatures sequences';
|
|
||||||
button.addEventListener('click', () => this._ligaturesTest());
|
|
||||||
dd.appendChild(button);
|
|
||||||
dl.appendChild(dd);
|
|
||||||
|
|
||||||
container.appendChild(dl);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _ligaturesTest(): void {
|
|
||||||
this._terminal.write([
|
|
||||||
'',
|
|
||||||
'-<< -< -<- <-- <--- <<- <- -> ->> --> ---> ->- >- >>-',
|
|
||||||
'=<< =< =<= <== <=== <<= <= => =>> ==> ===> =>= >= >>=',
|
|
||||||
'<-> <--> <---> <----> <=> <==> <===> <====> :: ::: __',
|
|
||||||
'<~~ </ </> /> ~~> == != /= ~= <> === !== !=== =/= =!=',
|
|
||||||
'<: := *= *+ <* <*> *> <| <|> |> <. <.> .> +* =* =: :>',
|
|
||||||
'(* *) /* */ [| |] {| |} ++ +++ \/ /\ |- -| <!-- <!---',
|
|
||||||
'==== ===== ====== ======= ======== =========',
|
|
||||||
'---- ----- ------ ------- -------- ---------'
|
|
||||||
].join('\r\n'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
|
||||||
* @license MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { BaseWindow } from './baseWindow';
|
|
||||||
import type { IControlWindow } from '../controlBar';
|
|
||||||
import type { IProgressState } from '@xterm/addon-progress';
|
|
||||||
|
|
||||||
export class AddonProgressWindow extends BaseWindow implements IControlWindow {
|
|
||||||
public readonly id = 'addon-progress';
|
|
||||||
public readonly label = 'progress';
|
|
||||||
|
|
||||||
public build(container: HTMLElement): void {
|
|
||||||
const dl = document.createElement('dl');
|
|
||||||
const dt = document.createElement('dt');
|
|
||||||
dt.textContent = 'Progress Addon';
|
|
||||||
dl.appendChild(dt);
|
|
||||||
|
|
||||||
this._addDdWithButton(dl, 'progress-run', 'full set run', '');
|
|
||||||
this._addDdWithButton(dl, 'progress-0', 'state 0: remove', '');
|
|
||||||
this._addDdWithButton(dl, 'progress-1', 'state 1: set 20%', '');
|
|
||||||
this._addDdWithButton(dl, 'progress-2', 'state 2: error', '');
|
|
||||||
this._addDdWithButton(dl, 'progress-3', 'state 3: indeterminate', '');
|
|
||||||
this._addDdWithButton(dl, 'progress-4', 'state 4: pause', '');
|
|
||||||
|
|
||||||
const progressDd = document.createElement('dd');
|
|
||||||
const progressDiv = document.createElement('div');
|
|
||||||
progressDiv.id = 'progress-progress';
|
|
||||||
const progressPercent = document.createElement('div');
|
|
||||||
progressPercent.id = 'progress-percent';
|
|
||||||
const progressIndeterminate = document.createElement('div');
|
|
||||||
progressIndeterminate.id = 'progress-indeterminate';
|
|
||||||
progressDiv.appendChild(progressPercent);
|
|
||||||
progressDiv.appendChild(progressIndeterminate);
|
|
||||||
progressDd.appendChild(progressDiv);
|
|
||||||
dl.appendChild(progressDd);
|
|
||||||
|
|
||||||
const stateDd = document.createElement('dd');
|
|
||||||
const stateDiv = document.createElement('div');
|
|
||||||
stateDiv.id = 'progress-state';
|
|
||||||
stateDiv.textContent = 'State:';
|
|
||||||
stateDd.appendChild(stateDiv);
|
|
||||||
dl.appendChild(stateDd);
|
|
||||||
|
|
||||||
container.appendChild(dl);
|
|
||||||
|
|
||||||
this._initProgress();
|
|
||||||
this._addProgressStyles(container);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _addDdWithButton(dl: HTMLElement, id: string, label: string, title: string): void {
|
|
||||||
const dd = document.createElement('dd');
|
|
||||||
const button = document.createElement('button');
|
|
||||||
button.id = id;
|
|
||||||
button.textContent = label;
|
|
||||||
if (title) {
|
|
||||||
button.title = title;
|
|
||||||
}
|
|
||||||
dd.appendChild(button);
|
|
||||||
dl.appendChild(dd);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _initProgress(): void {
|
|
||||||
const states = { 0: 'remove', 1: 'set', 2: 'error', 3: 'indeterminate', 4: 'pause' };
|
|
||||||
const colors = { 0: '', 1: 'green', 2: 'red', 3: '', 4: 'yellow' };
|
|
||||||
|
|
||||||
function progressHandler({ state, value }: IProgressState): void {
|
|
||||||
// Simulate windows taskbar hack by windows terminal:
|
|
||||||
// Since the taskbar has no means to indicate error/pause state other than by coloring
|
|
||||||
// the current progress, we move 0 to 10% and distribute higher values in the remaining 90 %
|
|
||||||
// NOTE: This is most likely not what you want to do for other progress indicators,
|
|
||||||
// that have a proper visual state for error/paused.
|
|
||||||
value = Math.min(10 + value * 0.9, 100);
|
|
||||||
document.getElementById('progress-percent')!.style.width = `${value}%`;
|
|
||||||
document.getElementById('progress-percent')!.style.backgroundColor = colors[state];
|
|
||||||
document.getElementById('progress-state')!.innerText = `State: ${states[state]}`;
|
|
||||||
|
|
||||||
document.getElementById('progress-percent')!.style.display = state === 3 ? 'none' : 'block';
|
|
||||||
document.getElementById('progress-indeterminate')!.style.display = state === 3 ? 'block' : 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
const progressAddon = this._addons.progress.instance!;
|
|
||||||
progressAddon.onChange(progressHandler);
|
|
||||||
|
|
||||||
const initialProgress = progressAddon.progress;
|
|
||||||
progressHandler(initialProgress);
|
|
||||||
|
|
||||||
document.getElementById('progress-run')!.addEventListener('click', async () => {
|
|
||||||
this._terminal.write('\x1b]9;4;0\x1b\\');
|
|
||||||
for (let i = 0; i <= 100; i += 5) {
|
|
||||||
this._terminal.write(`\x1b]9;4;1;${i}\x1b\\`);
|
|
||||||
await new Promise(res => setTimeout(res, 200));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.getElementById('progress-0')!.addEventListener('click', () => this._terminal.write('\x1b]9;4;0\x1b\\'));
|
|
||||||
document.getElementById('progress-1')!.addEventListener('click', () => this._terminal.write('\x1b]9;4;1;20\x1b\\'));
|
|
||||||
document.getElementById('progress-2')!.addEventListener('click', () => this._terminal.write('\x1b]9;4;2\x1b\\'));
|
|
||||||
document.getElementById('progress-3')!.addEventListener('click', () => this._terminal.write('\x1b]9;4;3\x1b\\'));
|
|
||||||
document.getElementById('progress-4')!.addEventListener('click', () => this._terminal.write('\x1b]9;4;4\x1b\\'));
|
|
||||||
}
|
|
||||||
|
|
||||||
private _addProgressStyles(container: HTMLElement): void {
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.textContent = `
|
|
||||||
#progress-progress {
|
|
||||||
border: 1px solid black;
|
|
||||||
height: 10px;
|
|
||||||
}
|
|
||||||
#progress-percent {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
#progress-indeterminate {
|
|
||||||
display: none;
|
|
||||||
position: relative;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
#progress-indeterminate:before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
bottom: 0px;
|
|
||||||
width: 50px;
|
|
||||||
height: 10px;
|
|
||||||
background: blue;
|
|
||||||
animation: ballbns 1s ease-in-out infinite alternate;
|
|
||||||
}
|
|
||||||
@keyframes ballbns {
|
|
||||||
0% { left: 0; transform: translateX(0%); }
|
|
||||||
100% { left: 100%; transform: translateX(-100%); }
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
container.appendChild(style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,13 +10,13 @@ export class AddonSearchWindow extends BaseWindow implements IControlWindow {
|
|||||||
public readonly id = 'addon-search';
|
public readonly id = 'addon-search';
|
||||||
public readonly label = 'search';
|
public readonly label = 'search';
|
||||||
|
|
||||||
private _findNextInput!: HTMLInputElement;
|
private _findNextInput: HTMLInputElement;
|
||||||
private _findPreviousInput!: HTMLInputElement;
|
private _findPreviousInput: HTMLInputElement;
|
||||||
private _findResultsSpan!: HTMLElement;
|
private _findResultsSpan: HTMLElement;
|
||||||
private _regexCheckbox!: HTMLInputElement;
|
private _regexCheckbox: HTMLInputElement;
|
||||||
private _caseSensitiveCheckbox!: HTMLInputElement;
|
private _caseSensitiveCheckbox: HTMLInputElement;
|
||||||
private _wholeWordCheckbox!: HTMLInputElement;
|
private _wholeWordCheckbox: HTMLInputElement;
|
||||||
private _highlightAllMatchesCheckbox!: HTMLInputElement;
|
private _highlightAllMatchesCheckbox: HTMLInputElement;
|
||||||
|
|
||||||
public build(container: HTMLElement): void {
|
public build(container: HTMLElement): void {
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ export class AddonSerializeWindow extends BaseWindow implements IControlWindow {
|
|||||||
public readonly id = 'addon-serialize';
|
public readonly id = 'addon-serialize';
|
||||||
public readonly label = 'serialize';
|
public readonly label = 'serialize';
|
||||||
|
|
||||||
private _serializeOutputPre!: HTMLPreElement;
|
private _serializeOutputPre: HTMLPreElement;
|
||||||
private _htmlSerializeOutputPre!: HTMLPreElement;
|
private _htmlSerializeOutputPre: HTMLPreElement;
|
||||||
private _htmlSerializeOutputResult!: HTMLElement;
|
private _htmlSerializeOutputResult: HTMLElement;
|
||||||
private _writeToTerminalCheckbox!: HTMLInputElement;
|
private _writeToTerminalCheckbox: HTMLInputElement;
|
||||||
|
|
||||||
public build(container: HTMLElement): void {
|
public build(container: HTMLElement): void {
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
@@ -64,7 +64,7 @@ export class AddonSerializeWindow extends BaseWindow implements IControlWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _serializeButtonHandler(): void {
|
private _serializeButtonHandler(): void {
|
||||||
const output = this._addons.serialize.instance!.serialize();
|
const output = this._addons.serialize.instance.serialize();
|
||||||
const outputString = JSON.stringify(output);
|
const outputString = JSON.stringify(output);
|
||||||
|
|
||||||
this._serializeOutputPre.innerText = outputString;
|
this._serializeOutputPre.innerText = outputString;
|
||||||
@@ -75,7 +75,7 @@ export class AddonSerializeWindow extends BaseWindow implements IControlWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _htmlSerializeButtonHandler(): void {
|
private _htmlSerializeButtonHandler(): void {
|
||||||
const output = this._addons.serialize.instance!.serializeAsHTML();
|
const output = this._addons.serialize.instance.serializeAsHTML();
|
||||||
this._htmlSerializeOutputPre.innerText = output;
|
this._htmlSerializeOutputPre.innerText = output;
|
||||||
|
|
||||||
// Deprecated, but the most supported for now.
|
// Deprecated, but the most supported for now.
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
|
||||||
* @license MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { BaseWindow } from './baseWindow';
|
|
||||||
import type { IControlWindow } from '../controlBar';
|
|
||||||
|
|
||||||
export class AddonWebLinksWindow extends BaseWindow implements IControlWindow {
|
|
||||||
public readonly id = 'addon-web-links';
|
|
||||||
public readonly label = 'web-links';
|
|
||||||
|
|
||||||
public build(container: HTMLElement): void {
|
|
||||||
const dl = document.createElement('dl');
|
|
||||||
const dt = document.createElement('dt');
|
|
||||||
dt.textContent = 'Weblinks Addon';
|
|
||||||
dl.appendChild(dt);
|
|
||||||
|
|
||||||
const dd = document.createElement('dd');
|
|
||||||
const button = document.createElement('button');
|
|
||||||
button.id = 'weblinks-test';
|
|
||||||
button.textContent = 'Test URLs';
|
|
||||||
button.title = 'Various url conditions from demo data, hover&click to test';
|
|
||||||
button.addEventListener('click', () => this._testWeblinks());
|
|
||||||
dd.appendChild(button);
|
|
||||||
dl.appendChild(dd);
|
|
||||||
|
|
||||||
container.appendChild(dl);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _testWeblinks(): void {
|
|
||||||
const linkExamples = `
|
|
||||||
aaa http://example.com aaa http://example.com aaa
|
|
||||||
\uFFE5\uFFE5\uFFE5 http://example.com aaa http://example.com aaa
|
|
||||||
aaa http://example.com \uFFE5\uFFE5\uFFE5 http://example.com aaa
|
|
||||||
\uFFE5\uFFE5\uFFE5 http://example.com \uFFE5\uFFE5\uFFE5 http://example.com aaa
|
|
||||||
aaa https://ko.wikipedia.org/wiki/\uC704\uD0A4\uBC31\uACFC:\uB300\uBB38 aaa https://ko.wikipedia.org/wiki/\uC704\uD0A4\uBC31\uACFC:\uB300\uBB38 aaa
|
|
||||||
\uFFE5\uFFE5\uFFE5 https://ko.wikipedia.org/wiki/\uC704\uD0A4\uBC31\uACFC:\uB300\uBB38 aaa https://ko.wikipedia.org/wiki/\uC704\uD0A4\uBC31\uACFC:\uB300\uBB38 \uFFE5\uFFE5\uFFE5
|
|
||||||
aaa http://test:password@example.com/some_path aaa
|
|
||||||
brackets enclosed:
|
|
||||||
aaa [http://example.de] aaa
|
|
||||||
aaa (http://example.de) aaa
|
|
||||||
aaa <http://example.de> aaa
|
|
||||||
aaa {http://example.de} aaa
|
|
||||||
ipv6 https://[::1]/with/some?vars=and&a#hash aaa
|
|
||||||
stop at final '.': This is a sentence with an url to http://example.com.
|
|
||||||
stop at final '?': Is this the right url http://example.com/?
|
|
||||||
stop at final '?': Maybe this one http://example.com/with?arguments=false?
|
|
||||||
`;
|
|
||||||
this._terminal.write(linkExamples.split('\n').join('\r\n'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,7 @@ export class AddonsWindow extends BaseWindow implements IControlWindow {
|
|||||||
public readonly id = 'addons';
|
public readonly id = 'addons';
|
||||||
public readonly label = 'Addons';
|
public readonly label = 'Addons';
|
||||||
|
|
||||||
private _addonsContainer!: HTMLElement;
|
private _addonsContainer: HTMLElement;
|
||||||
|
|
||||||
public build(container: HTMLElement): void {
|
public build(container: HTMLElement): void {
|
||||||
// Description
|
// Description
|
||||||
|
|||||||
@@ -30,14 +30,14 @@ export class CellInspectorWindow extends BaseWindow implements IControlWindow {
|
|||||||
public readonly id = 'cell-inspector';
|
public readonly id = 'cell-inspector';
|
||||||
public readonly label = 'Cell Inspector';
|
public readonly label = 'Cell Inspector';
|
||||||
|
|
||||||
private _container!: HTMLElement;
|
private _container: HTMLElement;
|
||||||
private _positionEl!: HTMLElement;
|
private _positionEl: HTMLElement;
|
||||||
private _charEl!: HTMLElement;
|
private _charEl: HTMLElement;
|
||||||
private _codeEl!: HTMLElement;
|
private _codeEl: HTMLElement;
|
||||||
private _widthEl!: HTMLElement;
|
private _widthEl: HTMLElement;
|
||||||
private _fgEl!: HTMLElement;
|
private _fgEl: HTMLElement;
|
||||||
private _bgEl!: HTMLElement;
|
private _bgEl: HTMLElement;
|
||||||
private _attrsEl!: HTMLElement;
|
private _attrsEl: HTMLElement;
|
||||||
|
|
||||||
public build(container: HTMLElement): void {
|
public build(container: HTMLElement): void {
|
||||||
this._container = container;
|
this._container = container;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export class GpuWindow extends BaseWindow implements IControlWindow {
|
|||||||
public readonly id = 'gpu';
|
public readonly id = 'gpu';
|
||||||
public readonly label = 'WebGL';
|
public readonly label = 'WebGL';
|
||||||
|
|
||||||
private _textureAtlasContainer!: HTMLElement;
|
private _textureAtlasContainer: HTMLElement;
|
||||||
|
|
||||||
public build(container: HTMLElement): void {
|
public build(container: HTMLElement): void {
|
||||||
const zoomCheckbox = document.createElement('input');
|
const zoomCheckbox = document.createElement('input');
|
||||||
|
|||||||
@@ -80,8 +80,8 @@ export class OptionsWindow extends BaseWindow implements IControlWindow {
|
|||||||
public readonly id = 'options';
|
public readonly id = 'options';
|
||||||
public readonly label = 'Options';
|
public readonly label = 'Options';
|
||||||
|
|
||||||
private _container!: HTMLElement;
|
private _container: HTMLElement;
|
||||||
private _optionsContainer!: HTMLElement;
|
private _optionsContainer: HTMLElement;
|
||||||
private _autoResize: boolean = true;
|
private _autoResize: boolean = true;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -109,26 +109,24 @@ export class OptionsWindow extends BaseWindow implements IControlWindow {
|
|||||||
|
|
||||||
public initOptions(addDomListener: (el: HTMLElement, type: string, handler: (...args: any[]) => any) => void): void {
|
public initOptions(addDomListener: (el: HTMLElement, type: string, handler: (...args: any[]) => any) => void): void {
|
||||||
const blacklistedOptions = [
|
const blacklistedOptions = [
|
||||||
|
'cancelEvents',
|
||||||
'convertEol',
|
'convertEol',
|
||||||
'termName',
|
'termName',
|
||||||
'cols', 'rows',
|
'cols', 'rows',
|
||||||
'documentOverride',
|
'documentOverride',
|
||||||
'linkHandler',
|
'linkHandler',
|
||||||
'logger',
|
'logger',
|
||||||
|
'overviewRuler',
|
||||||
'quirks',
|
'quirks',
|
||||||
'theme',
|
'theme',
|
||||||
'vtExtensions',
|
'vtExtensions',
|
||||||
'windowOptions',
|
'windowOptions',
|
||||||
'windowsPty',
|
'windowsPty',
|
||||||
];
|
];
|
||||||
const nestedBooleanOptions: { label: string, path: string[], prop: string }[] = [
|
const nestedBooleanOptions: { label: string, parent: string, prop: string }[] = [
|
||||||
{ label: 'scrollbar.showScrollbar', path: ['scrollbar'], prop: 'showScrollbar' },
|
{ label: 'vtExtensions.kittyKeyboard', parent: 'vtExtensions', prop: 'kittyKeyboard' },
|
||||||
{ label: 'scrollbar.showArrows', path: ['scrollbar'], prop: 'showArrows' },
|
{ label: 'vtExtensions.kittySgrBoldFaintControl', parent: 'vtExtensions', prop: 'kittySgrBoldFaintControl' },
|
||||||
{ label: 'scrollbar.overviewRuler.showTopBorder', path: ['scrollbar', 'overviewRuler'], prop: 'showTopBorder' },
|
{ label: 'vtExtensions.win32InputMode', parent: 'vtExtensions', prop: 'win32InputMode' }
|
||||||
{ label: 'scrollbar.overviewRuler.showBottomBorder', path: ['scrollbar', 'overviewRuler'], prop: 'showBottomBorder' },
|
|
||||||
{ label: 'vtExtensions.kittyKeyboard', path: ['vtExtensions'], prop: 'kittyKeyboard' },
|
|
||||||
{ label: 'vtExtensions.kittySgrBoldFaintControl', path: ['vtExtensions'], prop: 'kittySgrBoldFaintControl' },
|
|
||||||
{ label: 'vtExtensions.win32InputMode', path: ['vtExtensions'], prop: 'win32InputMode' }
|
|
||||||
];
|
];
|
||||||
const stringOptions: { [key: string]: string[] | null } = {
|
const stringOptions: { [key: string]: string[] | null } = {
|
||||||
cursorStyle: ['block', 'underline', 'bar'],
|
cursorStyle: ['block', 'underline', 'bar'],
|
||||||
@@ -145,7 +143,7 @@ export class OptionsWindow extends BaseWindow implements IControlWindow {
|
|||||||
const booleanOptions: string[] = [];
|
const booleanOptions: string[] = [];
|
||||||
const numberOptions: string[] = [];
|
const numberOptions: string[] = [];
|
||||||
options.filter(o => blacklistedOptions.indexOf(o) === -1).forEach(o => {
|
options.filter(o => blacklistedOptions.indexOf(o) === -1).forEach(o => {
|
||||||
switch (typeof (this._terminal.options as Record<string, unknown>)[o]) {
|
switch (typeof this._terminal.options[o]) {
|
||||||
case 'boolean':
|
case 'boolean':
|
||||||
booleanOptions.push(o);
|
booleanOptions.push(o);
|
||||||
break;
|
break;
|
||||||
@@ -162,27 +160,25 @@ export class OptionsWindow extends BaseWindow implements IControlWindow {
|
|||||||
let html = '';
|
let html = '';
|
||||||
html += '<div class="option-group">';
|
html += '<div class="option-group">';
|
||||||
booleanOptions.forEach(o => {
|
booleanOptions.forEach(o => {
|
||||||
html += `<div class="option"><label><input id="opt-${o}" type="checkbox" ${(this._terminal.options as Record<string, unknown>)[o] ? 'checked' : ''}/> ${o}</label></div>`;
|
html += `<div class="option"><label><input id="opt-${o}" type="checkbox" ${this._terminal.options[o] ? 'checked' : ''}/> ${o}</label></div>`;
|
||||||
});
|
});
|
||||||
nestedBooleanOptions.forEach(({ label, path, prop }) => {
|
nestedBooleanOptions.forEach(({ label, parent, prop }) => {
|
||||||
const options = this._terminal.options as Record<string, unknown>;
|
const checked = this._terminal.options[parent]?.[prop] ?? false;
|
||||||
const parent = path.reduce<Record<string, unknown> | undefined>((acc, key) => (acc as Record<string, unknown> | undefined)?.[key] as Record<string, unknown> | undefined, options);
|
|
||||||
const checked = (parent as Record<string, unknown> | undefined)?.[prop] ?? false;
|
|
||||||
html += `<div class="option"><label><input id="opt-${label.replace('.', '-')}" type="checkbox" ${checked ? 'checked' : ''}/> ${label}</label></div>`;
|
html += `<div class="option"><label><input id="opt-${label.replace('.', '-')}" type="checkbox" ${checked ? 'checked' : ''}/> ${label}</label></div>`;
|
||||||
});
|
});
|
||||||
html += '</div><div class="option-group">';
|
html += '</div><div class="option-group">';
|
||||||
numberOptions.forEach(o => {
|
numberOptions.forEach(o => {
|
||||||
html += `<div class="option"><label>${o} <input id="opt-${o}" type="number" value="${(this._terminal.options as Record<string, unknown>)[o] ?? ''}" step="${o === 'lineHeight' || o === 'scrollSensitivity' ? '0.1' : '1'}"/></label></div>`;
|
html += `<div class="option"><label>${o} <input id="opt-${o}" type="number" value="${this._terminal.options[o] ?? ''}" step="${o === 'lineHeight' || o === 'scrollSensitivity' ? '0.1' : '1'}"/></label></div>`;
|
||||||
});
|
});
|
||||||
html += '</div><div class="option-group">';
|
html += '</div><div class="option-group">';
|
||||||
Object.keys(stringOptions).forEach(o => {
|
Object.keys(stringOptions).forEach(o => {
|
||||||
if (o === 'colsRows') {
|
if (o === 'colsRows') {
|
||||||
html += `<div class="option"><label>size (<var>cols</var><code>x</code><var>rows</var> or <code>auto</code>) <input id="opt-${o}" type="text" value="auto"/></label></div>`;
|
html += `<div class="option"><label>size (<var>cols</var><code>x</code><var>rows</var> or <code>auto</code>) <input id="opt-${o}" type="text" value="auto"/></label></div>`;
|
||||||
} else if (stringOptions[o]) {
|
} else if (stringOptions[o]) {
|
||||||
const selectedOption = o === 'theme' ? 'xtermjs' : (this._terminal.options as Record<string, unknown>)[o];
|
const selectedOption = o === 'theme' ? 'xtermjs' : this._terminal.options[o];
|
||||||
html += `<div class="option"><label>${o} <select id="opt-${o}">${stringOptions[o]!.map(v => `<option ${v === selectedOption ? 'selected' : ''}>${v}</option>`).join('')}</select></label></div>`;
|
html += `<div class="option"><label>${o} <select id="opt-${o}">${stringOptions[o]!.map(v => `<option ${v === selectedOption ? 'selected' : ''}>${v}</option>`).join('')}</select></label></div>`;
|
||||||
} else {
|
} else {
|
||||||
html += `<div class="option"><label>${o} <input id="opt-${o}" type="text" value="${(this._terminal.options as Record<string, unknown>)[o]}"/></label></div>`;
|
html += `<div class="option"><label>${o} <input id="opt-${o}" type="text" value="${this._terminal.options[o]}"/></label></div>`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
@@ -194,29 +190,18 @@ export class OptionsWindow extends BaseWindow implements IControlWindow {
|
|||||||
const input = document.getElementById(`opt-${o}`) as HTMLInputElement;
|
const input = document.getElementById(`opt-${o}`) as HTMLInputElement;
|
||||||
addDomListener(input, 'change', () => {
|
addDomListener(input, 'change', () => {
|
||||||
console.log('change', o, input.checked);
|
console.log('change', o, input.checked);
|
||||||
(this._terminal.options as Record<string, unknown>)[o] = input.checked;
|
this._terminal.options[o] = input.checked;
|
||||||
if (o ==='allowTransparency') {
|
if (o ==='allowTransparency') {
|
||||||
this._terminal.options.theme = this._getTheme();
|
this._terminal.options.theme = this._getTheme();
|
||||||
this._handlers.updateTerminalContainerBackground();
|
this._handlers.updateTerminalContainerBackground();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
nestedBooleanOptions.forEach(({ label, path, prop }) => {
|
nestedBooleanOptions.forEach(({ label, parent, prop }) => {
|
||||||
const input = document.getElementById(`opt-${label.replace('.', '-')}`) as HTMLInputElement;
|
const input = document.getElementById(`opt-${label.replace('.', '-')}`) as HTMLInputElement;
|
||||||
addDomListener(input, 'change', () => {
|
addDomListener(input, 'change', () => {
|
||||||
console.log('change', label, input.checked);
|
console.log('change', label, input.checked);
|
||||||
const options = this._terminal.options as Record<string, unknown>;
|
this._terminal.options[parent] = { ...this._terminal.options[parent], [prop]: input.checked };
|
||||||
if (path.length === 1) {
|
|
||||||
const parentKey = path[0];
|
|
||||||
options[parentKey] = { ...(options[parentKey] as Record<string, unknown> | undefined), [prop]: input.checked };
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (path.length === 2) {
|
|
||||||
const [parentKey, childKey] = path;
|
|
||||||
const parent = (options[parentKey] as Record<string, unknown> | undefined) ?? {};
|
|
||||||
const child = (parent[childKey] as Record<string, unknown> | undefined) ?? {};
|
|
||||||
options[parentKey] = { ...parent, [childKey]: { ...child, [prop]: input.checked } };
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
numberOptions.forEach(o => {
|
numberOptions.forEach(o => {
|
||||||
@@ -231,7 +216,7 @@ export class OptionsWindow extends BaseWindow implements IControlWindow {
|
|||||||
this._terminal.options.scrollback = parseInt(input.value);
|
this._terminal.options.scrollback = parseInt(input.value);
|
||||||
setTimeout(() => this._handlers.updateTerminalSize(), 5);
|
setTimeout(() => this._handlers.updateTerminalSize(), 5);
|
||||||
} else {
|
} else {
|
||||||
(this._terminal.options as Record<string, unknown>)[o] = parseInt(input.value);
|
this._terminal.options[o] = parseInt(input.value);
|
||||||
}
|
}
|
||||||
this._handlers.updateTerminalSize();
|
this._handlers.updateTerminalSize();
|
||||||
});
|
});
|
||||||
@@ -240,7 +225,7 @@ export class OptionsWindow extends BaseWindow implements IControlWindow {
|
|||||||
const input = document.getElementById(`opt-${o}`) as HTMLInputElement;
|
const input = document.getElementById(`opt-${o}`) as HTMLInputElement;
|
||||||
addDomListener(input, 'change', () => {
|
addDomListener(input, 'change', () => {
|
||||||
console.log('change', o, input.value);
|
console.log('change', o, input.value);
|
||||||
let value: unknown = input.value;
|
let value: any = input.value;
|
||||||
if (o === 'colsRows') {
|
if (o === 'colsRows') {
|
||||||
const m = input.value.match(/^([0-9]+)x([0-9]+)$/);
|
const m = input.value.match(/^([0-9]+)x([0-9]+)$/);
|
||||||
if (m) {
|
if (m) {
|
||||||
@@ -254,7 +239,7 @@ export class OptionsWindow extends BaseWindow implements IControlWindow {
|
|||||||
} else if (o === 'theme') {
|
} else if (o === 'theme') {
|
||||||
value = this._getTheme();
|
value = this._getTheme();
|
||||||
}
|
}
|
||||||
(this._terminal.options as Record<string, unknown>)[o] = value;
|
this._terminal.options[o] = value;
|
||||||
if (o === 'theme') {
|
if (o === 'theme') {
|
||||||
this._handlers.updateTerminalContainerBackground();
|
this._handlers.updateTerminalContainerBackground();
|
||||||
}
|
}
|
||||||
@@ -274,11 +259,11 @@ export class OptionsWindow extends BaseWindow implements IControlWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _getTheme(): ITheme {
|
private _getTheme(): ITheme {
|
||||||
const input = document.querySelector<HTMLInputElement>('#opt-theme')!;
|
const input = document.querySelector<HTMLInputElement>('#opt-theme');
|
||||||
let theme: ITheme = {};
|
let theme: ITheme;
|
||||||
switch (input.value) {
|
switch (input.value) {
|
||||||
case 'default':
|
case 'default':
|
||||||
theme = {};
|
theme = undefined;
|
||||||
break;
|
break;
|
||||||
case 'xtermjs':
|
case 'xtermjs':
|
||||||
theme = { ...xtermjsTheme };
|
theme = { ...xtermjsTheme };
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export class StyleWindow extends BaseWindow implements IControlWindow {
|
|||||||
public readonly id = 'style';
|
public readonly id = 'style';
|
||||||
public readonly label = 'Style';
|
public readonly label = 'Style';
|
||||||
|
|
||||||
private _paddingElement!: HTMLInputElement;
|
private _paddingElement: HTMLInputElement;
|
||||||
|
|
||||||
public build(container: HTMLElement): void {
|
public build(container: HTMLElement): void {
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import type { IControlWindow } from '../controlBar';
|
|||||||
import { BaseWindow } from './baseWindow';
|
import { BaseWindow } from './baseWindow';
|
||||||
import type { IDisposable, Terminal } from '@xterm/xterm';
|
import type { IDisposable, Terminal } from '@xterm/xterm';
|
||||||
import type { AddonCollection } from 'types';
|
import type { AddonCollection } from 'types';
|
||||||
|
import type { IProgressState } from '@xterm/addon-progress';
|
||||||
|
import type { IImageAddonOptions } from '@xterm/addon-image';
|
||||||
|
|
||||||
export class TestWindow extends BaseWindow implements IControlWindow {
|
export class TestWindow extends BaseWindow implements IControlWindow {
|
||||||
public readonly id = 'test';
|
public readonly id = 'test';
|
||||||
@@ -64,13 +66,61 @@ export class TestWindow extends BaseWindow implements IControlWindow {
|
|||||||
this._addDdWithButton(dl, 'add-overview-ruler', 'Add Overview Ruler', 'Add an overview ruler to the terminal', () => addOverviewRuler(this._terminal));
|
this._addDdWithButton(dl, 'add-overview-ruler', 'Add Overview Ruler', 'Add an overview ruler to the terminal', () => addOverviewRuler(this._terminal));
|
||||||
this._addDdWithButton(dl, 'decoration-stress-test', 'Stress Test', 'Toggle between adding and removing a decoration to each line', () => decorationStressTest(this._terminal));
|
this._addDdWithButton(dl, 'decoration-stress-test', 'Stress Test', 'Toggle between adding and removing a decoration to each line', () => decorationStressTest(this._terminal));
|
||||||
|
|
||||||
|
// Ligatures Addon section
|
||||||
|
this._addDt(dl, 'Ligatures Addon');
|
||||||
|
this._addDdWithButton(dl, 'ligatures-test', 'Common ligatures', 'Write common ligatures sequences', () => ligaturesTest(this._terminal));
|
||||||
|
|
||||||
|
// Weblinks Addon section
|
||||||
|
this._addDt(dl, 'Weblinks Addon');
|
||||||
|
this._addDdWithButton(dl, 'weblinks-test', 'Test URLs', 'Various url conditions from demo data, hover&click to test', () => testWeblinks(this._terminal));
|
||||||
|
|
||||||
|
// Image Test section
|
||||||
|
this._addDt(dl, 'Image Test');
|
||||||
|
this._addDdWithButton(dl, 'image-demo1', 'snake (sixel)', '');
|
||||||
|
this._addDdWithButton(dl, 'image-demo2', 'oranges (sixel)', '');
|
||||||
|
this._addDdWithButton(dl, 'image-demo3', 'palette (iip)', '');
|
||||||
|
|
||||||
// Events Test section
|
// Events Test section
|
||||||
this._addDt(dl, 'Events Test');
|
this._addDt(dl, 'Events Test');
|
||||||
this._addDdWithButton(dl, 'event-focus', 'focus', '', () => this._terminal.focus());
|
this._addDdWithButton(dl, 'event-focus', 'focus', '', () => this._terminal.focus());
|
||||||
this._addDdWithButton(dl, 'event-blur', 'blur', '', () => this._terminal.blur());
|
this._addDdWithButton(dl, 'event-blur', 'blur', '', () => this._terminal.blur());
|
||||||
|
|
||||||
|
// Progress Addon section
|
||||||
|
this._addDt(dl, 'Progress Addon');
|
||||||
|
this._addDdWithButton(dl, 'progress-run', 'full set run', '');
|
||||||
|
this._addDdWithButton(dl, 'progress-0', 'state 0: remove', '');
|
||||||
|
this._addDdWithButton(dl, 'progress-1', 'state 1: set 20%', '');
|
||||||
|
this._addDdWithButton(dl, 'progress-2', 'state 2: error', '');
|
||||||
|
this._addDdWithButton(dl, 'progress-3', 'state 3: indeterminate', '');
|
||||||
|
this._addDdWithButton(dl, 'progress-4', 'state 4: pause', '');
|
||||||
|
|
||||||
|
// Progress bar
|
||||||
|
const progressDd = document.createElement('dd');
|
||||||
|
const progressDiv = document.createElement('div');
|
||||||
|
progressDiv.id = 'progress-progress';
|
||||||
|
const progressPercent = document.createElement('div');
|
||||||
|
progressPercent.id = 'progress-percent';
|
||||||
|
const progressIndeterminate = document.createElement('div');
|
||||||
|
progressIndeterminate.id = 'progress-indeterminate';
|
||||||
|
progressDiv.appendChild(progressPercent);
|
||||||
|
progressDiv.appendChild(progressIndeterminate);
|
||||||
|
progressDd.appendChild(progressDiv);
|
||||||
|
dl.appendChild(progressDd);
|
||||||
|
|
||||||
|
// Progress state
|
||||||
|
const stateDd = document.createElement('dd');
|
||||||
|
const stateDiv = document.createElement('div');
|
||||||
|
stateDiv.id = 'progress-state';
|
||||||
|
stateDiv.textContent = 'State:';
|
||||||
|
stateDd.appendChild(stateDiv);
|
||||||
|
dl.appendChild(stateDd);
|
||||||
|
|
||||||
wrapper.appendChild(dl);
|
wrapper.appendChild(dl);
|
||||||
container.appendChild(wrapper);
|
container.appendChild(wrapper);
|
||||||
|
|
||||||
|
initProgress(this._terminal, this._addons);
|
||||||
|
this._addProgressStyles(container);
|
||||||
|
initImageAddonExposed(this._terminal, this._addons);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _addDt(dl: HTMLElement, text: string): void {
|
private _addDt(dl: HTMLElement, text: string): void {
|
||||||
@@ -89,9 +139,7 @@ export class TestWindow extends BaseWindow implements IControlWindow {
|
|||||||
}
|
}
|
||||||
dd.appendChild(button);
|
dd.appendChild(button);
|
||||||
dl.appendChild(dd);
|
dl.appendChild(dd);
|
||||||
if (handler) {
|
button.addEventListener('click', handler);
|
||||||
button.addEventListener('click', handler);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _addDdWithCheckbox(dl: HTMLElement, id: string, label: string, title: string, checked: boolean): void {
|
private _addDdWithCheckbox(dl: HTMLElement, id: string, label: string, title: string, checked: boolean): void {
|
||||||
@@ -111,6 +159,38 @@ export class TestWindow extends BaseWindow implements IControlWindow {
|
|||||||
dl.appendChild(dd);
|
dl.appendChild(dd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _addProgressStyles(container: HTMLElement): void {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
#progress-progress {
|
||||||
|
border: 1px solid black;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
#progress-percent {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
#progress-indeterminate {
|
||||||
|
display: none;
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
#progress-indeterminate:before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0px;
|
||||||
|
width: 50px;
|
||||||
|
height: 10px;
|
||||||
|
background: blue;
|
||||||
|
animation: ballbns 1s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
@keyframes ballbns {
|
||||||
|
0% { left: 0; transform: translateX(0%); }
|
||||||
|
100% { left: 100%; transform: translateX(-100%); }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
container.appendChild(style);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -385,7 +465,7 @@ function customGlyphAlignmentHandler(term: Terminal): void {
|
|||||||
while (fillChars.length > 0) {
|
while (fillChars.length > 0) {
|
||||||
const batch = fillChars.splice(0, 10);
|
const batch = fillChars.splice(0, 10);
|
||||||
for (const fillChar of batch) {
|
for (const fillChar of batch) {
|
||||||
term.write(`${fillChar.codePointAt(0)!.toString(16).toUpperCase().padEnd(5, ' ')} `);
|
term.write(`${fillChar.codePointAt(0).toString(16).toUpperCase().padEnd(5, ' ')} `);
|
||||||
}
|
}
|
||||||
term.write('\n\r');
|
term.write('\n\r');
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 3; i++) {
|
||||||
@@ -397,13 +477,6 @@ function customGlyphAlignmentHandler(term: Terminal): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
term.write('\x1b[0mTriangular fill tests:\x1b[36m\n\r');
|
|
||||||
term.write('1FB9C 1FB9D 1FB9E 1FB9F all\n\r');
|
|
||||||
term.write('\u{02592}\u{02592}\u{02592}\u{1FB9C} \u{1FB9D}\u{02592}\u{02592}\u{02592} \u{00020}\u{00020}\u{00020}\u{1FB9E} \u{1FB9F}\u{00020}\u{00020}\u{00020} \u{00020}\u{1FB9E}\u{1FB9F}\u{00020}\n\r');
|
|
||||||
term.write('\u{02592}\u{02592}\u{1FB9C}\u{00020} \u{00020}\u{1FB9D}\u{02592}\u{02592} \u{00020}\u{00020}\u{1FB9E}\u{02592} \u{02592}\u{1FB9F}\u{00020}\u{00020} \u{1FB9E}\u{02592}\u{02592}\u{1FB9F}\n\r');
|
|
||||||
term.write('\u{02592}\u{1FB9C}\u{00020}\u{00020} \u{00020}\u{00020}\u{1FB9D}\u{02592} \u{00020}\u{1FB9E}\u{02592}\u{02592} \u{02592}\u{02592}\u{1FB9F}\u{00020} \u{1FB9D}\u{02592}\u{02592}\u{1FB9C}\n\r');
|
|
||||||
term.write('\u{1FB9C}\u{00020}\u{00020}\u{00020} \u{00020}\u{00020}\u{00020}\u{1FB9D} \u{1FB9E}\u{02592}\u{02592}\u{02592} \u{02592}\u{02592}\u{02592}\u{1FB9F} \u{00020}\u{1FB9D}\u{1FB9C}\u{00020}\n\r');
|
|
||||||
|
|
||||||
term.write('\x1b[0mPowerline alignment tests:\n\r');
|
term.write('\x1b[0mPowerline alignment tests:\n\r');
|
||||||
const powerlineLeftChars = ['\u{E0B2}', '\u{E0B3}', '\u{E0B6}', '\u{E0B7}', '\u{E0BA}', '\u{E0BB}', '\u{E0BE}', '\u{E0BF}', '\u{E0C2}', '\u{E0C3}', '\u{E0C5}', '\u{E0C7}', '\u{E0CA}', '\u{E0D4}'];
|
const powerlineLeftChars = ['\u{E0B2}', '\u{E0B3}', '\u{E0B6}', '\u{E0B7}', '\u{E0BA}', '\u{E0BB}', '\u{E0BE}', '\u{E0BF}', '\u{E0C2}', '\u{E0C3}', '\u{E0C5}', '\u{E0C7}', '\u{E0CA}', '\u{E0D4}'];
|
||||||
const powerlineRightChars = ['\u{E0B0}', '\u{E0B1}', '\u{E0B4}', '\u{E0B5}', '\u{E0B8}', '\u{E0B9}', '\u{E0BC}', '\u{E0BD}', '\u{E0C0}', '\u{E0C1}', '\u{E0C4}', '\u{E0C6}', '\u{E0C8}', '\u{E0D2}', '\u{E0CC}', '\u{E0CD}', '\u{E0CE}', '\u{E0CF}', '\u{E0D0}', '\u{E0D1}'];
|
const powerlineRightChars = ['\u{E0B0}', '\u{E0B1}', '\u{E0B4}', '\u{E0B5}', '\u{E0B8}', '\u{E0B9}', '\u{E0BC}', '\u{E0BD}', '\u{E0C0}', '\u{E0C1}', '\u{E0C4}', '\u{E0C6}', '\u{E0C8}', '\u{E0D2}', '\u{E0CC}', '\u{E0CD}', '\u{E0CE}', '\u{E0CF}', '\u{E0D0}', '\u{E0D1}'];
|
||||||
@@ -678,6 +751,42 @@ Test BG-colored Erase (BCE):
|
|||||||
term.write(data.split('\n').join('\r\n'));
|
term.write(data.split('\n').join('\r\n'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ligaturesTest(term: Terminal): void {
|
||||||
|
term.write([
|
||||||
|
'',
|
||||||
|
'-<< -< -<- <-- <--- <<- <- -> ->> --> ---> ->- >- >>-',
|
||||||
|
'=<< =< =<= <== <=== <<= <= => =>> ==> ===> =>= >= >>=',
|
||||||
|
'<-> <--> <---> <----> <=> <==> <===> <====> :: ::: __',
|
||||||
|
'<~~ </ </> /> ~~> == != /= ~= <> === !== !=== =/= =!=',
|
||||||
|
'<: := *= *+ <* <*> *> <| <|> |> <. <.> .> +* =* =: :>',
|
||||||
|
'(* *) /* */ [| |] {| |} ++ +++ \/ /\ |- -| <!-- <!---',
|
||||||
|
'==== ===== ====== ======= ======== =========',
|
||||||
|
'---- ----- ------ ------- -------- ---------'
|
||||||
|
].join('\r\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function testWeblinks(term: Terminal): void {
|
||||||
|
const linkExamples = `
|
||||||
|
aaa http://example.com aaa http://example.com aaa
|
||||||
|
¥¥¥ http://example.com aaa http://example.com aaa
|
||||||
|
aaa http://example.com ¥¥¥ http://example.com aaa
|
||||||
|
¥¥¥ http://example.com ¥¥¥ http://example.com aaa
|
||||||
|
aaa https://ko.wikipedia.org/wiki/위키백과:대문 aaa https://ko.wikipedia.org/wiki/위키백과:대문 aaa
|
||||||
|
¥¥¥ https://ko.wikipedia.org/wiki/위키백과:대문 aaa https://ko.wikipedia.org/wiki/위키백과:대문 ¥¥¥
|
||||||
|
aaa http://test:password@example.com/some_path aaa
|
||||||
|
brackets enclosed:
|
||||||
|
aaa [http://example.de] aaa
|
||||||
|
aaa (http://example.de) aaa
|
||||||
|
aaa <http://example.de> aaa
|
||||||
|
aaa {http://example.de} aaa
|
||||||
|
ipv6 https://[::1]/with/some?vars=and&a#hash aaa
|
||||||
|
stop at final '.': This is a sentence with an url to http://example.com.
|
||||||
|
stop at final '?': Is this the right url http://example.com/?
|
||||||
|
stop at final '?': Maybe this one http://example.com/with?arguments=false?
|
||||||
|
`;
|
||||||
|
term.write(linkExamples.split('\n').join('\r\n'));
|
||||||
|
}
|
||||||
|
|
||||||
function loadTest(term: Terminal, addons: AddonCollection): void {
|
function loadTest(term: Terminal, addons: AddonCollection): void {
|
||||||
const rendererName = addons.webgl.instance ? 'webgl' : 'dom';
|
const rendererName = addons.webgl.instance ? 'webgl' : 'dom';
|
||||||
const testData = [];
|
const testData = [];
|
||||||
@@ -745,7 +854,7 @@ function loadTestLongLines(term: Terminal, addons: AddonCollection): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addDecoration(term: Terminal, dim: number = 1): void {
|
function addDecoration(term: Terminal, dim: number = 1): void {
|
||||||
term.options.scrollbar = { ...(term.options.scrollbar ?? {}), width: 14, overviewRuler: term.options.scrollbar?.overviewRuler ?? {} };
|
term.options['overviewRuler'] = { width: 14 };
|
||||||
const marker = term.registerMarker(1);
|
const marker = term.registerMarker(1);
|
||||||
const decoration = term.registerDecoration({
|
const decoration = term.registerDecoration({
|
||||||
marker,
|
marker,
|
||||||
@@ -755,14 +864,14 @@ function addDecoration(term: Terminal, dim: number = 1): void {
|
|||||||
foregroundColor: '#00FE00',
|
foregroundColor: '#00FE00',
|
||||||
overviewRulerOptions: { color: '#ef292980', position: 'left' }
|
overviewRulerOptions: { color: '#ef292980', position: 'left' }
|
||||||
});
|
});
|
||||||
decoration?.onRender((e: HTMLElement) => {
|
decoration.onRender((e: HTMLElement) => {
|
||||||
e.style.right = '100%';
|
e.style.right = '100%';
|
||||||
e.style.backgroundColor = '#ef292980';
|
e.style.backgroundColor = '#ef292980';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addOverviewRuler(term: Terminal): void {
|
function addOverviewRuler(term: Terminal): void {
|
||||||
term.options.scrollbar = { ...(term.options.scrollbar ?? {}), width: 14, overviewRuler: term.options.scrollbar?.overviewRuler ?? {} };
|
term.options['overviewRuler'] = { width: 14 };
|
||||||
term.registerDecoration({ marker: term.registerMarker(1), overviewRulerOptions: { color: '#ef2929' } });
|
term.registerDecoration({ marker: term.registerMarker(1), overviewRulerOptions: { color: '#ef2929' } });
|
||||||
term.registerDecoration({ marker: term.registerMarker(3), overviewRulerOptions: { color: '#8ae234' } });
|
term.registerDecoration({ marker: term.registerMarker(3), overviewRulerOptions: { color: '#8ae234' } });
|
||||||
term.registerDecoration({ marker: term.registerMarker(5), overviewRulerOptions: { color: '#729fcf' } });
|
term.registerDecoration({ marker: term.registerMarker(5), overviewRulerOptions: { color: '#729fcf' } });
|
||||||
@@ -787,17 +896,133 @@ function decorationStressTest(term: Terminal): void {
|
|||||||
for (const x of [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]) {
|
for (const x of [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]) {
|
||||||
for (let y = 0; y < term.buffer.active.length; y++) {
|
for (let y = 0; y < term.buffer.active.length; y++) {
|
||||||
const cursorOffsetY = y - cursorY;
|
const cursorOffsetY = y - cursorY;
|
||||||
const decoration = term.registerDecoration({
|
decorationStressTestDecorations.push(term.registerDecoration({
|
||||||
marker: term.registerMarker(cursorOffsetY),
|
marker: term.registerMarker(cursorOffsetY),
|
||||||
x,
|
x,
|
||||||
width: 4,
|
width: 4,
|
||||||
backgroundColor: '#FF0000',
|
backgroundColor: '#FF0000',
|
||||||
overviewRulerOptions: { color: '#FF0000' }
|
overviewRulerOptions: { color: '#FF0000' }
|
||||||
});
|
}));
|
||||||
if (decoration) {
|
|
||||||
decorationStressTestDecorations.push(decoration);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initProgress(term: Terminal, addons: AddonCollection): void {
|
||||||
|
const STATES = { 0: 'remove', 1: 'set', 2: 'error', 3: 'indeterminate', 4: 'pause' };
|
||||||
|
const COLORS = { 0: '', 1: 'green', 2: 'red', 3: '', 4: 'yellow' };
|
||||||
|
|
||||||
|
function progressHandler({ state, value }: IProgressState): void {
|
||||||
|
// Simulate windows taskbar hack by windows terminal:
|
||||||
|
// Since the taskbar has no means to indicate error/pause state other than by coloring
|
||||||
|
// the current progress, we move 0 to 10% and distribute higher values in the remaining 90 %
|
||||||
|
// NOTE: This is most likely not what you want to do for other progress indicators,
|
||||||
|
// that have a proper visual state for error/paused.
|
||||||
|
value = Math.min(10 + value * 0.9, 100);
|
||||||
|
document.getElementById('progress-percent').style.width = `${value}%`;
|
||||||
|
document.getElementById('progress-percent').style.backgroundColor = COLORS[state];
|
||||||
|
document.getElementById('progress-state').innerText = `State: ${STATES[state]}`;
|
||||||
|
|
||||||
|
document.getElementById('progress-percent').style.display = state === 3 ? 'none' : 'block';
|
||||||
|
document.getElementById('progress-indeterminate').style.display = state === 3 ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
const progressAddon = addons.progress.instance;
|
||||||
|
progressAddon.onChange(progressHandler);
|
||||||
|
|
||||||
|
// apply initial state once to make it visible on page load
|
||||||
|
const initialProgress = progressAddon.progress;
|
||||||
|
progressHandler(initialProgress);
|
||||||
|
|
||||||
|
document.getElementById('progress-run').addEventListener('click', async () => {
|
||||||
|
term.write('\x1b]9;4;0\x1b\\');
|
||||||
|
for (let i = 0; i <= 100; i += 5) {
|
||||||
|
term.write(`\x1b]9;4;1;${i}\x1b\\`);
|
||||||
|
await new Promise(res => setTimeout(res, 200));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.getElementById('progress-0').addEventListener('click', () => term.write('\x1b]9;4;0\x1b\\'));
|
||||||
|
document.getElementById('progress-1').addEventListener('click', () => term.write('\x1b]9;4;1;20\x1b\\'));
|
||||||
|
document.getElementById('progress-2').addEventListener('click', () => term.write('\x1b]9;4;2\x1b\\'));
|
||||||
|
document.getElementById('progress-3').addEventListener('click', () => term.write('\x1b]9;4;3\x1b\\'));
|
||||||
|
document.getElementById('progress-4').addEventListener('click', () => term.write('\x1b]9;4;4\x1b\\'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function initImageAddonExposed(term: Terminal, addons: AddonCollection): void {
|
||||||
|
const DEFAULT_OPTIONS: IImageAddonOptions = (addons.image.instance as any)._defaultOpts;
|
||||||
|
const limitStorageElement = document.querySelector<HTMLInputElement>('#image-storagelimit');
|
||||||
|
limitStorageElement.valueAsNumber = addons.image.instance.storageLimit;
|
||||||
|
addDomListener(term, limitStorageElement, 'change', () => {
|
||||||
|
try {
|
||||||
|
addons.image.instance.storageLimit = limitStorageElement.valueAsNumber;
|
||||||
|
limitStorageElement.valueAsNumber = addons.image.instance.storageLimit;
|
||||||
|
console.log('changed storageLimit to', addons.image.instance.storageLimit);
|
||||||
|
} catch (e) {
|
||||||
|
limitStorageElement.valueAsNumber = addons.image.instance.storageLimit;
|
||||||
|
console.log('storageLimit at', addons.image.instance.storageLimit);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const showPlaceholderElement = document.querySelector<HTMLInputElement>('#image-showplaceholder');
|
||||||
|
showPlaceholderElement.checked = addons.image.instance.showPlaceholder;
|
||||||
|
addDomListener(term, showPlaceholderElement, 'change', () => {
|
||||||
|
addons.image.instance.showPlaceholder = showPlaceholderElement.checked;
|
||||||
|
});
|
||||||
|
const ctorOptionsElement = document.querySelector<HTMLTextAreaElement>('#image-options');
|
||||||
|
ctorOptionsElement.value = JSON.stringify(DEFAULT_OPTIONS, null, 2);
|
||||||
|
|
||||||
|
const sixelDemo = (url: string) => () => fetch(url)
|
||||||
|
.then(resp => resp.arrayBuffer())
|
||||||
|
.then(buffer => {
|
||||||
|
term.write('\r\n');
|
||||||
|
term.write(new Uint8Array(buffer));
|
||||||
|
});
|
||||||
|
|
||||||
|
const iipDemo = (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]);
|
||||||
|
term.write('\r\n');
|
||||||
|
term.write(`\x1b]1337;File=inline=1;size=${data.length}:${btoa(sdata)}\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'));
|
||||||
|
|
||||||
|
// demo for image retrieval API
|
||||||
|
term.element.addEventListener('click', (ev: MouseEvent) => {
|
||||||
|
if (!ev.ctrlKey || !addons.image.instance) return;
|
||||||
|
|
||||||
|
// TODO...
|
||||||
|
// if (ev.altKey) {
|
||||||
|
// const sel = term.getSelectionPosition();
|
||||||
|
// if (sel) {
|
||||||
|
// addons.image.instance
|
||||||
|
// .extractCanvasAtBufferRange(term.getSelectionPosition())
|
||||||
|
// ?.toBlob(data => window.open(URL.createObjectURL(data), '_blank'));
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
const pos = (term as any)._core._mouseService!.getCoords(ev, (term as any)._core.screenElement!, term.cols, term.rows);
|
||||||
|
const x = pos[0] - 1;
|
||||||
|
const y = pos[1] - 1;
|
||||||
|
const canvas = ev.shiftKey
|
||||||
|
// ctrl+shift+click: get single tile
|
||||||
|
? addons.image.instance.extractTileAtBufferCell(x, term.buffer.active.viewportY + y)
|
||||||
|
// ctrl+click: get original image
|
||||||
|
: addons.image.instance.getImageAtBufferCell(x, term.buffer.active.viewportY + y);
|
||||||
|
canvas?.toBlob(data => window.open(URL.createObjectURL(data), '_blank'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addDomListener(term: Terminal, element: HTMLElement, type: string, handler: (...args: any[]) => any): void {
|
||||||
|
element.addEventListener(type, handler);
|
||||||
|
(term as any)._core._register({ dispose: () => element.removeEventListener(type, handler) });
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,12 +5,14 @@
|
|||||||
|
|
||||||
import { BaseWindow } from './baseWindow';
|
import { BaseWindow } from './baseWindow';
|
||||||
import type { IControlWindow } from '../controlBar';
|
import type { IControlWindow } from '../controlBar';
|
||||||
|
import type { Terminal } from '@xterm/xterm';
|
||||||
|
|
||||||
export class VtWindow extends BaseWindow implements IControlWindow {
|
export class VtWindow extends BaseWindow implements IControlWindow {
|
||||||
public readonly id = 'vt';
|
public readonly id = 'vt';
|
||||||
public readonly label = 'VT';
|
public readonly label = 'VT';
|
||||||
|
|
||||||
private _container!: HTMLElement;
|
private _container: HTMLElement;
|
||||||
|
private _term: Terminal | undefined;
|
||||||
|
|
||||||
public build(container: HTMLElement): void {
|
public build(container: HTMLElement): void {
|
||||||
this._container = container;
|
this._container = container;
|
||||||
@@ -63,7 +65,7 @@ export class VtWindow extends BaseWindow implements IControlWindow {
|
|||||||
const writeCsiSplit = writeCsi.split('|');
|
const writeCsiSplit = writeCsi.split('|');
|
||||||
const prefix = writeCsiSplit.length === 2 ? writeCsiSplit[0] : '';
|
const prefix = writeCsiSplit.length === 2 ? writeCsiSplit[0] : '';
|
||||||
const suffix = writeCsiSplit[writeCsiSplit.length - 1];
|
const suffix = writeCsiSplit[writeCsiSplit.length - 1];
|
||||||
element.addEventListener('click', () => this._terminal.write(this._csi(`${prefix}${inputs.map(e => e.value).join(';')}${suffix}`)));
|
element.addEventListener('click', () => this._term?.write(this._csi(`${prefix}${inputs.map(e => e.value).join(';')}${suffix}`)));
|
||||||
|
|
||||||
const desc = document.createElement('span');
|
const desc = document.createElement('span');
|
||||||
desc.textContent = description;
|
desc.textContent = description;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export class WebglWindow extends BaseWindow implements IControlWindow {
|
|||||||
public readonly id = 'addon-webgl';
|
public readonly id = 'addon-webgl';
|
||||||
public readonly label = 'webgl';
|
public readonly label = 'webgl';
|
||||||
|
|
||||||
private _textureAtlasContainer!: HTMLElement;
|
private _textureAtlasContainer: HTMLElement;
|
||||||
|
|
||||||
public build(container: HTMLElement): void {
|
public build(container: HTMLElement): void {
|
||||||
const zoomCheckbox = document.createElement('input');
|
const zoomCheckbox = document.createElement('input');
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"strict": true,
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@xterm/addon-attach": ["../../addons/addon-attach"],
|
"@xterm/addon-attach": ["../../addons/addon-attach"],
|
||||||
"@xterm/addon-clipboard": ["../../addons/addon-clipboard"],
|
"@xterm/addon-clipboard": ["../../addons/addon-clipboard"],
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ body {
|
|||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
outline-offset: -1px;
|
|
||||||
}
|
}
|
||||||
.banner-tabs button:hover {
|
.banner-tabs button:hover {
|
||||||
background: rgba(255,255,255,0.1);
|
background: rgba(255,255,255,0.1);
|
||||||
|
|||||||
+1
-1
@@ -23,6 +23,6 @@
|
|||||||
<div id="sidebar" class="grid">
|
<div id="sidebar" class="grid">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="dist/client-bundle.js" defer ></script>
|
<script src="dist/client-bundle.mjs" type="module" defer ></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ function startServer(): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.use('/fonts', express.static(demoRoot + '/fonts'));
|
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('/dist', express.static(demoRoot + '/dist'));
|
||||||
app.use('/src', express.static(demoRoot + '/src'));
|
app.use('/src', express.static(demoRoot + '/src'));
|
||||||
|
|||||||
+1
-1
@@ -18,6 +18,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body id="test">
|
<body id="test">
|
||||||
<div id="terminal-container"></div>
|
<div id="terminal-container"></div>
|
||||||
<script src="/dist/client-bundle.js" defer></script>
|
<script src="/dist/client-bundle.mjs" type="module" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+1
-9
@@ -10,6 +10,7 @@ export default tseslint.config(
|
|||||||
{
|
{
|
||||||
ignores: [
|
ignores: [
|
||||||
'addons/*/src/third-party/*.ts',
|
'addons/*/src/third-party/*.ts',
|
||||||
|
'src/vs/*',
|
||||||
'**/out/*',
|
'**/out/*',
|
||||||
'**/out-test/*',
|
'**/out-test/*',
|
||||||
'**/out-esbuild/*',
|
'**/out-esbuild/*',
|
||||||
@@ -34,7 +35,6 @@ export default tseslint.config(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
'@stylistic/comma-spacing': ['warn', { before: false, after: true }],
|
|
||||||
'@stylistic/indent': ['warn', 2],
|
'@stylistic/indent': ['warn', 2],
|
||||||
'@stylistic/semi': ['warn', 'always'],
|
'@stylistic/semi': ['warn', 'always'],
|
||||||
'@stylistic/quotes': ['warn', 'single', { allowTemplateLiterals: true }],
|
'@stylistic/quotes': ['warn', 'single', { allowTemplateLiterals: true }],
|
||||||
@@ -66,7 +66,6 @@ export default tseslint.config(
|
|||||||
],
|
],
|
||||||
'@typescript-eslint/no-confusing-void-expression': ['warn', { ignoreArrowShorthand: true }],
|
'@typescript-eslint/no-confusing-void-expression': ['warn', { ignoreArrowShorthand: true }],
|
||||||
'@typescript-eslint/no-useless-constructor': 'warn',
|
'@typescript-eslint/no-useless-constructor': 'warn',
|
||||||
'@typescript-eslint/prefer-nullish-coalescing': ['warn', { ignorePrimitives: true }],
|
|
||||||
'@typescript-eslint/prefer-namespace-keyword': 'warn',
|
'@typescript-eslint/prefer-namespace-keyword': 'warn',
|
||||||
'@typescript-eslint/no-unused-vars': ['warn', { vars: 'all', args: 'none' }],
|
'@typescript-eslint/no-unused-vars': ['warn', { vars: 'all', args: 'none' }],
|
||||||
'@typescript-eslint/no-require-imports': 'off',
|
'@typescript-eslint/no-require-imports': 'off',
|
||||||
@@ -141,12 +140,5 @@ export default tseslint.config(
|
|||||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
'@typescript-eslint/explicit-member-accessibility': 'off'
|
'@typescript-eslint/explicit-member-accessibility': 'off'
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
// Disable prefer-nullish-coalescing for files without strictNullChecks
|
|
||||||
files: ['demo/**/*.ts', '**/*.benchmark.ts'],
|
|
||||||
rules: {
|
|
||||||
'@typescript-eslint/prefer-nullish-coalescing': 'off'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@xterm/headless",
|
"name": "@xterm/headless",
|
||||||
"description": "A headless terminal component that runs in Node.js",
|
"description": "A headless terminal component that runs in Node.js",
|
||||||
"version": "6.0.0",
|
"version": "5.5.0",
|
||||||
"main": "lib-headless/xterm-headless.js",
|
"main": "lib-headless/xterm-headless.js",
|
||||||
"module": "lib-headless/xterm-headless.mjs",
|
"module": "lib-headless/xterm-headless.mjs",
|
||||||
"types": "typings/xterm-headless.d.ts",
|
"types": "typings/xterm-headless.d.ts",
|
||||||
|
|||||||
Generated
+4
-5
@@ -41,7 +41,7 @@
|
|||||||
"jsdom": "^27.3.0",
|
"jsdom": "^27.3.0",
|
||||||
"mocha": "^10.1.0",
|
"mocha": "^10.1.0",
|
||||||
"mustache": "^4.2.0",
|
"mustache": "^4.2.0",
|
||||||
"node-pty": "^1.2.0-beta.9",
|
"node-pty": "1.1.0-beta19",
|
||||||
"nyc": "^17.1.0",
|
"nyc": "^17.1.0",
|
||||||
"source-map-loader": "^3.0.0",
|
"source-map-loader": "^3.0.0",
|
||||||
"source-map-support": "^0.5.20",
|
"source-map-support": "^0.5.20",
|
||||||
@@ -6016,12 +6016,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/node-pty": {
|
"node_modules/node-pty": {
|
||||||
"version": "1.2.0-beta.9",
|
"version": "1.1.0-beta19",
|
||||||
"resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.2.0-beta.9.tgz",
|
"resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0-beta19.tgz",
|
||||||
"integrity": "sha512-XR3x/4OQZ7DlceespOQ99kk9jZHwmO78RuEIZtGsSPdsDi11s79VcK04aqcaK3x09Y6ORJM/KBz/oQ/p+WGRLg==",
|
"integrity": "sha512-/p4Zu56EYDdXjjaLWzrIlFyrBnND11LQGP0/L6GEVGURfCNkAlHc3Twg/2I4NPxghimHXgvDlwp7Z2GtvDIh8A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"node-addon-api": "^7.1.0"
|
"node-addon-api": "^7.1.0"
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-6
@@ -45,20 +45,18 @@
|
|||||||
"esbuild-demo-client-watch": "node bin/esbuild.mjs --demo-client --watch",
|
"esbuild-demo-client-watch": "node bin/esbuild.mjs --demo-client --watch",
|
||||||
"esbuild-demo-server": "node bin/esbuild.mjs --demo-server",
|
"esbuild-demo-server": "node bin/esbuild.mjs --demo-server",
|
||||||
"esbuild-demo-server-watch": "node bin/esbuild.mjs --demo-server --watch",
|
"esbuild-demo-server-watch": "node bin/esbuild.mjs --demo-server --watch",
|
||||||
"ghostty-wasm": "node bin/build_ghostty_wasm.mjs",
|
|
||||||
"test": "npm run test-unit",
|
"test": "npm run test-unit",
|
||||||
"posttest": "npm run lint",
|
"posttest": "npm run lint",
|
||||||
"lint": "eslint --max-warnings 0 src/ addons/ demo/ test/",
|
"lint": "eslint --max-warnings 0 src/ addons/ demo/",
|
||||||
"lint-changes": "node ./bin/lint_changes.js",
|
"lint-changes": "node ./bin/lint_changes.js",
|
||||||
"lint-changes-fix": "node ./bin/lint_changes.js --fix",
|
"lint-changes-fix": "node ./bin/lint_changes.js --fix",
|
||||||
"lint-fix": "eslint --fix src/ addons/ demo/ test/",
|
"lint-fix": "eslint --fix src/ addons/ demo/",
|
||||||
"lint-api": "eslint --config eslint.config.typings.mjs --max-warnings 0 typings/",
|
"lint-api": "eslint --config eslint.config.typings.mjs --max-warnings 0 typings/",
|
||||||
"test-unit": "node ./bin/test_unit.js",
|
"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-coverage": "node ./bin/test_unit.js --coverage",
|
||||||
"test-unit-dev": "cross-env NODE_PATH='./out' mocha",
|
"test-unit-dev": "cross-env NODE_PATH='./out' mocha",
|
||||||
"test-integration": "node ./bin/test_integration.js --workers=75%",
|
"test-integration": "node ./bin/test_integration.js --workers=75%",
|
||||||
"test-integration-chromium": "node ./bin/test_integration.js --workers=75% \"--project=Chromium\"",
|
"test-integration-chromium": "node ./bin/test_integration.js --workers=75% \"--project=ChromeStable\"",
|
||||||
"test-integration-firefox": "node ./bin/test_integration.js --workers=75% \"--project=FirefoxStable\"",
|
"test-integration-firefox": "node ./bin/test_integration.js --workers=75% \"--project=FirefoxStable\"",
|
||||||
"test-integration-webkit": "node ./bin/test_integration.js --workers=75% \"--project=WebKit\"",
|
"test-integration-webkit": "node ./bin/test_integration.js --workers=75% \"--project=WebKit\"",
|
||||||
"test-integration-debug": "node ./bin/test_integration.js --workers=1 --headed --timeout=30000",
|
"test-integration-debug": "node ./bin/test_integration.js --workers=1 --headed --timeout=30000",
|
||||||
@@ -105,7 +103,7 @@
|
|||||||
"jsdom": "^27.3.0",
|
"jsdom": "^27.3.0",
|
||||||
"mocha": "^10.1.0",
|
"mocha": "^10.1.0",
|
||||||
"mustache": "^4.2.0",
|
"mustache": "^4.2.0",
|
||||||
"node-pty": "^1.2.0-beta.9",
|
"node-pty": "1.1.0-beta19",
|
||||||
"nyc": "^17.1.0",
|
"nyc": "^17.1.0",
|
||||||
"source-map-loader": "^3.0.0",
|
"source-map-loader": "^3.0.0",
|
||||||
"source-map-support": "^0.5.20",
|
"source-map-support": "^0.5.20",
|
||||||
|
|||||||
@@ -6,11 +6,11 @@
|
|||||||
import * as Strings from 'browser/LocalizableStrings';
|
import * as Strings from 'browser/LocalizableStrings';
|
||||||
import { ITerminal, IRenderDebouncer } from 'browser/Types';
|
import { ITerminal, IRenderDebouncer } from 'browser/Types';
|
||||||
import { TimeBasedDebouncer } from 'browser/TimeBasedDebouncer';
|
import { TimeBasedDebouncer } from 'browser/TimeBasedDebouncer';
|
||||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||||
import { ICoreBrowserService, IRenderService } from 'browser/services/Services';
|
import { ICoreBrowserService, IRenderService } from 'browser/services/Services';
|
||||||
import { IBuffer } from 'common/buffer/Types';
|
import { IBuffer } from 'common/buffer/Types';
|
||||||
import { IInstantiationService } from 'common/services/Services';
|
import { IInstantiationService } from 'common/services/Services';
|
||||||
import { addDisposableListener } from 'browser/Dom';
|
import { addDisposableListener } from 'vs/base/browser/dom';
|
||||||
|
|
||||||
const MAX_ROWS_TO_READ = 20;
|
const MAX_ROWS_TO_READ = 20;
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { OscLinkProvider } from 'browser/OscLinkProvider';
|
|||||||
import { CharacterJoinerHandler, CustomKeyEventHandler, CustomWheelEventHandler, IBrowser, IBufferRange, ICompositionHelper, ILinkifier2, ITerminal } from 'browser/Types';
|
import { CharacterJoinerHandler, CustomKeyEventHandler, CustomWheelEventHandler, IBrowser, IBufferRange, ICompositionHelper, ILinkifier2, ITerminal } from 'browser/Types';
|
||||||
import { Viewport } from 'browser/Viewport';
|
import { Viewport } from 'browser/Viewport';
|
||||||
import { BufferDecorationRenderer } from 'browser/decorations/BufferDecorationRenderer';
|
import { BufferDecorationRenderer } from 'browser/decorations/BufferDecorationRenderer';
|
||||||
import { OverviewRulerRenderer } from 'browser/decorations/OverviewRulerRenderer';
|
import type { OverviewRulerRenderer } from 'browser/decorations/OverviewRulerRenderer';
|
||||||
import { CompositionHelper } from 'browser/input/CompositionHelper';
|
import { CompositionHelper } from 'browser/input/CompositionHelper';
|
||||||
import { DomRenderer } from 'browser/renderer/dom/DomRenderer';
|
import { DomRenderer } from 'browser/renderer/dom/DomRenderer';
|
||||||
import { IRenderer } from 'browser/renderer/shared/Types';
|
import { IRenderer } from 'browser/renderer/shared/Types';
|
||||||
@@ -42,22 +42,44 @@ 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, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
|
||||||
import { ThemeService } from 'browser/services/ThemeService';
|
import { ThemeService } from 'browser/services/ThemeService';
|
||||||
import { KeyboardService } from 'browser/services/KeyboardService';
|
import { KeyboardService } from 'browser/services/KeyboardService';
|
||||||
import { channels, color, rgb } from 'common/Color';
|
import { channels, color } from 'common/Color';
|
||||||
import { CoreTerminal } from 'common/CoreTerminal';
|
import { CoreTerminal } from 'common/CoreTerminal';
|
||||||
import * as Browser from 'common/Platform';
|
import * as Browser from 'common/Platform';
|
||||||
import { ColorRequestType, CoreMouseAction, CoreMouseButton, CoreMouseEventType, IColorEvent, ITerminalOptions, KeyboardResultType, SpecialColorIndex } from 'common/Types';
|
import { ColorRequestType, CoreMouseAction, CoreMouseButton, CoreMouseEventType, IColorEvent, ITerminalOptions, KeyboardResultType, SpecialColorIndex } from 'common/Types';
|
||||||
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
|
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
|
||||||
import { IBuffer } from 'common/buffer/Types';
|
import { IBuffer } from 'common/buffer/Types';
|
||||||
import { C0, C1ESCAPED } from 'common/data/EscapeSequences';
|
import { C0, C1_ESCAPED } from 'common/data/EscapeSequences';
|
||||||
import { toRgbString } from 'common/input/XParseColor';
|
import { toRgbString } from 'common/input/XParseColor';
|
||||||
import { DecorationService } from 'common/services/DecorationService';
|
import { DecorationService } from 'common/services/DecorationService';
|
||||||
import { IDecorationService } from 'common/services/Services';
|
import { IDecorationService } from 'common/services/Services';
|
||||||
import { WindowsOptionsReportType } from '../common/InputHandler';
|
import { WindowsOptionsReportType } from '../common/InputHandler';
|
||||||
import { AccessibilityManager } from './AccessibilityManager';
|
import type { AccessibilityManager } from './AccessibilityManager';
|
||||||
import { Linkifier } from './Linkifier';
|
import { Linkifier } from './Linkifier';
|
||||||
import { Emitter, EventUtils, type IEvent } from 'common/Event';
|
import { Emitter, Event } from 'vs/base/common/event';
|
||||||
import { addDisposableListener } from 'browser/Dom';
|
import { addDisposableListener } from 'vs/base/browser/dom';
|
||||||
import { MutableDisposable, toDisposable } from 'common/Lifecycle';
|
import { MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazily loaded AccessibilityManager module.
|
||||||
|
*/
|
||||||
|
let accessibilityManagerModule: Promise<typeof import('./AccessibilityManager')> | undefined;
|
||||||
|
function loadAccessibilityManager(): Promise<typeof import('./AccessibilityManager')> {
|
||||||
|
if (!accessibilityManagerModule) {
|
||||||
|
accessibilityManagerModule = import('./AccessibilityManager');
|
||||||
|
}
|
||||||
|
return accessibilityManagerModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazily loaded OverviewRulerRenderer module.
|
||||||
|
*/
|
||||||
|
let overviewRulerRendererModule: Promise<typeof import('browser/decorations/OverviewRulerRenderer')> | undefined;
|
||||||
|
function loadOverviewRulerRenderer(): Promise<typeof import('browser/decorations/OverviewRulerRenderer')> {
|
||||||
|
if (!overviewRulerRendererModule) {
|
||||||
|
overviewRulerRendererModule = import('browser/decorations/OverviewRulerRenderer');
|
||||||
|
}
|
||||||
|
return overviewRulerRendererModule;
|
||||||
|
}
|
||||||
|
|
||||||
export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||||
public textarea: HTMLTextAreaElement | undefined;
|
public textarea: HTMLTextAreaElement | undefined;
|
||||||
@@ -135,15 +157,15 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
|||||||
public readonly onBell = this._onBell.event;
|
public readonly onBell = this._onBell.event;
|
||||||
|
|
||||||
private _onFocus = this._register(new Emitter<void>());
|
private _onFocus = this._register(new Emitter<void>());
|
||||||
public get onFocus(): IEvent<void> { return this._onFocus.event; }
|
public get onFocus(): Event<void> { return this._onFocus.event; }
|
||||||
private _onBlur = this._register(new Emitter<void>());
|
private _onBlur = this._register(new Emitter<void>());
|
||||||
public get onBlur(): IEvent<void> { return this._onBlur.event; }
|
public get onBlur(): Event<void> { return this._onBlur.event; }
|
||||||
private _onA11yCharEmitter = this._register(new Emitter<string>());
|
private _onA11yCharEmitter = this._register(new Emitter<string>());
|
||||||
public get onA11yChar(): IEvent<string> { return this._onA11yCharEmitter.event; }
|
public get onA11yChar(): Event<string> { return this._onA11yCharEmitter.event; }
|
||||||
private _onA11yTabEmitter = this._register(new Emitter<number>());
|
private _onA11yTabEmitter = this._register(new Emitter<number>());
|
||||||
public get onA11yTab(): IEvent<number> { return this._onA11yTabEmitter.event; }
|
public get onA11yTab(): Event<number> { return this._onA11yTabEmitter.event; }
|
||||||
private _onWillOpen = this._register(new Emitter<HTMLElement>());
|
private _onWillOpen = this._register(new Emitter<HTMLElement>());
|
||||||
public get onWillOpen(): IEvent<HTMLElement> { return this._onWillOpen.event; }
|
public get onWillOpen(): Event<HTMLElement> { return this._onWillOpen.event; }
|
||||||
private readonly _onDimensionsChange = this._register(new Emitter<IRenderDimensionsApi>());
|
private readonly _onDimensionsChange = this._register(new Emitter<IRenderDimensionsApi>());
|
||||||
public readonly onDimensionsChange = this._onDimensionsChange.event;
|
public readonly onDimensionsChange = this._onDimensionsChange.event;
|
||||||
|
|
||||||
@@ -187,10 +209,10 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
|||||||
this._register(this._inputHandler.onRequestReset(() => this.reset()));
|
this._register(this._inputHandler.onRequestReset(() => this.reset()));
|
||||||
this._register(this._inputHandler.onRequestWindowsOptionsReport(type => this._reportWindowsOptions(type)));
|
this._register(this._inputHandler.onRequestWindowsOptionsReport(type => this._reportWindowsOptions(type)));
|
||||||
this._register(this._inputHandler.onColor((event) => this._handleColorEvent(event)));
|
this._register(this._inputHandler.onColor((event) => this._handleColorEvent(event)));
|
||||||
this._register(EventUtils.forward(this._inputHandler.onCursorMove, this._onCursorMove));
|
this._register(Event.forward(this._inputHandler.onCursorMove, this._onCursorMove));
|
||||||
this._register(EventUtils.forward(this._inputHandler.onTitleChange, this._onTitleChange));
|
this._register(Event.forward(this._inputHandler.onTitleChange, this._onTitleChange));
|
||||||
this._register(EventUtils.forward(this._inputHandler.onA11yChar, this._onA11yCharEmitter));
|
this._register(Event.forward(this._inputHandler.onA11yChar, this._onA11yCharEmitter));
|
||||||
this._register(EventUtils.forward(this._inputHandler.onA11yTab, this._onA11yTabEmitter));
|
this._register(Event.forward(this._inputHandler.onA11yTab, this._onA11yTabEmitter));
|
||||||
|
|
||||||
// Setup listeners
|
// Setup listeners
|
||||||
this._register(this._bufferService.onResize(e => this._afterResize(e.cols, e.rows)));
|
this._register(this._bufferService.onResize(e => this._afterResize(e.cols, e.rows)));
|
||||||
@@ -235,7 +257,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
|||||||
const colorRgb = color.toColorRGB(acc === 'ansi'
|
const colorRgb = color.toColorRGB(acc === 'ansi'
|
||||||
? this._themeService.colors.ansi[req.index]
|
? this._themeService.colors.ansi[req.index]
|
||||||
: this._themeService.colors[acc]);
|
: this._themeService.colors[acc]);
|
||||||
this.coreService.triggerDataEvent(`${C0.ESC}]${ident};${toRgbString(colorRgb)}${C1ESCAPED.ST}`);
|
this.coreService.triggerDataEvent(`${C0.ESC}]${ident};${toRgbString(colorRgb)}${C1_ESCAPED.ST}`);
|
||||||
break;
|
break;
|
||||||
case ColorRequestType.SET:
|
case ColorRequestType.SET:
|
||||||
if (acc === 'ansi') {
|
if (acc === 'ansi') {
|
||||||
@@ -252,20 +274,6 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Reports the current color scheme (dark or light) based on the relative luminance
|
|
||||||
* of the background and foreground theme colors.
|
|
||||||
* Sends CSI ? 997 ; 1 n for dark mode or CSI ? 997 ; 2 n for light mode.
|
|
||||||
*/
|
|
||||||
private _reportColorScheme(): void {
|
|
||||||
if (!this._themeService) return;
|
|
||||||
const bgLuminance = rgb.relativeLuminance(this._themeService.colors.background.rgba >> 8);
|
|
||||||
const fgLuminance = rgb.relativeLuminance(this._themeService.colors.foreground.rgba >> 8);
|
|
||||||
// Dark mode = background is darker than foreground (lower luminance)
|
|
||||||
const colorSchemeMode = bgLuminance < fgLuminance ? 1 : 2;
|
|
||||||
this.coreService.triggerDataEvent(`${C0.ESC}[?997;${colorSchemeMode}n`);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _setup(): void {
|
protected _setup(): void {
|
||||||
super._setup();
|
super._setup();
|
||||||
|
|
||||||
@@ -291,7 +299,14 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
|||||||
private _handleScreenReaderModeOptionChange(value: boolean): void {
|
private _handleScreenReaderModeOptionChange(value: boolean): void {
|
||||||
if (value) {
|
if (value) {
|
||||||
if (!this._accessibilityManager.value && this._renderService) {
|
if (!this._accessibilityManager.value && this._renderService) {
|
||||||
this._accessibilityManager.value = this._instantiationService.createInstance(AccessibilityManager, this);
|
loadAccessibilityManager().then(module => {
|
||||||
|
// Check again after async load in case option changed
|
||||||
|
if (this.options.screenReaderMode && !this._accessibilityManager.value && this._renderService) {
|
||||||
|
this._accessibilityManager.value = this._instantiationService.createInstance(module.AccessibilityManager, this);
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
// Failed to load accessibility manager module
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this._accessibilityManager.clear();
|
this._accessibilityManager.clear();
|
||||||
@@ -509,16 +524,6 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
|||||||
this._themeService = this._instantiationService.createInstance(ThemeService);
|
this._themeService = this._instantiationService.createInstance(ThemeService);
|
||||||
this._instantiationService.setService(IThemeService, this._themeService);
|
this._instantiationService.setService(IThemeService, this._themeService);
|
||||||
|
|
||||||
// CSI ? 996 n - color scheme query (https://contour-terminal.org/vt-extensions/color-palette-update-notifications/)
|
|
||||||
this._register(this._inputHandler.onRequestColorSchemeQuery(() => this._reportColorScheme()));
|
|
||||||
|
|
||||||
// Emit unsolicited color scheme notification on theme change when DECSET 2031 is enabled
|
|
||||||
this._register(this._themeService.onChangeColors(() => {
|
|
||||||
if (this.coreService.decPrivateModes.colorSchemeUpdates) {
|
|
||||||
this._reportColorScheme();
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
this._characterJoinerService = this._instantiationService.createInstance(CharacterJoinerService);
|
this._characterJoinerService = this._instantiationService.createInstance(CharacterJoinerService);
|
||||||
this._instantiationService.setService(ICharacterJoinerService, this._characterJoinerService);
|
this._instantiationService.setService(ICharacterJoinerService, this._characterJoinerService);
|
||||||
|
|
||||||
@@ -590,7 +595,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
|||||||
this.textarea!.focus();
|
this.textarea!.focus();
|
||||||
this.textarea!.select();
|
this.textarea!.select();
|
||||||
}));
|
}));
|
||||||
this._register(EventUtils.any(
|
this._register(Event.any(
|
||||||
this._onScroll.event,
|
this._onScroll.event,
|
||||||
this._inputHandler.onScroll
|
this._inputHandler.onScroll
|
||||||
)(() => {
|
)(() => {
|
||||||
@@ -612,19 +617,34 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
|||||||
if (this.options.screenReaderMode) {
|
if (this.options.screenReaderMode) {
|
||||||
// Note that this must be done *after* the renderer is created in order to
|
// Note that this must be done *after* the renderer is created in order to
|
||||||
// ensure the correct order of the dprchange event
|
// ensure the correct order of the dprchange event
|
||||||
this._accessibilityManager.value = this._instantiationService.createInstance(AccessibilityManager, this);
|
loadAccessibilityManager().then(module => {
|
||||||
|
if (this.options.screenReaderMode && !this._accessibilityManager.value && this._renderService) {
|
||||||
|
this._accessibilityManager.value = this._instantiationService.createInstance(module.AccessibilityManager, this);
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
// Failed to load accessibility manager module
|
||||||
|
});
|
||||||
}
|
}
|
||||||
this._register(this.optionsService.onSpecificOptionChange('screenReaderMode', e => this._handleScreenReaderModeOptionChange(e)));
|
this._register(this.optionsService.onSpecificOptionChange('screenReaderMode', e => this._handleScreenReaderModeOptionChange(e)));
|
||||||
|
|
||||||
const showScrollbar = this.options.scrollbar?.showScrollbar ?? true;
|
if (this.options.overviewRuler.width) {
|
||||||
const overviewRulerWidth = this.options.scrollbar?.width;
|
loadOverviewRulerRenderer().then(module => {
|
||||||
if (showScrollbar && overviewRulerWidth) {
|
if (this.options.overviewRuler.width && !this._overviewRulerRenderer && this._viewportElement && this.screenElement) {
|
||||||
this._overviewRulerRenderer = this._register(this._instantiationService.createInstance(OverviewRulerRenderer, this._viewportElement, this.screenElement));
|
this._overviewRulerRenderer = this._register(this._instantiationService.createInstance(module.OverviewRulerRenderer, this._viewportElement, this.screenElement));
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
// Failed to load overview ruler renderer module
|
||||||
|
});
|
||||||
}
|
}
|
||||||
this.optionsService.onSpecificOptionChange('scrollbar', value => {
|
this.optionsService.onSpecificOptionChange('overviewRuler', value => {
|
||||||
const shouldShow = (value?.showScrollbar ?? true) && !!value?.width;
|
if (!this._overviewRulerRenderer && value && this._viewportElement && this.screenElement) {
|
||||||
if (!this._overviewRulerRenderer && shouldShow && this._viewportElement && this.screenElement) {
|
loadOverviewRulerRenderer().then(module => {
|
||||||
this._overviewRulerRenderer = this._register(this._instantiationService.createInstance(OverviewRulerRenderer, this._viewportElement, this.screenElement));
|
if (!this._overviewRulerRenderer && this.options.overviewRuler.width && this._viewportElement && this.screenElement) {
|
||||||
|
this._overviewRulerRenderer = this._register(this._instantiationService.createInstance(module.OverviewRulerRenderer, this._viewportElement, this.screenElement));
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
// Failed to load overview ruler renderer module
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Measure the character size
|
// Measure the character size
|
||||||
@@ -766,12 +786,11 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
|||||||
this._document!.removeEventListener('mousemove', requestedEvents.mousedrag);
|
this._document!.removeEventListener('mousemove', requestedEvents.mousedrag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return this.cancel(ev);
|
||||||
},
|
},
|
||||||
wheel: (ev: WheelEvent) => {
|
wheel: (ev: WheelEvent) => {
|
||||||
sendEvent(ev);
|
sendEvent(ev);
|
||||||
ev.preventDefault();
|
return this.cancel(ev, true);
|
||||||
ev.stopPropagation();
|
|
||||||
return false;
|
|
||||||
},
|
},
|
||||||
mousedrag: (ev: MouseEvent) => {
|
mousedrag: (ev: MouseEvent) => {
|
||||||
// deal only with move while a button is held
|
// deal only with move while a button is held
|
||||||
@@ -821,15 +840,15 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
|||||||
if (!(events & CoreMouseEventType.UP)) {
|
if (!(events & CoreMouseEventType.UP)) {
|
||||||
this._document!.removeEventListener('mouseup', requestedEvents.mouseup!);
|
this._document!.removeEventListener('mouseup', requestedEvents.mouseup!);
|
||||||
requestedEvents.mouseup = null;
|
requestedEvents.mouseup = null;
|
||||||
} else {
|
} else if (!requestedEvents.mouseup) {
|
||||||
requestedEvents.mouseup ??= eventListeners.mouseup;
|
requestedEvents.mouseup = eventListeners.mouseup;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(events & CoreMouseEventType.DRAG)) {
|
if (!(events & CoreMouseEventType.DRAG)) {
|
||||||
this._document!.removeEventListener('mousemove', requestedEvents.mousedrag!);
|
this._document!.removeEventListener('mousemove', requestedEvents.mousedrag!);
|
||||||
requestedEvents.mousedrag = null;
|
requestedEvents.mousedrag = null;
|
||||||
} else {
|
} else if (!requestedEvents.mousedrag) {
|
||||||
requestedEvents.mousedrag ??= eventListeners.mousedrag;
|
requestedEvents.mousedrag = eventListeners.mousedrag;
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
// force initial onProtocolChange so we dont miss early mouse requests
|
// force initial onProtocolChange so we dont miss early mouse requests
|
||||||
@@ -871,6 +890,8 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
|||||||
if (requestedEvents.mousedrag) {
|
if (requestedEvents.mousedrag) {
|
||||||
this._document!.addEventListener('mousemove', requestedEvents.mousedrag);
|
this._document!.addEventListener('mousemove', requestedEvents.mousedrag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return this.cancel(ev);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this._register(addDisposableListener(el, 'wheel', (ev: WheelEvent) => {
|
this._register(addDisposableListener(el, 'wheel', (ev: WheelEvent) => {
|
||||||
@@ -901,17 +922,13 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
|||||||
self._coreBrowserService?.dpr
|
self._coreBrowserService?.dpr
|
||||||
);
|
);
|
||||||
if (lines === 0) {
|
if (lines === 0) {
|
||||||
ev.preventDefault();
|
return this.cancel(ev, true);
|
||||||
ev.stopPropagation();
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct and send sequences
|
// Construct and send sequences
|
||||||
const sequence = C0.ESC + (this.coreService.decPrivateModes.applicationCursorKeys ? 'O' : '[') + (ev.deltaY < 0 ? 'A' : 'B');
|
const sequence = C0.ESC + (this.coreService.decPrivateModes.applicationCursorKeys ? 'O' : '[') + (ev.deltaY < 0 ? 'A' : 'B');
|
||||||
this.coreService.triggerDataEvent(sequence, true);
|
this.coreService.triggerDataEvent(sequence, true);
|
||||||
ev.preventDefault();
|
return this.cancel(ev, true);
|
||||||
ev.stopPropagation();
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}, { passive: false }));
|
}, { passive: false }));
|
||||||
}
|
}
|
||||||
@@ -1121,9 +1138,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
|||||||
if (result.type === KeyboardResultType.PAGE_DOWN || result.type === KeyboardResultType.PAGE_UP) {
|
if (result.type === KeyboardResultType.PAGE_DOWN || result.type === KeyboardResultType.PAGE_UP) {
|
||||||
const scrollCount = this.rows - 1;
|
const scrollCount = this.rows - 1;
|
||||||
this.scrollLines(result.type === KeyboardResultType.PAGE_UP ? -scrollCount : scrollCount);
|
this.scrollLines(result.type === KeyboardResultType.PAGE_UP ? -scrollCount : scrollCount);
|
||||||
event.preventDefault();
|
return this.cancel(event, true);
|
||||||
event.stopPropagation();
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.type === KeyboardResultType.SELECT_ALL) {
|
if (result.type === KeyboardResultType.SELECT_ALL) {
|
||||||
@@ -1136,8 +1151,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
|||||||
|
|
||||||
if (result.cancel) {
|
if (result.cancel) {
|
||||||
// The event is canceled at the end already, is this necessary?
|
// The event is canceled at the end already, is this necessary?
|
||||||
event.preventDefault();
|
this.cancel(event, true);
|
||||||
event.stopPropagation();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result.key) {
|
if (!result.key) {
|
||||||
@@ -1165,19 +1179,16 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
|||||||
this.textarea!.value = '';
|
this.textarea!.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const wasModifierOnly = this._keyboardService.useWin32InputMode && wasModifierKeyOnlyEvent(event);
|
|
||||||
this._onKey.fire({ key: result.key, domEvent: event });
|
this._onKey.fire({ key: result.key, domEvent: event });
|
||||||
this._showCursor();
|
this._showCursor();
|
||||||
this.coreService.triggerDataEvent(result.key, !wasModifierOnly);
|
this.coreService.triggerDataEvent(result.key, true);
|
||||||
|
|
||||||
// Cancel events when not in screen reader mode so events don't get bubbled up and handled by
|
// Cancel events when not in screen reader mode so events don't get bubbled up and handled by
|
||||||
// other listeners. When screen reader mode is enabled, we don't cancel them (unless ctrl or alt
|
// other listeners. When screen reader mode is enabled, we don't cancel them (unless ctrl or alt
|
||||||
// is also depressed) so that the cursor textarea can be updated, which triggers the screen
|
// is also depressed) so that the cursor textarea can be updated, which triggers the screen
|
||||||
// reader to read it.
|
// reader to read it.
|
||||||
if (!this.optionsService.rawOptions.screenReaderMode || event.altKey || event.ctrlKey) {
|
if (!this.optionsService.rawOptions.screenReaderMode || event.altKey || event.ctrlKey) {
|
||||||
event.preventDefault();
|
return this.cancel(event, true);
|
||||||
event.stopPropagation();
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this._keyDownHandled = true;
|
this._keyDownHandled = true;
|
||||||
@@ -1211,8 +1222,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
|||||||
// Handle key release for Kitty keyboard protocol
|
// Handle key release for Kitty keyboard protocol
|
||||||
const result = this._keyboardService.evaluateKeyUp(ev);
|
const result = this._keyboardService.evaluateKeyUp(ev);
|
||||||
if (result?.key) {
|
if (result?.key) {
|
||||||
const wasModifierOnly = this._keyboardService.useWin32InputMode && wasModifierKeyOnlyEvent(ev);
|
this.coreService.triggerDataEvent(result.key, true);
|
||||||
this.coreService.triggerDataEvent(result.key, !wasModifierOnly);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateCursorStyle(ev);
|
this.updateCursorStyle(ev);
|
||||||
@@ -1238,6 +1248,8 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.cancel(ev);
|
||||||
|
|
||||||
if (ev.charCode) {
|
if (ev.charCode) {
|
||||||
key = ev.charCode;
|
key = ev.charCode;
|
||||||
} else if (ev.which === null || ev.which === undefined) {
|
} else if (ev.which === null || ev.which === undefined) {
|
||||||
@@ -1290,6 +1302,8 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
|||||||
|
|
||||||
const text = ev.data;
|
const text = ev.data;
|
||||||
this.coreService.triggerDataEvent(text, true);
|
this.coreService.triggerDataEvent(text, true);
|
||||||
|
|
||||||
|
this.cancel(ev);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1401,6 +1415,15 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Remove cancel function and cancelEvents option
|
||||||
|
public cancel(ev: MouseEvent | WheelEvent | KeyboardEvent | InputEvent, force?: boolean): boolean | undefined {
|
||||||
|
if (!this.options.cancelEvents && !force) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1410,10 +1433,5 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
|||||||
function wasModifierKeyOnlyEvent(ev: KeyboardEvent): boolean {
|
function wasModifierKeyOnlyEvent(ev: KeyboardEvent): boolean {
|
||||||
return ev.keyCode === 16 || // Shift
|
return ev.keyCode === 16 || // Shift
|
||||||
ev.keyCode === 17 || // Ctrl
|
ev.keyCode === 17 || // Ctrl
|
||||||
ev.keyCode === 18 || // Alt
|
ev.keyCode === 18; // Alt
|
||||||
ev.keyCode === 91 || // Meta (Left)
|
|
||||||
ev.keyCode === 92 || // Meta (Right)
|
|
||||||
ev.keyCode === 93 || // Meta (Menu)
|
|
||||||
ev.keyCode === 224 || // Meta (Firefox)
|
|
||||||
ev.key === 'Meta';
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,178 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
|
||||||
* @license MIT
|
|
||||||
*
|
|
||||||
* Minimal DOM helpers for xterm.js browser code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { IntervalTimer } from 'common/Async';
|
|
||||||
import { IDisposable } from 'common/Lifecycle';
|
|
||||||
|
|
||||||
export function getWindow(e: Node | UIEvent | undefined | null): Window {
|
|
||||||
const candidateNode = e as Node | undefined | null;
|
|
||||||
if (candidateNode?.ownerDocument?.defaultView) {
|
|
||||||
return candidateNode.ownerDocument.defaultView;
|
|
||||||
}
|
|
||||||
|
|
||||||
const candidateEvent = e as UIEvent | undefined | null;
|
|
||||||
if (candidateEvent?.view) {
|
|
||||||
return candidateEvent.view;
|
|
||||||
}
|
|
||||||
|
|
||||||
return window;
|
|
||||||
}
|
|
||||||
|
|
||||||
class DomListener implements IDisposable {
|
|
||||||
private _handler: ((e: any) => void) | null;
|
|
||||||
private _node: EventTarget | null;
|
|
||||||
private readonly _type: string;
|
|
||||||
private readonly _options: boolean | AddEventListenerOptions | undefined;
|
|
||||||
|
|
||||||
constructor(node: EventTarget, type: string, handler: (e: any) => void, options?: boolean | AddEventListenerOptions) {
|
|
||||||
this._node = node;
|
|
||||||
this._type = type;
|
|
||||||
this._handler = handler;
|
|
||||||
this._options = options;
|
|
||||||
node.addEventListener(type, handler, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
public dispose(): void {
|
|
||||||
if (!this._node || !this._handler) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._node.removeEventListener(this._type, this._handler, this._options);
|
|
||||||
this._node = null;
|
|
||||||
this._handler = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addDisposableListener<K extends keyof GlobalEventHandlersEventMap>(node: EventTarget, type: K, handler: (event: GlobalEventHandlersEventMap[K]) => void, useCapture?: boolean): IDisposable;
|
|
||||||
export function addDisposableListener(node: EventTarget, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable;
|
|
||||||
export function addDisposableListener(node: EventTarget, type: string, handler: (event: any) => void, options: AddEventListenerOptions): IDisposable;
|
|
||||||
export function addDisposableListener(node: EventTarget, type: string, handler: (event: any) => void, useCaptureOrOptions?: boolean | AddEventListenerOptions): IDisposable {
|
|
||||||
return new DomListener(node, type, handler, useCaptureOrOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addStandardDisposableListener(node: HTMLElement, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable {
|
|
||||||
return addDisposableListener(node, type, handler, useCapture);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const eventType = {
|
|
||||||
CLICK: 'click',
|
|
||||||
MOUSE_DOWN: 'mousedown',
|
|
||||||
MOUSE_OVER: 'mouseover',
|
|
||||||
MOUSE_LEAVE: 'mouseleave',
|
|
||||||
KEY_DOWN: 'keydown',
|
|
||||||
KEY_UP: 'keyup',
|
|
||||||
INPUT: 'input',
|
|
||||||
BLUR: 'blur',
|
|
||||||
FOCUS: 'focus',
|
|
||||||
CHANGE: 'change',
|
|
||||||
POINTER_DOWN: 'pointerdown',
|
|
||||||
POINTER_MOVE: 'pointermove',
|
|
||||||
POINTER_UP: 'pointerup',
|
|
||||||
MOUSE_WHEEL: 'wheel',
|
|
||||||
WHEEL: 'wheel'
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export function getDomNodePagePosition(domNode: HTMLElement): { left: number, top: number, width: number, height: number } {
|
|
||||||
const bb = domNode.getBoundingClientRect();
|
|
||||||
const win = getWindow(domNode);
|
|
||||||
return {
|
|
||||||
left: bb.left + win.scrollX,
|
|
||||||
top: bb.top + win.scrollY,
|
|
||||||
width: bb.width,
|
|
||||||
height: bb.height
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
class AnimationFrameQueueItem implements IDisposable {
|
|
||||||
private _canceled = false;
|
|
||||||
|
|
||||||
constructor(private readonly _runner: () => void, public priority: number) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public dispose(): void {
|
|
||||||
this._canceled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public execute(): void {
|
|
||||||
if (this._canceled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
this._runner();
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static sort(a: AnimationFrameQueueItem, b: AnimationFrameQueueItem): number {
|
|
||||||
return b.priority - a.priority;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IWindowAnimationFrameState {
|
|
||||||
next: AnimationFrameQueueItem[];
|
|
||||||
current: AnimationFrameQueueItem[];
|
|
||||||
animFrameRequested: boolean;
|
|
||||||
inAnimationFrameRunner: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const animationFrameState = new Map<Window, IWindowAnimationFrameState>();
|
|
||||||
|
|
||||||
function getAnimationFrameState(targetWindow: Window): IWindowAnimationFrameState {
|
|
||||||
let state = animationFrameState.get(targetWindow);
|
|
||||||
if (!state) {
|
|
||||||
state = {
|
|
||||||
next: [],
|
|
||||||
current: [],
|
|
||||||
animFrameRequested: false,
|
|
||||||
inAnimationFrameRunner: false
|
|
||||||
};
|
|
||||||
animationFrameState.set(targetWindow, state);
|
|
||||||
}
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
function animationFrameRunner(targetWindow: Window): void {
|
|
||||||
const state = getAnimationFrameState(targetWindow);
|
|
||||||
state.animFrameRequested = false;
|
|
||||||
|
|
||||||
state.current = state.next;
|
|
||||||
state.next = [];
|
|
||||||
|
|
||||||
state.inAnimationFrameRunner = true;
|
|
||||||
while (state.current.length > 0) {
|
|
||||||
state.current.sort(AnimationFrameQueueItem.sort);
|
|
||||||
const top = state.current.shift()!;
|
|
||||||
top.execute();
|
|
||||||
}
|
|
||||||
state.inAnimationFrameRunner = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function scheduleAtNextAnimationFrame(targetWindow: Window, runner: () => void, priority: number = 0): IDisposable {
|
|
||||||
const state = getAnimationFrameState(targetWindow);
|
|
||||||
const item = new AnimationFrameQueueItem(runner, priority);
|
|
||||||
state.next.push(item);
|
|
||||||
|
|
||||||
if (!state.animFrameRequested) {
|
|
||||||
state.animFrameRequested = true;
|
|
||||||
targetWindow.requestAnimationFrame(() => animationFrameRunner(targetWindow));
|
|
||||||
}
|
|
||||||
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class WindowIntervalTimer extends IntervalTimer {
|
|
||||||
private readonly _defaultTarget?: Window;
|
|
||||||
|
|
||||||
constructor(node?: Node) {
|
|
||||||
super();
|
|
||||||
this._defaultTarget = node ? getWindow(node) : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
public cancelAndSet(runner: () => void, interval: number, targetWindow?: Window): void {
|
|
||||||
super.cancelAndSet(runner, interval, targetWindow ?? this._defaultTarget ?? window);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,12 +4,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { IBufferCellPosition, ILink, ILinkDecorations, ILinkWithState, ILinkifier2, ILinkifierEvent } from 'browser/Types';
|
import { IBufferCellPosition, ILink, ILinkDecorations, ILinkWithState, ILinkifier2, ILinkifierEvent } from 'browser/Types';
|
||||||
import { Disposable, dispose, toDisposable } from 'common/Lifecycle';
|
import { Disposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
|
||||||
import { IDisposable } from 'common/Types';
|
import { IDisposable } from 'common/Types';
|
||||||
import { IBufferService } from 'common/services/Services';
|
import { IBufferService } from 'common/services/Services';
|
||||||
import { ILinkProviderService, IMouseService, IRenderService } from './services/Services';
|
import { ILinkProviderService, IMouseService, IRenderService } from './services/Services';
|
||||||
import { Emitter } from 'common/Event';
|
import { Emitter } from 'vs/base/common/event';
|
||||||
import { addDisposableListener } from 'browser/Dom';
|
import { addDisposableListener } from 'vs/base/browser/dom';
|
||||||
|
|
||||||
export class Linkifier extends Disposable implements ILinkifier2 {
|
export class Linkifier extends Disposable implements ILinkifier2 {
|
||||||
public get currentLink(): ILinkWithState | undefined { return this._currentLink; }
|
public get currentLink(): ILinkWithState | undefined { return this._currentLink; }
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ import { CellData } from 'common/buffer/CellData';
|
|||||||
import { IBufferService, IOptionsService, IOscLinkService } from 'common/services/Services';
|
import { IBufferService, IOptionsService, IOscLinkService } from 'common/services/Services';
|
||||||
|
|
||||||
export class OscLinkProvider implements ILinkProvider {
|
export class OscLinkProvider implements ILinkProvider {
|
||||||
private readonly _workCell = new CellData();
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@IBufferService private readonly _bufferService: IBufferService,
|
@IBufferService private readonly _bufferService: IBufferService,
|
||||||
@IOptionsService private readonly _optionsService: IOptionsService,
|
@IOptionsService private readonly _optionsService: IOptionsService,
|
||||||
@@ -27,7 +25,7 @@ export class OscLinkProvider implements ILinkProvider {
|
|||||||
|
|
||||||
const result: ILink[] = [];
|
const result: ILink[] = [];
|
||||||
const linkHandler = this._optionsService.rawOptions.linkHandler;
|
const linkHandler = this._optionsService.rawOptions.linkHandler;
|
||||||
const cell = this._workCell;
|
const cell = new CellData();
|
||||||
const lineLength = line.getTrimmedLength();
|
const lineLength = line.getTrimmedLength();
|
||||||
let currentLinkId = -1;
|
let currentLinkId = -1;
|
||||||
let currentStart = -1;
|
let currentStart = -1;
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback {
|
|||||||
public refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void {
|
public refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void {
|
||||||
this._rowCount = rowCount;
|
this._rowCount = rowCount;
|
||||||
// Get the min/max row start/end for the arg values
|
// Get the min/max row start/end for the arg values
|
||||||
rowStart = rowStart ?? 0;
|
rowStart = rowStart !== undefined ? rowStart : 0;
|
||||||
rowEnd = rowEnd ?? this._rowCount - 1;
|
rowEnd = rowEnd !== undefined ? rowEnd : this._rowCount - 1;
|
||||||
// Set the properties to the updated values
|
// Set the properties to the updated values
|
||||||
this._rowStart = this._rowStart !== undefined ? Math.min(this._rowStart, rowStart) : rowStart;
|
this._rowStart = this._rowStart !== undefined ? Math.min(this._rowStart, rowStart) : rowStart;
|
||||||
this._rowEnd = this._rowEnd !== undefined ? Math.max(this._rowEnd, rowEnd) : rowEnd;
|
this._rowEnd = this._rowEnd !== undefined ? Math.max(this._rowEnd, rowEnd) : rowEnd;
|
||||||
|
|||||||
+43
-107
@@ -17,21 +17,6 @@ const INIT_ROWS = 24;
|
|||||||
// grab wcwidth from mock unicode service (hardcoded to V6)
|
// grab wcwidth from mock unicode service (hardcoded to V6)
|
||||||
const wcwidth = (new MockUnicodeService()).wcwidth;
|
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', () => {
|
describe('Terminal', () => {
|
||||||
let term: TestTerminal;
|
let term: TestTerminal;
|
||||||
const termOptions = {
|
const termOptions = {
|
||||||
@@ -198,14 +183,16 @@ describe('Terminal', () => {
|
|||||||
assert.equal(term.buffer.ybase, 0);
|
assert.equal(term.buffer.ybase, 0);
|
||||||
assert.equal(term.buffer.ydisp, 0);
|
assert.equal(term.buffer.ydisp, 0);
|
||||||
assert.equal(term.buffer.lines.length, term.rows);
|
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++) {
|
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 () => {
|
it('should clear a buffer larger than rows', async () => {
|
||||||
// Fill the buffer with dummy rows
|
// Fill the buffer with dummy rows
|
||||||
await term.writeP('test\n'.repeat(term.rows * 2));
|
for (let i = 0; i < term.rows * 2; i++) {
|
||||||
|
await term.writeP('test\n');
|
||||||
|
}
|
||||||
|
|
||||||
const promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y);
|
const promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y);
|
||||||
term.clear();
|
term.clear();
|
||||||
@@ -213,9 +200,9 @@ describe('Terminal', () => {
|
|||||||
assert.equal(term.buffer.ybase, 0);
|
assert.equal(term.buffer.ybase, 0);
|
||||||
assert.equal(term.buffer.ydisp, 0);
|
assert.equal(term.buffer.ydisp, 0);
|
||||||
assert.equal(term.buffer.lines.length, term.rows);
|
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++) {
|
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', () => {
|
it('should not break the prompt when cleared twice', () => {
|
||||||
@@ -226,9 +213,9 @@ describe('Terminal', () => {
|
|||||||
assert.equal(term.buffer.ybase, 0);
|
assert.equal(term.buffer.ybase, 0);
|
||||||
assert.equal(term.buffer.ydisp, 0);
|
assert.equal(term.buffer.ydisp, 0);
|
||||||
assert.equal(term.buffer.lines.length, term.rows);
|
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++) {
|
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));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -402,7 +389,9 @@ describe('Terminal', () => {
|
|||||||
|
|
||||||
it('should not scroll down, when a custom keydown handler prevents the event', async () => {
|
it('should not scroll down, when a custom keydown handler prevents the event', async () => {
|
||||||
// Add some output to the terminal
|
// Add some output to the terminal
|
||||||
await term.writeP('test\r\n'.repeat(term.rows * 3));
|
for (let i = 0; i < term.rows * 3; i++) {
|
||||||
|
await term.writeP('test\r\n');
|
||||||
|
}
|
||||||
const startYDisp = (term.rows * 2) + 1;
|
const startYDisp = (term.rows * 2) + 1;
|
||||||
term.attachCustomKeyEventHandler(() => {
|
term.attachCustomKeyEventHandler(() => {
|
||||||
return false;
|
return false;
|
||||||
@@ -416,42 +405,6 @@ describe('Terminal', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('keyDown', () => {
|
|
||||||
it('should not scroll down on modifier-only input in win32 input mode', async () => {
|
|
||||||
term.options.vtExtensions = { win32InputMode: true };
|
|
||||||
term.coreService.decPrivateModes.win32InputMode = true;
|
|
||||||
(term as any).textarea = { value: '' };
|
|
||||||
|
|
||||||
await term.writeP('test\r\n'.repeat(term.rows * 3));
|
|
||||||
const startYDisp = term.buffer.ydisp;
|
|
||||||
term.scrollLines(-1);
|
|
||||||
const scrolledYDisp = term.buffer.ydisp;
|
|
||||||
assert.equal(scrolledYDisp, startYDisp - 1);
|
|
||||||
|
|
||||||
const evKeyDown = {
|
|
||||||
type: 'keydown',
|
|
||||||
key: 'Control',
|
|
||||||
keyCode: 17,
|
|
||||||
ctrlKey: true,
|
|
||||||
preventDefault: () => { },
|
|
||||||
stopPropagation: () => { }
|
|
||||||
} as KeyboardEvent;
|
|
||||||
|
|
||||||
const evKeyUp = {
|
|
||||||
type: 'keyup',
|
|
||||||
key: 'Control',
|
|
||||||
keyCode: 17,
|
|
||||||
preventDefault: () => { },
|
|
||||||
stopPropagation: () => { }
|
|
||||||
} as KeyboardEvent;
|
|
||||||
|
|
||||||
term.keyDown(evKeyDown);
|
|
||||||
assert.equal(term.buffer.ydisp, scrolledYDisp);
|
|
||||||
(term as any)._keyUp(evKeyUp);
|
|
||||||
assert.equal(term.buffer.ydisp, scrolledYDisp);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('scroll() function', () => {
|
describe('scroll() function', () => {
|
||||||
describe('when scrollback > 0', () => {
|
describe('when scrollback > 0', () => {
|
||||||
it('should create a new line and scroll', () => {
|
it('should create a new line and scroll', () => {
|
||||||
@@ -778,87 +731,70 @@ describe('Terminal', () => {
|
|||||||
it(`${range}: 2 characters per cell`, async function (): Promise<void> {
|
it(`${range}: 2 characters per cell`, async function (): Promise<void> {
|
||||||
const high = String.fromCharCode(0xD800);
|
const high = String.fromCharCode(0xD800);
|
||||||
const cell = new CellData();
|
const cell = new CellData();
|
||||||
const values: string[] = [];
|
|
||||||
for (let j = i; j <= i + 0xF; j++) {
|
for (let j = i; j <= i + 0xF; j++) {
|
||||||
values.push(high + String.fromCharCode(j));
|
await term.writeP(high + String.fromCharCode(j));
|
||||||
}
|
const tchar = term.buffer.lines.get(0)!.loadCell(0, cell);
|
||||||
await term.writeP(values.join('\r\n'));
|
assert.equal(tchar.getChars(), high + String.fromCharCode(j));
|
||||||
for (let idx = 0; idx < values.length; idx++) {
|
|
||||||
const expected = values[idx];
|
|
||||||
const tchar = term.buffer.lines.get(idx)!.loadCell(0, cell);
|
|
||||||
assert.equal(tchar.getChars(), expected);
|
|
||||||
assert.equal(tchar.getChars().length, 2);
|
assert.equal(tchar.getChars().length, 2);
|
||||||
assert.equal(tchar.getWidth(), 1);
|
assert.equal(tchar.getWidth(), 1);
|
||||||
assert.equal(term.buffer.lines.get(idx)!.loadCell(1, cell).getChars(), '');
|
assert.equal(term.buffer.lines.get(0)!.loadCell(1, cell).getChars(), '');
|
||||||
|
term.reset();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
it(`${range}: 2 characters at last cell`, async () => {
|
it(`${range}: 2 characters at last cell`, async () => {
|
||||||
const high = String.fromCharCode(0xD800);
|
const high = String.fromCharCode(0xD800);
|
||||||
const cell = new CellData();
|
const cell = new CellData();
|
||||||
const values: string[] = [];
|
term.buffer.x = term.cols - 1;
|
||||||
for (let j = i; j <= i + 0xF; j++) {
|
for (let j = i; j <= i + 0xF; j++) {
|
||||||
values.push(high + String.fromCharCode(j));
|
await term.writeP(high + String.fromCharCode(j));
|
||||||
}
|
assert.equal(term.buffer.lines.get(0)!.loadCell(term.buffer.x - 1, cell).getChars(), high + String.fromCharCode(j));
|
||||||
await term.writeP(values.map((value, idx) => `\x1b[${idx + 1};${term.cols}H${value}`).join(''));
|
assert.equal(term.buffer.lines.get(0)!.loadCell(term.buffer.x - 1, cell).getChars().length, 2);
|
||||||
for (let idx = 0; idx < values.length; idx++) {
|
assert.equal(term.buffer.lines.get(1)!.loadCell(0, cell).getChars(), '');
|
||||||
const expected = values[idx];
|
term.reset();
|
||||||
assert.equal(term.buffer.lines.get(idx)!.loadCell(term.cols - 1, cell).getChars(), expected);
|
|
||||||
assert.equal(term.buffer.lines.get(idx)!.loadCell(term.cols - 1, cell).getChars().length, 2);
|
|
||||||
assert.equal(term.buffer.lines.get(idx + 1)!.loadCell(0, cell).getChars(), '');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
it(`${range}: 2 characters per cell over line end with autowrap`, async function (): Promise<void> {
|
it(`${range}: 2 characters per cell over line end with autowrap`, async function (): Promise<void> {
|
||||||
const high = String.fromCharCode(0xD800);
|
const high = String.fromCharCode(0xD800);
|
||||||
const cell = new CellData();
|
const cell = new CellData();
|
||||||
term.resize(term.cols, 40);
|
|
||||||
const values: string[] = [];
|
|
||||||
for (let j = i; j <= i + 0xF; j++) {
|
for (let j = i; j <= i + 0xF; j++) {
|
||||||
values.push(high + String.fromCharCode(j));
|
term.buffer.x = term.cols - 1;
|
||||||
}
|
await term.writeP('a' + high + String.fromCharCode(j));
|
||||||
await term.writeP(values.map((value, idx) => `\x1b[${idx * 2 + 1};${term.cols}H` + 'a' + value).join(''));
|
assert.equal(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars(), 'a');
|
||||||
for (let idx = 0; idx < values.length; idx++) {
|
assert.equal(term.buffer.lines.get(1)!.loadCell(0, cell).getChars(), high + String.fromCharCode(j));
|
||||||
const expected = values[idx];
|
assert.equal(term.buffer.lines.get(1)!.loadCell(0, cell).getChars().length, 2);
|
||||||
const row = idx * 2;
|
assert.equal(term.buffer.lines.get(1)!.loadCell(1, cell).getChars(), '');
|
||||||
assert.equal(term.buffer.lines.get(row)!.loadCell(term.cols - 1, cell).getChars(), 'a');
|
term.reset();
|
||||||
assert.equal(term.buffer.lines.get(row + 1)!.loadCell(0, cell).getChars(), expected);
|
|
||||||
assert.equal(term.buffer.lines.get(row + 1)!.loadCell(0, cell).getChars().length, 2);
|
|
||||||
assert.equal(term.buffer.lines.get(row + 1)!.loadCell(1, cell).getChars(), '');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
it(`${range}: 2 characters per cell over line end without autowrap`, async function (): Promise<void> {
|
it(`${range}: 2 characters per cell over line end without autowrap`, async function (): Promise<void> {
|
||||||
const high = String.fromCharCode(0xD800);
|
const high = String.fromCharCode(0xD800);
|
||||||
const cell = new CellData();
|
const cell = new CellData();
|
||||||
const values: string[] = [];
|
|
||||||
for (let j = i; j <= i + 0xF; j++) {
|
for (let j = i; j <= i + 0xF; j++) {
|
||||||
|
term.buffer.x = term.cols - 1;
|
||||||
|
await term.writeP('\x1b[?7l'); // Disable wraparound mode
|
||||||
const width = wcwidth((0xD800 - 0xD800) * 0x400 + j - 0xDC00 + 0x10000);
|
const width = wcwidth((0xD800 - 0xD800) * 0x400 + j - 0xDC00 + 0x10000);
|
||||||
if (width !== 1) {
|
if (width !== 1) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
values.push(high + String.fromCharCode(j));
|
await term.writeP('a' + high + String.fromCharCode(j));
|
||||||
}
|
// auto wraparound mode should cut off the rest of the line
|
||||||
await term.writeP('\x1b[?7l' + values.map((value, idx) => `\x1b[${idx + 1};${term.cols}H` + 'a' + value).join(''));
|
assert.equal(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars(), high + String.fromCharCode(j));
|
||||||
for (let idx = 0; idx < values.length; idx++) {
|
assert.equal(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars().length, 2);
|
||||||
const expected = values[idx];
|
assert.equal(term.buffer.lines.get(1)!.loadCell(1, cell).getChars(), '');
|
||||||
assert.equal(term.buffer.lines.get(idx)!.loadCell(term.cols - 1, cell).getChars(), expected);
|
term.reset();
|
||||||
assert.equal(term.buffer.lines.get(idx)!.loadCell(term.cols - 1, cell).getChars().length, 2);
|
|
||||||
assert.equal(term.buffer.lines.get(idx + 1)!.loadCell(1, cell).getChars(), '');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
it(`${range}: splitted surrogates`, async function (): Promise<void> {
|
it(`${range}: splitted surrogates`, async function (): Promise<void> {
|
||||||
const high = String.fromCharCode(0xD800);
|
const high = String.fromCharCode(0xD800);
|
||||||
const cell = new CellData();
|
const cell = new CellData();
|
||||||
const values: string[] = [];
|
|
||||||
for (let j = i; j <= i + 0xF; j++) {
|
for (let j = i; j <= i + 0xF; j++) {
|
||||||
values.push(high + String.fromCharCode(j));
|
await term.writeP(high + String.fromCharCode(j));
|
||||||
}
|
const tchar = term.buffer.lines.get(0)!.loadCell(0, cell);
|
||||||
await term.writeP(values.join('\r\n'));
|
assert.equal(tchar.getChars(), high + String.fromCharCode(j));
|
||||||
for (let idx = 0; idx < values.length; idx++) {
|
|
||||||
const expected = values[idx];
|
|
||||||
const tchar = term.buffer.lines.get(idx)!.loadCell(0, cell);
|
|
||||||
assert.equal(tchar.getChars(), expected);
|
|
||||||
assert.equal(tchar.getChars().length, 2);
|
assert.equal(tchar.getChars().length, 2);
|
||||||
assert.equal(tchar.getWidth(), 1);
|
assert.equal(tchar.getWidth(), 1);
|
||||||
assert.equal(term.buffer.lines.get(idx)!.loadCell(1, cell).getChars(), '');
|
assert.equal(term.buffer.lines.get(0)!.loadCell(1, cell).getChars(), '');
|
||||||
|
term.reset();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { AttributeData } from 'common/buffer/AttributeData';
|
|||||||
import { ISelectionRedrawRequestEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
|
import { ISelectionRedrawRequestEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
|
||||||
import { css } from 'common/Color';
|
import { css } from 'common/Color';
|
||||||
import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
|
import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
|
||||||
import { Emitter, type IEvent } from 'common/Event';
|
import { Emitter, type Event } from 'vs/base/common/event';
|
||||||
|
|
||||||
export class TestTerminal extends CoreBrowserTerminal {
|
export class TestTerminal extends CoreBrowserTerminal {
|
||||||
public get curAttrData(): IAttributeData { return (this as any)._inputHandler._curAttrData; }
|
public get curAttrData(): IAttributeData { return (this as any)._inputHandler._curAttrData; }
|
||||||
@@ -30,24 +30,24 @@ export class TestTerminal extends CoreBrowserTerminal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class MockTerminal implements ITerminal {
|
export class MockTerminal implements ITerminal {
|
||||||
public onBlur!: IEvent<void>;
|
public onBlur!: Event<void>;
|
||||||
public onFocus!: IEvent<void>;
|
public onFocus!: Event<void>;
|
||||||
public onA11yChar!: IEvent<string>;
|
public onA11yChar!: Event<string>;
|
||||||
public onWriteParsed!: IEvent<void>;
|
public onWriteParsed!: Event<void>;
|
||||||
public onA11yTab!: IEvent<number>;
|
public onA11yTab!: Event<number>;
|
||||||
public onCursorMove!: IEvent<void>;
|
public onCursorMove!: Event<void>;
|
||||||
public onLineFeed!: IEvent<void>;
|
public onLineFeed!: Event<void>;
|
||||||
public onSelectionChange!: IEvent<void>;
|
public onSelectionChange!: Event<void>;
|
||||||
public onData!: IEvent<string>;
|
public onData!: Event<string>;
|
||||||
public onBinary!: IEvent<string>;
|
public onBinary!: Event<string>;
|
||||||
public onTitleChange!: IEvent<string>;
|
public onTitleChange!: Event<string>;
|
||||||
public onBell!: IEvent<void>;
|
public onBell!: Event<void>;
|
||||||
public onScroll!: IEvent<number>;
|
public onScroll!: Event<number>;
|
||||||
public onWillOpen!: IEvent<HTMLElement>;
|
public onWillOpen!: Event<HTMLElement>;
|
||||||
public onKey!: IEvent<{ key: string, domEvent: KeyboardEvent }>;
|
public onKey!: Event<{ key: string, domEvent: KeyboardEvent }>;
|
||||||
public onRender!: IEvent<{ start: number, end: number }>;
|
public onRender!: Event<{ start: number, end: number }>;
|
||||||
public onResize!: IEvent<{ cols: number, rows: number }>;
|
public onResize!: Event<{ cols: number, rows: number }>;
|
||||||
public onDimensionsChange!: IEvent<IRenderDimensionsApi>;
|
public onDimensionsChange!: Event<IRenderDimensionsApi>;
|
||||||
public dimensions: IRenderDimensionsApi | undefined;
|
public dimensions: IRenderDimensionsApi | undefined;
|
||||||
public markers!: IMarker[];
|
public markers!: IMarker[];
|
||||||
public linkifier: ILinkifier2 | undefined;
|
public linkifier: ILinkifier2 | undefined;
|
||||||
@@ -107,9 +107,6 @@ export class MockTerminal implements ITerminal {
|
|||||||
public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
|
public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
public registerApcHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
|
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
@@ -198,6 +195,9 @@ export class MockTerminal implements ITerminal {
|
|||||||
public scrollToRow(absoluteRow: number): number {
|
public scrollToRow(absoluteRow: number): number {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
public cancel(ev: MouseEvent | WheelEvent | KeyboardEvent | InputEvent, force?: boolean): void {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
public log(text: string): void {
|
public log(text: string): void {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
@@ -273,9 +273,9 @@ export class MockBuffer implements IBuffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class MockRenderer implements IRenderer {
|
export class MockRenderer implements IRenderer {
|
||||||
public onRequestRedraw!: IEvent<IRequestRedrawEvent>;
|
public onRequestRedraw!: Event<IRequestRedrawEvent>;
|
||||||
public onCanvasResize!: IEvent<{ width: number, height: number }>;
|
public onCanvasResize!: Event<{ width: number, height: number }>;
|
||||||
public onRender!: IEvent<{ start: number, end: number }>;
|
public onRender!: Event<{ start: number, end: number }>;
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
@@ -378,7 +378,7 @@ export class MockCoreBrowserService implements ICoreBrowserService {
|
|||||||
export class MockCharSizeService implements ICharSizeService {
|
export class MockCharSizeService implements ICharSizeService {
|
||||||
public serviceBrand: undefined;
|
public serviceBrand: undefined;
|
||||||
public get hasValidSize(): boolean { return this.width > 0 && this.height > 0; }
|
public get hasValidSize(): boolean { return this.width > 0 && this.height > 0; }
|
||||||
public onCharSizeChange: IEvent<void> = new Emitter<void>().event;
|
public onCharSizeChange: Event<void> = new Emitter<void>().event;
|
||||||
constructor(public width: number, public height: number) {}
|
constructor(public width: number, public height: number) {}
|
||||||
public measure(): void {}
|
public measure(): void {}
|
||||||
}
|
}
|
||||||
@@ -396,10 +396,10 @@ export class MockMouseService implements IMouseService {
|
|||||||
|
|
||||||
export class MockRenderService implements IRenderService {
|
export class MockRenderService implements IRenderService {
|
||||||
public serviceBrand: undefined;
|
public serviceBrand: undefined;
|
||||||
public onDimensionsChange: IEvent<IRenderDimensions> = new Emitter<IRenderDimensions>().event;
|
public onDimensionsChange: Event<IRenderDimensions> = new Emitter<IRenderDimensions>().event;
|
||||||
public onRenderedViewportChange: IEvent<{ start: number, end: number }> = new Emitter<{ start: number, end: number }>().event;
|
public onRenderedViewportChange: Event<{ start: number, end: number }> = new Emitter<{ start: number, end: number }>().event;
|
||||||
public onRender: IEvent<{ start: number, end: number }> = new Emitter<{ start: number, end: number }>().event;
|
public onRender: Event<{ start: number, end: number }> = new Emitter<{ start: number, end: number }>().event;
|
||||||
public onRefreshRequest: IEvent<{ start: number, end: number}> = new Emitter<{ start: number, end: number }>().event;
|
public onRefreshRequest: Event<{ start: number, end: number}> = new Emitter<{ start: number, end: number }>().event;
|
||||||
public dimensions: IRenderDimensions = createRenderDimensions();
|
public dimensions: IRenderDimensions = createRenderDimensions();
|
||||||
public refreshRows(start: number, end: number): void {
|
public refreshRows(start: number, end: number): void {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ export class TimeBasedDebouncer implements IRenderDebouncer {
|
|||||||
public refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void {
|
public refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void {
|
||||||
this._rowCount = rowCount;
|
this._rowCount = rowCount;
|
||||||
// Get the min/max row start/end for the arg values
|
// Get the min/max row start/end for the arg values
|
||||||
rowStart = rowStart ?? 0;
|
rowStart = rowStart !== undefined ? rowStart : 0;
|
||||||
rowEnd = rowEnd ?? this._rowCount - 1;
|
rowEnd = rowEnd !== undefined ? rowEnd : this._rowCount - 1;
|
||||||
// Set the properties to the updated values
|
// Set the properties to the updated values
|
||||||
this._rowStart = this._rowStart !== undefined ? Math.min(this._rowStart, rowStart) : rowStart;
|
this._rowStart = this._rowStart !== undefined ? Math.min(this._rowStart, rowStart) : rowStart;
|
||||||
this._rowEnd = this._rowEnd !== undefined ? Math.max(this._rowEnd, rowEnd) : rowEnd;
|
this._rowEnd = this._rowEnd !== undefined ? Math.max(this._rowEnd, rowEnd) : rowEnd;
|
||||||
|
|||||||
+12
-10
@@ -7,7 +7,7 @@ import { CharData, IColor, ICoreTerminal, ITerminalOptions } from 'common/Types'
|
|||||||
import { IBuffer } from 'common/buffer/Types';
|
import { IBuffer } from 'common/buffer/Types';
|
||||||
import { IDisposable, IRenderDimensions as IRenderDimensionsApi, Terminal as ITerminalApi } from '@xterm/xterm';
|
import { IDisposable, IRenderDimensions as IRenderDimensionsApi, Terminal as ITerminalApi } from '@xterm/xterm';
|
||||||
import { channels, css } from 'common/Color';
|
import { channels, css } from 'common/Color';
|
||||||
import type { IEvent } from 'common/Event';
|
import type { Event } from 'vs/base/common/event';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A portion of the public API that are implemented identially internally and simply passed through.
|
* A portion of the public API that are implemented identially internally and simply passed through.
|
||||||
@@ -23,12 +23,14 @@ export interface ITerminal extends InternalPassthroughApis, ICoreTerminal {
|
|||||||
|
|
||||||
readonly dimensions: IRenderDimensionsApi | undefined;
|
readonly dimensions: IRenderDimensionsApi | undefined;
|
||||||
|
|
||||||
onBlur: IEvent<void>;
|
onBlur: Event<void>;
|
||||||
onFocus: IEvent<void>;
|
onFocus: Event<void>;
|
||||||
onDimensionsChange: IEvent<IRenderDimensionsApi>;
|
onDimensionsChange: Event<IRenderDimensionsApi>;
|
||||||
onA11yChar: IEvent<string>;
|
onA11yChar: Event<string>;
|
||||||
onA11yTab: IEvent<number>;
|
onA11yTab: Event<number>;
|
||||||
onWillOpen: IEvent<HTMLElement>;
|
onWillOpen: Event<HTMLElement>;
|
||||||
|
|
||||||
|
cancel(ev: MouseEvent | WheelEvent | KeyboardEvent | InputEvent, force?: boolean): boolean | void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
|
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
|
||||||
@@ -99,7 +101,7 @@ export interface IPartialColorSet {
|
|||||||
|
|
||||||
export interface IViewport extends IDisposable {
|
export interface IViewport extends IDisposable {
|
||||||
scrollBarWidth: number;
|
scrollBarWidth: number;
|
||||||
readonly onRequestScrollLines: IEvent<{ amount: number, suppressScrollEvent: boolean }>;
|
readonly onRequestScrollLines: Event<{ amount: number, suppressScrollEvent: boolean }>;
|
||||||
syncScrollArea(immediate?: boolean, force?: boolean): void;
|
syncScrollArea(immediate?: boolean, force?: boolean): void;
|
||||||
getLinesScrolled(ev: WheelEvent): number;
|
getLinesScrolled(ev: WheelEvent): number;
|
||||||
getBufferElements(startLine: number, endLine?: number): { bufferElements: HTMLElement[], cursorElement?: HTMLElement };
|
getBufferElements(startLine: number, endLine?: number): { bufferElements: HTMLElement[], cursorElement?: HTMLElement };
|
||||||
@@ -129,8 +131,8 @@ export interface ILinkWithState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ILinkifier2 extends IDisposable {
|
export interface ILinkifier2 extends IDisposable {
|
||||||
onShowLinkUnderline: IEvent<ILinkifierEvent>;
|
onShowLinkUnderline: Event<ILinkifierEvent>;
|
||||||
onHideLinkUnderline: IEvent<ILinkifierEvent>;
|
onHideLinkUnderline: Event<ILinkifierEvent>;
|
||||||
readonly currentLink: ILinkWithState | undefined;
|
readonly currentLink: ILinkWithState | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+20
-28
@@ -5,15 +5,15 @@
|
|||||||
|
|
||||||
import { ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
|
import { ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
|
||||||
import { ViewportConstants } from 'browser/shared/Constants';
|
import { ViewportConstants } from 'browser/shared/Constants';
|
||||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||||
import { IBufferService, ICoreMouseService, IOptionsService } from 'common/services/Services';
|
import { IBufferService, ICoreMouseService, IOptionsService } from 'common/services/Services';
|
||||||
import { CoreMouseEventType } from 'common/Types';
|
import { CoreMouseEventType } from 'common/Types';
|
||||||
import { addDisposableListener, scheduleAtNextAnimationFrame } from 'browser/Dom';
|
import { addDisposableListener, scheduleAtNextAnimationFrame } from 'vs/base/browser/dom';
|
||||||
import { SmoothScrollableElement } from 'browser/scrollable/scrollableElement';
|
import { SmoothScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
|
||||||
import type { IScrollableElementChangeOptions } from 'browser/scrollable/scrollableElementOptions';
|
import type { ScrollableElementChangeOptions } from 'vs/base/browser/ui/scrollbar/scrollableElementOptions';
|
||||||
import { Emitter, EventUtils } from 'common/Event';
|
import { Emitter, Event } from 'vs/base/common/event';
|
||||||
import { Scrollable, ScrollbarVisibility, type IScrollEvent } from 'browser/scrollable/scrollable';
|
import { Scrollable, ScrollbarVisibility, type ScrollEvent } from 'vs/base/common/scrollable';
|
||||||
import { Gesture, EventType as GestureEventType, type IGestureEvent } from 'browser/scrollable/touch';
|
import { Gesture, EventType as GestureEventType, type GestureEvent } from 'vs/base/browser/touch';
|
||||||
|
|
||||||
export class Viewport extends Disposable {
|
export class Viewport extends Disposable {
|
||||||
|
|
||||||
@@ -52,17 +52,16 @@ export class Viewport extends Disposable {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
this._scrollableElement = this._register(new SmoothScrollableElement(screenElement, {
|
this._scrollableElement = this._register(new SmoothScrollableElement(screenElement, {
|
||||||
vertical: ScrollbarVisibility.AUTO,
|
vertical: ScrollbarVisibility.Auto,
|
||||||
horizontal: ScrollbarVisibility.HIDDEN,
|
horizontal: ScrollbarVisibility.Hidden,
|
||||||
useShadows: false,
|
useShadows: false,
|
||||||
mouseWheelSmoothScroll: true,
|
mouseWheelSmoothScroll: true,
|
||||||
verticalHasArrows: this._optionsService.rawOptions.scrollbar?.showArrows ?? false,
|
|
||||||
...this._getChangeOptions()
|
...this._getChangeOptions()
|
||||||
}, scrollable));
|
}, scrollable));
|
||||||
this._register(this._optionsService.onMultipleOptionChange([
|
this._register(this._optionsService.onMultipleOptionChange([
|
||||||
'scrollSensitivity',
|
'scrollSensitivity',
|
||||||
'fastScrollSensitivity',
|
'fastScrollSensitivity',
|
||||||
'scrollbar'
|
'overviewRuler'
|
||||||
], () => this._scrollableElement.updateOptions(this._getChangeOptions())));
|
], () => this._scrollableElement.updateOptions(this._getChangeOptions())));
|
||||||
// Don't handle mouse wheel if wheel events are supported by the current mouse prototcol
|
// Don't handle mouse wheel if wheel events are supported by the current mouse prototcol
|
||||||
this._register(coreMouseService.onProtocolChange(type => {
|
this._register(coreMouseService.onProtocolChange(type => {
|
||||||
@@ -72,7 +71,7 @@ export class Viewport extends Disposable {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
this._scrollableElement.setScrollDimensions({ height: 0, scrollHeight: 0 });
|
this._scrollableElement.setScrollDimensions({ height: 0, scrollHeight: 0 });
|
||||||
this._register(EventUtils.runAndSubscribe(themeService.onChangeColors, () => {
|
this._register(Event.runAndSubscribe(themeService.onChangeColors, () => {
|
||||||
element.style.backgroundColor = themeService.colors.background.css;
|
element.style.backgroundColor = themeService.colors.background.css;
|
||||||
this._scrollableElement.getDomNode().style.backgroundColor = themeService.colors.background.css;
|
this._scrollableElement.getDomNode().style.backgroundColor = themeService.colors.background.css;
|
||||||
}));
|
}));
|
||||||
@@ -82,15 +81,15 @@ export class Viewport extends Disposable {
|
|||||||
this._styleElement = coreBrowserService.mainDocument.createElement('style');
|
this._styleElement = coreBrowserService.mainDocument.createElement('style');
|
||||||
screenElement.appendChild(this._styleElement);
|
screenElement.appendChild(this._styleElement);
|
||||||
this._register(toDisposable(() => this._styleElement.remove()));
|
this._register(toDisposable(() => this._styleElement.remove()));
|
||||||
this._register(EventUtils.runAndSubscribe(themeService.onChangeColors, () => {
|
this._register(Event.runAndSubscribe(themeService.onChangeColors, () => {
|
||||||
this._styleElement.textContent = [
|
this._styleElement.textContent = [
|
||||||
`.xterm .xterm-scrollable-element > .xterm-scrollbar > .xterm-slider {`,
|
`.xterm .xterm-scrollable-element > .scrollbar > .slider {`,
|
||||||
` background: ${themeService.colors.scrollbarSliderBackground.css};`,
|
` background: ${themeService.colors.scrollbarSliderBackground.css};`,
|
||||||
`}`,
|
`}`,
|
||||||
`.xterm .xterm-scrollable-element > .xterm-scrollbar > .xterm-slider:hover {`,
|
`.xterm .xterm-scrollable-element > .scrollbar > .slider:hover {`,
|
||||||
` background: ${themeService.colors.scrollbarSliderHoverBackground.css};`,
|
` background: ${themeService.colors.scrollbarSliderHoverBackground.css};`,
|
||||||
`}`,
|
`}`,
|
||||||
`.xterm .xterm-scrollable-element > .xterm-scrollbar > .xterm-slider.xterm-active {`,
|
`.xterm .xterm-scrollable-element > .scrollbar > .slider.active {`,
|
||||||
` background: ${themeService.colors.scrollbarSliderActiveBackground.css};`,
|
` background: ${themeService.colors.scrollbarSliderActiveBackground.css};`,
|
||||||
`}`
|
`}`
|
||||||
].join('\n');
|
].join('\n');
|
||||||
@@ -109,7 +108,7 @@ export class Viewport extends Disposable {
|
|||||||
|
|
||||||
// Touch/gesture scrolling support
|
// Touch/gesture scrolling support
|
||||||
this._register(Gesture.addTarget(screenElement));
|
this._register(Gesture.addTarget(screenElement));
|
||||||
this._register(addDisposableListener(screenElement, GestureEventType.CHANGE, (e: IGestureEvent) => this._handleGestureChange(e)));
|
this._register(addDisposableListener(screenElement, GestureEventType.Change, (e: GestureEvent) => this._handleGestureChange(e)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public scrollLines(disp: number): void {
|
public scrollLines(disp: number): void {
|
||||||
@@ -130,18 +129,11 @@ export class Viewport extends Disposable {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getChangeOptions(): IScrollableElementChangeOptions {
|
private _getChangeOptions(): ScrollableElementChangeOptions {
|
||||||
const showScrollbar = this._optionsService.rawOptions.scrollbar?.showScrollbar ?? true;
|
|
||||||
const showArrows = this._optionsService.rawOptions.scrollbar?.showArrows ?? false;
|
|
||||||
const verticalScrollbarSize = showScrollbar
|
|
||||||
? (this._optionsService.rawOptions.scrollbar?.width ?? ViewportConstants.DEFAULT_SCROLL_BAR_WIDTH)
|
|
||||||
: 0;
|
|
||||||
return {
|
return {
|
||||||
mouseWheelScrollSensitivity: this._optionsService.rawOptions.scrollSensitivity,
|
mouseWheelScrollSensitivity: this._optionsService.rawOptions.scrollSensitivity,
|
||||||
fastScrollSensitivity: this._optionsService.rawOptions.fastScrollSensitivity,
|
fastScrollSensitivity: this._optionsService.rawOptions.fastScrollSensitivity,
|
||||||
vertical: showScrollbar ? ScrollbarVisibility.AUTO : ScrollbarVisibility.HIDDEN,
|
verticalScrollbarSize: this._optionsService.rawOptions.overviewRuler?.width || ViewportConstants.DEFAULT_SCROLL_BAR_WIDTH
|
||||||
verticalScrollbarSize,
|
|
||||||
verticalHasArrows: showArrows
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +179,7 @@ export class Viewport extends Disposable {
|
|||||||
this._isSyncing = false;
|
this._isSyncing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleScroll(e: IScrollEvent): void {
|
private _handleScroll(e: ScrollEvent): void {
|
||||||
if (!this._renderService) {
|
if (!this._renderService) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -204,7 +196,7 @@ export class Viewport extends Disposable {
|
|||||||
this._isHandlingScroll = false;
|
this._isHandlingScroll = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleGestureChange(e: IGestureEvent): void {
|
private _handleGestureChange(e: GestureEvent): void {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const pos = this._scrollableElement.getScrollPosition();
|
const pos = this._scrollableElement.getScrollPosition();
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import { ICoreBrowserService, IRenderService } from 'browser/services/Services';
|
import { ICoreBrowserService, IRenderService } from 'browser/services/Services';
|
||||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||||
import { IBufferService, IDecorationService, IInternalDecoration } from 'common/services/Services';
|
import { IBufferService, IDecorationService, IInternalDecoration } from 'common/services/Services';
|
||||||
|
|
||||||
export class BufferDecorationRenderer extends Disposable {
|
export class BufferDecorationRenderer extends Disposable {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { ColorZoneStore, IColorZone, IColorZoneStore } from 'browser/decorations/ColorZoneStore';
|
import { ColorZoneStore, IColorZone, IColorZoneStore } from 'browser/decorations/ColorZoneStore';
|
||||||
import { ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
|
import { ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
|
||||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||||
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
|
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||||
|
|
||||||
const enum Constants {
|
const enum Constants {
|
||||||
@@ -38,12 +38,7 @@ export class OverviewRulerRenderer extends Disposable {
|
|||||||
private readonly _ctx: CanvasRenderingContext2D;
|
private readonly _ctx: CanvasRenderingContext2D;
|
||||||
private readonly _colorZoneStore: IColorZoneStore = new ColorZoneStore();
|
private readonly _colorZoneStore: IColorZoneStore = new ColorZoneStore();
|
||||||
private get _width(): number {
|
private get _width(): number {
|
||||||
const scrollbar = this._optionsService.rawOptions.scrollbar;
|
return this._optionsService.options.overviewRuler?.width || 0;
|
||||||
const showScrollbar = scrollbar?.showScrollbar ?? true;
|
|
||||||
if (!showScrollbar) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return scrollbar?.width ?? 0;
|
|
||||||
}
|
}
|
||||||
private _animationFrame: number | undefined;
|
private _animationFrame: number | undefined;
|
||||||
|
|
||||||
@@ -100,7 +95,7 @@ export class OverviewRulerRenderer extends Disposable {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
this._register(this._coreBrowserService.onDprChange(() => this._queueRefresh(true)));
|
this._register(this._coreBrowserService.onDprChange(() => this._queueRefresh(true)));
|
||||||
this._register(this._optionsService.onSpecificOptionChange('scrollbar', () => this._queueRefresh(true)));
|
this._register(this._optionsService.onSpecificOptionChange('overviewRuler', () => this._queueRefresh(true)));
|
||||||
this._register(this._themeService.onChangeColors(() => this._queueRefresh()));
|
this._register(this._themeService.onChangeColors(() => this._queueRefresh()));
|
||||||
this._queueRefresh(true);
|
this._queueRefresh(true);
|
||||||
}
|
}
|
||||||
@@ -181,10 +176,10 @@ export class OverviewRulerRenderer extends Disposable {
|
|||||||
private _renderRulerOutline(): void {
|
private _renderRulerOutline(): void {
|
||||||
this._ctx.fillStyle = this._themeService.colors.overviewRulerBorder.css;
|
this._ctx.fillStyle = this._themeService.colors.overviewRulerBorder.css;
|
||||||
this._ctx.fillRect(0, 0, Constants.OVERVIEW_RULER_BORDER_WIDTH, this._canvas.height);
|
this._ctx.fillRect(0, 0, Constants.OVERVIEW_RULER_BORDER_WIDTH, this._canvas.height);
|
||||||
if (this._optionsService.rawOptions.scrollbar?.overviewRuler?.showTopBorder) {
|
if (this._optionsService.rawOptions.overviewRuler.showTopBorder) {
|
||||||
this._ctx.fillRect(Constants.OVERVIEW_RULER_BORDER_WIDTH, 0, this._canvas.width - Constants.OVERVIEW_RULER_BORDER_WIDTH, Constants.OVERVIEW_RULER_BORDER_WIDTH);
|
this._ctx.fillRect(Constants.OVERVIEW_RULER_BORDER_WIDTH, 0, this._canvas.width - Constants.OVERVIEW_RULER_BORDER_WIDTH, Constants.OVERVIEW_RULER_BORDER_WIDTH);
|
||||||
}
|
}
|
||||||
if (this._optionsService.rawOptions.scrollbar?.overviewRuler?.showBottomBorder) {
|
if (this._optionsService.rawOptions.overviewRuler.showBottomBorder) {
|
||||||
this._ctx.fillRect(Constants.OVERVIEW_RULER_BORDER_WIDTH, this._canvas.height - Constants.OVERVIEW_RULER_BORDER_WIDTH, this._canvas.width - Constants.OVERVIEW_RULER_BORDER_WIDTH, this._canvas.height);
|
this._ctx.fillRect(Constants.OVERVIEW_RULER_BORDER_WIDTH, this._canvas.height - Constants.OVERVIEW_RULER_BORDER_WIDTH, this._canvas.width - Constants.OVERVIEW_RULER_BORDER_WIDTH, this._canvas.height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user