buildRemotePlan function

RemotePlan buildRemotePlan(
  1. CellBuffer prev,
  2. CellBuffer next, {
  3. required bool fullRepaint,
  4. TuiDirtyRows? dirtyRows,
})

Builds a RemotePlan from the rendered prev/next buffers — the changed column ranges only, grouped into same-style runs with a per-frame style table. On fullRepaint (or a size change) every row is treated as changed.

When the frame is a beneficial upward scroll (the shared detector the ANSI renderer uses), the plan carries scrollUpRows and the patches cover only the residual rows — the client shifts its mirror up by that amount before applying them, so a scrolling log ships one row instead of the whole screen.

dirtyRows, when provided, must cover every cell that can differ between prev and next — the same soundness contract as AnsiRenderer.renderDiff's dirtyBounds. The plan is byte-identical to the unbounded build; the hint only skips work that provably cannot change it. On a plain diff the builder scans only the dirty rows (rows outside sound damage are equal by definition). Scroll detection stays exact: its whole-screen scans are skipped only when no shift could pass the detector's entry guards, and a detected scroll falls back to the unbounded residual walk, because the mirror shift redistributes damage across rows the frame diff never touched (a clean static row still needs re-patching once the mirror under it moves). Null means "no damage hint": every row is scanned, exactly the prior behavior.

A debug oracle re-runs the unbounded build under assert and fails loudly if a hint ever under-covers, so a broken damage producer is caught in dev/CI instead of desyncing a live peer's mirror.

Implementation

RemotePlan buildRemotePlan(
  CellBuffer prev,
  CellBuffer next, {
  required bool fullRepaint,
  TuiDirtyRows? dirtyRows,
}) {
  final full = fullRepaint || prev.size != next.size;
  final rows = next.size.rows;
  // First-line defense for a hint built against the wrong row count: an
  // isFull hint whose single range stops short of next's rows would
  // silently skip the tail while claiming full coverage. Unreachable from
  // the runtime planner, which builds damage against frame.next.size.
  assert(
    dirtyRows == null ||
        !dirtyRows.isFull ||
        (dirtyRows.ranges.length == 1 &&
            dirtyRows.ranges.single.startRow == 0 &&
            dirtyRows.ranges.single.endRow >= rows),
    'buildRemotePlan: an isFull dirtyRows hint does not cover every row of '
    'next ($rows rows) — the damage was built against a different row '
    'count.',
  );
  final styleIndices = <CellStyle, int>{};
  final styleTable = <CellStyle>[];
  int styleIndex(CellStyle s) => styleIndices.putIfAbsent(s, () {
    styleTable.add(s);
    return styleTable.length - 1;
  });

  // An empty (non-full) hint asserts zero dirty cells: no patch and no
  // beneficial scroll is possible, so skip detection outright.
  final emptyDamage = dirtyRows != null && dirtyRows.isEmpty;

  // Detect a beneficial upward scroll on steady-state frames. The residual
  // patches then compare against prev shifted up by `shift`. Under bounded
  // damage, the whole-screen stats scan is skipped when the cheap
  // candidate prefilter proves the detector could not fire — most
  // steady-state frames. (A scroll can be beneficial under sound partial
  // damage: rows a scrolling log leaves untouched — blank tails, repeated
  // chrome — are often shift-invariant, so the detector's outcome must not
  // depend on the hint.)
  int? scrollUpRows;
  if (!full &&
      rows >= 2 &&
      !emptyDamage &&
      (dirtyRows == null ||
          dirtyRows.isFull ||
          _scrollCandidateExists(prev, next))) {
    final shift = detectBeneficialScrollUp(
      prev,
      next,
      screenDiffStats(prev, next),
    );
    if (shift != null && shift > 0) scrollUpRows = shift;
  }
  final shift = scrollUpRows ?? 0;

  // The reference cell for (col, row): the cell `prev` holds at that
  // position after the scroll shift. Off the top of `prev` reads empty.
  Cell prevRef(int col, int row) {
    final source = row + shift;
    return source < rows ? prev.atColRow(col, source) : const Cell.empty();
  }

  final patches = _buildPatches(
    next,
    prevRef,
    full,
    styleIndex,
    // Full frames must emit every row regardless of the hint, and a
    // detected scroll compares residually against the SHIFTED prev — the
    // shift moves damage onto rows the frame diff never marked (a clean
    // row over new mirror content still needs its patch), so only a plain
    // steady-state diff may ride the damage bound.
    rowRanges: full || scrollUpRows != null ? null : dirtyRows?.ranges,
  );

  final plan = RemotePlan(
    size: next.size,
    fullRepaint: full,
    scrollUpRows: scrollUpRows,
    styleTable: styleTable,
    patches: patches,
    placements: _imagePlacements(next),
  );
  // Full frames ignore the hint entirely, so only steady-state bounded
  // builds are oracle-checked.
  assert(
    full ||
        dirtyRows == null ||
        _boundedPlanOracleHolds(
          prev,
          next,
          fullRepaint: fullRepaint,
          bounded: plan,
        ),
    'buildRemotePlan: the damage-bounded plan diverges from an unbounded '
    'rebuild — the dirtyRows hint under-covers the true diff (soundness '
    'contract violated). Shipping it would desync the peer\'s mirror until '
    'the next full repaint.',
  );
  return plan;
}