From 0684e0587757020ffbc8ac0058c52d00ac749456 Mon Sep 17 00:00:00 2001 From: Exile Date: Mon, 16 Oct 2017 07:17:51 +0000 Subject: [PATCH 01/10] Smart Keyboard Arrows --- src/Terminal.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/Terminal.ts b/src/Terminal.ts index 5ea01092..f6b9f637 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1415,6 +1415,36 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT }; const modifiers = (ev.shiftKey ? 1 : 0) | (ev.altKey ? 2 : 0) | (ev.ctrlKey ? 4 : 0) | (ev.metaKey ? 8 : 0); switch (ev.keyCode) { + case 0: + if (ev.key === "UIKeyInputUpArrow") { + if (this.applicationCursor) { + result.key = C0.ESC + 'OA'; + } else { + result.key = C0.ESC + '[A'; + } + } + else if (ev.key === "UIKeyInputLeftArrow") { + if (this.applicationCursor) { + result.key = C0.ESC + 'OD'; + } else { + result.key = C0.ESC + '[D'; + } + } + else if (ev.key === "UIKeyInputRightArrow") { + if (this.applicationCursor) { + result.key = C0.ESC + 'OC'; + } else { + result.key = C0.ESC + '[C'; + } + } + else if (ev.key === "UIKeyInputDownArrow") { + if (this.applicationCursor) { + result.key = C0.ESC + 'OB'; + } else { + result.key = C0.ESC + '[B'; + } + } + break; case 8: // backspace if (ev.shiftKey) { From 822b31f7b763a873c94f36d544b85893674b8844 Mon Sep 17 00:00:00 2001 From: Exile Date: Mon, 16 Oct 2017 08:02:03 +0000 Subject: [PATCH 02/10] Addressing linter errors --- src/Terminal.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index f6b9f637..6c68f23a 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1416,28 +1416,28 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT const modifiers = (ev.shiftKey ? 1 : 0) | (ev.altKey ? 2 : 0) | (ev.ctrlKey ? 4 : 0) | (ev.metaKey ? 8 : 0); switch (ev.keyCode) { case 0: - if (ev.key === "UIKeyInputUpArrow") { + if (ev.key === 'UIKeyInputUpArrow') { if (this.applicationCursor) { result.key = C0.ESC + 'OA'; } else { result.key = C0.ESC + '[A'; } } - else if (ev.key === "UIKeyInputLeftArrow") { + else if (ev.key === 'UIKeyInputLeftArrow') { if (this.applicationCursor) { result.key = C0.ESC + 'OD'; } else { result.key = C0.ESC + '[D'; } } - else if (ev.key === "UIKeyInputRightArrow") { + else if (ev.key === 'UIKeyInputRightArrow') { if (this.applicationCursor) { result.key = C0.ESC + 'OC'; } else { result.key = C0.ESC + '[C'; } } - else if (ev.key === "UIKeyInputDownArrow") { + else if (ev.key === 'UIKeyInputDownArrow') { if (this.applicationCursor) { result.key = C0.ESC + 'OB'; } else { From 700a10f3f4a6a840fcdd55fec668f5cfd64a5712 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 27 Oct 2017 10:23:46 -0700 Subject: [PATCH 03/10] Initial winptyCompat addon --- demo/index.html | 1 + demo/main.js | 1 + gulpfile.js | 36 ++++++++++++++-- package.json | 2 +- src/addons/winptyCompat/tsconfig.json | 10 +++++ src/addons/winptyCompat/winptyCompat.ts | 56 +++++++++++++++++++++++++ 6 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 src/addons/winptyCompat/tsconfig.json create mode 100644 src/addons/winptyCompat/winptyCompat.ts diff --git a/demo/index.html b/demo/index.html index daa60b6f..8d900903 100644 --- a/demo/index.html +++ b/demo/index.html @@ -12,6 +12,7 @@ +

xterm.js: xterm, in the browser

diff --git a/demo/main.js b/demo/main.js index 2ed2b33c..9de682bf 100644 --- a/demo/main.js +++ b/demo/main.js @@ -91,6 +91,7 @@ function createTerminal() { term.open(terminalContainer); term.fit(); + term.winptyCompatInit(); // fit is called within a setTimeout, cols and rows need this. setTimeout(function () { diff --git a/gulpfile.js b/gulpfile.js index cf001da3..7e852cf2 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -19,7 +19,6 @@ const util = require('gulp-util'); let buildDir = process.env.BUILD_DIR || 'build'; let tsProject = ts.createProject('tsconfig.json'); -let tsProjectSearchAddon = ts.createProject('./src/addons/search/tsconfig.json'); let srcDir = tsProject.config.compilerOptions.rootDir; let outDir = tsProject.config.compilerOptions.outDir; @@ -46,16 +45,29 @@ gulp.task('tsc', function () { ); fs.emptyDirSync(`${outDir}/addons/search`); + fs.emptyDirSync(`${outDir}/addons/winptyCompat`); + + let tsProjectSearchAddon = ts.createProject('./src/addons/search/tsconfig.json'); let tsResultSearchAddon = tsProjectSearchAddon.src().pipe(sourcemaps.init()).pipe(tsProjectSearchAddon()); let tscSearchAddon = tsResultSearchAddon.js.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})).pipe(gulp.dest(`${outDir}/addons/search`)); + let tsProjectWinptyCompatAddon = ts.createProject('./src/addons/winptyCompat/tsconfig.json'); + let tsResultWinptyCompatAddon = tsProjectWinptyCompatAddon.src().pipe(sourcemaps.init()).pipe(tsProjectWinptyCompatAddon()); + let tscWinptyCompatAddon = tsResultWinptyCompatAddon.js.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})).pipe(gulp.dest(`${outDir}/addons/winptyCompat`)); + // Copy all addons from ${srcDir}/ to ${outDir}/ - let copyAddons = gulp.src([`${srcDir}/addons/**/*`, `!${srcDir}/addons/search`, `!${srcDir}/addons/search/**`]).pipe(gulp.dest(`${outDir}/addons`)); + let copyAddons = gulp.src([ + `${srcDir}/addons/**/*`, + `!${srcDir}/addons/search`, + `!${srcDir}/addons/search/**`, + `!${srcDir}/addons/winptyCompat`, + `!${srcDir}/addons/winptyCompat/**` + ]).pipe(gulp.dest(`${outDir}/addons`)); // Copy stylesheets from ${srcDir}/ to ${outDir}/ let copyStylesheets = gulp.src(`${srcDir}/**/*.css`).pipe(gulp.dest(outDir)); - return merge(tsc, tscSearchAddon, copyAddons, copyStylesheets); + return merge(tsc, tscSearchAddon, tscWinptyCompatAddon, copyAddons, copyStylesheets); }); /** @@ -105,6 +117,22 @@ gulp.task('browserify-addons', ['tsc'], function() { .pipe(sourcemaps.write('./')) .pipe(gulp.dest(buildDir)); + let winptyCompatOptions = { + basedir: `${buildDir}/addons/winptyCompat`, + debug: true, + entries: [`${outDir}/addons/winptyCompat/winptyCompat.js`], + cache: {}, + packageCache: {} + }; + let winptyCompatBundle = browserify(winptyCompatOptions) + .external(path.join(outDir, 'Terminal.js')) + .bundle() + .pipe(source('./addons/winptyCompat/winptyCompat.js')) + .pipe(buffer()) + .pipe(sourcemaps.init({loadMaps: true, sourceRoot: ''})) + .pipe(sourcemaps.write('./')) + .pipe(gulp.dest(buildDir)); + // Copy all add-ons from outDir to buildDir let copyAddons = gulp.src([ // Copy JS addons @@ -114,7 +142,7 @@ gulp.task('browserify-addons', ['tsc'], function() { `!${outDir}/addons/search/**` ]).pipe(gulp.dest(`${buildDir}/addons`)); - return merge(searchBundle, copyAddons); + return merge(searchBundle, winptyCompatBundle, copyAddons); }); gulp.task('instrument-test', function () { diff --git a/package.json b/package.json index f32918df..91860da5 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "jsdoc": "3.4.3", "jsdom": "^11.1.0", "merge-stream": "^1.0.1", - "node-pty": "^0.4.1", + "node-pty": "^0.6.0", "nodemon": "1.10.2", "sorcery": "^0.10.0", "tslint": "^4.0.2", diff --git a/src/addons/winptyCompat/tsconfig.json b/src/addons/winptyCompat/tsconfig.json new file mode 100644 index 00000000..e15a09a8 --- /dev/null +++ b/src/addons/winptyCompat/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "rootDir": ".", + "outDir": "../../../lib/addons/winptyCompat/", + "sourceMap": true, + "removeComments": true + } +} diff --git a/src/addons/winptyCompat/winptyCompat.ts b/src/addons/winptyCompat/winptyCompat.ts new file mode 100644 index 00000000..06a2a81a --- /dev/null +++ b/src/addons/winptyCompat/winptyCompat.ts @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +declare var exports: any; +declare var module: any; +declare var define: any; +declare var require: any; +declare var window: any; + +(function (addon) { + if (typeof window !== 'undefined' && 'Terminal' in window) { + /** + * Plain browser environment + */ + addon((window).Terminal); + } else if (typeof exports === 'object' && typeof module === 'object') { + /** + * CommonJS environment + */ + module.exports = addon(require('../../Terminal').Terminal); + } else if (typeof define === 'function') { + /** + * Require.js is available + */ + define(['../../xterm'], addon); + } +})((Terminal: any) => { + Terminal.prototype.winptyCompatInit = function(): void { + // Don't do anything when the platform is not Windows + const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0; + if (!isWindows) { + return; + } + + // Winpty does not support wraparound mode which means that lines will never + // be marked as wrapped. This causes issues for things like copying a line + // retaining the wrapped new line characters or if consumers are listening + // in on the data stream. + // + // The workaround for this is to listen to every incoming line feed and mark + // the line as wrapped if the last character in the previous line is not a + // space. This is certainly not without its problems, but generally on + // Windows when text reaches the end of the terminal it's likely going to be + // wrapped. + this.on('lineFeed', () => { + const line = this.buffer.lines.get(this.buffer.ybase + this.buffer.y - 1); + const lastChar = line[line.length - 1]; + if (lastChar[3] !== 32 /* ' ' */) { + const nextLine = this.buffer.lines.get(this.buffer.ybase + this.buffer.y); + (nextLine).isWrapped = true; + } + }); + }; +}); From 59759c718bffb2322820774fd798b06ac92cfa93 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 27 Oct 2017 11:33:05 -0700 Subject: [PATCH 04/10] node-pty@0.7.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f32918df..8d337fc8 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "jsdoc": "3.4.3", "jsdom": "^11.1.0", "merge-stream": "^1.0.1", - "node-pty": "^0.4.1", + "node-pty": "^0.7.2", "nodemon": "1.10.2", "sorcery": "^0.10.0", "tslint": "^4.0.2", From d81df46453637efa0bd979f992454febb3e01ff2 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 27 Oct 2017 16:48:00 -0700 Subject: [PATCH 05/10] Don't send mouse events when selection is forced Fixes #1090 --- src/SelectionManager.ts | 13 ++++++++++--- src/Terminal.ts | 7 ++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 5dd652e0..4f50d35a 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -307,6 +307,15 @@ export class SelectionManager extends EventEmitter implements ISelectionManager return (offset / Math.abs(offset)) + Math.round(offset * (DRAG_SCROLL_MAX_SPEED - 1)); } + /** + * Returns whether the selection manager should force selection, regardless of + * whether the terminal is in mouse events mode. + * @param event The mouse event. + */ + public shouldForceSelection(event: MouseEvent): boolean { + return Browser.isMac ? event.altKey : event.shiftKey; + } + /** * Handles te mousedown event, setting up for a new selection. * @param event The mousedown event. @@ -325,9 +334,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager // Allow selection when using a specific modifier key, even when disabled if (!this._enabled) { - const shouldForceSelection = Browser.isMac ? event.altKey : event.shiftKey; - - if (!shouldForceSelection) { + if (!this.shouldForceSelection(event)) { return; } diff --git a/src/Terminal.ts b/src/Terminal.ts index 394fb4de..dd32d266 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -943,7 +943,12 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT ev.preventDefault(); this.focus(); - if (!this.mouseEvents) return; + // Don't send the mouse button to the pty if mouse events are disabled or + // if the selection manager is having selection forced (ie. a modifier is + // held). + if (!this.mouseEvents || this.selectionManager.shouldForceSelection(ev)) { + return; + } // send the button sendButton(ev); From 664c0f0f777265e6d8893eb118a2e0581fb9b908 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 28 Oct 2017 17:44:15 -0700 Subject: [PATCH 06/10] Clarify that parent needs to be visible when calling open Fixes #1089 --- typings/xterm.d.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index f0ab9daf..2cba5deb 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -290,7 +290,9 @@ declare module 'xterm' { /** * Opens the terminal within an element. - * @param parent The element to create the terminal within. + * @param parent The element to create the terminal within. This element + * must be visible (have dimensions) when `open` is called as several DOM- + * based measurements need to be performed when this function is called. */ open(parent: HTMLElement): void; From 2fbc0be98d3f730a68898cc244ab439aba5a240f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 29 Oct 2017 12:45:37 -0700 Subject: [PATCH 07/10] Fix build issues --- package.json | 2 +- src/addons/winptyCompat/winptyCompat.ts | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/package.json b/package.json index 91860da5..8d337fc8 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "jsdoc": "3.4.3", "jsdom": "^11.1.0", "merge-stream": "^1.0.1", - "node-pty": "^0.6.0", + "node-pty": "^0.7.2", "nodemon": "1.10.2", "sorcery": "^0.10.0", "tslint": "^4.0.2", diff --git a/src/addons/winptyCompat/winptyCompat.ts b/src/addons/winptyCompat/winptyCompat.ts index 06a2a81a..1c289c70 100644 --- a/src/addons/winptyCompat/winptyCompat.ts +++ b/src/addons/winptyCompat/winptyCompat.ts @@ -4,10 +4,7 @@ */ declare var exports: any; -declare var module: any; declare var define: any; -declare var require: any; -declare var window: any; (function (addon) { if (typeof window !== 'undefined' && 'Terminal' in window) { From d7f6f96ba678887c2100e8a4047a1a72f4d45957 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 29 Oct 2017 13:01:38 -0700 Subject: [PATCH 08/10] Add winptyCompat addon typings --- fixtures/typings-test/typings-test.ts | 1 + typings/xterm.d.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/fixtures/typings-test/typings-test.ts b/fixtures/typings-test/typings-test.ts index c0ac8b50..be362395 100644 --- a/fixtures/typings-test/typings-test.ts +++ b/fixtures/typings-test/typings-test.ts @@ -41,6 +41,7 @@ namespace static_methods { Terminal.loadAddon('fullscreen'); Terminal.loadAddon('search'); Terminal.loadAddon('terminado'); + Terminal.loadAddon('winptyCompat'); } } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index f20f3502..0997debf 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -513,6 +513,6 @@ declare module 'xterm' { * available to all newly created Terminals. * @param addon The addon to load. */ - static loadAddon(addon: 'attach' | 'fit' | 'fullscreen' | 'search' | 'terminado'): void; + static loadAddon(addon: 'attach' | 'fit' | 'fullscreen' | 'search' | 'terminado' | 'winptyCompat'): void; } } From aa5a9911de92e87a262ebad475c8df976d567da4 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 29 Oct 2017 13:16:29 -0700 Subject: [PATCH 09/10] Base wrapped line on current width, not original width Lines are never trimmed, so even if the term's size is reduced it could contain empty lines at the end when wrapping is valid --- src/addons/winptyCompat/winptyCompat.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/addons/winptyCompat/winptyCompat.ts b/src/addons/winptyCompat/winptyCompat.ts index 1c289c70..7474df2e 100644 --- a/src/addons/winptyCompat/winptyCompat.ts +++ b/src/addons/winptyCompat/winptyCompat.ts @@ -43,7 +43,7 @@ declare var define: any; // wrapped. this.on('lineFeed', () => { const line = this.buffer.lines.get(this.buffer.ybase + this.buffer.y - 1); - const lastChar = line[line.length - 1]; + const lastChar = line[this.cols - 1]; if (lastChar[3] !== 32 /* ' ' */) { const nextLine = this.buffer.lines.get(this.buffer.ybase + this.buffer.y); (nextLine).isWrapped = true; From 79cdaa8d3bfe6c2f3af2ba2a4837abf319c404e8 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 30 Oct 2017 11:21:22 -0700 Subject: [PATCH 10/10] Initialize scaled dimension variables Fixes #1089 --- src/renderer/BaseRenderLayer.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 5559d270..b4becbce 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -15,12 +15,12 @@ const DIM_OPACITY = 0.5; export abstract class BaseRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; protected _ctx: CanvasRenderingContext2D; - private _scaledCharWidth: number; - private _scaledCharHeight: number; - private _scaledCellWidth: number; - private _scaledCellHeight: number; - private _scaledCharLeft: number; - private _scaledCharTop: number; + private _scaledCharWidth: number = 0; + private _scaledCharHeight: number = 0; + private _scaledCellWidth: number = 0; + private _scaledCellHeight: number = 0; + private _scaledCharLeft: number = 0; + private _scaledCharTop: number = 0; private _charAtlas: HTMLCanvasElement | ImageBitmap;