Merge branch 'master' into left_right_scroll

This commit is contained in:
jerch
2019-10-24 18:07:35 +02:00
committed by GitHub
44 changed files with 490 additions and 128 deletions
+9 -13
View File
@@ -76,7 +76,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
/**
* The HTMLElement that the terminal is created in, set by Terminal.open.
*/
private _parent: HTMLElement;
private _parent: HTMLElement | null;
private _document: Document;
private _viewportScrollArea: HTMLElement;
private _viewportElement: HTMLElement;
@@ -383,18 +383,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
case 'theme':
this._setTheme(this.optionsService.options.theme);
break;
case 'scrollback':
const newBufferLength = this.rows + this.optionsService.options.scrollback;
if (this.buffer.lines.length > newBufferLength) {
const amountToTrim = this.buffer.lines.length - newBufferLength;
const needsRefresh = (this.buffer.ydisp - amountToTrim < 0);
this.buffer.lines.trimStart(amountToTrim);
this.buffer.ybase = Math.max(this.buffer.ybase - amountToTrim, 0);
this.buffer.ydisp = Math.max(this.buffer.ydisp - amountToTrim, 0);
if (needsRefresh) {
this.refresh(0, this.rows - 1);
}
}
case 'windowsMode':
if (this.optionsService.options.windowsMode) {
if (!this._windowsMode) {
@@ -520,6 +508,10 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
throw new Error('Terminal requires a parent element.');
}
if (!document.body.contains(parent)) {
this._logService.warn('Terminal.open was called on an element that was not attached to the DOM');
}
this._document = this._parent.ownerDocument;
// Create main element container
@@ -1469,6 +1461,10 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
this._charSizeService.measure();
}
// Sync the scroll area to make sure scroll events don't fire and scroll the viewport to an
// invalid location
this.viewport.syncScrollArea(true);
this.refresh(0, this.rows - 1);
this._onResize.fire({ cols: x, rows: y });
}
+2 -2
View File
@@ -177,7 +177,7 @@ export interface ITerminal extends IPublicTerminal, IElementAccessor, IBufferAcc
// Portions of the public API that are required by the internal Terminal
export interface IPublicTerminal extends IDisposable {
textarea: HTMLTextAreaElement;
textarea: HTMLTextAreaElement | undefined;
rows: number;
cols: number;
buffer: IBuffer;
@@ -230,7 +230,7 @@ export interface IBufferAccessor {
}
export interface IElementAccessor {
readonly element: HTMLElement;
readonly element: HTMLElement | undefined;
}
export interface ILinkifierAccessor {
+3 -1
View File
@@ -306,7 +306,9 @@ export class Linkifier implements ILinkifier {
e => {
this._onLinkTooltip.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg));
if (matcher.hoverTooltipCallback) {
matcher.hoverTooltipCallback(e, uri);
// Note that IViewportRange use 1-based coordinates to align with escape sequences such
// as CUP which use 1,1 as the default for row/col
matcher.hoverTooltipCallback(e, uri, { start: { row: y1 + 1, col: x1 + 1 }, end: { row: y2 + 1, col: x2 } });
}
},
() => {
+14 -3
View File
@@ -35,7 +35,7 @@ export interface IPartialColorSet {
export interface IViewport extends IDisposable {
scrollBarWidth: number;
syncScrollArea(): void;
syncScrollArea(immediate?: boolean): void;
getLinesScrolled(ev: WheelEvent): number;
onWheel(ev: WheelEvent): boolean;
onTouchStart(ev: TouchEvent): void;
@@ -43,14 +43,25 @@ export interface IViewport extends IDisposable {
onThemeChange(colors: IColorSet): void;
}
export interface IViewportRange {
start: IViewportCellPosition;
end: IViewportCellPosition;
}
export interface IViewportCellPosition {
col: number;
row: number;
}
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void;
export type LinkMatcherHoverTooltipCallback = (event: MouseEvent, uri: string, position: IViewportRange) => void;
export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
export interface ILinkMatcher {
id: number;
regex: RegExp;
handler: LinkMatcherHandler;
hoverTooltipCallback?: LinkMatcherHandler;
hoverTooltipCallback?: LinkMatcherHoverTooltipCallback;
hoverLeaveCallback?: () => void;
matchIndex?: number;
validationCallback?: LinkMatcherValidationCallback;
@@ -96,7 +107,7 @@ export interface ILinkMatcherOptions {
/**
* A callback that fires when the mouse hovers over a link.
*/
tooltipCallback?: LinkMatcherHandler;
tooltipCallback?: LinkMatcherHoverTooltipCallback;
/**
* A callback that fires when the mouse leaves a link that was hovered.
*/
+30 -11
View File
@@ -7,7 +7,7 @@ import { Disposable } from 'common/Lifecycle';
import { addDisposableDomListener } from 'browser/Lifecycle';
import { IColorSet, IViewport } from 'browser/Types';
import { ICharSizeService, IRenderService } from 'browser/services/Services';
import { IBufferService } from 'common/services/Services';
import { IBufferService, IOptionsService } from 'common/services/Services';
const FALLBACK_SCROLL_BAR_WIDTH = 15;
@@ -37,6 +37,7 @@ export class Viewport extends Disposable implements IViewport {
private readonly _viewportElement: HTMLElement,
private readonly _scrollArea: HTMLElement,
@IBufferService private readonly _bufferService: IBufferService,
@IOptionsService private readonly _optionsService: IOptionsService,
@ICharSizeService private readonly _charSizeService: ICharSizeService,
@IRenderService private readonly _renderService: IRenderService
) {
@@ -60,7 +61,14 @@ export class Viewport extends Disposable implements IViewport {
* Refreshes row height, setting line-height, viewport height and scroll area height if
* necessary.
*/
private _refresh(): void {
private _refresh(immediate: boolean): void {
if (immediate) {
this._innerRefresh();
if (this._refreshAnimationFrame !== null) {
cancelAnimationFrame(this._refreshAnimationFrame);
}
return;
}
if (this._refreshAnimationFrame === null) {
this._refreshAnimationFrame = requestAnimationFrame(() => this._innerRefresh());
}
@@ -88,40 +96,39 @@ export class Viewport extends Disposable implements IViewport {
this._refreshAnimationFrame = null;
}
/**
* Updates dimensions and synchronizes the scroll area if necessary.
*/
public syncScrollArea(): void {
public syncScrollArea(immediate: boolean = false): void {
// If buffer height changed
if (this._lastRecordedBufferLength !== this._bufferService.buffer.lines.length) {
this._lastRecordedBufferLength = this._bufferService.buffer.lines.length;
this._refresh();
this._refresh(immediate);
return;
}
// If viewport height changed
if (this._lastRecordedViewportHeight !== this._renderService.dimensions.canvasHeight) {
this._refresh();
this._refresh(immediate);
return;
}
// If the buffer position doesn't match last scroll top
const newScrollTop = this._bufferService.buffer.ydisp * this._currentRowHeight;
if (this._lastScrollTop !== newScrollTop) {
this._refresh();
this._refresh(immediate);
return;
}
// If element's scroll top changed, this can happen when hiding the element
if (this._lastScrollTop !== this._viewportElement.scrollTop) {
this._refresh();
this._refresh(immediate);
return;
}
// If row height changed
if (this._renderService.dimensions.scaledCellHeight / window.devicePixelRatio !== this._currentRowHeight) {
this._refresh();
this._refresh(immediate);
return;
}
}
@@ -191,7 +198,7 @@ export class Viewport extends Disposable implements IViewport {
}
// Fallback to WheelEvent.DOM_DELTA_PIXEL
let amount = ev.deltaY;
let amount = this._applyScrollModifier(ev.deltaY, ev);
if (ev.deltaMode === WheelEvent.DOM_DELTA_LINE) {
amount *= this._currentRowHeight;
} else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
@@ -212,7 +219,7 @@ export class Viewport extends Disposable implements IViewport {
}
// Fallback to WheelEvent.DOM_DELTA_LINE
let amount = ev.deltaY;
let amount = this._applyScrollModifier(ev.deltaY, ev);
if (ev.deltaMode === WheelEvent.DOM_DELTA_PIXEL) {
amount /= this._currentRowHeight + 0.0; // Prevent integer division
this._wheelPartialScroll += amount;
@@ -224,6 +231,18 @@ export class Viewport extends Disposable implements IViewport {
return amount;
}
private _applyScrollModifier(amount: number, ev: WheelEvent): number {
const modifier = this._optionsService.options.fastScrollModifier;
// Multiply the scroll speed when the modifier is down
if ((modifier === 'alt' && ev.altKey) ||
(modifier === 'ctrl' && ev.ctrlKey) ||
(modifier === 'shift' && ev.shiftKey)) {
return amount * this._optionsService.options.fastScrollSensitivity;
}
return amount * this._optionsService.options.scrollSensitivity;
}
/**
* Handles the touchstart event, recording the touch occurred.
* @param ev The touch event.
@@ -303,7 +303,6 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry {
// current range
ranges[i - 1][1] = Math.max(newRange[1], range[1]);
ranges.splice(i, 1);
inRange = false;
return ranges;
}
@@ -8,7 +8,7 @@ import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
import { AttributeData } from 'common/buffer/AttributeData';
import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR } from 'common/buffer/Constants';
import { CellData } from 'common/buffer/CellData';
import { ITerminalOptions, IOptionsService } from 'common/services/Services';
import { IOptionsService } from 'common/services/Services';
export const BOLD_CLASS = 'xterm-bold';
export const DIM_CLASS = 'xterm-dim';
+2 -2
View File
@@ -210,7 +210,7 @@ export class SelectionService implements ISelectionService {
for (let i = start[1] + 1; i <= end[1] - 1; i++) {
const bufferLine = buffer.lines.get(i);
const lineText = buffer.translateBufferLineToString(i, true);
if (bufferLine!.isWrapped) {
if (bufferLine && bufferLine.isWrapped) {
result[result.length - 1] += lineText;
} else {
result.push(lineText);
@@ -221,7 +221,7 @@ export class SelectionService implements ISelectionService {
if (start[1] !== end[1]) {
const bufferLine = buffer.lines.get(end[1]);
const lineText = buffer.translateBufferLineToString(end[1], true, 0, end[0]);
if (bufferLine!.isWrapped) {
if (bufferLine && bufferLine!.isWrapped) {
result[result.length - 1] += lineText;
} else {
result.push(lineText);
+18 -12
View File
@@ -167,19 +167,25 @@ export class Buffer implements IBuffer {
if (this._rows < newRows) {
for (let y = this._rows; y < newRows; y++) {
if (this.lines.length < newRows + this.ybase) {
if (this.ybase > 0 && this.lines.length <= this.ybase + this.y + addToY + 1) {
// There is room above the buffer and there are no empty elements below the line,
// scroll up
this.ybase--;
addToY++;
if (this.ydisp > 0) {
// Viewport is at the top of the buffer, must increase downwards
this.ydisp--;
}
} else {
// Add a blank line if there is no buffer left at the top to scroll to, or if there
// are blank lines after the cursor
if (this._optionsService.options.windowsMode) {
// Just add the new missing rows on Windows as conpty reprints the screen with it's
// view of the world. Once a line enters scrollback for conpty it remains there
this.lines.push(new BufferLine(newCols, nullCell));
} else {
if (this.ybase > 0 && this.lines.length <= this.ybase + this.y + addToY + 1) {
// There is room above the buffer and there are no empty elements below the line,
// scroll up
this.ybase--;
addToY++;
if (this.ydisp > 0) {
// Viewport is at the top of the buffer, must increase downwards
this.ydisp--;
}
} else {
// Add a blank line if there is no buffer left at the top to scroll to, or if there
// are blank lines after the cursor
this.lines.push(new BufferLine(newCols, nullCell));
}
}
}
}
+1 -1
View File
@@ -5,7 +5,7 @@
import { CharData, IBufferLine, ICellData } from 'common/Types';
import { stringFromCodePoint } from 'common/input/TextDecoder';
import { DEFAULT_COLOR, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_ATTR_INDEX, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Content } from 'common/buffer/Constants';
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_ATTR_INDEX, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Content } from 'common/buffer/Constants';
import { CellData } from 'common/buffer/CellData';
import { AttributeData } from 'common/buffer/AttributeData';
+12
View File
@@ -108,6 +108,12 @@ describe('Keyboard', () => {
it('should return \\x1b[5C for alt+right', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 39 }, { isMac: false }).key, '\x1b[1;5C'); // CSI 5 C
});
it('should return \\x1b[5D for alt+up', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 38 }, { isMac: false }).key, '\x1b[1;5A'); // CSI 5 D
});
it('should return \\x1b[5C for alt+down', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 40 }, { isMac: false }).key, '\x1b[1;5B'); // CSI 5 C
});
it('should return \\x1ba for alt+a', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 65 }, { isMac: false }).key, '\x1ba');
});
@@ -120,6 +126,12 @@ describe('Keyboard', () => {
it('should return \\x1bf for alt+right', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 39 }, { isMac: true }).key, '\x1bf'); // CSI 5 C
});
it('should return \\x1bb for alt+up', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 38 }, { isMac: true }).key, '\x1b[1;3A'); // CSI 5 D
});
it('should return \\x1bf for alt+down', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 40 }, { isMac: true }).key, '\x1b[1;3B'); // CSI 5 C
});
it('should return undefined for alt+a', () => {
assert.strictEqual(testEvaluateKeyboardEvent({ altKey: true, keyCode: 65 }, { isMac: true }).key, undefined), { isMac: true };
});
+18 -4
View File
@@ -113,13 +113,16 @@ export function evaluateKeyboardEvent(
break;
case 37:
// left-arrow
if (ev.metaKey) {
break;
}
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'D';
// HACK: Make Alt + left-arrow behave like Ctrl + left-arrow: move one word backwards
// http://unix.stackexchange.com/a/108106
// macOS uses different escape sequences than linux
if (result.key === C0.ESC + '[1;3D') {
result.key = isMac ? C0.ESC + 'b' : C0.ESC + '[1;5D';
result.key = C0.ESC + (isMac ? 'b' : '[1;5D');
}
} else if (applicationCursorMode) {
result.key = C0.ESC + 'OD';
@@ -129,13 +132,16 @@ export function evaluateKeyboardEvent(
break;
case 39:
// right-arrow
if (ev.metaKey) {
break;
}
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'C';
// HACK: Make Alt + right-arrow behave like Ctrl + right-arrow: move one word forward
// http://unix.stackexchange.com/a/108106
// macOS uses different escape sequences than linux
if (result.key === C0.ESC + '[1;3C') {
result.key = isMac ? C0.ESC + 'f' : C0.ESC + '[1;5C';
result.key = C0.ESC + (isMac ? 'f' : '[1;5C');
}
} else if (applicationCursorMode) {
result.key = C0.ESC + 'OC';
@@ -145,11 +151,15 @@ export function evaluateKeyboardEvent(
break;
case 38:
// up-arrow
if (ev.metaKey) {
break;
}
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'A';
// HACK: Make Alt + up-arrow behave like Ctrl + up-arrow
// http://unix.stackexchange.com/a/108106
if (result.key === C0.ESC + '[1;3A') {
// macOS uses different escape sequences than linux
if (!isMac && result.key === C0.ESC + '[1;3A') {
result.key = C0.ESC + '[1;5A';
}
} else if (applicationCursorMode) {
@@ -160,11 +170,15 @@ export function evaluateKeyboardEvent(
break;
case 40:
// down-arrow
if (ev.metaKey) {
break;
}
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'B';
// HACK: Make Alt + down-arrow behave like Ctrl + down-arrow
// http://unix.stackexchange.com/a/108106
if (result.key === C0.ESC + '[1;3B') {
// macOS uses different escape sequences than linux
if (!isMac && result.key === C0.ESC + '[1;3B') {
result.key = C0.ESC + '[1;5B';
}
} else if (applicationCursorMode) {
+10 -1
View File
@@ -23,6 +23,8 @@ export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({
bellSound: DEFAULT_BELL_SOUND,
bellStyle: 'none',
drawBoldTextInBrightColors: true,
fastScrollModifier: 'alt',
fastScrollSensitivity: 5,
fontFamily: 'courier-new, courier, monospace',
fontSize: 15,
fontWeight: 'normal',
@@ -31,6 +33,7 @@ export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({
letterSpacing: 0,
logLevel: 'info',
scrollback: 1000,
scrollSensitivity: 1,
screenReaderMode: false,
macOptionIsMeta: false,
macOptionClickForcesSelection: false,
@@ -47,7 +50,7 @@ export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({
screenKeys: false,
cancelEvents: false,
useFlowControl: false,
wordSeparator: ' ()[]{}\'"'
wordSeparator: ' ()[]{}\',:;"'
});
/**
@@ -119,6 +122,12 @@ export class OptionsService implements IOptionsService {
throw new Error(`${key} cannot be less than 0, value: ${value}`);
}
break;
case 'fastScrollSensitivity':
case 'scrollSensitivity':
if (value <= 0) {
throw new Error(`${key} cannot be less than or equal to 0, value: ${value}`);
}
break;
}
return value;
}
+6
View File
@@ -180,6 +180,8 @@ export interface IPartialTerminalOptions {
cursorStyle?: 'block' | 'underline' | 'bar';
disableStdin?: boolean;
drawBoldTextInBrightColors?: boolean;
fastScrollModifier?: 'alt' | 'ctrl' | 'shift';
fastScrollSensitivity?: number;
fontSize?: number;
fontFamily?: string;
fontWeight?: FontWeight;
@@ -194,6 +196,7 @@ export interface IPartialTerminalOptions {
rows?: number;
screenReaderMode?: boolean;
scrollback?: number;
scrollSensitivity?: number;
tabStopWidth?: number;
theme?: ITheme;
windowsMode?: boolean;
@@ -209,6 +212,8 @@ export interface ITerminalOptions {
cursorStyle: 'block' | 'underline' | 'bar';
disableStdin: boolean;
drawBoldTextInBrightColors: boolean;
fastScrollModifier: 'alt' | 'ctrl' | 'shift' | undefined;
fastScrollSensitivity: number;
fontSize: number;
fontFamily: string;
fontWeight: FontWeight;
@@ -223,6 +228,7 @@ export interface ITerminalOptions {
rows: number;
screenReaderMode: boolean;
scrollback: number;
scrollSensitivity: number;
tabStopWidth: number;
theme: ITheme;
windowsMode: boolean;
+4 -4
View File
@@ -33,14 +33,14 @@ export class Terminal implements ITerminalApi {
public get onRender(): IEvent<{ start: number, end: number }> { return this._core.onRender; }
public get onResize(): IEvent<{ cols: number, rows: number }> { return this._core.onResize; }
public get element(): HTMLElement { return this._core.element; }
public get element(): HTMLElement | undefined { return this._core.element; }
public get parser(): IParser {
if (!this._parser) {
this._parser = new ParserApi(this._core);
}
return this._parser;
}
public get textarea(): HTMLTextAreaElement { return this._core.textarea; }
public get textarea(): HTMLTextAreaElement | undefined { return this._core.textarea; }
public get rows(): number { return this._core.rows; }
public get cols(): number { return this._core.cols; }
public get buffer(): IBufferApi { return new BufferApiView(this._core.buffer); }
@@ -178,8 +178,8 @@ export class Terminal implements ITerminalApi {
private _verifyIntegers(...values: number[]): void {
values.forEach(value => {
if (value % 1 !== 0) {
throw new Error('This API does not accept floating point numbers');
if (value === Infinity || isNaN(value) || value % 1 !== 0) {
throw new Error('This API only accepts integers');
}
});
}
+10 -5
View File
@@ -103,10 +103,10 @@ export class CursorRenderLayer extends BaseRenderLayer {
this._cursorBlinkStateManager.dispose();
this._cursorBlinkStateManager = null;
}
// Request a refresh from the terminal as management of rendering is being
// moved back to the terminal
this._terminal.refresh(this._bufferService.buffer.y, this._bufferService.buffer.y);
}
// Request a refresh from the terminal as management of rendering is being
// moved back to the terminal
this._terminal.refresh(this._bufferService.buffer.y, this._bufferService.buffer.y);
}
public onCursorMove(): void {
@@ -148,12 +148,17 @@ export class CursorRenderLayer extends BaseRenderLayer {
this._clearCursor();
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor.css;
this._renderBlurCursor(this._bufferService.buffer.x, viewportRelativeCursorY, this._cell);
const cursorStyle = this._optionsService.options.cursorStyle;
if (cursorStyle && cursorStyle !== 'block') {
this._cursorRenderers[cursorStyle](this._bufferService.buffer.x, viewportRelativeCursorY, this._cell);
} else {
this._renderBlurCursor(this._bufferService.buffer.x, viewportRelativeCursorY, this._cell);
}
this._ctx.restore();
this._state.x = this._bufferService.buffer.x;
this._state.y = viewportRelativeCursorY;
this._state.isFocused = false;
this._state.style = this._optionsService.options.cursorStyle;
this._state.style = cursorStyle;
this._state.width = this._cell.getWidth();
return;
}
+3 -3
View File
@@ -183,7 +183,7 @@ export class DomRenderer extends Disposable implements IRenderer {
`}`;
// Cursor
styles +=
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${CURSOR_CLASS} {` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` +
` outline: 1px solid ${this._colors.cursor.css};` +
` outline-offset: -1px;` +
`}` +
@@ -197,10 +197,10 @@ export class DomRenderer extends Disposable implements IRenderer {
` background-color: ${this._colors.cursor.css};` +
` color: ${this._colors.cursorAccent.css};` +
`}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BAR_CLASS} {` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BAR_CLASS} {` +
` box-shadow: 1px 0 0 ${this._colors.cursor.css} inset;` +
`}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_UNDERLINE_CLASS} {` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_UNDERLINE_CLASS} {` +
` box-shadow: 0 -1px 0 ${this._colors.cursor.css} inset;` +
`}`;
// Selection