Merge remote-tracking branch 'upstream/master' into osc52

This commit is contained in:
Ayman Bagabas
2024-04-07 23:22:01 +03:00
65 changed files with 1099 additions and 475 deletions
@@ -5,11 +5,13 @@
import { assert } from 'chai';
import { IBufferService } from 'common/services/Services';
import { Linkifier2 } from 'browser/Linkifier2';
import { Linkifier } from './Linkifier';
import { MockBufferService } from 'common/TestUtils.test';
import { ILink } from 'browser/Types';
import { LinkProviderService } from 'browser/services/LinkProviderService';
import jsdom = require('jsdom');
class TestLinkifier2 extends Linkifier2 {
class TestLinkifier2 extends Linkifier {
public set currentLink(link: any) {
this._currentLink = link;
}
@@ -43,8 +45,9 @@ describe('Linkifier2', () => {
};
beforeEach(() => {
const dom = new jsdom.JSDOM();
bufferService = new MockBufferService(100, 10);
linkifier = new TestLinkifier2(bufferService);
linkifier = new TestLinkifier2(dom.window.document.createElement('div'), null!, null!, bufferService, new LinkProviderService());
linkifier.currentLink = {
link,
state: {
@@ -4,18 +4,14 @@
*/
import { addDisposableDomListener } from 'browser/Lifecycle';
import { IBufferCellPosition, ILink, ILinkDecorations, ILinkProvider, ILinkWithState, ILinkifier2, ILinkifierEvent } from 'browser/Types';
import { IBufferCellPosition, ILink, ILinkDecorations, ILinkWithState, ILinkifier2, ILinkifierEvent } from 'browser/Types';
import { EventEmitter } from 'common/EventEmitter';
import { Disposable, disposeArray, getDisposeArrayDisposable, toDisposable } from 'common/Lifecycle';
import { IDisposable } from 'common/Types';
import { IBufferService } from 'common/services/Services';
import { IMouseService, IRenderService } from './services/Services';
import { ILinkProviderService, IMouseService, IRenderService } from './services/Services';
export class Linkifier2 extends Disposable implements ILinkifier2 {
private _element: HTMLElement | undefined;
private _mouseService: IMouseService | undefined;
private _renderService: IRenderService | undefined;
private _linkProviders: ILinkProvider[] = [];
export class Linkifier extends Disposable implements ILinkifier2 {
public get currentLink(): ILinkWithState | undefined { return this._currentLink; }
protected _currentLink: ILinkWithState | undefined;
private _mouseDownLink: ILinkWithState | undefined;
@@ -33,39 +29,24 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
public readonly onHideLinkUnderline = this._onHideLinkUnderline.event;
constructor(
@IBufferService private readonly _bufferService: IBufferService
private readonly _element: HTMLElement,
@IMouseService private readonly _mouseService: IMouseService,
@IRenderService private readonly _renderService: IRenderService,
@IBufferService private readonly _bufferService: IBufferService,
@ILinkProviderService private readonly _linkProviderService: ILinkProviderService
) {
super();
this.register(getDisposeArrayDisposable(this._linkCacheDisposables));
this.register(toDisposable(() => {
this._lastMouseEvent = undefined;
// Clear out link providers as they could easily cause an embedder memory leak
this._activeProviderReplies?.clear();
}));
// Listen to resize to catch the case where it's resized and the cursor is out of the viewport.
this.register(this._bufferService.onResize(() => {
this._clearCurrentLink();
this._wasResized = true;
}));
}
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
this._linkProviders.push(linkProvider);
return {
dispose: () => {
// Remove the link provider from the list
const providerIndex = this._linkProviders.indexOf(linkProvider);
if (providerIndex !== -1) {
this._linkProviders.splice(providerIndex, 1);
}
}
};
}
public attachToDom(element: HTMLElement, mouseService: IMouseService, renderService: IRenderService): void {
this._element = element;
this._mouseService = mouseService;
this._renderService = renderService;
this.register(addDisposableDomListener(this._element, 'mouseleave', () => {
this._isMouseOut = true;
this._clearCurrentLink();
@@ -78,10 +59,6 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
private _handleMouseMove(event: MouseEvent): void {
this._lastMouseEvent = event;
if (!this._element || !this._mouseService) {
return;
}
const position = this._positionFromMouseEvent(event, this._element, this._mouseService);
if (!position) {
return;
@@ -142,7 +119,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
let linkProvided = false;
// There is no link cached, so ask for one
for (const [i, linkProvider] of this._linkProviders.entries()) {
for (const [i, linkProvider] of this._linkProviderService.linkProviders.entries()) {
if (useLineCache) {
const existingReply = this._activeProviderReplies?.get(i);
// If there isn't a reply, the provider hasn't responded yet.
@@ -164,7 +141,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
// If all providers have responded, remove lower priority links that intersect ranges of
// higher priority links
if (this._activeProviderReplies?.size === this._linkProviders.length) {
if (this._activeProviderReplies?.size === this._linkProviderService.linkProviders.length) {
this._removeIntersectingLinks(position.y, this._activeProviderReplies);
}
});
@@ -220,7 +197,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
}
// Check if all the providers have responded
if (this._activeProviderReplies.size === this._linkProviders.length && !linkProvided) {
if (this._activeProviderReplies.size === this._linkProviderService.linkProviders.length && !linkProvided) {
// Respect the order of the link providers
for (let j = 0; j < this._activeProviderReplies.size; j++) {
const currentLink = this._activeProviderReplies.get(j)?.find(link => this._linkAtPosition(link.link, position));
@@ -240,7 +217,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
}
private _handleMouseUp(event: MouseEvent): void {
if (!this._element || !this._mouseService || !this._currentLink) {
if (!this._currentLink) {
return;
}
@@ -255,7 +232,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
}
private _clearCurrentLink(startRow?: number, endRow?: number): void {
if (!this._element || !this._currentLink || !this._lastMouseEvent) {
if (!this._currentLink || !this._lastMouseEvent) {
return;
}
@@ -268,7 +245,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
}
private _handleNewLink(linkWithState: ILinkWithState): void {
if (!this._element || !this._lastMouseEvent || !this._mouseService) {
if (!this._lastMouseEvent) {
return;
}
@@ -299,7 +276,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
if (this._currentLink?.state && this._currentLink.state.decorations.pointerCursor !== v) {
this._currentLink.state.decorations.pointerCursor = v;
if (this._currentLink.state.isHovered) {
this._element?.classList.toggle('xterm-cursor-pointer', v);
this._element.classList.toggle('xterm-cursor-pointer', v);
}
}
}
@@ -319,29 +296,27 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
// Listen to viewport changes to re-render the link under the cursor (only when the line the
// link is on changes)
if (this._renderService) {
this._linkCacheDisposables.push(this._renderService.onRenderedViewportChange(e => {
// Sanity check, this shouldn't happen in practice as this listener would be disposed
if (!this._currentLink) {
return;
}
// 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;
const end = this._bufferService.buffer.ydisp + 1 + e.end;
// Only clear the link if the viewport change happened on this line
if (this._currentLink.link.range.start.y >= start && this._currentLink.link.range.end.y <= end) {
this._clearCurrentLink(start, end);
if (this._lastMouseEvent && this._element) {
// re-eval previously active link after changes
const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element, this._mouseService!);
if (position) {
this._askForLink(position, false);
}
this._linkCacheDisposables.push(this._renderService.onRenderedViewportChange(e => {
// Sanity check, this shouldn't happen in practice as this listener would be disposed
if (!this._currentLink) {
return;
}
// 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;
const end = this._bufferService.buffer.ydisp + 1 + e.end;
// Only clear the link if the viewport change happened on this line
if (this._currentLink.link.range.start.y >= start && this._currentLink.link.range.end.y <= end) {
this._clearCurrentLink(start, end);
if (this._lastMouseEvent) {
// re-eval previously active link after changes
const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element, this._mouseService!);
if (position) {
this._askForLink(position, false);
}
}
}));
}
}
}));
}
}
+2 -1
View File
@@ -3,7 +3,8 @@
* @license MIT
*/
import { IBufferRange, ILink, ILinkProvider } from 'browser/Types';
import { IBufferRange, ILink } from 'browser/Types';
import { ILinkProvider } from 'browser/services/Services';
import { CellData } from 'common/buffer/CellData';
import { IBufferService, IOptionsService, IOscLinkService } from 'common/services/Services';
+6 -5
View File
@@ -4,6 +4,7 @@
*/
import { IRenderDebouncerWithCallback } from 'browser/Types';
import { ICoreBrowserService } from 'browser/services/Services';
/**
* Debounces calls to render terminal rows using animation frames.
@@ -16,14 +17,14 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback {
private _refreshCallbacks: FrameRequestCallback[] = [];
constructor(
private _parentWindow: Window,
private _renderCallback: (start: number, end: number) => void
private _renderCallback: (start: number, end: number) => void,
private readonly _coreBrowserService: ICoreBrowserService
) {
}
public dispose(): void {
if (this._animationFrame) {
this._parentWindow.cancelAnimationFrame(this._animationFrame);
this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame);
this._animationFrame = undefined;
}
}
@@ -31,7 +32,7 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback {
public addRefreshCallback(callback: FrameRequestCallback): number {
this._refreshCallbacks.push(callback);
if (!this._animationFrame) {
this._animationFrame = this._parentWindow.requestAnimationFrame(() => this._innerRefresh());
this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => this._innerRefresh());
}
return this._animationFrame;
}
@@ -49,7 +50,7 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback {
return;
}
this._animationFrame = this._parentWindow.requestAnimationFrame(() => this._innerRefresh());
this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => this._innerRefresh());
}
private _innerRefresh(): void {
+41 -33
View File
@@ -23,10 +23,10 @@
import { copyHandler, handlePasteEvent, moveTextAreaUnderMouseCursor, paste, rightClickHandler } from 'browser/Clipboard';
import { addDisposableDomListener } from 'browser/Lifecycle';
import { Linkifier2 } from 'browser/Linkifier2';
import { Linkifier } from './Linkifier';
import * as Strings from 'browser/LocalizableStrings';
import { OscLinkProvider } from 'browser/OscLinkProvider';
import { CharacterJoinerHandler, CustomKeyEventHandler, IBrowser, IBufferRange, ICompositionHelper, ILinkifier2, ITerminal, IViewport } from 'browser/Types';
import { CharacterJoinerHandler, CustomKeyEventHandler, CustomWheelEventHandler, IBrowser, IBufferRange, ICompositionHelper, ILinkifier2, ITerminal, IViewport } from 'browser/Types';
import { Viewport } from 'browser/Viewport';
import { BufferDecorationRenderer } from 'browser/decorations/BufferDecorationRenderer';
import { OverviewRulerRenderer } from 'browser/decorations/OverviewRulerRenderer';
@@ -39,9 +39,9 @@ import { CoreBrowserService } from 'browser/services/CoreBrowserService';
import { MouseService } from 'browser/services/MouseService';
import { RenderService } from 'browser/services/RenderService';
import { SelectionService } from 'browser/services/SelectionService';
import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, ILinkProviderService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
import { ThemeService } from 'browser/services/ThemeService';
import { color, rgba } from 'common/Color';
import { channels, color } from 'common/Color';
import { CoreTerminal } from 'common/CoreTerminal';
import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter';
import { MutableDisposable, toDisposable } from 'common/Lifecycle';
@@ -57,6 +57,7 @@ import { IDecorationService } from 'common/services/Services';
import { IDecoration, IDecorationOptions, IDisposable, ILinkProvider, IMarker, IClipboardProvider } from '@xterm/xterm';
import { WindowsOptionsReportType } from '../common/InputHandler';
import { AccessibilityManager } from './AccessibilityManager';
import { LinkProviderService } from 'browser/services/LinkProviderService';
export class Terminal extends CoreTerminal implements ITerminal {
public textarea: HTMLTextAreaElement | undefined;
@@ -69,14 +70,19 @@ export class Terminal extends CoreTerminal implements ITerminal {
private _helperContainer: HTMLElement | undefined;
private _compositionView: HTMLElement | undefined;
public linkifier: ILinkifier2 | undefined;
private _overviewRulerRenderer: OverviewRulerRenderer | undefined;
public browser: IBrowser = Browser as any;
private _customKeyEventHandler: CustomKeyEventHandler | undefined;
private _customWheelEventHandler: CustomWheelEventHandler | undefined;
// browser services
// Browser services
private _decorationService: DecorationService;
private _linkProviderService: ILinkProviderService;
// Optional browser services
private _charSizeService: ICharSizeService | undefined;
private _coreBrowserService: ICoreBrowserService | undefined;
private _mouseService: IMouseService | undefined;
@@ -112,7 +118,6 @@ export class Terminal extends CoreTerminal implements ITerminal {
*/
private _unprocessedDeadKey: boolean = false;
public linkifier2: ILinkifier2;
public viewport: IViewport | undefined;
private _compositionHelper: ICompositionHelper | undefined;
private _accessibilityManager: MutableDisposable<AccessibilityManager> = this.register(new MutableDisposable());
@@ -149,10 +154,11 @@ 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);
this._linkProviderService = this._instantiationService.createInstance(LinkProviderService);
this._instantiationService.setService(ILinkProviderService, this._linkProviderService);
this._linkProviderService.registerLinkProvider(this._instantiationService.createInstance(OscLinkProvider));
// Setup InputHandler listeners
this.register(this._inputHandler.onRequestBell(() => this._onBell.fire()));
@@ -207,17 +213,17 @@ export class Terminal extends CoreTerminal implements ITerminal {
}
switch (req.type) {
case ColorRequestType.REPORT:
const channels = color.toColorRGB(acc === 'ansi'
const colorRgb = color.toColorRGB(acc === 'ansi'
? this._themeService.colors.ansi[req.index]
: this._themeService.colors[acc]);
this.coreService.triggerDataEvent(`${C0.ESC}]${ident};${toRgbString(channels)}${C1_ESCAPED.ST}`);
this.coreService.triggerDataEvent(`${C0.ESC}]${ident};${toRgbString(colorRgb)}${C1_ESCAPED.ST}`);
break;
case ColorRequestType.SET:
if (acc === 'ansi') {
this._themeService.modifyColors(colors => colors.ansi[req.index] = rgba.toColor(...req.color));
this._themeService.modifyColors(colors => colors.ansi[req.index] = channels.toColor(...req.color));
} else {
const narrowedAcc = acc;
this._themeService.modifyColors(colors => colors[narrowedAcc] = rgba.toColor(...req.color));
this._themeService.modifyColors(colors => colors[narrowedAcc] = channels.toColor(...req.color));
}
break;
case ColorRequestType.RESTORE:
@@ -262,11 +268,10 @@ export class Terminal extends CoreTerminal implements ITerminal {
/**
* Binds the desired focus behavior on a given terminal object.
*/
private _handleTextAreaFocus(ev: KeyboardEvent): void {
private _handleTextAreaFocus(ev: FocusEvent): void {
if (this.coreService.decPrivateModes.sendFocus) {
this.coreService.triggerDataEvent(C0.ESC + '[I');
}
this.updateCursorStyle(ev);
this.element!.classList.add('focus');
this._showCursor();
this._onFocus.fire();
@@ -430,6 +435,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
this.screenElement = this._document.createElement('div');
this.screenElement.classList.add('xterm-screen');
this.register(addDisposableDomListener(this.screenElement, 'mousemove', (ev: MouseEvent) => this.updateCursorStyle(ev)));
// Create the container that will hold helpers like the textarea for
// capturing DOM Events. Then produce the helpers.
this._helperContainer = this._document.createElement('div');
@@ -460,11 +466,10 @@ export class Terminal extends CoreTerminal implements ITerminal {
));
this._instantiationService.setService(ICoreBrowserService, this._coreBrowserService);
this.register(addDisposableDomListener(this.textarea, 'focus', (ev: KeyboardEvent) => this._handleTextAreaFocus(ev)));
this.register(addDisposableDomListener(this.textarea, 'focus', (ev: FocusEvent) => this._handleTextAreaFocus(ev)));
this.register(addDisposableDomListener(this.textarea, 'blur', () => this._handleTextAreaBlur()));
this._helperContainer.appendChild(this.textarea);
this._charSizeService = this._instantiationService.createInstance(CharSizeService, this._document, this._helperContainer);
this._instantiationService.setService(ICharSizeService, this._charSizeService);
@@ -484,6 +489,11 @@ export class Terminal extends CoreTerminal implements ITerminal {
this._compositionHelper = this._instantiationService.createInstance(CompositionHelper, this.textarea, this._compositionView);
this._helperContainer.appendChild(this._compositionView);
this._mouseService = this._instantiationService.createInstance(MouseService);
this._instantiationService.setService(IMouseService, this._mouseService);
this.linkifier = this.register(this._instantiationService.createInstance(Linkifier, this.screenElement));
// Performance: Add viewport and helper elements from the fragment
this.element.appendChild(fragment);
@@ -495,9 +505,6 @@ export class Terminal extends CoreTerminal implements ITerminal {
this._renderService.setRenderer(this._createRenderer());
}
this._mouseService = this._instantiationService.createInstance(MouseService);
this._instantiationService.setService(IMouseService, this._mouseService);
this.viewport = this._instantiationService.createInstance(Viewport, this._viewportElement, this._viewportScrollArea);
this.viewport.onRequestScrollLines(e => this.scrollLines(e.amount, e.suppressScrollEvent, ScrollSource.VIEWPORT)),
this.register(this._inputHandler.onRequestSyncScrollBar(() => this.viewport!.syncScrollArea()));
@@ -515,7 +522,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
this._selectionService = this.register(this._instantiationService.createInstance(SelectionService,
this.element,
this.screenElement,
this.linkifier2
this.linkifier
));
this._instantiationService.setService(ISelectionService, this._selectionService);
this.register(this._selectionService.onRequestScrollLines(e => this.scrollLines(e.amount, e.suppressScrollEvent)));
@@ -535,7 +542,6 @@ export class Terminal extends CoreTerminal implements ITerminal {
}));
this.register(addDisposableDomListener(this._viewportElement, 'scroll', () => this._selectionService!.refresh()));
this.linkifier2.attachToDom(this.screenElement, this._mouseService, this._renderService);
this.register(this._instantiationService.createInstance(BufferDecorationRenderer, this.screenElement));
this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService!.handleMouseDown(e)));
@@ -577,7 +583,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
}
private _createRenderer(): IRenderer {
return this._instantiationService.createInstance(DomRenderer, this, this._document!, this.element!, this.screenElement!, this._viewportElement!, this._helperContainer!, this.linkifier2);
return this._instantiationService.createInstance(DomRenderer, this, this._document!, this.element!, this.screenElement!, this._viewportElement!, this._helperContainer!, this.linkifier!);
}
/**
@@ -635,6 +641,9 @@ export class Terminal extends CoreTerminal implements ITerminal {
but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
break;
case 'wheel':
if (self._customWheelEventHandler && self._customWheelEventHandler(ev as WheelEvent) === false) {
return false;
}
const amount = self.viewport!.getLinesScrolled(ev as WheelEvent);
if (amount === 0) {
@@ -794,6 +803,10 @@ export class Terminal extends CoreTerminal implements ITerminal {
// do nothing, if app side handles wheel itself
if (requestedEvents.wheel) return;
if (this._customWheelEventHandler && this._customWheelEventHandler(ev) === false) {
return false;
}
if (!this.buffer.hasScrollback) {
// Convert wheel events into up/down events when the buffer does not have scrollback, this
// enables scrolling in apps hosted in the alt buffer such as vim or tmux.
@@ -849,7 +862,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
/**
* Change the cursor style for different selection modes
*/
public updateCursorStyle(ev: KeyboardEvent): void {
public updateCursorStyle(ev: KeyboardEvent | MouseEvent): void {
if (this._selectionService?.shouldColumnSelect(ev)) {
this.element!.classList.add('column-select');
} else {
@@ -880,21 +893,16 @@ export class Terminal extends CoreTerminal implements ITerminal {
paste(data, this.textarea!, this.coreService, this.optionsService);
}
/**
* Attaches a custom key event handler which is run before keys are processed,
* giving consumers of xterm.js ultimate control as to what keys should be
* processed by the terminal and what keys should not.
* @param customKeyEventHandler The custom KeyboardEvent handler to attach.
* This is a function that takes a KeyboardEvent, allowing consumers to stop
* propagation and/or prevent the default action. The function returns whether
* the event should be processed by xterm.js.
*/
public attachCustomKeyEventHandler(customKeyEventHandler: CustomKeyEventHandler): void {
this._customKeyEventHandler = customKeyEventHandler;
}
public attachCustomWheelEventHandler(customWheelEventHandler: CustomWheelEventHandler): void {
this._customWheelEventHandler = customWheelEventHandler;
}
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
return this.linkifier2.registerLinkProvider(linkProvider);
return this._linkProviderService.registerLinkProvider(linkProvider);
}
public registerClipboardProvider(provider: IClipboardProvider): IDisposable {
+7 -1
View File
@@ -48,6 +48,7 @@ export class MockTerminal implements ITerminal {
public onRender!: IEvent<{ start: number, end: number }>;
public onResize!: IEvent<{ cols: number, rows: number }>;
public markers!: IMarker[];
public linkifier: ILinkifier2 | undefined;
public coreMouseService!: ICoreMouseService;
public coreService!: ICoreService;
public optionsService!: IOptionsService;
@@ -71,6 +72,9 @@ export class MockTerminal implements ITerminal {
public focus(): void {
throw new Error('Method not implemented.');
}
public input(data: string, wasUserInput: boolean = true): void {
throw new Error('Method not implemented.');
}
public resize(columns: number, rows: number): void {
throw new Error('Method not implemented.');
}
@@ -86,6 +90,9 @@ export class MockTerminal implements ITerminal {
public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
throw new Error('Method not implemented.');
}
public attachCustomWheelEventHandler(customWheelEventHandler: (event: WheelEvent) => boolean): void {
throw new Error('Method not implemented.');
}
public registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise<boolean>): IDisposable {
throw new Error('Method not implemented.');
}
@@ -151,7 +158,6 @@ export class MockTerminal implements ITerminal {
}
public bracketedPasteMode!: boolean;
public renderer!: IRenderer;
public linkifier2!: ILinkifier2;
public isFocused!: boolean;
public options!: Required<ITerminalOptions>;
public element!: HTMLElement;
+2 -9
View File
@@ -7,7 +7,6 @@ import { IEvent } from 'common/EventEmitter';
import { CharData, IColor, ICoreTerminal, ITerminalOptions } from 'common/Types';
import { IBuffer } from 'common/buffer/Types';
import { IDisposable, Terminal as ITerminalApi } from '@xterm/xterm';
import { IMouseService, IRenderService } from './services/Services';
/**
* A portion of the public API that are implemented identially internally and simply passed through.
@@ -18,9 +17,9 @@ export interface ITerminal extends InternalPassthroughApis, ICoreTerminal {
screenElement: HTMLElement | undefined;
browser: IBrowser;
buffer: IBuffer;
linkifier: ILinkifier2 | undefined;
viewport: IViewport | undefined;
options: Required<ITerminalOptions>;
linkifier2: ILinkifier2;
onBlur: IEvent<void>;
onFocus: IEvent<void>;
@@ -32,6 +31,7 @@ export interface ITerminal extends InternalPassthroughApis, ICoreTerminal {
}
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
export type CustomWheelEventHandler = (event: WheelEvent) => boolean;
export type LineData = CharData[];
@@ -127,13 +127,6 @@ export interface ILinkifier2 extends IDisposable {
onShowLinkUnderline: IEvent<ILinkifierEvent>;
onHideLinkUnderline: IEvent<ILinkifierEvent>;
readonly currentLink: ILinkWithState | undefined;
attachToDom(element: HTMLElement, mouseService: IMouseService, renderService: IRenderService): void;
registerLinkProvider(linkProvider: ILinkProvider): IDisposable;
}
interface ILinkProvider {
provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void;
}
interface ILink {
+6
View File
@@ -138,6 +138,9 @@ export class Terminal extends Disposable implements ITerminalApi {
public focus(): void {
this._core.focus();
}
public input(data: string, wasUserInput: boolean = true): void {
this._core.input(data, wasUserInput);
}
public resize(columns: number, rows: number): void {
this._verifyIntegers(columns, rows);
this._core.resize(columns, rows);
@@ -148,6 +151,9 @@ export class Terminal extends Disposable implements ITerminalApi {
public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
this._core.attachCustomKeyEventHandler(customKeyEventHandler);
}
public attachCustomWheelEventHandler(customWheelEventHandler: (event: WheelEvent) => boolean): void {
this._core.attachCustomWheelEventHandler(customWheelEventHandler);
}
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
return this._core.registerLinkProvider(linkProvider);
}
+24 -5
View File
@@ -183,14 +183,23 @@ export class DomRenderer extends Disposable implements IRenderer {
` font-style: italic;` +
`}`;
// Blink animation
const blinkAnimationUnderlineId = `blink_underline_${this._terminalClass}`;
const blinkAnimationBarId = `blink_bar_${this._terminalClass}`;
const blinkAnimationBlockId = `blink_block_${this._terminalClass}`;
styles +=
`@keyframes blink_box_shadow` + `_` + this._terminalClass + ` {` +
`@keyframes ${blinkAnimationUnderlineId} {` +
` 50% {` +
` border-bottom-style: hidden;` +
` }` +
`}`;
styles +=
`@keyframes blink_block` + `_` + this._terminalClass + ` {` +
`@keyframes ${blinkAnimationBarId} {` +
` 50% {` +
` box-shadow: none;` +
` }` +
`}`;
styles +=
`@keyframes ${blinkAnimationBlockId} {` +
` 0% {` +
` background-color: ${colors.cursor.css};` +
` color: ${colors.cursorAccent.css};` +
@@ -202,13 +211,23 @@ export class DomRenderer extends Disposable implements IRenderer {
`}`;
// Cursor
styles +=
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}:not(.${RowCss.CURSOR_STYLE_BLOCK_CLASS}) {` +
` animation: blink_box_shadow` + `_` + this._terminalClass + ` 1s step-end infinite;` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}.${RowCss.CURSOR_STYLE_UNDERLINE_CLASS} {` +
` animation: ${blinkAnimationUnderlineId} 1s step-end infinite;` +
`}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}.${RowCss.CURSOR_STYLE_BAR_CLASS} {` +
` animation: ${blinkAnimationBarId} 1s step-end infinite;` +
`}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS} {` +
` animation: blink_block` + `_` + this._terminalClass + ` 1s step-end infinite;` +
` animation: ${blinkAnimationBlockId} 1s step-end infinite;` +
`}` +
// !important helps fix an issue where the cursor will not render on top of the selection,
// however it's very hard to fix this issue and retain the blink animation without the use of
// !important. So this edge case fails when cursor blink is on.
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS} {` +
` background-color: ${colors.cursor.css};` +
` color: ${colors.cursorAccent.css};` +
`}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS}:not(.${RowCss.CURSOR_BLINK_CLASS}) {` +
` background-color: ${colors.cursor.css} !important;` +
` color: ${colors.cursorAccent.css} !important;` +
`}` +
@@ -8,10 +8,10 @@ import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
import { WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants';
import { CellData } from 'common/buffer/CellData';
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { color, rgba } from 'common/Color';
import { channels, color } from 'common/Color';
import { ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
import { excludeFromContrastRatioDemands } from 'browser/renderer/shared/RendererUtils';
import { treatGlyphAsBackgroundColor } from 'browser/renderer/shared/RendererUtils';
import { AttributeData } from 'common/buffer/AttributeData';
import { WidthCache } from 'browser/renderer/dom/WidthCache';
import { IColorContrastCache } from 'browser/Types';
@@ -376,7 +376,7 @@ export class DomRendererRowFactory {
classes.push(`xterm-bg-${bg}`);
break;
case Attributes.CM_RGB:
resolvedBg = rgba.toColor(bg >> 16, bg >> 8 & 0xFF, bg & 0xFF);
resolvedBg = channels.toColor(bg >> 16, bg >> 8 & 0xFF, bg & 0xFF);
this._addStyle(charElement, `background-color:#${padStart((bg >>> 0).toString(16), '0', 6)}`);
break;
case Attributes.CM_DEFAULT:
@@ -408,7 +408,7 @@ export class DomRendererRowFactory {
}
break;
case Attributes.CM_RGB:
const color = rgba.toColor(
const color = channels.toColor(
(fg >> 16) & 0xFF,
(fg >> 8) & 0xFF,
(fg ) & 0xFF
@@ -458,7 +458,7 @@ export class DomRendererRowFactory {
}
private _applyMinimumContrast(element: HTMLElement, bg: IColor, fg: IColor, cell: ICellData, bgOverride: IColor | undefined, fgOverride: IColor | undefined): boolean {
if (this._optionsService.rawOptions.minimumContrastRatio === 1 || excludeFromContrastRatioDemands(cell.getCode())) {
if (this._optionsService.rawOptions.minimumContrastRatio === 1 || treatGlyphAsBackgroundColor(cell.getCode())) {
return false;
}
+11 -4
View File
@@ -134,9 +134,14 @@ export class WidthCache implements IDisposable {
public get(c: string, bold: boolean | number, italic: boolean | number): number {
let cp = 0;
if (!bold && !italic && c.length === 1 && (cp = c.charCodeAt(0)) < WidthCacheSettings.FLAT_SIZE) {
return this._flat[cp] !== WidthCacheSettings.FLAT_UNSET
? this._flat[cp]
: (this._flat[cp] = this._measure(c, 0));
if (this._flat[cp] !== WidthCacheSettings.FLAT_UNSET) {
return this._flat[cp];
}
const width = this._measure(c, 0);
if (width > 0) {
this._flat[cp] = width;
}
return width;
}
let key = c;
if (bold) key += 'B';
@@ -147,7 +152,9 @@ export class WidthCache implements IDisposable {
if (bold) variant |= FontVariant.BOLD;
if (italic) variant |= FontVariant.ITALIC;
width = this._measure(c, variant);
this._holey!.set(key, width);
if (width > 0) {
this._holey!.set(key, width);
}
}
return width;
}
@@ -5,6 +5,8 @@ import { Attributes, BgFlags, ExtFlags, FgFlags, NULL_CELL_CODE, UnderlineStyle
import { IDecorationService, IOptionsService } from 'common/services/Services';
import { ICellData } from 'common/Types';
import { Terminal } from '@xterm/xterm';
import { rgba } from 'common/Color';
import { treatGlyphAsBackgroundColor } from 'browser/renderer/shared/RendererUtils';
// Work variables to avoid garbage collection
let $fg = 0;
@@ -65,11 +67,11 @@ export class CellColorResolver {
// Apply decorations on the bottom layer
this._decorationService.forEachDecorationAtCell(x, y, 'bottom', d => {
if (d.backgroundColorRGB) {
$bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
$bg = d.backgroundColorRGB.rgba >> 8 & Attributes.RGB_MASK;
$hasBg = true;
}
if (d.foregroundColorRGB) {
$fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
$fg = d.foregroundColorRGB.rgba >> 8 & Attributes.RGB_MASK;
$hasFg = true;
}
});
@@ -77,10 +79,94 @@ export class CellColorResolver {
// Apply the selection color if needed
$isSelected = this._selectionRenderModel.isCellSelected(this._terminal, x, y);
if ($isSelected) {
$bg = (this._coreBrowserService.isFocused ? $colors.selectionBackgroundOpaque : $colors.selectionInactiveBackgroundOpaque).rgba >> 8 & 0xFFFFFF;
// If the cell has a bg color, retain the color by blending it with the selection color
if (
(this.result.fg & FgFlags.INVERSE) ||
(this.result.bg & Attributes.CM_MASK) !== Attributes.CM_DEFAULT
) {
// Resolve the standard bg color
if (this.result.fg & FgFlags.INVERSE) {
switch (this.result.fg & Attributes.CM_MASK) {
case Attributes.CM_P16:
case Attributes.CM_P256:
$bg = this._themeService.colors.ansi[this.result.fg & Attributes.PCOLOR_MASK].rgba;
break;
case Attributes.CM_RGB:
$bg = ((this.result.fg & Attributes.RGB_MASK) << 8) | 0xFF;
break;
case Attributes.CM_DEFAULT:
default:
$bg = this._themeService.colors.foreground.rgba;
}
} else {
switch (this.result.bg & Attributes.CM_MASK) {
case Attributes.CM_P16:
case Attributes.CM_P256:
$bg = this._themeService.colors.ansi[this.result.bg & Attributes.PCOLOR_MASK].rgba;
break;
case Attributes.CM_RGB:
$bg = ((this.result.bg & Attributes.RGB_MASK) << 8) | 0xFF;
break;
// No need to consider default bg color here as it's not possible
}
}
// Blend with selection bg color
$bg = rgba.blend(
$bg,
((this._coreBrowserService.isFocused ? $colors.selectionBackgroundOpaque : $colors.selectionInactiveBackgroundOpaque).rgba & 0xFFFFFF00) | 0x80
) >> 8 & Attributes.RGB_MASK;
} else {
$bg = (this._coreBrowserService.isFocused ? $colors.selectionBackgroundOpaque : $colors.selectionInactiveBackgroundOpaque).rgba >> 8 & Attributes.RGB_MASK;
}
$hasBg = true;
// Apply explicit selection foreground if present
if ($colors.selectionForeground) {
$fg = $colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
$fg = $colors.selectionForeground.rgba >> 8 & Attributes.RGB_MASK;
$hasFg = true;
}
// Overwrite fg as bg if it's a special decorative glyph (eg. powerline)
if (treatGlyphAsBackgroundColor(cell.getCode())) {
// Inverse default background should be treated as transparent
if (
(this.result.fg & FgFlags.INVERSE) &&
(this.result.bg & Attributes.CM_MASK) === Attributes.CM_DEFAULT
) {
$fg = (this._coreBrowserService.isFocused ? $colors.selectionBackgroundOpaque : $colors.selectionInactiveBackgroundOpaque).rgba >> 8 & Attributes.RGB_MASK;
} else {
if (this.result.fg & FgFlags.INVERSE) {
switch (this.result.bg & Attributes.CM_MASK) {
case Attributes.CM_P16:
case Attributes.CM_P256:
$fg = this._themeService.colors.ansi[this.result.bg & Attributes.PCOLOR_MASK].rgba;
break;
case Attributes.CM_RGB:
$fg = ((this.result.bg & Attributes.RGB_MASK) << 8) | 0xFF;
break;
// No need to consider default bg color here as it's not possible
}
} else {
switch (this.result.fg & Attributes.CM_MASK) {
case Attributes.CM_P16:
case Attributes.CM_P256:
$fg = this._themeService.colors.ansi[this.result.fg & Attributes.PCOLOR_MASK].rgba;
break;
case Attributes.CM_RGB:
$fg = ((this.result.fg & Attributes.RGB_MASK) << 8) | 0xFF;
break;
case Attributes.CM_DEFAULT:
default:
$fg = this._themeService.colors.foreground.rgba;
}
}
$fg = rgba.blend(
$fg,
((this._coreBrowserService.isFocused ? $colors.selectionBackgroundOpaque : $colors.selectionInactiveBackgroundOpaque).rgba & 0xFFFFFF00) | 0x80
) >> 8 & Attributes.RGB_MASK;
}
$hasFg = true;
}
}
@@ -88,11 +174,11 @@ export class CellColorResolver {
// Apply decorations on the top layer
this._decorationService.forEachDecorationAtCell(x, y, 'top', d => {
if (d.backgroundColorRGB) {
$bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
$bg = d.backgroundColorRGB.rgba >> 8 & Attributes.RGB_MASK;
$hasBg = true;
}
if (d.foregroundColorRGB) {
$fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
$fg = d.foregroundColorRGB.rgba >> 8 & Attributes.RGB_MASK;
$hasFg = true;
}
});
@@ -119,7 +205,7 @@ export class CellColorResolver {
if ($hasBg && !$hasFg) {
// Resolve bg color type (default color has a different meaning in fg vs bg)
if ((this.result.bg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
$fg = (this.result.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | (($colors.background.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
$fg = (this.result.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | (($colors.background.rgba >> 8 & Attributes.RGB_MASK) & Attributes.RGB_MASK) | Attributes.CM_RGB;
} else {
$fg = (this.result.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | this.result.bg & (Attributes.RGB_MASK | Attributes.CM_MASK);
}
@@ -128,7 +214,7 @@ export class CellColorResolver {
if (!$hasBg && $hasFg) {
// Resolve bg color type (default color has a different meaning in fg vs bg)
if ((this.result.fg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
$bg = (this.result.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | (($colors.foreground.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
$bg = (this.result.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | (($colors.foreground.rgba >> 8 & Attributes.RGB_MASK) & Attributes.RGB_MASK) | Attributes.CM_RGB;
} else {
$bg = (this.result.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | this.result.fg & (Attributes.RGB_MASK | Attributes.CM_MASK);
}
+34 -1
View File
@@ -23,11 +23,44 @@ export function isRestrictedPowerlineGlyph(codepoint: number): boolean {
return 0xE0B0 <= codepoint && codepoint <= 0xE0B7;
}
function isNerdFontGlyph(codepoint: number): boolean {
return 0xE000 <= codepoint && codepoint <= 0xF8FF;
}
function isBoxOrBlockGlyph(codepoint: number): boolean {
return 0x2500 <= codepoint && codepoint <= 0x259F;
}
export function excludeFromContrastRatioDemands(codepoint: number): boolean {
export function isEmoji(codepoint: number): boolean {
return (
codepoint >= 0x1F600 && codepoint <= 0x1F64F || // Emoticons
codepoint >= 0x1F300 && codepoint <= 0x1F5FF || // Misc Symbols and Pictographs
codepoint >= 0x1F680 && codepoint <= 0x1F6FF || // Transport and Map
codepoint >= 0x2600 && codepoint <= 0x26FF || // Misc symbols
codepoint >= 0x2700 && codepoint <= 0x27BF || // Dingbats
codepoint >= 0xFE00 && codepoint <= 0xFE0F || // Variation Selectors
codepoint >= 0x1F900 && codepoint <= 0x1F9FF || // Supplemental Symbols and Pictographs
codepoint >= 0x1F1E6 && codepoint <= 0x1F1FF
);
}
export function allowRescaling(codepoint: number | undefined, width: number, glyphSizeX: number, deviceCellWidth: number): boolean {
return (
// Is single cell width
width === 1 &&
// Glyph exceeds cell bounds, add 50% to avoid hurting readability by rescaling glyphs that
// barely overlap
glyphSizeX > Math.ceil(deviceCellWidth * 1.5) &&
// Never rescale ascii
codepoint !== undefined && codepoint > 0xFF &&
// Never rescale emoji
!isEmoji(codepoint) &&
// Never rescale powerline or nerd fonts
!isPowerlineGlyph(codepoint) && !isNerdFontGlyph(codepoint)
);
}
export function treatGlyphAsBackgroundColor(codepoint: number): boolean {
return isPowerlineGlyph(codepoint) || isBoxOrBlockGlyph(codepoint);
}
+14 -10
View File
@@ -6,16 +6,15 @@
import { IColorContrastCache } from 'browser/Types';
import { DIM_OPACITY, TEXT_BASELINE } from 'browser/renderer/shared/Constants';
import { tryDrawCustomChar } from 'browser/renderer/shared/CustomGlyphs';
import { computeNextVariantOffset, excludeFromContrastRatioDemands, isPowerlineGlyph, isRestrictedPowerlineGlyph, throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
import { computeNextVariantOffset, treatGlyphAsBackgroundColor, isPowerlineGlyph, isRestrictedPowerlineGlyph, throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
import { IBoundingBox, ICharAtlasConfig, IRasterizedGlyph, ITextureAtlas } from 'browser/renderer/shared/Types';
import { NULL_COLOR, color, rgba } from 'common/Color';
import { NULL_COLOR, channels, color, rgba } from 'common/Color';
import { EventEmitter } from 'common/EventEmitter';
import { FourKeyMap } from 'common/MultiKeyMap';
import { IdleTaskQueue } from 'common/TaskQueue';
import { IColor } from 'common/Types';
import { AttributeData } from 'common/buffer/AttributeData';
import { Attributes, DEFAULT_COLOR, DEFAULT_EXT, UnderlineStyle } from 'common/buffer/Constants';
import { traceCall } from 'common/services/LogService';
import { IUnicodeService } from 'common/services/Services';
/**
@@ -292,8 +291,7 @@ export class TextureAtlas implements ITextureAtlas {
break;
case Attributes.CM_RGB:
const arr = AttributeData.toColorRGB(bgColor);
// TODO: This object creation is slow
result = rgba.toColor(arr[0], arr[1], arr[2]);
result = channels.toColor(arr[0], arr[1], arr[2]);
break;
case Attributes.CM_DEFAULT:
default:
@@ -325,7 +323,7 @@ export class TextureAtlas implements ITextureAtlas {
break;
case Attributes.CM_RGB:
const arr = AttributeData.toColorRGB(fgColor);
result = rgba.toColor(arr[0], arr[1], arr[2]);
result = channels.toColor(arr[0], arr[1], arr[2]);
break;
case Attributes.CM_DEFAULT:
default:
@@ -407,7 +405,7 @@ export class TextureAtlas implements ITextureAtlas {
return undefined;
}
const color = rgba.toColor(
const color = channels.toColor(
(result >> 24) & 0xFF,
(result >> 16) & 0xFF,
(result >> 8) & 0xFF
@@ -424,7 +422,6 @@ export class TextureAtlas implements ITextureAtlas {
return this._config.colors.contrastCache;
}
@traceCall
private _drawToCache(codeOrChars: number | string, bg: number, fg: number, ext: number, restrictToCellHeight: boolean = false): IRasterizedGlyph {
const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars;
@@ -492,7 +489,7 @@ export class TextureAtlas implements ITextureAtlas {
const powerlineGlyph = chars.length === 1 && isPowerlineGlyph(chars.charCodeAt(0));
const restrictedPowerlineGlyph = chars.length === 1 && isRestrictedPowerlineGlyph(chars.charCodeAt(0));
const foregroundColor = this._getForegroundColor(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, dim, bold, excludeFromContrastRatioDemands(chars.charCodeAt(0)));
const foregroundColor = this._getForegroundColor(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, dim, bold, treatGlyphAsBackgroundColor(chars.charCodeAt(0)));
this._tmpCtx.fillStyle = foregroundColor.css;
// For powerline glyphs left/top padding is excluded (https://github.com/microsoft/vscode/issues/120129)
@@ -613,7 +610,14 @@ export class TextureAtlas implements ITextureAtlas {
nextOffset = computeNextVariantOffset(xChRight - xChLeft, lineWidth, nextOffset);
break;
case UnderlineStyle.DASHED:
this._tmpCtx.setLineDash([this._config.devicePixelRatio * 4, this._config.devicePixelRatio * 3]);
const lineRatio = 0.6;
const gapRatio = 0.3;
// End line ratio is approximately equal to 0.1
const xChWidth = xChRight - xChLeft;
const line = Math.floor(lineRatio * xChWidth);
const gap = Math.floor(gapRatio * xChWidth);
const end = xChWidth - line - gap;
this._tmpCtx.setLineDash([line, gap, end]);
this._tmpCtx.moveTo(xChLeft, yTop);
this._tmpCtx.lineTo(xChRight, yTop);
break;
+55 -30
View File
@@ -8,12 +8,6 @@ import { EventEmitter } from 'common/EventEmitter';
import { ICharSizeService } from 'browser/services/Services';
import { Disposable } from 'common/Lifecycle';
const enum MeasureSettings {
REPEAT = 32
}
export class CharSizeService extends Disposable implements ICharSizeService {
public serviceBrand: undefined;
@@ -32,7 +26,11 @@ export class CharSizeService extends Disposable implements ICharSizeService {
@IOptionsService private readonly _optionsService: IOptionsService
) {
super();
this._measureStrategy = new DomMeasureStrategy(document, parentElement, this._optionsService);
try {
this._measureStrategy = this.register(new TextMetricsMeasureStrategy(this._optionsService));
} catch {
this._measureStrategy = this.register(new DomMeasureStrategy(document, parentElement, this._optionsService));
}
this.register(this._optionsService.onMultipleOptionChange(['fontFamily', 'fontSize'], () => this.measure()));
}
@@ -47,12 +45,7 @@ export class CharSizeService extends Disposable implements ICharSizeService {
}
interface IMeasureStrategy {
measure(): IReadonlyMeasureResult;
}
interface IReadonlyMeasureResult {
readonly width: number;
readonly height: number;
measure(): Readonly<IMeasureResult>;
}
interface IMeasureResult {
@@ -60,10 +53,26 @@ interface IMeasureResult {
height: number;
}
// TODO: For supporting browsers we should also provide a CanvasCharDimensionsProvider that uses
// ctx.measureText
class DomMeasureStrategy implements IMeasureStrategy {
private _result: IMeasureResult = { width: 0, height: 0 };
const enum DomMeasureStrategyConstants {
REPEAT = 32
}
abstract class BaseMeasureStategy extends Disposable implements IMeasureStrategy {
protected _result: IMeasureResult = { width: 0, height: 0 };
protected _validateAndSet(width: number | undefined, height: number | undefined): void {
// If values are 0 then the element is likely currently display:none, in which case we should
// retain the previous value.
if (width !== undefined && width > 0 && height !== undefined && height > 0) {
this._result.width = width;
this._result.height = height;
}
}
public abstract measure(): Readonly<IMeasureResult>;
}
class DomMeasureStrategy extends BaseMeasureStategy {
private _measureElement: HTMLElement;
constructor(
@@ -71,32 +80,48 @@ class DomMeasureStrategy implements IMeasureStrategy {
private _parentElement: HTMLElement,
private _optionsService: IOptionsService
) {
super();
this._measureElement = this._document.createElement('span');
this._measureElement.classList.add('xterm-char-measure-element');
this._measureElement.textContent = 'W'.repeat(MeasureSettings.REPEAT);
this._measureElement.textContent = 'W'.repeat(DomMeasureStrategyConstants.REPEAT);
this._measureElement.setAttribute('aria-hidden', 'true');
this._measureElement.style.whiteSpace = 'pre';
this._measureElement.style.fontKerning = 'none';
this._parentElement.appendChild(this._measureElement);
}
public measure(): IReadonlyMeasureResult {
public measure(): Readonly<IMeasureResult> {
this._measureElement.style.fontFamily = this._optionsService.rawOptions.fontFamily;
this._measureElement.style.fontSize = `${this._optionsService.rawOptions.fontSize}px`;
// Note that this triggers a synchronous layout
const geometry = {
height: Number(this._measureElement.offsetHeight),
width: Number(this._measureElement.offsetWidth)
};
// If values are 0 then the element is likely currently display:none, in which case we should
// retain the previous value.
if (geometry.width !== 0 && geometry.height !== 0) {
this._result.width = geometry.width / MeasureSettings.REPEAT;
this._result.height = Math.ceil(geometry.height);
}
this._validateAndSet(Number(this._measureElement.offsetWidth) / DomMeasureStrategyConstants.REPEAT, Number(this._measureElement.offsetHeight));
return this._result;
}
}
class TextMetricsMeasureStrategy extends BaseMeasureStategy {
private _canvas: OffscreenCanvas;
private _ctx: OffscreenCanvasRenderingContext2D;
constructor(
private _optionsService: IOptionsService
) {
super();
// This will throw if any required API is not supported
this._canvas = new OffscreenCanvas(100, 100);
this._ctx = this._canvas.getContext('2d')!;
const a = this._ctx.measureText('W');
if (!('width' in a && 'fontBoundingBoxAscent' in a && 'fontBoundingBoxDescent' in a)) {
throw new Error('Required font metrics not supported');
}
}
public measure(): Readonly<IMeasureResult> {
this._ctx.font = `${this._optionsService.rawOptions.fontSize}px ${this._optionsService.rawOptions.fontFamily}`;
const metrics = this._ctx.measureText('W');
this._validateAndSet(metrics.width, metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent);
return this._result;
}
}
@@ -0,0 +1,28 @@
import { ILinkProvider, ILinkProviderService } from 'browser/services/Services';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { IDisposable } from 'common/Types';
export class LinkProviderService extends Disposable implements ILinkProviderService {
declare public serviceBrand: undefined;
public readonly linkProviders: ILinkProvider[] = [];
constructor() {
super();
this.register(toDisposable(() => this.linkProviders.length = 0));
}
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
this.linkProviders.push(linkProvider);
return {
dispose: () => {
// Remove the link provider from the list
const providerIndex = this.linkProviders.indexOf(linkProvider);
if (providerIndex !== -1) {
this.linkProviders.splice(providerIndex, 1);
}
}
};
}
}
+16 -10
View File
@@ -8,9 +8,9 @@ import { IRenderDebouncerWithCallback } from 'browser/Types';
import { IRenderDimensions, IRenderer } from 'browser/renderer/shared/Types';
import { ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
import { EventEmitter } from 'common/EventEmitter';
import { Disposable, MutableDisposable } from 'common/Lifecycle';
import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
import { DebouncedIdleTask } from 'common/TaskQueue';
import { IBufferService, IDecorationService, IInstantiationService, IOptionsService } from 'common/services/Services';
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
interface ISelectionState {
start: [number, number] | undefined;
@@ -24,6 +24,7 @@ export class RenderService extends Disposable implements IRenderService {
private _renderer: MutableDisposable<IRenderer> = this.register(new MutableDisposable());
private _renderDebouncer: IRenderDebouncerWithCallback;
private _pausedResizeTask = new DebouncedIdleTask();
private _observerDisposable = this.register(new MutableDisposable());
private _isPaused: boolean = false;
private _needsFullRefresh: boolean = false;
@@ -38,7 +39,7 @@ export class RenderService extends Disposable implements IRenderService {
};
private readonly _onDimensionsChange = this.register(new EventEmitter<IRenderDimensions>());
public readonly onDimensionsChange = this._onDimensionsChange.event;
public readonly onDimensionsChange = this._onDimensionsChange.event;
private readonly _onRenderedViewportChange = this.register(new EventEmitter<{ start: number, end: number }>());
public readonly onRenderedViewportChange = this._onRenderedViewportChange.event;
private readonly _onRender = this.register(new EventEmitter<{ start: number, end: number }>());
@@ -56,12 +57,11 @@ export class RenderService extends Disposable implements IRenderService {
@IDecorationService decorationService: IDecorationService,
@IBufferService bufferService: IBufferService,
@ICoreBrowserService coreBrowserService: ICoreBrowserService,
@IInstantiationService instantiationService: IInstantiationService,
@IThemeService themeService: IThemeService
) {
super();
this._renderDebouncer = new RenderDebouncer(coreBrowserService.window, (start, end) => this._renderRows(start, end));
this._renderDebouncer = new RenderDebouncer((start, end) => this._renderRows(start, end), coreBrowserService);
this.register(this._renderDebouncer);
this.register(coreBrowserService.onDprChange(() => this.handleDevicePixelRatioChange()));
@@ -87,7 +87,8 @@ export class RenderService extends Disposable implements IRenderService {
'fontSize',
'fontWeight',
'fontWeightBold',
'minimumContrastRatio'
'minimumContrastRatio',
'rescaleOverlappingGlyphs'
], () => {
this.clear();
this.handleResize(bufferService.cols, bufferService.rows);
@@ -102,12 +103,17 @@ export class RenderService extends Disposable implements IRenderService {
this.register(themeService.onChangeColors(() => this._fullRefresh()));
this._registerIntersectionObserver(coreBrowserService.window, screenElement);
this.register(coreBrowserService.onWindowChange((w) => this._registerIntersectionObserver(w, screenElement)));
}
private _registerIntersectionObserver(w: Window & typeof globalThis, screenElement: HTMLElement): void {
// Detect whether IntersectionObserver is detected and enable renderer pause
// and resume based on terminal visibility if so
if ('IntersectionObserver' in coreBrowserService.window) {
const observer = new coreBrowserService.window.IntersectionObserver(e => this._handleIntersectionChange(e[e.length - 1]), { threshold: 0 });
if ('IntersectionObserver' in w) {
const observer = new w.IntersectionObserver(e => this._handleIntersectionChange(e[e.length - 1]), { threshold: 0 });
observer.observe(screenElement);
this.register({ dispose: () => observer.disconnect() });
this._observerDisposable.value = toDisposable(() => observer.disconnect());
}
}
@@ -242,7 +248,7 @@ export class RenderService extends Disposable implements IRenderService {
return;
}
if (this._isPaused) {
this._pausedResizeTask.set(() => this._renderer.value!.handleResize(cols, rows));
this._pausedResizeTask.set(() => this._renderer.value?.handleResize(cols, rows));
} else {
this._renderer.value.handleResize(cols, rows);
}
+12 -1
View File
@@ -5,7 +5,7 @@
import { IEvent } from 'common/EventEmitter';
import { IRenderDimensions, IRenderer } from 'browser/renderer/shared/Types';
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
import { IColorSet, ILink, ReadonlyColorSet } from 'browser/Types';
import { ISelectionRedrawRequestEvent as ISelectionRequestRedrawEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
import { createDecorator } from 'common/services/ServiceRegistry';
import { AllColorIndex, IDisposable } from 'common/Types';
@@ -145,3 +145,14 @@ export interface IThemeService {
*/
modifyColors(callback: (colors: IColorSet) => void): void;
}
export const ILinkProviderService = createDecorator<ILinkProviderService>('LinkProviderService');
export interface ILinkProviderService extends IDisposable {
serviceBrand: undefined;
readonly linkProviders: ReadonlyArray<ILinkProvider>;
registerLinkProvider(linkProvider: ILinkProvider): IDisposable;
}
export interface ILinkProvider {
provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void;
}
+81
View File
@@ -29,6 +29,25 @@ describe('Color', () => {
assert.equal(channels.toCss(0xf0, 0xf0, 0xf0), '#f0f0f0');
assert.equal(channels.toCss(0xff, 0xff, 0xff), '#ffffff');
});
it('should convert an rgba array to css hex string', () => {
assert.equal(channels.toCss(0x00, 0x00, 0x00, 0x00), '#00000000');
assert.equal(channels.toCss(0x10, 0x10, 0x10, 0x10), '#10101010');
assert.equal(channels.toCss(0x20, 0x20, 0x20, 0x20), '#20202020');
assert.equal(channels.toCss(0x30, 0x30, 0x30, 0x30), '#30303030');
assert.equal(channels.toCss(0x40, 0x40, 0x40, 0x40), '#40404040');
assert.equal(channels.toCss(0x50, 0x50, 0x50, 0x50), '#50505050');
assert.equal(channels.toCss(0x60, 0x60, 0x60, 0x60), '#60606060');
assert.equal(channels.toCss(0x70, 0x70, 0x70, 0x70), '#70707070');
assert.equal(channels.toCss(0x80, 0x80, 0x80, 0x80), '#80808080');
assert.equal(channels.toCss(0x90, 0x90, 0x90, 0x90), '#90909090');
assert.equal(channels.toCss(0xa0, 0xa0, 0xa0, 0xa0), '#a0a0a0a0');
assert.equal(channels.toCss(0xb0, 0xb0, 0xb0, 0xb0), '#b0b0b0b0');
assert.equal(channels.toCss(0xc0, 0xc0, 0xc0, 0xc0), '#c0c0c0c0');
assert.equal(channels.toCss(0xd0, 0xd0, 0xd0, 0xd0), '#d0d0d0d0');
assert.equal(channels.toCss(0xe0, 0xe0, 0xe0, 0xe0), '#e0e0e0e0');
assert.equal(channels.toCss(0xf0, 0xf0, 0xf0, 0xf0), '#f0f0f0f0');
assert.equal(channels.toCss(0xff, 0xff, 0xff, 0xff), '#ffffffff');
});
});
describe('toRgba', () => {
@@ -71,6 +90,47 @@ describe('Color', () => {
assert.equal(channels.toRgba(0xff, 0xff, 0xff, 0xff), 0xffffffff);
});
});
describe('toColor', () => {
it('should convert an rgb array to an IColor', () => {
assert.deepStrictEqual(channels.toColor(0x00, 0x00, 0x00), { css: '#000000', rgba: 0x000000FF });
assert.deepStrictEqual(channels.toColor(0x10, 0x10, 0x10), { css: '#101010', rgba: 0x101010FF });
assert.deepStrictEqual(channels.toColor(0x20, 0x20, 0x20), { css: '#202020', rgba: 0x202020FF });
assert.deepStrictEqual(channels.toColor(0x30, 0x30, 0x30), { css: '#303030', rgba: 0x303030FF });
assert.deepStrictEqual(channels.toColor(0x40, 0x40, 0x40), { css: '#404040', rgba: 0x404040FF });
assert.deepStrictEqual(channels.toColor(0x50, 0x50, 0x50), { css: '#505050', rgba: 0x505050FF });
assert.deepStrictEqual(channels.toColor(0x60, 0x60, 0x60), { css: '#606060', rgba: 0x606060FF });
assert.deepStrictEqual(channels.toColor(0x70, 0x70, 0x70), { css: '#707070', rgba: 0x707070FF });
assert.deepStrictEqual(channels.toColor(0x80, 0x80, 0x80), { css: '#808080', rgba: 0x808080FF });
assert.deepStrictEqual(channels.toColor(0x90, 0x90, 0x90), { css: '#909090', rgba: 0x909090FF });
assert.deepStrictEqual(channels.toColor(0xa0, 0xa0, 0xa0), { css: '#a0a0a0', rgba: 0xa0a0a0FF });
assert.deepStrictEqual(channels.toColor(0xb0, 0xb0, 0xb0), { css: '#b0b0b0', rgba: 0xb0b0b0FF });
assert.deepStrictEqual(channels.toColor(0xc0, 0xc0, 0xc0), { css: '#c0c0c0', rgba: 0xc0c0c0FF });
assert.deepStrictEqual(channels.toColor(0xd0, 0xd0, 0xd0), { css: '#d0d0d0', rgba: 0xd0d0d0FF });
assert.deepStrictEqual(channels.toColor(0xe0, 0xe0, 0xe0), { css: '#e0e0e0', rgba: 0xe0e0e0FF });
assert.deepStrictEqual(channels.toColor(0xf0, 0xf0, 0xf0), { css: '#f0f0f0', rgba: 0xf0f0f0FF });
assert.deepStrictEqual(channels.toColor(0xff, 0xff, 0xff), { css: '#ffffff', rgba: 0xffffffFF });
});
it('should convert an rgba array to an IColor', () => {
assert.deepStrictEqual(channels.toColor(0x00, 0x00, 0x00, 0x00), { css: '#00000000', rgba: 0x00000000 });
assert.deepStrictEqual(channels.toColor(0x10, 0x10, 0x10, 0x10), { css: '#10101010', rgba: 0x10101010 });
assert.deepStrictEqual(channels.toColor(0x20, 0x20, 0x20, 0x20), { css: '#20202020', rgba: 0x20202020 });
assert.deepStrictEqual(channels.toColor(0x30, 0x30, 0x30, 0x30), { css: '#30303030', rgba: 0x30303030 });
assert.deepStrictEqual(channels.toColor(0x40, 0x40, 0x40, 0x40), { css: '#40404040', rgba: 0x40404040 });
assert.deepStrictEqual(channels.toColor(0x50, 0x50, 0x50, 0x50), { css: '#50505050', rgba: 0x50505050 });
assert.deepStrictEqual(channels.toColor(0x60, 0x60, 0x60, 0x60), { css: '#60606060', rgba: 0x60606060 });
assert.deepStrictEqual(channels.toColor(0x70, 0x70, 0x70, 0x70), { css: '#70707070', rgba: 0x70707070 });
assert.deepStrictEqual(channels.toColor(0x80, 0x80, 0x80, 0x80), { css: '#80808080', rgba: 0x80808080 });
assert.deepStrictEqual(channels.toColor(0x90, 0x90, 0x90, 0x90), { css: '#90909090', rgba: 0x90909090 });
assert.deepStrictEqual(channels.toColor(0xa0, 0xa0, 0xa0, 0xa0), { css: '#a0a0a0a0', rgba: 0xa0a0a0a0 });
assert.deepStrictEqual(channels.toColor(0xb0, 0xb0, 0xb0, 0xb0), { css: '#b0b0b0b0', rgba: 0xb0b0b0b0 });
assert.deepStrictEqual(channels.toColor(0xc0, 0xc0, 0xc0, 0xc0), { css: '#c0c0c0c0', rgba: 0xc0c0c0c0 });
assert.deepStrictEqual(channels.toColor(0xd0, 0xd0, 0xd0, 0xd0), { css: '#d0d0d0d0', rgba: 0xd0d0d0d0 });
assert.deepStrictEqual(channels.toColor(0xe0, 0xe0, 0xe0, 0xe0), { css: '#e0e0e0e0', rgba: 0xe0e0e0e0 });
assert.deepStrictEqual(channels.toColor(0xf0, 0xf0, 0xf0, 0xf0), { css: '#f0f0f0f0', rgba: 0xf0f0f0f0 });
assert.deepStrictEqual(channels.toColor(0xff, 0xff, 0xff, 0xff), { css: '#ffffffff', rgba: 0xffffffff });
});
});
});
describe('color', () => {
@@ -271,6 +331,27 @@ describe('Color', () => {
});
describe('rgba', () => {
describe('blend', () => {
it('should blend colors based on the alpha channel', () => {
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF00), 0x000000FF);
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF10), 0x101010FF);
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF20), 0x202020FF);
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF30), 0x303030FF);
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF40), 0x404040FF);
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF50), 0x505050FF);
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF60), 0x606060FF);
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF70), 0x707070FF);
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF80), 0x808080FF);
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF90), 0x909090FF);
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFFA0), 0xA0A0A0FF);
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFFB0), 0xB0B0B0FF);
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFFC0), 0xC0C0C0FF);
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFFD0), 0xD0D0D0FF);
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFFE0), 0xE0E0E0FF);
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFFF0), 0xF0F0F0FF);
assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFFFF), 0xFFFFFFFF);
});
});
describe('ensureContrastRatio', () => {
it('should return undefined if the color already meets the contrast ratio (black bg)', () => {
assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 1), undefined);
+33 -14
View File
@@ -3,7 +3,6 @@
* @license MIT
*/
import { isNode } from 'common/Platform';
import { IColor, IColorRGB } from 'common/Types';
let $r = 0;
@@ -33,6 +32,13 @@ export namespace channels {
// >>> 0 forces an unsigned int
return (r << 24 | g << 16 | b << 8 | a) >>> 0;
}
export function toColor(r: number, g: number, b: number, a?: number): IColor {
return {
css: channels.toCss(r, g, b, a),
rgba: channels.toRgba(r, g, b, a)
};
}
}
/**
@@ -70,7 +76,7 @@ export namespace color {
if (!result) {
return undefined;
}
return rgba.toColor(
return channels.toColor(
(result >> 24 & 0xFF),
(result >> 16 & 0xFF),
(result >> 8 & 0xFF)
@@ -110,9 +116,10 @@ export namespace color {
* '#rrggbbaa').
*/
export namespace css {
// Attempt to set get the shared canvas context
let $ctx: CanvasRenderingContext2D | undefined;
let $litmusColor: CanvasGradient | undefined;
if (!isNode) {
try {
// This is guaranteed to run in the first window, so document should be correct
const canvas = document.createElement('canvas');
canvas.width = 1;
@@ -126,6 +133,9 @@ export namespace css {
$litmusColor = $ctx.createLinearGradient(0, 0, 1, 1);
}
}
catch {
// noop
}
/**
* Converts a css string to an IColor, this should handle all valid CSS color strings and will
@@ -142,14 +152,14 @@ export namespace css {
$r = parseInt(css.slice(1, 2).repeat(2), 16);
$g = parseInt(css.slice(2, 3).repeat(2), 16);
$b = parseInt(css.slice(3, 4).repeat(2), 16);
return rgba.toColor($r, $g, $b);
return channels.toColor($r, $g, $b);
}
case 5: { // #rgba
$r = parseInt(css.slice(1, 2).repeat(2), 16);
$g = parseInt(css.slice(2, 3).repeat(2), 16);
$b = parseInt(css.slice(3, 4).repeat(2), 16);
$a = parseInt(css.slice(4, 5).repeat(2), 16);
return rgba.toColor($r, $g, $b, $a);
return channels.toColor($r, $g, $b, $a);
}
case 7: // #rrggbb
return {
@@ -171,7 +181,7 @@ export namespace css {
$g = parseInt(rgbaMatch[2]);
$b = parseInt(rgbaMatch[3]);
$a = Math.round((rgbaMatch[5] === undefined ? 1 : parseFloat(rgbaMatch[5])) * 0xFF);
return rgba.toColor($r, $g, $b, $a);
return channels.toColor($r, $g, $b, $a);
}
// Validate the context is available for canvas-based color parsing
@@ -245,6 +255,23 @@ export namespace rgb {
* Helper functions where the source type is "rgba" (number: 0xrrggbbaa).
*/
export namespace rgba {
export function blend(bg: number, fg: number): number {
$a = (fg & 0xFF) / 0xFF;
if ($a === 1) {
return fg;
}
const fgR = (fg >> 24) & 0xFF;
const fgG = (fg >> 16) & 0xFF;
const fgB = (fg >> 8) & 0xFF;
const bgR = (bg >> 24) & 0xFF;
const bgG = (bg >> 16) & 0xFF;
const bgB = (bg >> 8) & 0xFF;
$r = bgR + Math.round((fgR - bgR) * $a);
$g = bgG + Math.round((fgG - bgG) * $a);
$b = bgB + Math.round((fgB - bgB) * $a);
return channels.toRgba($r, $g, $b);
}
/**
* 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
@@ -325,17 +352,9 @@ export namespace rgba {
return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0;
}
// FIXME: Move this to channels NS?
export function toChannels(value: number): [number, number, number, number] {
return [(value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF];
}
export function toColor(r: number, g: number, b: number, a?: number): IColor {
return {
css: channels.toCss(r, g, b, a),
rgba: channels.toRgba(r, g, b, a)
};
}
}
export function toPaddedHex(c: number): string {
+5
View File
@@ -120,6 +120,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
this._oscLinkService = this._instantiationService.createInstance(OscLinkService);
this._instantiationService.setService(IOscLinkService, this._oscLinkService);
// Register input handler and handle/forward events
this._inputHandler = this.register(new InputHandler(this._bufferService, this._charsetService, this.coreService, this._logService, this.optionsService, this._oscLinkService, this.coreMouseService, this.unicodeService));
this.register(forwardEvent(this._inputHandler.onLineFeed, this._onLineFeed));
@@ -167,6 +168,10 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
this._writeBuffer.writeSync(data, maxSubsequentCalls);
}
public input(data: string, wasUserInput: boolean = true): void {
this.coreService.triggerDataEvent(data, wasUserInput);
}
public resize(x: number, y: number): void {
if (isNaN(x) || isNaN(y)) {
return;
+1 -1
View File
@@ -14,7 +14,7 @@ interface INavigator {
declare const navigator: INavigator;
declare const process: unknown;
export const isNode = (typeof process !== 'undefined') ? true : false;
export const isNode = (typeof process !== 'undefined' && 'title' in (process as any)) ? true : false;
const userAgent = (isNode) ? 'node' : navigator.userAgent;
const platform = (isNode) ? 'node' : navigator.platform;
+2 -2
View File
@@ -110,8 +110,8 @@ export interface ICharset {
export type CharData = [number, string, number, number];
export interface IColor {
css: string;
rgba: number; // 32-bit int with rgba in each byte
readonly css: string;
readonly rgba: number; // 32-bit int with rgba in each byte
}
export type IColorRGB = [number, number, number];
+9 -1
View File
@@ -4,7 +4,7 @@
*/
import { EventEmitter } from 'common/EventEmitter';
import { Disposable } from 'common/Lifecycle';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { isMac } from 'common/Platform';
import { CursorStyle, IDisposable } from 'common/Types';
import { FontWeight, IOptionsService, ITerminalOptions } from 'common/services/Services';
@@ -44,6 +44,7 @@ export const DEFAULT_OPTIONS: Readonly<Required<ITerminalOptions>> = {
allowTransparency: false,
tabStopWidth: 8,
theme: {},
rescaleOverlappingGlyphs: false,
rightClickSelectsWord: isMac,
windowOptions: {},
windowsMode: false,
@@ -87,6 +88,13 @@ export class OptionsService extends Disposable implements IOptionsService {
this.rawOptions = defaultOptions;
this.options = { ... defaultOptions };
this._setupOptions();
// Clear out options that could link outside xterm.js as they could easily cause an embedder
// memory leak
this.register(toDisposable(() => {
this.rawOptions.linkHandler = null;
this.rawOptions.documentOverride = null;
}));
}
// eslint-disable-next-line @typescript-eslint/naming-convention
+1
View File
@@ -235,6 +235,7 @@ export interface ITerminalOptions {
macOptionIsMeta?: boolean;
macOptionClickForcesSelection?: boolean;
minimumContrastRatio?: number;
rescaleOverlappingGlyphs?: boolean;
rightClickSelectsWord?: boolean;
rows?: number;
screenReaderMode?: boolean;
+4
View File
@@ -81,6 +81,10 @@ export class Terminal extends CoreTerminal {
this._onBell.fire();
}
public input(data: string, wasUserInput: boolean = true): void {
this.coreService.triggerDataEvent(data, wasUserInput);
}
/**
* Resizes the terminal.
*
+3
View File
@@ -134,6 +134,9 @@ export class Terminal extends Disposable implements ITerminalApi {
this._publicOptions[propName] = options[propName];
}
}
public input(data: string, wasUserInput: boolean = true): void {
this._core.input(data, wasUserInput);
}
public resize(columns: number, rows: number): void {
this._verifyIntegers(columns, rows);
this._core.resize(columns, rows);