@teammates/consolonia 0.7.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,14 @@
1
1
  /**
2
- * Parses SGR extended mouse tracking sequences.
2
+ * Parses terminal mouse tracking sequences.
3
3
  *
4
- * Format: \x1b[<Cb;Cx;CyM (press/motion)
5
- * \x1b[<Cb;Cx;Cym (release)
4
+ * Supported formats:
5
+ * SGR: \x1b[<Cb;Cx;CyM (press/motion)
6
+ * \x1b[<Cb;Cx;Cym (release)
7
+ * SGR-Pixels: \x1b[<Cb;Cx;CyM (same wire format as SGR, pixel coords)
8
+ * \x1b[<Cb;Cx;Cym
9
+ * X10: \x1b[M Cb Cx Cy (classic xterm byte encoding)
10
+ * UTF-8: \x1b[M Cb Cx Cy (same prefix as X10, UTF-8 encoded coords)
11
+ * URXVT: \x1b[Cb;Cx;CyM (decimal params, no < prefix)
6
12
  *
7
13
  * Cb encodes button and modifiers:
8
14
  * bits 0-1: 0=left, 1=middle, 2=right
@@ -13,15 +19,27 @@
13
19
  * bit 4 (+16): ctrl
14
20
  *
15
21
  * Cx, Cy are 1-based coordinates.
22
+ *
23
+ * Note: UTF-8 mode uses the same \x1b[M prefix as X10 but encodes
24
+ * coordinates as UTF-8 characters for values > 127. Node.js decodes
25
+ * UTF-8 stdin automatically, so the X10 parser handles both formats.
26
+ *
27
+ * Note: SGR-Pixels mode uses the same wire format as SGR but reports
28
+ * pixel coordinates instead of cell coordinates. These are passed
29
+ * through as-is (the caller must convert to cells if needed).
16
30
  */
17
31
  import { type InputEvent } from "./events.js";
18
32
  import { type IMatcher, MatchResult } from "./matcher.js";
19
33
  export declare class MouseMatcher implements IMatcher {
20
34
  private state;
21
35
  private params;
36
+ private x10Bytes;
37
+ private urxvtParams;
22
38
  private result;
23
39
  append(char: string): MatchResult;
24
40
  flush(): InputEvent | null;
25
41
  reset(): void;
26
42
  private finalize;
43
+ private finalizeUrxvt;
44
+ private finalizeX10;
27
45
  }
@@ -1,8 +1,14 @@
1
1
  /**
2
- * Parses SGR extended mouse tracking sequences.
2
+ * Parses terminal mouse tracking sequences.
3
3
  *
4
- * Format: \x1b[<Cb;Cx;CyM (press/motion)
5
- * \x1b[<Cb;Cx;Cym (release)
4
+ * Supported formats:
5
+ * SGR: \x1b[<Cb;Cx;CyM (press/motion)
6
+ * \x1b[<Cb;Cx;Cym (release)
7
+ * SGR-Pixels: \x1b[<Cb;Cx;CyM (same wire format as SGR, pixel coords)
8
+ * \x1b[<Cb;Cx;Cym
9
+ * X10: \x1b[M Cb Cx Cy (classic xterm byte encoding)
10
+ * UTF-8: \x1b[M Cb Cx Cy (same prefix as X10, UTF-8 encoded coords)
11
+ * URXVT: \x1b[Cb;Cx;CyM (decimal params, no < prefix)
6
12
  *
7
13
  * Cb encodes button and modifiers:
8
14
  * bits 0-1: 0=left, 1=middle, 2=right
@@ -13,6 +19,14 @@
13
19
  * bit 4 (+16): ctrl
14
20
  *
15
21
  * Cx, Cy are 1-based coordinates.
22
+ *
23
+ * Note: UTF-8 mode uses the same \x1b[M prefix as X10 but encodes
24
+ * coordinates as UTF-8 characters for values > 127. Node.js decodes
25
+ * UTF-8 stdin automatically, so the X10 parser handles both formats.
26
+ *
27
+ * Note: SGR-Pixels mode uses the same wire format as SGR but reports
28
+ * pixel coordinates instead of cell coordinates. These are passed
29
+ * through as-is (the caller must convert to cells if needed).
16
30
  */
17
31
  import { mouseEvent } from "./events.js";
