diff --git a/README.md b/README.md index 498278b8..5cf2848f 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,8 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**PHP App Server**](https://github.com/cubiclesoft/php-app-server/): Create lightweight, installable almost-native applications for desktop OSes. ExecTerminal (nicely wraps the xterm.js Terminal), TerminalManager, and RunProcessSDK are self-contained, reusable ES5+ compliant Javascript components. - [**NgTerminal**](https://github.com/qwefgh90/ng-terminal): NgTerminal is a web terminal that leverages xterm.js on Angular 7+. You can easily add it into your application by adding `` into your component. - [**tty-share**](https://tty-share.com): Extremely simple terminal sharing over the Internet. +- [**Ten Hands**](https://github.com/saisandeepvaddi/ten-hands): One place to run your command-line tasks. +- [**WebAssembly.sh**](https://webassembly.sh): A WebAssembly WASI browser terminal [And much more...](https://github.com/xtermjs/xterm.js/network/dependents) diff --git a/addons/xterm-addon-attach/package.json b/addons/xterm-addon-attach/package.json index 5a5c5d75..5718f356 100644 --- a/addons/xterm-addon-attach/package.json +++ b/addons/xterm-addon-attach/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-attach", - "version": "0.2.1", + "version": "0.3.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 4d3e8841..5893f79f 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -344,7 +344,7 @@ export class SearchAddon implements ITerminalAddon { } terminal.select(result.col, result.row, result.term.length); // If it is not in the viewport then we scroll else it just gets selected - if (result.row > (terminal.buffer.viewportY + terminal.rows) || result.row < terminal.buffer.viewportY) { + if (result.row >= (terminal.buffer.viewportY + terminal.rows) || result.row < terminal.buffer.viewportY) { let scroll = result.row - terminal.buffer.viewportY; scroll = scroll - Math.floor(terminal.rows / 2); terminal.scrollLines(scroll); diff --git a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts index 6aed5f53..8a67526e 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts @@ -92,10 +92,10 @@ export class CursorRenderLayer extends BaseRenderLayer { if (this._cursorBlinkStateManager) { this._cursorBlinkStateManager.dispose(); } - // Request a refresh from the terminal as management of rendering is being - // moved back to the terminal - terminal.refresh(terminal.buffer.cursorY, terminal.buffer.cursorY); } + // Request a refresh from the terminal as management of rendering is being + // moved back to the terminal + terminal.refresh(terminal.buffer.cursorY, terminal.buffer.cursorY); } public onCursorMove(terminal: Terminal): void { @@ -139,12 +139,17 @@ export class CursorRenderLayer extends BaseRenderLayer { this._clearCursor(); this._ctx.save(); this._ctx.fillStyle = this._colors.cursor.css; - this._renderBlurCursor(terminal, terminal.buffer.cursorX, viewportRelativeCursorY, this._cell); + const cursorStyle = terminal.getOption('cursorStyle'); + if (cursorStyle && cursorStyle !== 'block') { + this._cursorRenderers[cursorStyle](terminal, terminal.buffer.cursorX, viewportRelativeCursorY, this._cell); + } else { + this._renderBlurCursor(terminal, terminal.buffer.cursorX, viewportRelativeCursorY, this._cell); + } this._ctx.restore(); this._state.x = terminal.buffer.cursorX; this._state.y = viewportRelativeCursorY; this._state.isFocused = false; - this._state.style = terminal.getOption('cursorStyle'); + this._state.style = cursorStyle; this._state.width = this._cell.getWidth(); return; } diff --git a/package.json b/package.json index f52f7b79..709a1415 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "xterm", "description": "Full xterm terminal, in your browser", - "version": "4.0.0", + "version": "4.1.0", "main": "lib/xterm.js", "style": "css/xterm.css", "types": "typings/xterm.d.ts", @@ -47,7 +47,7 @@ "ts-loader": "^6.0.4", "tslint": "^5.18.0", "tslint-consistent-codestyle": "^1.13.0", - "typescript": "3.5", + "typescript": "3.6", "utf8": "^3.0.0", "webpack": "^4.35.3", "webpack-cli": "^3.1.0", diff --git a/src/common/input/Keyboard.test.ts b/src/common/input/Keyboard.test.ts index 409a3192..a304923e 100644 --- a/src/common/input/Keyboard.test.ts +++ b/src/common/input/Keyboard.test.ts @@ -108,6 +108,12 @@ describe('Keyboard', () => { it('should return \\x1b[5C for alt+right', () => { assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 39 }, { isMac: false }).key, '\x1b[1;5C'); // CSI 5 C }); + it('should return \\x1b[5D for alt+up', () => { + assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 38 }, { isMac: false }).key, '\x1b[1;5A'); // CSI 5 D + }); + it('should return \\x1b[5C for alt+down', () => { + assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 40 }, { isMac: false }).key, '\x1b[1;5B'); // CSI 5 C + }); it('should return \\x1ba for alt+a', () => { assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 65 }, { isMac: false }).key, '\x1ba'); }); @@ -120,6 +126,12 @@ describe('Keyboard', () => { it('should return \\x1bf for alt+right', () => { assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 39 }, { isMac: true }).key, '\x1bf'); // CSI 5 C }); + it('should return \\x1bb for alt+up', () => { + assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 38 }, { isMac: true }).key, '\x1b[1;3A'); // CSI 5 D + }); + it('should return \\x1bf for alt+down', () => { + assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 40 }, { isMac: true }).key, '\x1b[1;3B'); // CSI 5 C + }); it('should return undefined for alt+a', () => { assert.strictEqual(testEvaluateKeyboardEvent({ altKey: true, keyCode: 65 }, { isMac: true }).key, undefined), { isMac: true }; }); diff --git a/src/common/input/Keyboard.ts b/src/common/input/Keyboard.ts index 2f54add6..1bf378c1 100644 --- a/src/common/input/Keyboard.ts +++ b/src/common/input/Keyboard.ts @@ -113,13 +113,16 @@ export function evaluateKeyboardEvent( break; case 37: // left-arrow + if (ev.metaKey) { + break; + } if (modifiers) { result.key = C0.ESC + '[1;' + (modifiers + 1) + 'D'; // HACK: Make Alt + left-arrow behave like Ctrl + left-arrow: move one word backwards // http://unix.stackexchange.com/a/108106 // macOS uses different escape sequences than linux if (result.key === C0.ESC + '[1;3D') { - result.key = isMac ? C0.ESC + 'b' : C0.ESC + '[1;5D'; + result.key = C0.ESC + (isMac ? 'b' : '[1;5D'); } } else if (applicationCursorMode) { result.key = C0.ESC + 'OD'; @@ -129,13 +132,16 @@ export function evaluateKeyboardEvent( break; case 39: // right-arrow + if (ev.metaKey) { + break; + } if (modifiers) { result.key = C0.ESC + '[1;' + (modifiers + 1) + 'C'; // HACK: Make Alt + right-arrow behave like Ctrl + right-arrow: move one word forward // http://unix.stackexchange.com/a/108106 // macOS uses different escape sequences than linux if (result.key === C0.ESC + '[1;3C') { - result.key = isMac ? C0.ESC + 'f' : C0.ESC + '[1;5C'; + result.key = C0.ESC + (isMac ? 'f' : '[1;5C'); } } else if (applicationCursorMode) { result.key = C0.ESC + 'OC'; @@ -145,11 +151,15 @@ export function evaluateKeyboardEvent( break; case 38: // up-arrow + if (ev.metaKey) { + break; + } if (modifiers) { result.key = C0.ESC + '[1;' + (modifiers + 1) + 'A'; // HACK: Make Alt + up-arrow behave like Ctrl + up-arrow // http://unix.stackexchange.com/a/108106 - if (result.key === C0.ESC + '[1;3A') { + // macOS uses different escape sequences than linux + if (!isMac && result.key === C0.ESC + '[1;3A') { result.key = C0.ESC + '[1;5A'; } } else if (applicationCursorMode) { @@ -160,11 +170,15 @@ export function evaluateKeyboardEvent( break; case 40: // down-arrow + if (ev.metaKey) { + break; + } if (modifiers) { result.key = C0.ESC + '[1;' + (modifiers + 1) + 'B'; // HACK: Make Alt + down-arrow behave like Ctrl + down-arrow // http://unix.stackexchange.com/a/108106 - if (result.key === C0.ESC + '[1;3B') { + // macOS uses different escape sequences than linux + if (!isMac && result.key === C0.ESC + '[1;3B') { result.key = C0.ESC + '[1;5B'; } } else if (applicationCursorMode) { diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index b8a70ff7..4b895614 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -56,6 +56,9 @@ export class Terminal implements ITerminalApi { this._core.resize(columns, rows); } public open(parent: HTMLElement): void { + if (!document.body.contains(parent)) { + throw new Error('open must be called on an element that is attached to the DOM'); + } this._core.open(parent); } public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void { @@ -178,8 +181,8 @@ export class Terminal implements ITerminalApi { private _verifyIntegers(...values: number[]): void { values.forEach(value => { - if (value % 1 !== 0) { - throw new Error('This API does not accept floating point numbers'); + if (value === Infinity || value === NaN || value % 1 !== 0) { + throw new Error('This API only accepts integers'); } }); } diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index f847c5f6..1f2791f1 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -103,10 +103,10 @@ export class CursorRenderLayer extends BaseRenderLayer { this._cursorBlinkStateManager.dispose(); this._cursorBlinkStateManager = null; } - // Request a refresh from the terminal as management of rendering is being - // moved back to the terminal - this._terminal.refresh(this._bufferService.buffer.y, this._bufferService.buffer.y); } + // Request a refresh from the terminal as management of rendering is being + // moved back to the terminal + this._terminal.refresh(this._bufferService.buffer.y, this._bufferService.buffer.y); } public onCursorMove(): void { @@ -148,12 +148,17 @@ export class CursorRenderLayer extends BaseRenderLayer { this._clearCursor(); this._ctx.save(); this._ctx.fillStyle = this._colors.cursor.css; - this._renderBlurCursor(this._bufferService.buffer.x, viewportRelativeCursorY, this._cell); + const cursorStyle = this._optionsService.options.cursorStyle; + if (cursorStyle && cursorStyle !== 'block') { + this._cursorRenderers[cursorStyle](this._bufferService.buffer.x, viewportRelativeCursorY, this._cell); + } else { + this._renderBlurCursor(this._bufferService.buffer.x, viewportRelativeCursorY, this._cell); + } this._ctx.restore(); this._state.x = this._bufferService.buffer.x; this._state.y = viewportRelativeCursorY; this._state.isFocused = false; - this._state.style = this._optionsService.options.cursorStyle; + this._state.style = cursorStyle; this._state.width = this._cell.getWidth(); return; } diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts index d2c80235..ef927f20 100644 --- a/src/renderer/dom/DomRenderer.ts +++ b/src/renderer/dom/DomRenderer.ts @@ -183,7 +183,7 @@ export class DomRenderer extends Disposable implements IRenderer { `}`; // Cursor styles += - `${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${CURSOR_CLASS} {` + + `${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` + ` outline: 1px solid ${this._colors.cursor.css};` + ` outline-offset: -1px;` + `}` + @@ -197,10 +197,10 @@ export class DomRenderer extends Disposable implements IRenderer { ` background-color: ${this._colors.cursor.css};` + ` color: ${this._colors.cursorAccent.css};` + `}` + - `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BAR_CLASS} {` + + `${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BAR_CLASS} {` + ` box-shadow: 1px 0 0 ${this._colors.cursor.css} inset;` + `}` + - `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_UNDERLINE_CLASS} {` + + `${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_UNDERLINE_CLASS} {` + ` box-shadow: 0 -1px 0 ${this._colors.cursor.css} inset;` + `}`; // Selection diff --git a/test/api/Terminal.api.ts b/test/api/Terminal.api.ts index 034b04c4..edbdf8a3 100644 --- a/test/api/Terminal.api.ts +++ b/test/api/Terminal.api.ts @@ -510,6 +510,20 @@ describe('API Integration Tests', function(): void { }); }); }); + + it('dispose', async function(): Promise { + await page.evaluate(` + window.term = new Terminal(); + window.term.dispose(); + `); + assert.equal(await page.evaluate(`window.term._core._isDisposed`), true); + }); + + it('dispose (opened)', async function(): Promise { + await openTerminal(); + await page.evaluate(`window.term.dispose()`); + assert.equal(await page.evaluate(`window.term._core._isDisposed`), true); + }); }); async function openTerminal(options: ITerminalOptions = {}): Promise { diff --git a/yarn.lock b/yarn.lock index 7b987a00..0019809a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4719,7 +4719,12 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@3.5, typescript@^3.5.1: +typescript@3.6: + version "3.6.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.6.3.tgz#fea942fabb20f7e1ca7164ff626f1a9f3f70b4da" + integrity sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw== + +typescript@^3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.1.tgz#ba72a6a600b2158139c5dd8850f700e231464202" integrity sha512-64HkdiRv1yYZsSe4xC1WVgamNigVYjlssIoaH2HcZF0+ijsk5YK2g0G34w9wJkze8+5ow4STd22AynfO6ZYYLw==