@mariozechner/pi-tui 0.5.48 → 0.6.2
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 +166 -475
- package/dist/autocomplete.d.ts.map +1 -1
- package/dist/autocomplete.js +2 -0
- package/dist/autocomplete.js.map +1 -1
- package/dist/components/{text-editor.d.ts → editor.d.ts} +9 -5
- package/dist/components/editor.d.ts.map +1 -0
- package/dist/components/{text-editor.js → editor.js} +125 -70
- package/dist/components/editor.js.map +1 -0
- package/dist/components/input.d.ts +14 -0
- package/dist/components/input.d.ts.map +1 -0
- package/dist/components/input.js +120 -0
- package/dist/components/input.js.map +1 -0
- package/dist/components/{loading-animation.d.ts → loader.d.ts} +5 -5
- package/dist/components/loader.d.ts.map +1 -0
- package/dist/components/{loading-animation.js → loader.js} +13 -10
- package/dist/components/loader.js.map +1 -0
- package/dist/components/markdown.d.ts +46 -0
- package/dist/components/markdown.d.ts.map +1 -0
- package/dist/components/markdown.js +499 -0
- package/dist/components/markdown.js.map +1 -0
- package/dist/components/select-list.d.ts +3 -3
- package/dist/components/select-list.d.ts.map +1 -1
- package/dist/components/select-list.js +24 -16
- package/dist/components/select-list.js.map +1 -1
- package/dist/components/spacer.d.ts +11 -0
- package/dist/components/spacer.d.ts.map +1 -0
- package/dist/components/spacer.js +20 -0
- package/dist/components/spacer.js.map +1 -0
- package/dist/components/text.d.ts +26 -0
- package/dist/components/text.d.ts.map +1 -0
- package/dist/components/text.js +141 -0
- package/dist/components/text.js.map +1 -0
- package/dist/index.d.ts +8 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -12
- package/dist/index.js.map +1 -1
- package/dist/terminal.d.ts +12 -0
- package/dist/terminal.d.ts.map +1 -1
- package/dist/terminal.js +33 -3
- package/dist/terminal.js.map +1 -1
- package/dist/tui.d.ts +30 -52
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +131 -337
- package/dist/tui.js.map +1 -1
- package/dist/utils.d.ts +10 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +15 -0
- package/dist/utils.js.map +1 -0
- package/package.json +6 -5
- package/dist/components/loading-animation.d.ts.map +0 -1
- package/dist/components/loading-animation.js.map +0 -1
- package/dist/components/markdown-component.d.ts +0 -15
- package/dist/components/markdown-component.d.ts.map +0 -1
- package/dist/components/markdown-component.js +0 -247
- package/dist/components/markdown-component.js.map +0 -1
- package/dist/components/text-component.d.ts +0 -14
- package/dist/components/text-component.d.ts.map +0 -1
- package/dist/components/text-component.js +0 -90
- package/dist/components/text-component.js.map +0 -1
- package/dist/components/text-editor.d.ts.map +0 -1
- package/dist/components/text-editor.js.map +0 -1
- package/dist/components/whitespace-component.d.ts +0 -13
- package/dist/components/whitespace-component.d.ts.map +0 -1
- package/dist/components/whitespace-component.js +0 -22
- package/dist/components/whitespace-component.js.map +0 -1
package/dist/tui.js
CHANGED
|
@@ -1,389 +1,183 @@
|
|
|
1
|
-
import process from "process";
|
|
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++;
|
|
8
|
-
}
|
|
9
1
|
/**
|
|
10
|
-
*
|
|
2
|
+
* Minimal TUI implementation with differential rendering
|
|
3
|
+
*/
|
|
4
|
+
import { visibleWidth } from "./utils.js";
|
|
5
|
+
export { visibleWidth };
|
|
6
|
+
/**
|
|
7
|
+
* Container - a component that contains other components
|
|
11
8
|
*/
|
|
12
9
|
export class Container {
|
|
13
|
-
|
|
14
|
-
this.children = [];
|
|
15
|
-
this.previousChildCount = 0;
|
|
16
|
-
this.id = getNextComponentId();
|
|
17
|
-
}
|
|
18
|
-
setTui(tui) {
|
|
19
|
-
this.tui = tui;
|
|
20
|
-
for (const child of this.children) {
|
|
21
|
-
if (child instanceof Container) {
|
|
22
|
-
child.setTui(tui);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
10
|
+
children = [];
|
|
26
11
|
addChild(component) {
|
|
27
12
|
this.children.push(component);
|
|
28
|
-
if (component instanceof Container) {
|
|
29
|
-
component.setTui(this.tui);
|
|
30
|
-
}
|
|
31
|
-
this.tui?.requestRender();
|
|
32
13
|
}
|
|
33
14
|
removeChild(component) {
|
|
34
15
|
const index = this.children.indexOf(component);
|
|
35
|
-
if (index
|
|
36
|
-
this.children.splice(index, 1);
|
|
37
|
-
if (component instanceof Container) {
|
|
38
|
-
component.setTui(undefined);
|
|
39
|
-
}
|
|
40
|
-
this.tui?.requestRender();
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
removeChildAt(index) {
|
|
44
|
-
if (index >= 0 && index < this.children.length) {
|
|
45
|
-
const component = this.children[index];
|
|
16
|
+
if (index !== -1) {
|
|
46
17
|
this.children.splice(index, 1);
|
|
47
|
-
if (component instanceof Container) {
|
|
48
|
-
component.setTui(undefined);
|
|
49
|
-
}
|
|
50
|
-
this.tui?.requestRender();
|
|
51
18
|
}
|
|
52
19
|
}
|
|
53
20
|
clear() {
|
|
54
|
-
for (const child of this.children) {
|
|
55
|
-
if (child instanceof Container) {
|
|
56
|
-
child.setTui(undefined);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
21
|
this.children = [];
|
|
60
|
-
this.tui?.requestRender();
|
|
61
|
-
}
|
|
62
|
-
getChild(index) {
|
|
63
|
-
return this.children[index];
|
|
64
|
-
}
|
|
65
|
-
getChildCount() {
|
|
66
|
-
return this.children.length;
|
|
67
22
|
}
|
|
68
23
|
render(width) {
|
|
69
24
|
const lines = [];
|
|
70
|
-
let changed = false;
|
|
71
|
-
// Check if the number of children changed (important for detecting clears)
|
|
72
|
-
if (this.children.length !== this.previousChildCount) {
|
|
73
|
-
changed = true;
|
|
74
|
-
this.previousChildCount = this.children.length;
|
|
75
|
-
}
|
|
76
25
|
for (const child of this.children) {
|
|
77
|
-
|
|
78
|
-
lines.push(...result.lines);
|
|
79
|
-
if (result.changed) {
|
|
80
|
-
changed = true;
|
|
81
|
-
}
|
|
26
|
+
lines.push(...child.render(width));
|
|
82
27
|
}
|
|
83
|
-
return
|
|
28
|
+
return lines;
|
|
84
29
|
}
|
|
85
30
|
}
|
|
86
31
|
/**
|
|
87
|
-
* TUI -
|
|
32
|
+
* TUI - Main class for managing terminal UI with differential rendering
|
|
88
33
|
*/
|
|
89
34
|
export class TUI extends Container {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
35
|
+
terminal;
|
|
36
|
+
previousLines = [];
|
|
37
|
+
previousWidth = 0;
|
|
38
|
+
focusedComponent = null;
|
|
39
|
+
renderRequested = false;
|
|
40
|
+
cursorRow = 0; // Track where cursor is (0-indexed, relative to our first line)
|
|
96
41
|
constructor(terminal) {
|
|
97
42
|
super();
|
|
98
|
-
this.
|
|
99
|
-
this.needsRender = false;
|
|
100
|
-
this.isFirstRender = true;
|
|
101
|
-
this.isStarted = false;
|
|
102
|
-
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: Used in renderToScreen method on lines 260 and 276
|
|
103
|
-
this.previousRenderCommands = [];
|
|
104
|
-
this.previousLines = []; // What we rendered last time
|
|
105
|
-
// Performance metrics
|
|
106
|
-
this.totalLinesRedrawn = 0;
|
|
107
|
-
this.renderCount = 0;
|
|
108
|
-
this.setTui(this);
|
|
109
|
-
this.handleResize = this.handleResize.bind(this);
|
|
110
|
-
this.handleKeypress = this.handleKeypress.bind(this);
|
|
111
|
-
// Use provided terminal or default to ProcessTerminal
|
|
112
|
-
this.terminal = terminal || new ProcessTerminal();
|
|
43
|
+
this.terminal = terminal;
|
|
113
44
|
}
|
|
114
45
|
setFocus(component) {
|
|
115
|
-
|
|
116
|
-
this.focusedComponent = component;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
findComponent(component) {
|
|
120
|
-
if (this.children.includes(component)) {
|
|
121
|
-
return true;
|
|
122
|
-
}
|
|
123
|
-
for (const child of this.children) {
|
|
124
|
-
if (child instanceof Container) {
|
|
125
|
-
if (this.findInContainer(child, component)) {
|
|
126
|
-
return true;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
findInContainer(container, component) {
|
|
133
|
-
const childCount = container.getChildCount();
|
|
134
|
-
for (let i = 0; i < childCount; i++) {
|
|
135
|
-
const child = container.getChild(i);
|
|
136
|
-
if (child === component) {
|
|
137
|
-
return true;
|
|
138
|
-
}
|
|
139
|
-
if (child instanceof Container) {
|
|
140
|
-
if (this.findInContainer(child, component)) {
|
|
141
|
-
return true;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
return false;
|
|
146
|
-
}
|
|
147
|
-
requestRender() {
|
|
148
|
-
if (!this.isStarted)
|
|
149
|
-
return;
|
|
150
|
-
// Only queue a render if we haven't already
|
|
151
|
-
if (!this.needsRender) {
|
|
152
|
-
this.needsRender = true;
|
|
153
|
-
process.nextTick(() => {
|
|
154
|
-
if (this.needsRender) {
|
|
155
|
-
this.renderToScreen();
|
|
156
|
-
this.needsRender = false;
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
}
|
|
46
|
+
this.focusedComponent = component;
|
|
160
47
|
}
|
|
161
48
|
start() {
|
|
162
|
-
this.
|
|
163
|
-
|
|
164
|
-
this.
|
|
165
|
-
// Start terminal with handlers
|
|
166
|
-
try {
|
|
167
|
-
this.terminal.start(this.handleKeypress, this.handleResize);
|
|
168
|
-
}
|
|
169
|
-
catch (error) {
|
|
170
|
-
console.error("Error starting terminal:", error);
|
|
171
|
-
}
|
|
172
|
-
// Trigger initial render if we have components
|
|
173
|
-
if (this.children.length > 0) {
|
|
174
|
-
this.requestRender();
|
|
175
|
-
}
|
|
49
|
+
this.terminal.start((data) => this.handleInput(data), () => this.requestRender());
|
|
50
|
+
this.terminal.hideCursor();
|
|
51
|
+
this.requestRender();
|
|
176
52
|
}
|
|
177
53
|
stop() {
|
|
178
|
-
|
|
179
|
-
this.terminal.write("\x1b[?25h");
|
|
180
|
-
// Stop terminal
|
|
54
|
+
this.terminal.showCursor();
|
|
181
55
|
this.terminal.stop();
|
|
182
|
-
this.isStarted = false;
|
|
183
|
-
}
|
|
184
|
-
renderToScreen(resize = false) {
|
|
185
|
-
const termWidth = this.terminal.columns;
|
|
186
|
-
const termHeight = this.terminal.rows;
|
|
187
|
-
if (resize) {
|
|
188
|
-
this.isFirstRender = true;
|
|
189
|
-
this.previousRenderCommands = [];
|
|
190
|
-
this.previousLines = [];
|
|
191
|
-
}
|
|
192
|
-
// Collect all render commands
|
|
193
|
-
const currentRenderCommands = [];
|
|
194
|
-
this.collectRenderCommands(this, termWidth, currentRenderCommands);
|
|
195
|
-
if (this.isFirstRender) {
|
|
196
|
-
this.renderInitial(currentRenderCommands);
|
|
197
|
-
this.isFirstRender = false;
|
|
198
|
-
}
|
|
199
|
-
else {
|
|
200
|
-
this.renderLineBased(currentRenderCommands, termHeight);
|
|
201
|
-
}
|
|
202
|
-
// Save for next render
|
|
203
|
-
this.previousRenderCommands = currentRenderCommands;
|
|
204
|
-
this.renderCount++;
|
|
205
56
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
let output = "";
|
|
222
|
-
const lines = [];
|
|
223
|
-
for (const command of commands) {
|
|
224
|
-
lines.push(...command.lines);
|
|
225
|
-
}
|
|
226
|
-
// Output all lines
|
|
227
|
-
for (let i = 0; i < lines.length; i++) {
|
|
228
|
-
if (i > 0)
|
|
229
|
-
output += "\r\n";
|
|
230
|
-
output += lines[i];
|
|
57
|
+
requestRender() {
|
|
58
|
+
if (this.renderRequested)
|
|
59
|
+
return;
|
|
60
|
+
this.renderRequested = true;
|
|
61
|
+
process.nextTick(() => {
|
|
62
|
+
this.renderRequested = false;
|
|
63
|
+
this.doRender();
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
handleInput(data) {
|
|
67
|
+
// Pass input to focused component (including Ctrl+C)
|
|
68
|
+
// The focused component can decide how to handle Ctrl+C
|
|
69
|
+
if (this.focusedComponent?.handleInput) {
|
|
70
|
+
this.focusedComponent.handleInput(data);
|
|
71
|
+
this.requestRender();
|
|
231
72
|
}
|
|
232
|
-
// Add final newline to position cursor below content
|
|
233
|
-
if (lines.length > 0)
|
|
234
|
-
output += "\r\n";
|
|
235
|
-
this.terminal.write(output);
|
|
236
|
-
// Save what we rendered
|
|
237
|
-
this.previousLines = lines;
|
|
238
|
-
this.totalLinesRedrawn += lines.length;
|
|
239
73
|
}
|
|
240
|
-
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
firstChangedLine = i;
|
|
255
|
-
break;
|
|
74
|
+
doRender() {
|
|
75
|
+
const width = this.terminal.columns;
|
|
76
|
+
const height = this.terminal.rows;
|
|
77
|
+
// Render all components to get new lines
|
|
78
|
+
const newLines = this.render(width);
|
|
79
|
+
// Width changed - need full re-render
|
|
80
|
+
const widthChanged = this.previousWidth !== 0 && this.previousWidth !== width;
|
|
81
|
+
// First render - just output everything without clearing
|
|
82
|
+
if (this.previousLines.length === 0) {
|
|
83
|
+
let buffer = "\x1b[?2026h"; // Begin synchronized output
|
|
84
|
+
for (let i = 0; i < newLines.length; i++) {
|
|
85
|
+
if (i > 0)
|
|
86
|
+
buffer += "\r\n";
|
|
87
|
+
buffer += newLines[i];
|
|
256
88
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
// No changes at all
|
|
263
|
-
if (firstChangedLine === -1) {
|
|
89
|
+
buffer += "\x1b[?2026l"; // End synchronized output
|
|
90
|
+
this.terminal.write(buffer);
|
|
91
|
+
// After rendering N lines, cursor is at end of last line (line N-1)
|
|
92
|
+
this.cursorRow = newLines.length - 1;
|
|
264
93
|
this.previousLines = newLines;
|
|
94
|
+
this.previousWidth = width;
|
|
265
95
|
return;
|
|
266
96
|
}
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
let linesRedrawn = 0;
|
|
272
|
-
// Check if change is in scrollback (unreachable by cursor)
|
|
273
|
-
if (firstChangedLine < oldViewportStart) {
|
|
274
|
-
// Must do full clear and re-render
|
|
275
|
-
output = "\x1b[3J\x1b[H"; // Clear scrollback and screen, home cursor
|
|
97
|
+
// Width changed - full re-render
|
|
98
|
+
if (widthChanged) {
|
|
99
|
+
let buffer = "\x1b[?2026h"; // Begin synchronized output
|
|
100
|
+
buffer += "\x1b[3J\x1b[2J\x1b[H"; // Clear scrollback, screen, and home
|
|
276
101
|
for (let i = 0; i < newLines.length; i++) {
|
|
277
102
|
if (i > 0)
|
|
278
|
-
|
|
279
|
-
|
|
103
|
+
buffer += "\r\n";
|
|
104
|
+
buffer += newLines[i];
|
|
280
105
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
106
|
+
buffer += "\x1b[?2026l"; // End synchronized output
|
|
107
|
+
this.terminal.write(buffer);
|
|
108
|
+
this.cursorRow = newLines.length - 1;
|
|
109
|
+
this.previousLines = newLines;
|
|
110
|
+
this.previousWidth = width;
|
|
111
|
+
return;
|
|
284
112
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
let currentLine = firstChangedLine;
|
|
296
|
-
const currentViewportLine = viewportChangePosition;
|
|
297
|
-
// If we have significant structural changes, just clear and re-render from here
|
|
298
|
-
const hasSignificantChanges = totalNewLines !== totalOldLines || totalNewLines - firstChangedLine > 10; // Arbitrary threshold
|
|
299
|
-
if (hasSignificantChanges) {
|
|
300
|
-
// Clear from cursor to end of screen and render all remaining lines
|
|
301
|
-
output += "\r\x1b[0J";
|
|
302
|
-
for (let i = firstChangedLine; i < newLines.length; i++) {
|
|
303
|
-
if (i > firstChangedLine)
|
|
304
|
-
output += "\r\n";
|
|
305
|
-
output += newLines[i];
|
|
306
|
-
linesRedrawn++;
|
|
307
|
-
}
|
|
308
|
-
if (newLines.length > firstChangedLine)
|
|
309
|
-
output += "\r\n";
|
|
310
|
-
}
|
|
311
|
-
else {
|
|
312
|
-
// Do surgical line-by-line updates
|
|
313
|
-
for (let i = firstChangedLine; i < minLines; i++) {
|
|
314
|
-
if (this.previousLines[i] !== newLines[i]) {
|
|
315
|
-
// Move to this line if needed
|
|
316
|
-
const moveLines = i - currentLine;
|
|
317
|
-
if (moveLines > 0) {
|
|
318
|
-
output += `\x1b[${moveLines}B`;
|
|
319
|
-
}
|
|
320
|
-
// Clear and rewrite the line
|
|
321
|
-
output += "\r\x1b[2K" + newLines[i];
|
|
322
|
-
currentLine = i;
|
|
323
|
-
linesRedrawn++;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
// Handle added/removed lines at the end
|
|
327
|
-
if (totalNewLines > totalOldLines) {
|
|
328
|
-
// Move to end of old content and add new lines
|
|
329
|
-
const moveToEnd = totalOldLines - 1 - currentLine;
|
|
330
|
-
if (moveToEnd > 0) {
|
|
331
|
-
output += `\x1b[${moveToEnd}B`;
|
|
332
|
-
}
|
|
333
|
-
output += "\r\n";
|
|
334
|
-
for (let i = totalOldLines; i < totalNewLines; i++) {
|
|
335
|
-
if (i > totalOldLines)
|
|
336
|
-
output += "\r\n";
|
|
337
|
-
output += newLines[i];
|
|
338
|
-
linesRedrawn++;
|
|
339
|
-
}
|
|
340
|
-
output += "\r\n";
|
|
341
|
-
}
|
|
342
|
-
else if (totalNewLines < totalOldLines) {
|
|
343
|
-
// Move to end of new content and clear rest
|
|
344
|
-
const moveToEnd = totalNewLines - 1 - currentLine;
|
|
345
|
-
if (moveToEnd > 0) {
|
|
346
|
-
output += `\x1b[${moveToEnd}B`;
|
|
347
|
-
}
|
|
348
|
-
else if (moveToEnd < 0) {
|
|
349
|
-
output += `\x1b[${-moveToEnd}A`;
|
|
350
|
-
}
|
|
351
|
-
output += "\r\n\x1b[0J";
|
|
352
|
-
}
|
|
353
|
-
else {
|
|
354
|
-
// Same length, just position cursor at end
|
|
355
|
-
const moveToEnd = totalNewLines - 1 - currentLine;
|
|
356
|
-
if (moveToEnd > 0) {
|
|
357
|
-
output += `\x1b[${moveToEnd}B`;
|
|
358
|
-
}
|
|
359
|
-
else if (moveToEnd < 0) {
|
|
360
|
-
output += `\x1b[${-moveToEnd}A`;
|
|
361
|
-
}
|
|
362
|
-
output += "\r\n";
|
|
113
|
+
// Find first and last changed lines
|
|
114
|
+
let firstChanged = -1;
|
|
115
|
+
let lastChanged = -1;
|
|
116
|
+
const maxLines = Math.max(newLines.length, this.previousLines.length);
|
|
117
|
+
for (let i = 0; i < maxLines; i++) {
|
|
118
|
+
const oldLine = i < this.previousLines.length ? this.previousLines[i] : "";
|
|
119
|
+
const newLine = i < newLines.length ? newLines[i] : "";
|
|
120
|
+
if (oldLine !== newLine) {
|
|
121
|
+
if (firstChanged === -1) {
|
|
122
|
+
firstChanged = i;
|
|
363
123
|
}
|
|
124
|
+
lastChanged = i;
|
|
364
125
|
}
|
|
365
126
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
//
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
127
|
+
// No changes
|
|
128
|
+
if (firstChanged === -1) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
// Check if firstChanged is outside the viewport
|
|
132
|
+
// cursorRow is the line where cursor is (0-indexed)
|
|
133
|
+
// Viewport shows lines from (cursorRow - height + 1) to cursorRow
|
|
134
|
+
// If firstChanged < viewportTop, we need full re-render
|
|
135
|
+
const viewportTop = this.cursorRow - height + 1;
|
|
136
|
+
if (firstChanged < viewportTop) {
|
|
137
|
+
// First change is above viewport - need full re-render
|
|
138
|
+
let buffer = "\x1b[?2026h"; // Begin synchronized output
|
|
139
|
+
buffer += "\x1b[3J\x1b[2J\x1b[H"; // Clear scrollback, screen, and home
|
|
140
|
+
for (let i = 0; i < newLines.length; i++) {
|
|
141
|
+
if (i > 0)
|
|
142
|
+
buffer += "\r\n";
|
|
143
|
+
buffer += newLines[i];
|
|
381
144
|
}
|
|
145
|
+
buffer += "\x1b[?2026l"; // End synchronized output
|
|
146
|
+
this.terminal.write(buffer);
|
|
147
|
+
this.cursorRow = newLines.length - 1;
|
|
148
|
+
this.previousLines = newLines;
|
|
149
|
+
this.previousWidth = width;
|
|
150
|
+
return;
|
|
382
151
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
152
|
+
// Render from first changed line to end
|
|
153
|
+
// Build buffer with all updates wrapped in synchronized output
|
|
154
|
+
let buffer = "\x1b[?2026h"; // Begin synchronized output
|
|
155
|
+
// Move cursor to first changed line
|
|
156
|
+
const lineDiff = firstChanged - this.cursorRow;
|
|
157
|
+
if (lineDiff > 0) {
|
|
158
|
+
buffer += `\x1b[${lineDiff}B`; // Move down
|
|
159
|
+
}
|
|
160
|
+
else if (lineDiff < 0) {
|
|
161
|
+
buffer += `\x1b[${-lineDiff}A`; // Move up
|
|
162
|
+
}
|
|
163
|
+
buffer += "\r"; // Move to column 0
|
|
164
|
+
buffer += "\x1b[J"; // Clear from cursor to end of screen
|
|
165
|
+
// Render from first changed line to end
|
|
166
|
+
for (let i = firstChanged; i < newLines.length; i++) {
|
|
167
|
+
if (i > firstChanged)
|
|
168
|
+
buffer += "\r\n";
|
|
169
|
+
if (visibleWidth(newLines[i]) > width) {
|
|
170
|
+
throw new Error(`Rendered line ${i} exceeds terminal width\n\n${newLines[i]}`);
|
|
171
|
+
}
|
|
172
|
+
buffer += newLines[i];
|
|
386
173
|
}
|
|
174
|
+
buffer += "\x1b[?2026l"; // End synchronized output
|
|
175
|
+
// Write entire buffer at once
|
|
176
|
+
this.terminal.write(buffer);
|
|
177
|
+
// Cursor is now at end of last line
|
|
178
|
+
this.cursorRow = newLines.length - 1;
|
|
179
|
+
this.previousLines = newLines;
|
|
180
|
+
this.previousWidth = width;
|
|
387
181
|
}
|
|
388
182
|
}
|
|
389
183
|
//# sourceMappingURL=tui.js.map
|
package/dist/tui.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tui.js","sourceRoot":"","sources":["../src/tui.ts"],"names":[],"mappings":"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","sourcesContent":["import process from \"process\";\nimport { ProcessTerminal, type Terminal } from \"./terminal.js\";\n\n/**\n * Result of rendering a component\n */\nexport interface ComponentRenderResult {\n\tlines: string[];\n\tchanged: boolean;\n}\n\n/**\n * Component interface\n */\nexport interface Component {\n\treadonly id: number;\n\trender(width: number): ComponentRenderResult;\n\thandleInput?(keyData: string): void;\n}\n\n// Global component ID counter\nlet nextComponentId = 1;\n\n// Helper to get next component ID\nexport function getNextComponentId(): number {\n\treturn nextComponentId++;\n}\n\n// Padding type for components\nexport interface Padding {\n\ttop?: number;\n\tbottom?: number;\n\tleft?: number;\n\tright?: number;\n}\n\n/**\n * Container for managing child components\n */\nexport class Container implements Component {\n\treadonly id: number;\n\tpublic children: (Component | Container)[] = [];\n\tprivate tui?: TUI;\n\tprivate previousChildCount: number = 0;\n\n\tconstructor() {\n\t\tthis.id = getNextComponentId();\n\t}\n\n\tsetTui(tui: TUI | undefined): void {\n\t\tthis.tui = tui;\n\t\tfor (const child of this.children) {\n\t\t\tif (child instanceof Container) {\n\t\t\t\tchild.setTui(tui);\n\t\t\t}\n\t\t}\n\t}\n\n\taddChild(component: Component | Container): void {\n\t\tthis.children.push(component);\n\t\tif (component instanceof Container) {\n\t\t\tcomponent.setTui(this.tui);\n\t\t}\n\t\tthis.tui?.requestRender();\n\t}\n\n\tremoveChild(component: Component | Container): void {\n\t\tconst index = this.children.indexOf(component);\n\t\tif (index >= 0) {\n\t\t\tthis.children.splice(index, 1);\n\t\t\tif (component instanceof Container) {\n\t\t\t\tcomponent.setTui(undefined);\n\t\t\t}\n\t\t\tthis.tui?.requestRender();\n\t\t}\n\t}\n\n\tremoveChildAt(index: number): void {\n\t\tif (index >= 0 && index < this.children.length) {\n\t\t\tconst component = this.children[index];\n\t\t\tthis.children.splice(index, 1);\n\t\t\tif (component instanceof Container) {\n\t\t\t\tcomponent.setTui(undefined);\n\t\t\t}\n\t\t\tthis.tui?.requestRender();\n\t\t}\n\t}\n\n\tclear(): void {\n\t\tfor (const child of this.children) {\n\t\t\tif (child instanceof Container) {\n\t\t\t\tchild.setTui(undefined);\n\t\t\t}\n\t\t}\n\t\tthis.children = [];\n\t\tthis.tui?.requestRender();\n\t}\n\n\tgetChild(index: number): (Component | Container) | undefined {\n\t\treturn this.children[index];\n\t}\n\n\tgetChildCount(): number {\n\t\treturn this.children.length;\n\t}\n\n\trender(width: number): ComponentRenderResult {\n\t\tconst lines: string[] = [];\n\t\tlet changed = false;\n\n\t\t// Check if the number of children changed (important for detecting clears)\n\t\tif (this.children.length !== this.previousChildCount) {\n\t\t\tchanged = true;\n\t\t\tthis.previousChildCount = this.children.length;\n\t\t}\n\n\t\tfor (const child of this.children) {\n\t\t\tconst result = child.render(width);\n\t\t\tlines.push(...result.lines);\n\t\t\tif (result.changed) {\n\t\t\t\tchanged = true;\n\t\t\t}\n\t\t}\n\n\t\treturn { lines, changed };\n\t}\n}\n\n/**\n * Render command for tracking component output\n */\ninterface RenderCommand {\n\tid: number;\n\tlines: string[];\n\tchanged: boolean;\n}\n\n/**\n * TUI - Smart differential rendering TUI implementation.\n */\nexport class TUI extends Container {\n\tprivate focusedComponent: Component | null = null;\n\tprivate needsRender = false;\n\tprivate isFirstRender = true;\n\tprivate isStarted = false;\n\tpublic onGlobalKeyPress?: (data: string) => boolean;\n\tprivate terminal: Terminal;\n\t// biome-ignore lint/correctness/noUnusedPrivateClassMembers: Used in renderToScreen method on lines 260 and 276\n\tprivate previousRenderCommands: RenderCommand[] = [];\n\tprivate previousLines: string[] = []; // What we rendered last time\n\n\t// Performance metrics\n\tprivate totalLinesRedrawn = 0;\n\tprivate renderCount = 0;\n\tpublic getLinesRedrawn(): number {\n\t\treturn this.totalLinesRedrawn;\n\t}\n\tpublic getAverageLinesRedrawn(): number {\n\t\treturn this.renderCount > 0 ? this.totalLinesRedrawn / this.renderCount : 0;\n\t}\n\n\tconstructor(terminal?: Terminal) {\n\t\tsuper();\n\t\tthis.setTui(this);\n\t\tthis.handleResize = this.handleResize.bind(this);\n\t\tthis.handleKeypress = this.handleKeypress.bind(this);\n\n\t\t// Use provided terminal or default to ProcessTerminal\n\t\tthis.terminal = terminal || new ProcessTerminal();\n\t}\n\n\tsetFocus(component: Component): void {\n\t\tif (this.findComponent(component)) {\n\t\t\tthis.focusedComponent = component;\n\t\t}\n\t}\n\n\tprivate findComponent(component: Component): boolean {\n\t\tif (this.children.includes(component)) {\n\t\t\treturn true;\n\t\t}\n\n\t\tfor (const child of this.children) {\n\t\t\tif (child instanceof Container) {\n\t\t\t\tif (this.findInContainer(child, component)) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\tprivate findInContainer(container: Container, component: Component): boolean {\n\t\tconst childCount = container.getChildCount();\n\n\t\tfor (let i = 0; i < childCount; i++) {\n\t\t\tconst child = container.getChild(i);\n\t\t\tif (child === component) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif (child instanceof Container) {\n\t\t\t\tif (this.findInContainer(child, component)) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\trequestRender(): void {\n\t\tif (!this.isStarted) return;\n\n\t\t// Only queue a render if we haven't already\n\t\tif (!this.needsRender) {\n\t\t\tthis.needsRender = true;\n\t\t\tprocess.nextTick(() => {\n\t\t\t\tif (this.needsRender) {\n\t\t\t\t\tthis.renderToScreen();\n\t\t\t\t\tthis.needsRender = false;\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}\n\n\tstart(): void {\n\t\tthis.isStarted = true;\n\n\t\t// Hide cursor\n\t\tthis.terminal.write(\"\\x1b[?25l\");\n\n\t\t// Start terminal with handlers\n\t\ttry {\n\t\t\tthis.terminal.start(this.handleKeypress, this.handleResize);\n\t\t} catch (error) {\n\t\t\tconsole.error(\"Error starting terminal:\", error);\n\t\t}\n\n\t\t// Trigger initial render if we have components\n\t\tif (this.children.length > 0) {\n\t\t\tthis.requestRender();\n\t\t}\n\t}\n\n\tstop(): void {\n\t\t// Show cursor\n\t\tthis.terminal.write(\"\\x1b[?25h\");\n\n\t\t// Stop terminal\n\t\tthis.terminal.stop();\n\n\t\tthis.isStarted = false;\n\t}\n\n\tprivate renderToScreen(resize = false): void {\n\t\tconst termWidth = this.terminal.columns;\n\t\tconst termHeight = this.terminal.rows;\n\n\t\tif (resize) {\n\t\t\tthis.isFirstRender = true;\n\t\t\tthis.previousRenderCommands = [];\n\t\t\tthis.previousLines = [];\n\t\t}\n\n\t\t// Collect all render commands\n\t\tconst currentRenderCommands: RenderCommand[] = [];\n\t\tthis.collectRenderCommands(this, termWidth, currentRenderCommands);\n\n\t\tif (this.isFirstRender) {\n\t\t\tthis.renderInitial(currentRenderCommands);\n\t\t\tthis.isFirstRender = false;\n\t\t} else {\n\t\t\tthis.renderLineBased(currentRenderCommands, termHeight);\n\t\t}\n\n\t\t// Save for next render\n\t\tthis.previousRenderCommands = currentRenderCommands;\n\t\tthis.renderCount++;\n\t}\n\n\tprivate collectRenderCommands(container: Container, width: number, commands: RenderCommand[]): void {\n\t\tconst childCount = container.getChildCount();\n\n\t\tfor (let i = 0; i < childCount; i++) {\n\t\t\tconst child = container.getChild(i);\n\t\t\tif (!child) continue;\n\n\t\t\tconst result = child.render(width);\n\t\t\tcommands.push({\n\t\t\t\tid: child.id,\n\t\t\t\tlines: result.lines,\n\t\t\t\tchanged: result.changed,\n\t\t\t});\n\t\t}\n\t}\n\n\tprivate renderInitial(commands: RenderCommand[]): void {\n\t\tlet output = \"\";\n\t\tconst lines: string[] = [];\n\n\t\tfor (const command of commands) {\n\t\t\tlines.push(...command.lines);\n\t\t}\n\n\t\t// Output all lines\n\t\tfor (let i = 0; i < lines.length; i++) {\n\t\t\tif (i > 0) output += \"\\r\\n\";\n\t\t\toutput += lines[i];\n\t\t}\n\n\t\t// Add final newline to position cursor below content\n\t\tif (lines.length > 0) output += \"\\r\\n\";\n\n\t\tthis.terminal.write(output);\n\n\t\t// Save what we rendered\n\t\tthis.previousLines = lines;\n\t\tthis.totalLinesRedrawn += lines.length;\n\t}\n\n\tprivate renderLineBased(currentCommands: RenderCommand[], termHeight: number): void {\n\t\tconst viewportHeight = termHeight - 1; // Leave one line for cursor\n\n\t\t// Build the new lines array\n\t\tconst newLines: string[] = [];\n\t\tfor (const command of currentCommands) {\n\t\t\tnewLines.push(...command.lines);\n\t\t}\n\n\t\tconst totalNewLines = newLines.length;\n\t\tconst totalOldLines = this.previousLines.length;\n\n\t\t// Find first changed line by comparing old and new\n\t\tlet firstChangedLine = -1;\n\t\tconst minLines = Math.min(totalOldLines, totalNewLines);\n\n\t\tfor (let i = 0; i < minLines; i++) {\n\t\t\tif (this.previousLines[i] !== newLines[i]) {\n\t\t\t\tfirstChangedLine = i;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\t// If all common lines are the same, check if we have different lengths\n\t\tif (firstChangedLine === -1 && totalOldLines !== totalNewLines) {\n\t\t\tfirstChangedLine = minLines;\n\t\t}\n\n\t\t// No changes at all\n\t\tif (firstChangedLine === -1) {\n\t\t\tthis.previousLines = newLines;\n\t\t\treturn;\n\t\t}\n\n\t\t// Calculate viewport boundaries\n\t\tconst oldViewportStart = Math.max(0, totalOldLines - viewportHeight);\n\t\tconst cursorPosition = totalOldLines; // Cursor is one line below last content\n\n\t\tlet output = \"\";\n\t\tlet linesRedrawn = 0;\n\n\t\t// Check if change is in scrollback (unreachable by cursor)\n\t\tif (firstChangedLine < oldViewportStart) {\n\t\t\t// Must do full clear and re-render\n\t\t\toutput = \"\\x1b[3J\\x1b[H\"; // Clear scrollback and screen, home cursor\n\n\t\t\tfor (let i = 0; i < newLines.length; i++) {\n\t\t\t\tif (i > 0) output += \"\\r\\n\";\n\t\t\t\toutput += newLines[i];\n\t\t\t}\n\n\t\t\tif (newLines.length > 0) output += \"\\r\\n\";\n\t\t\tlinesRedrawn = newLines.length;\n\t\t} else {\n\t\t\t// Change is in viewport - we can reach it with cursor movements\n\t\t\t// Calculate viewport position of the change\n\t\t\tconst viewportChangePosition = firstChangedLine - oldViewportStart;\n\n\t\t\t// Move cursor to the change position\n\t\t\tconst linesToMoveUp = cursorPosition - oldViewportStart - viewportChangePosition;\n\t\t\tif (linesToMoveUp > 0) {\n\t\t\t\toutput += `\\x1b[${linesToMoveUp}A`;\n\t\t\t}\n\n\t\t\t// Now do surgical updates or partial clear based on what's more efficient\n\t\t\tlet currentLine = firstChangedLine;\n\t\t\tconst currentViewportLine = viewportChangePosition;\n\n\t\t\t// If we have significant structural changes, just clear and re-render from here\n\t\t\tconst hasSignificantChanges = totalNewLines !== totalOldLines || totalNewLines - firstChangedLine > 10; // Arbitrary threshold\n\n\t\t\tif (hasSignificantChanges) {\n\t\t\t\t// Clear from cursor to end of screen and render all remaining lines\n\t\t\t\toutput += \"\\r\\x1b[0J\";\n\n\t\t\t\tfor (let i = firstChangedLine; i < newLines.length; i++) {\n\t\t\t\t\tif (i > firstChangedLine) output += \"\\r\\n\";\n\t\t\t\t\toutput += newLines[i];\n\t\t\t\t\tlinesRedrawn++;\n\t\t\t\t}\n\n\t\t\t\tif (newLines.length > firstChangedLine) output += \"\\r\\n\";\n\t\t\t} else {\n\t\t\t\t// Do surgical line-by-line updates\n\t\t\t\tfor (let i = firstChangedLine; i < minLines; i++) {\n\t\t\t\t\tif (this.previousLines[i] !== newLines[i]) {\n\t\t\t\t\t\t// Move to this line if needed\n\t\t\t\t\t\tconst moveLines = i - currentLine;\n\t\t\t\t\t\tif (moveLines > 0) {\n\t\t\t\t\t\t\toutput += `\\x1b[${moveLines}B`;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Clear and rewrite the line\n\t\t\t\t\t\toutput += \"\\r\\x1b[2K\" + newLines[i];\n\t\t\t\t\t\tcurrentLine = i;\n\t\t\t\t\t\tlinesRedrawn++;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Handle added/removed lines at the end\n\t\t\t\tif (totalNewLines > totalOldLines) {\n\t\t\t\t\t// Move to end of old content and add new lines\n\t\t\t\t\tconst moveToEnd = totalOldLines - 1 - currentLine;\n\t\t\t\t\tif (moveToEnd > 0) {\n\t\t\t\t\t\toutput += `\\x1b[${moveToEnd}B`;\n\t\t\t\t\t}\n\t\t\t\t\toutput += \"\\r\\n\";\n\n\t\t\t\t\tfor (let i = totalOldLines; i < totalNewLines; i++) {\n\t\t\t\t\t\tif (i > totalOldLines) output += \"\\r\\n\";\n\t\t\t\t\t\toutput += newLines[i];\n\t\t\t\t\t\tlinesRedrawn++;\n\t\t\t\t\t}\n\t\t\t\t\toutput += \"\\r\\n\";\n\t\t\t\t} else if (totalNewLines < totalOldLines) {\n\t\t\t\t\t// Move to end of new content and clear rest\n\t\t\t\t\tconst moveToEnd = totalNewLines - 1 - currentLine;\n\t\t\t\t\tif (moveToEnd > 0) {\n\t\t\t\t\t\toutput += `\\x1b[${moveToEnd}B`;\n\t\t\t\t\t} else if (moveToEnd < 0) {\n\t\t\t\t\t\toutput += `\\x1b[${-moveToEnd}A`;\n\t\t\t\t\t}\n\t\t\t\t\toutput += \"\\r\\n\\x1b[0J\";\n\t\t\t\t} else {\n\t\t\t\t\t// Same length, just position cursor at end\n\t\t\t\t\tconst moveToEnd = totalNewLines - 1 - currentLine;\n\t\t\t\t\tif (moveToEnd > 0) {\n\t\t\t\t\t\toutput += `\\x1b[${moveToEnd}B`;\n\t\t\t\t\t} else if (moveToEnd < 0) {\n\t\t\t\t\t\toutput += `\\x1b[${-moveToEnd}A`;\n\t\t\t\t\t}\n\t\t\t\t\toutput += \"\\r\\n\";\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tthis.terminal.write(output);\n\t\tthis.previousLines = newLines;\n\t\tthis.totalLinesRedrawn += linesRedrawn;\n\t}\n\n\tprivate handleResize(): void {\n\t\t// Clear screen and reset\n\t\tthis.terminal.write(\"\\x1b[2J\\x1b[H\\x1b[?25l\");\n\t\tthis.renderToScreen(true);\n\t}\n\n\tprivate handleKeypress(data: string): void {\n\t\tif (this.onGlobalKeyPress) {\n\t\t\tconst shouldForward = this.onGlobalKeyPress(data);\n\t\t\tif (!shouldForward) {\n\t\t\t\tthis.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tif (this.focusedComponent?.handleInput) {\n\t\t\tthis.focusedComponent.handleInput(data);\n\t\t\tthis.requestRender();\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"tui.js","sourceRoot":"","sources":["../src/tui.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Minimal TUI implementation with differential rendering\n */\n\nimport type { Terminal } from \"./terminal.js\";\nimport { visibleWidth } from \"./utils.js\";\n\n/**\n * Component interface - all components must implement this\n */\nexport interface Component {\n\t/**\n\t * Render the component to lines for the given viewport width\n\t * @param width - Current viewport width\n\t * @returns Array of strings, each representing a line\n\t */\n\trender(width: number): string[];\n\n\t/**\n\t * Optional handler for keyboard input when component has focus\n\t */\n\thandleInput?(data: string): void;\n}\n\nexport { visibleWidth };\n\n/**\n * Container - a component that contains other components\n */\nexport class Container implements Component {\n\tchildren: Component[] = [];\n\n\taddChild(component: Component): void {\n\t\tthis.children.push(component);\n\t}\n\n\tremoveChild(component: Component): void {\n\t\tconst index = this.children.indexOf(component);\n\t\tif (index !== -1) {\n\t\t\tthis.children.splice(index, 1);\n\t\t}\n\t}\n\n\tclear(): void {\n\t\tthis.children = [];\n\t}\n\n\trender(width: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tfor (const child of this.children) {\n\t\t\tlines.push(...child.render(width));\n\t\t}\n\t\treturn lines;\n\t}\n}\n\n/**\n * TUI - Main class for managing terminal UI with differential rendering\n */\nexport class TUI extends Container {\n\tprivate terminal: Terminal;\n\tprivate previousLines: string[] = [];\n\tprivate previousWidth = 0;\n\tprivate focusedComponent: Component | null = null;\n\tprivate renderRequested = false;\n\tprivate cursorRow = 0; // Track where cursor is (0-indexed, relative to our first line)\n\n\tconstructor(terminal: Terminal) {\n\t\tsuper();\n\t\tthis.terminal = terminal;\n\t}\n\n\tsetFocus(component: Component | null): void {\n\t\tthis.focusedComponent = component;\n\t}\n\n\tstart(): void {\n\t\tthis.terminal.start(\n\t\t\t(data) => this.handleInput(data),\n\t\t\t() => this.requestRender(),\n\t\t);\n\t\tthis.terminal.hideCursor();\n\t\tthis.requestRender();\n\t}\n\n\tstop(): void {\n\t\tthis.terminal.showCursor();\n\t\tthis.terminal.stop();\n\t}\n\n\trequestRender(): void {\n\t\tif (this.renderRequested) return;\n\t\tthis.renderRequested = true;\n\t\tprocess.nextTick(() => {\n\t\t\tthis.renderRequested = false;\n\t\t\tthis.doRender();\n\t\t});\n\t}\n\n\tprivate handleInput(data: string): void {\n\t\t// Pass input to focused component (including Ctrl+C)\n\t\t// The focused component can decide how to handle Ctrl+C\n\t\tif (this.focusedComponent?.handleInput) {\n\t\t\tthis.focusedComponent.handleInput(data);\n\t\t\tthis.requestRender();\n\t\t}\n\t}\n\n\tprivate doRender(): void {\n\t\tconst width = this.terminal.columns;\n\t\tconst height = this.terminal.rows;\n\n\t\t// Render all components to get new lines\n\t\tconst newLines = this.render(width);\n\n\t\t// Width changed - need full re-render\n\t\tconst widthChanged = this.previousWidth !== 0 && this.previousWidth !== width;\n\n\t\t// First render - just output everything without clearing\n\t\tif (this.previousLines.length === 0) {\n\t\t\tlet buffer = \"\\x1b[?2026h\"; // Begin synchronized output\n\t\t\tfor (let i = 0; i < newLines.length; i++) {\n\t\t\t\tif (i > 0) buffer += \"\\r\\n\";\n\t\t\t\tbuffer += newLines[i];\n\t\t\t}\n\t\t\tbuffer += \"\\x1b[?2026l\"; // End synchronized output\n\t\t\tthis.terminal.write(buffer);\n\t\t\t// After rendering N lines, cursor is at end of last line (line N-1)\n\t\t\tthis.cursorRow = newLines.length - 1;\n\t\t\tthis.previousLines = newLines;\n\t\t\tthis.previousWidth = width;\n\t\t\treturn;\n\t\t}\n\n\t\t// Width changed - full re-render\n\t\tif (widthChanged) {\n\t\t\tlet buffer = \"\\x1b[?2026h\"; // Begin synchronized output\n\t\t\tbuffer += \"\\x1b[3J\\x1b[2J\\x1b[H\"; // Clear scrollback, screen, and home\n\t\t\tfor (let i = 0; i < newLines.length; i++) {\n\t\t\t\tif (i > 0) buffer += \"\\r\\n\";\n\t\t\t\tbuffer += newLines[i];\n\t\t\t}\n\t\t\tbuffer += \"\\x1b[?2026l\"; // End synchronized output\n\t\t\tthis.terminal.write(buffer);\n\t\t\tthis.cursorRow = newLines.length - 1;\n\t\t\tthis.previousLines = newLines;\n\t\t\tthis.previousWidth = width;\n\t\t\treturn;\n\t\t}\n\n\t\t// Find first and last changed lines\n\t\tlet firstChanged = -1;\n\t\tlet lastChanged = -1;\n\n\t\tconst maxLines = Math.max(newLines.length, this.previousLines.length);\n\t\tfor (let i = 0; i < maxLines; i++) {\n\t\t\tconst oldLine = i < this.previousLines.length ? this.previousLines[i] : \"\";\n\t\t\tconst newLine = i < newLines.length ? newLines[i] : \"\";\n\n\t\t\tif (oldLine !== newLine) {\n\t\t\t\tif (firstChanged === -1) {\n\t\t\t\t\tfirstChanged = i;\n\t\t\t\t}\n\t\t\t\tlastChanged = i;\n\t\t\t}\n\t\t}\n\n\t\t// No changes\n\t\tif (firstChanged === -1) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Check if firstChanged is outside the viewport\n\t\t// cursorRow is the line where cursor is (0-indexed)\n\t\t// Viewport shows lines from (cursorRow - height + 1) to cursorRow\n\t\t// If firstChanged < viewportTop, we need full re-render\n\t\tconst viewportTop = this.cursorRow - height + 1;\n\t\tif (firstChanged < viewportTop) {\n\t\t\t// First change is above viewport - need full re-render\n\t\t\tlet buffer = \"\\x1b[?2026h\"; // Begin synchronized output\n\t\t\tbuffer += \"\\x1b[3J\\x1b[2J\\x1b[H\"; // Clear scrollback, screen, and home\n\t\t\tfor (let i = 0; i < newLines.length; i++) {\n\t\t\t\tif (i > 0) buffer += \"\\r\\n\";\n\t\t\t\tbuffer += newLines[i];\n\t\t\t}\n\t\t\tbuffer += \"\\x1b[?2026l\"; // End synchronized output\n\t\t\tthis.terminal.write(buffer);\n\t\t\tthis.cursorRow = newLines.length - 1;\n\t\t\tthis.previousLines = newLines;\n\t\t\tthis.previousWidth = width;\n\t\t\treturn;\n\t\t}\n\n\t\t// Render from first changed line to end\n\t\t// Build buffer with all updates wrapped in synchronized output\n\t\tlet buffer = \"\\x1b[?2026h\"; // Begin synchronized output\n\n\t\t// Move cursor to first changed line\n\t\tconst lineDiff = firstChanged - this.cursorRow;\n\t\tif (lineDiff > 0) {\n\t\t\tbuffer += `\\x1b[${lineDiff}B`; // Move down\n\t\t} else if (lineDiff < 0) {\n\t\t\tbuffer += `\\x1b[${-lineDiff}A`; // Move up\n\t\t}\n\n\t\tbuffer += \"\\r\"; // Move to column 0\n\t\tbuffer += \"\\x1b[J\"; // Clear from cursor to end of screen\n\n\t\t// Render from first changed line to end\n\t\tfor (let i = firstChanged; i < newLines.length; i++) {\n\t\t\tif (i > firstChanged) buffer += \"\\r\\n\";\n\t\t\tif (visibleWidth(newLines[i]) > width) {\n\t\t\t\tthrow new Error(`Rendered line ${i} exceeds terminal width\\n\\n${newLines[i]}`);\n\t\t\t}\n\t\t\tbuffer += newLines[i];\n\t\t}\n\n\t\tbuffer += \"\\x1b[?2026l\"; // End synchronized output\n\n\t\t// Write entire buffer at once\n\t\tthis.terminal.write(buffer);\n\n\t\t// Cursor is now at end of last line\n\t\tthis.cursorRow = newLines.length - 1;\n\n\t\tthis.previousLines = newLines;\n\t\tthis.previousWidth = width;\n\t}\n}\n"]}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calculate the visible width of a string in terminal columns.
|
|
3
|
+
* This correctly handles:
|
|
4
|
+
* - ANSI escape codes (ignored)
|
|
5
|
+
* - Emojis and wide characters (counted as 2 columns)
|
|
6
|
+
* - Combining characters (counted correctly)
|
|
7
|
+
* - Tabs (replaced with 3 spaces for consistent width)
|
|
8
|
+
*/
|
|
9
|
+
export declare function visibleWidth(str: string): number;
|
|
10
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAIhD","sourcesContent":["import stringWidth from \"string-width\";\n\n/**\n * Calculate the visible width of a string in terminal columns.\n * This correctly handles:\n * - ANSI escape codes (ignored)\n * - Emojis and wide characters (counted as 2 columns)\n * - Combining characters (counted correctly)\n * - Tabs (replaced with 3 spaces for consistent width)\n */\nexport function visibleWidth(str: string): number {\n\t// Replace tabs with 3 spaces before measuring\n\tconst normalized = str.replace(/\\t/g, \" \");\n\treturn stringWidth(normalized);\n}\n"]}
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import stringWidth from "string-width";
|
|
2
|
+
/**
|
|
3
|
+
* Calculate the visible width of a string in terminal columns.
|
|
4
|
+
* This correctly handles:
|
|
5
|
+
* - ANSI escape codes (ignored)
|
|
6
|
+
* - Emojis and wide characters (counted as 2 columns)
|
|
7
|
+
* - Combining characters (counted correctly)
|
|
8
|
+
* - Tabs (replaced with 3 spaces for consistent width)
|
|
9
|
+
*/
|
|
10
|
+
export function visibleWidth(str) {
|
|
11
|
+
// Replace tabs with 3 spaces before measuring
|
|
12
|
+
const normalized = str.replace(/\t/g, " ");
|
|
13
|
+
return stringWidth(normalized);
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,WAAW,MAAM,cAAc,CAAC;AAEvC;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,GAAW,EAAU;IACjD,8CAA8C;IAC9C,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IAC7C,OAAO,WAAW,CAAC,UAAU,CAAC,CAAC;AAAA,CAC/B","sourcesContent":["import stringWidth from \"string-width\";\n\n/**\n * Calculate the visible width of a string in terminal columns.\n * This correctly handles:\n * - ANSI escape codes (ignored)\n * - Emojis and wide characters (counted as 2 columns)\n * - Combining characters (counted correctly)\n * - Tabs (replaced with 3 spaces for consistent width)\n */\nexport function visibleWidth(str: string): number {\n\t// Replace tabs with 3 spaces before measuring\n\tconst normalized = str.replace(/\\t/g, \" \");\n\treturn stringWidth(normalized);\n}\n"]}
|