* 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, )
| 1633 | * terminal, and the next transition is computed from phantom state. |
| 1634 | */ |
| 1635 | function writeCellWithStyleStr( |
| 1636 | screen: VirtualScreen, |
| 1637 | cell: Cell, |
| 1638 | styleStr: string, |
| 1639 | ): boolean { |
| 1640 | const cellWidth = cell.width === CellWidth.Wide ? 2 : 1 |
| 1641 | const px = screen.cursor.x |
| 1642 | const vw = screen.viewportWidth |
| 1643 | const needsCompensation = cellWidth === 2 && needsWidthCompensation(cell.char) |
| 1644 | |
| 1645 | // Don't write wide chars that would cross the viewport edge. |
| 1646 | // Single-codepoint chars (CJK) at vw-2 are safe; multi-codepoint |
| 1647 | // graphemes (flags, ZWJ emoji) need stricter threshold. |
| 1648 | if (cellWidth === 2 && px < vw) { |
| 1649 | const threshold = cell.char.length > 2 ? vw : vw + 1 |
| 1650 | if (px + 2 >= threshold) { |
| 1651 | recordWriteCellStats({ |
| 1652 | styleStrNonEmpty: styleStr.length > 0, |
| 1653 | wideEdgeSkip: true, |
| 1654 | needsWidthCompensation: needsCompensation, |
| 1655 | wideCell: true, |
| 1656 | }) |
| 1657 | return false |
| 1658 | } |
| 1659 | } |
| 1660 | |
| 1661 | recordWriteCellStats({ |
| 1662 | styleStrNonEmpty: styleStr.length > 0, |
| 1663 | wideEdgeSkip: false, |
| 1664 | needsWidthCompensation: needsCompensation, |
| 1665 | wideCell: cellWidth === 2, |
| 1666 | }) |
| 1667 | |
| 1668 | const diff = screen.diff |
| 1669 | if (styleStr.length > 0) { |
| 1670 | diff.push({ type: 'styleStr', str: styleStr }) |
| 1671 | } |
| 1672 | |
| 1673 | // On terminals with old wcwidth tables, a compensated emoji only advances |
| 1674 | // the cursor 1 column, so the CHA below skips column x+1 without painting |
| 1675 | // it. Write a styled space there first — on correct terminals the emoji |
| 1676 | // glyph (width 2) overwrites it harmlessly; on old terminals it fills the |
| 1677 | // gap with the emoji's background. Also clears any stale content at x+1. |
| 1678 | // CHA is 1-based, so column px+1 (0-based) is CHA target px+2. |
| 1679 | if (needsCompensation && px + 1 < vw) { |
| 1680 | diff.push({ type: 'cursorTo', col: px + 2 }) |
| 1681 | diff.push({ type: 'stdout', content: ' ' }) |
| 1682 | diff.push({ type: 'cursorTo', col: px + 1 }) |
| 1683 | } |
| 1684 | |
| 1685 | diff.push({ type: 'stdout', content: cell.char }) |
| 1686 | |
| 1687 | // Force terminal cursor to correct column after the emoji. |
| 1688 | if (needsCompensation) { |
| 1689 | diff.push({ type: 'cursorTo', col: px + cellWidth + 1 }) |
| 1690 | } |
| 1691 | |
| 1692 | // Update cursor — mutate in place to avoid Point allocation |
no test coverage detected