Compare commits
1
Commits
bc22a9bf00
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df94556101 |
No files matched your search
@@ -45,7 +45,9 @@ behaves as `New`'s Terminal does, the writer goroutine included; `New` is
|
||||
The application owns a flat `[]Cell` buffer (row-major, `cells[y*width+x]`) and
|
||||
passes it to `Flush`. The output buffer diffs against the previously flushed frame:
|
||||
|
||||
- Rows are scanned with early termination (trailing unchanged cells skipped).
|
||||
- A row's first and last changed cells are found by memcmp, eight cells a compare:
|
||||
an unchanged row costs those compares alone, and only the cells between are
|
||||
scanned one by one.
|
||||
- Cursor moves are emitted only when the write position is non-contiguous.
|
||||
- SGR state (fg, bg, attributes) is coalesced across cells; redundant sequences
|
||||
are suppressed.
|
||||
|
||||
@@ -76,8 +76,8 @@ func cellEqual(a, b Cell) bool {
|
||||
// fails to compile should a field change leave padding behind
|
||||
var _ [0]struct{} = [unsafe.Sizeof(Cell{}) - unsafe.Sizeof(rune(0)) - 2*unsafe.Sizeof(color.RGB{}) - unsafe.Sizeof(Attr(0))]struct{}{}
|
||||
|
||||
// rowEqual reports whether two rows of cells hold the same cells, as one memcmp
|
||||
func rowEqual(a, b []Cell) bool {
|
||||
// cellsEqual reports whether two runs of cells hold the same cells, as one memcmp
|
||||
func cellsEqual(a, b []Cell) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
@@ -88,6 +88,34 @@ func rowEqual(a, b []Cell) bool {
|
||||
return bytes.Equal(unsafe.Slice((*byte)(unsafe.Pointer(&a[0])), n), unsafe.Slice((*byte)(unsafe.Pointer(&b[0])), n))
|
||||
}
|
||||
|
||||
// diffChunk is the cells one memcmp settles while a row's changes are sought: few
|
||||
// enough that a changed chunk costs little to search again cell by cell
|
||||
const diffChunk = 8
|
||||
|
||||
// firstChange is the first cell where a and b differ, len(a) when none does
|
||||
func firstChange(a, b []Cell) int {
|
||||
i := 0
|
||||
for i+diffChunk <= len(a) && cellsEqual(a[i:i+diffChunk], b[i:i+diffChunk]) {
|
||||
i += diffChunk
|
||||
}
|
||||
for i < len(a) && cellEqual(a[i], b[i]) {
|
||||
i++
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
// lastChange is one past the last cell from on where a and b differ, from when none does
|
||||
func lastChange(a, b []Cell, from int) int {
|
||||
j := len(a)
|
||||
for j-diffChunk >= from && cellsEqual(a[j-diffChunk:j], b[j-diffChunk:j]) {
|
||||
j -= diffChunk
|
||||
}
|
||||
for j > from && cellEqual(a[j-1], b[j-1]) {
|
||||
j--
|
||||
}
|
||||
return j
|
||||
}
|
||||
|
||||
// flush renders the back buffer into buf, diffing against front buffer
|
||||
func (o *outputBuffer) flush(cells []Cell, width, height int) {
|
||||
if width != o.width || height != o.height {
|
||||
@@ -104,20 +132,15 @@ func (o *outputBuffer) flush(cells []Cell, width, height int) {
|
||||
|
||||
for y := 0; y < height; y++ {
|
||||
rowStart := y * width
|
||||
// Most rows of a frame are unchanged; one compare settles each of those
|
||||
if rowEqual(cells[rowStart:rowStart+width], o.front[rowStart:rowStart+width]) {
|
||||
// Memcmp finds the row's first and last changed cells, so an unchanged row is
|
||||
// one pass of compares and a changed one is scanned cell by cell between them
|
||||
row, front := cells[rowStart:rowStart+width], o.front[rowStart:rowStart+width]
|
||||
x := firstChange(row, front)
|
||||
if x == width {
|
||||
continue
|
||||
}
|
||||
rowEnd := lastChange(row, front, x+1)
|
||||
|
||||
rowEnd := width
|
||||
for rowEnd > 0 && cellEqual(cells[rowStart+rowEnd-1], o.front[rowStart+rowEnd-1]) {
|
||||
rowEnd--
|
||||
}
|
||||
if rowEnd == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
x := 0
|
||||
for x < rowEnd {
|
||||
idx := rowStart + x
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package terminal
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"math/rand/v2"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -133,6 +134,25 @@ func TestUnchangedFrameWritesNothing(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Every cell a frame changes is written, wherever in its row it lies and however
|
||||
// wide the row: after a flush the screen the terminal holds is the frame
|
||||
func TestEveryChangedCellIsWritten(t *testing.T) {
|
||||
rng := rand.New(rand.NewPCG(1, 2))
|
||||
for _, width := range []int{1, 7, 8, 9, 17, 120} {
|
||||
o := newOutputBuffer(ColorModeTrueColor)
|
||||
frame := make([]Cell, width*4)
|
||||
for range 200 {
|
||||
for range rng.IntN(len(frame)/2 + 1) {
|
||||
frame[rng.IntN(len(frame))] = Cell{Rune: 'a' + rng.Int32N(4), Fg: color.RGB{G: uint8(rng.IntN(2))}}
|
||||
}
|
||||
o.flush(frame, width, 4)
|
||||
if !slices.Equal(o.front, frame) {
|
||||
t.Fatalf("width %d: a changed cell went unwritten", width)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A paste arrives as one event, whatever reads split it into: an escape
|
||||
// inside it is text even when a poll times out on it, and a paste the
|
||||
// terminal never ends ends when input pauses, whether reads time out or
|
||||
|
||||
Reference in new issue
Block a user