current commit
261030.1 · fix(vt): reset grapheme state after resizing
// Copyright 2026 The TCell Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package tests

import (
	"strings"
	"testing"
	"time"

	"github.com/gdamore/tcell/v3/color"
	"github.com/gdamore/tcell/v3/vt"
)

// TestCursorMove tests several aspects of cursor movement.
func TestCursorMovement(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 5, Y: 3}, vt.MockOptColors(0))
	defer MustClose(t, term)

	MustStart(t, term)

	if size, err := term.WindowSize(); err != nil {
		t.Fatalf("failed getting window size: %v", err)
	} else if size.Height != 3 || size.Width != 5 {
		t.Fatalf("wrong window size X %d Y %d", size.Width, size.Height)
	}
	WriteF(t, term, "\x1b[2;3H")
	CheckPos(t, term, 2, 1)

	WriteF(t, term, "\x1b[20A") // up 20
	CheckPos(t, term, 2, 0)

	WriteF(t, term, "\x1b[20B") // down 20
	CheckPos(t, term, 2, 2)

	WriteF(t, term, "\x1b[A") // up 1
	CheckPos(t, term, 2, 1)

	WriteF(t, term, "\x1b[2C") // right 2
	CheckPos(t, term, 4, 1)

	WriteF(t, term, "\x1b[3D") // left 3
	CheckPos(t, term, 1, 1)

	WriteF(t, term, "\x1b[100D") // left 100
	CheckPos(t, term, 0, 1)

	// Now try the next line and previous line
	WriteF(t, term, "\x1b[2;3H")
	CheckPos(t, term, 2, 1)

	WriteF(t, term, "\x1b[1E")
	CheckPos(t, term, 0, 2)

	WriteF(t, term, "\x1b[2;3H")
	CheckPos(t, term, 2, 1)

	WriteF(t, term, "\x1b[1F")
	CheckPos(t, term, 0, 0)

	WriteF(t, term, "\x1b9")
	CheckPos(t, term, 1, 0)

	WriteF(t, term, "\x1b6")
	CheckPos(t, term, 0, 0)
}

// TestDECALN tests the DEC alignment test (screen filled with 'E').
func TestDECALN(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 5, Y: 3}, vt.MockOptColors(0))
	defer MustClose(t, term)

	MustStart(t, term)

	WriteF(t, term, "\x1b#8")

	for y := range Row(3) {
		for x := range Col(5) {
			CheckAttrs(t, term, x, y, vt.Plain)
			CheckContent(t, term, x, y, "E")
		}
	}

	// clear screen
	WriteF(t, term, "\x1b[H\x1b[J")

	for y := range Row(3) {
		for x := range Col(5) {
			CheckAttrs(t, term, x, y, vt.Plain)
			CheckContent(t, term, x, y, "")
		}
	}

	WriteF(t, term, "\x1b[1m\x1b#8") // bold, DECALN
	for y := range Row(3) {
		for x := range Col(5) {
			CheckAttrs(t, term, x, y, vt.Plain)
			CheckContent(t, term, x, y, "E")
		}
	}
}

// TestBell tests the bell.
func TestBell(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 5, Y: 3}, vt.MockOptColors(0))
	defer MustClose(t, term)
	MustStart(t, term)

	if term.Bells() != 0 {
		t.Errorf("wrong bell count: %d", term.Bells())
	}
	WriteF(t, term, "\x07")
	if term.Bells() != 1 {
		t.Errorf("wrong bell count: %d", term.Bells())
	}
	WriteF(t, term, "\x07")
	if term.Bells() != 2 {
		t.Errorf("wrong bell count: %d", term.Bells())
	}
}

// TestPrimaryDA tests primary device attributes using several mechanisms.
func TestPrimaryDA(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 5, Y: 3}, vt.MockOptColors(0))
	defer MustClose(t, term)

	MustStart(t, term)

	buf := make([]byte, 32)
	WriteF(t, term, "\x1b[c")

	n, err := term.Read(buf)
	if err != nil {
		t.Errorf("failed read: %v", err)
	}
	result := string(buf[:n])
	if !strings.HasSuffix(result, "c") {
		t.Errorf("Missing suffix 'c': %q", result)
	}
	if !strings.HasPrefix(result, "\x1b[?63") {
		t.Errorf("Missing prefix '\x1b[?63': %q", result)
	}

	// Legacy version
	buf = make([]byte, 32) // to start over
	WriteF(t, term, "\x1bZ")

	n, err = term.Read(buf)
	if err != nil {
		t.Errorf("failed read: %v", err)
	}
	result = string(buf[:n])
	if !strings.HasSuffix(result, "c") {
		t.Errorf("Missing suffix 'c': %q", result)
	}
	if !strings.HasPrefix(result, "\x1b[?63") {
		t.Errorf("Missing prefix '\x1b[?63': %q", result)
	}
}

// TestExtendedAttr requests the terminal ID using CSI > q.
func TestExtendedAttr(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 5, Y: 3}, vt.MockOptColors(0))
	defer MustClose(t, term)

	MustStart(t, term)

	buf := make([]byte, 64)
	WriteF(t, term, "\x1b[>q")

	n, err := term.Read(buf)
	AssertF(t, err == nil, "read failed: %v", err)

	result := string(buf[:n])

	VerifyF(t, strings.HasSuffix(result, "\x1b\\"), "missing suffix ST: %q", result)
	VerifyF(t, strings.HasPrefix(result, "\x1bP>|"), "missing prefix 'ESC P>|': %q", result)
	VerifyF(t, strings.Contains(result, "TCellMock 1.0"), "missing terminal identification: %q", result)
}

// TestCursorReport verifies that cursor position reporting works.
func TestCursorReport(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24}, vt.MockOptColors(0))
	defer MustClose(t, term)

	MustStart(t, term)

	WriteF(t, term, "\x1b[5;10H") // fifth row, tenth column
	CheckPos(t, term, 9, 4)

	WriteF(t, term, "\x1b[6n") // cursor position report

	buf := make([]byte, 32)
	n, err := term.Read(buf)
	if err != nil {
		t.Errorf("failed read: %v", err)
	}
	result := string(buf[:n])
	if result != "\x1b[5;10R" {
		t.Errorf("wrong report: %q", result)
	}

	buf = make([]byte, 32)
	// move the cursor back one
	WriteF(t, term, "\b\x1b[6n")
	CheckPos(t, term, 8, 4)
	n, err = term.Read(buf)
	if err != nil {
		t.Errorf("failed read: %v", err)
	}
	result = string(buf[:n])
	if result != "\x1b[5;9R" {
		t.Errorf("wrong report: %q", result)
	}
}

// TestAnsiModes tests the private mode feature.
func TestAnsiModes(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24}, vt.MockOptColors(0))
	defer MustClose(t, term)

	MustStart(t, term)

	WriteF(t, term, "\x1b[20$p") // query for newline mode
	WriteF(t, term, "\x1b[20h")  // turn it on
	WriteF(t, term, "\x1b[20$p") // should read back on
	WriteF(t, term, "\x1b[20l")  // turn it back off
	WriteF(t, term, "\x1b[20$p") // should read back off

	buf := make([]byte, 128)
	n, err := term.Read(buf)
	if err != nil {
		t.Errorf("failed read: %v", err)
	}
	result := string(buf[:n])
	want := "\x1b[20;2$y" + "\x1b[20;1$y" + "\x1b[20;2$y"
	if result != want {
		t.Errorf("wrong response: %q != %q", result, want)
	}
}

