@nghyane/arcane-tui 0.1.8 → 0.1.10
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/package.json +1 -1
- package/src/buffer/render.ts +22 -6
- package/src/components/markdown.ts +87 -10
- package/src/components/tab-bar.ts +45 -12
- package/src/index.ts +1 -1
- package/src/terminal.ts +50 -0
- package/src/tui.ts +20 -5
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@nghyane/arcane-tui",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.10",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://github.com/nghyane/arcane",
|
|
7
7
|
"author": "Can Bölük",
|
package/src/buffer/render.ts
CHANGED
|
@@ -5,7 +5,7 @@ function styleEquals(a: Style, b: Style): boolean {
|
|
|
5
5
|
return a.fg === b.fg && a.bg === b.bg && a.mods === b.mods && a.link === b.link;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
function emitStyleDiff(prev: Style, next: Style, parts: string[]): void {
|
|
8
|
+
function emitStyleDiff(prev: Style, next: Style, parts: string[], defaultBg = 0): void {
|
|
9
9
|
// Check if reset is simpler
|
|
10
10
|
const needReset =
|
|
11
11
|
(prev.mods & ~next.mods) !== 0 ||
|
|
@@ -23,6 +23,9 @@ function emitStyleDiff(prev: Style, next: Style, parts: string[]): void {
|
|
|
23
23
|
if (hasColor(next.bg)) {
|
|
24
24
|
const [r, g, b] = unpackRgb(next.bg);
|
|
25
25
|
parts.push(`\x1b[48;2;${r};${g};${b}m`);
|
|
26
|
+
} else if (defaultBg) {
|
|
27
|
+
const [r, g, b] = unpackRgb(defaultBg);
|
|
28
|
+
parts.push(`\x1b[48;2;${r};${g};${b}m`);
|
|
26
29
|
}
|
|
27
30
|
} else {
|
|
28
31
|
if (prev.fg !== next.fg) {
|
|
@@ -38,7 +41,12 @@ function emitStyleDiff(prev: Style, next: Style, parts: string[]): void {
|
|
|
38
41
|
const [r, g, b] = unpackRgb(next.bg);
|
|
39
42
|
parts.push(`\x1b[48;2;${r};${g};${b}m`);
|
|
40
43
|
} else {
|
|
41
|
-
|
|
44
|
+
if (defaultBg) {
|
|
45
|
+
const [r, g, b] = unpackRgb(defaultBg);
|
|
46
|
+
parts.push(`\x1b[48;2;${r};${g};${b}m`);
|
|
47
|
+
} else {
|
|
48
|
+
parts.push("\x1b[49m");
|
|
49
|
+
}
|
|
42
50
|
}
|
|
43
51
|
}
|
|
44
52
|
if (prev.mods !== next.mods) {
|
|
@@ -78,7 +86,7 @@ function emitMods(prev: number, next: number, parts: string[]): void {
|
|
|
78
86
|
if (removed & Mod.Strikethrough) parts.push("\x1b[29m");
|
|
79
87
|
}
|
|
80
88
|
|
|
81
|
-
export function renderDiff(changes: CellChange[], _width: number): string {
|
|
89
|
+
export function renderDiff(changes: CellChange[], _width: number, defaultBg = 0): string {
|
|
82
90
|
if (changes.length === 0) return "";
|
|
83
91
|
|
|
84
92
|
const sorted = [...changes].sort((a, b) => (a.row !== b.row ? a.row - b.row : a.col - b.col));
|
|
@@ -104,7 +112,7 @@ export function renderDiff(changes: CellChange[], _width: number): string {
|
|
|
104
112
|
|
|
105
113
|
// Style changes
|
|
106
114
|
if (!styleEquals(curStyle, cell.style)) {
|
|
107
|
-
emitStyleDiff(curStyle, cell.style, parts);
|
|
115
|
+
emitStyleDiff(curStyle, cell.style, parts, defaultBg);
|
|
108
116
|
curStyle = { ...cell.style };
|
|
109
117
|
}
|
|
110
118
|
|
|
@@ -113,10 +121,14 @@ export function renderDiff(changes: CellChange[], _width: number): string {
|
|
|
113
121
|
}
|
|
114
122
|
|
|
115
123
|
parts.push("\x1b[0m");
|
|
124
|
+
if (defaultBg) {
|
|
125
|
+
const [r, g, b] = unpackRgb(defaultBg);
|
|
126
|
+
parts.push(`\x1b[48;2;${r};${g};${b}m`);
|
|
127
|
+
}
|
|
116
128
|
return parts.join("");
|
|
117
129
|
}
|
|
118
130
|
|
|
119
|
-
export function renderBuffer(buf: Buffer): string[] {
|
|
131
|
+
export function renderBuffer(buf: Buffer, defaultBg = 0): string[] {
|
|
120
132
|
const lines: string[] = [];
|
|
121
133
|
|
|
122
134
|
for (let row = 0; row < buf.height; row++) {
|
|
@@ -130,7 +142,7 @@ export function renderBuffer(buf: Buffer): string[] {
|
|
|
130
142
|
if (cell.width === 0) continue;
|
|
131
143
|
|
|
132
144
|
if (!styleEquals(curStyle, cell.style)) {
|
|
133
|
-
emitStyleDiff(curStyle, cell.style, parts);
|
|
145
|
+
emitStyleDiff(curStyle, cell.style, parts, defaultBg);
|
|
134
146
|
curStyle = { ...cell.style };
|
|
135
147
|
}
|
|
136
148
|
|
|
@@ -140,6 +152,10 @@ export function renderBuffer(buf: Buffer): string[] {
|
|
|
140
152
|
// Reset at end of line
|
|
141
153
|
if (!styleEquals(curStyle, DEFAULT_STYLE)) {
|
|
142
154
|
parts.push("\x1b[0m");
|
|
155
|
+
if (defaultBg) {
|
|
156
|
+
const [r, g, b] = unpackRgb(defaultBg);
|
|
157
|
+
parts.push(`\x1b[48;2;${r};${g};${b}m`);
|
|
158
|
+
}
|
|
143
159
|
}
|
|
144
160
|
|
|
145
161
|
lines.push(parts.join(""));
|
|
@@ -22,6 +22,8 @@ export interface DefaultTextStyle {
|
|
|
22
22
|
strikethrough?: boolean;
|
|
23
23
|
/** Underline text */
|
|
24
24
|
underline?: boolean;
|
|
25
|
+
leftBorder?: string;
|
|
26
|
+
/** Left border string (already styled, prepended to each line) */
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
/**
|
|
@@ -106,8 +108,9 @@ export class Markdown implements Component {
|
|
|
106
108
|
return this.#cachedLines;
|
|
107
109
|
}
|
|
108
110
|
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
+
const leftBorder = this.#defaultTextStyle?.leftBorder ?? "";
|
|
112
|
+
const borderWidth = visibleWidth(leftBorder);
|
|
113
|
+
const contentWidth = Math.max(1, width - this.#paddingX * 2 - borderWidth);
|
|
111
114
|
|
|
112
115
|
// Don't render anything if there's no actual text
|
|
113
116
|
if (!this.#text || this.#text.trim() === "") {
|
|
@@ -153,26 +156,23 @@ export class Markdown implements Component {
|
|
|
153
156
|
const contentLines: string[] = [];
|
|
154
157
|
|
|
155
158
|
for (const line of wrappedLines) {
|
|
156
|
-
// Image lines must be output raw - no margins or background
|
|
157
159
|
if (TERMINAL.isImageLine(line)) {
|
|
158
160
|
contentLines.push(line);
|
|
159
161
|
continue;
|
|
160
162
|
}
|
|
161
163
|
|
|
162
|
-
const lineWithMargins = leftMargin + line + rightMargin;
|
|
164
|
+
const lineWithMargins = leftBorder + leftMargin + line + rightMargin;
|
|
163
165
|
|
|
164
166
|
if (bgFn) {
|
|
165
167
|
contentLines.push(applyBackgroundToLine(lineWithMargins, width, bgFn));
|
|
166
168
|
} else {
|
|
167
|
-
// No background - just pad to width
|
|
168
169
|
const visibleLen = visibleWidth(lineWithMargins);
|
|
169
170
|
const paddingNeeded = Math.max(0, width - visibleLen);
|
|
170
171
|
contentLines.push(lineWithMargins + padding(paddingNeeded));
|
|
171
172
|
}
|
|
172
173
|
}
|
|
173
174
|
|
|
174
|
-
|
|
175
|
-
const emptyLine = padding(width);
|
|
175
|
+
const emptyLine = leftBorder + padding(width - borderWidth);
|
|
176
176
|
const emptyLines: string[] = [];
|
|
177
177
|
for (let i = 0; i < this.#paddingY; i++) {
|
|
178
178
|
const line = bgFn ? applyBackgroundToLine(emptyLine, width, bgFn) : emptyLine;
|
|
@@ -365,13 +365,15 @@ export class Markdown implements Component {
|
|
|
365
365
|
applyText: quoteStyle,
|
|
366
366
|
stylePrefix: this.#getStylePrefix(quoteStyle),
|
|
367
367
|
};
|
|
368
|
-
|
|
369
|
-
|
|
368
|
+
|
|
369
|
+
// Blockquote tokens contain block-level children (paragraph, code, list, etc.)
|
|
370
|
+
// Render each child appropriately, then prefix every line with the quote border.
|
|
371
|
+
const quoteContentLines = this.#renderBlockquoteTokens(token.tokens || [], quoteStyleContext, width - 2);
|
|
370
372
|
|
|
371
373
|
// Calculate available width for quote content (subtract border + space = 2 chars)
|
|
372
374
|
const quoteContentWidth = Math.max(1, width - 2);
|
|
373
375
|
|
|
374
|
-
for (const quoteLine of
|
|
376
|
+
for (const quoteLine of quoteContentLines) {
|
|
375
377
|
// Wrap the styled line, then add border to each wrapped line
|
|
376
378
|
const wrappedLines = wrapTextWithAnsi(quoteLine, quoteContentWidth);
|
|
377
379
|
for (const wrappedLine of wrappedLines) {
|
|
@@ -601,6 +603,81 @@ export class Markdown implements Component {
|
|
|
601
603
|
return lines;
|
|
602
604
|
}
|
|
603
605
|
|
|
606
|
+
/**
|
|
607
|
+
* Render tokens inside a blockquote, handling block-level elements
|
|
608
|
+
* (code, list, heading) that #renderInlineTokens cannot process.
|
|
609
|
+
*/
|
|
610
|
+
#renderBlockquoteTokens(tokens: Token[], styleContext: InlineStyleContext, availableWidth: number): string[] {
|
|
611
|
+
const lines: string[] = [];
|
|
612
|
+
|
|
613
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
614
|
+
const token = tokens[i];
|
|
615
|
+
const nextToken = tokens[i + 1];
|
|
616
|
+
|
|
617
|
+
switch (token.type) {
|
|
618
|
+
case "paragraph": {
|
|
619
|
+
const text = this.#renderInlineTokens(token.tokens || [], styleContext);
|
|
620
|
+
lines.push(text);
|
|
621
|
+
if (nextToken && nextToken.type !== "space") {
|
|
622
|
+
lines.push("");
|
|
623
|
+
}
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
case "code": {
|
|
628
|
+
const codeIndent = padding(this.#codeBlockIndent);
|
|
629
|
+
lines.push(this.#theme.codeBlockBorder(`\`\`\`${token.lang || ""}`));
|
|
630
|
+
if (this.#theme.highlightCode) {
|
|
631
|
+
const highlightedLines = this.#theme.highlightCode(token.text, token.lang);
|
|
632
|
+
for (const hlLine of highlightedLines) {
|
|
633
|
+
lines.push(`${codeIndent}${hlLine}`);
|
|
634
|
+
}
|
|
635
|
+
} else {
|
|
636
|
+
const codeLines = token.text.split("\n");
|
|
637
|
+
for (const codeLine of codeLines) {
|
|
638
|
+
lines.push(`${codeIndent}${this.#theme.codeBlock(codeLine)}`);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
lines.push(this.#theme.codeBlockBorder("```"));
|
|
642
|
+
if (nextToken && nextToken.type !== "space") {
|
|
643
|
+
lines.push("");
|
|
644
|
+
}
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
case "heading": {
|
|
649
|
+
const headingLines = this.#renderToken(token, availableWidth, nextToken?.type);
|
|
650
|
+
lines.push(...headingLines);
|
|
651
|
+
break;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
case "list": {
|
|
655
|
+
const listLines = this.#renderList(token as any, 0);
|
|
656
|
+
lines.push(...listLines);
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
case "hr": {
|
|
661
|
+
lines.push(this.#theme.hr(this.#theme.symbols.hrChar.repeat(Math.min(availableWidth, 80))));
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
case "space":
|
|
666
|
+
lines.push("");
|
|
667
|
+
break;
|
|
668
|
+
|
|
669
|
+
default: {
|
|
670
|
+
const text = this.#renderInlineTokens([token], styleContext);
|
|
671
|
+
if (text) {
|
|
672
|
+
lines.push(text);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return lines;
|
|
679
|
+
}
|
|
680
|
+
|
|
604
681
|
/**
|
|
605
682
|
* Get the visible width of the longest word in a string.
|
|
606
683
|
*/
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { matchesKey } from "../keys";
|
|
12
12
|
import type { Component } from "../tui";
|
|
13
|
-
import {
|
|
13
|
+
import { truncateToWidth, visibleWidth } from "../utils";
|
|
14
14
|
|
|
15
15
|
/** Tab definition */
|
|
16
16
|
export interface Tab {
|
|
@@ -112,31 +112,64 @@ export class TabBar implements Component {
|
|
|
112
112
|
|
|
113
113
|
/** Render the tab bar, wrapping to multiple lines if needed */
|
|
114
114
|
render(width: number): string[] {
|
|
115
|
-
const
|
|
115
|
+
const maxWidth = Math.max(1, width);
|
|
116
|
+
const chunks: string[] = [];
|
|
116
117
|
|
|
117
118
|
// Label prefix
|
|
118
|
-
|
|
119
|
-
|
|
119
|
+
chunks.push(this.#theme.label(`${this.#label}:`));
|
|
120
|
+
chunks.push(" ");
|
|
120
121
|
|
|
121
122
|
// Tab buttons
|
|
122
123
|
for (let i = 0; i < this.#tabs.length; i++) {
|
|
123
124
|
const tab = this.#tabs[i];
|
|
124
125
|
if (i === this.#activeIndex) {
|
|
125
|
-
|
|
126
|
+
chunks.push(this.#theme.activeTab(` ${tab.label} `));
|
|
126
127
|
} else {
|
|
127
|
-
|
|
128
|
+
chunks.push(this.#theme.inactiveTab(` ${tab.label} `));
|
|
128
129
|
}
|
|
129
130
|
if (i < this.#tabs.length - 1) {
|
|
130
|
-
|
|
131
|
+
chunks.push(" ");
|
|
131
132
|
}
|
|
132
133
|
}
|
|
133
134
|
|
|
134
135
|
// Navigation hint
|
|
135
|
-
|
|
136
|
-
|
|
136
|
+
chunks.push(" ");
|
|
137
|
+
chunks.push(this.#theme.hint("(tab to cycle)"));
|
|
137
138
|
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
const lines: string[] = [];
|
|
140
|
+
let currentLine = "";
|
|
141
|
+
let currentWidth = 0;
|
|
142
|
+
|
|
143
|
+
for (const chunk of chunks) {
|
|
144
|
+
const chunkWidth = visibleWidth(chunk);
|
|
145
|
+
if (chunkWidth <= 0) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (chunkWidth > maxWidth) {
|
|
150
|
+
if (currentLine) {
|
|
151
|
+
lines.push(currentLine);
|
|
152
|
+
currentLine = "";
|
|
153
|
+
currentWidth = 0;
|
|
154
|
+
}
|
|
155
|
+
lines.push(truncateToWidth(chunk, maxWidth));
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (currentWidth > 0 && currentWidth + chunkWidth > maxWidth) {
|
|
160
|
+
lines.push(currentLine);
|
|
161
|
+
currentLine = "";
|
|
162
|
+
currentWidth = 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
currentLine += chunk;
|
|
166
|
+
currentWidth += chunkWidth;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (currentLine) {
|
|
170
|
+
lines.push(currentLine);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return lines.length > 0 ? lines : [""];
|
|
141
174
|
}
|
|
142
175
|
}
|
package/src/index.ts
CHANGED
|
@@ -59,7 +59,7 @@ export {
|
|
|
59
59
|
export { StdinBuffer, type StdinBufferEventMap, type StdinBufferOptions } from "./stdin-buffer";
|
|
60
60
|
export type { BoxSymbols, SymbolTheme } from "./symbols";
|
|
61
61
|
// Terminal interface and implementations
|
|
62
|
-
export { emergencyTerminalRestore, ProcessTerminal, type Terminal } from "./terminal";
|
|
62
|
+
export { emergencyTerminalRestore, ProcessTerminal, queryTerminalBackground, type Terminal } from "./terminal";
|
|
63
63
|
// Terminal image support
|
|
64
64
|
export * from "./terminal-capabilities";
|
|
65
65
|
// TTY ID
|
package/src/terminal.ts
CHANGED
|
@@ -56,6 +56,56 @@ export function emergencyTerminalRestore(): void {
|
|
|
56
56
|
// Terminal may already be dead during crash cleanup - ignore errors
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Query terminal background color via OSC 11.
|
|
62
|
+
* Returns hex color string (e.g. "#1c1e26") or null if terminal does not respond.
|
|
63
|
+
* Must be called before TUI start() — temporarily enters raw mode.
|
|
64
|
+
*/
|
|
65
|
+
export async function queryTerminalBackground(timeoutMs = 150): Promise<string | null> {
|
|
66
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return null;
|
|
67
|
+
|
|
68
|
+
const wasRaw = process.stdin.isRaw;
|
|
69
|
+
if (process.stdin.setRawMode) process.stdin.setRawMode(true);
|
|
70
|
+
process.stdin.setEncoding("utf8");
|
|
71
|
+
process.stdin.resume();
|
|
72
|
+
|
|
73
|
+
const { promise, resolve } = Promise.withResolvers<string | null>();
|
|
74
|
+
let settled = false;
|
|
75
|
+
let buf = "";
|
|
76
|
+
|
|
77
|
+
const onData = (data: string) => {
|
|
78
|
+
buf += data;
|
|
79
|
+
// OSC 11 response: \x1b]11;rgb:RRRR/GGGG/BBBB\x1b\\ or ...\x07
|
|
80
|
+
const match = buf.match(/\x1b\]11;rgb:([0-9a-fA-F]{2,4})\/([0-9a-fA-F]{2,4})\/([0-9a-fA-F]{2,4})/);
|
|
81
|
+
if (match && !settled) {
|
|
82
|
+
settled = true;
|
|
83
|
+
const r = parseInt(match[1].slice(0, 2), 16);
|
|
84
|
+
const g = parseInt(match[2].slice(0, 2), 16);
|
|
85
|
+
const b = parseInt(match[3].slice(0, 2), 16);
|
|
86
|
+
resolve(
|
|
87
|
+
`#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
process.stdin.on("data", onData);
|
|
93
|
+
process.stdout.write("\x1b]11;?\x1b\\");
|
|
94
|
+
|
|
95
|
+
const timer = setTimeout(() => {
|
|
96
|
+
if (!settled) {
|
|
97
|
+
settled = true;
|
|
98
|
+
resolve(null);
|
|
99
|
+
}
|
|
100
|
+
}, timeoutMs);
|
|
101
|
+
|
|
102
|
+
const result = await promise;
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
process.stdin.removeListener("data", onData);
|
|
105
|
+
if (process.stdin.setRawMode) process.stdin.setRawMode(wasRaw);
|
|
106
|
+
if (!wasRaw) process.stdin.pause();
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
59
109
|
export interface Terminal {
|
|
60
110
|
// Start the terminal with input and resize handlers
|
|
61
111
|
start(onInput: (data: string) => void, onResize: () => void): void;
|
package/src/tui.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { linesToBuffer } from "./buffer/ansi-parser.js";
|
|
2
2
|
import type { Buffer as CellBuffer, CellChange } from "./buffer/buffer.js";
|
|
3
|
-
import { Mod } from "./buffer/cell.js";
|
|
3
|
+
import { Mod, unpackRgb } from "./buffer/cell.js";
|
|
4
4
|
import { renderBuffer, renderDiff } from "./buffer/render.js";
|
|
5
5
|
import { isKeyRelease, matchesKey } from "./keys";
|
|
6
6
|
import { type MouseEvent, SCROLL_DOWN, SCROLL_UP, type Terminal } from "./terminal";
|
|
@@ -186,6 +186,7 @@ export class Container implements Component {
|
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
render(width: number): string[] {
|
|
189
|
+
width = Math.max(1, width);
|
|
189
190
|
const lines: string[] = [];
|
|
190
191
|
for (const child of this.children) {
|
|
191
192
|
lines.push(...child.render(width));
|
|
@@ -222,6 +223,7 @@ export class TUI extends Container {
|
|
|
222
223
|
#scrollFlushScheduled = false; // Whether a flush is scheduled on next tick
|
|
223
224
|
#fullRenderCache: string[] = []; // Cached full render output for native scroll
|
|
224
225
|
#previousBuffer: CellBuffer | null = null; // Previous viewport buffer for cell-level diff
|
|
226
|
+
#appBg = 0; // Packed RGB for app-wide background (0 = terminal default)
|
|
225
227
|
|
|
226
228
|
// Selection state for mouse text selection
|
|
227
229
|
#selectionActive = false;
|
|
@@ -275,6 +277,11 @@ export class TUI extends Container {
|
|
|
275
277
|
this.#clearOnShrink = enabled;
|
|
276
278
|
}
|
|
277
279
|
|
|
280
|
+
setAppBg(packedRgb: number): void {
|
|
281
|
+
this.#appBg = packedRgb;
|
|
282
|
+
this.#previousBuffer = null; // Force full redraw
|
|
283
|
+
}
|
|
284
|
+
|
|
278
285
|
setFocus(component: Component | null): void {
|
|
279
286
|
// Clear focused flag on old component
|
|
280
287
|
if (isFocusable(this.#focusedComponent)) {
|
|
@@ -475,10 +482,14 @@ export class TUI extends Container {
|
|
|
475
482
|
if (this.#previousBuffer) {
|
|
476
483
|
const changes = currentBuffer.diff(this.#previousBuffer);
|
|
477
484
|
if (changes.length > 0) {
|
|
478
|
-
out += renderDiff(changes, width);
|
|
485
|
+
out += renderDiff(changes, width, this.#appBg);
|
|
479
486
|
}
|
|
480
487
|
} else {
|
|
481
488
|
// No previous buffer — full viewport redraw
|
|
489
|
+
if (this.#appBg) {
|
|
490
|
+
const [r, g, b] = unpackRgb(this.#appBg);
|
|
491
|
+
out += `\x1b[48;2;${r};${g};${b}m`;
|
|
492
|
+
}
|
|
482
493
|
out += "\x1b[H";
|
|
483
494
|
for (let i = 0; i < viewportLines.length; i++) {
|
|
484
495
|
if (i > 0) out += "\r\n";
|
|
@@ -715,7 +726,7 @@ export class TUI extends Container {
|
|
|
715
726
|
// Output only the changed cells
|
|
716
727
|
let out = "\x1b[?2026h"; // Synchronized output
|
|
717
728
|
out += "\x1b7"; // Save cursor position
|
|
718
|
-
out += renderDiff(changes, width);
|
|
729
|
+
out += renderDiff(changes, width, this.#appBg);
|
|
719
730
|
out += "\x1b8"; // Restore cursor position
|
|
720
731
|
out += "\x1b[?2026l";
|
|
721
732
|
this.terminal.write(out);
|
|
@@ -1046,8 +1057,12 @@ export class TUI extends Container {
|
|
|
1046
1057
|
// No previous buffer — full render
|
|
1047
1058
|
this.#fullRedrawCount += 1;
|
|
1048
1059
|
let out = "\x1b[?2026h"; // Begin synchronized output
|
|
1060
|
+
if (this.#appBg) {
|
|
1061
|
+
const [r, g, b] = unpackRgb(this.#appBg);
|
|
1062
|
+
out += `\x1b[48;2;${r};${g};${b}m`;
|
|
1063
|
+
}
|
|
1049
1064
|
out += this.#clearScrollbackOnNextFullRender ? "\x1b[3J\x1b[2J\x1b[H" : "\x1b[2J\x1b[H";
|
|
1050
|
-
const renderedLines = renderBuffer(currentBuffer);
|
|
1065
|
+
const renderedLines = renderBuffer(currentBuffer, this.#appBg);
|
|
1051
1066
|
for (let i = 0; i < renderedLines.length; i++) {
|
|
1052
1067
|
if (i > 0) out += "\r\n";
|
|
1053
1068
|
out += renderedLines[i];
|
|
@@ -1083,7 +1098,7 @@ export class TUI extends Container {
|
|
|
1083
1098
|
} else {
|
|
1084
1099
|
// Emit minimal ANSI for changed cells
|
|
1085
1100
|
let out = "\x1b[?2026h"; // Begin synchronized output
|
|
1086
|
-
out += renderDiff(changes, width);
|
|
1101
|
+
out += renderDiff(changes, width, this.#appBg);
|
|
1087
1102
|
const adjCursorPos = cursorPos
|
|
1088
1103
|
? { row: cursorPos.row - Math.max(0, newLines.length - height), col: cursorPos.col }
|
|
1089
1104
|
: null;
|