Merge pull request #171 from bnema/fix/viewport-sync-after-delete
fix(vt): clear erased characters during viewport sync
2 files · +87 -0
M pkg/vt/screen_csi.go +17 -0
| @@ -99,6 +99,8 @@ func (s *Screen) applyCSI(params string, cmd byte) { | |||
| 99 | 99 | mode = parts[0] | |
| 100 | 100 | } | |
| 101 | 101 | s.clearLineMode(mode) | |
| 102 | + | case 'X': | |
| 103 | + | s.eraseChars(firstPositive(parts, 1)) | |
| 102 | 104 | case 'S': | |
| 103 | 105 | s.scrollUpBy(firstPositive(parts, 1)) | |
| 104 | 106 | case 'T': | |
| @@ -439,6 +441,21 @@ func (s *Screen) clearLineMode(mode int) { | |||
| 439 | 441 | } | |
| 440 | 442 | } | |
| 441 | 443 | ||
| 444 | + | func (s *Screen) eraseChars(n int) { | |
| 445 | + | if n <= 0 { | |
| 446 | + | return | |
| 447 | + | } | |
| 448 | + | s.clampCursor() | |
| 449 | + | if remaining := s.Frame.Width - s.Col; n > remaining { | |
| 450 | + | n = remaining | |
| 451 | + | } | |
| 452 | + | end := s.Col + n | |
| 453 | + | start, width := s.clearRow(s.Row, s.Col, end) | |
| 454 | + | if width > 0 { | |
| 455 | + | s.record(renderer.Damage{Kind: renderer.DamageClear, X: start, Y: s.Row, Width: width, Height: 1, Count: 1}) | |
| 456 | + | } | |
| 457 | + | } | |
| 458 | + | ||
| 442 | 459 | func (s *Screen) clampCursor() { | |
| 443 | 460 | s.Row = clamp(s.Row, 0, s.Frame.Height-1) | |
| 444 | 461 | s.Col = clamp(s.Col, 0, s.Frame.Width-1) | |
| @@ -99,6 +99,8 @@ func (s *Screen) applyCSI(params string, cmd byte) { | |||||
| 99 | mode = parts[0] | 99 | mode = parts[0] | ||
| 100 | } | 100 | } | ||
| 101 | s.clearLineMode(mode) | 101 | s.clearLineMode(mode) | ||
| 102 | + | case 'X': | |||
| 103 | + | s.eraseChars(firstPositive(parts, 1)) | |||
| 102 | case 'S': | 104 | case 'S': | ||
| 103 | s.scrollUpBy(firstPositive(parts, 1)) | 105 | s.scrollUpBy(firstPositive(parts, 1)) | ||
| 104 | case 'T': | 106 | case 'T': | ||
| @@ -439,6 +441,21 @@ func (s *Screen) clearLineMode(mode int) { | |||||
| 439 | } | 441 | } | ||
| 440 | } | 442 | } | ||
| 441 | 443 | ||||
| 444 | + | func (s *Screen) eraseChars(n int) { | |||
| 445 | + | if n <= 0 { | |||
| 446 | + | return | |||
| 447 | + | } | |||
| 448 | + | s.clampCursor() | |||
| 449 | + | if remaining := s.Frame.Width - s.Col; n > remaining { | |||
| 450 | + | n = remaining | |||
| 451 | + | } | |||
| 452 | + | end := s.Col + n | |||
| 453 | + | start, width := s.clearRow(s.Row, s.Col, end) | |||
| 454 | + | if width > 0 { | |||
| 455 | + | s.record(renderer.Damage{Kind: renderer.DamageClear, X: start, Y: s.Row, Width: width, Height: 1, Count: 1}) | |||
| 456 | + | } | |||
| 457 | + | } | |||
| 458 | + | ||||
| 442 | func (s *Screen) clampCursor() { | 459 | func (s *Screen) clampCursor() { | ||
| 443 | s.Row = clamp(s.Row, 0, s.Frame.Height-1) | 460 | s.Row = clamp(s.Row, 0, s.Frame.Height-1) | ||
| 444 | s.Col = clamp(s.Col, 0, s.Frame.Width-1) | 461 | s.Col = clamp(s.Col, 0, s.Frame.Width-1) | ||
M pkg/vt/screen_csi_test.go +70 -0
| @@ -1,6 +1,7 @@ | |||
| 1 | 1 | package vt | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "strconv" | |
| 4 | 5 | "testing" | |
| 5 | 6 | ||
| 6 | 7 | "github.com/bnema/vev/pkg/renderer" | |
| @@ -608,6 +609,75 @@ func TestClearAndErase(t *testing.T) { | |||
| 608 | 609 | } | |
| 609 | 610 | } | |
| 610 | 611 | ||
| 612 | + | func TestEraseCharacters(t *testing.T) { | |
| 613 | + | maxInt := strconv.Itoa(int(^uint(0) >> 1)) | |
| 614 | + | tests := []struct { | |
| 615 | + | name string | |
| 616 | + | width int | |
| 617 | + | row int | |
| 618 | + | initial string | |
| 619 | + | seq string | |
| 620 | + | wantRow string | |
| 621 | + | wantCol int | |
| 622 | + | wantDamage []renderer.Damage | |
| 623 | + | }{ | |
| 624 | + | { | |
| 625 | + | name: "explicit count", | |
| 626 | + | width: 50, | |
| 627 | + | row: 1, | |
| 628 | + | initial: ` // scroll-method "on-button-down"`, | |
| 629 | + | seq: "\x1b[19Gbutton 273\x1b[13X", | |
| 630 | + | wantRow: " // scroll-button 273", | |
| 631 | + | wantCol: 28, | |
| 632 | + | wantDamage: []renderer.Damage{ | |
| 633 | + | {Kind: renderer.DamageText, X: 18, Y: 1, Width: 10, Height: 1, Count: 1}, | |
| 634 | + | {Kind: renderer.DamageClear, X: 28, Y: 1, Width: 13, Height: 1, Count: 1}, | |
| 635 | + | }, | |
| 636 | + | }, | |
| 637 | + | { | |
| 638 | + | name: "omitted count erases exactly one cell", | |
| 639 | + | width: 6, | |
| 640 | + | row: 1, | |
| 641 | + | initial: "abcdef", | |
| 642 | + | seq: "\x1b[3G\x1b[X", | |
| 643 | + | wantRow: "ab def", | |
| 644 | + | wantCol: 2, | |
| 645 | + | wantDamage: []renderer.Damage{{Kind: renderer.DamageClear, X: 2, Y: 1, Width: 1, Height: 1, Count: 1}}, | |
| 646 | + | }, | |
| 647 | + | { | |
| 648 | + | name: "maximum count clips to screen edge", | |
| 649 | + | width: 6, | |
| 650 | + | row: 1, | |
| 651 | + | initial: "abcdef", | |
| 652 | + | seq: "\x1b[4G\x1b[" + maxInt + "X", | |
| 653 | + | wantRow: "abc", | |
| 654 | + | wantCol: 3, | |
| 655 | + | wantDamage: []renderer.Damage{{Kind: renderer.DamageClear, X: 3, Y: 1, Width: 3, Height: 1, Count: 1}}, | |
| 656 | + | }, | |
| 657 | + | } | |
| 658 | + | ||
| 659 | + | for _, tt := range tests { | |
| 660 | + | t.Run(tt.name, func(t *testing.T) { | |
| 661 | + | s := NewScreen(tt.width, 2) | |
| 662 | + | s.Write([]byte("\x1b[2;1H")) | |
| 663 | + | s.Write([]byte(tt.initial)) | |
| 664 | + | s.ClearDamage() | |
| 665 | + | s.Write([]byte(tt.seq)) | |
| 666 | + | ||
| 667 | + | for x := range tt.width { | |
| 668 | + | want := ' ' | |
| 669 | + | if x < len(tt.wantRow) { | |
| 670 | + | want = rune(tt.wantRow[x]) | |
| 671 | + | } | |
| 672 | + | require.Equal(t, want, cellAt(s, x, tt.row).Rune, "cell(%d,%d)", x, tt.row) | |
| 673 | + | } | |
| 674 | + | require.Equal(t, tt.row, s.Row) | |
| 675 | + | require.Equal(t, tt.wantCol, s.Col) | |
| 676 | + | require.Equal(t, tt.wantDamage, s.Damage()) | |
| 677 | + | }) | |
| 678 | + | } | |
| 679 | + | } | |
| 680 | + | ||
| 611 | 681 | func TestBackgroundColorErase(t *testing.T) { | |
| 612 | 682 | tests := []struct { | |
| 613 | 683 | name string | |
| @@ -1,6 +1,7 @@ | |||||
| 1 | package vt | 1 | package vt | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "strconv" | |||
| 4 | "testing" | 5 | "testing" | ||
| 5 | 6 | ||||
| 6 | "github.com/bnema/vev/pkg/renderer" | 7 | "github.com/bnema/vev/pkg/renderer" | ||
| @@ -608,6 +609,75 @@ func TestClearAndErase(t *testing.T) { | |||||
| 608 | } | 609 | } | ||
| 609 | } | 610 | } | ||
| 610 | 611 | ||||
| 612 | + | func TestEraseCharacters(t *testing.T) { | |||
| 613 | + | maxInt := strconv.Itoa(int(^uint(0) >> 1)) | |||
| 614 | + | tests := []struct { | |||
| 615 | + | name string | |||
| 616 | + | width int | |||
| 617 | + | row int | |||
| 618 | + | initial string | |||
| 619 | + | seq string | |||
| 620 | + | wantRow string | |||
| 621 | + | wantCol int | |||
| 622 | + | wantDamage []renderer.Damage | |||
| 623 | + | }{ | |||
| 624 | + | { | |||
| 625 | + | name: "explicit count", | |||
| 626 | + | width: 50, | |||
| 627 | + | row: 1, | |||
| 628 | + | initial: ` // scroll-method "on-button-down"`, | |||
| 629 | + | seq: "\x1b[19Gbutton 273\x1b[13X", | |||
| 630 | + | wantRow: " // scroll-button 273", | |||
| 631 | + | wantCol: 28, | |||
| 632 | + | wantDamage: []renderer.Damage{ | |||
| 633 | + | {Kind: renderer.DamageText, X: 18, Y: 1, Width: 10, Height: 1, Count: 1}, | |||
| 634 | + | {Kind: renderer.DamageClear, X: 28, Y: 1, Width: 13, Height: 1, Count: 1}, | |||
| 635 | + | }, | |||
| 636 | + | }, | |||
| 637 | + | { | |||
| 638 | + | name: "omitted count erases exactly one cell", | |||
| 639 | + | width: 6, | |||
| 640 | + | row: 1, | |||
| 641 | + | initial: "abcdef", | |||
| 642 | + | seq: "\x1b[3G\x1b[X", | |||
| 643 | + | wantRow: "ab def", | |||
| 644 | + | wantCol: 2, | |||
| 645 | + | wantDamage: []renderer.Damage{{Kind: renderer.DamageClear, X: 2, Y: 1, Width: 1, Height: 1, Count: 1}}, | |||
| 646 | + | }, | |||
| 647 | + | { | |||
| 648 | + | name: "maximum count clips to screen edge", | |||
| 649 | + | width: 6, | |||
| 650 | + | row: 1, | |||
| 651 | + | initial: "abcdef", | |||
| 652 | + | seq: "\x1b[4G\x1b[" + maxInt + "X", | |||
| 653 | + | wantRow: "abc", | |||
| 654 | + | wantCol: 3, | |||
| 655 | + | wantDamage: []renderer.Damage{{Kind: renderer.DamageClear, X: 3, Y: 1, Width: 3, Height: 1, Count: 1}}, | |||
| 656 | + | }, | |||
| 657 | + | } | |||
| 658 | + | ||||
| 659 | + | for _, tt := range tests { | |||
| 660 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 661 | + | s := NewScreen(tt.width, 2) | |||
| 662 | + | s.Write([]byte("\x1b[2;1H")) | |||
| 663 | + | s.Write([]byte(tt.initial)) | |||
| 664 | + | s.ClearDamage() | |||
| 665 | + | s.Write([]byte(tt.seq)) | |||
| 666 | + | ||||
| 667 | + | for x := range tt.width { | |||
| 668 | + | want := ' ' | |||
| 669 | + | if x < len(tt.wantRow) { | |||
| 670 | + | want = rune(tt.wantRow[x]) | |||
| 671 | + | } | |||
| 672 | + | require.Equal(t, want, cellAt(s, x, tt.row).Rune, "cell(%d,%d)", x, tt.row) | |||
| 673 | + | } | |||
| 674 | + | require.Equal(t, tt.row, s.Row) | |||
| 675 | + | require.Equal(t, tt.wantCol, s.Col) | |||
| 676 | + | require.Equal(t, tt.wantDamage, s.Damage()) | |||
| 677 | + | }) | |||
| 678 | + | } | |||
| 679 | + | } | |||
| 680 | + | ||||
| 611 | func TestBackgroundColorErase(t *testing.T) { | 681 | func TestBackgroundColorErase(t *testing.T) { | ||
| 612 | tests := []struct { | 682 | tests := []struct { | ||
| 613 | name string | 683 | name string | ||