@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.
Files changed (55) hide show
  1. package/README.md +238 -314
  2. package/dist/autocomplete.d.ts.map +1 -1
  3. package/dist/autocomplete.js +0 -34
  4. package/dist/autocomplete.js.map +1 -1
  5. package/dist/components/loading-animation.d.ts +19 -0
  6. package/dist/components/loading-animation.d.ts.map +1 -0
  7. package/dist/components/loading-animation.js +44 -0
  8. package/dist/components/loading-animation.js.map +1 -0
  9. package/dist/{markdown-component.d.ts → components/markdown-component.d.ts} +2 -1
  10. package/dist/components/markdown-component.d.ts.map +1 -0
  11. package/dist/{markdown-component.js → components/markdown-component.js} +29 -6
  12. package/dist/components/markdown-component.js.map +1 -0
  13. package/dist/{select-list.d.ts → components/select-list.d.ts} +2 -1
  14. package/dist/components/select-list.d.ts.map +1 -0
  15. package/dist/{select-list.js → components/select-list.js} +2 -0
  16. package/dist/components/select-list.js.map +1 -0
  17. package/dist/{text-component.d.ts → components/text-component.d.ts} +2 -1
  18. package/dist/components/text-component.d.ts.map +1 -0
  19. package/dist/{text-component.js → components/text-component.js} +2 -0
  20. package/dist/components/text-component.js.map +1 -0
  21. package/dist/{text-editor.d.ts → components/text-editor.d.ts} +3 -2
  22. package/dist/components/text-editor.d.ts.map +1 -0
  23. package/dist/{text-editor.js → components/text-editor.js} +3 -82
  24. package/dist/components/text-editor.js.map +1 -0
  25. package/dist/{whitespace-component.d.ts → components/whitespace-component.d.ts} +2 -1
  26. package/dist/components/whitespace-component.d.ts.map +1 -0
  27. package/dist/{whitespace-component.js → components/whitespace-component.js} +2 -0
  28. package/dist/components/whitespace-component.js.map +1 -0
  29. package/dist/index.d.ts +8 -7
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +10 -8
  32. package/dist/index.js.map +1 -1
  33. package/dist/terminal.d.ts +24 -0
  34. package/dist/terminal.d.ts.map +1 -0
  35. package/dist/terminal.js +47 -0
  36. package/dist/terminal.js.map +1 -0
  37. package/dist/tui.d.ts +44 -30
  38. package/dist/tui.d.ts.map +1 -1
  39. package/dist/tui.js +269 -267
  40. package/dist/tui.js.map +1 -1
  41. package/package.json +6 -2
  42. package/dist/logger.d.ts +0 -23
  43. package/dist/logger.d.ts.map +0 -1
  44. package/dist/logger.js +0 -83
  45. package/dist/logger.js.map +0 -1
  46. package/dist/markdown-component.d.ts.map +0 -1
  47. package/dist/markdown-component.js.map +0 -1
  48. package/dist/select-list.d.ts.map +0 -1
  49. package/dist/select-list.js.map +0 -1
  50. package/dist/text-component.d.ts.map +0 -1
  51. package/dist/text-component.js.map +0 -1
  52. package/dist/text-editor.d.ts.map +0 -1
  53. package/dist/text-editor.js.map +0 -1
  54. package/dist/whitespace-component.d.ts.map +0 -1
  55. 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 { 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
- }
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
- // Base Container class that manages child components
9
+ /**
10
+ * Container for managing child components
11
+ */
14
12
  export class Container {
13
+ id;
15
14
  children = [];
16
- lines = [];
17
- parentTui; // Reference to parent TUI for triggering re-renders
18
- constructor(parentTui) {
19
- this.parentTui = parentTui;
15
+ tui;
16
+ previousChildCount = 0;
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
131
- for (const child of this.children) {
132
- if (child instanceof Container) {
133
- child.setParentTui(undefined);
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
- 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++;
79
+ const result = child.render(width);
80
+ lines.push(...result.lines);
81
+ if (result.changed) {
82
+ changed = true;
158
83
  }
159
84
  }
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
- }
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
- constructor() {
179
- super(); // No parent TUI for root
180
- this.handleResize = this.handleResize.bind(this);
181
- this.handleKeypress = this.handleKeypress.bind(this);
182
- logger.componentLifecycle("TUI", "created");
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
- configureLogging(config) {
185
- logger.configure(config);
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
- removeChild(component) {
200
- super.removeChild(component);
201
- this.requestRender();
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
- // 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,148 +151,243 @@ 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);
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 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
- }
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 = 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);
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
- 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
196
+ // Collect all render commands
197
+ const currentRenderCommands = [];
198
+ this.collectRenderCommands(this, termWidth, currentRenderCommands);
312
199
  if (this.isFirstRender) {
313
- // First render: just append to current terminal position
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
- // 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,
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\x1b[0J`;
296
+ output += `\x1b[${linesToMoveUp}A`;
332
297
  }
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`;
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.totalLines = result.lines.length;
349
- // Clean up sentinels after rendering
350
- this.cleanupSentinels();
370
+ this.terminal.write(output);
371
+ this.previousLines = newLines;
372
+ this.totalLinesRedrawn += linesRedrawn;
351
373
  }
352
374
  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
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