@tuidom/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 (91) hide show
  1. package/dist/backend/iTerminalBackend.d.ts +41 -0
  2. package/dist/backend/iTerminalBackend.js +1 -0
  3. package/dist/common/colorUtils.d.ts +19 -0
  4. package/dist/common/colorUtils.js +30 -0
  5. package/dist/common/displayLine.d.ts +72 -0
  6. package/dist/common/displayLine.js +190 -0
  7. package/dist/common/disposable.d.ts +9 -0
  8. package/dist/common/disposable.js +18 -0
  9. package/dist/common/geometryPromitives.d.ts +47 -0
  10. package/dist/common/geometryPromitives.js +115 -0
  11. package/dist/common/iTerminalSurface.d.ts +68 -0
  12. package/dist/common/iTerminalSurface.js +8 -0
  13. package/dist/common/measureTextWidth.d.ts +21 -0
  14. package/dist/common/measureTextWidth.js +43 -0
  15. package/dist/common/styleFlags.d.ts +17 -0
  16. package/dist/common/styleFlags.js +16 -0
  17. package/dist/common/textTruncation.d.ts +30 -0
  18. package/dist/common/textTruncation.js +124 -0
  19. package/dist/common/typingUtils.d.ts +1 -0
  20. package/dist/common/typingUtils.js +3 -0
  21. package/dist/common/unicodeWidth.d.ts +18 -0
  22. package/dist/common/unicodeWidth.js +339 -0
  23. package/dist/dom/borderStyle.d.ts +31 -0
  24. package/dist/dom/borderStyle.js +40 -0
  25. package/dist/dom/compositeElement.d.ts +20 -0
  26. package/dist/dom/compositeElement.js +44 -0
  27. package/dist/dom/events/contextMenuEventSource.d.ts +17 -0
  28. package/dist/dom/events/contextMenuEventSource.js +44 -0
  29. package/dist/dom/events/focusManager.d.ts +13 -0
  30. package/dist/dom/events/focusManager.js +56 -0
  31. package/dist/dom/events/mouseEventDispatcher.d.ts +24 -0
  32. package/dist/dom/events/mouseEventDispatcher.js +177 -0
  33. package/dist/dom/events/tuiEventBase.d.ts +25 -0
  34. package/dist/dom/events/tuiEventBase.js +39 -0
  35. package/dist/dom/events/tuiFocusEvent.d.ts +6 -0
  36. package/dist/dom/events/tuiFocusEvent.js +8 -0
  37. package/dist/dom/events/tuiKeyboardEvent.d.ts +21 -0
  38. package/dist/dom/events/tuiKeyboardEvent.js +20 -0
  39. package/dist/dom/events/tuiMouseEvent.d.ts +40 -0
  40. package/dist/dom/events/tuiMouseEvent.js +32 -0
  41. package/dist/dom/events/tuiPasteEvent.d.ts +10 -0
  42. package/dist/dom/events/tuiPasteEvent.js +13 -0
  43. package/dist/dom/overlayLayer.d.ts +92 -0
  44. package/dist/dom/overlayLayer.js +343 -0
  45. package/dist/dom/styles/index.d.ts +4 -0
  46. package/dist/dom/styles/index.js +2 -0
  47. package/dist/dom/styles/styleTokens.d.ts +114 -0
  48. package/dist/dom/styles/styleTokens.js +122 -0
  49. package/dist/dom/styles/tuiStyle.d.ts +75 -0
  50. package/dist/dom/styles/tuiStyle.js +121 -0
  51. package/dist/dom/tuiApplication.d.ts +57 -0
  52. package/dist/dom/tuiApplication.js +231 -0
  53. package/dist/dom/tuiElement.d.ts +528 -0
  54. package/dist/dom/tuiElement.js +1168 -0
  55. package/dist/dom/tuiSelector.d.ts +9 -0
  56. package/dist/dom/tuiSelector.js +82 -0
  57. package/dist/dom/validateTree.d.ts +42 -0
  58. package/dist/dom/validateTree.js +125 -0
  59. package/dist/input/convertToken.d.ts +3 -0
  60. package/dist/input/convertToken.js +114 -0
  61. package/dist/input/keyEvent.d.ts +46 -0
  62. package/dist/input/keyEvent.js +26 -0
  63. package/dist/input/keyInputParser.d.ts +72 -0
  64. package/dist/input/keyInputParser.js +249 -0
  65. package/dist/input/mouseTracking.d.ts +18 -0
  66. package/dist/input/mouseTracking.js +18 -0
  67. package/dist/input/parseInput.d.ts +13 -0
  68. package/dist/input/parseInput.js +17 -0
  69. package/dist/input/rawTerminalToken.d.ts +142 -0
  70. package/dist/input/rawTerminalToken.js +2 -0
  71. package/dist/input/serializeKey.d.ts +14 -0
  72. package/dist/input/serializeKey.js +179 -0
  73. package/dist/input/serializeMouse.d.ts +26 -0
  74. package/dist/input/serializeMouse.js +38 -0
  75. package/dist/input/tokenize.d.ts +59 -0
  76. package/dist/input/tokenize.js +681 -0
  77. package/dist/rendering/cell.d.ts +24 -0
  78. package/dist/rendering/cell.js +46 -0
  79. package/dist/rendering/damage.d.ts +35 -0
  80. package/dist/rendering/damage.js +99 -0
  81. package/dist/rendering/grid.d.ts +49 -0
  82. package/dist/rendering/grid.js +216 -0
  83. package/dist/rendering/gridSnapshot.d.ts +33 -0
  84. package/dist/rendering/gridSnapshot.js +32 -0
  85. package/dist/rendering/gridToSvg.d.ts +29 -0
  86. package/dist/rendering/gridToSvg.js +145 -0
  87. package/dist/rendering/terminalRenderer.d.ts +28 -0
  88. package/dist/rendering/terminalRenderer.js +161 -0
  89. package/dist/rendering/terminalScreen.d.ts +25 -0
  90. package/dist/rendering/terminalScreen.js +50 -0
  91. package/package.json +29 -0