// TestPrivateModes tests the private mode feature.
func TestPrivateModes(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24}, vt.MockOptColors(0))
	defer MustClose(t, term)

	MustStart(t, term)

	WriteF(t, term, "\x1b[?7$p")              // query for auto-margin (should start on by default)
	WriteF(t, term, "\x1b[?7l")               // turn it off
	WriteF(t, term, "\x1b[?7$p")              // should read back positive
	WriteF(t, term, "\x1b[?7h")               // put it back on
	WriteF(t, term, "\x1b[?7$p")              // should read back negative
	WriteF(t, term, "\x1b[?1919$p")           // read invalid mode
	WriteF(t, term, "\x1b[?1919h\x1b[?1919l") // togle invalid mode
	WriteF(t, term, "\x1b[?1919$p")           // read invalid mode one more time

	buf := make([]byte, 128)
	n, err := term.Read(buf)
	if err != nil {
		t.Errorf("failed read: %v", err)
	}
	result := string(buf[:n])
	want := "\x1b[?7;1$y" + "\x1b[?7;2$y" + "\x1b[?7;1$y" + "\x1b[?1919;0$y" + "\x1b[?1919;0$y"
	if result != want {
		t.Errorf("wrong response: %q != %q", result, want)
	}

	// Lets also test the cursor (show vs hide)
	WriteF(t, term, "\x1b[?25$p")
	WriteF(t, term, "\x1b[?25l")
	WriteF(t, term, "\x1b[?25$p")
	WriteF(t, term, "\x1b[?25h")
	WriteF(t, term, "\x1b[?25$p")

	buf = make([]byte, 128)
	n, err = term.Read(buf)
	if err != nil {
		t.Errorf("failed read: %v", err)
	}
	result = string(buf[:n])

	want = "\x1b[?25;1$y" + "\x1b[?25;2$y" + "\x1b[?25;1$y"
	if result != want {
		t.Errorf("wrong response: %q != %q", result, want)
	}
}

// TestAutoMargin tests the auto margin feature.
func TestAutoMargin(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24}, vt.MockOptColors(0))
	defer MustClose(t, term)
	MustStart(t, term)

	// default is auto-margin is enabled
	WriteF(t, term, "\x1b[2J") // clear the screen
	WriteF(t, term, "\x1b[1;80HAB")
	CheckPos(t, term, 1, 1)
	if s := string(term.GetCell(Coord{X: 79, Y: 0}).C); s != "A" {
		t.Errorf("last column wrong: %q", s)
	}
	if s := string(term.GetCell(Coord{X: 0, Y: 1}).C); s != "B" {
		t.Errorf("auto wrap did not work: %q", s)
	}

	// now turn it off
	WriteF(t, term, "\x1b[?7l")

	// mess with 3rd row
	WriteF(t, term, "\x1b[3;80HCD")
	CheckPos(t, term, 79, 2)
	if s := string(term.GetCell(Coord{X: 79, Y: 2}).C); s != "D" {
		t.Errorf("last column wrong: %q", s)
	}

	// turn it back on
	WriteF(t, term, "\x1b[?7h")

	// demonstrate that writing to the last column does not advance (pending)
	WriteF(t, term, "\x1b[1;80HA")
	CheckPos(t, term, 79, 0)

	// but one more character does advance
	WriteF(t, term, "\x1b[1;80HAB")
	CheckPos(t, term, 1, 1)

	// tab does not advance, but leaves pending state
	WriteF(t, term, "\x1b[1;80HA\t")
	CheckPos(t, term, 79, 0)
	WriteF(t, term, "\x1b[1;80HA\tb")
	CheckPos(t, term, 1, 1)

	// up or down movement resets the pending state
	WriteF(t, term, "\x1b[1;80HA\x1b[AB")
	CheckPos(t, term, 79, 0)
	WriteF(t, term, "\x1b[1;80HA\x1b[BB")
	CheckPos(t, term, 79, 1)

	// forward also resets pending state (which is clipped)
	WriteF(t, term, "\x1b[1;80HA\x1b[CB")
	CheckPos(t, term, 79, 0)
	WriteF(t, term, "\x1b[1;80HA\x1b[CBC")
	CheckPos(t, term, 1, 1)

	// explicit column also resets pending state (which is clipped)
	WriteF(t, term, "\x1b[1;80HA\x1b[80GB")
	CheckPos(t, term, 79, 0)
	WriteF(t, term, "\x1b[1;80HA\x1b[80GBC")
	CheckPos(t, term, 1, 1)

	// newline of course as well (and also VF and FF)
	WriteF(t, term, "\x1b[1;80HA\nB")
	CheckPos(t, term, 79, 1)
	WriteF(t, term, "\x1b[1;80HA\fB")
	CheckPos(t, term, 79, 1)
	WriteF(t, term, "\x1b[1;80HA\vB")
	CheckPos(t, term, 79, 1)

	// and also index
	WriteF(t, term, "\x1b[1;80HA\x1bDB")
	CheckPos(t, term, 79, 1)

	// and also reverse index
	WriteF(t, term, "\x1b[2;80HA\x1bMB")
	CheckPos(t, term, 79, 0)
}

// TestUnicode tests basic placement of unicode glyphs.
// For now it assumes that the terminal itself supports unicode / latin 1.
func TestUnicode(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24}, vt.MockOptColors(0))
	defer MustClose(t, term)
	MustStart(t, term)

	WriteF(t, term, "\x1b[2J") // clear the screen
	WriteF(t, term, "\x1b[2;2H")
	CheckPos(t, term, 1, 1)
	WriteF(t, term, "åßcπ")
	CheckPos(t, term, 5, 1)
	if s := string(term.GetCell(Coord{X: 1, Y: 1}).C); s != "å" {
		t.Errorf("decode error wrong: %q", s)
	}
	if s := string(term.GetCell(Coord{X: 2, Y: 1}).C); s != "ß" {
		t.Errorf("decode error wrong: %q", s)
	}
	if s := string(term.GetCell(Coord{X: 3, Y: 1}).C); s != "c" {
		t.Errorf("decode error wrong: %q", s)
	}
	if s := string(term.GetCell(Coord{X: 4, Y: 1}).C); s != "π" {
		t.Errorf("decode error wrong: %q", s)
	}
}

// TestUnicodeWide tests a wide unicode glyph.
func TestUnicodeWide(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24}, vt.MockOptColors(0))
	defer MustClose(t, term)
	MustStart(t, term)

	WriteF(t, term, "\x1b#8") // fill it with E's (so we can see that wide clears the next cell)
	WriteF(t, term, "\x1b[2;2H")
	CheckPos(t, term, 1, 1)
	WriteF(t, term, "å宽cπ")
	CheckPos(t, term, 6, 1)
	if s := string(term.GetCell(Coord{X: 1, Y: 1}).C); s != "å" {
		t.Errorf("decode error wrong: %q", s)
	}
	if s := string(term.GetCell(Coord{X: 2, Y: 1}).C); s != "宽" {
		t.Errorf("decode error wrong: %q", s)
	}
	if s := string(term.GetCell(Coord{X: 3, Y: 1}).C); s != "" {
		t.Errorf("decode error wrong: %q", s)
	}
	if s := string(term.GetCell(Coord{X: 4, Y: 1}).C); s != "c" {
		t.Errorf("decode error wrong: %q", s)
	}
	if s := string(term.GetCell(Coord{X: 5, Y: 1}).C); s != "π" {
		t.Errorf("decode error wrong: %q", s)
	}
}

