xterm
[VSoRC/.git] / node_modules / xterm / src / common / services / CoreMouseService.ts
1 /**
2  * Copyright (c) 2019 The xterm.js authors. All rights reserved.
3  * @license MIT
4  */
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';
8
9 /**
10  * Supported default protocols.
11  */
12 const DEFAULT_PROTOCOLS: {[key: string]: ICoreMouseProtocol} = {
13   /**
14    * NONE
15    * Events: none
16    * Modifiers: none
17    */
18   NONE: {
19     events: CoreMouseEventType.NONE,
20     restrict: () => false
21   },
22   /**
23    * X10
24    * Events: mousedown
25    * Modifiers: none
26    */
27   X10: {
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) {
32         return false;
33       }
34       // no modifiers
35       e.ctrl = false;
36       e.alt = false;
37       e.shift = false;
38       return true;
39     }
40   },
41   /**
42    * VT200
43    * Events: mousedown / mouseup / wheel
44    * Modifiers: all
45    */
46   VT200: {
47     events: CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL,
48     restrict: (e: ICoreMouseEvent) => {
49       // no move
50       if (e.action === CoreMouseAction.MOVE) {
51         return false;
52       }
53       return true;
54     }
55   },
56   /**
57    * DRAG
58    * Events: mousedown / mouseup / wheel / mousedrag
59    * Modifiers: all
60    */
61   DRAG: {
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) {
66         return false;
67       }
68       return true;
69     }
70   },
71   /**
72    * ANY
73    * Events: all mouse related events
74    * Modifiers: all
75    */
76   ANY: {
77     events:
78       CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL
79       | CoreMouseEventType.DRAG | CoreMouseEventType.MOVE,
80     restrict: (e: ICoreMouseEvent) => true
81   }
82 };
83
84 const enum Modifiers {
85   SHIFT = 4,
86   ALT = 8,
87   CTRL = 16
88 }
89
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) {
94     code |= 64;
95     code |= e.action;
96   } else {
97     code |= e.button & 3;
98     if (e.button & 4) {
99       code |= 64;
100     }
101     if (e.button & 8) {
102       code |= 128;
103     }
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;
110     }
111   }
112   return code;
113 }
114
115 const S = String.fromCharCode;
116
117 /**
118  * Supported default encodings.
119  */
120 const DEFAULT_ENCODINGS: {[key: string]: CoreMouseEncoding} = {
121   /**
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).
127    */
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])}`;
134   },
135   /**
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.
139    */
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}`;
143   }
144 };
145
146 /**
147  * CoreMouseService
148  *
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)
152  *
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.
157  *
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`.
161  */
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;
169
170   constructor(
171     @IBufferService private readonly _bufferService: IBufferService,
172     @ICoreService private readonly _coreService: ICoreService
173   ) {
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
178     this.reset();
179   }
180
181   public addProtocol(name: string, protocol: ICoreMouseProtocol): void {
182     this._protocols[name] = protocol;
183   }
184
185   public addEncoding(name: string, encoding: CoreMouseEncoding): void {
186     this._encodings[name] = encoding;
187   }
188
189   public get activeProtocol(): string {
190     return this._activeProtocol;
191   }
192
193   public set activeProtocol(name: string) {
194     if (!this._protocols[name]) {
195       throw new Error(`unknown protocol "${name}"`);
196     }
197     this._activeProtocol = name;
198     this._onProtocolChange.fire(this._protocols[name].events);
199   }
200
201   public get activeEncoding(): string {
202     return this._activeEncoding;
203   }
204
205   public set activeEncoding(name: string) {
206     if (!this._encodings[name]) {
207       throw new Error(`unknown encoding "${name}"`);
208     }
209     this._activeEncoding = name;
210   }
211
212   public reset(): void {
213     this.activeProtocol = 'NONE';
214     this.activeEncoding = 'DEFAULT';
215     this._lastEvent = null;
216   }
217
218   /**
219    * Event to announce changes in mouse tracking.
220    */
221   public get onProtocolChange(): IEvent<CoreMouseEventType> {
222     return this._onProtocolChange.event;
223   }
224
225   /**
226    * Triggers a mouse event to be sent.
227    *
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.
231    *
232    * Note: The method will change values of the given event object
233    * to fullfill protocol and encoding restrictions.
234    */
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) {
239       return false;
240     }
241
242     // filter nonsense combinations of button + action
243     if (e.button === CoreMouseButton.WHEEL && e.action === CoreMouseAction.MOVE) {
244       return false;
245     }
246     if (e.button === CoreMouseButton.NONE && e.action !== CoreMouseAction.MOVE) {
247       return false;
248     }
249     if (e.button !== CoreMouseButton.WHEEL && (e.action === CoreMouseAction.LEFT || e.action === CoreMouseAction.RIGHT)) {
250       return false;
251     }
252
253     // report 1-based coords
254     e.col++;
255     e.row++;
256
257     // debounce move at grid level
258     if (e.action === CoreMouseAction.MOVE && this._lastEvent && this._compareEvents(this._lastEvent, e)) {
259       return false;
260     }
261
262     // apply protocol restrictions
263     if (!this._protocols[this._activeProtocol].restrict(e)) {
264       return false;
265     }
266
267     // encode report and send
268     const report = this._encodings[this._activeEncoding](e);
269     this._coreService.triggerDataEvent(report, true);
270
271     this._lastEvent = e;
272
273     return true;
274   }
275
276   public explainEvents(events: CoreMouseEventType): {[event: string]: boolean} {
277     return {
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)
283     };
284   }
285
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;
294     return true;
295   }
296 }