@oh-my-pi/pi-tui 12.10.0 → 12.11.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.
Files changed (2) hide show
  1. package/package.json +3 -3
  2. package/src/tui.ts +40 -41
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-tui",
3
- "version": "12.10.0",
3
+ "version": "12.11.0",
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",
@@ -52,8 +52,8 @@
52
52
  "bun": ">=1.3.7"
53
53
  },
54
54
  "dependencies": {
55
- "@oh-my-pi/pi-natives": "12.10.0",
56
- "@oh-my-pi/pi-utils": "12.10.0",
55
+ "@oh-my-pi/pi-natives": "12.11.0",
56
+ "@oh-my-pi/pi-utils": "12.11.0",
57
57
  "@types/mime-types": "^3.0.1",
58
58
  "chalk": "^5.6.2",
59
59
  "marked": "^17.0.2",
package/src/tui.ts CHANGED
@@ -885,10 +885,13 @@ export class TUI extends Container {
885
885
  if (i > 0) buffer += "\r\n";
886
886
  buffer += newLines[i];
887
887
  }
888
+ const renderCursorRow = Math.max(0, newLines.length - 1);
889
+ const cursorUpdate = this.#buildHardwareCursorSequence(cursorPos, newLines.length, renderCursorRow);
890
+ buffer += cursorUpdate.sequence;
888
891
  buffer += "\x1b[?2026l"; // End synchronized output
889
892
  this.terminal.write(buffer);
890
- this.#cursorRow = Math.max(0, newLines.length - 1);
891
- this.#hardwareCursorRow = this.#cursorRow;
893
+ this.#cursorRow = renderCursorRow;
894
+ this.#hardwareCursorRow = cursorUpdate.row;
892
895
  // Reset max lines when clearing, otherwise track growth
893
896
  if (clear) {
894
897
  this.#maxLinesRendered = newLines.length;
@@ -896,7 +899,6 @@ export class TUI extends Container {
896
899
  this.#maxLinesRendered = Math.max(this.#maxLinesRendered, newLines.length);
897
900
  }
898
901
  this.#previousViewportTop = Math.max(0, this.#maxLinesRendered - height);
899
- this.#positionHardwareCursor(cursorPos, newLines.length);
900
902
  this.#previousLines = newLines;
901
903
  this.#previousWidth = width;
902
904
  };
@@ -965,10 +967,9 @@ export class TUI extends Container {
965
967
 
966
968
  // All changes are in deleted lines (nothing to render, just clear)
967
969
  if (firstChanged >= newLines.length) {
970
+ const targetRow = Math.max(0, newLines.length - 1);
968
971
  if (this.#previousLines.length > newLines.length) {
969
972
  let buffer = "\x1b[?2026h";
970
- // Move to end of new content (clamp to 0 for empty content)
971
- const targetRow = Math.max(0, newLines.length - 1);
972
973
  const lineDiff = computeLineDiff(targetRow);
973
974
  if (lineDiff > 0) buffer += `\x1b[${lineDiff}B`;
974
975
  else if (lineDiff < 0) buffer += `\x1b[${-lineDiff}A`;
@@ -990,12 +991,15 @@ export class TUI extends Container {
990
991
  if (extraLines > 0) {
991
992
  buffer += `\x1b[${extraLines}A`;
992
993
  }
994
+ const cursorUpdate = this.#buildHardwareCursorSequence(cursorPos, newLines.length, targetRow);
995
+ buffer += cursorUpdate.sequence;
993
996
  buffer += "\x1b[?2026l";
994
997
  this.terminal.write(buffer);
995
- this.#cursorRow = targetRow;
996
- this.#hardwareCursorRow = targetRow;
998
+ this.#hardwareCursorRow = cursorUpdate.row;
999
+ } else {
1000
+ this.#positionHardwareCursor(cursorPos, newLines.length);
997
1001
  }
998
- this.#positionHardwareCursor(cursorPos, newLines.length);
1002
+ this.#cursorRow = targetRow;
999
1003
  this.#previousLines = newLines;
1000
1004
  this.#previousWidth = width;
1001
1005
  this.#previousViewportTop = Math.max(0, this.#maxLinesRendered - height);
@@ -1098,8 +1102,9 @@ export class TUI extends Container {
1098
1102
  buffer += `\x1b[${extraLines}A`;
1099
1103
  }
1100
1104
 
1105
+ const cursorUpdate = this.#buildHardwareCursorSequence(cursorPos, newLines.length, finalCursorRow);
1106
+ buffer += cursorUpdate.sequence;
1101
1107
  buffer += "\x1b[?2026l"; // End synchronized output
1102
-
1103
1108
  if (process.env.PI_TUI_DEBUG === "1") {
1104
1109
  const debugDir = "/tmp/tui";
1105
1110
  fs.mkdirSync(debugDir, { recursive: true });
@@ -1128,61 +1133,55 @@ export class TUI extends Container {
1128
1133
  ].join("\n");
1129
1134
  fs.writeFileSync(debugPath, debugData);
1130
1135
  }
1131
-
1132
1136
  // Write entire buffer at once
1133
1137
  this.terminal.write(buffer);
1134
-
1135
- // Track cursor position for next render
1136
1138
  // cursorRow tracks end of content (for viewport calculation)
1137
1139
  // hardwareCursorRow tracks actual terminal cursor position (for movement)
1138
1140
  this.#cursorRow = Math.max(0, newLines.length - 1);
1139
- this.#hardwareCursorRow = finalCursorRow;
1141
+ this.#hardwareCursorRow = cursorUpdate.row;
1140
1142
  // Track terminal's working area (grows but doesn't shrink unless cleared)
1141
1143
  this.#maxLinesRendered = Math.max(this.#maxLinesRendered, newLines.length);
1142
1144
  this.#previousViewportTop = Math.max(0, this.#maxLinesRendered - height);
1143
-
1144
- // Position hardware cursor for IME
1145
- this.#positionHardwareCursor(cursorPos, newLines.length);
1146
-
1147
1145
  this.#previousLines = newLines;
1148
1146
  this.#previousWidth = width;
1149
1147
  }
1150
1148
 
1151
1149
  /**
1152
- * Position the hardware cursor for IME candidate window.
1153
- * @param cursorPos The cursor position extracted from rendered output, or null
1154
- * @param totalLines Total number of rendered lines
1150
+ * Build cursor movement and visibility escape sequence and return resulting row.
1151
+ * Used by differential rendering so cursor placement stays inside synchronized output.
1155
1152
  */
1156
- #positionHardwareCursor(cursorPos: { row: number; col: number } | null, totalLines: number): void {
1153
+ #buildHardwareCursorSequence(
1154
+ cursorPos: { row: number; col: number } | null,
1155
+ totalLines: number,
1156
+ currentRow: number,
1157
+ ): { sequence: string; row: number } {
1157
1158
  if (!cursorPos || totalLines <= 0) {
1158
- this.terminal.hideCursor();
1159
- return;
1159
+ return { sequence: "\x1b[?25l", row: currentRow };
1160
1160
  }
1161
-
1162
1161
  // Clamp cursor position to valid range
1163
1162
  const targetRow = Math.max(0, Math.min(cursorPos.row, totalLines - 1));
1164
1163
  const targetCol = Math.max(0, cursorPos.col);
1165
-
1166
- // Move cursor from current position to target
1167
- const rowDelta = targetRow - this.#hardwareCursorRow;
1168
- let buffer = "";
1164
+ let sequence = "";
1165
+ const rowDelta = targetRow - currentRow;
1169
1166
  if (rowDelta > 0) {
1170
- buffer += `\x1b[${rowDelta}B`; // Move down
1167
+ sequence += `\x1b[${rowDelta}B`; // Move down
1171
1168
  } else if (rowDelta < 0) {
1172
- buffer += `\x1b[${-rowDelta}A`; // Move up
1169
+ sequence += `\x1b[${-rowDelta}A`; // Move up
1173
1170
  }
1174
- // Move to absolute column (1-indexed)
1175
- buffer += `\x1b[${targetCol + 1}G`;
1171
+ sequence += `\x1b[${targetCol + 1}G`; // Move to absolute column (1-indexed)
1172
+ sequence += this.#showHardwareCursor ? "\x1b[?25h" : "\x1b[?25l";
1176
1173
 
1177
- if (buffer) {
1178
- this.terminal.write(buffer);
1179
- }
1174
+ return { sequence, row: targetRow };
1175
+ }
1180
1176
 
1181
- this.#hardwareCursorRow = targetRow;
1182
- if (this.#showHardwareCursor) {
1183
- this.terminal.showCursor();
1184
- } else {
1185
- this.terminal.hideCursor();
1186
- }
1177
+ /**
1178
+ * Position the hardware cursor for IME candidate window.
1179
+ * @param cursorPos The cursor position extracted from rendered output, or null
1180
+ * @param totalLines Total number of rendered lines
1181
+ */
1182
+ #positionHardwareCursor(cursorPos: { row: number; col: number } | null, totalLines: number): void {
1183
+ const update = this.#buildHardwareCursorSequence(cursorPos, totalLines, this.#hardwareCursorRow);
1184
+ this.terminal.write(update.sequence);
1185
+ this.#hardwareCursorRow = update.row;
1187
1186
  }
1188
1187
  }