mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge branch 'master' into layering4
This commit is contained in:
@@ -153,6 +153,7 @@ Xterm.js is used in several world-class applications to provide great terminal e
|
||||
- [**Bastillion**](https://www.bastillion.io): Bastillion is an open-source web-based SSH console that centrally manages administrative access to systems.
|
||||
- [**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 `<ng-terminal></ng-terminal>` into your component.
|
||||
- [**tty-share**](https://tty-share.com): Extremely simple terminal sharing over the Internet.
|
||||
|
||||
[And much more...](https://github.com/xtermjs/xterm.js/network/dependents)
|
||||
|
||||
|
||||
+3
-1
@@ -21,7 +21,7 @@ if (process.argv.length > 2) {
|
||||
testFiles = process.argv.slice(2);
|
||||
}
|
||||
|
||||
cp.spawnSync(
|
||||
const run = cp.spawnSync(
|
||||
path.resolve(__dirname, '../node_modules/.bin/mocha'),
|
||||
testFiles,
|
||||
{
|
||||
@@ -30,3 +30,5 @@ cp.spawnSync(
|
||||
stdio: 'inherit'
|
||||
}
|
||||
);
|
||||
|
||||
process.exit(run.status);
|
||||
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*
|
||||
* Script to test different mouse modes in terminal emulators.
|
||||
* Tests for protocols DECSET 9, 1000, 1002, 1003 with different
|
||||
* report encodings (default, UTF8, SGR, URXVT).
|
||||
*
|
||||
* VT200 Highlight mode (DECSET 1001) is not implemented.
|
||||
*
|
||||
* The test basically applies the report data to the cursor, thus
|
||||
* a mouse report should move the cursor to the cell under the mouse.
|
||||
* Furthermore the reports are printed in the left lower corner as
|
||||
* raw data and their meaning.
|
||||
*
|
||||
* A failing test might show:
|
||||
* - wrong coords: the cursor will jump to some other place
|
||||
* - wrong buttons: see meaning output and check whether it makes sense
|
||||
* - faulty reports: inspect the raw data and compare with other emulators
|
||||
* - missing events: compare with spec / other emulators
|
||||
*/
|
||||
|
||||
let activeProtocol = 0;
|
||||
let activeEnc = 0;
|
||||
|
||||
const stdin = process.openStdin();
|
||||
process.stdin.setRawMode(true);
|
||||
|
||||
// close handler - reset terminal on exit
|
||||
stdin.addListener('data', function(data) {
|
||||
if (data[0] === 0x03) {
|
||||
process.stdin.setRawMode(false);
|
||||
process.stdout.write('\x1bc');
|
||||
process.exit();
|
||||
}
|
||||
if (data[0] === 0x01) {
|
||||
switchActiveProtocol();
|
||||
printMenu();
|
||||
}
|
||||
if (data[0] === 0x02) {
|
||||
switchActiveEnc();
|
||||
printMenu();
|
||||
}
|
||||
console.log('\x1b[100;H\x1b[2A\x1b[2KReport:', data, [data.toString('binary')]);
|
||||
// filter mouse reports
|
||||
if (data[0] === 0x1b && data[1] === '['.charCodeAt(0)) {
|
||||
applyReportData(data);
|
||||
}
|
||||
});
|
||||
|
||||
// basic button codes (modifier keys are added on top)
|
||||
const BUTTONS = {
|
||||
0: ['left', 'press'],
|
||||
1: ['middle', 'press'],
|
||||
2: ['right', 'press'],
|
||||
3: ['', 'release'],
|
||||
32: ['left', 'move'],
|
||||
33: ['middle', 'move'],
|
||||
34: ['right', 'move'],
|
||||
35: ['', 'move'],
|
||||
64: ['wheel', 'up'],
|
||||
65: ['wheel', 'down']
|
||||
};
|
||||
|
||||
function evalButtonCode(code) {
|
||||
// 2 bits: 0 - left, 1 - middle, 2 - right, 3 - release
|
||||
// higher bits: 4 - shift, 8 - meta, 16 - control
|
||||
const modifier = {shift: !!(code & 4), meta: !!(code & 8), control: !!(code & 16)};
|
||||
const wheel = code & 64;
|
||||
let action;
|
||||
let button;
|
||||
if (wheel) {
|
||||
action = (code & 1) ? 'down' : 'up';
|
||||
button = 'wheel';
|
||||
} else {
|
||||
action = code & 32 ? 'move' : code === 3 ? 'release' : 'press';
|
||||
code &= 3; // TODO: more than 3 buttons + wheel
|
||||
button = code === 0 ? 'left' : code === 1 ? 'middle' : code === 2 ? 'right' : '<none>';
|
||||
}
|
||||
return {button, action, modifier};
|
||||
}
|
||||
|
||||
// protocols
|
||||
const PROTOCOLS = {
|
||||
'9 (X10: press only)': '\x1b[?9h',
|
||||
'1000 (VT200: press, release, wheel)': '\x1b[?1000h',
|
||||
// '1001 (VT200 highlight)': '\x1b[?1001h', // handle of backreport not implemented
|
||||
'1002 (press, release, move on pressed, wheel)': '\x1b[?1002h',
|
||||
'1003 (press, relase, move, wheel)': '\x1b[?1003h'
|
||||
}
|
||||
|
||||
// encodings: ENCODING_NAME => [sequence, parse_report]
|
||||
const ENC = {
|
||||
'DEFAULT' : [
|
||||
'',
|
||||
// format: CSI M <button + 32> <row + 32> <col + 32>
|
||||
report => ({
|
||||
state: evalButtonCode(report[3] - 32),
|
||||
row: report[4] - 32,
|
||||
col: report[5] - 32
|
||||
})
|
||||
],
|
||||
'UTF8' : [
|
||||
'\x1b[?1005h',
|
||||
// format: CSI M <button + 32> <row + 32> <col + 32>
|
||||
// + utf8 encoding on row/col
|
||||
report => {
|
||||
const sReport = report.toString(); // decode with utf8
|
||||
return {
|
||||
state: evalButtonCode(sReport.charCodeAt(3) - 32),
|
||||
row: sReport.charCodeAt(4) - 32,
|
||||
col: sReport.charCodeAt(5) - 32
|
||||
};
|
||||
}
|
||||
],
|
||||
'SGR' : [
|
||||
'\x1b[?1006h',
|
||||
// format: CSI < Pbutton ; Prow ; Pcol M
|
||||
report => {
|
||||
// strip off introducer + M
|
||||
const sReport = report.toString().slice(3, -1);
|
||||
const [buttonCode, row, col] = sReport.split(';');
|
||||
const state = evalButtonCode(buttonCode);
|
||||
if (report[report.length - 1] === 'm'.charCodeAt(0)) {
|
||||
state.action = 'release';
|
||||
}
|
||||
return {state, row, col};
|
||||
}
|
||||
],
|
||||
'URXVT': [
|
||||
'\x1b[?1015h',
|
||||
// format: CSI <button + 32> ; Prow ; Pcol M
|
||||
report => {
|
||||
// strip off introducer + M
|
||||
const sReport = report.toString().slice(2, -1);
|
||||
const [button, row, col] = sReport.split(';');
|
||||
return {state: evalButtonCode(button - 32), row, col};
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
function printMenu() {
|
||||
console.log('\x1b[2J\x1b\[HTest mouse reports [Ctrl-C to exit]');
|
||||
console.log();
|
||||
console.log(' Selected protocol [Ctrl-A to switch]');
|
||||
const protocols = Object.keys(PROTOCOLS);
|
||||
for (let i = 0; i < protocols.length; ++i) {
|
||||
console.log(` ${activeProtocol === i ? '->' : ' '} ${protocols[i]}`);
|
||||
}
|
||||
console.log();
|
||||
console.log(' Selected encoding [Ctrl-B to switch]');
|
||||
const encs = Object.keys(ENC);
|
||||
for (let i = 0; i < encs.length; ++i) {
|
||||
console.log(` ${activeEnc === i ? '->' : ' '} ${encs[i]}`);
|
||||
}
|
||||
process.stdout.write('\x1b[100;H');
|
||||
}
|
||||
|
||||
function switchActiveProtocol() {
|
||||
activeProtocol++;
|
||||
activeProtocol %= Object.keys(PROTOCOLS).length;
|
||||
activate();
|
||||
}
|
||||
|
||||
function switchActiveEnc() {
|
||||
activeEnc++;
|
||||
activeEnc %= Object.keys(ENC).length;
|
||||
activate();
|
||||
}
|
||||
|
||||
function activate() {
|
||||
// clear all protocols and encodings
|
||||
process.stdout.write('\x1b[?9l\x1b[?1000l\x1b[?1001l\x1b[?1002l\x1b[?1003l');
|
||||
process.stdout.write('\x1b[?1005l\x1b[?1006l\x1b[?1015l');
|
||||
// apply new protocol and encoding
|
||||
process.stdout.write(PROTOCOLS[Object.keys(PROTOCOLS)[activeProtocol]]);
|
||||
process.stdout.write(ENC[Object.keys(ENC)[activeEnc]][0]);
|
||||
console.log('\x1b[100;H\x1b[2A\x1b[2KReport:');
|
||||
}
|
||||
|
||||
function applyReportData(data) {
|
||||
let {state, row, col} = ENC[Object.keys(ENC)[activeEnc]][1](data);
|
||||
console.log('\x1b[2KButton:', state.button, 'Action:', state.action, 'Modifier:', state.modifier, 'row:', row, 'col:', col);
|
||||
// apply to cursor position
|
||||
process.stdout.write(`\x1b[${col};${row}H`);
|
||||
}
|
||||
|
||||
printMenu();
|
||||
activate();
|
||||
+32
-11
@@ -94,6 +94,7 @@ describe('Terminal', () => {
|
||||
expect(e.domEvent).to.be.an.instanceof(Object);
|
||||
done();
|
||||
});
|
||||
(<any>term).textarea = { value: '' };
|
||||
const evKeyDown = <KeyboardEvent>{
|
||||
preventDefault: () => { },
|
||||
stopPropagation: () => { },
|
||||
@@ -555,12 +556,16 @@ describe('Terminal', () => {
|
||||
afterEach(() => term.browser.isMac = originalIsMac);
|
||||
|
||||
it('should interfere with the alt key on keyDown', () => {
|
||||
(<any>term)._keyDownHandled = false;
|
||||
evKeyDown.altKey = true;
|
||||
evKeyDown.keyCode = 81;
|
||||
assert.equal(term.keyDown(evKeyDown), false);
|
||||
term.keyDown(evKeyDown);
|
||||
assert.equal((<any>term)._keyDownHandled, true);
|
||||
(<any>term)._keyDownHandled = false;
|
||||
evKeyDown.altKey = true;
|
||||
evKeyDown.keyCode = 192;
|
||||
assert.equal(term.keyDown(evKeyDown), false);
|
||||
term.keyDown(evKeyDown);
|
||||
assert.equal((<any>term)._keyDownHandled, true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -573,21 +578,29 @@ describe('Terminal', () => {
|
||||
afterEach(() => term.browser.isMac = originalIsMac);
|
||||
|
||||
it('should not interfere with the alt key on keyDown', () => {
|
||||
(<any>term)._keyDownHandled = false;
|
||||
evKeyDown.altKey = true;
|
||||
evKeyDown.keyCode = 81;
|
||||
assert.equal(term.keyDown(evKeyDown), true);
|
||||
term.keyDown(evKeyDown);
|
||||
assert.equal((<any>term)._keyDownHandled, false);
|
||||
(<any>term)._keyDownHandled = false;
|
||||
evKeyDown.altKey = true;
|
||||
evKeyDown.keyCode = 192;
|
||||
assert.equal(term.keyDown(evKeyDown), true);
|
||||
term.keyDown(evKeyDown);
|
||||
assert.equal((<any>term)._keyDownHandled, false);
|
||||
});
|
||||
|
||||
it('should interefere with the alt + arrow keys', () => {
|
||||
it('should interfere with the alt + arrow keys', () => {
|
||||
(<any>term)._keyDownHandled = false;
|
||||
evKeyDown.altKey = true;
|
||||
evKeyDown.keyCode = 37;
|
||||
assert.equal(term.keyDown(evKeyDown), false);
|
||||
term.keyDown(evKeyDown);
|
||||
assert.equal((<any>term)._keyDownHandled, true);
|
||||
(<any>term)._keyDownHandled = false;
|
||||
evKeyDown.altKey = true;
|
||||
evKeyDown.keyCode = 39;
|
||||
assert.equal(term.keyDown(evKeyDown), false);
|
||||
term.keyDown(evKeyDown);
|
||||
assert.equal((<any>term)._keyDownHandled, true);
|
||||
});
|
||||
|
||||
it('should emit key with alt + key on keyPress', (done) => {
|
||||
@@ -639,24 +652,32 @@ describe('Terminal', () => {
|
||||
afterEach(() => term.browser.isWindows = originalIsWindows);
|
||||
|
||||
it('should not interfere with the alt + ctrl key on keyDown', () => {
|
||||
(<any>term)._keyDownHandled = false;
|
||||
evKeyPress.altKey = true;
|
||||
evKeyPress.ctrlKey = true;
|
||||
evKeyPress.keyCode = 81;
|
||||
assert.equal(term.keyDown(evKeyPress), true);
|
||||
term.keyDown(evKeyPress);
|
||||
assert.equal((<any>term)._keyDownHandled, false);
|
||||
(<any>term)._keyDownHandled = false;
|
||||
evKeyDown.altKey = true;
|
||||
evKeyDown.ctrlKey = true;
|
||||
evKeyDown.keyCode = 81;
|
||||
assert.equal(term.keyDown(evKeyDown), true);
|
||||
term.keyDown(evKeyDown);
|
||||
assert.equal((<any>term)._keyDownHandled, false);
|
||||
});
|
||||
|
||||
it('should interefere with the alt + ctrl + arrow keys', () => {
|
||||
evKeyDown.altKey = true;
|
||||
evKeyDown.ctrlKey = true;
|
||||
|
||||
(<any>term)._keyDownHandled = false;
|
||||
evKeyDown.keyCode = 37;
|
||||
assert.equal(term.keyDown(evKeyDown), false);
|
||||
term.keyDown(evKeyDown);
|
||||
assert.equal((<any>term)._keyDownHandled, true);
|
||||
(<any>term)._keyDownHandled = false;
|
||||
evKeyDown.keyCode = 39;
|
||||
assert.equal(term.keyDown(evKeyDown), false);
|
||||
term.keyDown(evKeyDown);
|
||||
assert.equal((<any>term)._keyDownHandled, true);
|
||||
});
|
||||
|
||||
it('should emit key with alt + ctrl + key on keyPress', (done) => {
|
||||
|
||||
+23
-10
@@ -178,6 +178,13 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
|
||||
// Store if user went browsing history in scrollback
|
||||
private _userScrolling: boolean;
|
||||
|
||||
/**
|
||||
* Records whether the keydown event has already been handled and triggered a data event, if so
|
||||
* the keypress event should not trigger a data event but should still print to the textarea so
|
||||
* screen readers will announce it.
|
||||
*/
|
||||
private _keyDownHandled: boolean = false;
|
||||
|
||||
private _inputHandler: InputHandler;
|
||||
public linkifier: ILinkifier;
|
||||
public viewport: IViewport;
|
||||
@@ -245,13 +252,13 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
|
||||
this._instantiationService.setService(IOptionsService, this.optionsService);
|
||||
this._bufferService = this._instantiationService.createInstance(BufferService);
|
||||
this._instantiationService.setService(IBufferService, this._bufferService);
|
||||
this._logService = this._instantiationService.createInstance(LogService);
|
||||
this._instantiationService.setService(ILogService, this._logService);
|
||||
this._coreService = this._instantiationService.createInstance(CoreService, () => this.scrollToBottom());
|
||||
this._instantiationService.setService(ICoreService, this._coreService);
|
||||
this._coreService.onData(e => this._onData.fire(e));
|
||||
this._dirtyRowService = this._instantiationService.createInstance(DirtyRowService);
|
||||
this._instantiationService.setService(IDirtyRowService, this._dirtyRowService);
|
||||
this._logService = this._instantiationService.createInstance(LogService);
|
||||
this._instantiationService.setService(ILogService, this._logService);
|
||||
|
||||
this._setupOptionsListeners();
|
||||
this._setup();
|
||||
@@ -1514,6 +1521,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
|
||||
* @param ev The keydown event to be handled.
|
||||
*/
|
||||
protected _keyDown(event: KeyboardEvent): boolean {
|
||||
this._keyDownHandled = false;
|
||||
|
||||
if (this._customKeyEventHandler && this._customKeyEventHandler(event) === false) {
|
||||
return false;
|
||||
}
|
||||
@@ -1529,12 +1538,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
|
||||
|
||||
this.updateCursorStyle(event);
|
||||
|
||||
// if (result.key === C0.DC3) { // XOFF
|
||||
// this._writeStopped = true;
|
||||
// } else if (result.key === C0.DC1) { // XON
|
||||
// this._writeStopped = false;
|
||||
// }
|
||||
|
||||
if (result.type === KeyboardResultType.PAGE_DOWN || result.type === KeyboardResultType.PAGE_UP) {
|
||||
const scrollCount = this.rows - 1;
|
||||
this.scrollLines(result.type === KeyboardResultType.PAGE_UP ? -scrollCount : scrollCount);
|
||||
@@ -1558,11 +1561,17 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
|
||||
return true;
|
||||
}
|
||||
|
||||
// If ctrl+c or enter is being sent, clear out the textarea. This is done so that screen readers
|
||||
// will announce deleted characters. This will not work 100% of the time but it should cover
|
||||
// most scenarios.
|
||||
if (result.key === C0.ETX || result.key === C0.CR) {
|
||||
this.textarea.value = '';
|
||||
}
|
||||
|
||||
this._keyDownHandled = true;
|
||||
this._onKey.fire({ key: result.key, domEvent: event });
|
||||
this.showCursor();
|
||||
this._coreService.triggerDataEvent(result.key, true);
|
||||
|
||||
return this.cancel(event, true);
|
||||
}
|
||||
|
||||
private _isThirdLevelShift(browser: IBrowser, ev: IKeyboardEvent): boolean {
|
||||
@@ -1620,6 +1629,10 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
|
||||
protected _keyPress(ev: KeyboardEvent): boolean {
|
||||
let key;
|
||||
|
||||
if (this._keyDownHandled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this._customKeyEventHandler && this._customKeyEventHandler(ev) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ICoreService, IOptionsService, IBufferService } from 'common/services/Services';
|
||||
import { ICoreService, ILogService, IOptionsService, IBufferService } from 'common/services/Services';
|
||||
import { EventEmitter, IEvent } from 'common/EventEmitter';
|
||||
import { IDecPrivateModes } from 'common/Types';
|
||||
import { clone } from 'common/Clone';
|
||||
@@ -26,6 +26,7 @@ export class CoreService implements ICoreService {
|
||||
// TODO: Move this into a service
|
||||
private readonly _scrollToBottom: () => void,
|
||||
@IBufferService private readonly _bufferService: IBufferService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@IOptionsService private readonly _optionsService: IOptionsService
|
||||
) {
|
||||
this.decPrivateModes = clone(DEFAULT_DEC_PRIVATE_MODES);
|
||||
@@ -53,6 +54,7 @@ export class CoreService implements ICoreService {
|
||||
}
|
||||
|
||||
// Fire onData API
|
||||
this._logService.debug('sending data', data);
|
||||
this._onData.fire(data);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user