UNPKG

xterm

Version:

Full xterm terminal, in your browser

316 lines (290 loc) • 9.69 kB
/** * 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 { let code = (e.ctrl ? Modifiers.CTRL : 0) | (e.shift ? Modifiers.SHIFT : 0) | (e.alt ? Modifiers.ALT : 0); if (e.button === CoreMouseButton.WHEEL) { code |= 64; code |= e.action; } else { code |= e.button & 3; if (e.button & 4) { code |= 64; } if (e.button & 8) { code |= 128; } 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(e: ICoreMouseEvent): boolean { // range check for col/row if (e.col < 0 || e.col >= this._bufferService.cols || e.row < 0 || e.row >= this._bufferService.rows) { return false; } // filter nonsense combinations of button + action if (e.button === CoreMouseButton.WHEEL && e.action === CoreMouseAction.MOVE) { return false; } if (e.button === CoreMouseButton.NONE && e.action !== CoreMouseAction.MOVE) { return false; } if (e.button !== CoreMouseButton.WHEEL && (e.action === CoreMouseAction.LEFT || e.action === CoreMouseAction.RIGHT)) { return false; } // report 1-based coords e.col++; e.row++; // debounce move at grid level if (e.action === CoreMouseAction.MOVE && this._lastEvent && this._compareEvents(this._lastEvent, e)) { return false; } // apply protocol restrictions if (!this._protocols[this._activeProtocol].restrict(e)) { return false; } // encode report and send const report = this._encodings[this._activeEncoding](e); this._coreService.triggerDataEvent(report, true); this._lastEvent = e; return true; } public explainEvents(events: CoreMouseEventType): {[event: string]: boolean} { return { DOWN: !!(events & CoreMouseEventType.DOWN), UP: !!(events & CoreMouseEventType.UP), DRAG: !!(events & CoreMouseEventType.DRAG), MOVE: !!(events & CoreMouseEventType.MOVE), WHEEL: !!(events & CoreMouseEventType.WHEEL) }; } 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; } }