// TestKeyEventLegacy tests key events when using the default legacy key protocol.
func TestKeyEventLegacy(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24}, vt.MockOptColors(0))
	defer MustClose(t, term)
	MustStart(t, term)

	term.KeyTap(vt.KeyA)
	term.KeyTap(vt.KeyLShift, vt.KeyB)
	term.KeyTap(vt.KeyEnter)
	term.KeyTap(vt.KeyRCtrl, vt.KeyI)
	term.KeyTap(vt.KeyEsc)

	buf := make([]byte, 256)
	n, err := term.Read(buf)
	if err != nil {
		t.Errorf("failed read: %v", err)
	}
	result := string(buf[:n])
	want := "aB\r\x09\x1B"
	if result != want {
		t.Errorf("key responses failed: %q != %q", result, want)
	}

	// SS3 based F-keys
	clear(buf)
	term.KeyTap(vt.KeyF1)                                        // SS3 P
	term.KeyTap(vt.KeyLShift, vt.KeyF1)                          // CSI 1 ; 2 P
	term.KeyTap(vt.KeyLCtrl, vt.KeyF2)                           // CSI 1 ; 5 Q
	term.KeyTap(vt.KeyLAlt, vt.KeyLCtrl, vt.KeyLShift, vt.KeyF3) // ESC CSI 1 ; 6 R
	term.KeyTap(vt.KeyRAlt, vt.KeyRCtrl, vt.KeyF4)               // ESC CSI 1 ; 5 S

	want = "\x1bOP"
	want += "\x1b[1;2P"
	want += "\x1b[1;5Q"
	want += "\x1b\x1b[1;6R"
	want += "\x1b\x1b[1;5S"
	n, err = term.Read(buf)
	if err != nil {
		t.Errorf("failed read: %v", err)
	}
	result = string(buf[:n])
	if result != want {
		t.Errorf("key responses failed: %q != %q", result, want)
	}

	// CSI based F-keys
	buf = make([]byte, 256)
	term.KeyTap(vt.KeyF5)                                        // CSI 15 ~
	term.KeyTap(vt.KeyRShift, vt.KeyF6)                          // CSI 17 ; 2 ~
	term.KeyTap(vt.KeyLCtrl, vt.KeyF7)                           // CSI 18 ; 5 ~
	term.KeyTap(vt.KeyLAlt, vt.KeyRCtrl, vt.KeyLShift, vt.KeyF8) // ESC CSI 19 ; 6 ~
	term.KeyTap(vt.KeyRAlt, vt.KeyLCtrl, vt.KeyF9)               // ESC CSI 20 ; 5 ~
	term.KeyTap(vt.KeyF20)                                       // CSI 34 ~
	term.KeyTap(vt.KeyF15)                                       // CSI 28 ~
	term.KeyTap(vt.KeyMenu)                                      // CSI 29 ~
	want = "\x1b[15~"
	want += "\x1b[17;2~"
	want += "\x1b[18;5~"
	want += "\x1b\x1b[19;6~"
	want += "\x1b\x1b[20;5~"
	want += "\x1b[34~"
	want += "\x1b[28~"
	want += "\x1b[29~"
	n, err = term.Read(buf)
	AssertF(t, err == nil, "failed read: %v", err)

	result = string(buf[:n])
	VerifyF(t, result == want, "key responses failed: %q != %q", result, want)

	// Misc other keys
	clear(buf)
	term.KeyTap(vt.KeyEnter)                                // \r
	term.KeyTap(vt.KeyTab)                                  // \t
	term.KeyTap(vt.KeyLShift, vt.KeyTab)                    // CSI Z
	term.KeyTap(vt.KeyLCtrl, vt.KeyM)                       // \r
	term.KeyTap(vt.KeyLCtrl, vt.KeyL)                       // \f
	term.KeyTap(vt.KeyBackspace)                            // \x7f
	term.KeyTap(vt.KeyRCtrl, vt.KeyBackspace)               // \x08
	term.KeyTap(vt.KeyRCtrl, vt.KeyLShift, vt.KeyBackspace) // none
	term.KeyTap(vt.KeyRCtrl, vt.KeySpace)                   // \x00
	term.KeyTap(vt.KeySpace)                                // ' '
	term.KeyTap(vt.KeyRAlt, vt.KeyA)                        // \x1b a
	term.KeyTap(vt.KeyRHyper, vt.KeyA)                      // none
	term.KeyTap(vt.KeyRMeta, vt.KeyA)                       // none
	term.KeyTap(vt.KeyRAlt, vt.KeyLCtrl, vt.KeyJ)           // \x1b\n
	term.KeyTap(vt.KeyRCtrl, vt.KeyL)                       // \x0c
	term.KeyTap(vt.KeyLCtrl, vt.KeyLBrace)                  // \x0c

	want = "\r\t\x1b[Z\r\f\x7f\x08\x00 \x1ba\x1b\n\x0c\x1b"
	n, err = term.Read(buf)
	AssertF(t, err == nil, "failed read: %v", err)

	result = string(buf[:n])
	VerifyF(t, result == want, "key responses failed: %q != %q", result, want)

	// Legacy control key mappings (weird ones)
	// Declining a few of the strange ones (control-?)
	clear(buf)
	term.KeyTap(vt.KeyLCtrl, vt.Key8)      // \x7F
	term.KeyTap(vt.KeyLCtrl, vt.Key4)      // \x1c
	term.KeyTap(vt.KeyLCtrl, vt.Key7)      // \x1f
	term.KeyTap(vt.Key7)                   // 7
	term.KeyTap(vt.KeyLShift, vt.KeySlash) // ?
	term.KeyTap(vt.KeyRCtrl, vt.KeyLBrace) // \x1b

	want = "\x7f\x1c\x1f7?\x1b"
	n, err = term.Read(buf)
	AssertF(t, err == nil, "failed read: %v", err)

	result = string(buf[:n])
	VerifyF(t, result == want, "key responses failed: %q != %q", result, want)

	// Application cursor keys
	term.KeyTap(vt.KeyUp)
	WriteF(t, term, "\x1b[?1h")
	term.KeyTap(vt.KeyDown)
	want = "\x1b[A\x1bOB"
	n, err = term.Read(buf)
	AssertF(t, err == nil, "failed read: %v", err)

	result = string(buf[:n])
	VerifyF(t, result == want, "key responses failed: %q != %q", result, want)

	// Special Tab
	term.KeyTap(vt.KeyLShift, vt.KeyLCtrl, vt.KeyTab)
	term.KeyTap(vt.KeySpace)
	CheckRead(t, term, "\x1b[Z ")
}

// TestSgrAttr tests a variety of combinations of Sgr settings.
func TestSgrAttr(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24})
	defer MustClose(t, term)
	MustStart(t, term)

	WriteF(t, term, "\x1b[H")
	WriteF(t, term, "\x1b[1mA") // bold
	CheckAttrs(t, term, 0, 0, vt.Bold)
	CheckContent(t, term, 0, 0, "A")

	WriteF(t, term, "\x1b[2mB") // dim
	CheckAttrs(t, term, 1, 0, vt.Dim)
	CheckContent(t, term, 1, 0, "B")

	WriteF(t, term, "\x1b[22mC") // clear both
	CheckAttrs(t, term, 2, 0, vt.Plain)
	CheckContent(t, term, 2, 0, "C")

	WriteF(t, term, "\x1b[3;2mD") // italic, dim
	CheckAttrs(t, term, 3, 0, vt.Italic|vt.Dim)
	CheckContent(t, term, 3, 0, "D")

	WriteF(t, term, "\x1b[22mE") // remove dim, should leave italic
	CheckAttrs(t, term, 4, 0, vt.Italic)
	CheckContent(t, term, 4, 0, "E")

	WriteF(t, term, "\x1b[23mF") // clear italic
	CheckAttrs(t, term, 5, 0, vt.Plain)
	CheckContent(t, term, 5, 0, "F")

	WriteF(t, term, "\x1b[3;4mG") // simple underline
	CheckAttrs(t, term, 6, 0, vt.Italic|vt.Underline)
	CheckContent(t, term, 6, 0, "G")

	WriteF(t, term, "\x1b[21mH") // double underline (ECMA)
	CheckAttrs(t, term, 7, 0, vt.Italic|vt.DoubleUnderline)
	CheckContent(t, term, 7, 0, "H")

	WriteF(t, term, "\x1b[4mI") // simple underline
	CheckAttrs(t, term, 8, 0, vt.Italic|vt.Underline)
	CheckContent(t, term, 8, 0, "I")

	WriteF(t, term, "\x1b[4:2mJ") // double underline
	CheckAttrs(t, term, 9, 0, vt.Italic|vt.DoubleUnderline)
	CheckContent(t, term, 9, 0, "J")

	WriteF(t, term, "\x1b[4:3mK") // curly underline
	CheckAttrs(t, term, 10, 0, vt.Italic|vt.CurlyUnderline)
	CheckContent(t, term, 10, 0, "K")

	WriteF(t, term, "\x1b[4:4mL") // dotted underline
	CheckAttrs(t, term, 11, 0, vt.Italic|vt.DottedUnderline)
	CheckContent(t, term, 11, 0, "L")

	WriteF(t, term, "\x1b[4:5mM") // dashed underline
	CheckAttrs(t, term, 12, 0, vt.Italic|vt.DashedUnderline)
	CheckContent(t, term, 12, 0, "M")

	WriteF(t, term, "\x1b[4:9mN") // junk treats as plain underline
	CheckAttrs(t, term, 13, 0, vt.Italic|vt.Underline)
	CheckContent(t, term, 13, 0, "N")

	WriteF(t, term, "\x1b[4:5;24mO") // clustering, clear it
	CheckAttrs(t, term, 14, 0, vt.Italic)
	CheckContent(t, term, 14, 0, "O")

	WriteF(t, term, "\x1b[0;9;7;53mP") // clear, strike-through, reverse, over-lined
	CheckAttrs(t, term, 15, 0, vt.StrikeThrough|vt.Reverse|vt.Overline)
	CheckContent(t, term, 15, 0, "P")

	WriteF(t, term, "\x1b[5;27;29;55mQ")
	CheckAttrs(t, term, 16, 0, vt.Blink)
	CheckContent(t, term, 16, 0, "Q")

	WriteF(t, term, "\x1b[25mR")
	CheckAttrs(t, term, 17, 0, vt.Plain)
	CheckContent(t, term, 17, 0, "R")
}

