Merge branch 'master' into fix-caps-lock-ime

This commit is contained in:
SerKo
2022-05-24 22:49:52 +08:00
committed by GitHub
39 changed files with 487 additions and 283 deletions
+1 -1
View File
@@ -34,7 +34,7 @@ describe('ColorManager', () => {
describe('constructor', () => {
it('should fill all colors with values', () => {
for (const key of Object.keys(cm.colors)) {
if (key !== 'ansi' && key !== 'contrastCache') {
if (key !== 'ansi' && key !== 'contrastCache' && key !== 'selectionForeground') {
// A #rrggbb or rgba(...)
assert.ok((cm.colors as any)[key].css.length >= 7);
}
+10
View File
@@ -104,6 +104,7 @@ export class ColorManager implements IColorManager {
cursorAccent: DEFAULT_CURSOR_ACCENT,
selectionTransparent: DEFAULT_SELECTION,
selectionOpaque: color.blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION),
selectionForeground: undefined,
ansi: DEFAULT_ANSI_COLORS.slice(),
contrastCache: this._contrastCache
};
@@ -128,6 +129,15 @@ export class ColorManager implements IColorManager {
this.colors.cursorAccent = this._parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT, true);
this.colors.selectionTransparent = this._parseColor(theme.selection, DEFAULT_SELECTION, true);
this.colors.selectionOpaque = color.blend(this.colors.background, this.colors.selectionTransparent);
const nullColor: IColor = {
css: '',
rgba: 0
};
this.colors.selectionForeground = theme.selectionForeground ? this._parseColor(theme.selectionForeground, nullColor) : undefined;
if (this.colors.selectionForeground === nullColor) {
this.colors.selectionForeground = undefined;
}
/**
* If selection color is opaque, blend it with background with 0.3 opacity
* Issue #2737
+12 -7
View File
@@ -18,6 +18,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
private _linkProviders: ILinkProvider[] = [];
public get currentLink(): ILinkWithState | undefined { return this._currentLink; }
protected _currentLink: ILinkWithState | undefined;
private _mouseDownLink: ILinkWithState | undefined;
private _lastMouseEvent: MouseEvent | undefined;
private _linkCacheDisposables: IDisposable[] = [];
private _lastBufferCell: IBufferCellPosition | undefined;
@@ -61,7 +62,8 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
this._clearCurrentLink();
}));
this.register(addDisposableDomListener(this._element, 'mousemove', this._onMouseMove.bind(this)));
this.register(addDisposableDomListener(this._element, 'click', this._onClick.bind(this)));
this.register(addDisposableDomListener(this._element, 'mousedown', this._handleMouseDown.bind(this)));
this.register(addDisposableDomListener(this._element, 'mouseup', this._handleMouseUp.bind(this)));
}
private _onMouseMove(event: MouseEvent): void {
@@ -129,7 +131,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
let linkProvided = false;
// There is no link cached, so ask for one
this._linkProviders.forEach((linkProvider, i) => {
for (const [i, linkProvider] of this._linkProviders.entries()) {
if (useLineCache) {
const existingReply = this._activeProviderReplies?.get(i);
// If there isn't a reply, the provider hasn't responded yet.
@@ -156,7 +158,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
}
});
}
});
}
}
private _removeIntersectingLinks(y: number, replies: Map<Number, ILinkWithState[] | undefined>): void {
@@ -222,18 +224,21 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
return linkProvided;
}
private _onClick(event: MouseEvent): void {
private _handleMouseDown(): void {
this._mouseDownLink = this._currentLink;
}
private _handleMouseUp(event: MouseEvent): void {
if (!this._element || !this._mouseService || !this._currentLink) {
return;
}
const position = this._positionFromMouseEvent(event, this._element, this._mouseService);
if (!position) {
return;
}
if (this._linkAtPosition(this._currentLink.link, position)) {
if (this._mouseDownLink === this._currentLink && this._linkAtPosition(this._currentLink.link, position)) {
this._currentLink.link.activate(event, this._currentLink.link.text);
}
}
@@ -303,7 +308,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
// Add listener for rerendering
if (this._renderService) {
this._linkCacheDisposables.push(this._renderService.onRenderedBufferChange(e => {
this._linkCacheDisposables.push(this._renderService.onRenderedViewportChange(e => {
// When start is 0 a scroll most likely occurred, make sure links above the fold also get
// cleared.
const start = e.start === 0 ? 0 : e.start + 1 + this._bufferService.buffer.ydisp;
+22 -3
View File
@@ -3,16 +3,17 @@
* @license MIT
*/
import { IRenderDebouncer } from 'browser/Types';
import { IRenderDebouncerWithCallback } from 'browser/Types';
/**
* Debounces calls to render terminal rows using animation frames.
*/
export class RenderDebouncer implements IRenderDebouncer {
export class RenderDebouncer implements IRenderDebouncerWithCallback {
private _rowStart: number | undefined;
private _rowEnd: number | undefined;
private _rowCount: number | undefined;
private _animationFrame: number | undefined;
private _refreshCallbacks: FrameRequestCallback[] = [];
constructor(
private _renderCallback: (start: number, end: number) => void
@@ -26,6 +27,14 @@ export class RenderDebouncer implements IRenderDebouncer {
}
}
public addRefreshCallback(callback: FrameRequestCallback): number {
this._refreshCallbacks.push(callback);
if (!this._animationFrame) {
this._animationFrame = window.requestAnimationFrame(() => this._innerRefresh());
}
return this._animationFrame;
}
public refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void {
this._rowCount = rowCount;
// Get the min/max row start/end for the arg values
@@ -43,8 +52,11 @@ export class RenderDebouncer implements IRenderDebouncer {
}
private _innerRefresh(): void {
this._animationFrame = undefined;
// Make sure values are set
if (this._rowStart === undefined || this._rowEnd === undefined || this._rowCount === undefined) {
this._runRefreshCallbacks();
return;
}
@@ -55,9 +67,16 @@ export class RenderDebouncer implements IRenderDebouncer {
// Reset debouncer (this happens before render callback as the render could trigger it again)
this._rowStart = undefined;
this._rowEnd = undefined;
this._animationFrame = undefined;
// Run render callback
this._renderCallback(start, end);
this._runRefreshCallbacks();
}
private _runRefreshCallbacks(): void {
for (const callback of this._refreshCallbacks) {
callback(0);
}
this._refreshCallbacks = [];
}
}
+1 -1
View File
@@ -526,7 +526,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
const renderer = this._createRenderer();
this._renderService = this.register(this._instantiationService.createInstance(RenderService, renderer, this.rows, this.screenElement));
this._instantiationService.setService(IRenderService, this._renderService);
this.register(this._renderService.onRenderedBufferChange(e => this._onRender.fire(e)));
this.register(this._renderService.onRenderedViewportChange(e => this._onRender.fire(e)));
this.onResize(e => this._renderService!.resize(e.cols, e.rows));
this._compositionView = document.createElement('div');
+58 -5
View File
@@ -7,7 +7,7 @@ import { IDisposable, IMarker, ISelectionPosition, ILinkProvider, IDecorationOpt
import { IEvent, EventEmitter } from 'common/EventEmitter';
import { ICharacterJoinerService, ICharSizeService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types';
import { IColorSet, ILinkMatcherOptions, ITerminal, ILinkifier, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper, CharacterJoinerHandler } from 'browser/Types';
import { IColorSet, ILinkMatcherOptions, ITerminal, ILinkifier, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper, CharacterJoinerHandler, IRenderDebouncer } 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';
@@ -16,6 +16,7 @@ import { Terminal } from 'browser/Terminal';
import { IUnicodeService, IOptionsService, ICoreService, ICoreMouseService } from 'common/services/Services';
import { IFunctionIdentifier, IParams } from 'common/parser/Types';
import { AttributeData } from 'common/buffer/AttributeData';
import { ISelectionRedrawRequestEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
export class TestTerminal extends Terminal {
public get curAttrData(): IAttributeData { return (this as any)._inputHandler._curAttrData; }
@@ -30,6 +31,7 @@ export class MockTerminal implements ITerminal {
public onBlur!: IEvent<void>;
public onFocus!: IEvent<void>;
public onA11yChar!: IEvent<string>;
public onWriteParsed!: IEvent<void>;
public onA11yTab!: IEvent<number>;
public onCursorMove!: IEvent<void>;
public onLineFeed!: IEvent<void>;
@@ -370,7 +372,7 @@ export class MockMouseService implements IMouseService {
export class MockRenderService implements IRenderService {
public serviceBrand: undefined;
public onDimensionsChange: IEvent<IRenderDimensions> = new EventEmitter<IRenderDimensions>().event;
public onRenderedBufferChange: IEvent<{ start: number, end: number }, void> = new EventEmitter<{ start: number, end: number }>().event;
public onRenderedViewportChange: IEvent<{ start: number, end: number }, void> = new EventEmitter<{ start: number, end: number }>().event;
public onRender: IEvent<{ start: number, end: number }, void> = new EventEmitter<{ start: number, end: number }>().event;
public onRefreshRequest: IEvent<{ start: number, end: number}, void> = new EventEmitter<{ start: number, end: number }>().event;
public dimensions: IRenderDimensions = {
@@ -390,15 +392,15 @@ export class MockRenderService implements IRenderService {
public refreshRows(start: number, end: number): void {
throw new Error('Method not implemented.');
}
public addRefreshCallback(callback: FrameRequestCallback): number {
throw new Error('Method not implemented.');
}
public clearTextureAtlas(): void {
throw new Error('Method not implemented.');
}
public resize(cols: number, rows: number): void {
throw new Error('Method not implemented.');
}
public changeOptions(): void {
throw new Error('Method not implemented.');
}
public setRenderer(renderer: IRenderer): void {
throw new Error('Method not implemented.');
}
@@ -449,3 +451,54 @@ export class MockCharacterJoinerService implements ICharacterJoinerService {
return [];
}
}
export class MockSelectionService implements ISelectionService {
public serviceBrand: undefined;
public selectionText: string = '';
public hasSelection: boolean = false;
public selectionStart: [number, number] | undefined;
public selectionEnd: [number, number] | undefined;
public onLinuxMouseSelection = new EventEmitter<string>().event;
public onRequestRedraw = new EventEmitter<ISelectionRedrawRequestEvent>().event;
public onRequestScrollLines = new EventEmitter<ISelectionRequestScrollLinesEvent>().event;
public onSelectionChange = new EventEmitter<void>().event;
public disable(): void {
throw new Error('Method not implemented.');
}
public enable(): void {
throw new Error('Method not implemented.');
}
public reset(): void {
throw new Error('Method not implemented.');
}
public setSelection(row: number, col: number, length: number): void {
throw new Error('Method not implemented.');
}
public selectAll(): void {
throw new Error('Method not implemented.');
}
public selectLines(start: number, end: number): void {
throw new Error('Method not implemented.');
}
public clearSelection(): void {
throw new Error('Method not implemented.');
}
public rightClickSelect(event: MouseEvent): void {
throw new Error('Method not implemented.');
}
public shouldColumnSelect(event: MouseEvent | KeyboardEvent): boolean {
throw new Error('Method not implemented.');
}
public shouldForceSelection(event: MouseEvent): boolean {
throw new Error('Method not implemented.');
}
public refresh(isLinuxMouseSelection?: boolean): void {
throw new Error('Method not implemented.');
}
public onMouseDown(event: MouseEvent): void {
throw new Error('Method not implemented.');
}
public isCellInSelection(x: number, y: number): boolean {
return false;
}
}
+6
View File
@@ -44,6 +44,7 @@ export interface IPublicTerminal extends IDisposable {
onSelectionChange: IEvent<void>;
onRender: IEvent<{ start: number, end: number }>;
onResize: IEvent<{ cols: number, rows: number }>;
onWriteParsed: IEvent<void>;
onTitleChange: IEvent<string>;
onBell: IEvent<void>;
blur(): void;
@@ -120,6 +121,7 @@ export interface IColorSet {
selectionTransparent: IColor;
/** The selection blended on top of background. */
selectionOpaque: IColor;
selectionForeground: IColor | undefined;
ansi: IColor[];
contrastCache: IColorContrastCache;
}
@@ -309,3 +311,7 @@ export interface ICharacterJoiner {
export interface IRenderDebouncer extends IDisposable {
refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void;
}
export interface IRenderDebouncerWithCallback extends IRenderDebouncer {
addRefreshCallback(callback: FrameRequestCallback): number;
}
-18
View File
@@ -26,7 +26,6 @@ export class Viewport extends Disposable implements IViewport {
private _lastRecordedBufferHeight: number = 0;
private _lastTouchY: number = 0;
private _lastScrollTop: number = 0;
private _lastHadScrollBar: boolean = false;
private _activeBuffer: IBuffer;
private _renderDimensions: IRenderDimensions;
@@ -54,7 +53,6 @@ export class Viewport extends Disposable implements IViewport {
// Unfortunately the overlay scrollbar would be hidden underneath the screen element in that case,
// therefore we account for a standard amount to make it visible
this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH;
this._lastHadScrollBar = true;
this.register(addDisposableDomListener(this._viewportElement, 'scroll', this._onScroll.bind(this)));
// Track properties used in performance critical code manually to avoid using slow getters
@@ -109,17 +107,6 @@ export class Viewport extends Disposable implements IViewport {
this._viewportElement.scrollTop = scrollTop;
}
// Update scroll bar width
if (this._optionsService.rawOptions.scrollback === 0) {
this.scrollBarWidth = 0;
} else {
this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH;
}
this._lastHadScrollBar = this.scrollBarWidth > 0;
const elementStyle = window.getComputedStyle(this._element);
const elementPadding = parseInt(elementStyle.paddingLeft) + parseInt(elementStyle.paddingRight);
this._viewportElement.style.width = (this._renderService.dimensions.actualCellWidth * (this._bufferService.cols) + this.scrollBarWidth + (this._lastHadScrollBar ? elementPadding : 0)).toString() + 'px';
this._refreshAnimationFrame = null;
}
@@ -151,11 +138,6 @@ export class Viewport extends Disposable implements IViewport {
this._refresh(immediate);
return;
}
// If the scroll bar visibility changed
if (this._lastHadScrollBar !== (this._optionsService.rawOptions.scrollback > 0)) {
this._refresh(immediate);
}
}
/**
@@ -14,6 +14,7 @@ export class BufferDecorationRenderer extends Disposable {
private _animationFrame: number | undefined;
private _altBufferIsActive: boolean = false;
private _dimensionsChanged: boolean = false;
constructor(
private readonly _screenElement: HTMLElement,
@@ -27,8 +28,11 @@ export class BufferDecorationRenderer extends Disposable {
this._container.classList.add('xterm-decoration-container');
this._screenElement.appendChild(this._container);
this.register(this._renderService.onRenderedBufferChange(() => this._queueRefresh()));
this.register(this._renderService.onDimensionsChange(() => this._queueRefresh()));
this.register(this._renderService.onRenderedViewportChange(() => this._queueRefresh()));
this.register(this._renderService.onDimensionsChange(() => {
this._dimensionsChanged = true;
this._queueRefresh();
}));
this.register(addDisposableDomListener(window, 'resize', () => this._queueRefresh()));
this.register(this._bufferService.buffers.onBufferActivate(() => {
this._altBufferIsActive = this._bufferService.buffer === this._bufferService.buffers.alt;
@@ -47,7 +51,7 @@ export class BufferDecorationRenderer extends Disposable {
if (this._animationFrame !== undefined) {
return;
}
this._animationFrame = window.requestAnimationFrame(() => {
this._animationFrame = this._renderService.addRefreshCallback(() => {
this.refreshDecorations();
this._animationFrame = undefined;
});
@@ -57,10 +61,14 @@ export class BufferDecorationRenderer extends Disposable {
for (const decoration of this._decorationService.decorations) {
this._renderDecoration(decoration);
}
this._dimensionsChanged = false;
}
private _renderDecoration(decoration: IInternalDecoration): void {
this._refreshStyle(decoration);
if (this._dimensionsChanged) {
this._refreshXPosition(decoration);
}
}
private _createElement(decoration: IInternalDecoration): HTMLElement {
@@ -76,11 +84,7 @@ export class BufferDecorationRenderer extends Disposable {
// exceeded the container width, so hide
element.style.display = 'none';
}
if ((decoration.options.anchor || 'left') === 'right') {
element.style.right = x ? `${x * this._renderService.dimensions.actualCellWidth}px` : '';
} else {
element.style.left = x ? `${x * this._renderService.dimensions.actualCellWidth}px` : '';
}
this._refreshXPosition(decoration, element);
return element;
}
@@ -108,6 +112,18 @@ export class BufferDecorationRenderer extends Disposable {
}
}
private _refreshXPosition(decoration: IInternalDecoration, element: HTMLElement | undefined = decoration.element): void {
if (!element) {
return;
}
const x = decoration.options.x ?? 0;
if ((decoration.options.anchor || 'left') === 'right') {
element.style.right = x ? `${x * this._renderService.dimensions.actualCellWidth}px` : '';
} else {
element.style.left = x ? `${x * this._renderService.dimensions.actualCellWidth}px` : '';
}
}
private _removeDecoration(decoration: IInternalDecoration): void {
this._decorationElements.get(decoration)?.remove();
this._decorationElements.delete(decoration);
@@ -82,7 +82,7 @@ export class OverviewRulerRenderer extends Disposable {
* and hide the canvas if the alt buffer is active
*/
private _registerBufferChangeListeners(): void {
this.register(this._renderService.onRenderedBufferChange(() => this._queueRefresh()));
this.register(this._renderService.onRenderedViewportChange(() => this._queueRefresh()));
this.register(this._bufferService.buffers.onBufferActivate(() => {
this._canvas!.style.display = this._bufferService.buffer === this._bufferService.buffers.alt ? 'none' : 'block';
}));
+14 -6
View File
@@ -11,30 +11,38 @@ const CHAR_WIDTH = 10;
const CHAR_HEIGHT = 20;
describe('Mouse getCoords', () => {
let windowOverride: Pick<Window, 'getComputedStyle'>;
let document: Document;
beforeEach(() => {
windowOverride = {
getComputedStyle(): any {
return {
getPropertyValue: () => '0px'
} as Pick<CSSStyleDeclaration, 'getPropertyValue'>;
}
};
document = new jsdom.JSDOM('').window.document;
});
it('should return the cell that was clicked', () => {
let coords: [number, number] | undefined;
coords = getCoords({ clientX: CHAR_WIDTH / 2, clientY: CHAR_HEIGHT / 2 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
coords = getCoords(windowOverride, { clientX: CHAR_WIDTH / 2, clientY: CHAR_HEIGHT / 2 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
assert.deepEqual(coords, [1, 1]);
coords = getCoords({ clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
coords = getCoords(windowOverride, { clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
assert.deepEqual(coords, [1, 1]);
coords = getCoords({ clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT + 1 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
coords = getCoords(windowOverride, { clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT + 1 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
assert.deepEqual(coords, [1, 2]);
coords = getCoords({ clientX: CHAR_WIDTH + 1, clientY: CHAR_HEIGHT }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
coords = getCoords(windowOverride, { clientX: CHAR_WIDTH + 1, clientY: CHAR_HEIGHT }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
assert.deepEqual(coords, [2, 1]);
});
it('should ensure the coordinates are returned within the terminal bounds', () => {
let coords: [number, number] | undefined;
coords = getCoords({ clientX: -1, clientY: -1 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
coords = getCoords(windowOverride, { clientX: -1, clientY: -1 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
assert.deepEqual(coords, [1, 1]);
// Event are double the cols/rows
coords = getCoords({ clientX: CHAR_WIDTH * 20, clientY: CHAR_HEIGHT * 20 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
coords = getCoords(windowOverride, { clientX: CHAR_WIDTH * 20, clientY: CHAR_HEIGHT * 20 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT);
assert.deepEqual(coords, [10, 10], 'coordinates should never come back as larger than the terminal');
});
});
+10 -4
View File
@@ -3,9 +3,15 @@
* @license MIT
*/
export function getCoordsRelativeToElement(event: {clientX: number, clientY: number}, element: HTMLElement): [number, number] {
export function getCoordsRelativeToElement(window: Pick<Window, 'getComputedStyle'>, event: {clientX: number, clientY: number}, element: HTMLElement): [number, number] {
const rect = element.getBoundingClientRect();
return [event.clientX - rect.left, event.clientY - rect.top];
const elementStyle = window.getComputedStyle(element);
const leftPadding = parseInt(elementStyle.getPropertyValue('padding-left'));
const topPadding = parseInt(elementStyle.getPropertyValue('padding-top'));
return [
event.clientX - rect.left - leftPadding,
event.clientY - rect.top - topPadding
];
}
/**
@@ -20,13 +26,13 @@ export function getCoordsRelativeToElement(event: {clientX: number, clientY: num
* apply an offset to the x value such that the left half of the cell will
* select that cell and the right half will select the next cell.
*/
export function getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, hasValidCharSize: boolean, actualCellWidth: number, actualCellHeight: number, isSelection?: boolean): [number, number] | undefined {
export function getCoords(window: Pick<Window, 'getComputedStyle'>, event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, hasValidCharSize: boolean, actualCellWidth: number, actualCellHeight: number, isSelection?: boolean): [number, number] | undefined {
// Coordinates cannot be measured if there are no valid
if (!hasValidCharSize) {
return undefined;
}
const coords = getCoordsRelativeToElement(event, element);
const coords = getCoordsRelativeToElement(window, event, element);
if (!coords) {
return undefined;
}
+1
View File
@@ -74,6 +74,7 @@ export class Terminal implements ITerminalApi {
public get onScroll(): IEvent<number> { return this._core.onScroll; }
public get onSelectionChange(): IEvent<void> { return this._core.onSelectionChange; }
public get onTitleChange(): IEvent<string> { return this._core.onTitleChange; }
public get onWriteParsed(): IEvent<void> { return this._core.onWriteParsed; }
public get element(): HTMLElement | undefined { return this._core.element; }
public get parser(): IParser {
+33 -1
View File
@@ -29,6 +29,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
private _scaledCharLeft: number = 0;
private _scaledCharTop: number = 0;
private _selectionStart: [number, number] | undefined;
private _selectionEnd: [number, number] | undefined;
private _columnSelectMode: boolean = false;
protected _charAtlas: BaseCharAtlas | undefined;
/**
@@ -80,7 +84,12 @@ export abstract class BaseRenderLayer implements IRenderLayer {
public onFocus(): void {}
public onCursorMove(): void {}
public onGridChanged(startRow: number, endRow: number): void {}
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {}
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
this._selectionStart = start;
this._selectionEnd = end;
this._columnSelectMode = columnSelectMode;
}
public setColors(colorSet: IColorSet): void {
this._refreshCharAtlas(colorSet);
@@ -457,6 +466,13 @@ export abstract class BaseRenderLayer implements IRenderLayer {
isTop = d.options.layer === 'top';
}
// Apply selection foreground if applicable
if (!isTop) {
if (this._colors.selectionForeground && this._isCellInSelection(x, y)) {
fgOverride = this._colors.selectionForeground.rgba;
}
}
if (!bgOverride && !fgOverride && (this._optionsService.rawOptions.minimumContrastRatio === 1 || isPowerlineGlyph(cell.getCode()))) {
return undefined;
}
@@ -546,5 +562,21 @@ export abstract class BaseRenderLayer implements IRenderLayer {
return this._colors.foreground.rgba;
}
}
private _isCellInSelection(x: number, y: number): boolean {
const start = this._selectionStart;
const end = this._selectionEnd;
if (!start || !end) {
return false;
}
if (this._columnSelectMode) {
return x >= start[0] && y >= start[1] &&
x < end[0] && y < end[1];
}
return (y > start[1] && y < end[1]) ||
(start[1] === end[1] && y === start[1] && x >= start[0] && x < end[0]) ||
(start[1] < end[1] && y === end[1] && x < end[0]) ||
(start[1] < end[1] && y === start[1] && x >= start[0]);
}
}
+4
View File
@@ -119,6 +119,10 @@ export class Renderer extends Disposable implements IRenderer {
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
this._runOperation(l => l.onSelectionChanged(start, end, columnSelectMode));
// Selection foreground requires a full re-render
if (this._colors.selectionForeground) {
this._onRequestRedraw.fire({ start: 0, end: this._bufferService.rows - 1 });
}
}
public onCursorMove(): void {
@@ -54,6 +54,8 @@ export class SelectionRenderLayer extends BaseRenderLayer {
}
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
super.onSelectionChanged(start, end, columnSelectMode);
// Selection has not changed
if (!this._didStateChange(start, end, columnSelectMode, this._bufferService.buffer.ydisp)) {
return;
+3
View File
@@ -281,6 +281,9 @@ export class DomRenderer extends Disposable implements IRenderer {
this._selectionContainer.removeChild(this._selectionContainer.children[0]);
}
this._rowFactory.onSelectionChanged(start, end, columnSelectMode);
this.renderRows(0, this._bufferService.rows - 1);
// Selection does not exist
if (!start || !end) {
return;
@@ -12,7 +12,7 @@ import { IBufferLine } from 'common/Types';
import { CellData } from 'common/buffer/CellData';
import { MockCoreService, MockDecorationService, MockOptionsService } from 'common/TestUtils.test';
import { css } from 'common/Color';
import { MockCharacterJoinerService } from 'browser/TestUtils.test';
import { MockCharacterJoinerService, MockSelectionService } from 'browser/TestUtils.test';
describe('DomRendererRowFactory', () => {
let dom: jsdom.JSDOM;
@@ -7,10 +7,10 @@ import { IBufferLine, ICellData, IColor } from 'common/Types';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants';
import { CellData } from 'common/buffer/CellData';
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { color, rgba } from 'common/Color';
import { IColorSet } from 'browser/Types';
import { ICharacterJoinerService } from 'browser/services/Services';
import { ICharacterJoinerService, ISelectionService } from 'browser/services/Services';
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
import { isPowerlineGlyph } from 'browser/renderer/RendererUtils';
@@ -28,6 +28,10 @@ export const CURSOR_STYLE_UNDERLINE_CLASS = 'xterm-cursor-underline';
export class DomRendererRowFactory {
private _workCell: CellData = new CellData();
private _selectionStart: [number, number] | undefined;
private _selectionEnd: [number, number] | undefined;
private _columnSelectMode: boolean = false;
constructor(
private readonly _document: Document,
private _colors: IColorSet,
@@ -42,6 +46,12 @@ export class DomRendererRowFactory {
this._colors = colors;
}
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
this._selectionStart = start;
this._selectionEnd = end;
this._columnSelectMode = columnSelectMode;
}
public createRow(lineData: IBufferLine, row: number, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cursorBlink: boolean, cellWidth: number, cols: number): DocumentFragment {
const fragment = this._document.createDocumentFragment();
@@ -195,6 +205,20 @@ export class DomRendererRowFactory {
isTop = d.options.layer === 'top';
}
// Apply selection foreground if applicable
if (!isTop) {
if (this._colors.selectionForeground && this._isCellInSelection(x, row)) {
fgColorMode = Attributes.CM_RGB;
fg = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
fgOverride = this._colors.selectionForeground;
}
}
// If it's a top decoration, render above the selection
if (isTop) {
charElement.classList.add(`xterm-decoration-top`);
}
// Foreground
switch (fgColorMode) {
case Attributes.CM_P16:
@@ -278,6 +302,22 @@ export class DomRendererRowFactory {
private _addStyle(element: HTMLElement, style: string): void {
element.setAttribute('style', `${element.getAttribute('style') || ''}${style};`);
}
private _isCellInSelection(x: number, y: number): boolean {
const start = this._selectionStart;
const end = this._selectionEnd;
if (!start || !end) {
return false;
}
if (this._columnSelectMode) {
return x >= start[0] && y >= start[1] &&
x < end[0] && y < end[1];
}
return (y > start[1] && y < end[1]) ||
(start[1] === end[1] && y === start[1] && x >= start[0] && x < end[0]) ||
(start[1] < end[1] && y === end[1] && x < end[0]) ||
(start[1] < end[1] && y === start[1] && x >= start[0]);
}
}
function padStart(text: string, padChar: string, length: number): string {
+1
View File
@@ -17,6 +17,7 @@ export class MouseService implements IMouseService {
public getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined {
return getCoords(
window,
event,
element,
colCount,
+11 -7
View File
@@ -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 } from 'browser/Types';
import { IColorSet, IRenderDebouncer, IRenderDebouncerWithCallback } from 'browser/Types';
import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services';
import { ICharSizeService, IRenderService } from 'browser/services/Services';
@@ -22,7 +22,7 @@ interface ISelectionState {
export class RenderService extends Disposable implements IRenderService {
public serviceBrand: undefined;
private _renderDebouncer: IRenderDebouncer;
private _renderDebouncer: IRenderDebouncerWithCallback;
private _screenDprMonitor: ScreenDprMonitor;
private _isPaused: boolean = false;
@@ -39,8 +39,8 @@ export class RenderService extends Disposable implements IRenderService {
private _onDimensionsChange = new EventEmitter<IRenderDimensions>();
public get onDimensionsChange(): IEvent<IRenderDimensions> { return this._onDimensionsChange.event; }
private _onRenderedBufferChange = new EventEmitter<{ start: number, end: number }>();
public get onRenderedBufferChange(): IEvent<{ start: number, end: number }> { return this._onRenderedBufferChange.event; }
private _onRenderedViewportChange = new EventEmitter<{ start: number, end: number }>();
public get onRenderedViewportChange(): IEvent<{ start: number, end: number }> { return this._onRenderedViewportChange.event; }
private _onRender = new EventEmitter<{ start: number, end: number }>();
public get onRender(): IEvent<{ start: number, end: number }> { return this._onRender.event; }
private _onRefreshRequest = new EventEmitter<{ start: number, end: number }>();
@@ -70,7 +70,7 @@ export class RenderService extends Disposable implements IRenderService {
this.register(bufferService.onResize(() => this._fullRefresh()));
this.register(bufferService.buffers.onBufferActivate(() => this._renderer?.clear()));
this.register(optionsService.onOptionChange(() => this._renderer.onOptionsChanged()));
this.register(optionsService.onOptionChange(() => this._handleOptionsChanged()));
this.register(this._charSizeService.onCharSizeChange(() => this.onCharSizeChanged()));
// Do a full refresh whenever any decoration is added or removed. This may not actually result
@@ -131,7 +131,7 @@ export class RenderService extends Disposable implements IRenderService {
// Fire render event only if it was not a redraw
if (!this._isNextRenderRedrawOnly) {
this._onRenderedBufferChange.fire({ start, end });
this._onRenderedViewportChange.fire({ start, end });
}
this._onRender.fire({ start, end });
this._isNextRenderRedrawOnly = true;
@@ -142,7 +142,7 @@ export class RenderService extends Disposable implements IRenderService {
this._fireOnCanvasResize();
}
public changeOptions(): void {
private _handleOptionsChanged(): void {
this._renderer.onOptionsChanged();
this.refreshRows(0, this._rowCount - 1);
this._fireOnCanvasResize();
@@ -171,6 +171,10 @@ export class RenderService extends Disposable implements IRenderService {
this._fullRefresh();
}
public addRefreshCallback(callback: FrameRequestCallback): number {
return this._renderDebouncer.addRefreshCallback(callback);
}
private _fullRefresh(): void {
if (this._isPaused) {
this._needsFullRefresh = true;
+11 -1
View File
@@ -308,6 +308,15 @@ export class SelectionService extends Disposable implements ISelectionService {
return this._areCoordsInSelection(coords, start, end);
}
public isCellInSelection(x: number, y: number): boolean {
const start = this._model.finalSelectionStart;
const end = this._model.finalSelectionEnd;
if (!start || !end) {
return false;
}
return this._areCoordsInSelection([x, y], start, end);
}
protected _areCoordsInSelection(coords: [number, number], start: [number, number], end: [number, number]): boolean {
return (coords[1] > start[1] && coords[1] < end[1]) ||
(start[1] === end[1] && coords[1] === start[1] && coords[0] >= start[0] && coords[0] < end[0]) ||
@@ -393,7 +402,7 @@ export class SelectionService extends Disposable implements ISelectionService {
* @param event The mouse event.
*/
private _getMouseEventScrollAmount(event: MouseEvent): number {
let offset = getCoordsRelativeToElement(event, this._screenElement)[1];
let offset = getCoordsRelativeToElement(window, event, this._screenElement)[1];
const terminalHeight = this._renderService.dimensions.canvasHeight;
if (offset >= 0 && offset <= terminalHeight) {
return 0;
@@ -771,6 +780,7 @@ export class SelectionService extends Disposable implements ISelectionService {
this._model.selectionStart = [col, row];
this._model.selectionStartLength = length;
this.refresh();
this._fireEventIfSelectionChanged();
}
public rightClickSelect(ev: MouseEvent): void {
+5 -3
View File
@@ -5,7 +5,7 @@
import { IEvent } from 'common/EventEmitter';
import { IRenderDimensions, IRenderer } from 'browser/renderer/Types';
import { IColorSet } from 'browser/Types';
import { IColorSet, IRenderDebouncer } from 'browser/Types';
import { ISelectionRedrawRequestEvent as ISelectionRequestRedrawEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
import { createDecorator } from 'common/services/ServiceRegistry';
import { IDisposable } from 'common/Types';
@@ -49,7 +49,7 @@ export interface IRenderService extends IDisposable {
* Fires when buffer changes are rendered. This does not fire when only cursor
* or selections are rendered.
*/
onRenderedBufferChange: IEvent<{ start: number, end: number }>;
onRenderedViewportChange: IEvent<{ start: number, end: number }>;
/**
* Fires on render
*/
@@ -58,10 +58,11 @@ export interface IRenderService extends IDisposable {
dimensions: IRenderDimensions;
addRefreshCallback(callback: FrameRequestCallback): number;
refreshRows(start: number, end: number): void;
clearTextureAtlas(): void;
resize(cols: number, rows: number): void;
changeOptions(): void;
setRenderer(renderer: IRenderer): void;
setColors(colors: IColorSet): void;
onDevicePixelRatioChange(): void;
@@ -101,6 +102,7 @@ export interface ISelectionService {
shouldForceSelection(event: MouseEvent): boolean;
refresh(isLinuxMouseSelection?: boolean): void;
onMouseDown(event: MouseEvent): void;
isCellInSelection(x: number, y: number): boolean;
}
export const ISoundService = createDecorator<ISoundService>('SoundService');
+52
View File
@@ -153,6 +153,44 @@ describe('Color', () => {
describe('css', () => {
describe('toColor', () => {
it('should convert the #rgb format to an IColor', () => {
assert.deepEqual(css.toColor('#000'), { css: '#000000', rgba: 0x000000FF });
assert.deepEqual(css.toColor('#111'), { css: '#111111', rgba: 0x111111FF });
assert.deepEqual(css.toColor('#222'), { css: '#222222', rgba: 0x222222FF });
assert.deepEqual(css.toColor('#333'), { css: '#333333', rgba: 0x333333FF });
assert.deepEqual(css.toColor('#444'), { css: '#444444', rgba: 0x444444FF });
assert.deepEqual(css.toColor('#555'), { css: '#555555', rgba: 0x555555FF });
assert.deepEqual(css.toColor('#666'), { css: '#666666', rgba: 0x666666FF });
assert.deepEqual(css.toColor('#777'), { css: '#777777', rgba: 0x777777FF });
assert.deepEqual(css.toColor('#888'), { css: '#888888', rgba: 0x888888FF });
assert.deepEqual(css.toColor('#999'), { css: '#999999', rgba: 0x999999FF });
assert.deepEqual(css.toColor('#aaa'), { css: '#aaaaaa', rgba: 0xaaaaaaFF });
assert.deepEqual(css.toColor('#bbb'), { css: '#bbbbbb', rgba: 0xbbbbbbFF });
assert.deepEqual(css.toColor('#ccc'), { css: '#cccccc', rgba: 0xccccccFF });
assert.deepEqual(css.toColor('#ddd'), { css: '#dddddd', rgba: 0xddddddFF });
assert.deepEqual(css.toColor('#eee'), { css: '#eeeeee', rgba: 0xeeeeeeFF });
assert.deepEqual(css.toColor('#fff'), { css: '#ffffff', rgba: 0xffffffFF });
assert.deepEqual(css.toColor('#fff'), { css: '#ffffff', rgba: 0xffffffFF });
});
it('should convert the #rgb format to an IColor', () => {
assert.deepEqual(css.toColor('#0000'), { css: '#00000000', rgba: 0x00000000 });
assert.deepEqual(css.toColor('#1111'), { css: '#11111111', rgba: 0x11111111 });
assert.deepEqual(css.toColor('#2222'), { css: '#22222222', rgba: 0x22222222 });
assert.deepEqual(css.toColor('#3333'), { css: '#33333333', rgba: 0x33333333 });
assert.deepEqual(css.toColor('#4444'), { css: '#44444444', rgba: 0x44444444 });
assert.deepEqual(css.toColor('#5555'), { css: '#55555555', rgba: 0x55555555 });
assert.deepEqual(css.toColor('#6666'), { css: '#66666666', rgba: 0x66666666 });
assert.deepEqual(css.toColor('#7777'), { css: '#77777777', rgba: 0x77777777 });
assert.deepEqual(css.toColor('#8888'), { css: '#88888888', rgba: 0x88888888 });
assert.deepEqual(css.toColor('#9999'), { css: '#99999999', rgba: 0x99999999 });
assert.deepEqual(css.toColor('#aaaa'), { css: '#aaaaaaaa', rgba: 0xaaaaaaaa });
assert.deepEqual(css.toColor('#bbbb'), { css: '#bbbbbbbb', rgba: 0xbbbbbbbb });
assert.deepEqual(css.toColor('#cccc'), { css: '#cccccccc', rgba: 0xcccccccc });
assert.deepEqual(css.toColor('#dddd'), { css: '#dddddddd', rgba: 0xdddddddd });
assert.deepEqual(css.toColor('#eeee'), { css: '#eeeeeeee', rgba: 0xeeeeeeee });
assert.deepEqual(css.toColor('#ffff'), { css: '#ffffffff', rgba: 0xffffffff });
assert.deepEqual(css.toColor('#ffff'), { css: '#ffffffff', rgba: 0xffffffff });
});
it('should convert the #rrggbb format to an IColor', () => {
assert.deepEqual(css.toColor('#000000'), { css: '#000000', rgba: 0x000000FF });
assert.deepEqual(css.toColor('#101010'), { css: '#101010', rgba: 0x101010FF });
@@ -191,6 +229,20 @@ describe('Color', () => {
assert.deepEqual(css.toColor('#f0f0f0f0'), { css: '#f0f0f0f0', rgba: 0xf0f0f0f0 });
assert.deepEqual(css.toColor('#ffffffff'), { css: '#ffffffff', rgba: 0xffffffff });
});
it('should convert the rgb() format to an IColor', () => {
assert.deepEqual(css.toColor('rgb(0, 0, 0)'), { css: '#000000ff', rgba: 0x000000ff });
assert.deepEqual(css.toColor('rgb(80, 0, 0)'), { css: '#500000ff', rgba: 0x500000ff });
assert.deepEqual(css.toColor('rgb(0, 80, 0)'), { css: '#005000ff', rgba: 0x005000ff });
assert.deepEqual(css.toColor('rgb(0, 0, 80)'), { css: '#000050ff', rgba: 0x000050ff });
assert.deepEqual(css.toColor('rgb(255, 255, 255)'), { css: '#ffffffff', rgba: 0xffffffff });
});
it('should convert the rgba() format to an IColor', () => {
assert.deepEqual(css.toColor('rgba(0, 0, 0, 0)'), { css: '#00000000', rgba: 0x00000000 });
assert.deepEqual(css.toColor('rgba(80, 0, 0, 0.5)'), { css: '#50000080', rgba: 0x50000080 });
assert.deepEqual(css.toColor('rgba(0, 80, 0, 0.5)'), { css: '#00500080', rgba: 0x00500080 });
assert.deepEqual(css.toColor('rgba(0, 0, 80, 0.5)'), { css: '#00005080', rgba: 0x00005080 });
assert.deepEqual(css.toColor('rgba(255, 255, 255, 1)'), { css: '#ffffffff', rgba: 0xffffffff });
});
});
});
+66 -16
View File
@@ -94,17 +94,40 @@ export namespace color {
*/
export namespace css {
export function toColor(css: string): IColor {
switch (css.length) {
case 7: // #rrggbb
return {
css,
rgba: (parseInt(css.slice(1), 16) << 8 | 0xFF) >>> 0
};
case 9: // #rrggbbaa
return {
css,
rgba: parseInt(css.slice(1), 16) >>> 0
};
if (css.match(/#[0-9a-f]{3,8}/i)) {
switch (css.length) {
case 4: { // #rgb
const r = parseInt(css.slice(1, 2).repeat(2), 16);
const g = parseInt(css.slice(2, 3).repeat(2), 16);
const b = parseInt(css.slice(3, 4).repeat(2), 16);
return rgba.toColor(r, g, b);
}
case 5: { // #rgba
const r = parseInt(css.slice(1, 2).repeat(2), 16);
const g = parseInt(css.slice(2, 3).repeat(2), 16);
const b = parseInt(css.slice(3, 4).repeat(2), 16);
const a = parseInt(css.slice(4, 5).repeat(2), 16);
return rgba.toColor(r, g, b, a);
}
case 7: // #rrggbb
return {
css,
rgba: (parseInt(css.slice(1), 16) << 8 | 0xFF) >>> 0
};
case 9: // #rrggbbaa
return {
css,
rgba: parseInt(css.slice(1), 16) >>> 0
};
}
}
const rgbaMatch = css.match(/rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(,\s*(0|1|\d?\.(\d+))\s*)?\)/);
if (rgbaMatch) { // rgb() or rgba()
const r = parseInt(rgbaMatch[1]);
const g = parseInt(rgbaMatch[2]);
const b = parseInt(rgbaMatch[3]);
const a = Math.round((rgbaMatch[5] === undefined ? 1 : parseFloat(rgbaMatch[5])) * 0xFF);
return rgba.toColor(r, g, b, a);
}
throw new Error('css.toColor: Unsupported css format');
}
@@ -150,15 +173,42 @@ export namespace rgb {
* Helper functions where the source type is "rgba" (number: 0xrrggbbaa).
*/
export namespace rgba {
/**
* Given a foreground color and a background color, either increase or reduce the luminance of the
* foreground color until the specified contrast ratio is met. If pure white or black is hit
* without the contrast ratio being met, go the other direction using the background color as the
* foreground color and take either the first or second result depending on which has the higher
* contrast ratio.
*
* `undefined` will be returned if the contrast ratio is already met.
*
* @param bgRgba The background color in rgba format.
* @param fgRgba The foreground color in rgba format.
* @param ratio The contrast ratio to achieve.
*/
export function ensureContrastRatio(bgRgba: number, fgRgba: number, ratio: number): number | undefined {
const bgL = rgb.relativeLuminance(bgRgba >> 8);
const fgL = rgb.relativeLuminance(fgRgba >> 8);
const cr = contrastRatio(bgL, fgL);
if (cr < ratio) {
if (fgL < bgL) {
return reduceLuminance(bgRgba, fgRgba, ratio);
const resultA = reduceLuminance(bgRgba, fgRgba, ratio);
const resultARatio = contrastRatio(bgL, rgb.relativeLuminance(resultA >> 8));
if (resultARatio < ratio) {
const resultB = increaseLuminance(bgRgba, bgRgba, ratio);
const resultBRatio = contrastRatio(bgL, rgb.relativeLuminance(resultB >> 8));
return resultARatio > resultBRatio ? resultA : resultB;
}
return resultA;
}
return increaseLuminance(bgRgba, fgRgba, ratio);
const resultA = increaseLuminance(bgRgba, fgRgba, ratio);
const resultARatio = contrastRatio(bgL, rgb.relativeLuminance(resultA >> 8));
if (resultARatio < ratio) {
const resultB = reduceLuminance(bgRgba, bgRgba, ratio);
const resultBRatio = contrastRatio(bgL, rgb.relativeLuminance(resultB >> 8));
return resultARatio > resultBRatio ? resultA : resultB;
}
return resultA;
}
return undefined;
}
@@ -208,10 +258,10 @@ export namespace rgba {
return [(value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF];
}
export function toColor(r: number, g: number, b: number): IColor {
export function toColor(r: number, g: number, b: number, a?: number): IColor {
return {
css: channels.toCss(r, g, b),
rgba: channels.toRgba(r, g, b)
css: channels.toCss(r, g, b, a),
rgba: channels.toRgba(r, g, b, a)
};
}
}
+3
View File
@@ -68,6 +68,8 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
private _onResize = new EventEmitter<{ cols: number, rows: number }>();
public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; }
protected _onScroll = new EventEmitter<IScrollEvent, void>();
public get onWriteParsed(): IEvent<void> { return this._onWriteParsed.event; }
protected _onWriteParsed = new EventEmitter<void>();
/**
* Internally we track the source of the scroll but this is meaningless outside the library so
* it's filtered out.
@@ -138,6 +140,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
// Setup WriteBuffer
this._writeBuffer = new WriteBuffer((data, promiseResult) => this._inputHandler.parse(data, promiseResult));
this.register(forwardEvent(this._writeBuffer.onWriteParsed, this._onWriteParsed));
}
public dispose(): void {
+5
View File
@@ -4,6 +4,8 @@
* @license MIT
*/
import { EventEmitter, IEvent } from 'common/EventEmitter';
declare const setTimeout: (handler: () => void, timeout?: number) => void;
/**
@@ -44,6 +46,8 @@ export class WriteBuffer {
private _bufferOffset = 0;
private _isSyncWriting = false;
private _syncCalls = 0;
public get onWriteParsed(): IEvent<void> { return this._onWriteParsed.event; }
private _onWriteParsed = new EventEmitter<void>();
constructor(private _action: (data: string | Uint8Array, promiseResult?: boolean) => void | Promise<boolean>) { }
@@ -220,5 +224,6 @@ export class WriteBuffer {
this._pendingData = 0;
this._bufferOffset = 0;
}
this._onWriteParsed.fire();
}
}
+1
View File
@@ -259,6 +259,7 @@ export interface ITheme {
cursor?: string;
cursorAccent?: string;
selection?: string;
selectionForeground?: string;
black?: string;
red?: string;
green?: string;