(desiredTop: number, reason: string)
| 1397 | // otherwise nothing scrolls, so scrollback and the user's scroll |
| 1398 | // position stay intact. |
| 1399 | const repaintViewport = (desiredTop: number, reason: string): void => { |
| 1400 | // An advancing anchor must commit the rows it skips: paint from |
| 1401 | // the old anchor and let the paint loop scroll `advance` rows out |
| 1402 | // of the screen top. A pinned or rewound anchor paints the target |
| 1403 | // window in place without scrolling. |
| 1404 | const advance = Math.max(0, desiredTop - prevViewportTop); |
| 1405 | const paintStart = advance > 0 ? prevViewportTop : desiredTop; |
| 1406 | const paintCount = height + advance; |
| 1407 | // Kitty image lines need multi-row placement; fall back to a |
| 1408 | // destructive full render for that rare combination. |
| 1409 | for (let r = 0; r < paintCount; r++) { |
| 1410 | const idx = paintStart + r; |
| 1411 | if (idx < newLines.length && isImageLine(newLines[idx]!)) { |
| 1412 | logRedraw(`viewport repaint hit kitty image at line ${idx}`); |
| 1413 | fullRender(true); |
| 1414 | return; |
| 1415 | } |
| 1416 | } |
| 1417 | logRedraw(`viewport repaint: ${reason}`); |
| 1418 | let repaint = "\x1b[?2026h"; // Begin synchronized output |
| 1419 | // Delete kitty images placed in the old visible viewport; images |
| 1420 | // above it live in scrollback and are left alone. A multi-row |
| 1421 | // image can straddle the viewport top: its id lives on the image |
| 1422 | // line above prevViewportTop while its reserved rows are still |
| 1423 | // visible, so widen the range to include such blocks — otherwise |
| 1424 | // the stale overlay would survive the repaint and its id would |
| 1425 | // drop out of previousKittyImageIds, never to be cleaned up. |
| 1426 | let deleteFrom = prevViewportTop; |
| 1427 | for (let k = 0; k < prevViewportTop; k++) { |
| 1428 | const prevLine = this.previousLines[k] ?? ""; |
| 1429 | if (!isImageLine(prevLine)) continue; |
| 1430 | const blockEnd = k + this.getKittyImageReservedRows(this.previousLines, k) - 1; |
| 1431 | if (blockEnd >= prevViewportTop) { |
| 1432 | deleteFrom = k; |
| 1433 | break; |
| 1434 | } |
| 1435 | } |
| 1436 | repaint += this.deleteChangedKittyImages(deleteFrom, this.previousLines.length - 1); |
| 1437 | // Move the cursor to the top of the screen (old anchor still valid). |
| 1438 | const cursorScreenRow = Math.max(0, Math.min(height - 1, hardwareCursorRow - prevViewportTop)); |
| 1439 | if (cursorScreenRow > 0) { |
| 1440 | repaint += `\x1b[${cursorScreenRow}A`; |
| 1441 | } |
| 1442 | repaint += "\r"; |
| 1443 | // Rewrite paintCount rows from paintStart. Rows written past the |
| 1444 | // bottom screen row scroll the terminal: with an advancing anchor |
| 1445 | // the first `advance` rows (freshly painted) scroll into |
| 1446 | // scrollback, committing them exactly once. With advance = 0 the |
| 1447 | // last row gets no trailing \r\n, so nothing scrolls. |
| 1448 | for (let r = 0; r < paintCount; r++) { |
| 1449 | if (r > 0) repaint += "\r\n"; |
| 1450 | repaint += "\x1b[2K"; |
| 1451 | const idx = paintStart + r; |
| 1452 | if (idx < newLines.length) { |
| 1453 | repaint += newLines[idx]!; |
| 1454 | } |
| 1455 | } |
| 1456 | repaint += "\x1b[?2026l"; // End synchronized output |
nothing calls this directly
no test coverage detected