// TestSgrColor8 tests simple ECMA 48 ANSI color (only 8 possible color values.)
func TestSgrColor8(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24})
	defer MustClose(t, term)
	MustStart(t, term)

	WriteF(t, term, "\x1b[36;42m\x1b#8")
	CheckColors(t, term, 0, 0, color.Teal, color.Green)

	WriteF(t, term, "\x1b[H\x1b[39mA")
	CheckColors(t, term, 0, 0, color.Silver, color.Green)

	WriteF(t, term, "\x1b[49mA")
	CheckColors(t, term, 1, 0, color.Silver, color.Black)

	// verify zero clears colors, first write some non zero colors
	WriteF(t, term, "\x1b[36;42mD")
	CheckColors(t, term, 2, 0, color.Teal, color.Green)

	// then send zero, should go to default colors
	WriteF(t, term, "\x1b[0mA")
	CheckColors(t, term, 3, 0, color.Silver, color.Black)
}

// TestSgrMalformedParam verifies that malformed SGR parameters are ignored
// individually without dropping the rest of the sequence.
func TestSgrMalformedParam(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24})
	defer MustClose(t, term)
	MustStart(t, term)

	WriteF(t, term, "\x1b[31;<mA")
	CheckColors(t, term, 0, 0, color.Maroon, color.Black)

	WriteF(t, term, "\x1b[31;<;32mB")
	CheckColors(t, term, 1, 0, color.Green, color.Black)
}

// TestSgrColor256 tests simple ECMA 48 ANSI color (256 possible color values.)
func TestSgrColor256(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24}, vt.MockOptColors(256))
	defer MustClose(t, term)
	MustStart(t, term)

	// foreground
	WriteF(t, term, "\x1b[38:5:6m\x1b[42m\x1b#8")
	CheckColors(t, term, 0, 0, color.Teal, color.Green)
	WriteF(t, term, "\x1b[38;5;5m\x1b[42mA")
	CheckColors(t, term, 0, 0, color.XTerm5, color.Green)

	WriteF(t, term, "\x1b[38;5;212;1mB")
	CheckColors(t, term, 1, 0, color.XTerm212, color.Green)
	CheckAttrs(t, term, 1, 0, vt.Bold)

	// background
	WriteF(t, term, "\x1b[48;5;2mA")
	CheckColors(t, term, 2, 0, color.XTerm212, color.XTerm2)
	CheckAttrs(t, term, 2, 0, vt.Bold)

	WriteF(t, term, "\x1b[48:5:134;2mC")
	CheckColors(t, term, 3, 0, color.XTerm212, color.XTerm134)
	CheckAttrs(t, term, 3, 0, vt.Dim)

	// mix background and foreground using colons
	WriteF(t, term, "\x1b[48:5:135;38:5:22mC")
	CheckColors(t, term, 4, 0, color.XTerm22, color.XTerm135)

	// and using semicolons
	WriteF(t, term, "\x1b[48;5;136;38;5;23mC")
	CheckColors(t, term, 5, 0, color.XTerm23, color.XTerm136)

	// underline colors - it uses the same parser so we won't check
	// all the variations
	WriteF(t, term, "\x1b[58;5;21;4mC")
	VerifyF(t, term.GetCell(Coord{X: 6, Y: 0}).S.Uc() == color.XTerm21, "underline color is wrong")

	WriteF(t, term, "\x1b[H\x1b[39;49mA")
	CheckColors(t, term, 0, 0, color.Silver, color.Black)

	// verify zero clears colors, first write some non zero colors
	WriteF(t, term, "\x1b[H\x1b[36;42mA")
	CheckColors(t, term, 0, 0, color.Teal, color.Green)

	// then send zero, should go to default colors
	WriteF(t, term, "\x1b[H\x1b[0mA")
	CheckColors(t, term, 0, 0, color.Silver, color.Black)

	// fuzz some things
	WriteF(t, term, "\x1b[m\x1b[H")
	WriteF(t, term, "\x1b[38:3m")
	WriteF(t, term, "\x1b[38;3m")
	WriteF(t, term, "\x1b[38;2m")
	WriteF(t, term, "\x1b[38;2:300m")
	WriteF(t, term, "\x1b[38;5m")
	WriteF(t, term, "\x1b[38;5:300m")
	WriteF(t, term, "\x1b[38:2m")
	WriteF(t, term, "\x1b[38:5m")
	WriteF(t, term, "\x1b[38:2;1;1;1m")
	WriteF(t, term, "\x1b[38:2:1:1m")
	WriteF(t, term, "A")
	CheckColors(t, term, 0, 0, color.Silver, color.Black)
}

// TestSgrColorRGB tests simple ECMA 48 ANSI color (full 24-bit color.)
func TestSgrColorRGB(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24}, vt.MockOptColors(1<<24))
	defer MustClose(t, term)
	MustStart(t, term)

	// foreground
	WriteF(t, term, "\x1b[m\x1b[H\x1b[38:2:255:0:0mA")
	CheckColors(t, term, 0, 0, color.NewRGBColor(255, 0, 0), color.Black)

	WriteF(t, term, "\x1b[m\x1b[H\x1b[38;2;2;0;0mA")
	CheckColors(t, term, 0, 0, color.NewRGBColor(2, 0, 0), color.Black)

	// background
	WriteF(t, term, "\x1b[m\x1b[H\x1b[48:2:1:2:3mA")
	CheckColors(t, term, 0, 0, color.Silver, color.NewRGBColor(1, 2, 3))

	WriteF(t, term, "\x1b[m\x1b[H\x1b[48;2;4;5;6mA")
	CheckColors(t, term, 0, 0, color.Silver, color.NewRGBColor(4, 5, 6))

	// full colors
	WriteF(t, term, "\x1b[m\x1b[H\x1b[38;2;99;88;77;48;2;4;5;6;58;2;99;98;91;1mA")
	CheckColors(t, term, 0, 0, color.NewRGBColor(99, 88, 77), color.NewRGBColor(4, 5, 6))
	VerifyF(t, term.GetCell(Coord{X: 0, Y: 0}).S.Uc() == color.NewRGBColor(99, 98, 91), "underline color is wrong")
	CheckAttrs(t, term, 0, 0, vt.Bold)
}

// TestTitles tests that we can set a window title.
func TestTitles(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24})
	defer MustClose(t, term)
	MustStart(t, term)
	WriteF(t, term, "\x1b]2;Test Application\x1b\\")
	if s := term.GetTitle(); s != "Test Application" {
		t.Errorf("wrong title: %q", s)
	}

	// test ST termination using legacy bell character
	WriteF(t, term, "\x1b]2;Bell Ring\x07")
	if s := term.GetTitle(); s != "Bell Ring" {
		t.Errorf("wrong title: %q", s)
	}

	// 8-bit C1 controls are disabled by default.
	WriteF(t, term, "\x9d2;Eight Bits\x9c")
	if s := term.GetTitle(); s != "Bell Ring" {
		t.Errorf("wrong title: %q", s)
	}

	// ESC SP G cannot enable 8-bit C1 controls unless the emulator permits them.
	WriteF(t, term, "\x1b G\x9d2;Eight Bits\x9c")
	if s := term.GetTitle(); s != "Bell Ring" {
		t.Errorf("wrong title: %q", s)
	}
}

