diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 4d4e605b..e5736562 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -8,12 +8,19 @@ RUN apt-get update \ # Verify git and process tools are installed RUN apt-get install -y git procps -# Install yarn +# Install yarn, puppeteer deps RUN apt-get install -y curl apt-transport-https lsb-release \ && curl -sS https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/pubkey.gpg | apt-key add - 2>/dev/null \ && echo "deb https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/ stable main" | tee /etc/apt/sources.list.d/yarn.list \ && apt-get update \ - && apt-get -y install --no-install-recommends yarn + && apt-get -y install --no-install-recommends \ + yarn fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst ttf-freefont \ + # https://github.com/Googlechrome/puppeteer/issues/290#issuecomment-322921352 + gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \ + libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \ + libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 \ + libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 \ + ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget # Clean up RUN apt-get autoremove -y \ diff --git a/addons/xterm-addon-attach/src/AttachAddon.api.ts b/addons/xterm-addon-attach/src/AttachAddon.api.ts index c5b2d858..94d11139 100644 --- a/addons/xterm-addon-attach/src/AttachAddon.api.ts +++ b/addons/xterm-addon-attach/src/AttachAddon.api.ts @@ -21,7 +21,7 @@ describe('AttachAddon', () => { browser = await puppeteer.launch({ headless: process.argv.indexOf('--headless') !== -1, slowMo: 80, - args: [`--window-size=${width},${height}`] + args: [`--window-size=${width},${height}`, `--no-sandbox`] }); page = (await browser.pages())[0]; await page.setViewport({ width, height }); diff --git a/addons/xterm-addon-fit/src/FitAddon.ts b/addons/xterm-addon-fit/src/FitAddon.ts index f23bd161..e25a4783 100644 --- a/addons/xterm-addon-fit/src/FitAddon.ts +++ b/addons/xterm-addon-fit/src/FitAddon.ts @@ -49,7 +49,7 @@ export class FitAddon implements ITerminalAddon { return undefined; } - if (!this._terminal.element.parentElement) { + if (!this._terminal.element || !this._terminal.element.parentElement) { return undefined; } diff --git a/addons/xterm-addon-search/src/SearchAddon.api.ts b/addons/xterm-addon-search/src/SearchAddon.api.ts index 14e12a8c..cb5a02b8 100644 --- a/addons/xterm-addon-search/src/SearchAddon.api.ts +++ b/addons/xterm-addon-search/src/SearchAddon.api.ts @@ -21,7 +21,7 @@ describe('Search Tests', function (): void { browser = await puppeteer.launch({ headless: process.argv.indexOf('--headless') !== -1, slowMo: 80, - args: [`--window-size=${width},${height}`] + args: [`--window-size=${width},${height}`, `--no-sandbox`] }); page = (await browser.pages())[0]; await page.setViewport({ width, height }); diff --git a/addons/xterm-addon-web-links/src/WebLinksAddon.api.ts b/addons/xterm-addon-web-links/src/WebLinksAddon.api.ts index 2c0044be..3fb1a536 100644 --- a/addons/xterm-addon-web-links/src/WebLinksAddon.api.ts +++ b/addons/xterm-addon-web-links/src/WebLinksAddon.api.ts @@ -20,7 +20,7 @@ describe('WebLinksAddon', () => { browser = await puppeteer.launch({ headless: process.argv.indexOf('--headless') !== -1, slowMo: 80, - args: [`--window-size=${width},${height}`] + args: [`--window-size=${width},${height}`, `--no-sandbox`] }); page = (await browser.pages())[0]; await page.setViewport({ width, height }); diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts index 66be22d9..9303782d 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts @@ -22,7 +22,7 @@ describe('WebGL Renderer Integration Tests', function(): void { browser = await puppeteer.launch({ headless: process.argv.indexOf('--headless') !== -1, slowMo: 80, - args: [`--window-size=${width},${height}`] + args: [`--window-size=${width},${height}`, `--no-sandbox`] }); page = (await browser.pages())[0]; await page.setViewport({ width, height }); diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 26f0ca73..b1ccdbf0 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -107,7 +107,7 @@ jobs: - Windows - Linux_IntegrationTests - macOS_IntegrationTests - condition: and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/master'), eq(variables['Build.SourceBranch'], 'refs/heads/release/*'))) + condition: and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/master'), eq(variables['FORCE_RELEASE'], 'true'))) pool: vmImage: 'ubuntu-16.04' steps: diff --git a/bin/publish.js b/bin/publish.js index d95ecc57..9d58ed15 100644 --- a/bin/publish.js +++ b/bin/publish.js @@ -5,6 +5,7 @@ const cp = require('child_process'); const fs = require('fs'); +const os = require('os'); const path = require('path'); // Setup auth @@ -18,8 +19,9 @@ if (isDryRun) { const changedFiles = getChangedFilesInCommit('HEAD'); // Publish xterm if any files were changed outside of the addons directory +let isStableRelease = false; if (changedFiles.some(e => e.search(/^addons\//) === -1)) { - checkAndPublishPackage(path.resolve(__dirname, '..')); + isStableRelease = checkAndPublishPackage(path.resolve(__dirname, '..')); } // Publish addons if any files were changed inside of the addon @@ -39,6 +41,11 @@ addonPackageDirs.forEach(p => { } }); +// Publish website if it's a stable release +if (isStableRelease) { + updateWebsite(); +} + function checkAndPublishPackage(packageDir) { const packageJson = require(path.join(packageDir, 'package.json')); @@ -76,6 +83,8 @@ function checkAndPublishPackage(packageDir) { } console.groupEnd(); + + return isStableRelease; } function getNextBetaVersion(packageJson) { @@ -115,3 +124,12 @@ function getChangedFilesInCommit(commit) { const changedFiles = output.split('\n').filter(e => e.length > 0); return changedFiles; } + +function updateWebsite() { + console.log('Updating website'); + const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'website-')); + const packageJson = require(path.join(path.resolve(__dirname, '..'), 'package.json')); + if (!isDryRun) { + cp.spawnSync('sh', [path.join(__dirname, 'update-website.sh'), packageJson.version], { cwd, stdio: [process.stdin, process.stdout, process.stderr] }); + } +} diff --git a/bin/update-website.sh b/bin/update-website.sh new file mode 100644 index 00000000..4379d915 --- /dev/null +++ b/bin/update-website.sh @@ -0,0 +1,41 @@ +#!/bin/sh + +# Name the arguments +VERSION=$1 + +# Clone docs repo and update the documentation +git clone https://github.com/xtermjs/xtermjs.org +cd xtermjs.org +yarn +./bin/update-docs + +# Add changes to index and only proceed if there are changes to commit +touch test-file +git add . +if ! git diff-index --quiet HEAD --; then + + # Delete the upstream branch if it exists for some reason + export BRANCH_NAME=update-$VERSION + git branch -D $BRANCH_NAME || true + git push origin :$BRANCH_NAME || true + + # Create commit and push it to update-x.y.z + git checkout -b $BRANCH_NAME + git config --global user.name Daniel Imms + git config --global user.email tyriar@tyriar.com + git commit -m 'Update docs for v$VERSION' + git push --set-upstream origin update-4.2.0 + git push -f + + # Create a PR in the GitHub repo + curl \ + -H "Authorization: token $GITHUB_TOKEN" \ + -X POST \ + -d "{\"title\":\"Update docs for v$VERSION\",\"base\":\"master\",\"head\":\"xtermjs:$BRANCH_NAME\"}" \ + https://api.github.com/repos/xtermjs/xtermjs.org/pulls + +else + + echo "No changes to commit" + +fi diff --git a/demo/client.ts b/demo/client.ts index 92bac731..e05a7105 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -221,6 +221,7 @@ function initOptions(term: TerminalType): void { bellSound: null, bellStyle: ['none', 'sound'], cursorStyle: ['block', 'underline', 'bar'], + fastScrollModifier: ['alt', 'ctrl', 'shift', undefined], fontFamily: null, fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], diff --git a/src/Terminal.ts b/src/Terminal.ts index 63400bcb..852d424d 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -76,7 +76,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp /** * The HTMLElement that the terminal is created in, set by Terminal.open. */ - private _parent: HTMLElement; + private _parent: HTMLElement | null; private _document: Document; private _viewportScrollArea: HTMLElement; private _viewportElement: HTMLElement; @@ -1469,6 +1469,10 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._charSizeService.measure(); } + // Sync the scroll area to make sure scroll events don't fire and scroll the viewport to an + // invalid location + this.viewport.syncScrollArea(true); + this.refresh(0, this.rows - 1); this._onResize.fire({ cols: x, rows: y }); } diff --git a/src/Types.d.ts b/src/Types.d.ts index cb45d3b1..1b0285b1 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -173,7 +173,7 @@ export interface ITerminal extends IPublicTerminal, IElementAccessor, IBufferAcc // Portions of the public API that are required by the internal Terminal export interface IPublicTerminal extends IDisposable { - textarea: HTMLTextAreaElement; + textarea: HTMLTextAreaElement | undefined; rows: number; cols: number; buffer: IBuffer; @@ -226,7 +226,7 @@ export interface IBufferAccessor { } export interface IElementAccessor { - readonly element: HTMLElement; + readonly element: HTMLElement | undefined; } export interface ILinkifierAccessor { diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index 2b9a4a9b..274fc16f 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -35,7 +35,7 @@ export interface IPartialColorSet { export interface IViewport extends IDisposable { scrollBarWidth: number; - syncScrollArea(): void; + syncScrollArea(immediate?: boolean): void; getLinesScrolled(ev: WheelEvent): number; onWheel(ev: WheelEvent): boolean; onTouchStart(ev: TouchEvent): void; diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index 4f9363d9..4817ab3b 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -7,7 +7,7 @@ import { Disposable } from 'common/Lifecycle'; import { addDisposableDomListener } from 'browser/Lifecycle'; import { IColorSet, IViewport } from 'browser/Types'; import { ICharSizeService, IRenderService } from 'browser/services/Services'; -import { IBufferService } from 'common/services/Services'; +import { IBufferService, IOptionsService } from 'common/services/Services'; const FALLBACK_SCROLL_BAR_WIDTH = 15; @@ -37,6 +37,7 @@ export class Viewport extends Disposable implements IViewport { private readonly _viewportElement: HTMLElement, private readonly _scrollArea: HTMLElement, @IBufferService private readonly _bufferService: IBufferService, + @IOptionsService private readonly _optionsService: IOptionsService, @ICharSizeService private readonly _charSizeService: ICharSizeService, @IRenderService private readonly _renderService: IRenderService ) { @@ -60,7 +61,14 @@ export class Viewport extends Disposable implements IViewport { * Refreshes row height, setting line-height, viewport height and scroll area height if * necessary. */ - private _refresh(): void { + private _refresh(immediate: boolean): void { + if (immediate) { + this._innerRefresh(); + if (this._refreshAnimationFrame !== null) { + cancelAnimationFrame(this._refreshAnimationFrame); + } + return; + } if (this._refreshAnimationFrame === null) { this._refreshAnimationFrame = requestAnimationFrame(() => this._innerRefresh()); } @@ -88,40 +96,39 @@ export class Viewport extends Disposable implements IViewport { this._refreshAnimationFrame = null; } - /** * Updates dimensions and synchronizes the scroll area if necessary. */ - public syncScrollArea(): void { + public syncScrollArea(immediate: boolean = false): void { // If buffer height changed if (this._lastRecordedBufferLength !== this._bufferService.buffer.lines.length) { this._lastRecordedBufferLength = this._bufferService.buffer.lines.length; - this._refresh(); + this._refresh(immediate); return; } // If viewport height changed if (this._lastRecordedViewportHeight !== this._renderService.dimensions.canvasHeight) { - this._refresh(); + this._refresh(immediate); return; } // If the buffer position doesn't match last scroll top const newScrollTop = this._bufferService.buffer.ydisp * this._currentRowHeight; if (this._lastScrollTop !== newScrollTop) { - this._refresh(); + this._refresh(immediate); return; } // If element's scroll top changed, this can happen when hiding the element if (this._lastScrollTop !== this._viewportElement.scrollTop) { - this._refresh(); + this._refresh(immediate); return; } // If row height changed if (this._renderService.dimensions.scaledCellHeight / window.devicePixelRatio !== this._currentRowHeight) { - this._refresh(); + this._refresh(immediate); return; } } @@ -191,7 +198,7 @@ export class Viewport extends Disposable implements IViewport { } // Fallback to WheelEvent.DOM_DELTA_PIXEL - let amount = ev.deltaY; + let amount = this._applyFastScrollModifier(ev.deltaY, ev); if (ev.deltaMode === WheelEvent.DOM_DELTA_LINE) { amount *= this._currentRowHeight; } else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) { @@ -212,7 +219,7 @@ export class Viewport extends Disposable implements IViewport { } // Fallback to WheelEvent.DOM_DELTA_LINE - let amount = ev.deltaY; + let amount = this._applyFastScrollModifier(ev.deltaY, ev); if (ev.deltaMode === WheelEvent.DOM_DELTA_PIXEL) { amount /= this._currentRowHeight + 0.0; // Prevent integer division this._wheelPartialScroll += amount; @@ -224,6 +231,17 @@ export class Viewport extends Disposable implements IViewport { return amount; } + private _applyFastScrollModifier(amount: number, ev: WheelEvent): number { + const modifier = this._optionsService.options.fastScrollModifier; + // Multiply the scroll speed when the modifier is down + if ((modifier === 'alt' && ev.altKey) || + (modifier === 'ctrl' && ev.ctrlKey) || + (modifier === 'shift' && ev.shiftKey)) { + return amount * Math.max(1, this._optionsService.options.fastScrollSensitivity); + } + return amount; + } + /** * Handles the touchstart event, recording the touch occurred. * @param ev The touch event. diff --git a/src/browser/services/SelectionService.ts b/src/browser/services/SelectionService.ts index 2efb095d..3631bbb7 100644 --- a/src/browser/services/SelectionService.ts +++ b/src/browser/services/SelectionService.ts @@ -210,7 +210,7 @@ export class SelectionService implements ISelectionService { for (let i = start[1] + 1; i <= end[1] - 1; i++) { const bufferLine = buffer.lines.get(i); const lineText = buffer.translateBufferLineToString(i, true); - if (bufferLine!.isWrapped) { + if (bufferLine && bufferLine.isWrapped) { result[result.length - 1] += lineText; } else { result.push(lineText); @@ -221,7 +221,7 @@ export class SelectionService implements ISelectionService { if (start[1] !== end[1]) { const bufferLine = buffer.lines.get(end[1]); const lineText = buffer.translateBufferLineToString(end[1], true, 0, end[0]); - if (bufferLine!.isWrapped) { + if (bufferLine && bufferLine!.isWrapped) { result[result.length - 1] += lineText; } else { result.push(lineText); diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index 1b3e5d86..152bab7a 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -167,19 +167,25 @@ export class Buffer implements IBuffer { if (this._rows < newRows) { for (let y = this._rows; y < newRows; y++) { if (this.lines.length < newRows + this.ybase) { - if (this.ybase > 0 && this.lines.length <= this.ybase + this.y + addToY + 1) { - // There is room above the buffer and there are no empty elements below the line, - // scroll up - this.ybase--; - addToY++; - if (this.ydisp > 0) { - // Viewport is at the top of the buffer, must increase downwards - this.ydisp--; - } - } else { - // Add a blank line if there is no buffer left at the top to scroll to, or if there - // are blank lines after the cursor + if (this._optionsService.options.windowsMode) { + // Just add the new missing rows on Windows as conpty reprints the screen with it's + // view of the world. Once a line enters scrollback for conpty it remains there this.lines.push(new BufferLine(newCols, nullCell)); + } else { + if (this.ybase > 0 && this.lines.length <= this.ybase + this.y + addToY + 1) { + // There is room above the buffer and there are no empty elements below the line, + // scroll up + this.ybase--; + addToY++; + if (this.ydisp > 0) { + // Viewport is at the top of the buffer, must increase downwards + this.ydisp--; + } + } else { + // Add a blank line if there is no buffer left at the top to scroll to, or if there + // are blank lines after the cursor + this.lines.push(new BufferLine(newCols, nullCell)); + } } } } 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 e4ae3d23..1bf378c1 100644 --- a/src/common/input/Keyboard.ts +++ b/src/common/input/Keyboard.ts @@ -122,7 +122,7 @@ export function evaluateKeyboardEvent( // 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'; @@ -141,7 +141,7 @@ export function evaluateKeyboardEvent( // 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'; @@ -158,7 +158,8 @@ export function evaluateKeyboardEvent( 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) { @@ -176,7 +177,8 @@ export function evaluateKeyboardEvent( 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/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index 9a5d2151..9230421c 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -23,6 +23,8 @@ export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({ bellSound: DEFAULT_BELL_SOUND, bellStyle: 'none', drawBoldTextInBrightColors: true, + fastScrollModifier: 'alt', + fastScrollSensitivity: 5, fontFamily: 'courier-new, courier, monospace', fontSize: 15, fontWeight: 'normal', @@ -47,7 +49,7 @@ export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({ screenKeys: false, cancelEvents: false, useFlowControl: false, - wordSeparator: ' ()[]{}\'"' + wordSeparator: ' ()[]{}\',:;"' }); /** diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 568eeaff..1df59f84 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -180,6 +180,8 @@ export interface IPartialTerminalOptions { cursorStyle?: 'block' | 'underline' | 'bar'; disableStdin?: boolean; drawBoldTextInBrightColors?: boolean; + fastScrollModifier?: 'alt' | 'ctrl' | 'shift'; + fastScrollSensitivity?: number; fontSize?: number; fontFamily?: string; fontWeight?: FontWeight; @@ -209,6 +211,8 @@ export interface ITerminalOptions { cursorStyle: 'block' | 'underline' | 'bar'; disableStdin: boolean; drawBoldTextInBrightColors: boolean; + fastScrollModifier: 'alt' | 'ctrl' | 'shift' | undefined; + fastScrollSensitivity: number; fontSize: number; fontFamily: string; fontWeight: FontWeight; diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 4b895614..b72a4cff 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -33,14 +33,14 @@ export class Terminal implements ITerminalApi { public get onRender(): IEvent<{ start: number, end: number }> { return this._core.onRender; } public get onResize(): IEvent<{ cols: number, rows: number }> { return this._core.onResize; } - public get element(): HTMLElement { return this._core.element; } + public get element(): HTMLElement | undefined { return this._core.element; } public get parser(): IParser { if (!this._parser) { this._parser = new ParserApi(this._core); } return this._parser; } - public get textarea(): HTMLTextAreaElement { return this._core.textarea; } + public get textarea(): HTMLTextAreaElement | undefined { return this._core.textarea; } public get rows(): number { return this._core.rows; } public get cols(): number { return this._core.cols; } public get buffer(): IBufferApi { return new BufferApiView(this._core.buffer); } diff --git a/test/api/CharWidth.api.ts b/test/api/CharWidth.api.ts index 035208cc..0561bf07 100644 --- a/test/api/CharWidth.api.ts +++ b/test/api/CharWidth.api.ts @@ -21,7 +21,7 @@ describe('CharWidth Integration Tests', function(): void { browser = await puppeteer.launch({ headless: process.argv.indexOf('--headless') !== -1, slowMo: 80, - args: [`--window-size=${width},${height}`] + args: [`--window-size=${width},${height}`, `--no-sandbox`] }); page = (await browser.pages())[0]; await page.setViewport({ width, height }); diff --git a/test/api/InputHandler.api.ts b/test/api/InputHandler.api.ts index 05b16010..fe068610 100644 --- a/test/api/InputHandler.api.ts +++ b/test/api/InputHandler.api.ts @@ -21,7 +21,7 @@ describe('InputHandler Integration Tests', function(): void { browser = await puppeteer.launch({ headless: process.argv.indexOf('--headless') !== -1, slowMo: 80, - args: [`--window-size=${width},${height}`] + args: [`--window-size=${width},${height}`, `--no-sandbox`] }); page = (await browser.pages())[0]; await page.setViewport({ width, height }); diff --git a/test/api/MouseTracking.api.ts b/test/api/MouseTracking.api.ts index 90c18f75..1372b567 100644 --- a/test/api/MouseTracking.api.ts +++ b/test/api/MouseTracking.api.ts @@ -11,8 +11,10 @@ const APP = 'http://127.0.0.1:3000/test'; let browser: puppeteer.Browser; let page: puppeteer.Page; -const width = 1024; -const height = 768; +// adjusted to work inside devcontainer +// see https://github.com/xtermjs/xterm.js/issues/2379 +const width = 1280; +const height = 960; // adjust terminal row/col size so we can test // >80 up to 223 and >255 @@ -216,7 +218,7 @@ describe('Mouse Tracking Tests', function(): void { browser = await puppeteer.launch({ headless: process.argv.indexOf('--headless') !== -1, slowMo: 80, - args: [`--window-size=${width},${height}`] + args: [`--window-size=${width},${height}`, `--no-sandbox`] }); page = (await browser.pages())[0]; await page.setViewport({ width, height }); diff --git a/test/api/Parser.api.ts b/test/api/Parser.api.ts index 28f7d8b3..e46242ab 100644 --- a/test/api/Parser.api.ts +++ b/test/api/Parser.api.ts @@ -21,7 +21,7 @@ describe('Parser Integration Tests', function(): void { browser = await puppeteer.launch({ headless: process.argv.indexOf('--headless') !== -1, slowMo: 80, - args: [`--window-size=${width},${height}`] + args: [`--window-size=${width},${height}`, `--no-sandbox`] }); page = (await browser.pages())[0]; await page.setViewport({ width, height }); diff --git a/test/api/Terminal.api.ts b/test/api/Terminal.api.ts index 034b04c4..08963d9c 100644 --- a/test/api/Terminal.api.ts +++ b/test/api/Terminal.api.ts @@ -21,7 +21,7 @@ describe('API Integration Tests', function(): void { browser = await puppeteer.launch({ headless: process.argv.indexOf('--headless') !== -1, slowMo: 80, - args: [`--window-size=${width},${height}`] + args: [`--window-size=${width},${height}`, `--no-sandbox`] }); page = (await browser.pages())[0]; await page.setViewport({ width, height }); @@ -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/typings/xterm.d.ts b/typings/xterm.d.ts index c1f5eab9..af7d5ca8 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -82,6 +82,16 @@ declare module 'xterm' { */ drawBoldTextInBrightColors?: boolean; + /** + * The modifier key hold to multiply scroll speed. + */ + fastScrollModifier?: 'alt' | 'ctrl' | 'shift' | undefined; + + /** + * The scroll speed multiplier used for fast scrolling. + */ + fastScrollSensitivity?: number; + /** * The font size used to render text. */ @@ -352,12 +362,12 @@ declare module 'xterm' { /** * The element containing the terminal. */ - readonly element: HTMLElement; + readonly element: HTMLElement | undefined; /** * The textarea that accepts input for the terminal. */ - readonly textarea: HTMLTextAreaElement; + readonly textarea: HTMLTextAreaElement | undefined; /** * The number of rows in the terminal's viewport. Use