296 lines
8.9 KiB
TypeScript
296 lines
8.9 KiB
TypeScript
/**
|
|
* 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, CoreMouseButton, CoreMouseAction } from 'common/Types';
|
|
|
|
/**
|
|
* Supported default protocols.
|
|
*/
|
|
const DEFAULT_PROTOCOLS: {[key: string]: ICoreMouseProtocol} = {
|
|
/**
|
|
* NONE
|
|
* Events: none
|
|
* Modifiers: none
|
|
*/
|
|
NONE: {
|
|
events: CoreMouseEventType.NONE,
|
|
restrict: () => false
|
|
},
|
|
/**
|
|
* X10
|
|
* Events: mousedown
|
|
* Modifiers: none
|
|
*/
|
|
X10: {
|
|
events: CoreMouseEventType.DOWN,
|
|
restrict: (e: ICoreMouseEvent) => {
|
|
// no wheel, no move, no up
|
|
if (e.button === CoreMouseButton.WHEEL || e.action !== CoreMouseAction.DOWN) {
|
|
return false;
|
|
}
|
|
// no modifiers
|
|
e.ctrl = false;
|
|
e.alt = false;
|
|
e.shift = false;
|
|
return true;
|
|
}
|
|
},
|
|
/**
|
|
* VT200
|
|
* Events: mousedown / mouseup / wheel
|
|
* Modifiers: all
|
|
*/
|
|
VT200: {
|
|
events: CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL,
|
|
restrict: (e: ICoreMouseEvent) => {
|
|
// no move
|
|
if (e.action === CoreMouseAction.MOVE) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
},
|
|
/**
|
|
* DRAG
|
|
* Events: mousedown / mouseup / wheel / mousedrag
|
|
* Modifiers: all
|
|
*/
|
|
DRAG: {
|
|
events: CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL | CoreMouseEventType.DRAG,
|
|
restrict: (e: ICoreMouseEvent) => {
|
|
// no move without button
|
|
if (e.action === CoreMouseAction.MOVE && e.button === CoreMouseButton.NONE) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
},
|
|
/**
|
|
* ANY
|
|
* Events: all mouse related events
|
|
* Modifiers: all
|
|
*/
|
|
ANY: {
|
|
events:
|
|
CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL
|
|
| CoreMouseEventType.DRAG | CoreMouseEventType.MOVE,
|
|
restrict: (e: ICoreMouseEvent) => true
|
|
}
|
|
};
|
|
|
|
const enum Modifiers {
|
|
SHIFT = 4,
|
|
ALT = 8,
|
|
CTRL = 16
|
|
}
|
|
|
|
// helper for default encoders to generate the event code.
|
|
function eventCode(e: ICoreMouseEvent, isSGR: boolean): number {
|
|
const modifier = (e.ctrl ? Modifiers.CTRL : 0) | (e.shift ? Modifiers.SHIFT : 0) | (e.alt ? Modifiers.ALT : 0);
|
|
let code = e.button | modifier;
|
|
if (e.button === CoreMouseButton.WHEEL) {
|
|
code |= e.action;
|
|
} else {
|
|
if (e.action === CoreMouseAction.MOVE) {
|
|
code |= CoreMouseAction.MOVE;
|
|
} else if (e.action === CoreMouseAction.UP && !isSGR) {
|
|
// special case - only SGR can report button on release
|
|
// all others have to go with NONE
|
|
code |= CoreMouseButton.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 === CoreMouseAction.UP && e.button !== CoreMouseButton.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>();
|
|
private _lastEvent: ICoreMouseEvent | null = null;
|
|
|
|
constructor(
|
|
@IBufferService private readonly _bufferService: IBufferService,
|
|
@ICoreService private readonly _coreService: ICoreService
|
|
) {
|
|
// register default protocols and encodings
|
|
Object.keys(DEFAULT_PROTOCOLS).forEach(name => this.addProtocol(name, DEFAULT_PROTOCOLS[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';
|
|
this._lastEvent = null;
|
|
}
|
|
|
|
/**
|
|
* 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 === CoreMouseButton.WHEEL && event.action === CoreMouseAction.MOVE) {
|
|
return false;
|
|
}
|
|
if (event.button === CoreMouseButton.NONE && event.action !== CoreMouseAction.MOVE) {
|
|
return false;
|
|
}
|
|
|
|
// report 1-based coords
|
|
event.col++;
|
|
event.row++;
|
|
|
|
// debounce move at grid level
|
|
if (event.action === CoreMouseAction.MOVE && this._lastEvent && this._compareEvents(this._lastEvent, event)) {
|
|
return false;
|
|
}
|
|
|
|
// 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);
|
|
|
|
this._lastEvent = event;
|
|
|
|
return true;
|
|
}
|
|
|
|
private _compareEvents(e1: ICoreMouseEvent, e2: ICoreMouseEvent): boolean {
|
|
if (e1.col !== e2.col) return false;
|
|
if (e1.row !== e2.row) return false;
|
|
if (e1.button !== e2.button) return false;
|
|
if (e1.action !== e2.action) return false;
|
|
if (e1.ctrl !== e2.ctrl) return false;
|
|
if (e1.alt !== e2.alt) return false;
|
|
if (e1.shift !== e2.shift) return false;
|
|
return true;
|
|
}
|
|
}
|