func Test8BitControls(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24}, vt.MockOpt8BitControls{})
	defer MustClose(t, term)
	MustStart(t, term)

	WriteF(t, term, "\x9d2;Eight Bits\x9c")
	if s := term.GetTitle(); s != "Eight Bits" {
		t.Errorf("wrong title: %q", s)
	}

	WriteF(t, term, "\x1b F\x9d2;Disabled\x9c")
	if s := term.GetTitle(); s != "Eight Bits" {
		t.Errorf("wrong title: %q", s)
	}

	WriteF(t, term, "\x1b G\xc2\x9d2;UTF-8 Eight Bits\xc2\x9c")
	if s := term.GetTitle(); s != "UTF-8 Eight Bits" {
		t.Errorf("wrong title: %q", s)
	}

	WriteF(t, term, "\xc2\x9b2;3H")
	CheckPos(t, term, 2, 1)

	WriteF(t, term, "\x1b F\x1b[H\xc2\x9b3;4H")
	CheckPos(t, term, 4, 0)

	WriteF(t, term, "\x1b G\x9b4;5H")
	CheckPos(t, term, 4, 3)
}

func Test8BitControlsInString(t *testing.T) {
	t.Run("DefaultDisabled", func(t *testing.T) {
		term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 1})
		defer MustClose(t, term)
		MustStart(t, term)

		WriteF(t, term, "\x1bPdiscard\x9cA\x07B")
		CheckContent(t, term, 0, 0, "B")
		CheckContent(t, term, 1, 0, "")

		WriteF(t, term, "\x1b[H\x1bPdiscard\xc2\x9cC\x07D")
		CheckContent(t, term, 0, 0, "D")
		CheckContent(t, term, 1, 0, "")
	})

	t.Run("Enabled", func(t *testing.T) {
		term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 1}, vt.MockOpt8BitControls{})
		defer MustClose(t, term)
		MustStart(t, term)

		WriteF(t, term, "\x1bPdiscard\x9cA")
		CheckContent(t, term, 0, 0, "A")

		WriteF(t, term, "\x1b[H\x1bPdiscard\xc2\x9cB")
		CheckContent(t, term, 0, 0, "B")
	})

	t.Run("RuntimeDisabled", func(t *testing.T) {
		term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 1}, vt.MockOpt8BitControls{})
		defer MustClose(t, term)
		MustStart(t, term)

		WriteF(t, term, "\x1b F\x1bPdiscard\x9cA\x07B")
		CheckContent(t, term, 0, 0, "B")
		CheckContent(t, term, 1, 0, "")

		WriteF(t, term, "\x1b G\x1b[H\x1bPdiscard\xc2\x9cC")
		CheckContent(t, term, 0, 0, "C")
	})
}

// TestResize tests resizing the terminal
func TestResize(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24})
	defer MustClose(t, term)
	MustStart(t, term)

	// with E, and enable notifications
	WriteF(t, term, "\x1b#8\x1b[?2048h")
	resizeQ := make(chan bool, 1)
	term.NotifyResize(resizeQ)

	term.SetSize(Coord{X: 132, Y: 24})
	if sz, err := term.WindowSize(); err != nil || sz.Height != 24 || sz.Width != 132 {
		t.Errorf("resize did not occur: %v %d %d", err, sz.Height, sz.Width)
	}
	for y := range Row(24) {
		for x := range Col(80) {
			if s := string(term.GetCell(Coord{X: x, Y: y}).C); s != "E" {
				t.Errorf("resize content at %d,%d wrong: %q", x, y, s)
			}
		}
	}
	for y := range Row(24) {
		for x := Col(80); x < 132; x++ {
			if s := string(term.GetCell(Coord{X: x, Y: y}).C); s != "" {
				t.Errorf("resize content at %d,%d wrong: %q", x, y, s)
			}
		}
	}
	select {
	case <-resizeQ:
	case <-time.After(time.Millisecond * 100):
		t.Errorf("resize signal failure")
	}
	// this forces a flush of the write queue
	term.Write([]byte{})

	buf := make([]byte, 128)
	n, err := term.Read(buf)
	if err != nil {
		t.Errorf("failed read: %v", err)
	}
	result := string(buf[:n])
	if result != "\x1b[48;24;132;0;0t" && result != "\x1b[48;24;132t" {
		t.Errorf("key responses failed: %q", result)
	}
}

// TestCSISetSize tests that CSI 8;h;wt updates mock terminal size.
func TestCSISetSize(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24})
	defer MustClose(t, term)
	MustStart(t, term)

	resizeQ := make(chan bool, 1)
	term.NotifyResize(resizeQ)

	WriteF(t, term, "\x1b[8;32;128t")
	if sz, err := term.WindowSize(); err != nil || sz.Height != 32 || sz.Width != 128 {
		t.Errorf("resize did not occur: %v %d %d", err, sz.Height, sz.Width)
	}

	select {
	case <-resizeQ:
	case <-time.After(time.Millisecond * 100):
		t.Errorf("resize signal failure")
	}
}

// TestTabs tests tab stop functionality.
func TestTabs(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24})
	defer MustClose(t, term)
	MustStart(t, term)

	WriteF(t, term, "a\tC")
	if s := string(term.GetCell(Coord{X: 8, Y: 0}).C); s != "C" {
		t.Errorf("tab did not work: %q", s)
	}
	WriteF(t, term, "\x1b[3I")
	if x := term.Pos().X; x != 32 {
		t.Errorf("wrong position %d", x)
	}

	WriteF(t, term, "\x1b[2Z")
	if x := term.Pos().X; x != 16 {
		t.Errorf("wrong position %d", x)
	}

	WriteF(t, term, "\x1b[3g") // clear all tabs
	WriteF(t, term, "\x1b[I")  // one tab, should go to right margin
	if x := term.Pos().X; x != 79 {
		t.Errorf("wrong position: %d", x)
	}

	WriteF(t, term, "\x1b[Z")
	if x := term.Pos().X; x != 0 {
		t.Errorf("wrong position: %d", x)
	}

	// reset tabs
	WriteF(t, term, "\x1b[?5W")

	WriteF(t, term, "\t")
	if x := term.Pos().X; x != 8 {
		t.Errorf("wrong position: %d", x)
	}
	// clear this position, advance one
	WriteF(t, term, "\x1b[gA")
	if x := term.Pos().X; x != 9 {
		t.Errorf("wrong position: %d", x)
	}
	WriteF(t, term, "\x1bH")
	WriteF(t, term, "\t")
	if x := term.Pos().X; x != 16 {
		t.Errorf("wrong position: %d", x)
	}
	WriteF(t, term, "\x1b[Z")
	if x := term.Pos().X; x != 9 {
		t.Errorf("wrong position: %d", x)
	}
	WriteF(t, term, "\x1b[Z")
	if x := term.Pos().X; x != 0 {
		t.Errorf("wrong position: %d", x)
	}
	WriteF(t, term, "\x1b[1;10H") // goto position 9
	if x := term.Pos().X; x != 9 {
		t.Errorf("wrong position: %d", x)
	}
	// delete this one (do it twice to exercise the does not exist flow)
	WriteF(t, term, "\x1b[0g")
	WriteF(t, term, "\x1b[0g")

	// advance to next tab, then back, we should go to 0
	WriteF(t, term, "\t")
	if x := term.Pos().X; x != 16 {
		t.Errorf("wrong position: %d", x)
	}
	WriteF(t, term, "\x1b[Z")
	if x := term.Pos().X; x != 0 {
		t.Errorf("wrong position: %d", x)
	}
	WriteF(t, term, "\x1b[20I")
	WriteF(t, term, "\t\t")
	if pos := term.Pos(); pos.X != 79 || pos.Y != 0 {
		t.Errorf("wrong position: %d %d", pos.X, pos.Y)
	}

	// now backwards
	WriteF(t, term, "\x1b[20Z")
	WriteF(t, term, "\x1b[Z")
	if pos := term.Pos(); pos.X != 0 || pos.Y != 0 {
		t.Errorf("wrong position: %d %d", pos.X, pos.Y)
	}
}

