@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.
- package/dist/__tests__/ansi.test.js +332 -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/ansi/win32-console.d.ts +28 -0
- package/dist/ansi/win32-console.js +122 -0
- package/dist/app.d.ts +4 -0
- package/dist/app.js +34 -28
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/input/mouse-matcher.d.ts +21 -3
- package/dist/input/mouse-matcher.js +123 -30
- package/package.json +4 -1
package/dist/ansi/esc.js
CHANGED
|
@@ -74,12 +74,94 @@ export const eraseLine = `${ESC}2K`;
|
|
|
74
74
|
export const bracketedPasteOn = `${ESC}?2004h`;
|
|
75
75
|
/** Disable bracketed paste mode. */
|
|
76
76
|
export const bracketedPasteOff = `${ESC}?2004l`;
|
|
77
|
-
/**
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Enable mouse tracking with all supported reporting modes.
|
|
79
|
+
*
|
|
80
|
+
* Modes enabled (in order):
|
|
81
|
+
* ?1000h — Normal/VT200 click tracking (press + release)
|
|
82
|
+
* ?1003h — Any-event tracking (all mouse movement)
|
|
83
|
+
* ?1005h — UTF-8 coordinate encoding (extends X10 beyond col/row 223)
|
|
84
|
+
* ?1006h — SGR extended coordinates (";"-delimited decimals, M/m terminator)
|
|
85
|
+
* ?1015h — URXVT extended coordinates (decimal params, no "<" prefix)
|
|
86
|
+
* ?1016h — SGR-Pixels (same wire format as SGR, pixel coordinates)
|
|
87
|
+
*
|
|
88
|
+
* Terminals pick the highest mode they support. SGR is preferred by most
|
|
89
|
+
* modern terminals; URXVT and UTF-8 provide fallback for older ones.
|
|
90
|
+
*/
|
|
91
|
+
export const mouseTrackingOn = `${ESC}?1000h${ESC}?1003h${ESC}?1005h${ESC}?1006h${ESC}?1015h${ESC}?1016h`;
|
|
92
|
+
/**
|
|
93
|
+
* Disable all mouse tracking modes (reverse order of enable).
|
|
94
|
+
*/
|
|
95
|
+
export const mouseTrackingOff = `${ESC}?1016l${ESC}?1015l${ESC}?1006l${ESC}?1005l${ESC}?1003l${ESC}?1000l`;
|
|
81
96
|
// ── Window ──────────────────────────────────────────────────────────
|
|
82
97
|
/** Set the terminal window title. */
|
|
83
98
|
export function setTitle(title) {
|
|
84
99
|
return `${OSC}0;${title}\x07`;
|
|
85
100
|
}
|
|
101
|
+
// ── Environment-aware init/restore ─────────────────────────────────
|
|
102
|
+
/**
|
|
103
|
+
* Mouse tracking sequence tailored to detected capabilities.
|
|
104
|
+
*
|
|
105
|
+
* When the terminal supports SGR, request all six modes so the terminal
|
|
106
|
+
* picks the highest one it handles. When SGR is not available (e.g. GNU
|
|
107
|
+
* screen), fall back to normal + any-event tracking only — UTF-8 and
|
|
108
|
+
* URXVT modes could confuse terminals that don't understand them.
|
|
109
|
+
*/
|
|
110
|
+
export function mouseOn(caps) {
|
|
111
|
+
if (!caps.mouse)
|
|
112
|
+
return "";
|
|
113
|
+
if (caps.sgrMouse)
|
|
114
|
+
return mouseTrackingOn;
|
|
115
|
+
// Minimal: normal click + any-event only
|
|
116
|
+
return `${ESC}?1000h${ESC}?1003h`;
|
|
117
|
+
}
|
|
118
|
+
/** Matching disable sequence for mouseOn(). */
|
|
119
|
+
export function mouseOff(caps) {
|
|
120
|
+
if (!caps.mouse)
|
|
121
|
+
return "";
|
|
122
|
+
if (caps.sgrMouse)
|
|
123
|
+
return mouseTrackingOff;
|
|
124
|
+
return `${ESC}?1003l${ESC}?1000l`;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Build the full terminal preparation sequence for the given environment.
|
|
128
|
+
*
|
|
129
|
+
* @param caps - Detected terminal capabilities
|
|
130
|
+
* @param opts - Which optional features the app requested
|
|
131
|
+
*/
|
|
132
|
+
export function initSequence(caps, opts) {
|
|
133
|
+
if (!caps.isTTY)
|
|
134
|
+
return "";
|
|
135
|
+
let seq = "";
|
|
136
|
+
if (opts.alternateScreen && caps.alternateScreen) {
|
|
137
|
+
seq += alternateScreenOn;
|
|
138
|
+
}
|
|
139
|
+
seq += hideCursor;
|
|
140
|
+
if (caps.bracketedPaste) {
|
|
141
|
+
seq += bracketedPasteOn;
|
|
142
|
+
}
|
|
143
|
+
if (opts.mouse) {
|
|
144
|
+
seq += mouseOn(caps);
|
|
145
|
+
}
|
|
146
|
+
seq += clearScreen;
|
|
147
|
+
return seq;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Build the full terminal restore sequence (mirror of initSequence).
|
|
151
|
+
*/
|
|
152
|
+
export function restoreSequence(caps, opts) {
|
|
153
|
+
if (!caps.isTTY)
|
|
154
|
+
return "";
|
|
155
|
+
let seq = reset;
|
|
156
|
+
if (opts.mouse) {
|
|
157
|
+
seq += mouseOff(caps);
|
|
158
|
+
}
|
|
159
|
+
if (caps.bracketedPaste) {
|
|
160
|
+
seq += bracketedPasteOff;
|
|
161
|
+
}
|
|
162
|
+
seq += showCursor;
|
|
163
|
+
if (opts.alternateScreen && caps.alternateScreen) {
|
|
164
|
+
seq += alternateScreenOff;
|
|
165
|
+
}
|
|
166
|
+
return seq;
|
|
167
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
export interface TerminalCaps {
|
|
9
|
+
/** Terminal is a TTY (not piped). */
|
|
10
|
+
isTTY: boolean;
|
|
11
|
+
/** Supports alternate screen buffer (?1049h). */
|
|
12
|
+
alternateScreen: boolean;
|
|
13
|
+
/** Supports bracketed paste mode (?2004h). */
|
|
14
|
+
bracketedPaste: boolean;
|
|
15
|
+
/** Supports escape-based mouse tracking (?1000h and above). */
|
|
16
|
+
mouse: boolean;
|
|
17
|
+
/** Supports SGR extended mouse encoding (?1006h). */
|
|
18
|
+
sgrMouse: boolean;
|
|
19
|
+
/** Supports truecolor (24-bit) SGR sequences. */
|
|
20
|
+
truecolor: boolean;
|
|
21
|
+
/** Supports 256-color SGR sequences. */
|
|
22
|
+
color256: boolean;
|
|
23
|
+
/** Detected terminal name (for diagnostics). */
|
|
24
|
+
name: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Detect terminal capabilities from the current environment.
|
|
28
|
+
*
|
|
29
|
+
* On Windows the main differentiator is whether we're running under
|
|
30
|
+
* Windows Terminal / ConPTY (full VT support) or legacy conhost
|
|
31
|
+
* (very limited escape handling). On Unix the TERM variable and
|
|
32
|
+
* TERM_PROGRAM give us enough signal.
|
|
33
|
+
*/
|
|
34
|
+
export declare function detectTerminal(): TerminalCaps;
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Win32 Console Mode — enables mouse input on Windows terminals.
|
|
3
|
+
*
|
|
4
|
+
* Node.js `setRawMode(true)` enables ENABLE_VIRTUAL_TERMINAL_INPUT but
|
|
5
|
+
* does NOT disable ENABLE_QUICK_EDIT_MODE (which intercepts mouse clicks
|
|
6
|
+
* for text selection) or enable ENABLE_MOUSE_INPUT. This module uses
|
|
7
|
+
* koffi to call the Win32 API directly and set the correct flags.
|
|
8
|
+
*
|
|
9
|
+
* Only loaded on win32 — no-ops on other platforms.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Configure the Windows console for mouse input.
|
|
13
|
+
*
|
|
14
|
+
* Disables Quick Edit Mode (which swallows mouse clicks) and enables
|
|
15
|
+
* ENABLE_MOUSE_INPUT + ENABLE_EXTENDED_FLAGS + ENABLE_WINDOW_INPUT.
|
|
16
|
+
* Saves the original mode so it can be restored later.
|
|
17
|
+
*
|
|
18
|
+
* No-op on non-Windows platforms or if koffi is not available.
|
|
19
|
+
* Returns true if the mode was successfully changed.
|
|
20
|
+
*/
|
|
21
|
+
export declare function enableWin32Mouse(): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Restore the original Windows console mode saved by enableWin32Mouse().
|
|
24
|
+
*
|
|
25
|
+
* No-op if enableWin32Mouse() was never called or failed.
|
|
26
|
+
* Returns true if the mode was successfully restored.
|
|
27
|
+
*/
|
|
28
|
+
export declare function restoreWin32Console(): boolean;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Win32 Console Mode — enables mouse input on Windows terminals.
|
|
3
|
+
*
|
|
4
|
+
* Node.js `setRawMode(true)` enables ENABLE_VIRTUAL_TERMINAL_INPUT but
|
|
5
|
+
* does NOT disable ENABLE_QUICK_EDIT_MODE (which intercepts mouse clicks
|
|
6
|
+
* for text selection) or enable ENABLE_MOUSE_INPUT. This module uses
|
|
7
|
+
* koffi to call the Win32 API directly and set the correct flags.
|
|
8
|
+
*
|
|
9
|
+
* Only loaded on win32 — no-ops on other platforms.
|
|
10
|
+
*/
|
|
11
|
+
import { createRequire } from "node:module";
|
|
12
|
+
// ── Console mode flag constants ─────────────────────────────────────
|
|
13
|
+
const ENABLE_PROCESSED_INPUT = 0x0001;
|
|
14
|
+
const ENABLE_LINE_INPUT = 0x0002;
|
|
15
|
+
const ENABLE_ECHO_INPUT = 0x0004;
|
|
16
|
+
const ENABLE_WINDOW_INPUT = 0x0008;
|
|
17
|
+
const ENABLE_MOUSE_INPUT = 0x0010;
|
|
18
|
+
const ENABLE_QUICK_EDIT_MODE = 0x0040;
|
|
19
|
+
const ENABLE_EXTENDED_FLAGS = 0x0080;
|
|
20
|
+
const ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200;
|
|
21
|
+
const STD_INPUT_HANDLE = -10;
|
|
22
|
+
// ── State ───────────────────────────────────────────────────────────
|
|
23
|
+
let originalMode = null;
|
|
24
|
+
let _kernel32;
|
|
25
|
+
function getKernel32() {
|
|
26
|
+
if (_kernel32 !== undefined)
|
|
27
|
+
return _kernel32;
|
|
28
|
+
try {
|
|
29
|
+
// koffi is an optional native dependency — dynamic require so the
|
|
30
|
+
// module loads cleanly even when koffi is absent.
|
|
31
|
+
const require = createRequire(import.meta.url);
|
|
32
|
+
const koffi = require("koffi");
|
|
33
|
+
const lib = koffi.load("kernel32.dll");
|
|
34
|
+
_kernel32 = {
|
|
35
|
+
GetStdHandle: lib.func("void* __stdcall GetStdHandle(int nStdHandle)"),
|
|
36
|
+
GetConsoleMode: lib.func("bool __stdcall GetConsoleMode(void* hConsoleHandle, _Out_ uint32_t* lpMode)"),
|
|
37
|
+
SetConsoleMode: lib.func("bool __stdcall SetConsoleMode(void* hConsoleHandle, uint32_t dwMode)"),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
_kernel32 = null;
|
|
42
|
+
}
|
|
43
|
+
return _kernel32;
|
|
44
|
+
}
|
|
45
|
+
// ── Public API ──────────────────────────────────────────────────────
|
|
46
|
+
/**
|
|
47
|
+
* Configure the Windows console for mouse input.
|
|
48
|
+
*
|
|
49
|
+
* Disables Quick Edit Mode (which swallows mouse clicks) and enables
|
|
50
|
+
* ENABLE_MOUSE_INPUT + ENABLE_EXTENDED_FLAGS + ENABLE_WINDOW_INPUT.
|
|
51
|
+
* Saves the original mode so it can be restored later.
|
|
52
|
+
*
|
|
53
|
+
* No-op on non-Windows platforms or if koffi is not available.
|
|
54
|
+
* Returns true if the mode was successfully changed.
|
|
55
|
+
*/
|
|
56
|
+
export function enableWin32Mouse() {
|
|
57
|
+
if (process.platform !== "win32")
|
|
58
|
+
return false;
|
|
59
|
+
const k32 = getKernel32();
|
|
60
|
+
if (!k32)
|
|
61
|
+
return false;
|
|
62
|
+
try {
|
|
63
|
+
const handle = k32.GetStdHandle(STD_INPUT_HANDLE);
|
|
64
|
+
if (!handle)
|
|
65
|
+
return false;
|
|
66
|
+
// Read current mode
|
|
67
|
+
const modeBuffer = Buffer.alloc(4);
|
|
68
|
+
if (!k32.GetConsoleMode(handle, modeBuffer))
|
|
69
|
+
return false;
|
|
70
|
+
originalMode = modeBuffer.readUInt32LE(0);
|
|
71
|
+
// Build new mode:
|
|
72
|
+
// - Keep ENABLE_VIRTUAL_TERMINAL_INPUT (set by Node raw mode)
|
|
73
|
+
// - Add ENABLE_MOUSE_INPUT + ENABLE_WINDOW_INPUT + ENABLE_EXTENDED_FLAGS
|
|
74
|
+
// - Remove ENABLE_QUICK_EDIT_MODE
|
|
75
|
+
// - Remove line/echo/processed (already cleared by raw mode)
|
|
76
|
+
let newMode = originalMode;
|
|
77
|
+
newMode |= ENABLE_MOUSE_INPUT;
|
|
78
|
+
newMode |= ENABLE_WINDOW_INPUT;
|
|
79
|
+
newMode |= ENABLE_EXTENDED_FLAGS;
|
|
80
|
+
newMode &= ~ENABLE_QUICK_EDIT_MODE;
|
|
81
|
+
newMode &= ~ENABLE_LINE_INPUT;
|
|
82
|
+
newMode &= ~ENABLE_ECHO_INPUT;
|
|
83
|
+
newMode &= ~ENABLE_PROCESSED_INPUT;
|
|
84
|
+
// Preserve VT input if it was set
|
|
85
|
+
if (originalMode & ENABLE_VIRTUAL_TERMINAL_INPUT) {
|
|
86
|
+
newMode |= ENABLE_VIRTUAL_TERMINAL_INPUT;
|
|
87
|
+
}
|
|
88
|
+
return k32.SetConsoleMode(handle, newMode);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Restore the original Windows console mode saved by enableWin32Mouse().
|
|
96
|
+
*
|
|
97
|
+
* No-op if enableWin32Mouse() was never called or failed.
|
|
98
|
+
* Returns true if the mode was successfully restored.
|
|
99
|
+
*/
|
|
100
|
+
export function restoreWin32Console() {
|
|
101
|
+
if (process.platform !== "win32" || originalMode === null)
|
|
102
|
+
return false;
|
|
103
|
+
const k32 = getKernel32();
|
|
104
|
+
if (!k32) {
|
|
105
|
+
originalMode = null;
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const handle = k32.GetStdHandle(STD_INPUT_HANDLE);
|
|
110
|
+
if (!handle) {
|
|
111
|
+
originalMode = null;
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
const result = k32.SetConsoleMode(handle, originalMode);
|
|
115
|
+
originalMode = null;
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
originalMode = null;
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
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,8 @@
|
|
|
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";
|
|
11
|
+
import { enableWin32Mouse, restoreWin32Console } from "./ansi/win32-console.js";
|
|
10
12
|
import { DrawingContext } from "./drawing/context.js";
|
|
11
13
|
import { createInputProcessor } from "./input/processor.js";
|
|
12
14
|
import { disableRawMode, enableRawMode } from "./input/raw-mode.js";
|
|
@@ -19,6 +21,7 @@ export class App {
|
|
|
19
21
|
_alternateScreen;
|
|
20
22
|
_mouse;
|
|
21
23
|
_title;
|
|
24
|
+
_caps;
|
|
22
25
|
// Subsystems — created during run()
|
|
23
26
|
_output;
|
|
24
27
|
_buffer;
|
|
@@ -34,11 +37,16 @@ export class App {
|
|
|
34
37
|
_resizeListener = null;
|
|
35
38
|
_sigintListener = null;
|
|
36
39
|
_renderScheduled = false;
|
|
40
|
+
/** Detected terminal capabilities (read-only, for diagnostics). */
|
|
41
|
+
get caps() {
|
|
42
|
+
return this._caps;
|
|
43
|
+
}
|
|
37
44
|
constructor(options) {
|
|
38
45
|
this.root = options.root;
|
|
39
46
|
this._alternateScreen = options.alternateScreen ?? true;
|
|
40
47
|
this._mouse = options.mouse ?? false;
|
|
41
48
|
this._title = options.title;
|
|
49
|
+
this._caps = detectTerminal();
|
|
42
50
|
}
|
|
43
51
|
// ── Public API ───────────────────────────────────────────────────
|
|
44
52
|
/**
|
|
@@ -90,54 +98,50 @@ export class App {
|
|
|
90
98
|
const stdout = process.stdout;
|
|
91
99
|
// 1. Enable raw mode
|
|
92
100
|
enableRawMode();
|
|
93
|
-
// 2.
|
|
101
|
+
// 2. On Windows, configure console mode for mouse input
|
|
102
|
+
// (must happen after raw mode so we modify the right base flags)
|
|
103
|
+
if (this._mouse) {
|
|
104
|
+
enableWin32Mouse();
|
|
105
|
+
}
|
|
106
|
+
// 3. Create ANSI output
|
|
94
107
|
this._output = new AnsiOutput(stdout);
|
|
95
|
-
//
|
|
108
|
+
// 4. Prepare terminal (custom sequence instead of prepareTerminal()
|
|
96
109
|
// so we can conditionally enable mouse tracking)
|
|
97
110
|
this._prepareTerminal();
|
|
98
|
-
//
|
|
111
|
+
// 5. Set terminal title
|
|
99
112
|
if (this._title) {
|
|
100
113
|
stdout.write(esc.setTitle(this._title));
|
|
101
114
|
}
|
|
102
|
-
//
|
|
115
|
+
// 6. Create pixel buffer at terminal dimensions
|
|
103
116
|
const cols = stdout.columns || 80;
|
|
104
117
|
const rows = stdout.rows || 24;
|
|
105
118
|
this._createRenderPipeline(cols, rows);
|
|
106
|
-
//
|
|
119
|
+
// 7. Wire up input
|
|
107
120
|
this._setupInput();
|
|
108
|
-
//
|
|
121
|
+
// 8. Wire up resize
|
|
109
122
|
this._resizeListener = () => this._handleResize();
|
|
110
123
|
stdout.on("resize", this._resizeListener);
|
|
111
|
-
//
|
|
124
|
+
// 9. SIGINT fallback
|
|
112
125
|
this._sigintListener = () => this.stop();
|
|
113
126
|
process.on("SIGINT", this._sigintListener);
|
|
114
127
|
}
|
|
115
128
|
_prepareTerminal() {
|
|
116
129
|
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);
|
|
130
|
+
const seq = esc.initSequence(this._caps, {
|
|
131
|
+
alternateScreen: this._alternateScreen,
|
|
132
|
+
mouse: this._mouse,
|
|
133
|
+
});
|
|
134
|
+
if (seq)
|
|
135
|
+
stream.write(seq);
|
|
128
136
|
}
|
|
129
137
|
_restoreTerminal() {
|
|
130
138
|
const stream = process.stdout;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
seq
|
|
136
|
-
|
|
137
|
-
if (this._alternateScreen) {
|
|
138
|
-
seq += esc.alternateScreenOff;
|
|
139
|
-
}
|
|
140
|
-
stream.write(seq);
|
|
139
|
+
const seq = esc.restoreSequence(this._caps, {
|
|
140
|
+
alternateScreen: this._alternateScreen,
|
|
141
|
+
mouse: this._mouse,
|
|
142
|
+
});
|
|
143
|
+
if (seq)
|
|
144
|
+
stream.write(seq);
|
|
141
145
|
}
|
|
142
146
|
_createRenderPipeline(cols, rows) {
|
|
143
147
|
this._buffer = new PixelBuffer(cols, rows);
|
|
@@ -296,6 +300,8 @@ export class App {
|
|
|
296
300
|
}
|
|
297
301
|
// Restore terminal
|
|
298
302
|
this._restoreTerminal();
|
|
303
|
+
// Restore Win32 console mode (before disabling raw mode)
|
|
304
|
+
restoreWin32Console();
|
|
299
305
|
// Disable raw mode
|
|
300
306
|
disableRawMode();
|
|
301
307
|
// Resolve the run() promise
|
package/dist/index.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ 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 { detectTerminal, type TerminalCaps, } from "./ansi/terminal-env.js";
|
|
20
|
+
export { enableWin32Mouse, restoreWin32Console, } from "./ansi/win32-console.js";
|
|
19
21
|
export { DirtyRegions, DirtySnapshot } from "./render/regions.js";
|
|
20
22
|
export { RenderTarget } from "./render/render-target.js";
|
|
21
23
|
export { EscapeMatcher } from "./input/escape-matcher.js";
|
package/dist/index.js
CHANGED
|
@@ -23,6 +23,8 @@ 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";
|
|
27
|
+
export { enableWin32Mouse, restoreWin32Console, } from "./ansi/win32-console.js";
|
|
26
28
|
// ── Render pipeline ─────────────────────────────────────────────────
|
|
27
29
|
export { DirtyRegions, DirtySnapshot } from "./render/regions.js";
|
|
28
30
|
export { RenderTarget } from "./render/render-target.js";
|