package ansi import ( "bytes" "strconv" "github.com/bnema/vev-vt/core" ) func findSafeScroll(frame CellSource, damage []Damage) (Damage, bool) { for _, d := range damage { if (d.Kind == DamageScrollUp || d.Kind == DamageScrollDown) && isSafeScroll(frame, d) { return d, true } } return Damage{}, false } func isSafeScroll(frame CellSource, d Damage) bool { return d.X == 0 && d.Width == frame.Columns() && d.Y >= 0 && d.Y <= frame.Rows() && d.Height > 0 && d.Height <= frame.Rows()-d.Y && d.Count > 0 && d.Count < d.Height } func emitScroll(out *bytes.Buffer, d Damage) { out.WriteString("\x1b[0m") // \x1b[%d;%dr — set scroll region out.WriteString("\x1b[") var b [16]byte n := strconv.AppendInt(b[:0], int64(d.Y+1), 10) out.Write(n) out.WriteByte(';') n = strconv.AppendInt(b[:0], int64(d.Y+d.Height), 10) out.Write(n) out.WriteByte('r') // SD scrolls down without moving content outside the chosen region. if d.Kind == DamageScrollDown { out.WriteString("\x1b[") n = strconv.AppendInt(b[:0], int64(d.Count), 10) out.Write(n) out.WriteByte('T') out.WriteString("\x1b[r") return } // Position cursor at bottom of scroll region. writeCursor(out, d.Y+d.Height-1, 0) if d.Count == 1 { out.WriteByte('\n') } else { out.WriteString("\x1b[") n = strconv.AppendInt(b[:0], int64(d.Count), 10) out.Write(n) out.WriteByte('S') } out.WriteString("\x1b[r") } func canApplyScrollAgainst(frame CellSource, scroll Damage, damage []Damage, committed Frame) bool { // Scrolling fills exposed rows with default-style blanks. Every target // cell that differs from that fill must be repainted, in either direction. exposed := scroll.Y + scroll.Height - scroll.Count if scroll.Kind == DamageScrollDown { exposed = scroll.Y } blank := BlankCell() for y := exposed; y < exposed+scroll.Count; y++ { for x := range scroll.Width { column := scroll.X + x if !damageCoversCell(damage, column, y) && !frame.Cell(column, y).Equal(blank) { return false } } } // The plan emits one scroll plus text/clear spans. Rows outside the chosen // region must therefore already match the committed shadow wherever damage // does not repaint them; another scroll region in the same batch moves rows // without text damage and must fall back to a snapshot. if !outsideScrollMatches(frame, scroll, damage, committed) { return false } if dense, ok := asFrame(frame); ok { return canApplyDenseScrollAgainst(dense, scroll, damage, committed) } start, end, offset := scrollRetainedRows(scroll) for y := start; y < end; y++ { for x := range scroll.Width { column := scroll.X + x committedCell, frameCell := committed.Cell(column, y+offset), frame.Cell(column, y) if committedCell == frameCell || committedCell.Equal(frameCell) { continue } if !damageCoversCell(damage, column, y) { return false } } } return true } func canApplyDenseScrollAgainst(frame Frame, scroll Damage, damage []Damage, committed Frame) bool { start, end, offset := scrollRetainedRows(scroll) for y := start; y < end; y++ { // isSafeScroll guarantees full-width scrolls, so a retained row that // matches whole needs no per-cell damage lookups. if core.RowsEqualAt(committed, y+offset, frame, y) { continue } for x := range scroll.Width { column := scroll.X + x committedCell, frameCell := committed.At(column, y+offset), frame.At(column, y) if committedCell == frameCell || committedCell.Equal(frameCell) { continue } if !damageCoversCell(damage, column, y) { return false } } } return true } func outsideScrollMatches(frame CellSource, scroll Damage, damage []Damage, committed Frame) bool { dense, isDense := asFrame(frame) columns := frame.Columns() for y := range frame.Rows() { if y >= scroll.Y && y < scroll.Y+scroll.Height { continue } if isDense && core.RowsEqualAt(committed, y, dense, y) { continue } for x := range columns { committedCell, frameCell := committed.Cell(x, y), frame.Cell(x, y) if committedCell == frameCell || committedCell.Equal(frameCell) { continue } if !damageCoversCell(damage, x, y) { return false } } } return true } func scrollRetainedRows(scroll Damage) (start, end, offset int) { if scroll.Kind == DamageScrollDown { return scroll.Y + scroll.Count, scroll.Y + scroll.Height, -scroll.Count } return scroll.Y, scroll.Y + scroll.Height - scroll.Count, scroll.Count } func damageCoversCell(damage []Damage, x, y int) bool { for _, d := range damage { if (d.Kind == DamageText || d.Kind == DamageClear) && x >= d.X && x < d.X+d.Width && y >= d.Y && y < d.Y+d.Height { return true } } return false }