func TestVerticalPos(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24})
	defer MustClose(t, term)
	MustStart(t, term)

	WriteF(t, term, "\x1b[2;2H")
	WriteF(t, term, "\x1b[10d")
	if pos := term.Pos(); pos.X != 1 || pos.Y != 9 {
		t.Errorf("wrong position: %d %d", pos.X, pos.Y)
	}
	WriteF(t, term, "\x1b[2e")
	if pos := term.Pos(); pos.X != 1 || pos.Y != 11 {
		t.Errorf("wrong position; %d %d", pos.X, pos.Y)
	}
	WriteF(t, term, "\x1b[e")
	if pos := term.Pos(); pos.X != 1 || pos.Y != 12 {
		t.Errorf("wrong position; %d %d", pos.X, pos.Y)
	}
	WriteF(t, term, "\x1b[0e")
	if pos := term.Pos(); pos.X != 1 || pos.Y != 13 {
		t.Errorf("wrong position; %d %d", pos.X, pos.Y)
	}
	WriteF(t, term, "\x1b[50d")
	if pos := term.Pos(); pos.X != 1 || pos.Y != 23 {
		t.Errorf("wrong position; %d %d", pos.X, pos.Y)
	}
	WriteF(t, term, "\x1b[50e")
	if pos := term.Pos(); pos.X != 1 || pos.Y != 23 {
		t.Errorf("wrong position; %d %d", pos.X, pos.Y)
	}
	WriteF(t, term, "\x1b[0d")
	if pos := term.Pos(); pos.X != 1 || pos.Y != 0 {
		t.Errorf("wrong position; %d %d", pos.X, pos.Y)
	}
	WriteF(t, term, "\x1b[10d")
	if pos := term.Pos(); pos.X != 1 || pos.Y != 9 {
		t.Errorf("wrong position; %d %d", pos.X, pos.Y)
	}
	WriteF(t, term, "\x1b[1d")
	if pos := term.Pos(); pos.X != 1 || pos.Y != 0 {
		t.Errorf("wrong position; %d %d", pos.X, pos.Y)
	}
}

func TestSaveCursorPosition(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24})
	defer MustClose(t, term)
	MustStart(t, term)

	WriteF(t, term, "\x1b[1;1H\x1b[J")
	WriteF(t, term, "\x1b[1;5H")
	WriteF(t, term, "A")
	WriteF(t, term, "\x1b7")
	WriteF(t, term, "\x1b[1;1H")
	WriteF(t, term, "B")
	WriteF(t, term, "\x1b8")
	WriteF(t, term, "X")

	CheckContent(t, term, 0, 0, "B")
	CheckContent(t, term, 4, 0, "A")
	CheckContent(t, term, 5, 0, "X")
}

func TestSaveCursorWrapState(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24})
	defer MustClose(t, term)
	MustStart(t, term)

	WriteF(t, term, "\x1b[1;1H\x1b[J")
	WriteF(t, term, "\x1b[80G")
	WriteF(t, term, "A")
	WriteF(t, term, "\x1b7") // save cursor
	WriteF(t, term, "\x1b[1;1H")
	WriteF(t, term, "B")
	WriteF(t, term, "\x1b8") // restore cursor
	WriteF(t, term, "X")
	CheckContent(t, term, 0, 0, "B")
	CheckContent(t, term, 79, 0, "A")
	CheckContent(t, term, 0, 1, "X")
}

func TestSaveCursorSgr(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24})
	defer MustClose(t, term)
	MustStart(t, term)
	WriteF(t, term, "\x1b[1;1H\x1b[0J")
	WriteF(t, term, "\x1b[1;4;33;44m")
	WriteF(t, term, "A")
	CheckPos(t, term, 1, 0)
	WriteF(t, term, "\x1b7")
	CheckPos(t, term, 1, 0)
	WriteF(t, term, "\x1b[0m")
	CheckPos(t, term, 1, 0)
	WriteF(t, term, "BE")
	CheckPos(t, term, 3, 0)
	WriteF(t, term, "\x1b8")
	CheckPos(t, term, 1, 0)
	WriteF(t, term, "X")
	CheckContent(t, term, 0, 0, "A")
	CheckContent(t, term, 1, 0, "X")
	CheckContent(t, term, 2, 0, "E")
	CheckAttrs(t, term, 0, 0, vt.Bold|vt.Underline)
	CheckAttrs(t, term, 1, 0, vt.Bold|vt.Underline)
	CheckAttrs(t, term, 2, 0, vt.Plain)
	CheckColors(t, term, 0, 0, color.XTerm3, color.XTerm4)
	CheckColors(t, term, 1, 0, color.XTerm3, color.XTerm4)
	CheckColors(t, term, 2, 0, color.Silver, color.Black)
}

func TestReset(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24})
	defer MustClose(t, term)
	MustStart(t, term)

	// write a bunch of stuff to create state (so we can verify it gets reset)
	WriteF(t, term, "\x1b[1;4;33;44m")
	WriteF(t, term, "\x1b#8")
	WriteF(t, term, "\x1b[1;80HX")
	WriteF(t, term, "\x1b7")    // save cursor
	WriteF(t, term, "\x1b[?7l") // disable automargin
	WriteF(t, term, "\x1bc")
	CheckPos(t, term, 0, 0)
	for row := range Row(24) {
		for col := range Col(80) {
			CheckAttrs(t, term, col, row, vt.Plain)
			CheckContent(t, term, col, row, "")
			CheckColors(t, term, col, row, color.Silver, color.Black)
		}
	}
	WriteF(t, term, "\x1b8") // restore cursor
	CheckPos(t, term, 0, 0)
	WriteF(t, term, "X")
	CheckAttrs(t, term, 0, 0, vt.Plain)
	CheckContent(t, term, 0, 0, "X")
	CheckColors(t, term, 0, 0, color.Silver, color.Black)
	WriteF(t, term, "\x1b[?7$p")

	// verify mode reset
	want := "\x1b[?7;1$y"
	buf := make([]byte, 128)
	n, err := term.Read(buf)
	if err != nil {
		t.Errorf("failed read: %v", err)
	}
	result := string(buf[:n])
	VerifyF(t, result == want, "wrong mode: %q != %q", result, want)
}

// backendBox makes a backend box, filled with increasing letters (modulo 16)
func backendBox(t *testing.T, mb vt.MockBackend, tl Coord, br Coord, attr Attr) {
	t.Helper()
	style := vt.BaseStyle.WithAttr(attr)
	hex := []rune{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P'}
	AssertF(t, len(hex) == 16, "wrong hex string")
	i := 0
	for row := tl.Y; row <= br.Y; row++ {
		for col := tl.X; col <= br.X; col++ {
			mb.Put(Coord{Y: row, X: col}, vt.Cell{C: string(hex[i%16]), W: 1, S: style})
			i++
		}
	}
}

func TestBackendBlit(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 10, Y: 5})
	defer MustClose(t, term)
	MustStart(t, term)

	mb := term.Backend()
	AssertF(t, mb != nil, "backend is nil")

	backendBox(t, mb, Coord{X: 0, Y: 0}, Coord{X: 1, Y: 1}, vt.Bold)
	CheckContent(t, term, 0, 0, "A")
	CheckContent(t, term, 1, 0, "B")
	CheckContent(t, term, 2, 0, "")
	CheckContent(t, term, 0, 1, "C")
	CheckContent(t, term, 1, 1, "D")
	CheckContent(t, term, 2, 1, "")
	CheckContent(t, term, 0, 2, "")
	CheckContent(t, term, 1, 2, "")
	CheckContent(t, term, 2, 2, "")
	CheckAttrs(t, term, 2, 2, vt.Plain)

	// blit the entire box down and right 1
	mb.(vt.Blitter).Blit(Coord{X: 0, Y: 0}, Coord{X: 1, Y: 1}, Coord{X: 2, Y: 2})
	CheckContent(t, term, 0, 0, "A")
	CheckContent(t, term, 1, 0, "B")
	CheckContent(t, term, 2, 0, "")
	CheckContent(t, term, 0, 1, "C")
	CheckContent(t, term, 1, 1, "A")
	CheckContent(t, term, 2, 1, "B")
	CheckContent(t, term, 0, 2, "")
	CheckContent(t, term, 1, 2, "C")
	CheckContent(t, term, 2, 2, "D")

	// spot check attributes
	CheckAttrs(t, term, 2, 2, vt.Bold)
}

