@tuidom/core 0.2.3 → 0.4.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 +10 -0
- package/dist/common/colorUtils.d.ts +49 -0
- package/dist/common/colorUtils.js +101 -0
- package/dist/dom/styles/tuiStyle.d.ts +6 -0
- package/dist/dom/styles/tuiStyle.js +11 -3
- package/dist/dom/tuiElement.d.ts +9 -2
- package/dist/dom/tuiElement.js +36 -12
- package/dist/input/keyInputParser.js +11 -1
- package/dist/input/rawTerminalToken.d.ts +10 -4
- package/dist/input/serializeKey.d.ts +3 -0
- package/dist/input/serializeKey.js +32 -0
- package/dist/input/tokenize.d.ts +13 -0
- package/dist/input/tokenize.js +38 -0
- package/dist/rendering/grid.d.ts +6 -0
- package/dist/rendering/grid.js +20 -10
- package/package.json +1 -1
|
@@ -27,6 +27,16 @@ export interface ITerminalBackend {
|
|
|
27
27
|
* reply within the probe window). Fire-and-forget — callers must not block on it.
|
|
28
28
|
*/
|
|
29
29
|
probeKeyboardProtocol(onResult: (supported: boolean) => void): void;
|
|
30
|
+
/**
|
|
31
|
+
* Asynchronously ask the terminal for its name and version (XTVERSION: `CSI > 0 q`
|
|
32
|
+
* → `DCS > | <name(version)> ST`). `onResult` fires exactly once with the reported
|
|
33
|
+
* string as-is (e.g. `"iTerm2 3.5.0"`, `"kitty(0.35.2)"`, `"WezTerm 20240203…"`), or
|
|
34
|
+
* `undefined` if the terminal doesn't answer within the probe window. The reply is
|
|
35
|
+
* consumed by the backend and never reaches `onInput`. Inside tmux the query goes
|
|
36
|
+
* to tmux itself (like the other probes), so the answer is tmux's own (`"tmux 3.4"`).
|
|
37
|
+
* Fire-and-forget — callers must not block on it.
|
|
38
|
+
*/
|
|
39
|
+
probeTerminalVersion(onResult: (nameAndVersion: string | undefined) => void): void;
|
|
30
40
|
/**
|
|
31
41
|
* Render a frame: receive the current grid and cursor position.
|
|
32
42
|
* The backend decides how to output it (ANSI diffing, simple copy, etc.).
|
|
@@ -3,17 +3,66 @@
|
|
|
3
3
|
* Must be negative so it's never confused with a packed RGB value (0x000000–0xFFFFFF).
|
|
4
4
|
*/
|
|
5
5
|
export declare const DEFAULT_COLOR = -1;
|
|
6
|
+
/**
|
|
7
|
+
* Полностью прозрачный цвет (`#RRGGBB00`, CSS `transparent`): при наложении
|
|
8
|
+
* ничего не меняет, а как собственный bg элемента — не красит. Сентинел, а не
|
|
9
|
+
* упаковка с альфой 0: старший байт 0 занят непрозрачными 24-битными числами.
|
|
10
|
+
*/
|
|
11
|
+
export declare const TRANSPARENT_COLOR = -2;
|
|
6
12
|
/** Pack three 8-bit channels into a single 24-bit integer. */
|
|
7
13
|
export declare function packRgb(r: number, g: number, b: number): number;
|
|
14
|
+
/**
|
|
15
|
+
* Упаковывает цвет с альфой (0..255). Непрозрачный (`a === 255`) даёт обычное
|
|
16
|
+
* 24-битное число — то же, что {@link packRgb}; `a === 0` — {@link TRANSPARENT_COLOR};
|
|
17
|
+
* промежуточная альфа кладётся в старший байт (`0xAARRGGBB`, беззнаковое
|
|
18
|
+
* значение > 0xFFFFFF). Так весь существующий код с 24-битными литералами
|
|
19
|
+
* остаётся валидным без миграции, а полупрозрачность — отличима одним сравнением.
|
|
20
|
+
*/
|
|
21
|
+
export declare function packRgba(r: number, g: number, b: number, a: number): number;
|
|
8
22
|
/** Extract the red channel (bits 16–23). */
|
|
9
23
|
export declare function unpackR(color: number): number;
|
|
10
24
|
/** Extract the green channel (bits 8–15). */
|
|
11
25
|
export declare function unpackG(color: number): number;
|
|
12
26
|
/** Extract the blue channel (bits 0–7). */
|
|
13
27
|
export declare function unpackB(color: number): number;
|
|
28
|
+
/**
|
|
29
|
+
* Альфа цвета: 255 у 24-битных чисел и сентинелов (`DEFAULT_COLOR` — цвет
|
|
30
|
+
* терминала, он непрозрачен), 0 у {@link TRANSPARENT_COLOR}, иначе старший байт.
|
|
31
|
+
*/
|
|
32
|
+
export declare function unpackA(color: number): number;
|
|
33
|
+
/** true для полупрозрачного цвета (альфа 1..254) — того, что требует композитинга с подложкой. */
|
|
34
|
+
export declare function isTranslucent(color: number): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* true, если число — легальное значение цвета: 24-битный RGB, упаковка с
|
|
37
|
+
* альфой, {@link DEFAULT_COLOR} или {@link TRANSPARENT_COLOR}. Сентинелы
|
|
38
|
+
* каскада (`INHERITED_*`) и мусор — false.
|
|
39
|
+
*/
|
|
40
|
+
export declare function isColorValue(color: number): boolean;
|
|
14
41
|
/**
|
|
15
42
|
* Накладывает полупрозрачный `fg` на непрозрачную подложку `bg` и возвращает
|
|
16
43
|
* непрозрачный результат: терминал альфы не умеет, поэтому композитинг
|
|
17
44
|
* выполняется заранее (`alpha` — доля 0..1).
|
|
18
45
|
*/
|
|
19
46
|
export declare function blendRgb(fg: number, bg: number, alpha: number): number;
|
|
47
|
+
/**
|
|
48
|
+
* Композитинг «`color` поверх `under`» (source-over): результат всегда
|
|
49
|
+
* непрозрачен или `DEFAULT_COLOR`. Непрозрачный `color` (и `DEFAULT_COLOR`)
|
|
50
|
+
* возвращается как есть — путь нулевой стоимости, одно сравнение;
|
|
51
|
+
* {@link TRANSPARENT_COLOR} оставляет подложку. Полупрозрачный цвет смешивается
|
|
52
|
+
* с `under` по альфе; если подложка — `DEFAULT_COLOR` (цвет терминала, нам
|
|
53
|
+
* неизвестен), смешивать не с чем — альфа отбрасывается, цвет ложится непрозрачным.
|
|
54
|
+
*/
|
|
55
|
+
export declare function compositeOver(color: number, under: number): number;
|
|
56
|
+
/**
|
|
57
|
+
* Разбирает CSS-подобный hex: `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`
|
|
58
|
+
* (решётка необязательна, регистр любой) в упакованный цвет (см.
|
|
59
|
+
* {@link packRgba}). Некорректная строка — throw: цвет темы с опечаткой
|
|
60
|
+
* должен падать на разборе, а не красить в чёрный.
|
|
61
|
+
*/
|
|
62
|
+
export declare function parseHexColor(hex: string): number;
|
|
63
|
+
/**
|
|
64
|
+
* Обратное к {@link parseHexColor}: `#rrggbb` для непрозрачного, `#rrggbbaa`
|
|
65
|
+
* с альфой, `#00000000` для {@link TRANSPARENT_COLOR}. `DEFAULT_COLOR` hex не
|
|
66
|
+
* имеет — throw.
|
|
67
|
+
*/
|
|
68
|
+
export declare function formatHexColor(color: number): string;
|
|
@@ -3,10 +3,34 @@
|
|
|
3
3
|
* Must be negative so it's never confused with a packed RGB value (0x000000–0xFFFFFF).
|
|
4
4
|
*/
|
|
5
5
|
export const DEFAULT_COLOR = -1;
|
|
6
|
+
/**
|
|
7
|
+
* Полностью прозрачный цвет (`#RRGGBB00`, CSS `transparent`): при наложении
|
|
8
|
+
* ничего не меняет, а как собственный bg элемента — не красит. Сентинел, а не
|
|
9
|
+
* упаковка с альфой 0: старший байт 0 занят непрозрачными 24-битными числами.
|
|
10
|
+
*/
|
|
11
|
+
export const TRANSPARENT_COLOR = -2;
|
|
12
|
+
/** Максимальное упакованное значение с альфой (`0xFE_FFFFFF`); всё выше — не цвет. */
|
|
13
|
+
const MAX_PACKED_COLOR = 0xfeffffff;
|
|
14
|
+
/** Множитель старшего байта — альфа хранится через умножение, а не `<<`, чтобы не уйти в отрицательные. */
|
|
15
|
+
const ALPHA_UNIT = 0x1000000;
|
|
6
16
|
/** Pack three 8-bit channels into a single 24-bit integer. */
|
|
7
17
|
export function packRgb(r, g, b) {
|
|
8
18
|
return (r << 16) | (g << 8) | b;
|
|
9
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Упаковывает цвет с альфой (0..255). Непрозрачный (`a === 255`) даёт обычное
|
|
22
|
+
* 24-битное число — то же, что {@link packRgb}; `a === 0` — {@link TRANSPARENT_COLOR};
|
|
23
|
+
* промежуточная альфа кладётся в старший байт (`0xAARRGGBB`, беззнаковое
|
|
24
|
+
* значение > 0xFFFFFF). Так весь существующий код с 24-битными литералами
|
|
25
|
+
* остаётся валидным без миграции, а полупрозрачность — отличима одним сравнением.
|
|
26
|
+
*/
|
|
27
|
+
export function packRgba(r, g, b, a) {
|
|
28
|
+
if (a >= 0xff)
|
|
29
|
+
return packRgb(r, g, b);
|
|
30
|
+
if (a <= 0)
|
|
31
|
+
return TRANSPARENT_COLOR;
|
|
32
|
+
return a * ALPHA_UNIT + packRgb(r, g, b);
|
|
33
|
+
}
|
|
10
34
|
/** Extract the red channel (bits 16–23). */
|
|
11
35
|
export function unpackR(color) {
|
|
12
36
|
return (color >> 16) & 0xff;
|
|
@@ -19,6 +43,31 @@ export function unpackG(color) {
|
|
|
19
43
|
export function unpackB(color) {
|
|
20
44
|
return color & 0xff;
|
|
21
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Альфа цвета: 255 у 24-битных чисел и сентинелов (`DEFAULT_COLOR` — цвет
|
|
48
|
+
* терминала, он непрозрачен), 0 у {@link TRANSPARENT_COLOR}, иначе старший байт.
|
|
49
|
+
*/
|
|
50
|
+
export function unpackA(color) {
|
|
51
|
+
if (color === TRANSPARENT_COLOR)
|
|
52
|
+
return 0;
|
|
53
|
+
if (color <= 0xffffff)
|
|
54
|
+
return 0xff;
|
|
55
|
+
return color >>> 24;
|
|
56
|
+
}
|
|
57
|
+
/** true для полупрозрачного цвета (альфа 1..254) — того, что требует композитинга с подложкой. */
|
|
58
|
+
export function isTranslucent(color) {
|
|
59
|
+
return color > 0xffffff;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* true, если число — легальное значение цвета: 24-битный RGB, упаковка с
|
|
63
|
+
* альфой, {@link DEFAULT_COLOR} или {@link TRANSPARENT_COLOR}. Сентинелы
|
|
64
|
+
* каскада (`INHERITED_*`) и мусор — false.
|
|
65
|
+
*/
|
|
66
|
+
export function isColorValue(color) {
|
|
67
|
+
return (color === DEFAULT_COLOR ||
|
|
68
|
+
color === TRANSPARENT_COLOR ||
|
|
69
|
+
(Number.isInteger(color) && color >= 0 && color <= MAX_PACKED_COLOR));
|
|
70
|
+
}
|
|
22
71
|
/**
|
|
23
72
|
* Накладывает полупрозрачный `fg` на непрозрачную подложку `bg` и возвращает
|
|
24
73
|
* непрозрачный результат: терминал альфы не умеет, поэтому композитинг
|
|
@@ -28,3 +77,55 @@ export function blendRgb(fg, bg, alpha) {
|
|
|
28
77
|
const mix = (f, b) => Math.round(f * alpha + b * (1 - alpha));
|
|
29
78
|
return packRgb(mix(unpackR(fg), unpackR(bg)), mix(unpackG(fg), unpackG(bg)), mix(unpackB(fg), unpackB(bg)));
|
|
30
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Композитинг «`color` поверх `under`» (source-over): результат всегда
|
|
82
|
+
* непрозрачен или `DEFAULT_COLOR`. Непрозрачный `color` (и `DEFAULT_COLOR`)
|
|
83
|
+
* возвращается как есть — путь нулевой стоимости, одно сравнение;
|
|
84
|
+
* {@link TRANSPARENT_COLOR} оставляет подложку. Полупрозрачный цвет смешивается
|
|
85
|
+
* с `under` по альфе; если подложка — `DEFAULT_COLOR` (цвет терминала, нам
|
|
86
|
+
* неизвестен), смешивать не с чем — альфа отбрасывается, цвет ложится непрозрачным.
|
|
87
|
+
*/
|
|
88
|
+
export function compositeOver(color, under) {
|
|
89
|
+
if (color <= 0xffffff)
|
|
90
|
+
return color === TRANSPARENT_COLOR ? under : color;
|
|
91
|
+
const rgb = color & 0xffffff;
|
|
92
|
+
if (under < 0)
|
|
93
|
+
return rgb;
|
|
94
|
+
return blendRgb(rgb, under, (color >>> 24) / 0xff);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Разбирает CSS-подобный hex: `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`
|
|
98
|
+
* (решётка необязательна, регистр любой) в упакованный цвет (см.
|
|
99
|
+
* {@link packRgba}). Некорректная строка — throw: цвет темы с опечаткой
|
|
100
|
+
* должен падать на разборе, а не красить в чёрный.
|
|
101
|
+
*/
|
|
102
|
+
export function parseHexColor(hex) {
|
|
103
|
+
const digits = hex.startsWith("#") ? hex.slice(1) : hex;
|
|
104
|
+
const short = digits.length === 3 || digits.length === 4;
|
|
105
|
+
if (!(short || digits.length === 6 || digits.length === 8) || !/^[0-9a-fA-F]+$/.test(digits)) {
|
|
106
|
+
throw new Error(`parseHexColor: некорректный hex-цвет "${hex}"`);
|
|
107
|
+
}
|
|
108
|
+
const channel = (i) => {
|
|
109
|
+
if (short) {
|
|
110
|
+
const d = parseInt(digits[i], 16);
|
|
111
|
+
return d * 16 + d;
|
|
112
|
+
}
|
|
113
|
+
return parseInt(digits.slice(i * 2, i * 2 + 2), 16);
|
|
114
|
+
};
|
|
115
|
+
const hasAlpha = digits.length === 4 || digits.length === 8;
|
|
116
|
+
return packRgba(channel(0), channel(1), channel(2), hasAlpha ? channel(3) : 0xff);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Обратное к {@link parseHexColor}: `#rrggbb` для непрозрачного, `#rrggbbaa`
|
|
120
|
+
* с альфой, `#00000000` для {@link TRANSPARENT_COLOR}. `DEFAULT_COLOR` hex не
|
|
121
|
+
* имеет — throw.
|
|
122
|
+
*/
|
|
123
|
+
export function formatHexColor(color) {
|
|
124
|
+
if (color === DEFAULT_COLOR)
|
|
125
|
+
throw new Error("formatHexColor: DEFAULT_COLOR не имеет hex-представления");
|
|
126
|
+
if (color === TRANSPARENT_COLOR)
|
|
127
|
+
return "#00000000";
|
|
128
|
+
const hex2 = (n) => n.toString(16).padStart(2, "0");
|
|
129
|
+
const rgb = `#${hex2(unpackR(color))}${hex2(unpackG(color))}${hex2(unpackB(color))}`;
|
|
130
|
+
return isTranslucent(color) ? `${rgb}${hex2(unpackA(color))}` : rgb;
|
|
131
|
+
}
|
|
@@ -72,4 +72,10 @@ export declare function mergeStyleVariants(style: TUIStyle, isActive: (selector:
|
|
|
72
72
|
fg: StyleColor | undefined;
|
|
73
73
|
bg: StyleColor | undefined;
|
|
74
74
|
};
|
|
75
|
+
/**
|
|
76
|
+
* Резолв базовых fg/bg без when-вариантов и var-scope (утилита для тестов и
|
|
77
|
+
* простых потребителей; ядро идёт через `TUIElement.performStyleResolution`,
|
|
78
|
+
* та же модель): цвет с альфой композитится с унаследованным bg, fg — с
|
|
79
|
+
* итоговым bg, наружу уходят непрозрачные значения.
|
|
80
|
+
*/
|
|
75
81
|
export declare function resolveStyle(style: TUIStyle, inherited: ResolvedTUIStyle): ResolvedTUIStyle;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_COLOR } from "../../common/colorUtils.js";
|
|
1
|
+
import { compositeOver, DEFAULT_COLOR } from "../../common/colorUtils.js";
|
|
2
2
|
import { ROOT_VAR_SCOPE } from "./styleTokens.js";
|
|
3
3
|
// ─── Inherited Color Sentinels ───
|
|
4
4
|
// Sentinel values that resolve to the inherited fg/bg from the parent.
|
|
@@ -114,8 +114,16 @@ export function mergeStyleVariants(style, isActive) {
|
|
|
114
114
|
}
|
|
115
115
|
return { fg, bg };
|
|
116
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Резолв базовых fg/bg без when-вариантов и var-scope (утилита для тестов и
|
|
119
|
+
* простых потребителей; ядро идёт через `TUIElement.performStyleResolution`,
|
|
120
|
+
* та же модель): цвет с альфой композитится с унаследованным bg, fg — с
|
|
121
|
+
* итоговым bg, наружу уходят непрозрачные значения.
|
|
122
|
+
*/
|
|
117
123
|
export function resolveStyle(style, inherited) {
|
|
118
|
-
const
|
|
119
|
-
const bg =
|
|
124
|
+
const ownBg = style.bg !== undefined ? resolveStyleColor(style.bg, inherited.fg, inherited.bg) : inherited.bg;
|
|
125
|
+
const bg = compositeOver(ownBg, inherited.bg);
|
|
126
|
+
const ownFg = style.fg !== undefined ? resolveStyleColor(style.fg, inherited.fg, inherited.bg) : inherited.fg;
|
|
127
|
+
const fg = compositeOver(ownFg, bg);
|
|
120
128
|
return { fg, bg };
|
|
121
129
|
}
|
package/dist/dom/tuiElement.d.ts
CHANGED
|
@@ -143,6 +143,8 @@ export declare class TUIElement {
|
|
|
143
143
|
private subtreeStyleDirty;
|
|
144
144
|
private appliedFgValue;
|
|
145
145
|
private appliedBgValue;
|
|
146
|
+
/** Красит ли элемент собственный фон: bg задан своим стилем и не TRANSPARENT_COLOR. */
|
|
147
|
+
private paintsOwnBackgroundValue;
|
|
146
148
|
private styleStatesSet;
|
|
147
149
|
private styleVarsValue;
|
|
148
150
|
private varScopeRef;
|
|
@@ -283,8 +285,9 @@ export declare class TUIElement {
|
|
|
283
285
|
* предков и дефолтов tuidom (STYLE_TOKEN_DEFAULTS). Обычное место — корень:
|
|
284
286
|
* хост транслирует сюда палитру темы одним вызовом (hot-swap = повторный
|
|
285
287
|
* вызов). Таблица заменяется целиком, null — снимает. Значения — только
|
|
286
|
-
* конкретные числа (packed RGB
|
|
287
|
-
*
|
|
288
|
+
* конкретные числа (packed RGB, в том числе с альфой — `packRgba`/
|
|
289
|
+
* `parseHexColor`, | DEFAULT_COLOR | TRANSPARENT_COLOR); сентинелы
|
|
290
|
+
* INHERITED_* нелегальны.
|
|
288
291
|
*/
|
|
289
292
|
setStyleVars(vars: Readonly<Record<string, number>> | null): void;
|
|
290
293
|
/**
|
|
@@ -503,6 +506,10 @@ export declare class TUIElement {
|
|
|
503
506
|
* флагов стиля: собственный фон — это новая поверхность, и атрибуты того,
|
|
504
507
|
* что нарисовано под ней раньше в этом кадре (подчёркивание диагностики под
|
|
505
508
|
* попапом, жирный токен под диалогом), на ней проступать не должны.
|
|
509
|
+
*
|
|
510
|
+
* Полупрозрачный собственный bg уже скомпозичен каскадом с унаследованным
|
|
511
|
+
* (см. {@link performStyleResolution}); `TRANSPARENT_COLOR` (`#RRGGBB00`)
|
|
512
|
+
* считается «фона нет» — как CSS `transparent`, элемент ничего не красит.
|
|
506
513
|
*/
|
|
507
514
|
protected paintOwnBackground(context: RenderContext): void;
|
|
508
515
|
/** true, если фон задан собственным стилем (см. {@link paintOwnBackground}). */
|
package/dist/dom/tuiElement.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { compositeOver, isColorValue, TRANSPARENT_COLOR } from "../common/colorUtils.js";
|
|
2
2
|
import { DisplayLine } from "../common/displayLine.js";
|
|
3
3
|
import { BoxConstraints, Offset, Point, Rect, Size } from "../common/geometryPromitives.js";
|
|
4
4
|
import { StyleFlags } from "../common/styleFlags.js";
|
|
@@ -217,6 +217,8 @@ export class TUIElement {
|
|
|
217
217
|
// вопрос «задан ли цвет собственным стилем» (заливка фона, инспектор).
|
|
218
218
|
appliedFgValue;
|
|
219
219
|
appliedBgValue;
|
|
220
|
+
/** Красит ли элемент собственный фон: bg задан своим стилем и не TRANSPARENT_COLOR. */
|
|
221
|
+
paintsOwnBackgroundValue = false;
|
|
220
222
|
// Активные состояния (hover/focus ведёт ядро, прочие — виджеты). Lazy:
|
|
221
223
|
// у подавляющего большинства элементов состояний нет.
|
|
222
224
|
styleStatesSet = null;
|
|
@@ -379,7 +381,9 @@ export class TUIElement {
|
|
|
379
381
|
/** Гасит фокус, если activeElement — этот элемент или его потомок. */
|
|
380
382
|
releaseFocusIfInside() {
|
|
381
383
|
const fm = this.getRoot()?.focusManager ?? null;
|
|
382
|
-
|
|
384
|
+
if (fm === null)
|
|
385
|
+
return;
|
|
386
|
+
let node = fm.activeElement ?? null;
|
|
383
387
|
while (node !== null && node !== this) {
|
|
384
388
|
node = node.getParent();
|
|
385
389
|
}
|
|
@@ -569,13 +573,19 @@ export class TUIElement {
|
|
|
569
573
|
const vars = this.styleVarsValue !== null ? extendVarScope(context.vars, this.styleVarsValue) : context.vars;
|
|
570
574
|
this.varScopeRef = vars;
|
|
571
575
|
const describe = () => this.describeForStyleError();
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
const
|
|
576
|
+
// Цвет с альфой композитится уже здесь, с унаследованным bg: в
|
|
577
|
+
// resolvedStyle и детям уходят непрозрачные значения, иначе заливка
|
|
578
|
+
// фона и текст с тем же bg наложились бы на ячейку дважды.
|
|
579
|
+
const ownBg = applied.bg !== undefined
|
|
576
580
|
? resolveStyleColor(applied.bg, context.fg, context.bg, vars, describe)
|
|
577
581
|
: context.bg;
|
|
582
|
+
const bg = compositeOver(ownBg, context.bg);
|
|
583
|
+
const ownFg = applied.fg !== undefined
|
|
584
|
+
? resolveStyleColor(applied.fg, context.fg, context.bg, vars, describe)
|
|
585
|
+
: context.fg;
|
|
586
|
+
const fg = compositeOver(ownFg, bg);
|
|
578
587
|
this.resolvedStyleValue = { fg, bg };
|
|
588
|
+
this.paintsOwnBackgroundValue = applied.bg !== undefined && ownBg !== TRANSPARENT_COLOR;
|
|
579
589
|
this.childStyleContext = this.buildChildStyleContext(context);
|
|
580
590
|
}
|
|
581
591
|
this.isStyleDirty = false;
|
|
@@ -634,15 +644,16 @@ export class TUIElement {
|
|
|
634
644
|
* предков и дефолтов tuidom (STYLE_TOKEN_DEFAULTS). Обычное место — корень:
|
|
635
645
|
* хост транслирует сюда палитру темы одним вызовом (hot-swap = повторный
|
|
636
646
|
* вызов). Таблица заменяется целиком, null — снимает. Значения — только
|
|
637
|
-
* конкретные числа (packed RGB
|
|
638
|
-
*
|
|
647
|
+
* конкретные числа (packed RGB, в том числе с альфой — `packRgba`/
|
|
648
|
+
* `parseHexColor`, | DEFAULT_COLOR | TRANSPARENT_COLOR); сентинелы
|
|
649
|
+
* INHERITED_* нелегальны.
|
|
639
650
|
*/
|
|
640
651
|
setStyleVars(vars) {
|
|
641
652
|
if (vars === this.styleVarsValue)
|
|
642
653
|
return;
|
|
643
654
|
if (vars !== null) {
|
|
644
655
|
for (const key of Object.keys(vars)) {
|
|
645
|
-
if (vars[key]
|
|
656
|
+
if (!isColorValue(vars[key])) {
|
|
646
657
|
throw new Error(`${this.describeForStyleError()}.setStyleVars: токен "${key}" содержит сентинел/некорректное значение ${vars[key]} — таблицы принимают только конкретные цвета`);
|
|
647
658
|
}
|
|
648
659
|
}
|
|
@@ -731,6 +742,7 @@ export class TUIElement {
|
|
|
731
742
|
* состояние нужно ДО стилевого прохода, прямо в performLayout.
|
|
732
743
|
*/
|
|
733
744
|
hasStyleStateWithin(state) {
|
|
745
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
734
746
|
let node = this;
|
|
735
747
|
while (node !== null) {
|
|
736
748
|
if (node.styleStatesSet?.has(state) === true)
|
|
@@ -769,6 +781,7 @@ export class TUIElement {
|
|
|
769
781
|
// цепочка предков (иначе любой markDirty = полноэкранный damage).
|
|
770
782
|
this.isPaintDirty = true;
|
|
771
783
|
this.isLayoutDirty = true;
|
|
784
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
772
785
|
let top = this;
|
|
773
786
|
for (let current = this._parent; current !== null; current = current._parent) {
|
|
774
787
|
current.isLayoutDirty = true;
|
|
@@ -802,7 +815,11 @@ export class TUIElement {
|
|
|
802
815
|
}
|
|
803
816
|
const rect = new Rect(new Point(parentOrigin.x + this.localPosition.dx, parentOrigin.y + this.localPosition.dy), this.allocatedSize);
|
|
804
817
|
const old = this.lastPaintedRect;
|
|
805
|
-
|
|
818
|
+
// Ещё не рисовался — тоже moved. Optional chain здесь не подходит: после
|
|
819
|
+
// первого `old?.x` тип уже сужен, и остальные `old?.` стали бы лишними.
|
|
820
|
+
const moved =
|
|
821
|
+
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
|
|
822
|
+
old === null ||
|
|
806
823
|
old.x !== rect.x ||
|
|
807
824
|
old.y !== rect.y ||
|
|
808
825
|
old.width !== rect.width ||
|
|
@@ -835,6 +852,9 @@ export class TUIElement {
|
|
|
835
852
|
* собственный хром по состоянию ребёнка вне его rect'а (ScrollBarDecorator:
|
|
836
853
|
* бегунок в колонке за пределами ребёнка).
|
|
837
854
|
*/
|
|
855
|
+
// Именно геттер, не readonly-поле: подклассы переопределяют его геттером
|
|
856
|
+
// (ListViewElement, ScrollContainerElement), а поле базы затенило бы их прототипный аксессор.
|
|
857
|
+
// eslint-disable-next-line @typescript-eslint/class-literal-property-style
|
|
838
858
|
get paintsSubtreeAtomically() {
|
|
839
859
|
return false;
|
|
840
860
|
}
|
|
@@ -1090,9 +1110,13 @@ export class TUIElement {
|
|
|
1090
1110
|
* флагов стиля: собственный фон — это новая поверхность, и атрибуты того,
|
|
1091
1111
|
* что нарисовано под ней раньше в этом кадре (подчёркивание диагностики под
|
|
1092
1112
|
* попапом, жирный токен под диалогом), на ней проступать не должны.
|
|
1113
|
+
*
|
|
1114
|
+
* Полупрозрачный собственный bg уже скомпозичен каскадом с унаследованным
|
|
1115
|
+
* (см. {@link performStyleResolution}); `TRANSPARENT_COLOR` (`#RRGGBB00`)
|
|
1116
|
+
* считается «фона нет» — как CSS `transparent`, элемент ничего не красит.
|
|
1093
1117
|
*/
|
|
1094
1118
|
paintOwnBackground(context) {
|
|
1095
|
-
if (this.
|
|
1119
|
+
if (!this.paintsOwnBackgroundValue)
|
|
1096
1120
|
return;
|
|
1097
1121
|
const { fg, bg } = this.resolvedStyleValue;
|
|
1098
1122
|
const { width, height } = this.layoutSize;
|
|
@@ -1104,7 +1128,7 @@ export class TUIElement {
|
|
|
1104
1128
|
}
|
|
1105
1129
|
/** true, если фон задан собственным стилем (см. {@link paintOwnBackground}). */
|
|
1106
1130
|
get hasOwnBackground() {
|
|
1107
|
-
return this.
|
|
1131
|
+
return this.paintsOwnBackgroundValue;
|
|
1108
1132
|
}
|
|
1109
1133
|
/**
|
|
1110
1134
|
* Сырые цвета после when-merge, до резолва токенов/сентинелов: что элемент
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { convertTokenToKeyPressEvent } from "./convertToken.js";
|
|
2
2
|
import { createKeyPressEvent } from "./keyEvent.js";
|
|
3
|
-
import { parseCSI, parseOSC, tokenize } from "./tokenize.js";
|
|
3
|
+
import { parseCSI, parseOSC, parseXtversionReply, tokenize, XTVERSION_REPLY_PREFIX } from "./tokenize.js";
|
|
4
4
|
/**
|
|
5
5
|
* Index where an *incomplete* trailing escape sequence begins, or -1 if `data`
|
|
6
6
|
* ends on a clean token boundary.
|
|
@@ -16,6 +16,12 @@ import { parseCSI, parseOSC, tokenize } from "./tokenize.js";
|
|
|
16
16
|
* incomplete — anything before it is already followed by more bytes.
|
|
17
17
|
*/
|
|
18
18
|
function incompleteTailStart(data) {
|
|
19
|
+
// Ответ XTVERSION (`ESC P > | … ESC \\`) — единственная строка, внутри которой
|
|
20
|
+
// может стоять «последний» ESC (от ST, разрезанного между чтениями), поэтому тело
|
|
21
|
+
// без терминатора проверяем раньше, чем ищем последний ESC.
|
|
22
|
+
const dcs = data.lastIndexOf(XTVERSION_REPLY_PREFIX);
|
|
23
|
+
if (dcs !== -1 && parseXtversionReply(data, dcs) === null)
|
|
24
|
+
return dcs;
|
|
19
25
|
const esc = data.lastIndexOf("\x1b");
|
|
20
26
|
if (esc === -1)
|
|
21
27
|
return -1;
|
|
@@ -30,6 +36,10 @@ function incompleteTailStart(data) {
|
|
|
30
36
|
return parseOSC(data, esc) === null ? esc : -1; // ESC ]
|
|
31
37
|
if (next === 0x4f)
|
|
32
38
|
return esc + 2 >= data.length ? esc : -1; // ESC O (SS3) needs its final letter
|
|
39
|
+
// ESC P: незавершённое начало ответа XTVERSION (`ESC P >`) ждём. Хвост, уже
|
|
40
|
+
// разошедшийся с префиксом, — обычный Alt+Shift+P (ESC + символ), он завершён.
|
|
41
|
+
if (next === 0x50)
|
|
42
|
+
return XTVERSION_REPLY_PREFIX.startsWith(data.slice(esc)) ? esc : -1;
|
|
33
43
|
// ESC + printable / control / special is complete once the second byte is present (it is).
|
|
34
44
|
return -1;
|
|
35
45
|
}
|
|
@@ -111,18 +111,24 @@ export interface OscToken {
|
|
|
111
111
|
readonly raw: string;
|
|
112
112
|
}
|
|
113
113
|
/**
|
|
114
|
-
* A
|
|
114
|
+
* A response to one of our capability probes — NOT a key event.
|
|
115
115
|
* - "kitty-flags": reply to the Kitty keyboard-protocol query `CSI ? u` → `CSI ? <flags> u`.
|
|
116
116
|
* - "da1": reply to Primary Device Attributes `CSI c` → `CSI ? <attrs> c`.
|
|
117
|
-
*
|
|
117
|
+
* - "xtversion": reply to XTVERSION `CSI > 0 q` → `DCS > | <name(version)> ST`.
|
|
118
|
+
* The CSI replies begin with the private marker `?`, which never appears in a real key
|
|
119
|
+
* event; the XTVERSION reply is a DCS string (`ESC P > |`), not a key either.
|
|
118
120
|
*/
|
|
119
121
|
export interface DeviceReportToken {
|
|
120
122
|
readonly kind: "device-report";
|
|
121
|
-
readonly report:
|
|
122
|
-
/**
|
|
123
|
+
readonly report: DeviceReportKind;
|
|
124
|
+
/**
|
|
125
|
+
* For CSI replies — raw parameter bytes including the leading `?` (e.g. "?15" or
|
|
126
|
+
* "?62;1;6"). For "xtversion" — the payload between `DCS > |` and ST (e.g. "iTerm2 3.5.0").
|
|
127
|
+
*/
|
|
123
128
|
readonly params: string;
|
|
124
129
|
readonly raw: string;
|
|
125
130
|
}
|
|
131
|
+
export type DeviceReportKind = "kitty-flags" | "da1" | "xtversion";
|
|
126
132
|
export type MouseButton = "left" | "middle" | "right" | "none";
|
|
127
133
|
export type MouseAction = "press" | "release" | "move" | "scroll-up" | "scroll-down" | "scroll-left" | "scroll-right";
|
|
128
134
|
export interface MouseToken {
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
* serializeKey('Ctrl+ArrowUp') → '\x1b[1;5A'
|
|
9
9
|
* serializeKey('F5') → '\x1b[15~'
|
|
10
10
|
* serializeKey('Alt+a') → '\x1ba'
|
|
11
|
+
* serializeKey('Meta+s') → '\x1b[115;9u'
|
|
12
|
+
* serializeKey('Ctrl+Shift+a') → '\x1b[97;6u'
|
|
11
13
|
*
|
|
12
14
|
* Supports modifier prefixes: Ctrl+, Shift+, Alt+, Meta+ (combinable).
|
|
15
|
+
* Meta — это super-бит kitty (8): так приходит Cmd на маке.
|
|
13
16
|
*/
|
|
14
17
|
export declare function serializeKey(name: string): string;
|
|
@@ -8,9 +8,13 @@
|
|
|
8
8
|
* serializeKey('Ctrl+ArrowUp') → '\x1b[1;5A'
|
|
9
9
|
* serializeKey('F5') → '\x1b[15~'
|
|
10
10
|
* serializeKey('Alt+a') → '\x1ba'
|
|
11
|
+
* serializeKey('Meta+s') → '\x1b[115;9u'
|
|
12
|
+
* serializeKey('Ctrl+Shift+a') → '\x1b[97;6u'
|
|
11
13
|
*
|
|
12
14
|
* Supports modifier prefixes: Ctrl+, Shift+, Alt+, Meta+ (combinable).
|
|
15
|
+
* Meta — это super-бит kitty (8): так приходит Cmd на маке.
|
|
13
16
|
*/
|
|
17
|
+
import { kittyCodepointMap } from "./tokenize.js";
|
|
14
18
|
/** Simple special keys (no modifiers, no CSI) */
|
|
15
19
|
const simpleSpecialKeys = {
|
|
16
20
|
Enter: "\x0d",
|
|
@@ -73,6 +77,24 @@ function encodeModifier(ctrl, shift, alt, meta) {
|
|
|
73
77
|
mod += 8;
|
|
74
78
|
return mod;
|
|
75
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Codepoint для общей CSI u-формы, если `ch` — один печатный символ, который
|
|
82
|
+
* parseInput вернёт как есть: не управляющий и не занятый kitty под
|
|
83
|
+
* функциональную клавишу (PUA). Буква сворачивается в нижний регистр — так
|
|
84
|
+
* kitty шлёт базовую клавишу; регистр в событии даёт Shift-бит.
|
|
85
|
+
*/
|
|
86
|
+
function printableCodepoint(ch) {
|
|
87
|
+
const lower = ch.toLowerCase();
|
|
88
|
+
const codepoint = lower.codePointAt(0);
|
|
89
|
+
// Пусто («Meta+»), имя клавиши («Foo») или буква, чей lowercase — не один символ («İ»).
|
|
90
|
+
if (codepoint === undefined || String.fromCodePoint(codepoint) !== lower)
|
|
91
|
+
return undefined;
|
|
92
|
+
if (codepoint < 0x20 || (codepoint >= 0x7f && codepoint <= 0x9f))
|
|
93
|
+
return undefined;
|
|
94
|
+
if (kittyCodepointMap[codepoint] !== undefined)
|
|
95
|
+
return undefined;
|
|
96
|
+
return codepoint;
|
|
97
|
+
}
|
|
76
98
|
export function serializeKey(name) {
|
|
77
99
|
// Parse modifier prefixes: "Ctrl+Shift+ArrowUp" → modifiers + "ArrowUp"
|
|
78
100
|
let ctrl = false;
|
|
@@ -175,5 +197,15 @@ export function serializeKey(name) {
|
|
|
175
197
|
if (!hasModifiers && remaining.length === 1) {
|
|
176
198
|
return remaining;
|
|
177
199
|
}
|
|
200
|
+
// Печатный символ с любым другим набором модификаторов — общая форма kitty
|
|
201
|
+
// CSI <codepoint>;<mod>u. Сюда доходят только комбинации, у которых нет
|
|
202
|
+
// legacy-кодировки (ветки выше их уже вернули): Meta+s, Ctrl+Shift+a, Shift+1…
|
|
203
|
+
// Как и kitty, шлём codepoint базовой клавиши: буква — в нижнем регистре, а
|
|
204
|
+
// регистр задаёт только Shift-префикс (Meta+S ≡ Meta+s, как Ctrl+A ≡ Ctrl+a).
|
|
205
|
+
const codepoint = printableCodepoint(remaining === "Space" ? " " : remaining);
|
|
206
|
+
if (codepoint !== undefined) {
|
|
207
|
+
const mod = encodeModifier(ctrl, shift, alt, meta);
|
|
208
|
+
return `\x1b[${codepoint.toString()};${mod.toString()}u`;
|
|
209
|
+
}
|
|
178
210
|
throw new Error(`serializeKey: unknown key name "${name}". Add it to the mapping.`);
|
|
179
211
|
}
|
package/dist/input/tokenize.d.ts
CHANGED
|
@@ -56,4 +56,17 @@ interface OSCTokenResult {
|
|
|
56
56
|
* Returns null if the sequence is not complete.
|
|
57
57
|
*/
|
|
58
58
|
export declare function parseOSC(data: string, start: number): OSCTokenResult | null;
|
|
59
|
+
/**
|
|
60
|
+
* Начало ответа на XTVERSION (`CSI > 0 q`): `DCS > |`. Разбираем только его: голое
|
|
61
|
+
* `ESC P` — это ещё и Alt+Shift+P legacy-терминала, так что произвольный DCS не ловим.
|
|
62
|
+
*/
|
|
63
|
+
export declare const XTVERSION_REPLY_PREFIX = "\u001BP>|";
|
|
64
|
+
/**
|
|
65
|
+
* Parse an XTVERSION reply `ESC P > | <text> ESC \` starting at data[start] = ESC.
|
|
66
|
+
* Returns null if the string terminator (ST) hasn't arrived yet.
|
|
67
|
+
*/
|
|
68
|
+
export declare function parseXtversionReply(data: string, start: number): {
|
|
69
|
+
token: DeviceReportToken;
|
|
70
|
+
nextIndex: number;
|
|
71
|
+
} | null;
|
|
59
72
|
export {};
|
package/dist/input/tokenize.js
CHANGED
|
@@ -57,6 +57,18 @@ export function tokenize(data) {
|
|
|
57
57
|
tokens.push({ kind: "standalone-esc", raw: data[i] });
|
|
58
58
|
i++;
|
|
59
59
|
}
|
|
60
|
+
else if (next === 0x50 && data.startsWith(XTVERSION_REPLY_PREFIX, i)) {
|
|
61
|
+
// DCS-ответ на XTVERSION: \x1bP>|<name(version)>\x1b\\. Без ST (обрезан, и
|
|
62
|
+
// KeyInputParser его уже не ждёт) — падаем в ESC+символ, как было до разбора DCS.
|
|
63
|
+
const dcsResult = parseXtversionReply(data, i);
|
|
64
|
+
if (dcsResult) {
|
|
65
|
+
tokens.push(dcsResult.token);
|
|
66
|
+
i = dcsResult.nextIndex;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
tokens.push({ kind: "esc-char", char: data[i + 1], charCode: next, raw: data.slice(i, i + 2) });
|
|
70
|
+
i += 2;
|
|
71
|
+
}
|
|
60
72
|
else if (next === 0x4f) {
|
|
61
73
|
// SS3 sequence: \x1bO<letter>
|
|
62
74
|
if (i + 2 < data.length) {
|
|
@@ -679,3 +691,29 @@ function buildOscResult(body, raw, nextIndex) {
|
|
|
679
691
|
nextIndex,
|
|
680
692
|
};
|
|
681
693
|
}
|
|
694
|
+
// ─── DCS: XTVERSION reply ───
|
|
695
|
+
/**
|
|
696
|
+
* Начало ответа на XTVERSION (`CSI > 0 q`): `DCS > |`. Разбираем только его: голое
|
|
697
|
+
* `ESC P` — это ещё и Alt+Shift+P legacy-терминала, так что произвольный DCS не ловим.
|
|
698
|
+
*/
|
|
699
|
+
export const XTVERSION_REPLY_PREFIX = "\x1bP>|";
|
|
700
|
+
/**
|
|
701
|
+
* Parse an XTVERSION reply `ESC P > | <text> ESC \` starting at data[start] = ESC.
|
|
702
|
+
* Returns null if the string terminator (ST) hasn't arrived yet.
|
|
703
|
+
*/
|
|
704
|
+
export function parseXtversionReply(data, start) {
|
|
705
|
+
const bodyStart = start + XTVERSION_REPLY_PREFIX.length;
|
|
706
|
+
const end = data.indexOf("\x1b\\", bodyStart);
|
|
707
|
+
if (end === -1)
|
|
708
|
+
return null;
|
|
709
|
+
const nextIndex = end + 2;
|
|
710
|
+
return {
|
|
711
|
+
token: {
|
|
712
|
+
kind: "device-report",
|
|
713
|
+
report: "xtversion",
|
|
714
|
+
params: data.slice(bodyStart, end),
|
|
715
|
+
raw: data.slice(start, nextIndex),
|
|
716
|
+
},
|
|
717
|
+
nextIndex,
|
|
718
|
+
};
|
|
719
|
+
}
|
package/dist/rendering/grid.d.ts
CHANGED
|
@@ -15,6 +15,12 @@ export interface CellPatch {
|
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
17
|
* 2D grid of terminal cells backed by a flat array for cache-friendly access.
|
|
18
|
+
*
|
|
19
|
+
* Ячейки хранят только непрозрачные цвета (или DEFAULT_COLOR): цвет с альфой,
|
|
20
|
+
* пришедший в {@link setCell}/{@link updateCell}, композитится с текущим
|
|
21
|
+
* содержимым ячейки на месте (см. `compositeOver`). Так порядок отрисовки —
|
|
22
|
+
* родитель, дети, оверлеи, патчи подсветок — становится порядком наложения
|
|
23
|
+
* слоёв, как в браузере, а бэкенды/снапшоты/SVG видят уже готовые цвета.
|
|
18
24
|
*/
|
|
19
25
|
export declare class Grid {
|
|
20
26
|
readonly size: Size;
|
package/dist/rendering/grid.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import { DEFAULT_COLOR } from "../common/colorUtils.js";
|
|
1
|
+
import { compositeOver, DEFAULT_COLOR } from "../common/colorUtils.js";
|
|
2
2
|
import { Point, Rect, Size } from "../common/geometryPromitives.js";
|
|
3
3
|
import { StyleFlags } from "../common/styleFlags.js";
|
|
4
4
|
import { Cell } from "./cell.js";
|
|
5
5
|
/**
|
|
6
6
|
* 2D grid of terminal cells backed by a flat array for cache-friendly access.
|
|
7
|
+
*
|
|
8
|
+
* Ячейки хранят только непрозрачные цвета (или DEFAULT_COLOR): цвет с альфой,
|
|
9
|
+
* пришедший в {@link setCell}/{@link updateCell}, композитится с текущим
|
|
10
|
+
* содержимым ячейки на месте (см. `compositeOver`). Так порядок отрисовки —
|
|
11
|
+
* родитель, дети, оверлеи, патчи подсветок — становится порядком наложения
|
|
12
|
+
* слоёв, как в браузере, а бэкенды/снапшоты/SVG видят уже готовые цвета.
|
|
7
13
|
*/
|
|
8
14
|
export class Grid {
|
|
9
15
|
size;
|
|
@@ -55,8 +61,8 @@ export class Grid {
|
|
|
55
61
|
/* v8 ignore stop */
|
|
56
62
|
}
|
|
57
63
|
cell.char = char;
|
|
58
|
-
cell.
|
|
59
|
-
cell.
|
|
64
|
+
cell.bg = compositeOver(bg, cell.bg);
|
|
65
|
+
cell.fg = compositeOver(fg, cell.bg);
|
|
60
66
|
cell.style = style;
|
|
61
67
|
cell.width = width;
|
|
62
68
|
// For wide chars, set up the continuation cell
|
|
@@ -73,8 +79,8 @@ export class Grid {
|
|
|
73
79
|
/* v8 ignore stop */
|
|
74
80
|
}
|
|
75
81
|
cont.char = "";
|
|
76
|
-
cont.fg = fg;
|
|
77
|
-
cont.bg = bg;
|
|
82
|
+
cont.fg = cell.fg;
|
|
83
|
+
cont.bg = cell.bg;
|
|
78
84
|
cont.style = style;
|
|
79
85
|
cont.width = 0;
|
|
80
86
|
}
|
|
@@ -108,10 +114,13 @@ export class Grid {
|
|
|
108
114
|
}
|
|
109
115
|
if (patch.char !== undefined)
|
|
110
116
|
cell.char = patch.char;
|
|
111
|
-
|
|
112
|
-
|
|
117
|
+
// Композитинг в порядке отрисовки: полупрозрачный bg ложится на то, что
|
|
118
|
+
// уже лежит в ячейке в этом кадре, полупрозрачный fg — на итоговый bg.
|
|
119
|
+
// Непрозрачные цвета проходят как раньше (compositeOver — одно сравнение).
|
|
113
120
|
if (patch.bg !== undefined)
|
|
114
|
-
cell.bg = patch.bg;
|
|
121
|
+
cell.bg = compositeOver(patch.bg, cell.bg);
|
|
122
|
+
if (patch.fg !== undefined)
|
|
123
|
+
cell.fg = compositeOver(patch.fg, cell.bg);
|
|
115
124
|
if (patch.style !== undefined)
|
|
116
125
|
cell.style = patch.style;
|
|
117
126
|
if (patch.width !== undefined)
|
|
@@ -132,10 +141,11 @@ export class Grid {
|
|
|
132
141
|
}
|
|
133
142
|
cont.char = "";
|
|
134
143
|
cont.width = 0;
|
|
144
|
+
// Продолжение — та же ячейка: берёт уже скомпозиченные цвета головы.
|
|
135
145
|
if (patch.fg !== undefined)
|
|
136
|
-
cont.fg =
|
|
146
|
+
cont.fg = cell.fg;
|
|
137
147
|
if (patch.bg !== undefined)
|
|
138
|
-
cont.bg =
|
|
148
|
+
cont.bg = cell.bg;
|
|
139
149
|
if (patch.style !== undefined)
|
|
140
150
|
cont.style = patch.style;
|
|
141
151
|
}
|
package/package.json
CHANGED