()
| 1262 | } |
| 1263 | |
| 1264 | private doRender(): void { |
| 1265 | if (this.stopped) return; |
| 1266 | const width = this.terminal.columns; |
| 1267 | const height = this.terminal.rows; |
| 1268 | const widthChanged = this.previousWidth !== 0 && this.previousWidth !== width; |
| 1269 | const heightChanged = this.previousHeight !== 0 && this.previousHeight !== height; |
| 1270 | const previousBufferLength = this.previousHeight > 0 ? this.previousViewportTop + this.previousHeight : height; |
| 1271 | let prevViewportTop = heightChanged ? Math.max(0, previousBufferLength - height) : this.previousViewportTop; |
| 1272 | let viewportTop = prevViewportTop; |
| 1273 | let hardwareCursorRow = this.hardwareCursorRow; |
| 1274 | const computeLineDiff = (targetRow: number): number => { |
| 1275 | const currentScreenRow = hardwareCursorRow - prevViewportTop; |
| 1276 | const targetScreenRow = targetRow - viewportTop; |
| 1277 | return targetScreenRow - currentScreenRow; |
| 1278 | }; |
| 1279 | |
| 1280 | // Render all components to get new lines |
| 1281 | let newLines = this.render(width); |
| 1282 | |
| 1283 | // Composite overlays into the rendered lines (before differential compare) |
| 1284 | if (this.overlayStack.length > 0) { |
| 1285 | newLines = this.compositeOverlays(newLines, width, height); |
| 1286 | } |
| 1287 | |
| 1288 | // Extract cursor position before applying line resets (marker must be found first) |
| 1289 | const cursorPos = this.extractCursorPosition(newLines, height); |
| 1290 | |
| 1291 | // Never write a line wider than the terminal: truncate defensively |
| 1292 | // instead of crashing. Extremely narrow terminals can make |
| 1293 | // components overflow by a column (e.g. wide graphemes at width 1). |
| 1294 | // applyLineResets() runs afterwards, so truncated lines still get |
| 1295 | // their trailing reset and cannot leak styles. |
| 1296 | for (let i = 0; i < newLines.length; i++) { |
| 1297 | const line = newLines[i]!; |
| 1298 | if (isImageLine(line)) continue; |
| 1299 | const lineWidth = asciiVisibleWidth(line, width) ?? visibleWidth(line); |
| 1300 | if (lineWidth > width) { |
| 1301 | newLines[i] = sliceByColumn(line, 0, width, true); |
| 1302 | } |
| 1303 | } |
| 1304 | |
| 1305 | newLines = this.applyLineResets(newLines); |
| 1306 | |
| 1307 | // Helper to clear scrollback and viewport and render all new lines |
| 1308 | const fullRender = (clear: boolean): void => { |
| 1309 | this.fullRedrawCount += 1; |
| 1310 | let buffer = "\x1b[?2026h"; // Begin synchronized output |
| 1311 | if (clear) { |
| 1312 | buffer += this.deleteKittyImages(this.previousKittyImageIds); |
| 1313 | buffer += "\x1b[2J\x1b[H\x1b[3J"; // Clear screen, home, then clear scrollback |
| 1314 | } |
| 1315 | for (let i = 0; i < newLines.length; i++) { |
| 1316 | if (i > 0) buffer += "\r\n"; |
| 1317 | const line = newLines[i]!; |
| 1318 | const isImage = isImageLine(line); |
| 1319 | const imageReservedRows = isImage ? this.getKittyImageReservedRows(newLines, i) : 1; |
| 1320 | if (imageReservedRows > 1 && imageReservedRows <= height) { |
| 1321 | for (let row = 1; row < imageReservedRows; row++) { |
no test coverage detected