func TestBackendBlitReverse(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 10, Y: 5})
	defer MustClose(t, term)
	MustStart(t, term)

	mb := term.Backend()
	AssertF(t, mb != nil, "backend is nil")

	backendBox(t, mb, Coord{X: 1, Y: 1}, Coord{X: 2, Y: 2}, vt.Bold)
	CheckContent(t, term, 0, 0, "")
	CheckContent(t, term, 1, 0, "")
	CheckContent(t, term, 2, 0, "")
	CheckContent(t, term, 0, 1, "")
	CheckContent(t, term, 1, 1, "A")
	CheckContent(t, term, 2, 1, "B")
	CheckContent(t, term, 0, 2, "")
	CheckContent(t, term, 1, 2, "C")
	CheckContent(t, term, 2, 2, "D")
	CheckAttrs(t, term, 0, 0, vt.Plain)

	// blit the entire box down and right 1
	mb.(vt.Blitter).Blit(Coord{X: 1, Y: 1}, Coord{X: 0, Y: 0}, Coord{X: 2, Y: 2})
	CheckContent(t, term, 0, 0, "A")
	CheckContent(t, term, 1, 0, "B")
	CheckContent(t, term, 2, 0, "")
	CheckContent(t, term, 0, 1, "C")
	CheckContent(t, term, 1, 1, "D")
	CheckContent(t, term, 2, 1, "B")
	CheckContent(t, term, 0, 2, "")
	CheckContent(t, term, 1, 2, "C")
	CheckContent(t, term, 2, 2, "D")

	// spot check attributes
	CheckAttrs(t, term, 2, 2, vt.Bold)
}

func TestGraphemeClusterAfterResize(t *testing.T) {
	for _, tc := range []struct {
		name               string
		size, cell, cursor Coord
	}{
		{"shrink", Coord{X: 4, Y: 2}, Coord{X: 3, Y: 1}, Coord{X: 3, Y: 1}},
		{"grow", Coord{X: 16, Y: 8}, Coord{X: 7, Y: 3}, Coord{X: 8, Y: 3}},
	} {
		t.Run(tc.name, func(t *testing.T) {
			term := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 4})
			defer MustClose(t, term)
			MustStart(t, term)

			WriteF(t, term, "\x1b[?2027h\x1b[4;7He")
			term.SetSize(tc.size)
			WriteF(t, term, "x\u0301")
			CheckContent(t, term, tc.cell.X, tc.cell.Y, "x\u0301")
			CheckPos(t, term, tc.cursor.X, tc.cursor.Y)
		})
	}
}

func TestGraphemeCluster(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 10, Y: 5})
	defer MustClose(t, term)
	MustStart(t, term)

	WriteF(t, term, "\x1b[H")
	// grapheme clustering is off
	WriteF(t, term, "🇨🇭") // flag + regional indicator C + regional indicator H
	WriteF(t, term, "A")

	// we advanced by four columns (two wide emoji), and a single character
	CheckPos(t, term, 5, 0)
	CheckContent(t, term, 0, 0, "\U0001f1e8") // regional indicator C
	CheckContent(t, term, 1, 0, "")           // empty
	CheckContent(t, term, 2, 0, "\U0001f1ed") // regional indicator H
	CheckContent(t, term, 3, 0, "")           // empty
	CheckContent(t, term, 4, 0, "A")          // empty

	// now turn on grapheme clustering
	WriteF(t, term, "\x1b[?2027h")
	WriteF(t, term, "\x1b[H\x1b[J")
	CheckPos(t, term, 0, 0)
	WriteF(t, term, "\U0001f1e8\U0001f1ed")
	CheckPos(t, term, 2, 0)
	WriteF(t, term, "A")
	CheckPos(t, term, 3, 0)
	CheckContent(t, term, 0, 0, "🇨🇭")
	CheckContent(t, term, 1, 0, "")
	CheckContent(t, term, 2, 0, "A")

	// lets also verify it works with automargin
	// RECALL: Maximum width is 10
	WriteF(t, term, "\x1b[7h")    // should already be on
	WriteF(t, term, "\x1b[1;10H") // last position in first row
	WriteF(t, term, "🇨🇭A")        // flag + regional indicator C + regional indicator H
	CheckPos(t, term, 1, 1)
	CheckContent(t, term, 9, 0, "🇨🇭")
	CheckContent(t, term, 0, 1, "A")
}

// Characters that attach to an ASCII base must cluster with it even when
// Go's unicode tables do not classify them as marks. U+200C is a format
// character (Cf), U+1F3FB a modifier symbol (Sk), and U+1ACF is a combining
// mark that Go's tables, currently Unicode 15, still report as unassigned.
func TestGraphemeClusterAsciiBase(t *testing.T) {
	for _, tc := range []struct {
		name string
		text string
		next Col
	}{
		{"zwnj", "a\u200c", 1},
		{"emoji modifier", "a\U0001f3fb", 1},
		{"recent combining mark", "a\u1acf", 1},
	} {
		t.Run(tc.name, func(t *testing.T) {
			term := vt.NewMockTerm(vt.MockOptSize{X: 10, Y: 5})
			defer MustClose(t, term)
			MustStart(t, term)

			WriteF(t, term, "\x1b[?2027h")
			WriteF(t, term, "\x1b[H\x1b[J")
			WriteF(t, term, "%s", tc.text)

			CheckContent(t, term, 0, 0, tc.text)
			CheckPos(t, term, tc.next, 0)

			// the cell past the cluster must not have been written to
			CheckContent(t, term, tc.next, 0, "")
		})
	}
}

func TestEraseAbove(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 10, Y: 5})
	defer MustClose(t, term)
	MustStart(t, term)

	WriteF(t, term, "\x1b#8")
	WriteF(t, term, "\x1b[3;5H")

	WriteF(t, term, "\x1b[31;1;42m") // set some colors and bold
	WriteF(t, term, "\x1b[1J")
	// cursor is at 4,2
	for row := range Row(5) {
		for col := range Col(10) {
			if row < 2 || row == 2 && col < 5 {
				CheckContent(t, term, col, row, "")
				CheckAttrs(t, term, col, row, vt.Plain)
				CheckColors(t, term, col, row, color.XTerm1, color.XTerm2)
			} else {
				CheckContent(t, term, col, row, "E")
				CheckAttrs(t, term, col, row, vt.Plain)
				CheckColors(t, term, col, row, color.Silver, color.Black)
			}
		}
	}
}

func TestEraseLine(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 10, Y: 5})
	defer MustClose(t, term)
	MustStart(t, term)

	WriteF(t, term, "\x1b#8")

	// erase to end
	WriteF(t, term, "\x1b[31;1;42m") // set some colors and bold
	WriteF(t, term, "\x1b[1;5H")
	WriteF(t, term, "\x1b[0K")
	// cursor is at 4,0
	CheckPos(t, term, 4, 0)
	for col := range Col(10) {
		row := Row(0)
		if col < 4 {
			CheckContent(t, term, col, row, "E")
			CheckAttrs(t, term, col, row, vt.Plain)
			CheckColors(t, term, col, row, color.Silver, color.Black)
		} else {
			CheckContent(t, term, col, row, "")
			CheckAttrs(t, term, col, row, vt.Plain)
			CheckColors(t, term, col, row, color.XTerm1, color.XTerm2)
		}
	}
	CheckPos(t, term, 4, 0)

	// erase to beginning
	WriteF(t, term, "\x1b[2;5H")
	WriteF(t, term, "\x1b[1K")
	// cursor is at 4,1
	CheckPos(t, term, 4, 1)
	for col := range Col(10) {
		row := Row(1)
		if col > 4 {
			CheckContent(t, term, col, row, "E")
			CheckAttrs(t, term, col, row, vt.Plain)
			CheckColors(t, term, col, row, color.Silver, color.Black)
		} else {
			CheckContent(t, term, col, row, "")
			CheckAttrs(t, term, col, row, vt.Plain)
			CheckColors(t, term, col, row, color.XTerm1, color.XTerm2)
		}
	}
	CheckPos(t, term, 4, 1)

	// erase entire line
	WriteF(t, term, "\x1b[3;5H")
	WriteF(t, term, "\x1b[2K")
	// cursor is at 4,2
	CheckPos(t, term, 4, 2)
	for col := range Col(10) {
		row := Row(2)
		CheckContent(t, term, col, row, "")
		CheckAttrs(t, term, col, row, vt.Plain)
		CheckColors(t, term, col, row, color.XTerm1, color.XTerm2)
	}
	CheckPos(t, term, 4, 2)
}

