@oh-my-pi/pi-tui 3.14.0 → 3.15.1

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 CHANGED
@@ -154,9 +154,52 @@ input.getValue();
154
154
  Multi-line text editor with autocomplete, file completion, and paste handling.
155
155
 
156
156
  ```typescript
157
+ interface SymbolTheme {
158
+ cursor: string;
159
+ ellipsis: string;
160
+ boxRound: {
161
+ topLeft: string;
162
+ topRight: string;
163
+ bottomLeft: string;
164
+ bottomRight: string;
165
+ horizontal: string;
166
+ vertical: string;
167
+ };
168
+ boxSharp: {
169
+ topLeft: string;
170
+ topRight: string;
171
+ bottomLeft: string;
172
+ bottomRight: string;
173
+ horizontal: string;
174
+ vertical: string;
175
+ teeDown: string;
176
+ teeUp: string;
177
+ teeLeft: string;
178
+ teeRight: string;
179
+ cross: string;
180
+ };
181
+ table: {
182
+ topLeft: string;
183
+ topRight: string;
184
+ bottomLeft: string;
185
+ bottomRight: string;
186
+ horizontal: string;
187
+ vertical: string;
188
+ teeDown: string;
189
+ teeUp: string;
190
+ teeLeft: string;
191
+ teeRight: string;
192
+ cross: string;
193
+ };
194
+ quoteBorder: string;
195
+ hrChar: string;
196
+ spinnerFrames: string[];
197
+ }
198
+
157
199
  interface EditorTheme {
158
200
  borderColor: (str: string) => string;
159
201
  selectList: SelectListTheme;
202
+ symbols: SymbolTheme;
160
203
  }
161
204
 
162
205
  const editor = new Editor(theme);
@@ -206,6 +249,7 @@ interface MarkdownTheme {
206
249
  strikethrough: (text: string) => string;
207
250
  underline: (text: string) => string;
208
251
  highlightCode?: (code: string, lang?: string) => string[];
252
+ symbols: SymbolTheme;
209
253
  }
210
254
 
211
255
  interface DefaultTextStyle {
@@ -289,6 +333,7 @@ interface SelectListTheme {
289
333
  description: (text: string) => string;
290
334
  scrollInfo: (text: string) => string;
291
335
  noMatch: (text: string) => string;
336
+ symbols: SymbolTheme;
292
337
  }
293
338
 
294
339
  const list = new SelectList(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-tui",
3
- "version": "3.14.0",
3
+ "version": "3.15.1",
4
4
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -25,6 +25,7 @@ import {
25
25
  isShiftEnter,
26
26
  isTab,
27
27
  } from "../keys";
28
+ import type { SymbolTheme } from "../symbols";
28
29
  import type { Component } from "../tui";
29
30
  import { getSegmenter, isPunctuationChar, isWhitespaceChar, truncateToWidth, visibleWidth } from "../utils";
30
31
  import { SelectList, type SelectListTheme } from "./select-list";
@@ -46,6 +47,7 @@ interface LayoutLine {
46
47
  export interface EditorTheme {
47
48
  borderColor: (str: string) => string;
48
49
  selectList: SelectListTheme;
50
+ symbols: SymbolTheme;
49
51
  }
50
52
 
51
53
  export interface EditorTopBorder {
@@ -63,6 +65,7 @@ export class Editor implements Component {
63
65
  };
64
66
 
65
67
  private theme: EditorTheme;
68
+ private useTerminalCursor = false;
66
69
 
67
70
  // Store last render width for cursor navigation
68
71
  private lastWidth: number = 80;
@@ -112,6 +115,13 @@ export class Editor implements Component {
112
115
  this.topBorderContent = content;
113
116
  }
114
117
 
118
+ /**
119
+ * Use the real terminal cursor instead of rendering a cursor glyph.
120
+ */
121
+ setUseTerminalCursor(useTerminalCursor: boolean): void {
122
+ this.useTerminalCursor = useTerminalCursor;
123
+ }
124
+
115
125
  /**
116
126
  * Add a prompt to history for up/down arrow navigation.
117
127
  * Called after successful submission.
@@ -181,11 +191,12 @@ export class Editor implements Component {
181
191
  this.lastWidth = width;
182
192
 
183
193
  // Box-drawing characters for rounded corners
184
- const topLeft = this.borderColor("╭─");
185
- const topRight = this.borderColor("─╮");
186
- const bottomLeft = this.borderColor("╰─");
187
- const bottomRight = this.borderColor("─╯");
188
- const horizontal = this.borderColor("─");
194
+ const box = this.theme.symbols.boxRound;
195
+ const topLeft = this.borderColor(`${box.topLeft}${box.horizontal}`);
196
+ const topRight = this.borderColor(`${box.horizontal}${box.topRight}`);
197
+ const bottomLeft = this.borderColor(`${box.bottomLeft}${box.horizontal}`);
198
+ const bottomRight = this.borderColor(`${box.horizontal}${box.bottomRight}`);
199
+ const horizontal = this.borderColor(box.horizontal);
189
200
 
190
201
  // Layout the text - content area is width minus 6 for borders (3 left + 3 right)
191
202
  const contentAreaWidth = width - 6;
@@ -201,13 +212,13 @@ export class Editor implements Component {
201
212
  if (statusWidth <= topFillWidth) {
202
213
  // Status fits - add fill after it
203
214
  const fillWidth = topFillWidth - statusWidth;
204
- result.push(topLeft + content + this.borderColor("─".repeat(fillWidth)) + topRight);
215
+ result.push(topLeft + content + this.borderColor(box.horizontal.repeat(fillWidth)) + topRight);
205
216
  } else {
206
217
  // Status too long - truncate it
207
- const truncated = truncateToWidth(content, topFillWidth - 1, this.borderColor("…"));
218
+ const truncated = truncateToWidth(content, topFillWidth - 1, this.borderColor(this.theme.symbols.ellipsis));
208
219
  const truncatedWidth = visibleWidth(truncated);
209
220
  const fillWidth = Math.max(0, topFillWidth - truncatedWidth);
210
- result.push(topLeft + truncated + this.borderColor("─".repeat(fillWidth)) + topRight);
221
+ result.push(topLeft + truncated + this.borderColor(box.horizontal.repeat(fillWidth)) + topRight);
211
222
  }
212
223
  } else {
213
224
  result.push(topLeft + horizontal.repeat(topFillWidth) + topRight);
@@ -221,7 +232,7 @@ export class Editor implements Component {
221
232
  let displayWidth = visibleWidth(layoutLine.text);
222
233
 
223
234
  // Add cursor if this line has it
224
- if (layoutLine.hasCursor && layoutLine.cursorPos !== undefined) {
235
+ if (!this.useTerminalCursor && layoutLine.hasCursor && layoutLine.cursorPos !== undefined) {
225
236
  const before = displayText.slice(0, layoutLine.cursorPos);
226
237
  const after = displayText.slice(layoutLine.cursorPos);
227
238
 
@@ -236,10 +247,10 @@ export class Editor implements Component {
236
247
  // displayWidth stays the same - we're replacing, not adding
237
248
  } else {
238
249
  // Cursor is at the end - add thin blinking bar cursor
239
- // The character has width 1
240
- const cursor = "\x1b[5m▏\x1b[0m";
250
+ const cursorChar = this.theme.symbols.inputCursor;
251
+ const cursor = `\x1b[5m${cursorChar}\x1b[0m`;
241
252
  displayText = before + cursor;
242
- displayWidth += 1; // Account for cursor width
253
+ displayWidth += visibleWidth(cursorChar);
243
254
  if (displayWidth > lineContentWidth) {
244
255
  // Line is at full width - use reverse video on last grapheme if possible
245
256
  // or just show cursor at the end without adding space
@@ -267,8 +278,8 @@ export class Editor implements Component {
267
278
  // Last line: "╰─ " (3) + content + padding + " ─╯" (3) = 6 chars border
268
279
  result.push(`${bottomLeft} ${displayText}${padding} ${bottomRight}`);
269
280
  } else {
270
- const leftBorder = this.borderColor("│ ");
271
- const rightBorder = this.borderColor(" │");
281
+ const leftBorder = this.borderColor(`${box.vertical} `);
282
+ const rightBorder = this.borderColor(` ${box.vertical}`);
272
283
  result.push(leftBorder + displayText + padding + rightBorder);
273
284
  }
274
285
  }
@@ -282,6 +293,36 @@ export class Editor implements Component {
282
293
  return result;
283
294
  }
284
295
 
296
+ getCursorPosition(width: number): { row: number; col: number } | null {
297
+ if (!this.useTerminalCursor) return null;
298
+
299
+ const contentWidth = width - 6;
300
+ if (contentWidth <= 0) return null;
301
+
302
+ const layoutLines = this.layoutText(contentWidth);
303
+ for (let i = 0; i < layoutLines.length; i++) {
304
+ const layoutLine = layoutLines[i];
305
+ if (!layoutLine || !layoutLine.hasCursor || layoutLine.cursorPos === undefined) continue;
306
+
307
+ const lineWidth = visibleWidth(layoutLine.text);
308
+ const isCursorAtLineEnd = layoutLine.cursorPos === layoutLine.text.length;
309
+
310
+ if (isCursorAtLineEnd && lineWidth >= contentWidth && layoutLine.text.length > 0) {
311
+ const graphemes = [...segmenter.segment(layoutLine.text)];
312
+ const lastGrapheme = graphemes[graphemes.length - 1]?.segment || "";
313
+ const lastWidth = visibleWidth(lastGrapheme) || 1;
314
+ const colOffset = 3 + Math.max(0, lineWidth - lastWidth);
315
+ return { row: 1 + i, col: colOffset };
316
+ }
317
+
318
+ const before = layoutLine.text.slice(0, layoutLine.cursorPos);
319
+ const colOffset = 3 + visibleWidth(before);
320
+ return { row: 1 + i, col: colOffset };
321
+ }
322
+
323
+ return null;
324
+ }
325
+
285
326
  handleInput(data: string): void {
286
327
  // Handle bracketed paste mode
287
328
  // Start of paste: \x1b[200~
@@ -15,9 +15,13 @@ export class Loader extends Text {
15
15
  private spinnerColorFn: (str: string) => string,
16
16
  private messageColorFn: (str: string) => string,
17
17
  private message: string = "Loading...",
18
+ spinnerFrames?: string[],
18
19
  ) {
19
20
  super("", 1, 0);
20
21
  this.ui = ui;
22
+ if (spinnerFrames && spinnerFrames.length > 0) {
23
+ this.frames = spinnerFrames;
24
+ }
21
25
  this.start();
22
26
  }
23
27
 
@@ -1,4 +1,5 @@
1
1
  import { marked, type Token } from "marked";
2
+ import type { SymbolTheme } from "../symbols";
2
3
  import type { Component } from "../tui";
3
4
  import { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from "../utils";
4
5
 
@@ -41,6 +42,7 @@ export interface MarkdownTheme {
41
42
  strikethrough: (text: string) => string;
42
43
  underline: (text: string) => string;
43
44
  highlightCode?: (code: string, lang?: string) => string[];
45
+ symbols: SymbolTheme;
44
46
  }
45
47
 
46
48
  export class Markdown implements Component {
@@ -301,7 +303,10 @@ export class Markdown implements Component {
301
303
  const quoteText = this.renderInlineTokens(token.tokens || []);
302
304
  const quoteLines = quoteText.split("\n");
303
305
  for (const quoteLine of quoteLines) {
304
- lines.push(this.theme.quoteBorder("│ ") + this.theme.quote(this.theme.italic(quoteLine)));
306
+ lines.push(
307
+ this.theme.quoteBorder(`${this.theme.symbols.quoteBorder} `) +
308
+ this.theme.quote(this.theme.italic(quoteLine)),
309
+ );
305
310
  }
306
311
  if (nextTokenType !== "space") {
307
312
  lines.push(""); // Add spacing after blockquotes (unless space token follows)
@@ -310,7 +315,7 @@ export class Markdown implements Component {
310
315
  }
311
316
 
312
317
  case "hr":
313
- lines.push(this.theme.hr("─".repeat(Math.min(width, 80))));
318
+ lines.push(this.theme.hr(this.theme.symbols.hrChar.repeat(Math.min(width, 80))));
314
319
  if (nextTokenType !== "space") {
315
320
  lines.push(""); // Add spacing after horizontal rules (unless space token follows)
316
321
  }
@@ -595,9 +600,13 @@ export class Markdown implements Component {
595
600
  }
596
601
  }
597
602
 
603
+ const t = this.theme.symbols.table;
604
+ const h = t.horizontal;
605
+ const v = t.vertical;
606
+
598
607
  // Render top border
599
- const topBorderCells = columnWidths.map((w) => "─".repeat(w));
600
- lines.push(`┌─${topBorderCells.join("─┬─")}─┐`);
608
+ const topBorderCells = columnWidths.map((w) => h.repeat(w));
609
+ lines.push(`${t.topLeft}${h}${topBorderCells.join(`${h}${t.teeDown}${h}`)}${h}${t.topRight}`);
601
610
 
602
611
  // Render header with wrapping
603
612
  const headerCellLines: string[][] = token.header.map((cell, i) => {
@@ -612,12 +621,12 @@ export class Markdown implements Component {
612
621
  const padded = text + " ".repeat(Math.max(0, columnWidths[colIdx] - visibleWidth(text)));
613
622
  return this.theme.bold(padded);
614
623
  });
615
- lines.push(`│ ${rowParts.join(" ")} │`);
624
+ lines.push(`${v} ${rowParts.join(` ${v} `)} ${v}`);
616
625
  }
617
626
 
618
627
  // Render separator
619
- const separatorCells = columnWidths.map((w) => "─".repeat(w));
620
- lines.push(`├─${separatorCells.join("─┼─")}─┤`);
628
+ const separatorCells = columnWidths.map((w) => h.repeat(w));
629
+ lines.push(`${t.teeRight}${h}${separatorCells.join(`${h}${t.cross}${h}`)}${h}${t.teeLeft}`);
621
630
 
622
631
  // Render rows with wrapping
623
632
  for (const row of token.rows) {
@@ -632,13 +641,13 @@ export class Markdown implements Component {
632
641
  const text = cellLines[lineIdx] || "";
633
642
  return text + " ".repeat(Math.max(0, columnWidths[colIdx] - visibleWidth(text)));
634
643
  });
635
- lines.push(`│ ${rowParts.join(" ")} │`);
644
+ lines.push(`${v} ${rowParts.join(` ${v} `)} ${v}`);
636
645
  }
637
646
  }
638
647
 
639
648
  // Render bottom border
640
- const bottomBorderCells = columnWidths.map((w) => "─".repeat(w));
641
- lines.push(`└─${bottomBorderCells.join("─┴─")}─┘`);
649
+ const bottomBorderCells = columnWidths.map((w) => h.repeat(w));
650
+ lines.push(`${t.bottomLeft}${h}${bottomBorderCells.join(`${h}${t.teeUp}${h}`)}${h}${t.bottomRight}`);
642
651
 
643
652
  lines.push(""); // Add spacing after table
644
653
  return lines;
@@ -1,6 +1,7 @@
1
1
  import { isArrowDown, isArrowUp, isCtrlC, isEnter, isEscape } from "../keys";
2
+ import type { SymbolTheme } from "../symbols";
2
3
  import type { Component } from "../tui";
3
- import { truncateToWidth } from "../utils";
4
+ import { truncateToWidth, visibleWidth } from "../utils";
4
5
 
5
6
  export interface SelectItem {
6
7
  value: string;
@@ -14,6 +15,7 @@ export interface SelectListTheme {
14
15
  description: (text: string) => string;
15
16
  scrollInfo: (text: string) => string;
16
17
  noMatch: (text: string) => string;
18
+ symbols: SymbolTheme;
17
19
  }
18
20
 
19
21
  export class SelectList implements Component {
@@ -74,7 +76,8 @@ export class SelectList implements Component {
74
76
  let line = "";
75
77
  if (isSelected) {
76
78
  // Use arrow indicator for selection - entire line uses selectedText color
77
- const prefixWidth = 2; // "→ " is 2 characters visually
79
+ const prefix = `${this.theme.symbols.cursor} `;
80
+ const prefixWidth = visibleWidth(prefix);
78
81
  const displayValue = item.label || item.value;
79
82
 
80
83
  if (item.description && width > 40) {
@@ -90,20 +93,20 @@ export class SelectList implements Component {
90
93
  if (remainingWidth > 10) {
91
94
  const truncatedDesc = truncateToWidth(item.description, remainingWidth, "");
92
95
  // Apply selectedText to entire line content
93
- line = this.theme.selectedText(`→ ${truncatedValue}${spacing}${truncatedDesc}`);
96
+ line = this.theme.selectedText(`${prefix}${truncatedValue}${spacing}${truncatedDesc}`);
94
97
  } else {
95
98
  // Not enough space for description
96
99
  const maxWidth = width - prefixWidth - 2;
97
- line = this.theme.selectedText(`→ ${truncateToWidth(displayValue, maxWidth, "")}`);
100
+ line = this.theme.selectedText(`${prefix}${truncateToWidth(displayValue, maxWidth, "")}`);
98
101
  }
99
102
  } else {
100
103
  // No description or not enough width
101
104
  const maxWidth = width - prefixWidth - 2;
102
- line = this.theme.selectedText(`→ ${truncateToWidth(displayValue, maxWidth, "")}`);
105
+ line = this.theme.selectedText(`${prefix}${truncateToWidth(displayValue, maxWidth, "")}`);
103
106
  }
104
107
  } else {
105
108
  const displayValue = item.label || item.value;
106
- const prefix = " ";
109
+ const prefix = " ".repeat(visibleWidth(`${this.theme.symbols.cursor} `));
107
110
 
108
111
  if (item.description && width > 40) {
109
112
  // Calculate how much space we have for value + description
package/src/index.ts CHANGED
@@ -61,6 +61,7 @@ export {
61
61
  isTab,
62
62
  Keys,
63
63
  } from "./keys";
64
+ export type { BoxSymbols, SymbolTheme } from "./symbols";
64
65
  // Terminal interface and implementations
65
66
  export { emergencyTerminalRestore, ProcessTerminal, type Terminal } from "./terminal";
66
67
  // Terminal image support
package/src/symbols.ts ADDED
@@ -0,0 +1,25 @@
1
+ export interface BoxSymbols {
2
+ topLeft: string;
3
+ topRight: string;
4
+ bottomLeft: string;
5
+ bottomRight: string;
6
+ horizontal: string;
7
+ vertical: string;
8
+ teeDown: string;
9
+ teeUp: string;
10
+ teeLeft: string;
11
+ teeRight: string;
12
+ cross: string;
13
+ }
14
+
15
+ export interface SymbolTheme {
16
+ cursor: string;
17
+ inputCursor: string;
18
+ ellipsis: string;
19
+ boxRound: Omit<BoxSymbols, "teeDown" | "teeUp" | "teeLeft" | "teeRight" | "cross">;
20
+ boxSharp: BoxSymbols;
21
+ table: BoxSymbols;
22
+ quoteBorder: string;
23
+ hrChar: string;
24
+ spinnerFrames: string[];
25
+ }
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;