@oh-my-pi/pi-tui 14.9.2 → 14.9.5

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 (3) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/package.json +3 -3
  3. package/src/tui.ts +51 -31
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+
6
+ ## [14.9.5] - 2026-05-12
7
+ ### Fixed
8
+
9
+ - Fixed rapidly blinking cursor artifact during task execution by consolidating cursor control sequences into the synchronized output buffer ([#992](https://github.com/can1357/oh-my-pi/issues/992))
10
+
5
11
  ## [14.5.7] - 2026-04-29
6
12
 
7
13
  ### Fixed
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-tui",
4
- "version": "14.9.2",
4
+ "version": "14.9.5",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -37,8 +37,8 @@
37
37
  "fmt": "biome format --write ."
38
38
  },
39
39
  "dependencies": {
40
- "@oh-my-pi/pi-natives": "14.9.2",
41
- "@oh-my-pi/pi-utils": "14.9.2",
40
+ "@oh-my-pi/pi-natives": "14.9.5",
41
+ "@oh-my-pi/pi-utils": "14.9.5",
42
42
  "lru-cache": "11.3.6",
43
43
  "marked": "^18.0.3"
44
44
  },
package/src/tui.ts CHANGED
@@ -1018,10 +1018,12 @@ export class TUI extends Container {
1018
1018
  const line = newLines[i];
1019
1019
  buffer += TERMINAL.isImageLine(line) ? line : line + reset;
1020
1020
  }
1021
+ this.#cursorRow = Math.max(0, newLines.length - 1);
1022
+ const { seq, toRow } = this.#cursorControlSequence(cursorPos, newLines.length, this.#cursorRow);
1023
+ this.#hardwareCursorRow = toRow;
1024
+ buffer += seq;
1021
1025
  buffer += "\x1b[?2026l"; // End synchronized output
1022
1026
  this.terminal.write(buffer);
1023
- this.#cursorRow = Math.max(0, newLines.length - 1);
1024
- this.#hardwareCursorRow = this.#cursorRow;
1025
1027
  // Reset max lines when clearing, otherwise track growth
1026
1028
  if (clear) {
1027
1029
  this.#maxLinesRendered = newLines.length;
@@ -1029,7 +1031,6 @@ export class TUI extends Container {
1029
1031
  this.#maxLinesRendered = Math.max(this.#maxLinesRendered, newLines.length);
1030
1032
  }
1031
1033
  this.#viewportTopRow = Math.max(0, this.#maxLinesRendered - height);
1032
- this.#positionHardwareCursor(cursorPos, newLines.length);
1033
1034
  this.#previousLines = newLines;
1034
1035
  this.#previousWidth = width;
1035
1036
  this.#previousHeight = height;
@@ -1101,7 +1102,7 @@ export class TUI extends Container {
1101
1102
 
1102
1103
  // No changes - but still need to update hardware cursor position if it moved
1103
1104
  if (firstChanged === -1) {
1104
- this.#positionHardwareCursor(cursorPos, newLines.length);
1105
+ this.#writeCursorPosition(cursorPos, newLines.length);
1105
1106
  this.#viewportTopRow = Math.max(0, this.#maxLinesRendered - height);
1106
1107
  return;
1107
1108
  }
@@ -1135,12 +1136,13 @@ export class TUI extends Container {
1135
1136
  if (moveUp > 0) {
1136
1137
  buffer += `\x1b[${moveUp}A`;
1137
1138
  }
1139
+ this.#cursorRow = targetRow;
1140
+ const { seq, toRow } = this.#cursorControlSequence(cursorPos, newLines.length, targetRow);
1141
+ this.#hardwareCursorRow = toRow;
1142
+ buffer += seq;
1138
1143
  buffer += "\x1b[?2026l";
1139
1144
  this.terminal.write(buffer);
1140
- this.#cursorRow = targetRow;
1141
- this.#hardwareCursorRow = targetRow;
1142
1145
  }
1143
- this.#positionHardwareCursor(cursorPos, newLines.length);
1144
1146
  this.#previousLines = newLines;
1145
1147
  this.#previousWidth = width;
1146
1148
  this.#previousHeight = height;
@@ -1166,7 +1168,7 @@ export class TUI extends Container {
1166
1168
  this.#cursorRow = Math.max(0, newLines.length - 1);
1167
1169
  this.#maxLinesRendered = newLines.length;
1168
1170
  this.#viewportTopRow = Math.max(0, newLines.length - height);
1169
- this.#positionHardwareCursor(cursorPos, newLines.length);
1171
+ this.#writeCursorPosition(cursorPos, newLines.length);
1170
1172
  this.#previousLines = newLines;
1171
1173
  this.#previousWidth = width;
1172
1174
  this.#previousHeight = height;
@@ -1249,6 +1251,9 @@ export class TUI extends Container {
1249
1251
  buffer += `\x1b[${extraLines}A`;
1250
1252
  }
1251
1253
 
1254
+ const { seq, toRow } = this.#cursorControlSequence(cursorPos, newLines.length, finalCursorRow);
1255
+ this.#hardwareCursorRow = toRow;
1256
+ buffer += seq;
1252
1257
  buffer += "\x1b[?2026l"; // End synchronized output
1253
1258
 
1254
1259
  if ($flag("PI_TUI_DEBUG")) {
@@ -1262,6 +1267,7 @@ export class TUI extends Container {
1262
1267
  `height: ${height}`,
1263
1268
  `lineDiff: ${lineDiff}`,
1264
1269
  `hardwareCursorRow: ${hardwareCursorRow}`,
1270
+ `hardwareCursorRow (post): ${this.#hardwareCursorRow}`,
1265
1271
  `renderEnd: ${renderEnd}`,
1266
1272
  `finalCursorRow: ${finalCursorRow}`,
1267
1273
  `cursorPos: ${JSON.stringify(cursorPos)}`,
@@ -1283,51 +1289,65 @@ export class TUI extends Container {
1283
1289
  // Write entire buffer at once
1284
1290
  this.terminal.write(buffer);
1285
1291
 
1286
- // Track cursor position for next render
1287
- // cursorRow tracks end of content (for viewport calculation)
1288
- // hardwareCursorRow tracks actual terminal cursor position (for movement)
1292
+ // Track cursor position for next render.
1293
+ // cursorRow tracks end of content (for viewport calculation).
1294
+ // #hardwareCursorRow was already updated by #cursorControlSequence above.
1289
1295
  this.#cursorRow = Math.max(0, newLines.length - 1);
1290
- this.#hardwareCursorRow = finalCursorRow;
1291
1296
  // Track content height for viewport calculation
1292
1297
  this.#maxLinesRendered = newLines.length;
1293
1298
  this.#viewportTopRow = Math.max(0, newLines.length - height);
1294
1299
 
1295
- // Position hardware cursor for IME
1296
- this.#positionHardwareCursor(cursorPos, newLines.length);
1297
-
1298
1300
  this.#previousLines = newLines;
1299
1301
  this.#previousWidth = width;
1300
1302
  this.#previousHeight = height;
1301
1303
  }
1302
1304
 
1303
1305
  /**
1304
- * Position the hardware cursor for IME candidate window.
1305
- * @param cursorPos The cursor position extracted from rendered output, or null
1306
- * @param totalLines Total number of rendered lines
1306
+ * Build cursor control sequences to position the hardware cursor for the IME
1307
+ * candidate window. Returns escape sequences and the resulting cursor row for
1308
+ * the caller to update `#hardwareCursorRow`. The sequences should be appended
1309
+ * into the caller's own synchronized output block to avoid a flicker between
1310
+ * content and cursor frames.
1307
1311
  */
1308
- #positionHardwareCursor(cursorPos: { row: number; col: number } | null, totalLines: number): void {
1309
- if (!cursorPos || totalLines <= 0) {
1310
- this.terminal.hideCursor();
1311
- return;
1312
- }
1312
+ #cursorControlSequence(
1313
+ cursorPos: { row: number; col: number } | null,
1314
+ totalLines: number,
1315
+ fromRow: number,
1316
+ ): { seq: string; toRow: number } {
1317
+ // No IME target or no content — hide cursor regardless of preference
1318
+ if (!cursorPos || totalLines <= 0) return { seq: "\x1b[?25l", toRow: fromRow };
1313
1319
 
1314
1320
  // Clamp cursor position to valid range
1315
1321
  const targetRow = Math.max(0, Math.min(cursorPos.row, totalLines - 1));
1316
1322
  const targetCol = Math.max(0, cursorPos.col);
1317
1323
 
1318
1324
  // Move cursor from current position to target
1319
- const rowDelta = targetRow - this.#hardwareCursorRow;
1320
- let buffer = "";
1325
+ const rowDelta = targetRow - fromRow;
1326
+ let seq = "";
1321
1327
  if (rowDelta > 0) {
1322
- buffer += `\x1b[${rowDelta}B`; // Move down
1328
+ seq += `\x1b[${rowDelta}B`; // Move down
1323
1329
  } else if (rowDelta < 0) {
1324
- buffer += `\x1b[${-rowDelta}A`; // Move up
1330
+ seq += `\x1b[${-rowDelta}A`; // Move up
1325
1331
  }
1326
1332
  // Move to absolute column (1-indexed)
1327
- buffer += `\x1b[${targetCol + 1}G`;
1328
- buffer += this.#showHardwareCursor ? "\x1b[?25h" : "\x1b[?25l";
1333
+ seq += `\x1b[${targetCol + 1}G`;
1334
+ seq += this.#showHardwareCursor ? "\x1b[?25h" : "\x1b[?25l";
1329
1335
 
1330
- this.terminal.write(`\x1b[?2026h${buffer}\x1b[?2026l`);
1331
- this.#hardwareCursorRow = targetRow;
1336
+ return { seq, toRow: targetRow };
1337
+ }
1338
+
1339
+ /**
1340
+ * Write the hardware cursor position to the terminal as a standalone
1341
+ * synchronized output block. Use when there is no surrounding render buffer
1342
+ * to embed the sequences into.
1343
+ */
1344
+ #writeCursorPosition(cursorPos: { row: number; col: number } | null, totalLines: number): void {
1345
+ if (!cursorPos || totalLines <= 0) {
1346
+ this.terminal.hideCursor();
1347
+ return;
1348
+ }
1349
+ const { seq, toRow } = this.#cursorControlSequence(cursorPos, totalLines, this.#hardwareCursorRow);
1350
+ this.#hardwareCursorRow = toRow;
1351
+ this.terminal.write(`\x1b[?2026h${seq}\x1b[?2026l`);
1332
1352
  }
1333
1353
  }