mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge branch 'master' into fix-caps-lock-ime
This commit is contained in:
@@ -189,6 +189,8 @@ Xterm.js is used in several world-class applications to provide great terminal e
|
||||
- [**KubeSail**](https://kubesail.com): The Self-Hosting Company - uses xterm to allow users to exec into kubernetes pods and build github apps
|
||||
- [**WiTTY**](https://github.com/syssecfsu/witty): Web-based interactive terminal emulator that allows users to easily record, share, and replay console sessions.
|
||||
- [**libv86 Terminal Forwarding**](https://github.com/hello-smile6/libv86-terminal-forwarding): Peer-to-peer SSH for the web, using WebRTC via [Bugout](https://github.com/chr15m/bugout) for data transfer and [v86](https://github.com/copy/v86) for web-based virtualization.
|
||||
- [**hack.courses**](https://hack.courses): Interactive Linux and command-line classes using xterm.js to expose a real terminal available for everyone.
|
||||
- [**Render**](https://render.com): Platform-as-a-service for your apps, websites, and databases using xterm.js to provide a command prompt for user containers and for streaming build and runtime logs.
|
||||
- [And much more...](https://github.com/xtermjs/xterm.js/network/dependents?package_id=UGFja2FnZS0xNjYzMjc4OQ%3D%3D)
|
||||
|
||||
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it on our list. Note: Please add any new contributions to the end of the list only.
|
||||
|
||||
@@ -134,7 +134,7 @@ export class SearchAddon implements ITerminalAddon {
|
||||
}
|
||||
this._lastSearchOptions = searchOptions;
|
||||
if (searchOptions?.decorations) {
|
||||
if (this._resultIndex !== undefined || this._cachedSearchTerm && term !== this._cachedSearchTerm) {
|
||||
if (this._resultIndex !== undefined || this._cachedSearchTerm === undefined || term !== this._cachedSearchTerm) {
|
||||
this._highlightAllMatches(term, searchOptions);
|
||||
}
|
||||
}
|
||||
@@ -288,7 +288,9 @@ export class SearchAddon implements ITerminalAddon {
|
||||
}
|
||||
|
||||
if (this._searchResults) {
|
||||
if (this._resultIndex === undefined) {
|
||||
if (this._searchResults.size === 0) {
|
||||
this._resultIndex = -1;
|
||||
} else if (this._resultIndex === undefined) {
|
||||
this._resultIndex = 0;
|
||||
} else {
|
||||
this._resultIndex++;
|
||||
@@ -312,8 +314,10 @@ export class SearchAddon implements ITerminalAddon {
|
||||
throw new Error('Cannot use addon until it has been loaded');
|
||||
}
|
||||
this._lastSearchOptions = searchOptions;
|
||||
if (searchOptions?.decorations && (this._resultIndex !== undefined || term !== this._cachedSearchTerm)) {
|
||||
this._highlightAllMatches(term, searchOptions);
|
||||
if (searchOptions?.decorations) {
|
||||
if (this._resultIndex !== undefined || this._cachedSearchTerm === undefined || term !== this._cachedSearchTerm) {
|
||||
this._highlightAllMatches(term, searchOptions);
|
||||
}
|
||||
}
|
||||
return this._fireResults(term, this._findPreviousAndSelect(term, searchOptions), searchOptions);
|
||||
}
|
||||
@@ -408,12 +412,14 @@ export class SearchAddon implements ITerminalAddon {
|
||||
}
|
||||
|
||||
if (this._searchResults) {
|
||||
if (this._resultIndex === undefined || this._resultIndex < 0) {
|
||||
this._resultIndex = this._searchResults?.size - 1;
|
||||
if (this._searchResults.size === 0) {
|
||||
this._resultIndex = -1;
|
||||
} else if (this._resultIndex === undefined || this._resultIndex < 0) {
|
||||
this._resultIndex = this._searchResults.size - 1;
|
||||
} else {
|
||||
this._resultIndex--;
|
||||
if (this._resultIndex === -1) {
|
||||
this._resultIndex = this._searchResults?.size - 1;
|
||||
this._resultIndex = this._searchResults.size - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -604,7 +610,8 @@ export class SearchAddon implements ITerminalAddon {
|
||||
break;
|
||||
}
|
||||
if (cell.getWidth()) {
|
||||
offset += cell.getChars().length;
|
||||
// Treat null characters as whitespace to align with the translateToString API
|
||||
offset += cell.getCode() === 0 ? 1 : cell.getChars().length;
|
||||
}
|
||||
}
|
||||
lineIndex++;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { assert } from 'chai';
|
||||
import { readFile } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
import { openTerminal, writeSync, launchBrowser } from '../../../out-test/api/TestUtils';
|
||||
import { openTerminal, writeSync, launchBrowser, timeout } from '../../../out-test/api/TestUtils';
|
||||
import { Browser, Page } from 'playwright';
|
||||
|
||||
const APP = 'http://127.0.0.1:3001/test';
|
||||
@@ -23,8 +23,6 @@ describe('Search Tests', function(): void {
|
||||
await page.setViewportSize({ width, height });
|
||||
await page.goto(APP);
|
||||
await openTerminal(page);
|
||||
await page.evaluate(`window.search = new SearchAddon();`);
|
||||
await page.evaluate(`window.term.loadAddon(window.search);`);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
@@ -32,7 +30,12 @@ describe('Search Tests', function(): void {
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await page.evaluate(`window.term.reset()`);
|
||||
await page.evaluate(`
|
||||
window.term.reset()
|
||||
window.search?.dispose();
|
||||
window.search = new SearchAddon();
|
||||
window.term.loadAddon(window.search);
|
||||
`);
|
||||
});
|
||||
|
||||
it('Simple Search', async () => {
|
||||
@@ -120,6 +123,176 @@ describe('Search Tests', function(): void {
|
||||
});
|
||||
});
|
||||
|
||||
describe('onDidChangeResults', async () => {
|
||||
describe('findNext', () => {
|
||||
it('should not fire unless the decorations option is set', async () => {
|
||||
await page.evaluate(`
|
||||
window.calls = [];
|
||||
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||
`);
|
||||
await writeSync(page, 'abc');
|
||||
assert.strictEqual(await page.evaluate(`window.search.findNext('a')`), true);
|
||||
assert.strictEqual(await page.evaluate('window.calls.length'), 0);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findNext('b', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.strictEqual(await page.evaluate('window.calls.length'), 1);
|
||||
});
|
||||
it('should fire with correct event values', async () => {
|
||||
await page.evaluate(`
|
||||
window.calls = [];
|
||||
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||
`);
|
||||
await writeSync(page, 'abc bc c');
|
||||
assert.strictEqual(await page.evaluate(`window.search.findNext('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1, resultIndex: 0 }
|
||||
]);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findNext('b', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 }
|
||||
]);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findNext('d', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), false);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: -1, resultIndex: -1 }
|
||||
]);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findNext('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findNext('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findNext('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: -1, resultIndex: -1 },
|
||||
{ resultCount: 3, resultIndex: 0 },
|
||||
{ resultCount: 3, resultIndex: 1 },
|
||||
{ resultCount: 3, resultIndex: 2 }
|
||||
]);
|
||||
});
|
||||
it('should fire with correct event values (incremental)', async () => {
|
||||
await page.evaluate(`
|
||||
window.calls = [];
|
||||
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||
`);
|
||||
await writeSync(page, 'abc aabc');
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('a', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 0 }
|
||||
]);
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('ab', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 }
|
||||
]);
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('abc', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 }
|
||||
]);
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('abc', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 1 }
|
||||
]);
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('abcd', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), false);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: -1, resultIndex: -1 }
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('findPrevious', () => {
|
||||
it('should not fire unless the decorations option is set', async () => {
|
||||
await page.evaluate(`
|
||||
window.calls = [];
|
||||
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||
`);
|
||||
await writeSync(page, 'abc');
|
||||
assert.strictEqual(await page.evaluate(`window.search.findPrevious('a')`), true);
|
||||
assert.strictEqual(await page.evaluate('window.calls.length'), 0);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findPrevious('b', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.strictEqual(await page.evaluate('window.calls.length'), 1);
|
||||
});
|
||||
it('should fire with correct event values', async () => {
|
||||
await page.evaluate(`
|
||||
window.calls = [];
|
||||
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||
`);
|
||||
await writeSync(page, 'abc bc c');
|
||||
assert.strictEqual(await page.evaluate(`window.search.findPrevious('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1, resultIndex: 0 }
|
||||
]);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findPrevious('b', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 1 }
|
||||
]);
|
||||
await timeout(2000);
|
||||
assert.strictEqual(await page.evaluate(`debugger; window.search.findPrevious('d', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), false);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: -1, resultIndex: -1 }
|
||||
]);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findPrevious('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findPrevious('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findPrevious('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: -1, resultIndex: -1 },
|
||||
{ resultCount: 3, resultIndex: 2 },
|
||||
{ resultCount: 3, resultIndex: 1 },
|
||||
{ resultCount: 3, resultIndex: 0 }
|
||||
]);
|
||||
});
|
||||
it('should fire with correct event values (incremental)', async () => {
|
||||
await page.evaluate(`
|
||||
window.calls = [];
|
||||
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||
`);
|
||||
await writeSync(page, 'abc aabc');
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('a', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 2 }
|
||||
]);
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('ab', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 2 },
|
||||
{ resultCount: 2, resultIndex: 1 }
|
||||
]);
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('abc', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 2 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 2, resultIndex: 1 }
|
||||
]);
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('abc', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 2 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 2, resultIndex: 0 }
|
||||
]);
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('abcd', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), false);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 2 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: -1, resultIndex: -1 }
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Regression tests', () => {
|
||||
describe('#2444 wrapped line content not being found', () => {
|
||||
let fixture: string;
|
||||
@@ -206,6 +379,21 @@ describe('Search Tests', function(): void {
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('#3834 lines with null characters before search terms', () => {
|
||||
// This case can be triggered by the prompt when using starship under conpty
|
||||
it('should find all matches on a line containing null characters', async () => {
|
||||
await page.evaluate(`
|
||||
window.calls = [];
|
||||
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||
`);
|
||||
// Move cursor forward 1 time to create a null character, as opposed to regular whitespace
|
||||
await writeSync(page, '\\x1b[CHi Hi');
|
||||
assert.strictEqual(await page.evaluate(`window.search.findPrevious('h', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 2, resultIndex: 1 }
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function makeData(length: number): string {
|
||||
|
||||
@@ -83,7 +83,7 @@ describe('SerializeAddon', () => {
|
||||
const buffer3 = await page.evaluate(`inspectBuffer(term.buffer.normal);`);
|
||||
|
||||
await page.evaluate(`term.reset();`);
|
||||
await writeRawSync(page, '1234567890n12345');
|
||||
await writeRawSync(page, '123456789012345');
|
||||
const buffer4 = await page.evaluate(`inspectBuffer(term.buffer.normal);`);
|
||||
|
||||
assert.throw(() => {
|
||||
|
||||
@@ -455,8 +455,12 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
}
|
||||
y -= this._terminal.buffer.active.viewportY;
|
||||
if (this._model.selection.columnSelectMode) {
|
||||
return x >= this._model.selection.startCol && y >= this._model.selection.viewportCappedStartRow &&
|
||||
x < this._model.selection.endCol && y < this._model.selection.viewportCappedEndRow;
|
||||
if (this._model.selection.startCol <= this._model.selection.endCol) {
|
||||
return x >= this._model.selection.startCol && y >= this._model.selection.viewportCappedStartRow &&
|
||||
x < this._model.selection.endCol && y <= this._model.selection.viewportCappedEndRow;
|
||||
}
|
||||
return x < this._model.selection.startCol && y >= this._model.selection.viewportCappedStartRow &&
|
||||
x >= this._model.selection.endCol && y <= this._model.selection.viewportCappedEndRow;
|
||||
}
|
||||
return (y > this._model.selection.viewportStartRow && y < this._model.selection.viewportEndRow) ||
|
||||
(this._model.selection.viewportStartRow === this._model.selection.viewportEndRow && y === this._model.selection.viewportStartRow && x >= this._model.selection.startCol && x < this._model.selection.endCol) ||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { IDisposable } from 'xterm';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { channels, rgba } from 'common/Color';
|
||||
import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs';
|
||||
import { isPowerlineGlyph } from 'browser/renderer/RendererUtils';
|
||||
import { excludeFromContrastRatioDemands, isPowerlineGlyph } from 'browser/renderer/RendererUtils';
|
||||
|
||||
// For debugging purposes, it can be useful to set this to a really tiny value,
|
||||
// to verify that LRU eviction works.
|
||||
@@ -217,8 +217,8 @@ export class WebglCharAtlas implements IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
private _getForegroundCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean, isPowerLineGlyph: boolean): string {
|
||||
const minimumContrastCss = this._getMinimumContrastCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold, isPowerLineGlyph);
|
||||
private _getForegroundCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean, excludeFromContrastRatioDemands: boolean): string {
|
||||
const minimumContrastCss = this._getMinimumContrastCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold, excludeFromContrastRatioDemands);
|
||||
if (minimumContrastCss) {
|
||||
return minimumContrastCss;
|
||||
}
|
||||
@@ -282,8 +282,8 @@ export class WebglCharAtlas implements IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
private _getMinimumContrastCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean, isPowerLineGlyph: boolean): string | undefined {
|
||||
if (this._config.minimumContrastRatio === 1 || isPowerLineGlyph) {
|
||||
private _getMinimumContrastCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean, excludeFromContrastRatioDemands: boolean): string | undefined {
|
||||
if (this._config.minimumContrastRatio === 1 || excludeFromContrastRatioDemands) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -322,10 +322,15 @@ export class WebglCharAtlas implements IDisposable {
|
||||
// Allow 1 cell width per character, with a minimum of 2 (CJK), plus some padding. This is used
|
||||
// to draw the glyph to the canvas as well as to restrict the bounding box search to ensure
|
||||
// giant ligatures (eg. =====>) don't impact overall performance.
|
||||
const allowedWidth = this._config.scaledCharWidth * Math.max(chars.length, 2) + TMP_CANVAS_GLYPH_PADDING * 2;
|
||||
const allowedWidth = this._config.scaledCellWidth * Math.max(chars.length, 2) + TMP_CANVAS_GLYPH_PADDING * 2;
|
||||
if (this._tmpCanvas.width < allowedWidth) {
|
||||
this._tmpCanvas.width = allowedWidth;
|
||||
}
|
||||
// Include line height when drawing glyphs
|
||||
const allowedHeight = this._config.scaledCellHeight + TMP_CANVAS_GLYPH_PADDING * 2;
|
||||
if (this._tmpCanvas.height < allowedHeight) {
|
||||
this._tmpCanvas.height = allowedHeight;
|
||||
}
|
||||
this._tmpCtx.save();
|
||||
|
||||
this._workAttributeData.fg = fg;
|
||||
@@ -372,7 +377,7 @@ export class WebglCharAtlas implements IDisposable {
|
||||
this._tmpCtx.textBaseline = TEXT_BASELINE;
|
||||
|
||||
const powerLineGlyph = chars.length === 1 && isPowerlineGlyph(chars.charCodeAt(0));
|
||||
this._tmpCtx.fillStyle = this._getForegroundCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold, powerLineGlyph);
|
||||
this._tmpCtx.fillStyle = this._getForegroundCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold, excludeFromContrastRatioDemands(chars.charCodeAt(0)));
|
||||
|
||||
// Apply alpha to dim the character
|
||||
if (dim) {
|
||||
@@ -485,7 +490,7 @@ export class WebglCharAtlas implements IDisposable {
|
||||
*/
|
||||
private _findGlyphBoundingBox(imageData: ImageData, boundingBox: IBoundingBox, allowedWidth: number, restrictedGlyph: boolean, customGlyph: boolean): IRasterizedGlyph {
|
||||
boundingBox.top = 0;
|
||||
const height = restrictedGlyph ? this._config.scaledCharHeight : this._tmpCanvas.height;
|
||||
const height = restrictedGlyph ? this._config.scaledCellHeight : this._tmpCanvas.height;
|
||||
const width = restrictedGlyph ? this._config.scaledCharWidth : allowedWidth;
|
||||
let found = false;
|
||||
for (let y = 0; y < height; y++) {
|
||||
|
||||
@@ -718,9 +718,10 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
window.term.options.theme = ${JSON.stringify(theme)};
|
||||
window.term.options.minimumContrastRatio = 1;
|
||||
`);
|
||||
// Block characters ignore block elements so a different char is used here
|
||||
await writeSync(page,
|
||||
`\\x1b[30m█\\x1b[31m█\\x1b[32m█\\x1b[33m█\\x1b[34m█\\x1b[35m█\\x1b[36m█\\x1b[37m█\\r\\n` +
|
||||
`\\x1b[90m█\\x1b[91m█\\x1b[92m█\\x1b[93m█\\x1b[94m█\\x1b[95m█\\x1b[96m█\\x1b[97m█`
|
||||
`\\x1b[30m■\\x1b[31m■\\x1b[32m■\\x1b[33m■\\x1b[34m■\\x1b[35m■\\x1b[36m■\\x1b[37m■\\r\\n` +
|
||||
`\\x1b[90m■\\x1b[91m■\\x1b[92m■\\x1b[93m■\\x1b[94m■\\x1b[95m■\\x1b[96m■\\x1b[97m■`
|
||||
);
|
||||
// Validate before minimumContrastRatio is applied
|
||||
await pollFor(page, () => getCellColor(1, 1), [0x2e, 0x34, 0x36, 255]);
|
||||
@@ -786,9 +787,10 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
window.term.options.theme = ${JSON.stringify(theme)};
|
||||
window.term.options.minimumContrastRatio = 1;
|
||||
`);
|
||||
// Block characters ignore block elements so a different char is used here
|
||||
await writeSync(page,
|
||||
`\\x1b[30m█\\x1b[31m█\\x1b[32m█\\x1b[33m█\\x1b[34m█\\x1b[35m█\\x1b[36m█\\x1b[37m█\\r\\n` +
|
||||
`\\x1b[90m█\\x1b[91m█\\x1b[92m█\\x1b[93m█\\x1b[94m█\\x1b[95m█\\x1b[96m█\\x1b[97m█`
|
||||
`\\x1b[30m■\\x1b[31m■\\x1b[32m■\\x1b[33m■\\x1b[34m■\\x1b[35m■\\x1b[36m■\\x1b[37m■\\r\\n` +
|
||||
`\\x1b[90m■\\x1b[91m■\\x1b[92m■\\x1b[93m■\\x1b[94m■\\x1b[95m■\\x1b[96m■\\x1b[97m■`
|
||||
);
|
||||
// Validate before minimumContrastRatio is applied
|
||||
await pollFor(page, () => getCellColor(1, 1), [0x2e, 0x34, 0x36, 255]);
|
||||
|
||||
+67
-2
@@ -94,7 +94,8 @@ const terminalContainer = document.getElementById('terminal-container');
|
||||
const actionElements = {
|
||||
find: <HTMLInputElement>document.querySelector('#find'),
|
||||
findNext: <HTMLInputElement>document.querySelector('#find-next'),
|
||||
findPrevious: <HTMLInputElement>document.querySelector('#find-previous')
|
||||
findPrevious: <HTMLInputElement>document.querySelector('#find-previous'),
|
||||
findResults: document.querySelector('#find-results')
|
||||
};
|
||||
const paddingElement = <HTMLInputElement>document.getElementById('padding');
|
||||
|
||||
@@ -159,6 +160,7 @@ if (document.location.pathname === '/test') {
|
||||
document.getElementById('htmlserialize').addEventListener('click', htmlSerializeButtonHandler);
|
||||
document.getElementById('custom-glyph').addEventListener('click', writeCustomGlyphHandler);
|
||||
document.getElementById('load-test').addEventListener('click', loadTest);
|
||||
document.getElementById('powerline-symbol-test').addEventListener('click', powerlineSymbolTest);
|
||||
document.getElementById('add-decoration').addEventListener('click', addDecoration);
|
||||
document.getElementById('add-overview-ruler').addEventListener('click', addOverviewRuler);
|
||||
}
|
||||
@@ -397,9 +399,12 @@ function initAddons(term: TerminalType): void {
|
||||
if (!addon.canChange) {
|
||||
checkbox.disabled = true;
|
||||
}
|
||||
if(name === 'unicode11' && checkbox.checked) {
|
||||
if (name === 'unicode11' && checkbox.checked) {
|
||||
term.unicode.activeVersion = '11';
|
||||
}
|
||||
if (name === 'search' && checkbox.checked) {
|
||||
addon.instance.onDidChangeResults(e => updateFindResults(e));
|
||||
}
|
||||
addDomListener(checkbox, 'change', () => {
|
||||
if (checkbox.checked) {
|
||||
addon.instance = new addon.ctor();
|
||||
@@ -410,6 +415,8 @@ function initAddons(term: TerminalType): void {
|
||||
}, 0);
|
||||
} else if (name === 'unicode11') {
|
||||
term.unicode.activeVersion = '11';
|
||||
} else if (name === 'search') {
|
||||
addon.instance.onDidChangeResults(e => updateFindResults(e));
|
||||
}
|
||||
} else {
|
||||
if (name === 'webgl') {
|
||||
@@ -438,6 +445,16 @@ function initAddons(term: TerminalType): void {
|
||||
container.appendChild(fragment);
|
||||
}
|
||||
|
||||
function updateFindResults(e: { resultIndex: number, resultCount: number } | undefined) {
|
||||
let content: string;
|
||||
if (e === undefined) {
|
||||
content = 'undefined';
|
||||
} else {
|
||||
content = `index: ${e.resultIndex}, count: ${e.resultCount}`;
|
||||
}
|
||||
actionElements.findResults.textContent = content;
|
||||
}
|
||||
|
||||
function addDomListener(element: HTMLElement, type: string, handler: (...args: any[]) => any): void {
|
||||
element.addEventListener(type, handler);
|
||||
term._core.register({ dispose: () => element.removeEventListener(type, handler) });
|
||||
@@ -558,6 +575,54 @@ function loadTest() {
|
||||
});
|
||||
}
|
||||
|
||||
function powerlineSymbolTest() {
|
||||
function s(char: string): string {
|
||||
return `${char} \x1b[7m${char}\x1b[0m `;
|
||||
}
|
||||
term.write('\n\n\r');
|
||||
term.writeln('Standard powerline symbols:');
|
||||
term.writeln(' 0 1 2 3 4 5 6 7 8 9 A B C D E F');
|
||||
term.writeln(`0xA_ ${s('\ue0a0')}${s('\ue0a1')}${s('\ue0a2')}`);
|
||||
term.writeln(`0xB_ ${s('\ue0b0')}${s('\ue0b1')}${s('\ue0b2')}${s('\ue0b3')}`);
|
||||
term.writeln('');
|
||||
term.writeln(
|
||||
`\x1b[7m` +
|
||||
` inverse \ue0b1 \x1b[0;40m\ue0b0` +
|
||||
` 0 \ue0b1 \x1b[30;41m\ue0b0\x1b[39m` +
|
||||
` 1 \ue0b1 \x1b[31;42m\ue0b0\x1b[39m` +
|
||||
` 2 \ue0b1 \x1b[32;43m\ue0b0\x1b[39m` +
|
||||
` 3 \ue0b1 \x1b[33;44m\ue0b0\x1b[39m` +
|
||||
` 4 \ue0b1 \x1b[34;45m\ue0b0\x1b[39m` +
|
||||
` 5 \ue0b1 \x1b[35;46m\ue0b0\x1b[39m` +
|
||||
` 6 \ue0b1 \x1b[36;47m\ue0b0\x1b[39m` +
|
||||
` 7 \ue0b1 \x1b[37;49m\ue0b0\x1b[0m`
|
||||
);
|
||||
term.writeln('');
|
||||
term.writeln(
|
||||
`\x1b[7m` +
|
||||
` inverse \ue0b3 \x1b[0;7;40m\ue0b2\x1b[27m` +
|
||||
` 0 \ue0b3 \x1b[7;30;41m\ue0b2\x1b[27;39m` +
|
||||
` 1 \ue0b3 \x1b[7;31;42m\ue0b2\x1b[27;39m` +
|
||||
` 2 \ue0b3 \x1b[7;32;43m\ue0b2\x1b[27;39m` +
|
||||
` 3 \ue0b3 \x1b[7;33;44m\ue0b2\x1b[27;39m` +
|
||||
` 4 \ue0b3 \x1b[7;34;45m\ue0b2\x1b[27;39m` +
|
||||
` 5 \ue0b3 \x1b[7;35;46m\ue0b2\x1b[27;39m` +
|
||||
` 6 \ue0b3 \x1b[7;36;47m\ue0b2\x1b[27;39m` +
|
||||
` 7 \ue0b3 \x1b[7;37;49m\ue0b2\x1b[0m`
|
||||
);
|
||||
term.writeln('');
|
||||
term.writeln('Powerline extra symbols:');
|
||||
term.writeln(' 0 1 2 3 4 5 6 7 8 9 A B C D E F');
|
||||
term.writeln(`0xA_ ${s('\ue0a3')}`);
|
||||
term.writeln(`0xB_ ${s('\ue0b4')}${s('\ue0b5')}${s('\ue0b6')}${s('\ue0b7')}${s('\ue0b8')}${s('\ue0b9')}${s('\ue0ba')}${s('\ue0bb')}${s('\ue0bc')}${s('\ue0bd')}${s('\ue0be')}${s('\ue0bf')}`);
|
||||
term.writeln(`0xC_ ${s('\ue0c0')}${s('\ue0c1')}${s('\ue0c2')}${s('\ue0c3')}${s('\ue0c4')}${s('\ue0c5')}${s('\ue0c6')}${s('\ue0c7')}${s('\ue0c8')}${s('\ue0c9')}${s('\ue0ca')}${s('\ue0cb')}${s('\ue0cc')}${s('\ue0cd')}${s('\ue0be')}${s('\ue0bf')}`);
|
||||
term.writeln(`0xD_ ${s('\ue0d0')}${s('\ue0d1')}${s('\ue0d2')} ${s('\ue0d4')}`);
|
||||
term.writeln('');
|
||||
term.writeln('Sample of nerd fonts icons:');
|
||||
term.writeln(' nf-linux-apple (\\uF302) \uf302');
|
||||
term.writeln('nf-mdi-github_face (\\uFbd9) \ufbd9');
|
||||
}
|
||||
|
||||
function addDecoration() {
|
||||
term.options['overviewRulerWidth'] = 15;
|
||||
const marker = term.addMarker(1);
|
||||
|
||||
+16
-5
@@ -40,6 +40,7 @@
|
||||
<div style= "display:flex; flex-direction:column;">
|
||||
<label>Find next <input id="find-next"/></label>
|
||||
<label>Find previous <input id="find-previous"/></label>
|
||||
<div>Results: <span id="find-results"></span></div>
|
||||
<label><input type="checkbox" id="regex"/>Use regex</label>
|
||||
<label><input type="checkbox" id="case-sensitive"/>Case sensitive</label>
|
||||
<label><input type="checkbox" id="whole-word"/>Whole word</label>
|
||||
@@ -66,11 +67,21 @@
|
||||
<div id="test" class="tabContent">
|
||||
<h3>Test</h3>
|
||||
<div style="display: inline-block; margin-right: 16px;">
|
||||
<button id="dispose" title="This is used to testing memory leaks">Dispose terminal</button>
|
||||
<button id="custom-glyph" title="Write custom box drawing and block element characters to the terminal">Test custom glyphs</button>
|
||||
<button id="load-test" title="Write several MB of data to simulate a lot of data coming from the process">Load test</button>
|
||||
<button id="add-decoration" title="Add a decoration to the terminal">Decoration</button>
|
||||
<button id="add-overview-ruler" title="Add an overview ruler to the terminal">Add Overview Ruler</button>
|
||||
<dl>
|
||||
<dt>Lifecycle</dt>
|
||||
<dd><button id="dispose" title="This is used to testing memory leaks">Dispose terminal</button></dd>
|
||||
|
||||
<dt>Performance</dt>
|
||||
<dd><button id="load-test" title="Write several MB of data to simulate a lot of data coming from the process">Load test</button></dd>
|
||||
|
||||
<dt>Styles</dt>
|
||||
<dd><button id="custom-glyph" title="Write custom box drawing and block element characters to the terminal">Test custom glyphs</button></dd>
|
||||
<dd><button id="powerline-symbol-test" title="Write powerline symbol characters to the terminal (\ue0a0+)">Powerline symbol test</button></dd>
|
||||
|
||||
<dt>Decorations</dt>
|
||||
<dd><button id="add-decoration" title="Add a decoration to the terminal">Decoration</button></dd>
|
||||
<dd><button id="add-overview-ruler" title="Add an overview ruler to the terminal">Add Overview Ruler</button></dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+12
-6
@@ -703,10 +703,13 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
|
||||
break;
|
||||
case 'wheel':
|
||||
// only UP/DOWN wheel events are respected
|
||||
if ((ev as WheelEvent).deltaY !== 0) {
|
||||
action = (ev as WheelEvent).deltaY < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN;
|
||||
const amount = self.viewport!.getLinesScrolled(ev as WheelEvent);
|
||||
|
||||
if (amount === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
action = (ev as WheelEvent).deltaY < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN;
|
||||
but = CoreMouseButton.WHEEL;
|
||||
break;
|
||||
default:
|
||||
@@ -1096,14 +1099,17 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this._compositionHelper!.keydown(event)) {
|
||||
// Ignore composing with Alt key on Mac when macOptionIsMeta is enabled
|
||||
const shouldIgnoreComposition = this.browser.isMac && this.options.macOptionIsMeta && event.altKey;
|
||||
|
||||
if (!shouldIgnoreComposition && !this._compositionHelper!.keydown(event)) {
|
||||
if (this.buffer.ybase !== this.buffer.ydisp) {
|
||||
this._bufferService.scrollToBottom();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (event.key === 'Dead' || event.key === 'AltGraph') {
|
||||
if (!shouldIgnoreComposition && (event.key === 'Dead' || event.key === 'AltGraph')) {
|
||||
this._unprocessedDeadKey = true;
|
||||
}
|
||||
|
||||
@@ -1334,7 +1340,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
// Don't clear if it's already clear
|
||||
return;
|
||||
}
|
||||
this.buffer.clearAllMarkers(0);
|
||||
this.buffer.clearAllMarkers();
|
||||
this.buffer.lines.set(0, this.buffer.lines.get(this.buffer.ybase + this.buffer.y)!);
|
||||
this.buffer.lines.length = 1;
|
||||
this.buffer.ydisp = 0;
|
||||
|
||||
@@ -262,7 +262,7 @@ export class MockBuffer implements IBuffer {
|
||||
public clearMarkers(y: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public clearAllMarkers(excludeY: number): void {
|
||||
public clearAllMarkers(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ export class OverviewRulerRenderer extends Disposable {
|
||||
}));
|
||||
this.register(this._bufferService.onScroll(() => {
|
||||
if (this._lastKnownBufferLength !== this._bufferService.buffers.normal.lines.length) {
|
||||
this._refreshDrawHeightConstants();
|
||||
this._refreshColorZonePadding();
|
||||
}
|
||||
}));
|
||||
@@ -132,10 +133,7 @@ export class OverviewRulerRenderer extends Disposable {
|
||||
drawWidth.center = innerWidth;
|
||||
drawWidth.right = outerWidth;
|
||||
// height
|
||||
drawHeight.full = Math.round(2 * window.devicePixelRatio);
|
||||
drawHeight.left = Math.round(6 * window.devicePixelRatio);
|
||||
drawHeight.center = Math.round(6 * window.devicePixelRatio);
|
||||
drawHeight.right = Math.round(6 * window.devicePixelRatio);
|
||||
this._refreshDrawHeightConstants();
|
||||
// x
|
||||
drawX.full = 0;
|
||||
drawX.left = 0;
|
||||
@@ -143,6 +141,17 @@ export class OverviewRulerRenderer extends Disposable {
|
||||
drawX.right = drawWidth.left + drawWidth.center;
|
||||
}
|
||||
|
||||
private _refreshDrawHeightConstants(): void {
|
||||
drawHeight.full = Math.round(2 * window.devicePixelRatio);
|
||||
// Calculate actual pixels per line
|
||||
const pixelsPerLine = this._canvas.height / this._bufferService.buffer.lines.length;
|
||||
// Clamp actual pixels within a range
|
||||
const nonFullHeight = Math.round(Math.max(Math.min(pixelsPerLine, 12), 6) * window.devicePixelRatio);
|
||||
drawHeight.left = nonFullHeight;
|
||||
drawHeight.center = nonFullHeight;
|
||||
drawHeight.right = nonFullHeight;
|
||||
}
|
||||
|
||||
private _refreshColorZonePadding(): void {
|
||||
this._colorZoneStore.setPadding({
|
||||
full: Math.floor(this._bufferService.buffers.active.lines.length / (this._canvas.height - 1) * drawHeight.full),
|
||||
|
||||
@@ -14,7 +14,7 @@ import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { isPowerlineGlyph, throwIfFalsy } from 'browser/renderer/RendererUtils';
|
||||
import { excludeFromContrastRatioDemands, throwIfFalsy } from 'browser/renderer/RendererUtils';
|
||||
import { channels, color, rgba } from 'common/Color';
|
||||
import { removeElementFromParent } from 'browser/Dom';
|
||||
import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs';
|
||||
@@ -473,7 +473,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
}
|
||||
}
|
||||
|
||||
if (!bgOverride && !fgOverride && (this._optionsService.rawOptions.minimumContrastRatio === 1 || isPowerlineGlyph(cell.getCode()))) {
|
||||
if (!bgOverride && !fgOverride && (this._optionsService.rawOptions.minimumContrastRatio === 1 || excludeFromContrastRatioDemands(cell.getCode()))) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -325,6 +325,37 @@ export const boxDrawingDefinitions: { [character: string]: { [fontWeight: number
|
||||
'╰': { [Style.NORMAL]: 'C.5,0,.5,.5,1,.5' }
|
||||
};
|
||||
|
||||
interface IVectorShape {
|
||||
d: string;
|
||||
type: VectorType;
|
||||
/** Padding to apply to the vector's x axis in CSS pixels. */
|
||||
horizontalPadding?: number;
|
||||
}
|
||||
|
||||
const enum VectorType {
|
||||
FILL,
|
||||
STROKE
|
||||
}
|
||||
|
||||
/**
|
||||
* This contains the definitions of the primarily used box drawing characters as vector shapes. The
|
||||
* reason these characters are defined specially is to avoid common problems if a user's font has
|
||||
* not been patched with powerline characters and also to get pixel perfect rendering as rendering
|
||||
* issues can occur around AA/SPAA.
|
||||
*
|
||||
* Original symbols defined in https://github.com/powerline/fontpatcher
|
||||
*/
|
||||
export const powerlineDefinitions: { [index: string]: IVectorShape } = {
|
||||
// Right triangle solid
|
||||
'\u{E0B0}': { d: 'M0,0 L1,.5 L0,1', type: VectorType.FILL },
|
||||
// Right triangle line
|
||||
'\u{E0B1}': { d: 'M0,0 L1,.5 L0,1', type: VectorType.STROKE, horizontalPadding: 0.5 },
|
||||
// Left triangle solid
|
||||
'\u{E0B2}': { d: 'M1,0 L0,.5 L1,1', type: VectorType.FILL },
|
||||
// Left triangle line
|
||||
'\u{E0B3}': { d: 'M1,0 L0,.5 L1,1', type: VectorType.STROKE, horizontalPadding: 0.5 }
|
||||
};
|
||||
|
||||
/**
|
||||
* Try drawing a custom block element or box drawing character, returning whether it was
|
||||
* successfully drawn.
|
||||
@@ -355,6 +386,12 @@ export function tryDrawCustomChar(
|
||||
return true;
|
||||
}
|
||||
|
||||
const powerlineDefinition = powerlineDefinitions[c];
|
||||
if (powerlineDefinition) {
|
||||
drawPowerlineChar(ctx, powerlineDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -518,6 +555,38 @@ function drawBoxDrawingChar(
|
||||
}
|
||||
}
|
||||
|
||||
function drawPowerlineChar(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
charDefinition: IVectorShape,
|
||||
xOffset: number,
|
||||
yOffset: number,
|
||||
scaledCellWidth: number,
|
||||
scaledCellHeight: number
|
||||
): void {
|
||||
ctx.beginPath();
|
||||
ctx.lineWidth = window.devicePixelRatio;
|
||||
for (const instruction of charDefinition.d.split(' ')) {
|
||||
const type = instruction[0];
|
||||
const f = svgToCanvasInstructionMap[type];
|
||||
if (!f) {
|
||||
console.error(`Could not find drawing instructions for "${type}"`);
|
||||
continue;
|
||||
}
|
||||
const args: string[] = instruction.substring(1).split(',');
|
||||
if (!args[0] || !args[1]) {
|
||||
continue;
|
||||
}
|
||||
f(ctx, translateArgs(args, scaledCellWidth, scaledCellHeight, xOffset, yOffset, charDefinition.horizontalPadding));
|
||||
}
|
||||
if (charDefinition.type === VectorType.STROKE) {
|
||||
ctx.strokeStyle = ctx.fillStyle;
|
||||
ctx.stroke();
|
||||
} else {
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
function clamp(value: number, max: number, min: number = 0): number {
|
||||
return Math.max(Math.min(value, max), min);
|
||||
}
|
||||
@@ -528,7 +597,7 @@ const svgToCanvasInstructionMap: { [index: string]: any } = {
|
||||
'M': (ctx: CanvasRenderingContext2D, args: number[]) => ctx.moveTo(args[0], args[1])
|
||||
};
|
||||
|
||||
function translateArgs(args: string[], cellWidth: number, cellHeight: number, xOffset: number, yOffset: number): number[] {
|
||||
function translateArgs(args: string[], cellWidth: number, cellHeight: number, xOffset: number, yOffset: number, horizontalPadding: number = 0): number[] {
|
||||
const result = args.map(e => parseFloat(e) || parseInt(e));
|
||||
|
||||
if (result.length < 2) {
|
||||
@@ -537,14 +606,14 @@ function translateArgs(args: string[], cellWidth: number, cellHeight: number, xO
|
||||
|
||||
for (let x = 0; x < result.length; x += 2) {
|
||||
// Translate from 0-1 to 0-cellWidth
|
||||
result[x] *= cellWidth;
|
||||
result[x] *= cellWidth - (horizontalPadding * 2 * window.devicePixelRatio);
|
||||
// Ensure coordinate doesn't escape cell bounds and round to the nearest 0.5 to ensure a crisp
|
||||
// line at 100% devicePixelRatio
|
||||
if (result[x] !== 0) {
|
||||
result[x] = clamp(Math.round(result[x] + 0.5) - 0.5, cellWidth, 0);
|
||||
}
|
||||
// Apply the cell's offset (ie. x*cellWidth)
|
||||
result[x] += xOffset;
|
||||
result[x] += xOffset + (horizontalPadding * window.devicePixelRatio);
|
||||
}
|
||||
|
||||
for (let y = 1; y < result.length; y += 2) {
|
||||
|
||||
@@ -14,5 +14,13 @@ export function isPowerlineGlyph(codepoint: number): boolean {
|
||||
// Only return true for Powerline symbols which require
|
||||
// different padding and should be excluded from minimum contrast
|
||||
// ratio standards
|
||||
return 0xE0A0 <= codepoint && codepoint <= 0xE0D6;
|
||||
return 0xE0A4 <= codepoint && codepoint <= 0xE0D6;
|
||||
}
|
||||
|
||||
function isBoxOrBlockGlyph(codepoint: number): boolean {
|
||||
return (0x2500 <= codepoint && codepoint <= 0x259F);
|
||||
}
|
||||
|
||||
export function excludeFromContrastRatioDemands(codepoint: number): boolean {
|
||||
return isPowerlineGlyph(codepoint) || isBoxOrBlockGlyph(codepoint);
|
||||
}
|
||||
|
||||
@@ -226,7 +226,7 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
`}` +
|
||||
`${this._terminalSelector} .${SELECTION_CLASS} div {` +
|
||||
` position: absolute;` +
|
||||
` background-color: ${this._colors.selectionTransparent.css};` +
|
||||
` background-color: ${this._colors.selectionOpaque.css};` +
|
||||
`}`;
|
||||
// Colors
|
||||
this._colors.ansi.forEach((c, i) => {
|
||||
@@ -304,8 +304,9 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
const documentFragment = document.createDocumentFragment();
|
||||
|
||||
if (columnSelectMode) {
|
||||
const isXFlipped = start[0] > end[0];
|
||||
documentFragment.appendChild(
|
||||
this._createSelectionElement(viewportCappedStartRow, start[0], end[0], viewportCappedEndRow - viewportCappedStartRow + 1)
|
||||
this._createSelectionElement(viewportCappedStartRow, isXFlipped ? end[0] : start[0], isXFlipped ? start[0] : end[0], viewportCappedEndRow - viewportCappedStartRow + 1)
|
||||
);
|
||||
} else {
|
||||
// Draw first row
|
||||
|
||||
@@ -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, MockSelectionService } from 'browser/TestUtils.test';
|
||||
import { MockCharacterJoinerService } from 'browser/TestUtils.test';
|
||||
|
||||
describe('DomRendererRowFactory', () => {
|
||||
let dom: jsdom.JSDOM;
|
||||
@@ -184,7 +184,7 @@ describe('DomRendererRowFactory', () => {
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-fg-1 xterm-bg-2">a</span>'
|
||||
'<span class="xterm-bg-2 xterm-fg-1">a</span>'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -195,7 +195,7 @@ describe('DomRendererRowFactory', () => {
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-fg-1 xterm-bg-257">a</span>'
|
||||
'<span class="xterm-bg-257 xterm-fg-1">a</span>'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -205,7 +205,7 @@ describe('DomRendererRowFactory', () => {
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-fg-257 xterm-bg-1">a</span>'
|
||||
'<span class="xterm-bg-1 xterm-fg-257">a</span>'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -230,7 +230,7 @@ describe('DomRendererRowFactory', () => {
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span style="color:#010203;background-color:#040506;">a</span>'
|
||||
'<span style="background-color:#040506;color:#010203;">a</span>'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -241,7 +241,27 @@ describe('DomRendererRowFactory', () => {
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span style="color:#040506;background-color:#010203;">a</span>'
|
||||
'<span style="background-color:#010203;color:#040506;">a</span>'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectionForeground', () => {
|
||||
it('should force selected cells with content to be rendered above the background', () => {
|
||||
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]));
|
||||
rowFactory.onSelectionChanged([1, 0], [2, 0], false);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span>a</span><span class="xterm-decoration-top">b</span>'
|
||||
);
|
||||
});
|
||||
it('should force whitespace cells to be rendered above the background', () => {
|
||||
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
rowFactory.onSelectionChanged([0, 0], [2, 0], false);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-decoration-top"> </span><span class="xterm-decoration-top">a</span>'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ import { color, rgba } from 'common/Color';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { ICharacterJoinerService, ISelectionService } from 'browser/services/Services';
|
||||
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
|
||||
import { isPowerlineGlyph } from 'browser/renderer/RendererUtils';
|
||||
import { excludeFromContrastRatioDemands } from 'browser/renderer/RendererUtils';
|
||||
|
||||
export const BOLD_CLASS = 'xterm-bold';
|
||||
export const DIM_CLASS = 'xterm-dim';
|
||||
@@ -206,19 +206,49 @@ export class DomRendererRowFactory {
|
||||
}
|
||||
|
||||
// Apply selection foreground if applicable
|
||||
const isInSelection = this._isCellInSelection(x, row);
|
||||
if (!isTop) {
|
||||
if (this._colors.selectionForeground && this._isCellInSelection(x, row)) {
|
||||
if (this._colors.selectionForeground && isInSelection) {
|
||||
fgColorMode = Attributes.CM_RGB;
|
||||
fg = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
|
||||
fgOverride = this._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._colors.selectionOpaque;
|
||||
isTop = true;
|
||||
}
|
||||
|
||||
// If it's a top decoration, render above the selection
|
||||
if (isTop) {
|
||||
charElement.classList.add(`xterm-decoration-top`);
|
||||
}
|
||||
|
||||
// Background
|
||||
let resolvedBg: IColor;
|
||||
switch (bgColorMode) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256:
|
||||
resolvedBg = this._colors.ansi[bg];
|
||||
charElement.classList.add(`xterm-bg-${bg}`);
|
||||
break;
|
||||
case Attributes.CM_RGB:
|
||||
resolvedBg = rgba.toColor(bg >> 16, bg >> 8 & 0xFF, bg & 0xFF);
|
||||
this._addStyle(charElement, `background-color:#${padStart((bg >>> 0).toString(16), '0', 6)}`);
|
||||
break;
|
||||
case Attributes.CM_DEFAULT:
|
||||
default:
|
||||
if (isInverse) {
|
||||
resolvedBg = this._colors.foreground;
|
||||
charElement.classList.add(`xterm-bg-${INVERTED_DEFAULT_COLOR}`);
|
||||
} else {
|
||||
resolvedBg = this._colors.background;
|
||||
}
|
||||
}
|
||||
|
||||
// Foreground
|
||||
switch (fgColorMode) {
|
||||
case Attributes.CM_P16:
|
||||
@@ -226,7 +256,7 @@ export class DomRendererRowFactory {
|
||||
if (cell.isBold() && fg < 8 && this._optionsService.rawOptions.drawBoldTextInBrightColors) {
|
||||
fg += 8;
|
||||
}
|
||||
if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.ansi[fg], cell, undefined, undefined)) {
|
||||
if (!this._applyMinimumContrast(charElement, resolvedBg, this._colors.ansi[fg], cell, bgOverride, undefined)) {
|
||||
charElement.classList.add(`xterm-fg-${fg}`);
|
||||
}
|
||||
break;
|
||||
@@ -236,35 +266,19 @@ export class DomRendererRowFactory {
|
||||
(fg >> 8) & 0xFF,
|
||||
(fg ) & 0xFF
|
||||
);
|
||||
if (!this._applyMinimumContrast(charElement, this._colors.background, color, cell, bgOverride, fgOverride)) {
|
||||
if (!this._applyMinimumContrast(charElement, resolvedBg, color, cell, bgOverride, fgOverride)) {
|
||||
this._addStyle(charElement, `color:#${padStart(fg.toString(16), '0', 6)}`);
|
||||
}
|
||||
break;
|
||||
case Attributes.CM_DEFAULT:
|
||||
default:
|
||||
if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.foreground, cell, undefined, undefined)) {
|
||||
if (!this._applyMinimumContrast(charElement, resolvedBg, this._colors.foreground, cell, bgOverride, undefined)) {
|
||||
if (isInverse) {
|
||||
charElement.classList.add(`xterm-fg-${INVERTED_DEFAULT_COLOR}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Background
|
||||
switch (bgColorMode) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256:
|
||||
charElement.classList.add(`xterm-bg-${bg}`);
|
||||
break;
|
||||
case Attributes.CM_RGB:
|
||||
this._addStyle(charElement, `background-color:#${padStart((bg >>> 0).toString(16), '0', 6)}`);
|
||||
break;
|
||||
case Attributes.CM_DEFAULT:
|
||||
default:
|
||||
if (isInverse) {
|
||||
charElement.classList.add(`xterm-bg-${INVERTED_DEFAULT_COLOR}`);
|
||||
}
|
||||
}
|
||||
|
||||
fragment.appendChild(charElement);
|
||||
|
||||
x = lastCharX;
|
||||
@@ -273,22 +287,20 @@ export class DomRendererRowFactory {
|
||||
}
|
||||
|
||||
private _applyMinimumContrast(element: HTMLElement, bg: IColor, fg: IColor, cell: ICellData, bgOverride: IColor | undefined, fgOverride: IColor | undefined): boolean {
|
||||
if (this._optionsService.rawOptions.minimumContrastRatio === 1 || isPowerlineGlyph(cell.getCode())) {
|
||||
if (this._optionsService.rawOptions.minimumContrastRatio === 1 || excludeFromContrastRatioDemands(cell.getCode())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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(this._workCell.bg, this._workCell.fg);
|
||||
if (!bgOverride && !fgOverride) {
|
||||
adjustedColor = this._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);
|
||||
if (!bgOverride || !fgOverride) {
|
||||
this._colors.contrastCache.setColor(this._workCell.bg, this._workCell.fg, adjustedColor ?? null);
|
||||
}
|
||||
this._colors.contrastCache.setColor((bgOverride || bg).rgba, (fgOverride || fg).rgba, adjustedColor ?? null);
|
||||
}
|
||||
|
||||
if (adjustedColor) {
|
||||
@@ -310,8 +322,12 @@ export class DomRendererRowFactory {
|
||||
return false;
|
||||
}
|
||||
if (this._columnSelectMode) {
|
||||
return x >= start[0] && y >= start[1] &&
|
||||
x < end[0] && y < end[1];
|
||||
if (start[0] <= end[0]) {
|
||||
return x >= start[0] && y >= start[1] &&
|
||||
x < end[0] && y <= end[1];
|
||||
}
|
||||
return x < start[0] && y >= start[1] &&
|
||||
x >= end[0] && y <= end[1];
|
||||
}
|
||||
return (y > start[1] && y < end[1]) ||
|
||||
(start[1] === end[1] && y === start[1] && x >= start[0] && x < end[0]) ||
|
||||
|
||||
@@ -207,8 +207,12 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
return '';
|
||||
}
|
||||
|
||||
// For column selection it's not enough to rely on final selection's swapping of reversed
|
||||
// values, it also needs the x coordinates to swap independently of the y coordinate is needed
|
||||
const startCol = start[0] < end[0] ? start[0] : end[0];
|
||||
const endCol = start[0] < end[0] ? end[0] : start[0];
|
||||
for (let i = start[1]; i <= end[1]; i++) {
|
||||
const lineText = buffer.translateBufferLineToString(i, true, start[0], end[0]);
|
||||
const lineText = buffer.translateBufferLineToString(i, true, startCol, endCol);
|
||||
result.push(lineText);
|
||||
}
|
||||
} else {
|
||||
|
||||
Vendored
+1
@@ -50,6 +50,7 @@ export interface IKeyboardEvent {
|
||||
keyCode: number;
|
||||
key: string;
|
||||
type: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
export interface IScrollEvent {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user