@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.
- package/dist/backend/iTerminalBackend.d.ts +41 -0
- package/dist/backend/iTerminalBackend.js +1 -0
- package/dist/common/colorUtils.d.ts +19 -0
- package/dist/common/colorUtils.js +30 -0
- package/dist/common/displayLine.d.ts +72 -0
- package/dist/common/displayLine.js +190 -0
- package/dist/common/disposable.d.ts +9 -0
- package/dist/common/disposable.js +18 -0
- package/dist/common/geometryPromitives.d.ts +47 -0
- package/dist/common/geometryPromitives.js +115 -0
- package/dist/common/iTerminalSurface.d.ts +68 -0
- package/dist/common/iTerminalSurface.js +8 -0
- package/dist/common/measureTextWidth.d.ts +21 -0
- package/dist/common/measureTextWidth.js +43 -0
- package/dist/common/styleFlags.d.ts +17 -0
- package/dist/common/styleFlags.js +16 -0
- package/dist/common/textTruncation.d.ts +30 -0
- package/dist/common/textTruncation.js +124 -0
- package/dist/common/typingUtils.d.ts +1 -0
- package/dist/common/typingUtils.js +3 -0
- package/dist/common/unicodeWidth.d.ts +18 -0
- package/dist/common/unicodeWidth.js +339 -0
- package/dist/dom/borderStyle.d.ts +31 -0
- package/dist/dom/borderStyle.js +40 -0
- package/dist/dom/compositeElement.d.ts +20 -0
- package/dist/dom/compositeElement.js +44 -0
- package/dist/dom/events/contextMenuEventSource.d.ts +17 -0
- package/dist/dom/events/contextMenuEventSource.js +44 -0
- package/dist/dom/events/focusManager.d.ts +13 -0
- package/dist/dom/events/focusManager.js +56 -0
- package/dist/dom/events/mouseEventDispatcher.d.ts +24 -0
- package/dist/dom/events/mouseEventDispatcher.js +177 -0
- package/dist/dom/events/tuiEventBase.d.ts +25 -0
- package/dist/dom/events/tuiEventBase.js +39 -0
- package/dist/dom/events/tuiFocusEvent.d.ts +6 -0
- package/dist/dom/events/tuiFocusEvent.js +8 -0
- package/dist/dom/events/tuiKeyboardEvent.d.ts +21 -0
- package/dist/dom/events/tuiKeyboardEvent.js +20 -0
- package/dist/dom/events/tuiMouseEvent.d.ts +40 -0
- package/dist/dom/events/tuiMouseEvent.js +32 -0
- package/dist/dom/events/tuiPasteEvent.d.ts +10 -0
- package/dist/dom/events/tuiPasteEvent.js +13 -0
- package/dist/dom/overlayLayer.d.ts +92 -0
- package/dist/dom/overlayLayer.js +343 -0
- package/dist/dom/styles/index.d.ts +4 -0
- package/dist/dom/styles/index.js +2 -0
- package/dist/dom/styles/styleTokens.d.ts +114 -0
- package/dist/dom/styles/styleTokens.js +122 -0
- package/dist/dom/styles/tuiStyle.d.ts +75 -0
- package/dist/dom/styles/tuiStyle.js +121 -0
- package/dist/dom/tuiApplication.d.ts +57 -0
- package/dist/dom/tuiApplication.js +231 -0
- package/dist/dom/tuiElement.d.ts +528 -0
- package/dist/dom/tuiElement.js +1168 -0
- package/dist/dom/tuiSelector.d.ts +9 -0
- package/dist/dom/tuiSelector.js +82 -0
- package/dist/dom/validateTree.d.ts +42 -0
- package/dist/dom/validateTree.js +125 -0
- package/dist/input/convertToken.d.ts +3 -0
- package/dist/input/convertToken.js +114 -0
- package/dist/input/keyEvent.d.ts +46 -0
- package/dist/input/keyEvent.js +26 -0
- package/dist/input/keyInputParser.d.ts +72 -0
- package/dist/input/keyInputParser.js +249 -0
- package/dist/input/mouseTracking.d.ts +18 -0
- package/dist/input/mouseTracking.js +18 -0
- package/dist/input/parseInput.d.ts +13 -0
- package/dist/input/parseInput.js +17 -0
- package/dist/input/rawTerminalToken.d.ts +142 -0
- package/dist/input/rawTerminalToken.js +2 -0
- package/dist/input/serializeKey.d.ts +14 -0
- package/dist/input/serializeKey.js +179 -0
- package/dist/input/serializeMouse.d.ts +26 -0
- package/dist/input/serializeMouse.js +38 -0
- package/dist/input/tokenize.d.ts +59 -0
- package/dist/input/tokenize.js +681 -0
- package/dist/rendering/cell.d.ts +24 -0
- package/dist/rendering/cell.js +46 -0
- package/dist/rendering/damage.d.ts +35 -0
- package/dist/rendering/damage.js +99 -0
- package/dist/rendering/grid.d.ts +49 -0
- package/dist/rendering/grid.js +216 -0
- package/dist/rendering/gridSnapshot.d.ts +33 -0
- package/dist/rendering/gridSnapshot.js +32 -0
- package/dist/rendering/gridToSvg.d.ts +29 -0
- package/dist/rendering/gridToSvg.js +145 -0
- package/dist/rendering/terminalRenderer.d.ts +28 -0
- package/dist/rendering/terminalRenderer.js +161 -0
- package/dist/rendering/terminalScreen.d.ts +25 -0
- package/dist/rendering/terminalScreen.js +50 -0
- 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,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
|
+
};
|