@@ -0,0 +1,43 @@
1
+ import { getCharDisplayWidth, getGraphemeDisplayWidth } from "./unicodeWidth.js";
2
+ const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
3
+ const DEFAULT_TAB_SIZE = 4;
4
+ /**
5
+ * Display width of `raw` in terminal columns.
6
+ *
7
+ * Returns **exactly** the same number as
8
+ * `new DisplayLine(raw, tabSize, stopAfter).displayWidth`, but without
9
+ * allocating the per-grapheme slot array or the offset→column `Int32Array`.
10
+ * Used by the horizontal-scrollbar width cache ({@link LineWidthCache}), where
11
+ * only the width matters and building a full `DisplayLine` per document line is
12
+ * what freezes the editor on long lines.
13
+ *
14
+ * `stopAfter` bounds the scan: any grapheme whose start offset is at or past it
15
+ * is ignored (segmentation is lazy, so the tail is never touched). An extreme
16
+ * line therefore costs O(stopAfter), not O(length) — mirroring VS Code's
17
+ * `stopRenderingLineAfter`.
18
+ *
19
+ * The per-grapheme width rules are kept in lock-step with `DisplayLine`'s
20
+ * constructor: tab expands to the next tab stop, `\r` is zero-width, single
21
+ * code units go through {@link getCharDisplayWidth}, multi-code-unit clusters
22
+ * through {@link getGraphemeDisplayWidth}.
23
+ */
24
+ export function measureTextWidth(raw, tabSize = DEFAULT_TAB_SIZE, stopAfter = Infinity) {
25
+ let column = 0;
26
+ for (const { segment, index } of segmenter.segment(raw)) {
27
+ if (index >= stopAfter)
28
+ break;
29
+ if (segment === "\t") {
30
+ column += tabSize - (column % tabSize);
31
+ }
32
+ else if (segment === "\r") {
33
+ // zero width
34
+ }
35
+ else if (segment.length === 1) {
36
+ column += getCharDisplayWidth(segment.charCodeAt(0));
37
+ }
38
+ else {
39
+ column += getGraphemeDisplayWidth(segment);
40
+ }
41
+ }
42
+ return column;
43
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Bit-mask flags for terminal text styles.
3
+ *
4
+ * Combine with bitwise OR: `StyleFlags.Bold | StyleFlags.Italic`
5
+ * Test with bitwise AND: `(style & StyleFlags.Bold) !== 0`
6
+ */
7
+ export declare const StyleFlags: {
8
+ readonly None: 0;
9
+ readonly Bold: number;
10
+ readonly Italic: number;
11
+ readonly Underline: number;
12
+ readonly Undercurl: number;
13
+ readonly Inverse: number;
14
+ readonly Strikethrough: number;
15
+ readonly Dim: number;
16
+ };
17
+ export type StyleFlags = (typeof StyleFlags)[keyof typeof StyleFlags];
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Bit-mask flags for terminal text styles.
3
+ *
4
+ * Combine with bitwise OR: `StyleFlags.Bold | StyleFlags.Italic`
5
+ * Test with bitwise AND: `(style & StyleFlags.Bold) !== 0`
6
+ */
7
+ export const StyleFlags = {
8
+ None: 0,
9
+ Bold: 1 << 0, // 1
10
+ Italic: 1 << 1, // 2
11
+ Underline: 1 << 2, // 4
12
+ Undercurl: 1 << 3, // 8
13
+ Inverse: 1 << 4, // 16
14
+ Strikethrough: 1 << 5, // 32
15
+ Dim: 1 << 6, // 64
16
+ };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Truncate `text` to `maxWidth` columns, putting the ellipsis at the END:
3
+ * "VeryLongFileName.ts" → "VeryLongFile…"
4
+ *
5
+ * The kept prefix is preserved grapheme-for-grapheme (so byte offsets of the
6
+ * prefix are unchanged — important for match highlighting). Returns `text`
7
+ * unchanged when it already fits, and "" when even the ellipsis doesn't fit.
8
+ */
9
+ export declare function truncateEnd(text: string, maxWidth: number, ellipsis?: string): string;
10
+ /**
11
+ * Truncate `text` to `maxWidth` columns, putting the ellipsis in the MIDDLE:
12
+ * "VeryLongFileName" → "VeryL…Name"
13
+ *
14
+ * Returns `text` unchanged when it fits, the ellipsis alone when only one
15
+ * column (its width) is available, and "" when even that doesn't fit.
16
+ */
17
+ export declare function truncateMiddle(text: string, maxWidth: number, ellipsis?: string): string;
18
+ /**
19
+ * Abbreviate a `/`-separated path to fit in `maxWidth` columns, keeping the
20
+ * most useful segments visible. Priority of what stays visible:
21
+ * 1. the first segment,
22
+ * 2. the last segment,
23
+ * 3. then segments from the 2nd onward, filling the remaining space.
24
+ * The ellipsis sits between them, so it usually lands in the middle:
25
+ * "src/components/widgets/popups/menu" → "src/components/…/menu"
26
+ *
27
+ * Returns the path unchanged when it fits. Degrades gracefully on very narrow
28
+ * widths: "…/last", then a middle-truncated last segment.
29
+ */
30
+ export declare function abbreviatePath(path: string, maxWidth: number, ellipsis?: string): string;
@@ -0,0 +1,124 @@
1
+ import { DisplayLine } from "./displayLine.js";
2
+ /**
3
+ * Display-width-aware text truncation helpers.
4
+ *
5
+ * All widths are measured in terminal columns via {@link DisplayLine}, so wide
6
+ * (CJK / emoji) characters and combining marks are handled correctly. The
7
+ * ellipsis is whatever string you pass (default "…", width 1).
8
+ */
9
+ const DEFAULT_ELLIPSIS = "…";
10
+ function width(text) {
11
+ return new DisplayLine(text).displayWidth;
12
+ }
13
+ /** Take the longest leading run of graphemes that fits in `maxWidth` columns. */
14
+ function takePrefix(dl, maxWidth) {
15
+ let w = 0;
16
+ let out = "";
17
+ for (const slot of dl.slots) {
18
+ if (w + slot.displayWidth > maxWidth)
19
+ break;
20
+ out += slot.grapheme;
21
+ w += slot.displayWidth;
22
+ }
23
+ return { text: out, width: w };
24
+ }
25
+ /** Take the longest trailing run of graphemes that fits in `maxWidth` columns. */
26
+ function takeSuffix(dl, maxWidth) {
27
+ let w = 0;
28
+ let out = "";
29
+ for (let i = dl.slots.length - 1; i >= 0; i--) {
30
+ const slot = dl.slots[i];
31
+ if (w + slot.displayWidth > maxWidth)
32
+ break;
33
+ out = slot.grapheme + out;
34
+ w += slot.displayWidth;
35
+ }
36
+ return { text: out, width: w };
37
+ }
38
+ /**
39
+ * Truncate `text` to `maxWidth` columns, putting the ellipsis at the END:
40
+ * "VeryLongFileName.ts" → "VeryLongFile…"
41
+ *
42
+ * The kept prefix is preserved grapheme-for-grapheme (so byte offsets of the
43
+ * prefix are unchanged — important for match highlighting). Returns `text`
44
+ * unchanged when it already fits, and "" when even the ellipsis doesn't fit.
45
+ */
46
+ export function truncateEnd(text, maxWidth, ellipsis = DEFAULT_ELLIPSIS) {
47
+ if (maxWidth <= 0)
48
+ return "";
49
+ const dl = new DisplayLine(text);
50
+ if (dl.displayWidth <= maxWidth)
51
+ return text;
52
+ const ew = width(ellipsis);
53
+ if (maxWidth < ew)
54
+ return "";
55
+ return takePrefix(dl, maxWidth - ew).text + ellipsis;
56
+ }
57
+ /**
58
+ * Truncate `text` to `maxWidth` columns, putting the ellipsis in the MIDDLE:
59
+ * "VeryLongFileName" → "VeryL…Name"
60
+ *
61
+ * Returns `text` unchanged when it fits, the ellipsis alone when only one
62
+ * column (its width) is available, and "" when even that doesn't fit.
63
+ */
64
+ export function truncateMiddle(text, maxWidth, ellipsis = DEFAULT_ELLIPSIS) {
65
+ if (maxWidth <= 0)
66
+ return "";
67
+ const dl = new DisplayLine(text);
68
+ if (dl.displayWidth <= maxWidth)
69
+ return text;
70
+ const ew = width(ellipsis);
71
+ if (maxWidth < ew)
72
+ return "";
73
+ if (maxWidth === ew)
74
+ return ellipsis;
75
+ const budget = maxWidth - ew;
76
+ const headBudget = Math.ceil(budget / 2);
77
+ const tailBudget = budget - headBudget;
78
+ const head = takePrefix(dl, headBudget);
79
+ const tail = takeSuffix(dl, tailBudget);
80
+ return head.text + ellipsis + tail.text;
81
+ }
82
+ /**
83
+ * Abbreviate a `/`-separated path to fit in `maxWidth` columns, keeping the
84
+ * most useful segments visible. Priority of what stays visible:
85
+ * 1. the first segment,
86
+ * 2. the last segment,
87
+ * 3. then segments from the 2nd onward, filling the remaining space.
88
+ * The ellipsis sits between them, so it usually lands in the middle:
89
+ * "src/components/widgets/popups/menu" → "src/components/…/menu"
90
+ *
91
+ * Returns the path unchanged when it fits. Degrades gracefully on very narrow
92
+ * widths: "…/last", then a middle-truncated last segment.
93
+ */
94
+ export function abbreviatePath(path, maxWidth, ellipsis = DEFAULT_ELLIPSIS) {
95
+ if (maxWidth <= 0)
96
+ return "";
97
+ if (width(path) <= maxWidth)
98
+ return path;
99
+ const segments = path.split("/").filter((s) => s.length > 0);
100
+ // One segment (or a degenerate path) has no directory structure to exploit.
101
+ // Two segments can't show "first/…/last" shorter than "first/last" itself,
102
+ // so fall back to middle-truncating the whole string.
103
+ if (segments.length <= 2)
104
+ return truncateMiddle(path, maxWidth, ellipsis);
105
+ const first = segments[0];
106
+ const last = segments[segments.length - 1];
107
+ const base = `${first}/${ellipsis}/${last}`;
108
+ if (width(base) > maxWidth) {
109
+ const tailOnly = `${ellipsis}/${last}`;
110
+ if (width(tailOnly) <= maxWidth)
111
+ return tailOnly;
112
+ return truncateMiddle(last, maxWidth, ellipsis);
113
+ }
114
+ // Greedily grow the visible prefix from the 2nd segment onward, never
115
+ // including the last segment (it is always shown separately).
116
+ let best = 1;
117
+ for (let p = 2; p <= segments.length - 2; p++) {
118
+ const candidate = `${segments.slice(0, p).join("/")}/${ellipsis}/${last}`;
119
+ if (width(candidate) > maxWidth)
120
+ break;
121
+ best = p;
122
+ }
123
+ return `${segments.slice(0, best).join("/")}/${ellipsis}/${last}`;
124
+ }
@@ -0,0 +1 @@
1
+ export declare function reject(): never;
@@ -0,0 +1,3 @@
1
+ export function reject() {
2
+ throw new Error("Unexpected state");
3
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Determines the display width of a Unicode code point in a terminal.
3
+ *
4
+ * Returns:
5
+ * - 0 for control characters, combining marks, zero-width chars
6
+ * - 2 for East Asian Wide / Fullwidth characters and emoji
7
+ * - 1 for everything else
8
+ *
9
+ * Does NOT handle tabs — tab width depends on column position and is
10
+ * computed by DisplayLine.
11
+ */
12
+ export declare function getCharDisplayWidth(codePoint: number): number;
13
+ /**
14
+ * Compute the display width of a grapheme cluster.
15
+ * A cluster may contain multiple code points (e.g. emoji + ZWJ sequences).
16
+ * The width is determined by the widest non-zero-width code point.
17
+ */
18
+ export declare function getGraphemeDisplayWidth(grapheme: string): number;
@@ -0,0 +1,339 @@
1
+ /**
2
+ * Determines the display width of a Unicode code point in a terminal.
3
+ *
4
+ * Returns:
5
+ * - 0 for control characters, combining marks, zero-width chars
6
+ * - 2 for East Asian Wide / Fullwidth characters and emoji
7
+ * - 1 for everything else
8
+ *
9
+ * Does NOT handle tabs — tab width depends on column position and is
10
+ * computed by DisplayLine.
11
+ */
12
+ export function getCharDisplayWidth(codePoint) {
13
+ // Control characters (C0, DEL, C1)
14
+ if (codePoint < 0x20 || (codePoint >= 0x7f && codePoint < 0xa0)) {
15
+ return 0;
16
+ }
17
+ // Zero-width characters
18
+ if (isZeroWidth(codePoint)) {
19
+ return 0;
20
+ }
21
+ // Wide / Fullwidth characters
22
+ if (isWide(codePoint)) {
23
+ return 2;
24
+ }
25
+ return 1;
26
+ }
27
+ function isZeroWidth(cp) {
28
+ // Combining Diacritical Marks
29
+ if (cp >= 0x0300 && cp <= 0x036f)
30
+ return true;
31
+ // Combining Diacritical Marks Extended
32
+ if (cp >= 0x1ab0 && cp <= 0x1aff)
33
+ return true;
34
+ // Combining Diacritical Marks Supplement
35
+ if (cp >= 0x1dc0 && cp <= 0x1dff)
36
+ return true;
37
+ // Combining Diacritical Marks for Symbols
38
+ if (cp >= 0x20d0 && cp <= 0x20ff)
39
+ return true;
40
+ // Combining Half Marks
41
+ if (cp >= 0xfe20 && cp <= 0xfe2f)
42
+ return true;
43
+ // Thai combining marks
44
+ if (cp >= 0x0e31 && cp <= 0x0e3a)
45
+ return true;
46
+ if (cp >= 0x0e47 && cp <= 0x0e4e)
47
+ return true;
48
+ // General combining marks (Mn, Mc, Me categories — major blocks)
49
+ // Hebrew points
50
+ if (cp >= 0x0591 && cp <= 0x05bd)
51
+ return true;
52
+ if (cp === 0x05bf)
53
+ return true;
54
+ if (cp >= 0x05c1 && cp <= 0x05c2)
55
+ return true;
56
+ if (cp >= 0x05c4 && cp <= 0x05c5)
57
+ return true;
58
+ if (cp === 0x05c7)
59
+ return true;
60
+ // Arabic combining
61
+ if (cp >= 0x0610 && cp <= 0x061a)
62
+ return true;
63
+ if (cp >= 0x064b && cp <= 0x065f)
64
+ return true;
65
+ if (cp === 0x0670)
66
+ return true;
67
+ if (cp >= 0x06d6 && cp <= 0x06dc)
68
+ return true;
69
+ if (cp >= 0x06df && cp <= 0x06e4)
70
+ return true;
71
+ if (cp >= 0x06e7 && cp <= 0x06e8)
72
+ return true;
73
+ if (cp >= 0x06ea && cp <= 0x06ed)
74
+ return true;
75
+ // Devanagari combining
76
+ if (cp >= 0x0900 && cp <= 0x0903)
77
+ return true;
78
+ if (cp >= 0x093a && cp <= 0x094f)
79
+ return true;
80
+ if (cp >= 0x0951 && cp <= 0x0957)
81
+ return true;
82
+ // Hangul Jamo combining (medial/final)
83
+ if (cp >= 0x1160 && cp <= 0x11ff)
84
+ return true;
85
+ // Variation Selectors
86
+ if (cp >= 0xfe00 && cp <= 0xfe0f)
87
+ return true;
88
+ // Variation Selectors Supplement
89
+ if (cp >= 0xe0100 && cp <= 0xe01ef)
90
+ return true;
91
+ // Zero Width Space, ZWNJ, ZWJ, Soft Hyphen
92
+ if (cp === 0x200b || cp === 0x200c || cp === 0x200d || cp === 0x00ad)
93
+ return true;
94
+ // Word Joiner, BOM
95
+ if (cp === 0x2060 || cp === 0xfeff)
96
+ return true;
97
+ // CGJ (Combining Grapheme Joiner)
98
+ /* v8 ignore start -- unreachable: U+034F already matches the Combining Diacritical Marks range (0x0300..0x036f) above */
99
+ if (cp === 0x034f)
100
+ return true;
101
+ /* v8 ignore stop */
102
+ return false;
103
+ }
104
+ function isWide(cp) {
105
+ // CJK Radicals Supplement .. Kangxi Radicals
106
+ if (cp >= 0x2e80 && cp <= 0x2fdf)
107
+ return true;
108
+ // Ideographic Description Characters .. CJK Symbols and Punctuation
109
+ if (cp >= 0x2ff0 && cp <= 0x303e)
110
+ return true;
111
+ // Hiragana, Katakana, Bopomofo, Hangul Compatibility Jamo, Kanbun, etc.
112
+ if (cp >= 0x3040 && cp <= 0x33bf)
113
+ return true;
114
+ // CJK Compatibility (3300–33FF already covered), CJK Unified Ideographs Extension A
115
+ if (cp >= 0x33c0 && cp <= 0x4dbf)
116
+ return true;
117
+ // CJK Unified Ideographs
118
+ if (cp >= 0x4e00 && cp <= 0x9fff)
119
+ return true;
120
+ // Yi Syllables, Yi Radicals
121
+ if (cp >= 0xa000 && cp <= 0xa4cf)
122
+ return true;
123
+ // Hangul Syllables
124
+ if (cp >= 0xac00 && cp <= 0xd7a3)
125
+ return true;
126
+ // CJK Compatibility Ideographs
127
+ if (cp >= 0xf900 && cp <= 0xfaff)
128
+ return true;
129
+ // Vertical Forms
130
+ if (cp >= 0xfe10 && cp <= 0xfe19)
131
+ return true;
132
+ // CJK Compatibility Forms
133
+ if (cp >= 0xfe30 && cp <= 0xfe6f)
134
+ return true;
135
+ // Fullwidth forms (excluding halfwidth katakana)
136
+ if (cp >= 0xff01 && cp <= 0xff60)
137
+ return true;
138
+ if (cp >= 0xffe0 && cp <= 0xffe6)
139
+ return true;
140
+ // CJK Unified Ideographs Extension B .. Extension H (SIP/TIP)
141
+ if (cp >= 0x20000 && cp <= 0x3134f)
142
+ return true;
143
+ // Emoji — only code points with Emoji_Presentation = Yes.
144
+ // Source: Unicode 15 emoji-data.txt
145
+ //
146
+ // Miscellaneous Symbols and Pictographs (1F300-1F5FF)
147
+ // Emoji_Presentation=Yes sub-ranges only (text-presentation chars excluded):
148
+ if (cp >= 0x1f300 && cp <= 0x1f320)
149
+ return true; // weather, misc
150
+ if (cp >= 0x1f32d && cp <= 0x1f335)
151
+ return true; // food
152
+ if (cp >= 0x1f337 && cp <= 0x1f37c)
153
+ return true; // food, plants
154
+ if (cp >= 0x1f37e && cp <= 0x1f393)
155
+ return true; // activities
156
+ if (cp >= 0x1f3a0 && cp <= 0x1f3ca)
157
+ return true; // entertainment
158
+ if (cp >= 0x1f3cf && cp <= 0x1f3d3)
159
+ return true; // sports
160
+ if (cp >= 0x1f3e0 && cp <= 0x1f3f0)
161
+ return true; // buildings
162
+ if (cp === 0x1f3f4)
163
+ return true; // BLACK FLAG
164
+ if (cp >= 0x1f3f8 && cp <= 0x1f43e)
165
+ return true; // animals, nature
166
+ if (cp === 0x1f440)
167
+ return true; // EYES
168
+ if (cp >= 0x1f442 && cp <= 0x1f4fc)
169
+ return true; // objects
170
+ if (cp >= 0x1f4ff && cp <= 0x1f53d)
171
+ return true; // more objects
172
+ if (cp >= 0x1f54b && cp <= 0x1f54e)
173
+ return true; // religious
174
+ if (cp >= 0x1f550 && cp <= 0x1f567)
175
+ return true; // clocks
176
+ if (cp === 0x1f57a)
177
+ return true; // MAN DANCING
178
+ if (cp >= 0x1f595 && cp <= 0x1f596)
179
+ return true; // hand gestures
180
+ if (cp === 0x1f5a4)
181
+ return true; // BLACK HEART
182
+ if (cp >= 0x1f5fb && cp <= 0x1f5ff)
183
+ return true; // MOUNT FUJI..MOYAI
184
+ // Emoticons (1F600-1F64F) — all Emoji_Presentation=Yes
185
+ if (cp >= 0x1f600 && cp <= 0x1f64f)
186
+ return true;
187
+ // Transport and Map Symbols (1F680-1F6FF) — Emoji_Presentation=Yes sub-ranges
188
+ if (cp >= 0x1f680 && cp <= 0x1f6ca)
189
+ return true;
190
+ if (cp === 0x1f6cc)
191
+ return true; // 🛌 SLEEPING ACCOMMODATION
192
+ if (cp >= 0x1f6d0 && cp <= 0x1f6d2)
193
+ return true;
194
+ if (cp >= 0x1f6d5 && cp <= 0x1f6d7)
195
+ return true;
196
+ if (cp === 0x1f6dc)
197
+ return true; // 🛜 WIRELESS (added Unicode 15)
198
+ if (cp >= 0x1f6dd && cp <= 0x1f6df)
199
+ return true;
200
+ if (cp >= 0x1f6eb && cp <= 0x1f6ec)
201
+ return true;
202
+ if (cp >= 0x1f6f4 && cp <= 0x1f6fc)
203
+ return true;
204
+ // Geometric Shapes Extended (1F7E0–1F7EB) — colored circles/squares 🟠🟡🟢🔵🟣🟤 🟥🟦🟨🟩🟧🟪🟫
205
+ if (cp >= 0x1f7e0 && cp <= 0x1f7eb)
206
+ return true;
207
+ if (cp === 0x1f7f0)
208
+ return true; // 🟰 HEAVY EQUALS SIGN (added Unicode 14)
209
+ // Supplemental Symbols and Pictographs
210
+ if (cp >= 0x1f900 && cp <= 0x1f9ff)
211
+ return true;
212
+ // Symbols and Pictographs Extended-A
213
+ if (cp >= 0x1fa00 && cp <= 0x1fa6f)
214
+ return true;
215
+ // Symbols and Pictographs Extended-B (added Unicode 14+)
216
+ if (cp >= 0x1fa70 && cp <= 0x1faff)
217
+ return true;
218
+ // Dingbats (2700–27BF) — only the Emoji_Presentation=Yes code points are wide.
219
+ // The rest (e.g. ✕ U+2715, ✓ U+2713, scissors U+2700) are narrow text symbols.
220
+ // Source: Unicode 15 emoji-data.txt
221
+ if (cp === 0x2705)
222
+ return true; // ✅ WHITE HEAVY CHECK MARK
223
+ if (cp >= 0x270a && cp <= 0x270b)
224
+ return true; // ✊ RAISED FIST .. ✋ RAISED HAND
225
+ if (cp === 0x2728)
226
+ return true; // ✨ SPARKLES
227
+ if (cp === 0x274c)
228
+ return true; // ❌ CROSS MARK
229
+ if (cp === 0x274e)
230
+ return true; // ❎ NEGATIVE SQUARED CROSS MARK
231
+ if (cp >= 0x2753 && cp <= 0x2755)
232
+ return true; // ❓ .. ❕ question / exclamation ornaments
233
+ if (cp === 0x2757)
234
+ return true; // ❗ HEAVY EXCLAMATION MARK SYMBOL
235
+ if (cp >= 0x2795 && cp <= 0x2797)
236
+ return true; // ➕ ➖ ➗ heavy plus / minus / division
237
+ if (cp === 0x27b0)
238
+ return true; // ➰ CURLY LOOP
239
+ if (cp === 0x27bf)
240
+ return true; // ➿ DOUBLE CURLY LOOP
241
+ // BMP emoji with Emoji_Presentation=Yes that live outside the Dingbats and
242
+ // SMP blocks above — Miscellaneous Technical, Misc Symbols (2600–26FF),
243
+ // Geometric Shapes and the 2B00 block. Terminals render all of these two
244
+ // columns wide; omitting them desynced our column model from the terminal
245
+ // and left glyph tails as scrolling artifacts (e.g. ⭐ U+2B50, ⚡ U+26A1).
246
+ // Source: Unicode 15 emoji-data.txt (Emoji_Presentation=Yes).
247
+ if (cp >= 0x231a && cp <= 0x231b)
248
+ return true; // ⌚ WATCH .. ⌛ HOURGLASS
249
+ if (cp >= 0x23e9 && cp <= 0x23ec)
250
+ return true; // ⏩⏪⏫⏬ fast-forward/rewind
251
+ if (cp === 0x23f0)
252
+ return true; // ⏰ ALARM CLOCK
253
+ if (cp === 0x23f3)
254
+ return true; // ⏳ HOURGLASS WITH FLOWING SAND
255
+ if (cp >= 0x25fd && cp <= 0x25fe)
256
+ return true; // ◽ ◾ medium-small squares
257
+ if (cp >= 0x2614 && cp <= 0x2615)
258
+ return true; // ☔ UMBRELLA WITH RAIN .. ☕ HOT BEVERAGE
259
+ if (cp >= 0x2648 && cp <= 0x2653)
260
+ return true; // ♈..♓ zodiac signs
261
+ if (cp === 0x267f)
262
+ return true; // ♿ WHEELCHAIR SYMBOL
263
+ if (cp === 0x2693)
264
+ return true; // ⚓ ANCHOR
265
+ if (cp === 0x26a1)
266
+ return true; // ⚡ HIGH VOLTAGE SIGN
267
+ if (cp >= 0x26aa && cp <= 0x26ab)
268
+ return true; // ⚪ ⚫ medium circles
269
+ if (cp >= 0x26bd && cp <= 0x26be)
270
+ return true; // ⚽ SOCCER BALL .. ⚾ BASEBALL
271
+ if (cp >= 0x26c4 && cp <= 0x26c5)
272
+ return true; // ⛄ SNOWMAN .. ⛅ SUN BEHIND CLOUD
273
+ if (cp === 0x26ce)
274
+ return true; // ⛎ OPHIUCHUS
275
+ if (cp === 0x26d4)
276
+ return true; // ⛔ NO ENTRY
277
+ if (cp === 0x26ea)
278
+ return true; // ⛪ CHURCH
279
+ if (cp >= 0x26f2 && cp <= 0x26f3)
280
+ return true; // ⛲ FOUNTAIN .. ⛳ FLAG IN HOLE
281
+ if (cp === 0x26f5)
282
+ return true; // ⛵ SAILBOAT
283
+ if (cp === 0x26fa)
284
+ return true; // ⛺ TENT
285
+ if (cp === 0x26fd)
286
+ return true; // ⛽ FUEL PUMP
287
+ if (cp >= 0x2b1b && cp <= 0x2b1c)
288
+ return true; // ⬛ ⬜ large squares
289
+ if (cp === 0x2b50)
290
+ return true; // ⭐ WHITE MEDIUM STAR
291
+ if (cp === 0x2b55)
292
+ return true; // ⭕ HEAVY LARGE CIRCLE
293
+ // Enclosed Alphanumeric Supplement (circled numbers, emoji)
294
+ if (cp >= 0x1f100 && cp <= 0x1f1ff)
295
+ return true;
296
+ // Mahjong Tiles / Playing Cards — Emoji_Presentation=Yes code points
297
+ if (cp === 0x1f004)
298
+ return true; // 🀄 MAHJONG TILE RED DRAGON
299
+ if (cp === 0x1f0cf)
300
+ return true; // 🃏 PLAYING CARD BLACK JOKER
301
+ // Enclosed Ideographic Supplement (1F200–1F2FF) — Emoji_Presentation=Yes squared CJK
302
+ if (cp === 0x1f201)
303
+ return true; // 🈁 SQUARED KATAKANA KOKO
304
+ if (cp === 0x1f21a)
305
+ return true; // 🈚 SQUARED CJK UNIFIED IDEOGRAPH-7121
306
+ if (cp === 0x1f22f)
307
+ return true; // 🈯 SQUARED CJK UNIFIED IDEOGRAPH-6307
308
+ if (cp >= 0x1f232 && cp <= 0x1f236)
309
+ return true; // 🈲🈳🈴🈵🈶
310
+ if (cp >= 0x1f238 && cp <= 0x1f23a)
311
+ return true; // 🈸🈹🈺
312
+ if (cp >= 0x1f250 && cp <= 0x1f251)
313
+ return true; // 🉐🉑
314
+ return false;
315
+ }
316
+ /**
317
+ * Compute the display width of a grapheme cluster.
318
+ * A cluster may contain multiple code points (e.g. emoji + ZWJ sequences).
319
+ * The width is determined by the widest non-zero-width code point.
320
+ */
321
+ export function getGraphemeDisplayWidth(grapheme) {
322
+ // VS16 (U+FE0F, Variation Selector-16) forces emoji presentation.
323
+ // Any cluster containing VS16 is rendered as wide (2 columns) by terminals.
324
+ if (grapheme.includes("\uFE0F")) {
325
+ return 2;
326
+ }
327
+ let width = 0;
328
+ for (const ch of grapheme) {
329
+ /* v8 ignore start -- defensive: `ch` from a string for-of is always a non-empty code point, so codePointAt(0) is never undefined */
330
+ const cp = ch.codePointAt(0) ?? 0;
331
+ /* v8 ignore stop */
332
+ const w = getCharDisplayWidth(cp);
333
+ if (w > width)
334
+ width = w;
335
+ }
336
+ // A grapheme cluster always occupies at least 1 column unless it's empty
337
+ // or entirely zero-width (which shouldn't happen for properly segmented graphemes).
338
+ return width || (grapheme.length > 0 ? 1 : 0);
339
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Толщина рамки drawBox в ячейках. В клеточном терминале рамка — это
3
+ * box-drawing-глиф, физически всегда 1 ячейка; константа никогда не сменит
4
+ * значение. Её ценность — имя: все места, где рамка участвует в формулах
5
+ * размеров/смещений, находятся грепом и отличимы от настоящих отступов.
6
+ */
7
+ export declare const BORDER_THICKNESS = 1;
8
+ /**
9
+ * Набор box-drawing глифов для рамки виджета. Используется общим хелпером
10
+ * {@link RenderContext.drawBox}, чтобы отрисовка рамок не дублировалась по
11
+ * виджетам и стиль углов был единым.
12
+ *
13
+ * `leftJoint`/`rightJoint` — T-коннекторы (`├`/`┤`) для строк-сепараторов
14
+ * (ряд-разделитель внутри рамки).
15
+ */
16
+ export interface BorderStyle {
17
+ topLeft: string;
18
+ topRight: string;
19
+ bottomLeft: string;
20
+ bottomRight: string;
21
+ horizontal: string;
22
+ vertical: string;
23
+ leftJoint: string;
24
+ rightJoint: string;
25
+ }
26
+ /** Прямые углы `┌┐└┘`. Пресет на будущее (по умолчанию не используется). */
27
+ export declare const BORDER_SINGLE: BorderStyle;
28
+ /** Скруглённые углы `╭╮╰╯` (nvchad-стиль) — канонический стиль рамок во всех оверлеях Vexx. */
29
+ export declare const BORDER_ROUNDED: BorderStyle;
30
+ /** Двойные линии `╔╗╚╝`. Пресет на будущее (по умолчанию не используется). */
31
+ export declare const BORDER_DOUBLE: BorderStyle;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Толщина рамки drawBox в ячейках. В клеточном терминале рамка — это
3
+ * box-drawing-глиф, физически всегда 1 ячейка; константа никогда не сменит
4
+ * значение. Её ценность — имя: все места, где рамка участвует в формулах
5
+ * размеров/смещений, находятся грепом и отличимы от настоящих отступов.
6
+ */
7
+ export const BORDER_THICKNESS = 1;
8
+ /** Прямые углы `┌┐└┘`. Пресет на будущее (по умолчанию не используется). */
9
+ export const BORDER_SINGLE = {
10
+ topLeft: "┌",
11
+ topRight: "┐",
12
+ bottomLeft: "└",
13
+ bottomRight: "┘",
14
+ horizontal: "─",
15
+ vertical: "│",
16
+ leftJoint: "├",
17
+ rightJoint: "┤",
18
+ };
19
+ /** Скруглённые углы `╭╮╰╯` (nvchad-стиль) — канонический стиль рамок во всех оверлеях Vexx. */
20
+ export const BORDER_ROUNDED = {
21
+ topLeft: "╭",
22
+ topRight: "╮",
23
+ bottomLeft: "╰",
24
+ bottomRight: "╯",
25
+ horizontal: "─",
26
+ vertical: "│",
27
+ leftJoint: "├",
28
+ rightJoint: "┤",
29
+ };
30
+ /** Двойные линии `╔╗╚╝`. Пресет на будущее (по умолчанию не используется). */
31
+ export const BORDER_DOUBLE = {
32
+ topLeft: "╔",
33
+ topRight: "╗",
34
+ bottomLeft: "╚",
35
+ bottomRight: "╝",
36
+ horizontal: "═",
37
+ vertical: "║",
38
+ leftJoint: "╠",
39
+ rightJoint: "╣",
40
+ };