@mariozechner/pi-tui 0.5.6 → 0.5.8
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/components/loading-animation.d.ts +19 -0
- package/dist/components/loading-animation.d.ts.map +1 -0
- package/dist/components/loading-animation.js +44 -0
- package/dist/components/loading-animation.js.map +1 -0
- package/dist/{markdown-component.d.ts → components/markdown-component.d.ts} +2 -1
- package/dist/components/markdown-component.d.ts.map +1 -0
- package/dist/{markdown-component.js → components/markdown-component.js} +2 -0
- package/dist/components/markdown-component.js.map +1 -0
- package/dist/{select-list.d.ts → components/select-list.d.ts} +2 -1
- package/dist/components/select-list.d.ts.map +1 -0
- package/dist/{select-list.js → components/select-list.js} +2 -0
- package/dist/components/select-list.js.map +1 -0
- package/dist/{text-component.d.ts → components/text-component.d.ts} +2 -1
- package/dist/components/text-component.d.ts.map +1 -0
- package/dist/{text-component.js → components/text-component.js} +2 -0
- package/dist/components/text-component.js.map +1 -0
- package/dist/{text-editor.d.ts → components/text-editor.d.ts} +3 -2
- package/dist/components/text-editor.d.ts.map +1 -0
- package/dist/{text-editor.js → components/text-editor.js} +4 -2
- package/dist/components/text-editor.js.map +1 -0
- package/dist/{whitespace-component.d.ts → components/whitespace-component.d.ts} +2 -1
- package/dist/components/whitespace-component.d.ts.map +1 -0
- package/dist/{whitespace-component.js → components/whitespace-component.js} +2 -0
- package/dist/components/whitespace-component.js.map +1 -0
- package/dist/index.d.ts +8 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -8
- package/dist/index.js.map +1 -1
- package/dist/terminal.d.ts +24 -0
- package/dist/terminal.d.ts.map +1 -0
- package/dist/terminal.js +47 -0
- package/dist/terminal.js.map +1 -0
- package/dist/tui.d.ts +43 -28
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +268 -251
- package/dist/tui.js.map +1 -1
- package/package.json +6 -2
- package/dist/markdown-component.d.ts.map +0 -1
- package/dist/markdown-component.js.map +0 -1
- package/dist/select-list.d.ts.map +0 -1
- package/dist/select-list.js.map +0 -1
- package/dist/text-component.d.ts.map +0 -1
- package/dist/text-component.js.map +0 -1
- package/dist/text-editor.d.ts.map +0 -1
- package/dist/text-editor.js.map +0 -1
- package/dist/whitespace-component.d.ts.map +0 -1
- package/dist/whitespace-component.js.map +0 -1
package/dist/tui.js
CHANGED
|
@@ -1,220 +1,132 @@
|
|
|
1
|
-
import { writeSync } from "fs";
|
|
2
1
|
import process from "process";
|
|
3
2
|
import { logger } from "./logger.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
};
|
|
11
|
-
}
|
|
3
|
+
import { ProcessTerminal } from "./terminal.js";
|
|
4
|
+
// Global component ID counter
|
|
5
|
+
let nextComponentId = 1;
|
|
6
|
+
// Helper to get next component ID
|
|
7
|
+
export function getNextComponentId() {
|
|
8
|
+
return nextComponentId++;
|
|
12
9
|
}
|
|
13
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Container for managing child components
|
|
12
|
+
*/
|
|
14
13
|
export class Container {
|
|
14
|
+
id;
|
|
15
15
|
children = [];
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
this.parentTui = parentTui;
|
|
16
|
+
tui;
|
|
17
|
+
constructor() {
|
|
18
|
+
this.id = getNextComponentId();
|
|
20
19
|
}
|
|
21
|
-
|
|
22
|
-
this.
|
|
20
|
+
setTui(tui) {
|
|
21
|
+
this.tui = tui;
|
|
22
|
+
for (const child of this.children) {
|
|
23
|
+
if (child instanceof Container) {
|
|
24
|
+
child.setTui(tui);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
23
27
|
}
|
|
24
28
|
addChild(component) {
|
|
25
29
|
this.children.push(component);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
component.setParentTui(this.parentTui);
|
|
29
|
-
}
|
|
30
|
-
if (this.parentTui) {
|
|
31
|
-
this.parentTui.requestRender();
|
|
30
|
+
if (component instanceof Container) {
|
|
31
|
+
component.setTui(this.tui);
|
|
32
32
|
}
|
|
33
|
+
this.tui?.requestRender();
|
|
33
34
|
}
|
|
34
35
|
removeChild(component) {
|
|
35
36
|
const index = this.children.indexOf(component);
|
|
36
37
|
if (index >= 0) {
|
|
37
|
-
|
|
38
|
-
this.children[index] = new SentinelComponent();
|
|
39
|
-
// Keep the childTotalLines entry - sentinel will update it to 0
|
|
40
|
-
// Clear parent TUI reference for nested containers
|
|
38
|
+
this.children.splice(index, 1);
|
|
41
39
|
if (component instanceof Container) {
|
|
42
|
-
component.
|
|
43
|
-
}
|
|
44
|
-
// Use normal render - sentinel will trigger cascade naturally
|
|
45
|
-
if (this.parentTui) {
|
|
46
|
-
this.parentTui.requestRender();
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
for (const child of this.children) {
|
|
51
|
-
if (child instanceof Container) {
|
|
52
|
-
child.removeChild(component);
|
|
53
|
-
}
|
|
40
|
+
component.setTui(undefined);
|
|
54
41
|
}
|
|
42
|
+
this.tui?.requestRender();
|
|
55
43
|
}
|
|
56
44
|
}
|
|
57
45
|
removeChildAt(index) {
|
|
58
46
|
if (index >= 0 && index < this.children.length) {
|
|
59
47
|
const component = this.children[index];
|
|
60
|
-
|
|
61
|
-
this.children[index] = new SentinelComponent();
|
|
62
|
-
// Clear parent TUI reference for nested containers
|
|
48
|
+
this.children.splice(index, 1);
|
|
63
49
|
if (component instanceof Container) {
|
|
64
|
-
component.
|
|
65
|
-
}
|
|
66
|
-
// Use normal render - sentinel will trigger cascade naturally
|
|
67
|
-
if (this.parentTui) {
|
|
68
|
-
this.parentTui.requestRender();
|
|
50
|
+
component.setTui(undefined);
|
|
69
51
|
}
|
|
52
|
+
this.tui?.requestRender();
|
|
70
53
|
}
|
|
71
54
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
let changed = false;
|
|
75
|
-
const newLines = [];
|
|
76
|
-
for (let i = 0; i < this.children.length; i++) {
|
|
77
|
-
const child = this.children[i];
|
|
78
|
-
if (!child)
|
|
79
|
-
continue;
|
|
55
|
+
clear() {
|
|
56
|
+
for (const child of this.children) {
|
|
80
57
|
if (child instanceof Container) {
|
|
81
|
-
|
|
82
|
-
newLines.push(...result.lines);
|
|
83
|
-
if (!changed && !result.changed) {
|
|
84
|
-
keepLines += result.lines.length;
|
|
85
|
-
}
|
|
86
|
-
else {
|
|
87
|
-
if (!changed) {
|
|
88
|
-
// First change - use the child's keepLines
|
|
89
|
-
changed = true;
|
|
90
|
-
keepLines += result.keepLines;
|
|
91
|
-
}
|
|
92
|
-
// After first change, don't add any more keepLines
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
else {
|
|
96
|
-
const result = child.render(width);
|
|
97
|
-
newLines.push(...result.lines);
|
|
98
|
-
if (!changed && !result.changed) {
|
|
99
|
-
keepLines += result.lines.length;
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
if (!changed) {
|
|
103
|
-
// First change for a non-container component
|
|
104
|
-
changed = true;
|
|
105
|
-
}
|
|
106
|
-
// After first change, don't add any more keepLines
|
|
107
|
-
}
|
|
58
|
+
child.setTui(undefined);
|
|
108
59
|
}
|
|
109
60
|
}
|
|
110
|
-
this.
|
|
111
|
-
|
|
112
|
-
lines: this.lines,
|
|
113
|
-
changed,
|
|
114
|
-
keepLines,
|
|
115
|
-
};
|
|
61
|
+
this.children = [];
|
|
62
|
+
this.tui?.requestRender();
|
|
116
63
|
}
|
|
117
|
-
// Get child for external manipulation
|
|
118
|
-
// Get child at index
|
|
119
|
-
// Note: This may return a SentinelComponent if a child was removed but not yet cleaned up
|
|
120
64
|
getChild(index) {
|
|
121
65
|
return this.children[index];
|
|
122
66
|
}
|
|
123
|
-
// Get number of children
|
|
124
|
-
// Note: This count includes sentinel components until they are cleaned up after the next render pass
|
|
125
67
|
getChildCount() {
|
|
126
68
|
return this.children.length;
|
|
127
69
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
70
|
+
render(width) {
|
|
71
|
+
const lines = [];
|
|
72
|
+
let changed = false;
|
|
131
73
|
for (const child of this.children) {
|
|
132
|
-
|
|
133
|
-
|
|
74
|
+
const result = child.render(width);
|
|
75
|
+
lines.push(...result.lines);
|
|
76
|
+
if (result.changed) {
|
|
77
|
+
changed = true;
|
|
134
78
|
}
|
|
135
79
|
}
|
|
136
|
-
|
|
137
|
-
this.children = [];
|
|
138
|
-
// Request render if we have a parent TUI
|
|
139
|
-
if (this.parentTui) {
|
|
140
|
-
this.parentTui.requestRender();
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
// Clean up sentinel components
|
|
144
|
-
cleanupSentinels() {
|
|
145
|
-
const originalCount = this.children.length;
|
|
146
|
-
const validChildren = [];
|
|
147
|
-
let sentinelCount = 0;
|
|
148
|
-
for (const child of this.children) {
|
|
149
|
-
if (child && !(child instanceof SentinelComponent)) {
|
|
150
|
-
validChildren.push(child);
|
|
151
|
-
// Recursively clean up nested containers
|
|
152
|
-
if (child instanceof Container) {
|
|
153
|
-
child.cleanupSentinels();
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
else if (child instanceof SentinelComponent) {
|
|
157
|
-
sentinelCount++;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
this.children = validChildren;
|
|
161
|
-
if (sentinelCount > 0) {
|
|
162
|
-
logger.debug("Container", "Cleaned up sentinels", {
|
|
163
|
-
originalCount,
|
|
164
|
-
newCount: this.children.length,
|
|
165
|
-
sentinelsRemoved: sentinelCount,
|
|
166
|
-
});
|
|
167
|
-
}
|
|
80
|
+
return { lines, changed };
|
|
168
81
|
}
|
|
169
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* TUI - Smart differential rendering TUI implementation.
|
|
85
|
+
*/
|
|
170
86
|
export class TUI extends Container {
|
|
171
87
|
focusedComponent = null;
|
|
172
88
|
needsRender = false;
|
|
173
|
-
wasRaw = false;
|
|
174
|
-
totalLines = 0;
|
|
175
89
|
isFirstRender = true;
|
|
176
90
|
isStarted = false;
|
|
177
91
|
onGlobalKeyPress;
|
|
178
|
-
|
|
179
|
-
|
|
92
|
+
terminal;
|
|
93
|
+
// Tracking for differential rendering
|
|
94
|
+
previousRenderCommands = [];
|
|
95
|
+
previousLines = []; // What we rendered last time
|
|
96
|
+
// Performance metrics
|
|
97
|
+
totalLinesRedrawn = 0;
|
|
98
|
+
renderCount = 0;
|
|
99
|
+
getLinesRedrawn() {
|
|
100
|
+
return this.totalLinesRedrawn;
|
|
101
|
+
}
|
|
102
|
+
getAverageLinesRedrawn() {
|
|
103
|
+
return this.renderCount > 0 ? this.totalLinesRedrawn / this.renderCount : 0;
|
|
104
|
+
}
|
|
105
|
+
constructor(terminal) {
|
|
106
|
+
super();
|
|
107
|
+
this.setTui(this);
|
|
180
108
|
this.handleResize = this.handleResize.bind(this);
|
|
181
109
|
this.handleKeypress = this.handleKeypress.bind(this);
|
|
110
|
+
// Use provided terminal or default to ProcessTerminal
|
|
111
|
+
this.terminal = terminal || new ProcessTerminal();
|
|
182
112
|
logger.componentLifecycle("TUI", "created");
|
|
183
113
|
}
|
|
184
114
|
configureLogging(config) {
|
|
185
115
|
logger.configure(config);
|
|
186
116
|
logger.info("TUI", "Logging configured", config);
|
|
187
117
|
}
|
|
188
|
-
addChild(component) {
|
|
189
|
-
// Set parent TUI reference for containers
|
|
190
|
-
if (component instanceof Container) {
|
|
191
|
-
component.setParentTui(this);
|
|
192
|
-
}
|
|
193
|
-
super.addChild(component);
|
|
194
|
-
// Only auto-render if TUI has been started
|
|
195
|
-
if (this.isStarted) {
|
|
196
|
-
this.requestRender();
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
removeChild(component) {
|
|
200
|
-
super.removeChild(component);
|
|
201
|
-
this.requestRender();
|
|
202
|
-
}
|
|
203
118
|
setFocus(component) {
|
|
204
|
-
// Check if component exists anywhere in the hierarchy
|
|
205
119
|
if (this.findComponent(component)) {
|
|
206
120
|
this.focusedComponent = component;
|
|
207
121
|
}
|
|
208
122
|
}
|
|
209
123
|
findComponent(component) {
|
|
210
|
-
// Check direct children
|
|
211
124
|
if (this.children.includes(component)) {
|
|
212
125
|
return true;
|
|
213
126
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (this.findInContainer(comp, component)) {
|
|
127
|
+
for (const child of this.children) {
|
|
128
|
+
if (child instanceof Container) {
|
|
129
|
+
if (this.findInContainer(child, component)) {
|
|
218
130
|
return true;
|
|
219
131
|
}
|
|
220
132
|
}
|
|
@@ -223,16 +135,11 @@ export class TUI extends Container {
|
|
|
223
135
|
}
|
|
224
136
|
findInContainer(container, component) {
|
|
225
137
|
const childCount = container.getChildCount();
|
|
226
|
-
// Check direct children
|
|
227
138
|
for (let i = 0; i < childCount; i++) {
|
|
228
139
|
const child = container.getChild(i);
|
|
229
140
|
if (child === component) {
|
|
230
141
|
return true;
|
|
231
142
|
}
|
|
232
|
-
}
|
|
233
|
-
// Recursively search in nested containers
|
|
234
|
-
for (let i = 0; i < childCount; i++) {
|
|
235
|
-
const child = container.getChild(i);
|
|
236
143
|
if (child instanceof Container) {
|
|
237
144
|
if (this.findInContainer(child, component)) {
|
|
238
145
|
return true;
|
|
@@ -244,140 +151,250 @@ export class TUI extends Container {
|
|
|
244
151
|
requestRender() {
|
|
245
152
|
if (!this.isStarted)
|
|
246
153
|
return;
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
this.
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
154
|
+
// Only queue a render if we haven't already
|
|
155
|
+
if (!this.needsRender) {
|
|
156
|
+
this.needsRender = true;
|
|
157
|
+
process.nextTick(() => {
|
|
158
|
+
if (this.needsRender) {
|
|
159
|
+
this.renderToScreen();
|
|
160
|
+
this.needsRender = false;
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
255
164
|
}
|
|
256
165
|
start() {
|
|
257
|
-
// Set started flag
|
|
258
166
|
this.isStarted = true;
|
|
259
|
-
// Hide
|
|
260
|
-
|
|
261
|
-
//
|
|
167
|
+
// Hide cursor
|
|
168
|
+
this.terminal.write("\x1b[?25l");
|
|
169
|
+
// Start terminal with handlers
|
|
262
170
|
try {
|
|
263
|
-
this.
|
|
264
|
-
if (process.stdin.setRawMode) {
|
|
265
|
-
process.stdin.setRawMode(true);
|
|
266
|
-
}
|
|
267
|
-
process.stdin.setEncoding("utf8");
|
|
268
|
-
process.stdin.resume();
|
|
269
|
-
// Listen for events
|
|
270
|
-
process.stdout.on("resize", this.handleResize);
|
|
271
|
-
process.stdin.on("data", this.handleKeypress);
|
|
171
|
+
this.terminal.start(this.handleKeypress, this.handleResize);
|
|
272
172
|
}
|
|
273
173
|
catch (error) {
|
|
274
|
-
console.error("Error
|
|
174
|
+
console.error("Error starting terminal:", error);
|
|
275
175
|
}
|
|
276
176
|
// Initial render
|
|
277
177
|
this.renderToScreen();
|
|
278
178
|
}
|
|
279
179
|
stop() {
|
|
280
|
-
// Show
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
process.stdin.setRawMode(this.wasRaw);
|
|
286
|
-
}
|
|
180
|
+
// Show cursor
|
|
181
|
+
this.terminal.write("\x1b[?25h");
|
|
182
|
+
// Stop terminal
|
|
183
|
+
this.terminal.stop();
|
|
184
|
+
this.isStarted = false;
|
|
287
185
|
}
|
|
288
186
|
renderToScreen(resize = false) {
|
|
289
|
-
const termWidth =
|
|
290
|
-
|
|
291
|
-
termWidth,
|
|
292
|
-
componentCount: this.children.length,
|
|
293
|
-
isFirstRender: this.isFirstRender,
|
|
294
|
-
});
|
|
295
|
-
const result = this.render(termWidth);
|
|
187
|
+
const termWidth = this.terminal.columns;
|
|
188
|
+
const termHeight = this.terminal.rows;
|
|
296
189
|
if (resize) {
|
|
297
|
-
this.totalLines = result.lines.length;
|
|
298
|
-
result.keepLines = 0;
|
|
299
190
|
this.isFirstRender = true;
|
|
191
|
+
this.previousRenderCommands = [];
|
|
192
|
+
this.previousLines = [];
|
|
300
193
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
changed: result.changed,
|
|
305
|
-
previousTotalLines: this.totalLines,
|
|
306
|
-
});
|
|
307
|
-
if (!result.changed) {
|
|
308
|
-
// Nothing changed - skip render
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
// Handle cursor positioning
|
|
194
|
+
// Collect all render commands
|
|
195
|
+
const currentRenderCommands = [];
|
|
196
|
+
this.collectRenderCommands(this, termWidth, currentRenderCommands);
|
|
312
197
|
if (this.isFirstRender) {
|
|
313
|
-
|
|
198
|
+
this.executeInitialRender(currentRenderCommands);
|
|
314
199
|
this.isFirstRender = false;
|
|
315
|
-
// Output all lines normally on first render
|
|
316
|
-
for (const line of result.lines) {
|
|
317
|
-
console.log(line);
|
|
318
|
-
}
|
|
319
200
|
}
|
|
320
201
|
else {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
202
|
+
this.executeDifferentialRender(currentRenderCommands, termHeight);
|
|
203
|
+
}
|
|
204
|
+
// Save for next render
|
|
205
|
+
this.previousRenderCommands = currentRenderCommands;
|
|
206
|
+
this.renderCount++;
|
|
207
|
+
}
|
|
208
|
+
collectRenderCommands(container, width, commands) {
|
|
209
|
+
const childCount = container.getChildCount();
|
|
210
|
+
for (let i = 0; i < childCount; i++) {
|
|
211
|
+
const child = container.getChild(i);
|
|
212
|
+
if (!child)
|
|
213
|
+
continue;
|
|
214
|
+
const result = child.render(width);
|
|
215
|
+
commands.push({
|
|
216
|
+
id: child.id,
|
|
217
|
+
lines: result.lines,
|
|
218
|
+
changed: result.changed,
|
|
329
219
|
});
|
|
330
|
-
|
|
331
|
-
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
executeInitialRender(commands) {
|
|
223
|
+
let output = "";
|
|
224
|
+
const lines = [];
|
|
225
|
+
for (const command of commands) {
|
|
226
|
+
lines.push(...command.lines);
|
|
227
|
+
}
|
|
228
|
+
// Output all lines
|
|
229
|
+
for (let i = 0; i < lines.length; i++) {
|
|
230
|
+
if (i > 0)
|
|
231
|
+
output += "\r\n";
|
|
232
|
+
output += lines[i];
|
|
233
|
+
}
|
|
234
|
+
// Add final newline to position cursor below content
|
|
235
|
+
if (lines.length > 0)
|
|
236
|
+
output += "\r\n";
|
|
237
|
+
this.terminal.write(output);
|
|
238
|
+
// Save what we rendered
|
|
239
|
+
this.previousLines = lines;
|
|
240
|
+
this.totalLinesRedrawn += lines.length;
|
|
241
|
+
logger.debug("TUI", "Initial render", {
|
|
242
|
+
commandsExecuted: commands.length,
|
|
243
|
+
linesRendered: lines.length,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
executeDifferentialRender(currentCommands, termHeight) {
|
|
247
|
+
let output = "";
|
|
248
|
+
let linesRedrawn = 0;
|
|
249
|
+
const viewportHeight = termHeight - 1; // Leave one line for cursor
|
|
250
|
+
// Build the new lines
|
|
251
|
+
const newLines = [];
|
|
252
|
+
for (const command of currentCommands) {
|
|
253
|
+
newLines.push(...command.lines);
|
|
254
|
+
}
|
|
255
|
+
// Calculate total lines for both old and new
|
|
256
|
+
const totalNewLines = newLines.length;
|
|
257
|
+
const totalOldLines = this.previousLines.length;
|
|
258
|
+
// Calculate what's visible in viewport
|
|
259
|
+
const oldVisibleLines = Math.min(totalOldLines, viewportHeight);
|
|
260
|
+
const newVisibleLines = Math.min(totalNewLines, viewportHeight);
|
|
261
|
+
// Check if we need to do a full redraw
|
|
262
|
+
let needFullRedraw = false;
|
|
263
|
+
let currentLineOffset = 0;
|
|
264
|
+
// Compare commands to detect structural changes
|
|
265
|
+
for (let i = 0; i < currentCommands.length; i++) {
|
|
266
|
+
const current = currentCommands[i];
|
|
267
|
+
const previous = i < this.previousRenderCommands.length ? this.previousRenderCommands[i] : null;
|
|
268
|
+
// Check if component order changed or new component
|
|
269
|
+
if (!previous || previous.id !== current.id) {
|
|
270
|
+
needFullRedraw = true;
|
|
271
|
+
break;
|
|
332
272
|
}
|
|
333
|
-
//
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
273
|
+
// Check if component changed
|
|
274
|
+
if (current.changed) {
|
|
275
|
+
// Check if line count changed
|
|
276
|
+
if (current.lines.length !== previous.lines.length) {
|
|
277
|
+
needFullRedraw = true;
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
// Check if component is fully visible
|
|
281
|
+
const componentEnd = currentLineOffset + current.lines.length;
|
|
282
|
+
const visibleStart = Math.max(0, totalNewLines - viewportHeight);
|
|
283
|
+
if (currentLineOffset < visibleStart) {
|
|
284
|
+
// Component is partially or fully outside viewport
|
|
285
|
+
needFullRedraw = true;
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
344
288
|
}
|
|
345
|
-
|
|
346
|
-
writeSync(process.stdout.fd, output);
|
|
289
|
+
currentLineOffset += current.lines.length;
|
|
347
290
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
291
|
+
// Move cursor to top of our content
|
|
292
|
+
if (oldVisibleLines > 0) {
|
|
293
|
+
output += `\x1b[${oldVisibleLines}A`;
|
|
294
|
+
}
|
|
295
|
+
if (needFullRedraw) {
|
|
296
|
+
// Clear each old line to avoid wrapping artifacts
|
|
297
|
+
for (let i = 0; i < oldVisibleLines; i++) {
|
|
298
|
+
if (i > 0)
|
|
299
|
+
output += `\x1b[1B`; // Move down one line
|
|
300
|
+
output += "\x1b[2K"; // Clear entire line
|
|
301
|
+
}
|
|
302
|
+
// Move back to start position
|
|
303
|
+
if (oldVisibleLines > 1) {
|
|
304
|
+
output += `\x1b[${oldVisibleLines - 1}A`;
|
|
305
|
+
}
|
|
306
|
+
// Ensure cursor is at beginning of line
|
|
307
|
+
output += "\r";
|
|
308
|
+
// Clear any remaining lines
|
|
309
|
+
output += "\x1b[0J"; // Clear from cursor to end of screen
|
|
310
|
+
// Determine what to render
|
|
311
|
+
let linesToRender;
|
|
312
|
+
if (totalNewLines <= viewportHeight) {
|
|
313
|
+
// Everything fits - render all
|
|
314
|
+
linesToRender = newLines;
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
// Only render what fits in viewport (last N lines)
|
|
318
|
+
linesToRender = newLines.slice(-viewportHeight);
|
|
319
|
+
}
|
|
320
|
+
// Output the lines
|
|
321
|
+
for (let i = 0; i < linesToRender.length; i++) {
|
|
322
|
+
if (i > 0)
|
|
323
|
+
output += "\r\n";
|
|
324
|
+
output += linesToRender[i];
|
|
325
|
+
}
|
|
326
|
+
// Add final newline
|
|
327
|
+
if (linesToRender.length > 0)
|
|
328
|
+
output += "\r\n";
|
|
329
|
+
linesRedrawn = linesToRender.length;
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
// Do line-by-line diff for visible portion only
|
|
333
|
+
const oldVisible = totalOldLines > viewportHeight ? this.previousLines.slice(-viewportHeight) : this.previousLines;
|
|
334
|
+
const newVisible = totalNewLines > viewportHeight ? newLines.slice(-viewportHeight) : newLines;
|
|
335
|
+
// Compare and update only changed lines
|
|
336
|
+
const maxLines = Math.max(oldVisible.length, newVisible.length);
|
|
337
|
+
for (let i = 0; i < maxLines; i++) {
|
|
338
|
+
const oldLine = i < oldVisible.length ? oldVisible[i] : "";
|
|
339
|
+
const newLine = i < newVisible.length ? newVisible[i] : "";
|
|
340
|
+
if (i >= newVisible.length) {
|
|
341
|
+
// This line no longer exists - clear it
|
|
342
|
+
if (i > 0) {
|
|
343
|
+
output += `\x1b[${i}B`; // Move to line i
|
|
344
|
+
}
|
|
345
|
+
output += "\x1b[2K"; // Clear line
|
|
346
|
+
output += `\x1b[${i}A`; // Move back to top
|
|
347
|
+
}
|
|
348
|
+
else if (oldLine !== newLine) {
|
|
349
|
+
// Line changed - update it
|
|
350
|
+
if (i > 0) {
|
|
351
|
+
output += `\x1b[${i}B`; // Move to line i
|
|
352
|
+
}
|
|
353
|
+
output += "\x1b[2K\r"; // Clear line and return to start
|
|
354
|
+
output += newLine;
|
|
355
|
+
if (i > 0) {
|
|
356
|
+
output += `\x1b[${i}A`; // Move back to top
|
|
357
|
+
}
|
|
358
|
+
linesRedrawn++;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
// Move cursor to end
|
|
362
|
+
output += `\x1b[${newVisible.length}B`;
|
|
363
|
+
// Clear any remaining lines if we have fewer lines now
|
|
364
|
+
if (newVisible.length < oldVisible.length) {
|
|
365
|
+
output += "\x1b[0J";
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
this.terminal.write(output);
|
|
369
|
+
// Save what we rendered
|
|
370
|
+
this.previousLines = newLines;
|
|
371
|
+
this.totalLinesRedrawn += linesRedrawn;
|
|
372
|
+
logger.debug("TUI", "Differential render", {
|
|
373
|
+
linesRedrawn,
|
|
374
|
+
needFullRedraw,
|
|
375
|
+
totalNewLines,
|
|
376
|
+
totalOldLines,
|
|
377
|
+
});
|
|
351
378
|
}
|
|
352
379
|
handleResize() {
|
|
353
|
-
// Clear screen
|
|
354
|
-
|
|
355
|
-
// Terminal size changed - force re-render all
|
|
380
|
+
// Clear screen and reset
|
|
381
|
+
this.terminal.write("\x1b[2J\x1b[H\x1b[?25l");
|
|
356
382
|
this.renderToScreen(true);
|
|
357
383
|
}
|
|
358
384
|
handleKeypress(data) {
|
|
359
385
|
logger.keyInput("TUI", data);
|
|
360
|
-
// Don't handle Ctrl+C here - let the global key handler deal with it
|
|
361
|
-
// if (data.charCodeAt(0) === 3) {
|
|
362
|
-
// logger.info("TUI", "Ctrl+C received");
|
|
363
|
-
// return; // Don't process this key further
|
|
364
|
-
// }
|
|
365
|
-
// Call global key handler if set
|
|
366
386
|
if (this.onGlobalKeyPress) {
|
|
367
387
|
const shouldForward = this.onGlobalKeyPress(data);
|
|
368
388
|
if (!shouldForward) {
|
|
369
|
-
// Global handler consumed the key, don't forward to focused component
|
|
370
389
|
this.requestRender();
|
|
371
390
|
return;
|
|
372
391
|
}
|
|
373
392
|
}
|
|
374
|
-
// Send input to focused component
|
|
375
393
|
if (this.focusedComponent?.handleInput) {
|
|
376
394
|
logger.debug("TUI", "Forwarding input to focused component", {
|
|
377
395
|
componentType: this.focusedComponent.constructor.name,
|
|
378
396
|
});
|
|
379
397
|
this.focusedComponent.handleInput(data);
|
|
380
|
-
// Trigger re-render after input
|
|
381
398
|
this.requestRender();
|
|
382
399
|
}
|
|
383
400
|
else {
|