@vui-rs/core 0.1.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 (44) hide show
  1. package/README.md +29 -0
  2. package/dist/char-width.d.ts +7 -0
  3. package/dist/char-width.js +20 -0
  4. package/dist/color-names.js +46 -0
  5. package/dist/color.d.ts +5 -0
  6. package/dist/color.js +57 -0
  7. package/dist/image-decode.d.ts +21 -0
  8. package/dist/image-decode.js +42 -0
  9. package/dist/index.d.ts +402 -0
  10. package/dist/index.js +27 -0
  11. package/dist/keys.d.ts +76 -0
  12. package/dist/keys.js +373 -0
  13. package/dist/named-colors.d.ts +7 -0
  14. package/dist/named-colors.js +20 -0
  15. package/dist/native/darwin-arm64/libvui_core.dylib +0 -0
  16. package/dist/native/darwin-x64/libvui_core.dylib +0 -0
  17. package/dist/native/ffi-symbols.d.ts +453 -0
  18. package/dist/native/ffi-symbols.js +680 -0
  19. package/dist/native/linux-arm64/libvui_core.so +0 -0
  20. package/dist/native/linux-x64/libvui_core.so +0 -0
  21. package/dist/native/load-native-lib.d.ts +384 -0
  22. package/dist/native/load-native-lib.js +63 -0
  23. package/dist/native/win32-x64/vui_core.dll +0 -0
  24. package/dist/node.d.ts +61 -0
  25. package/dist/node.js +157 -0
  26. package/dist/offscreen-buffer.d.ts +28 -0
  27. package/dist/offscreen-buffer.js +73 -0
  28. package/dist/renderer.d.ts +106 -0
  29. package/dist/renderer.js +186 -0
  30. package/dist/style.d.ts +48 -0
  31. package/dist/style.js +134 -0
  32. package/dist/terminal-session.d.ts +43 -0
  33. package/dist/terminal-session.js +82 -0
  34. package/dist/text/edit-buffer.d.ts +31 -0
  35. package/dist/text/edit-buffer.js +96 -0
  36. package/dist/text/editor-view.d.ts +22 -0
  37. package/dist/text/editor-view.js +48 -0
  38. package/dist/text/index.d.ts +5 -0
  39. package/dist/text/index.js +5 -0
  40. package/dist/text/text-buffer-view.d.ts +22 -0
  41. package/dist/text/text-buffer-view.js +49 -0
  42. package/dist/text/text-buffer.d.ts +16 -0
  43. package/dist/text/text-buffer.js +43 -0
  44. package/package.json +46 -0
