@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,41 @@
1
+ import type { Point, Size } from "../common/geometryPromitives.js";
2
+ import type { KeyPressEvent } from "../input/keyEvent.js";
3
+ import type { MouseToken } from "../input/rawTerminalToken.js";
4
+ import type { Grid } from "../rendering/grid.js";
5
+ /**
6
+ * Unified abstraction over terminal I/O.
7
+ *
8
+ * Two implementations:
9
+ * - `NodeTerminalBackend` — real process.stdin/stdout
10
+ * - `MockTerminalBackend` — in-memory for tests
11
+ */
12
+ export interface ITerminalBackend {
13
+ /** Subscribe to parsed keyboard input */
14
+ onInput(callback: (event: KeyPressEvent) => void): void;
15
+ /** Subscribe to raw mouse events */
16
+ onMouse(callback: (event: MouseToken) => void): void;
17
+ /** Subscribe to bracketed-paste text blocks (whole paste delivered as one string) */
18
+ onPaste(callback: (text: string) => void): void;
19
+ /** Subscribe to terminal resize events */
20
+ onResize(callback: (size: Size) => void): void;
21
+ /** Subscribe to OSC response sequences from the terminal (e.g. OSC 52 clipboard replies) */
22
+ onOscResponse(callback: (code: number, data: string) => void): void;
23
+ /**
24
+ * Asynchronously probe whether the terminal supports the Kitty keyboard protocol.
25
+ * The request/response escape-sequence dance is fully encapsulated by the backend;
26
+ * `onResult` fires exactly once with the answer (or `false` if the terminal doesn't
27
+ * reply within the probe window). Fire-and-forget — callers must not block on it.
28
+ */
29
+ probeKeyboardProtocol(onResult: (supported: boolean) => void): void;
30
+ /**
31
+ * Render a frame: receive the current grid and cursor position.
32
+ * The backend decides how to output it (ANSI diffing, simple copy, etc.).
33
+ */
34
+ renderFrame(grid: Grid, cursorPosition: Point | null): void;
35
+ /** Current terminal dimensions */
36
+ getSize(): Size;
37
+ /** Initialize terminal: alternate screen, raw mode, hide cursor, etc. */
38
+ setup(): void;
39
+ /** Restore terminal state: show cursor, normal screen, etc. */
40
+ teardown(): void;
41
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Sentinel value meaning "use terminal's default color".
3
+ * Must be negative so it's never confused with a packed RGB value (0x000000–0xFFFFFF).
4
+ */
5
+ export declare const DEFAULT_COLOR = -1;
6
+ /** Pack three 8-bit channels into a single 24-bit integer. */
7
+ export declare function packRgb(r: number, g: number, b: number): number;
8
+ /** Extract the red channel (bits 16–23). */
9
+ export declare function unpackR(color: number): number;
10
+ /** Extract the green channel (bits 8–15). */
11
+ export declare function unpackG(color: number): number;
12
+ /** Extract the blue channel (bits 0–7). */
13
+ export declare function unpackB(color: number): number;
14
+ /**
15
+ * Накладывает полупрозрачный `fg` на непрозрачную подложку `bg` и возвращает
16
+ * непрозрачный результат: терминал альфы не умеет, поэтому композитинг
17
+ * выполняется заранее (`alpha` — доля 0..1).
18
+ */
19
+ export declare function blendRgb(fg: number, bg: number, alpha: number): number;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Sentinel value meaning "use terminal's default color".
3
+ * Must be negative so it's never confused with a packed RGB value (0x000000–0xFFFFFF).
4
+ */
5
+ export const DEFAULT_COLOR = -1;
6
+ /** Pack three 8-bit channels into a single 24-bit integer. */
7
+ export function packRgb(r, g, b) {
8
+ return (r << 16) | (g << 8) | b;
9
+ }
10
+ /** Extract the red channel (bits 16–23). */
11
+ export function unpackR(color) {
12
+ return (color >> 16) & 0xff;
13
+ }
14
+ /** Extract the green channel (bits 8–15). */
15
+ export function unpackG(color) {
16
+ return (color >> 8) & 0xff;
17
+ }
18
+ /** Extract the blue channel (bits 0–7). */
19
+ export function unpackB(color) {
20
+ return color & 0xff;
21
+ }
22
+ /**
23
+ * Накладывает полупрозрачный `fg` на непрозрачную подложку `bg` и возвращает
24
+ * непрозрачный результат: терминал альфы не умеет, поэтому композитинг
25
+ * выполняется заранее (`alpha` — доля 0..1).
26
+ */
27
+ export function blendRgb(fg, bg, alpha) {
28
+ const mix = (f, b) => Math.round(f * alpha + b * (1 - alpha));
29
+ return packRgb(mix(unpackR(fg), unpackR(bg)), mix(unpackG(fg), unpackG(bg)), mix(unpackB(fg), unpackB(bg)));
30
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * A single visual unit in a display line — one grapheme cluster with its
3
+ * mapping between string offset and screen column.
4
+ */
5
+ export interface GraphemeSlot {
6
+ /** The grapheme cluster string (may contain multiple code points) */
7
+ readonly grapheme: string;
8
+ /** Display width in terminal columns (1, 2, or variable for tabs) */
9
+ readonly displayWidth: number;
10
+ /** Start index in the original string (code unit offset) */
11
+ readonly offset: number;
12
+ /** Length in code units in the original string */
13
+ readonly length: number;
14
+ }
15
+ /**
16
+ * Maps a raw document string to an array of grapheme slots with correct
17
+ * display widths, providing bidirectional offset↔column conversion.
18
+ *
19
+ * Handles: tabs, \\r, emoji, CJK wide chars, combining marks, surrogate pairs.
20
+ */
21
+ export declare class DisplayLine {
22
+ /** Ordered array of grapheme slots */
23
+ readonly slots: readonly GraphemeSlot[];
24
+ /** Total display width in terminal columns */
25
+ readonly displayWidth: number;
26
+ /**
27
+ * `true`, если строка была длиннее `stopAfter` и разобран лишь префикс —
28
+ * рендер по этому флагу рисует маркер обрезки. При `stopAfter = Infinity`
29
+ * (дефолт) всегда `false`.
30
+ */
31
+ readonly isTruncated: boolean;
32
+ private readonly columnMap;
33
+ private readonly rawLength;
34
+ /**
35
+ * @param stopAfter Максимум code units, которые разбираем. За порогом
36
+ * сегментация обрывается (лениво — `Intl.Segmenter` не трогает хвост), а
37
+ * `columnMap` аллоцируется по длине **префикса**, а не всей строки, — иначе
38
+ * одна мегабайтная строка съедала бы мегабайтный `Int32Array`. Дефолт
39
+ * `Infinity` сохраняет прежнее поведение для всех не-редакторных вызовов.
40
+ */
41
+ constructor(raw: string, tabSize?: number, stopAfter?: number);
42
+ /**
43
+ * Convert a string offset (code unit index) to a display column.
44
+ * For offsets pointing into the middle of a grapheme cluster,
45
+ * returns the column of that cluster's start.
46
+ */
47
+ offsetToColumn(offset: number): number;
48
+ /**
49
+ * Convert a display column to a string offset.
50
+ * If the column falls on the second cell of a wide character,
51
+ * returns the offset of that wide character.
52
+ */
53
+ columnToOffset(column: number): number;
54
+ /**
55
+ * Get the grapheme slot that occupies the given display column,
56
+ * or undefined if the column is out of range.
57
+ */
58
+ graphemeAtColumn(column: number): GraphemeSlot | undefined;
59
+ /**
60
+ * Get the character to render at a given display column.
61
+ * - For the first column of a wide char: returns the grapheme.
62
+ * - For the second column of a wide char: returns "" (continuation).
63
+ * - For a tab: returns " " (space).
64
+ * - For out-of-range: returns " ".
65
+ */
66
+ charAtColumn(column: number): string;
67
+ /**
68
+ * Find the slot index for the grapheme at the given string offset,
69
+ * or -1 if out of range.
70
+ */
71
+ slotIndexAtOffset(offset: number): number;
72
+ }
@@ -0,0 +1,190 @@
1
+ import { getCharDisplayWidth, getGraphemeDisplayWidth } from "./unicodeWidth.js";
2
+ const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
3
+ const DEFAULT_TAB_SIZE = 4;
4
+ /**
5
+ * Maps a raw document string to an array of grapheme slots with correct
6
+ * display widths, providing bidirectional offset↔column conversion.
7
+ *
8
+ * Handles: tabs, \\r, emoji, CJK wide chars, combining marks, surrogate pairs.
9
+ */
10
+ export class DisplayLine {
11
+ /** Ordered array of grapheme slots */
12
+ slots;
13
+ /** Total display width in terminal columns */
14
+ displayWidth;
15
+ /**
16
+ * `true`, если строка была длиннее `stopAfter` и разобран лишь префикс —
17
+ * рендер по этому флагу рисует маркер обрезки. При `stopAfter = Infinity`
18
+ * (дефолт) всегда `false`.
19
+ */
20
+ isTruncated;
21
+ columnMap;
22
+ rawLength;
23
+ /**
24
+ * @param stopAfter Максимум code units, которые разбираем. За порогом
25
+ * сегментация обрывается (лениво — `Intl.Segmenter` не трогает хвост), а
26
+ * `columnMap` аллоцируется по длине **префикса**, а не всей строки, — иначе
27
+ * одна мегабайтная строка съедала бы мегабайтный `Int32Array`. Дефолт
28
+ * `Infinity` сохраняет прежнее поведение для всех не-редакторных вызовов.
29
+ */
30
+ constructor(raw, tabSize = DEFAULT_TAB_SIZE, stopAfter = Infinity) {
31
+ const slots = [];
32
+ let column = 0;
33
+ // Длина разобранного префикса: либо вся строка, либо offset первой
34
+ // отброшенной графемы (сегменты непрерывны, поэтому это ровно граница).
35
+ let scannedLength = raw.length;
36
+ let truncated = false;
37
+ for (const { segment, index } of segmenter.segment(raw)) {
38
+ if (index >= stopAfter) {
39
+ truncated = true;
40
+ scannedLength = index;
41
+ break;
42
+ }
43
+ let displayWidth;
44
+ if (segment === "\t") {
45
+ displayWidth = tabSize - (column % tabSize);
46
+ }
47
+ else if (segment === "\r") {
48
+ displayWidth = 0;
49
+ }
50
+ else if (segment.length === 1) {
51
+ displayWidth = getCharDisplayWidth(segment.charCodeAt(0));
52
+ }
53
+ else {
54
+ displayWidth = getGraphemeDisplayWidth(segment);
55
+ }
56
+ slots.push({
57
+ grapheme: segment,
58
+ displayWidth,
59
+ offset: index,
60
+ length: segment.length,
61
+ });
62
+ column += displayWidth;
63
+ }
64
+ this.slots = slots;
65
+ this.displayWidth = column;
66
+ this.isTruncated = truncated;
67
+ // За порогом строка ведёт себя как оканчивающаяся на `scannedLength`:
68
+ // офсеты в хвосте клампятся к `displayWidth` (курсор прилипает к маркеру).
69
+ this.rawLength = scannedLength;
70
+ // Build reverse lookup: for each code unit offset → display column
71
+ // This allows O(1) offsetToColumn lookups.
72
+ const map = new Int32Array(scannedLength + 1);
73
+ column = 0;
74
+ for (const slot of slots) {
75
+ for (let i = 0; i < slot.length; i++) {
76
+ map[slot.offset + i] = column;
77
+ }
78
+ column += slot.displayWidth;
79
+ }
80
+ map[scannedLength] = column; // past-the-end = total width
81
+ this.columnMap = map;
82
+ }
83
+ /**
84
+ * Convert a string offset (code unit index) to a display column.
85
+ * For offsets pointing into the middle of a grapheme cluster,
86
+ * returns the column of that cluster's start.
87
+ */
88
+ offsetToColumn(offset) {
89
+ if (offset <= 0)
90
+ return 0;
91
+ if (offset >= this.rawLength)
92
+ return this.displayWidth;
93
+ return this.columnMap[offset];
94
+ }
95
+ /**
96
+ * Convert a display column to a string offset.
97
+ * If the column falls on the second cell of a wide character,
98
+ * returns the offset of that wide character.
99
+ */
100
+ columnToOffset(column) {
101
+ if (column <= 0)
102
+ return 0;
103
+ if (column >= this.displayWidth)
104
+ return this.rawLength;
105
+ let col = 0;
106
+ for (const slot of this.slots) {
107
+ if (column >= col && column < col + slot.displayWidth) {
108
+ return slot.offset;
109
+ }
110
+ col += slot.displayWidth;
111
+ }
112
+ /* v8 ignore start -- unreachable: column < displayWidth is guaranteed to fall inside some slot, since slots tile [0, displayWidth) */
113
+ return this.rawLength;
114
+ /* v8 ignore stop */
115
+ }
116
+ /**
117
+ * Get the grapheme slot that occupies the given display column,
118
+ * or undefined if the column is out of range.
119
+ */
120
+ graphemeAtColumn(column) {
121
+ if (column < 0 || column >= this.displayWidth)
122
+ return undefined;
123
+ let col = 0;
124
+ for (const slot of this.slots) {
125
+ if (column >= col && column < col + slot.displayWidth) {
126
+ return slot;
127
+ }
128
+ col += slot.displayWidth;
129
+ }
130
+ /* v8 ignore start -- unreachable: the guard above bounds column to [0, displayWidth), which always falls inside a slot */
131
+ return undefined;
132
+ /* v8 ignore stop */
133
+ }
134
+ /**
135
+ * Get the character to render at a given display column.
136
+ * - For the first column of a wide char: returns the grapheme.
137
+ * - For the second column of a wide char: returns "" (continuation).
138
+ * - For a tab: returns " " (space).
139
+ * - For out-of-range: returns " ".
140
+ */
141
+ charAtColumn(column) {
142
+ if (column < 0 || column >= this.displayWidth)
143
+ return " ";
144
+ let col = 0;
145
+ for (const slot of this.slots) {
146
+ const slotEnd = col + slot.displayWidth;
147
+ if (column >= col && column < slotEnd) {
148
+ if (slot.grapheme === "\t") {
149
+ return " ";
150
+ }
151
+ if (column === col) {
152
+ return slot.grapheme;
153
+ }
154
+ // Second (or later) column of a wide char
155
+ return "";
156
+ }
157
+ col = slotEnd;
158
+ }
159
+ /* v8 ignore start -- unreachable: the guard above bounds column to [0, displayWidth), which always falls inside a slot */
160
+ return " ";
161
+ /* v8 ignore stop */
162
+ }
163
+ /**
164
+ * Find the slot index for the grapheme at the given string offset,
165
+ * or -1 if out of range.
166
+ */
167
+ slotIndexAtOffset(offset) {
168
+ if (offset < 0 || offset >= this.rawLength)
169
+ return -1;
170
+ // Binary search by offset
171
+ let lo = 0;
172
+ let hi = this.slots.length - 1;
173
+ while (lo <= hi) {
174
+ const mid = (lo + hi) >> 1;
175
+ const slot = this.slots[mid];
176
+ if (offset < slot.offset) {
177
+ hi = mid - 1;
178
+ }
179
+ else if (offset >= slot.offset + slot.length) {
180
+ lo = mid + 1;
181
+ }
182
+ else {
183
+ return mid;
184
+ }
185
+ }
186
+ /* v8 ignore start -- unreachable: every offset in [0, rawLength) belongs to exactly one slot, so the binary search always hits */
187
+ return -1;
188
+ /* v8 ignore stop */
189
+ }
190
+ }
@@ -0,0 +1,9 @@
1
+ export interface IDisposable {
2
+ dispose(): void;
3
+ }
4
+ export declare class Disposable implements IDisposable {
5
+ private disposables;
6
+ private disposed;
7
+ protected register<T extends IDisposable>(disposable: T): T;
8
+ dispose(): void;
9
+ }
@@ -0,0 +1,18 @@
1
+ export class Disposable {
2
+ disposables = [];
3
+ disposed = false;
4
+ register(disposable) {
5
+ this.disposables.push(disposable);
6
+ return disposable;
7
+ }
8
+ dispose() {
9
+ if (this.disposed)
10
+ return;
11
+ this.disposed = true;
12
+ // Dispose in reverse order (LIFO)
13
+ for (let i = this.disposables.length - 1; i >= 0; i--) {
14
+ this.disposables[i].dispose();
15
+ }
16
+ this.disposables.length = 0;
17
+ }
18
+ }
@@ -0,0 +1,47 @@
1
+ export declare class Point {
2
+ readonly x: number;
3
+ readonly y: number;
4
+ constructor(x: number, y: number);
5
+ }
6
+ export declare class Offset {
7
+ readonly dx: number;
8
+ readonly dy: number;
9
+ constructor(dx: number, dy: number);
10
+ }
11
+ export declare class Size {
12
+ readonly width: number;
13
+ readonly height: number;
14
+ constructor(width: number, height: number);
15
+ }
16
+ export declare class BoxConstraints {
17
+ readonly minWidth: number;
18
+ readonly maxWidth: number;
19
+ readonly minHeight: number;
20
+ readonly maxHeight: number;
21
+ constructor(minWidth: number, maxWidth: number, minHeight: number, maxHeight: number);
22
+ static tight(size: Size): BoxConstraints;
23
+ static loose(size: Size): BoxConstraints;
24
+ constrain(size: Size): Size;
25
+ /** Удовлетворяет ли размер этим constraints (контракт layout: см. docs/LAYOUT.md). */
26
+ isSatisfiedBy(size: Size): boolean;
27
+ }
28
+ export declare class Rect {
29
+ readonly origin: Point;
30
+ readonly size: Size;
31
+ constructor(origin: Point, size: Size);
32
+ get x(): number;
33
+ get y(): number;
34
+ get width(): number;
35
+ get height(): number;
36
+ get right(): number;
37
+ get bottom(): number;
38
+ containsPoint(point: Point): boolean;
39
+ intersect(other: Rect): Rect;
40
+ get isEmpty(): boolean;
41
+ /** Есть ли непустое пересечение (касание рёбрами — не пересечение). */
42
+ intersects(other: Rect): boolean;
43
+ /** Минимальный rect, накрывающий оба. */
44
+ union(other: Rect): Rect;
45
+ /** Содержит ли целиком другой rect (пустой содержится в любом). */
46
+ containsRect(other: Rect): boolean;
47
+ }
@@ -0,0 +1,115 @@
1
+ export class Point {
2
+ x;
3
+ y;
4
+ constructor(x, y) {
5
+ this.x = x;
6
+ this.y = y;
7
+ }
8
+ }
9
+ export class Offset {
10
+ dx;
11
+ dy;
12
+ constructor(dx, dy) {
13
+ this.dx = dx;
14
+ this.dy = dy;
15
+ }
16
+ }
17
+ export class Size {
18
+ width;
19
+ height;
20
+ constructor(width, height) {
21
+ this.width = width;
22
+ this.height = height;
23
+ }
24
+ }
25
+ export class BoxConstraints {
26
+ minWidth;
27
+ maxWidth;
28
+ minHeight;
29
+ maxHeight;
30
+ constructor(minWidth, maxWidth, minHeight, maxHeight) {
31
+ this.minWidth = minWidth;
32
+ this.maxWidth = maxWidth;
33
+ this.minHeight = minHeight;
34
+ this.maxHeight = maxHeight;
35
+ }
36
+ static tight(size) {
37
+ return new BoxConstraints(size.width, size.width, size.height, size.height);
38
+ }
39
+ static loose(size) {
40
+ return new BoxConstraints(0, size.width, 0, size.height);
41
+ }
42
+ constrain(size) {
43
+ return new Size(Math.min(this.maxWidth, Math.max(this.minWidth, size.width)), Math.min(this.maxHeight, Math.max(this.minHeight, size.height)));
44
+ }
45
+ /** Удовлетворяет ли размер этим constraints (контракт layout: см. docs/LAYOUT.md). */
46
+ isSatisfiedBy(size) {
47
+ return (size.width >= this.minWidth &&
48
+ size.width <= this.maxWidth &&
49
+ size.height >= this.minHeight &&
50
+ size.height <= this.maxHeight);
51
+ }
52
+ }
53
+ export class Rect {
54
+ origin;
55
+ size;
56
+ constructor(origin, size) {
57
+ this.origin = origin;
58
+ this.size = size;
59
+ }
60
+ get x() {
61
+ return this.origin.x;
62
+ }
63
+ get y() {
64
+ return this.origin.y;
65
+ }
66
+ get width() {
67
+ return this.size.width;
68
+ }
69
+ get height() {
70
+ return this.size.height;
71
+ }
72
+ get right() {
73
+ return this.origin.x + this.size.width;
74
+ }
75
+ get bottom() {
76
+ return this.origin.y + this.size.height;
77
+ }
78
+ containsPoint(point) {
79
+ return point.x >= this.x && point.x < this.right && point.y >= this.y && point.y < this.bottom;
80
+ }
81
+ intersect(other) {
82
+ const x = Math.max(this.x, other.x);
83
+ const y = Math.max(this.y, other.y);
84
+ const right = Math.min(this.right, other.right);
85
+ const bottom = Math.min(this.bottom, other.bottom);
86
+ const width = Math.max(0, right - x);
87
+ const height = Math.max(0, bottom - y);
88
+ return new Rect(new Point(x, y), new Size(width, height));
89
+ }
90
+ get isEmpty() {
91
+ return this.size.width <= 0 || this.size.height <= 0;
92
+ }
93
+ /** Есть ли непустое пересечение (касание рёбрами — не пересечение). */
94
+ intersects(other) {
95
+ return this.x < other.right && other.x < this.right && this.y < other.bottom && other.y < this.bottom;
96
+ }
97
+ /** Минимальный rect, накрывающий оба. */
98
+ union(other) {
99
+ if (this.isEmpty)
100
+ return other;
101
+ if (other.isEmpty)
102
+ return this;
103
+ const x = Math.min(this.x, other.x);
104
+ const y = Math.min(this.y, other.y);
105
+ const right = Math.max(this.right, other.right);
106
+ const bottom = Math.max(this.bottom, other.bottom);
107
+ return new Rect(new Point(x, y), new Size(right - x, bottom - y));
108
+ }
109
+ /** Содержит ли целиком другой rect (пустой содержится в любом). */
110
+ containsRect(other) {
111
+ if (other.isEmpty)
112
+ return true;
113
+ return other.x >= this.x && other.y >= this.y && other.right <= this.right && other.bottom <= this.bottom;
114
+ }
115
+ }
@@ -0,0 +1,68 @@
1
+ import type { IDisposable } from "./disposable.js";
2
+ /**
3
+ * Переиспользуемый out-параметр `readCell` — вызывающий аллоцирует один раз,
4
+ * без аллокаций на ячейку.
5
+ */
6
+ export interface TerminalCell {
7
+ char: string;
8
+ fg: number;
9
+ bg: number;
10
+ style: number;
11
+ width: number;
12
+ }
13
+ export type TerminalMouseButton = "left" | "middle" | "right" | "none" | "wheel";
14
+ export type TerminalMouseAction = "down" | "up" | "move" | "wheelUp" | "wheelDown" | "wheelLeft" | "wheelRight";
15
+ export interface TerminalMouseEventData {
16
+ col: number;
17
+ row: number;
18
+ button: TerminalMouseButton;
19
+ action: TerminalMouseAction;
20
+ ctrl: boolean;
21
+ alt: boolean;
22
+ shift: boolean;
23
+ }
24
+ export interface ITerminalSurface {
25
+ /**
26
+ * Прочитать ячейку вьюпорта в переданный `out`. Координата `y` отсчитывается от верха
27
+ * вьюпорта — с учётом текущего `scrollOffset`, поэтому при заглядывании в скролбэк
28
+ * виджету ничего пересчитывать не надо.
29
+ * `false` = continuation-ячейка wide-char (голова уже нарисована с width=2) либо
30
+ * координата вне диапазона; в обоих случаях `out` не тронут.
31
+ */
32
+ readCell(x: number, y: number, out: TerminalCell): boolean;
33
+ /** Позиция курсора в видимой области или `null`, когда его показывать не надо. */
34
+ getCursor(): {
35
+ x: number;
36
+ y: number;
37
+ } | null;
38
+ readonly isExited: boolean;
39
+ /**
40
+ * Уже закодированные байты в PTY (см. encodeKeyForPty). Ввод возвращает вьюпорт
41
+ * на дно — печатать, глядя в историю, бессмысленно.
42
+ */
43
+ write(data: string): void;
44
+ sendMouse(event: TerminalMouseEventData): void;
45
+ /**
46
+ * Включила ли запущенная в шелле программа mouse-tracking (htop/vim/less --mouse).
47
+ * Пока `true` — колесо принадлежит программе и уходит в `sendMouse`; пока `false` —
48
+ * колесо обрабатывает виджет сам и крутит вьюпорт по скролбэку (как xterm.js
49
+ * смотрит на `coreMouseService.areMouseEventsActive`).
50
+ */
51
+ readonly mouseEventsActive: boolean;
52
+ /**
53
+ * На сколько строк вьюпорт поднят в скролбэк. `0` — смотрим на дно, живой вывод.
54
+ * Пока значение > 0, `getCursor()` отдаёт `null`: курсор шелла живёт на дне, и
55
+ * показывать его поверх истории нельзя.
56
+ */
57
+ readonly scrollOffset: number;
58
+ /**
59
+ * Прокрутить вьюпорт: `delta < 0` — вверх, в историю, `delta > 0` — вниз, к живому
60
+ * выводу. Результат клампится в `[0, <длина скролбэка>]`. Если смещение изменилось,
61
+ * поверхность дёргает `onUpdate` — виджету достаточно обычной подписки.
62
+ */
63
+ scrollLines(delta: number): void;
64
+ /** Держать PTY+эмулятор по выделенной области; no-op при совпадении. */
65
+ resize(cols: number, rows: number): void;
66
+ onUpdate(cb: () => void): IDisposable;
67
+ onExit(cb: (exitCode: number) => void): IDisposable;
68
+ }
@@ -0,0 +1,8 @@
1
+ // Абстрактная модель ячеек терминала — то, что тикет требует держать в TUIDom «чистым».
2
+ //
3
+ // `TerminalViewElement` рендерится ТОЛЬКО через этот интерфейс и ничего не знает про
4
+ // PTY и VT-эмулятор. Реальная связка (node-pty + @xterm/headless) реализует
5
+ // `ITerminalSurface` на слой выше — в Workbench, — поэтому под `src/TUIDom/` не
6
+ // протекают импорты `@xterm/headless`/`node-pty`, а виджет остаётся тестируемым
7
+ // через скриптованный фейк (см. TestUtils/FakeTerminalSurface).
8
+ export {};
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Display width of `raw` in terminal columns.
3
+ *
4
+ * Returns **exactly** the same number as
5
+ * `new DisplayLine(raw, tabSize, stopAfter).displayWidth`, but without
6
+ * allocating the per-grapheme slot array or the offset→column `Int32Array`.
7
+ * Used by the horizontal-scrollbar width cache ({@link LineWidthCache}), where
8
+ * only the width matters and building a full `DisplayLine` per document line is
9
+ * what freezes the editor on long lines.
10
+ *
11
+ * `stopAfter` bounds the scan: any grapheme whose start offset is at or past it
12
+ * is ignored (segmentation is lazy, so the tail is never touched). An extreme
13
+ * line therefore costs O(stopAfter), not O(length) — mirroring VS Code's
14
+ * `stopRenderingLineAfter`.
15
+ *
16
+ * The per-grapheme width rules are kept in lock-step with `DisplayLine`'s
17
+ * constructor: tab expands to the next tab stop, `\r` is zero-width, single
18
+ * code units go through {@link getCharDisplayWidth}, multi-code-unit clusters
19
+ * through {@link getGraphemeDisplayWidth}.
20
+ */
21
+ export declare function measureTextWidth(raw: string, tabSize?: number, stopAfter?: number): number;