@teammates/consolonia 0.7.2 → 0.7.4
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 +0 -31
- package/dist/__tests__/pixel.test.js +38 -0
- package/dist/__tests__/styled.test.js +1 -1
- package/dist/app.js +7 -16
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/pixel/symbol.js +102 -36
- package/package.json +1 -4
- package/dist/ansi/win32-console.d.ts +0 -28
- package/dist/ansi/win32-console.js +0 -122
|
@@ -4,7 +4,6 @@ import * as esc from "../ansi/esc.js";
|
|
|
4
4
|
import { AnsiOutput } from "../ansi/output.js";
|
|
5
5
|
import { stripAnsi, truncateAnsi, visibleLength } from "../ansi/strip.js";
|
|
6
6
|
import { detectTerminal } from "../ansi/terminal-env.js";
|
|
7
|
-
import { enableWin32Mouse, restoreWin32Console, } from "../ansi/win32-console.js";
|
|
8
7
|
// ── Helpers ────────────────────────────────────────────────────────
|
|
9
8
|
const ESC = "\x1b[";
|
|
10
9
|
/** Build a mock writable stream that accumulates output to a string. */
|
|
@@ -815,33 +814,3 @@ describe("esc environment-aware sequences", () => {
|
|
|
815
814
|
});
|
|
816
815
|
});
|
|
817
816
|
});
|
|
818
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
819
|
-
// win32-console.ts
|
|
820
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
821
|
-
describe("win32-console", () => {
|
|
822
|
-
const originalPlatform = process.platform;
|
|
823
|
-
afterEach(() => {
|
|
824
|
-
Object.defineProperty(process, "platform", { value: originalPlatform });
|
|
825
|
-
});
|
|
826
|
-
describe("enableWin32Mouse", () => {
|
|
827
|
-
it("returns false on non-win32 platforms", () => {
|
|
828
|
-
Object.defineProperty(process, "platform", { value: "linux" });
|
|
829
|
-
expect(enableWin32Mouse()).toBe(false);
|
|
830
|
-
});
|
|
831
|
-
it("returns false on darwin", () => {
|
|
832
|
-
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
833
|
-
expect(enableWin32Mouse()).toBe(false);
|
|
834
|
-
});
|
|
835
|
-
});
|
|
836
|
-
describe("restoreWin32Console", () => {
|
|
837
|
-
it("returns false on non-win32 platforms", () => {
|
|
838
|
-
Object.defineProperty(process, "platform", { value: "linux" });
|
|
839
|
-
expect(restoreWin32Console()).toBe(false);
|
|
840
|
-
});
|
|
841
|
-
it("returns false when no original mode was saved", () => {
|
|
842
|
-
// Even on win32, if enableWin32Mouse was never called, restore is a no-op
|
|
843
|
-
Object.defineProperty(process, "platform", { value: "win32" });
|
|
844
|
-
expect(restoreWin32Console()).toBe(false);
|
|
845
|
-
});
|
|
846
|
-
});
|
|
847
|
-
});
|
|
@@ -296,6 +296,44 @@ describe("symbol", () => {
|
|
|
296
296
|
// U+1F600 = grinning face (in emoji range 0x1f000-0x1faff)
|
|
297
297
|
expect(charWidth(0x1f600)).toBe(2);
|
|
298
298
|
});
|
|
299
|
+
it("text-presentation symbols that remain width 1", () => {
|
|
300
|
+
// These render as 1 cell even on Windows Terminal
|
|
301
|
+
expect(charWidth(0x2713)).toBe(1); // ✓ Check Mark
|
|
302
|
+
expect(charWidth(0x2702)).toBe(1); // ✂ Scissors
|
|
303
|
+
expect(charWidth(0x00a9)).toBe(1); // © Copyright
|
|
304
|
+
expect(charWidth(0x00ae)).toBe(1); // ® Registered
|
|
305
|
+
});
|
|
306
|
+
it("emoji-presentation symbols are width 2", () => {
|
|
307
|
+
// These have Emoji_Presentation=Yes and render as 2 cells
|
|
308
|
+
expect(charWidth(0x2614)).toBe(2); // ☔ Umbrella with Rain Drops
|
|
309
|
+
expect(charWidth(0x2615)).toBe(2); // ☕ Hot Beverage
|
|
310
|
+
expect(charWidth(0x2705)).toBe(2); // ✅ White Heavy Check Mark
|
|
311
|
+
expect(charWidth(0x274c)).toBe(2); // ❌ Cross Mark
|
|
312
|
+
expect(charWidth(0x2757)).toBe(2); // ❗ Heavy Exclamation Mark
|
|
313
|
+
expect(charWidth(0x26bd)).toBe(2); // ⚽ Soccer Ball
|
|
314
|
+
expect(charWidth(0x26fd)).toBe(2); // ⛽ Fuel Pump
|
|
315
|
+
expect(charWidth(0x2b50)).toBe(2); // ⭐ White Medium Star
|
|
316
|
+
});
|
|
317
|
+
it("Windows Terminal wide symbols are width 2", () => {
|
|
318
|
+
// Text-presentation chars that Windows Terminal renders as double-width
|
|
319
|
+
expect(charWidth(0x2139)).toBe(2); // ℹ Information Source
|
|
320
|
+
expect(charWidth(0x2605)).toBe(2); // ★ Black Star
|
|
321
|
+
expect(charWidth(0x2606)).toBe(2); // ☆ White Star
|
|
322
|
+
expect(charWidth(0x2660)).toBe(2); // ♠ Black Spade Suit
|
|
323
|
+
expect(charWidth(0x2663)).toBe(2); // ♣ Black Club Suit
|
|
324
|
+
expect(charWidth(0x2665)).toBe(2); // ♥ Black Heart Suit
|
|
325
|
+
expect(charWidth(0x2666)).toBe(2); // ♦ Black Diamond Suit
|
|
326
|
+
expect(charWidth(0x2690)).toBe(2); // ⚐ White Flag
|
|
327
|
+
expect(charWidth(0x2691)).toBe(2); // ⚑ Black Flag
|
|
328
|
+
expect(charWidth(0x2699)).toBe(2); // ⚙ Gear
|
|
329
|
+
expect(charWidth(0x26a0)).toBe(2); // ⚠ Warning Sign
|
|
330
|
+
expect(charWidth(0x2714)).toBe(2); // ✔ Heavy Check Mark
|
|
331
|
+
expect(charWidth(0x2716)).toBe(2); // ✖ Heavy Multiplication X
|
|
332
|
+
expect(charWidth(0x279c)).toBe(2); // ➜ Heavy Round-Tipped Arrow
|
|
333
|
+
expect(charWidth(0x27a4)).toBe(2); // ➤ Black Right Arrowhead
|
|
334
|
+
expect(charWidth(0x25b6)).toBe(2); // ▶ Black Right Triangle
|
|
335
|
+
expect(charWidth(0x23f1)).toBe(2); // ⏱ Stopwatch
|
|
336
|
+
});
|
|
299
337
|
});
|
|
300
338
|
describe("sym() factory", () => {
|
|
301
339
|
it("creates a symbol with auto-detected width 1 for ASCII", () => {
|
|
@@ -80,7 +80,7 @@ describe("concat", () => {
|
|
|
80
80
|
const s = concat(pen.green("✔ "), pen.white("done"));
|
|
81
81
|
expect(s).toHaveLength(2);
|
|
82
82
|
expect(spanText(s)).toBe("✔ done");
|
|
83
|
-
expect(spanLength(s)).toBe(7); // ✔ is width 2 (
|
|
83
|
+
expect(spanLength(s)).toBe(7); // ✔ is width 2 (renders wide on Windows Terminal)
|
|
84
84
|
});
|
|
85
85
|
it("accepts plain strings", () => {
|
|
86
86
|
const s = concat("hello ", pen.cyan("world"));
|
package/dist/app.js
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
import * as esc from "./ansi/esc.js";
|
|
9
9
|
import { AnsiOutput } from "./ansi/output.js";
|
|
10
10
|
import { detectTerminal } from "./ansi/terminal-env.js";
|
|
11
|
-
import { enableWin32Mouse, restoreWin32Console } from "./ansi/win32-console.js";
|
|
12
11
|
import { DrawingContext } from "./drawing/context.js";
|
|
13
12
|
import { createInputProcessor } from "./input/processor.js";
|
|
14
13
|
import { disableRawMode, enableRawMode } from "./input/raw-mode.js";
|
|
@@ -98,30 +97,24 @@ export class App {
|
|
|
98
97
|
const stdout = process.stdout;
|
|
99
98
|
// 1. Enable raw mode
|
|
100
99
|
enableRawMode();
|
|
101
|
-
// 2.
|
|
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
|
|
100
|
+
// 2. Create ANSI output
|
|
107
101
|
this._output = new AnsiOutput(stdout);
|
|
108
|
-
//
|
|
109
|
-
// so we can conditionally enable mouse tracking)
|
|
102
|
+
// 3. Prepare terminal (ANSI sequences for alternate screen, mouse, etc.)
|
|
110
103
|
this._prepareTerminal();
|
|
111
|
-
//
|
|
104
|
+
// 4. Set terminal title
|
|
112
105
|
if (this._title) {
|
|
113
106
|
stdout.write(esc.setTitle(this._title));
|
|
114
107
|
}
|
|
115
|
-
//
|
|
108
|
+
// 5. Create pixel buffer at terminal dimensions
|
|
116
109
|
const cols = stdout.columns || 80;
|
|
117
110
|
const rows = stdout.rows || 24;
|
|
118
111
|
this._createRenderPipeline(cols, rows);
|
|
119
|
-
//
|
|
112
|
+
// 6. Wire up input
|
|
120
113
|
this._setupInput();
|
|
121
|
-
//
|
|
114
|
+
// 7. Wire up resize
|
|
122
115
|
this._resizeListener = () => this._handleResize();
|
|
123
116
|
stdout.on("resize", this._resizeListener);
|
|
124
|
-
//
|
|
117
|
+
// 8. SIGINT fallback
|
|
125
118
|
this._sigintListener = () => this.stop();
|
|
126
119
|
process.on("SIGINT", this._sigintListener);
|
|
127
120
|
}
|
|
@@ -300,8 +293,6 @@ export class App {
|
|
|
300
293
|
}
|
|
301
294
|
// Restore terminal
|
|
302
295
|
this._restoreTerminal();
|
|
303
|
-
// Restore Win32 console mode (before disabling raw mode)
|
|
304
|
-
restoreWin32Console();
|
|
305
296
|
// Disable raw mode
|
|
306
297
|
disableRawMode();
|
|
307
298
|
// Resolve the run() promise
|
package/dist/index.d.ts
CHANGED
|
@@ -17,7 +17,6 @@ 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
19
|
export { detectTerminal, type TerminalCaps, } from "./ansi/terminal-env.js";
|
|
20
|
-
export { enableWin32Mouse, restoreWin32Console, } from "./ansi/win32-console.js";
|
|
21
20
|
export { DirtyRegions, DirtySnapshot } from "./render/regions.js";
|
|
22
21
|
export { RenderTarget } from "./render/render-target.js";
|
|
23
22
|
export { EscapeMatcher } from "./input/escape-matcher.js";
|
package/dist/index.js
CHANGED
|
@@ -24,7 +24,6 @@ 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
26
|
export { detectTerminal, } from "./ansi/terminal-env.js";
|
|
27
|
-
export { enableWin32Mouse, restoreWin32Console, } from "./ansi/win32-console.js";
|
|
28
27
|
// ── Render pipeline ─────────────────────────────────────────────────
|
|
29
28
|
export { DirtyRegions, DirtySnapshot } from "./render/regions.js";
|
|
30
29
|
export { RenderTarget } from "./render/render-target.js";
|
package/dist/pixel/symbol.js
CHANGED
|
@@ -114,49 +114,115 @@ export function charWidth(codePoint) {
|
|
|
114
114
|
// CJK Compatibility Ideographs Supplement
|
|
115
115
|
if (codePoint >= 0x2f800 && codePoint <= 0x2fa1f)
|
|
116
116
|
return 2;
|
|
117
|
-
// ── Emoji
|
|
118
|
-
//
|
|
117
|
+
// ── Emoji with Emoji_Presentation=Yes (rendered as 2 cells by default) ──
|
|
118
|
+
// Only characters that terminals render as wide WITHOUT a variation selector.
|
|
119
|
+
// ── Text-presentation characters that Windows Terminal renders as wide ──
|
|
120
|
+
// These have Emoji_Presentation=No in Unicode but modern terminals (Windows
|
|
121
|
+
// Terminal, VS Code integrated terminal) render them as double-width emoji.
|
|
122
|
+
if (codePoint === 0x2139)
|
|
123
|
+
return 2; // ℹ Information Source
|
|
124
|
+
if (codePoint === 0x2605 || codePoint === 0x2606)
|
|
125
|
+
return 2; // ★☆ Stars
|
|
126
|
+
if (codePoint === 0x2660)
|
|
127
|
+
return 2; // ♠ Black Spade Suit
|
|
128
|
+
if (codePoint === 0x2663)
|
|
129
|
+
return 2; // ♣ Black Club Suit
|
|
130
|
+
if (codePoint === 0x2665)
|
|
131
|
+
return 2; // ♥ Black Heart Suit
|
|
132
|
+
if (codePoint === 0x2666)
|
|
133
|
+
return 2; // ♦ Black Diamond Suit
|
|
134
|
+
if (codePoint === 0x2690 || codePoint === 0x2691)
|
|
135
|
+
return 2; // ⚐⚑ Flags
|
|
136
|
+
if (codePoint === 0x2699)
|
|
137
|
+
return 2; // ⚙ Gear
|
|
138
|
+
if (codePoint === 0x26a0)
|
|
139
|
+
return 2; // ⚠ Warning Sign
|
|
140
|
+
if (codePoint === 0x2714)
|
|
141
|
+
return 2; // ✔ Heavy Check Mark
|
|
142
|
+
if (codePoint === 0x2716)
|
|
143
|
+
return 2; // ✖ Heavy Multiplication X
|
|
144
|
+
if (codePoint === 0x279c)
|
|
145
|
+
return 2; // ➜ Heavy Round-Tipped Arrow
|
|
146
|
+
if (codePoint === 0x27a4)
|
|
147
|
+
return 2; // ➤ Black Right Arrowhead
|
|
148
|
+
if (codePoint === 0x25b6)
|
|
149
|
+
return 2; // ▶ Black Right Triangle
|
|
150
|
+
if (codePoint === 0x23f1)
|
|
151
|
+
return 2; // ⏱ Stopwatch
|
|
152
|
+
// Hourglass + Watch (⌚⌛)
|
|
119
153
|
if (codePoint === 0x231a || codePoint === 0x231b)
|
|
120
154
|
return 2;
|
|
121
|
-
//
|
|
122
|
-
if (codePoint >= 0x23e9 && codePoint <=
|
|
123
|
-
return 2;
|
|
124
|
-
//
|
|
125
|
-
if (codePoint
|
|
126
|
-
return 2;
|
|
127
|
-
//
|
|
128
|
-
if (codePoint ===
|
|
129
|
-
return 2;
|
|
130
|
-
//
|
|
131
|
-
if (codePoint
|
|
132
|
-
return 2;
|
|
133
|
-
//
|
|
134
|
-
if (codePoint
|
|
135
|
-
return 2;
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (codePoint ===
|
|
141
|
-
return 2;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (codePoint
|
|
147
|
-
return 2;
|
|
155
|
+
// Fast-forward through rewind (⏩⏪⏫⏬)
|
|
156
|
+
if (codePoint >= 0x23e9 && codePoint <= 0x23ec)
|
|
157
|
+
return 2;
|
|
158
|
+
// Alarm clock (⏰)
|
|
159
|
+
if (codePoint === 0x23f0)
|
|
160
|
+
return 2;
|
|
161
|
+
// Hourglass flowing (⏳)
|
|
162
|
+
if (codePoint === 0x23f3)
|
|
163
|
+
return 2;
|
|
164
|
+
// White/black medium small square with emoji pres (◽◾)
|
|
165
|
+
if (codePoint === 0x25fd || codePoint === 0x25fe)
|
|
166
|
+
return 2;
|
|
167
|
+
// Misc Symbols — only Emoji_Presentation=Yes entries
|
|
168
|
+
if (codePoint === 0x2614 || codePoint === 0x2615)
|
|
169
|
+
return 2; // ☔☕
|
|
170
|
+
if (codePoint >= 0x2648 && codePoint <= 0x2653)
|
|
171
|
+
return 2; // ♈-♓
|
|
172
|
+
if (codePoint === 0x267f)
|
|
173
|
+
return 2; // ♿
|
|
174
|
+
if (codePoint === 0x2693)
|
|
175
|
+
return 2; // ⚓
|
|
176
|
+
if (codePoint === 0x26a1)
|
|
177
|
+
return 2; // ⚡
|
|
178
|
+
if (codePoint === 0x26aa || codePoint === 0x26ab)
|
|
179
|
+
return 2; // ⚪⚫
|
|
180
|
+
if (codePoint === 0x26bd || codePoint === 0x26be)
|
|
181
|
+
return 2; // ⚽⚾
|
|
182
|
+
if (codePoint === 0x26c4 || codePoint === 0x26c5)
|
|
183
|
+
return 2; // ⛄⛅
|
|
184
|
+
if (codePoint === 0x26ce)
|
|
185
|
+
return 2; // ⛎
|
|
186
|
+
if (codePoint === 0x26d4)
|
|
187
|
+
return 2; // ⛔
|
|
188
|
+
if (codePoint === 0x26ea)
|
|
189
|
+
return 2; // ⛪
|
|
190
|
+
if (codePoint === 0x26f2 || codePoint === 0x26f3)
|
|
191
|
+
return 2; // ⛲⛳
|
|
192
|
+
if (codePoint === 0x26f5)
|
|
193
|
+
return 2; // ⛵
|
|
194
|
+
if (codePoint === 0x26fa)
|
|
195
|
+
return 2; // ⛺
|
|
196
|
+
if (codePoint === 0x26fd)
|
|
197
|
+
return 2; // ⛽
|
|
198
|
+
// Dingbats — only Emoji_Presentation=Yes entries
|
|
199
|
+
if (codePoint === 0x2705)
|
|
200
|
+
return 2; // ✅
|
|
201
|
+
if (codePoint === 0x270a || codePoint === 0x270b)
|
|
202
|
+
return 2; // ✊✋
|
|
203
|
+
if (codePoint === 0x2728)
|
|
204
|
+
return 2; // ✨
|
|
205
|
+
if (codePoint === 0x274c)
|
|
206
|
+
return 2; // ❌
|
|
207
|
+
if (codePoint === 0x274e)
|
|
208
|
+
return 2; // ❎
|
|
209
|
+
if (codePoint >= 0x2753 && codePoint <= 0x2755)
|
|
210
|
+
return 2; // ❓❔❕
|
|
211
|
+
if (codePoint === 0x2757)
|
|
212
|
+
return 2; // ❗
|
|
213
|
+
if (codePoint === 0x2764)
|
|
214
|
+
return 2; // ❤
|
|
215
|
+
if (codePoint >= 0x2795 && codePoint <= 0x2797)
|
|
216
|
+
return 2; // ➕➖➗
|
|
217
|
+
// Curly loop / double curly loop (➰➿)
|
|
218
|
+
if (codePoint === 0x27b0 || codePoint === 0x27bf)
|
|
219
|
+
return 2;
|
|
220
|
+
// Black large square/circle, star, hollow circle (⬛⬜⭐⭕)
|
|
148
221
|
if (codePoint === 0x2b1b || codePoint === 0x2b1c)
|
|
149
222
|
return 2;
|
|
150
223
|
if (codePoint === 0x2b50 || codePoint === 0x2b55)
|
|
151
224
|
return 2;
|
|
152
|
-
// Copyright / Registered / TM (when emoji-styled)
|
|
153
|
-
if (codePoint === 0x00a9 || codePoint === 0x00ae)
|
|
154
|
-
return 2;
|
|
155
|
-
// Wavy dash, part alternation mark
|
|
156
|
-
if (codePoint === 0x3030 || codePoint === 0x303d)
|
|
157
|
-
return 2;
|
|
158
225
|
// SMP Emoji: Mahjong through Symbols & Pictographs Extended-A
|
|
159
|
-
// Covers emoticons, transport, flags, supplemental symbols, etc.
|
|
160
226
|
if (codePoint >= 0x1f000 && codePoint <= 0x1faff)
|
|
161
227
|
return 2;
|
|
162
228
|
return 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teammates/consolonia",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
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,8 +41,5 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"marked": "^17.0.4"
|
|
44
|
-
},
|
|
45
|
-
"optionalDependencies": {
|
|
46
|
-
"koffi": "^2.9.0"
|
|
47
44
|
}
|
|
48
45
|
}
|
|
@@ -1,28 +0,0 @@
|
|
|
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;
|
|
@@ -1,122 +0,0 @@
|
|
|
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
|
-
}
|