@oh-my-pi/pi-tui 3.15.0 → 3.20.0

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/src/terminal.ts CHANGED
@@ -51,6 +51,9 @@ export interface Terminal {
51
51
  clearLine(): void; // Clear current line
52
52
  clearFromCursor(): void; // Clear from cursor to end of screen
53
53
  clearScreen(): void; // Clear entire screen and move cursor to (0,0)
54
+
55
+ // Title operations
56
+ setTitle(title: string): void; // Set terminal window title
54
57
  }
55
58
 
56
59
  /**
@@ -160,4 +163,9 @@ export class ProcessTerminal implements Terminal {
160
163
  clearScreen(): void {
161
164
  process.stdout.write("\x1b[2J\x1b[H"); // Clear screen and move to home (1,1)
162
165
  }
166
+
167
+ setTitle(title: string): void {
168
+ // OSC 0;title BEL - set terminal window title
169
+ process.stdout.write(`\x1b]0;${title}\x07`);
170
+ }
163
171
  }
package/src/tui.ts CHANGED
@@ -26,6 +26,11 @@ export interface Component {
26
26
  */
27
27
  handleInput?(data: string): void;
28
28
 
29
+ /**
30
+ * Optional cursor position within the rendered output (0-based row/col).
31
+ */
32
+ getCursorPosition?(width: number): { row: number; col: number } | null;
33
+
29
34
  /**
30
35
  * Invalidate any cached rendering state.
31
36
  * Called when theme changes or when component needs to re-render from scratch.
@@ -62,6 +67,19 @@ export class Container implements Component {
62
67
  }
63
68
  }
64
69
 
70
+ getCursorPosition(width: number): { row: number; col: number } | null {
71
+ let rowOffset = 0;
72
+ for (const child of this.children) {
73
+ const lines = child.render(width);
74
+ const childCursor = child.getCursorPosition?.(width) ?? null;
75
+ if (childCursor) {
76
+ return { row: rowOffset + childCursor.row, col: childCursor.col };
77
+ }
78
+ rowOffset += lines.length;
79
+ }
80
+ return null;
81
+ }
82
+
65
83
  render(width: number): string[] {
66
84
  const lines: string[] = [];
67
85
  for (const child of this.children) {
@@ -84,6 +102,7 @@ export class TUI extends Container {
84
102
  public onDebug?: () => void;
85
103
  private renderRequested = false;
86
104
  private cursorRow = 0; // Track where cursor is (0-indexed, relative to our first line)
105
+ private previousCursor: { row: number; col: number } | null = null;
87
106
  private inputBuffer = ""; // Buffer for parsing terminal responses
88
107
  private cellSizeQueryPending = false;
89
108
  private inputQueue: string[] = []; // Queue input during cell size query to avoid interleaving
@@ -132,6 +151,7 @@ export class TUI extends Container {
132
151
  this.previousLines = [];
133
152
  this.previousWidth = 0;
134
153
  this.cursorRow = 0;
154
+ this.previousCursor = null;
135
155
  }
136
156
  if (this.renderRequested) return;
137
157
  this.renderRequested = true;
@@ -141,6 +161,42 @@ export class TUI extends Container {
141
161
  });
142
162
  }
143
163
 
164
+ private areCursorsEqual(
165
+ left: { row: number; col: number } | null,
166
+ right: { row: number; col: number } | null,
167
+ ): boolean {
168
+ if (!left && !right) return true;
169
+ if (!left || !right) return false;
170
+ return left.row === right.row && left.col === right.col;
171
+ }
172
+
173
+ private updateHardwareCursor(
174
+ width: number,
175
+ totalLines: number,
176
+ cursor: { row: number; col: number } | null,
177
+ currentCursorRow: number,
178
+ ): void {
179
+ if (!cursor || totalLines <= 0) {
180
+ this.terminal.hideCursor();
181
+ return;
182
+ }
183
+
184
+ const targetRow = Math.max(0, Math.min(cursor.row, totalLines - 1));
185
+ const targetCol = Math.max(0, Math.min(cursor.col, width - 1));
186
+ const rowDelta = targetRow - currentCursorRow;
187
+
188
+ let buffer = "";
189
+ if (rowDelta > 0) {
190
+ buffer += `\x1b[${rowDelta}B`;
191
+ } else if (rowDelta < 0) {
192
+ buffer += `\x1b[${-rowDelta}A`;
193
+ }
194
+ buffer += `\r\x1b[${targetCol + 1}G`;
195
+ this.terminal.write(buffer);
196
+ this.cursorRow = targetRow;
197
+ this.terminal.showCursor();
198
+ }
199
+
144
200
  private handleInput(data: string): void {
145
201
  // If we're waiting for cell size response, buffer input and parse
146
202
  if (this.cellSizeQueryPending) {
@@ -235,6 +291,7 @@ export class TUI extends Container {
235
291
 
236
292
  // Render all components to get new lines
237
293
  const newLines = this.render(width);
294
+ const cursorInfo = this.getCursorPosition(width);
238
295
 
239
296
  // Width changed - need full re-render
240
297
  const widthChanged = this.previousWidth !== 0 && this.previousWidth !== width;
@@ -250,6 +307,8 @@ export class TUI extends Container {
250
307
  this.terminal.write(buffer);
251
308
  // After rendering N lines, cursor is at end of last line (line N-1)
252
309
  this.cursorRow = newLines.length - 1;
310
+ this.updateHardwareCursor(width, newLines.length, cursorInfo, this.cursorRow);
311
+ this.previousCursor = cursorInfo;
253
312
  this.previousLines = newLines;
254
313
  this.previousWidth = width;
255
314
  return;
@@ -266,6 +325,8 @@ export class TUI extends Container {
266
325
  buffer += "\x1b[?2026l"; // End synchronized output
267
326
  this.terminal.write(buffer);
268
327
  this.cursorRow = newLines.length - 1;
328
+ this.updateHardwareCursor(width, newLines.length, cursorInfo, this.cursorRow);
329
+ this.previousCursor = cursorInfo;
269
330
  this.previousLines = newLines;
270
331
  this.previousWidth = width;
271
332
  return;
@@ -287,6 +348,10 @@ export class TUI extends Container {
287
348
 
288
349
  // No changes
289
350
  if (firstChanged === -1) {
351
+ if (!this.areCursorsEqual(cursorInfo, this.previousCursor)) {
352
+ this.updateHardwareCursor(width, newLines.length, cursorInfo, currentCursorRow);
353
+ this.previousCursor = cursorInfo;
354
+ }
290
355
  return;
291
356
  }
292
357
 
@@ -306,6 +371,8 @@ export class TUI extends Container {
306
371
  buffer += "\x1b[?2026l"; // End synchronized output
307
372
  this.terminal.write(buffer);
308
373
  this.cursorRow = newLines.length - 1;
374
+ this.updateHardwareCursor(width, newLines.length, cursorInfo, this.cursorRow);
375
+ this.previousCursor = cursorInfo;
309
376
  this.previousLines = newLines;
310
377
  this.previousWidth = width;
311
378
  return;
@@ -372,6 +439,8 @@ export class TUI extends Container {
372
439
 
373
440
  // Cursor is now at end of last line
374
441
  this.cursorRow = newLines.length - 1;
442
+ this.updateHardwareCursor(width, newLines.length, cursorInfo, this.cursorRow);
443
+ this.previousCursor = cursorInfo;
375
444
 
376
445
  this.previousLines = newLines;
377
446
  this.previousWidth = width;