2 * Copyright (c) 2019 The xterm.js authors. All rights reserved.
5 import { IBufferService, ICoreService, ICoreMouseService } from 'common/services/Services';
6 import { EventEmitter, IEvent } from 'common/EventEmitter';
7 import { ICoreMouseProtocol, ICoreMouseEvent, CoreMouseEncoding, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types';
10 * Supported default protocols.
12 const DEFAULT_PROTOCOLS: {[key: string]: ICoreMouseProtocol} = {
19 events: CoreMouseEventType.NONE,
28 events: CoreMouseEventType.DOWN,
29 restrict: (e: ICoreMouseEvent) => {
30 // no wheel, no move, no up
31 if (e.button === CoreMouseButton.WHEEL || e.action !== CoreMouseAction.DOWN) {
43 * Events: mousedown / mouseup / wheel
47 events: CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL,
48 restrict: (e: ICoreMouseEvent) => {
50 if (e.action === CoreMouseAction.MOVE) {
58 * Events: mousedown / mouseup / wheel / mousedrag
62 events: CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL | CoreMouseEventType.DRAG,
63 restrict: (e: ICoreMouseEvent) => {
64 // no move without button
65 if (e.action === CoreMouseAction.MOVE && e.button === CoreMouseButton.NONE) {
73 * Events: all mouse related events
78 CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL
79 | CoreMouseEventType.DRAG | CoreMouseEventType.MOVE,
80 restrict: (e: ICoreMouseEvent) => true
84 const enum Modifiers {
90 // helper for default encoders to generate the event code.
91 function eventCode(e: ICoreMouseEvent, isSGR: boolean): number {
92 let code = (e.ctrl ? Modifiers.CTRL : 0) | (e.shift ? Modifiers.SHIFT : 0) | (e.alt ? Modifiers.ALT : 0);
93 if (e.button === CoreMouseButton.WHEEL) {
104 if (e.action === CoreMouseAction.MOVE) {
105 code |= CoreMouseAction.MOVE;
106 } else if (e.action === CoreMouseAction.UP && !isSGR) {
107 // special case - only SGR can report button on release
108 // all others have to go with NONE
109 code |= CoreMouseButton.NONE;
115 const S = String.fromCharCode;
118 * Supported default encodings.
120 const DEFAULT_ENCODINGS: {[key: string]: CoreMouseEncoding} = {
122 * DEFAULT - CSI M Pb Px Py
123 * Single byte encoding for coords and event code.
124 * Can encode values up to 223. The Encoding of higher
125 * values is not UTF-8 compatible (and currently limited
126 * to 95 in xterm.js).
128 DEFAULT: (e: ICoreMouseEvent) => {
129 let params = [eventCode(e, false) + 32, e.col + 32, e.row + 32];
130 // FIXME: we are currently limited to ASCII range
131 params = params.map(v => (v > 127) ? 127 : v);
132 // FIXED: params = params.map(v => (v > 255) ? 0 : value);
133 return `\x1b[M${S(params[0])}${S(params[1])}${S(params[2])}`;
136 * SGR - CSI < Pb ; Px ; Py M|m
137 * No encoding limitation.
138 * Can report button on release and works with a well formed sequence.
140 SGR: (e: ICoreMouseEvent) => {
141 const final = (e.action === CoreMouseAction.UP && e.button !== CoreMouseButton.WHEEL) ? 'm' : 'M';
142 return `\x1b[<${eventCode(e, true)};${e.col};${e.row}${final}`;
149 * Provides mouse tracking reports with different protocols and encodings.
150 * - protocols: NONE (default), X10, VT200, DRAG, ANY
151 * - encodings: DEFAULT, SGR (UTF8, URXVT removed in #2507)
153 * Custom protocols/encodings can be added by `addProtocol` / `addEncoding`.
154 * To activate a protocol/encoding, set `activeProtocol` / `activeEncoding`.
155 * Switching a protocol will send a notification event `onProtocolChange`
156 * with a list of needed events to track.
158 * The service handles the mouse tracking state and decides whether to send
159 * a tracking report to the backend based on protocol and encoding limitations.
160 * To send a mouse event call `triggerMouseEvent`.
162 export class CoreMouseService implements ICoreMouseService {
163 private _protocols: {[name: string]: ICoreMouseProtocol} = {};
164 private _encodings: {[name: string]: CoreMouseEncoding} = {};
165 private _activeProtocol: string = '';
166 private _activeEncoding: string = '';
167 private _onProtocolChange = new EventEmitter<CoreMouseEventType>();
168 private _lastEvent: ICoreMouseEvent | null = null;
171 @IBufferService private readonly _bufferService: IBufferService,
172 @ICoreService private readonly _coreService: ICoreService
174 // register default protocols and encodings
175 Object.keys(DEFAULT_PROTOCOLS).forEach(name => this.addProtocol(name, DEFAULT_PROTOCOLS[name]));
176 Object.keys(DEFAULT_ENCODINGS).forEach(name => this.addEncoding(name, DEFAULT_ENCODINGS[name]));
177 // call reset to set defaults
181 public addProtocol(name: string, protocol: ICoreMouseProtocol): void {
182 this._protocols[name] = protocol;
185 public addEncoding(name: string, encoding: CoreMouseEncoding): void {
186 this._encodings[name] = encoding;
189 public get activeProtocol(): string {
190 return this._activeProtocol;
193 public set activeProtocol(name: string) {
194 if (!this._protocols[name]) {
195 throw new Error(`unknown protocol "${name}"`);
197 this._activeProtocol = name;
198 this._onProtocolChange.fire(this._protocols[name].events);
201 public get activeEncoding(): string {
202 return this._activeEncoding;
205 public set activeEncoding(name: string) {
206 if (!this._encodings[name]) {
207 throw new Error(`unknown encoding "${name}"`);
209 this._activeEncoding = name;
212 public reset(): void {
213 this.activeProtocol = 'NONE';
214 this.activeEncoding = 'DEFAULT';
215 this._lastEvent = null;
219 * Event to announce changes in mouse tracking.
221 public get onProtocolChange(): IEvent<CoreMouseEventType> {
222 return this._onProtocolChange.event;
226 * Triggers a mouse event to be sent.
228 * Returns true if the event passed all protocol restrictions and a report
229 * was sent, otherwise false. The return value may be used to decide whether
230 * the default event action in the bowser component should be omitted.
232 * Note: The method will change values of the given event object
233 * to fullfill protocol and encoding restrictions.
235 public triggerMouseEvent(e: ICoreMouseEvent): boolean {
236 // range check for col/row
237 if (e.col < 0 || e.col >= this._bufferService.cols
238 || e.row < 0 || e.row >= this._bufferService.rows) {
242 // filter nonsense combinations of button + action
243 if (e.button === CoreMouseButton.WHEEL && e.action === CoreMouseAction.MOVE) {
246 if (e.button === CoreMouseButton.NONE && e.action !== CoreMouseAction.MOVE) {
249 if (e.button !== CoreMouseButton.WHEEL && (e.action === CoreMouseAction.LEFT || e.action === CoreMouseAction.RIGHT)) {
253 // report 1-based coords
257 // debounce move at grid level
258 if (e.action === CoreMouseAction.MOVE && this._lastEvent && this._compareEvents(this._lastEvent, e)) {
262 // apply protocol restrictions
263 if (!this._protocols[this._activeProtocol].restrict(e)) {
267 // encode report and send
268 const report = this._encodings[this._activeEncoding](e);
269 this._coreService.triggerDataEvent(report, true);
276 public explainEvents(events: CoreMouseEventType): {[event: string]: boolean} {
278 DOWN: !!(events & CoreMouseEventType.DOWN),
279 UP: !!(events & CoreMouseEventType.UP),
280 DRAG: !!(events & CoreMouseEventType.DRAG),
281 MOVE: !!(events & CoreMouseEventType.MOVE),
282 WHEEL: !!(events & CoreMouseEventType.WHEEL)
286 private _compareEvents(e1: ICoreMouseEvent, e2: ICoreMouseEvent): boolean {
287 if (e1.col !== e2.col) return false;
288 if (e1.row !== e2.row) return false;
289 if (e1.button !== e2.button) return false;
290 if (e1.action !== e2.action) return false;
291 if (e1.ctrl !== e2.ctrl) return false;
292 if (e1.alt !== e2.alt) return false;
293 if (e1.shift !== e2.shift) return false;