// TestNewLineScroll tests scrolling with a new line.
// This is one of the most fundamental operations for a terminal.
func TestNewLineScroll(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 10, Y: 5})
	defer MustClose(t, term)
	MustStart(t, term)

	WriteF(t, term, "\x1b[H\x1b[J") // home and clear
	WriteF(t, term, "\x1b[1;1HA")
	WriteF(t, term, "\x1b[2;1HB")
	WriteF(t, term, "\x1b[5;1HC") // first column on last row
	CheckContent(t, term, 0, 4, "C")
	WriteF(t, term, "\n") // new line should scroll
	CheckPos(t, term, 1, 4)
	CheckContent(t, term, 0, 0, "B")
	CheckContent(t, term, 0, 1, "")
	CheckContent(t, term, 0, 4, "")
	CheckContent(t, term, 0, 3, "C")
}

// TestNewLineScrollNoBlitter tests scrolling with a new line,
// using the fallback copy for backends without Blit support.
func TestNewLineScrollNoBlitter(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 10, Y: 5}, vt.MockOptNoBlit{})
	defer MustClose(t, term)
	MustStart(t, term)

	WriteF(t, term, "\x1b[H\x1b[J") // home and clear
	WriteF(t, term, "\x1b[1;1HA")
	WriteF(t, term, "\x1b[2;1HB")
	WriteF(t, term, "\x1b[5;1HC") // first column on last row
	CheckContent(t, term, 0, 4, "C")
	WriteF(t, term, "\n") // new line should scroll
	CheckPos(t, term, 1, 4)
	CheckContent(t, term, 0, 0, "B")
	CheckContent(t, term, 0, 1, "")
	CheckContent(t, term, 0, 4, "")
	CheckContent(t, term, 0, 3, "C")
}

func TestNewLineModes(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 10, Y: 4})
	defer MustClose(t, term)
	MustStart(t, term)
	WriteF(t, term, "\x1b[H\x1b[J")
	WriteF(t, term, "ABC\n")
	CheckPos(t, term, 3, 1)
	term.KeyTap(vt.KeyEnter)
	WriteF(t, term, "\x1b[20h")
	term.KeyTap(vt.KeyEnter)
	WriteF(t, term, "DEF")
	CheckPos(t, term, 6, 1)
	WriteF(t, term, "\n")
	CheckPos(t, term, 0, 2)
	WriteF(t, term, "GHI")
	CheckPos(t, term, 3, 2)
	WriteF(t, term, "\x1b[20l")
	WriteF(t, term, "\n")
	CheckPos(t, term, 3, 3)
	term.KeyTap(vt.KeyEnter)

	// |ABC_____|
	// |___DEF__|
	// |GHI_____|
	// |___c____|
	//
	// input stream contains \r\r\n\r

	CheckContent(t, term, 0, 0, "A")
	CheckContent(t, term, 1, 0, "B")
	CheckContent(t, term, 2, 0, "C")
	CheckContent(t, term, 3, 0, "")
	CheckContent(t, term, 4, 0, "")
	CheckContent(t, term, 5, 0, "")
	CheckContent(t, term, 0, 1, "")
	CheckContent(t, term, 1, 1, "")
	CheckContent(t, term, 2, 1, "")
	CheckContent(t, term, 3, 1, "D")
	CheckContent(t, term, 4, 1, "E")
	CheckContent(t, term, 5, 1, "F")
	CheckContent(t, term, 0, 2, "G")
	CheckContent(t, term, 1, 2, "H")
	CheckContent(t, term, 2, 2, "I")
	CheckContent(t, term, 3, 2, "")
	CheckContent(t, term, 4, 2, "")
	CheckContent(t, term, 5, 2, "")

	result := ReadF(t, term)
	want := "\r\r\n\r"
	VerifyF(t, result == want, "response incorrect: %q != %q", result, want)
}

// TestScrollUp tests scrolling up. The cursor position is retained.
func TestScrollUp(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 10, Y: 5})
	defer MustClose(t, term)
	MustStart(t, term)

	WriteF(t, term, "\x1b[H\x1b[J") // home and clear
	WriteF(t, term, "\x1b[1;1HA")
	WriteF(t, term, "\x1b[2;1HB")
	WriteF(t, term, "\x1b[5;1HC") // first column on last row
	CheckContent(t, term, 0, 4, "C")
	WriteF(t, term, "\x1b[5;5H") // fifth column on last row
	CheckPos(t, term, 4, 4)
	WriteF(t, term, "\x1bD")
	CheckPos(t, term, 4, 4)
	CheckContent(t, term, 0, 0, "B")
	CheckContent(t, term, 0, 1, "")
	CheckContent(t, term, 0, 4, "")
	CheckContent(t, term, 0, 3, "C")
	WriteF(t, term, "\x1bE") // this is like a newline
	CheckPos(t, term, 0, 4)
	CheckContent(t, term, 0, 2, "C")
	CheckContent(t, term, 0, 3, "")
	CheckContent(t, term, 0, 4, "")

	WriteF(t, term, "\x1b[H\x1bJ")
	WriteF(t, term, "\x1b[1;1HA")
	WriteF(t, term, "\x1b[2;1HB")
	WriteF(t, term, "\x1b[3;1HC")
	WriteF(t, term, "\x1b[4;1HD")
	WriteF(t, term, "\x1b[5;1HE")
	WriteF(t, term, "\x1b[3;3H")
	WriteF(t, term, "\x1b[3S") // scroll in place, leaves cursor where it is
	CheckContent(t, term, 0, 0, "D")
	CheckContent(t, term, 0, 1, "E")
	CheckContent(t, term, 0, 2, "")
	CheckContent(t, term, 0, 3, "")
	CheckContent(t, term, 0, 4, "")
	CheckPos(t, term, 2, 2)
}

// TestScrollDown tests scrolling down. The cursor position is retained.
func TestScrollDown(t *testing.T) {
	term := vt.NewMockTerm(vt.MockOptSize{X: 10, Y: 5})
	defer MustClose(t, term)
	MustStart(t, term)

	WriteF(t, term, "\x1b[H\x1b[J") // home and clear
	WriteF(t, term, "\x1b[1;1HA")
	WriteF(t, term, "\x1b[2;1HB")
	WriteF(t, term, "\x1b[4;1HC") // first column on penultimate row
	WriteF(t, term, "\x1b[5;1HD") // first column on last row
	CheckContent(t, term, 0, 3, "C")
	CheckContent(t, term, 0, 4, "D")
	WriteF(t, term, "\x1b[1;5H") // fifth column on first row
	CheckPos(t, term, 4, 0)
	WriteF(t, term, "\x1bM")
	CheckPos(t, term, 4, 0)
	CheckContent(t, term, 0, 0, "")
	CheckContent(t, term, 0, 1, "A")
	CheckContent(t, term, 0, 2, "B")
	CheckContent(t, term, 0, 3, "")
	CheckContent(t, term, 0, 4, "C")

	WriteF(t, term, "\x1b[H\x1bJ")
	WriteF(t, term, "\x1b[1;1HA")
	WriteF(t, term, "\x1b[2;1HB")
	WriteF(t, term, "\x1b[3;1HC")
	WriteF(t, term, "\x1b[4;1HD")
	WriteF(t, term, "\x1b[5;1HE")
	WriteF(t, term, "\x1b[3;3H")
	WriteF(t, term, "\x1b[3T") // scroll in place, leaves cursor where it is
	CheckContent(t, term, 0, 0, "")
	CheckContent(t, term, 0, 1, "")
	CheckContent(t, term, 0, 2, "")
	CheckContent(t, term, 0, 3, "A")
	CheckContent(t, term, 0, 4, "B")
	CheckPos(t, term, 2, 2)
}