Merge remote-tracking branch 'upstream/master' into pr/silamon/4004
This commit is contained in:
@@ -5,35 +5,30 @@
|
||||
|
||||
import { IColorContrastCache } from 'browser/Types';
|
||||
import { IColor } from 'common/Types';
|
||||
import { TwoKeyMap } from 'common/MultiKeyMap';
|
||||
|
||||
export class ColorContrastCache implements IColorContrastCache {
|
||||
private _color: { [bg: number]: { [fg: number]: IColor | null | undefined } | undefined } = {};
|
||||
private _rgba: { [bg: number]: { [fg: number]: string | null | undefined } | undefined } = {};
|
||||
|
||||
public clear(): void {
|
||||
this._color = {};
|
||||
this._rgba = {};
|
||||
}
|
||||
private _color: TwoKeyMap</* bg */number, /* fg */number, IColor | null> = new TwoKeyMap();
|
||||
private _css: TwoKeyMap</* bg */number, /* fg */number, string | null> = new TwoKeyMap();
|
||||
|
||||
public setCss(bg: number, fg: number, value: string | null): void {
|
||||
if (!this._rgba[bg]) {
|
||||
this._rgba[bg] = {};
|
||||
}
|
||||
this._rgba[bg]![fg] = value;
|
||||
this._css.set(bg, fg, value);
|
||||
}
|
||||
|
||||
public getCss(bg: number, fg: number): string | null | undefined {
|
||||
return this._rgba[bg] ? this._rgba[bg]![fg] : undefined;
|
||||
return this._css.get(bg, fg);
|
||||
}
|
||||
|
||||
public setColor(bg: number, fg: number, value: IColor | null): void {
|
||||
if (!this._color[bg]) {
|
||||
this._color[bg] = {};
|
||||
}
|
||||
this._color[bg]![fg] = value;
|
||||
this._color.set(bg, fg, value);
|
||||
}
|
||||
|
||||
public getColor(bg: number, fg: number): IColor | null | undefined {
|
||||
return this._color[bg] ? this._color[bg]![fg] : undefined;
|
||||
return this._color.get(bg, fg);
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._color.clear();
|
||||
this._css.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
// This file contains strings that get exported in the API so they can be localized
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
export let promptLabel = 'Terminal input';
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Copyright (c) 2022 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IBufferRange, ILink, ILinkProvider } from 'browser/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { IBufferService, IOptionsService, IOscLinkService } from 'common/services/Services';
|
||||
|
||||
export class OscLinkProvider implements ILinkProvider {
|
||||
constructor(
|
||||
@IBufferService private readonly _bufferService: IBufferService,
|
||||
@IOptionsService private readonly _optionsService: IOptionsService,
|
||||
@IOscLinkService private readonly _oscLinkService: IOscLinkService
|
||||
) {
|
||||
}
|
||||
|
||||
public provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void {
|
||||
const line = this._bufferService.buffer.lines.get(y - 1);
|
||||
if (!line) {
|
||||
callback(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const result: ILink[] = [];
|
||||
const linkHandler = this._optionsService.rawOptions.linkHandler;
|
||||
const cell = new CellData();
|
||||
const lineLength = line.getTrimmedLength();
|
||||
let currentLinkId = -1;
|
||||
let currentStart = -1;
|
||||
let finishLink = false;
|
||||
for (let x = 0; x < lineLength; x++) {
|
||||
// Minor optimization, only check for content if there isn't a link in case the link ends with
|
||||
// a null cell
|
||||
if (currentStart === -1 && !line.hasContent(x)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
line.loadCell(x, cell);
|
||||
if (cell.hasExtendedAttrs() && cell.extended.urlId) {
|
||||
if (currentStart === -1) {
|
||||
currentStart = x;
|
||||
currentLinkId = cell.extended.urlId;
|
||||
continue;
|
||||
} else {
|
||||
finishLink = cell.extended.urlId !== currentLinkId;
|
||||
}
|
||||
} else {
|
||||
if (currentStart !== -1) {
|
||||
finishLink = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (finishLink || (currentStart !== -1 && x === lineLength - 1)) {
|
||||
const text = this._oscLinkService.getLinkData(currentLinkId)?.uri;
|
||||
if (text) {
|
||||
// These ranges are 1-based
|
||||
const range: IBufferRange = {
|
||||
start: {
|
||||
x: currentStart + 1,
|
||||
y
|
||||
},
|
||||
end: {
|
||||
// Offset end x if it's a link that ends on the last cell in the line
|
||||
x: x + (!finishLink && x === lineLength - 1 ? 1 : 0),
|
||||
y
|
||||
}
|
||||
};
|
||||
// OSC links always use underline and pointer decorations
|
||||
result.push({
|
||||
text,
|
||||
range,
|
||||
activate: (e, text) => (linkHandler ? linkHandler.activate(e, text, range) : defaultActivate(e, text)),
|
||||
hover: (e, text) => linkHandler?.hover?.(e, text, range),
|
||||
leave: (e, text) => linkHandler?.leave?.(e, text, range)
|
||||
});
|
||||
}
|
||||
finishLink = false;
|
||||
|
||||
// Clear link or start a new link if one starts immediately
|
||||
if (cell.hasExtendedAttrs() && cell.extended.urlId) {
|
||||
currentStart = x;
|
||||
currentLinkId = cell.extended.urlId;
|
||||
} else {
|
||||
currentStart = -1;
|
||||
currentLinkId = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Handle fetching and returning other link ranges to underline other links with the same id
|
||||
callback(result);
|
||||
}
|
||||
}
|
||||
|
||||
function defaultActivate(e: MouseEvent, uri: string): void {
|
||||
const answer = confirm(`Do you want to navigate to ${uri}?`);
|
||||
if (answer) {
|
||||
const newWindow = window.open();
|
||||
if (newWindow) {
|
||||
try {
|
||||
newWindow.opener = null;
|
||||
} catch {
|
||||
// no-op, Electron can throw
|
||||
}
|
||||
newWindow.location.href = uri;
|
||||
} else {
|
||||
console.warn('Opening link blocked as opener could not be cleared');
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
-23
@@ -55,6 +55,7 @@ import { BufferDecorationRenderer } from 'browser/decorations/BufferDecorationRe
|
||||
import { OverviewRulerRenderer } from 'browser/decorations/OverviewRulerRenderer';
|
||||
import { DecorationService } from 'common/services/DecorationService';
|
||||
import { IDecorationService } from 'common/services/Services';
|
||||
import { OscLinkProvider } from 'browser/OscLinkProvider';
|
||||
|
||||
// Let it work inside Node.js for automated testing purposes.
|
||||
const document: Document = (typeof window !== 'undefined') ? window.document : null as any;
|
||||
@@ -163,6 +164,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
this._setup();
|
||||
|
||||
this.linkifier2 = this.register(this._instantiationService.createInstance(Linkifier2));
|
||||
this.linkifier2.registerLinkProvider(this._instantiationService.createInstance(OscLinkProvider));
|
||||
this._decorationService = this._instantiationService.createInstance(DecorationService);
|
||||
this._instantiationService.setService(IDecorationService, this._decorationService);
|
||||
|
||||
@@ -211,22 +213,20 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
acc = 'ansi';
|
||||
ident = '4;' + req.index;
|
||||
}
|
||||
if (acc) {
|
||||
switch (req.type) {
|
||||
case ColorRequestType.REPORT:
|
||||
const channels = color.toColorRGB(acc === 'ansi'
|
||||
? this._colorManager.colors.ansi[req.index]
|
||||
: this._colorManager.colors[acc]);
|
||||
this.coreService.triggerDataEvent(`${C0.ESC}]${ident};${toRgbString(channels)}${C1_ESCAPED.ST}`);
|
||||
break;
|
||||
case ColorRequestType.SET:
|
||||
if (acc === 'ansi') this._colorManager.colors.ansi[req.index] = rgba.toColor(...req.color);
|
||||
else this._colorManager.colors[acc] = rgba.toColor(...req.color);
|
||||
break;
|
||||
case ColorRequestType.RESTORE:
|
||||
this._colorManager.restoreColor(req.index);
|
||||
break;
|
||||
}
|
||||
switch (req.type) {
|
||||
case ColorRequestType.REPORT:
|
||||
const channels = color.toColorRGB(acc === 'ansi'
|
||||
? this._colorManager.colors.ansi[req.index]
|
||||
: this._colorManager.colors[acc]);
|
||||
this.coreService.triggerDataEvent(`${C0.ESC}]${ident};${toRgbString(channels)}${C1_ESCAPED.ST}`);
|
||||
break;
|
||||
case ColorRequestType.SET:
|
||||
if (acc === 'ansi') this._colorManager.colors.ansi[req.index] = rgba.toColor(...req.color);
|
||||
else this._colorManager.colors[acc] = rgba.toColor(...req.color);
|
||||
break;
|
||||
case ColorRequestType.RESTORE:
|
||||
this._colorManager.restoreColor(req.index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
this._renderService?.setColors(this._colorManager.colors);
|
||||
@@ -645,7 +645,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
// send event to CoreMouseService
|
||||
function sendEvent(ev: MouseEvent | WheelEvent): boolean {
|
||||
// get mouse coordinates
|
||||
const pos = self._mouseService!.getRawByteCoords(ev, self.screenElement!, self.cols, self.rows);
|
||||
const pos = self._mouseService!.getMouseReportCoords(ev, self.screenElement!);
|
||||
if (!pos) {
|
||||
return false;
|
||||
}
|
||||
@@ -699,8 +699,10 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
}
|
||||
|
||||
return self.coreMouseService.triggerMouseEvent({
|
||||
col: pos.x - 33, // FIXME: why -33 here?
|
||||
row: pos.y - 33,
|
||||
col: pos.col,
|
||||
row: pos.row,
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
button: but,
|
||||
action,
|
||||
ctrl: ev.ctrlKey,
|
||||
@@ -1320,13 +1322,13 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
|
||||
switch (type) {
|
||||
case WindowsOptionsReportType.GET_WIN_SIZE_PIXELS:
|
||||
const canvasWidth = this._renderService.dimensions.scaledCanvasWidth.toFixed(0);
|
||||
const canvasHeight = this._renderService.dimensions.scaledCanvasHeight.toFixed(0);
|
||||
const canvasWidth = this._renderService.dimensions.canvasWidth.toFixed(0);
|
||||
const canvasHeight = this._renderService.dimensions.canvasHeight.toFixed(0);
|
||||
this.coreService.triggerDataEvent(`${C0.ESC}[4;${canvasHeight};${canvasWidth}t`);
|
||||
break;
|
||||
case WindowsOptionsReportType.GET_CELL_SIZE_PIXELS:
|
||||
const cellWidth = this._renderService.dimensions.scaledCellWidth.toFixed(0);
|
||||
const cellHeight = this._renderService.dimensions.scaledCellHeight.toFixed(0);
|
||||
const cellWidth = this._renderService.dimensions.actualCellWidth.toFixed(0);
|
||||
const cellHeight = this._renderService.dimensions.actualCellHeight.toFixed(0);
|
||||
this.coreService.triggerDataEvent(`${C0.ESC}[6;${cellHeight};${cellWidth}t`);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { IDisposable, IMarker, ILinkProvider, IDecorationOptions, IDecoration }
|
||||
import { IEvent, EventEmitter } from 'common/EventEmitter';
|
||||
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services';
|
||||
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types';
|
||||
import { IColorSet, ITerminal, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper, CharacterJoinerHandler, IRenderDebouncer, IBufferRange } from 'browser/Types';
|
||||
import { IColorSet, ITerminal, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper, CharacterJoinerHandler, IBufferRange } from 'browser/Types';
|
||||
import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types';
|
||||
import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, ITerminalOptions } from 'common/Types';
|
||||
import { Buffer } from 'common/buffer/Buffer';
|
||||
@@ -359,7 +359,7 @@ export class MockMouseService implements IMouseService {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
public getRawByteCoords(event: MouseEvent, element: HTMLElement, colCount: number, rowCount: number): { x: number, y: number } | undefined {
|
||||
public getMouseReportCoords(event: MouseEvent, element: HTMLElement): { col: number, row: number, x: number, y: number } | undefined {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ColorZoneStore, IColorZone, IColorZoneStore } from 'browser/decorations
|
||||
import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
import { IRenderService } from 'browser/services/Services';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { IBufferService, IDecorationService, IInternalDecoration, IOptionsService } from 'common/services/Services';
|
||||
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
|
||||
// Helper objects to avoid excessive calculation and garbage collection during rendering. These are
|
||||
// static values for each render and can be accessed using the decoration position as the key.
|
||||
|
||||
@@ -48,17 +48,3 @@ export function getCoords(window: Pick<Window, 'getComputedStyle'>, event: {clie
|
||||
|
||||
return coords;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets coordinates within the terminal for a particular mouse event, wrapping
|
||||
* them to the bounds of the terminal and adding 32 to both the x and y values
|
||||
* as expected by xterm.
|
||||
*/
|
||||
export function getRawByteCoords(coords: [number, number] | undefined): { x: number, y: number } | undefined {
|
||||
if (!coords) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// xterm sends raw bytes and starts at 32 (SP) for each.
|
||||
return { x: coords[0] + 32, y: coords[1] + 32 };
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal as ITerminalApi, IMarker, IDisposable, ILocalizableStrings, ITerminalAddon, IBufferNamespace as IBufferNamespaceApi, IParser, ILinkProvider, IUnicodeHandling, FontWeight, IModes, IDecorationOptions, IDecoration } from 'xterm';
|
||||
import { Terminal as ITerminalApi, IMarker, IDisposable, ILocalizableStrings, ITerminalAddon, IBufferNamespace as IBufferNamespaceApi, IParser, ILinkProvider, IUnicodeHandling, IModes, IDecorationOptions, IDecoration } from 'xterm';
|
||||
import { IBufferRange, ITerminal } from 'browser/Types';
|
||||
import { Terminal as TerminalCore } from 'browser/Terminal';
|
||||
import * as Strings from 'browser/LocalizableStrings';
|
||||
|
||||
@@ -349,25 +349,27 @@ const enum VectorType {
|
||||
* not been patched with powerline characters and also to get pixel perfect rendering as rendering
|
||||
* issues can occur around AA/SPAA.
|
||||
*
|
||||
* The line variants draw beyond the cell and get clipped to ensure the end of the line is not visible.
|
||||
*
|
||||
* Original symbols defined in https://github.com/powerline/fontpatcher
|
||||
*/
|
||||
export const powerlineDefinitions: { [index: string]: IVectorShape } = {
|
||||
// Right triangle solid
|
||||
'\u{E0B0}': { d: 'M0,0 L1,.5 L0,1', type: VectorType.FILL },
|
||||
'\u{E0B0}': { d: 'M0,0 L1,.5 L0,1', type: VectorType.FILL, rightPadding: 2 },
|
||||
// Right triangle line
|
||||
'\u{E0B1}': { d: 'M0,0 L1,.5 L0,1', type: VectorType.STROKE, leftPadding: window.devicePixelRatio / 2, rightPadding: window.devicePixelRatio / 2 },
|
||||
'\u{E0B1}': { d: 'M-1,-.5 L1,.5 L-1,1.5', type: VectorType.STROKE, leftPadding: 1, rightPadding: 1 },
|
||||
// Left triangle solid
|
||||
'\u{E0B2}': { d: 'M1,0 L0,.5 L1,1', type: VectorType.FILL },
|
||||
'\u{E0B2}': { d: 'M1,0 L0,.5 L1,1', type: VectorType.FILL, leftPadding: 2 },
|
||||
// Left triangle line
|
||||
'\u{E0B3}': { d: 'M1,0 L0,.5 L1,1', type: VectorType.STROKE, leftPadding: window.devicePixelRatio / 2, rightPadding: window.devicePixelRatio / 2 },
|
||||
'\u{E0B3}': { d: 'M2,-.5 L0,.5 L2,1.5', type: VectorType.STROKE, leftPadding: 1, rightPadding: 1 },
|
||||
// Right semi-circle solid,
|
||||
'\u{E0B4}': { d: 'M0,0 L0,1 C0.552,1,1,0.776,1,.5 C1,0.224,0.552,0,0,0', type: VectorType.FILL },
|
||||
'\u{E0B4}': { d: 'M0,0 L0,1 C0.552,1,1,0.776,1,.5 C1,0.224,0.552,0,0,0', type: VectorType.FILL, rightPadding: 1 },
|
||||
// Right semi-circle line,
|
||||
'\u{E0B5}': { d: 'M0,1 C0.552,1,1,0.776,1,.5 C1,0.224,0.552,0,0,0', type: VectorType.STROKE, rightPadding: window.devicePixelRatio / 2 },
|
||||
'\u{E0B5}': { d: 'M0,1 C0.552,1,1,0.776,1,.5 C1,0.224,0.552,0,0,0', type: VectorType.STROKE, rightPadding: 1 },
|
||||
// Left semi-circle solid,
|
||||
'\u{E0B6}': { d: 'M1,0 L1,1 C0.448,1,0,0.776,0,.5 C0,0.224,0.448,0,1,0', type: VectorType.FILL },
|
||||
'\u{E0B6}': { d: 'M1,0 L1,1 C0.448,1,0,0.776,0,.5 C0,0.224,0.448,0,1,0', type: VectorType.FILL, leftPadding: 1 },
|
||||
// Left semi-circle line,
|
||||
'\u{E0B7}': { d: 'M1,1 C0.448,1,0,0.776,0,.5 C0,0.224,0.448,0,1,0', type: VectorType.STROKE, leftPadding: window.devicePixelRatio / 2 }
|
||||
'\u{E0B7}': { d: 'M1,1 C0.448,1,0,0.776,0,.5 C0,0.224,0.448,0,1,0', type: VectorType.STROKE, leftPadding: 1 }
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -380,7 +382,8 @@ export function tryDrawCustomChar(
|
||||
xOffset: number,
|
||||
yOffset: number,
|
||||
scaledCellWidth: number,
|
||||
scaledCellHeight: number
|
||||
scaledCellHeight: number,
|
||||
fontSize: number
|
||||
): boolean {
|
||||
const blockElementDefinition = blockElementDefinitions[c];
|
||||
if (blockElementDefinition) {
|
||||
@@ -402,7 +405,7 @@ export function tryDrawCustomChar(
|
||||
|
||||
const powerlineDefinition = powerlineDefinitions[c];
|
||||
if (powerlineDefinition) {
|
||||
drawPowerlineChar(ctx, powerlineDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight);
|
||||
drawPowerlineChar(ctx, powerlineDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight, fontSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -562,7 +565,7 @@ function drawBoxDrawingChar(
|
||||
if (!args[0] || !args[1]) {
|
||||
continue;
|
||||
}
|
||||
f(ctx, translateArgs(args, scaledCellWidth, scaledCellHeight, xOffset, yOffset));
|
||||
f(ctx, translateArgs(args, scaledCellWidth, scaledCellHeight, xOffset, yOffset, true));
|
||||
}
|
||||
ctx.stroke();
|
||||
ctx.closePath();
|
||||
@@ -575,10 +578,13 @@ function drawPowerlineChar(
|
||||
xOffset: number,
|
||||
yOffset: number,
|
||||
scaledCellWidth: number,
|
||||
scaledCellHeight: number
|
||||
scaledCellHeight: number,
|
||||
fontSize: number
|
||||
): void {
|
||||
ctx.beginPath();
|
||||
ctx.lineWidth = window.devicePixelRatio;
|
||||
// Scale the stroke with DPR and font size
|
||||
const cssLineWidth = fontSize / 12;
|
||||
ctx.lineWidth = window.devicePixelRatio * cssLineWidth;
|
||||
for (const instruction of charDefinition.d.split(' ')) {
|
||||
const type = instruction[0];
|
||||
const f = svgToCanvasInstructionMap[type];
|
||||
@@ -590,7 +596,16 @@ function drawPowerlineChar(
|
||||
if (!args[0] || !args[1]) {
|
||||
continue;
|
||||
}
|
||||
f(ctx, translateArgs(args, scaledCellWidth, scaledCellHeight, xOffset, yOffset, charDefinition.leftPadding, charDefinition.rightPadding));
|
||||
f(ctx, translateArgs(
|
||||
args,
|
||||
scaledCellWidth,
|
||||
scaledCellHeight,
|
||||
xOffset,
|
||||
yOffset,
|
||||
false,
|
||||
(charDefinition.leftPadding ?? 0) * (cssLineWidth / 2),
|
||||
(charDefinition.rightPadding ?? 0) * (cssLineWidth / 2)
|
||||
));
|
||||
}
|
||||
if (charDefinition.type === VectorType.STROKE) {
|
||||
ctx.strokeStyle = ctx.fillStyle;
|
||||
@@ -611,7 +626,7 @@ const svgToCanvasInstructionMap: { [index: string]: any } = {
|
||||
'M': (ctx: CanvasRenderingContext2D, args: number[]) => ctx.moveTo(args[0], args[1])
|
||||
};
|
||||
|
||||
function translateArgs(args: string[], cellWidth: number, cellHeight: number, xOffset: number, yOffset: number, leftPadding: number = 0, rightPadding: number = 0): number[] {
|
||||
function translateArgs(args: string[], cellWidth: number, cellHeight: number, xOffset: number, yOffset: number, doClamp: boolean, leftPadding: number = 0, rightPadding: number = 0): number[] {
|
||||
const result = args.map(e => parseFloat(e) || parseInt(e));
|
||||
|
||||
if (result.length < 2) {
|
||||
@@ -623,7 +638,7 @@ function translateArgs(args: string[], cellWidth: number, cellHeight: number, xO
|
||||
result[x] *= cellWidth - (leftPadding * window.devicePixelRatio) - (rightPadding * window.devicePixelRatio);
|
||||
// Ensure coordinate doesn't escape cell bounds and round to the nearest 0.5 to ensure a crisp
|
||||
// line at 100% devicePixelRatio
|
||||
if (result[x] !== 0) {
|
||||
if (doClamp && result[x] !== 0) {
|
||||
result[x] = clamp(Math.round(result[x] + 0.5) - 0.5, cellWidth, 0);
|
||||
}
|
||||
// Apply the cell's offset (ie. x*cellWidth)
|
||||
@@ -635,7 +650,7 @@ function translateArgs(args: string[], cellWidth: number, cellHeight: number, xO
|
||||
result[y] *= cellHeight;
|
||||
// Ensure coordinate doesn't escape cell bounds and round to the nearest 0.5 to ensure a crisp
|
||||
// line at 100% devicePixelRatio
|
||||
if (result[y] !== 0) {
|
||||
if (doClamp && result[y] !== 0) {
|
||||
result[y] = clamp(Math.round(result[y] + 0.5) - 0.5, cellHeight, 0);
|
||||
}
|
||||
// Apply the cell's offset (ie. x*cellHeight)
|
||||
|
||||
@@ -24,10 +24,12 @@ export function observeDevicePixelDimensions(element: HTMLElement, callback: (de
|
||||
return;
|
||||
}
|
||||
|
||||
callback(
|
||||
entry.devicePixelContentBoxSize[0].inlineSize,
|
||||
entry.devicePixelContentBoxSize[0].blockSize
|
||||
);
|
||||
// Fire the callback, ignore events where the dimensions are 0x0 as the canvas is likely hidden
|
||||
const width = entry.devicePixelContentBoxSize[0].inlineSize;
|
||||
const height = entry.devicePixelContentBoxSize[0].blockSize;
|
||||
if (width > 0 && height > 0) {
|
||||
callback(width, height);
|
||||
}
|
||||
});
|
||||
observer.observe(element, { box: ['device-pixel-content-box'] } as any);
|
||||
return toDisposable(() => observer?.disconnect());
|
||||
|
||||
@@ -14,11 +14,15 @@ export function isPowerlineGlyph(codepoint: number): boolean {
|
||||
// Only return true for Powerline symbols which require
|
||||
// different padding and should be excluded from minimum contrast
|
||||
// ratio standards
|
||||
return 0xE0A4 <= codepoint && codepoint <= 0xE0D6;
|
||||
return 0xE0A4 <= codepoint && codepoint <= 0xE0D6;
|
||||
}
|
||||
|
||||
export function isRestrictedPowerlineGlyph(codepoint: number): boolean {
|
||||
return 0xE0B0 <= codepoint && codepoint <= 0xE0B7;
|
||||
}
|
||||
|
||||
function isBoxOrBlockGlyph(codepoint: number): boolean {
|
||||
return (0x2500 <= codepoint && codepoint <= 0x259F);
|
||||
return 0x2500 <= codepoint && codepoint <= 0x259F;
|
||||
}
|
||||
|
||||
export function excludeFromContrastRatioDemands(codepoint: number): boolean {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { IColorSet, ILinkifierEvent, ILinkifier2 } from 'browser/Types';
|
||||
import { ICharSizeService } from 'browser/services/Services';
|
||||
import { IOptionsService, IBufferService, IInstantiationService, IDecorationService } from 'common/services/Services';
|
||||
import { IOptionsService, IBufferService, IInstantiationService } from 'common/services/Services';
|
||||
import { EventEmitter, IEvent } from 'common/EventEmitter';
|
||||
import { color } from 'common/Color';
|
||||
import { removeElementFromParent } from 'browser/Dom';
|
||||
|
||||
@@ -204,9 +204,9 @@ export class DomRendererRowFactory {
|
||||
let bgOverride: IColor | undefined;
|
||||
let fgOverride: IColor | undefined;
|
||||
let isTop = false;
|
||||
for (const d of this._decorationService.getDecorationsAtCell(x, row)) {
|
||||
this._decorationService.forEachDecorationAtCell(x, row, undefined, d => {
|
||||
if (d.options.layer !== 'top' && isTop) {
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
if (d.backgroundColorRGB) {
|
||||
bgColorMode = Attributes.CM_RGB;
|
||||
@@ -219,7 +219,7 @@ export class DomRendererRowFactory {
|
||||
fgOverride = d.foregroundColorRGB;
|
||||
}
|
||||
isTop = d.options.layer === 'top';
|
||||
}
|
||||
});
|
||||
|
||||
// Apply selection foreground if applicable
|
||||
const isInSelection = this._isCellInSelection(x, row);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { ICharSizeService, IRenderService, IMouseService } from './Services';
|
||||
import { getCoords, getRawByteCoords } from 'browser/input/Mouse';
|
||||
import { getCoords, getCoordsRelativeToElement } from 'browser/input/Mouse';
|
||||
|
||||
export class MouseService implements IMouseService {
|
||||
public serviceBrand: undefined;
|
||||
@@ -29,8 +29,24 @@ export class MouseService implements IMouseService {
|
||||
);
|
||||
}
|
||||
|
||||
public getRawByteCoords(event: MouseEvent, element: HTMLElement, colCount: number, rowCount: number): { x: number, y: number } | undefined {
|
||||
const coords = this.getCoords(event, element, colCount, rowCount);
|
||||
return getRawByteCoords(coords);
|
||||
public getMouseReportCoords(event: MouseEvent, element: HTMLElement): { col: number, row: number, x: number, y: number } | undefined {
|
||||
const coords = getCoordsRelativeToElement(window, event, element);
|
||||
|
||||
// due to rounding issues in zoom states pixel values might be negative or overflow actual canvas
|
||||
// ignore those events effectively narrowing mouse area a tiny bit at the edges
|
||||
if (!this._charSizeService.hasValidSize
|
||||
|| coords[0] < 0
|
||||
|| coords[1] < 0
|
||||
|| coords[0] >= this._renderService.dimensions.canvasWidth
|
||||
|| coords[1] >= this._renderService.dimensions.canvasHeight) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
col: Math.floor(coords[0] / this._renderService.dimensions.actualCellWidth),
|
||||
row: Math.floor(coords[1] / this._renderService.dimensions.actualCellHeight),
|
||||
x: Math.floor(coords[0]),
|
||||
y: Math.floor(coords[1])
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { EventEmitter, IEvent } from 'common/EventEmitter';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { ScreenDprMonitor } from 'browser/ScreenDprMonitor';
|
||||
import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
import { IColorSet, IRenderDebouncer, IRenderDebouncerWithCallback } from 'browser/Types';
|
||||
import { IColorSet, IRenderDebouncerWithCallback } from 'browser/Types';
|
||||
import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services';
|
||||
import { ICharSizeService, IRenderService } from 'browser/services/Services';
|
||||
|
||||
|
||||
@@ -5,12 +5,10 @@
|
||||
|
||||
import { IEvent } from 'common/EventEmitter';
|
||||
import { IRenderDimensions, IRenderer } from 'browser/renderer/Types';
|
||||
import { IColorSet, IRenderDebouncer } from 'browser/Types';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { ISelectionRedrawRequestEvent as ISelectionRequestRedrawEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
|
||||
import { createDecorator } from 'common/services/ServiceRegistry';
|
||||
import { IDisposable } from 'common/Types';
|
||||
import { IDecorationOptions, IDecoration } from 'xterm';
|
||||
import { IBufferService } from 'common/services/Services';
|
||||
|
||||
export const ICharSizeService = createDecorator<ICharSizeService>('CharSizeService');
|
||||
export interface ICharSizeService {
|
||||
@@ -37,7 +35,7 @@ export interface IMouseService {
|
||||
serviceBrand: undefined;
|
||||
|
||||
getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined;
|
||||
getRawByteCoords(event: MouseEvent, element: HTMLElement, colCount: number, rowCount: number): { x: number, y: number } | undefined;
|
||||
getMouseReportCoords(event: MouseEvent, element: HTMLElement): { col: number, row: number, x: number, y: number } | undefined;
|
||||
}
|
||||
|
||||
export const IRenderService = createDecorator<IRenderService>('RenderService');
|
||||
|
||||
@@ -22,12 +22,12 @@
|
||||
*/
|
||||
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, ICoreMouseService, IUnicodeService, IDirtyRowService, LogLevelEnum, ITerminalOptions } from 'common/services/Services';
|
||||
import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, ICoreMouseService, IUnicodeService, IDirtyRowService, LogLevelEnum, ITerminalOptions, IOscLinkService } from 'common/services/Services';
|
||||
import { InstantiationService } from 'common/services/InstantiationService';
|
||||
import { LogService } from 'common/services/LogService';
|
||||
import { BufferService, MINIMUM_COLS, MINIMUM_ROWS } from 'common/services/BufferService';
|
||||
import { OptionsService } from 'common/services/OptionsService';
|
||||
import { IDisposable, IBufferLine, IAttributeData, ICoreTerminal, IKeyboardEvent, IScrollEvent, ScrollSource, ITerminalOptions as IPublicTerminalOptions } from 'common/Types';
|
||||
import { IDisposable, IAttributeData, ICoreTerminal, IScrollEvent, ScrollSource } from 'common/Types';
|
||||
import { CoreService } from 'common/services/CoreService';
|
||||
import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter';
|
||||
import { CoreMouseService } from 'common/services/CoreMouseService';
|
||||
@@ -39,6 +39,7 @@ import { IFunctionIdentifier, IParams } from 'common/parser/Types';
|
||||
import { IBufferSet } from 'common/buffer/Types';
|
||||
import { InputHandler } from 'common/InputHandler';
|
||||
import { WriteBuffer } from 'common/input/WriteBuffer';
|
||||
import { OscLinkService } from 'common/services/OscLinkService';
|
||||
|
||||
// Only trigger this warning a single time per session
|
||||
let hasWriteSyncWarnHappened = false;
|
||||
@@ -49,6 +50,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
protected readonly _logService: ILogService;
|
||||
protected readonly _charsetService: ICharsetService;
|
||||
protected readonly _dirtyRowService: IDirtyRowService;
|
||||
protected readonly _oscLinkService: IOscLinkService;
|
||||
|
||||
public readonly coreMouseService: ICoreMouseService;
|
||||
public readonly coreService: ICoreService;
|
||||
@@ -118,9 +120,11 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
this._instantiationService.setService(IUnicodeService, this.unicodeService);
|
||||
this._charsetService = this._instantiationService.createInstance(CharsetService);
|
||||
this._instantiationService.setService(ICharsetService, this._charsetService);
|
||||
this._oscLinkService = this._instantiationService.createInstance(OscLinkService);
|
||||
this._instantiationService.setService(IOscLinkService, this._oscLinkService);
|
||||
|
||||
// Register input handler and handle/forward events
|
||||
this._inputHandler = new InputHandler(this._bufferService, this._charsetService, this.coreService, this._dirtyRowService, this._logService, this.optionsService, this.coreMouseService, this.unicodeService);
|
||||
this._inputHandler = new InputHandler(this._bufferService, this._charsetService, this.coreService, this._dirtyRowService, this._logService, this.optionsService, this._oscLinkService, this.coreMouseService, this.unicodeService);
|
||||
this.register(forwardEvent(this._inputHandler.onLineFeed, this._onLineFeed));
|
||||
this.register(this._inputHandler);
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { CellData } from 'common/buffer/CellData';
|
||||
import { Attributes, UnderlineStyle } from 'common/buffer/Constants';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { Params } from 'common/parser/Params';
|
||||
import { MockCoreService, MockBufferService, MockDirtyRowService, MockOptionsService, MockLogService, MockCoreMouseService, MockCharsetService, MockUnicodeService } from 'common/TestUtils.test';
|
||||
import { MockCoreService, MockBufferService, MockDirtyRowService, MockOptionsService, MockLogService, MockCoreMouseService, MockCharsetService, MockUnicodeService, MockOscLinkService } from 'common/TestUtils.test';
|
||||
import { IBufferService, ICoreService } from 'common/services/Services';
|
||||
import { DEFAULT_OPTIONS } from 'common/services/OptionsService';
|
||||
import { clone } from 'common/Clone';
|
||||
@@ -67,7 +67,7 @@ describe('InputHandler', () => {
|
||||
bufferService.resize(80, 30);
|
||||
coreService = new CoreService(() => { }, bufferService, new MockLogService(), optionsService);
|
||||
|
||||
inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockCoreMouseService(), new MockUnicodeService());
|
||||
inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
|
||||
});
|
||||
|
||||
describe('SL/SR/DECIC/DECDC', () => {
|
||||
@@ -236,7 +236,7 @@ describe('InputHandler', () => {
|
||||
describe('setMode', () => {
|
||||
it('should toggle bracketedPasteMode', () => {
|
||||
const coreService = new MockCoreService();
|
||||
const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService());
|
||||
const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
|
||||
// Set bracketed paste mode
|
||||
inputHandler.setModePrivate(Params.fromArray([2004]));
|
||||
assert.equal(coreService.decPrivateModes.bracketedPasteMode, true);
|
||||
@@ -261,6 +261,7 @@ describe('InputHandler', () => {
|
||||
new MockDirtyRowService(),
|
||||
new MockLogService(),
|
||||
new MockOptionsService(),
|
||||
new MockOscLinkService(),
|
||||
new MockCoreMouseService(),
|
||||
new MockUnicodeService()
|
||||
);
|
||||
@@ -307,6 +308,7 @@ describe('InputHandler', () => {
|
||||
new MockDirtyRowService(),
|
||||
new MockLogService(),
|
||||
new MockOptionsService(),
|
||||
new MockOscLinkService(),
|
||||
new MockCoreMouseService(),
|
||||
new MockUnicodeService()
|
||||
);
|
||||
@@ -357,6 +359,7 @@ describe('InputHandler', () => {
|
||||
new MockDirtyRowService(),
|
||||
new MockLogService(),
|
||||
new MockOptionsService(),
|
||||
new MockOscLinkService(),
|
||||
new MockCoreMouseService(),
|
||||
new MockUnicodeService()
|
||||
);
|
||||
@@ -394,6 +397,7 @@ describe('InputHandler', () => {
|
||||
new MockDirtyRowService(),
|
||||
new MockLogService(),
|
||||
new MockOptionsService(),
|
||||
new MockOscLinkService(),
|
||||
new MockCoreMouseService(),
|
||||
new MockUnicodeService()
|
||||
);
|
||||
@@ -444,6 +448,7 @@ describe('InputHandler', () => {
|
||||
new MockDirtyRowService(),
|
||||
new MockLogService(),
|
||||
new MockOptionsService(),
|
||||
new MockOscLinkService(),
|
||||
new MockCoreMouseService(),
|
||||
new MockUnicodeService()
|
||||
);
|
||||
@@ -570,6 +575,7 @@ describe('InputHandler', () => {
|
||||
new MockDirtyRowService(),
|
||||
new MockLogService(),
|
||||
new MockOptionsService(),
|
||||
new MockOscLinkService(),
|
||||
new MockCoreMouseService(),
|
||||
new MockUnicodeService()
|
||||
);
|
||||
@@ -593,7 +599,7 @@ describe('InputHandler', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
bufferService = new MockBufferService(80, 30);
|
||||
handler = new TestInputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService());
|
||||
handler = new TestInputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
|
||||
});
|
||||
it('should handle DECSET/DECRST 47 (alt screen buffer)', async () => {
|
||||
await handler.parseP('\x1b[?47h\r\n\x1b[31mJUNK\x1b[?47lTEST');
|
||||
@@ -790,7 +796,7 @@ describe('InputHandler', () => {
|
||||
describe('colon notation', () => {
|
||||
let inputHandler2: TestInputHandler;
|
||||
beforeEach(() => {
|
||||
inputHandler2 = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockCoreMouseService(), new MockUnicodeService());
|
||||
inputHandler2 = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
|
||||
});
|
||||
describe('should equal to semicolon', () => {
|
||||
it('CSI 38:2::50:100:150 m', async () => {
|
||||
@@ -2156,7 +2162,7 @@ describe('InputHandler - async handlers', () => {
|
||||
coreService = new CoreService(() => { }, bufferService, new MockLogService(), optionsService);
|
||||
coreService.onData(data => { console.log(data); });
|
||||
|
||||
inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockCoreMouseService(), new MockUnicodeService());
|
||||
inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
|
||||
});
|
||||
|
||||
it('async CUP with CPR check', async () => {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IInputHandler, IAttributeData, IDisposable, IWindowOptions, IColorEvent, IParseStack, ColorIndex, ColorRequestType } from 'common/Types';
|
||||
import { IInputHandler, IAttributeData, IDisposable, IWindowOptions, IColorEvent, IParseStack, ColorIndex, ColorRequestType, IOscLinkData } from 'common/Types';
|
||||
import { C0, C1 } from 'common/data/EscapeSequences';
|
||||
import { CHARSETS, DEFAULT_CHARSET } from 'common/data/Charsets';
|
||||
import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser';
|
||||
@@ -17,7 +17,7 @@ import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IFunctionId
|
||||
import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content, UnderlineStyle } from 'common/buffer/Constants';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, LogLevelEnum } from 'common/services/Services';
|
||||
import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, LogLevelEnum, IOscLinkService } from 'common/services/Services';
|
||||
import { OscHandler } from 'common/parser/OscParser';
|
||||
import { DcsHandler } from 'common/parser/DcsParser';
|
||||
import { IBuffer } from 'common/buffer/Types';
|
||||
@@ -214,8 +214,6 @@ class DECRQSS implements IDcsHandler {
|
||||
* @vt: #N DCS XTSETTCAP "Set Terminfo Data" "DCS + p Pt ST" "Set Terminfo Data."
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* The terminal's standard implementation of IInputHandler, this handles all
|
||||
* input from the Parser.
|
||||
@@ -230,6 +228,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
private _workCell: CellData = new CellData();
|
||||
private _windowTitle = '';
|
||||
private _iconName = '';
|
||||
private _currentLinkId?: number;
|
||||
protected _windowTitleStack: string[] = [];
|
||||
protected _iconNameStack: string[] = [];
|
||||
|
||||
@@ -281,6 +280,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
private readonly _dirtyRowService: IDirtyRowService,
|
||||
private readonly _logService: ILogService,
|
||||
private readonly _optionsService: IOptionsService,
|
||||
private readonly _oscLinkService: IOscLinkService,
|
||||
private readonly _coreMouseService: ICoreMouseService,
|
||||
private readonly _unicodeService: IUnicodeService,
|
||||
private readonly _parser: IEscapeSequenceParser = new EscapeSequenceParser()
|
||||
@@ -403,6 +403,8 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
// 5 - Change Special Color Number
|
||||
// 6 - Enable/disable Special Color Number c
|
||||
// 7 - current directory? (not in xterm spec, see https://gitlab.com/gnachman/iterm2/issues/3939)
|
||||
// 8 - create hyperlink (not in xterm spec, see https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda)
|
||||
this._parser.registerOscHandler(8, new OscHandler(data => this.setHyperlink(data)));
|
||||
// 10 - Change VT100 text foreground color to Pt.
|
||||
this._parser.registerOscHandler(10, new OscHandler(data => this.setOrReportFgColor(data)));
|
||||
// 11 - Change VT100 text background color to Pt.
|
||||
@@ -637,6 +639,9 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
if (screenReaderMode) {
|
||||
this._onA11yChar.fire(stringFromCodePoint(code));
|
||||
}
|
||||
if (this._currentLinkId !== undefined) {
|
||||
this._oscLinkService.addLineToLink(this._currentLinkId, this._activeBuffer.ybase + this._activeBuffer.y);
|
||||
}
|
||||
|
||||
// insert combining char at last cursor position
|
||||
// this._activeBuffer.x should never be 0 for a combining char
|
||||
@@ -1903,6 +1908,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
* | 1005 | Enable UTF-8 Mouse Mode. | #N |
|
||||
* | 1006 | Enable SGR Mouse Mode. | #Y |
|
||||
* | 1015 | Enable urxvt Mouse Mode. | #N |
|
||||
* | 1016 | Enable SGR-Pixels Mouse Mode. | #Y |
|
||||
* | 1047 | Use Alternate Screen Buffer. | #Y |
|
||||
* | 1048 | Save cursor as in DECSC. | #Y |
|
||||
* | 1049 | Save cursor and switch to alternate buffer clearing it. | #P[Does not clear the alternate buffer.] |
|
||||
@@ -1984,6 +1990,9 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
case 1015: // urxvt ext mode mouse - removed in #2507
|
||||
this._logService.debug('DECSET 1015 not supported (see #2507)');
|
||||
break;
|
||||
case 1016: // sgr pixels mode mouse
|
||||
this._coreMouseService.activeEncoding = 'SGR_PIXELS';
|
||||
break;
|
||||
case 25: // show cursor
|
||||
this._coreService.isCursorHidden = false;
|
||||
break;
|
||||
@@ -2144,6 +2153,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
* | 1005 | Disable UTF-8 Mouse Mode. | #N |
|
||||
* | 1006 | Disable SGR Mouse Mode. | #Y |
|
||||
* | 1015 | Disable urxvt Mouse Mode. | #N |
|
||||
* | 1006 | Disable SGR-Pixels Mouse Mode. | #Y |
|
||||
* | 1047 | Use Normal Screen Buffer (clearing screen if in alt). | #Y |
|
||||
* | 1048 | Restore cursor as in DECRC. | #Y |
|
||||
* | 1049 | Use Normal Screen Buffer and restore cursor. | #Y |
|
||||
@@ -2205,6 +2215,9 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
case 1015: // urxvt ext mode mouse - removed in #2507
|
||||
this._logService.debug('DECRST 1015 not supported (see #2507)');
|
||||
break;
|
||||
case 1006: // sgr pixels mode mouse
|
||||
this._coreMouseService.activeEncoding = 'DEFAULT';
|
||||
break;
|
||||
case 25: // hide cursor
|
||||
this._coreService.isCursorHidden = true;
|
||||
break;
|
||||
@@ -2495,6 +2508,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
} else if (p === 24) {
|
||||
// not underlined
|
||||
attr.fg &= ~FgFlags.UNDERLINE;
|
||||
this._processUnderline(UnderlineStyle.NONE, attr);
|
||||
} else if (p === 25) {
|
||||
// not blink
|
||||
attr.fg &= ~FgFlags.BLINK;
|
||||
@@ -2889,6 +2903,62 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* OSC 8 ; <params> ; <uri> ST - create hyperlink
|
||||
* OSC 8 ; ; ST - finish hyperlink
|
||||
*
|
||||
* Test case:
|
||||
*
|
||||
* ```sh
|
||||
* printf '\e]8;;http://example.com\e\\This is a link\e]8;;\e\\\n'
|
||||
* ```
|
||||
*
|
||||
* @vt: #Y OSC 8 "Create hyperlink" "OSC 8 ; params ; uri BEL" "Create a hyperlink to `uri` using `params`."
|
||||
* `uri` is a hyperlink starting with `http://`, `https://`, `ftp://`, `file://` or `mailto://`. `params` is an
|
||||
* optional list of key=value assignments, separated by the : character. Example: `id=xyz123:foo=bar:baz=quux`.
|
||||
* Currently only the id key is defined. Cells that share the same ID and URI share hover feedback.
|
||||
* Use `OSC 8 ; ; BEL` to finish the current hyperlink.
|
||||
*/
|
||||
public setHyperlink(data: string): boolean {
|
||||
const args = data.split(';');
|
||||
if (args.length < 2) {
|
||||
return false;
|
||||
}
|
||||
if (args[1]) {
|
||||
return this._createHyperlink(args[0], args[1]);
|
||||
}
|
||||
if (args[0]) {
|
||||
return false;
|
||||
}
|
||||
return this._finishHyperlink();
|
||||
}
|
||||
|
||||
private _createHyperlink(params: string, uri: string): boolean {
|
||||
// It's legal to open a new hyperlink without explicitly finishing the previous one
|
||||
if (this._currentLinkId !== undefined) {
|
||||
this._finishHyperlink();
|
||||
}
|
||||
const parsedParams = params.split(':');
|
||||
let id: string | undefined;
|
||||
const idParamIndex = parsedParams.findIndex(e => e.startsWith('id='));
|
||||
if (idParamIndex !== -1) {
|
||||
id = parsedParams[idParamIndex].slice(3) || undefined;
|
||||
}
|
||||
this._curAttrData.extended = this._curAttrData.extended.clone();
|
||||
this._currentLinkId = this._oscLinkService.registerLink({ id, uri });
|
||||
this._curAttrData.extended.urlId = this._currentLinkId;
|
||||
this._curAttrData.updateExtended();
|
||||
return true;
|
||||
}
|
||||
|
||||
private _finishHyperlink(): boolean {
|
||||
this._curAttrData.extended = this._curAttrData.extended.clone();
|
||||
this._curAttrData.extended.urlId = 0;
|
||||
this._curAttrData.updateExtended();
|
||||
this._currentLinkId = undefined;
|
||||
return true;
|
||||
}
|
||||
|
||||
// special colors - OSC 10 | 11 | 12
|
||||
private _specialColors = [ColorIndex.FOREGROUND, ColorIndex.BACKGROUND, ColorIndex.CURSOR];
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { FourKeyMap, TwoKeyMap } from 'common/MultiKeyMap';
|
||||
|
||||
const strictEqual = assert.strictEqual;
|
||||
|
||||
describe('TwoKeyMap', () => {
|
||||
let map: TwoKeyMap<number | string, number | string, string>;
|
||||
|
||||
beforeEach(() => {
|
||||
map = new TwoKeyMap();
|
||||
});
|
||||
|
||||
it('set, get', () => {
|
||||
strictEqual(map.get(1, 2), undefined);
|
||||
map.set(1, 2, 'foo');
|
||||
strictEqual(map.get(1, 2), 'foo');
|
||||
map.set(1, 3, 'bar');
|
||||
strictEqual(map.get(1, 2), 'foo');
|
||||
strictEqual(map.get(1, 3), 'bar');
|
||||
map.set(2, 2, 'foo2');
|
||||
map.set(2, 3, 'bar2');
|
||||
strictEqual(map.get(1, 2), 'foo');
|
||||
strictEqual(map.get(1, 3), 'bar');
|
||||
strictEqual(map.get(2, 2), 'foo2');
|
||||
strictEqual(map.get(2, 3), 'bar2');
|
||||
});
|
||||
it('clear', () => {
|
||||
strictEqual(map.get(1, 2), undefined);
|
||||
map.set(1, 2, 'foo');
|
||||
strictEqual(map.get(1, 2), 'foo');
|
||||
map.clear();
|
||||
strictEqual(map.get(1, 2), undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('FourKeyMap', () => {
|
||||
let map: FourKeyMap<number | string, number | string, number | string, number | string, string>;
|
||||
|
||||
beforeEach(() => {
|
||||
map = new FourKeyMap();
|
||||
});
|
||||
|
||||
it('set, get', () => {
|
||||
strictEqual(map.get(1, 2, 3, 4), undefined);
|
||||
map.set(1, 2, 3, 4, 'foo');
|
||||
strictEqual(map.get(1, 2, 3, 4), 'foo');
|
||||
map.set(1, 3, 3, 4, 'bar');
|
||||
strictEqual(map.get(1, 2, 3, 4), 'foo');
|
||||
strictEqual(map.get(1, 3, 3, 4), 'bar');
|
||||
map.set(2, 2, 3, 4, 'foo2');
|
||||
map.set(2, 3, 3, 4, 'bar2');
|
||||
strictEqual(map.get(1, 2, 3, 4), 'foo');
|
||||
strictEqual(map.get(1, 3, 3, 4), 'bar');
|
||||
strictEqual(map.get(2, 2, 3, 4), 'foo2');
|
||||
strictEqual(map.get(2, 3, 3, 4), 'bar2');
|
||||
});
|
||||
it('clear', () => {
|
||||
strictEqual(map.get(1, 2, 3, 4), undefined);
|
||||
map.set(1, 2, 3, 4, 'foo');
|
||||
strictEqual(map.get(1, 2, 3, 4), 'foo');
|
||||
map.clear();
|
||||
strictEqual(map.get(1, 2, 3, 4), undefined);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) 2022 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
export class TwoKeyMap<TFirst extends string | number, TSecond extends string | number, TValue> {
|
||||
private _data: { [bg: string | number]: { [fg: string | number]: TValue | undefined } | undefined } = {};
|
||||
|
||||
public set(first: TFirst, second: TSecond, value: TValue): void {
|
||||
if (!this._data[first]) {
|
||||
this._data[first] = {};
|
||||
}
|
||||
this._data[first as string | number]![second] = value;
|
||||
}
|
||||
|
||||
public get(first: TFirst, second: TSecond): TValue | undefined {
|
||||
return this._data[first as string | number] ? this._data[first as string | number]![second] : undefined;
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._data = {};
|
||||
}
|
||||
}
|
||||
|
||||
export class FourKeyMap<TFirst extends string | number, TSecond extends string | number, TThird extends string | number, TFourth extends string | number, TValue> {
|
||||
private _data: TwoKeyMap<TFirst, TSecond, TwoKeyMap<TThird, TFourth, TValue>> = new TwoKeyMap();
|
||||
|
||||
public set(first: TFirst, second: TSecond, third: TThird, fourth: TFourth, value: TValue): void {
|
||||
if (!this._data.get(first, second)) {
|
||||
this._data.set(first, second, new TwoKeyMap());
|
||||
}
|
||||
this._data.get(first, second)!.set(third, fourth, value);
|
||||
}
|
||||
|
||||
public get(first: TFirst, second: TSecond, third: TThird, fourth: TFourth): TValue | undefined {
|
||||
return this._data.get(first, second)?.get(third, fourth);
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._data.clear();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,9 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
// Work variables to avoid garbage collection.
|
||||
let i = 0;
|
||||
|
||||
/**
|
||||
* A generic list that is maintained in sorted order and allows values with duplicate keys. This
|
||||
* list is based on binary search and as such locating a key will take O(log n) amortized, this
|
||||
@@ -25,7 +28,7 @@ export class SortedList<T> {
|
||||
this._array.push(value);
|
||||
return;
|
||||
}
|
||||
const i = this._search(this._getKey(value), 0, this._array.length - 1);
|
||||
i = this._search(this._getKey(value), 0, this._array.length - 1);
|
||||
this._array.splice(i, 0, value);
|
||||
}
|
||||
|
||||
@@ -37,7 +40,7 @@ export class SortedList<T> {
|
||||
if (key === undefined) {
|
||||
return false;
|
||||
}
|
||||
let i = this._search(key, 0, this._array.length - 1);
|
||||
i = this._search(key, 0, this._array.length - 1);
|
||||
if (i === -1) {
|
||||
return false;
|
||||
}
|
||||
@@ -57,7 +60,7 @@ export class SortedList<T> {
|
||||
if (this._array.length === 0) {
|
||||
return;
|
||||
}
|
||||
let i = this._search(key, 0, this._array.length - 1);
|
||||
i = this._search(key, 0, this._array.length - 1);
|
||||
if (i < 0 || i >= this._array.length) {
|
||||
return;
|
||||
}
|
||||
@@ -69,6 +72,22 @@ export class SortedList<T> {
|
||||
} while (++i < this._array.length && this._getKey(this._array[i]) === key);
|
||||
}
|
||||
|
||||
public forEachByKey(key: number, callback: (value: T) => void): void {
|
||||
if (this._array.length === 0) {
|
||||
return;
|
||||
}
|
||||
i = this._search(key, 0, this._array.length - 1);
|
||||
if (i < 0 || i >= this._array.length) {
|
||||
return;
|
||||
}
|
||||
if (this._getKey(this._array[i]) !== key) {
|
||||
return;
|
||||
}
|
||||
do {
|
||||
callback(this._array[i]);
|
||||
} while (++i < this._array.length && this._getKey(this._array[i]) === key);
|
||||
}
|
||||
|
||||
public values(): IterableIterator<T> {
|
||||
return this._array.values();
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum, IDecorationService, IInternalDecoration } from 'common/services/Services';
|
||||
import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum, IDecorationService, IInternalDecoration, IOscLinkService } from 'common/services/Services';
|
||||
import { IEvent, EventEmitter } from 'common/EventEmitter';
|
||||
import { clone } from 'common/Clone';
|
||||
import { DEFAULT_OPTIONS } from 'common/services/OptionsService';
|
||||
import { IBufferSet, IBuffer } from 'common/buffer/Types';
|
||||
import { BufferSet } from 'common/buffer/BufferSet';
|
||||
import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharset, IModes, IAttributeData } from 'common/Types';
|
||||
import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharset, IModes, IAttributeData, IOscLinkData } from 'common/Types';
|
||||
import { UnicodeV6 } from 'common/input/UnicodeV6';
|
||||
import { IDecorationOptions, IDecoration } from 'xterm';
|
||||
|
||||
@@ -138,6 +138,18 @@ export class MockOptionsService implements IOptionsService {
|
||||
}
|
||||
}
|
||||
|
||||
export class MockOscLinkService implements IOscLinkService {
|
||||
public serviceBrand: any;
|
||||
public registerLink(linkData: IOscLinkData): number {
|
||||
return 1;
|
||||
}
|
||||
public getLinkData(linkId: number): IOscLinkData | undefined {
|
||||
return undefined;
|
||||
}
|
||||
public addLineToLink(linkId: number, y: number): void {
|
||||
}
|
||||
}
|
||||
|
||||
// defaults to V6 always to keep tests passing
|
||||
export class MockUnicodeService implements IUnicodeService {
|
||||
public serviceBrand: any;
|
||||
@@ -161,7 +173,6 @@ export class MockDecorationService implements IDecorationService {
|
||||
public onDecorationRemoved = new EventEmitter<IInternalDecoration>().event;
|
||||
public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined { return undefined; }
|
||||
public reset(): void { }
|
||||
public *getDecorationsAtLine(line: number): IterableIterator<IInternalDecoration> { }
|
||||
public *getDecorationsAtCell(x: number, line: number): IterableIterator<IInternalDecoration> { }
|
||||
public forEachDecorationAtCell(x: number, line: number, layer: 'bottom' | 'top' | undefined, callback: (decoration: IInternalDecoration) => void): void { }
|
||||
public dispose(): void { }
|
||||
}
|
||||
|
||||
Vendored
+17
-1
@@ -9,6 +9,7 @@ import { IDeleteEvent, IInsertEvent } from 'common/CircularList';
|
||||
import { IParams } from 'common/parser/Types';
|
||||
import { ICoreMouseService, ICoreService, IOptionsService, IUnicodeService } from 'common/services/Services';
|
||||
import { IBufferSet } from 'common/buffer/Types';
|
||||
import { UnderlineStyle } from 'common/buffer/Constants';
|
||||
|
||||
export interface ICoreTerminal {
|
||||
coreMouseService: ICoreMouseService;
|
||||
@@ -114,12 +115,24 @@ export type IColorRGB = [number, number, number];
|
||||
|
||||
export interface IExtendedAttrs {
|
||||
ext: number;
|
||||
underlineStyle: number;
|
||||
underlineStyle: UnderlineStyle;
|
||||
underlineColor: number;
|
||||
urlId: number;
|
||||
clone(): IExtendedAttrs;
|
||||
isEmpty(): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks the current hyperlink. Since these are treated as extended attirbutes, these get passed on
|
||||
* to the linkifier when anything is printed. Doing it this way ensures that even when the cursor
|
||||
* moves around unexpectedly the link is tracked, as opposed to using a start position and
|
||||
* finalizing it at the end.
|
||||
*/
|
||||
export interface IOscLinkData {
|
||||
id?: string;
|
||||
uri: string;
|
||||
}
|
||||
|
||||
/** Attribute data */
|
||||
export interface IAttributeData {
|
||||
fg: number;
|
||||
@@ -268,6 +281,9 @@ export interface ICoreMouseEvent {
|
||||
col: number;
|
||||
/** row (zero based). */
|
||||
row: number;
|
||||
/** xy pixel positions. */
|
||||
x: number;
|
||||
y: number;
|
||||
/**
|
||||
* Button the action occured. Due to restrictions of the tracking protocols
|
||||
* it is not possible to report multiple buttons at once.
|
||||
|
||||
@@ -35,7 +35,12 @@ export class AttributeData implements IAttributeData {
|
||||
// flags
|
||||
public isInverse(): number { return this.fg & FgFlags.INVERSE; }
|
||||
public isBold(): number { return this.fg & FgFlags.BOLD; }
|
||||
public isUnderline(): number { return this.fg & FgFlags.UNDERLINE; }
|
||||
public isUnderline(): number {
|
||||
if (this.hasExtendedAttrs() && this.extended.underlineStyle !== UnderlineStyle.NONE) {
|
||||
return 1;
|
||||
}
|
||||
return this.fg & FgFlags.UNDERLINE;
|
||||
}
|
||||
public isBlink(): number { return this.fg & FgFlags.BLINK; }
|
||||
public isInvisible(): number { return this.fg & FgFlags.INVISIBLE; }
|
||||
public isItalic(): number { return this.bg & BgFlags.ITALIC; }
|
||||
@@ -128,10 +133,22 @@ export class AttributeData implements IAttributeData {
|
||||
*/
|
||||
export class ExtendedAttrs implements IExtendedAttrs {
|
||||
private _ext: number = 0;
|
||||
public get ext(): number { return this._ext; }
|
||||
public get ext(): number {
|
||||
if (this._urlId) {
|
||||
return (
|
||||
(this._ext & ~ExtFlags.UNDERLINE_STYLE) |
|
||||
(this.underlineStyle << 26)
|
||||
);
|
||||
}
|
||||
return this._ext;
|
||||
}
|
||||
public set ext(value: number) { this._ext = value; }
|
||||
|
||||
public get underlineStyle(): UnderlineStyle {
|
||||
// Always return the URL style if it has one
|
||||
if (this._urlId) {
|
||||
return UnderlineStyle.DASHED;
|
||||
}
|
||||
return (this._ext & ExtFlags.UNDERLINE_STYLE) >> 26;
|
||||
}
|
||||
public set underlineStyle(value: UnderlineStyle) {
|
||||
@@ -147,16 +164,24 @@ export class ExtendedAttrs implements IExtendedAttrs {
|
||||
this._ext |= value & (Attributes.CM_MASK | Attributes.RGB_MASK);
|
||||
}
|
||||
|
||||
private _urlId: number = 0;
|
||||
public get urlId(): number {
|
||||
return this._urlId;
|
||||
}
|
||||
public set urlId(value: number) {
|
||||
this._urlId = value;
|
||||
}
|
||||
|
||||
constructor(
|
||||
underlineStyle: UnderlineStyle = UnderlineStyle.NONE,
|
||||
underlineColor: number = Attributes.CM_DEFAULT
|
||||
ext: number = 0,
|
||||
urlId: number = 0
|
||||
) {
|
||||
this.underlineStyle = underlineStyle;
|
||||
this.underlineColor = underlineColor;
|
||||
this._ext = ext;
|
||||
this._urlId = urlId;
|
||||
}
|
||||
|
||||
public clone(): IExtendedAttrs {
|
||||
return new ExtendedAttrs(this.underlineStyle, this.underlineColor);
|
||||
return new ExtendedAttrs(this._ext, this._urlId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -164,6 +189,6 @@ export class ExtendedAttrs implements IExtendedAttrs {
|
||||
* that needs to be persistant in the buffer.
|
||||
*/
|
||||
public isEmpty(): boolean {
|
||||
return this.underlineStyle === UnderlineStyle.NONE;
|
||||
return this.underlineStyle === UnderlineStyle.NONE && this._urlId === 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,11 @@ const enum Cell {
|
||||
|
||||
export const DEFAULT_ATTR_DATA = Object.freeze(new AttributeData());
|
||||
|
||||
/** Work variables to avoid garbage collection. */
|
||||
const w: { startIndex: number } = {
|
||||
startIndex: 0
|
||||
};
|
||||
|
||||
/**
|
||||
* Typed array based bufferline implementation.
|
||||
*
|
||||
@@ -168,10 +173,10 @@ export class BufferLine implements IBufferLine {
|
||||
* to GC as it significantly reduced the amount of new objects/references needed.
|
||||
*/
|
||||
public loadCell(index: number, cell: ICellData): ICellData {
|
||||
const startIndex = index * CELL_SIZE;
|
||||
cell.content = this._data[startIndex + Cell.CONTENT];
|
||||
cell.fg = this._data[startIndex + Cell.FG];
|
||||
cell.bg = this._data[startIndex + Cell.BG];
|
||||
w.startIndex = index * CELL_SIZE;
|
||||
cell.content = this._data[w.startIndex + Cell.CONTENT];
|
||||
cell.fg = this._data[w.startIndex + Cell.FG];
|
||||
cell.bg = this._data[w.startIndex + Cell.BG];
|
||||
if (cell.content & Content.IS_COMBINED_MASK) {
|
||||
cell.combinedData = this._combined[index];
|
||||
}
|
||||
|
||||
@@ -58,14 +58,14 @@ export class BufferSet extends Disposable implements IBufferSet {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the normal Buffer of the BufferSet
|
||||
* Returns the currently active Buffer of the BufferSet
|
||||
*/
|
||||
public get active(): Buffer {
|
||||
return this._activeBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently active Buffer of the BufferSet
|
||||
* Returns the normal Buffer of the BufferSet
|
||||
*/
|
||||
public get normal(): Buffer {
|
||||
return this._normal;
|
||||
|
||||
@@ -32,13 +32,11 @@ export class BufferService extends Disposable implements IBufferService {
|
||||
/** An IBufferline to clone/copy from for new blank lines */
|
||||
private _cachedBlankLine: IBufferLine | undefined;
|
||||
|
||||
constructor(
|
||||
@IOptionsService private _optionsService: IOptionsService
|
||||
) {
|
||||
constructor(@IOptionsService optionsService: IOptionsService) {
|
||||
super();
|
||||
this.cols = Math.max(_optionsService.rawOptions.cols || 0, MINIMUM_COLS);
|
||||
this.rows = Math.max(_optionsService.rawOptions.rows || 0, MINIMUM_ROWS);
|
||||
this.buffers = new BufferSet(_optionsService, this);
|
||||
this.cols = Math.max(optionsService.rawOptions.cols || 0, MINIMUM_COLS);
|
||||
this.rows = Math.max(optionsService.rawOptions.rows || 0, MINIMUM_ROWS);
|
||||
this.buffers = new BufferSet(optionsService, this);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
|
||||
@@ -34,7 +34,7 @@ describe('CoreMouseService', () => {
|
||||
});
|
||||
it('default encodings - DEFAULT, SGR', () => {
|
||||
const cms = new CoreMouseService(bufferService, coreService);
|
||||
assert.deepEqual(Object.keys((cms as any)._encodings), ['DEFAULT', 'SGR']);
|
||||
assert.deepEqual(Object.keys((cms as any)._encodings), ['DEFAULT', 'SGR', 'SGR_PIXELS']);
|
||||
});
|
||||
it('protocol/encoding setter, reset', () => {
|
||||
const cms = new CoreMouseService(bufferService, coreService);
|
||||
@@ -82,68 +82,68 @@ describe('CoreMouseService', () => {
|
||||
coreService.triggerBinaryEvent = (data: string) => reports.push(data);
|
||||
});
|
||||
it('NONE', () => {
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false);
|
||||
});
|
||||
it('X10', () => {
|
||||
cms.activeProtocol = 'X10';
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false);
|
||||
});
|
||||
it('VT200', () => {
|
||||
cms.activeProtocol = 'VT200';
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false);
|
||||
});
|
||||
it('DRAG', () => {
|
||||
cms.activeProtocol = 'DRAG';
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true);
|
||||
});
|
||||
it('ANY', () => {
|
||||
cms.activeProtocol = 'ANY';
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), true);
|
||||
// should not report in any case
|
||||
// invalid button + action combinations
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.MOVE }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.DOWN }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.UP }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.MOVE }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.DOWN }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.UP }), false);
|
||||
// invalid coords
|
||||
assert.equal(cms.triggerMouseEvent({ col: -1, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 500, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: -1, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 500, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: -1, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 500, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: -1, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 500, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
|
||||
});
|
||||
describe('coords', () => {
|
||||
it('DEFAULT encoding', () => {
|
||||
cms.activeProtocol = 'ANY';
|
||||
for (let i = 0; i < bufferService.cols; ++i) {
|
||||
assert.equal(cms.triggerMouseEvent({ col: i, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: i, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
|
||||
if (i > 222) {
|
||||
// supress mouse reports if we are out of addressible range (max. 222)
|
||||
assert.deepEqual(toBytes(reports.pop()), []);
|
||||
@@ -156,46 +156,54 @@ describe('CoreMouseService', () => {
|
||||
cms.activeProtocol = 'ANY';
|
||||
cms.activeEncoding = 'SGR';
|
||||
for (let i = 0; i < bufferService.cols; ++i) {
|
||||
assert.equal(cms.triggerMouseEvent({ col: i, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: i, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.deepEqual(reports.pop(), `\x1b[<0;${i + 1};1M`);
|
||||
}
|
||||
});
|
||||
it('SGR_PIXELS encoding', () => {
|
||||
cms.activeProtocol = 'ANY';
|
||||
cms.activeEncoding = 'SGR_PIXELS';
|
||||
for (let i = 0; i < 500; ++i) {
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: i, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.deepEqual(reports.pop(), `\x1b[<0;${i};0M`);
|
||||
}
|
||||
});
|
||||
});
|
||||
it('eventCodes with modifiers (DEFAULT encoding)', () => {
|
||||
// TODO: implement AUX button tests
|
||||
cms.activeProtocol = 'ANY';
|
||||
cms.activeEncoding = 'DEFAULT';
|
||||
// all buttons + down + no modifer
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.deepEqual(reports, ['\x1b[M !!', '\x1b[M!!!', '\x1b[M"!!', '\x1b[Ma!!']);
|
||||
reports = [];
|
||||
|
||||
// all buttons + up + no modifier
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.deepEqual(reports, ['\x1b[M#!!', '\x1b[M#!!', '\x1b[M#!!', '\x1b[M`!!']);
|
||||
reports = [];
|
||||
|
||||
// all buttons + move + no modifier
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.deepEqual(reports, ['\x1b[M@!!', '\x1b[MA!!', '\x1b[MB!!', '\x1b[MC!!']);
|
||||
reports = [];
|
||||
|
||||
// button none + move + modifiers
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: true, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: true }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: true, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: true, shift: true }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: true, shift: true }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: true, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: true }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: true, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: true, shift: true }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, x: 0, y: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: true, shift: true }), true);
|
||||
assert.deepEqual(reports, ['\x1b[MS!!', '\x1b[MK!!', '\x1b[MG!!', '\x1b[M[!!', '\x1b[MO!!', '\x1b[M_!!']);
|
||||
reports = [];
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ICoreMouseProtocol, ICoreMouseEvent, CoreMouseEncoding, CoreMouseEventT
|
||||
/**
|
||||
* Supported default protocols.
|
||||
*/
|
||||
const DEFAULT_PROTOCOLS: {[key: string]: ICoreMouseProtocol} = {
|
||||
const DEFAULT_PROTOCOLS: { [key: string]: ICoreMouseProtocol } = {
|
||||
/**
|
||||
* NONE
|
||||
* Events: none
|
||||
@@ -117,7 +117,7 @@ const S = String.fromCharCode;
|
||||
/**
|
||||
* Supported default encodings.
|
||||
*/
|
||||
const DEFAULT_ENCODINGS: {[key: string]: CoreMouseEncoding} = {
|
||||
const DEFAULT_ENCODINGS: { [key: string]: CoreMouseEncoding } = {
|
||||
/**
|
||||
* DEFAULT - CSI M Pb Px Py
|
||||
* Single byte encoding for coords and event code.
|
||||
@@ -142,6 +142,10 @@ const DEFAULT_ENCODINGS: {[key: string]: CoreMouseEncoding} = {
|
||||
SGR: (e: ICoreMouseEvent) => {
|
||||
const final = (e.action === CoreMouseAction.UP && e.button !== CoreMouseButton.WHEEL) ? 'm' : 'M';
|
||||
return `\x1b[<${eventCode(e, true)};${e.col};${e.row}${final}`;
|
||||
},
|
||||
SGR_PIXELS: (e: ICoreMouseEvent) => {
|
||||
const final = (e.action === CoreMouseAction.UP && e.button !== CoreMouseButton.WHEEL) ? 'm' : 'M';
|
||||
return `\x1b[<${eventCode(e, true)};${e.x};${e.y}${final}`;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -162,8 +166,8 @@ const DEFAULT_ENCODINGS: {[key: string]: CoreMouseEncoding} = {
|
||||
* To send a mouse event call `triggerMouseEvent`.
|
||||
*/
|
||||
export class CoreMouseService implements ICoreMouseService {
|
||||
private _protocols: {[name: string]: ICoreMouseProtocol} = {};
|
||||
private _encodings: {[name: string]: CoreMouseEncoding} = {};
|
||||
private _protocols: { [name: string]: ICoreMouseProtocol } = {};
|
||||
private _encodings: { [name: string]: CoreMouseEncoding } = {};
|
||||
private _activeProtocol: string = '';
|
||||
private _activeEncoding: string = '';
|
||||
private _onProtocolChange = new EventEmitter<CoreMouseEventType>();
|
||||
@@ -241,7 +245,7 @@ export class CoreMouseService implements ICoreMouseService {
|
||||
public triggerMouseEvent(e: ICoreMouseEvent): boolean {
|
||||
// range check for col/row
|
||||
if (e.col < 0 || e.col >= this._bufferService.cols
|
||||
|| e.row < 0 || e.row >= this._bufferService.rows) {
|
||||
|| e.row < 0 || e.row >= this._bufferService.rows) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -260,8 +264,11 @@ export class CoreMouseService implements ICoreMouseService {
|
||||
e.col++;
|
||||
e.row++;
|
||||
|
||||
// debounce move at grid level
|
||||
if (e.action === CoreMouseAction.MOVE && this._lastEvent && this._compareEvents(this._lastEvent, e)) {
|
||||
// debounce move events at grid or pixel level
|
||||
if (e.action === CoreMouseAction.MOVE
|
||||
&& this._lastEvent
|
||||
&& this._equalEvents(this._lastEvent, e, this._activeEncoding === 'SGR_PIXELS')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -286,7 +293,7 @@ export class CoreMouseService implements ICoreMouseService {
|
||||
return true;
|
||||
}
|
||||
|
||||
public explainEvents(events: CoreMouseEventType): {[event: string]: boolean} {
|
||||
public explainEvents(events: CoreMouseEventType): { [event: string]: boolean } {
|
||||
return {
|
||||
down: !!(events & CoreMouseEventType.DOWN),
|
||||
up: !!(events & CoreMouseEventType.UP),
|
||||
@@ -296,9 +303,14 @@ export class CoreMouseService implements ICoreMouseService {
|
||||
};
|
||||
}
|
||||
|
||||
private _compareEvents(e1: ICoreMouseEvent, e2: ICoreMouseEvent): boolean {
|
||||
if (e1.col !== e2.col) return false;
|
||||
if (e1.row !== e2.row) return false;
|
||||
private _equalEvents(e1: ICoreMouseEvent, e2: ICoreMouseEvent, pixels: boolean): boolean {
|
||||
if (pixels) {
|
||||
if (e1.x !== e2.x) return false;
|
||||
if (e1.y !== e2.y) return false;
|
||||
} else {
|
||||
if (e1.col !== e2.col) return false;
|
||||
if (e1.row !== e2.row) return false;
|
||||
}
|
||||
if (e1.button !== e2.button) return false;
|
||||
if (e1.action !== e2.action) return false;
|
||||
if (e1.ctrl !== e2.ctrl) return false;
|
||||
|
||||
@@ -11,6 +11,12 @@ import { SortedList } from 'common/SortedList';
|
||||
import { IColor } from 'common/Types';
|
||||
import { IDecorationOptions, IDecoration, IMarker, IEvent } from 'xterm';
|
||||
|
||||
/** Work variables to avoid garbage collection. */
|
||||
const w = {
|
||||
xmin: 0,
|
||||
xmax: 0
|
||||
};
|
||||
|
||||
export class DecorationService extends Disposable implements IDecorationService {
|
||||
public serviceBrand: any;
|
||||
|
||||
@@ -56,10 +62,6 @@ export class DecorationService extends Disposable implements IDecorationService
|
||||
this._decorations.clear();
|
||||
}
|
||||
|
||||
public *getDecorationsAtLine(line: number): IterableIterator<IInternalDecoration> {
|
||||
return this._decorations.getKeyIterator(line);
|
||||
}
|
||||
|
||||
public *getDecorationsAtCell(x: number, line: number, layer?: 'bottom' | 'top'): IterableIterator<IInternalDecoration> {
|
||||
let xmin = 0;
|
||||
let xmax = 0;
|
||||
@@ -72,6 +74,16 @@ export class DecorationService extends Disposable implements IDecorationService
|
||||
}
|
||||
}
|
||||
|
||||
public forEachDecorationAtCell(x: number, line: number, layer: 'bottom' | 'top' | undefined, callback: (decoration: IInternalDecoration) => void): void {
|
||||
this._decorations.forEachByKey(line, d => {
|
||||
w.xmin = d.options.x ?? 0;
|
||||
w.xmax = w.xmin + (d.options.width ?? 1);
|
||||
if (x >= w.xmin && x < w.xmax && (!layer || (d.options.layer ?? 'bottom') === layer)) {
|
||||
callback(d);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
for (const d of this._decorations.values()) {
|
||||
this._onDecorationRemoved.fire(d);
|
||||
|
||||
@@ -24,6 +24,7 @@ export const DEFAULT_OPTIONS: Readonly<Required<ITerminalOptions>> = {
|
||||
fontWeightBold: 'bold',
|
||||
lineHeight: 1.0,
|
||||
letterSpacing: 0,
|
||||
linkHandler: null,
|
||||
logLevel: 'info',
|
||||
scrollback: 1000,
|
||||
scrollSensitivity: 1,
|
||||
@@ -119,7 +120,6 @@ export class OptionsService implements IOptionsService {
|
||||
throw new Error(`"${value}" is not a valid value for ${key}`);
|
||||
}
|
||||
break;
|
||||
case 'cursorStyle':
|
||||
case 'wordSeparator':
|
||||
if (!value) {
|
||||
value = DEFAULT_OPTIONS[key];
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2020 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { BufferService } from 'common/services/BufferService';
|
||||
import { OptionsService } from 'common/services/OptionsService';
|
||||
import { OscLinkService } from 'common/services/OscLinkService';
|
||||
import { IBufferService, IOptionsService, IOscLinkService } from 'common/services/Services';
|
||||
|
||||
describe('OscLinkService', () => {
|
||||
describe('constructor', () => {
|
||||
let bufferService: IBufferService;
|
||||
let optionsService: IOptionsService;
|
||||
let oscLinkService: IOscLinkService;
|
||||
beforeEach(() => {
|
||||
optionsService = new OptionsService({ rows: 3, cols: 10 });
|
||||
bufferService = new BufferService(optionsService);
|
||||
oscLinkService = new OscLinkService(bufferService);
|
||||
});
|
||||
|
||||
it('link IDs are created and fetched consistently', () => {
|
||||
const linkId = oscLinkService.registerLink({ id: 'foo', uri: 'bar' });
|
||||
assert.ok(linkId);
|
||||
assert.equal(oscLinkService.registerLink({ id: 'foo', uri: 'bar' }), linkId);
|
||||
});
|
||||
|
||||
it('should dispose the link ID when the last marker is trimmed from the buffer', () => {
|
||||
// Activate the alt buffer to get 0 scrollback
|
||||
bufferService.buffers.activateAltBuffer();
|
||||
const linkId = oscLinkService.registerLink({ id: 'foo', uri: 'bar' });
|
||||
assert.ok(linkId);
|
||||
bufferService.scroll(new AttributeData());
|
||||
assert.notStrictEqual(oscLinkService.registerLink({ id: 'foo', uri: 'bar' }), linkId);
|
||||
});
|
||||
|
||||
it('should fetch link data from link id', () => {
|
||||
const linkId = oscLinkService.registerLink({ id: 'foo', uri: 'bar' });
|
||||
assert.deepStrictEqual(oscLinkService.getLinkData(linkId), { id: 'foo', uri: 'bar' });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Copyright (c) 2022 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
import { IBufferService, IOscLinkService } from 'common/services/Services';
|
||||
import { IMarker, IOscLinkData } from 'common/Types';
|
||||
|
||||
export class OscLinkService implements IOscLinkService {
|
||||
public serviceBrand: any;
|
||||
|
||||
private _nextId = 1;
|
||||
|
||||
/**
|
||||
* A map of the link key to link entry. This is used to add additional lines to links with ids.
|
||||
*/
|
||||
private _entriesWithId: Map<string, IOscLinkEntryWithId> = new Map();
|
||||
|
||||
/**
|
||||
* A map of the link id to the link entry. The "link id" (number) which is the numberic
|
||||
* representation of a unique link should not be confused with "id" (string) which comes in with
|
||||
* `id=` in the OSC link's properties.
|
||||
*/
|
||||
private _dataByLinkId: Map<number, IOscLinkEntryNoId | IOscLinkEntryWithId> = new Map();
|
||||
|
||||
constructor(
|
||||
@IBufferService private readonly _bufferService: IBufferService
|
||||
) {
|
||||
}
|
||||
|
||||
public registerLink(data: IOscLinkData): number {
|
||||
const buffer = this._bufferService.buffer;
|
||||
|
||||
// Links with no id will only ever be registered a single time
|
||||
if (data.id === undefined) {
|
||||
const marker = buffer.addMarker(buffer.ybase + buffer.y);
|
||||
const entry: IOscLinkEntryNoId = {
|
||||
data,
|
||||
id: this._nextId++,
|
||||
lines: [marker]
|
||||
};
|
||||
marker.onDispose(() => this._removeMarkerFromLink(entry, marker));
|
||||
this._dataByLinkId.set(entry.id, entry);
|
||||
return entry.id;
|
||||
}
|
||||
|
||||
// Add the line to the link if it already exists
|
||||
const castData = data as Required<IOscLinkData>;
|
||||
const key = this._getEntryIdKey(castData);
|
||||
const match = this._entriesWithId.get(key);
|
||||
if (match) {
|
||||
this.addLineToLink(match.id, buffer.ybase + buffer.y);
|
||||
return match.id;
|
||||
}
|
||||
|
||||
// Create the link
|
||||
const marker = buffer.addMarker(buffer.ybase + buffer.y);
|
||||
const entry: IOscLinkEntryWithId = {
|
||||
id: this._nextId++,
|
||||
key: this._getEntryIdKey(castData),
|
||||
data: castData,
|
||||
lines: [marker]
|
||||
};
|
||||
marker.onDispose(() => this._removeMarkerFromLink(entry, marker));
|
||||
this._entriesWithId.set(entry.key, entry);
|
||||
this._dataByLinkId.set(entry.id, entry);
|
||||
return entry.id;
|
||||
}
|
||||
|
||||
public addLineToLink(linkId: number, y: number): void {
|
||||
const entry = this._dataByLinkId.get(linkId);
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
if (entry.lines.every(e => e.line !== y)) {
|
||||
const marker = this._bufferService.buffer.addMarker(y);
|
||||
entry.lines.push(marker);
|
||||
marker.onDispose(() => this._removeMarkerFromLink(entry, marker));
|
||||
}
|
||||
}
|
||||
|
||||
public getLinkData(linkId: number): IOscLinkData | undefined {
|
||||
return this._dataByLinkId.get(linkId)?.data;
|
||||
}
|
||||
|
||||
private _getEntryIdKey(linkData: Required<IOscLinkData>): string {
|
||||
return `${linkData.id};;${linkData.uri}`;
|
||||
}
|
||||
|
||||
private _removeMarkerFromLink(entry: IOscLinkEntryNoId | IOscLinkEntryWithId, marker: IMarker): void {
|
||||
const index = entry.lines.indexOf(marker);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
entry.lines.splice(index, 1);
|
||||
if (entry.lines.length === 0) {
|
||||
if (entry.data.id !== undefined) {
|
||||
this._entriesWithId.delete((entry as IOscLinkEntryWithId).key);
|
||||
}
|
||||
this._dataByLinkId.delete(entry.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface IOscLinkEntry<T extends IOscLinkData> {
|
||||
data: T;
|
||||
id: number;
|
||||
lines: IMarker[];
|
||||
}
|
||||
|
||||
interface IOscLinkEntryNoId extends IOscLinkEntry<IOscLinkData> {
|
||||
}
|
||||
|
||||
interface IOscLinkEntryWithId extends IOscLinkEntry<Required<IOscLinkData>> {
|
||||
key: string;
|
||||
}
|
||||
@@ -5,9 +5,9 @@
|
||||
|
||||
import { IEvent, IEventEmitter } from 'common/EventEmitter';
|
||||
import { IBuffer, IBufferSet } from 'common/buffer/Types';
|
||||
import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes, IAttributeData, ScrollSource, IDisposable, IColorRGB, IColor, CursorStyle } from 'common/Types';
|
||||
import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes, IAttributeData, ScrollSource, IDisposable, IColor, CursorStyle, IOscLinkData } from 'common/Types';
|
||||
import { createDecorator } from 'common/services/ServiceRegistry';
|
||||
import { IDecorationOptions, IDecoration } from 'xterm';
|
||||
import { IDecorationOptions, IDecoration, ILinkHandler } from 'xterm';
|
||||
|
||||
export const IBufferService = createDecorator<IBufferService>('BufferService');
|
||||
export interface IBufferService {
|
||||
@@ -223,6 +223,7 @@ export interface ITerminalOptions {
|
||||
fontWeightBold?: FontWeight;
|
||||
letterSpacing?: number;
|
||||
lineHeight?: number;
|
||||
linkHandler?: ILinkHandler | null;
|
||||
logLevel?: LogLevel;
|
||||
macOptionIsMeta?: boolean;
|
||||
macOptionClickForcesSelection?: boolean;
|
||||
@@ -272,6 +273,22 @@ export interface ITheme {
|
||||
extendedAnsi?: string[];
|
||||
}
|
||||
|
||||
export const IOscLinkService = createDecorator<IOscLinkService>('OscLinkService');
|
||||
export interface IOscLinkService {
|
||||
serviceBrand: undefined;
|
||||
/**
|
||||
* Registers a link to the service, returning the link ID. The link data is managed by this
|
||||
* service and will be freed when this current cursor position is trimmed off the buffer.
|
||||
*/
|
||||
registerLink(linkData: IOscLinkData): number;
|
||||
/**
|
||||
* Adds a line to a link if needed.
|
||||
*/
|
||||
addLineToLink(linkId: number, y: number): void;
|
||||
/** Get the link data associated with a link ID. */
|
||||
getLinkData(linkId: number): IOscLinkData | undefined;
|
||||
}
|
||||
|
||||
export const IUnicodeService = createDecorator<IUnicodeService>('UnicodeService');
|
||||
export interface IUnicodeService {
|
||||
serviceBrand: undefined;
|
||||
@@ -304,10 +321,11 @@ export interface IDecorationService extends IDisposable {
|
||||
readonly onDecorationRemoved: IEvent<IInternalDecoration>;
|
||||
registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined;
|
||||
reset(): void;
|
||||
/** Iterates over the decorations at a line (in no particular order). */
|
||||
getDecorationsAtLine(line: number): IterableIterator<IInternalDecoration>;
|
||||
/** Iterates over the decorations at a cell (in no particular order). */
|
||||
getDecorationsAtCell(x: number, line: number, layer?: 'bottom' | 'top'): IterableIterator<IInternalDecoration>;
|
||||
/**
|
||||
* Trigger a callback over the decoration at a cell (in no particular order). This uses a callback
|
||||
* instead of an iterator as it's typically used in hot code paths.
|
||||
*/
|
||||
forEachDecorationAtCell(x: number, line: number, layer: 'bottom' | 'top' | undefined, callback: (decoration: IInternalDecoration) => void): void;
|
||||
}
|
||||
export interface IInternalDecoration extends IDecoration {
|
||||
readonly options: IDecorationOptions;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [ "es5" ],
|
||||
"module": "commonjs",
|
||||
"target": "es2015",
|
||||
"rootDir": ".",
|
||||
|
||||
"sourceMap": true,
|
||||
|
||||
Reference in New Issue
Block a user