* Write a cell with a pre-serialized style transition string (from * StylePool.transition). Inlines the txn logic to avoid closure/tuple/delta * allocations on every cell. * * Returns true if the cell was written, false if skipped (wide char at * viewport edge). Callers MUST gate currentStyleId
( screen: VirtualScreen, cell: Cell, styleStr: string, )
| 636 | * terminal, and the next transition is computed from phantom state. |
| 637 | */ |
| 638 | function writeCellWithStyleStr( |
| 639 | screen: VirtualScreen, |
| 640 | cell: Cell, |
| 641 | styleStr: string, |
| 642 | ): boolean { |
| 643 | const cellWidth = cell.width === CellWidth.Wide ? 2 : 1 |
| 644 | const px = screen.cursor.x |
| 645 | const vw = screen.viewportWidth |
| 646 | |
| 647 | // Don't write wide chars that would cross the viewport edge. |
| 648 | // Single-codepoint chars (CJK) at vw-2 are safe; multi-codepoint |
| 649 | // graphemes (flags, ZWJ emoji) need stricter threshold. |
| 650 | if (cellWidth === 2 && px < vw) { |
| 651 | const threshold = cell.char.length > 2 ? vw : vw + 1 |
| 652 | if (px + 2 >= threshold) { |
| 653 | return false |
| 654 | } |
| 655 | } |
| 656 | |
| 657 | const diff = screen.diff |
| 658 | if (styleStr.length > 0) { |
| 659 | diff.push({ type: 'styleStr', str: styleStr }) |
| 660 | } |
| 661 | |
| 662 | const needsCompensation = cellWidth === 2 && needsWidthCompensation(cell.char) |
| 663 | |
| 664 | // On terminals with old wcwidth tables, a compensated emoji only advances |
| 665 | // the cursor 1 column, so the CHA below skips column x+1 without painting |
| 666 | // it. Write a styled space there first — on correct terminals the emoji |
| 667 | // glyph (width 2) overwrites it harmlessly; on old terminals it fills the |
| 668 | // gap with the emoji's background. Also clears any stale content at x+1. |
| 669 | // CHA is 1-based, so column px+1 (0-based) is CHA target px+2. |
| 670 | if (needsCompensation && px + 1 < vw) { |
| 671 | diff.push({ type: 'cursorTo', col: px + 2 }) |
| 672 | diff.push({ type: 'stdout', content: ' ' }) |
| 673 | diff.push({ type: 'cursorTo', col: px + 1 }) |
| 674 | } |
| 675 | |
| 676 | diff.push({ type: 'stdout', content: cell.char }) |
| 677 | |
| 678 | // Force terminal cursor to correct column after the emoji. |
| 679 | if (needsCompensation) { |
| 680 | diff.push({ type: 'cursorTo', col: px + cellWidth + 1 }) |
| 681 | } |
| 682 | |
| 683 | // Update cursor — mutate in place to avoid Point allocation |
| 684 | if (px >= vw) { |
| 685 | screen.cursor.x = cellWidth |
| 686 | screen.cursor.y++ |
| 687 | } else { |
| 688 | screen.cursor.x = px + cellWidth |
| 689 | } |
| 690 | return true |
| 691 | } |
| 692 | |
| 693 | function moveCursorTo(screen: VirtualScreen, targetX: number, targetY: number) { |
| 694 | screen.txn(prev => { |
no test coverage detected