diff --git a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts index 2990fe03..54fea5f2 100644 --- a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts @@ -10,8 +10,8 @@ import { tryDrawCustomChar } from 'browser/renderer/shared/CustomGlyphs'; import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; import { IRasterizedGlyph, IRenderDimensions, ISelectionRenderModel, ITextureAtlas } from 'browser/renderer/shared/Types'; import { createSelectionRenderModel } from 'browser/renderer/shared/SelectionRenderModel'; -import { ICoreBrowserService } from 'browser/services/Services'; -import { IColorSet } from 'browser/Types'; +import { ICoreBrowserService, IThemeService } from 'browser/services/Services'; +import { IColorSet, ReadonlyColorSet } from 'browser/Types'; import { CellData } from 'common/buffer/CellData'; import { WHITESPACE_CELL_CODE } from 'common/buffer/Constants'; import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; @@ -46,20 +46,24 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer id: string, zIndex: number, private _alpha: boolean, - protected _colors: IColorSet, + protected readonly _themeService: IThemeService, protected readonly _bufferService: IBufferService, protected readonly _optionsService: IOptionsService, protected readonly _decorationService: IDecorationService, protected readonly _coreBrowserService: ICoreBrowserService ) { super(); - this._cellColorResolver = new CellColorResolver(this._terminal, this._colors, this._selectionModel, this._decorationService, this._coreBrowserService); + this._cellColorResolver = new CellColorResolver(this._terminal, this._selectionModel, this._decorationService, this._coreBrowserService, this._themeService); this._canvas = document.createElement('canvas'); this._canvas.classList.add(`xterm-${id}-layer`); this._canvas.style.zIndex = zIndex.toString(); this._initCanvas(); this._container.appendChild(this._canvas); - this._refreshCharAtlas(this._colors); + this._refreshCharAtlas(this._themeService.colors); + this.register(this._themeService.onChangeColors(e => { + this._refreshCharAtlas(e); + this.reset(); + })); this.register(toDisposable(() => { removeElementFromParent(this._canvas); @@ -85,10 +89,6 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer this._selectionModel.update(this._terminal, start, end, columnSelectMode); } - public setColors(colorSet: IColorSet): void { - this._refreshCharAtlas(colorSet); - } - protected _setTransparency(alpha: boolean): void { // Do nothing when alpha doesn't change if (alpha === this._alpha) { @@ -104,7 +104,7 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer this._container.replaceChild(this._canvas, oldCanvas); // Regenerate char atlas and force a full redraw - this._refreshCharAtlas(this._colors); + this._refreshCharAtlas(this._themeService.colors); this.handleGridChanged(0, this._bufferService.rows - 1); } @@ -112,7 +112,7 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer * Refreshes the char atlas, aquiring a new one if necessary. * @param colorSet The color set to use for the char atlas. */ - private _refreshCharAtlas(colorSet: IColorSet): void { + private _refreshCharAtlas(colorSet: ReadonlyColorSet): void { if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) { return; } @@ -138,7 +138,7 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer this._clearAll(); } - this._refreshCharAtlas(this._colors); + this._refreshCharAtlas(this._themeService.colors); } public abstract reset(): void; @@ -294,7 +294,7 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer if (this._alpha) { this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); } else { - this._ctx.fillStyle = this._colors.background.css; + this._ctx.fillStyle = this._themeService.colors.background.css; this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height); } } @@ -314,7 +314,7 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer width * this._scaledCellWidth, height * this._scaledCellHeight); } else { - this._ctx.fillStyle = this._colors.background.css; + this._ctx.fillStyle = this._themeService.colors.background.css; this._ctx.fillRect( x * this._scaledCellWidth, y * this._scaledCellHeight, diff --git a/addons/xterm-addon-canvas/src/CanvasAddon.ts b/addons/xterm-addon-canvas/src/CanvasAddon.ts index c9c55e0d..1dc607e5 100644 --- a/addons/xterm-addon-canvas/src/CanvasAddon.ts +++ b/addons/xterm-addon-canvas/src/CanvasAddon.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService } from 'browser/services/Services'; +import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services'; import { IColorSet } from 'browser/Types'; import { CanvasRenderer } from './CanvasRenderer'; import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; @@ -38,11 +38,11 @@ export class CanvasAddon extends Disposable implements ITerminalAddon { const coreBrowserService: ICoreBrowserService = core._coreBrowserService; const decorationService: IDecorationService = core._decorationService; const optionsService: IOptionsService = core.optionsService; - const colors: IColorSet = core._colorManager.colors; + const themeService: IThemeService = core._themeService; const screenElement: HTMLElement = core.screenElement; const linkifier = core.linkifier2; - this._renderer = new CanvasRenderer(terminal, colors, screenElement, linkifier, bufferService, charSizeService, optionsService, characterJoinerService, coreService, coreBrowserService, decorationService); + this._renderer = new CanvasRenderer(terminal, screenElement, linkifier, bufferService, charSizeService, optionsService, characterJoinerService, coreService, coreBrowserService, decorationService, themeService); this.register(forwardEvent(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas)); renderService.setRenderer(this._renderer); renderService.handleResize(bufferService.cols, bufferService.rows); diff --git a/addons/xterm-addon-canvas/src/CanvasRenderer.ts b/addons/xterm-addon-canvas/src/CanvasRenderer.ts index 1abda096..ff92e94b 100644 --- a/addons/xterm-addon-canvas/src/CanvasRenderer.ts +++ b/addons/xterm-addon-canvas/src/CanvasRenderer.ts @@ -6,8 +6,8 @@ import { removeTerminalFromCache } from 'browser/renderer/shared/CharAtlasCache'; import { observeDevicePixelDimensions } from 'browser/renderer/shared/DevicePixelObserver'; import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types'; -import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService } from 'browser/services/Services'; -import { IColorSet, ILinkifier2 } from 'browser/Types'; +import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IThemeService } from 'browser/services/Services'; +import { IColorSet, ILinkifier2, ReadonlyColorSet } from 'browser/Types'; import { EventEmitter } from 'common/EventEmitter'; import { Disposable, toDisposable } from 'common/Lifecycle'; import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; @@ -31,7 +31,6 @@ export class CanvasRenderer extends Disposable implements IRenderer { constructor( private readonly _terminal: Terminal, - private _colors: IColorSet, private readonly _screenElement: HTMLElement, linkifier2: ILinkifier2, private readonly _bufferService: IBufferService, @@ -40,15 +39,16 @@ export class CanvasRenderer extends Disposable implements IRenderer { characterJoinerService: ICharacterJoinerService, coreService: ICoreService, private readonly _coreBrowserService: ICoreBrowserService, - decorationService: IDecorationService + decorationService: IDecorationService, + private readonly _themeService: IThemeService ) { super(); const allowTransparency = this._optionsService.rawOptions.allowTransparency; this._renderLayers = [ - new TextRenderLayer(this._terminal, this._screenElement, 0, this._colors, allowTransparency, this._bufferService, this._optionsService, characterJoinerService, decorationService, this._coreBrowserService), - new SelectionRenderLayer(this._terminal, this._screenElement, 1, this._colors, this._bufferService, this._coreBrowserService, decorationService, this._optionsService), - new LinkRenderLayer(this._terminal, this._screenElement, 2, this._colors, linkifier2, this._bufferService, this._optionsService, decorationService, this._coreBrowserService), - new CursorRenderLayer(this._terminal, this._screenElement, 3, this._colors, this._onRequestRedraw, this._bufferService, this._optionsService, coreService, this._coreBrowserService, decorationService) + new TextRenderLayer(this._terminal, this._screenElement, 0, allowTransparency, this._bufferService, this._optionsService, characterJoinerService, decorationService, this._coreBrowserService, _themeService), + new SelectionRenderLayer(this._terminal, this._screenElement, 1, this._bufferService, this._coreBrowserService, decorationService, this._optionsService, _themeService), + new LinkRenderLayer(this._terminal, this._screenElement, 2, linkifier2, this._bufferService, this._optionsService, decorationService, this._coreBrowserService, _themeService), + new CursorRenderLayer(this._terminal, this._screenElement, 3, this._onRequestRedraw, this._bufferService, this._optionsService, coreService, this._coreBrowserService, decorationService, _themeService) ]; this.dimensions = { scaledCharWidth: 0, @@ -92,15 +92,6 @@ export class CanvasRenderer extends Disposable implements IRenderer { } } - public setColors(colors: IColorSet): void { - this._colors = colors; - // Clear layers and force a full render - for (const l of this._renderLayers) { - l.setColors(this._colors); - l.reset(); - } - } - public handleResize(cols: number, rows: number): void { // Update character and canvas dimensions this._updateDimensions(); @@ -130,7 +121,7 @@ export class CanvasRenderer extends Disposable implements IRenderer { public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void { this._runOperation(l => l.handleSelectionChanged(start, end, columnSelectMode)); // Selection foreground requires a full re-render - if (this._colors.selectionForeground) { + if (this._themeService.colors.selectionForeground) { this._onRequestRedraw.fire({ start: 0, end: this._bufferService.rows - 1 }); } } diff --git a/addons/xterm-addon-canvas/src/CursorRenderLayer.ts b/addons/xterm-addon-canvas/src/CursorRenderLayer.ts index d32eb229..ab8b1e66 100644 --- a/addons/xterm-addon-canvas/src/CursorRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/CursorRenderLayer.ts @@ -7,10 +7,10 @@ import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/shared/ import { BaseRenderLayer } from './BaseRenderLayer'; import { ICellData } from 'common/Types'; import { CellData } from 'common/buffer/CellData'; -import { IColorSet } from 'browser/Types'; +import { IColorSet, ReadonlyColorSet } from 'browser/Types'; import { IBufferService, IOptionsService, ICoreService, IDecorationService } from 'common/services/Services'; import { IEventEmitter } from 'common/EventEmitter'; -import { ICoreBrowserService } from 'browser/services/Services'; +import { ICoreBrowserService, IThemeService } from 'browser/services/Services'; import { Terminal } from 'xterm'; import { toDisposable } from 'common/Lifecycle'; @@ -37,15 +37,15 @@ export class CursorRenderLayer extends BaseRenderLayer { terminal: Terminal, container: HTMLElement, zIndex: number, - colors: IColorSet, private readonly _onRequestRedraw: IEventEmitter, bufferService: IBufferService, optionsService: IOptionsService, private readonly _coreService: ICoreService, coreBrowserService: ICoreBrowserService, - decorationService: IDecorationService + decorationService: IDecorationService, + themeService: IThemeService ) { - super(terminal, container, 'cursor', zIndex, true, colors, bufferService, optionsService, decorationService, coreBrowserService); + super(terminal, container, 'cursor', zIndex, true, themeService, bufferService, optionsService, decorationService, coreBrowserService); this._state = { x: 0, y: 0, @@ -146,7 +146,7 @@ export class CursorRenderLayer extends BaseRenderLayer { if (!this._coreBrowserService.isFocused) { this._clearCursor(); this._ctx.save(); - this._ctx.fillStyle = this._colors.cursor.css; + this._ctx.fillStyle = this._themeService.colors.cursor.css; const cursorStyle = this._optionsService.rawOptions.cursorStyle; if (cursorStyle && cursorStyle !== 'block') { this._cursorRenderers[cursorStyle](cursorX, viewportRelativeCursorY, this._cell); @@ -211,30 +211,30 @@ export class CursorRenderLayer extends BaseRenderLayer { private _renderBarCursor(x: number, y: number, cell: ICellData): void { this._ctx.save(); - this._ctx.fillStyle = this._colors.cursor.css; + this._ctx.fillStyle = this._themeService.colors.cursor.css; this._fillLeftLineAtCell(x, y, this._optionsService.rawOptions.cursorWidth); this._ctx.restore(); } private _renderBlockCursor(x: number, y: number, cell: ICellData): void { this._ctx.save(); - this._ctx.fillStyle = this._colors.cursor.css; + this._ctx.fillStyle = this._themeService.colors.cursor.css; this._fillCells(x, y, cell.getWidth(), 1); - this._ctx.fillStyle = this._colors.cursorAccent.css; + this._ctx.fillStyle = this._themeService.colors.cursorAccent.css; this._fillCharTrueColor(cell, x, y); this._ctx.restore(); } private _renderUnderlineCursor(x: number, y: number, cell: ICellData): void { this._ctx.save(); - this._ctx.fillStyle = this._colors.cursor.css; + this._ctx.fillStyle = this._themeService.colors.cursor.css; this._fillBottomLineAtCells(x, y); this._ctx.restore(); } private _renderBlurCursor(x: number, y: number, cell: ICellData): void { this._ctx.save(); - this._ctx.strokeStyle = this._colors.cursor.css; + this._ctx.strokeStyle = this._themeService.colors.cursor.css; this._strokeRectAtCell(x, y, cell.getWidth(), 1); this._ctx.restore(); } diff --git a/addons/xterm-addon-canvas/src/LinkRenderLayer.ts b/addons/xterm-addon-canvas/src/LinkRenderLayer.ts index b1403f7a..2d4c217c 100644 --- a/addons/xterm-addon-canvas/src/LinkRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/LinkRenderLayer.ts @@ -6,8 +6,8 @@ import { IRenderDimensions } from 'browser/renderer/shared/Types'; import { BaseRenderLayer } from './BaseRenderLayer'; import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants'; -import { ICoreBrowserService } from 'browser/services/Services'; -import { IColorSet, ILinkifierEvent, ILinkifier2 } from 'browser/Types'; +import { ICoreBrowserService, IThemeService } from 'browser/services/Services'; +import { IColorSet, ILinkifierEvent, ILinkifier2, ReadonlyColorSet } from 'browser/Types'; import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; import { is256Color } from 'browser/renderer/shared/CharAtlasUtils'; import { Terminal } from 'xterm'; @@ -19,14 +19,14 @@ export class LinkRenderLayer extends BaseRenderLayer { terminal: Terminal, container: HTMLElement, zIndex: number, - colors: IColorSet, linkifier2: ILinkifier2, bufferService: IBufferService, optionsService: IOptionsService, decorationService: IDecorationService, - coreBrowserService: ICoreBrowserService + coreBrowserService: ICoreBrowserService, + themeService: IThemeService ) { - super(terminal, container, 'link', zIndex, true, colors, bufferService, optionsService, decorationService, coreBrowserService); + super(terminal, container, 'link', zIndex, true, themeService, bufferService, optionsService, decorationService, coreBrowserService); this.register(linkifier2.onShowLinkUnderline(e => this._handleShowLinkUnderline(e))); this.register(linkifier2.onHideLinkUnderline(e => this._handleHideLinkUnderline(e))); @@ -56,12 +56,12 @@ export class LinkRenderLayer extends BaseRenderLayer { private _handleShowLinkUnderline(e: ILinkifierEvent): void { if (e.fg === INVERTED_DEFAULT_COLOR) { - this._ctx.fillStyle = this._colors.background.css; + this._ctx.fillStyle = this._themeService.colors.background.css; } else if (e.fg && is256Color(e.fg)) { // 256 color support - this._ctx.fillStyle = this._colors.ansi[e.fg].css; + this._ctx.fillStyle = this._themeService.colors.ansi[e.fg].css; } else { - this._ctx.fillStyle = this._colors.foreground.css; + this._ctx.fillStyle = this._themeService.colors.foreground.css; } if (e.y1 === e.y2) { diff --git a/addons/xterm-addon-canvas/src/SelectionRenderLayer.ts b/addons/xterm-addon-canvas/src/SelectionRenderLayer.ts index 0b8408c0..1c96dcc8 100644 --- a/addons/xterm-addon-canvas/src/SelectionRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/SelectionRenderLayer.ts @@ -5,9 +5,9 @@ import { IRenderDimensions } from 'browser/renderer/shared/Types'; import { BaseRenderLayer } from './BaseRenderLayer'; -import { IColorSet } from 'browser/Types'; +import { IColorSet, ReadonlyColorSet } from 'browser/Types'; import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; -import { ICoreBrowserService } from 'browser/services/Services'; +import { ICoreBrowserService, IThemeService } from 'browser/services/Services'; import { Terminal } from 'xterm'; interface ISelectionState { @@ -24,13 +24,13 @@ export class SelectionRenderLayer extends BaseRenderLayer { terminal: Terminal, container: HTMLElement, zIndex: number, - colors: IColorSet, bufferService: IBufferService, coreBrowserService: ICoreBrowserService, decorationService: IDecorationService, - optionsService: IOptionsService + optionsService: IOptionsService, + themeService: IThemeService ) { - super(terminal, container, 'selection', zIndex, true, colors, bufferService, optionsService, decorationService, coreBrowserService); + super(terminal, container, 'selection', zIndex, true, themeService, bufferService, optionsService, decorationService, coreBrowserService); this._clearState(); } @@ -102,8 +102,8 @@ export class SelectionRenderLayer extends BaseRenderLayer { } this._ctx.fillStyle = (this._coreBrowserService.isFocused - ? this._colors.selectionBackgroundTransparent - : this._colors.selectionInactiveBackgroundTransparent).css; + ? this._themeService.colors.selectionBackgroundTransparent + : this._themeService.colors.selectionInactiveBackgroundTransparent).css; if (columnSelectMode) { const startCol = start[0]; diff --git a/addons/xterm-addon-canvas/src/TextRenderLayer.ts b/addons/xterm-addon-canvas/src/TextRenderLayer.ts index 23ac55ad..ca9eae56 100644 --- a/addons/xterm-addon-canvas/src/TextRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/TextRenderLayer.ts @@ -9,10 +9,10 @@ import { GridCache } from './GridCache'; import { BaseRenderLayer } from './BaseRenderLayer'; import { AttributeData } from 'common/buffer/AttributeData'; import { NULL_CELL_CODE, Content, UnderlineStyle } from 'common/buffer/Constants'; -import { IColorSet } from 'browser/Types'; +import { IColorSet, ReadonlyColorSet } from 'browser/Types'; import { CellData } from 'common/buffer/CellData'; import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services'; -import { ICharacterJoinerService, ICoreBrowserService } from 'browser/services/Services'; +import { ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services'; import { JoinedCellData } from 'browser/services/CharacterJoinerService'; import { color, css } from 'common/Color'; import { Terminal } from 'xterm'; @@ -35,15 +35,15 @@ export class TextRenderLayer extends BaseRenderLayer { terminal: Terminal, container: HTMLElement, zIndex: number, - colors: IColorSet, alpha: boolean, bufferService: IBufferService, optionsService: IOptionsService, private readonly _characterJoinerService: ICharacterJoinerService, decorationService: IDecorationService, - coreBrowserService: ICoreBrowserService + coreBrowserService: ICoreBrowserService, + themeService: IThemeService ) { - super(terminal, container, 'text', zIndex, alpha, colors, bufferService, optionsService, decorationService, coreBrowserService); + super(terminal, container, 'text', zIndex, alpha, themeService, bufferService, optionsService, decorationService, coreBrowserService); this._state = new GridCache(); } @@ -168,16 +168,16 @@ export class TextRenderLayer extends BaseRenderLayer { if (cell.isInverse()) { if (cell.isFgDefault()) { - nextFillStyle = this._colors.foreground.css; + nextFillStyle = this._themeService.colors.foreground.css; } else if (cell.isFgRGB()) { nextFillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`; } else { - nextFillStyle = this._colors.ansi[cell.getFgColor()].css; + nextFillStyle = this._themeService.colors.ansi[cell.getFgColor()].css; } } else if (cell.isBgRGB()) { nextFillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`; } else if (cell.isBgPalette()) { - nextFillStyle = this._colors.ansi[cell.getBgColor()].css; + nextFillStyle = this._themeService.colors.ansi[cell.getBgColor()].css; } // Apply dim to the background, this is relatively slow as the CSS is re-parsed but dim is diff --git a/addons/xterm-addon-canvas/src/Types.d.ts b/addons/xterm-addon-canvas/src/Types.d.ts index b1c3f0e3..1840284f 100644 --- a/addons/xterm-addon-canvas/src/Types.d.ts +++ b/addons/xterm-addon-canvas/src/Types.d.ts @@ -4,7 +4,7 @@ */ import { IDisposable } from 'common/Types'; -import { IColorSet } from 'browser/Types'; +import { IColorSet, ReadonlyColorSet } from 'browser/Types'; import { IEvent } from 'common/EventEmitter'; // TODO: Use core interfaces @@ -41,7 +41,6 @@ export interface IRenderer extends IDisposable { */ readonly onRequestRedraw: IEvent; - setColors(colors: IColorSet): void; handleDevicePixelRatioChange(): void; handleResize(cols: number, rows: number): void; handleCharSizeChanged(): void; @@ -79,11 +78,6 @@ export interface IRenderLayer extends IDisposable { */ handleOptionsChanged(): void; - /** - * Called when the theme changes. - */ - setColors(colorSet: IColorSet): void; - /** * Called when the data in the grid has changed (or needs to be rendered * again). diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.test.ts b/addons/xterm-addon-serialize/src/SerializeAddon.test.ts index df4a72da..4c5dd645 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.test.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.test.ts @@ -7,9 +7,10 @@ import jsdom = require('jsdom'); import { assert } from 'chai'; import { SerializeAddon } from './SerializeAddon'; import { Terminal } from 'browser/public/Terminal'; -import { ColorManager } from 'browser/ColorManager'; import { SelectionModel } from 'browser/selection/SelectionModel'; import { IBufferService } from 'common/services/Services'; +import { OptionsService } from 'common/services/OptionsService'; +import { ThemeService } from 'browser/services/ThemeService'; function sgr(...seq: string[]): string { return `\x1b[${seq.join(';')}m`; @@ -44,14 +45,11 @@ class TestSelectionService { } describe('xterm-addon-serialize', () => { - let cm: ColorManager; let dom: jsdom.JSDOM; - let document: Document; let window: jsdom.DOMWindow; let serializeAddon: SerializeAddon; let terminal: Terminal; - let selectionService: any; before(() => { serializeAddon = new SerializeAddon(); @@ -60,7 +58,6 @@ describe('xterm-addon-serialize', () => { beforeEach(() => { dom = new jsdom.JSDOM(''); window = dom.window; - document = window.document; (window as any).HTMLCanvasElement.prototype.getContext = () => ({ createLinearGradient(): any { @@ -77,10 +74,8 @@ describe('xterm-addon-serialize', () => { terminal = new Terminal({ cols: 10, rows: 2, allowProposedApi: true }); terminal.loadAddon(serializeAddon); - selectionService = new TestSelectionService((terminal as any)._core._bufferService); - cm = new ColorManager(); - (terminal as any)._core._colorManager = cm; - (terminal as any)._core._selectionService = selectionService; + (terminal as any)._core._themeService = new ThemeService(new OptionsService({})); + (terminal as any)._core._selectionService = new TestSelectionService((terminal as any)._core._bufferService); }); describe('text', () => { diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 99967374..b83ff160 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -544,7 +544,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler { super(buffer); // https://github.com/xtermjs/xterm.js/issues/3601 - this._colors = (_terminal as any)._core._colorManager.colors; + this._colors = (_terminal as any)._core._themeService.colors; } private _padStart(target: string, targetLength: number, padString: string): string { diff --git a/addons/xterm-addon-webgl/src/RectangleRenderer.ts b/addons/xterm-addon-webgl/src/RectangleRenderer.ts index ff935d1c..04368711 100644 --- a/addons/xterm-addon-webgl/src/RectangleRenderer.ts +++ b/addons/xterm-addon-webgl/src/RectangleRenderer.ts @@ -8,12 +8,13 @@ import { IRenderModel, IWebGLVertexArrayObject, IWebGL2RenderingContext } from ' import { Attributes, BgFlags, FgFlags } from 'common/buffer/Constants'; import { Terminal } from 'xterm'; import { IColor } from 'common/Types'; -import { IColorSet } from 'browser/Types'; +import { IColorSet, ReadonlyColorSet } from 'browser/Types'; import { IRenderDimensions } from 'browser/renderer/shared/Types'; import { RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel'; import { Disposable, toDisposable } from 'common/Lifecycle'; import { DIM_OPACITY } from 'browser/renderer/shared/Constants'; import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; +import { IThemeService } from 'browser/services/Services'; const enum VertexAttribLocations { POSITION = 0, @@ -84,9 +85,9 @@ export class RectangleRenderer extends Disposable { constructor( private _terminal: Terminal, - private _colors: IColorSet, private _gl: IWebGL2RenderingContext, - private _dimensions: IRenderDimensions + private _dimensions: IRenderDimensions, + private readonly _themeService: IThemeService ) { super(); @@ -133,7 +134,11 @@ export class RectangleRenderer extends Disposable { gl.vertexAttribPointer(VertexAttribLocations.COLOR, 4, gl.FLOAT, false, BYTES_PER_RECTANGLE, 4 * Float32Array.BYTES_PER_ELEMENT); gl.vertexAttribDivisor(VertexAttribLocations.COLOR, 1); - this._updateCachedColors(); + this._updateCachedColors(_themeService.colors); + this.register(this._themeService.onChangeColors(e => { + this._updateCachedColors(e); + this._updateViewportRectangle(); + })); } public render(): void { @@ -155,17 +160,12 @@ export class RectangleRenderer extends Disposable { this._updateViewportRectangle(); } - public setColors(): void { - this._updateCachedColors(); - this._updateViewportRectangle(); - } - public setDimensions(dimensions: IRenderDimensions): void { this._dimensions = dimensions; } - private _updateCachedColors(): void { - this._bgFloat = this._colorToFloat32Array(this._colors.background); + private _updateCachedColors(colors: ReadonlyColorSet): void { + this._bgFloat = this._colorToFloat32Array(colors.background); } private _updateViewportRectangle(): void { @@ -236,27 +236,27 @@ export class RectangleRenderer extends Disposable { switch (fg & Attributes.CM_MASK) { case Attributes.CM_P16: case Attributes.CM_P256: - $rgba = this._colors.ansi[fg & Attributes.PCOLOR_MASK].rgba; + $rgba = this._themeService.colors.ansi[fg & Attributes.PCOLOR_MASK].rgba; break; case Attributes.CM_RGB: $rgba = (fg & Attributes.RGB_MASK) << 8; break; case Attributes.CM_DEFAULT: default: - $rgba = this._colors.foreground.rgba; + $rgba = this._themeService.colors.foreground.rgba; } } else { switch (bg & Attributes.CM_MASK) { case Attributes.CM_P16: case Attributes.CM_P256: - $rgba = this._colors.ansi[bg & Attributes.PCOLOR_MASK].rgba; + $rgba = this._themeService.colors.ansi[bg & Attributes.PCOLOR_MASK].rgba; break; case Attributes.CM_RGB: $rgba = (bg & Attributes.RGB_MASK) << 8; break; case Attributes.CM_DEFAULT: default: - $rgba = this._colors.background.rgba; + $rgba = this._themeService.colors.background.rgba; $isDefault = true; } } diff --git a/addons/xterm-addon-webgl/src/WebglAddon.ts b/addons/xterm-addon-webgl/src/WebglAddon.ts index 59dd1e40..71487315 100644 --- a/addons/xterm-addon-webgl/src/WebglAddon.ts +++ b/addons/xterm-addon-webgl/src/WebglAddon.ts @@ -5,7 +5,7 @@ import { Terminal, ITerminalAddon, IEvent } from 'xterm'; import { WebglRenderer } from './WebglRenderer'; -import { ICharacterJoinerService, ICoreBrowserService, IRenderService } from 'browser/services/Services'; +import { ICharacterJoinerService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services'; import { IColorSet } from 'browser/Types'; import { EventEmitter, forwardEvent } from 'common/EventEmitter'; import { isSafari } from 'common/Platform'; @@ -42,8 +42,8 @@ export class WebglAddon extends Disposable implements ITerminalAddon { const coreBrowserService: ICoreBrowserService = core._coreBrowserService; const coreService: ICoreService = core.coreService; const decorationService: IDecorationService = core._decorationService; - const colors: IColorSet = core._colorManager.colors; - this._renderer = this.register(new WebglRenderer(terminal, colors, characterJoinerService, coreBrowserService, coreService, decorationService, this._preserveDrawingBuffer)); + const themeService: IThemeService = core._themeService; + this._renderer = this.register(new WebglRenderer(terminal, themeService, characterJoinerService, coreBrowserService, coreService, decorationService, this._preserveDrawingBuffer)); this.register(forwardEvent(this._renderer.onContextLoss, this._onContextLoss)); this.register(forwardEvent(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas)); renderService.setRenderer(this._renderer); diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index bd906b22..ed99a225 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -8,8 +8,8 @@ import { CellColorResolver } from 'browser/renderer/shared/CellColorResolver'; import { acquireTextureAtlas, removeTerminalFromCache } from 'browser/renderer/shared/CharAtlasCache'; import { observeDevicePixelDimensions } from 'browser/renderer/shared/DevicePixelObserver'; import { IRenderDimensions, IRenderer, IRequestRedrawEvent, ITextureAtlas } from 'browser/renderer/shared/Types'; -import { ICharacterJoinerService, ICoreBrowserService } from 'browser/services/Services'; -import { IColorSet, ITerminal } from 'browser/Types'; +import { ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services'; +import { IColorSet, ITerminal, ReadonlyColorSet } from 'browser/Types'; import { AttributeData } from 'common/buffer/AttributeData'; import { CellData } from 'common/buffer/CellData'; import { Content, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants'; @@ -55,7 +55,7 @@ export class WebglRenderer extends Disposable implements IRenderer { constructor( private _terminal: Terminal, - private _colors: IColorSet, + private readonly _themeService: IThemeService, private readonly _characterJoinerService: ICharacterJoinerService, private readonly _coreBrowserService: ICoreBrowserService, coreService: ICoreService, @@ -64,13 +64,15 @@ export class WebglRenderer extends Disposable implements IRenderer { ) { super(); - this._cellColorResolver = new CellColorResolver(this._terminal, this._colors, this._model.selection, this._decorationService, this._coreBrowserService); + this.register(this._themeService.onChangeColors(() => this._handleColorChange())); + + this._cellColorResolver = new CellColorResolver(this._terminal, this._model.selection, this._decorationService, this._coreBrowserService, this._themeService); this._core = (this._terminal as any)._core; this._renderLayers = [ - new LinkRenderLayer(this._core.screenElement!, 2, this._colors, this._core, this._coreBrowserService), - new CursorRenderLayer(_terminal, this._core.screenElement!, 3, this._colors, this._onRequestRedraw, this._coreBrowserService, coreService) + new LinkRenderLayer(this._core.screenElement!, 2, this._terminal, this._core.linkifier2, this._coreBrowserService, this._themeService), + new CursorRenderLayer(_terminal, this._core.screenElement!, 3, this._onRequestRedraw, this._coreBrowserService, coreService, this._themeService) ]; this.dimensions = { scaledCharWidth: 0, @@ -145,17 +147,7 @@ export class WebglRenderer extends Disposable implements IRenderer { return this._charAtlas?.cacheCanvas; } - public setColors(colors: IColorSet): void { - this._colors = colors; - // Clear layers and force a full render - for (const l of this._renderLayers) { - l.setColors(this._terminal, this._colors); - l.reset(this._terminal); - } - - this._cellColorResolver.setColors(colors); - this._rectangleRenderer.setColors(); - + private _handleColorChange(): void { this._refreshCharAtlas(); // Force a full refresh @@ -254,7 +246,7 @@ export class WebglRenderer extends Disposable implements IRenderer { this._rectangleRenderer?.dispose(); this._glyphRenderer?.dispose(); - this._rectangleRenderer = this.register(new RectangleRenderer(this._terminal, this._colors, this._gl, this.dimensions)); + this._rectangleRenderer = this.register(new RectangleRenderer(this._terminal, this._gl, this.dimensions, this._themeService)); this._glyphRenderer = this.register(new GlyphRenderer(this._terminal, this._gl, this.dimensions)); // Update dimensions and acquire char atlas @@ -273,7 +265,7 @@ export class WebglRenderer extends Disposable implements IRenderer { return; } - const atlas = acquireTextureAtlas(this._terminal, this._colors, this.dimensions.scaledCellWidth, this.dimensions.scaledCellHeight, this.dimensions.scaledCharWidth, this.dimensions.scaledCharHeight, this._coreBrowserService.dpr); + const atlas = acquireTextureAtlas(this._terminal, this._themeService.colors, this.dimensions.scaledCellWidth, this.dimensions.scaledCellHeight, this.dimensions.scaledCharWidth, this.dimensions.scaledCharHeight, this._coreBrowserService.dpr); if (this._charAtlas !== atlas) { this._onChangeTextureAtlas.fire(atlas.cacheCanvas); } diff --git a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts index 50f47f6c..e99a1464 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts @@ -6,9 +6,9 @@ import { IRenderLayer } from './Types'; import { acquireTextureAtlas } from 'browser/renderer/shared/CharAtlasCache'; import { Terminal } from 'xterm'; -import { IColorSet } from 'browser/Types'; +import { IColorSet, ReadonlyColorSet } from 'browser/Types'; import { TEXT_BASELINE } from 'browser/renderer/shared/Constants'; -import { ICoreBrowserService } from 'browser/services/Services'; +import { ICoreBrowserService, IThemeService } from 'browser/services/Services'; import { IRenderDimensions, ITextureAtlas } from 'browser/renderer/shared/Types'; import { CellData } from 'common/buffer/CellData'; import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; @@ -27,12 +27,13 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer protected _charAtlas: ITextureAtlas | undefined; constructor( + terminal: Terminal, private _container: HTMLElement, id: string, zIndex: number, private _alpha: boolean, - protected _colors: IColorSet, - protected readonly _coreBrowserService: ICoreBrowserService + protected readonly _coreBrowserService: ICoreBrowserService, + protected readonly _themeService: IThemeService ) { super(); this._canvas = document.createElement('canvas'); @@ -40,6 +41,10 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer this._canvas.style.zIndex = zIndex.toString(); this._initCanvas(); this._container.appendChild(this._canvas); + this.register(this._themeService.onChangeColors(e => { + this._refreshCharAtlas(terminal, e); + this.reset(terminal); + })); this.register(toDisposable(() => { this._canvas.remove(); this._charAtlas?.dispose(); @@ -61,10 +66,6 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer public handleGridChanged(terminal: Terminal, startRow: number, endRow: number): void {} public handleSelectionChanged(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {} - public setColors(terminal: Terminal, colorSet: IColorSet): void { - this._refreshCharAtlas(terminal, colorSet); - } - protected _setTransparency(terminal: Terminal, alpha: boolean): void { // Do nothing when alpha doesn't change if (alpha === this._alpha) { @@ -80,7 +81,7 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer this._container.replaceChild(this._canvas, oldCanvas); // Regenerate char atlas and force a full redraw - this._refreshCharAtlas(terminal, this._colors); + this._refreshCharAtlas(terminal, this._themeService.colors); this.handleGridChanged(terminal, 0, terminal.rows - 1); } @@ -89,7 +90,7 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer * @param terminal The terminal. * @param colorSet The color set to use for the char atlas. */ - private _refreshCharAtlas(terminal: Terminal, colorSet: IColorSet): void { + private _refreshCharAtlas(terminal: Terminal, colorSet: ReadonlyColorSet): void { if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) { return; } @@ -114,7 +115,7 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer this._clearAll(); } - this._refreshCharAtlas(terminal, this._colors); + this._refreshCharAtlas(terminal, this._themeService.colors); } public abstract reset(terminal: Terminal): void; @@ -184,7 +185,7 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer if (this._alpha) { this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); } else { - this._ctx.fillStyle = this._colors.background.css; + this._ctx.fillStyle = this._themeService.colors.background.css; this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height); } } @@ -204,7 +205,7 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer width * this._scaledCellWidth, height * this._scaledCellHeight); } else { - this._ctx.fillStyle = this._colors.background.css; + this._ctx.fillStyle = this._themeService.colors.background.css; this._ctx.fillRect( x * this._scaledCellWidth, y * this._scaledCellHeight, diff --git a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts index d685dd94..74801f4e 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts @@ -7,10 +7,10 @@ import { Terminal } from 'xterm'; import { BaseRenderLayer } from './BaseRenderLayer'; import { ICellData } from 'common/Types'; import { CellData } from 'common/buffer/CellData'; -import { IColorSet } from 'browser/Types'; +import { IColorSet, ReadonlyColorSet } from 'browser/Types'; import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/shared/Types'; import { IEventEmitter } from 'common/EventEmitter'; -import { ICoreBrowserService } from 'browser/services/Services'; +import { ICoreBrowserService, IThemeService } from 'browser/services/Services'; import { ICoreService } from 'common/services/Services'; import { toDisposable } from 'common/Lifecycle'; @@ -37,12 +37,12 @@ export class CursorRenderLayer extends BaseRenderLayer { terminal: Terminal, container: HTMLElement, zIndex: number, - colors: IColorSet, private _onRequestRefreshRowsEvent: IEventEmitter, coreBrowserService: ICoreBrowserService, - private readonly _coreService: ICoreService + private readonly _coreService: ICoreService, + themeService: IThemeService ) { - super(container, 'cursor', zIndex, true, colors, coreBrowserService); + super(terminal, container, 'cursor', zIndex, true, coreBrowserService, themeService); this._state = { x: 0, y: 0, @@ -146,7 +146,7 @@ export class CursorRenderLayer extends BaseRenderLayer { if (!this._coreBrowserService.isFocused) { this._clearCursor(); this._ctx.save(); - this._ctx.fillStyle = this._colors.cursor.css; + this._ctx.fillStyle = this._themeService.colors.cursor.css; const cursorStyle = terminal.options.cursorStyle; if (cursorStyle && cursorStyle !== 'block') { this._cursorRenderers[cursorStyle](terminal, cursorX, viewportRelativeCursorY, this._cell); @@ -211,30 +211,30 @@ export class CursorRenderLayer extends BaseRenderLayer { private _renderBarCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void { this._ctx.save(); - this._ctx.fillStyle = this._colors.cursor.css; + this._ctx.fillStyle = this._themeService.colors.cursor.css; this._fillLeftLineAtCell(x, y, terminal.options.cursorWidth); this._ctx.restore(); } private _renderBlockCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void { this._ctx.save(); - this._ctx.fillStyle = this._colors.cursor.css; + this._ctx.fillStyle = this._themeService.colors.cursor.css; this._fillCells(x, y, cell.getWidth(), 1); - this._ctx.fillStyle = this._colors.cursorAccent.css; + this._ctx.fillStyle = this._themeService.colors.cursorAccent.css; this._fillCharTrueColor(terminal, cell, x, y); this._ctx.restore(); } private _renderUnderlineCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void { this._ctx.save(); - this._ctx.fillStyle = this._colors.cursor.css; + this._ctx.fillStyle = this._themeService.colors.cursor.css; this._fillBottomLineAtCells(x, y); this._ctx.restore(); } private _renderBlurCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void { this._ctx.save(); - this._ctx.strokeStyle = this._colors.cursor.css; + this._ctx.strokeStyle = this._themeService.colors.cursor.css; this._strokeRectAtCell(x, y, cell.getWidth(), 1); this._ctx.restore(); } diff --git a/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts index 2ce90daf..2d2af192 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts @@ -3,14 +3,13 @@ * @license MIT */ +import { is256Color } from 'browser/renderer/shared/CharAtlasUtils'; +import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants'; +import { IRenderDimensions } from 'browser/renderer/shared/Types'; +import { ICoreBrowserService, IThemeService } from 'browser/services/Services'; +import { ILinkifier2, ILinkifierEvent, ITerminal } from 'browser/Types'; import { Terminal } from 'xterm'; import { BaseRenderLayer } from './BaseRenderLayer'; -import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants'; -import { ITerminal, IColorSet, ILinkifierEvent } from 'browser/Types'; -import { IRenderDimensions } from 'browser/renderer/shared/Types'; -import { ICoreBrowserService } from 'browser/services/Services'; -import { is256Color } from 'browser/renderer/shared/CharAtlasUtils'; -import { toDisposable } from 'common/Lifecycle'; export class LinkRenderLayer extends BaseRenderLayer { private _state: ILinkifierEvent | undefined; @@ -18,14 +17,15 @@ export class LinkRenderLayer extends BaseRenderLayer { constructor( container: HTMLElement, zIndex: number, - colors: IColorSet, - terminal: ITerminal, - coreBrowserService: ICoreBrowserService + terminal: Terminal, + linkifier2: ILinkifier2, + coreBrowserService: ICoreBrowserService, + themeService: IThemeService ) { - super(container, 'link', zIndex, true, colors, coreBrowserService); + super(terminal, container, 'link', zIndex, true, coreBrowserService, themeService); - this.register(terminal.linkifier2.onShowLinkUnderline(e => this._handleShowLinkUnderline(e))); - this.register(terminal.linkifier2.onHideLinkUnderline(e => this._handleHideLinkUnderline(e))); + this.register(linkifier2.onShowLinkUnderline(e => this._handleShowLinkUnderline(e))); + this.register(linkifier2.onHideLinkUnderline(e => this._handleHideLinkUnderline(e))); } public resize(terminal: Terminal, dim: IRenderDimensions): void { @@ -52,12 +52,12 @@ export class LinkRenderLayer extends BaseRenderLayer { private _handleShowLinkUnderline(e: ILinkifierEvent): void { if (e.fg === INVERTED_DEFAULT_COLOR) { - this._ctx.fillStyle = this._colors.background.css; + this._ctx.fillStyle = this._themeService.colors.background.css; } else if (e.fg !== undefined && is256Color(e.fg)) { // 256 color support - this._ctx.fillStyle = this._colors.ansi[e.fg!].css; + this._ctx.fillStyle = this._themeService.colors.ansi[e.fg!].css; } else { - this._ctx.fillStyle = this._colors.foreground.css; + this._ctx.fillStyle = this._themeService.colors.foreground.css; } if (e.y1 === e.y2) { diff --git a/addons/xterm-addon-webgl/src/renderLayer/Types.ts b/addons/xterm-addon-webgl/src/renderLayer/Types.ts index 08561b44..089680ca 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/Types.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/Types.ts @@ -4,7 +4,7 @@ */ import { IDisposable, Terminal } from 'xterm'; -import { IColorSet } from 'browser/Types'; +import { IColorSet, ReadonlyColorSet } from 'browser/Types'; import { IRenderDimensions } from 'browser/renderer/shared/Types'; export interface IRenderLayer extends IDisposable { @@ -28,11 +28,6 @@ export interface IRenderLayer extends IDisposable { */ handleOptionsChanged(terminal: Terminal): void; - /** - * Called when the theme changes. - */ - setColors(terminal: Terminal, colorSet: IColorSet): void; - /** * Called when the data in the grid has changed (or needs to be rendered * again). diff --git a/src/browser/ColorManager.test.ts b/src/browser/ColorManager.test.ts deleted file mode 100644 index cf60a1f5..00000000 --- a/src/browser/ColorManager.test.ts +++ /dev/null @@ -1,366 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import jsdom = require('jsdom'); -import { assert } from 'chai'; -import { ColorManager, DEFAULT_ANSI_COLORS } from 'browser/ColorManager'; - -describe('ColorManager', () => { - let cm: ColorManager; - let dom: jsdom.JSDOM; - let document: Document; - let window: jsdom.DOMWindow; - - beforeEach(() => { - dom = new jsdom.JSDOM(''); - window = dom.window; - document = window.document; - (window as any).HTMLCanvasElement.prototype.getContext = () => ({ - createLinearGradient(): any { - return null; - }, - - fillRect(): void { }, - - getImageData(): any { - return {data: [0, 0, 0, 0xFF]}; - } - }); - cm = new ColorManager(); - }); - - describe('constructor', () => { - it('should fill all colors with values', () => { - for (const key of Object.keys(cm.colors)) { - if (key !== 'ansi' && key !== 'contrastCache' && key !== 'selectionForeground') { - // A #rrggbb or rgba(...) - assert.ok((cm.colors as any)[key].css.length >= 7); - } - } - assert.equal(cm.colors.ansi.length, 256); - }); - - it('should fill 240 colors with expected values', () => { - assert.equal(cm.colors.ansi[16].css, '#000000'); - assert.equal(cm.colors.ansi[17].css, '#00005f'); - assert.equal(cm.colors.ansi[18].css, '#000087'); - assert.equal(cm.colors.ansi[19].css, '#0000af'); - assert.equal(cm.colors.ansi[20].css, '#0000d7'); - assert.equal(cm.colors.ansi[21].css, '#0000ff'); - assert.equal(cm.colors.ansi[22].css, '#005f00'); - assert.equal(cm.colors.ansi[23].css, '#005f5f'); - assert.equal(cm.colors.ansi[24].css, '#005f87'); - assert.equal(cm.colors.ansi[25].css, '#005faf'); - assert.equal(cm.colors.ansi[26].css, '#005fd7'); - assert.equal(cm.colors.ansi[27].css, '#005fff'); - assert.equal(cm.colors.ansi[28].css, '#008700'); - assert.equal(cm.colors.ansi[29].css, '#00875f'); - assert.equal(cm.colors.ansi[30].css, '#008787'); - assert.equal(cm.colors.ansi[31].css, '#0087af'); - assert.equal(cm.colors.ansi[32].css, '#0087d7'); - assert.equal(cm.colors.ansi[33].css, '#0087ff'); - assert.equal(cm.colors.ansi[34].css, '#00af00'); - assert.equal(cm.colors.ansi[35].css, '#00af5f'); - assert.equal(cm.colors.ansi[36].css, '#00af87'); - assert.equal(cm.colors.ansi[37].css, '#00afaf'); - assert.equal(cm.colors.ansi[38].css, '#00afd7'); - assert.equal(cm.colors.ansi[39].css, '#00afff'); - assert.equal(cm.colors.ansi[40].css, '#00d700'); - assert.equal(cm.colors.ansi[41].css, '#00d75f'); - assert.equal(cm.colors.ansi[42].css, '#00d787'); - assert.equal(cm.colors.ansi[43].css, '#00d7af'); - assert.equal(cm.colors.ansi[44].css, '#00d7d7'); - assert.equal(cm.colors.ansi[45].css, '#00d7ff'); - assert.equal(cm.colors.ansi[46].css, '#00ff00'); - assert.equal(cm.colors.ansi[47].css, '#00ff5f'); - assert.equal(cm.colors.ansi[48].css, '#00ff87'); - assert.equal(cm.colors.ansi[49].css, '#00ffaf'); - assert.equal(cm.colors.ansi[50].css, '#00ffd7'); - assert.equal(cm.colors.ansi[51].css, '#00ffff'); - assert.equal(cm.colors.ansi[52].css, '#5f0000'); - assert.equal(cm.colors.ansi[53].css, '#5f005f'); - assert.equal(cm.colors.ansi[54].css, '#5f0087'); - assert.equal(cm.colors.ansi[55].css, '#5f00af'); - assert.equal(cm.colors.ansi[56].css, '#5f00d7'); - assert.equal(cm.colors.ansi[57].css, '#5f00ff'); - assert.equal(cm.colors.ansi[58].css, '#5f5f00'); - assert.equal(cm.colors.ansi[59].css, '#5f5f5f'); - assert.equal(cm.colors.ansi[60].css, '#5f5f87'); - assert.equal(cm.colors.ansi[61].css, '#5f5faf'); - assert.equal(cm.colors.ansi[62].css, '#5f5fd7'); - assert.equal(cm.colors.ansi[63].css, '#5f5fff'); - assert.equal(cm.colors.ansi[64].css, '#5f8700'); - assert.equal(cm.colors.ansi[65].css, '#5f875f'); - assert.equal(cm.colors.ansi[66].css, '#5f8787'); - assert.equal(cm.colors.ansi[67].css, '#5f87af'); - assert.equal(cm.colors.ansi[68].css, '#5f87d7'); - assert.equal(cm.colors.ansi[69].css, '#5f87ff'); - assert.equal(cm.colors.ansi[70].css, '#5faf00'); - assert.equal(cm.colors.ansi[71].css, '#5faf5f'); - assert.equal(cm.colors.ansi[72].css, '#5faf87'); - assert.equal(cm.colors.ansi[73].css, '#5fafaf'); - assert.equal(cm.colors.ansi[74].css, '#5fafd7'); - assert.equal(cm.colors.ansi[75].css, '#5fafff'); - assert.equal(cm.colors.ansi[76].css, '#5fd700'); - assert.equal(cm.colors.ansi[77].css, '#5fd75f'); - assert.equal(cm.colors.ansi[78].css, '#5fd787'); - assert.equal(cm.colors.ansi[79].css, '#5fd7af'); - assert.equal(cm.colors.ansi[80].css, '#5fd7d7'); - assert.equal(cm.colors.ansi[81].css, '#5fd7ff'); - assert.equal(cm.colors.ansi[82].css, '#5fff00'); - assert.equal(cm.colors.ansi[83].css, '#5fff5f'); - assert.equal(cm.colors.ansi[84].css, '#5fff87'); - assert.equal(cm.colors.ansi[85].css, '#5fffaf'); - assert.equal(cm.colors.ansi[86].css, '#5fffd7'); - assert.equal(cm.colors.ansi[87].css, '#5fffff'); - assert.equal(cm.colors.ansi[88].css, '#870000'); - assert.equal(cm.colors.ansi[89].css, '#87005f'); - assert.equal(cm.colors.ansi[90].css, '#870087'); - assert.equal(cm.colors.ansi[91].css, '#8700af'); - assert.equal(cm.colors.ansi[92].css, '#8700d7'); - assert.equal(cm.colors.ansi[93].css, '#8700ff'); - assert.equal(cm.colors.ansi[94].css, '#875f00'); - assert.equal(cm.colors.ansi[95].css, '#875f5f'); - assert.equal(cm.colors.ansi[96].css, '#875f87'); - assert.equal(cm.colors.ansi[97].css, '#875faf'); - assert.equal(cm.colors.ansi[98].css, '#875fd7'); - assert.equal(cm.colors.ansi[99].css, '#875fff'); - assert.equal(cm.colors.ansi[100].css, '#878700'); - assert.equal(cm.colors.ansi[101].css, '#87875f'); - assert.equal(cm.colors.ansi[102].css, '#878787'); - assert.equal(cm.colors.ansi[103].css, '#8787af'); - assert.equal(cm.colors.ansi[104].css, '#8787d7'); - assert.equal(cm.colors.ansi[105].css, '#8787ff'); - assert.equal(cm.colors.ansi[106].css, '#87af00'); - assert.equal(cm.colors.ansi[107].css, '#87af5f'); - assert.equal(cm.colors.ansi[108].css, '#87af87'); - assert.equal(cm.colors.ansi[109].css, '#87afaf'); - assert.equal(cm.colors.ansi[110].css, '#87afd7'); - assert.equal(cm.colors.ansi[111].css, '#87afff'); - assert.equal(cm.colors.ansi[112].css, '#87d700'); - assert.equal(cm.colors.ansi[113].css, '#87d75f'); - assert.equal(cm.colors.ansi[114].css, '#87d787'); - assert.equal(cm.colors.ansi[115].css, '#87d7af'); - assert.equal(cm.colors.ansi[116].css, '#87d7d7'); - assert.equal(cm.colors.ansi[117].css, '#87d7ff'); - assert.equal(cm.colors.ansi[118].css, '#87ff00'); - assert.equal(cm.colors.ansi[119].css, '#87ff5f'); - assert.equal(cm.colors.ansi[120].css, '#87ff87'); - assert.equal(cm.colors.ansi[121].css, '#87ffaf'); - assert.equal(cm.colors.ansi[122].css, '#87ffd7'); - assert.equal(cm.colors.ansi[123].css, '#87ffff'); - assert.equal(cm.colors.ansi[124].css, '#af0000'); - assert.equal(cm.colors.ansi[125].css, '#af005f'); - assert.equal(cm.colors.ansi[126].css, '#af0087'); - assert.equal(cm.colors.ansi[127].css, '#af00af'); - assert.equal(cm.colors.ansi[128].css, '#af00d7'); - assert.equal(cm.colors.ansi[129].css, '#af00ff'); - assert.equal(cm.colors.ansi[130].css, '#af5f00'); - assert.equal(cm.colors.ansi[131].css, '#af5f5f'); - assert.equal(cm.colors.ansi[132].css, '#af5f87'); - assert.equal(cm.colors.ansi[133].css, '#af5faf'); - assert.equal(cm.colors.ansi[134].css, '#af5fd7'); - assert.equal(cm.colors.ansi[135].css, '#af5fff'); - assert.equal(cm.colors.ansi[136].css, '#af8700'); - assert.equal(cm.colors.ansi[137].css, '#af875f'); - assert.equal(cm.colors.ansi[138].css, '#af8787'); - assert.equal(cm.colors.ansi[139].css, '#af87af'); - assert.equal(cm.colors.ansi[140].css, '#af87d7'); - assert.equal(cm.colors.ansi[141].css, '#af87ff'); - assert.equal(cm.colors.ansi[142].css, '#afaf00'); - assert.equal(cm.colors.ansi[143].css, '#afaf5f'); - assert.equal(cm.colors.ansi[144].css, '#afaf87'); - assert.equal(cm.colors.ansi[145].css, '#afafaf'); - assert.equal(cm.colors.ansi[146].css, '#afafd7'); - assert.equal(cm.colors.ansi[147].css, '#afafff'); - assert.equal(cm.colors.ansi[148].css, '#afd700'); - assert.equal(cm.colors.ansi[149].css, '#afd75f'); - assert.equal(cm.colors.ansi[150].css, '#afd787'); - assert.equal(cm.colors.ansi[151].css, '#afd7af'); - assert.equal(cm.colors.ansi[152].css, '#afd7d7'); - assert.equal(cm.colors.ansi[153].css, '#afd7ff'); - assert.equal(cm.colors.ansi[154].css, '#afff00'); - assert.equal(cm.colors.ansi[155].css, '#afff5f'); - assert.equal(cm.colors.ansi[156].css, '#afff87'); - assert.equal(cm.colors.ansi[157].css, '#afffaf'); - assert.equal(cm.colors.ansi[158].css, '#afffd7'); - assert.equal(cm.colors.ansi[159].css, '#afffff'); - assert.equal(cm.colors.ansi[160].css, '#d70000'); - assert.equal(cm.colors.ansi[161].css, '#d7005f'); - assert.equal(cm.colors.ansi[162].css, '#d70087'); - assert.equal(cm.colors.ansi[163].css, '#d700af'); - assert.equal(cm.colors.ansi[164].css, '#d700d7'); - assert.equal(cm.colors.ansi[165].css, '#d700ff'); - assert.equal(cm.colors.ansi[166].css, '#d75f00'); - assert.equal(cm.colors.ansi[167].css, '#d75f5f'); - assert.equal(cm.colors.ansi[168].css, '#d75f87'); - assert.equal(cm.colors.ansi[169].css, '#d75faf'); - assert.equal(cm.colors.ansi[170].css, '#d75fd7'); - assert.equal(cm.colors.ansi[171].css, '#d75fff'); - assert.equal(cm.colors.ansi[172].css, '#d78700'); - assert.equal(cm.colors.ansi[173].css, '#d7875f'); - assert.equal(cm.colors.ansi[174].css, '#d78787'); - assert.equal(cm.colors.ansi[175].css, '#d787af'); - assert.equal(cm.colors.ansi[176].css, '#d787d7'); - assert.equal(cm.colors.ansi[177].css, '#d787ff'); - assert.equal(cm.colors.ansi[178].css, '#d7af00'); - assert.equal(cm.colors.ansi[179].css, '#d7af5f'); - assert.equal(cm.colors.ansi[180].css, '#d7af87'); - assert.equal(cm.colors.ansi[181].css, '#d7afaf'); - assert.equal(cm.colors.ansi[182].css, '#d7afd7'); - assert.equal(cm.colors.ansi[183].css, '#d7afff'); - assert.equal(cm.colors.ansi[184].css, '#d7d700'); - assert.equal(cm.colors.ansi[185].css, '#d7d75f'); - assert.equal(cm.colors.ansi[186].css, '#d7d787'); - assert.equal(cm.colors.ansi[187].css, '#d7d7af'); - assert.equal(cm.colors.ansi[188].css, '#d7d7d7'); - assert.equal(cm.colors.ansi[189].css, '#d7d7ff'); - assert.equal(cm.colors.ansi[190].css, '#d7ff00'); - assert.equal(cm.colors.ansi[191].css, '#d7ff5f'); - assert.equal(cm.colors.ansi[192].css, '#d7ff87'); - assert.equal(cm.colors.ansi[193].css, '#d7ffaf'); - assert.equal(cm.colors.ansi[194].css, '#d7ffd7'); - assert.equal(cm.colors.ansi[195].css, '#d7ffff'); - assert.equal(cm.colors.ansi[196].css, '#ff0000'); - assert.equal(cm.colors.ansi[197].css, '#ff005f'); - assert.equal(cm.colors.ansi[198].css, '#ff0087'); - assert.equal(cm.colors.ansi[199].css, '#ff00af'); - assert.equal(cm.colors.ansi[200].css, '#ff00d7'); - assert.equal(cm.colors.ansi[201].css, '#ff00ff'); - assert.equal(cm.colors.ansi[202].css, '#ff5f00'); - assert.equal(cm.colors.ansi[203].css, '#ff5f5f'); - assert.equal(cm.colors.ansi[204].css, '#ff5f87'); - assert.equal(cm.colors.ansi[205].css, '#ff5faf'); - assert.equal(cm.colors.ansi[206].css, '#ff5fd7'); - assert.equal(cm.colors.ansi[207].css, '#ff5fff'); - assert.equal(cm.colors.ansi[208].css, '#ff8700'); - assert.equal(cm.colors.ansi[209].css, '#ff875f'); - assert.equal(cm.colors.ansi[210].css, '#ff8787'); - assert.equal(cm.colors.ansi[211].css, '#ff87af'); - assert.equal(cm.colors.ansi[212].css, '#ff87d7'); - assert.equal(cm.colors.ansi[213].css, '#ff87ff'); - assert.equal(cm.colors.ansi[214].css, '#ffaf00'); - assert.equal(cm.colors.ansi[215].css, '#ffaf5f'); - assert.equal(cm.colors.ansi[216].css, '#ffaf87'); - assert.equal(cm.colors.ansi[217].css, '#ffafaf'); - assert.equal(cm.colors.ansi[218].css, '#ffafd7'); - assert.equal(cm.colors.ansi[219].css, '#ffafff'); - assert.equal(cm.colors.ansi[220].css, '#ffd700'); - assert.equal(cm.colors.ansi[221].css, '#ffd75f'); - assert.equal(cm.colors.ansi[222].css, '#ffd787'); - assert.equal(cm.colors.ansi[223].css, '#ffd7af'); - assert.equal(cm.colors.ansi[224].css, '#ffd7d7'); - assert.equal(cm.colors.ansi[225].css, '#ffd7ff'); - assert.equal(cm.colors.ansi[226].css, '#ffff00'); - assert.equal(cm.colors.ansi[227].css, '#ffff5f'); - assert.equal(cm.colors.ansi[228].css, '#ffff87'); - assert.equal(cm.colors.ansi[229].css, '#ffffaf'); - assert.equal(cm.colors.ansi[230].css, '#ffffd7'); - assert.equal(cm.colors.ansi[231].css, '#ffffff'); - assert.equal(cm.colors.ansi[232].css, '#080808'); - assert.equal(cm.colors.ansi[233].css, '#121212'); - assert.equal(cm.colors.ansi[234].css, '#1c1c1c'); - assert.equal(cm.colors.ansi[235].css, '#262626'); - assert.equal(cm.colors.ansi[236].css, '#303030'); - assert.equal(cm.colors.ansi[237].css, '#3a3a3a'); - assert.equal(cm.colors.ansi[238].css, '#444444'); - assert.equal(cm.colors.ansi[239].css, '#4e4e4e'); - assert.equal(cm.colors.ansi[240].css, '#585858'); - assert.equal(cm.colors.ansi[241].css, '#626262'); - assert.equal(cm.colors.ansi[242].css, '#6c6c6c'); - assert.equal(cm.colors.ansi[243].css, '#767676'); - assert.equal(cm.colors.ansi[244].css, '#808080'); - assert.equal(cm.colors.ansi[245].css, '#8a8a8a'); - assert.equal(cm.colors.ansi[246].css, '#949494'); - assert.equal(cm.colors.ansi[247].css, '#9e9e9e'); - assert.equal(cm.colors.ansi[248].css, '#a8a8a8'); - assert.equal(cm.colors.ansi[249].css, '#b2b2b2'); - assert.equal(cm.colors.ansi[250].css, '#bcbcbc'); - assert.equal(cm.colors.ansi[251].css, '#c6c6c6'); - assert.equal(cm.colors.ansi[252].css, '#d0d0d0'); - assert.equal(cm.colors.ansi[253].css, '#dadada'); - assert.equal(cm.colors.ansi[254].css, '#e4e4e4'); - assert.equal(cm.colors.ansi[255].css, '#eeeeee'); - }); - }); - - describe('setTheme', () => { - it('should not throw when not setting all colors', () => { - assert.doesNotThrow(() => { - cm.setTheme({}); - }); - }); - - it('should set a partial set of colors, using the default if not present', () => { - assert.equal(cm.colors.background.css, '#000000'); - assert.equal(cm.colors.foreground.css, '#ffffff'); - cm.setTheme({ - background: '#FF0000', - foreground: '#00FF00' - }); - assert.equal(cm.colors.background.css, '#FF0000'); - assert.equal(cm.colors.foreground.css, '#00FF00'); - cm.setTheme({ - background: '#0000FF' - }); - assert.equal(cm.colors.background.css, '#0000FF'); - // FG reverts back to default - assert.equal(cm.colors.foreground.css, '#ffffff'); - }); - - it('should set all extended ansi colors in reverse order', () => { - cm.setTheme({ - extendedAnsi: DEFAULT_ANSI_COLORS.map(a => a.css).slice().reverse() - }); - - for (let ansiColor = 16; ansiColor <= 255; ansiColor++) { - assert.equal(cm.colors.ansi[ansiColor].css, DEFAULT_ANSI_COLORS[255 + 16 - ansiColor].css); - } - }); - - it('should set one extended ansi color and keep the other default', () => { - cm.setTheme({ - extendedAnsi: ['#ffffff'] - }); - - assert.equal(cm.colors.ansi[16].css, '#ffffff'); - assert.equal(cm.colors.ansi[17].css, DEFAULT_ANSI_COLORS[17].css); - }); - - it('should set extended ansi colors to the default when they are unset', () => { - cm.setTheme({ - extendedAnsi: ['#ffffff'] - }); - assert.equal(cm.colors.ansi[16].css, '#ffffff'); - - cm.setTheme({ - extendedAnsi: [] - }); - assert.equal(cm.colors.ansi[16].css, DEFAULT_ANSI_COLORS[16].css); - - cm.setTheme({ - extendedAnsi: ['#ffffff'] - }); - assert.equal(cm.colors.ansi[16].css, '#ffffff'); - - cm.setTheme({}); - assert.equal(cm.colors.ansi[16].css, DEFAULT_ANSI_COLORS[16].css); - }); - - it('should set extended ansi colors to the default when they are partially unset', () => { - cm.setTheme({ - extendedAnsi: ['#ffffff', '#000000'] - }); - assert.equal(cm.colors.ansi[16].css, '#ffffff'); - assert.equal(cm.colors.ansi[17].css, '#000000'); - - cm.setTheme({ - extendedAnsi: ['#ffffff'] - }); - assert.equal(cm.colors.ansi[16].css, '#ffffff'); - assert.equal(cm.colors.ansi[17].css, DEFAULT_ANSI_COLORS[17].css); - }); - }); -}); diff --git a/src/browser/ColorManager.ts b/src/browser/ColorManager.ts deleted file mode 100644 index de5c828c..00000000 --- a/src/browser/ColorManager.ts +++ /dev/null @@ -1,217 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { IColorManager, IColorSet, IColorContrastCache } from 'browser/Types'; -import { ITheme } from 'common/services/Services'; -import { channels, color, css, NULL_COLOR } from 'common/Color'; -import { ColorContrastCache } from 'browser/ColorContrastCache'; -import { ColorIndex, IColor } from 'common/Types'; - - -interface IRestoreColorSet { - foreground: IColor; - background: IColor; - cursor: IColor; - ansi: IColor[]; -} - - -const DEFAULT_FOREGROUND = css.toColor('#ffffff'); -const DEFAULT_BACKGROUND = css.toColor('#000000'); -const DEFAULT_CURSOR = css.toColor('#ffffff'); -const DEFAULT_CURSOR_ACCENT = css.toColor('#000000'); -const DEFAULT_SELECTION = { - css: 'rgba(255, 255, 255, 0.3)', - rgba: 0xFFFFFF4D -}; - -// An IIFE to generate DEFAULT_ANSI_COLORS. -export const DEFAULT_ANSI_COLORS = Object.freeze((() => { - const colors = [ - // dark: - css.toColor('#2e3436'), - css.toColor('#cc0000'), - css.toColor('#4e9a06'), - css.toColor('#c4a000'), - css.toColor('#3465a4'), - css.toColor('#75507b'), - css.toColor('#06989a'), - css.toColor('#d3d7cf'), - // bright: - css.toColor('#555753'), - css.toColor('#ef2929'), - css.toColor('#8ae234'), - css.toColor('#fce94f'), - css.toColor('#729fcf'), - css.toColor('#ad7fa8'), - css.toColor('#34e2e2'), - css.toColor('#eeeeec') - ]; - - // Fill in the remaining 240 ANSI colors. - // Generate colors (16-231) - const v = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]; - for (let i = 0; i < 216; i++) { - const r = v[(i / 36) % 6 | 0]; - const g = v[(i / 6) % 6 | 0]; - const b = v[i % 6]; - colors.push({ - css: channels.toCss(r, g, b), - rgba: channels.toRgba(r, g, b) - }); - } - - // Generate greys (232-255) - for (let i = 0; i < 24; i++) { - const c = 8 + i * 10; - colors.push({ - css: channels.toCss(c, c, c), - rgba: channels.toRgba(c, c, c) - }); - } - - return colors; -})()); - -/** - * Manages the source of truth for a terminal's colors. - */ -export class ColorManager implements IColorManager { - public colors: IColorSet; - - private _contrastCache: IColorContrastCache; - private _restoreColors!: IRestoreColorSet; - - constructor() { - this._contrastCache = new ColorContrastCache(); - this.colors = { - foreground: DEFAULT_FOREGROUND, - background: DEFAULT_BACKGROUND, - cursor: DEFAULT_CURSOR, - cursorAccent: DEFAULT_CURSOR_ACCENT, - selectionForeground: undefined, - selectionBackgroundTransparent: DEFAULT_SELECTION, - selectionBackgroundOpaque: color.blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION), - selectionInactiveBackgroundTransparent: DEFAULT_SELECTION, - selectionInactiveBackgroundOpaque: color.blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION), - ansi: DEFAULT_ANSI_COLORS.slice(), - contrastCache: this._contrastCache - }; - this._updateRestoreColors(); - } - - public handleOptionsChange(key: string, value: any): void { - switch (key) { - case 'minimumContrastRatio': - this._contrastCache.clear(); - break; - } - } - - /** - * Sets the terminal's theme. - * @param theme The theme to use. If a partial theme is provided then default - * colors will be used where colors are not defined. - */ - public setTheme(theme: ITheme = {}): void { - this.colors.foreground = this._parseColor(theme.foreground, DEFAULT_FOREGROUND); - this.colors.background = this._parseColor(theme.background, DEFAULT_BACKGROUND); - this.colors.cursor = this._parseColor(theme.cursor, DEFAULT_CURSOR); - this.colors.cursorAccent = this._parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT); - this.colors.selectionBackgroundTransparent = this._parseColor(theme.selectionBackground, DEFAULT_SELECTION); - this.colors.selectionBackgroundOpaque = color.blend(this.colors.background, this.colors.selectionBackgroundTransparent); - this.colors.selectionInactiveBackgroundTransparent = this._parseColor(theme.selectionInactiveBackground, this.colors.selectionBackgroundTransparent); - this.colors.selectionInactiveBackgroundOpaque = color.blend(this.colors.background, this.colors.selectionInactiveBackgroundTransparent); - this.colors.selectionForeground = theme.selectionForeground ? this._parseColor(theme.selectionForeground, NULL_COLOR) : undefined; - if (this.colors.selectionForeground === NULL_COLOR) { - this.colors.selectionForeground = undefined; - } - - /** - * If selection color is opaque, blend it with background with 0.3 opacity - * Issue #2737 - */ - if (color.isOpaque(this.colors.selectionBackgroundTransparent)) { - const opacity = 0.3; - this.colors.selectionBackgroundTransparent = color.opacity(this.colors.selectionBackgroundTransparent, opacity); - } - if (color.isOpaque(this.colors.selectionInactiveBackgroundTransparent)) { - const opacity = 0.3; - this.colors.selectionInactiveBackgroundTransparent = color.opacity(this.colors.selectionInactiveBackgroundTransparent, opacity); - } - this.colors.ansi = DEFAULT_ANSI_COLORS.slice(); - this.colors.ansi[0] = this._parseColor(theme.black, DEFAULT_ANSI_COLORS[0]); - this.colors.ansi[1] = this._parseColor(theme.red, DEFAULT_ANSI_COLORS[1]); - this.colors.ansi[2] = this._parseColor(theme.green, DEFAULT_ANSI_COLORS[2]); - this.colors.ansi[3] = this._parseColor(theme.yellow, DEFAULT_ANSI_COLORS[3]); - this.colors.ansi[4] = this._parseColor(theme.blue, DEFAULT_ANSI_COLORS[4]); - this.colors.ansi[5] = this._parseColor(theme.magenta, DEFAULT_ANSI_COLORS[5]); - this.colors.ansi[6] = this._parseColor(theme.cyan, DEFAULT_ANSI_COLORS[6]); - this.colors.ansi[7] = this._parseColor(theme.white, DEFAULT_ANSI_COLORS[7]); - this.colors.ansi[8] = this._parseColor(theme.brightBlack, DEFAULT_ANSI_COLORS[8]); - this.colors.ansi[9] = this._parseColor(theme.brightRed, DEFAULT_ANSI_COLORS[9]); - this.colors.ansi[10] = this._parseColor(theme.brightGreen, DEFAULT_ANSI_COLORS[10]); - this.colors.ansi[11] = this._parseColor(theme.brightYellow, DEFAULT_ANSI_COLORS[11]); - this.colors.ansi[12] = this._parseColor(theme.brightBlue, DEFAULT_ANSI_COLORS[12]); - this.colors.ansi[13] = this._parseColor(theme.brightMagenta, DEFAULT_ANSI_COLORS[13]); - this.colors.ansi[14] = this._parseColor(theme.brightCyan, DEFAULT_ANSI_COLORS[14]); - this.colors.ansi[15] = this._parseColor(theme.brightWhite, DEFAULT_ANSI_COLORS[15]); - if (theme.extendedAnsi) { - const colorCount = Math.min(this.colors.ansi.length - 16, theme.extendedAnsi.length); - for (let i = 0; i < colorCount; i++) { - this.colors.ansi[i + 16] = this._parseColor(theme.extendedAnsi[i], DEFAULT_ANSI_COLORS[i + 16]); - } - } - // Clear our the cache - this._contrastCache.clear(); - this._updateRestoreColors(); - } - - public restoreColor(slot?: ColorIndex): void { - // unset slot restores all ansi colors - if (slot === undefined) { - for (let i = 0; i < this._restoreColors.ansi.length; ++i) { - this.colors.ansi[i] = this._restoreColors.ansi[i]; - } - return; - } - switch (slot) { - case ColorIndex.FOREGROUND: - this.colors.foreground = this._restoreColors.foreground; - break; - case ColorIndex.BACKGROUND: - this.colors.background = this._restoreColors.background; - break; - case ColorIndex.CURSOR: - this.colors.cursor = this._restoreColors.cursor; - break; - default: - this.colors.ansi[slot] = this._restoreColors.ansi[slot]; - } - } - - private _updateRestoreColors(): void { - this._restoreColors = { - foreground: this.colors.foreground, - background: this.colors.background, - cursor: this.colors.cursor, - ansi: this.colors.ansi.slice() - }; - } - - private _parseColor( - cssString: string | undefined, - fallback: IColor - ): IColor { - if (cssString !== undefined) { - try { - return css.toColor(cssString); - } catch { - // no-op - } - } - return fallback; - } -} diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index b3a5b317..e0ba8a94 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -39,9 +39,8 @@ import { KeyboardResultType, CoreMouseEventType, CoreMouseButton, CoreMouseActio import { evaluateKeyboardEvent } from 'common/input/Keyboard'; import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter'; import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; -import { ColorManager } from 'browser/ColorManager'; import { RenderService } from 'browser/services/RenderService'; -import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ICoreBrowserService, ICharacterJoinerService } from 'browser/services/Services'; +import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ICoreBrowserService, ICharacterJoinerService, IThemeService } from 'browser/services/Services'; import { CharSizeService } from 'browser/services/CharSizeService'; import { IBuffer } from 'common/buffer/Types'; import { MouseService } from 'browser/services/MouseService'; @@ -57,6 +56,7 @@ import { DecorationService } from 'common/services/DecorationService'; import { IDecorationService } from 'common/services/Services'; import { OscLinkProvider } from 'browser/OscLinkProvider'; import { toDisposable } from 'common/Lifecycle'; +import { ThemeService } from 'browser/services/ThemeService'; // Let it work inside Node.js for automated testing purposes. const document: Document = (typeof window !== 'undefined') ? window.document : null as any; @@ -86,6 +86,7 @@ export class Terminal extends CoreTerminal implements ITerminal { private _coreBrowserService: ICoreBrowserService | undefined; private _mouseService: IMouseService | undefined; private _renderService: IRenderService | undefined; + private _themeService: IThemeService | undefined; private _characterJoinerService: ICharacterJoinerService | undefined; private _selectionService: ISelectionService | undefined; @@ -120,8 +121,6 @@ export class Terminal extends CoreTerminal implements ITerminal { public viewport: IViewport | undefined; private _compositionHelper: ICompositionHelper | undefined; private _accessibilityManager: AccessibilityManager | undefined; - private _colorManager: ColorManager | undefined; - private _theme: ITheme | undefined; private readonly _onCursorMove = this.register(new EventEmitter()); public readonly onCursorMove = this._onCursorMove.event; @@ -199,9 +198,9 @@ export class Terminal extends CoreTerminal implements ITerminal { * while an event from OSC 10|110 | 11|111 | 12|112 always contains a single request. */ private _handleColorEvent(event: IColorEvent): void { - if (!this._colorManager) return; + if (!this._themeService) return; for (const req of event) { - let acc: 'foreground' | 'background' | 'cursor' | 'ansi' | undefined = undefined; + let acc: 'foreground' | 'background' | 'cursor' | 'ansi'; let ident = ''; switch (req.index) { case ColorIndex.FOREGROUND: // OSC 10 | 110 @@ -224,21 +223,23 @@ export class Terminal extends CoreTerminal implements ITerminal { switch (req.type) { case ColorRequestType.REPORT: const channels = color.toColorRGB(acc === 'ansi' - ? this._colorManager.colors.ansi[req.index] - : this._colorManager.colors[acc]); + ? this._themeService.colors.ansi[req.index] + : this._themeService.colors[acc]); this.coreService.triggerDataEvent(`${C0.ESC}]${ident};${toRgbString(channels)}${C1_ESCAPED.ST}`); break; case ColorRequestType.SET: - if (acc === 'ansi') this._colorManager.colors.ansi[req.index] = rgba.toColor(...req.color); - else this._colorManager.colors[acc] = rgba.toColor(...req.color); + if (acc === 'ansi') { + this._themeService.modifyColors(colors => colors.ansi[req.index] = rgba.toColor(...req.color)); + } else { + const narrowedAcc = acc; + this._themeService.modifyColors(colors => colors[narrowedAcc] = rgba.toColor(...req.color)); + } break; case ColorRequestType.RESTORE: - this._colorManager.restoreColor(req.index); + this._themeService.restoreColor(req.index); break; } } - this._renderService?.setColors(this._colorManager.colors); - this.viewport?.handleThemeChange(this._colorManager.colors); } protected _setup(): void { @@ -307,9 +308,6 @@ export class Terminal extends CoreTerminal implements ITerminal { } break; case 'tabStopWidth': this.buffers.setupTabStops(); break; - case 'theme': - this._setTheme(this.optionsService.rawOptions.theme); - break; } } @@ -498,10 +496,8 @@ export class Terminal extends CoreTerminal implements ITerminal { this._charSizeService = this._instantiationService.createInstance(CharSizeService, this._document, this._helperContainer); this._instantiationService.setService(ICharSizeService, this._charSizeService); - this._theme = this.options.theme || this._theme; - this._colorManager = new ColorManager(); - this.register(this.optionsService.onOptionChange(e => this._colorManager!.handleOptionsChange(e, this.optionsService.rawOptions[e]))); - this._colorManager.setTheme(this._theme); + this._themeService = this._instantiationService.createInstance(ThemeService); + this._instantiationService.setService(IThemeService, this._themeService); this._characterJoinerService = this._instantiationService.createInstance(CharacterJoinerService); this._instantiationService.setService(ICharacterJoinerService, this._characterJoinerService); @@ -530,10 +526,8 @@ export class Terminal extends CoreTerminal implements ITerminal { this.viewport = this._instantiationService.createInstance(Viewport, (amount: number) => this.scrollLines(amount, true, ScrollSource.VIEWPORT), this._viewportElement, - this._viewportScrollArea, - this.element + this._viewportScrollArea ); - this.viewport.handleThemeChange(this._colorManager.colors); this.register(this._inputHandler.onRequestSyncScrollBar(() => this.viewport!.syncScrollArea())); this.register(this.viewport); @@ -610,18 +604,7 @@ export class Terminal extends CoreTerminal implements ITerminal { } private _createRenderer(): IRenderer { - return this._instantiationService.createInstance(DomRenderer, this._colorManager!.colors, this.element!, this.screenElement!, this._viewportElement!, this.linkifier2); - } - - /** - * Sets the theme on the renderer. The renderer must have been initialized. - * @param theme The theme to set. - */ - private _setTheme(theme: ITheme): void { - this._theme = theme; - this._colorManager?.setTheme(theme); - this._renderService?.setColors(this._colorManager!.colors); - this.viewport?.handleThemeChange(this._colorManager!.colors); + return this._instantiationService.createInstance(DomRenderer, this.element!, this.screenElement!, this._viewportElement!, this.linkifier2); } /** diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index 282d77a7..97ad90d8 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -5,11 +5,11 @@ import { IDisposable, IMarker, ILinkProvider, IDecorationOptions, IDecoration } from 'xterm'; import { IEvent, EventEmitter } from 'common/EventEmitter'; -import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services'; +import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services'; import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types'; -import { IColorSet, ITerminal, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper, CharacterJoinerHandler, IBufferRange } from 'browser/Types'; +import { IColorSet, ITerminal, ILinkifier2, IBrowser, IViewport, ICompositionHelper, CharacterJoinerHandler, IBufferRange, ReadonlyColorSet } from 'browser/Types'; import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types'; -import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, ITerminalOptions } from 'common/Types'; +import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, ITerminalOptions, ColorIndex } from 'common/Types'; import { Buffer } from 'common/buffer/Buffer'; import * as Browser from 'common/Platform'; import { Terminal } from 'browser/Terminal'; @@ -17,6 +17,7 @@ import { IUnicodeService, IOptionsService, ICoreService, ICoreMouseService } fro import { IFunctionIdentifier, IParams } from 'common/parser/Types'; import { AttributeData } from 'common/buffer/AttributeData'; import { ISelectionRedrawRequestEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types'; +import { css } from 'common/Color'; export class TestTerminal extends Terminal { public get curAttrData(): IAttributeData { return (this as any)._inputHandler._curAttrData; } @@ -265,7 +266,6 @@ export class MockRenderer implements IRenderer { public dispose(): void { throw new Error('Method not implemented.'); } - public colorManager!: IColorManager; public on(type: string, listener: XtermListener): void { throw new Error('Method not implemented.'); } @@ -279,9 +279,6 @@ export class MockRenderer implements IRenderer { throw new Error('Method not implemented.'); } public dimensions!: IRenderDimensions; - public setColors(colors: IColorSet): void { - throw new Error('Method not implemented.'); - } public registerDecoration(decorationOptions: IDecorationOptions): IDecoration { throw new Error('Method not implemented.'); } @@ -407,9 +404,6 @@ export class MockRenderService implements IRenderService { public setRenderer(renderer: IRenderer): void { throw new Error('Method not implemented.'); } - public setColors(colors: IColorSet): void { - throw new Error('Method not implemented.'); - } public handleDevicePixelRatioChange(): void { throw new Error('Method not implemented.'); } @@ -505,3 +499,38 @@ export class MockSelectionService implements ISelectionService { return false; } } + +export class MockThemeService implements IThemeService{ + public serviceBrand: undefined; + public onChangeColors = new EventEmitter().event; + public restoreColor(slot?: ColorIndex | undefined): void { + throw new Error('Method not implemented.'); + } + public modifyColors(callback: (colors: IColorSet) => void): void { + throw new Error('Method not implemented.'); + } + public colors: ReadonlyColorSet = { + background: css.toColor('#010101'), + foreground: css.toColor('#020202'), + ansi: [ + // dark: + css.toColor('#2e3436'), + css.toColor('#cc0000'), + css.toColor('#4e9a06'), + css.toColor('#c4a000'), + css.toColor('#3465a4'), + css.toColor('#75507b'), + css.toColor('#06989a'), + css.toColor('#d3d7cf'), + // bright: + css.toColor('#555753'), + css.toColor('#ef2929'), + css.toColor('#8ae234'), + css.toColor('#fce94f'), + css.toColor('#729fcf'), + css.toColor('#ad7fa8'), + css.toColor('#34e2e2'), + css.toColor('#eeeeec') + ] + } as any; +} diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index 5b475b52..cfd7156f 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -106,11 +106,6 @@ export interface IBrowser { isWindows: boolean; } -export interface IColorManager { - colors: IColorSet; - handleOptionsChange(key: string, value: any): void; -} - export interface IColorSet { foreground: IColor; background: IColor; @@ -126,6 +121,8 @@ export interface IColorSet { contrastCache: IColorContrastCache; } +export type ReadonlyColorSet = Readonly> & { ansi: Readonly['ansi']> }; + export interface IColorContrastCache { clear(): void; setCss(bg: number, fg: number, value: string | null): void; @@ -150,7 +147,6 @@ export interface IViewport extends IDisposable { handleWheel(ev: WheelEvent): boolean; handleTouchStart(ev: TouchEvent): void; handleTouchMove(ev: TouchEvent): boolean; - handleThemeChange(colors: IColorSet): void; } export interface ILinkifierEvent { diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index 395052e6..8f88c559 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -5,8 +5,8 @@ import { Disposable } from 'common/Lifecycle'; import { addDisposableDomListener } from 'browser/Lifecycle'; -import { IColorSet, IViewport } from 'browser/Types'; -import { ICharSizeService, ICoreBrowserService, IRenderService } from 'browser/services/Services'; +import { IColorSet, IViewport, ReadonlyColorSet } from 'browser/Types'; +import { ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services'; import { IBufferService, IOptionsService } from 'common/services/Services'; import { IBuffer } from 'common/buffer/Types'; import { IRenderDimensions } from 'browser/renderer/shared/Types'; @@ -52,12 +52,12 @@ export class Viewport extends Disposable implements IViewport { private readonly _scrollLines: (amount: number) => void, private readonly _viewportElement: HTMLElement, private readonly _scrollArea: HTMLElement, - private readonly _element: HTMLElement, @IBufferService private readonly _bufferService: IBufferService, @IOptionsService private readonly _optionsService: IOptionsService, @ICharSizeService private readonly _charSizeService: ICharSizeService, @IRenderService private readonly _renderService: IRenderService, - @ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService + @ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService, + @IThemeService themeService: IThemeService ) { super(); @@ -73,11 +73,14 @@ export class Viewport extends Disposable implements IViewport { this._renderDimensions = this._renderService.dimensions; this.register(this._renderService.onDimensionsChange(e => this._renderDimensions = e)); + this._handleThemeChange(themeService.colors); + this.register(themeService.onChangeColors(e => this._handleThemeChange(e))); + // Perform this async to ensure the ICharSizeService is ready. setTimeout(() => this.syncScrollArea(), 0); } - public handleThemeChange(colors: IColorSet): void { + private _handleThemeChange(colors: ReadonlyColorSet): void { this._viewportElement.style.backgroundColor = colors.background.css; } diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index a961bbe0..5d2729c0 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -7,8 +7,8 @@ import { IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/rende import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_BLINK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from 'browser/renderer/dom/DomRendererRowFactory'; import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants'; import { Disposable, toDisposable } from 'common/Lifecycle'; -import { IColorSet, ILinkifierEvent, ILinkifier2 } from 'browser/Types'; -import { ICharSizeService, ICoreBrowserService } from 'browser/services/Services'; +import { IColorSet, ILinkifierEvent, ILinkifier2, ReadonlyColorSet } from 'browser/Types'; +import { ICharSizeService, ICoreBrowserService, IThemeService } from 'browser/services/Services'; import { IOptionsService, IBufferService, IInstantiationService } from 'common/services/Services'; import { EventEmitter, IEvent } from 'common/EventEmitter'; import { color } from 'common/Color'; @@ -43,7 +43,6 @@ export class DomRenderer extends Disposable implements IRenderer { public readonly onRequestRedraw = this.register(new EventEmitter()).event; constructor( - private _colors: IColorSet, private readonly _element: HTMLElement, private readonly _screenElement: HTMLElement, private readonly _viewportElement: HTMLElement, @@ -52,7 +51,8 @@ export class DomRenderer extends Disposable implements IRenderer { @ICharSizeService private readonly _charSizeService: ICharSizeService, @IOptionsService private readonly _optionsService: IOptionsService, @IBufferService private readonly _bufferService: IBufferService, - @ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService + @ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService, + @IThemeService themeService: IThemeService ) { super(); this._rowContainer = document.createElement('div'); @@ -79,9 +79,11 @@ export class DomRenderer extends Disposable implements IRenderer { actualCellHeight: 0 }; this._updateDimensions(); - this._injectCss(); - this._rowFactory = instantiationService.createInstance(DomRendererRowFactory, document, this._colors); + this.register(themeService.onChangeColors(e => this._injectCss(e))); + this._injectCss(themeService.colors); + + this._rowFactory = instantiationService.createInstance(DomRendererRowFactory, document); this._element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass); this._screenElement.appendChild(this._rowContainer); @@ -142,12 +144,7 @@ export class DomRenderer extends Disposable implements IRenderer { this._screenElement.style.height = `${this.dimensions.canvasHeight}px`; } - public setColors(colors: IColorSet): void { - this._colors = colors; - this._injectCss(); - } - - private _injectCss(): void { + private _injectCss(colors: ReadonlyColorSet): void { if (!this._themeStyleElement) { this._themeStyleElement = document.createElement('style'); this._screenElement.appendChild(this._themeStyleElement); @@ -156,7 +153,7 @@ export class DomRenderer extends Disposable implements IRenderer { // Base CSS let styles = `${this._terminalSelector} .${ROW_CONTAINER_CLASS} {` + - ` color: ${this._colors.foreground.css};` + + ` color: ${colors.foreground.css};` + ` font-family: ${this._optionsService.rawOptions.fontFamily};` + ` font-size: ${this._optionsService.rawOptions.fontSize}px;` + `}`; @@ -181,18 +178,18 @@ export class DomRenderer extends Disposable implements IRenderer { styles += `@keyframes blink_block` + `_` + this._terminalClass + ` {` + ` 0% {` + - ` background-color: ${this._colors.cursor.css};` + - ` color: ${this._colors.cursorAccent.css};` + + ` background-color: ${colors.cursor.css};` + + ` color: ${colors.cursorAccent.css};` + ` }` + ` 50% {` + - ` background-color: ${this._colors.cursorAccent.css};` + - ` color: ${this._colors.cursor.css};` + + ` background-color: ${colors.cursorAccent.css};` + + ` color: ${colors.cursor.css};` + ` }` + `}`; // Cursor styles += `${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` + - ` outline: 1px solid ${this._colors.cursor.css};` + + ` outline: 1px solid ${colors.cursor.css};` + ` outline-offset: -1px;` + `}` + `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_BLINK_CLASS}:not(.${CURSOR_STYLE_BLOCK_CLASS}) {` + @@ -202,14 +199,14 @@ export class DomRenderer extends Disposable implements IRenderer { ` animation: blink_block` + `_` + this._terminalClass + ` 1s step-end infinite;` + `}` + `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` + - ` background-color: ${this._colors.cursor.css};` + - ` color: ${this._colors.cursorAccent.css};` + + ` background-color: ${colors.cursor.css};` + + ` color: ${colors.cursorAccent.css};` + `}` + `${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BAR_CLASS} {` + - ` box-shadow: ${this._optionsService.rawOptions.cursorWidth}px 0 0 ${this._colors.cursor.css} inset;` + + ` box-shadow: ${this._optionsService.rawOptions.cursorWidth}px 0 0 ${colors.cursor.css} inset;` + `}` + `${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_UNDERLINE_CLASS} {` + - ` box-shadow: 0 -1px 0 ${this._colors.cursor.css} inset;` + + ` box-shadow: 0 -1px 0 ${colors.cursor.css} inset;` + `}`; // Selection styles += @@ -222,21 +219,21 @@ export class DomRenderer extends Disposable implements IRenderer { `}` + `${this._terminalSelector}.focus .${SELECTION_CLASS} div {` + ` position: absolute;` + - ` background-color: ${this._colors.selectionBackgroundOpaque.css};` + + ` background-color: ${colors.selectionBackgroundOpaque.css};` + `}` + `${this._terminalSelector} .${SELECTION_CLASS} div {` + ` position: absolute;` + - ` background-color: ${this._colors.selectionInactiveBackgroundOpaque.css};` + + ` background-color: ${colors.selectionInactiveBackgroundOpaque.css};` + `}`; // Colors - for (const [i, c] of this._colors.ansi.entries()) { + for (const [i, c] of colors.ansi.entries()) { styles += `${this._terminalSelector} .${FG_CLASS_PREFIX}${i} { color: ${c.css}; }` + `${this._terminalSelector} .${BG_CLASS_PREFIX}${i} { background-color: ${c.css}; }`; } styles += - `${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { color: ${color.opaque(this._colors.background).css}; }` + - `${this._terminalSelector} .${BG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { background-color: ${this._colors.foreground.css}; }`; + `${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { color: ${color.opaque(colors.background).css}; }` + + `${this._terminalSelector} .${BG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { background-color: ${colors.foreground.css}; }`; this._themeStyleElement.textContent = styles; } @@ -348,7 +345,6 @@ export class DomRenderer extends Disposable implements IRenderer { public handleOptionsChanged(): void { // Force a refresh this._updateDimensions(); - this._injectCss(); } public clear(): void { diff --git a/src/browser/renderer/dom/DomRendererRowFactory.test.ts b/src/browser/renderer/dom/DomRendererRowFactory.test.ts index b228f3d6..fbb48536 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.test.ts @@ -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, MockCoreBrowserService } from 'browser/TestUtils.test'; +import { MockCharacterJoinerService, MockCoreBrowserService, MockThemeService } from 'browser/TestUtils.test'; describe('DomRendererRowFactory', () => { let dom: jsdom.JSDOM; @@ -23,35 +23,12 @@ describe('DomRendererRowFactory', () => { dom = new jsdom.JSDOM(''); rowFactory = new DomRendererRowFactory( dom.window.document, - { - background: css.toColor('#010101'), - foreground: css.toColor('#020202'), - ansi: [ - // dark: - css.toColor('#2e3436'), - css.toColor('#cc0000'), - css.toColor('#4e9a06'), - css.toColor('#c4a000'), - css.toColor('#3465a4'), - css.toColor('#75507b'), - css.toColor('#06989a'), - css.toColor('#d3d7cf'), - // bright: - css.toColor('#555753'), - css.toColor('#ef2929'), - css.toColor('#8ae234'), - css.toColor('#fce94f'), - css.toColor('#729fcf'), - css.toColor('#ad7fa8'), - css.toColor('#34e2e2'), - css.toColor('#eeeeec') - ] - } as any, new MockCharacterJoinerService(), new MockOptionsService({ drawBoldTextInBrightColors: true }), new MockCoreBrowserService(), new MockCoreService(), - new MockDecorationService() + new MockDecorationService(), + new MockThemeService() ); lineData = createEmptyLineData(2); }); diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index 6c9f1845..14b26c92 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -9,8 +9,8 @@ import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/ import { CellData } from 'common/buffer/CellData'; import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; import { color, rgba } from 'common/Color'; -import { IColorSet } from 'browser/Types'; -import { ICharacterJoinerService, ICoreBrowserService } from 'browser/services/Services'; +import { IColorSet, ReadonlyColorSet } from 'browser/Types'; +import { ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services'; import { JoinedCellData } from 'browser/services/CharacterJoinerService'; import { excludeFromContrastRatioDemands } from 'browser/renderer/shared/RendererUtils'; import { AttributeData } from 'common/buffer/AttributeData'; @@ -35,19 +35,15 @@ export class DomRendererRowFactory { constructor( private readonly _document: Document, - private _colors: IColorSet, @ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService, @IOptionsService private readonly _optionsService: IOptionsService, @ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService, @ICoreService private readonly _coreService: ICoreService, - @IDecorationService private readonly _decorationService: IDecorationService + @IDecorationService private readonly _decorationService: IDecorationService, + @IThemeService private readonly _themeService: IThemeService ) { } - public setColors(colors: IColorSet): void { - this._colors = colors; - } - public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void { this._selectionStart = start; this._selectionEnd = end; @@ -71,6 +67,8 @@ export class DomRendererRowFactory { } } + const colors = this._themeService.colors; + for (let x = 0; x < lineLength; x++) { lineData.loadCell(x, this._workCell); let width = this._workCell.getWidth(); @@ -176,7 +174,7 @@ export class DomRendererRowFactory { if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8) { fg += 8; } - charElement.style.textDecorationColor = this._colors.ansi[fg].css; + charElement.style.textDecorationColor = colors.ansi[fg].css; } } } @@ -224,17 +222,17 @@ export class DomRendererRowFactory { // Apply selection foreground if applicable const isInSelection = this._isCellInSelection(x, row); if (!isTop) { - if (this._colors.selectionForeground && isInSelection) { + if (colors.selectionForeground && isInSelection) { fgColorMode = Attributes.CM_RGB; - fg = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF; - fgOverride = this._colors.selectionForeground; + fg = colors.selectionForeground.rgba >> 8 & 0xFFFFFF; + fgOverride = colors.selectionForeground; } } // If in the selection, force the element to be above the selection to improve contrast and // support opaque selections if (isInSelection) { - bgOverride = this._coreBrowserService.isFocused ? this._colors.selectionBackgroundOpaque : this._colors.selectionInactiveBackgroundOpaque; + bgOverride = this._coreBrowserService.isFocused ? colors.selectionBackgroundOpaque : colors.selectionInactiveBackgroundOpaque; isTop = true; } @@ -248,7 +246,7 @@ export class DomRendererRowFactory { switch (bgColorMode) { case Attributes.CM_P16: case Attributes.CM_P256: - resolvedBg = this._colors.ansi[bg]; + resolvedBg = colors.ansi[bg]; charElement.classList.add(`xterm-bg-${bg}`); break; case Attributes.CM_RGB: @@ -258,10 +256,10 @@ export class DomRendererRowFactory { case Attributes.CM_DEFAULT: default: if (isInverse) { - resolvedBg = this._colors.foreground; + resolvedBg = colors.foreground; charElement.classList.add(`xterm-bg-${INVERTED_DEFAULT_COLOR}`); } else { - resolvedBg = this._colors.background; + resolvedBg = colors.background; } } @@ -279,7 +277,7 @@ export class DomRendererRowFactory { if (cell.isBold() && fg < 8 && this._optionsService.rawOptions.drawBoldTextInBrightColors) { fg += 8; } - if (!this._applyMinimumContrast(charElement, resolvedBg, this._colors.ansi[fg], cell, bgOverride, undefined)) { + if (!this._applyMinimumContrast(charElement, resolvedBg, colors.ansi[fg], cell, bgOverride, undefined)) { charElement.classList.add(`xterm-fg-${fg}`); } break; @@ -295,7 +293,7 @@ export class DomRendererRowFactory { break; case Attributes.CM_DEFAULT: default: - if (!this._applyMinimumContrast(charElement, resolvedBg, this._colors.foreground, cell, bgOverride, undefined)) { + if (!this._applyMinimumContrast(charElement, resolvedBg, colors.foreground, cell, bgOverride, undefined)) { if (isInverse) { charElement.classList.add(`xterm-fg-${INVERTED_DEFAULT_COLOR}`); } @@ -317,13 +315,13 @@ export class DomRendererRowFactory { // Try get from cache first, only use the cache when there are no decoration overrides let adjustedColor: IColor | undefined | null = undefined; if (!bgOverride && !fgOverride) { - adjustedColor = this._colors.contrastCache.getColor(bg.rgba, fg.rgba); + adjustedColor = this._themeService.colors.contrastCache.getColor(bg.rgba, fg.rgba); } // Calculate and store in cache if (adjustedColor === undefined) { adjustedColor = color.ensureContrastRatio(bgOverride || bg, fgOverride || fg, this._optionsService.rawOptions.minimumContrastRatio); - this._colors.contrastCache.setColor((bgOverride || bg).rgba, (fgOverride || fg).rgba, adjustedColor ?? null); + this._themeService.colors.contrastCache.setColor((bgOverride || bg).rgba, (fgOverride || fg).rgba, adjustedColor ?? null); } if (adjustedColor) { diff --git a/src/browser/renderer/shared/CellColorResolver.ts b/src/browser/renderer/shared/CellColorResolver.ts index b29475b9..ae5019e2 100644 --- a/src/browser/renderer/shared/CellColorResolver.ts +++ b/src/browser/renderer/shared/CellColorResolver.ts @@ -1,6 +1,6 @@ import { ISelectionRenderModel } from 'browser/renderer/shared/Types'; -import { ICoreBrowserService } from 'browser/services/Services'; -import { IColorSet } from 'browser/Types'; +import { ICoreBrowserService, IThemeService } from 'browser/services/Services'; +import { IColorSet, ReadonlyColorSet } from 'browser/Types'; import { Attributes, BgFlags, FgFlags } from 'common/buffer/Constants'; import { IDecorationService } from 'common/services/Services'; import { ICellData } from 'common/Types'; @@ -12,6 +12,7 @@ let $bg = 0; let $hasFg = false; let $hasBg = false; let $isSelected = false; +let $colors: ReadonlyColorSet | undefined; export class CellColorResolver { /** @@ -26,17 +27,13 @@ export class CellColorResolver { constructor( private readonly _terminal: Terminal, - private _colors: IColorSet, private readonly _selectionRenderModel: ISelectionRenderModel, private readonly _decorationService: IDecorationService, - private readonly _coreBrowserService: ICoreBrowserService + private readonly _coreBrowserService: ICoreBrowserService, + private readonly _themeService: IThemeService ) { } - public setColors(colors: IColorSet): void { - this._colors = colors; - } - /** * Resolves colors for the cell, putting the result into the shared {@link result}. This resolves * overrides, inverse and selection for the cell which can then be used to feed into the renderer. @@ -54,6 +51,7 @@ export class CellColorResolver { $hasBg = false; $hasFg = false; $isSelected = false; + $colors = this._themeService.colors; // Apply decorations on the bottom layer this._decorationService.forEachDecorationAtCell(x, y, 'bottom', d => { @@ -70,10 +68,10 @@ export class CellColorResolver { // Apply the selection color if needed $isSelected = this._selectionRenderModel.isCellSelected(this._terminal, x, y); if ($isSelected) { - $bg = (this._coreBrowserService.isFocused ? this._colors.selectionBackgroundOpaque : this._colors.selectionInactiveBackgroundOpaque).rgba >> 8 & 0xFFFFFF; + $bg = (this._coreBrowserService.isFocused ? $colors.selectionBackgroundOpaque : $colors.selectionInactiveBackgroundOpaque).rgba >> 8 & 0xFFFFFF; $hasBg = true; - if (this._colors.selectionForeground) { - $fg = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF; + if ($colors.selectionForeground) { + $fg = $colors.selectionForeground.rgba >> 8 & 0xFFFFFF; $hasFg = true; } } @@ -112,7 +110,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)) | ((this._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 & 0xFFFFFF) & 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); } @@ -121,7 +119,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)) | ((this._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 & 0xFFFFFF) & 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); } @@ -129,6 +127,9 @@ export class CellColorResolver { } } + // Release object + $colors = undefined; + // Use the override if it exists this.result.bg = $hasBg ? $bg : this.result.bg; this.result.fg = $hasFg ? $fg : this.result.fg; diff --git a/src/browser/renderer/shared/CharAtlasCache.ts b/src/browser/renderer/shared/CharAtlasCache.ts index ee88c708..e953dc46 100644 --- a/src/browser/renderer/shared/CharAtlasCache.ts +++ b/src/browser/renderer/shared/CharAtlasCache.ts @@ -5,7 +5,7 @@ import { TextureAtlas } from 'browser/renderer/shared/TextureAtlas'; import { Terminal } from 'xterm'; -import { IColorSet, ITerminal } from 'browser/Types'; +import { ITerminal, ReadonlyColorSet } from 'browser/Types'; import { ICharAtlasConfig, ITextureAtlas } from 'browser/renderer/shared/Types'; import { generateConfig, configEquals } from 'browser/renderer/shared/CharAtlasUtils'; @@ -27,7 +27,7 @@ const charAtlasCache: ITextureAtlasCacheEntry[] = []; */ export function acquireTextureAtlas( terminal: Terminal, - colors: IColorSet, + colors: ReadonlyColorSet, scaledCellWidth: number, scaledCellHeight: number, scaledCharWidth: number, diff --git a/src/browser/renderer/shared/CharAtlasUtils.ts b/src/browser/renderer/shared/CharAtlasUtils.ts index 58c207e8..e443168e 100644 --- a/src/browser/renderer/shared/CharAtlasUtils.ts +++ b/src/browser/renderer/shared/CharAtlasUtils.ts @@ -6,10 +6,10 @@ import { ICharAtlasConfig } from './Types'; import { Attributes } from 'common/buffer/Constants'; import { Terminal } from 'xterm'; -import { IColorSet } from 'browser/Types'; +import { IColorSet, ReadonlyColorSet } from 'browser/Types'; import { NULL_COLOR } from 'common/Color'; -export function generateConfig(scaledCellWidth: number, scaledCellHeight: number, scaledCharWidth: number, scaledCharHeight: number, terminal: Terminal, colors: IColorSet, devicePixelRatio: number): ICharAtlasConfig { +export function generateConfig(scaledCellWidth: number, scaledCellHeight: number, scaledCharWidth: number, scaledCharHeight: number, terminal: Terminal, colors: ReadonlyColorSet, devicePixelRatio: number): ICharAtlasConfig { // null out some fields that don't matter const clonedColors: IColorSet = { foreground: colors.foreground, diff --git a/src/browser/renderer/shared/Types.d.ts b/src/browser/renderer/shared/Types.d.ts index 5704f312..1def0f77 100644 --- a/src/browser/renderer/shared/Types.d.ts +++ b/src/browser/renderer/shared/Types.d.ts @@ -4,7 +4,7 @@ */ import { FontWeight, Terminal } from 'xterm'; -import { IColorSet } from 'browser/Types'; +import { IColorSet, ReadonlyColorSet } from 'browser/Types'; import { IDisposable } from 'common/Types'; import { IEvent } from 'common/EventEmitter'; @@ -61,7 +61,6 @@ export interface IRenderer extends IDisposable { readonly onRequestRedraw: IEvent; dispose(): void; - setColors(colors: IColorSet): void; handleDevicePixelRatioChange(): void; handleResize(cols: number, rows: number): void; handleCharSizeChanged(): void; diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index a51fe722..dd0c6b50 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -9,10 +9,11 @@ import { EventEmitter, IEvent } from 'common/EventEmitter'; import { Disposable } from 'common/Lifecycle'; import { ScreenDprMonitor } from 'browser/ScreenDprMonitor'; import { addDisposableDomListener } from 'browser/Lifecycle'; -import { IColorSet, IRenderDebouncerWithCallback } from 'browser/Types'; +import { IColorSet, IRenderDebouncerWithCallback, ReadonlyColorSet } from 'browser/Types'; import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services'; -import { ICharSizeService, ICoreBrowserService, IRenderService } from 'browser/services/Services'; +import { ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services'; import { DebouncedIdleTask } from 'common/TaskQueue'; +import { ThemeService } from 'browser/services/ThemeService'; interface ISelectionState { start: [number, number] | undefined; @@ -58,7 +59,8 @@ export class RenderService extends Disposable implements IRenderService { @ICharSizeService private readonly _charSizeService: ICharSizeService, @IDecorationService decorationService: IDecorationService, @IBufferService bufferService: IBufferService, - @ICoreBrowserService coreBrowserService: ICoreBrowserService + @ICoreBrowserService coreBrowserService: ICoreBrowserService, + @IThemeService themeService: IThemeService ) { super(); @@ -89,6 +91,8 @@ export class RenderService extends Disposable implements IRenderService { // matchMedia query. this.register(addDisposableDomListener(coreBrowserService.window, 'resize', () => this.handleDevicePixelRatioChange())); + this.register(themeService.onChangeColors(() => this._fullRefresh())); + // Detect whether IntersectionObserver is detected and enable renderer pause // and resume based on terminal visibility if so if ('IntersectionObserver' in coreBrowserService.window) { @@ -204,14 +208,6 @@ export class RenderService extends Disposable implements IRenderService { this._fullRefresh(); } - public setColors(colors: IColorSet): void { - if (!this._renderer) { - return; - } - this._renderer.setColors(colors); - this._fullRefresh(); - } - public handleDevicePixelRatioChange(): void { // Force char size measurement as DomMeasureStrategy(getBoundingClientRect) is not stable // when devicePixelRatio changes diff --git a/src/browser/services/Services.ts b/src/browser/services/Services.ts index bbc439e8..76a0f7df 100644 --- a/src/browser/services/Services.ts +++ b/src/browser/services/Services.ts @@ -5,10 +5,11 @@ import { IEvent } from 'common/EventEmitter'; import { IRenderDimensions, IRenderer } from 'browser/renderer/shared/Types'; -import { IColorSet } from 'browser/Types'; +import { IColorSet, ReadonlyColorSet } from 'browser/Types'; import { ISelectionRedrawRequestEvent as ISelectionRequestRedrawEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; -import { IDisposable } from 'common/Types'; +import { ColorIndex, IDisposable } from 'common/Types'; +import { ITheme } from 'common/services/Services'; export const ICharSizeService = createDecorator('CharSizeService'); export interface ICharSizeService { @@ -73,7 +74,6 @@ export interface IRenderService extends IDisposable { resize(cols: number, rows: number): void; hasRenderer(): boolean; setRenderer(renderer: IRenderer): void; - setColors(colors: IColorSet): void; handleDevicePixelRatioChange(): void; handleResize(cols: number, rows: number): void; handleCharSizeChanged(): void; @@ -121,3 +121,19 @@ export interface ICharacterJoinerService { deregister(joinerId: number): boolean; getJoinedCharacters(row: number): [number, number][]; } + +export const IThemeService = createDecorator('ThemeService'); +export interface IThemeService { + serviceBrand: undefined; + + readonly colors: ReadonlyColorSet; + + readonly onChangeColors: IEvent; + + restoreColor(slot?: ColorIndex): void; + /** + * Allows external modifying of colors in the theme, this is used instead of {@link colors} to + * prevent accidental writes. + */ + modifyColors(callback: (colors: IColorSet) => void): void; +} diff --git a/src/browser/services/ThemeService.test.ts b/src/browser/services/ThemeService.test.ts new file mode 100644 index 00000000..f2b2def3 --- /dev/null +++ b/src/browser/services/ThemeService.test.ts @@ -0,0 +1,367 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import jsdom = require('jsdom'); +import { assert } from 'chai'; +import { ThemeService, DEFAULT_ANSI_COLORS } from 'browser/services/ThemeService'; +import { OptionsService } from 'common/services/OptionsService'; + +describe('ThemeService', () => { + let themeService: ThemeService; + let dom: jsdom.JSDOM; + let window: jsdom.DOMWindow; + let optionsService: OptionsService; + + beforeEach(() => { + dom = new jsdom.JSDOM(''); + window = dom.window; + (window as any).HTMLCanvasElement.prototype.getContext = () => ({ + createLinearGradient(): any { + return null; + }, + + fillRect(): void { }, + + getImageData(): any { + return {data: [0, 0, 0, 0xFF]}; + } + }); + optionsService = new OptionsService({}); + themeService = new ThemeService(optionsService); + }); + + describe('constructor', () => { + it('should fill all colors with values', () => { + for (const key of Object.keys(themeService.colors)) { + if (key !== 'ansi' && key !== 'contrastCache' && key !== 'selectionForeground') { + // A #rrggbb or rgba(...) + assert.ok((themeService.colors as any)[key].css.length >= 7); + } + } + assert.equal(themeService.colors.ansi.length, 256); + }); + + it('should fill 240 colors with expected values', () => { + assert.equal(themeService.colors.ansi[16].css, '#000000'); + assert.equal(themeService.colors.ansi[17].css, '#00005f'); + assert.equal(themeService.colors.ansi[18].css, '#000087'); + assert.equal(themeService.colors.ansi[19].css, '#0000af'); + assert.equal(themeService.colors.ansi[20].css, '#0000d7'); + assert.equal(themeService.colors.ansi[21].css, '#0000ff'); + assert.equal(themeService.colors.ansi[22].css, '#005f00'); + assert.equal(themeService.colors.ansi[23].css, '#005f5f'); + assert.equal(themeService.colors.ansi[24].css, '#005f87'); + assert.equal(themeService.colors.ansi[25].css, '#005faf'); + assert.equal(themeService.colors.ansi[26].css, '#005fd7'); + assert.equal(themeService.colors.ansi[27].css, '#005fff'); + assert.equal(themeService.colors.ansi[28].css, '#008700'); + assert.equal(themeService.colors.ansi[29].css, '#00875f'); + assert.equal(themeService.colors.ansi[30].css, '#008787'); + assert.equal(themeService.colors.ansi[31].css, '#0087af'); + assert.equal(themeService.colors.ansi[32].css, '#0087d7'); + assert.equal(themeService.colors.ansi[33].css, '#0087ff'); + assert.equal(themeService.colors.ansi[34].css, '#00af00'); + assert.equal(themeService.colors.ansi[35].css, '#00af5f'); + assert.equal(themeService.colors.ansi[36].css, '#00af87'); + assert.equal(themeService.colors.ansi[37].css, '#00afaf'); + assert.equal(themeService.colors.ansi[38].css, '#00afd7'); + assert.equal(themeService.colors.ansi[39].css, '#00afff'); + assert.equal(themeService.colors.ansi[40].css, '#00d700'); + assert.equal(themeService.colors.ansi[41].css, '#00d75f'); + assert.equal(themeService.colors.ansi[42].css, '#00d787'); + assert.equal(themeService.colors.ansi[43].css, '#00d7af'); + assert.equal(themeService.colors.ansi[44].css, '#00d7d7'); + assert.equal(themeService.colors.ansi[45].css, '#00d7ff'); + assert.equal(themeService.colors.ansi[46].css, '#00ff00'); + assert.equal(themeService.colors.ansi[47].css, '#00ff5f'); + assert.equal(themeService.colors.ansi[48].css, '#00ff87'); + assert.equal(themeService.colors.ansi[49].css, '#00ffaf'); + assert.equal(themeService.colors.ansi[50].css, '#00ffd7'); + assert.equal(themeService.colors.ansi[51].css, '#00ffff'); + assert.equal(themeService.colors.ansi[52].css, '#5f0000'); + assert.equal(themeService.colors.ansi[53].css, '#5f005f'); + assert.equal(themeService.colors.ansi[54].css, '#5f0087'); + assert.equal(themeService.colors.ansi[55].css, '#5f00af'); + assert.equal(themeService.colors.ansi[56].css, '#5f00d7'); + assert.equal(themeService.colors.ansi[57].css, '#5f00ff'); + assert.equal(themeService.colors.ansi[58].css, '#5f5f00'); + assert.equal(themeService.colors.ansi[59].css, '#5f5f5f'); + assert.equal(themeService.colors.ansi[60].css, '#5f5f87'); + assert.equal(themeService.colors.ansi[61].css, '#5f5faf'); + assert.equal(themeService.colors.ansi[62].css, '#5f5fd7'); + assert.equal(themeService.colors.ansi[63].css, '#5f5fff'); + assert.equal(themeService.colors.ansi[64].css, '#5f8700'); + assert.equal(themeService.colors.ansi[65].css, '#5f875f'); + assert.equal(themeService.colors.ansi[66].css, '#5f8787'); + assert.equal(themeService.colors.ansi[67].css, '#5f87af'); + assert.equal(themeService.colors.ansi[68].css, '#5f87d7'); + assert.equal(themeService.colors.ansi[69].css, '#5f87ff'); + assert.equal(themeService.colors.ansi[70].css, '#5faf00'); + assert.equal(themeService.colors.ansi[71].css, '#5faf5f'); + assert.equal(themeService.colors.ansi[72].css, '#5faf87'); + assert.equal(themeService.colors.ansi[73].css, '#5fafaf'); + assert.equal(themeService.colors.ansi[74].css, '#5fafd7'); + assert.equal(themeService.colors.ansi[75].css, '#5fafff'); + assert.equal(themeService.colors.ansi[76].css, '#5fd700'); + assert.equal(themeService.colors.ansi[77].css, '#5fd75f'); + assert.equal(themeService.colors.ansi[78].css, '#5fd787'); + assert.equal(themeService.colors.ansi[79].css, '#5fd7af'); + assert.equal(themeService.colors.ansi[80].css, '#5fd7d7'); + assert.equal(themeService.colors.ansi[81].css, '#5fd7ff'); + assert.equal(themeService.colors.ansi[82].css, '#5fff00'); + assert.equal(themeService.colors.ansi[83].css, '#5fff5f'); + assert.equal(themeService.colors.ansi[84].css, '#5fff87'); + assert.equal(themeService.colors.ansi[85].css, '#5fffaf'); + assert.equal(themeService.colors.ansi[86].css, '#5fffd7'); + assert.equal(themeService.colors.ansi[87].css, '#5fffff'); + assert.equal(themeService.colors.ansi[88].css, '#870000'); + assert.equal(themeService.colors.ansi[89].css, '#87005f'); + assert.equal(themeService.colors.ansi[90].css, '#870087'); + assert.equal(themeService.colors.ansi[91].css, '#8700af'); + assert.equal(themeService.colors.ansi[92].css, '#8700d7'); + assert.equal(themeService.colors.ansi[93].css, '#8700ff'); + assert.equal(themeService.colors.ansi[94].css, '#875f00'); + assert.equal(themeService.colors.ansi[95].css, '#875f5f'); + assert.equal(themeService.colors.ansi[96].css, '#875f87'); + assert.equal(themeService.colors.ansi[97].css, '#875faf'); + assert.equal(themeService.colors.ansi[98].css, '#875fd7'); + assert.equal(themeService.colors.ansi[99].css, '#875fff'); + assert.equal(themeService.colors.ansi[100].css, '#878700'); + assert.equal(themeService.colors.ansi[101].css, '#87875f'); + assert.equal(themeService.colors.ansi[102].css, '#878787'); + assert.equal(themeService.colors.ansi[103].css, '#8787af'); + assert.equal(themeService.colors.ansi[104].css, '#8787d7'); + assert.equal(themeService.colors.ansi[105].css, '#8787ff'); + assert.equal(themeService.colors.ansi[106].css, '#87af00'); + assert.equal(themeService.colors.ansi[107].css, '#87af5f'); + assert.equal(themeService.colors.ansi[108].css, '#87af87'); + assert.equal(themeService.colors.ansi[109].css, '#87afaf'); + assert.equal(themeService.colors.ansi[110].css, '#87afd7'); + assert.equal(themeService.colors.ansi[111].css, '#87afff'); + assert.equal(themeService.colors.ansi[112].css, '#87d700'); + assert.equal(themeService.colors.ansi[113].css, '#87d75f'); + assert.equal(themeService.colors.ansi[114].css, '#87d787'); + assert.equal(themeService.colors.ansi[115].css, '#87d7af'); + assert.equal(themeService.colors.ansi[116].css, '#87d7d7'); + assert.equal(themeService.colors.ansi[117].css, '#87d7ff'); + assert.equal(themeService.colors.ansi[118].css, '#87ff00'); + assert.equal(themeService.colors.ansi[119].css, '#87ff5f'); + assert.equal(themeService.colors.ansi[120].css, '#87ff87'); + assert.equal(themeService.colors.ansi[121].css, '#87ffaf'); + assert.equal(themeService.colors.ansi[122].css, '#87ffd7'); + assert.equal(themeService.colors.ansi[123].css, '#87ffff'); + assert.equal(themeService.colors.ansi[124].css, '#af0000'); + assert.equal(themeService.colors.ansi[125].css, '#af005f'); + assert.equal(themeService.colors.ansi[126].css, '#af0087'); + assert.equal(themeService.colors.ansi[127].css, '#af00af'); + assert.equal(themeService.colors.ansi[128].css, '#af00d7'); + assert.equal(themeService.colors.ansi[129].css, '#af00ff'); + assert.equal(themeService.colors.ansi[130].css, '#af5f00'); + assert.equal(themeService.colors.ansi[131].css, '#af5f5f'); + assert.equal(themeService.colors.ansi[132].css, '#af5f87'); + assert.equal(themeService.colors.ansi[133].css, '#af5faf'); + assert.equal(themeService.colors.ansi[134].css, '#af5fd7'); + assert.equal(themeService.colors.ansi[135].css, '#af5fff'); + assert.equal(themeService.colors.ansi[136].css, '#af8700'); + assert.equal(themeService.colors.ansi[137].css, '#af875f'); + assert.equal(themeService.colors.ansi[138].css, '#af8787'); + assert.equal(themeService.colors.ansi[139].css, '#af87af'); + assert.equal(themeService.colors.ansi[140].css, '#af87d7'); + assert.equal(themeService.colors.ansi[141].css, '#af87ff'); + assert.equal(themeService.colors.ansi[142].css, '#afaf00'); + assert.equal(themeService.colors.ansi[143].css, '#afaf5f'); + assert.equal(themeService.colors.ansi[144].css, '#afaf87'); + assert.equal(themeService.colors.ansi[145].css, '#afafaf'); + assert.equal(themeService.colors.ansi[146].css, '#afafd7'); + assert.equal(themeService.colors.ansi[147].css, '#afafff'); + assert.equal(themeService.colors.ansi[148].css, '#afd700'); + assert.equal(themeService.colors.ansi[149].css, '#afd75f'); + assert.equal(themeService.colors.ansi[150].css, '#afd787'); + assert.equal(themeService.colors.ansi[151].css, '#afd7af'); + assert.equal(themeService.colors.ansi[152].css, '#afd7d7'); + assert.equal(themeService.colors.ansi[153].css, '#afd7ff'); + assert.equal(themeService.colors.ansi[154].css, '#afff00'); + assert.equal(themeService.colors.ansi[155].css, '#afff5f'); + assert.equal(themeService.colors.ansi[156].css, '#afff87'); + assert.equal(themeService.colors.ansi[157].css, '#afffaf'); + assert.equal(themeService.colors.ansi[158].css, '#afffd7'); + assert.equal(themeService.colors.ansi[159].css, '#afffff'); + assert.equal(themeService.colors.ansi[160].css, '#d70000'); + assert.equal(themeService.colors.ansi[161].css, '#d7005f'); + assert.equal(themeService.colors.ansi[162].css, '#d70087'); + assert.equal(themeService.colors.ansi[163].css, '#d700af'); + assert.equal(themeService.colors.ansi[164].css, '#d700d7'); + assert.equal(themeService.colors.ansi[165].css, '#d700ff'); + assert.equal(themeService.colors.ansi[166].css, '#d75f00'); + assert.equal(themeService.colors.ansi[167].css, '#d75f5f'); + assert.equal(themeService.colors.ansi[168].css, '#d75f87'); + assert.equal(themeService.colors.ansi[169].css, '#d75faf'); + assert.equal(themeService.colors.ansi[170].css, '#d75fd7'); + assert.equal(themeService.colors.ansi[171].css, '#d75fff'); + assert.equal(themeService.colors.ansi[172].css, '#d78700'); + assert.equal(themeService.colors.ansi[173].css, '#d7875f'); + assert.equal(themeService.colors.ansi[174].css, '#d78787'); + assert.equal(themeService.colors.ansi[175].css, '#d787af'); + assert.equal(themeService.colors.ansi[176].css, '#d787d7'); + assert.equal(themeService.colors.ansi[177].css, '#d787ff'); + assert.equal(themeService.colors.ansi[178].css, '#d7af00'); + assert.equal(themeService.colors.ansi[179].css, '#d7af5f'); + assert.equal(themeService.colors.ansi[180].css, '#d7af87'); + assert.equal(themeService.colors.ansi[181].css, '#d7afaf'); + assert.equal(themeService.colors.ansi[182].css, '#d7afd7'); + assert.equal(themeService.colors.ansi[183].css, '#d7afff'); + assert.equal(themeService.colors.ansi[184].css, '#d7d700'); + assert.equal(themeService.colors.ansi[185].css, '#d7d75f'); + assert.equal(themeService.colors.ansi[186].css, '#d7d787'); + assert.equal(themeService.colors.ansi[187].css, '#d7d7af'); + assert.equal(themeService.colors.ansi[188].css, '#d7d7d7'); + assert.equal(themeService.colors.ansi[189].css, '#d7d7ff'); + assert.equal(themeService.colors.ansi[190].css, '#d7ff00'); + assert.equal(themeService.colors.ansi[191].css, '#d7ff5f'); + assert.equal(themeService.colors.ansi[192].css, '#d7ff87'); + assert.equal(themeService.colors.ansi[193].css, '#d7ffaf'); + assert.equal(themeService.colors.ansi[194].css, '#d7ffd7'); + assert.equal(themeService.colors.ansi[195].css, '#d7ffff'); + assert.equal(themeService.colors.ansi[196].css, '#ff0000'); + assert.equal(themeService.colors.ansi[197].css, '#ff005f'); + assert.equal(themeService.colors.ansi[198].css, '#ff0087'); + assert.equal(themeService.colors.ansi[199].css, '#ff00af'); + assert.equal(themeService.colors.ansi[200].css, '#ff00d7'); + assert.equal(themeService.colors.ansi[201].css, '#ff00ff'); + assert.equal(themeService.colors.ansi[202].css, '#ff5f00'); + assert.equal(themeService.colors.ansi[203].css, '#ff5f5f'); + assert.equal(themeService.colors.ansi[204].css, '#ff5f87'); + assert.equal(themeService.colors.ansi[205].css, '#ff5faf'); + assert.equal(themeService.colors.ansi[206].css, '#ff5fd7'); + assert.equal(themeService.colors.ansi[207].css, '#ff5fff'); + assert.equal(themeService.colors.ansi[208].css, '#ff8700'); + assert.equal(themeService.colors.ansi[209].css, '#ff875f'); + assert.equal(themeService.colors.ansi[210].css, '#ff8787'); + assert.equal(themeService.colors.ansi[211].css, '#ff87af'); + assert.equal(themeService.colors.ansi[212].css, '#ff87d7'); + assert.equal(themeService.colors.ansi[213].css, '#ff87ff'); + assert.equal(themeService.colors.ansi[214].css, '#ffaf00'); + assert.equal(themeService.colors.ansi[215].css, '#ffaf5f'); + assert.equal(themeService.colors.ansi[216].css, '#ffaf87'); + assert.equal(themeService.colors.ansi[217].css, '#ffafaf'); + assert.equal(themeService.colors.ansi[218].css, '#ffafd7'); + assert.equal(themeService.colors.ansi[219].css, '#ffafff'); + assert.equal(themeService.colors.ansi[220].css, '#ffd700'); + assert.equal(themeService.colors.ansi[221].css, '#ffd75f'); + assert.equal(themeService.colors.ansi[222].css, '#ffd787'); + assert.equal(themeService.colors.ansi[223].css, '#ffd7af'); + assert.equal(themeService.colors.ansi[224].css, '#ffd7d7'); + assert.equal(themeService.colors.ansi[225].css, '#ffd7ff'); + assert.equal(themeService.colors.ansi[226].css, '#ffff00'); + assert.equal(themeService.colors.ansi[227].css, '#ffff5f'); + assert.equal(themeService.colors.ansi[228].css, '#ffff87'); + assert.equal(themeService.colors.ansi[229].css, '#ffffaf'); + assert.equal(themeService.colors.ansi[230].css, '#ffffd7'); + assert.equal(themeService.colors.ansi[231].css, '#ffffff'); + assert.equal(themeService.colors.ansi[232].css, '#080808'); + assert.equal(themeService.colors.ansi[233].css, '#121212'); + assert.equal(themeService.colors.ansi[234].css, '#1c1c1c'); + assert.equal(themeService.colors.ansi[235].css, '#262626'); + assert.equal(themeService.colors.ansi[236].css, '#303030'); + assert.equal(themeService.colors.ansi[237].css, '#3a3a3a'); + assert.equal(themeService.colors.ansi[238].css, '#444444'); + assert.equal(themeService.colors.ansi[239].css, '#4e4e4e'); + assert.equal(themeService.colors.ansi[240].css, '#585858'); + assert.equal(themeService.colors.ansi[241].css, '#626262'); + assert.equal(themeService.colors.ansi[242].css, '#6c6c6c'); + assert.equal(themeService.colors.ansi[243].css, '#767676'); + assert.equal(themeService.colors.ansi[244].css, '#808080'); + assert.equal(themeService.colors.ansi[245].css, '#8a8a8a'); + assert.equal(themeService.colors.ansi[246].css, '#949494'); + assert.equal(themeService.colors.ansi[247].css, '#9e9e9e'); + assert.equal(themeService.colors.ansi[248].css, '#a8a8a8'); + assert.equal(themeService.colors.ansi[249].css, '#b2b2b2'); + assert.equal(themeService.colors.ansi[250].css, '#bcbcbc'); + assert.equal(themeService.colors.ansi[251].css, '#c6c6c6'); + assert.equal(themeService.colors.ansi[252].css, '#d0d0d0'); + assert.equal(themeService.colors.ansi[253].css, '#dadada'); + assert.equal(themeService.colors.ansi[254].css, '#e4e4e4'); + assert.equal(themeService.colors.ansi[255].css, '#eeeeee'); + }); + }); + + describe('setTheme', () => { + it('should not throw when not setting all colors', () => { + assert.doesNotThrow(() => { + optionsService.options.theme = {}; + }); + }); + + it('should set a partial set of colors, using the default if not present', () => { + assert.equal(themeService.colors.background.css, '#000000'); + assert.equal(themeService.colors.foreground.css, '#ffffff'); + optionsService.options.theme = { + background: '#FF0000', + foreground: '#00FF00' + }; + assert.equal(themeService.colors.background.css, '#FF0000'); + assert.equal(themeService.colors.foreground.css, '#00FF00'); + optionsService.options.theme = { + background: '#0000FF' + }; + assert.equal(themeService.colors.background.css, '#0000FF'); + // FG reverts back to default + assert.equal(themeService.colors.foreground.css, '#ffffff'); + }); + + it('should set all extended ansi colors in reverse order', () => { + optionsService.options.theme = { + extendedAnsi: DEFAULT_ANSI_COLORS.map(a => a.css).slice().reverse() + }; + + for (let ansiColor = 16; ansiColor <= 255; ansiColor++) { + assert.equal(themeService.colors.ansi[ansiColor].css, DEFAULT_ANSI_COLORS[255 + 16 - ansiColor].css); + } + }); + + it('should set one extended ansi color and keep the other default', () => { + optionsService.options.theme = { + extendedAnsi: ['#ffffff'] + }; + + assert.equal(themeService.colors.ansi[16].css, '#ffffff'); + assert.equal(themeService.colors.ansi[17].css, DEFAULT_ANSI_COLORS[17].css); + }); + + it('should set extended ansi colors to the default when they are unset', () => { + optionsService.options.theme = { + extendedAnsi: ['#ffffff'] + }; + assert.equal(themeService.colors.ansi[16].css, '#ffffff'); + + optionsService.options.theme = { + extendedAnsi: [] + }; + assert.equal(themeService.colors.ansi[16].css, DEFAULT_ANSI_COLORS[16].css); + + optionsService.options.theme = { + extendedAnsi: ['#ffffff'] + }; + assert.equal(themeService.colors.ansi[16].css, '#ffffff'); + + optionsService.options.theme = {}; + assert.equal(themeService.colors.ansi[16].css, DEFAULT_ANSI_COLORS[16].css); + }); + + it('should set extended ansi colors to the default when they are partially unset', () => { + optionsService.options.theme = { + extendedAnsi: ['#ffffff', '#000000'] + }; + assert.equal(themeService.colors.ansi[16].css, '#ffffff'); + assert.equal(themeService.colors.ansi[17].css, '#000000'); + + optionsService.options.theme = { + extendedAnsi: ['#ffffff'] + }; + assert.equal(themeService.colors.ansi[16].css, '#ffffff'); + assert.equal(themeService.colors.ansi[17].css, DEFAULT_ANSI_COLORS[17].css); + }); + }); +}); diff --git a/src/browser/services/ThemeService.ts b/src/browser/services/ThemeService.ts new file mode 100644 index 00000000..e584a8d1 --- /dev/null +++ b/src/browser/services/ThemeService.ts @@ -0,0 +1,243 @@ +/** + * Copyright (c) 2022 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ColorContrastCache } from 'browser/ColorContrastCache'; +import { IThemeService } from 'browser/services/Services'; +import { IColorContrastCache, IColorSet, ReadonlyColorSet } from 'browser/Types'; +import { channels, color, css, NULL_COLOR } from 'common/Color'; +import { EventEmitter } from 'common/EventEmitter'; +import { Disposable } from 'common/Lifecycle'; +import { IOptionsService, ITheme } from 'common/services/Services'; +import { ColorIndex, IColor } from 'common/Types'; + +interface IRestoreColorSet { + foreground: IColor; + background: IColor; + cursor: IColor; + ansi: IColor[]; +} + + +const DEFAULT_FOREGROUND = css.toColor('#ffffff'); +const DEFAULT_BACKGROUND = css.toColor('#000000'); +const DEFAULT_CURSOR = css.toColor('#ffffff'); +const DEFAULT_CURSOR_ACCENT = css.toColor('#000000'); +const DEFAULT_SELECTION = { + css: 'rgba(255, 255, 255, 0.3)', + rgba: 0xFFFFFF4D +}; + +// An IIFE to generate DEFAULT_ANSI_COLORS. +export const DEFAULT_ANSI_COLORS = Object.freeze((() => { + const colors = [ + // dark: + css.toColor('#2e3436'), + css.toColor('#cc0000'), + css.toColor('#4e9a06'), + css.toColor('#c4a000'), + css.toColor('#3465a4'), + css.toColor('#75507b'), + css.toColor('#06989a'), + css.toColor('#d3d7cf'), + // bright: + css.toColor('#555753'), + css.toColor('#ef2929'), + css.toColor('#8ae234'), + css.toColor('#fce94f'), + css.toColor('#729fcf'), + css.toColor('#ad7fa8'), + css.toColor('#34e2e2'), + css.toColor('#eeeeec') + ]; + + // Fill in the remaining 240 ANSI colors. + // Generate colors (16-231) + const v = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]; + for (let i = 0; i < 216; i++) { + const r = v[(i / 36) % 6 | 0]; + const g = v[(i / 6) % 6 | 0]; + const b = v[i % 6]; + colors.push({ + css: channels.toCss(r, g, b), + rgba: channels.toRgba(r, g, b) + }); + } + + // Generate greys (232-255) + for (let i = 0; i < 24; i++) { + const c = 8 + i * 10; + colors.push({ + css: channels.toCss(c, c, c), + rgba: channels.toRgba(c, c, c) + }); + } + + return colors; +})()); + +export class ThemeService extends Disposable implements IThemeService { + public serviceBrand: undefined; + + private _colors: IColorSet; + private _contrastCache: IColorContrastCache; + private _restoreColors!: IRestoreColorSet; + + public get colors(): ReadonlyColorSet { return this._colors; } + + private readonly _onChangeColors = this.register(new EventEmitter()); + public readonly onChangeColors = this._onChangeColors.event; + + constructor( + @IOptionsService private readonly _optionsService: IOptionsService + ) { + super(); + + this._contrastCache = new ColorContrastCache(); + this._colors = { + foreground: DEFAULT_FOREGROUND, + background: DEFAULT_BACKGROUND, + cursor: DEFAULT_CURSOR, + cursorAccent: DEFAULT_CURSOR_ACCENT, + selectionForeground: undefined, + selectionBackgroundTransparent: DEFAULT_SELECTION, + selectionBackgroundOpaque: color.blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION), + selectionInactiveBackgroundTransparent: DEFAULT_SELECTION, + selectionInactiveBackgroundOpaque: color.blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION), + ansi: DEFAULT_ANSI_COLORS.slice(), + contrastCache: this._contrastCache + }; + this._updateRestoreColors(); + this._setTheme(this._optionsService.rawOptions.theme); + + this.register(this._optionsService.onOptionChange(key => { + switch (key) { + case 'minimumContrastRatio': + this._contrastCache.clear(); + break; + case 'theme': + this._setTheme(this._optionsService.rawOptions.theme); + break; + } + })); + } + + /** + * Sets the terminal's theme. + * @param theme The theme to use. If a partial theme is provided then default + * colors will be used where colors are not defined. + */ + private _setTheme(theme: ITheme = {}): void { + const colors = this._colors; + colors.foreground = parseColor(theme.foreground, DEFAULT_FOREGROUND); + colors.background = parseColor(theme.background, DEFAULT_BACKGROUND); + colors.cursor = parseColor(theme.cursor, DEFAULT_CURSOR); + colors.cursorAccent = parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT); + colors.selectionBackgroundTransparent = parseColor(theme.selectionBackground, DEFAULT_SELECTION); + colors.selectionBackgroundOpaque = color.blend(colors.background, colors.selectionBackgroundTransparent); + colors.selectionInactiveBackgroundTransparent = parseColor(theme.selectionInactiveBackground, colors.selectionBackgroundTransparent); + colors.selectionInactiveBackgroundOpaque = color.blend(colors.background, colors.selectionInactiveBackgroundTransparent); + colors.selectionForeground = theme.selectionForeground ? parseColor(theme.selectionForeground, NULL_COLOR) : undefined; + if (colors.selectionForeground === NULL_COLOR) { + colors.selectionForeground = undefined; + } + + /** + * If selection color is opaque, blend it with background with 0.3 opacity + * Issue #2737 + */ + if (color.isOpaque(colors.selectionBackgroundTransparent)) { + const opacity = 0.3; + colors.selectionBackgroundTransparent = color.opacity(colors.selectionBackgroundTransparent, opacity); + } + if (color.isOpaque(colors.selectionInactiveBackgroundTransparent)) { + const opacity = 0.3; + colors.selectionInactiveBackgroundTransparent = color.opacity(colors.selectionInactiveBackgroundTransparent, opacity); + } + colors.ansi = DEFAULT_ANSI_COLORS.slice(); + colors.ansi[0] = parseColor(theme.black, DEFAULT_ANSI_COLORS[0]); + colors.ansi[1] = parseColor(theme.red, DEFAULT_ANSI_COLORS[1]); + colors.ansi[2] = parseColor(theme.green, DEFAULT_ANSI_COLORS[2]); + colors.ansi[3] = parseColor(theme.yellow, DEFAULT_ANSI_COLORS[3]); + colors.ansi[4] = parseColor(theme.blue, DEFAULT_ANSI_COLORS[4]); + colors.ansi[5] = parseColor(theme.magenta, DEFAULT_ANSI_COLORS[5]); + colors.ansi[6] = parseColor(theme.cyan, DEFAULT_ANSI_COLORS[6]); + colors.ansi[7] = parseColor(theme.white, DEFAULT_ANSI_COLORS[7]); + colors.ansi[8] = parseColor(theme.brightBlack, DEFAULT_ANSI_COLORS[8]); + colors.ansi[9] = parseColor(theme.brightRed, DEFAULT_ANSI_COLORS[9]); + colors.ansi[10] = parseColor(theme.brightGreen, DEFAULT_ANSI_COLORS[10]); + colors.ansi[11] = parseColor(theme.brightYellow, DEFAULT_ANSI_COLORS[11]); + colors.ansi[12] = parseColor(theme.brightBlue, DEFAULT_ANSI_COLORS[12]); + colors.ansi[13] = parseColor(theme.brightMagenta, DEFAULT_ANSI_COLORS[13]); + colors.ansi[14] = parseColor(theme.brightCyan, DEFAULT_ANSI_COLORS[14]); + colors.ansi[15] = parseColor(theme.brightWhite, DEFAULT_ANSI_COLORS[15]); + if (theme.extendedAnsi) { + const colorCount = Math.min(colors.ansi.length - 16, theme.extendedAnsi.length); + for (let i = 0; i < colorCount; i++) { + colors.ansi[i + 16] = parseColor(theme.extendedAnsi[i], DEFAULT_ANSI_COLORS[i + 16]); + } + } + // Clear our the cache + this._contrastCache.clear(); + this._updateRestoreColors(); + this._onChangeColors.fire(this.colors); + } + + public restoreColor(slot?: ColorIndex): void { + this._restoreColor(slot); + this._onChangeColors.fire(this.colors); + } + + private _restoreColor(slot: ColorIndex | undefined): void { + // unset slot restores all ansi colors + if (slot === undefined) { + for (let i = 0; i < this._restoreColors.ansi.length; ++i) { + this._colors.ansi[i] = this._restoreColors.ansi[i]; + } + return; + } + switch (slot) { + case ColorIndex.FOREGROUND: + this._colors.foreground = this._restoreColors.foreground; + break; + case ColorIndex.BACKGROUND: + this._colors.background = this._restoreColors.background; + break; + case ColorIndex.CURSOR: + this._colors.cursor = this._restoreColors.cursor; + break; + default: + this._colors.ansi[slot] = this._restoreColors.ansi[slot]; + } + } + + public modifyColors(callback: (colors: IColorSet) => void): void { + callback(this._colors); + // Assume the change happened + this._onChangeColors.fire(this.colors); + } + + private _updateRestoreColors(): void { + this._restoreColors = { + foreground: this._colors.foreground, + background: this._colors.background, + cursor: this._colors.cursor, + ansi: this._colors.ansi.slice() + }; + } +} + +function parseColor( + cssString: string | undefined, + fallback: IColor +): IColor { + if (cssString !== undefined) { + try { + return css.toColor(cssString); + } catch { + // no-op + } + } + return fallback; +}