package/dist/keys.d.ts ADDED
@@ -0,0 +1,76 @@
1
+ //#region src/keys.d.ts
2
+ interface KeyEvent {
3
+ type: 'key';
4
+ /** Lowercase name: a printable (`"a"`, `"世"`) or a named key (`"enter"`, `"up"`). */
5
+ name: string;
6
+ ctrl: boolean;
7
+ alt: boolean;
8
+ shift: boolean;
9
+ meta: boolean;
10
+ /** The exact source bytes (as a string) this event was parsed from. */
11
+ raw: string;
12
+ }
13
+ interface PasteEvent {
14
+ type: 'paste';
15
+ text: string;
16
+ }
17
+ type MouseButton = 'left' | 'middle' | 'right' | 'wheelUp' | 'wheelDown';
18
+ interface MouseEvent {
19
+ type: 'mouse';
20
+ kind: 'down' | 'up' | 'move' | 'drag' | 'wheel';
21
+ button: MouseButton | null;
22
+ x: number;
23
+ y: number;
24
+ ctrl: boolean;
25
+ alt: boolean;
26
+ shift: boolean;
27
+ meta: boolean;
28
+ raw: string;
29
+ }
30
+ type InputEvent = KeyEvent | PasteEvent | MouseEvent;
31
+ /** Parse a chunk of terminal input into discrete key/paste events (stateless). */
32
+ declare function parseKeys(data: string | Uint8Array): InputEvent[];
33
+ interface KeyDecoder {
34
+ /** Feed a chunk; returns the events decodable so far. Buffers a partial tail. */
35
+ feed(data: string | Uint8Array): InputEvent[];
36
+ /**
37
+ * Force-parse the buffered partial tail (best-effort: a lone ESC becomes a bare
38
+ * Escape) and clear it. Call on an idle/escape timeout so a standalone Escape
39
+ * keypress — indistinguishable from the start of a CSI/SS3 sequence until more
40
+ * bytes arrive — isn't held until the next key. No-op when nothing is pending.
41
+ */
42
+ flush(): InputEvent[];
43
+ /** The currently-buffered partial tail (empty when fully drained). */
44
+ pending(): string;
45
+ }
46
+ /**
47
+ * Stateful decoder for live input: it carries a partial trailing escape/paste
48
+ * across chunks, so a sequence (or a large paste) split over multiple stdin reads
49
+ * still parses correctly. One decoder per input stream.
50
+ */
51
+ declare function createKeyDecoder(): KeyDecoder;
52
+ /**
53
+ * Test whether an event matches a key spec like `"ctrl+c"`, `"shift+tab"`,
54
+ * `"enter"`. Modifiers are order-insensitive; `super` is an alias for `meta`.
55
+ */
56
+ declare function matchesKey(ev: InputEvent, spec: string): boolean;
57
+ /** Tiny helper for building key specs: `Key.ctrl("c")`, `Key.enter`. */
58
+ declare const Key: {
59
+ readonly ctrl: (k: string) => string;
60
+ readonly alt: (k: string) => string;
61
+ readonly shift: (k: string) => string;
62
+ readonly enter: "enter";
63
+ readonly tab: "tab";
64
+ readonly escape: "escape";
65
+ readonly backspace: "backspace";
66
+ readonly delete: "delete";
67
+ readonly up: "up";
68
+ readonly down: "down";
69
+ readonly left: "left";
70
+ readonly right: "right";
71
+ readonly home: "home";
72
+ readonly end: "end";
73
+ readonly space: "space";
74
+ };
75
+ //#endregion
76
+ export { InputEvent, Key, KeyDecoder, KeyEvent, MouseButton, MouseEvent, PasteEvent, createKeyDecoder, matchesKey, parseKeys };
package/dist/keys.js ADDED
@@ -0,0 +1,373 @@
1
+ //#region src/keys.ts
2
+ const decoder = new TextDecoder();
3
+ const PASTE_START = "\x1B[200~";
4
+ const PASTE_END = "\x1B[201~";
5
+ const MOUSE_BUTTONS = [
6
+ "left",
7
+ "middle",
8
+ "right"
9
+ ];
10
+ const ARROW_NAMES = {
11
+ A: "up",
12
+ B: "down",
13
+ C: "right",
14
+ D: "left",
15
+ H: "home",
16
+ F: "end"
17
+ };
18
+ const TILDE_NAMES = {
19
+ "1": "home",
20
+ "2": "insert",
21
+ "3": "delete",
22
+ "4": "end",
23
+ "5": "pageUp",
24
+ "6": "pageDown",
25
+ "7": "home",
26
+ "8": "end"
27
+ };
28
+ const SS3_NAMES = {
29
+ ...ARROW_NAMES,
30
+ P: "f1",
31
+ Q: "f2",
32
+ R: "f3",
33
+ S: "f4"
34
+ };
35
+ function key(name, opts = {}) {
36
+ return {
37
+ type: "key",
38
+ name,
39
+ ctrl: !!opts.ctrl,
40
+ alt: !!opts.alt,
41
+ shift: !!opts.shift,
42
+ meta: !!opts.meta,
43
+ raw: opts.raw ?? name
44
+ };
45
+ }
46
+ /** Decode a CSI modifier param (`m` in `1;m`) into modifier flags. */
47
+ function decodeMod(param) {
48
+ const m = param ? Number.parseInt(param, 10) - 1 : 0;
49
+ return {
50
+ shift: !!(m & 1),
51
+ alt: !!(m & 2),
52
+ ctrl: !!(m & 4),
53
+ meta: !!(m & 8)
54
+ };
55
+ }
56
+ /**
57
+ * Kitty keyboard protocol functional keycodes that map to a named key. The
58
+ * protocol reports most printable keys by their Unicode codepoint (handled
59
+ * below) and reserves these specific codepoints for named keys. Arrows, Home/
60
+ * End, etc. keep their legacy `CSI letter`/`CSI ~` encodings even in Kitty mode,
61
+ * so they continue through the existing decoder and aren't listed here.
62
+ */
63
+ const KITTY_NAMED = {
64
+ 13: "enter",
65
+ 27: "escape",
66
+ 9: "tab",
67
+ 127: "backspace",
68
+ 32: "space"
69
+ };
70
+ /**
71
+ * Decode a Kitty modifier+event field (`mods` or `mods:event-type`). The modifier
72
+ * bitfield is value-minus-1; bit layout: shift1 alt2 ctrl4 super8 hyper16 meta32.
73
+ * `super`/`meta` both fold to our `meta` flag. Event type: 1 press, 2 repeat, 3
74
+ * release (default 1 when absent).
75
+ */
76
+ function decodeKittyMod(param) {
77
+ const [modStr, evStr] = (param ?? "").split(":");
78
+ const m = modStr ? Number.parseInt(modStr, 10) - 1 : 0;
79
+ const eventType = evStr ? Number.parseInt(evStr, 10) : 1;
80
+ return {
81
+ mods: {
82
+ shift: !!(m & 1),
83
+ alt: !!(m & 2),
84
+ ctrl: !!(m & 4),
85
+ meta: !!(m & 8) || !!(m & 32)
86
+ },
87
+ eventType
88
+ };
89
+ }
90
+ /**
91
+ * Decode a Kitty keyboard `CSI <code>[:alt:base] ; <mods>[:event] u` sequence into
92
+ * a KeyEvent. Key releases (event type 3) are dropped in v0; repeats (2) fire as a
93
+ * normal press. Malformed/unmapped control keycodes are consumed silently.
94
+ */
95
+ function parseCsiU(params, raw, consumed, out) {
96
+ const parts = params.split(";");
97
+ const keycode = Number.parseInt(parts[0].split(":")[0] ?? "", 10);
98
+ if (!Number.isFinite(keycode)) return consumed;
99
+ const { mods, eventType } = decodeKittyMod(parts[1]);
100
+ if (eventType === 3) return consumed;
101
+ let name = KITTY_NAMED[keycode];
102
+ if (name === void 0) if (keycode >= 32 && keycode !== 127) name = String.fromCodePoint(keycode);
103
+ else return consumed;
104
+ out.push(key(name, {
105
+ ...mods,
106
+ raw
107
+ }));
108
+ return consumed;
109
+ }
110
+ function newMouseState() {
111
+ return { buttons: /* @__PURE__ */ new Set() };
112
+ }
113
+ function stepOne(s, i, out, mouse) {
114
+ const code = s.charCodeAt(i);
115
+ if (code === 27) {
116
+ const r = parseEscape(s, i, out, mouse);
117
+ if (r !== 0) return r;
118
+ out.push(key("escape", { raw: "\x1B" }));
119
+ return 1;
120
+ }
121
+ if (code === 13 || code === 10) out.push(key("enter", { raw: s[i] }));
122
+ else if (code === 9) out.push(key("tab", { raw: s[i] }));
123
+ else if (code === 127 || code === 8) out.push(key("backspace", { raw: s[i] }));
124
+ else if (code === 0) out.push(key("space", {
125
+ ctrl: true,
126
+ raw: s[i]
127
+ }));
128
+ else if (code >= 1 && code <= 26) out.push(key(String.fromCharCode(code + 96), {
129
+ ctrl: true,
130
+ raw: s[i]
131
+ }));
132
+ else if (code >= 28 && code <= 31) {} else {
133
+ const ch = String.fromCodePoint(s.codePointAt(i));
134
+ out.push(key(ch, {
135
+ raw: ch,
136
+ shift: ch.length === 1 && ch >= "A" && ch <= "Z"
137
+ }));
138
+ return ch.length;
139
+ }
140
+ return 1;
141
+ }
142
+ /** Parse a chunk of terminal input into discrete key/paste events (stateless). */
143
+ function parseKeys(data) {
144
+ const s = typeof data === "string" ? data : decoder.decode(data);
145
+ const events = [];
146
+ const mouse = newMouseState();
147
+ let i = 0;
148
+ while (i < s.length) {
149
+ const consumed = stepOne(s, i, events, mouse);
150
+ if (consumed === -1) {
151
+ events.push(key("escape", { raw: "\x1B" }));
152
+ i += 1;
153
+ } else i += consumed;
154
+ }
155
+ return events;
156
+ }
157
+ /** A bare partial CSI/SS3 (no terminator) buffered this long is treated as stuck
158
+ * and flushed, so a malformed stream can't grow the pending buffer unbounded. A
159
+ * bracketed paste (identified by its start marker) is exempt — pastes are large
160
+ * by nature and must buffer until their end marker. */
161
+ const MAX_PENDING = 64;
162
+ /**
163
+ * Stateful decoder for live input: it carries a partial trailing escape/paste
164
+ * across chunks, so a sequence (or a large paste) split over multiple stdin reads
165
+ * still parses correctly. One decoder per input stream.
166
+ */
167
+ function createKeyDecoder() {
168
+ let pending = "";
169
+ const mouse = newMouseState();
170
+ return {
171
+ feed(data) {
172
+ const s = pending + (typeof data === "string" ? data : decoder.decode(data));
173
+ const events = [];
174
+ let i = 0;
175
+ while (i < s.length) {
176
+ const consumed = stepOne(s, i, events, mouse);
177
+ if (consumed === -1) break;
178
+ i += consumed;
179
+ }
180
+ pending = s.slice(i);
181
+ if (pending.length > MAX_PENDING && !pending.startsWith(PASTE_START)) {
182
+ for (const ev of parseKeys(pending)) events.push(ev);
183
+ pending = "";
184
+ }
185
+ return events;
186
+ },
187
+ flush() {
188
+ if (pending === "" || pending.startsWith(PASTE_START)) return [];
189
+ const events = parseKeys(pending);
190
+ pending = "";
191
+ return events;
192
+ },
193
+ pending() {
194
+ return pending;
195
+ }
196
+ };
197
+ }
198
+ /**
199
+ * Parse an escape sequence starting at `i`. Returns bytes consumed, `0` for a
200
+ * bare/unrecognised ESC (caller emits Escape), or `-1` for a truncated sequence
201
+ * that needs more input.
202
+ */
203
+ function parseEscape(s, i, out, mouse) {
204
+ const next = s[i + 1];
205
+ if (next === void 0) return -1;
206
+ if (next === "[") return parseCSI(s, i, out, mouse);
207
+ if (next === "O") return parseSS3(s, i, out);
208
+ const code = s.charCodeAt(i + 1);
209
+ if (code === 127 || code === 8) {
210
+ out.push(key("backspace", {
211
+ alt: true,
212
+ raw: s.slice(i, i + 2)
213
+ }));
214
+ return 2;
215
+ }
216
+ if (code >= 32) {
217
+ const ch = String.fromCodePoint(s.codePointAt(i + 1));
218
+ out.push(key(ch, {
219
+ alt: true,
220
+ raw: "\x1B" + ch
221
+ }));
222
+ return 1 + ch.length;
223
+ }
224
+ return 0;
225
+ }
226
+ function parseCSI(s, i, out, mouse) {
227
+ if (s[i + 2] === "<") return parseSgrMouse(s, i, out, mouse);
228
+ if (s[i + 2] === "M") return parseX10Mouse(s, i, out, mouse);
229
+ let j = i + 2;
230
+ let params = "";
231
+ while (j < s.length && (s[j] === ";" || s[j] === ":" || s[j] >= "0" && s[j] <= "9")) {
232
+ params += s[j];
233
+ j += 1;
234
+ }
235
+ const final = s[j];
236
+ if (final === void 0) return -1;
237
+ if (params === "200" && final === "~") {
238
+ const start = j + 1;
239
+ const end = s.indexOf(PASTE_END, start);
240
+ if (end === -1) return -1;
241
+ out.push({
242
+ type: "paste",
243
+ text: s.slice(start, end)
244
+ });
245
+ return end + 6 - i;
246
+ }
247
+ const raw = s.slice(i, j + 1);
248
+ const consumed = j + 1 - i;
249
+ if (final === "u") return parseCsiU(params, raw, consumed, out);
250
+ if (final === "Z") {
251
+ out.push(key("tab", {
252
+ shift: true,
253
+ raw
254
+ }));
255
+ return consumed;
256
+ }
257
+ const parts = params.split(";");
258
+ const mods = {
259
+ ...decodeMod(parts[1]),
260
+ raw
261
+ };
262
+ if (ARROW_NAMES[final]) {
263
+ out.push(key(ARROW_NAMES[final], mods));
264
+ return consumed;
265
+ }
266
+ if (final === "~" && TILDE_NAMES[parts[0]]) {
267
+ out.push(key(TILDE_NAMES[parts[0]], mods));
268
+ return consumed;
269
+ }
270
+ return consumed;
271
+ }
272
+ function decodeMouseModifiers(code) {
273
+ return {
274
+ shift: !!(code & 4),
275
+ alt: !!(code & 8),
276
+ ctrl: !!(code & 16),
277
+ meta: false
278
+ };
279
+ }
280
+ function mouseButton(code) {
281
+ if (code & 64) return code & 1 ? "wheelDown" : "wheelUp";
282
+ return MOUSE_BUTTONS[code & 3] ?? null;
283
+ }
284
+ function pushMouse(out, mouse, code, x, y, final, raw) {
285
+ const wheel = !!(code & 64);
286
+ const motion = !!(code & 32);
287
+ const button = mouseButton(code);
288
+ const mods = decodeMouseModifiers(code);
289
+ let kind;
290
+ if (wheel) kind = "wheel";
291
+ else if (final === "m") kind = "up";
292
+ else if (motion) kind = mouse.buttons.size > 0 ? "drag" : "move";
293
+ else kind = "down";
294
+ if (kind === "down" && button) mouse.buttons.add(button);
295
+ if (kind === "up") if (button) mouse.buttons.delete(button);
296
+ else mouse.buttons.clear();
297
+ out.push({
298
+ type: "mouse",
299
+ kind,
300
+ button,
301
+ x,
302
+ y,
303
+ ctrl: !!mods.ctrl,
304
+ alt: !!mods.alt,
305
+ shift: !!mods.shift,
306
+ meta: false,
307
+ raw
308
+ });
309
+ }
310
+ function parseSgrMouse(s, i, out, mouse) {
311
+ let j = i + 3;
312
+ while (j < s.length && (s[j] === ";" || s[j] >= "0" && s[j] <= "9")) j += 1;
313
+ const final = s[j];
314
+ if (final === void 0) return -1;
315
+ if (final !== "M" && final !== "m") return j + 1 - i;
316
+ const raw = s.slice(i, j + 1);
317
+ const parts = s.slice(i + 3, j).split(";");
318
+ if (parts.length !== 3) return j + 1 - i;
319
+ const code = Number.parseInt(parts[0], 10);
320
+ const x = Number.parseInt(parts[1], 10) - 1;
321
+ const y = Number.parseInt(parts[2], 10) - 1;
322
+ if (!Number.isFinite(code) || !Number.isFinite(x) || !Number.isFinite(y) || x < 0 || y < 0) return j + 1 - i;
323
+ pushMouse(out, mouse, code, x, y, final, raw);
324
+ return j + 1 - i;
325
+ }
326
+ function parseX10Mouse(s, i, out, mouse) {
327
+ if (i + 5 >= s.length) return -1;
328
+ const raw = s.slice(i, i + 6);
329
+ const code = s.charCodeAt(i + 3) - 32;
330
+ const x = s.charCodeAt(i + 4) - 33;
331
+ const y = s.charCodeAt(i + 5) - 33;
332
+ if (x < 0 || y < 0) return 6;
333
+ pushMouse(out, mouse, code, x, y, code === 3 ? "m" : "M", raw);
334
+ return 6;
335
+ }
336
+ function parseSS3(s, i, out) {
337
+ const final = s[i + 2];
338
+ if (final === void 0) return -1;
339
+ const name = SS3_NAMES[final];
340
+ if (!name) return 0;
341
+ out.push(key(name, { raw: s.slice(i, i + 3) }));
342
+ return 3;
343
+ }
344
+ /**
345
+ * Test whether an event matches a key spec like `"ctrl+c"`, `"shift+tab"`,
346
+ * `"enter"`. Modifiers are order-insensitive; `super` is an alias for `meta`.
347
+ */
348
+ function matchesKey(ev, spec) {
349
+ if (ev.type !== "key") return false;
350
+ const parts = spec.toLowerCase().split("+");
351
+ const base = parts.pop();
352
+ return ev.name.toLowerCase() === base && ev.ctrl === parts.includes("ctrl") && ev.alt === parts.includes("alt") && ev.shift === parts.includes("shift") && ev.meta === (parts.includes("meta") || parts.includes("super"));
353
+ }
354
+ /** Tiny helper for building key specs: `Key.ctrl("c")`, `Key.enter`. */
355
+ const Key = {
356
+ ctrl: (k) => `ctrl+${k}`,
357
+ alt: (k) => `alt+${k}`,
358
+ shift: (k) => `shift+${k}`,
359
+ enter: "enter",
360
+ tab: "tab",
361
+ escape: "escape",
362
+ backspace: "backspace",
363
+ delete: "delete",
364
+ up: "up",
365
+ down: "down",
366
+ left: "left",
367
+ right: "right",
368
+ home: "home",
369
+ end: "end",
370
+ space: "space"
371
+ };
372
+ //#endregion
373
+ export { Key, createKeyDecoder, matchesKey, parseKeys };
@@ -0,0 +1,7 @@
1
+ //#region src/named-colors.d.ts
2
+ /** Parse a `#rgb`/`#rrggbb`/`#rrggbbaa` string to packed `0xRRGGBBAA`, or undefined. */
3
+ declare function parseHex(value: string): number | undefined;
4
+ /** Named color → packed `0xRRGGBBAA`. Built once from the shared JSON table. */
5
+ declare const NAMED_COLORS: ReadonlyMap<string, number>;
6
+ //#endregion
7
+ export { NAMED_COLORS, parseHex };
@@ -0,0 +1,20 @@
1
+ import color_names_default from "./color-names.js";
2
+ //#region src/named-colors.ts
3
+ /** Parse a `#rgb`/`#rrggbb`/`#rrggbbaa` string to packed `0xRRGGBBAA`, or undefined. */
4
+ function parseHex(value) {
5
+ if (!value.startsWith("#")) return void 0;
6
+ let hex = value.slice(1);
7
+ if (hex.length === 3) hex = hex.split("").map((c) => c + c).join("");
8
+ if (hex.length === 6) hex += "ff";
9
+ if (hex.length !== 8) return void 0;
10
+ if (!/^[0-9a-fA-F]{8}$/.test(hex)) return void 0;
11
+ return Number.parseInt(hex, 16) >>> 0;
12
+ }
13
+ /** Named color → packed `0xRRGGBBAA`. Built once from the shared JSON table. */
14
+ const NAMED_COLORS = new Map(Object.entries(color_names_default).map(([name, hex]) => {
15
+ const packed = parseHex(hex);
16
+ if (packed === void 0) throw new Error(`color-names.json: bad hex "${hex}" for "${name}"`);
17
+ return [name, packed];
18
+ }));
19
+ //#endregion
20
+ export { NAMED_COLORS, parseHex };