mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge branch 'v3' into zmodem
This commit is contained in:
@@ -101,6 +101,7 @@ Since xterm.js is typically implemented as a developer tool, only modern browser
|
||||
- Edge latest
|
||||
- Firefox latest
|
||||
- Safari latest
|
||||
- IE11
|
||||
|
||||
Xterm.js works seamlessly in Electron apps and may even work on earlier versions of the browsers but these are the browsers we strive to keep working.
|
||||
|
||||
@@ -129,7 +130,7 @@ Xterm.js is used in several world-class applications to provide great terminal e
|
||||
- [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible
|
||||
computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages.
|
||||
- [**Theia**](https://github.com/theia-ide/theia): Theia is a cloud & desktop IDE framework implemented in TypeScript.
|
||||
- [**DevOps Helper**](https://github.com/ricktbaker/devops_helper) DevOps Helper tool to make life easier working with AWS instances across multiple organizations.
|
||||
- [**Opshell**](https://github.com/ricktbaker/opshell) Ops Helper tool to make life easier working with AWS instances across multiple organizations.
|
||||
|
||||
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 in our list.
|
||||
|
||||
|
||||
+1
-1
@@ -95,7 +95,7 @@ function createTerminal() {
|
||||
term.fit();
|
||||
|
||||
// fit is called within a setTimeout, cols and rows need this.
|
||||
setTimeout(() => {
|
||||
setTimeout(function () {
|
||||
colsElement.value = term.cols;
|
||||
rowsElement.value = term.rows;
|
||||
|
||||
|
||||
@@ -140,6 +140,7 @@ namespace methods_core {
|
||||
const r18: (data: string) => void = t.getOption('handler');
|
||||
const r19: string = t.getOption('bellSound');
|
||||
const r20: string = t.getOption('bellStyle');
|
||||
const r22: number = t.getOption('letterSpacing');
|
||||
}
|
||||
{
|
||||
const t: Terminal = new Terminal();
|
||||
@@ -157,6 +158,7 @@ namespace methods_core {
|
||||
t.setOption('useFlowControl', true);
|
||||
t.setOption('visualBell', true);
|
||||
t.setOption('colors', ['a', 'b']);
|
||||
t.setOption('letterSpacing', 1);
|
||||
t.setOption('cols', 1);
|
||||
t.setOption('rows', 1);
|
||||
t.setOption('tabStopWidth', 1);
|
||||
|
||||
+2
-2
@@ -25,8 +25,8 @@ let outDir = tsProject.config.compilerOptions.outDir;
|
||||
|
||||
// Under some environments like TravisCI, this comes out at absolute which can
|
||||
// break the build. This ensures that the outDir is absolute.
|
||||
if (outDir.indexOf(__dirname) !== 0) {
|
||||
outDir = `${__dirname}/${outDir}`;
|
||||
if (path.normalize(outDir).indexOf(__dirname) !== 0) {
|
||||
outDir = `${__dirname}/${path.normalize(outDir)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+25
-8
@@ -208,12 +208,19 @@ export class Buffer implements IBuffer {
|
||||
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);
|
||||
if (!line) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Initialize column and index values. Column values represent the actual
|
||||
// cell column, indexes represent the index in the string. Indexes are
|
||||
// needed here because some chars are 0 characters long (eg. after wide
|
||||
// chars) and some chars are longer than 1 characters long (eg. emojis).
|
||||
let startIndex = startCol;
|
||||
endCol = endCol || line.length;
|
||||
let endIndex = endCol;
|
||||
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
const char = line[i];
|
||||
lineString += char[CHAR_DATA_CHAR_INDEX];
|
||||
@@ -221,29 +228,39 @@ export class Buffer implements IBuffer {
|
||||
// column indexes
|
||||
if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
|
||||
if (startCol >= i) {
|
||||
widthAdjustedStartCol--;
|
||||
startIndex--;
|
||||
}
|
||||
if (endCol >= i) {
|
||||
widthAdjustedEndCol--;
|
||||
endIndex--;
|
||||
}
|
||||
} else {
|
||||
// Adjust the columns to take glyphs that are represented by multiple
|
||||
// code points into account.
|
||||
if (char[CHAR_DATA_CHAR_INDEX].length > 1) {
|
||||
if (startCol > i) {
|
||||
startIndex += char[CHAR_DATA_CHAR_INDEX].length - 1;
|
||||
}
|
||||
if (endCol > i) {
|
||||
endIndex += char[CHAR_DATA_CHAR_INDEX].length - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
endIndex = Math.min(endIndex, rightWhitespaceIndex);
|
||||
}
|
||||
// Return the empty string if only trimmed whitespace is selected
|
||||
if (finalEndCol <= widthAdjustedStartCol) {
|
||||
if (endIndex <= startIndex) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
return lineString.substring(widthAdjustedStartCol, finalEndCol);
|
||||
return lineString.substring(startIndex, endIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -67,142 +67,3 @@ describe('InputHandler', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const old_wcwidth = (function(opts: {nul: number, control: number}): (ucs: number) => number {
|
||||
// extracted from https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c
|
||||
// combining characters
|
||||
const COMBINING = [
|
||||
[0x0300, 0x036F], [0x0483, 0x0486], [0x0488, 0x0489],
|
||||
[0x0591, 0x05BD], [0x05BF, 0x05BF], [0x05C1, 0x05C2],
|
||||
[0x05C4, 0x05C5], [0x05C7, 0x05C7], [0x0600, 0x0603],
|
||||
[0x0610, 0x0615], [0x064B, 0x065E], [0x0670, 0x0670],
|
||||
[0x06D6, 0x06E4], [0x06E7, 0x06E8], [0x06EA, 0x06ED],
|
||||
[0x070F, 0x070F], [0x0711, 0x0711], [0x0730, 0x074A],
|
||||
[0x07A6, 0x07B0], [0x07EB, 0x07F3], [0x0901, 0x0902],
|
||||
[0x093C, 0x093C], [0x0941, 0x0948], [0x094D, 0x094D],
|
||||
[0x0951, 0x0954], [0x0962, 0x0963], [0x0981, 0x0981],
|
||||
[0x09BC, 0x09BC], [0x09C1, 0x09C4], [0x09CD, 0x09CD],
|
||||
[0x09E2, 0x09E3], [0x0A01, 0x0A02], [0x0A3C, 0x0A3C],
|
||||
[0x0A41, 0x0A42], [0x0A47, 0x0A48], [0x0A4B, 0x0A4D],
|
||||
[0x0A70, 0x0A71], [0x0A81, 0x0A82], [0x0ABC, 0x0ABC],
|
||||
[0x0AC1, 0x0AC5], [0x0AC7, 0x0AC8], [0x0ACD, 0x0ACD],
|
||||
[0x0AE2, 0x0AE3], [0x0B01, 0x0B01], [0x0B3C, 0x0B3C],
|
||||
[0x0B3F, 0x0B3F], [0x0B41, 0x0B43], [0x0B4D, 0x0B4D],
|
||||
[0x0B56, 0x0B56], [0x0B82, 0x0B82], [0x0BC0, 0x0BC0],
|
||||
[0x0BCD, 0x0BCD], [0x0C3E, 0x0C40], [0x0C46, 0x0C48],
|
||||
[0x0C4A, 0x0C4D], [0x0C55, 0x0C56], [0x0CBC, 0x0CBC],
|
||||
[0x0CBF, 0x0CBF], [0x0CC6, 0x0CC6], [0x0CCC, 0x0CCD],
|
||||
[0x0CE2, 0x0CE3], [0x0D41, 0x0D43], [0x0D4D, 0x0D4D],
|
||||
[0x0DCA, 0x0DCA], [0x0DD2, 0x0DD4], [0x0DD6, 0x0DD6],
|
||||
[0x0E31, 0x0E31], [0x0E34, 0x0E3A], [0x0E47, 0x0E4E],
|
||||
[0x0EB1, 0x0EB1], [0x0EB4, 0x0EB9], [0x0EBB, 0x0EBC],
|
||||
[0x0EC8, 0x0ECD], [0x0F18, 0x0F19], [0x0F35, 0x0F35],
|
||||
[0x0F37, 0x0F37], [0x0F39, 0x0F39], [0x0F71, 0x0F7E],
|
||||
[0x0F80, 0x0F84], [0x0F86, 0x0F87], [0x0F90, 0x0F97],
|
||||
[0x0F99, 0x0FBC], [0x0FC6, 0x0FC6], [0x102D, 0x1030],
|
||||
[0x1032, 0x1032], [0x1036, 0x1037], [0x1039, 0x1039],
|
||||
[0x1058, 0x1059], [0x1160, 0x11FF], [0x135F, 0x135F],
|
||||
[0x1712, 0x1714], [0x1732, 0x1734], [0x1752, 0x1753],
|
||||
[0x1772, 0x1773], [0x17B4, 0x17B5], [0x17B7, 0x17BD],
|
||||
[0x17C6, 0x17C6], [0x17C9, 0x17D3], [0x17DD, 0x17DD],
|
||||
[0x180B, 0x180D], [0x18A9, 0x18A9], [0x1920, 0x1922],
|
||||
[0x1927, 0x1928], [0x1932, 0x1932], [0x1939, 0x193B],
|
||||
[0x1A17, 0x1A18], [0x1B00, 0x1B03], [0x1B34, 0x1B34],
|
||||
[0x1B36, 0x1B3A], [0x1B3C, 0x1B3C], [0x1B42, 0x1B42],
|
||||
[0x1B6B, 0x1B73], [0x1DC0, 0x1DCA], [0x1DFE, 0x1DFF],
|
||||
[0x200B, 0x200F], [0x202A, 0x202E], [0x2060, 0x2063],
|
||||
[0x206A, 0x206F], [0x20D0, 0x20EF], [0x302A, 0x302F],
|
||||
[0x3099, 0x309A], [0xA806, 0xA806], [0xA80B, 0xA80B],
|
||||
[0xA825, 0xA826], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F],
|
||||
[0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB],
|
||||
[0x10A01, 0x10A03], [0x10A05, 0x10A06], [0x10A0C, 0x10A0F],
|
||||
[0x10A38, 0x10A3A], [0x10A3F, 0x10A3F], [0x1D167, 0x1D169],
|
||||
[0x1D173, 0x1D182], [0x1D185, 0x1D18B], [0x1D1AA, 0x1D1AD],
|
||||
[0x1D242, 0x1D244], [0xE0001, 0xE0001], [0xE0020, 0xE007F],
|
||||
[0xE0100, 0xE01EF]
|
||||
];
|
||||
// binary search
|
||||
function bisearch(ucs: number): boolean {
|
||||
let min = 0;
|
||||
let max = COMBINING.length - 1;
|
||||
let mid;
|
||||
if (ucs < COMBINING[0][0] || ucs > COMBINING[max][1])
|
||||
return false;
|
||||
while (max >= min) {
|
||||
mid = Math.floor((min + max) / 2);
|
||||
if (ucs > COMBINING[mid][1])
|
||||
min = mid + 1;
|
||||
else if (ucs < COMBINING[mid][0])
|
||||
max = mid - 1;
|
||||
else
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function wcwidth(ucs: number): number {
|
||||
// test for 8-bit control characters
|
||||
if (ucs === 0) {
|
||||
return opts.nul;
|
||||
}
|
||||
if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0)) {
|
||||
return opts.control;
|
||||
}
|
||||
// binary search in table of non-spacing characters
|
||||
if (bisearch(ucs)) {
|
||||
return 0;
|
||||
}
|
||||
// if we arrive here, ucs is not a combining or C0/C1 control character
|
||||
if (isWide(ucs)) {
|
||||
return 2;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
function isWide(ucs: number): boolean {
|
||||
return (
|
||||
ucs >= 0x1100 && (
|
||||
ucs <= 0x115f || // Hangul Jamo init. consonants
|
||||
ucs === 0x2329 ||
|
||||
ucs === 0x232a ||
|
||||
(ucs >= 0x2e80 && ucs <= 0xa4cf && ucs !== 0x303f) || // CJK..Yi
|
||||
(ucs >= 0xac00 && ucs <= 0xd7a3) || // Hangul Syllables
|
||||
(ucs >= 0xf900 && ucs <= 0xfaff) || // CJK Compat Ideographs
|
||||
(ucs >= 0xfe10 && ucs <= 0xfe19) || // Vertical forms
|
||||
(ucs >= 0xfe30 && ucs <= 0xfe6f) || // CJK Compat Forms
|
||||
(ucs >= 0xff00 && ucs <= 0xff60) || // Fullwidth Forms
|
||||
(ucs >= 0xffe0 && ucs <= 0xffe6) ||
|
||||
(ucs >= 0x20000 && ucs <= 0x2fffd) ||
|
||||
(ucs >= 0x30000 && ucs <= 0x3fffd)));
|
||||
}
|
||||
return wcwidth;
|
||||
})({nul: 0, control: 0}); // configurable options
|
||||
|
||||
describe('wcwidth', () => {
|
||||
it('same as old implementation for BMP and individual higher', (done) => {
|
||||
for (let i = 0; i < 65536; ++i)
|
||||
assert.equal(wcwidth(i), old_wcwidth(i));
|
||||
// test some individual higher to fullfill branching
|
||||
assert.equal(wcwidth(0x10A01), old_wcwidth(0x10A01));
|
||||
assert.equal(wcwidth(0x30000), old_wcwidth(0x30000));
|
||||
assert.equal(wcwidth(0x3fffe), old_wcwidth(0x3fffe));
|
||||
done();
|
||||
}).timeout(3000);
|
||||
/*
|
||||
it('new is at least 5 times faster', () => {
|
||||
let start_new = new Date().getTime();
|
||||
let x = 0;
|
||||
for (let runs = 0; runs < 1; ++runs)
|
||||
for (let i = 0; i < 65536; ++i)
|
||||
x = wcwidth(i);
|
||||
let end_new = new Date().getTime();
|
||||
let start_old = new Date().getTime();
|
||||
let y = 0;
|
||||
for (let runs = 0; runs < 1; ++runs)
|
||||
for (let i = 0; i < 65536; ++i)
|
||||
y = old_wcwidth(i);
|
||||
let end_old = new Date().getTime();
|
||||
// console.log((end_new - start_new));
|
||||
// console.log((end_old - start_old));
|
||||
assert.equal(((end_new - start_new) * 5 < (end_old - start_old)), true);
|
||||
});
|
||||
*/
|
||||
});
|
||||
|
||||
+17
-11
@@ -9,6 +9,7 @@ import { C0 } from './EscapeSequences';
|
||||
import { DEFAULT_CHARSET } from './Charsets';
|
||||
import { CharData } from './Types';
|
||||
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from './Buffer';
|
||||
import { FLAGS } from './renderer/Types';
|
||||
|
||||
/**
|
||||
* The terminal's standard implementation of IInputHandler, this handles all
|
||||
@@ -1107,6 +1108,7 @@ export class InputHandler implements IInputHandler {
|
||||
* CSI Pm m Character Attributes (SGR).
|
||||
* Ps = 0 -> Normal (default).
|
||||
* Ps = 1 -> Bold.
|
||||
* Ps = 2 -> Faint, decreased intensity (ISO 6429).
|
||||
* Ps = 4 -> Underlined.
|
||||
* Ps = 5 -> Blink (appears as Bold).
|
||||
* Ps = 7 -> Inverse.
|
||||
@@ -1206,35 +1208,39 @@ export class InputHandler implements IInputHandler {
|
||||
// bg = 0x1ff;
|
||||
} else if (p === 1) {
|
||||
// bold text
|
||||
flags |= 1;
|
||||
flags |= FLAGS.BOLD;
|
||||
} else if (p === 4) {
|
||||
// underlined text
|
||||
flags |= 2;
|
||||
flags |= FLAGS.UNDERLINE;
|
||||
} else if (p === 5) {
|
||||
// blink
|
||||
flags |= 4;
|
||||
flags |= FLAGS.BLINK;
|
||||
} else if (p === 7) {
|
||||
// inverse and positive
|
||||
// test with: echo -e '\e[31m\e[42mhello\e[7mworld\e[27mhi\e[m'
|
||||
flags |= 8;
|
||||
flags |= FLAGS.INVERSE;
|
||||
} else if (p === 8) {
|
||||
// invisible
|
||||
flags |= 16;
|
||||
flags |= FLAGS.INVISIBLE;
|
||||
} else if (p === 2) {
|
||||
// dimmed text
|
||||
flags |= FLAGS.DIM;
|
||||
} else if (p === 22) {
|
||||
// not bold
|
||||
flags &= ~1;
|
||||
// not bold nor faint
|
||||
flags &= ~FLAGS.BOLD;
|
||||
flags &= ~FLAGS.DIM;
|
||||
} else if (p === 24) {
|
||||
// not underlined
|
||||
flags &= ~2;
|
||||
flags &= ~FLAGS.UNDERLINE;
|
||||
} else if (p === 25) {
|
||||
// not blink
|
||||
flags &= ~4;
|
||||
flags &= ~FLAGS.BLINK;
|
||||
} else if (p === 27) {
|
||||
// not inverse
|
||||
flags &= ~8;
|
||||
flags &= ~FLAGS.INVERSE;
|
||||
} else if (p === 28) {
|
||||
// not invisible
|
||||
flags &= ~16;
|
||||
flags &= ~FLAGS.INVISIBLE;
|
||||
} else if (p === 39) {
|
||||
// reset fg
|
||||
fg = (this._terminal.defAttr >> 9) & 0x1ff;
|
||||
|
||||
@@ -139,6 +139,7 @@ export interface ITerminalOptions {
|
||||
fontFamily?: string;
|
||||
geometry?: [number, number];
|
||||
handler?: (data: string) => void;
|
||||
letterSpacing?: number;
|
||||
lineHeight?: number;
|
||||
rows?: number;
|
||||
screenKeys?: boolean;
|
||||
|
||||
+1
-1
@@ -87,7 +87,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
this._rowsToLinkify.end = end;
|
||||
} else {
|
||||
this._rowsToLinkify.start = this._rowsToLinkify.start < start ? this._rowsToLinkify.start : start;
|
||||
this._rowsToLinkify.end = this._rowsToLinkify.end < end ? this._rowsToLinkify.end : end;
|
||||
this._rowsToLinkify.end = this._rowsToLinkify.end > end ? this._rowsToLinkify.end : end;
|
||||
}
|
||||
|
||||
// Clear out any existing links on this row range
|
||||
|
||||
@@ -12,7 +12,7 @@ import { SelectionManager } from './SelectionManager';
|
||||
import { SelectionModel } from './SelectionModel';
|
||||
import { BufferSet } from './BufferSet';
|
||||
import { MockTerminal } from './utils/TestUtils.test';
|
||||
import { LineData } from './Types';
|
||||
import { LineData, CharData } from './Types';
|
||||
|
||||
class TestSelectionManager extends SelectionManager {
|
||||
constructor(
|
||||
@@ -66,6 +66,10 @@ describe('SelectionManager', () => {
|
||||
return result;
|
||||
}
|
||||
|
||||
function stringArrayToRow(chars: string[]): LineData {
|
||||
return chars.map(c => <CharData>[0, c, 1, c.charCodeAt(0)]);
|
||||
}
|
||||
|
||||
describe('_selectWordAt', () => {
|
||||
it('should expand selection for normal width chars', () => {
|
||||
buffer.lines.set(0, stringToRow('foo bar'));
|
||||
@@ -185,6 +189,113 @@ describe('SelectionManager', () => {
|
||||
selectionManager.selectWordAt([15, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'ij"');
|
||||
});
|
||||
describe('emoji', () => {
|
||||
it('should treat a single emoji as a word when wrapped in spaces', () => {
|
||||
buffer.lines.set(0, stringToRow(' ⚽ a')); // The a is here to prevent the space being trimmed in selectionText
|
||||
selectionManager.selectWordAt([0, 0]);
|
||||
assert.equal(selectionManager.selectionText, ' ');
|
||||
selectionManager.selectWordAt([1, 0]);
|
||||
assert.equal(selectionManager.selectionText, '⚽');
|
||||
selectionManager.selectWordAt([2, 0]);
|
||||
assert.equal(selectionManager.selectionText, ' ');
|
||||
});
|
||||
it('should treat multiple emojis as a word when wrapped in spaces', () => {
|
||||
buffer.lines.set(0, stringToRow(' ⚽⚽ a')); // The a is here to prevent the space being trimmed in selectionText
|
||||
selectionManager.selectWordAt([0, 0]);
|
||||
assert.equal(selectionManager.selectionText, ' ');
|
||||
selectionManager.selectWordAt([1, 0]);
|
||||
assert.equal(selectionManager.selectionText, '⚽⚽');
|
||||
selectionManager.selectWordAt([2, 0]);
|
||||
assert.equal(selectionManager.selectionText, '⚽⚽');
|
||||
selectionManager.selectWordAt([3, 0]);
|
||||
assert.equal(selectionManager.selectionText, ' ');
|
||||
});
|
||||
it('should treat emojis using the zero-width-joiner as a single word', () => {
|
||||
// Note that the first 3 emojis include the invisible ZWJ char
|
||||
buffer.lines.set(0, stringArrayToRow([
|
||||
' ', '👨', '👩', '👧', '👦', ' ', 'a'
|
||||
])); // The a is here to prevent the space being trimmed in selectionText
|
||||
selectionManager.selectWordAt([0, 0]);
|
||||
assert.equal(selectionManager.selectionText, ' ');
|
||||
// ZWJ emojis do not combine in the terminal so the family emoji used here consumed 4 cells
|
||||
// The selection text should retain ZWJ chars despite not combining on the terminal
|
||||
selectionManager.selectWordAt([1, 0]);
|
||||
assert.equal(selectionManager.selectionText, '👨👩👧👦');
|
||||
selectionManager.selectWordAt([2, 0]);
|
||||
assert.equal(selectionManager.selectionText, '👨👩👧👦');
|
||||
selectionManager.selectWordAt([3, 0]);
|
||||
assert.equal(selectionManager.selectionText, '👨👩👧👦');
|
||||
selectionManager.selectWordAt([4, 0]);
|
||||
assert.equal(selectionManager.selectionText, '👨👩👧👦');
|
||||
selectionManager.selectWordAt([5, 0]);
|
||||
assert.equal(selectionManager.selectionText, ' ');
|
||||
});
|
||||
it('should treat emojis and characters joined together as a word', () => {
|
||||
buffer.lines.set(0, stringToRow(' ⚽ab cd⚽ ef⚽gh')); // The a is here to prevent the space being trimmed in selectionText
|
||||
selectionManager.selectWordAt([0, 0]);
|
||||
assert.equal(selectionManager.selectionText, ' ');
|
||||
selectionManager.selectWordAt([1, 0]);
|
||||
assert.equal(selectionManager.selectionText, '⚽ab');
|
||||
selectionManager.selectWordAt([2, 0]);
|
||||
assert.equal(selectionManager.selectionText, '⚽ab');
|
||||
selectionManager.selectWordAt([3, 0]);
|
||||
assert.equal(selectionManager.selectionText, '⚽ab');
|
||||
selectionManager.selectWordAt([4, 0]);
|
||||
assert.equal(selectionManager.selectionText, ' ');
|
||||
selectionManager.selectWordAt([5, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'cd⚽');
|
||||
selectionManager.selectWordAt([6, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'cd⚽');
|
||||
selectionManager.selectWordAt([7, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'cd⚽');
|
||||
selectionManager.selectWordAt([8, 0]);
|
||||
assert.equal(selectionManager.selectionText, ' ');
|
||||
selectionManager.selectWordAt([9, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'ef⚽gh');
|
||||
selectionManager.selectWordAt([10, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'ef⚽gh');
|
||||
selectionManager.selectWordAt([11, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'ef⚽gh');
|
||||
selectionManager.selectWordAt([12, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'ef⚽gh');
|
||||
selectionManager.selectWordAt([13, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'ef⚽gh');
|
||||
});
|
||||
it('should treat complex emojis and characters joined together as a word', () => {
|
||||
// This emoji is the flag for England and is made up of: 1F3F4 E0067 E0062 E0065 E006E E0067 E007F
|
||||
buffer.lines.set(0, stringArrayToRow([
|
||||
' ', '🏴', 'a', 'b', ' ', 'c', 'd', '🏴', ' ', 'e', 'f', '🏴', 'g', 'h', ' ', 'a'
|
||||
])); // The a is here to prevent the space being trimmed in selectionText
|
||||
selectionManager.selectWordAt([0, 0]);
|
||||
assert.equal(selectionManager.selectionText, ' ');
|
||||
selectionManager.selectWordAt([1, 0]);
|
||||
assert.equal(selectionManager.selectionText, '🏴ab');
|
||||
selectionManager.selectWordAt([2, 0]);
|
||||
assert.equal(selectionManager.selectionText, '🏴ab');
|
||||
selectionManager.selectWordAt([3, 0]);
|
||||
assert.equal(selectionManager.selectionText, '🏴ab');
|
||||
selectionManager.selectWordAt([4, 0]);
|
||||
assert.equal(selectionManager.selectionText, ' ');
|
||||
selectionManager.selectWordAt([5, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'cd🏴');
|
||||
selectionManager.selectWordAt([6, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'cd🏴');
|
||||
selectionManager.selectWordAt([7, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'cd🏴');
|
||||
selectionManager.selectWordAt([8, 0]);
|
||||
assert.equal(selectionManager.selectionText, ' ');
|
||||
selectionManager.selectWordAt([9, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'ef🏴gh');
|
||||
selectionManager.selectWordAt([10, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'ef🏴gh');
|
||||
selectionManager.selectWordAt([11, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'ef🏴gh');
|
||||
selectionManager.selectWordAt([12, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'ef🏴gh');
|
||||
selectionManager.selectWordAt([13, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'ef🏴gh');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('_selectLineAt', () => {
|
||||
|
||||
+66
-12
@@ -10,8 +10,8 @@ import { CircularList } from './utils/CircularList';
|
||||
import { EventEmitter } from './EventEmitter';
|
||||
import { ITerminal, ICircularList, ISelectionManager, IBuffer } from './Interfaces';
|
||||
import { SelectionModel } from './SelectionModel';
|
||||
import { LineData } from './Types';
|
||||
import { CHAR_DATA_WIDTH_INDEX } from './Buffer';
|
||||
import { LineData, CharData } from './Types';
|
||||
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer';
|
||||
|
||||
/**
|
||||
* The number of pixels the mouse needs to be above or below the viewport in
|
||||
@@ -281,6 +281,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
// Convert to 0-based
|
||||
coords[0]--;
|
||||
coords[1]--;
|
||||
|
||||
// Convert viewport coords to buffer coords
|
||||
coords[1] += this._terminal.buffer.ydisp;
|
||||
return coords;
|
||||
@@ -545,7 +546,14 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
for (let i = 0; coords[0] >= i; i++) {
|
||||
const char = bufferLine[i];
|
||||
if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
|
||||
// Wide characters aren't included in the line string so decrement the
|
||||
// index so the index is back on the wide character.
|
||||
charIndex--;
|
||||
} else if (char[CHAR_DATA_CHAR_INDEX].length > 1 && coords[0] !== i) {
|
||||
// Emojis take up multiple characters, so adjust accordingly. For these
|
||||
// we don't want ot include the character at the column as we're
|
||||
// returning the start index in the string, not the end index.
|
||||
charIndex += char[CHAR_DATA_CHAR_INDEX].length - 1;
|
||||
}
|
||||
}
|
||||
return charIndex;
|
||||
@@ -572,13 +580,15 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
const line = this._buffer.translateBufferLineToString(coords[1], false);
|
||||
|
||||
// Get actual index, taking into consideration wide characters
|
||||
let endIndex = this._convertViewportColToCharacterIndex(bufferLine, coords);
|
||||
let startIndex = endIndex;
|
||||
let startIndex = this._convertViewportColToCharacterIndex(bufferLine, coords);
|
||||
let endIndex = startIndex;
|
||||
|
||||
// Record offset to be used later
|
||||
const charOffset = coords[0] - startIndex;
|
||||
let leftWideCharCount = 0;
|
||||
let rightWideCharCount = 0;
|
||||
let leftLongCharOffset = 0;
|
||||
let rightLongCharOffset = 0;
|
||||
|
||||
if (line.charAt(startIndex) === ' ') {
|
||||
// Expand until non-whitespace is hit
|
||||
@@ -595,6 +605,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
// character is hit, it is recorded and the column index is adjusted.
|
||||
let startCol = coords[0];
|
||||
let endCol = coords[0];
|
||||
|
||||
// Consider the initial position, skip it and increment the wide char
|
||||
// variable
|
||||
if (bufferLine[startCol][CHAR_DATA_WIDTH_INDEX] === 0) {
|
||||
@@ -605,29 +616,67 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
rightWideCharCount++;
|
||||
endCol++;
|
||||
}
|
||||
|
||||
// Adjust the end index for characters whose length are > 1 (emojis)
|
||||
if (bufferLine[endCol][CHAR_DATA_CHAR_INDEX].length > 1) {
|
||||
rightLongCharOffset += bufferLine[endCol][CHAR_DATA_CHAR_INDEX].length - 1;
|
||||
endIndex += bufferLine[endCol][CHAR_DATA_CHAR_INDEX].length - 1;
|
||||
}
|
||||
|
||||
// Expand the string in both directions until a space is hit
|
||||
while (startIndex > 0 && !this._isCharWordSeparator(line.charAt(startIndex - 1))) {
|
||||
if (bufferLine[startCol - 1][CHAR_DATA_WIDTH_INDEX] === 0) {
|
||||
while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine[startCol - 1])) {
|
||||
const char = bufferLine[startCol - 1];
|
||||
if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
|
||||
// If the next character is a wide char, record it and skip the column
|
||||
leftWideCharCount++;
|
||||
startCol--;
|
||||
} else if (char[CHAR_DATA_CHAR_INDEX].length > 1) {
|
||||
// If the next character's string is longer than 1 char (eg. emoji),
|
||||
// adjust the index
|
||||
leftLongCharOffset += char[CHAR_DATA_CHAR_INDEX].length - 1;
|
||||
startIndex -= char[CHAR_DATA_CHAR_INDEX].length - 1;
|
||||
}
|
||||
startIndex--;
|
||||
startCol--;
|
||||
}
|
||||
while (endIndex + 1 < line.length && !this._isCharWordSeparator(line.charAt(endIndex + 1))) {
|
||||
if (bufferLine[endCol + 1][CHAR_DATA_WIDTH_INDEX] === 2) {
|
||||
while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine[endCol + 1])) {
|
||||
const char = bufferLine[endCol + 1];
|
||||
if (char[CHAR_DATA_WIDTH_INDEX] === 2) {
|
||||
// If the next character is a wide char, record it and skip the column
|
||||
rightWideCharCount++;
|
||||
endCol++;
|
||||
} else if (char[CHAR_DATA_CHAR_INDEX].length > 1) {
|
||||
// If the next character's string is longer than 1 char (eg. emoji),
|
||||
// adjust the index
|
||||
rightLongCharOffset += char[CHAR_DATA_CHAR_INDEX].length - 1;
|
||||
endIndex += char[CHAR_DATA_CHAR_INDEX].length - 1;
|
||||
}
|
||||
endIndex++;
|
||||
endCol++;
|
||||
}
|
||||
}
|
||||
|
||||
const start = startIndex + charOffset - leftWideCharCount;
|
||||
const length = Math.min(endIndex - startIndex + leftWideCharCount + rightWideCharCount + 1/*include endIndex char*/, this._terminal.cols);
|
||||
// Incremenet the end index so it is at the start of the next character
|
||||
endIndex++;
|
||||
|
||||
// Calculate the start _column_, converting the the string indexes back to
|
||||
// column coordinates.
|
||||
const start =
|
||||
startIndex // The index of the selection's start char in the line string
|
||||
+ charOffset // The difference between the initial char's column and index
|
||||
- leftWideCharCount // The number of wide chars left of the initial char
|
||||
+ leftLongCharOffset; // The number of additional chars left of the initial char added by columns with strings longer than 1 (emojis)
|
||||
|
||||
// Calculate the length in _columns_, converting the the string indexes back
|
||||
// to column coordinates.
|
||||
const length = Math.min(this._terminal.cols, // Disallow lengths larger than the terminal cols
|
||||
endIndex // The index of the selection's end char in the line string
|
||||
- startIndex // The index of the selection's start char in the line string
|
||||
+ leftWideCharCount // The number of wide chars left of the initial char
|
||||
+ rightWideCharCount // The number of wide chars right of the initial char (inclusive)
|
||||
- leftLongCharOffset // The number of additional chars left of the initial char added by columns with strings longer than 1 (emojis)
|
||||
- rightLongCharOffset); // The number of additional chars right of the initial char (inclusive) added by columns with strings longer than 1 (emojis)
|
||||
|
||||
return { start, length };
|
||||
}
|
||||
|
||||
@@ -659,8 +708,13 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
* word logic.
|
||||
* @param char The character to check.
|
||||
*/
|
||||
private _isCharWordSeparator(char: string): boolean {
|
||||
return WORD_SEPARATORS.indexOf(char) >= 0;
|
||||
private _isCharWordSeparator(charData: CharData): boolean {
|
||||
// Zero width characters are never separators as they are always to the
|
||||
// right of wide characters
|
||||
if (charData[CHAR_DATA_WIDTH_INDEX] === 0) {
|
||||
return false;
|
||||
}
|
||||
return WORD_SEPARATORS.indexOf(charData[CHAR_DATA_CHAR_INDEX]) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -97,6 +97,9 @@ export class SelectionModel {
|
||||
public areSelectionValuesReversed(): boolean {
|
||||
const start = this.selectionStart;
|
||||
const end = this.selectionEnd;
|
||||
if (!start || !end) {
|
||||
return false;
|
||||
}
|
||||
return start[1] > end[1] || (start[1] === end[1] && start[0] > end[0]);
|
||||
}
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ const DEFAULT_OPTIONS: ITerminalOptions = {
|
||||
fontFamily: 'courier-new, courier, monospace',
|
||||
fontSize: 15,
|
||||
lineHeight: 1.0,
|
||||
letterSpacing: 0,
|
||||
scrollback: 1000,
|
||||
screenKeys: false,
|
||||
debug: false,
|
||||
@@ -410,6 +411,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
|
||||
this.renderer.clear();
|
||||
this.charMeasure.measure(this.options);
|
||||
break;
|
||||
case 'letterSpacing':
|
||||
case 'lineHeight':
|
||||
// When the font changes the size of the cells may change which requires a renderer clear
|
||||
this.renderer.clear();
|
||||
|
||||
+2
-2
@@ -47,7 +47,7 @@ export class Viewport implements IViewport {
|
||||
*/
|
||||
private refresh(): void {
|
||||
if (this.charMeasure.height > 0) {
|
||||
this.currentRowHeight = this.terminal.renderer.dimensions.scaledLineHeight / window.devicePixelRatio;
|
||||
this.currentRowHeight = this.terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio;
|
||||
|
||||
if (this.lastRecordedViewportHeight !== this.terminal.renderer.dimensions.canvasHeight) {
|
||||
this.lastRecordedViewportHeight = this.terminal.renderer.dimensions.canvasHeight;
|
||||
@@ -75,7 +75,7 @@ export class Viewport implements IViewport {
|
||||
this.refresh();
|
||||
} else {
|
||||
// If size has changed, refresh viewport
|
||||
if (this.terminal.renderer.dimensions.scaledLineHeight / window.devicePixelRatio !== this.currentRowHeight) {
|
||||
if (this.terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio !== this.currentRowHeight) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
exports.fit = function (term) {
|
||||
// Wrap fit in a setTimeout as charMeasure needs time to get initialized
|
||||
// after calling Terminal.open
|
||||
setTimeout(() => {
|
||||
setTimeout(function () {
|
||||
var geometry = exports.proposeGeometry(term);
|
||||
|
||||
if (geometry) {
|
||||
|
||||
@@ -21,7 +21,6 @@ export class MouseZoneManager implements IMouseZoneManager {
|
||||
|
||||
private _areZonesActive: boolean = false;
|
||||
private _mouseMoveListener: (e: MouseEvent) => any;
|
||||
private _mouseDownListener: (e: MouseEvent) => any;
|
||||
private _clickListener: (e: MouseEvent) => any;
|
||||
|
||||
private _tooltipTimeout: number = null;
|
||||
@@ -31,6 +30,8 @@ export class MouseZoneManager implements IMouseZoneManager {
|
||||
constructor(
|
||||
private _terminal: ITerminal
|
||||
) {
|
||||
this._terminal.element.addEventListener('mousedown', e => this._onMouseDown(e));
|
||||
|
||||
// These events are expensive, only listen to it when mouse zones are active
|
||||
this._mouseMoveListener = e => this._onMouseMove(e);
|
||||
this._clickListener = e => this._onClick(e);
|
||||
@@ -140,11 +141,30 @@ export class MouseZoneManager implements IMouseZoneManager {
|
||||
}
|
||||
}
|
||||
|
||||
private _onMouseDown(e: MouseEvent): void {
|
||||
// Ignore the event if there are no zones active
|
||||
if (!this._areZonesActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the active zone, prevent event propagation if found to prevent other
|
||||
// components from handling the mouse event.
|
||||
const zone = this._findZoneEventAt(e);
|
||||
if (zone) {
|
||||
// TODO: When link modifier support is added, the event should only be
|
||||
// cancelled when the modifier is held (see #1021)
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
}
|
||||
|
||||
private _onClick(e: MouseEvent): void {
|
||||
// Find the active zone and click it if found
|
||||
const zone = this._findZoneEventAt(e);
|
||||
if (zone) {
|
||||
zone.clickCallback(e);
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,14 +10,17 @@ import { CharData } from '../Types';
|
||||
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
|
||||
|
||||
export const INVERTED_DEFAULT_COLOR = -1;
|
||||
const DIM_OPACITY = 0.5;
|
||||
|
||||
export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
private _canvas: HTMLCanvasElement;
|
||||
protected _ctx: CanvasRenderingContext2D;
|
||||
private _scaledCharWidth: number;
|
||||
private _scaledCharHeight: number;
|
||||
private _scaledLineHeight: number;
|
||||
private _scaledLineDrawY: number;
|
||||
private _scaledCellWidth: number;
|
||||
private _scaledCellHeight: number;
|
||||
private _scaledCharLeft: number;
|
||||
private _scaledCharTop: number;
|
||||
|
||||
private _charAtlas: HTMLCanvasElement | ImageBitmap;
|
||||
|
||||
@@ -31,7 +34,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
this._canvas = document.createElement('canvas');
|
||||
this._canvas.id = `xterm-${id}-layer`;
|
||||
this._canvas.style.zIndex = zIndex.toString();
|
||||
this._ctx = this._canvas.getContext('2d', {_alpha});
|
||||
this._ctx = this._canvas.getContext('2d', {alpha: _alpha});
|
||||
this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
|
||||
// Draw the background if this is an opaque layer
|
||||
if (!_alpha) {
|
||||
@@ -70,10 +73,12 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
}
|
||||
|
||||
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
|
||||
this._scaledCellWidth = dim.scaledCellWidth;
|
||||
this._scaledCellHeight = dim.scaledCellHeight;
|
||||
this._scaledCharWidth = dim.scaledCharWidth;
|
||||
this._scaledCharHeight = dim.scaledCharHeight;
|
||||
this._scaledLineHeight = dim.scaledLineHeight;
|
||||
this._scaledLineDrawY = dim.scaledLineDrawY;
|
||||
this._scaledCharLeft = dim.scaledCharLeft;
|
||||
this._scaledCharTop = dim.scaledCharTop;
|
||||
this._canvas.width = dim.scaledCanvasWidth;
|
||||
this._canvas.height = dim.scaledCanvasHeight;
|
||||
this._canvas.style.width = `${dim.canvasWidth}px`;
|
||||
@@ -100,10 +105,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
*/
|
||||
protected fillCells(x: number, y: number, width: number, height: number): void {
|
||||
this._ctx.fillRect(
|
||||
x * this._scaledCharWidth,
|
||||
y * this._scaledLineHeight,
|
||||
width * this._scaledCharWidth,
|
||||
height * this._scaledLineHeight);
|
||||
x * this._scaledCellWidth,
|
||||
y * this._scaledCellHeight,
|
||||
width * this._scaledCellWidth,
|
||||
height * this._scaledCellHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,9 +119,9 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
*/
|
||||
protected fillBottomLineAtCells(x: number, y: number, width: number = 1): void {
|
||||
this._ctx.fillRect(
|
||||
x * this._scaledCharWidth,
|
||||
(y + 1) * this._scaledLineHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */,
|
||||
width * this._scaledCharWidth,
|
||||
x * this._scaledCellWidth,
|
||||
(y + 1) * this._scaledCellHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */,
|
||||
width * this._scaledCellWidth,
|
||||
window.devicePixelRatio);
|
||||
}
|
||||
|
||||
@@ -128,10 +133,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
*/
|
||||
protected fillLeftLineAtCell(x: number, y: number): void {
|
||||
this._ctx.fillRect(
|
||||
x * this._scaledCharWidth,
|
||||
y * this._scaledLineHeight,
|
||||
x * this._scaledCellWidth,
|
||||
y * this._scaledCellHeight,
|
||||
window.devicePixelRatio,
|
||||
this._scaledLineHeight);
|
||||
this._scaledCellHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,10 +148,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
protected strokeRectAtCell(x: number, y: number, width: number, height: number): void {
|
||||
this._ctx.lineWidth = window.devicePixelRatio;
|
||||
this._ctx.strokeRect(
|
||||
x * this._scaledCharWidth + window.devicePixelRatio / 2,
|
||||
y * this._scaledLineHeight + (window.devicePixelRatio / 2),
|
||||
width * this._scaledCharWidth - window.devicePixelRatio,
|
||||
(height * this._scaledLineHeight) - window.devicePixelRatio);
|
||||
x * this._scaledCellWidth + window.devicePixelRatio / 2,
|
||||
y * this._scaledCellHeight + (window.devicePixelRatio / 2),
|
||||
width * this._scaledCellWidth - window.devicePixelRatio,
|
||||
(height * this._scaledCellHeight) - window.devicePixelRatio);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -171,17 +176,17 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
protected clearCells(x: number, y: number, width: number, height: number): void {
|
||||
if (this._alpha) {
|
||||
this._ctx.clearRect(
|
||||
x * this._scaledCharWidth,
|
||||
y * this._scaledLineHeight,
|
||||
width * this._scaledCharWidth,
|
||||
height * this._scaledLineHeight);
|
||||
x * this._scaledCellWidth,
|
||||
y * this._scaledCellHeight,
|
||||
width * this._scaledCellWidth,
|
||||
height * this._scaledCellHeight);
|
||||
} else {
|
||||
this._ctx.fillStyle = this._colors.background;
|
||||
this._ctx.fillRect(
|
||||
x * this._scaledCharWidth,
|
||||
y * this._scaledLineHeight,
|
||||
width * this._scaledCharWidth,
|
||||
height * this._scaledLineHeight);
|
||||
x * this._scaledCellWidth,
|
||||
y * this._scaledCellHeight,
|
||||
width * this._scaledCellWidth,
|
||||
height * this._scaledCellHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,15 +203,11 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
protected fillCharTrueColor(terminal: ITerminal, charData: CharData, x: number, y: number): void {
|
||||
this._ctx.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
|
||||
this._ctx.textBaseline = 'top';
|
||||
|
||||
// Since uncached characters are not coming off the char atlas with source
|
||||
// coordinates, it means that text drawn to the canvas (particularly '_')
|
||||
// can bleed into other cells. This code will clip the following fillText,
|
||||
// ensuring that its contents don't go beyond the cell bounds.
|
||||
this._ctx.beginPath();
|
||||
this._ctx.rect(x * this._scaledCharWidth, y * this._scaledLineHeight + this._scaledLineDrawY, charData[CHAR_DATA_WIDTH_INDEX] * this._scaledCharWidth, this._scaledCharHeight);
|
||||
this._ctx.clip();
|
||||
this._ctx.fillText(charData[CHAR_DATA_CHAR_INDEX], x * this._scaledCharWidth, y * this._scaledCharHeight);
|
||||
this._clipRow(terminal, y);
|
||||
this._ctx.fillText(
|
||||
charData[CHAR_DATA_CHAR_INDEX],
|
||||
x * this._scaledCellWidth + this._scaledCharLeft,
|
||||
y * this._scaledCellHeight + this._scaledCharTop);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -223,12 +224,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
* This is used to validate whether a cached image can be used.
|
||||
* @param bold Whether the text is bold.
|
||||
*/
|
||||
protected drawChar(terminal: ITerminal, char: string, code: number, width: number, x: number, y: number, fg: number, bg: number, bold: boolean): void {
|
||||
// Clear the cell next to this character if it's wide
|
||||
if (width === 2) {
|
||||
this.clearCells(x + 1, y, 1, 1);
|
||||
}
|
||||
|
||||
protected drawChar(terminal: ITerminal, char: string, code: number, width: number, x: number, y: number, fg: number, bg: number, bold: boolean, dim: boolean): void {
|
||||
let colorIndex = 0;
|
||||
if (fg < 256) {
|
||||
colorIndex = fg + 2;
|
||||
@@ -239,18 +235,31 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
}
|
||||
}
|
||||
const isAscii = code < 256;
|
||||
const isBasicColor = (colorIndex > 1 && fg < 16);
|
||||
// A color is basic if it is one of the standard normal or bold weight
|
||||
// colors of the characters held in the char atlas. Note that this excludes
|
||||
// the normal weight light color characters
|
||||
const isBasicColor = (colorIndex > 1 && fg < 16) && (fg < 8 || bold);
|
||||
const isDefaultColor = fg >= 256;
|
||||
const isDefaultBackground = bg >= 256;
|
||||
if (this._charAtlas && isAscii && (isBasicColor || isDefaultColor) && isDefaultBackground) {
|
||||
// ImageBitmap's draw about twice as fast as from a canvas
|
||||
const charAtlasCellWidth = this._scaledCharWidth + CHAR_ATLAS_CELL_SPACING;
|
||||
const charAtlasCellHeight = this._scaledCharHeight + CHAR_ATLAS_CELL_SPACING;
|
||||
// Apply alpha to dim the character
|
||||
if (dim) {
|
||||
this._ctx.globalAlpha = DIM_OPACITY;
|
||||
}
|
||||
this._ctx.drawImage(this._charAtlas,
|
||||
code * charAtlasCellWidth, colorIndex * charAtlasCellHeight, this._scaledCharWidth, this._scaledCharHeight,
|
||||
x * this._scaledCharWidth, y * this._scaledLineHeight + this._scaledLineDrawY, this._scaledCharWidth, this._scaledCharHeight);
|
||||
code * charAtlasCellWidth,
|
||||
colorIndex * charAtlasCellHeight,
|
||||
charAtlasCellWidth,
|
||||
this._scaledCharHeight,
|
||||
x * this._scaledCellWidth + this._scaledCharLeft,
|
||||
y * this._scaledCellHeight + this._scaledCharTop,
|
||||
charAtlasCellWidth,
|
||||
this._scaledCharHeight);
|
||||
} else {
|
||||
this._drawUncachedChar(terminal, char, width, fg, x, y, bold);
|
||||
this._drawUncachedChar(terminal, char, width, fg, x, y, bold, dim);
|
||||
}
|
||||
// This draws the atlas (for debugging purposes)
|
||||
// this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
|
||||
@@ -268,7 +277,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
* @param x The column to draw at.
|
||||
* @param y The row to draw at.
|
||||
*/
|
||||
private _drawUncachedChar(terminal: ITerminal, char: string, width: number, fg: number, x: number, y: number, bold: boolean): void {
|
||||
private _drawUncachedChar(terminal: ITerminal, char: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean): void {
|
||||
this._ctx.save();
|
||||
this._ctx.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
|
||||
if (bold) {
|
||||
@@ -285,17 +294,33 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
this._ctx.fillStyle = this._colors.foreground;
|
||||
}
|
||||
|
||||
// Since uncached characters are not coming off the char atlas with source
|
||||
// coordinates, it means that text drawn to the canvas (particularly '_')
|
||||
// can bleed into other cells. This code will clip the following fillText,
|
||||
// ensuring that its contents don't go beyond the cell bounds.
|
||||
this._ctx.beginPath();
|
||||
this._ctx.rect(x * this._scaledCharWidth, y * this._scaledLineHeight + this._scaledLineDrawY, width * this._scaledCharWidth, this._scaledCharHeight);
|
||||
this._ctx.clip();
|
||||
this._clipRow(terminal, y);
|
||||
|
||||
// Apply alpha to dim the character
|
||||
if (dim) {
|
||||
this._ctx.globalAlpha = DIM_OPACITY;
|
||||
}
|
||||
// Draw the character
|
||||
this._ctx.fillText(char, x * this._scaledCharWidth, y * this._scaledLineHeight + this._scaledLineDrawY);
|
||||
this._ctx.fillText(
|
||||
char,
|
||||
x * this._scaledCellWidth + this._scaledCharLeft,
|
||||
y * this._scaledCellHeight + this._scaledCharTop);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clips a row to ensure no pixels will be drawn outside the cells in the row.
|
||||
* @param terminal The terminal.
|
||||
* @param y The row to clip.
|
||||
*/
|
||||
private _clipRow(terminal: ITerminal, y: number): void {
|
||||
this._ctx.beginPath();
|
||||
this._ctx.rect(
|
||||
0,
|
||||
y * this._scaledCellHeight,
|
||||
terminal.cols * this._scaledCellWidth,
|
||||
this._scaledCellHeight);
|
||||
this._ctx.clip();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -142,13 +142,23 @@ class CharAtlasGenerator {
|
||||
|
||||
// Default color
|
||||
for (let i = 0; i < 256; i++) {
|
||||
this._ctx.save();
|
||||
this._ctx.beginPath();
|
||||
this._ctx.rect(i * cellWidth, 0, cellWidth, cellHeight);
|
||||
this._ctx.clip();
|
||||
this._ctx.fillText(String.fromCharCode(i), i * cellWidth, 0);
|
||||
this._ctx.restore();
|
||||
}
|
||||
// Default color bold
|
||||
this._ctx.save();
|
||||
this._ctx.font = `bold ${this._ctx.font}`;
|
||||
for (let i = 0; i < 256; i++) {
|
||||
this._ctx.save();
|
||||
this._ctx.beginPath();
|
||||
this._ctx.rect(i * cellWidth, cellHeight, cellWidth, cellHeight);
|
||||
this._ctx.clip();
|
||||
this._ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight);
|
||||
this._ctx.restore();
|
||||
}
|
||||
this._ctx.restore();
|
||||
|
||||
@@ -162,14 +172,17 @@ class CharAtlasGenerator {
|
||||
const y = (colorIndex + 2) * cellHeight;
|
||||
// Draw ascii characters
|
||||
for (let i = 0; i < 256; i++) {
|
||||
this._ctx.save();
|
||||
this._ctx.beginPath();
|
||||
this._ctx.rect(i * cellWidth, y, cellWidth, cellHeight);
|
||||
this._ctx.clip();
|
||||
this._ctx.fillStyle = ansiColors[colorIndex];
|
||||
this._ctx.fillText(String.fromCharCode(i), i * cellWidth, y);
|
||||
this._ctx.restore();
|
||||
}
|
||||
}
|
||||
this._ctx.restore();
|
||||
|
||||
const charAtlasImageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height);
|
||||
|
||||
// Support is patchy for createImageBitmap at the moment, pass a canvas back
|
||||
// if support is lacking as drawImage works there too. Firefox is also
|
||||
// included here as ImageBitmap appears both buggy and has horrible
|
||||
@@ -183,9 +196,27 @@ class CharAtlasGenerator {
|
||||
return result;
|
||||
}
|
||||
|
||||
const charAtlasImageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height);
|
||||
|
||||
// Remove the background color from the image so characters may overlap
|
||||
const r = parseInt(background.substr(1, 2), 16);
|
||||
const g = parseInt(background.substr(3, 2), 16);
|
||||
const b = parseInt(background.substr(5, 2), 16);
|
||||
this._clearColor(charAtlasImageData, r, g, b);
|
||||
|
||||
const promise = window.createImageBitmap(charAtlasImageData);
|
||||
// Clear the rect while the promise is in progress
|
||||
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
|
||||
return promise;
|
||||
}
|
||||
|
||||
private _clearColor(imageData: ImageData, r: number, g: number, b: number): void {
|
||||
for (let offset = 0; offset < imageData.data.length; offset += 4) {
|
||||
if (imageData.data[offset] === r &&
|
||||
imageData.data[offset + 1] === g &&
|
||||
imageData.data[offset + 2] === b) {
|
||||
imageData.data[offset + 3] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ export class ColorManager implements IColorManager {
|
||||
*/
|
||||
public setTheme(theme: ITheme): void {
|
||||
this.colors.foreground = theme.foreground || DEFAULT_FOREGROUND;
|
||||
this.colors.background = theme.background || DEFAULT_BACKGROUND;
|
||||
this.colors.background = this._validateColor(theme.background, DEFAULT_BACKGROUND);
|
||||
this.colors.cursor = theme.cursor || DEFAULT_CURSOR;
|
||||
this.colors.cursorAccent = theme.cursorAccent || DEFAULT_CURSOR_ACCENT;
|
||||
this.colors.selection = theme.selection || DEFAULT_SELECTION;
|
||||
@@ -107,4 +107,17 @@ export class ColorManager implements IColorManager {
|
||||
this.colors.ansi[14] = theme.brightCyan || DEFAULT_ANSI_COLORS[14];
|
||||
this.colors.ansi[15] = theme.brightWhite || DEFAULT_ANSI_COLORS[15];
|
||||
}
|
||||
|
||||
private _validateColor(color: string, fallback: string): string {
|
||||
if (color.length === 7 && color.charAt(0) === '#') {
|
||||
return color;
|
||||
}
|
||||
if (color.length === 4 && color.charAt(0) === '#') {
|
||||
const r = color.charAt(1);
|
||||
const g = color.charAt(2);
|
||||
const b = color.charAt(3);
|
||||
return `#${r}${r}${g}${g}${b}${b}`;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,9 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
|
||||
const charData = terminal.buffer.lines.get(cursorY)[terminal.buffer.x];
|
||||
if (!charData) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!terminal.isFocused) {
|
||||
this._clearCursor();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user