@teammates/consolonia 0.2.0

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.
Files changed (104) hide show
  1. package/README.md +48 -0
  2. package/dist/__tests__/ansi.test.d.ts +1 -0
  3. package/dist/__tests__/ansi.test.js +520 -0
  4. package/dist/__tests__/chat-view.test.d.ts +4 -0
  5. package/dist/__tests__/chat-view.test.js +480 -0
  6. package/dist/__tests__/drawing.test.d.ts +4 -0
  7. package/dist/__tests__/drawing.test.js +426 -0
  8. package/dist/__tests__/input.test.d.ts +5 -0
  9. package/dist/__tests__/input.test.js +911 -0
  10. package/dist/__tests__/layout.test.d.ts +4 -0
  11. package/dist/__tests__/layout.test.js +689 -0
  12. package/dist/__tests__/pixel.test.d.ts +1 -0
  13. package/dist/__tests__/pixel.test.js +674 -0
  14. package/dist/__tests__/render.test.d.ts +1 -0
  15. package/dist/__tests__/render.test.js +400 -0
  16. package/dist/__tests__/styled.test.d.ts +4 -0
  17. package/dist/__tests__/styled.test.js +149 -0
  18. package/dist/__tests__/widgets.test.d.ts +5 -0
  19. package/dist/__tests__/widgets.test.js +924 -0
  20. package/dist/ansi/esc.d.ts +61 -0
  21. package/dist/ansi/esc.js +85 -0
  22. package/dist/ansi/output.d.ts +66 -0
  23. package/dist/ansi/output.js +192 -0
  24. package/dist/ansi/strip.d.ts +16 -0
  25. package/dist/ansi/strip.js +74 -0
  26. package/dist/app.d.ts +68 -0
  27. package/dist/app.js +297 -0
  28. package/dist/drawing/clip.d.ts +23 -0
  29. package/dist/drawing/clip.js +67 -0
  30. package/dist/drawing/context.d.ts +77 -0
  31. package/dist/drawing/context.js +275 -0
  32. package/dist/index.d.ts +48 -0
  33. package/dist/index.js +63 -0
  34. package/dist/input/escape-matcher.d.ts +27 -0
  35. package/dist/input/escape-matcher.js +253 -0
  36. package/dist/input/events.d.ts +49 -0
  37. package/dist/input/events.js +17 -0
  38. package/dist/input/index.d.ts +15 -0
  39. package/dist/input/index.js +14 -0
  40. package/dist/input/matcher.d.ts +23 -0
  41. package/dist/input/matcher.js +14 -0
  42. package/dist/input/mouse-matcher.d.ts +27 -0
  43. package/dist/input/mouse-matcher.js +142 -0
  44. package/dist/input/paste-matcher.d.ts +23 -0
  45. package/dist/input/paste-matcher.js +104 -0
  46. package/dist/input/processor.d.ts +51 -0
  47. package/dist/input/processor.js +145 -0
  48. package/dist/input/raw-mode.d.ts +13 -0
  49. package/dist/input/raw-mode.js +24 -0
  50. package/dist/input/text-matcher.d.ts +14 -0
  51. package/dist/input/text-matcher.js +32 -0
  52. package/dist/layout/box.d.ts +33 -0
  53. package/dist/layout/box.js +92 -0
  54. package/dist/layout/column.d.ts +21 -0
  55. package/dist/layout/column.js +90 -0
  56. package/dist/layout/control.d.ts +73 -0
  57. package/dist/layout/control.js +215 -0
  58. package/dist/layout/row.d.ts +21 -0
  59. package/dist/layout/row.js +95 -0
  60. package/dist/layout/stack.d.ts +18 -0
  61. package/dist/layout/stack.js +64 -0
  62. package/dist/layout/types.d.ts +27 -0
  63. package/dist/layout/types.js +4 -0
  64. package/dist/pixel/background.d.ts +16 -0
  65. package/dist/pixel/background.js +16 -0
  66. package/dist/pixel/box-pattern.d.ts +38 -0
  67. package/dist/pixel/box-pattern.js +57 -0
  68. package/dist/pixel/buffer.d.ts +25 -0
  69. package/dist/pixel/buffer.js +51 -0
  70. package/dist/pixel/color.d.ts +48 -0
  71. package/dist/pixel/color.js +92 -0
  72. package/dist/pixel/foreground.d.ts +31 -0
  73. package/dist/pixel/foreground.js +64 -0
  74. package/dist/pixel/pixel.d.ts +21 -0
  75. package/dist/pixel/pixel.js +38 -0
  76. package/dist/pixel/symbol.d.ts +38 -0
  77. package/dist/pixel/symbol.js +192 -0
  78. package/dist/render/regions.d.ts +54 -0
  79. package/dist/render/regions.js +102 -0
  80. package/dist/render/render-target.d.ts +42 -0
  81. package/dist/render/render-target.js +118 -0
  82. package/dist/styled.d.ts +113 -0
  83. package/dist/styled.js +176 -0
  84. package/dist/widgets/border.d.ts +34 -0
  85. package/dist/widgets/border.js +121 -0
  86. package/dist/widgets/chat-view.d.ts +239 -0
  87. package/dist/widgets/chat-view.js +993 -0
  88. package/dist/widgets/interview.d.ts +87 -0
  89. package/dist/widgets/interview.js +187 -0
  90. package/dist/widgets/markdown.d.ts +87 -0
  91. package/dist/widgets/markdown.js +611 -0
  92. package/dist/widgets/panel.d.ts +19 -0
  93. package/dist/widgets/panel.js +35 -0
  94. package/dist/widgets/scroll-view.d.ts +43 -0
  95. package/dist/widgets/scroll-view.js +182 -0
  96. package/dist/widgets/styled-text.d.ts +38 -0
  97. package/dist/widgets/styled-text.js +183 -0
  98. package/dist/widgets/syntax.d.ts +37 -0
  99. package/dist/widgets/syntax.js +670 -0
  100. package/dist/widgets/text-input.d.ts +121 -0
  101. package/dist/widgets/text-input.js +618 -0
  102. package/dist/widgets/text.d.ts +34 -0
  103. package/dist/widgets/text.js +168 -0
  104. package/package.json +45 -0
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Input event type definitions for the raw terminal input system.
3
+ * Mirrors Consolonia's RawConsoleInputEventArgs and related types.
4
+ */
5
+ /** Keyboard event produced by key presses and escape sequences. */
6
+ export interface KeyEvent {
7
+ /** Logical key name: 'a', 'A', 'enter', 'backspace', 'up', 'f1', etc. */
8
+ key: string;
9
+ /** The actual character produced, empty string for non-printable keys. */
10
+ char: string;
11
+ shift: boolean;
12
+ ctrl: boolean;
13
+ alt: boolean;
14
+ }
15
+ /** Mouse event produced by SGR extended mouse tracking. */
16
+ export interface MouseEvent {
17
+ /** 0-based column. */
18
+ x: number;
19
+ /** 0-based row. */
20
+ y: number;
21
+ button: "left" | "middle" | "right" | "none";
22
+ type: "press" | "release" | "move" | "wheelup" | "wheeldown";
23
+ shift: boolean;
24
+ ctrl: boolean;
25
+ alt: boolean;
26
+ }
27
+ /** Bracketed paste event containing the pasted text. */
28
+ export interface PasteEvent {
29
+ text: string;
30
+ }
31
+ /** Discriminated union of all input events. */
32
+ export type InputEvent = {
33
+ type: "key";
34
+ event: KeyEvent;
35
+ } | {
36
+ type: "mouse";
37
+ event: MouseEvent;
38
+ } | {
39
+ type: "paste";
40
+ event: PasteEvent;
41
+ } | {
42
+ type: "resize";
43
+ width: number;
44
+ height: number;
45
+ };
46
+ export declare function keyEvent(key: string, char?: string, shift?: boolean, ctrl?: boolean, alt?: boolean): InputEvent;
47
+ export declare function mouseEvent(x: number, y: number, button: MouseEvent["button"], type: MouseEvent["type"], shift?: boolean, ctrl?: boolean, alt?: boolean): InputEvent;
48
+ export declare function pasteEvent(text: string): InputEvent;
49
+ export declare function resizeEvent(width: number, height: number): InputEvent;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Input event type definitions for the raw terminal input system.
3
+ * Mirrors Consolonia's RawConsoleInputEventArgs and related types.
4
+ */
5
+ // ── Factory helpers ─────────────────────────────────────────────────
6
+ export function keyEvent(key, char = "", shift = false, ctrl = false, alt = false) {
7
+ return { type: "key", event: { key, char, shift, ctrl, alt } };
8
+ }
9
+ export function mouseEvent(x, y, button, type, shift = false, ctrl = false, alt = false) {
10
+ return { type: "mouse", event: { x, y, button, type, shift, ctrl, alt } };
11
+ }
12
+ export function pasteEvent(text) {
13
+ return { type: "paste", event: { text } };
14
+ }
15
+ export function resizeEvent(width, height) {
16
+ return { type: "resize", width, height };
17
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Raw terminal input system.
3
+ *
4
+ * Provides escape sequence parsing, mouse tracking, bracketed paste
5
+ * detection, and a unified InputProcessor pipeline.
6
+ */
7
+ export { EscapeMatcher } from "./escape-matcher.js";
8
+ export type { InputEvent, KeyEvent, MouseEvent, PasteEvent, } from "./events.js";
9
+ export { keyEvent, mouseEvent, pasteEvent, resizeEvent, } from "./events.js";
10
+ export { type IMatcher, MatchResult } from "./matcher.js";
11
+ export { MouseMatcher } from "./mouse-matcher.js";
12
+ export { PasteMatcher } from "./paste-matcher.js";
13
+ export { createInputProcessor, InputProcessor } from "./processor.js";
14
+ export { disableRawMode, enableRawMode } from "./raw-mode.js";
15
+ export { TextMatcher } from "./text-matcher.js";
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Raw terminal input system.
3
+ *
4
+ * Provides escape sequence parsing, mouse tracking, bracketed paste
5
+ * detection, and a unified InputProcessor pipeline.
6
+ */
7
+ export { EscapeMatcher } from "./escape-matcher.js";
8
+ export { keyEvent, mouseEvent, pasteEvent, resizeEvent, } from "./events.js";
9
+ export { MatchResult } from "./matcher.js";
10
+ export { MouseMatcher } from "./mouse-matcher.js";
11
+ export { PasteMatcher } from "./paste-matcher.js";
12
+ export { createInputProcessor, InputProcessor } from "./processor.js";
13
+ export { disableRawMode, enableRawMode } from "./raw-mode.js";
14
+ export { TextMatcher } from "./text-matcher.js";
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Base matcher interface for the input processing pipeline.
3
+ * Inspired by Consolonia's InputProcessor matcher chain.
4
+ */
5
+ import type { InputEvent } from "./events.js";
6
+ /** Result of feeding a character to a matcher. */
7
+ export declare enum MatchResult {
8
+ /** This character is not part of a sequence this matcher handles. */
9
+ NoMatch = 0,
10
+ /** This character continues a partial sequence; more input needed. */
11
+ Partial = 1,
12
+ /** This character completes a recognized sequence; call flush(). */
13
+ Complete = 2
14
+ }
15
+ /** A matcher consumes characters and produces InputEvents. */
16
+ export interface IMatcher {
17
+ /** Feed a character to the matcher. Returns the match state. */
18
+ append(char: string): MatchResult;
19
+ /** Get the matched event and reset. Only valid after Complete. */
20
+ flush(): InputEvent | null;
21
+ /** Reset matcher state, discarding any partial sequence. */
22
+ reset(): void;
23
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Base matcher interface for the input processing pipeline.
3
+ * Inspired by Consolonia's InputProcessor matcher chain.
4
+ */
5
+ /** Result of feeding a character to a matcher. */
6
+ export var MatchResult;
7
+ (function (MatchResult) {
8
+ /** This character is not part of a sequence this matcher handles. */
9
+ MatchResult[MatchResult["NoMatch"] = 0] = "NoMatch";
10
+ /** This character continues a partial sequence; more input needed. */
11
+ MatchResult[MatchResult["Partial"] = 1] = "Partial";
12
+ /** This character completes a recognized sequence; call flush(). */
13
+ MatchResult[MatchResult["Complete"] = 2] = "Complete";
14
+ })(MatchResult || (MatchResult = {}));
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Parses SGR extended mouse tracking sequences.
3
+ *
4
+ * Format: \x1b[<Cb;Cx;CyM (press/motion)
5
+ * \x1b[<Cb;Cx;Cym (release)
6
+ *
7
+ * Cb encodes button and modifiers:
8
+ * bits 0-1: 0=left, 1=middle, 2=right
9
+ * bit 5 (+32): motion event
10
+ * bits 6-7: 64=wheel up, 65=wheel down
11
+ * bit 2 (+4): shift
12
+ * bit 3 (+8): alt/meta
13
+ * bit 4 (+16): ctrl
14
+ *
15
+ * Cx, Cy are 1-based coordinates.
16
+ */
17
+ import { type InputEvent } from "./events.js";
18
+ import { type IMatcher, MatchResult } from "./matcher.js";
19
+ export declare class MouseMatcher implements IMatcher {
20
+ private state;
21
+ private params;
22
+ private result;
23
+ append(char: string): MatchResult;
24
+ flush(): InputEvent | null;
25
+ reset(): void;
26
+ private finalize;
27
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Parses SGR extended mouse tracking sequences.
3
+ *
4
+ * Format: \x1b[<Cb;Cx;CyM (press/motion)
5
+ * \x1b[<Cb;Cx;Cym (release)
6
+ *
7
+ * Cb encodes button and modifiers:
8
+ * bits 0-1: 0=left, 1=middle, 2=right
9
+ * bit 5 (+32): motion event
10
+ * bits 6-7: 64=wheel up, 65=wheel down
11
+ * bit 2 (+4): shift
12
+ * bit 3 (+8): alt/meta
13
+ * bit 4 (+16): ctrl
14
+ *
15
+ * Cx, Cy are 1-based coordinates.
16
+ */
17
+ import { mouseEvent } from "./events.js";
18
+ import { MatchResult } from "./matcher.js";
19
+ const ESC = "\x1b";
20
+ var State;
21
+ (function (State) {
22
+ State[State["Idle"] = 0] = "Idle";
23
+ /** Got \x1b */
24
+ State[State["GotEsc"] = 1] = "GotEsc";
25
+ /** Got \x1b[ */
26
+ State[State["GotBracket"] = 2] = "GotBracket";
27
+ /** Got \x1b[< — now reading params until M or m */
28
+ State[State["Reading"] = 3] = "Reading";
29
+ })(State || (State = {}));
30
+ export class MouseMatcher {
31
+ state = State.Idle;
32
+ params = "";
33
+ result = null;
34
+ append(char) {
35
+ switch (this.state) {
36
+ case State.Idle:
37
+ if (char === ESC) {
38
+ this.state = State.GotEsc;
39
+ return MatchResult.Partial;
40
+ }
41
+ return MatchResult.NoMatch;
42
+ case State.GotEsc:
43
+ if (char === "[") {
44
+ this.state = State.GotBracket;
45
+ return MatchResult.Partial;
46
+ }
47
+ this.state = State.Idle;
48
+ return MatchResult.NoMatch;
49
+ case State.GotBracket:
50
+ if (char === "<") {
51
+ this.state = State.Reading;
52
+ this.params = "";
53
+ return MatchResult.Partial;
54
+ }
55
+ this.state = State.Idle;
56
+ return MatchResult.NoMatch;
57
+ case State.Reading: {
58
+ if (char === "M" || char === "m") {
59
+ return this.finalize(char === "m");
60
+ }
61
+ // Valid param chars: digits and semicolons
62
+ const code = char.charCodeAt(0);
63
+ if ((code >= 0x30 && code <= 0x39) || char === ";") {
64
+ this.params += char;
65
+ return MatchResult.Partial;
66
+ }
67
+ // Unexpected character — abort
68
+ this.state = State.Idle;
69
+ this.params = "";
70
+ return MatchResult.NoMatch;
71
+ }
72
+ default:
73
+ return MatchResult.NoMatch;
74
+ }
75
+ }
76
+ flush() {
77
+ const ev = this.result;
78
+ this.result = null;
79
+ return ev;
80
+ }
81
+ reset() {
82
+ this.state = State.Idle;
83
+ this.params = "";
84
+ this.result = null;
85
+ }
86
+ finalize(isRelease) {
87
+ this.state = State.Idle;
88
+ const parts = this.params.split(";");
89
+ this.params = "";
90
+ if (parts.length !== 3) {
91
+ return MatchResult.NoMatch;
92
+ }
93
+ const cb = parseInt(parts[0], 10);
94
+ const cx = parseInt(parts[1], 10);
95
+ const cy = parseInt(parts[2], 10);
96
+ if (Number.isNaN(cb) || Number.isNaN(cx) || Number.isNaN(cy)) {
97
+ return MatchResult.NoMatch;
98
+ }
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";
113
+ }
114
+ else if (isRelease) {
115
+ button = decodeButton(buttonBits);
116
+ type = "release";
117
+ }
118
+ else if (isMotion) {
119
+ button = buttonBits === 3 ? "none" : decodeButton(buttonBits);
120
+ type = "move";
121
+ }
122
+ else {
123
+ button = decodeButton(buttonBits);
124
+ type = "press";
125
+ }
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;
129
+ }
130
+ }
131
+ function decodeButton(bits) {
132
+ switch (bits) {
133
+ case 0:
134
+ return "left";
135
+ case 1:
136
+ return "middle";
137
+ case 2:
138
+ return "right";
139
+ default:
140
+ return "none";
141
+ }
142
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Detects bracketed paste mode sequences and collects pasted text.
3
+ *
4
+ * Start marker: \x1b[200~
5
+ * End marker: \x1b[201~
6
+ * Everything between the markers is emitted as a single PasteEvent.
7
+ */
8
+ import { type InputEvent } from "./events.js";
9
+ import { type IMatcher, MatchResult } from "./matcher.js";
10
+ export declare class PasteMatcher implements IMatcher {
11
+ private state;
12
+ /** How many characters of the start marker have been matched. */
13
+ private startPos;
14
+ /** How many characters of the end marker have been matched. */
15
+ private endPos;
16
+ /** Accumulated paste text. */
17
+ private buffer;
18
+ /** Completed event ready for flushing. */
19
+ private result;
20
+ append(char: string): MatchResult;
21
+ flush(): InputEvent | null;
22
+ reset(): void;
23
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Detects bracketed paste mode sequences and collects pasted text.
3
+ *
4
+ * Start marker: \x1b[200~
5
+ * End marker: \x1b[201~
6
+ * Everything between the markers is emitted as a single PasteEvent.
7
+ */
8
+ import { pasteEvent } from "./events.js";
9
+ import { MatchResult } from "./matcher.js";
10
+ /** The start marker as an array of characters. */
11
+ const PASTE_START = "\x1b[200~".split("");
12
+ /** The end marker as an array of characters. */
13
+ const PASTE_END = "\x1b[201~".split("");
14
+ var State;
15
+ (function (State) {
16
+ /** Waiting for the first character of the start marker. */
17
+ State[State["Idle"] = 0] = "Idle";
18
+ /** Matching characters of the start marker. */
19
+ State[State["MatchingStart"] = 1] = "MatchingStart";
20
+ /** Inside the paste — collecting text. */
21
+ State[State["Collecting"] = 2] = "Collecting";
22
+ /** Matching characters of the end marker. */
23
+ State[State["MatchingEnd"] = 3] = "MatchingEnd";
24
+ })(State || (State = {}));
25
+ export class PasteMatcher {
26
+ state = State.Idle;
27
+ /** How many characters of the start marker have been matched. */
28
+ startPos = 0;
29
+ /** How many characters of the end marker have been matched. */
30
+ endPos = 0;
31
+ /** Accumulated paste text. */
32
+ buffer = "";
33
+ /** Completed event ready for flushing. */
34
+ result = null;
35
+ append(char) {
36
+ switch (this.state) {
37
+ case State.Idle:
38
+ if (char === PASTE_START[0]) {
39
+ this.state = State.MatchingStart;
40
+ this.startPos = 1;
41
+ return MatchResult.Partial;
42
+ }
43
+ return MatchResult.NoMatch;
44
+ case State.MatchingStart:
45
+ if (char === PASTE_START[this.startPos]) {
46
+ this.startPos++;
47
+ if (this.startPos === PASTE_START.length) {
48
+ // Full start marker matched — begin collecting.
49
+ this.state = State.Collecting;
50
+ this.buffer = "";
51
+ this.endPos = 0;
52
+ }
53
+ return MatchResult.Partial;
54
+ }
55
+ // Mismatch — not a paste start sequence.
56
+ this.state = State.Idle;
57
+ this.startPos = 0;
58
+ return MatchResult.NoMatch;
59
+ case State.Collecting:
60
+ if (char === PASTE_END[0]) {
61
+ this.state = State.MatchingEnd;
62
+ this.endPos = 1;
63
+ return MatchResult.Partial;
64
+ }
65
+ this.buffer += char;
66
+ return MatchResult.Partial;
67
+ case State.MatchingEnd: {
68
+ if (char === PASTE_END[this.endPos]) {
69
+ this.endPos++;
70
+ if (this.endPos === PASTE_END.length) {
71
+ // Full end marker matched — paste is complete.
72
+ this.state = State.Idle;
73
+ this.result = pasteEvent(this.buffer);
74
+ this.buffer = "";
75
+ this.endPos = 0;
76
+ return MatchResult.Complete;
77
+ }
78
+ return MatchResult.Partial;
79
+ }
80
+ // End marker mismatch — the partially-matched end marker chars
81
+ // are actually part of the paste text.
82
+ const partial = PASTE_END.slice(0, this.endPos).join("");
83
+ this.buffer += partial + char;
84
+ this.state = State.Collecting;
85
+ this.endPos = 0;
86
+ return MatchResult.Partial;
87
+ }
88
+ default:
89
+ return MatchResult.NoMatch;
90
+ }
91
+ }
92
+ flush() {
93
+ const ev = this.result;
94
+ this.result = null;
95
+ return ev;
96
+ }
97
+ reset() {
98
+ this.state = State.Idle;
99
+ this.startPos = 0;
100
+ this.endPos = 0;
101
+ this.buffer = "";
102
+ this.result = null;
103
+ }
104
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * InputProcessor — the central input pipeline.
3
+ * Port of Consolonia's InputProcessor.cs.
4
+ *
5
+ * All matchers run in parallel on each character. When multiple matchers
6
+ * return Partial, all of them continue receiving input. The first matcher
7
+ * (by priority order) to return Complete wins, and the others are reset.
8
+ *
9
+ * Matcher priority order: PasteMatcher > MouseMatcher > EscapeMatcher > TextMatcher
10
+ *
11
+ * The escape key requires special handling: a lone ESC (\x1b) could be
12
+ * the start of an escape sequence or the escape key itself. We use a
13
+ * short timeout (50ms) to distinguish — if no follow-up character
14
+ * arrives within the timeout, we emit an escape key event.
15
+ */
16
+ import { EventEmitter } from "node:events";
17
+ export declare class InputProcessor {
18
+ private readonly matchers;
19
+ private readonly escapeMatcher;
20
+ private readonly emitter;
21
+ /** Which matchers are still active (Partial or not yet tried) for the current sequence. */
22
+ private active;
23
+ /** Timer for the escape key timeout. */
24
+ private escTimer;
25
+ constructor(emitter: EventEmitter);
26
+ /**
27
+ * Feed a chunk of raw stdin data into the processor.
28
+ * Characters are dispatched one at a time through the matcher chain.
29
+ */
30
+ feed(data: string): void;
31
+ /** Destroy timers and clean up. */
32
+ destroy(): void;
33
+ private feedChar;
34
+ /** Reset all matchers and re-activate them. */
35
+ private resetAll;
36
+ /**
37
+ * If the escape matcher is active and holding a partial \x1b,
38
+ * schedule a timeout to emit the escape key event.
39
+ */
40
+ private scheduleEscTimeoutIfNeeded;
41
+ private clearEscTimer;
42
+ private emit;
43
+ }
44
+ /**
45
+ * Create an InputProcessor wired to a fresh EventEmitter.
46
+ * Listen on the returned emitter's 'input' event to receive InputEvents.
47
+ */
48
+ export declare function createInputProcessor(): {
49
+ processor: InputProcessor;
50
+ events: EventEmitter;
51
+ };
@@ -0,0 +1,145 @@
1
+ /**
2
+ * InputProcessor — the central input pipeline.
3
+ * Port of Consolonia's InputProcessor.cs.
4
+ *
5
+ * All matchers run in parallel on each character. When multiple matchers
6
+ * return Partial, all of them continue receiving input. The first matcher
7
+ * (by priority order) to return Complete wins, and the others are reset.
8
+ *
9
+ * Matcher priority order: PasteMatcher > MouseMatcher > EscapeMatcher > TextMatcher
10
+ *
11
+ * The escape key requires special handling: a lone ESC (\x1b) could be
12
+ * the start of an escape sequence or the escape key itself. We use a
13
+ * short timeout (50ms) to distinguish — if no follow-up character
14
+ * arrives within the timeout, we emit an escape key event.
15
+ */
16
+ import { EventEmitter } from "node:events";
17
+ import { EscapeMatcher } from "./escape-matcher.js";
18
+ import { MatchResult } from "./matcher.js";
19
+ import { MouseMatcher } from "./mouse-matcher.js";
20
+ import { PasteMatcher } from "./paste-matcher.js";
21
+ import { TextMatcher } from "./text-matcher.js";
22
+ /** Duration in ms to wait for a follow-up character after a lone ESC. */
23
+ const ESC_TIMEOUT_MS = 50;
24
+ export class InputProcessor {
25
+ matchers;
26
+ escapeMatcher;
27
+ emitter;
28
+ /** Which matchers are still active (Partial or not yet tried) for the current sequence. */
29
+ active;
30
+ /** Timer for the escape key timeout. */
31
+ escTimer = null;
32
+ constructor(emitter) {
33
+ this.emitter = emitter;
34
+ this.escapeMatcher = new EscapeMatcher();
35
+ // Priority order — first matcher to complete wins.
36
+ this.matchers = [
37
+ new PasteMatcher(),
38
+ new MouseMatcher(),
39
+ this.escapeMatcher,
40
+ new TextMatcher(),
41
+ ];
42
+ this.active = this.matchers.map(() => true);
43
+ }
44
+ /**
45
+ * Feed a chunk of raw stdin data into the processor.
46
+ * Characters are dispatched one at a time through the matcher chain.
47
+ */
48
+ feed(data) {
49
+ for (let i = 0; i < data.length; i++) {
50
+ this.feedChar(data[i]);
51
+ }
52
+ }
53
+ /** Destroy timers and clean up. */
54
+ destroy() {
55
+ this.clearEscTimer();
56
+ }
57
+ feedChar(char) {
58
+ // Cancel any pending ESC timeout — we got more input.
59
+ this.clearEscTimer();
60
+ let completed = -1;
61
+ // Feed the character to ALL active matchers in priority order.
62
+ for (let i = 0; i < this.matchers.length; i++) {
63
+ if (!this.active[i])
64
+ continue;
65
+ const result = this.matchers[i].append(char);
66
+ if (result === MatchResult.Complete) {
67
+ completed = i;
68
+ break; // Highest-priority complete wins
69
+ }
70
+ if (result === MatchResult.NoMatch) {
71
+ this.matchers[i].reset();
72
+ this.active[i] = false;
73
+ }
74
+ // Partial: matcher stays active
75
+ }
76
+ if (completed >= 0) {
77
+ const ev = this.matchers[completed].flush();
78
+ // Reset all matchers EXCEPT the completed one (it manages its own state,
79
+ // e.g. EscapeMatcher stays in GotEsc after double-ESC).
80
+ for (let i = 0; i < this.matchers.length; i++) {
81
+ if (i !== completed) {
82
+ this.matchers[i].reset();
83
+ }
84
+ this.active[i] = true;
85
+ }
86
+ if (ev) {
87
+ this.emit(ev);
88
+ }
89
+ this.scheduleEscTimeoutIfNeeded();
90
+ return;
91
+ }
92
+ // Check if any matcher is still active (in Partial state).
93
+ const anyActive = this.active.some((a) => a);
94
+ if (!anyActive) {
95
+ // No matcher claimed this sequence. Reset all for next input.
96
+ this.resetAll();
97
+ return;
98
+ }
99
+ // Some matchers still in Partial state — check for ESC timeout.
100
+ this.scheduleEscTimeoutIfNeeded();
101
+ }
102
+ /** Reset all matchers and re-activate them. */
103
+ resetAll() {
104
+ for (let i = 0; i < this.matchers.length; i++) {
105
+ this.matchers[i].reset();
106
+ this.active[i] = true;
107
+ }
108
+ }
109
+ /**
110
+ * If the escape matcher is active and holding a partial \x1b,
111
+ * schedule a timeout to emit the escape key event.
112
+ */
113
+ scheduleEscTimeoutIfNeeded() {
114
+ const escIdx = this.matchers.indexOf(this.escapeMatcher);
115
+ if (escIdx < 0 || !this.active[escIdx])
116
+ return;
117
+ this.escTimer = setTimeout(() => {
118
+ this.escTimer = null;
119
+ const ev = this.escapeMatcher.flushEscapeTimeout();
120
+ if (ev) {
121
+ this.resetAll();
122
+ this.emit(ev);
123
+ }
124
+ }, ESC_TIMEOUT_MS);
125
+ }
126
+ clearEscTimer() {
127
+ if (this.escTimer !== null) {
128
+ clearTimeout(this.escTimer);
129
+ this.escTimer = null;
130
+ }
131
+ }
132
+ emit(event) {
133
+ this.emitter.emit("input", event);
134
+ }
135
+ }
136
+ // ── Convenience factory ───────────────────────────────────────────────
137
+ /**
138
+ * Create an InputProcessor wired to a fresh EventEmitter.
139
+ * Listen on the returned emitter's 'input' event to receive InputEvents.
140
+ */
141
+ export function createInputProcessor() {
142
+ const events = new EventEmitter();
143
+ const processor = new InputProcessor(events);
144
+ return { processor, events };
145
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Functions to enable and disable raw terminal mode on stdin.
3
+ */
4
+ /**
5
+ * Enable raw mode: disables line buffering and echo so we receive
6
+ * every keystroke as it arrives.
7
+ */
8
+ export declare function enableRawMode(): void;
9
+ /**
10
+ * Disable raw mode: restores normal line-buffered terminal input
11
+ * and pauses stdin so the process can exit cleanly.
12
+ */
13
+ export declare function disableRawMode(): void;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Functions to enable and disable raw terminal mode on stdin.
3
+ */
4
+ /**
5
+ * Enable raw mode: disables line buffering and echo so we receive
6
+ * every keystroke as it arrives.
7
+ */
8
+ export function enableRawMode() {
9
+ if (process.stdin.isTTY) {
10
+ process.stdin.setRawMode(true);
11
+ }
12
+ process.stdin.resume();
13
+ process.stdin.setEncoding("utf8");
14
+ }
15
+ /**
16
+ * Disable raw mode: restores normal line-buffered terminal input
17
+ * and pauses stdin so the process can exit cleanly.
18
+ */
19
+ export function disableRawMode() {
20
+ if (process.stdin.isTTY) {
21
+ process.stdin.setRawMode(false);
22
+ }
23
+ process.stdin.pause();
24
+ }