diff --git a/src/Buffer.ts b/src/Buffer.ts index 12b2137d6c..5743fb897a 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -2,8 +2,13 @@ * @license MIT */ -import { ITerminal } from './Interfaces'; +import { ITerminal, IBuffer } from './Interfaces'; import { CircularList } from './utils/CircularList'; +import { LineData } from './Types'; +import { TextStyle } from './TextStyle'; + +export const CHAR_DATA_CHAR_INDEX = 0; +export const CHAR_DATA_WIDTH_INDEX = 1; /** * This class represents a terminal buffer (an internal state of the terminal), where the @@ -12,8 +17,13 @@ import { CircularList } from './utils/CircularList'; * - cursor position * - scroll position */ -export class Buffer { - public lines: CircularList<[number, string, number][]>; +export class Buffer implements IBuffer { + public lines: CircularList; + public textStyles: TextStyle[]; + + private _currentTextStyle: TextStyle; + // linesIndexOffset usage should be encapsulated + private _linesIndexOffset: number; public savedY: number; public savedX: number; @@ -36,7 +46,90 @@ export class Buffer { public scrollTop: number = 0, public tabs: any = {}, ) { - this.lines = new CircularList<[number, string, number][]>(this.terminal.scrollback); + this.lines = new CircularList(this.terminal.scrollback); + this.textStyles = []; + this._linesIndexOffset = 0; this.scrollBottom = this.terminal.rows - 1; + + // TODO: Listen to line's trim and adjust char attributes + this.lines.on('trim', (amount: number) => this._onTrim(amount)); + } + + /** + * Starts a new character attributes at the cursor. + */ + public startTextStyle(flags: number, fgColor: number, bgColor: number): void { + // TODO: Move current* variables into the buffer? + this._currentTextStyle = new TextStyle(this.x, this.ybase + this.y + this._linesIndexOffset, null, null, [flags, fgColor, bgColor]); + this.textStyles.push(this._currentTextStyle); + } + + /** + * Finishes the current character attributes at the cursor. Do nothing if + * there is not a current character attributes. + */ + public finishTextStyle(): void { + if (!this._currentTextStyle) { + return; + } + this._currentTextStyle.x2 = this.x; + this._currentTextStyle.y2 = this._linesIndexOffset + this.ybase + this.y; + this._currentTextStyle = null; + } + + private _onTrim(amount: number): void { + // Trim the top of charAttributes to ensure it never points at trimmed rows + this._linesIndexOffset += amount; + while (this.textStyles.length > 0 && this.textStyles[0].y1 < this._linesIndexOffset) { + this.textStyles.shift(); + } + } + + /** + * Translates a buffer line to a string, with optional start and end columns. + * Wide characters will count as two columns in the resulting string. This + * function is useful for getting the actual text underneath the raw selection + * position. + * @param line The line being translated. + * @param trimRight Whether to trim whitespace to the right. + * @param startCol The column to start at. + * @param endCol The column to end at. + */ + public translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol: number = 0, endCol: number = null): string { + // Get full line + let lineString = ''; + let widthAdjustedStartCol = startCol; + let widthAdjustedEndCol = endCol; + const line = this.lines.get(lineIndex); + for (let i = 0; i < line.length; i++) { + const char = line[i]; + lineString += char[CHAR_DATA_CHAR_INDEX]; + // Adjust start and end cols for wide characters if they affect their + // column indexes + if (char[CHAR_DATA_WIDTH_INDEX] === 0) { + if (startCol >= i) { + widthAdjustedStartCol--; + } + if (endCol >= i) { + widthAdjustedEndCol--; + } + } + } + + // Calculate the final end col by trimming whitespace on the right of the + // line if needed. + let finalEndCol = widthAdjustedEndCol || line.length; + if (trimRight) { + const rightWhitespaceIndex = lineString.search(/\s+$/); + if (rightWhitespaceIndex !== -1) { + finalEndCol = Math.min(finalEndCol, rightWhitespaceIndex); + } + // Return the empty string if only trimmed whitespace is selected + if (finalEndCol <= widthAdjustedStartCol) { + return ''; + } + } + + return lineString.substring(widthAdjustedStartCol, finalEndCol); } } diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 64da3b3f15..b9995d7231 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -5,6 +5,8 @@ import { IInputHandler, ITerminal } from './Interfaces'; import { C0 } from './EscapeSequences'; import { DEFAULT_CHARSET } from './Charsets'; +import { TextStyle } from './TextStyle'; +import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer'; /** * The terminal's standard implementation of IInputHandler, this handles all @@ -34,14 +36,14 @@ export class InputHandler implements IInputHandler { if (!ch_width && this._terminal.buffer.x) { // dont overflow left if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1]) { - if (!this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][2]) { + if (!this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) { // found empty cell after fullwidth, need to go 2 cells back if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2]) - this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2][1] += char; + this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char; } else { - this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][1] += char; + this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char; } this._terminal.updateRange(this._terminal.buffer.y); } @@ -77,24 +79,24 @@ export class InputHandler implements IInputHandler { // remove last cell, if it's width is 0 // we have to adjust the second last cell as well const removed = this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).pop(); - if (removed[2] === 0 + if (removed[CHAR_DATA_WIDTH_INDEX] === 0 && this._terminal.buffer.lines.get(row)[this._terminal.cols - 2] - && this._terminal.buffer.lines.get(row)[this._terminal.cols - 2][2] === 2) { - this._terminal.buffer.lines.get(row)[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1]; + && this._terminal.buffer.lines.get(row)[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) { + this._terminal.buffer.lines.get(row)[this._terminal.cols - 2] = [' ', 1, this._terminal.currentFlags, this._terminal.currentFgColor, this._terminal.currentBgColor]; } // insert empty cell at cursor - this._terminal.buffer.lines.get(row).splice(this._terminal.buffer.x, 0, [this._terminal.curAttr, ' ', 1]); + this._terminal.buffer.lines.get(row).splice(this._terminal.buffer.x, 0, [' ', 1, this._terminal.currentFlags, this._terminal.currentFgColor, this._terminal.currentBgColor]); } } - this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, char, ch_width]; + this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [char, ch_width, this._terminal.currentFlags, this._terminal.currentFgColor, this._terminal.currentBgColor]; this._terminal.buffer.x++; this._terminal.updateRange(this._terminal.buffer.y); // fullwidth char - set next cell width to zero and advance cursor if (ch_width === 2) { - this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, '', 0]; + this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = ['', 0, this._terminal.currentFlags, this._terminal.currentFgColor, this._terminal.currentBgColor]; this._terminal.buffer.x++; } } @@ -196,7 +198,7 @@ export class InputHandler implements IInputHandler { row = this._terminal.buffer.y + this._terminal.buffer.ybase; j = this._terminal.buffer.x; - ch = [this._terminal.eraseAttr(), ' ', 1]; // xterm + ch = [' ', 1, this._terminal.defaultFlags, this._terminal.defaultFgColor, this._terminal.currentBgColor]; // xterm while (param-- && j < this._terminal.cols) { this._terminal.buffer.lines.get(row).splice(j++, 0, ch); @@ -517,7 +519,7 @@ export class InputHandler implements IInputHandler { } row = this._terminal.buffer.y + this._terminal.buffer.ybase; - ch = [this._terminal.eraseAttr(), ' ', 1]; // xterm + ch = [' ', 1, this._terminal.defaultFlags, this._terminal.defaultFgColor, this._terminal.currentBgColor]; // xterm while (param--) { this._terminal.buffer.lines.get(row).splice(this._terminal.buffer.x, 1); @@ -567,7 +569,7 @@ export class InputHandler implements IInputHandler { row = this._terminal.buffer.y + this._terminal.buffer.ybase; j = this._terminal.buffer.x; - ch = [this._terminal.eraseAttr(), ' ', 1]; // xterm + ch = [' ', 1, this._terminal.defaultFlags, this._terminal.defaultFgColor, this._terminal.currentBgColor]; // xterm while (param-- && j < this._terminal.cols) { this._terminal.buffer.lines.get(row)[j++] = ch; @@ -954,6 +956,8 @@ export class InputHandler implements IInputHandler { case 47: // alt screen buffer case 1047: // alt screen buffer this._terminal.buffers.activateAltBuffer(); + // TODO: Discard current charattribute? + this._terminal.selectionManager.setBuffer(this._terminal.buffer); this._terminal.reset(); this._terminal.viewport.syncScrollArea(); this._terminal.showCursor(); @@ -1123,7 +1127,7 @@ export class InputHandler implements IInputHandler { // if (params[0] === 1049) { // this.restoreCursor(params); // } - this._terminal.selectionManager.setBuffer(this._terminal.buffer.lines); + this._terminal.selectionManager.setBuffer(this._terminal.buffer); this._terminal.refresh(0, this._terminal.rows - 1); this._terminal.viewport.syncScrollArea(); this._terminal.showCursor(); @@ -1199,118 +1203,118 @@ export class InputHandler implements IInputHandler { public charAttributes(params: number[]): void { // Optimize a single SGR0. if (params.length === 1 && params[0] === 0) { - this._terminal.curAttr = this._terminal.defAttr; + (this._terminal).buffer.finishTextStyle(); + console.log('Current char attr list:', this._terminal.buffer.charAttributes); + this._terminal.currentFlags = this._terminal.defaultFlags; + this._terminal.currentFgColor = this._terminal.defaultFgColor; + this._terminal.currentBgColor = this._terminal.defaultBgColor; return; } - let l = params.length - , i = 0 - , flags = this._terminal.curAttr >> 18 - , fg = (this._terminal.curAttr >> 9) & 0x1ff - , bg = this._terminal.curAttr & 0x1ff - , p; + let l = params.length; + let p: number; - for (; i < l; i++) { + for (let i = 0; i < l; i++) { p = params[i]; if (p >= 30 && p <= 37) { // fg color 8 - fg = p - 30; + this._terminal.currentFgColor = p - 30; } else if (p >= 40 && p <= 47) { // bg color 8 - bg = p - 40; + this._terminal.currentBgColor = p - 40; } else if (p >= 90 && p <= 97) { // fg color 16 p += 8; - fg = p - 90; + this._terminal.currentFgColor = p - 90; } else if (p >= 100 && p <= 107) { // bg color 16 p += 8; - bg = p - 100; + this._terminal.currentBgColor = p - 100; } else if (p === 0) { // default - flags = this._terminal.defAttr >> 18; - fg = (this._terminal.defAttr >> 9) & 0x1ff; - bg = this._terminal.defAttr & 0x1ff; - // flags = 0; - // fg = 0x1ff; - // bg = 0x1ff; + this._terminal.currentFlags = this._terminal.defaultFlags; + this._terminal.currentFgColor = this._terminal.defaultFgColor; + this._terminal.currentBgColor = this._terminal.defaultBgColor; } else if (p === 1) { // bold text - flags |= 1; + this._terminal.currentFlags |= 1; } else if (p === 4) { // underlined text - flags |= 2; + this._terminal.currentFlags |= 2; } else if (p === 5) { // blink - flags |= 4; + this._terminal.currentFlags |= 4; } else if (p === 7) { // inverse and positive // test with: echo -e '\e[31m\e[42mhello\e[7mworld\e[27mhi\e[m' - flags |= 8; + this._terminal.currentFlags |= 8; } else if (p === 8) { // invisible - flags |= 16; + this._terminal.currentFlags |= 16; } else if (p === 22) { // not bold - flags &= ~1; + this._terminal.currentFlags &= ~1; } else if (p === 24) { // not underlined - flags &= ~2; + this._terminal.currentFlags &= ~2; } else if (p === 25) { // not blink - flags &= ~4; + this._terminal.currentFlags &= ~4; } else if (p === 27) { // not inverse - flags &= ~8; + this._terminal.currentFlags &= ~8; } else if (p === 28) { // not invisible - flags &= ~16; + this._terminal.currentFlags &= ~16; } else if (p === 39) { // reset fg - fg = (this._terminal.defAttr >> 9) & 0x1ff; + this._terminal.currentFgColor = this._terminal.defaultFgColor; } else if (p === 49) { // reset bg - bg = this._terminal.defAttr & 0x1ff; + this._terminal.currentBgColor = this._terminal.defaultBgColor; } else if (p === 38) { - // fg color 256 if (params[i + 1] === 2) { + // fg color 16mil + this._terminal.currentFlags |= 32; i += 2; - fg = this._terminal.matchColor( - params[i] & 0xff, - params[i + 1] & 0xff, - params[i + 2] & 0xff); - if (fg === -1) fg = 0x1ff; + this._terminal.currentFgColor = ((params[i] & 0xff) << 16) | ((params[i + 1]) & 0xff << 8) | ((params[i + 2]) & 0xff); + if (this._terminal.currentFgColor === -1) { + this._terminal.currentFgColor = this._terminal.defaultFgColor; + } i += 2; } else if (params[i + 1] === 5) { + // fg color 256 i += 2; p = params[i] & 0xff; - fg = p; + this._terminal.currentFgColor = p; } } else if (p === 48) { - // bg color 256 if (params[i + 1] === 2) { + // bg color 16mil + this._terminal.currentFlags |= 64; i += 2; - bg = this._terminal.matchColor( - params[i] & 0xff, - params[i + 1] & 0xff, - params[i + 2] & 0xff); - if (bg === -1) bg = 0x1ff; + this._terminal.currentBgColor = ((params[i] & 0xff) << 16) | ((params[i + 1] & 0xff) << 8) | (params[i + 2] & 0xff); + if (this._terminal.currentBgColor === -1) { + this._terminal.currentBgColor = this._terminal.defaultBgColor; + } i += 2; } else if (params[i + 1] === 5) { + // bg color 256 i += 2; p = params[i] & 0xff; - bg = p; + this._terminal.currentBgColor = p; } } else if (p === 100) { // reset fg/bg - fg = (this._terminal.defAttr >> 9) & 0x1ff; - bg = this._terminal.defAttr & 0x1ff; + this._terminal.currentFgColor = this._terminal.defaultFgColor; + this._terminal.currentBgColor = this._terminal.defaultBgColor; } else { this._terminal.error('Unknown SGR attribute: %d.', p); } } - this._terminal.curAttr = (flags << 18) | (fg << 9) | bg; + (this._terminal).buffer.finishTextStyle(); + (this._terminal).buffer.startTextStyle(this._terminal.currentFlags, this._terminal.currentFgColor, this._terminal.currentBgColor); } /** @@ -1398,7 +1402,9 @@ export class InputHandler implements IInputHandler { this._terminal.applicationCursor = false; this._terminal.buffer.scrollTop = 0; this._terminal.buffer.scrollBottom = this._terminal.rows - 1; - this._terminal.curAttr = this._terminal.defAttr; + this._terminal.currentFlags = this._terminal.defaultFlags; + this._terminal.currentFgColor = this._terminal.defaultFgColor; + this._terminal.currentBgColor = this._terminal.defaultBgColor; this._terminal.buffer.x = this._terminal.buffer.y = 0; // ? this._terminal.charset = null; this._terminal.glevel = 0; // ?? diff --git a/src/Interfaces.ts b/src/Interfaces.ts index f19a7f28f8..c218d65f84 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -3,7 +3,8 @@ */ import { LinkMatcherOptions } from './Interfaces'; -import { LinkMatcherHandler, LinkMatcherValidationCallback } from './Types'; +import { CharData, LinkMatcherHandler, LinkMatcherValidationCallback, LineData } from './Types'; +import { TextStyle } from "./TextStyle"; export interface IBrowser { isNode: boolean; @@ -36,6 +37,10 @@ export interface ITerminal { buffers: IBufferSet; buffer: IBuffer; + defaultFlags: number; + defaultFgColor: number; + defaultBgColor: number; + /** * Emit the 'data' event and populate the given data. * @param data The data to populate in the event. @@ -51,12 +56,17 @@ export interface ITerminal { } export interface IBuffer { - lines: ICircularList<[number, string, number][]>; + lines: ICircularList; + textStyles: TextStyle[]; ydisp: number; ybase: number; y: number; x: number; tabs: any; + + startTextStyle(flags: number, fgColor: number, bgColor: number): void; + finishTextStyle(): void; + translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string; } export interface IBufferSet { @@ -92,8 +102,8 @@ export interface ILinkifier { export interface ICircularList extends IEventEmitter { length: number; maxLength: number; + forEach: (callbackfn: (value: T, index: number) => void) => void; - forEach(callbackfn: (value: T, index: number, array: T[]) => void): void; get(index: number): T; set(index: number, value: T): void; push(value: T): void; diff --git a/src/Renderer.ts b/src/Renderer.ts index 165594fd25..61cde7d9a7 100644 --- a/src/Renderer.ts +++ b/src/Renderer.ts @@ -4,6 +4,8 @@ import { ITerminal } from './Interfaces'; import { DomElementObjectPool } from './utils/DomElementObjectPool'; +import { TextStyle } from './TextStyle'; +import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from './Buffer'; /** * The maximum number of refresh frames to skip when the write buffer is non- @@ -20,7 +22,9 @@ enum FLAGS { UNDERLINE = 2, BLINK = 4, INVERSE = 8, - INVISIBLE = 16 + INVISIBLE = 16, + TRUECOLOR_FG = 32, + TRUECOLOR_BG = 64 }; let brokenBold: boolean = null; @@ -101,28 +105,12 @@ export class Renderer { /** * Refreshes (re-renders) terminal content within two rows (inclusive) - * - * Rendering Engine: - * - * In the screen buffer, each character is stored as a an array with a character - * and a 32-bit integer: - * - First value: a utf-16 character. - * - Second value: - * - Next 9 bits: background color (0-511). - * - Next 9 bits: foreground color (0-511). - * - Next 14 bits: a mask for misc. flags: - * - 1=bold - * - 2=underline - * - 4=blink - * - 8=inverse - * - 16=invisible - * * @param {number} start The row to start from (between 0 and terminal's height terminal - 1) * @param {number} end The row to end at (between fromRow and terminal's height terminal - 1) */ private _refresh(start: number, end: number): void { // If this is a big refresh, remove the terminal rows from the DOM for faster calculations - let parent; + let parent: Node; if (end - start >= this._terminal.rows / 2) { parent = this._terminal.element.parentNode; if (parent) { @@ -131,32 +119,43 @@ export class Renderer { } let width = this._terminal.cols; - let y = start; if (end >= this._terminal.rows) { this._terminal.log('`end` is too large. Most likely a bad CSR.'); end = this._terminal.rows - 1; } - for (; y <= end; y++) { - let row = y + this._terminal.buffer.ydisp; + let nextTextStyleIndex: number = -1; + let currentTextStyle: TextStyle; + for (let i = 0; i < (this._terminal.buffer).textStyles.length; i++) { + const textStyle = (this._terminal.buffer).textStyles[i]; + if (textStyle.y1 - (this._terminal.buffer)._linesIndexOffset === start + this._terminal.buffer.ydisp || textStyle.y2 - (this._terminal.buffer)._linesIndexOffset >= start + this._terminal.buffer.ydisp) { + nextTextStyleIndex = i; + console.log(`initial char attributes index:`, nextTextStyleIndex); + break; + } + } + for (let y = start; y <= end; y++) { + let row = y + this._terminal.buffer.ydisp; let line = this._terminal.buffer.lines.get(row); - let x; + let cursorIndex: number; if (this._terminal.buffer.y === y - (this._terminal.buffer.ybase - this._terminal.buffer.ydisp) && - this._terminal.cursorState && - !this._terminal.cursorHidden) { - x = this._terminal.buffer.x; + this._terminal.cursorState && !this._terminal.cursorHidden) { + cursorIndex = this._terminal.buffer.x; } else { - x = -1; + cursorIndex = -1; } - let attr = this._terminal.defAttr; + let lastFlags = this._terminal.defaultFlags; + let lastFgColor = this._terminal.defaultFgColor; + let lastBgColor = this._terminal.defaultBgColor; + let lastTextStyle: TextStyle = null; const documentFragment = document.createDocumentFragment(); let innerHTML = ''; - let currentElement; + let currentElement: HTMLElement; // Return the row's spans to the pool while (this._terminal.children[y].children.length) { @@ -165,18 +164,50 @@ export class Renderer { this._spanElementObjectPool.release(child); } + // Process each character in the line for (let i = 0; i < width; i++) { - // TODO: Could data be a more specific type? - let data: any = line[i][0]; - const ch = line[i][1]; - const ch_width: any = line[i][2]; - const isCursor: boolean = i === x; + const ch: string = line[i][CHAR_DATA_CHAR_INDEX]; + const ch_width: number = line[i][CHAR_DATA_WIDTH_INDEX]; + let flags: number; + let fg: number; + let bg: number; + if (!ch_width) { continue; } - if (data !== attr || isCursor) { - if (attr !== this._terminal.defAttr && !isCursor) { + if (currentTextStyle && currentTextStyle.x2 === i && currentTextStyle.y2 - (this._terminal.buffer)._linesIndexOffset === y + (this._terminal).buffer.ydisp) { + currentTextStyle = null; + nextTextStyleIndex++; + if (nextTextStyleIndex === (this._terminal).buffer.textStyles.length) { + nextTextStyleIndex = -1; + } + } + if (nextTextStyleIndex !== -1 && + (this._terminal.buffer).textStyles[nextTextStyleIndex].x1 === i && + (this._terminal.buffer).textStyles[nextTextStyleIndex].y1 - ((this._terminal).buffer)._linesIndexOffset === y + (this._terminal).buffer.ydisp) { + currentTextStyle = (this._terminal.buffer).textStyles[nextTextStyleIndex]; + console.log(`current char attributes ${i},${y}:`, currentTextStyle); + } + + // TODO: This is temporary to test new method + if (currentTextStyle) { + flags = currentTextStyle.flags; + // v Temporary v + fg = currentTextStyle._data[1]; + bg = currentTextStyle._data[2]; + } + + // Force a refresh if the character is the cursor + const isCursor = i === cursorIndex; + if (isCursor) { + currentTextStyle = null; + } + + // Determine what element the character is going to be put in + if (isCursor || i === cursorIndex + 1 || currentTextStyle !== lastTextStyle) { + // Add the current element to the document fragment if it exists + if (currentElement) { if (innerHTML) { currentElement.innerHTML = innerHTML; innerHTML = ''; @@ -184,7 +215,9 @@ export class Renderer { documentFragment.appendChild(currentElement); currentElement = null; } - if (data !== this._terminal.defAttr || isCursor) { + + // Create a new span if the flags and colors are not the default + if (flags !== this._terminal.defaultFlags || fg !== this._terminal.defaultFgColor || bg !== this._terminal.defaultBgColor) { if (innerHTML && !currentElement) { currentElement = this._spanElementObjectPool.acquire(); } @@ -197,10 +230,6 @@ export class Renderer { } currentElement = this._spanElementObjectPool.acquire(); - let bg = data & 0x1ff; - let fg = (data >> 9) & 0x1ff; - let flags = data >> 18; - if (isCursor) { currentElement.classList.add('reverse-video'); currentElement.classList.add('terminal-cursor'); @@ -216,27 +245,29 @@ export class Renderer { } } - if (flags & FLAGS.UNDERLINE) { - currentElement.classList.add('xterm-underline'); - } - - if (flags & FLAGS.BLINK) { - currentElement.classList.add('xterm-blink'); - } + let isTrueColorFg = flags & FLAGS.TRUECOLOR_FG; + let isTrueColorBg = flags & FLAGS.TRUECOLOR_BG; // If inverse flag is on, then swap the foreground and background variables. if (flags & FLAGS.INVERSE) { let temp = bg; bg = fg; fg = temp; + temp = isTrueColorBg; + isTrueColorBg = isTrueColorFg; + isTrueColorFg = temp; // Should inverse just be before the above boldColors effect instead? if ((flags & 1) && fg < 8) { fg += 8; } } - if (flags & FLAGS.INVISIBLE && !isCursor) { - currentElement.classList.add('xterm-hidden'); + if (flags & FLAGS.UNDERLINE) { + currentElement.classList.add('xterm-underline'); + } + + if (flags & FLAGS.BLINK) { + currentElement.classList.add('xterm-blink'); } /** @@ -247,14 +278,47 @@ export class Renderer { * Source: https://github.com/sourcelair/xterm.js/issues/57 */ if (flags & FLAGS.INVERSE) { - if (bg === 257) { + if (bg === this._terminal.defaultBgColor) { bg = 15; } - if (fg === 256) { + if (fg === this._terminal.defaultFgColor) { fg = 0; } } + if (fg !== this._terminal.defaultFgColor) { + if (isTrueColorFg) { + // let rgb = fg.toString(16); + // while (rgb.length < 6) { + // rgb = '0' + rgb; + // } + // currentElement.style.color = `#${rgb}`; + currentElement.style.color = currentTextStyle.truecolorFg; + } else { + if (fg < 256) { + currentElement.classList.add(`xterm-color-${fg}`); + } + } + } + + if (bg !== this._terminal.defaultBgColor) { + if (isTrueColorBg) { + // let rgb = bg.toString(16); + // while (rgb.length < 6) { + // rgb = '0' + rgb; + // } + currentElement.style.backgroundColor = currentTextStyle.truecolorBg; + } else { + if (bg < 256) { + currentElement.classList.add(`xterm-bg-color-${bg}`); + } + } + } + + if (flags & FLAGS.INVISIBLE && !isCursor) { + currentElement.classList.add('xterm-hidden'); + } + if (bg < 256) { currentElement.classList.add(`xterm-bg-color-${bg}`); } @@ -294,10 +358,7 @@ export class Renderer { } } - // The cursor needs its own element, therefore we set attr to -1 - // which will cause the next character to be rendered in a new element - attr = isCursor ? -1 : data; - + lastTextStyle = currentTextStyle; } if (innerHTML && !currentElement) { diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index c2173d77f4..a9354755fb 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -3,17 +3,18 @@ */ import jsdom = require('jsdom'); import { assert } from 'chai'; -import { ITerminal, ICircularList } from './Interfaces'; +import { ITerminal, ICircularList, IBuffer } from './Interfaces'; import { CharMeasure } from './utils/CharMeasure'; import { CircularList } from './utils/CircularList'; import { SelectionManager } from './SelectionManager'; import { SelectionModel } from './SelectionModel'; +import { CharData, LineData } from './Types'; import { BufferSet } from './BufferSet'; class TestSelectionManager extends SelectionManager { constructor( terminal: ITerminal, - buffer: ICircularList<[number, string, number][]>, + buffer: IBuffer, rowContainer: HTMLElement, charMeasure: CharMeasure ) { @@ -37,7 +38,7 @@ describe('SelectionManager', () => { let document: Document; let terminal: ITerminal; - let bufferLines: ICircularList<[number, string, number][]>; + let bufferLines: ICircularList; let rowContainer: HTMLElement; let selectionManager: TestSelectionManager; @@ -51,13 +52,13 @@ describe('SelectionManager', () => { terminal.buffers = new BufferSet(terminal); terminal.buffer = terminal.buffers.active; bufferLines = terminal.buffer.lines; - selectionManager = new TestSelectionManager(terminal, bufferLines, rowContainer, null); + selectionManager = new TestSelectionManager(terminal, terminal.buffer, rowContainer, null); }); - function stringToRow(text: string): [number, string, number][] { - let result: [number, string, number][] = []; + function stringToRow(text: string): CharData[] { + let result: CharData[] = []; for (let i = 0; i < text.length; i++) { - result.push([0, text.charAt(i), 1]); + result.push([text.charAt(i), 1, 0, 0, 0]); } return result; } @@ -96,21 +97,21 @@ describe('SelectionManager', () => { it('should expand selection for wide characters', () => { // Wide characters use a special format bufferLines.push([ - [null, '中', 2], - [null, '', 0], - [null, '文', 2], - [null, '', 0], - [null, ' ', 1], - [null, 'a', 1], - [null, '中', 2], - [null, '', 0], - [null, '文', 2], - [null, '', 0], - [null, 'b', 1], - [null, ' ', 1], - [null, 'f', 1], - [null, 'o', 1], - [null, 'o', 1] + ['中', 2, null, null, null], + ['', 0, null, null, null], + ['文', 2, null, null, null], + ['', 0, null, null, null], + [' ', 1, null, null, null], + ['a', 1, null, null, null], + ['中', 2, null, null, null], + ['', 0, null, null, null], + ['文', 2, null, null, null], + ['', 0, null, null, null], + ['b', 1, null, null, null], + [' ', 1, null, null, null], + ['f', 1, null, null, null], + ['o', 1, null, null, null], + ['o', 1, null, null, null] ]); // Ensure wide characters take up 2 columns selectionManager.selectWordAt([0, 0]); diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index f6fb44da58..e5643debd7 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -7,9 +7,10 @@ import * as Browser from './utils/Browser'; import { CharMeasure } from './utils/CharMeasure'; import { CircularList } from './utils/CircularList'; import { EventEmitter } from './EventEmitter'; -import { ITerminal, ICircularList } from './Interfaces'; +import { ITerminal, ICircularList, IBuffer } from './Interfaces'; import { SelectionModel } from './SelectionModel'; -import { translateBufferLineToString } from './utils/BufferLine'; +import { CharData, LineData } from './Types'; +import { CHAR_DATA_WIDTH_INDEX } from './Buffer'; /** * The number of pixels the mouse needs to be above or below the viewport in @@ -33,11 +34,6 @@ const DRAG_SCROLL_INTERVAL = 50; */ const WORD_SEPARATORS = ' ()[]{}\'"'; -// TODO: Move these constants elsewhere, they belong in a buffer or buffer -// data/line class. -const LINE_DATA_CHAR_INDEX = 1; -const LINE_DATA_WIDTH_INDEX = 2; - const NON_BREAKING_SPACE_CHAR = String.fromCharCode(160); const ALL_NON_BREAKING_SPACE_REGEX = new RegExp(NON_BREAKING_SPACE_CHAR, 'g'); @@ -101,7 +97,7 @@ export class SelectionManager extends EventEmitter { constructor( private _terminal: ITerminal, - private _buffer: ICircularList<[number, string, number][]>, + private _buffer: IBuffer, private _rowContainer: HTMLElement, private _charMeasure: CharMeasure ) { @@ -126,7 +122,7 @@ export class SelectionManager extends EventEmitter { // reverseIndex) and delete in a splice is only ever used when the same // number of elements was just added. Given this is could actually be // beneficial to leave the selection as is for these cases. - this._buffer.on('trim', (amount: number) => this._onTrim(amount)); + this._buffer.lines.on('trim', (amount: number) => this._onTrim(amount)); } /** @@ -150,8 +146,10 @@ export class SelectionManager extends EventEmitter { * switched in or out. * @param buffer The active buffer. */ - public setBuffer(buffer: ICircularList<[number, string, number][]>): void { + public setBuffer(buffer: IBuffer): void { + this.disable(); this._buffer = buffer; + this.enable(); this.clearSelection(); } @@ -183,12 +181,12 @@ export class SelectionManager extends EventEmitter { // Get first row const startRowEndCol = start[1] === end[1] ? end[0] : null; let result: string[] = []; - result.push(translateBufferLineToString(this._buffer.get(start[1]), true, start[0], startRowEndCol)); + result.push(this._buffer.translateBufferLineToString(start[1], true, start[0], startRowEndCol)); // Get middle rows for (let i = start[1] + 1; i <= end[1] - 1; i++) { - const bufferLine = this._buffer.get(i); - const lineText = translateBufferLineToString(bufferLine, true); + const lineText = this._buffer.translateBufferLineToString(i, true); + const bufferLine = this._buffer.lines.get(i); if ((bufferLine).isWrapped) { result[result.length - 1] += lineText; } else { @@ -198,8 +196,8 @@ export class SelectionManager extends EventEmitter { // Get final row if (start[1] !== end[1]) { - const bufferLine = this._buffer.get(end[1]); - const lineText = translateBufferLineToString(bufferLine, true, 0, end[0]); + const lineText = this._buffer.translateBufferLineToString(end[1], true, 0, end[0]); + const bufferLine = this._buffer.lines.get(end[1]); if ((bufferLine).isWrapped) { result[result.length - 1] += lineText; } else { @@ -412,7 +410,7 @@ export class SelectionManager extends EventEmitter { this._model.selectionEnd = null; // Ensure the line exists - const line = this._buffer.get(this._model.selectionStart[1]); + const line = this._buffer.lines.get(this._model.selectionStart[1]); if (!line) { return; } @@ -420,7 +418,7 @@ export class SelectionManager extends EventEmitter { // If the mouse is over the second half of a wide character, adjust the // selection to cover the whole character const char = line[this._model.selectionStart[0]]; - if (char[LINE_DATA_WIDTH_INDEX] === 0) { + if (char[CHAR_DATA_WIDTH_INDEX] === 0) { this._model.selectionStart[0]++; } } @@ -492,9 +490,9 @@ export class SelectionManager extends EventEmitter { // If the character is a wide character include the cell to the right in the // selection. Note that selections at the very end of the line will never // have a character. - if (this._model.selectionEnd[1] < this._buffer.length) { - const char = this._buffer.get(this._model.selectionEnd[1])[this._model.selectionEnd[0]]; - if (char && char[2] === 0) { + if (this._model.selectionEnd[1] < this._buffer.lines.length) { + const char = this._buffer.lines.get(this._model.selectionEnd[1])[this._model.selectionEnd[0]]; + if (char && char[CHAR_DATA_WIDTH_INDEX] === 0) { this._model.selectionEnd[0]++; } } @@ -541,7 +539,7 @@ export class SelectionManager extends EventEmitter { let charIndex = coords[0]; for (let i = 0; coords[0] >= i; i++) { const char = bufferLine[i]; - if (char[LINE_DATA_WIDTH_INDEX] === 0) { + if (char[CHAR_DATA_WIDTH_INDEX] === 0) { charIndex--; } } @@ -561,12 +559,12 @@ export class SelectionManager extends EventEmitter { * @param coords The coordinates to get the word at. */ private _getWordAt(coords: [number, number]): IWordPosition { - const bufferLine = this._buffer.get(coords[1]); + const bufferLine = this._buffer.lines.get(coords[1]); if (!bufferLine) { return null; } - const line = translateBufferLineToString(bufferLine, false); + const line = this._buffer.translateBufferLineToString(coords[1], false); // Get actual index, taking into consideration wide characters let endIndex = this._convertViewportColToCharacterIndex(bufferLine, coords); @@ -594,17 +592,17 @@ export class SelectionManager extends EventEmitter { let endCol = coords[0]; // Consider the initial position, skip it and increment the wide char // variable - if (bufferLine[startCol][LINE_DATA_WIDTH_INDEX] === 0) { + if (bufferLine[startCol][CHAR_DATA_WIDTH_INDEX] === 0) { leftWideCharCount++; startCol--; } - if (bufferLine[endCol][LINE_DATA_WIDTH_INDEX] === 2) { + if (bufferLine[endCol][CHAR_DATA_WIDTH_INDEX] === 2) { rightWideCharCount++; endCol++; } // Expand the string in both directions until a space is hit while (startIndex > 0 && !this._isCharWordSeparator(line.charAt(startIndex - 1))) { - if (bufferLine[startCol - 1][LINE_DATA_WIDTH_INDEX] === 0) { + if (bufferLine[startCol - 1][CHAR_DATA_WIDTH_INDEX] === 0) { // If the next character is a wide char, record it and skip the column leftWideCharCount++; startCol--; @@ -613,7 +611,7 @@ export class SelectionManager extends EventEmitter { startCol--; } while (endIndex + 1 < line.length && !this._isCharWordSeparator(line.charAt(endIndex + 1))) { - if (bufferLine[endCol + 1][LINE_DATA_WIDTH_INDEX] === 2) { + if (bufferLine[endCol + 1][CHAR_DATA_WIDTH_INDEX] === 2) { // If the next character is a wide char, record it and skip the column rightWideCharCount++; endCol++; diff --git a/src/TextStyle.ts b/src/TextStyle.ts new file mode 100644 index 0000000000..4641484822 --- /dev/null +++ b/src/TextStyle.ts @@ -0,0 +1,45 @@ +/** + * @license MIT + */ + +const DEFAULT_COLOR = 1 << 24; + +export class TextStyle { + constructor( + public x1: number, + public y1: number, + public x2: number, + public y2: number, + // TODO: Int32Array (Maybe Int8?) + // TODO: This should be private + public _data: [number, number, number] + ) { + } + + public get flags(): number { + return this._data[0]; + } + + public get isDefaultFg(): boolean { + return this._data[1] === DEFAULT_COLOR; + } + + public get truecolorFg(): string { + return '#' + this._padLeft(this._data[1].toString(16), '0'); + } + + public get isDefaultBg(): boolean { + return this._data[2] === DEFAULT_COLOR; + } + + public get truecolorBg(): string { + return '#' + this._padLeft(this._data[2].toString(16), '0'); + } + + private _padLeft(text: string, padChar: string): string { + while (text.length < 6) { + text = padChar + text; + } + return text; + } +} diff --git a/src/Types.ts b/src/Types.ts index 896b729a81..c3806ea897 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -2,6 +2,41 @@ * @license MIT */ +/** + * Character data, the array's format is: + * - string: The character. + * - number: The width of the character. + * - number: Flags that decorate the character. + * + * truecolor fg + * | inverse + * | | underline + * | | | + * 0b 0 0 0 0 0 0 0 + * | | | | + * | | | bold + * | | blink + * | invisible + * truecolor bg + * + * - number: Foreground color. If default bit flag is set, color is the default + * (inherited from the DOM parent). If truecolor fg flag is true, this + * is a 24-bit color of the form 0xxRRGGBB, if not it's an xterm color + * code ranging from 0-255. + * + * red + * | blue + * 0x 0 R R G G B B + * | | + * | green + * default color bit + * + * - number: Background color. The same as foreground color. + */ +export type CharData = [string, number, number, number, number]; + +export type LineData = CharData[]; + export type LinkMatcher = { id: number, regex: RegExp, diff --git a/src/addons/search/search.ts b/src/addons/search/search.ts index 5a227a8a15..4d34f155e3 100644 --- a/src/addons/search/search.ts +++ b/src/addons/search/search.ts @@ -11,7 +11,7 @@ declare var require: any; declare var window: any; (function (addon) { - if ('Terminal' in window) { + if (typeof window !== 'undefined' && 'Terminal' in window) { /** * Plain browser environment */ @@ -22,7 +22,7 @@ declare var window: any; */ const xterm = '../../xterm'; module.exports = addon(require(xterm)); - } else if (typeof define == 'function') { + } else if (typeof define === 'function') { /** * Require.js is available */ diff --git a/src/test/escape-sequences-test.js b/src/test/escape-sequences-test.js index b4a53a17c3..c41fc9eada 100644 --- a/src/test/escape-sequences-test.js +++ b/src/test/escape-sequences-test.js @@ -65,7 +65,7 @@ function terminalToString(term) { for (var line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) { line_s = ''; for (var cell=0; cell= i) { - widthAdjustedStartCol--; - } - if (endCol >= i) { - widthAdjustedEndCol--; - } - } - } - - // Calculate the final end col by trimming whitespace on the right of the - // line if needed. - let finalEndCol = widthAdjustedEndCol || line.length; - if (trimRight) { - const rightWhitespaceIndex = lineString.search(/\s+$/); - if (rightWhitespaceIndex !== -1) { - finalEndCol = Math.min(finalEndCol, rightWhitespaceIndex); - } - // Return the empty string if only trimmed whitespace is selected - if (finalEndCol <= widthAdjustedStartCol) { - return ''; - } - } - - return lineString.substring(widthAdjustedStartCol, finalEndCol); -} diff --git a/src/utils/CircularList.ts b/src/utils/CircularList.ts index d0b2f685a4..54850ab7c3 100644 --- a/src/utils/CircularList.ts +++ b/src/utils/CircularList.ts @@ -5,8 +5,9 @@ * @license MIT */ import { EventEmitter } from '../EventEmitter'; +import { ICircularList } from '../Interfaces'; -export class CircularList extends EventEmitter { +export class CircularList extends EventEmitter implements ICircularList { private _array: T[]; private _startIndex: number; private _length: number; diff --git a/src/utils/DomElementObjectPool.test.ts b/src/utils/DomElementObjectPool.test.ts index 7298f1922f..c72f6a2154 100644 --- a/src/utils/DomElementObjectPool.test.ts +++ b/src/utils/DomElementObjectPool.test.ts @@ -1,7 +1,8 @@ import { assert } from 'chai'; import { DomElementObjectPool } from './DomElementObjectPool'; -class MockDocument { +class MockElement { + public style = {}; private _attr: {[key: string]: string} = {}; constructor() {} public getAttribute(key: string): string { return this._attr[key]; }; @@ -14,7 +15,7 @@ describe('DomElementObjectPool', () => { beforeEach(() => { pool = new DomElementObjectPool('span'); (global).document = { - createElement: () => new MockDocument() + createElement: () => new MockElement() }; }); diff --git a/src/utils/DomElementObjectPool.ts b/src/utils/DomElementObjectPool.ts index 7707ad9ab6..61466829b2 100644 --- a/src/utils/DomElementObjectPool.ts +++ b/src/utils/DomElementObjectPool.ts @@ -71,5 +71,7 @@ export class DomElementObjectPool { private _cleanElement(element: HTMLElement): void { element.className = ''; element.innerHTML = ''; + element.style.color = ''; + element.style.backgroundColor = ''; } } diff --git a/src/xterm.js b/src/xterm.js index 5aa069e796..f293bdf65e 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -27,7 +27,6 @@ import * as Browser from './utils/Browser'; import * as Mouse from './utils/Mouse'; import { CHARSETS } from './Charsets'; import { getRawByteCoords } from './utils/Mouse'; -import { translateBufferLineToString } from './utils/BufferLine'; /** * Terminal Emulation References: @@ -187,8 +186,18 @@ function Terminal(options) { this.readable = true; this.writable = true; - this.defAttr = (0 << 18) | (257 << 9) | (256 << 0); - this.curAttr = this.defAttr; + // this.defAttr = (0 << 18) | (257 << 9) | (256 << 0); + // this.curAttr = this.defAttr; + + + // TODO: Move these into buffer? Need a way of creating new attributes based on previous ones if so + this.defaultFlags = 0; + this.defaultFgColor = 1 << 24; + this.defaultBgColor = 1 << 24; + + this.currentFlags = this.defaultFlags; + this.currentFgColor = this.defaultFgColor; + this.currentBgColor = this.defaultBgColor; this.params = []; this.currentParam = 0; @@ -234,7 +243,7 @@ function Terminal(options) { } // Ensure the selection manager has the correct buffer if (this.selectionManager) { - this.selectionManager.setBuffer(this.buffer.lines); + this.selectionManager.setBuffer(this.buffer); } this.setupStops(); @@ -248,10 +257,10 @@ inherits(Terminal, EventEmitter); /** * back_color_erase feature for xterm. */ -Terminal.prototype.eraseAttr = function() { - // if (this.is('screen')) return this.defAttr; - return (this.defAttr & ~0x1ff) | (this.curAttr & 0x1ff); -}; +// Terminal.prototype.eraseAttr = function() { +// // if (this.is('screen')) return this.defAttr; +// return (this.defAttr & ~0x1ff) | (this.curAttr & 0x1ff); +// }; /** * Colors @@ -706,7 +715,7 @@ Terminal.prototype.open = function(parent, focus) { this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure); this.renderer = new Renderer(this); this.selectionManager = new SelectionManager( - this, this.buffer.lines, this.rowContainer, this.charMeasure + this, this.buffer, this.rowContainer, this.charMeasure ); this.selectionManager.on('refresh', data => { this.renderer.refreshSelection(data.start, data.end); @@ -1933,7 +1942,7 @@ Terminal.prototype.resize = function(x, y) { // resize cols j = this.cols; if (j < x) { - ch = [this.defAttr, ' ', 1]; // does xterm use the default attr? + ch = [' ', 1, this.defaultFlags, this.defaultFgColor, this.defaultBgColor]; // does xterm use the default attr? i = this.buffer.lines.length; while (i--) { if (this.buffer.lines.get(i) === undefined) { @@ -2100,7 +2109,7 @@ Terminal.prototype.eraseRight = function(x, y) { if (!line) { return; } - var ch = [this.eraseAttr(), ' ', 1]; // xterm + var ch = [' ', 1, this.defaultFlags, this.defaultFgColor, this.currentBgColor]; // xterm for (; x < this.cols; x++) { line[x] = ch; } @@ -2119,7 +2128,7 @@ Terminal.prototype.eraseLeft = function(x, y) { if (!line) { return; } - var ch = [this.eraseAttr(), ' ', 1]; // xterm + var ch = [' ', 1, this._terminal.defaultFlags, this._terminal.defaultFgColor, this._terminal.currentBgColor]; // xterm x++; while (x--) { line[x] = ch; @@ -2162,13 +2171,8 @@ Terminal.prototype.eraseLine = function(y) { * @param {boolean} isWrapped Whether the new line is wrapped from the previous line. */ Terminal.prototype.blankLine = function(cur, isWrapped) { - var attr = cur - ? this.eraseAttr() - : this.defAttr; - - var ch = [attr, ' ', 1] // width defaults to 1 halfwidth character - , line = [] - , i = 0; + var ch = this.ch(cur); // width defaults to 1 halfwidth character + var line = []; // TODO: It is not ideal that this is a property on an array, a buffer line // class should be added that will hold this data and other useful functions. @@ -2176,7 +2180,7 @@ Terminal.prototype.blankLine = function(cur, isWrapped) { line.isWrapped = isWrapped; } - for (; i < this.cols; i++) { + for (var i = 0; i < this.cols; i++) { line[i] = ch; } @@ -2190,8 +2194,8 @@ Terminal.prototype.blankLine = function(cur, isWrapped) { */ Terminal.prototype.ch = function(cur) { return cur - ? [this.eraseAttr(), ' ', 1] - : [this.defAttr, ' ', 1]; + ? [' ', 1, this.defaultFlags, this.defaultFgColor, this.currentBgColor] + : [' ', 1, this.defaultFlags, this.defaultFgColor, this.defaultBgColor]; }; @@ -2444,7 +2448,6 @@ function keys(obj) { * Expose */ -Terminal.translateBufferLineToString = translateBufferLineToString; Terminal.EventEmitter = EventEmitter; Terminal.inherits = inherits;