@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.
Files changed (47) hide show
  1. package/dist/components/loading-animation.d.ts +19 -0
  2. package/dist/components/loading-animation.d.ts.map +1 -0
  3. package/dist/components/loading-animation.js +44 -0
  4. package/dist/components/loading-animation.js.map +1 -0
  5. package/dist/{markdown-component.d.ts → components/markdown-component.d.ts} +2 -1
  6. package/dist/components/markdown-component.d.ts.map +1 -0
  7. package/dist/{markdown-component.js → components/markdown-component.js} +2 -0
  8. package/dist/components/markdown-component.js.map +1 -0
  9. package/dist/{select-list.d.ts → components/select-list.d.ts} +2 -1
  10. package/dist/components/select-list.d.ts.map +1 -0
  11. package/dist/{select-list.js → components/select-list.js} +2 -0
  12. package/dist/components/select-list.js.map +1 -0
  13. package/dist/{text-component.d.ts → components/text-component.d.ts} +2 -1
  14. package/dist/components/text-component.d.ts.map +1 -0
  15. package/dist/{text-component.js → components/text-component.js} +2 -0
  16. package/dist/components/text-component.js.map +1 -0
  17. package/dist/{text-editor.d.ts → components/text-editor.d.ts} +3 -2
  18. package/dist/components/text-editor.d.ts.map +1 -0
  19. package/dist/{text-editor.js → components/text-editor.js} +4 -2
  20. package/dist/components/text-editor.js.map +1 -0
  21. package/dist/{whitespace-component.d.ts → components/whitespace-component.d.ts} +2 -1
  22. package/dist/components/whitespace-component.d.ts.map +1 -0
  23. package/dist/{whitespace-component.js → components/whitespace-component.js} +2 -0
  24. package/dist/components/whitespace-component.js.map +1 -0
  25. package/dist/index.d.ts +8 -6
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +12 -8
  28. package/dist/index.js.map +1 -1
  29. package/dist/terminal.d.ts +24 -0
  30. package/dist/terminal.d.ts.map +1 -0
  31. package/dist/terminal.js +47 -0
  32. package/dist/terminal.js.map +1 -0
  33. package/dist/tui.d.ts +43 -28
  34. package/dist/tui.d.ts.map +1 -1
  35. package/dist/tui.js +268 -251
  36. package/dist/tui.js.map +1 -1
  37. package/package.json +6 -2
  38. package/dist/markdown-component.d.ts.map +0 -1
  39. package/dist/markdown-component.js.map +0 -1
  40. package/dist/select-list.d.ts.map +0 -1
  41. package/dist/select-list.js.map +0 -1
  42. package/dist/text-component.d.ts.map +0 -1
  43. package/dist/text-component.js.map +0 -1
  44. package/dist/text-editor.d.ts.map +0 -1
  45. package/dist/text-editor.js.map +0 -1
  46. package/dist/whitespace-component.d.ts.map +0 -1
  47. 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
- // Sentinel component used to mark removed components - triggers cascade rendering
5
- class SentinelComponent {
6
- render() {
7
- return {
8
- lines: [],
9
- changed: true, // Always trigger cascade
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
- // Base Container class that manages child components
10
+ /**
11
+ * Container for managing child components
12
+ */
14
13
  export class Container {
14
+ id;
15
15
  children = [];
16
- lines = [];
17
- parentTui; // Reference to parent TUI for triggering re-renders
18
- constructor(parentTui) {
19
- this.parentTui = parentTui;
16
+ tui;
17
+ constructor() {
18
+ this.id = getNextComponentId();
20
19
  }
21
- setParentTui(tui) {
22
- this.parentTui = tui;
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
- // Set parent TUI reference for nested containers
27
- if (component instanceof Container && this.parentTui) {
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
- // Replace with sentinel instead of splicing to maintain array structure
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.setParentTui(undefined);
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
- // Replace with sentinel instead of splicing to maintain array structure
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.setParentTui(undefined);
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
- render(width) {
73
- let keepLines = 0;
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
- const result = child.render(width);
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.lines = newLines;
111
- return {
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
- // Clear all children from the container
129
- clear() {
130
- // Clear parent TUI references for nested containers
70
+ render(width) {
71
+ const lines = [];
72
+ let changed = false;
131
73
  for (const child of this.children) {
132
- if (child instanceof Container) {
133
- child.setParentTui(undefined);
74
+ const result = child.render(width);
75
+ lines.push(...result.lines);
76
+ if (result.changed) {
77
+ changed = true;
134
78
  }
135
79
  }
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();
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
- constructor() {
179
- super(); // No parent TUI for root
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
- // Recursively search in containers
215
- for (const comp of this.children) {
216
- if (comp instanceof Container) {
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
- this.needsRender = true;
248
- // Batch renders on next tick
249
- process.nextTick(() => {
250
- if (this.needsRender) {
251
- this.renderToScreen();
252
- this.needsRender = false;
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 the terminal cursor
260
- process.stdout.write("\x1b[?25l");
261
- // Set up raw mode for key capture
167
+ // Hide cursor
168
+ this.terminal.write("\x1b[?25l");
169
+ // Start terminal with handlers
262
170
  try {
263
- this.wasRaw = process.stdin.isRaw || false;
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 setting up raw mode:", 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 the terminal cursor again
281
- process.stdout.write("\x1b[?25h");
282
- process.stdin.removeListener("data", this.handleKeypress);
283
- process.stdout.removeListener("resize", this.handleResize);
284
- if (process.stdin.setRawMode) {
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 = process.stdout.columns || 80;
290
- logger.debug("TUI", "Starting render cycle", {
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
- logger.debug("TUI", "Render result", {
302
- totalLines: result.lines.length,
303
- keepLines: result.keepLines,
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
- // First render: just append to current terminal position
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
- // Move cursor up to start of changing content and clear down
322
- const linesToMoveUp = this.totalLines - result.keepLines;
323
- let output = "";
324
- logger.debug("TUI", "Cursor movement", {
325
- linesToMoveUp,
326
- totalLines: this.totalLines,
327
- keepLines: result.keepLines,
328
- changingLineCount: result.lines.length - result.keepLines,
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
- if (linesToMoveUp > 0) {
331
- output += `\x1b[${linesToMoveUp}A\x1b[0J`;
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
- // Build the output string for all changing lines
334
- const changingLines = result.lines.slice(result.keepLines);
335
- logger.debug("TUI", "Output details", {
336
- linesToMoveUp,
337
- changingLinesCount: changingLines.length,
338
- keepLines: result.keepLines,
339
- totalLines: result.lines.length,
340
- previousTotalLines: this.totalLines,
341
- });
342
- for (const line of changingLines) {
343
- output += `${line}\n`;
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
- // Write everything at once - use synchronous write to prevent race conditions
346
- writeSync(process.stdout.fd, output);
289
+ currentLineOffset += current.lines.length;
347
290
  }
348
- this.totalLines = result.lines.length;
349
- // Clean up sentinels after rendering
350
- this.cleanupSentinels();
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, hide cursor, and reset color
354
- process.stdout.write("\u001Bc\x1b[?25l\u001B[3J");
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 {