mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
CoreMouseService
This commit is contained in:
+10
-10
@@ -13,7 +13,7 @@ import { CellData } from 'common/buffer/CellData';
|
||||
import { Attributes } from 'common/buffer/Constants';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { Params } from 'common/parser/Params';
|
||||
import { MockCoreService, MockBufferService, MockDirtyRowService, MockOptionsService, MockLogService } from 'common/TestUtils.test';
|
||||
import { MockCoreService, MockBufferService, MockDirtyRowService, MockOptionsService, MockLogService, MockCoreMouseService } from 'common/TestUtils.test';
|
||||
import { IBufferService } from 'common/services/Services';
|
||||
import { DEFAULT_OPTIONS } from 'common/services/OptionsService';
|
||||
import { clone } from 'common/Clone';
|
||||
@@ -33,7 +33,7 @@ describe('InputHandler', () => {
|
||||
bufferService.buffer.x = 1;
|
||||
bufferService.buffer.y = 2;
|
||||
bufferService.buffer.ybase = 0;
|
||||
const inputHandler = new InputHandler(terminal, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService());
|
||||
const inputHandler = new InputHandler(terminal, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService());
|
||||
// Save cursor position
|
||||
inputHandler.saveCursor();
|
||||
assert.equal(bufferService.buffer.x, 1);
|
||||
@@ -52,7 +52,7 @@ describe('InputHandler', () => {
|
||||
describe('setCursorStyle', () => {
|
||||
it('should call Terminal.setOption with correct params', () => {
|
||||
const optionsService = new MockOptionsService();
|
||||
const inputHandler = new InputHandler(new MockInputHandlingTerminal(), new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), optionsService);
|
||||
const inputHandler = new InputHandler(new MockInputHandlingTerminal(), new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), optionsService, new MockCoreMouseService());
|
||||
const collect = ' ';
|
||||
|
||||
inputHandler.setCursorStyle(Params.fromArray([0]), collect);
|
||||
@@ -95,7 +95,7 @@ describe('InputHandler', () => {
|
||||
const terminal = new MockInputHandlingTerminal();
|
||||
const collect = '?';
|
||||
terminal.bracketedPasteMode = false;
|
||||
const inputHandler = new InputHandler(terminal, new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService());
|
||||
const inputHandler = new InputHandler(terminal, new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService());
|
||||
// Set bracketed paste mode
|
||||
inputHandler.setMode(Params.fromArray([2004]), collect);
|
||||
assert.equal(terminal.bracketedPasteMode, true);
|
||||
@@ -114,7 +114,7 @@ describe('InputHandler', () => {
|
||||
it('insertChars', function(): void {
|
||||
const term = new Terminal();
|
||||
const bufferService = new MockBufferService(80, 30);
|
||||
const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService());
|
||||
const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService());
|
||||
|
||||
// insert some data in first and second line
|
||||
inputHandler.parse(Array(bufferService.cols - 9).join('a'));
|
||||
@@ -152,7 +152,7 @@ describe('InputHandler', () => {
|
||||
it('deleteChars', function(): void {
|
||||
const term = new Terminal();
|
||||
const bufferService = new MockBufferService(80, 30);
|
||||
const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService());
|
||||
const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService());
|
||||
|
||||
// insert some data in first and second line
|
||||
inputHandler.parse(Array(bufferService.cols - 9).join('a'));
|
||||
@@ -193,7 +193,7 @@ describe('InputHandler', () => {
|
||||
it('eraseInLine', function(): void {
|
||||
const term = new Terminal();
|
||||
const bufferService = new MockBufferService(80, 30);
|
||||
const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService());
|
||||
const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService());
|
||||
|
||||
// fill 6 lines to test 3 different states
|
||||
inputHandler.parse(Array(bufferService.cols + 1).join('a'));
|
||||
@@ -222,7 +222,7 @@ describe('InputHandler', () => {
|
||||
it('eraseInDisplay', function(): void {
|
||||
const term = new Terminal({cols: 80, rows: 7});
|
||||
const bufferService = new MockBufferService(80, 7);
|
||||
const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService());
|
||||
const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService());
|
||||
|
||||
// fill display with a's
|
||||
for (let i = 0; i < bufferService.rows; ++i) inputHandler.parse(Array(bufferService.cols + 1).join('a'));
|
||||
@@ -357,7 +357,7 @@ describe('InputHandler', () => {
|
||||
describe('print', () => {
|
||||
it('should not cause an infinite loop (regression test)', () => {
|
||||
const term = new Terminal();
|
||||
const inputHandler = new InputHandler(term, new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService());
|
||||
const inputHandler = new InputHandler(term, new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService());
|
||||
const container = new Uint32Array(10);
|
||||
container[0] = 0x200B;
|
||||
inputHandler.print(container, 0, 1);
|
||||
@@ -372,7 +372,7 @@ describe('InputHandler', () => {
|
||||
beforeEach(() => {
|
||||
term = new Terminal();
|
||||
bufferService = new MockBufferService(80, 30);
|
||||
handler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService());
|
||||
handler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService());
|
||||
});
|
||||
it('should handle DECSET/DECRST 47 (alt screen buffer)', () => {
|
||||
handler.parse('\x1b[?47h\r\n\x1b[31mJUNK\x1b[?47lTEST');
|
||||
|
||||
+10
-1
@@ -19,7 +19,7 @@ import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { IAttributeData, IDisposable } from 'common/Types';
|
||||
import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService } from 'common/services/Services';
|
||||
import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, ICoreMouseService } from 'common/services/Services';
|
||||
import { ISelectionService } from 'browser/services/Services';
|
||||
|
||||
/**
|
||||
@@ -134,6 +134,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
private readonly _dirtyRowService: IDirtyRowService,
|
||||
private readonly _logService: ILogService,
|
||||
private readonly _optionsService: IOptionsService,
|
||||
private readonly _coreMouseService: ICoreMouseService,
|
||||
private readonly _parser: IEscapeSequenceParser = new EscapeSequenceParser())
|
||||
{
|
||||
super();
|
||||
@@ -1285,6 +1286,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
// even if there is no button held down.
|
||||
|
||||
// TODO: Why are params[0] compares nested within a switch for params[0]?
|
||||
this._coreMouseService.activeProtocol = param === 9 ? 'X10' : param === 1000 ? 'VT200' : param === 1002 ? 'DRAG' : 'ANY';
|
||||
|
||||
this._terminal.x10Mouse = param === 9;
|
||||
this._terminal.vt200Mouse = param === 1000;
|
||||
@@ -1305,11 +1307,13 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
break;
|
||||
case 1005: // utf8 ext mode mouse
|
||||
this._terminal.utfMouse = true;
|
||||
this._coreMouseService.activeEncoding = 'UTF8';
|
||||
// for wide terminals
|
||||
// simply encodes large values as utf8 characters
|
||||
break;
|
||||
case 1006: // sgr ext mode mouse
|
||||
this._terminal.sgrMouse = true;
|
||||
this._coreMouseService.activeEncoding = 'SGR';
|
||||
// for wide terminals
|
||||
// does not add 32 to fields
|
||||
// press: ^[[<b;x;yM
|
||||
@@ -1317,6 +1321,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
break;
|
||||
case 1015: // urxvt ext mode mouse
|
||||
this._terminal.urxvtMouse = true;
|
||||
this._coreMouseService.activeEncoding = 'URXVT';
|
||||
// for wide terminals
|
||||
// numbers for fields
|
||||
// press: ^[[b;x;yM
|
||||
@@ -1481,6 +1486,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
case 1000: // vt200 mouse
|
||||
case 1002: // button event mouse
|
||||
case 1003: // any event mouse
|
||||
this._coreMouseService.activeProtocol = 'NONE';
|
||||
this._terminal.x10Mouse = false;
|
||||
this._terminal.vt200Mouse = false;
|
||||
this._terminal.normalMouse = false;
|
||||
@@ -1496,12 +1502,15 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
this._terminal.sendFocus = false;
|
||||
break;
|
||||
case 1005: // utf8 ext mode mouse
|
||||
this._coreMouseService.activeEncoding = 'DEFAULT';
|
||||
this._terminal.utfMouse = false;
|
||||
break;
|
||||
case 1006: // sgr ext mode mouse
|
||||
this._coreMouseService.activeEncoding = 'DEFAULT';
|
||||
this._terminal.sgrMouse = false;
|
||||
break;
|
||||
case 1015: // urxvt ext mode mouse
|
||||
this._coreMouseService.activeEncoding = 'DEFAULT';
|
||||
this._terminal.urxvtMouse = false;
|
||||
break;
|
||||
case 25: // hide cursor
|
||||
|
||||
+56
-193
@@ -40,14 +40,14 @@ import { AccessibilityManager } from './AccessibilityManager';
|
||||
import { ITheme, IMarker, IDisposable, ISelectionPosition } from 'xterm';
|
||||
import { removeTerminalFromCache } from './renderer/atlas/CharAtlasCache';
|
||||
import { DomRenderer } from './renderer/dom/DomRenderer';
|
||||
import { IKeyboardEvent, KeyboardResultType, ICharset, IBufferLine, IAttributeData } from 'common/Types';
|
||||
import { IKeyboardEvent, KeyboardResultType, ICharset, IBufferLine, IAttributeData, ICoreMouseEvent } from 'common/Types';
|
||||
import { evaluateKeyboardEvent } from 'common/input/Keyboard';
|
||||
import { EventEmitter, IEvent } from 'common/EventEmitter';
|
||||
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
|
||||
import { applyWindowsMode } from './WindowsMode';
|
||||
import { ColorManager } from 'browser/ColorManager';
|
||||
import { RenderService } from 'browser/services/RenderService';
|
||||
import { IOptionsService, IBufferService, ICoreService, ILogService, IDirtyRowService, IInstantiationService } from 'common/services/Services';
|
||||
import { IOptionsService, IBufferService, ICoreMouseService, ICoreService, ILogService, IDirtyRowService, IInstantiationService } from 'common/services/Services';
|
||||
import { OptionsService } from 'common/services/OptionsService';
|
||||
import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ISoundService } from 'browser/services/Services';
|
||||
import { CharSizeService } from 'browser/services/CharSizeService';
|
||||
@@ -62,6 +62,7 @@ import { LogService } from 'common/services/LogService';
|
||||
import { ILinkifier, IMouseZoneManager, LinkMatcherHandler, ILinkMatcherOptions, IViewport } from 'browser/Types';
|
||||
import { DirtyRowService } from 'common/services/DirtyRowService';
|
||||
import { InstantiationService } from 'common/services/InstantiationService';
|
||||
import { CoreMouseService } from 'common/services/CoreMouseService';
|
||||
|
||||
// Let it work inside Node.js for automated testing purposes.
|
||||
const document = (typeof window !== 'undefined') ? window.document : null;
|
||||
@@ -113,6 +114,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
|
||||
// common services
|
||||
private _bufferService: IBufferService;
|
||||
private _coreService: ICoreService;
|
||||
private _coreMouseService: ICoreMouseService;
|
||||
private _dirtyRowService: IDirtyRowService;
|
||||
private _instantiationService: IInstantiationService;
|
||||
private _logService: ILogService;
|
||||
@@ -249,6 +251,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
|
||||
this._coreService = this._instantiationService.createInstance(CoreService, () => this.scrollToBottom());
|
||||
this._instantiationService.setService(ICoreService, this._coreService);
|
||||
this._coreService.onData(e => this._onData.fire(e));
|
||||
this._coreMouseService = this._instantiationService.createInstance(CoreMouseService);
|
||||
this._instantiationService.setService(ICoreMouseService, this._coreMouseService);
|
||||
this._dirtyRowService = this._instantiationService.createInstance(DirtyRowService);
|
||||
this._instantiationService.setService(IDirtyRowService, this._dirtyRowService);
|
||||
this._logService = this._instantiationService.createInstance(LogService);
|
||||
@@ -309,7 +313,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
|
||||
this._userScrolling = false;
|
||||
|
||||
// Register input handler and refire/handle events
|
||||
this._inputHandler = new InputHandler(this, this._bufferService, this._coreService, this._dirtyRowService, this._logService, this.optionsService);
|
||||
this._inputHandler = new InputHandler(this, this._bufferService, this._coreService, this._dirtyRowService, this._logService, this.optionsService, this._coreMouseService);
|
||||
this._inputHandler.onCursorMove(() => this._onCursorMove.fire());
|
||||
this._inputHandler.onLineFeed(() => this._onLineFeed.fire());
|
||||
this.register(this._inputHandler);
|
||||
@@ -709,223 +713,85 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
|
||||
}
|
||||
|
||||
/**
|
||||
* XTerm mouse events
|
||||
* http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
|
||||
* To better understand these
|
||||
* the xterm code is very helpful:
|
||||
* Relevant files:
|
||||
* button.c, charproc.c, misc.c
|
||||
* Relevant functions in xterm/button.c:
|
||||
* BtnCode, EmitButtonCode, EditorButton, SendMousePosition
|
||||
* mouse events
|
||||
* FIXME: move event handler registration into browser MouseService
|
||||
*/
|
||||
public bindMouse(): void {
|
||||
const el = this.element;
|
||||
const self = this;
|
||||
let pressed = 32;
|
||||
|
||||
// mouseup, mousedown, wheel
|
||||
// left click: ^[[M 3<^[[M#3<
|
||||
// wheel up: ^[[M`3>
|
||||
function sendButton(ev: MouseEvent | WheelEvent): void {
|
||||
let button;
|
||||
let pos;
|
||||
|
||||
// get the xterm-style button
|
||||
button = getButton(ev);
|
||||
|
||||
// get mouse coordinates
|
||||
pos = self._mouseService.getRawByteCoords(ev, self.screenElement, self.cols, self.rows);
|
||||
if (!pos) return;
|
||||
|
||||
sendEvent(button, pos);
|
||||
|
||||
let but: ICoreMouseEvent['button'];
|
||||
let action: ICoreMouseEvent['action'];
|
||||
let code: number;
|
||||
switch ((<any>ev).overrideType || ev.type) {
|
||||
case 'mousedown':
|
||||
pressed = button;
|
||||
break;
|
||||
case 'mouseup':
|
||||
// keep it at the left
|
||||
// button, just in case.
|
||||
pressed = 32;
|
||||
break;
|
||||
case 'wheel':
|
||||
// nothing. don't
|
||||
// interfere with
|
||||
// `pressed`.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// motion example of a left click:
|
||||
// ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7<
|
||||
function sendMove(ev: MouseEvent): void {
|
||||
let button = pressed;
|
||||
const pos = self._mouseService.getRawByteCoords(ev, self.screenElement, self.cols, self.rows);
|
||||
if (!pos) return;
|
||||
|
||||
// buttons marked as motions
|
||||
// are incremented by 32
|
||||
button += 32;
|
||||
|
||||
sendEvent(button, pos);
|
||||
}
|
||||
|
||||
// encode button and
|
||||
// position to characters
|
||||
function encode(data: number[], ch: number): void {
|
||||
if (!self.utfMouse) {
|
||||
if (ch === 255) {
|
||||
data.push(0);
|
||||
return;
|
||||
}
|
||||
if (ch > 127) ch = 127;
|
||||
data.push(ch);
|
||||
} else {
|
||||
if (ch > 2047) {
|
||||
data.push(2047);
|
||||
return;
|
||||
}
|
||||
data.push(ch);
|
||||
}
|
||||
}
|
||||
|
||||
// send a mouse event:
|
||||
// regular/utf8: ^[[M Cb Cx Cy
|
||||
// urxvt: ^[[ Cb ; Cx ; Cy M
|
||||
// sgr: ^[[ Cb ; Cx ; Cy M/m
|
||||
// vt300: ^[[ 24(1/3/5)~ [ Cx , Cy ] \r
|
||||
// locator: CSI P e ; P b ; P r ; P c ; P p & w
|
||||
function sendEvent(button: number, pos: {x: number, y: number}): void {
|
||||
if (self._vt300Mouse) {
|
||||
// NOTE: Unstable.
|
||||
// http://www.vt100.net/docs/vt3xx-gp/chapter15.html
|
||||
button &= 3;
|
||||
pos.x -= 32;
|
||||
pos.y -= 32;
|
||||
let data = C0.ESC + '[24';
|
||||
if (button === 0) data += '1';
|
||||
else if (button === 1) data += '3';
|
||||
else if (button === 2) data += '5';
|
||||
else if (button === 3) return;
|
||||
else data += '0';
|
||||
data += '~[' + pos.x + ',' + pos.y + ']\r';
|
||||
self._coreService.triggerDataEvent(data, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (self._decLocator) {
|
||||
// NOTE: Unstable.
|
||||
button &= 3;
|
||||
pos.x -= 32;
|
||||
pos.y -= 32;
|
||||
if (button === 0) button = 2;
|
||||
else if (button === 1) button = 4;
|
||||
else if (button === 2) button = 6;
|
||||
else if (button === 3) button = 3;
|
||||
self._coreService.triggerDataEvent(C0.ESC + '['
|
||||
+ button
|
||||
+ ';'
|
||||
+ (button === 3 ? 4 : 0)
|
||||
+ ';'
|
||||
+ pos.y
|
||||
+ ';'
|
||||
+ pos.x
|
||||
+ ';'
|
||||
// Not sure what page is meant to be
|
||||
+ (<any>pos).page || 0
|
||||
+ '&w', true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.urxvtMouse) {
|
||||
pos.x -= 32;
|
||||
pos.y -= 32;
|
||||
pos.x++;
|
||||
pos.y++;
|
||||
self._coreService.triggerDataEvent(C0.ESC + '[' + button + ';' + pos.x + ';' + pos.y + 'M', true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.sgrMouse) {
|
||||
pos.x -= 32;
|
||||
pos.y -= 32;
|
||||
self._coreService.triggerDataEvent(C0.ESC + '[<'
|
||||
+ (((button & 3) === 3 ? button & ~3 : button) - 32)
|
||||
+ ';'
|
||||
+ pos.x
|
||||
+ ';'
|
||||
+ pos.y
|
||||
+ ((button & 3) === 3 ? 'm' : 'M'), true);
|
||||
return;
|
||||
}
|
||||
|
||||
const data: number[] = [];
|
||||
|
||||
encode(data, button);
|
||||
encode(data, pos.x);
|
||||
encode(data, pos.y);
|
||||
|
||||
self._coreService.triggerDataEvent(C0.ESC + '[M' + String.fromCharCode.apply(String, data), true);
|
||||
}
|
||||
|
||||
function getButton(ev: MouseEvent): number {
|
||||
let button;
|
||||
let shift;
|
||||
let meta;
|
||||
let ctrl;
|
||||
let mod;
|
||||
|
||||
// two low bits:
|
||||
// 0 = left
|
||||
// 1 = middle
|
||||
// 2 = right
|
||||
// 3 = release
|
||||
// wheel up/down:
|
||||
// 1, and 2 - with 64 added
|
||||
switch ((<any>ev).overrideType || ev.type) {
|
||||
case 'mousedown':
|
||||
button = ev.button !== null && ev.button !== undefined
|
||||
action = 'up';
|
||||
code = ev.button !== null && ev.button !== undefined
|
||||
? +ev.button
|
||||
: ev.which !== null && ev.which !== undefined
|
||||
? ev.which - 1
|
||||
: null;
|
||||
but = code === 0 ? 'left' : code === 1 ? 'middle' : 'right';
|
||||
break;
|
||||
case 'mouseup':
|
||||
button = 3;
|
||||
case 'mousedown':
|
||||
action = 'down';
|
||||
code = ev.button !== null && ev.button !== undefined
|
||||
? +ev.button
|
||||
: ev.which !== null && ev.which !== undefined
|
||||
? ev.which - 1
|
||||
: null;
|
||||
but = code === 0 ? 'left' : code === 1 ? 'middle' : 'right';
|
||||
break;
|
||||
case 'DOMMouseScroll':
|
||||
button = ev.detail < 0
|
||||
? 64
|
||||
: 65;
|
||||
but = 'wheel';
|
||||
action = ev.detail < 0 ? 'up' : 'down';
|
||||
break;
|
||||
case 'wheel':
|
||||
button = (<WheelEvent>ev).deltaY < 0
|
||||
? 64
|
||||
: 65;
|
||||
but = 'wheel';
|
||||
action = (<WheelEvent>ev).deltaY < 0 ? 'up' : 'down';
|
||||
break;
|
||||
}
|
||||
self._coreMouseService.triggerMouseEvent({
|
||||
col: pos.x - 33, // FIXME: why -33 here?
|
||||
row: pos.y - 33,
|
||||
button: but,
|
||||
action,
|
||||
ctrl: ev.ctrlKey,
|
||||
alt: ev.altKey,
|
||||
shift: ev.shiftKey
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// next three bits are the modifiers:
|
||||
// 4 = shift, 8 = meta, 16 = control
|
||||
shift = ev.shiftKey ? 4 : 0;
|
||||
meta = ev.metaKey ? 8 : 0;
|
||||
ctrl = ev.ctrlKey ? 16 : 0;
|
||||
mod = shift | meta | ctrl;
|
||||
function sendMove(ev: MouseEvent): void {
|
||||
const pos = self._mouseService.getRawByteCoords(ev, self.screenElement, self.cols, self.rows);
|
||||
if (!pos) return;
|
||||
|
||||
// no mods
|
||||
if (self.vt200Mouse) {
|
||||
// ctrl only
|
||||
mod &= ctrl;
|
||||
} else if (!self.normalMouse) {
|
||||
mod = 0;
|
||||
let but: ICoreMouseEvent['button'] = 'none';
|
||||
if (ev.buttons !== undefined) {
|
||||
but = ev.buttons & 1 ? 'left' : ev.buttons & 2 ? 'right' : ev.buttons & 4 ? 'middle' : 'none';
|
||||
}
|
||||
|
||||
// increment to SP
|
||||
button = (32 + (mod << 2)) + button;
|
||||
|
||||
return button;
|
||||
self._coreMouseService.triggerMouseEvent({
|
||||
col: pos.x - 33,
|
||||
row: pos.y - 33,
|
||||
button: but,
|
||||
action: 'move',
|
||||
ctrl: ev.ctrlKey,
|
||||
alt: ev.altKey,
|
||||
shift: ev.shiftKey
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
this.register(addDisposableDomListener(el, 'mousedown', (ev: MouseEvent) => {
|
||||
|
||||
// Prevent the focus on the textarea from getting lost
|
||||
@@ -988,10 +854,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
|
||||
return this.cancel(ev);
|
||||
}));
|
||||
|
||||
// if (this.normalMouse) {
|
||||
// on(this.document, 'mousemove', sendMove);
|
||||
// }
|
||||
|
||||
this.register(addDisposableDomListener(el, 'wheel', (ev: WheelEvent) => {
|
||||
if (!this.mouseEvents) {
|
||||
// Convert wheel events into up/down events when the buffer does not have scrollback, this
|
||||
@@ -1790,6 +1652,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
|
||||
this._setup();
|
||||
this._bufferService.reset();
|
||||
this._coreService.reset();
|
||||
this._coreMouseService.reset();
|
||||
if (this._selectionService) {
|
||||
this._selectionService.reset();
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IPartialTerminalOptions, IDirtyRowService } from 'common/services/Services';
|
||||
import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IPartialTerminalOptions, IDirtyRowService, ICoreMouseService } from 'common/services/Services';
|
||||
import { IEvent, EventEmitter } from 'common/EventEmitter';
|
||||
import { clone } from 'common/Clone';
|
||||
import { DEFAULT_OPTIONS } from 'common/services/OptionsService';
|
||||
import { IBufferSet, IBuffer } from 'common/buffer/Types';
|
||||
import { BufferSet } from 'common/buffer/BufferSet';
|
||||
import { IDecPrivateModes } from 'common/Types';
|
||||
import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType } from 'common/Types';
|
||||
|
||||
export class MockBufferService implements IBufferService {
|
||||
serviceBrand: any;
|
||||
@@ -29,6 +29,16 @@ export class MockBufferService implements IBufferService {
|
||||
reset(): void {}
|
||||
}
|
||||
|
||||
export class MockCoreMouseService implements ICoreMouseService {
|
||||
activeEncoding: string = '';
|
||||
activeProtocol: string = '';
|
||||
addEncoding(name: string): void {}
|
||||
addProtocol(name: string): void {}
|
||||
reset(): void {}
|
||||
triggerMouseEvent(event: ICoreMouseEvent): boolean { return false; }
|
||||
onProtocolChange: IEvent<CoreMouseEventType[]> = new EventEmitter<CoreMouseEventType[]>().event;
|
||||
}
|
||||
|
||||
export class MockCoreService implements ICoreService {
|
||||
serviceBrand: any;
|
||||
decPrivateModes: IDecPrivateModes = {} as any;
|
||||
|
||||
Vendored
+61
@@ -158,3 +158,64 @@ export interface IRowRange {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for mouse events in the core.
|
||||
*/
|
||||
export interface ICoreMouseEvent {
|
||||
/** column (zero based). */
|
||||
col: number;
|
||||
/** row (zero based). */
|
||||
row: number;
|
||||
/**
|
||||
* Button the action occured. Due to restrictions of the tracking protocols
|
||||
* it is not possible to report multiple buttons at once.
|
||||
* Wheel is treated as a button.
|
||||
* There are invalid combinations of buttons and actions possible
|
||||
* (like move + wheel), those are silently ignored by the CoreMouseService.
|
||||
*/
|
||||
button: 'left' | 'middle' | 'right' | 'wheel' | 'none';
|
||||
action: 'up' | 'down' | 'move';
|
||||
/**
|
||||
* Modifier states.
|
||||
* Protocols will add/ignore those based on specific restrictions.
|
||||
*/
|
||||
ctrl?: boolean;
|
||||
alt?: boolean;
|
||||
shift?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* CoreMouseEventType
|
||||
* To be reported to the browser component which events a mouse
|
||||
* protocol wants to be catched and forwarded as an ICoreMouseEvent
|
||||
* to CoreMouseService.
|
||||
* Known types:
|
||||
* - mousedown: any mousedown event
|
||||
* - mouseup: any mouseup event
|
||||
* - wheel: any wheel event
|
||||
* - mousedrag: any mousemove event while a button is pressed
|
||||
* - mousemove: any mousemove event
|
||||
*/
|
||||
export type CoreMouseEventType = 'mousedown' | 'mouseup' | 'wheel' | 'mousemove' | 'mousedrag';
|
||||
|
||||
/**
|
||||
* Mouse protocol interface.
|
||||
* A mouse protocol can be registered and activated at the CoreMouseService.
|
||||
* `events` should contain a list of needed events as a hint for the browser component
|
||||
* to install/remove the appropriate event handlers.
|
||||
* `restrict` applies further protocol specific restrictions like not allowed
|
||||
* modifiers or filtering invalid event types.
|
||||
*/
|
||||
export interface ICoreMouseProtocol {
|
||||
events: CoreMouseEventType[];
|
||||
restrict: (e: ICoreMouseEvent) => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* CoreMouseEncoding
|
||||
* The tracking encoding can be registered and activated at the CoreMouseService.
|
||||
* If a ICoreMouseEvent passes all procotol restrictions it will be encoded
|
||||
* with the active encoding and sent out.
|
||||
*/
|
||||
export type CoreMouseEncoding = (event: ICoreMouseEvent) => string;
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
import { CoreMouseService } from 'common/services/CoreMouseService';
|
||||
import { MockCoreService, MockBufferService } from 'common/TestUtils.test';
|
||||
import { assert } from 'chai';
|
||||
import { ICoreMouseEvent, CoreMouseEventType } from 'common/Types';
|
||||
|
||||
// needed mock services
|
||||
const bufferService = new MockBufferService(300, 100);
|
||||
const coreService = new MockCoreService();
|
||||
|
||||
function toBytes(s: string | undefined): number[] {
|
||||
if (!s) {
|
||||
return [];
|
||||
}
|
||||
const res: number[] = [];
|
||||
for (let i = 0; i < s.length; ++i) {
|
||||
res.push(s.charCodeAt(i));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
describe('CoreMouseService', () => {
|
||||
it('init', () => {
|
||||
const cms = new CoreMouseService(bufferService, coreService);
|
||||
assert.equal(cms.activeEncoding, 'DEFAULT');
|
||||
assert.equal(cms.activeProtocol, 'NONE');
|
||||
});
|
||||
it('default protocols - NONE, X10, VT200, DRAG, ANY', () => {
|
||||
const cms = new CoreMouseService(bufferService, coreService);
|
||||
assert.deepEqual(Object.keys((cms as any)._protocols), ['NONE', 'X10', 'VT200', 'DRAG', 'ANY']);
|
||||
});
|
||||
it('default encodings - DEFAULT, UTF8, SGR, URXVT', () => {
|
||||
const cms = new CoreMouseService(bufferService, coreService);
|
||||
assert.deepEqual(Object.keys((cms as any)._encodings), ['DEFAULT', 'UTF8', 'SGR', 'URXVT']);
|
||||
});
|
||||
it('protocol/encoding setter, reset', () => {
|
||||
const cms = new CoreMouseService(bufferService, coreService);
|
||||
cms.activeEncoding = 'SGR';
|
||||
cms.activeProtocol = 'ANY';
|
||||
assert.equal(cms.activeEncoding, 'SGR');
|
||||
assert.equal(cms.activeProtocol, 'ANY');
|
||||
cms.reset();
|
||||
assert.equal(cms.activeEncoding, 'DEFAULT');
|
||||
assert.equal(cms.activeProtocol, 'NONE');
|
||||
assert.throws(() => { cms.activeEncoding = 'xyz'; }, 'unknown encoding "xyz"');
|
||||
assert.throws(() => { cms.activeProtocol = 'xyz'; }, 'unknown protocol "xyz"');
|
||||
});
|
||||
it('addEncoding', () => {
|
||||
const cms = new CoreMouseService(bufferService, coreService);
|
||||
cms.addEncoding('XYZ', (e: ICoreMouseEvent) => '');
|
||||
cms.activeEncoding = 'XYZ';
|
||||
assert.equal(cms.activeEncoding, 'XYZ');
|
||||
});
|
||||
it('addProtocol', () => {
|
||||
const cms = new CoreMouseService(bufferService, coreService);
|
||||
cms.addProtocol('XYZ', { events: [], restrict: (e: ICoreMouseEvent) => false });
|
||||
cms.activeProtocol = 'XYZ';
|
||||
assert.equal(cms.activeProtocol, 'XYZ');
|
||||
});
|
||||
it('onProtocolChange', () => {
|
||||
const cms = new CoreMouseService(bufferService, coreService);
|
||||
const wantedEvents: CoreMouseEventType[][] = [];
|
||||
cms.onProtocolChange(events => wantedEvents.push(events));
|
||||
cms.activeProtocol = 'NONE';
|
||||
assert.deepEqual(wantedEvents, [[]]);
|
||||
cms.activeProtocol = 'ANY';
|
||||
assert.deepEqual(wantedEvents, [[], ['mousedown', 'mouseup', 'wheel', 'mousemove']]);
|
||||
});
|
||||
describe('triggerMouseEvent', () => {
|
||||
let cms: CoreMouseService;
|
||||
let reports: string[];
|
||||
beforeEach(() => {
|
||||
cms = new CoreMouseService(bufferService, coreService);
|
||||
reports = [];
|
||||
coreService.triggerDataEvent = (data: string, userInput?: boolean) => reports.push(data);
|
||||
});
|
||||
it('NONE', () => {
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'down' }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'up' }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'move' }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'middle', action: 'down' }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'right', action: 'down' }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'wheel', action: 'up' }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'move' }), false);
|
||||
});
|
||||
it('X10', () => {
|
||||
cms.activeProtocol = 'X10';
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'down' }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'up' }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'move' }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'middle', action: 'down' }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'right', action: 'down' }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'wheel', action: 'up' }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'move' }), false);
|
||||
});
|
||||
it('VT200', () => {
|
||||
cms.activeProtocol = 'VT200';
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'down' }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'up' }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'move' }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'middle', action: 'down' }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'right', action: 'down' }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'wheel', action: 'up' }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'move' }), false);
|
||||
});
|
||||
it('DRAG', () => {
|
||||
cms.activeProtocol = 'DRAG';
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'down' }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'up' }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'move' }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'middle', action: 'down' }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'right', action: 'down' }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'wheel', action: 'up' }), true);
|
||||
});
|
||||
it('ANY', () => {
|
||||
cms.activeProtocol = 'ANY';
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'down' }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'up' }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'move' }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'middle', action: 'down' }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'right', action: 'down' }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'wheel', action: 'up' }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'move' }), true);
|
||||
// should not report in any case
|
||||
// invalid button + action combinations
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'wheel', action: 'move' }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'down' }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'up' }), false);
|
||||
// invalid coords
|
||||
assert.equal(cms.triggerMouseEvent({ col: -1, row: 0, button: 'left', action: 'down' }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 500, row: 0, button: 'left', action: 'down' }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: -1, button: 'left', action: 'down' }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 500, button: 'left', action: 'down' }), false);
|
||||
});
|
||||
describe('coords', () => {
|
||||
it('DEFAULT encoding', () => {
|
||||
cms.activeProtocol = 'ANY';
|
||||
for (let i = 0; i < bufferService.cols; ++i) {
|
||||
assert.equal(cms.triggerMouseEvent({ col: i, row: 0, button: 'left', action: 'down' }), true);
|
||||
// capped at 95
|
||||
if (i < 95) {
|
||||
assert.deepEqual(toBytes(reports.pop()), [0x1b, 0x5b, 0x4d, 0x20, i + 33, 0x21]);
|
||||
} else {
|
||||
assert.deepEqual(toBytes(reports.pop()), [0x1b, 0x5b, 0x4d, 0x20, 0x7f, 0x21]);
|
||||
}
|
||||
}
|
||||
});
|
||||
it('UTF8 encoding', () => {
|
||||
cms.activeProtocol = 'ANY';
|
||||
cms.activeEncoding = 'UTF8';
|
||||
for (let i = 0; i < bufferService.cols; ++i) {
|
||||
assert.equal(cms.triggerMouseEvent({ col: i, row: 0, button: 'left', action: 'down' }), true);
|
||||
assert.deepEqual(toBytes(reports.pop()), [0x1b, 0x5b, 0x4d, 0x20, i + 33, 0x21]);
|
||||
}
|
||||
});
|
||||
it('SGR encoding', () => {
|
||||
cms.activeProtocol = 'ANY';
|
||||
cms.activeEncoding = 'SGR';
|
||||
for (let i = 0; i < bufferService.cols; ++i) {
|
||||
assert.equal(cms.triggerMouseEvent({ col: i, row: 0, button: 'left', action: 'down' }), true);
|
||||
assert.deepEqual(reports.pop(), `\x1b[<0;${i + 1};1M`);
|
||||
}
|
||||
});
|
||||
it('URXVT', () => {
|
||||
cms.activeProtocol = 'ANY';
|
||||
cms.activeEncoding = 'URXVT';
|
||||
for (let i = 0; i < bufferService.cols; ++i) {
|
||||
assert.equal(cms.triggerMouseEvent({ col: i, row: 0, button: 'left', action: 'down' }), true);
|
||||
assert.deepEqual(reports.pop(), `\x1b[32;${i + 1};1M`);
|
||||
}
|
||||
});
|
||||
});
|
||||
it('eventCodes with modifiers (DEFAULT encoding)', () => {
|
||||
cms.activeProtocol = 'ANY';
|
||||
cms.activeEncoding = 'DEFAULT';
|
||||
// all buttons + down + no modifer
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'down', ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'middle', action: 'down', ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'right', action: 'down', ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'wheel', action: 'down', ctrl: false, alt: false, shift: false }), true);
|
||||
assert.deepEqual(reports, ['\x1b[M !!', '\x1b[M!!!', '\x1b[M"!!', '\x1b[Ma!!']);
|
||||
while (reports.pop()) { }
|
||||
|
||||
// all buttons + up + no modifier
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'up', ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'middle', action: 'up', ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'right', action: 'up', ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'wheel', action: 'up', ctrl: false, alt: false, shift: false }), true);
|
||||
assert.deepEqual(reports, ['\x1b[M#!!', '\x1b[M#!!', '\x1b[M#!!', '\x1b[M`!!']);
|
||||
while (reports.pop()) { }
|
||||
|
||||
// all buttons + move + no modifier
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'left', action: 'move', ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'middle', action: 'move', ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'right', action: 'move', ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'move', ctrl: false, alt: false, shift: false }), true);
|
||||
assert.deepEqual(reports, ['\x1b[M@!!', '\x1b[MA!!', '\x1b[MB!!', '\x1b[MC!!']);
|
||||
while (reports.pop()) { }
|
||||
|
||||
// button none + move + modifiers
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'move', ctrl: true, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'move', ctrl: false, alt: true, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'move', ctrl: false, alt: false, shift: true }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'move', ctrl: true, alt: true, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'move', ctrl: false, alt: true, shift: true }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: 'none', action: 'move', ctrl: true, alt: true, shift: true }), true);
|
||||
assert.deepEqual(reports, ['\x1b[MS!!', '\x1b[MK!!', '\x1b[MG!!', '\x1b[M[!!', '\x1b[MO!!', '\x1b[M_!!']);
|
||||
while (reports.pop()) { }
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
import { IBufferService, ICoreService, ICoreMouseService } from 'common/services/Services';
|
||||
import { EventEmitter, IEvent } from 'common/EventEmitter';
|
||||
import { ICoreMouseProtocol, ICoreMouseEvent, CoreMouseEncoding, CoreMouseEventType } from 'common/Types';
|
||||
|
||||
/**
|
||||
* Supported default protocols.
|
||||
*/
|
||||
const DEFAULT_PROCOTOLS: {[key: string]: ICoreMouseProtocol} = {
|
||||
/**
|
||||
* NONE
|
||||
* Events: none
|
||||
* Modifiers: none
|
||||
*/
|
||||
NONE: {
|
||||
events: [],
|
||||
restrict: () => false
|
||||
},
|
||||
/**
|
||||
* X10
|
||||
* Events: mousedown
|
||||
* Modifiers: none (TBD)
|
||||
*/
|
||||
X10: {
|
||||
events: ['mousedown'],
|
||||
restrict: (e: ICoreMouseEvent) => {
|
||||
// no wheel (TBD), no move, no up
|
||||
if (e.button === 'wheel' || e.action !== 'down') {
|
||||
return false;
|
||||
}
|
||||
// no modifiers (TDB)
|
||||
e.ctrl = false;
|
||||
e.alt = false;
|
||||
e.shift = false;
|
||||
return true;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* VT200
|
||||
* Events: mousedown / mouseup / wheel
|
||||
* Modifiers: CTRL (TBD)
|
||||
*/
|
||||
VT200: {
|
||||
events: ['mousedown', 'mouseup', 'wheel'],
|
||||
restrict: (e: ICoreMouseEvent) => {
|
||||
// no move
|
||||
if (e.action === 'move') {
|
||||
return false;
|
||||
}
|
||||
// modifiers - only ctrl?
|
||||
e.alt = false;
|
||||
e.shift = false;
|
||||
return true;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* DRAG
|
||||
* Events: mousedown / mouseup / wheel / mousedrag
|
||||
* Modifiers: CTRL | ALT | SHIFT
|
||||
*/
|
||||
DRAG: {
|
||||
events: ['mousedown', 'mouseup', 'wheel', 'mousedrag'],
|
||||
restrict: (e: ICoreMouseEvent) => {
|
||||
// no move without button
|
||||
if (e.action === 'move' && e.button === 'none') {
|
||||
return false;
|
||||
}
|
||||
// modifiers unclear - let all pass for now
|
||||
return true;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* ANY
|
||||
* Events: all mouse related events
|
||||
* Modifiers: CTRL | ALT | SHIFT
|
||||
*/
|
||||
ANY: {
|
||||
events: ['mousedown', 'mouseup', 'wheel', 'mousemove'],
|
||||
restrict: (e: ICoreMouseEvent) => true
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Mapping of buttons and actions to event codes. (taken from xterm spec)
|
||||
* More than 3 buttons are not supported.
|
||||
*/
|
||||
enum CODEMAP {
|
||||
// buttons
|
||||
left = 0,
|
||||
middle = 1,
|
||||
right = 2,
|
||||
none = 3,
|
||||
wheel = 64,
|
||||
// actions
|
||||
up = 0,
|
||||
down = 1,
|
||||
move = 32,
|
||||
// modifiers
|
||||
shift = 4,
|
||||
alt = 8,
|
||||
ctrl = 16
|
||||
}
|
||||
|
||||
// helper for default encoders to generate the event code.
|
||||
function eventCode(e: ICoreMouseEvent, isSGR: boolean): number {
|
||||
const button = CODEMAP[e.button];
|
||||
const action = CODEMAP[e.action];
|
||||
const modifier = (e.ctrl ? CODEMAP.ctrl : 0) | (e.shift ? CODEMAP.shift : 0) | (e.alt ? CODEMAP.alt : 0);
|
||||
let code = button | modifier;
|
||||
if (e.button === 'wheel') {
|
||||
code |= action;
|
||||
} else {
|
||||
if (e.action === 'move') {
|
||||
code |= CODEMAP.move;
|
||||
} else if (e.action === 'up' && !isSGR) {
|
||||
// special case - only SGR can report button on release
|
||||
// all others have to go with NONE
|
||||
code |= CODEMAP.none;
|
||||
}
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
const S = String.fromCharCode;
|
||||
|
||||
/**
|
||||
* Supported default encodings.
|
||||
*/
|
||||
const DEFAULT_ENCODINGS: {[key: string]: CoreMouseEncoding} = {
|
||||
/**
|
||||
* DEFAULT - CSI M Pb Px Py
|
||||
* Single byte encoding for coords and event code.
|
||||
* Can encode values up to 223. The Encoding of higher
|
||||
* values is not UTF-8 compatible (and currently limited
|
||||
* to 95 in xterm.js).
|
||||
*/
|
||||
DEFAULT: (e: ICoreMouseEvent) => {
|
||||
let params = [eventCode(e, false) + 32, e.col + 32, e.row + 32];
|
||||
// FIXME: we are currently limited to ASCII range
|
||||
params = params.map(v => (v > 127) ? 127 : v);
|
||||
// FIXED: params = params.map(v => (v > 255) ? 0 : value);
|
||||
return `\x1b[M${S(params[0])}${S(params[1])}${S(params[2])}`;
|
||||
},
|
||||
/**
|
||||
* UTF8 - CSI M Pb Px Py
|
||||
* Same as DEFAULT, but with optional 2-byte UTF8
|
||||
* encoding for values > 223 (can encode up to 2015).
|
||||
*/
|
||||
UTF8: (e: ICoreMouseEvent) => {
|
||||
let params = [eventCode(e, false) + 32, e.col + 32, e.row + 32];
|
||||
// limit to 2-byte UTF8
|
||||
params = params.map(v => (v > 2047) ? 0 : v);
|
||||
return `\x1b[M${S(params[0])}${S(params[1])}${S(params[2])}`;
|
||||
},
|
||||
/**
|
||||
* SGR - CSI < Pb ; Px ; Py M|m
|
||||
* No encoding limitation.
|
||||
* Can report button on release and works with a well formed sequence.
|
||||
*/
|
||||
SGR: (e: ICoreMouseEvent) => {
|
||||
const final = (e.action === 'up' && e.button !== 'wheel') ? 'm' : 'M';
|
||||
return `\x1b[<${eventCode(e, true)};${e.col};${e.row}${final}`;
|
||||
},
|
||||
/**
|
||||
* URXVT - CSI Pb ; Px ; Py M
|
||||
* Same button encoding as default, decimal encoding for coords.
|
||||
* Ambiguity with other sequences, should not be used.
|
||||
*/
|
||||
URXVT: (e: ICoreMouseEvent) => {
|
||||
return `\x1b[${eventCode(e, false) + 32};${e.col};${e.row}M`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* CoreMouseService
|
||||
*
|
||||
* Provides mouse tracking reports with different protocols and encodings.
|
||||
* - protocols: NONE (default), X10, VT200, DRAG, ANY
|
||||
* - encodings: DEFAULT, SGR, UTF8, URXVT
|
||||
*
|
||||
* Custom protocols/encodings can be added by `addProtocol` / `addEncoding`.
|
||||
* To activate a protocol/encoding, set `activeProtocol` / `activeEncoding`.
|
||||
* Switching a protocol will send a notification event `onProtocolChange`
|
||||
* with a list of needed events to track.
|
||||
*
|
||||
* The service handles the mouse tracking state and decides whether to send
|
||||
* a tracking report to the backend based on protocol and encoding limitations.
|
||||
* To send a mouse event call `triggerMouseEvent`.
|
||||
*/
|
||||
export class CoreMouseService implements ICoreMouseService {
|
||||
private _protocols: {[name: string]: ICoreMouseProtocol} = {};
|
||||
private _encodings: {[name: string]: CoreMouseEncoding} = {};
|
||||
private _activeProtocol: string = '';
|
||||
private _activeEncoding: string = '';
|
||||
private _onProtocolChange = new EventEmitter<CoreMouseEventType[]>();
|
||||
|
||||
constructor(
|
||||
@IBufferService private readonly _bufferService: IBufferService,
|
||||
@ICoreService private readonly _coreService: ICoreService
|
||||
) {
|
||||
// register default protocols and encodings
|
||||
Object.keys(DEFAULT_PROCOTOLS).forEach(name => this.addProtocol(name, DEFAULT_PROCOTOLS[name]));
|
||||
Object.keys(DEFAULT_ENCODINGS).forEach(name => this.addEncoding(name, DEFAULT_ENCODINGS[name]));
|
||||
// call reset to set defaults
|
||||
this.reset();
|
||||
}
|
||||
|
||||
public addProtocol(name: string, protocol: ICoreMouseProtocol): void {
|
||||
this._protocols[name] = protocol;
|
||||
}
|
||||
|
||||
public addEncoding(name: string, encoding: CoreMouseEncoding): void {
|
||||
this._encodings[name] = encoding;
|
||||
}
|
||||
|
||||
public get activeProtocol(): string {
|
||||
return this._activeProtocol;
|
||||
}
|
||||
|
||||
public set activeProtocol(name: string) {
|
||||
if (!this._protocols[name]) {
|
||||
throw new Error(`unknown protocol "${name}"`);
|
||||
}
|
||||
this._activeProtocol = name;
|
||||
this._onProtocolChange.fire(this._protocols[name].events);
|
||||
}
|
||||
|
||||
public get activeEncoding(): string {
|
||||
return this._activeEncoding;
|
||||
}
|
||||
|
||||
public set activeEncoding(name: string) {
|
||||
if (!this._encodings[name]) {
|
||||
throw new Error(`unknown encoding "${name}"`);
|
||||
}
|
||||
this._activeEncoding = name;
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this.activeProtocol = 'NONE';
|
||||
this.activeEncoding = 'DEFAULT';
|
||||
}
|
||||
|
||||
/**
|
||||
* Event to announce changes in mouse tracking.
|
||||
*/
|
||||
public get onProtocolChange(): IEvent<CoreMouseEventType[]> {
|
||||
return this._onProtocolChange.event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers a mouse event to be sent.
|
||||
*
|
||||
* Returns true if the event passed all protocol restrictions and a report
|
||||
* was sent, otherwise false. The return value may be used to decide whether
|
||||
* the default event action in the bowser component should be omitted.
|
||||
*
|
||||
* Note: The method will change values of the given event object
|
||||
* to fullfill protocol and encoding restrictions.
|
||||
*/
|
||||
public triggerMouseEvent(event: ICoreMouseEvent): boolean {
|
||||
// range check for col/row
|
||||
if (event.col < 0 || event.col >= this._bufferService.cols
|
||||
|| event.row < 0 || event.row >= this._bufferService.rows) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// filter nonsense combinations of button + action
|
||||
if (event.button === 'wheel' && event.action === 'move') {
|
||||
return false;
|
||||
}
|
||||
if (event.button === 'none' && event.action !== 'move') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// report 1-based coords
|
||||
event.col++;
|
||||
event.row++;
|
||||
|
||||
// apply protocol restrictions
|
||||
if (!this._protocols[this._activeProtocol].restrict(event)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// encode report and send
|
||||
const report = this._encodings[this._activeEncoding](event);
|
||||
this._coreService.triggerDataEvent(report, true);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { IEvent } from 'common/EventEmitter';
|
||||
import { IBuffer, IBufferSet } from 'common/buffer/Types';
|
||||
import { IDecPrivateModes } from 'common/Types';
|
||||
import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType } from 'common/Types';
|
||||
import { createDecorator } from 'common/services/ServiceRegistry';
|
||||
|
||||
export const IBufferService = createDecorator<IBufferService>('BufferService');
|
||||
@@ -23,6 +23,32 @@ export interface IBufferService {
|
||||
reset(): void;
|
||||
}
|
||||
|
||||
export const ICoreMouseService = createDecorator<ICoreMouseService>('CoreMouseService');
|
||||
export interface ICoreMouseService {
|
||||
activeProtocol: string;
|
||||
activeEncoding: string;
|
||||
addProtocol(name: string, protocol: ICoreMouseProtocol): void;
|
||||
addEncoding(name: string, encoding: CoreMouseEncoding): void;
|
||||
reset(): void;
|
||||
|
||||
/**
|
||||
* Triggers a mouse event to be sent.
|
||||
*
|
||||
* Returns true if the event passed all protocol restrictions and a report
|
||||
* was sent, otherwise false. The return value may be used to decide whether
|
||||
* the default event action in the bowser component should be omitted.
|
||||
*
|
||||
* Note: The method will change values of the given event object
|
||||
* to fullfill protocol and encoding restrictions.
|
||||
*/
|
||||
triggerMouseEvent(event: ICoreMouseEvent): boolean;
|
||||
|
||||
/**
|
||||
* Event to announce changes in mouse tracking.
|
||||
*/
|
||||
onProtocolChange: IEvent<CoreMouseEventType[]>;
|
||||
}
|
||||
|
||||
export const ICoreService = createDecorator<ICoreService>('CoreService');
|
||||
export interface ICoreService {
|
||||
serviceBrand: any;
|
||||
|
||||
Reference in New Issue
Block a user