@mariozechner/pi-tui 0.5.7 → 0.5.9
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/README.md +238 -314
- package/dist/autocomplete.d.ts.map +1 -1
- package/dist/autocomplete.js +0 -34
- package/dist/autocomplete.js.map +1 -1
- 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} +29 -6
- 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} +3 -82
- 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 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -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 +44 -30
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +269 -267
- package/dist/tui.js.map +1 -1
- package/package.json +6 -2
- package/dist/logger.d.ts +0 -23
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -83
- package/dist/logger.js.map +0 -1
- 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
|
-
import {
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
changed: true, // Always trigger cascade
|
|
10
|
-
};
|
|
11
|
-
}
|
|
2
|
+
import { ProcessTerminal } from "./terminal.js";
|
|
3
|
+
// Global component ID counter
|
|
4
|
+
let nextComponentId = 1;
|
|
5
|
+
// Helper to get next component ID
|
|
6
|
+
export function getNextComponentId() {
|
|
7
|
+
return nextComponentId++;
|
|
12
8
|
}
|
|
13
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Container for managing child components
|
|
11
|
+
*/
|
|
14
12
|
export class Container {
|
|
13
|
+
id;
|
|
15
14
|
children = [];
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
constructor(
|
|
19
|
-
this.
|
|
15
|
+
tui;
|
|
16
|
+
previousChildCount = 0;
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
// Clear the children array
|
|
137
|
-
this.children = [];
|
|
138
|
-
// Request render if we have a parent TUI
|
|
139
|
-
if (this.parentTui) {
|
|
140
|
-
this.parentTui.requestRender();
|
|
70
|
+
render(width) {
|
|
71
|
+
const lines = [];
|
|
72
|
+
let changed = false;
|
|
73
|
+
// Check if the number of children changed (important for detecting clears)
|
|
74
|
+
if (this.children.length !== this.previousChildCount) {
|
|
75
|
+
changed = true;
|
|
76
|
+
this.previousChildCount = this.children.length;
|
|
141
77
|
}
|
|
142
|
-
}
|
|
143
|
-
// Clean up sentinel components
|
|
144
|
-
cleanupSentinels() {
|
|
145
|
-
const originalCount = this.children.length;
|
|
146
|
-
const validChildren = [];
|
|
147
|
-
let sentinelCount = 0;
|
|
148
78
|
for (const child of this.children) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
child.cleanupSentinels();
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
else if (child instanceof SentinelComponent) {
|
|
157
|
-
sentinelCount++;
|
|
79
|
+
const result = child.render(width);
|
|
80
|
+
lines.push(...result.lines);
|
|
81
|
+
if (result.changed) {
|
|
82
|
+
changed = true;
|
|
158
83
|
}
|
|
159
84
|
}
|
|
160
|
-
|
|
161
|
-
if (sentinelCount > 0) {
|
|
162
|
-
logger.debug("Container", "Cleaned up sentinels", {
|
|
163
|
-
originalCount,
|
|
164
|
-
newCount: this.children.length,
|
|
165
|
-
sentinelsRemoved: sentinelCount,
|
|
166
|
-
});
|
|
167
|
-
}
|
|
85
|
+
return { lines, changed };
|
|
168
86
|
}
|
|
169
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* TUI - Smart differential rendering TUI implementation.
|
|
90
|
+
*/
|
|
170
91
|
export class TUI extends Container {
|
|
171
92
|
focusedComponent = null;
|
|
172
93
|
needsRender = false;
|
|
173
|
-
wasRaw = false;
|
|
174
|
-
totalLines = 0;
|
|
175
94
|
isFirstRender = true;
|
|
176
95
|
isStarted = false;
|
|
177
96
|
onGlobalKeyPress;
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
97
|
+
terminal;
|
|
98
|
+
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: Used in renderToScreen method on lines 260 and 276
|
|
99
|
+
previousRenderCommands = [];
|
|
100
|
+
previousLines = []; // What we rendered last time
|
|
101
|
+
// Performance metrics
|
|
102
|
+
totalLinesRedrawn = 0;
|
|
103
|
+
renderCount = 0;
|
|
104
|
+
getLinesRedrawn() {
|
|
105
|
+
return this.totalLinesRedrawn;
|
|
183
106
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
logger.info("TUI", "Logging configured", config);
|
|
187
|
-
}
|
|
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
|
-
}
|
|
107
|
+
getAverageLinesRedrawn() {
|
|
108
|
+
return this.renderCount > 0 ? this.totalLinesRedrawn / this.renderCount : 0;
|
|
198
109
|
}
|
|
199
|
-
|
|
200
|
-
super
|
|
201
|
-
this.
|
|
110
|
+
constructor(terminal) {
|
|
111
|
+
super();
|
|
112
|
+
this.setTui(this);
|
|
113
|
+
this.handleResize = this.handleResize.bind(this);
|
|
114
|
+
this.handleKeypress = this.handleKeypress.bind(this);
|
|
115
|
+
// Use provided terminal or default to ProcessTerminal
|
|
116
|
+
this.terminal = terminal || new ProcessTerminal();
|
|
202
117
|
}
|
|
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,148 +151,243 @@ 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);
|
|
175
|
+
}
|
|
176
|
+
// Trigger initial render if we have components
|
|
177
|
+
if (this.children.length > 0) {
|
|
178
|
+
this.requestRender();
|
|
275
179
|
}
|
|
276
|
-
// Initial render
|
|
277
|
-
this.renderToScreen();
|
|
278
180
|
}
|
|
279
181
|
stop() {
|
|
280
|
-
// Show
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
process.stdin.setRawMode(this.wasRaw);
|
|
286
|
-
}
|
|
182
|
+
// Show cursor
|
|
183
|
+
this.terminal.write("\x1b[?25h");
|
|
184
|
+
// Stop terminal
|
|
185
|
+
this.terminal.stop();
|
|
186
|
+
this.isStarted = false;
|
|
287
187
|
}
|
|
288
188
|
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);
|
|
189
|
+
const termWidth = this.terminal.columns;
|
|
190
|
+
const termHeight = this.terminal.rows;
|
|
296
191
|
if (resize) {
|
|
297
|
-
this.totalLines = result.lines.length;
|
|
298
|
-
result.keepLines = 0;
|
|
299
192
|
this.isFirstRender = true;
|
|
193
|
+
this.previousRenderCommands = [];
|
|
194
|
+
this.previousLines = [];
|
|
300
195
|
}
|
|
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
|
|
196
|
+
// Collect all render commands
|
|
197
|
+
const currentRenderCommands = [];
|
|
198
|
+
this.collectRenderCommands(this, termWidth, currentRenderCommands);
|
|
312
199
|
if (this.isFirstRender) {
|
|
313
|
-
|
|
200
|
+
this.renderInitial(currentRenderCommands);
|
|
314
201
|
this.isFirstRender = false;
|
|
315
|
-
// Output all lines normally on first render
|
|
316
|
-
for (const line of result.lines) {
|
|
317
|
-
console.log(line);
|
|
318
|
-
}
|
|
319
202
|
}
|
|
320
203
|
else {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
204
|
+
this.renderLineBased(currentRenderCommands, termHeight);
|
|
205
|
+
}
|
|
206
|
+
// Save for next render
|
|
207
|
+
this.previousRenderCommands = currentRenderCommands;
|
|
208
|
+
this.renderCount++;
|
|
209
|
+
}
|
|
210
|
+
collectRenderCommands(container, width, commands) {
|
|
211
|
+
const childCount = container.getChildCount();
|
|
212
|
+
for (let i = 0; i < childCount; i++) {
|
|
213
|
+
const child = container.getChild(i);
|
|
214
|
+
if (!child)
|
|
215
|
+
continue;
|
|
216
|
+
const result = child.render(width);
|
|
217
|
+
commands.push({
|
|
218
|
+
id: child.id,
|
|
219
|
+
lines: result.lines,
|
|
220
|
+
changed: result.changed,
|
|
329
221
|
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
renderInitial(commands) {
|
|
225
|
+
let output = "";
|
|
226
|
+
const lines = [];
|
|
227
|
+
for (const command of commands) {
|
|
228
|
+
lines.push(...command.lines);
|
|
229
|
+
}
|
|
230
|
+
// Output all lines
|
|
231
|
+
for (let i = 0; i < lines.length; i++) {
|
|
232
|
+
if (i > 0)
|
|
233
|
+
output += "\r\n";
|
|
234
|
+
output += lines[i];
|
|
235
|
+
}
|
|
236
|
+
// Add final newline to position cursor below content
|
|
237
|
+
if (lines.length > 0)
|
|
238
|
+
output += "\r\n";
|
|
239
|
+
this.terminal.write(output);
|
|
240
|
+
// Save what we rendered
|
|
241
|
+
this.previousLines = lines;
|
|
242
|
+
this.totalLinesRedrawn += lines.length;
|
|
243
|
+
}
|
|
244
|
+
renderLineBased(currentCommands, termHeight) {
|
|
245
|
+
const viewportHeight = termHeight - 1; // Leave one line for cursor
|
|
246
|
+
// Build the new lines array
|
|
247
|
+
const newLines = [];
|
|
248
|
+
for (const command of currentCommands) {
|
|
249
|
+
newLines.push(...command.lines);
|
|
250
|
+
}
|
|
251
|
+
const totalNewLines = newLines.length;
|
|
252
|
+
const totalOldLines = this.previousLines.length;
|
|
253
|
+
// Find first changed line by comparing old and new
|
|
254
|
+
let firstChangedLine = -1;
|
|
255
|
+
const minLines = Math.min(totalOldLines, totalNewLines);
|
|
256
|
+
for (let i = 0; i < minLines; i++) {
|
|
257
|
+
if (this.previousLines[i] !== newLines[i]) {
|
|
258
|
+
firstChangedLine = i;
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// If all common lines are the same, check if we have different lengths
|
|
263
|
+
if (firstChangedLine === -1 && totalOldLines !== totalNewLines) {
|
|
264
|
+
firstChangedLine = minLines;
|
|
265
|
+
}
|
|
266
|
+
// No changes at all
|
|
267
|
+
if (firstChangedLine === -1) {
|
|
268
|
+
this.previousLines = newLines;
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
// Calculate viewport boundaries
|
|
272
|
+
const oldViewportStart = Math.max(0, totalOldLines - viewportHeight);
|
|
273
|
+
const cursorPosition = totalOldLines; // Cursor is one line below last content
|
|
274
|
+
let output = "";
|
|
275
|
+
let linesRedrawn = 0;
|
|
276
|
+
// Check if change is in scrollback (unreachable by cursor)
|
|
277
|
+
if (firstChangedLine < oldViewportStart) {
|
|
278
|
+
// Must do full clear and re-render
|
|
279
|
+
output = "\x1b[3J\x1b[H"; // Clear scrollback and screen, home cursor
|
|
280
|
+
for (let i = 0; i < newLines.length; i++) {
|
|
281
|
+
if (i > 0)
|
|
282
|
+
output += "\r\n";
|
|
283
|
+
output += newLines[i];
|
|
284
|
+
}
|
|
285
|
+
if (newLines.length > 0)
|
|
286
|
+
output += "\r\n";
|
|
287
|
+
linesRedrawn = newLines.length;
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
// Change is in viewport - we can reach it with cursor movements
|
|
291
|
+
// Calculate viewport position of the change
|
|
292
|
+
const viewportChangePosition = firstChangedLine - oldViewportStart;
|
|
293
|
+
// Move cursor to the change position
|
|
294
|
+
const linesToMoveUp = cursorPosition - oldViewportStart - viewportChangePosition;
|
|
330
295
|
if (linesToMoveUp > 0) {
|
|
331
|
-
output += `\x1b[${linesToMoveUp}A
|
|
296
|
+
output += `\x1b[${linesToMoveUp}A`;
|
|
332
297
|
}
|
|
333
|
-
//
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
298
|
+
// Now do surgical updates or partial clear based on what's more efficient
|
|
299
|
+
let currentLine = firstChangedLine;
|
|
300
|
+
const currentViewportLine = viewportChangePosition;
|
|
301
|
+
// If we have significant structural changes, just clear and re-render from here
|
|
302
|
+
const hasSignificantChanges = totalNewLines !== totalOldLines || totalNewLines - firstChangedLine > 10; // Arbitrary threshold
|
|
303
|
+
if (hasSignificantChanges) {
|
|
304
|
+
// Clear from cursor to end of screen and render all remaining lines
|
|
305
|
+
output += "\r\x1b[0J";
|
|
306
|
+
for (let i = firstChangedLine; i < newLines.length; i++) {
|
|
307
|
+
if (i > firstChangedLine)
|
|
308
|
+
output += "\r\n";
|
|
309
|
+
output += newLines[i];
|
|
310
|
+
linesRedrawn++;
|
|
311
|
+
}
|
|
312
|
+
if (newLines.length > firstChangedLine)
|
|
313
|
+
output += "\r\n";
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
// Do surgical line-by-line updates
|
|
317
|
+
for (let i = firstChangedLine; i < minLines; i++) {
|
|
318
|
+
if (this.previousLines[i] !== newLines[i]) {
|
|
319
|
+
// Move to this line if needed
|
|
320
|
+
const moveLines = i - currentLine;
|
|
321
|
+
if (moveLines > 0) {
|
|
322
|
+
output += `\x1b[${moveLines}B`;
|
|
323
|
+
}
|
|
324
|
+
// Clear and rewrite the line
|
|
325
|
+
output += "\r\x1b[2K" + newLines[i];
|
|
326
|
+
currentLine = i;
|
|
327
|
+
linesRedrawn++;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// Handle added/removed lines at the end
|
|
331
|
+
if (totalNewLines > totalOldLines) {
|
|
332
|
+
// Move to end of old content and add new lines
|
|
333
|
+
const moveToEnd = totalOldLines - 1 - currentLine;
|
|
334
|
+
if (moveToEnd > 0) {
|
|
335
|
+
output += `\x1b[${moveToEnd}B`;
|
|
336
|
+
}
|
|
337
|
+
output += "\r\n";
|
|
338
|
+
for (let i = totalOldLines; i < totalNewLines; i++) {
|
|
339
|
+
if (i > totalOldLines)
|
|
340
|
+
output += "\r\n";
|
|
341
|
+
output += newLines[i];
|
|
342
|
+
linesRedrawn++;
|
|
343
|
+
}
|
|
344
|
+
output += "\r\n";
|
|
345
|
+
}
|
|
346
|
+
else if (totalNewLines < totalOldLines) {
|
|
347
|
+
// Move to end of new content and clear rest
|
|
348
|
+
const moveToEnd = totalNewLines - 1 - currentLine;
|
|
349
|
+
if (moveToEnd > 0) {
|
|
350
|
+
output += `\x1b[${moveToEnd}B`;
|
|
351
|
+
}
|
|
352
|
+
else if (moveToEnd < 0) {
|
|
353
|
+
output += `\x1b[${-moveToEnd}A`;
|
|
354
|
+
}
|
|
355
|
+
output += "\r\n\x1b[0J";
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
// Same length, just position cursor at end
|
|
359
|
+
const moveToEnd = totalNewLines - 1 - currentLine;
|
|
360
|
+
if (moveToEnd > 0) {
|
|
361
|
+
output += `\x1b[${moveToEnd}B`;
|
|
362
|
+
}
|
|
363
|
+
else if (moveToEnd < 0) {
|
|
364
|
+
output += `\x1b[${-moveToEnd}A`;
|
|
365
|
+
}
|
|
366
|
+
output += "\r\n";
|
|
367
|
+
}
|
|
344
368
|
}
|
|
345
|
-
// Write everything at once - use synchronous write to prevent race conditions
|
|
346
|
-
writeSync(process.stdout.fd, output);
|
|
347
369
|
}
|
|
348
|
-
this.
|
|
349
|
-
|
|
350
|
-
this.
|
|
370
|
+
this.terminal.write(output);
|
|
371
|
+
this.previousLines = newLines;
|
|
372
|
+
this.totalLinesRedrawn += linesRedrawn;
|
|
351
373
|
}
|
|
352
374
|
handleResize() {
|
|
353
|
-
// Clear screen
|
|
354
|
-
|
|
355
|
-
// Terminal size changed - force re-render all
|
|
375
|
+
// Clear screen and reset
|
|
376
|
+
this.terminal.write("\x1b[2J\x1b[H\x1b[?25l");
|
|
356
377
|
this.renderToScreen(true);
|
|
357
378
|
}
|
|
358
379
|
handleKeypress(data) {
|
|
359
|
-
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
380
|
if (this.onGlobalKeyPress) {
|
|
367
381
|
const shouldForward = this.onGlobalKeyPress(data);
|
|
368
382
|
if (!shouldForward) {
|
|
369
|
-
// Global handler consumed the key, don't forward to focused component
|
|
370
383
|
this.requestRender();
|
|
371
384
|
return;
|
|
372
385
|
}
|
|
373
386
|
}
|
|
374
|
-
// Send input to focused component
|
|
375
387
|
if (this.focusedComponent?.handleInput) {
|
|
376
|
-
logger.debug("TUI", "Forwarding input to focused component", {
|
|
377
|
-
componentType: this.focusedComponent.constructor.name,
|
|
378
|
-
});
|
|
379
388
|
this.focusedComponent.handleInput(data);
|
|
380
|
-
// Trigger re-render after input
|
|
381
389
|
this.requestRender();
|
|
382
390
|
}
|
|
383
|
-
else {
|
|
384
|
-
logger.warn("TUI", "No focused component to handle input", {
|
|
385
|
-
focusedComponent: this.focusedComponent?.constructor.name || "none",
|
|
386
|
-
hasHandleInput: this.focusedComponent?.handleInput ? "yes" : "no",
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
391
|
}
|
|
390
392
|
}
|
|
391
393
|
//# sourceMappingURL=tui.js.map
|