@teammates/consolonia 0.6.3 → 0.7.1
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.
- package/dist/__tests__/ansi.test.js +211 -5
- package/dist/__tests__/input.test.js +157 -0
- package/dist/ansi/esc.d.ts +48 -4
- package/dist/ansi/esc.js +86 -4
- package/dist/ansi/terminal-env.d.ts +34 -0
- package/dist/ansi/terminal-env.js +206 -0
- package/dist/app.d.ts +4 -0
- package/dist/app.js +19 -21
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -0
- package/dist/input/mouse-matcher.d.ts +21 -3
- package/dist/input/mouse-matcher.js +123 -30
- package/dist/widgets/chat-view.d.ts +28 -35
- package/dist/widgets/chat-view.js +198 -251
- package/dist/widgets/feed-store.d.ts +58 -0
- package/dist/widgets/feed-store.js +69 -0
- package/dist/widgets/virtual-list.d.ts +85 -0
- package/dist/widgets/virtual-list.js +262 -0
- package/package.json +1 -1
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal environment detection.
|
|
3
|
+
*
|
|
4
|
+
* Probes environment variables and process state to determine which
|
|
5
|
+
* terminal capabilities are available. Used by App to send only the
|
|
6
|
+
* escape sequences the host terminal actually supports.
|
|
7
|
+
*/
|
|
8
|
+
// ── Detection ───────────────────────────────────────────────────────
|
|
9
|
+
/**
|
|
10
|
+
* Detect terminal capabilities from the current environment.
|
|
11
|
+
*
|
|
12
|
+
* On Windows the main differentiator is whether we're running under
|
|
13
|
+
* Windows Terminal / ConPTY (full VT support) or legacy conhost
|
|
14
|
+
* (very limited escape handling). On Unix the TERM variable and
|
|
15
|
+
* TERM_PROGRAM give us enough signal.
|
|
16
|
+
*/
|
|
17
|
+
export function detectTerminal() {
|
|
18
|
+
const env = process.env;
|
|
19
|
+
const isTTY = !!process.stdout.isTTY;
|
|
20
|
+
if (!isTTY) {
|
|
21
|
+
return {
|
|
22
|
+
isTTY: false,
|
|
23
|
+
alternateScreen: false,
|
|
24
|
+
bracketedPaste: false,
|
|
25
|
+
mouse: false,
|
|
26
|
+
sgrMouse: false,
|
|
27
|
+
truecolor: false,
|
|
28
|
+
color256: false,
|
|
29
|
+
name: "pipe",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
// ── Windows ─────────────────────────────────────────────────────
|
|
33
|
+
if (process.platform === "win32") {
|
|
34
|
+
// Windows Terminal sets WT_SESSION
|
|
35
|
+
if (env.WT_SESSION) {
|
|
36
|
+
return {
|
|
37
|
+
isTTY: true,
|
|
38
|
+
alternateScreen: true,
|
|
39
|
+
bracketedPaste: true,
|
|
40
|
+
mouse: true,
|
|
41
|
+
sgrMouse: true,
|
|
42
|
+
truecolor: true,
|
|
43
|
+
color256: true,
|
|
44
|
+
name: "windows-terminal",
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// VS Code's integrated terminal (xterm.js)
|
|
48
|
+
if (env.TERM_PROGRAM === "vscode") {
|
|
49
|
+
return {
|
|
50
|
+
isTTY: true,
|
|
51
|
+
alternateScreen: true,
|
|
52
|
+
bracketedPaste: true,
|
|
53
|
+
mouse: true,
|
|
54
|
+
sgrMouse: true,
|
|
55
|
+
truecolor: true,
|
|
56
|
+
color256: true,
|
|
57
|
+
name: "vscode",
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// ConEmu / Cmder
|
|
61
|
+
if (env.ConEmuPID) {
|
|
62
|
+
return {
|
|
63
|
+
isTTY: true,
|
|
64
|
+
alternateScreen: true,
|
|
65
|
+
bracketedPaste: true,
|
|
66
|
+
mouse: true,
|
|
67
|
+
sgrMouse: true,
|
|
68
|
+
truecolor: true,
|
|
69
|
+
color256: true,
|
|
70
|
+
name: "conemu",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// Mintty (Git Bash) — sets TERM=xterm*
|
|
74
|
+
if (env.TERM?.startsWith("xterm") && env.MSYSTEM) {
|
|
75
|
+
return {
|
|
76
|
+
isTTY: true,
|
|
77
|
+
alternateScreen: true,
|
|
78
|
+
bracketedPaste: true,
|
|
79
|
+
mouse: true,
|
|
80
|
+
sgrMouse: true,
|
|
81
|
+
truecolor: true,
|
|
82
|
+
color256: true,
|
|
83
|
+
name: "mintty",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
// Fallback: modern Windows 10+ conhost with ConPTY has decent VT
|
|
87
|
+
// support, but mouse tracking can be unreliable. Enable everything
|
|
88
|
+
// and let the terminal silently ignore what it doesn't support.
|
|
89
|
+
return {
|
|
90
|
+
isTTY: true,
|
|
91
|
+
alternateScreen: true,
|
|
92
|
+
bracketedPaste: true,
|
|
93
|
+
mouse: true,
|
|
94
|
+
sgrMouse: true,
|
|
95
|
+
truecolor: true,
|
|
96
|
+
color256: true,
|
|
97
|
+
name: "conhost",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// ── Unix / macOS ────────────────────────────────────────────────
|
|
101
|
+
const term = env.TERM ?? "";
|
|
102
|
+
const termProgram = env.TERM_PROGRAM ?? "";
|
|
103
|
+
// tmux — full VT support, passes through SGR mouse
|
|
104
|
+
if (env.TMUX || term.startsWith("tmux") || term === "screen-256color") {
|
|
105
|
+
return {
|
|
106
|
+
isTTY: true,
|
|
107
|
+
alternateScreen: true,
|
|
108
|
+
bracketedPaste: true,
|
|
109
|
+
mouse: true,
|
|
110
|
+
sgrMouse: true,
|
|
111
|
+
truecolor: !!env.COLORTERM || term.includes("256color"),
|
|
112
|
+
color256: true,
|
|
113
|
+
name: "tmux",
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
// GNU screen — limited mouse support, no SGR
|
|
117
|
+
if (term === "screen" && !env.TMUX) {
|
|
118
|
+
return {
|
|
119
|
+
isTTY: true,
|
|
120
|
+
alternateScreen: true,
|
|
121
|
+
bracketedPaste: false,
|
|
122
|
+
mouse: true,
|
|
123
|
+
sgrMouse: false,
|
|
124
|
+
truecolor: false,
|
|
125
|
+
color256: false,
|
|
126
|
+
name: "screen",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
// iTerm2
|
|
130
|
+
if (termProgram === "iTerm.app" || env.ITERM_SESSION_ID) {
|
|
131
|
+
return {
|
|
132
|
+
isTTY: true,
|
|
133
|
+
alternateScreen: true,
|
|
134
|
+
bracketedPaste: true,
|
|
135
|
+
mouse: true,
|
|
136
|
+
sgrMouse: true,
|
|
137
|
+
truecolor: true,
|
|
138
|
+
color256: true,
|
|
139
|
+
name: "iterm2",
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// VS Code integrated terminal (macOS/Linux)
|
|
143
|
+
if (termProgram === "vscode") {
|
|
144
|
+
return {
|
|
145
|
+
isTTY: true,
|
|
146
|
+
alternateScreen: true,
|
|
147
|
+
bracketedPaste: true,
|
|
148
|
+
mouse: true,
|
|
149
|
+
sgrMouse: true,
|
|
150
|
+
truecolor: true,
|
|
151
|
+
color256: true,
|
|
152
|
+
name: "vscode",
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
// Alacritty
|
|
156
|
+
if (termProgram === "Alacritty" || term === "alacritty") {
|
|
157
|
+
return {
|
|
158
|
+
isTTY: true,
|
|
159
|
+
alternateScreen: true,
|
|
160
|
+
bracketedPaste: true,
|
|
161
|
+
mouse: true,
|
|
162
|
+
sgrMouse: true,
|
|
163
|
+
truecolor: true,
|
|
164
|
+
color256: true,
|
|
165
|
+
name: "alacritty",
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
// xterm-compatible (most Linux/macOS terminals)
|
|
169
|
+
if (term.startsWith("xterm") || term.includes("256color")) {
|
|
170
|
+
const hasTruecolor = env.COLORTERM === "truecolor" || env.COLORTERM === "24bit";
|
|
171
|
+
return {
|
|
172
|
+
isTTY: true,
|
|
173
|
+
alternateScreen: true,
|
|
174
|
+
bracketedPaste: true,
|
|
175
|
+
mouse: true,
|
|
176
|
+
sgrMouse: true,
|
|
177
|
+
truecolor: hasTruecolor,
|
|
178
|
+
color256: true,
|
|
179
|
+
name: term,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
// Dumb terminal — absolute minimum
|
|
183
|
+
if (term === "dumb" || !term) {
|
|
184
|
+
return {
|
|
185
|
+
isTTY: true,
|
|
186
|
+
alternateScreen: false,
|
|
187
|
+
bracketedPaste: false,
|
|
188
|
+
mouse: false,
|
|
189
|
+
sgrMouse: false,
|
|
190
|
+
truecolor: false,
|
|
191
|
+
color256: false,
|
|
192
|
+
name: term || "unknown",
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
// Unknown but it is a TTY — enable common capabilities
|
|
196
|
+
return {
|
|
197
|
+
isTTY: true,
|
|
198
|
+
alternateScreen: true,
|
|
199
|
+
bracketedPaste: true,
|
|
200
|
+
mouse: true,
|
|
201
|
+
sgrMouse: false,
|
|
202
|
+
truecolor: false,
|
|
203
|
+
color256: true,
|
|
204
|
+
name: term,
|
|
205
|
+
};
|
|
206
|
+
}
|
package/dist/app.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* RenderTarget, DrawingContext, InputProcessor) and drives the
|
|
6
6
|
* measure → arrange → render loop in response to input and resize events.
|
|
7
7
|
*/
|
|
8
|
+
import { type TerminalCaps } from "./ansi/terminal-env.js";
|
|
8
9
|
import type { Control } from "./layout/control.js";
|
|
9
10
|
export interface AppOptions {
|
|
10
11
|
/** Root control to render. */
|
|
@@ -21,6 +22,7 @@ export declare class App {
|
|
|
21
22
|
private readonly _alternateScreen;
|
|
22
23
|
private readonly _mouse;
|
|
23
24
|
private readonly _title;
|
|
25
|
+
private readonly _caps;
|
|
24
26
|
private _output;
|
|
25
27
|
private _buffer;
|
|
26
28
|
private _dirtyRegions;
|
|
@@ -34,6 +36,8 @@ export declare class App {
|
|
|
34
36
|
private _resizeListener;
|
|
35
37
|
private _sigintListener;
|
|
36
38
|
private _renderScheduled;
|
|
39
|
+
/** Detected terminal capabilities (read-only, for diagnostics). */
|
|
40
|
+
get caps(): Readonly<TerminalCaps>;
|
|
37
41
|
constructor(options: AppOptions);
|
|
38
42
|
/**
|
|
39
43
|
* Start the app — enters raw mode, sets up terminal, runs the event
|
package/dist/app.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import * as esc from "./ansi/esc.js";
|
|
9
9
|
import { AnsiOutput } from "./ansi/output.js";
|
|
10
|
+
import { detectTerminal, } from "./ansi/terminal-env.js";
|
|
10
11
|
import { DrawingContext } from "./drawing/context.js";
|
|
11
12
|
import { createInputProcessor } from "./input/processor.js";
|
|
12
13
|
import { disableRawMode, enableRawMode } from "./input/raw-mode.js";
|
|
@@ -19,6 +20,7 @@ export class App {
|
|
|
19
20
|
_alternateScreen;
|
|
20
21
|
_mouse;
|
|
21
22
|
_title;
|
|
23
|
+
_caps;
|
|
22
24
|
// Subsystems — created during run()
|
|
23
25
|
_output;
|
|
24
26
|
_buffer;
|
|
@@ -34,11 +36,16 @@ export class App {
|
|
|
34
36
|
_resizeListener = null;
|
|
35
37
|
_sigintListener = null;
|
|
36
38
|
_renderScheduled = false;
|
|
39
|
+
/** Detected terminal capabilities (read-only, for diagnostics). */
|
|
40
|
+
get caps() {
|
|
41
|
+
return this._caps;
|
|
42
|
+
}
|
|
37
43
|
constructor(options) {
|
|
38
44
|
this.root = options.root;
|
|
39
45
|
this._alternateScreen = options.alternateScreen ?? true;
|
|
40
46
|
this._mouse = options.mouse ?? false;
|
|
41
47
|
this._title = options.title;
|
|
48
|
+
this._caps = detectTerminal();
|
|
42
49
|
}
|
|
43
50
|
// ── Public API ───────────────────────────────────────────────────
|
|
44
51
|
/**
|
|
@@ -114,30 +121,21 @@ export class App {
|
|
|
114
121
|
}
|
|
115
122
|
_prepareTerminal() {
|
|
116
123
|
const stream = process.stdout;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
seq
|
|
122
|
-
|
|
123
|
-
if (this._mouse) {
|
|
124
|
-
seq += esc.mouseTrackingOn;
|
|
125
|
-
}
|
|
126
|
-
seq += esc.clearScreen;
|
|
127
|
-
stream.write(seq);
|
|
124
|
+
const seq = esc.initSequence(this._caps, {
|
|
125
|
+
alternateScreen: this._alternateScreen,
|
|
126
|
+
mouse: this._mouse,
|
|
127
|
+
});
|
|
128
|
+
if (seq)
|
|
129
|
+
stream.write(seq);
|
|
128
130
|
}
|
|
129
131
|
_restoreTerminal() {
|
|
130
132
|
const stream = process.stdout;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
seq
|
|
136
|
-
|
|
137
|
-
if (this._alternateScreen) {
|
|
138
|
-
seq += esc.alternateScreenOff;
|
|
139
|
-
}
|
|
140
|
-
stream.write(seq);
|
|
133
|
+
const seq = esc.restoreSequence(this._caps, {
|
|
134
|
+
alternateScreen: this._alternateScreen,
|
|
135
|
+
mouse: this._mouse,
|
|
136
|
+
});
|
|
137
|
+
if (seq)
|
|
138
|
+
stream.write(seq);
|
|
141
139
|
}
|
|
142
140
|
_createRenderPipeline(cols, rows) {
|
|
143
141
|
this._buffer = new PixelBuffer(cols, rows);
|
package/dist/index.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export type { Constraint, Point, Rect, Size, } from "./layout/types.js";
|
|
|
16
16
|
export * as esc from "./ansi/esc.js";
|
|
17
17
|
export { AnsiOutput } from "./ansi/output.js";
|
|
18
18
|
export { stripAnsi, truncateAnsi, visibleLength, } from "./ansi/strip.js";
|
|
19
|
+
export { type TerminalCaps, detectTerminal, } from "./ansi/terminal-env.js";
|
|
19
20
|
export { DirtyRegions, DirtySnapshot } from "./render/regions.js";
|
|
20
21
|
export { RenderTarget } from "./render/render-target.js";
|
|
21
22
|
export { EscapeMatcher } from "./input/escape-matcher.js";
|
|
@@ -35,13 +36,15 @@ export { Control } from "./layout/control.js";
|
|
|
35
36
|
export { Row, type RowOptions } from "./layout/row.js";
|
|
36
37
|
export { Stack, type StackOptions } from "./layout/stack.js";
|
|
37
38
|
export { Border, type BorderOptions } from "./widgets/border.js";
|
|
38
|
-
export { ChatView, type ChatViewOptions, type DropdownItem, type FeedActionItem, } from "./widgets/chat-view.js";
|
|
39
|
+
export { ChatView, type ChatViewOptions, type DropdownItem, type FeedActionEntry, type FeedActionItem, type FeedItem, } from "./widgets/chat-view.js";
|
|
40
|
+
export { FeedStore } from "./widgets/feed-store.js";
|
|
39
41
|
export { Interview, type InterviewOptions, type InterviewQuestion, } from "./widgets/interview.js";
|
|
40
42
|
export { Panel, type PanelOptions } from "./widgets/panel.js";
|
|
41
43
|
export { ScrollView, type ScrollViewOptions } from "./widgets/scroll-view.js";
|
|
42
44
|
export { type StyledLine, StyledText, type StyledTextOptions, } from "./widgets/styled-text.js";
|
|
43
45
|
export { Text, type TextOptions } from "./widgets/text.js";
|
|
44
46
|
export { type DeleteSizer, type InputColorizer, TextInput, type TextInputOptions, } from "./widgets/text-input.js";
|
|
47
|
+
export { VirtualList, type VirtualListItem, type VirtualListOptions, } from "./widgets/virtual-list.js";
|
|
45
48
|
export { type MarkdownOptions, type MarkdownTheme, renderMarkdown, } from "./widgets/markdown.js";
|
|
46
49
|
export { DEFAULT_SYNTAX_THEME, getHighlighter, highlightLine, registerHighlighter, type SyntaxHighlighter, type SyntaxTheme, type SyntaxToken, type SyntaxTokenType, } from "./widgets/syntax.js";
|
|
47
50
|
export { concat, isStyledSpan, pen, type StyledSegment, type StyledSpan, spanLength, spanText, } from "./styled.js";
|
package/dist/index.js
CHANGED
|
@@ -23,6 +23,7 @@ export { charWidth, EMPTY_SYMBOL, isZeroWidth, stringDisplayWidth, sym, } from "
|
|
|
23
23
|
export * as esc from "./ansi/esc.js";
|
|
24
24
|
export { AnsiOutput } from "./ansi/output.js";
|
|
25
25
|
export { stripAnsi, truncateAnsi, visibleLength, } from "./ansi/strip.js";
|
|
26
|
+
export { detectTerminal, } from "./ansi/terminal-env.js";
|
|
26
27
|
// ── Render pipeline ─────────────────────────────────────────────────
|
|
27
28
|
export { DirtyRegions, DirtySnapshot } from "./render/regions.js";
|
|
28
29
|
export { RenderTarget } from "./render/render-target.js";
|
|
@@ -47,12 +48,14 @@ export { Stack } from "./layout/stack.js";
|
|
|
47
48
|
// ── Widgets ─────────────────────────────────────────────────────────
|
|
48
49
|
export { Border } from "./widgets/border.js";
|
|
49
50
|
export { ChatView, } from "./widgets/chat-view.js";
|
|
51
|
+
export { FeedStore } from "./widgets/feed-store.js";
|
|
50
52
|
export { Interview, } from "./widgets/interview.js";
|
|
51
53
|
export { Panel } from "./widgets/panel.js";
|
|
52
54
|
export { ScrollView } from "./widgets/scroll-view.js";
|
|
53
55
|
export { StyledText, } from "./widgets/styled-text.js";
|
|
54
56
|
export { Text } from "./widgets/text.js";
|
|
55
57
|
export { TextInput, } from "./widgets/text-input.js";
|
|
58
|
+
export { VirtualList, } from "./widgets/virtual-list.js";
|
|
56
59
|
// ── Markdown ─────────────────────────────────────────────────────────
|
|
57
60
|
export { renderMarkdown, } from "./widgets/markdown.js";
|
|
58
61
|
// ── Syntax highlighting ──────────────────────────────────────────────
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Parses
|
|
2
|
+
* Parses terminal mouse tracking sequences.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
2
|
+
* Parses terminal mouse tracking sequences.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
127
|
-
this.result
|
|
128
|
-
|
|
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) {
|