@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,122 @@
1
+ import { DEFAULT_COLOR, packRgb } from "../../common/colorUtils.js";
2
+ /**
3
+ * Дефолтные значения цветовых токенов tuidom — единственное место, где у
4
+ * виджетов tuidom/ui есть RGB-литералы. Правило: токен, на который ссылается
5
+ * виджет, ОБЯЗАН иметь строку здесь — иначе резолв без хостовой таблицы
6
+ * (тесты, stories, standalone-использование) упадёт fail-fast'ом.
7
+ *
8
+ * Имена — в конвенции color id VS Code ("list.activeSelectionBackground"),
9
+ * чтобы хост перекрывал дефолты ключами своей темы без функций-мостов;
10
+ * tuidom-специфика без темного эквивалента — под собственным именем
11
+ * ("menu.shortcutForeground", "quickPick.*", "titledPanel.*", "menuBar.*").
12
+ * Значения — только конкретные числа (packed RGB | DEFAULT_COLOR); сентинелы
13
+ * INHERITED_* в таблицах запрещены (проверяется в setStyleVars).
14
+ *
15
+ * Дефолты сохраняют исторический вид unthemed-палитр, которые сюда переехали
16
+ * (Н3): под управлением хоста всё перекрывается активной темой.
17
+ */
18
+ export const STYLE_TOKEN_DEFAULTS = {
19
+ // ── Кнопка (base = secondary-вид, focus/checked = primary) ──
20
+ "button.foreground": packRgb(255, 255, 255),
21
+ "button.background": packRgb(0, 120, 215),
22
+ "button.hoverBackground": packRgb(26, 134, 224),
23
+ "button.secondaryForeground": packRgb(204, 204, 204),
24
+ "button.secondaryBackground": packRgb(60, 60, 60),
25
+ "button.secondaryHoverBackground": packRgb(69, 73, 78),
26
+ // ── Меню (попапы, дропдауны, контекстные) ──
27
+ "menu.foreground": packRgb(204, 204, 204),
28
+ "menu.background": packRgb(37, 37, 38),
29
+ "menu.selectionForeground": packRgb(255, 255, 255),
30
+ "menu.selectionBackground": packRgb(4, 57, 94),
31
+ "menu.border": packRgb(83, 83, 83),
32
+ "menu.separatorBackground": packRgb(83, 83, 83),
33
+ "menu.shortcutForeground": packRgb(128, 128, 128),
34
+ // ── Полоса меню ──
35
+ "menuBar.foreground": DEFAULT_COLOR,
36
+ "menuBar.background": packRgb(64, 64, 64),
37
+ "menubar.selectionForeground": packRgb(255, 255, 255),
38
+ "menubar.selectionBackground": packRgb(0, 90, 180),
39
+ // ── Списки и деревья ──
40
+ "list.activeSelectionBackground": packRgb(4, 57, 94),
41
+ "list.activeSelectionForeground": packRgb(255, 255, 255),
42
+ "list.inactiveSelectionBackground": packRgb(55, 55, 61),
43
+ "list.inactiveSelectionForeground": packRgb(204, 204, 204),
44
+ "list.hoverBackground": packRgb(42, 45, 46),
45
+ "list.deemphasizedForeground": packRgb(150, 150, 150),
46
+ "list.highlightForeground": packRgb(100, 200, 255),
47
+ // ── Скроллбар (фон колонки наследуется каскадом от хозяина) ──
48
+ "scrollbarSlider.background": packRgb(100, 100, 100),
49
+ "scrollbar.background": packRgb(50, 50, 50),
50
+ // ── Вкладки редактора ──
51
+ "tab.activeForeground": packRgb(255, 255, 255),
52
+ "tab.activeBackground": packRgb(30, 30, 30),
53
+ "tab.inactiveForeground": packRgb(150, 150, 150),
54
+ "tab.inactiveBackground": packRgb(45, 45, 45),
55
+ "editorGroupHeader.tabsBackground": packRgb(37, 37, 38),
56
+ // ── Панель ──
57
+ "panel.background": packRgb(24, 24, 24),
58
+ "panelTitle.inactiveForeground": packRgb(142, 142, 142),
59
+ "panel.border": packRgb(43, 43, 43),
60
+ "titledPanel.titleForeground": packRgb(130, 130, 130),
61
+ // ── Терминал ──
62
+ "terminal.foreground": DEFAULT_COLOR,
63
+ "terminal.background": DEFAULT_COLOR,
64
+ // ── Селектбокс ──
65
+ "dropdown.foreground": packRgb(240, 240, 240),
66
+ "dropdown.background": packRgb(60, 60, 60),
67
+ "dropdown.border": packRgb(60, 60, 60),
68
+ "dropdown.listBackground": packRgb(37, 37, 38),
69
+ // ── Инпут ──
70
+ "input.foreground": packRgb(204, 204, 204),
71
+ "input.background": packRgb(60, 60, 60),
72
+ "input.placeholderForeground": packRgb(110, 110, 110),
73
+ "input.border": packRgb(60, 60, 60),
74
+ "input.selectionBackground": packRgb(38, 79, 120),
75
+ focusBorder: packRgb(0, 127, 212),
76
+ // ── Sash ──
77
+ "sash.hoverBorder": packRgb(0, 120, 215),
78
+ // ── Квик-пик ──
79
+ "quickInput.foreground": packRgb(204, 204, 204),
80
+ "quickInput.background": packRgb(37, 37, 38),
81
+ "quickPick.border": packRgb(83, 83, 83),
82
+ "quickPick.badgeForeground": packRgb(150, 190, 100),
83
+ "quickPick.shortcutForeground": packRgb(128, 128, 128),
84
+ "quickPick.hintForeground": packRgb(100, 150, 200),
85
+ "quickPick.titleForeground": packRgb(230, 230, 230),
86
+ "quickPick.promptForeground": packRgb(140, 140, 140),
87
+ descriptionForeground: packRgb(125, 125, 125),
88
+ // ── Автодополнение ──
89
+ "editorSuggestWidget.border": packRgb(83, 83, 83),
90
+ "editorSuggestWidget.background": packRgb(34, 34, 40),
91
+ "editorSuggestWidget.foreground": packRgb(204, 204, 204),
92
+ "editorSuggestWidget.selectedBackground": packRgb(56, 62, 90),
93
+ "editorSuggestWidget.selectedForeground": packRgb(255, 255, 255),
94
+ "editorSuggestWidget.iconForeground": packRgb(130, 170, 255),
95
+ "editorSuggestWidget.detailForeground": packRgb(120, 120, 130),
96
+ // ── Виджеты редактора (диалоги, find) ──
97
+ "editorWidget.foreground": packRgb(204, 204, 204),
98
+ "editorWidget.background": packRgb(37, 37, 38),
99
+ "editorWidget.border": packRgb(69, 69, 69),
100
+ "textLink.foreground": packRgb(55, 148, 255),
101
+ // ── Редактор ──
102
+ "editorLineNumber.foreground": packRgb(133, 133, 133),
103
+ "editorLineNumber.activeForeground": packRgb(198, 198, 198),
104
+ "editor.wordHighlightBackground": packRgb(71, 71, 71),
105
+ "editorGutter.foldingControlForeground": packRgb(197, 197, 197),
106
+ "editorIndentGuide.background1": packRgb(64, 64, 64),
107
+ "editorIndentGuide.activeBackground1": packRgb(112, 112, 112),
108
+ "editorError.foreground": packRgb(241, 76, 76),
109
+ "editorWarning.foreground": packRgb(204, 167, 0),
110
+ "editorInfo.foreground": packRgb(55, 148, 255),
111
+ "editorHint.foreground": packRgb(238, 238, 238),
112
+ // ── Diff ──
113
+ "diffEditor.insertedLineBackground": 0x373d29,
114
+ "diffEditor.removedLineBackground": 0x4b1818,
115
+ "diffEditor.unchangedRegionForeground": 0x8c8c8c,
116
+ };
117
+ /**
118
+ * Дно прототипной цепочки var-scope'ов: замороженные дефолты с null-прототипом
119
+ * (лукап по цепочке не должен находить ключи Object.prototype). Пользовательские
120
+ * таблицы (setStyleVars) ложатся ПОВЕРХ через Object.create.
121
+ */
122
+ export const ROOT_VAR_SCOPE = Object.freeze(Object.assign(Object.create(null), STYLE_TOKEN_DEFAULTS));
@@ -0,0 +1,75 @@
1
+ import type { AnyStyleToken } from "./styleTokens.js";
2
+ export declare const INHERITED_FG = -100;
3
+ export declare const INHERITED_BG = -101;
4
+ export type StyleColor = number | AnyStyleToken;
5
+ /** Таблица токен→число. Лукап идёт по прототипной цепочке до ROOT_VAR_SCOPE. */
6
+ export type StyleVarScope = Readonly<Record<string, number>>;
7
+ export type StyleState = "hover" | "focus" | (string & {});
8
+ /**
9
+ * Селектор состояния в when-записи:
10
+ * - `"selected"` — состояние активно на САМОМ элементе;
11
+ * - `"in:focus"` — активно на самом элементе ИЛИ любом предке («внутри
12
+ * сфокусированного»; заменяет descendant-комбинатор CSS). Осторожно:
13
+ * `in:hover` шире CSS-`:hover` — сработает и когда курсор на сиблинге
14
+ * внутри hovered-контейнера; «hover именно на мне» — простой `"hover"`.
15
+ */
16
+ export type StyleStateSelector = StyleState | `in:${string}`;
17
+ /** Запись применяется, когда активны ВСЕ её селекторы (AND). */
18
+ export interface StyleStateVariant {
19
+ readonly states: readonly StyleStateSelector[];
20
+ readonly fg?: StyleColor;
21
+ readonly bg?: StyleColor;
22
+ }
23
+ export interface TUIStyle {
24
+ fg?: StyleColor;
25
+ bg?: StyleColor;
26
+ /**
27
+ * Варианты по состояниям. Позже объявленная активная запись побеждает
28
+ * (порядок = приоритет, как порядок правил CSS). `states: []` легален —
29
+ * «всегда активна» (удобная база, переопределяемая последующими).
30
+ */
31
+ when?: readonly StyleStateVariant[];
32
+ }
33
+ export interface ResolvedTUIStyle {
34
+ readonly fg: number;
35
+ readonly bg: number;
36
+ }
37
+ export declare const ROOT_RESOLVED_STYLE: ResolvedTUIStyle;
38
+ export interface StyleResolutionContext {
39
+ readonly fg: number;
40
+ readonly bg: number;
41
+ /** Ближайший var-scope (собственные таблицы предков поверх дефолтов). */
42
+ readonly vars: StyleVarScope;
43
+ /** Объединение СОБСТВЕННЫХ состояний всех предков — для `in:`-селекторов. */
44
+ readonly ancestorStates: ReadonlySet<string>;
45
+ }
46
+ export declare const ROOT_STYLE_CONTEXT: StyleResolutionContext;
47
+ /**
48
+ * Пользовательская таблица поверх родительского scope — прототипная цепочка,
49
+ * без копий. Свойства создаются через defineProperty: обычное присваивание
50
+ * ([[Set]]) отказало бы — родительские scope'ы заморожены, и цепочка прототипов
51
+ * блокирует запись одноимённого ключа.
52
+ */
53
+ export declare function extendVarScope(parent: StyleVarScope, own: Readonly<Record<string, number>>): StyleVarScope;
54
+ /**
55
+ * Структурное равенство деклараций стиля. Используется сеттером `style`,
56
+ * чтобы повторный пуш того же значения (JSX-rebuild, reconcile, повторное
57
+ * применение темы) не помечал грязным всё поддерево.
58
+ */
59
+ export declare function styleEquals(a: TUIStyle, b: TUIStyle): boolean;
60
+ /**
61
+ * Токен → число из scope (fail-fast на незнакомом), затем сентинелы
62
+ * INHERITED_* → унаследованные цвета. Проверка значения через typeof — защита
63
+ * от ключей Object.prototype ("toString") у пользовательских таблиц.
64
+ */
65
+ export declare function resolveStyleColor(color: StyleColor, inheritedFg: number, inheritedBg: number, vars?: StyleVarScope, describeOwner?: () => string): number;
66
+ /**
67
+ * Схлопывает базовые fg/bg и активные when-записи в «применённые» сырые цвета
68
+ * (ещё до резолва сентинелов). Позже объявленная активная запись побеждает.
69
+ * undefined в результате = «наследовать».
70
+ */
71
+ export declare function mergeStyleVariants(style: TUIStyle, isActive: (selector: StyleStateSelector) => boolean): {
72
+ fg: StyleColor | undefined;
73
+ bg: StyleColor | undefined;
74
+ };
75
+ export declare function resolveStyle(style: TUIStyle, inherited: ResolvedTUIStyle): ResolvedTUIStyle;
@@ -0,0 +1,121 @@
1
+ import { DEFAULT_COLOR } from "../../common/colorUtils.js";
2
+ import { ROOT_VAR_SCOPE } from "./styleTokens.js";
3
+ // ─── Inherited Color Sentinels ───
4
+ // Sentinel values that resolve to the inherited fg/bg from the parent.
5
+ // Start at -100 to avoid collision with DEFAULT_COLOR (-1).
6
+ export const INHERITED_FG = -100;
7
+ export const INHERITED_BG = -101;
8
+ // ─── Root default ───
9
+ export const ROOT_RESOLVED_STYLE = {
10
+ fg: DEFAULT_COLOR,
11
+ bg: DEFAULT_COLOR,
12
+ };
13
+ const EMPTY_STATES = new Set();
14
+ export const ROOT_STYLE_CONTEXT = {
15
+ fg: DEFAULT_COLOR,
16
+ bg: DEFAULT_COLOR,
17
+ vars: ROOT_VAR_SCOPE,
18
+ ancestorStates: EMPTY_STATES,
19
+ };
20
+ /**
21
+ * Пользовательская таблица поверх родительского scope — прототипная цепочка,
22
+ * без копий. Свойства создаются через defineProperty: обычное присваивание
23
+ * ([[Set]]) отказало бы — родительские scope'ы заморожены, и цепочка прототипов
24
+ * блокирует запись одноимённого ключа.
25
+ */
26
+ export function extendVarScope(parent, own) {
27
+ const scope = Object.create(parent);
28
+ for (const key of Object.keys(own)) {
29
+ Object.defineProperty(scope, key, { value: own[key], enumerable: true, writable: false, configurable: false });
30
+ }
31
+ return scope;
32
+ }
33
+ // ─── Comparison ───
34
+ /**
35
+ * Структурное равенство деклараций стиля. Используется сеттером `style`,
36
+ * чтобы повторный пуш того же значения (JSX-rebuild, reconcile, повторное
37
+ * применение темы) не помечал грязным всё поддерево.
38
+ */
39
+ export function styleEquals(a, b) {
40
+ if (a === b)
41
+ return true;
42
+ // Сравниваются ВСЕ собственные ключи: подклассы расширяют стиль своими
43
+ // полями (исторический прецедент — TitledPanelStyle), и они обязаны участвовать в
44
+ // равенстве. Ключ с undefined против отсутствия ключа считается различием —
45
+ // консервативно в безопасную сторону (лишний резолв, а не пропущенный).
46
+ const aKeys = Object.keys(a);
47
+ const bKeys = Object.keys(b);
48
+ if (aKeys.length !== bKeys.length)
49
+ return false;
50
+ for (const key of aKeys) {
51
+ if (key === "when")
52
+ continue; // массив — структурно, ниже
53
+ if (a[key] !== b[key])
54
+ return false;
55
+ }
56
+ return whenEquals(a.when, b.when);
57
+ }
58
+ // Порядок when-записей — семантика (приоритет), сравнение позиционное.
59
+ function whenEquals(a, b) {
60
+ if (a === b)
61
+ return true;
62
+ if (a === undefined || a.length !== b?.length)
63
+ return false;
64
+ for (let i = 0; i < a.length; i++) {
65
+ const x = a[i];
66
+ const y = b[i];
67
+ if (x.fg !== y.fg || x.bg !== y.bg || x.states.length !== y.states.length)
68
+ return false;
69
+ for (let j = 0; j < x.states.length; j++) {
70
+ if (x.states[j] !== y.states[j])
71
+ return false;
72
+ }
73
+ }
74
+ return true;
75
+ }
76
+ // ─── Resolution functions ───
77
+ /**
78
+ * Токен → число из scope (fail-fast на незнакомом), затем сентинелы
79
+ * INHERITED_* → унаследованные цвета. Проверка значения через typeof — защита
80
+ * от ключей Object.prototype ("toString") у пользовательских таблиц.
81
+ */
82
+ export function resolveStyleColor(color, inheritedFg, inheritedBg, vars = ROOT_VAR_SCOPE, describeOwner) {
83
+ if (typeof color === "string") {
84
+ const value = vars[color];
85
+ if (typeof value !== "number") {
86
+ const owner = describeOwner !== undefined ? describeOwner() : "TUIStyle";
87
+ throw new Error(`${owner}: неизвестный цветовой токен "${color}"`);
88
+ }
89
+ return value;
90
+ }
91
+ if (color === INHERITED_FG)
92
+ return inheritedFg;
93
+ if (color === INHERITED_BG)
94
+ return inheritedBg;
95
+ return color;
96
+ }
97
+ /**
98
+ * Схлопывает базовые fg/bg и активные when-записи в «применённые» сырые цвета
99
+ * (ещё до резолва сентинелов). Позже объявленная активная запись побеждает.
100
+ * undefined в результате = «наследовать».
101
+ */
102
+ export function mergeStyleVariants(style, isActive) {
103
+ let fg = style.fg;
104
+ let bg = style.bg;
105
+ if (style.when !== undefined) {
106
+ for (const variant of style.when) {
107
+ if (variant.states.every(isActive)) {
108
+ if (variant.fg !== undefined)
109
+ fg = variant.fg;
110
+ if (variant.bg !== undefined)
111
+ bg = variant.bg;
112
+ }
113
+ }
114
+ }
115
+ return { fg, bg };
116
+ }
117
+ export function resolveStyle(style, inherited) {
118
+ const fg = style.fg !== undefined ? resolveStyleColor(style.fg, inherited.fg, inherited.bg) : inherited.fg;
119
+ const bg = style.bg !== undefined ? resolveStyleColor(style.bg, inherited.fg, inherited.bg) : inherited.bg;
120
+ return { fg, bg };
121
+ }
@@ -0,0 +1,57 @@
1
+ import type { ITerminalBackend } from "../backend/iTerminalBackend.js";
2
+ import { TerminalScreen } from "../rendering/terminalScreen.js";
3
+ import { FocusManager } from "./events/focusManager.js";
4
+ import { MouseEventDispatcher } from "./events/mouseEventDispatcher.js";
5
+ import { type TUIElement } from "./tuiElement.js";
6
+ export declare class TuiApplication {
7
+ backend: ITerminalBackend;
8
+ root: TUIElement | null;
9
+ screen: TerminalScreen;
10
+ focusManager: FocusManager | null;
11
+ mouseDispatcher: MouseEventDispatcher;
12
+ private renderScheduled;
13
+ /**
14
+ * Прогонять {@link assertValidTree} после каждого кадра. Включено в тестовых
15
+ * харнессах (TestApp) и опционально в приложении (env VEXX_VALIDATE_TREE=1):
16
+ * забытый setParent / неукоренённое поддерево падают громко на первом же
17
+ * кадре, а не молчаливым «не показывается / не кликается».
18
+ */
19
+ validateTreeAfterRender: boolean;
20
+ private frameCounter;
21
+ private pinnedKeypressTarget;
22
+ private needsFullRepaint;
23
+ constructor(backend: ITerminalBackend);
24
+ /** Сколько кадров отрисовано с запуска. Растёт при каждой перерисовке. */
25
+ get frameCount(): number;
26
+ /** Есть ли отложенный (setImmediate) рендер, ещё не выполненный. */
27
+ get isRenderScheduled(): boolean;
28
+ private renderFrame;
29
+ /**
30
+ * Принудительно сделать следующий кадр полным (clear + рендер всего
31
+ * дерева). Для потребителей, меняющих картинку мимо markDirty-контракта
32
+ * (ресайз, тестовые харнессы).
33
+ */
34
+ invalidateScreen(): void;
35
+ /**
36
+ * Schedules a deferred render via setImmediate.
37
+ * Batches multiple markDirty() calls into a single frame.
38
+ * Skips rendering if layout is already clean (e.g. a synchronous
39
+ * renderFrame from handleInput already ran).
40
+ */
41
+ scheduleRender(): void;
42
+ private handleInput;
43
+ private handlePaste;
44
+ private handleMouse;
45
+ /**
46
+ * Синхронный кадр после события ввода — только если обработчики что-то
47
+ * пометили грязным. Одно физическое нажатие приходит 2–3 событиями
48
+ * (keydown + синтезированный keypress, под Kitty ещё keyup) — раньше
49
+ * каждое рендерило полный кадр, из которых пустые давали чистый diff и
50
+ * жгли CPU впустую. Стилевые изменения гейт проходят: markStyleDirty
51
+ * заканчивается markDirty. Синхронность оставлена намеренно: тесты и
52
+ * обработчики читают экран/геометрию сразу после события.
53
+ */
54
+ private renderAfterInput;
55
+ private handleResize;
56
+ run(): void;
57
+ }
@@ -0,0 +1,231 @@
1
+ import { BoxConstraints, Offset, Point, Rect, Size } from "../common/geometryPromitives.js";
2
+ import { DamageList } from "../rendering/damage.js";
3
+ import { TerminalScreen } from "../rendering/terminalScreen.js";
4
+ import { contextMenuEventFromKeydown } from "./events/contextMenuEventSource.js";
5
+ import { FocusManager } from "./events/focusManager.js";
6
+ import { MouseEventDispatcher } from "./events/mouseEventDispatcher.js";
7
+ import { TUIKeyboardEvent } from "./events/tuiKeyboardEvent.js";
8
+ import { TUIPasteEvent } from "./events/tuiPasteEvent.js";
9
+ import { ROOT_STYLE_CONTEXT } from "./styles/tuiStyle.js";
10
+ import { RenderContext } from "./tuiElement.js";
11
+ import { assertValidTree } from "./validateTree.js";
12
+ export class TuiApplication {
13
+ backend;
14
+ root = null;
15
+ screen;
16
+ focusManager = null;
17
+ mouseDispatcher = new MouseEventDispatcher();
18
+ renderScheduled = false;
19
+ /**
20
+ * Прогонять {@link assertValidTree} после каждого кадра. Включено в тестовых
21
+ * харнессах (TestApp) и опционально в приложении (env VEXX_VALIDATE_TREE=1):
22
+ * забытый setParent / неукоренённое поддерево падают громко на первом же
23
+ * кадре, а не молчаливым «не показывается / не кликается».
24
+ */
25
+ validateTreeAfterRender = false;
26
+ // Монотонный счётчик отрисованных кадров. Инкремент на каждый renderFrame —
27
+ // инспектор ждёт «рендер устоялся» (frameCount не менялся + нет
28
+ // запланированного рендера), не завися от sleep. См. inspector/idleWaiter.
29
+ frameCounter = 0;
30
+ // Цель keydown, закреплённая за парным keypress того же физического нажатия
31
+ // (см. handleInput).
32
+ pinnedKeypressTarget = null;
33
+ // Следующий кадр — полный (первый кадр, ресайз, invalidateScreen).
34
+ needsFullRepaint = true;
35
+ constructor(backend) {
36
+ this.backend = backend;
37
+ this.screen = new TerminalScreen(backend.getSize());
38
+ }
39
+ /** Сколько кадров отрисовано с запуска. Растёт при каждой перерисовке. */
40
+ get frameCount() {
41
+ return this.frameCounter;
42
+ }
43
+ /** Есть ли отложенный (setImmediate) рендер, ещё не выполненный. */
44
+ get isRenderScheduled() {
45
+ return this.renderScheduled;
46
+ }
47
+ renderFrame() {
48
+ if (this.root) {
49
+ this.frameCounter++;
50
+ // Root at (0, 0) — top-left of screen. globalPosition производный
51
+ // (localPosition корня без родителя), выставлять его не нужно.
52
+ this.root.localPosition = new Offset(0, 0);
53
+ // Layout и стили — полные каждый кадр (дёшево, геометрия и
54
+ // validateTree не зависят от damage); стили гейтятся внутри.
55
+ const constraints = BoxConstraints.tight(this.screen.size);
56
+ this.root.layout(constraints);
57
+ this.root.performStyleResolution(ROOT_STYLE_CONTEXT);
58
+ // Damage-обход — всегда: и собирает области, и актуализирует
59
+ // lastPaintedRect/флаги (даже когда кадр будет полным).
60
+ const damage = new DamageList();
61
+ for (const detached of this.root.takePendingDetachDamage()) {
62
+ damage.add(detached);
63
+ }
64
+ this.root.collectDamage(damage, new Point(0, 0));
65
+ const screenBounds = new Rect(new Point(0, 0), this.screen.size);
66
+ let rects;
67
+ if (this.needsFullRepaint) {
68
+ this.needsFullRepaint = false;
69
+ this.screen.clear();
70
+ rects = [screenBounds];
71
+ }
72
+ else {
73
+ rects = damage.finalize(screenBounds);
74
+ }
75
+ // Экран — ретейн-буфер: перерисовываются только повреждённые
76
+ // области. Инвариант: множество очищенных ячеек == множеству,
77
+ // по которому клипуется (и прунится) проход отрисовки. Внутри
78
+ // области канонический обход идентичен полному кадру — прозрачные
79
+ // элементы, хром родителей и оверлеи корректны по построению.
80
+ for (let rect of rects) {
81
+ // Кромка области не должна рассекать wide-char пару ретейн-грида:
82
+ // очистка лечила бы голову вне области перерисовки (см. Grid).
83
+ rect = this.screen.snapToWideChars(rect);
84
+ this.screen.clearRect(rect);
85
+ // Аппаратный курсор ставится только в render его владельца:
86
+ // позиция в повреждённой области гаснет и пере-ставится тем,
87
+ // кто её снова нарисует (или остаётся погашенной — паритет с
88
+ // полным кадром, где clear() гасил её всегда).
89
+ if (this.screen.cursorPosition !== null && rect.containsPoint(this.screen.cursorPosition)) {
90
+ this.screen.clearCursorPosition();
91
+ }
92
+ this.root.render(new RenderContext(this.screen, new Offset(0, 0), rect));
93
+ }
94
+ // Всегда: пустой damage — пустой diff, ноль байт в терминал.
95
+ this.screen.flush(this.backend);
96
+ if (this.validateTreeAfterRender) {
97
+ assertValidTree(this.root);
98
+ }
99
+ }
100
+ }
101
+ /**
102
+ * Принудительно сделать следующий кадр полным (clear + рендер всего
103
+ * дерева). Для потребителей, меняющих картинку мимо markDirty-контракта
104
+ * (ресайз, тестовые харнессы).
105
+ */
106
+ invalidateScreen() {
107
+ this.needsFullRepaint = true;
108
+ }
109
+ /**
110
+ * Schedules a deferred render via setImmediate.
111
+ * Batches multiple markDirty() calls into a single frame.
112
+ * Skips rendering if layout is already clean (e.g. a synchronous
113
+ * renderFrame from handleInput already ran).
114
+ */
115
+ scheduleRender() {
116
+ if (this.renderScheduled)
117
+ return;
118
+ this.renderScheduled = true;
119
+ setImmediate(() => {
120
+ this.renderScheduled = false;
121
+ if (this.root?.isLayoutDirty) {
122
+ this.renderFrame();
123
+ }
124
+ });
125
+ }
126
+ handleInput(event) {
127
+ if (this.root) {
128
+ // Dispatch to focused element (or root) with capture/bubble
129
+ const tuiEvent = new TUIKeyboardEvent(event.type, {
130
+ key: event.key,
131
+ code: event.code,
132
+ ctrlKey: event.ctrlKey,
133
+ shiftKey: event.shiftKey,
134
+ altKey: event.altKey,
135
+ metaKey: event.metaKey,
136
+ raw: event.raw,
137
+ });
138
+ // Одно физическое нажатие — один логический получатель: парсер на каждый
139
+ // keydown синтезирует keypress сразу следом, и если обработчик keydown
140
+ // сменил фокус (оверлей закрылся, restoreFocus вернул фокус дереву),
141
+ // парный keypress не должен «протечь» новому владельцу фокуса. Поэтому
142
+ // цель keypress закрепляется за целью её keydown; keyup — отдельный
143
+ // момент времени, он идёт текущему фокусу.
144
+ let target = this.focusManager?.activeElement ?? this.root;
145
+ if (event.type === "keypress" &&
146
+ /* v8 ignore next 2 -- defensive: keypress эмитится парсером только сразу после парного keydown, который уже выставил пин */
147
+ this.pinnedKeypressTarget !== null) {
148
+ target = this.pinnedKeypressTarget;
149
+ }
150
+ this.pinnedKeypressTarget = event.type === "keydown" ? target : null;
151
+ const notPrevented = target.dispatchEvent(tuiEvent);
152
+ // Клавиатурный запрос контекстного меню (ContextMenu / Shift+F10)
153
+ // синтезируется в общее событие "contextmenu" — только если keydown
154
+ // не съеден (глобальный кейбинд делает preventDefault сам).
155
+ if (notPrevented && event.type === "keydown") {
156
+ const contextMenuEvent = contextMenuEventFromKeydown(tuiEvent, target);
157
+ if (contextMenuEvent) {
158
+ target.dispatchEvent(contextMenuEvent);
159
+ }
160
+ }
161
+ // Tab focus cycling (default behavior if not prevented, only on keydown)
162
+ if (notPrevented && event.key === "Tab" && event.type === "keydown" && this.focusManager) {
163
+ const direction = event.shiftKey ? "backward" : "forward";
164
+ this.focusManager.cycleFocus(direction);
165
+ }
166
+ this.renderAfterInput();
167
+ }
168
+ }
169
+ handlePaste(text) {
170
+ if (this.root) {
171
+ const event = new TUIPasteEvent(text);
172
+ const target = this.focusManager?.activeElement ?? this.root;
173
+ target.dispatchEvent(event);
174
+ this.renderAfterInput();
175
+ }
176
+ }
177
+ handleMouse(token) {
178
+ if (this.root) {
179
+ this.mouseDispatcher.handleMouseToken(token, this.root);
180
+ this.renderAfterInput();
181
+ }
182
+ }
183
+ /**
184
+ * Синхронный кадр после события ввода — только если обработчики что-то
185
+ * пометили грязным. Одно физическое нажатие приходит 2–3 событиями
186
+ * (keydown + синтезированный keypress, под Kitty ещё keyup) — раньше
187
+ * каждое рендерило полный кадр, из которых пустые давали чистый diff и
188
+ * жгли CPU впустую. Стилевые изменения гейт проходят: markStyleDirty
189
+ * заканчивается markDirty. Синхронность оставлена намеренно: тесты и
190
+ * обработчики читают экран/геометрию сразу после события.
191
+ */
192
+ renderAfterInput() {
193
+ if (this.root?.isLayoutDirty) {
194
+ this.renderFrame();
195
+ }
196
+ }
197
+ handleResize(size) {
198
+ this.screen = new TerminalScreen(size);
199
+ this.needsFullRepaint = true;
200
+ // Mark root as dirty so next render recalculates layout
201
+ if (this.root) {
202
+ this.root.markDirty();
203
+ }
204
+ this.renderFrame();
205
+ }
206
+ run() {
207
+ // Set up focus manager on root
208
+ if (this.root) {
209
+ this.focusManager = new FocusManager(this.root);
210
+ this.root.focusManager = this.focusManager;
211
+ this.root.setRequestRenderCallback(() => {
212
+ this.scheduleRender();
213
+ });
214
+ }
215
+ this.backend.setup();
216
+ this.backend.onInput((event) => {
217
+ this.handleInput(event);
218
+ });
219
+ this.backend.onResize((size) => {
220
+ this.handleResize(size);
221
+ });
222
+ this.backend.onPaste((text) => {
223
+ this.handlePaste(text);
224
+ });
225
+ this.backend.onMouse((token) => {
226
+ this.handleMouse(token);
227
+ });
228
+ // Initial render
229
+ this.renderFrame();
230
+ }
231
+ }