18
32
  import { MatchResult } from "./matcher.js";
@@ -24,12 +38,18 @@ var State;
24
38
  State[State["GotEsc"] = 1] = "GotEsc";
25
39
  /** Got \x1b[ */
26
40
  State[State["GotBracket"] = 2] = "GotBracket";
27
- /** Got \x1b[< — now reading params until M or m */
41
+ /** Got \x1b[< — now reading SGR/SGR-Pixels params until M or m */
28
42
  State[State["Reading"] = 3] = "Reading";
43
+ /** Got \x1b[M — now reading three encoded bytes (X10 or UTF-8) */
44
+ State[State["ReadingX10"] = 4] = "ReadingX10";
45
+ /** Got \x1b[ followed by a digit — reading URXVT decimal params until M */
46
+ State[State["ReadingUrxvt"] = 5] = "ReadingUrxvt";
29
47
  })(State || (State = {}));
30
48
  export class MouseMatcher {
31
49
  state = State.Idle;
32
50
  params = "";
51
+ x10Bytes = [];
52
+ urxvtParams = "";
33
53
  result = null;
34
54
  append(char) {
35
55
  switch (this.state) {
@@ -52,6 +72,17 @@ export class MouseMatcher {
52
72
  this.params = "";
53
73
  return MatchResult.Partial;
54
74
  }
75
+ if (char === "M") {
76
+ this.state = State.ReadingX10;
77
+ this.x10Bytes = [];
78
+ return MatchResult.Partial;
79
+ }
80
+ // URXVT: \x1b[ followed by a digit starts decimal param reading
81
+ if (char >= "0" && char <= "9") {
82
+ this.state = State.ReadingUrxvt;
83
+ this.urxvtParams = char;
84
+ return MatchResult.Partial;
85
+ }
55
86
  this.state = State.Idle;
56
87
  return MatchResult.NoMatch;
57
88
  case State.Reading: {
@@ -69,6 +100,26 @@ export class MouseMatcher {
69
100
  this.params = "";
70
101
  return MatchResult.NoMatch;
71
102
  }
103
+ case State.ReadingX10:
104
+ this.x10Bytes.push(char);
105
+ if (this.x10Bytes.length < 3) {
106
+ return MatchResult.Partial;
107
+ }
108
+ return this.finalizeX10();
109
+ case State.ReadingUrxvt: {
110
+ if (char === "M") {
111
+ return this.finalizeUrxvt();
112
+ }
113
+ const c = char.charCodeAt(0);
114
+ if ((c >= 0x30 && c <= 0x39) || char === ";") {
115
+ this.urxvtParams += char;
116
+ return MatchResult.Partial;
117
+ }
118
+ // Not a valid URXVT sequence — bail out
119
+ this.state = State.Idle;
120
+ this.urxvtParams = "";
121
+ return MatchResult.NoMatch;
122
+ }
72
123
  default:
73
124
  return MatchResult.NoMatch;
74
125
  }
@@ -81,6 +132,8 @@ export class MouseMatcher {
81
132
  reset() {
82
133
  this.state = State.Idle;
83
134
  this.params = "";
135
+ this.x10Bytes = [];
136
+ this.urxvtParams = "";
84
137
  this.result = null;
85
138
  }
86
139
  finalize(isRelease) {
@@ -96,37 +149,77 @@ export class MouseMatcher {
96
149
  if (Number.isNaN(cb) || Number.isNaN(cx) || Number.isNaN(cy)) {
97
150
  return MatchResult.NoMatch;
98
151
  }
99
- // Decode modifiers from cb
100
- const shift = (cb & 4) !== 0;
101
- const alt = (cb & 8) !== 0;
102
- const ctrl = (cb & 16) !== 0;
103
- const isMotion = (cb & 32) !== 0;
104
- // Decode button from low bits (masking out modifier/motion bits)
105
- const buttonBits = cb & 3;
106
- const highBits = cb & (64 | 128);
107
- let button;
108
- let type;
109
- if (highBits === 64) {
110
- // Wheel events
111
- button = "none";
112
- type = buttonBits === 0 ? "wheelup" : "wheeldown";
152
+ if (isRelease) {
153
+ const shift = (cb & 4) !== 0;
154
+ const alt = (cb & 8) !== 0;
155
+ const ctrl = (cb & 16) !== 0;
156
+ this.result = mouseEvent(cx - 1, cy - 1, decodeButton(cb & 3), "release", shift, ctrl, alt);
157
+ return MatchResult.Complete;
113
158
  }
114
- else if (isRelease) {
115
- button = decodeButton(buttonBits);
116
- type = "release";
159
+ this.result = decodeMouseEvent(cb, cx, cy, true);
160
+ return this.result ? MatchResult.Complete : MatchResult.NoMatch;
161
+ }
162
+ finalizeUrxvt() {
163
+ this.state = State.Idle;
164
+ const parts = this.urxvtParams.split(";");
165
+ this.urxvtParams = "";
166
+ if (parts.length !== 3) {
167
+ return MatchResult.NoMatch;
168
+ }
169
+ const cb = parseInt(parts[0], 10);
170
+ const cx = parseInt(parts[1], 10);
171
+ const cy = parseInt(parts[2], 10);
172
+ if (Number.isNaN(cb) || Number.isNaN(cx) || Number.isNaN(cy)) {
173
+ return MatchResult.NoMatch;
117
174
  }
118
- else if (isMotion) {
119
- button = buttonBits === 3 ? "none" : decodeButton(buttonBits);
120
- type = "move";
175
+ // URXVT uses the same button encoding as X10 (button 3 = release)
176
+ this.result = decodeMouseEvent(cb, cx, cy, true);
177
+ return this.result ? MatchResult.Complete : MatchResult.NoMatch;
178
+ }
179
+ finalizeX10() {
180
+ this.state = State.Idle;
181
+ if (this.x10Bytes.length !== 3) {
182
+ this.x10Bytes = [];
183
+ return MatchResult.NoMatch;
121
184
  }
122
- else {
123
- button = decodeButton(buttonBits);
124
- type = "press";
185
+ const [cbChar, cxChar, cyChar] = this.x10Bytes;
186
+ this.x10Bytes = [];
187
+ const cb = cbChar.charCodeAt(0) - 32;
188
+ const cx = cxChar.charCodeAt(0) - 32;
189
+ const cy = cyChar.charCodeAt(0) - 32;
190
+ if (cb < 0 || cx <= 0 || cy <= 0) {
191
+ return MatchResult.NoMatch;
125
192
  }
126
- // Convert from 1-based to 0-based coordinates
127
- this.result = mouseEvent(cx - 1, cy - 1, button, type, shift, ctrl, alt);
128
- return MatchResult.Complete;
193
+ this.result = decodeMouseEvent(cb, cx, cy, true);
194
+ return this.result ? MatchResult.Complete : MatchResult.NoMatch;
195
+ }
196
+ }
197
+ function decodeMouseEvent(cb, cx, cy, x10ReleaseUsesButton3) {
198
+ const shift = (cb & 4) !== 0;
199
+ const alt = (cb & 8) !== 0;
200
+ const ctrl = (cb & 16) !== 0;
201
+ const isMotion = (cb & 32) !== 0;
202
+ const buttonBits = cb & 3;
203
+ const highBits = cb & (64 | 128);
204
+ let button;
205
+ let type;
206
+ if (highBits === 64) {
207
+ button = "none";
208
+ type = buttonBits === 0 ? "wheelup" : "wheeldown";
209
+ }
210
+ else if (x10ReleaseUsesButton3 && !isMotion && buttonBits === 3) {
211
+ button = "none";
212
+ type = "release";
213
+ }
214
+ else if (isMotion) {
215
+ button = buttonBits === 3 ? "none" : decodeButton(buttonBits);
216
+ type = "move";
217
+ }
218
+ else {
219
+ button = decodeButton(buttonBits);
220
+ type = "press";
129
221
  }
222
+ return mouseEvent(cx - 1, cy - 1, button, type, shift, ctrl, alt);
130
223
  }
131
224
  function decodeButton(bits) {
132
225
  switch (bits) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teammates/consolonia",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "Terminal UI rendering engine inspired by Consolonia. Pixel-level compositing with ANSI output.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -41,5 +41,8 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "marked": "^17.0.4"
44
+ },
45
+ "optionalDependencies": {
46
+ "koffi": "^2.9.0"
44
47
  }
45
48
  }