diff --git a/css/xterm.css b/css/xterm.css index 4a202bc4..74acc267 100644 --- a/css/xterm.css +++ b/css/xterm.css @@ -172,6 +172,16 @@ .xterm-underline-4 { text-decoration: dotted underline; } .xterm-underline-5 { text-decoration: dashed underline; } +.xterm-overline { + text-decoration: overline; +} + +.xterm-overline.xterm-underline-1 { text-decoration: overline underline; } +.xterm-overline.xterm-underline-2 { text-decoration: overline double underline; } +.xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; } +.xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; } +.xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; } + .xterm-strikethrough { text-decoration: line-through; } diff --git a/demo/client.ts b/demo/client.ts index a9a8a23c..d72ddf65 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -976,7 +976,9 @@ function sgrTest(): void { { ps: 45, name: 'Background Magenta' }, { ps: 46, name: 'Background Cyan' }, { ps: 47, name: 'Background White' }, - { ps: 49, name: 'Background default' } + { ps: 49, name: 'Background default' }, + { ps: 53, name: 'Overlined' }, + { ps: 55, name: 'Not overlined' } ]; const maxNameLength = entries.reduce((p, c) => Math.max(c.name.length, p), 0); for (const e of entries) { @@ -988,7 +990,8 @@ function sgrTest(): void { } const comboEntries: { ps: number[] }[] = [ { ps: [1, 2, 3, 4, 5, 6, 7, 9] }, - { ps: [2, 41] } + { ps: [2, 41] }, + { ps: [4, 53] } ]; term.write('\n\n\r'); term.writeln(`Combinations`); diff --git a/src/browser/renderer/dom/DomRendererRowFactory.test.ts b/src/browser/renderer/dom/DomRendererRowFactory.test.ts index 5969abff..0c2fb79d 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.test.ts @@ -167,6 +167,16 @@ describe('DomRendererRowFactory', () => { }); }); + it('should add class for overline', () => { + const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); + cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.OVERLINE; + lineData.setCell(0, cell); + const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING); + assert.equal(getFragmentHtml(fragment), + 'a' + ); + }); + it('should add class for strikethrough', () => { const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]); cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.STRIKETHROUGH; diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index ae5e37cb..a0b44d2c 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -18,6 +18,7 @@ export const BOLD_CLASS = 'xterm-bold'; export const DIM_CLASS = 'xterm-dim'; export const ITALIC_CLASS = 'xterm-italic'; export const UNDERLINE_CLASS = 'xterm-underline'; +export const OVERLINE_CLASS = 'xterm-overline'; export const STRIKETHROUGH_CLASS = 'xterm-strikethrough'; export const CURSOR_CLASS = 'xterm-cursor'; export const CURSOR_BLINK_CLASS = 'xterm-cursor-blink'; @@ -185,6 +186,13 @@ export class DomRendererRowFactory { } } + if (cell.isOverline()) { + charElement.classList.add(OVERLINE_CLASS); + if (charElement.textContent === ' ') { + charElement.textContent = '\xa0'; // =   + } + } + if (cell.isStrikethrough()) { charElement.classList.add(STRIKETHROUGH_CLASS); } diff --git a/src/browser/renderer/shared/TextureAtlas.ts b/src/browser/renderer/shared/TextureAtlas.ts index e503dd7c..d6bf1581 100644 --- a/src/browser/renderer/shared/TextureAtlas.ts +++ b/src/browser/renderer/shared/TextureAtlas.ts @@ -449,6 +449,7 @@ export class TextureAtlas implements ITextureAtlas { const italic = !!this._workAttributeData.isItalic(); const underline = !!this._workAttributeData.isUnderline(); const strikethrough = !!this._workAttributeData.isStrikethrough(); + const overline = !!this._workAttributeData.isOverline(); let fgColor = this._workAttributeData.getFgColor(); let fgColorMode = this._workAttributeData.getFgColorMode(); let bgColor = this._workAttributeData.getBgColor(); @@ -632,12 +633,24 @@ export class TextureAtlas implements ITextureAtlas { } } + // Overline + if (overline) { + const lineWidth = Math.max(1, Math.floor(this._config.fontSize * this._config.devicePixelRatio / 15)); + const yOffset = lineWidth % 2 === 1 ? 0.5 : 0; + this._tmpCtx.lineWidth = lineWidth; + this._tmpCtx.strokeStyle = this._tmpCtx.fillStyle; + this._tmpCtx.beginPath(); + this._tmpCtx.moveTo(padding, padding + yOffset); + this._tmpCtx.lineTo(padding + this._config.deviceCharWidth * chWidth, padding + yOffset); + this._tmpCtx.stroke(); + } + // Draw the character if (!customGlyph) { this._tmpCtx.fillText(chars, padding, padding + this._config.deviceCharHeight); } - // If this charcater is underscore and beyond the cell bounds, shift it up until it is visible + // If this character is underscore and beyond the cell bounds, shift it up until it is visible // even on the bottom row, try for a maximum of 5 pixels. if (chars === '_' && !this._config.allowTransparency) { let isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.deviceCellWidth, this._config.deviceCellHeight), backgroundColor, foregroundColor, enableClearThresholdCheck); diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index fa65b93b..65942e4d 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -2551,6 +2551,12 @@ export class InputHandler extends Disposable implements IInputHandler { } else if (p === 38 || p === 48 || p === 58) { // fg color 256 and RGB i += this._extractColor(params, i, attr); + } else if (p === 53) { + // overline + attr.bg |= BgFlags.OVERLINE; + } else if (p === 55) { + // not overline + attr.bg &= ~BgFlags.OVERLINE; } else if (p === 59) { attr.extended = attr.extended.clone(); attr.extended.underlineColor = -1; diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index 73471512..70143525 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -165,6 +165,7 @@ export interface IAttributeData { isDim(): number; isStrikethrough(): number; isProtected(): number; + isOverline(): number; /** * The color mode of the foreground color which determines how to decode {@link getFgColor}, diff --git a/src/common/buffer/AttributeData.ts b/src/common/buffer/AttributeData.ts index c9f4cd61..f4d12c2b 100644 --- a/src/common/buffer/AttributeData.ts +++ b/src/common/buffer/AttributeData.ts @@ -47,6 +47,7 @@ export class AttributeData implements IAttributeData { public isDim(): number { return this.bg & BgFlags.DIM; } public isStrikethrough(): number { return this.fg & FgFlags.STRIKETHROUGH; } public isProtected(): number { return this.bg & BgFlags.PROTECTED; } + public isOverline(): number { return this.bg & BgFlags.OVERLINE; } // color modes public getFgColorMode(): number { return this.fg & Attributes.CM_MASK; } diff --git a/src/common/buffer/Constants.ts b/src/common/buffer/Constants.ts index da455794..f6a31be7 100644 --- a/src/common/buffer/Constants.ts +++ b/src/common/buffer/Constants.ts @@ -128,7 +128,8 @@ export const enum BgFlags { ITALIC = 0x4000000, DIM = 0x8000000, HAS_EXTENDED = 0x10000000, - PROTECTED = 0x20000000 + PROTECTED = 0x20000000, + OVERLINE = 0x40000000 } export const enum ExtFlags {