UNPKG

xterm

Version:

Full xterm terminal, in your browser

319 lines (292 loc) • 9.79 kB
/** * Copyright (c) 2019 The xterm.js authors. All rights reserved. * @license MIT */ import { IBufferService, ICoreService, ICoreMouseService } from 'common/services/Services'; import { EventEmitter } from 'common/EventEmitter'; import { ICoreMouseProtocol, ICoreMouseEvent, CoreMouseEncoding, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types'; import { Disposable } from 'common/Lifecycle'; /** * 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 (1-based). */ DEFAULT: (e: ICoreMouseEvent) => { const params = [eventCode(e, false) + 32, e.col + 32, e.row + 32]; // supress mouse report if we exceed addressible range // Note this is handled differently by emulators // - xterm: sends 0;0 coords instead // - vte, konsole: no report if (params[0] > 255 || params[1] > 255 || params[2] > 255) { return ''; } 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}`; }, SGR_PIXELS: (e: ICoreMouseEvent) => { const final = (e.action === CoreMouseAction.UP && e.button !== CoreMouseButton.WHEEL) ? 'm' : 'M'; return `\x1b[<${eventCode(e, true)};${e.x};${e.y}${final}`; } }; /** * CoreMouseService * * Provides mouse tracking reports with different protocols and encodings. * - protocols: NONE (default), X10, VT200, DRAG, ANY * - encodings: DEFAULT, SGR (UTF8, URXVT removed in #2507) * * 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 extends Disposable implements ICoreMouseService { private _protocols: { [name: string]: ICoreMouseProtocol } = {}; private _encodings: { [name: string]: CoreMouseEncoding } = {}; private _activeProtocol: string = ''; private _activeEncoding: string = ''; private _lastEvent: ICoreMouseEvent | null = null; private readonly _onProtocolChange = this.register(new EventEmitter<CoreMouseEventType>()); public readonly onProtocolChange = this._onProtocolChange.event; constructor( @IBufferService private readonly _bufferService: IBufferService, @ICoreService private readonly _coreService: ICoreService ) { super(); // register default protocols and encodings for (const name of Object.keys(DEFAULT_PROTOCOLS)) this.addProtocol(name, DEFAULT_PROTOCOLS[name]); for (const name of Object.keys(DEFAULT_ENCODINGS)) 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 get areMouseEventsActive(): boolean { return this._protocols[this._activeProtocol].events !== 0; } 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; } /** * 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 events at grid or pixel level if (e.action === CoreMouseAction.MOVE && this._lastEvent && this._equalEvents(this._lastEvent, e, this._activeEncoding === 'SGR_PIXELS') ) { 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); if (report) { // always send DEFAULT as binary data if (this._activeEncoding === 'DEFAULT') { this._coreService.triggerBinaryEvent(report); } else { 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 _equalEvents(e1: ICoreMouseEvent, e2: ICoreMouseEvent, pixels: boolean): boolean { if (pixels) { if (e1.x !== e2.x) return false; if (e1.y !== e2.y) return false; } else { 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; } }