summaryrefslogtreecommitdiffstats
path: root/src/libvterm/t
diff options
context:
space:
mode:
Diffstat (limited to 'src/libvterm/t')
-rw-r--r--src/libvterm/t/02parser.test266
-rw-r--r--src/libvterm/t/03encoding_utf8.test122
-rw-r--r--src/libvterm/t/10state_putglyph.test74
-rw-r--r--src/libvterm/t/11state_movecursor.test224
-rw-r--r--src/libvterm/t/12state_scroll.test156
-rw-r--r--src/libvterm/t/13state_edit.test304
-rw-r--r--src/libvterm/t/14state_encoding.test105
-rw-r--r--src/libvterm/t/15state_mode.test86
-rw-r--r--src/libvterm/t/16state_resize.test48
-rw-r--r--src/libvterm/t/17state_mouse.test181
-rw-r--r--src/libvterm/t/18state_termprops.test42
-rw-r--r--src/libvterm/t/20state_wrapping.test69
-rw-r--r--src/libvterm/t/21state_tabstops.test60
-rw-r--r--src/libvterm/t/22state_save.test64
-rw-r--r--src/libvterm/t/25state_input.test155
-rw-r--r--src/libvterm/t/26state_query.test67
-rw-r--r--src/libvterm/t/27state_reset.test32
-rw-r--r--src/libvterm/t/28state_dbl_wh.test61
-rw-r--r--src/libvterm/t/29state_fallback.test31
-rw-r--r--src/libvterm/t/30state_pen.test125
-rw-r--r--src/libvterm/t/31state_rep.test128
-rw-r--r--src/libvterm/t/32state_flow.test28
-rw-r--r--src/libvterm/t/40state_selection.test55
-rw-r--r--src/libvterm/t/60screen_ascii.test69
-rw-r--r--src/libvterm/t/61screen_unicode.test47
-rw-r--r--src/libvterm/t/62screen_damage.test155
-rw-r--r--src/libvterm/t/63screen_resize.test117
-rw-r--r--src/libvterm/t/64screen_pen.test61
-rw-r--r--src/libvterm/t/65screen_protect.test16
-rw-r--r--src/libvterm/t/66screen_extent.test11
-rw-r--r--src/libvterm/t/67screen_dbl_wh.test38
-rw-r--r--src/libvterm/t/68screen_termprops.test17
-rw-r--r--src/libvterm/t/69screen_reflow.test79
-rw-r--r--src/libvterm/t/90vttest_01-movement-1.test87
-rw-r--r--src/libvterm/t/90vttest_01-movement-2.test40
-rw-r--r--src/libvterm/t/90vttest_01-movement-3.test21
-rw-r--r--src/libvterm/t/90vttest_01-movement-4.test36
-rw-r--r--src/libvterm/t/90vttest_02-screen-1.test18
-rw-r--r--src/libvterm/t/90vttest_02-screen-2.test29
-rw-r--r--src/libvterm/t/90vttest_02-screen-3.test16
-rw-r--r--src/libvterm/t/90vttest_02-screen-4.test17
-rw-r--r--src/libvterm/t/92lp1640917.test13
-rw-r--r--src/libvterm/t/harness.c1233
-rw-r--r--src/libvterm/t/run-test.pl233
44 files changed, 4836 insertions, 0 deletions
diff --git a/src/libvterm/t/02parser.test b/src/libvterm/t/02parser.test
new file mode 100644
index 0000000..2cc51dc
--- /dev/null
+++ b/src/libvterm/t/02parser.test
@@ -0,0 +1,266 @@
+INIT
+UTF8 0
+WANTPARSER
+
+!Basic text
+PUSH "hello"
+ text 0x68, 0x65, 0x6c, 0x6c, 0x6f
+
+!C0
+PUSH "\x03"
+ control 3
+
+PUSH "\x1f"
+ control 0x1f
+
+!C1 8bit
+PUSH "\x83"
+ control 0x83
+
+PUSH "\x99"
+ control 0x99
+
+!C1 7bit
+PUSH "\e\x43"
+ control 0x83
+
+PUSH "\e\x59"
+ control 0x99
+
+!High bytes
+PUSH "\xa0\xcc\xfe"
+ text 0xa0, 0xcc, 0xfe
+
+!Mixed
+PUSH "1\n2"
+ text 0x31
+ control 10
+ text 0x32
+
+!Escape
+PUSH "\e="
+ escape "="
+
+!Escape 2-byte
+PUSH "\e(X"
+ escape "(X"
+
+!Split write Escape
+PUSH "\e("
+PUSH "Y"
+ escape "(Y"
+
+!Escape cancels Escape, starts another
+PUSH "\e(\e)Z"
+ escape ")Z"
+
+!CAN cancels Escape, returns to normal mode
+PUSH "\e(\x{18}AB"
+ text 0x41, 0x42
+
+!C0 in Escape interrupts and continues
+PUSH "\e(\nX"
+ control 10
+ escape "(X"
+
+!CSI 0 args
+PUSH "\e[a"
+ csi 0x61 *
+
+!CSI 1 arg
+PUSH "\e[9b"
+ csi 0x62 9
+
+!CSI 2 args
+PUSH "\e[3;4c"
+ csi 0x63 3,4
+
+!CSI 1 arg 1 sub
+PUSH "\e[1:2c"
+ csi 0x63 1+,2
+
+!CSI many digits
+PUSH "\e[678d"
+ csi 0x64 678
+
+!CSI leading zero
+PUSH "\e[007e"
+ csi 0x65 7
+
+!CSI qmark
+PUSH "\e[?2;7f"
+ csi 0x66 L=3f 2,7
+
+!CSI greater
+PUSH "\e[>c"
+ csi 0x63 L=3e *
+
+!CSI SP
+PUSH "\e[12 q"
+ csi 0x71 12 I=20
+
+!Mixed CSI
+PUSH "A\e[8mB"
+ text 0x41
+ csi 0x6d 8
+ text 0x42
+
+!Split write
+PUSH "\e"
+PUSH "[a"
+ csi 0x61 *
+PUSH "foo\e["
+ text 0x66, 0x6f, 0x6f
+PUSH "4b"
+ csi 0x62 4
+PUSH "\e[12;"
+PUSH "3c"
+ csi 0x63 12,3
+
+!Escape cancels CSI, starts Escape
+PUSH "\e[123\e9"
+ escape "9"
+
+!CAN cancels CSI, returns to normal mode
+PUSH "\e[12\x{18}AB"
+ text 0x41, 0x42
+
+!C0 in Escape interrupts and continues
+PUSH "\e[12\n;3X"
+ control 10
+ csi 0x58 12,3
+
+!OSC BEL
+PUSH "\e]1;Hello\x07"
+ osc [1 "Hello"]
+
+!OSC ST (7bit)
+PUSH "\e]1;Hello\e\\"
+ osc [1 "Hello"]
+
+!OSC ST (8bit)
+PUSH "\x{9d}1;Hello\x9c"
+ osc [1 "Hello"]
+
+!OSC in parts
+PUSH "\e]52;abc"
+ osc [52 "abc"
+PUSH "def"
+ osc "def"
+PUSH "ghi\e\\"
+ osc "ghi"]
+
+!OSC BEL without semicolon
+PUSH "\e]1234\x07"
+ osc [1234 ]
+
+!OSC ST without semicolon
+PUSH "\e]1234\e\\"
+ osc [1234 ]
+
+!Escape cancels OSC, starts Escape
+PUSH "\e]Something\e9"
+ escape "9"
+
+!CAN cancels OSC, returns to normal mode
+PUSH "\e]12\x{18}AB"
+ text 0x41, 0x42
+
+!C0 in OSC interrupts and continues
+PUSH "\e]2;\nBye\x07"
+ osc [2 ""
+ control 10
+ osc "Bye"]
+
+!DCS BEL
+PUSH "\ePHello\x07"
+ dcs ["Hello"]
+
+!DCS ST (7bit)
+PUSH "\ePHello\e\\"
+ dcs ["Hello"]
+
+!DCS ST (8bit)
+PUSH "\x{90}Hello\x9c"
+ dcs ["Hello"]
+
+!Split write of 7bit ST
+PUSH "\ePABC\e"
+ dcs ["ABC"
+PUSH "\\"
+ dcs ]
+
+!Escape cancels DCS, starts Escape
+PUSH "\ePSomething\e9"
+ escape "9"
+
+!CAN cancels DCS, returns to normal mode
+PUSH "\eP12\x{18}AB"
+ text 0x41, 0x42
+
+!C0 in OSC interrupts and continues
+PUSH "\ePBy\ne\x07"
+ dcs ["By"
+ control 10
+ dcs "e"]
+
+!APC BEL
+PUSH "\e_Hello\x07"
+ apc ["Hello"]
+
+!APC ST (7bit)
+PUSH "\e_Hello\e\\"
+ apc ["Hello"]
+
+!APC ST (8bit)
+PUSH "\x{9f}Hello\x9c"
+ apc ["Hello"]
+
+!PM BEL
+PUSH "\e^Hello\x07"
+ pm ["Hello"]
+
+!PM ST (7bit)
+PUSH "\e^Hello\e\\"
+ pm ["Hello"]
+
+!PM ST (8bit)
+PUSH "\x{9e}Hello\x9c"
+ pm ["Hello"]
+
+!SOS BEL
+PUSH "\eXHello\x07"
+ sos ["Hello"]
+
+!SOS ST (7bit)
+PUSH "\eXHello\e\\"
+ sos ["Hello"]
+
+!SOS ST (8bit)
+PUSH "\x{98}Hello\x9c"
+ sos ["Hello"]
+
+!SOS can contain any C0 or C1 code
+PUSH "\eXABC\x01DEF\e\\"
+ sos ["ABC\x01DEF"]
+PUSH "\eXABC\x99DEF\e\\"
+ sos ["ABC\x{99}DEF"]
+
+!NUL ignored
+PUSH "\x{00}"
+
+!NUL ignored within CSI
+PUSH "\e[12\x{00}3m"
+ csi 0x6d 123
+
+!DEL ignored
+PUSH "\x{7f}"
+
+!DEL ignored within CSI
+PUSH "\e[12\x{7f}3m"
+ csi 0x6d 123
+
+!DEL inside text"
+PUSH "AB\x{7f}C"
+ text 0x41,0x42
+ text 0x43
diff --git a/src/libvterm/t/03encoding_utf8.test b/src/libvterm/t/03encoding_utf8.test
new file mode 100644
index 0000000..7ee16ac
--- /dev/null
+++ b/src/libvterm/t/03encoding_utf8.test
@@ -0,0 +1,122 @@
+INIT
+WANTENCODING
+
+!Low
+ENCIN "123"
+ encout 0x31,0x32,0x33
+
+# We want to prove the UTF-8 parser correctly handles all the sequences.
+# Easy way to do this is to check it does low/high boundary cases, as that
+# leaves only two for each sequence length
+#
+# These ranges are therefore:
+#
+# Two bytes:
+# U+0080 = 000 10000000 => 00010 000000
+# => 11000010 10000000 = C2 80
+# U+07FF = 111 11111111 => 11111 111111
+# => 11011111 10111111 = DF BF
+#
+# Three bytes:
+# U+0800 = 00001000 00000000 => 0000 100000 000000
+# => 11100000 10100000 10000000 = E0 A0 80
+# U+FFFD = 11111111 11111101 => 1111 111111 111101
+# => 11101111 10111111 10111101 = EF BF BD
+# (We avoid U+FFFE and U+FFFF as they're invalid codepoints)
+#
+# Four bytes:
+# U+10000 = 00001 00000000 00000000 => 000 010000 000000 000000
+# => 11110000 10010000 10000000 10000000 = F0 90 80 80
+# U+1FFFFF = 11111 11111111 11111111 => 111 111111 111111 111111
+# => 11110111 10111111 10111111 10111111 = F7 BF BF BF
+
+!2 byte
+ENCIN "\xC2\x80\xDF\xBF"
+ encout 0x0080, 0x07FF
+
+!3 byte
+ENCIN "\xE0\xA0\x80\xEF\xBF\xBD"
+ encout 0x0800,0xFFFD
+
+!4 byte
+ENCIN "\xF0\x90\x80\x80\xF7\xBF\xBF\xBF"
+ encout 0x10000,0x1fffff
+
+# Next up, we check some invalid sequences
+# + Early termination (back to low bytes too soon)
+# + Early restart (another sequence introduction before the previous one was finished)
+
+!Early termination
+ENCIN "\xC2!"
+ encout 0xfffd,0x21
+
+ENCIN "\xE0!\xE0\xA0!"
+ encout 0xfffd,0x21,0xfffd,0x21
+
+ENCIN "\xF0!\xF0\x90!\xF0\x90\x80!"
+ encout 0xfffd,0x21,0xfffd,0x21,0xfffd,0x21
+
+!Early restart
+ENCIN "\xC2\xC2\x90"
+ encout 0xfffd,0x0090
+
+ENCIN "\xE0\xC2\x90\xE0\xA0\xC2\x90"
+ encout 0xfffd,0x0090,0xfffd,0x0090
+
+ENCIN "\xF0\xC2\x90\xF0\x90\xC2\x90\xF0\x90\x80\xC2\x90"
+ encout 0xfffd,0x0090,0xfffd,0x0090,0xfffd,0x0090
+
+# Test the overlong sequences by giving an overlong encoding of U+0000 and
+# an encoding of the highest codepoint still too short
+#
+# Two bytes:
+# U+0000 = C0 80
+# U+007F = 000 01111111 => 00001 111111 =>
+# => 11000001 10111111 => C1 BF
+#
+# Three bytes:
+# U+0000 = E0 80 80
+# U+07FF = 00000111 11111111 => 0000 011111 111111
+# => 11100000 10011111 10111111 = E0 9F BF
+#
+# Four bytes:
+# U+0000 = F0 80 80 80
+# U+FFFF = 11111111 11111111 => 000 001111 111111 111111
+# => 11110000 10001111 10111111 10111111 = F0 8F BF BF
+
+!Overlong
+ENCIN "\xC0\x80\xC1\xBF"
+ encout 0xfffd,0xfffd
+
+ENCIN "\xE0\x80\x80\xE0\x9F\xBF"
+ encout 0xfffd,0xfffd
+
+ENCIN "\xF0\x80\x80\x80\xF0\x8F\xBF\xBF"
+ encout 0xfffd,0xfffd
+
+# UTF-16 surrogates U+D800 and U+DFFF
+!UTF-16 Surrogates
+ENCIN "\xED\xA0\x80\xED\xBF\xBF"
+ encout 0xfffd,0xfffd
+
+!Split write
+ENCIN "\xC2"
+ENCIN "\xA0"
+ encout 0x000A0
+
+ENCIN "\xE0"
+ENCIN "\xA0\x80"
+ encout 0x00800
+ENCIN "\xE0\xA0"
+ENCIN "\x80"
+ encout 0x00800
+
+ENCIN "\xF0"
+ENCIN "\x90\x80\x80"
+ encout 0x10000
+ENCIN "\xF0\x90"
+ENCIN "\x80\x80"
+ encout 0x10000
+ENCIN "\xF0\x90\x80"
+ENCIN "\x80"
+ encout 0x10000
diff --git a/src/libvterm/t/10state_putglyph.test b/src/libvterm/t/10state_putglyph.test
new file mode 100644
index 0000000..c82c525
--- /dev/null
+++ b/src/libvterm/t/10state_putglyph.test
@@ -0,0 +1,74 @@
+INIT
+UTF8 1
+WANTSTATE g
+
+!Low
+RESET
+PUSH "ABC"
+ putglyph 0x41 1 0,0
+ putglyph 0x42 1 0,1
+ putglyph 0x43 1 0,2
+
+!UTF-8 1 char
+# U+00C1 = 0xC3 0x81 name: LATIN CAPITAL LETTER A WITH ACUTE
+# U+00E9 = 0xC3 0xA9 name: LATIN SMALL LETTER E WITH ACUTE
+RESET
+PUSH "\xC3\x81\xC3\xA9"
+ putglyph 0xc1 1 0,0
+ putglyph 0xe9 1 0,1
+
+!UTF-8 split writes
+RESET
+PUSH "\xC3"
+PUSH "\x81"
+ putglyph 0xc1 1 0,0
+
+!UTF-8 wide char
+# U+FF10 = 0xEF 0xBC 0x90 name: FULLWIDTH DIGIT ZERO
+RESET
+PUSH "\xEF\xBC\x90 "
+ putglyph 0xff10 2 0,0
+ putglyph 0x20 1 0,2
+
+!UTF-8 emoji wide char
+# U+1F600 = 0xF0 0x9F 0x98 0x80 name: GRINNING FACE
+RESET
+PUSH "\xF0\x9F\x98\x80 "
+ putglyph 0x1f600 2 0,0
+ putglyph 0x20 1 0,2
+
+!UTF-8 combining chars
+# U+0301 = 0xCC 0x81 name: COMBINING ACUTE
+RESET
+PUSH "e\xCC\x81Z"
+ putglyph 0x65,0x301 1 0,0
+ putglyph 0x5a 1 0,1
+
+!Combining across buffers
+RESET
+PUSH "e"
+ putglyph 0x65 1 0,0
+PUSH "\xCC\x81Z"
+ putglyph 0x65,0x301 1 0,0
+ putglyph 0x5a 1 0,1
+
+!Spare combining chars get truncated
+RESET
+PUSH "e" . "\xCC\x81" x 10
+ putglyph 0x65,0x301,0x301,0x301,0x301,0x301 1 0,0
+ # and nothing more
+
+RESET
+PUSH "e"
+ putglyph 0x65 1 0,0
+PUSH "\xCC\x81"
+ putglyph 0x65,0x301 1 0,0
+PUSH "\xCC\x82"
+ putglyph 0x65,0x301,0x302 1 0,0
+
+!DECSCA protected
+RESET
+PUSH "A\e[1\"qB\e[2\"qC"
+ putglyph 0x41 1 0,0
+ putglyph 0x42 1 0,1 prot
+ putglyph 0x43 1 0,2
diff --git a/src/libvterm/t/11state_movecursor.test b/src/libvterm/t/11state_movecursor.test
new file mode 100644
index 0000000..26e447e
--- /dev/null
+++ b/src/libvterm/t/11state_movecursor.test
@@ -0,0 +1,224 @@
+INIT
+UTF8 1
+WANTSTATE
+
+!Implicit
+PUSH "ABC"
+ ?cursor = 0,3
+!Backspace
+PUSH "\b"
+ ?cursor = 0,2
+!Horizontal Tab
+PUSH "\t"
+ ?cursor = 0,8
+!Carriage Return
+PUSH "\r"
+ ?cursor = 0,0
+!Linefeed
+PUSH "\n"
+ ?cursor = 1,0
+
+!Backspace bounded by lefthand edge
+PUSH "\e[4;2H"
+ ?cursor = 3,1
+PUSH "\b"
+ ?cursor = 3,0
+PUSH "\b"
+ ?cursor = 3,0
+
+!Backspace cancels phantom
+PUSH "\e[4;80H"
+ ?cursor = 3,79
+PUSH "X"
+ ?cursor = 3,79
+PUSH "\b"
+ ?cursor = 3,78
+
+!HT bounded by righthand edge
+PUSH "\e[1;78H"
+ ?cursor = 0,77
+PUSH "\t"
+ ?cursor = 0,79
+PUSH "\t"
+ ?cursor = 0,79
+
+RESET
+
+!Index
+PUSH "ABC\eD"
+ ?cursor = 1,3
+!Reverse Index
+PUSH "\eM"
+ ?cursor = 0,3
+!Newline
+PUSH "\eE"
+ ?cursor = 1,0
+
+RESET
+
+!Cursor Forward
+PUSH "\e[B"
+ ?cursor = 1,0
+PUSH "\e[3B"
+ ?cursor = 4,0
+PUSH "\e[0B"
+ ?cursor = 5,0
+
+!Cursor Down
+PUSH "\e[C"
+ ?cursor = 5,1
+PUSH "\e[3C"
+ ?cursor = 5,4
+PUSH "\e[0C"
+ ?cursor = 5,5
+
+!Cursor Up
+PUSH "\e[A"
+ ?cursor = 4,5
+PUSH "\e[3A"
+ ?cursor = 1,5
+PUSH "\e[0A"
+ ?cursor = 0,5
+
+!Cursor Backward
+PUSH "\e[D"
+ ?cursor = 0,4
+PUSH "\e[3D"
+ ?cursor = 0,1
+PUSH "\e[0D"
+ ?cursor = 0,0
+
+!Cursor Next Line
+PUSH " "
+ ?cursor = 0,3
+PUSH "\e[E"
+ ?cursor = 1,0
+PUSH " "
+ ?cursor = 1,3
+PUSH "\e[2E"
+ ?cursor = 3,0
+PUSH "\e[0E"
+ ?cursor = 4,0
+
+!Cursor Previous Line
+PUSH " "
+ ?cursor = 4,3
+PUSH "\e[F"
+ ?cursor = 3,0
+PUSH " "
+ ?cursor = 3,3
+PUSH "\e[2F"
+ ?cursor = 1,0
+PUSH "\e[0F"
+ ?cursor = 0,0
+
+!Cursor Horizontal Absolute
+PUSH "\n"
+ ?cursor = 1,0
+PUSH "\e[20G"
+ ?cursor = 1,19
+PUSH "\e[G"
+ ?cursor = 1,0
+
+!Cursor Position
+PUSH "\e[10;5H"
+ ?cursor = 9,4
+PUSH "\e[8H"
+ ?cursor = 7,0
+PUSH "\e[H"
+ ?cursor = 0,0
+
+!Cursor Position cancels phantom
+PUSH "\e[10;78H"
+ ?cursor = 9,77
+PUSH "ABC"
+ ?cursor = 9,79
+PUSH "\e[10;80H"
+PUSH "C"
+ ?cursor = 9,79
+PUSH "X"
+ ?cursor = 10,1
+
+RESET
+
+!Bounds Checking
+PUSH "\e[A"
+ ?cursor = 0,0
+PUSH "\e[D"
+ ?cursor = 0,0
+PUSH "\e[25;80H"
+ ?cursor = 24,79
+PUSH "\e[B"
+ ?cursor = 24,79
+PUSH "\e[C"
+ ?cursor = 24,79
+PUSH "\e[E"
+ ?cursor = 24,0
+PUSH "\e[H"
+ ?cursor = 0,0
+PUSH "\e[F"
+ ?cursor = 0,0
+PUSH "\e[999G"
+ ?cursor = 0,79
+PUSH "\e[99;99H"
+ ?cursor = 24,79
+
+RESET
+
+!Horizontal Position Absolute
+PUSH "\e[5`"
+ ?cursor = 0,4
+
+!Horizontal Position Relative
+PUSH "\e[3a"
+ ?cursor = 0,7
+
+!Horizontal Position Backward
+PUSH "\e[3j"
+ ?cursor = 0,4
+
+!Horizontal and Vertical Position
+PUSH "\e[3;3f"
+ ?cursor = 2,2
+
+!Vertical Position Absolute
+PUSH "\e[5d"
+ ?cursor = 4,2
+
+!Vertical Position Relative
+PUSH "\e[2e"
+ ?cursor = 6,2
+
+!Vertical Position Backward
+PUSH "\e[2k"
+ ?cursor = 4,2
+
+RESET
+
+!Horizontal Tab
+PUSH "\t"
+ ?cursor = 0,8
+PUSH " "
+ ?cursor = 0,11
+PUSH "\t"
+ ?cursor = 0,16
+PUSH " "
+ ?cursor = 0,23
+PUSH "\t"
+ ?cursor = 0,24
+PUSH " "
+ ?cursor = 0,32
+PUSH "\t"
+ ?cursor = 0,40
+
+!Cursor Horizontal Tab
+PUSH "\e[I"
+ ?cursor = 0,48
+PUSH "\e[2I"
+ ?cursor = 0,64
+
+!Cursor Backward Tab
+PUSH "\e[Z"
+ ?cursor = 0,56
+PUSH "\e[2Z"
+ ?cursor = 0,40
diff --git a/src/libvterm/t/12state_scroll.test b/src/libvterm/t/12state_scroll.test
new file mode 100644
index 0000000..c1f2791
--- /dev/null
+++ b/src/libvterm/t/12state_scroll.test
@@ -0,0 +1,156 @@
+INIT
+UTF8 1
+WANTSTATE s
+
+!Linefeed
+PUSH "\n"x24
+ ?cursor = 24,0
+PUSH "\n"
+ scrollrect 0..25,0..80 => +1,+0
+ ?cursor = 24,0
+
+RESET
+
+!Index
+PUSH "\e[25H"
+PUSH "\eD"
+ scrollrect 0..25,0..80 => +1,+0
+
+RESET
+
+!Reverse Index
+PUSH "\eM"
+ scrollrect 0..25,0..80 => -1,+0
+
+RESET
+
+!Linefeed in DECSTBM
+PUSH "\e[1;10r"
+ ?cursor = 0,0
+PUSH "\n"x9
+ ?cursor = 9,0
+PUSH "\n"
+ scrollrect 0..10,0..80 => +1,+0
+ ?cursor = 9,0
+
+!Linefeed outside DECSTBM
+PUSH "\e[20H"
+ ?cursor = 19,0
+PUSH "\n"
+ ?cursor = 20,0
+
+!Index in DECSTBM
+PUSH "\e[9;10r"
+PUSH "\e[10H"
+PUSH "\eM"
+ ?cursor = 8,0
+PUSH "\eM"
+ scrollrect 8..10,0..80 => -1,+0
+
+!Reverse Index in DECSTBM
+PUSH "\e[25H"
+ ?cursor = 24,0
+PUSH "\n"
+ # no scrollrect
+ ?cursor = 24,0
+
+!Linefeed in DECSTBM+DECSLRM
+PUSH "\e[?69h"
+PUSH "\e[3;10r\e[10;40s"
+PUSH "\e[10;10H\n"
+ scrollrect 2..10,9..40 => +1,+0
+
+!IND/RI in DECSTBM+DECSLRM
+PUSH "\eD"
+ scrollrect 2..10,9..40 => +1,+0
+PUSH "\e[3;10H\eM"
+ scrollrect 2..10,9..40 => -1,+0
+
+!DECRQSS on DECSTBM
+PUSH "\eP\$qr\e\\"
+ output "\eP1\$r3;10r\e\\"
+
+!DECRQSS on DECSLRM
+PUSH "\eP\$qs\e\\"
+ output "\eP1\$r10;40s\e\\"
+
+!Setting invalid DECSLRM with !DECVSSM is still rejected
+PUSH "\e[?69l\e[;0s\e[?69h"
+
+RESET
+
+!Scroll Down
+PUSH "\e[S"
+ scrollrect 0..25,0..80 => +1,+0
+ ?cursor = 0,0
+PUSH "\e[2S"
+ scrollrect 0..25,0..80 => +2,+0
+ ?cursor = 0,0
+PUSH "\e[100S"
+ scrollrect 0..25,0..80 => +25,+0
+
+!Scroll Up
+PUSH "\e[T"
+ scrollrect 0..25,0..80 => -1,+0
+ ?cursor = 0,0
+PUSH "\e[2T"
+ scrollrect 0..25,0..80 => -2,+0
+ ?cursor = 0,0
+PUSH "\e[100T"
+ scrollrect 0..25,0..80 => -25,+0
+
+!SD/SU in DECSTBM
+PUSH "\e[5;20r"
+PUSH "\e[S"
+ scrollrect 4..20,0..80 => +1,+0
+PUSH "\e[T"
+ scrollrect 4..20,0..80 => -1,+0
+
+RESET
+
+!SD/SU in DECSTBM+DECSLRM
+PUSH "\e[?69h"
+PUSH "\e[3;10r\e[10;40s"
+ ?cursor = 0,0
+PUSH "\e[3;10H"
+ ?cursor = 2,9
+PUSH "\e[S"
+ scrollrect 2..10,9..40 => +1,+0
+PUSH "\e[?69l"
+PUSH "\e[S"
+ scrollrect 2..10,0..80 => +1,+0
+
+!Invalid boundaries
+RESET
+
+PUSH "\e[100;105r\eD"
+PUSH "\e[5;2r\eD"
+
+RESET
+WANTSTATE -s+me
+
+!Scroll Down move+erase emulation
+PUSH "\e[S"
+ moverect 1..25,0..80 -> 0..24,0..80
+ erase 24..25,0..80
+ ?cursor = 0,0
+PUSH "\e[2S"
+ moverect 2..25,0..80 -> 0..23,0..80
+ erase 23..25,0..80
+ ?cursor = 0,0
+
+!Scroll Up move+erase emulation
+PUSH "\e[T"
+ moverect 0..24,0..80 -> 1..25,0..80
+ erase 0..1,0..80
+ ?cursor = 0,0
+PUSH "\e[2T"
+ moverect 0..23,0..80 -> 2..25,0..80
+ erase 0..2,0..80
+ ?cursor = 0,0
+
+!DECSTBM resets cursor position
+PUSH "\e[5;5H"
+ ?cursor = 4,4
+PUSH "\e[r"
+ ?cursor = 0,0
diff --git a/src/libvterm/t/13state_edit.test b/src/libvterm/t/13state_edit.test
new file mode 100644
index 0000000..d3f3e9e
--- /dev/null
+++ b/src/libvterm/t/13state_edit.test
@@ -0,0 +1,304 @@
+INIT
+UTF8 1
+WANTSTATE seb
+
+!ICH
+RESET
+ erase 0..25,0..80
+ ?cursor = 0,0
+PUSH "ACD"
+PUSH "\e[2D"
+ ?cursor = 0,1
+PUSH "\e[@"
+ scrollrect 0..1,1..80 => +0,-1
+ ?cursor = 0,1
+PUSH "B"
+ ?cursor = 0,2
+PUSH "\e[3@"
+ scrollrect 0..1,2..80 => +0,-3
+
+!ICH with DECSLRM
+PUSH "\e[?69h"
+PUSH "\e[;50s"
+PUSH "\e[20G\e[@"
+ scrollrect 0..1,19..50 => +0,-1
+
+!ICH outside DECSLRM
+PUSH "\e[70G\e[@"
+ # nothing happens
+
+!DCH
+RESET
+ erase 0..25,0..80
+ ?cursor = 0,0
+PUSH "ABBC"
+PUSH "\e[3D"
+ ?cursor = 0,1
+PUSH "\e[P"
+ scrollrect 0..1,1..80 => +0,+1
+ ?cursor = 0,1
+PUSH "\e[3P"
+ scrollrect 0..1,1..80 => +0,+3
+ ?cursor = 0,1
+
+!DCH with DECSLRM
+PUSH "\e[?69h"
+PUSH "\e[;50s"
+PUSH "\e[20G\e[P"
+ scrollrect 0..1,19..50 => +0,+1
+
+!DCH outside DECSLRM
+PUSH "\e[70G\e[P"
+ # nothing happens
+
+!ECH
+RESET
+ erase 0..25,0..80
+ ?cursor = 0,0
+PUSH "ABC"
+PUSH "\e[2D"
+ ?cursor = 0,1
+PUSH "\e[X"
+ erase 0..1,1..2
+ ?cursor = 0,1
+PUSH "\e[3X"
+ erase 0..1,1..4
+ ?cursor = 0,1
+# ECH more columns than there are should be bounded
+PUSH "\e[100X"
+ erase 0..1,1..80
+
+!IL
+RESET
+ erase 0..25,0..80
+ ?cursor = 0,0
+PUSH "A\r\nC"
+ ?cursor = 1,1
+PUSH "\e[L"
+ scrollrect 1..25,0..80 => -1,+0
+ # TODO: ECMA-48 says we should move to line home, but neither xterm nor
+ # xfce4-terminal do this
+ ?cursor = 1,1
+PUSH "\rB"
+ ?cursor = 1,1
+PUSH "\e[3L"
+ scrollrect 1..25,0..80 => -3,+0
+
+!IL with DECSTBM
+PUSH "\e[5;15r"
+PUSH "\e[5H\e[L"
+ scrollrect 4..15,0..80 => -1,+0
+
+!IL outside DECSTBM
+PUSH "\e[20H\e[L"
+ # nothing happens
+
+!IL with DECSTBM+DECSLRM
+PUSH "\e[?69h"
+PUSH "\e[10;50s"
+PUSH "\e[5;10H\e[L"
+ scrollrect 4..15,9..50 => -1,+0
+
+!DL
+RESET
+ erase 0..25,0..80
+ ?cursor = 0,0
+PUSH "A\r\nB\r\nB\r\nC"
+ ?cursor = 3,1
+PUSH "\e[2H"
+ ?cursor = 1,0
+PUSH "\e[M"
+ scrollrect 1..25,0..80 => +1,+0
+ ?cursor = 1,0
+PUSH "\e[3M"
+ scrollrect 1..25,0..80 => +3,+0
+ ?cursor = 1,0
+
+!DL with DECSTBM
+PUSH "\e[5;15r"
+PUSH "\e[5H\e[M"
+ scrollrect 4..15,0..80 => +1,+0
+
+!DL outside DECSTBM
+PUSH "\e[20H\e[M"
+ # nothing happens
+
+!DL with DECSTBM+DECSLRM
+PUSH "\e[?69h"
+PUSH "\e[10;50s"
+PUSH "\e[5;10H\e[M"
+ scrollrect 4..15,9..50 => +1,+0
+
+!DECIC
+RESET
+ erase 0..25,0..80
+PUSH "\e[20G\e[5'}"
+ scrollrect 0..25,19..80 => +0,-5
+
+!DECIC with DECSTBM+DECSLRM
+PUSH "\e[?69h"
+PUSH "\e[4;20r\e[20;60s"
+PUSH "\e[4;20H\e[3'}"
+ scrollrect 3..20,19..60 => +0,-3
+
+!DECIC outside DECSLRM
+PUSH "\e[70G\e['}"
+ # nothing happens
+
+!DECDC
+RESET
+ erase 0..25,0..80
+PUSH "\e[20G\e[5'~"
+ scrollrect 0..25,19..80 => +0,+5
+
+!DECDC with DECSTBM+DECSLRM
+PUSH "\e[?69h"
+PUSH "\e[4;20r\e[20;60s"
+PUSH "\e[4;20H\e[3'~"
+ scrollrect 3..20,19..60 => +0,+3
+
+!DECDC outside DECSLRM
+PUSH "\e[70G\e['~"
+ # nothing happens
+
+!EL 0
+RESET
+ erase 0..25,0..80
+ ?cursor = 0,0
+PUSH "ABCDE"
+PUSH "\e[3D"
+ ?cursor = 0,2
+PUSH "\e[0K"
+ erase 0..1,2..80
+ ?cursor = 0,2
+
+!EL 1
+RESET
+ erase 0..25,0..80
+ ?cursor = 0,0
+PUSH "ABCDE"
+PUSH "\e[3D"
+ ?cursor = 0,2
+PUSH "\e[1K"
+ erase 0..1,0..3
+ ?cursor = 0,2
+
+!EL 2
+RESET
+ erase 0..25,0..80
+ ?cursor = 0,0
+PUSH "ABCDE"
+PUSH "\e[3D"
+ ?cursor = 0,2
+PUSH "\e[2K"
+ erase 0..1,0..80
+ ?cursor = 0,2
+
+!SEL
+RESET
+ erase 0..25,0..80
+ ?cursor = 0,0
+PUSH "\e[11G"
+ ?cursor = 0,10
+PUSH "\e[?0K"
+ erase 0..1,10..80 selective
+ ?cursor = 0,10
+PUSH "\e[?1K"
+ erase 0..1,0..11 selective
+ ?cursor = 0,10
+PUSH "\e[?2K"
+ erase 0..1,0..80 selective
+ ?cursor = 0,10
+
+!ED 0
+RESET
+ erase 0..25,0..80
+ ?cursor = 0,0
+PUSH "\e[2;2H"
+ ?cursor = 1,1
+PUSH "\e[0J"
+ erase 1..2,1..80
+ erase 2..25,0..80
+ ?cursor = 1,1
+
+!ED 1
+RESET
+ erase 0..25,0..80
+ ?cursor = 0,0
+PUSH "\e[2;2H"
+ ?cursor = 1,1
+PUSH "\e[1J"
+ erase 0..1,0..80
+ erase 1..2,0..2
+ ?cursor = 1,1
+
+!ED 2
+RESET
+ erase 0..25,0..80
+ ?cursor = 0,0
+PUSH "\e[2;2H"
+ ?cursor = 1,1
+PUSH "\e[2J"
+ erase 0..25,0..80
+ ?cursor = 1,1
+
+!ED 3
+PUSH "\e[3J"
+ sb_clear
+
+!SED
+RESET
+ erase 0..25,0..80
+PUSH "\e[5;5H"
+ ?cursor = 4,4
+PUSH "\e[?0J"
+ erase 4..5,4..80 selective
+ erase 5..25,0..80 selective
+ ?cursor = 4,4
+PUSH "\e[?1J"
+ erase 0..4,0..80 selective
+ erase 4..5,0..5 selective
+ ?cursor = 4,4
+PUSH "\e[?2J"
+ erase 0..25,0..80 selective
+ ?cursor = 4,4
+
+!DECRQSS on DECSCA
+PUSH "\e[2\"q"
+PUSH "\eP\$q\"q\e\\"
+ output "\eP1\$r2\"q\e\\"
+
+WANTSTATE -s+m
+
+!ICH move+erase emuation
+RESET
+ erase 0..25,0..80
+ ?cursor = 0,0
+PUSH "ACD"
+PUSH "\e[2D"
+ ?cursor = 0,1
+PUSH "\e[@"
+ moverect 0..1,1..79 -> 0..1,2..80
+ erase 0..1,1..2
+ ?cursor = 0,1
+PUSH "B"
+ ?cursor = 0,2
+PUSH "\e[3@"
+ moverect 0..1,2..77 -> 0..1,5..80
+ erase 0..1,2..5
+
+!DCH move+erase emulation
+RESET
+ erase 0..25,0..80
+ ?cursor = 0,0
+PUSH "ABBC"
+PUSH "\e[3D"
+ ?cursor = 0,1
+PUSH "\e[P"
+ moverect 0..1,2..80 -> 0..1,1..79
+ erase 0..1,79..80
+ ?cursor = 0,1
+PUSH "\e[3P"
+ moverect 0..1,4..80 -> 0..1,1..77
+ erase 0..1,77..80
+ ?cursor = 0,1
diff --git a/src/libvterm/t/14state_encoding.test b/src/libvterm/t/14state_encoding.test
new file mode 100644
index 0000000..b1f5d69
--- /dev/null
+++ b/src/libvterm/t/14state_encoding.test
@@ -0,0 +1,105 @@
+INIT
+WANTSTATE g
+
+!Default
+RESET
+PUSH "#"
+ putglyph 0x23 1 0,0
+
+!Designate G0=UK
+RESET
+PUSH "\e(A"
+PUSH "#"
+ putglyph 0x00a3 1 0,0
+
+!Designate G0=DEC drawing
+RESET
+PUSH "\e(0"
+PUSH "a"
+ putglyph 0x2592 1 0,0
+
+!Designate G1 + LS1
+RESET
+PUSH "\e)0"
+PUSH "a"
+ putglyph 0x61 1 0,0
+PUSH "\x0e"
+PUSH "a"
+ putglyph 0x2592 1 0,1
+!LS0
+PUSH "\x0f"
+PUSH "a"
+ putglyph 0x61 1 0,2
+
+!Designate G2 + LS2
+PUSH "\e*0"
+PUSH "a"
+ putglyph 0x61 1 0,3
+PUSH "\en"
+PUSH "a"
+ putglyph 0x2592 1 0,4
+PUSH "\x0f"
+PUSH "a"
+ putglyph 0x61 1 0,5
+
+!Designate G3 + LS3
+PUSH "\e+0"
+PUSH "a"
+ putglyph 0x61 1 0,6
+PUSH "\eo"
+PUSH "a"
+ putglyph 0x2592 1 0,7
+PUSH "\x0f"
+PUSH "a"
+ putglyph 0x61 1 0,8
+
+!SS2
+PUSH "a\x{8e}aa"
+ putglyph 0x61 1 0,9
+ putglyph 0x2592 1 0,10
+ putglyph 0x61 1 0,11
+
+!SS3
+PUSH "a\x{8f}aa"
+ putglyph 0x61 1 0,12
+ putglyph 0x2592 1 0,13
+ putglyph 0x61 1 0,14
+
+!LS1R
+RESET
+PUSH "\e~"
+PUSH "\xe1"
+ putglyph 0x61 1 0,0
+PUSH "\e)0"
+PUSH "\xe1"
+ putglyph 0x2592 1 0,1
+
+!LS2R
+RESET
+PUSH "\e}"
+PUSH "\xe1"
+ putglyph 0x61 1 0,0
+PUSH "\e*0"
+PUSH "\xe1"
+ putglyph 0x2592 1 0,1
+
+!LS3R
+RESET
+PUSH "\e|"
+PUSH "\xe1"
+ putglyph 0x61 1 0,0
+PUSH "\e+0"
+PUSH "\xe1"
+ putglyph 0x2592 1 0,1
+
+UTF8 1
+
+!Mixed US-ASCII and UTF-8
+# U+0108 == 0xc4 0x88
+RESET
+PUSH "\e(B"
+PUSH "AB\xc4\x88D"
+ putglyph 0x0041 1 0,0
+ putglyph 0x0042 1 0,1
+ putglyph 0x0108 1 0,2
+ putglyph 0x0044 1 0,3
diff --git a/src/libvterm/t/15state_mode.test b/src/libvterm/t/15state_mode.test
new file mode 100644
index 0000000..b7917ad
--- /dev/null
+++ b/src/libvterm/t/15state_mode.test
@@ -0,0 +1,86 @@
+INIT
+UTF8 1
+WANTSTATE gme
+
+!Insert/Replace Mode
+RESET
+ erase 0..25,0..80
+ ?cursor = 0,0
+PUSH "AC\e[DB"
+ putglyph 0x41 1 0,0
+ putglyph 0x43 1 0,1
+ putglyph 0x42 1 0,1
+PUSH "\e[4h"
+PUSH "\e[G"
+PUSH "AC\e[DB"
+ moverect 0..1,0..79 -> 0..1,1..80
+ erase 0..1,0..1
+ putglyph 0x41 1 0,0
+ moverect 0..1,1..79 -> 0..1,2..80
+ erase 0..1,1..2
+ putglyph 0x43 1 0,1
+ moverect 0..1,1..79 -> 0..1,2..80
+ erase 0..1,1..2
+ putglyph 0x42 1 0,1
+
+!Insert mode only happens once for UTF-8 combining
+PUSH "e"
+ moverect 0..1,2..79 -> 0..1,3..80
+ erase 0..1,2..3
+ putglyph 0x65 1 0,2
+PUSH "\xCC\x81"
+ putglyph 0x65,0x301 1 0,2
+
+!Newline/Linefeed mode
+RESET
+ erase 0..25,0..80
+ ?cursor = 0,0
+PUSH "\e[5G\n"
+ ?cursor = 1,4
+PUSH "\e[20h"
+PUSH "\e[5G\n"
+ ?cursor = 2,0
+
+!DEC origin mode
+RESET
+ erase 0..25,0..80
+ ?cursor = 0,0
+PUSH "\e[5;15r"
+PUSH "\e[H"
+ ?cursor = 0,0
+PUSH "\e[3;3H"
+ ?cursor = 2,2
+PUSH "\e[?6h"
+PUSH "\e[H"
+ ?cursor = 4,0
+PUSH "\e[3;3H"
+ ?cursor = 6,2
+
+!DECRQM on DECOM
+PUSH "\e[?6h"
+PUSH "\e[?6\$p"
+ output "\e[?6;1\$y"
+PUSH "\e[?6l"
+PUSH "\e[?6\$p"
+ output "\e[?6;2\$y"
+
+!Origin mode with DECSLRM
+PUSH "\e[?6h"
+PUSH "\e[?69h"
+PUSH "\e[20;60s"
+PUSH "\e[H"
+ ?cursor = 4,19
+
+PUSH "\e[?69l"
+
+!Origin mode bounds cursor to scrolling region
+PUSH "\e[H"
+PUSH "\e[10A"
+ ?cursor = 4,0
+PUSH "\e[20B"
+ ?cursor = 14,0
+
+!Origin mode without scroll region
+PUSH "\e[?6l"
+PUSH "\e[r\e[?6h"
+ ?cursor = 0,0
diff --git a/src/libvterm/t/16state_resize.test b/src/libvterm/t/16state_resize.test
new file mode 100644
index 0000000..42c77c7
--- /dev/null
+++ b/src/libvterm/t/16state_resize.test
@@ -0,0 +1,48 @@
+INIT
+WANTSTATE g
+
+!Placement
+RESET
+PUSH "AB\e[79GCDE"
+ putglyph 0x41 1 0,0
+ putglyph 0x42 1 0,1
+ putglyph 0x43 1 0,78
+ putglyph 0x44 1 0,79
+ putglyph 0x45 1 1,0
+
+!Resize
+RESET
+RESIZE 27,85
+PUSH "AB\e[79GCDE"
+ putglyph 0x41 1 0,0
+ putglyph 0x42 1 0,1
+ putglyph 0x43 1 0,78
+ putglyph 0x44 1 0,79
+ putglyph 0x45 1 0,80
+ ?cursor = 0,81
+
+!Resize without reset
+RESIZE 28,90
+ ?cursor = 0,81
+PUSH "FGHI"
+ putglyph 0x46 1 0,81
+ putglyph 0x47 1 0,82
+ putglyph 0x48 1 0,83
+ putglyph 0x49 1 0,84
+ ?cursor = 0,85
+
+!Resize shrink moves cursor
+RESIZE 25,80
+ ?cursor = 0,79
+
+!Resize grow doesn't cancel phantom
+RESET
+PUSH "\e[79GAB"
+ putglyph 0x41 1 0,78
+ putglyph 0x42 1 0,79
+ ?cursor = 0,79
+RESIZE 30,100
+ ?cursor = 0,80
+PUSH "C"
+ putglyph 0x43 1 0,80
+ ?cursor = 0,81
diff --git a/src/libvterm/t/17state_mouse.test b/src/libvterm/t/17state_mouse.test
new file mode 100644
index 0000000..e5ba29b
--- /dev/null
+++ b/src/libvterm/t/17state_mouse.test
@@ -0,0 +1,181 @@
+INIT
+WANTSTATE p
+
+!DECRQM on with mouse off
+PUSH "\e[?1000\$p"
+ output "\e[?1000;2\$y"
+PUSH "\e[?1002\$p"
+ output "\e[?1002;2\$y"
+PUSH "\e[?1003\$p"
+ output "\e[?1003;2\$y"
+
+!Mouse in simple button report mode
+RESET
+ settermprop 1 true
+ settermprop 2 true
+ settermprop 7 1
+PUSH "\e[?1000h"
+ settermprop 8 1
+
+!Press 1
+MOUSEMOVE 0,0 0
+MOUSEBTN d 1 0
+ output "\e[M\x20\x21\x21"
+
+!Release 1
+MOUSEBTN u 1 0
+ output "\e[M\x23\x21\x21"
+
+!Ctrl-Press 1
+MOUSEBTN d 1 C
+ output "\e[M\x30\x21\x21"
+MOUSEBTN u 1 C
+ output "\e[M\x33\x21\x21"
+
+!Button 2
+MOUSEBTN d 2 0
+ output "\e[M\x21\x21\x21"
+MOUSEBTN u 2 0
+ output "\e[M\x23\x21\x21"
+
+!Position
+MOUSEMOVE 10,20 0
+MOUSEBTN d 1 0
+ output "\e[M\x20\x35\x2b"
+
+MOUSEBTN u 1 0
+ output "\e[M\x23\x35\x2b"
+MOUSEMOVE 10,21 0
+ # no output
+
+!Wheel events
+MOUSEBTN d 4 0
+ output "\e[M\x60\x36\x2b"
+MOUSEBTN d 4 0
+ output "\e[M\x60\x36\x2b"
+MOUSEBTN d 5 0
+ output "\e[M\x61\x36\x2b"
+
+!DECRQM on mouse button mode
+PUSH "\e[?1000\$p"
+ output "\e[?1000;1\$y"
+PUSH "\e[?1002\$p"
+ output "\e[?1002;2\$y"
+PUSH "\e[?1003\$p"
+ output "\e[?1003;2\$y"
+
+!Drag events
+RESET
+ settermprop 1 true
+ settermprop 2 true
+ settermprop 7 1
+PUSH "\e[?1002h"
+ settermprop 8 2
+
+MOUSEMOVE 5,5 0
+MOUSEBTN d 1 0
+ output "\e[M\x20\x26\x26"
+MOUSEMOVE 5,6 0
+ output "\e[M\x40\x27\x26"
+MOUSEMOVE 6,6 0
+ output "\e[M\x40\x27\x27"
+MOUSEMOVE 6,6 0
+ # no output
+MOUSEBTN u 1 0
+ output "\e[M\x23\x27\x27"
+MOUSEMOVE 6,7
+ # no output
+
+!DECRQM on mouse drag mode
+PUSH "\e[?1000\$p"
+ output "\e[?1000;2\$y"
+PUSH "\e[?1002\$p"
+ output "\e[?1002;1\$y"
+PUSH "\e[?1003\$p"
+ output "\e[?1003;2\$y"
+
+!Non-drag motion events
+PUSH "\e[?1003h"
+ settermprop 8 3
+
+MOUSEMOVE 6,8 0
+ output "\e[M\x43\x29\x27"
+
+!DECRQM on mouse motion mode
+PUSH "\e[?1000\$p"
+ output "\e[?1000;2\$y"
+PUSH "\e[?1002\$p"
+ output "\e[?1002;2\$y"
+PUSH "\e[?1003\$p"
+ output "\e[?1003;1\$y"
+
+!Bounds checking
+MOUSEMOVE 300,300 0
+ output "\e[M\x43\xff\xff"
+MOUSEBTN d 1 0
+ output "\e[M\x20\xff\xff"
+MOUSEBTN u 1 0
+ output "\e[M\x23\xff\xff"
+
+!DECRQM on standard encoding mode
+PUSH "\e[?1005\$p"
+ output "\e[?1005;2\$y"
+PUSH "\e[?1006\$p"
+ output "\e[?1006;2\$y"
+PUSH "\e[?1015\$p"
+ output "\e[?1015;2\$y"
+
+!UTF-8 extended encoding mode
+# 300 + 32 + 1 = 333 = U+014d = \xc5\x8d
+PUSH "\e[?1005h"
+MOUSEBTN d 1 0
+ output "\e[M\x20\xc5\x8d\xc5\x8d"
+MOUSEBTN u 1 0
+ output "\e[M\x23\xc5\x8d\xc5\x8d"
+
+!DECRQM on UTF-8 extended encoding mode
+PUSH "\e[?1005\$p"
+ output "\e[?1005;1\$y"
+PUSH "\e[?1006\$p"
+ output "\e[?1006;2\$y"
+PUSH "\e[?1015\$p"
+ output "\e[?1015;2\$y"
+
+!SGR extended encoding mode
+PUSH "\e[?1006h"
+MOUSEBTN d 1 0
+ output "\e[<0;301;301M"
+MOUSEBTN u 1 0
+ output "\e[<0;301;301m"
+
+!DECRQM on SGR extended encoding mode
+PUSH "\e[?1005\$p"
+ output "\e[?1005;2\$y"
+PUSH "\e[?1006\$p"
+ output "\e[?1006;1\$y"
+PUSH "\e[?1015\$p"
+ output "\e[?1015;2\$y"
+
+!rxvt extended encoding mode
+PUSH "\e[?1015h"
+MOUSEBTN d 1 0
+ output "\e[0;301;301M"
+MOUSEBTN u 1 0
+ output "\e[3;301;301M"
+
+!DECRQM on rxvt extended encoding mode
+PUSH "\e[?1005\$p"
+ output "\e[?1005;2\$y"
+PUSH "\e[?1006\$p"
+ output "\e[?1006;2\$y"
+PUSH "\e[?1015\$p"
+ output "\e[?1015;1\$y"
+
+!Mouse disabled reports nothing
+RESET
+ settermprop 1 true
+ settermprop 2 true
+ settermprop 7 1
+MOUSEMOVE 0,0 0
+MOUSEBTN d 1 0
+MOUSEBTN u 1 0
diff --git a/src/libvterm/t/18state_termprops.test b/src/libvterm/t/18state_termprops.test
new file mode 100644
index 0000000..83c333f
--- /dev/null
+++ b/src/libvterm/t/18state_termprops.test
@@ -0,0 +1,42 @@
+INIT
+WANTSTATE p
+
+RESET
+ settermprop 1 true
+ settermprop 2 true
+ settermprop 7 1
+
+!Cursor visibility
+PUSH "\e[?25h"
+ settermprop 1 true
+PUSH "\e[?25\$p"
+ output "\e[?25;1\$y"
+PUSH "\e[?25l"
+ settermprop 1 false
+PUSH "\e[?25\$p"
+ output "\e[?25;2\$y"
+
+!Cursor blink
+PUSH "\e[?12h"
+ settermprop 2 true
+PUSH "\e[?12\$p"
+ output "\e[?12;1\$y"
+PUSH "\e[?12l"
+ settermprop 2 false
+PUSH "\e[?12\$p"
+ output "\e[?12;2\$y"
+
+!Cursor shape
+PUSH "\e[3 q"
+ settermprop 2 true
+ settermprop 7 2
+
+!Title
+PUSH "\e]2;Here is my title\a"
+ settermprop 4 ["Here is my title"]
+
+!Title split write
+PUSH "\e]2;Here is"
+ settermprop 4 ["Here is"
+PUSH " another title\a"
+ settermprop 4 " another title"]
diff --git a/src/libvterm/t/20state_wrapping.test b/src/libvterm/t/20state_wrapping.test
new file mode 100644
index 0000000..606fa06
--- /dev/null
+++ b/src/libvterm/t/20state_wrapping.test
@@ -0,0 +1,69 @@
+INIT
+UTF8 1
+WANTSTATE gm
+
+!79th Column
+PUSH "\e[75G"
+PUSH "A"x5
+ putglyph 0x41 1 0,74
+ putglyph 0x41 1 0,75
+ putglyph 0x41 1 0,76
+ putglyph 0x41 1 0,77
+ putglyph 0x41 1 0,78
+ ?cursor = 0,79
+
+!80th Column Phantom
+PUSH "A"
+ putglyph 0x41 1 0,79
+ ?cursor = 0,79
+
+!Line Wraparound
+PUSH "B"
+ putglyph 0x42 1 1,0
+ ?cursor = 1,1
+
+!Line Wraparound during combined write
+PUSH "\e[78G"
+PUSH "BBBCC"
+ putglyph 0x42 1 1,77
+ putglyph 0x42 1 1,78
+ putglyph 0x42 1 1,79
+ putglyph 0x43 1 2,0
+ putglyph 0x43 1 2,1
+ ?cursor = 2,2
+
+!DEC Auto Wrap Mode
+RESET
+PUSH "\e[?7l"
+PUSH "\e[75G"
+PUSH "D"x6
+ putglyph 0x44 1 0,74
+ putglyph 0x44 1 0,75
+ putglyph 0x44 1 0,76
+ putglyph 0x44 1 0,77
+ putglyph 0x44 1 0,78
+ putglyph 0x44 1 0,79
+ ?cursor = 0,79
+PUSH "D"
+ putglyph 0x44 1 0,79
+ ?cursor = 0,79
+PUSH "\e[?7h"
+
+!80th column causes linefeed on wraparound
+PUSH "\e[25;78HABC"
+ putglyph 0x41 1 24,77
+ putglyph 0x42 1 24,78
+ putglyph 0x43 1 24,79
+ ?cursor = 24,79
+PUSH "D"
+ moverect 1..25,0..80 -> 0..24,0..80
+ putglyph 0x44 1 24,0
+
+!80th column phantom linefeed phantom cancelled by explicit cursor move
+PUSH "\e[25;78HABC"
+ putglyph 0x41 1 24,77
+ putglyph 0x42 1 24,78
+ putglyph 0x43 1 24,79
+ ?cursor = 24,79
+PUSH "\e[25;1HD"
+ putglyph 0x44 1 24,0
diff --git a/src/libvterm/t/21state_tabstops.test b/src/libvterm/t/21state_tabstops.test
new file mode 100644
index 0000000..df4a589
--- /dev/null
+++ b/src/libvterm/t/21state_tabstops.test
@@ -0,0 +1,60 @@
+INIT
+WANTSTATE g
+
+!Initial
+RESET
+PUSH "\tX"
+ putglyph 0x58 1 0,8
+PUSH "\tX"
+ putglyph 0x58 1 0,16
+ ?cursor = 0,17
+
+!HTS
+PUSH "\e[5G\eH"
+PUSH "\e[G\tX"
+ putglyph 0x58 1 0,4
+ ?cursor = 0,5
+
+!TBC 0
+PUSH "\e[9G\e[g"
+PUSH "\e[G\tX\tX"
+ putglyph 0x58 1 0,4
+ putglyph 0x58 1 0,16
+ ?cursor = 0,17
+
+!TBC 3
+PUSH "\e[3g\e[50G\eH\e[G"
+ ?cursor = 0,0
+PUSH "\tX"
+ putglyph 0x58 1 0,49
+ ?cursor = 0,50
+
+!Tabstops after resize
+RESET
+RESIZE 30,100
+# Should be 100/8 = 12 tabstops
+PUSH "\tX"
+ putglyph 0x58 1 0,8
+PUSH "\tX"
+ putglyph 0x58 1 0,16
+PUSH "\tX"
+ putglyph 0x58 1 0,24
+PUSH "\tX"
+ putglyph 0x58 1 0,32
+PUSH "\tX"
+ putglyph 0x58 1 0,40
+PUSH "\tX"
+ putglyph 0x58 1 0,48
+PUSH "\tX"
+ putglyph 0x58 1 0,56
+PUSH "\tX"
+ putglyph 0x58 1 0,64
+PUSH "\tX"
+ putglyph 0x58 1 0,72
+PUSH "\tX"
+ putglyph 0x58 1 0,80
+PUSH "\tX"
+ putglyph 0x58 1 0,88
+PUSH "\tX"
+ putglyph 0x58 1 0,96
+ ?cursor = 0,97
diff --git a/src/libvterm/t/22state_save.test b/src/libvterm/t/22state_save.test
new file mode 100644
index 0000000..81e9226
--- /dev/null
+++ b/src/libvterm/t/22state_save.test
@@ -0,0 +1,64 @@
+INIT
+WANTSTATE p
+
+RESET
+ settermprop 1 true
+ settermprop 2 true
+ settermprop 7 1
+
+!Set up state
+PUSH "\e[2;2H"
+ ?cursor = 1,1
+PUSH "\e[1m"
+ ?pen bold = on
+
+!Save
+PUSH "\e[?1048h"
+
+!Change state
+PUSH "\e[5;5H"
+ ?cursor = 4,4
+PUSH "\e[4 q"
+ settermprop 2 false
+ settermprop 7 2
+PUSH "\e[22;4m"
+ ?pen bold = off
+ ?pen underline = 1
+
+!Restore
+PUSH "\e[?1048l"
+ settermprop 1 true
+ settermprop 2 true
+ settermprop 7 1
+ ?cursor = 1,1
+ ?pen bold = on
+ ?pen underline = 0
+
+!Save/restore using DECSC/DECRC
+PUSH "\e[2;2H\e7"
+ ?cursor = 1,1
+
+PUSH "\e[5;5H"
+ ?cursor = 4,4
+PUSH "\e8"
+ settermprop 1 true
+ settermprop 2 true
+ settermprop 7 1
+ ?cursor = 1,1
+
+!Save twice, restore twice happens on both edge transitions
+PUSH "\e[2;10H\e[?1048h\e[6;10H\e[?1048h"
+PUSH "\e[H"
+ ?cursor = 0,0
+PUSH "\e[?1048l"
+ settermprop 1 true
+ settermprop 2 true
+ settermprop 7 1
+ ?cursor = 5,9
+PUSH "\e[H"
+ ?cursor = 0,0
+PUSH "\e[?1048l"
+ settermprop 1 true
+ settermprop 2 true
+ settermprop 7 1
+ ?cursor = 5,9
diff --git a/src/libvterm/t/25state_input.test b/src/libvterm/t/25state_input.test
new file mode 100644
index 0000000..4eb4c6a
--- /dev/null
+++ b/src/libvterm/t/25state_input.test
@@ -0,0 +1,155 @@
+INIT
+WANTSTATE
+
+!Unmodified ASCII
+INCHAR 0 41
+ output "A"
+INCHAR 0 61
+ output "a"
+
+!Ctrl modifier on ASCII letters
+INCHAR C 41
+ output "\e[65;5u"
+INCHAR C 61
+ output "\x01"
+
+!Alt modifier on ASCII letters
+INCHAR A 41
+ output "\eA"
+INCHAR A 61
+ output "\ea"
+
+!Ctrl-Alt modifier on ASCII letters
+INCHAR CA 41
+ output "\e[65;7u"
+INCHAR CA 61
+ output "\e\x01"
+
+!Special handling of Ctrl-I
+INCHAR 0 49
+ output "I"
+INCHAR 0 69
+ output "i"
+INCHAR C 49
+ output "\e[73;5u"
+INCHAR C 69
+ output "\e[105;5u"
+INCHAR A 49
+ output "\eI"
+INCHAR A 69
+ output "\ei"
+INCHAR CA 49
+ output "\e[73;7u"
+INCHAR CA 69
+ output "\e[105;7u"
+
+!Special handling of Space
+INCHAR 0 20
+ output " "
+INCHAR S 20
+ output "\e[32;2u"
+INCHAR C 20
+ output "\0"
+INCHAR SC 20
+ output "\e[32;6u"
+INCHAR A 20
+ output "\e "
+INCHAR SA 20
+ output "\e[32;4u"
+INCHAR CA 20
+ output "\e\0"
+INCHAR SCA 20
+ output "\e[32;8u"
+
+!Cursor keys in reset (cursor) mode
+INKEY 0 Up
+ output "\e[A"
+INKEY S Up
+ output "\e[1;2A"
+INKEY C Up
+ output "\e[1;5A"
+INKEY SC Up
+ output "\e[1;6A"
+INKEY A Up
+ output "\e[1;3A"
+INKEY SA Up
+ output "\e[1;4A"
+INKEY CA Up
+ output "\e[1;7A"
+INKEY SCA Up
+ output "\e[1;8A"
+
+!Cursor keys in application mode
+PUSH "\e[?1h"
+# Plain "Up" should be SS3 A now
+INKEY 0 Up
+ output "\eOA"
+# Modified keys should still use CSI
+INKEY S Up
+ output "\e[1;2A"
+INKEY C Up
+ output "\e[1;5A"
+
+!Shift-Tab should be different
+INKEY 0 Tab
+ output "\x09"
+INKEY S Tab
+ output "\e[Z"
+INKEY C Tab
+ output "\e[9;5u"
+INKEY A Tab
+ output "\e\x09"
+INKEY CA Tab
+ output "\e[9;7u"
+
+!Enter in linefeed mode
+INKEY 0 Enter
+ output "\x0d"
+
+!Enter in newline mode
+PUSH "\e[20h"
+INKEY 0 Enter
+ output "\x0d\x0a"
+
+!Unmodified F1 is SS3 P
+INKEY 0 F1
+ output "\eOP"
+
+!Modified F1 is CSI P
+INKEY S F1
+ output "\e[1;2P"
+INKEY A F1
+ output "\e[1;3P"
+INKEY C F1
+ output "\e[1;5P"
+
+!Keypad in DECKPNM
+INKEY 0 KP0
+ output "0"
+
+!Keypad in DECKPAM
+PUSH "\e="
+INKEY 0 KP0
+ output "\eOp"
+
+!Bracketed paste mode off
+PASTE START
+PASTE END
+
+!Bracketed paste mode on
+PUSH "\e[?2004h"
+PASTE START
+ output "\e[200~"
+PASTE END
+ output "\e[201~"
+
+!Focus reporting disabled
+FOCUS IN
+FOCUS OUT
+
+!Focus reporting enabled
+PUSH "\e[?1004h"
+FOCUS IN
+ output "\e[I"
+FOCUS OUT
+ output "\e[O"
diff --git a/src/libvterm/t/26state_query.test b/src/libvterm/t/26state_query.test
new file mode 100644
index 0000000..41e7cf8
--- /dev/null
+++ b/src/libvterm/t/26state_query.test
@@ -0,0 +1,67 @@
+INIT
+WANTSTATE
+
+!DA
+RESET
+PUSH "\e[c"
+ output "\e[?1;2c"
+
+!XTVERSION
+RESET
+PUSH "\e[>q"
+ output "\eP>|libvterm(0.3)\e\\"
+
+!DSR
+RESET
+PUSH "\e[5n"
+ output "\e[0n"
+
+!CPR
+PUSH "\e[6n"
+ output "\e[1;1R"
+PUSH "\e[10;10H\e[6n"
+ output "\e[10;10R"
+
+!DECCPR
+PUSH "\e[?6n"
+ output "\e[?10;10R"
+
+!DECRQSS on DECSCUSR
+PUSH "\e[3 q"
+PUSH "\eP\$q q\e\\"
+ output "\eP1\$r3 q\e\\"
+
+!DECRQSS on SGR
+PUSH "\e[1;5;7m"
+PUSH "\eP\$qm\e\\"
+ output "\eP1\$r1;5;7m\e\\"
+
+!DECRQSS on SGR ANSI colours
+PUSH "\e[0;31;42m"
+PUSH "\eP\$qm\e\\"
+ output "\eP1\$r31;42m\e\\"
+
+!DECRQSS on SGR ANSI hi-bright colours
+PUSH "\e[0;93;104m"
+PUSH "\eP\$qm\e\\"
+ output "\eP1\$r93;104m\e\\"
+
+##!DECRQSS on SGR 256-palette colours
+#PUSH "\e[0;38:5:56;48:5:78m"
+#PUSH "\eP\$qm\e\\"
+# output "\eP1\$r38:5:56;48:5:78m\e\\"
+
+!DECRQSS on SGR RGB8 colours
+PUSH "\e[0;38:2:24:68:112;48:2:13:57:101m"
+PUSH "\eP\$qm\e\\"
+ output "\eP1\$r38:2:24:68:112;48:2:13:57:101m\e\\"
+
+!S8C1T on DSR
+PUSH "\e G"
+PUSH "\e[5n"
+ output "\x{9b}0n"
+PUSH "\e F"
+
+#!Truncation on attempted buffer overflow
+#PUSH "\e[6n" x 30
+# output "\e[10;10R" x 25
diff --git a/src/libvterm/t/27state_reset.test b/src/libvterm/t/27state_reset.test
new file mode 100644
index 0000000..254f994
--- /dev/null
+++ b/src/libvterm/t/27state_reset.test
@@ -0,0 +1,32 @@
+INIT
+WANTSTATE
+
+RESET
+
+!RIS homes cursor
+PUSH "\e[5;5H"
+ ?cursor = 4,4
+WANTSTATE +m
+PUSH "\ec"
+ ?cursor = 0,0
+WANTSTATE -m
+
+!RIS cancels scrolling region
+PUSH "\e[5;10r"
+WANTSTATE +s
+PUSH "\ec\e[25H\n"
+ scrollrect 0..25,0..80 => +1,+0
+WANTSTATE -s
+
+!RIS erases screen
+PUSH "ABCDE"
+WANTSTATE +e
+PUSH "\ec"
+ erase 0..25,0..80
+WANTSTATE -e
+
+!RIS clears tabstops
+PUSH "\e[5G\eH\e[G\t"
+ ?cursor = 0,4
+PUSH "\ec\t"
+ ?cursor = 0,8
diff --git a/src/libvterm/t/28state_dbl_wh.test b/src/libvterm/t/28state_dbl_wh.test
new file mode 100644
index 0000000..596194d
--- /dev/null
+++ b/src/libvterm/t/28state_dbl_wh.test
@@ -0,0 +1,61 @@
+INIT
+WANTSTATE g
+
+!Single Width, Single Height
+RESET
+PUSH "\e#5"
+PUSH "Hello"
+ putglyph 0x48 1 0,0
+ putglyph 0x65 1 0,1
+ putglyph 0x6c 1 0,2
+ putglyph 0x6c 1 0,3
+ putglyph 0x6f 1 0,4
+
+!Double Width, Single Height
+RESET
+PUSH "\e#6"
+PUSH "Hello"
+ putglyph 0x48 1 0,0 dwl
+ putglyph 0x65 1 0,1 dwl
+ putglyph 0x6c 1 0,2 dwl
+ putglyph 0x6c 1 0,3 dwl
+ putglyph 0x6f 1 0,4 dwl
+ ?cursor = 0,5
+PUSH "\e[40GAB"
+ putglyph 0x41 1 0,39 dwl
+ putglyph 0x42 1 1,0
+ ?cursor = 1,1
+
+!Double Height
+RESET
+PUSH "\e#3"
+PUSH "Hello"
+ putglyph 0x48 1 0,0 dwl dhl-top
+ putglyph 0x65 1 0,1 dwl dhl-top
+ putglyph 0x6c 1 0,2 dwl dhl-top
+ putglyph 0x6c 1 0,3 dwl dhl-top
+ putglyph 0x6f 1 0,4 dwl dhl-top
+ ?cursor = 0,5
+PUSH "\r\n\e#4"
+PUSH "Hello"
+ putglyph 0x48 1 1,0 dwl dhl-bottom
+ putglyph 0x65 1 1,1 dwl dhl-bottom
+ putglyph 0x6c 1 1,2 dwl dhl-bottom
+ putglyph 0x6c 1 1,3 dwl dhl-bottom
+ putglyph 0x6f 1 1,4 dwl dhl-bottom
+ ?cursor = 1,5
+
+!Double Width scrolling
+RESET
+PUSH "\e[20H\e#6ABC"
+ putglyph 0x41 1 19,0 dwl
+ putglyph 0x42 1 19,1 dwl
+ putglyph 0x43 1 19,2 dwl
+PUSH "\e[25H\n"
+PUSH "\e[19;4HDE"
+ putglyph 0x44 1 18,3 dwl
+ putglyph 0x45 1 18,4 dwl
+PUSH "\e[H\eM"
+PUSH "\e[20;6HFG"
+ putglyph 0x46 1 19,5 dwl
+ putglyph 0x47 1 19,6 dwl
diff --git a/src/libvterm/t/29state_fallback.test b/src/libvterm/t/29state_fallback.test
new file mode 100644
index 0000000..4ab2e18
--- /dev/null
+++ b/src/libvterm/t/29state_fallback.test
@@ -0,0 +1,31 @@
+INIT
+WANTSTATE f
+RESET
+
+!Unrecognised control
+PUSH "\x03"
+ control 03
+
+!Unrecognised CSI
+PUSH "\e[?15;2z"
+ csi 0x7a L=3f 15,2
+
+!Unrecognised OSC
+PUSH "\e]27;Something\e\\"
+ osc [27 "Something"]
+
+!Unrecognised DCS
+PUSH "\ePz123\e\\"
+ dcs ["z123"]
+
+!Unrecognised APC
+PUSH "\e_z123\e\\"
+ apc ["z123"]
+
+!Unrecognised PM
+PUSH "\e^z123\e\\"
+ pm ["z123"]
+
+!Unrecognised SOS
+PUSH "\eXz123\e\\"
+ sos ["z123"]
diff --git a/src/libvterm/t/30state_pen.test b/src/libvterm/t/30state_pen.test
new file mode 100644
index 0000000..92cf01d
--- /dev/null
+++ b/src/libvterm/t/30state_pen.test
@@ -0,0 +1,125 @@
+INIT
+UTF8 1
+WANTSTATE
+
+!Reset
+PUSH "\e[m"
+ ?pen bold = off
+ ?pen underline = 0
+ ?pen italic = off
+ ?pen blink = off
+ ?pen reverse = off
+ ?pen font = 0
+ ?pen foreground = rgb(240,240,240,is_default_fg)
+ ?pen background = rgb(0,0,0,is_default_bg)
+
+!Bold
+PUSH "\e[1m"
+ ?pen bold = on
+PUSH "\e[22m"
+ ?pen bold = off
+PUSH "\e[1m\e[m"
+ ?pen bold = off
+
+!Underline
+PUSH "\e[4m"
+ ?pen underline = 1
+PUSH "\e[21m"
+ ?pen underline = 2
+PUSH "\e[24m"
+ ?pen underline = 0
+PUSH "\e[4m\e[4:0m"
+ ?pen underline = 0
+PUSH "\e[4:1m"
+ ?pen underline = 1
+PUSH "\e[4:2m"
+ ?pen underline = 2
+PUSH "\e[4:3m"
+ ?pen underline = 3
+PUSH "\e[4m\e[m"
+ ?pen underline = 0
+
+!Italic
+PUSH "\e[3m"
+ ?pen italic = on
+PUSH "\e[23m"
+ ?pen italic = off
+PUSH "\e[3m\e[m"
+ ?pen italic = off
+
+!Blink
+PUSH "\e[5m"
+ ?pen blink = on
+PUSH "\e[25m"
+ ?pen blink = off
+PUSH "\e[5m\e[m"
+ ?pen blink = off
+
+!Reverse
+PUSH "\e[7m"
+ ?pen reverse = on
+PUSH "\e[27m"
+ ?pen reverse = off
+PUSH "\e[7m\e[m"
+ ?pen reverse = off
+
+!Font Selection
+PUSH "\e[11m"
+ ?pen font = 1
+PUSH "\e[19m"
+ ?pen font = 9
+PUSH "\e[10m"
+ ?pen font = 0
+PUSH "\e[11m\e[m"
+ ?pen font = 0
+
+!Foreground
+PUSH "\e[31m"
+ ?pen foreground = idx(1)
+PUSH "\e[32m"
+ ?pen foreground = idx(2)
+PUSH "\e[34m"
+ ?pen foreground = idx(4)
+PUSH "\e[91m"
+ ?pen foreground = idx(9)
+PUSH "\e[38:2:10:20:30m"
+ ?pen foreground = rgb(10,20,30)
+PUSH "\e[38:5:1m"
+ ?pen foreground = idx(1)
+PUSH "\e[39m"
+ ?pen foreground = rgb(240,240,240,is_default_fg)
+
+!Background
+PUSH "\e[41m"
+ ?pen background = idx(1)
+PUSH "\e[42m"
+ ?pen background = idx(2)
+PUSH "\e[44m"
+ ?pen background = idx(4)
+PUSH "\e[101m"
+ ?pen background = idx(9)
+PUSH "\e[48:2:10:20:30m"
+ ?pen background = rgb(10,20,30)
+PUSH "\e[48:5:1m"
+ ?pen background = idx(1)
+PUSH "\e[49m"
+ ?pen background = rgb(0,0,0,is_default_bg)
+
+!Bold+ANSI colour == highbright
+PUSH "\e[m\e[1;37m"
+ ?pen bold = on
+ ?pen foreground = idx(15)
+PUSH "\e[m\e[37;1m"
+ ?pen bold = on
+ ?pen foreground = idx(15)
+
+!Super/Subscript
+PUSH "\e[73m"
+ ?pen small = on
+ ?pen baseline = raise
+PUSH "\e[74m"
+ ?pen small = on
+ ?pen baseline = lower
+PUSH "\e[75m"
+ ?pen small = off
+ ?pen baseline = normal
diff --git a/src/libvterm/t/31state_rep.test b/src/libvterm/t/31state_rep.test
new file mode 100644
index 0000000..f820e67
--- /dev/null
+++ b/src/libvterm/t/31state_rep.test
@@ -0,0 +1,128 @@
+INIT
+UTF8 1
+WANTSTATE g
+
+!REP no argument
+RESET
+PUSH "a\e[b"
+ putglyph 0x61 1 0,0
+ putglyph 0x61 1 0,1
+
+!REP zero (zero should be interpreted as one)
+RESET
+PUSH "a\e[0b"
+ putglyph 0x61 1 0,0
+ putglyph 0x61 1 0,1
+
+!REP lowercase a times two
+RESET
+PUSH "a\e[2b"
+ putglyph 0x61 1 0,0
+ putglyph 0x61 1 0,1
+ putglyph 0x61 1 0,2
+
+!REP with UTF-8 1 char
+# U+00E9 = 0xC3 0xA9 name: LATIN SMALL LETTER E WITH ACUTE
+RESET
+PUSH "\xC3\xA9\e[b"
+ putglyph 0xe9 1 0,0
+ putglyph 0xe9 1 0,1
+
+!REP with UTF-8 wide char
+# U+00E9 = 0xC3 0xA9 name: LATIN SMALL LETTER E WITH ACUTE
+RESET
+PUSH "\xEF\xBC\x90\e[b"
+ putglyph 0xff10 2 0,0
+ putglyph 0xff10 2 0,2
+
+!REP with UTF-8 combining character
+RESET
+PUSH "e\xCC\x81\e[b"
+ putglyph 0x65,0x301 1 0,0
+ putglyph 0x65,0x301 1 0,1
+
+!REP till end of line
+RESET
+PUSH "a\e[1000bb"
+ putglyph 0x61 1 0,0
+ putglyph 0x61 1 0,1
+ putglyph 0x61 1 0,2
+ putglyph 0x61 1 0,3
+ putglyph 0x61 1 0,4
+ putglyph 0x61 1 0,5
+ putglyph 0x61 1 0,6
+ putglyph 0x61 1 0,7
+ putglyph 0x61 1 0,8
+ putglyph 0x61 1 0,9
+ putglyph 0x61 1 0,10
+ putglyph 0x61 1 0,11
+ putglyph 0x61 1 0,12
+ putglyph 0x61 1 0,13
+ putglyph 0x61 1 0,14
+ putglyph 0x61 1 0,15
+ putglyph 0x61 1 0,16
+ putglyph 0x61 1 0,17
+ putglyph 0x61 1 0,18
+ putglyph 0x61 1 0,19
+ putglyph 0x61 1 0,20
+ putglyph 0x61 1 0,21
+ putglyph 0x61 1 0,22
+ putglyph 0x61 1 0,23
+ putglyph 0x61 1 0,24
+ putglyph 0x61 1 0,25
+ putglyph 0x61 1 0,26
+ putglyph 0x61 1 0,27
+ putglyph 0x61 1 0,28
+ putglyph 0x61 1 0,29
+ putglyph 0x61 1 0,30
+ putglyph 0x61 1 0,31
+ putglyph 0x61 1 0,32
+ putglyph 0x61 1 0,33
+ putglyph 0x61 1 0,34
+ putglyph 0x61 1 0,35
+ putglyph 0x61 1 0,36
+ putglyph 0x61 1 0,37
+ putglyph 0x61 1 0,38
+ putglyph 0x61 1 0,39
+ putglyph 0x61 1 0,40
+ putglyph 0x61 1 0,41
+ putglyph 0x61 1 0,42
+ putglyph 0x61 1 0,43
+ putglyph 0x61 1 0,44
+ putglyph 0x61 1 0,45
+ putglyph 0x61 1 0,46
+ putglyph 0x61 1 0,47
+ putglyph 0x61 1 0,48
+ putglyph 0x61 1 0,49
+ putglyph 0x61 1 0,50
+ putglyph 0x61 1 0,51
+ putglyph 0x61 1 0,52
+ putglyph 0x61 1 0,53
+ putglyph 0x61 1 0,54
+ putglyph 0x61 1 0,55
+ putglyph 0x61 1 0,56
+ putglyph 0x61 1 0,57
+ putglyph 0x61 1 0,58
+ putglyph 0x61 1 0,59
+ putglyph 0x61 1 0,60
+ putglyph 0x61 1 0,61
+ putglyph 0x61 1 0,62
+ putglyph 0x61 1 0,63
+ putglyph 0x61 1 0,64
+ putglyph 0x61 1 0,65
+ putglyph 0x61 1 0,66
+ putglyph 0x61 1 0,67
+ putglyph 0x61 1 0,68
+ putglyph 0x61 1 0,69
+ putglyph 0x61 1 0,70
+ putglyph 0x61 1 0,71
+ putglyph 0x61 1 0,72
+ putglyph 0x61 1 0,73
+ putglyph 0x61 1 0,74
+ putglyph 0x61 1 0,75
+ putglyph 0x61 1 0,76
+ putglyph 0x61 1 0,77
+ putglyph 0x61 1 0,78
+ putglyph 0x61 1 0,79
+ putglyph 0x62 1 1,0
+
diff --git a/src/libvterm/t/32state_flow.test b/src/libvterm/t/32state_flow.test
new file mode 100644
index 0000000..84a13df
--- /dev/null
+++ b/src/libvterm/t/32state_flow.test
@@ -0,0 +1,28 @@
+INIT
+WANTSTATE
+
+# Many of these test cases inspired by
+# https://blueprints.launchpad.net/libvterm/+spec/reflow-cases
+
+!Spillover text marks continuation on second line
+RESET
+PUSH "A"x100
+PUSH "\r\n"
+ ?lineinfo 0 =
+ ?lineinfo 1 = cont
+
+!CRLF in column 80 does not mark continuation
+RESET
+PUSH "B"x80
+PUSH "\r\n"
+PUSH "B"x20
+PUSH "\r\n"
+ ?lineinfo 0 =
+ ?lineinfo 1 =
+
+!EL cancels continuation of following line
+RESET
+PUSH "D"x100
+ ?lineinfo 1 = cont
+PUSH "\eM\e[79G\e[K"
+ ?lineinfo 1 =
diff --git a/src/libvterm/t/40state_selection.test b/src/libvterm/t/40state_selection.test
new file mode 100644
index 0000000..6ed8972
--- /dev/null
+++ b/src/libvterm/t/40state_selection.test
@@ -0,0 +1,55 @@
+INIT
+UTF8 1
+WANTSTATE
+
+!Set clipboard; final chunk len 4
+PUSH "\e]52;c;SGVsbG8s\e\\"
+ selection-set mask=0001 ["Hello,"]
+
+!Set clipboard; final chunk len 3
+PUSH "\e]52;c;SGVsbG8sIHc=\e\\"
+ selection-set mask=0001 ["Hello, w"]
+
+!Set clipboard; final chunk len 2
+PUSH "\e]52;c;SGVsbG8sIHdvcmxkCg==\e\\"
+ selection-set mask=0001 ["Hello, world\n"]
+
+!Set clipboard; split between chunks
+PUSH "\e]52;c;SGVs"
+ selection-set mask=0001 ["Hel"
+PUSH "bG8s\e\\"
+ selection-set mask=0001 "lo,"]
+
+!Set clipboard; split within chunk
+PUSH "\e]52;c;SGVsbG"
+ selection-set mask=0001 ["Hel"
+PUSH "8s\e\\"
+ selection-set mask=0001 "lo,"]
+
+!Query clipboard
+PUSH "\e]52;c;?\e\\"
+ selection-query mask=0001
+
+!Send clipboard; final chunk len 4
+SELECTION 1 ["Hello,"]
+ output "\e]52;c;SGVsbG8s\e\\"
+
+!Send clipboard; final chunk len 3
+SELECTION 1 ["Hello, w"]
+ output "\e]52;c;SGVsbG8sIHc=\e\\"
+
+!Send clipboard; final chunk len 2
+SELECTION 1 ["Hello, world\n"]
+ output "\e]52;c;SGVsbG8sIHdvcmxkCg==\e\\"
+
+!Send clipboard; split between chunks
+SELECTION 1 ["Hel"
+ output "\e]52;c;SGVs"
+SELECTION 1 "lo,"]
+ output "bG8s\e\\"
+
+!Send clipboard; split within chunk
+SELECTION 1 ["Hello"
+ output "\e]52;c;SGVs"
+SELECTION 1 ","]
+ output "bG8s\e\\"
diff --git a/src/libvterm/t/60screen_ascii.test b/src/libvterm/t/60screen_ascii.test
new file mode 100644
index 0000000..57729c5
--- /dev/null
+++ b/src/libvterm/t/60screen_ascii.test
@@ -0,0 +1,69 @@
+INIT
+WANTSCREEN ac
+
+!Get
+RESET
+PUSH "ABC"
+ movecursor 0,3
+ ?screen_chars 0,0,1,3 = "ABC"
+ ?screen_chars 0,0,1,80 = "ABC"
+ ?screen_text 0,0,1,3 = 0x41,0x42,0x43
+ ?screen_text 0,0,1,80 = 0x41,0x42,0x43
+ ?screen_cell 0,0 = {0x41} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)
+ ?screen_cell 0,1 = {0x42} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)
+ ?screen_cell 0,2 = {0x43} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)
+ ?screen_row 0 = "ABC"
+ ?screen_eol 0,0 = 0
+ ?screen_eol 0,2 = 0
+ ?screen_eol 0,3 = 1
+PUSH "\e[H"
+ movecursor 0,0
+ ?screen_row 0 = "ABC"
+ ?screen_text 0,0,1,80 = 0x41,0x42,0x43
+PUSH "E"
+ movecursor 0,1
+ ?screen_row 0 = "EBC"
+ ?screen_text 0,0,1,80 = 0x45,0x42,0x43
+
+WANTSCREEN -c
+
+!Erase
+RESET
+PUSH "ABCDE\e[H\e[K"
+ ?screen_row 0 = ""
+ ?screen_text 0,0,1,80 =
+
+!Copycell
+RESET
+PUSH "ABC\e[H\e[@"
+PUSH "1"
+ ?screen_row 0 = "1ABC"
+
+RESET
+PUSH "ABC\e[H\e[P"
+ ?screen_chars 0,0,1,1 = "B"
+ ?screen_chars 0,1,1,2 = "C"
+ ?screen_chars 0,0,1,80 = "BC"
+
+!Space padding
+RESET
+PUSH "Hello\e[CWorld"
+ ?screen_row 0 = "Hello World"
+ ?screen_text 0,0,1,80 = 0x48,0x65,0x6c,0x6c,0x6f,0x20,0x57,0x6f,0x72,0x6c,0x64
+
+!Linefeed padding
+RESET
+PUSH "Hello\r\nWorld"
+ ?screen_chars 0,0,2,80 = "Hello\nWorld"
+ ?screen_text 0,0,2,80 = 0x48,0x65,0x6c,0x6c,0x6f,0x0a,0x57,0x6f,0x72,0x6c,0x64
+
+!Altscreen
+RESET
+PUSH "P"
+ ?screen_row 0 = "P"
+PUSH "\e[?1049h"
+ ?screen_row 0 = ""
+PUSH "\e[2K\e[HA"
+ ?screen_row 0 = "A"
+PUSH "\e[?1049l"
+ ?screen_row 0 = "P"
diff --git a/src/libvterm/t/61screen_unicode.test b/src/libvterm/t/61screen_unicode.test
new file mode 100644
index 0000000..8bde2bd
--- /dev/null
+++ b/src/libvterm/t/61screen_unicode.test
@@ -0,0 +1,47 @@
+INIT
+UTF8 1
+WANTSCREEN
+
+!Single width UTF-8
+# U+00C1 = 0xC3 0x81 name: LATIN CAPITAL LETTER A WITH ACUTE
+# U+00E9 = 0xC3 0xA9 name: LATIN SMALL LETTER E WITH ACUTE
+RESET
+PUSH "\xC3\x81\xC3\xA9"
+ ?screen_row 0 = 0xc1,0xe9
+ ?screen_text 0,0,1,80 = 0xc3,0x81,0xc3,0xa9
+ ?screen_cell 0,0 = {0xc1} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)
+
+!Wide char
+# U+FF10 = 0xEF 0xBC 0x90 name: FULLWIDTH DIGIT ZERO
+RESET
+PUSH "0123\e[H"
+PUSH "\xEF\xBC\x90"
+ ?screen_row 0 = 0xff10,0x32,0x33
+ ?screen_text 0,0,1,80 = 0xef,0xbc,0x90,0x32,0x33
+ ?screen_cell 0,0 = {0xff10} width=2 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)
+
+!Combining char
+# U+0301 = 0xCC 0x81 name: COMBINING ACUTE
+RESET
+PUSH "0123\e[H"
+PUSH "e\xCC\x81"
+ ?screen_row 0 = 0x65,0x301,0x31,0x32,0x33
+ ?screen_text 0,0,1,80 = 0x65,0xcc,0x81,0x31,0x32,0x33
+ ?screen_cell 0,0 = {0x65,0x301} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)
+
+!10 combining accents should not crash
+RESET
+PUSH "e\xCC\x81\xCC\x82\xCC\x83\xCC\x84\xCC\x85\xCC\x86\xCC\x87\xCC\x88\xCC\x89\xCC\x8A"
+ ?screen_cell 0,0 = {0x65,0x301,0x302,0x303,0x304,0x305} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)
+
+!40 combining accents in two split writes of 20 should not crash
+RESET
+PUSH "e\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81"
+PUSH "\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81\xCC\x81"
+ ?screen_cell 0,0 = {0x65,0x301,0x301,0x301,0x301,0x301} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)
+
+!Outputting CJK doublewidth in 80th column should wraparound to next line and not crash"
+RESET
+PUSH "\e[80G\xEF\xBC\x90"
+ ?screen_cell 0,79 = {} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)
+ ?screen_cell 1,0 = {0xff10} width=2 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)
diff --git a/src/libvterm/t/62screen_damage.test b/src/libvterm/t/62screen_damage.test
new file mode 100644
index 0000000..3b1b238
--- /dev/null
+++ b/src/libvterm/t/62screen_damage.test
@@ -0,0 +1,155 @@
+INIT
+WANTSCREEN aDb
+
+!Putglyph
+RESET
+ damage 0..25,0..80
+PUSH "123"
+ damage 0..1,0..1 = 0<31>
+ damage 0..1,1..2 = 0<32>
+ damage 0..1,2..3 = 0<33>
+
+!Erase
+PUSH "\e[H"
+PUSH "\e[3X"
+ damage 0..1,0..3
+
+!Scroll damages entire line in two chunks
+PUSH "\e[H\e[5@"
+ damage 0..1,5..80
+ damage 0..1,0..5
+
+!Scroll down damages entire screen in two chunks
+PUSH "\e[T"
+ damage 1..25,0..80
+ damage 0..1,0..80
+
+!Altscreen damages entire area
+PUSH "\e[?1049h"
+ damage 0..25,0..80
+PUSH "\e[?1049l"
+ damage 0..25,0..80
+
+WANTSCREEN m
+
+!Scroll invokes moverect but not damage
+PUSH "\e[5@"
+ moverect 0..1,0..75 -> 0..1,5..80
+ damage 0..1,0..5
+
+WANTSCREEN -m
+
+!Merge to cells
+RESET
+ damage 0..25,0..80
+DAMAGEMERGE CELL
+
+PUSH "A"
+ damage 0..1,0..1 = 0<41>
+PUSH "B"
+ damage 0..1,1..2 = 0<42>
+PUSH "C"
+ damage 0..1,2..3 = 0<43>
+
+!Merge entire rows
+RESET
+ damage 0..25,0..80
+DAMAGEMERGE ROW
+
+PUSH "ABCDE\r\nEFGH"
+ damage 0..1,0..5 = 0<41 42 43 44 45>
+DAMAGEFLUSH
+ damage 1..2,0..4 = 1<45 46 47 48>
+PUSH "\e[3;6r\e[6H\eD"
+ damage 2..5,0..80
+DAMAGEFLUSH
+ damage 5..6,0..80
+
+!Merge entire screen
+RESET
+ damage 0..25,0..80
+DAMAGEMERGE SCREEN
+
+PUSH "ABCDE\r\nEFGH"
+DAMAGEFLUSH
+ damage 0..2,0..5 = 0<41 42 43 44 45> 1<45 46 47 48>
+PUSH "\e[3;6r\e[6H\eD"
+DAMAGEFLUSH
+ damage 2..6,0..80
+
+!Merge entire screen with moverect
+WANTSCREEN m
+
+RESET
+ damage 0..25,0..80
+DAMAGEMERGE SCREEN
+
+PUSH "ABCDE\r\nEFGH"
+PUSH "\e[3;6r\e[6H\eD"
+ damage 0..2,0..5 = 0<41 42 43 44 45> 1<45 46 47 48>
+ moverect 3..6,0..80 -> 2..5,0..80
+DAMAGEFLUSH
+ damage 5..6,0..80
+
+!Merge scroll
+RESET
+ damage 0..25,0..80
+DAMAGEMERGE SCROLL
+
+PUSH "\e[H1\r\n2\r\n3"
+PUSH "\e[25H\n\n\n"
+ sb_pushline 80 = 31
+ sb_pushline 80 = 32
+ sb_pushline 80 = 33
+DAMAGEFLUSH
+ moverect 3..25,0..80 -> 0..22,0..80
+ damage 0..25,0..80
+
+!Merge scroll with damage
+PUSH "\e[25H"
+PUSH "ABCDE\r\nEFGH\r\n"
+ sb_pushline 80 =
+ sb_pushline 80 =
+DAMAGEFLUSH
+ moverect 2..25,0..80 -> 0..23,0..80
+ damage 22..25,0..80 = 22<41 42 43 44 45> 23<45 46 47 48>
+
+!Merge scroll with damage past region
+PUSH "\e[3;6r\e[6H1\r\n2\r\n3\r\n4\r\n5"
+DAMAGEFLUSH
+ damage 2..6,0..80 = 2<32> 3<33> 4<34> 5<35>
+
+!Damage entirely outside scroll region
+PUSH "\e[HABC\e[3;6r\e[6H\r\n6"
+ damage 0..1,0..3 = 0<41 42 43>
+DAMAGEFLUSH
+ moverect 3..6,0..80 -> 2..5,0..80
+ damage 5..6,0..80 = 5<36>
+
+!Damage overlapping scroll region
+PUSH "\e[H\e[2J"
+DAMAGEFLUSH
+ damage 0..25,0..80
+
+PUSH "\e[HABCD\r\nEFGH\r\nIJKL\e[2;5r\e[5H\r\nMNOP"
+DAMAGEFLUSH
+ moverect 2..5,0..80 -> 1..4,0..80
+ damage 0..5,0..80 = 0<41 42 43 44> 1<49 4A 4B 4C>
+ ## TODO: is this right?
+
+!Merge scroll*2 with damage
+RESET
+ damage 0..25,0..80
+DAMAGEMERGE SCROLL
+
+PUSH "\e[25H\r\nABCDE\b\b\b\e[2P\r\n"
+ sb_pushline 80 =
+ moverect 1..25,0..80 -> 0..24,0..80
+ damage 24..25,0..80 = 24<41 42 43 44 45>
+ moverect 24..25,4..80 -> 24..25,2..78
+ damage 24..25,78..80
+ sb_pushline 80 =
+DAMAGEFLUSH
+ moverect 1..25,0..80 -> 0..24,0..80
+ damage 24..25,0..80
+ ?screen_row 23 = "ABE"
diff --git a/src/libvterm/t/63screen_resize.test b/src/libvterm/t/63screen_resize.test
new file mode 100644
index 0000000..6835222
--- /dev/null
+++ b/src/libvterm/t/63screen_resize.test
@@ -0,0 +1,117 @@
+INIT
+WANTSTATE
+WANTSCREEN
+
+!Resize wider preserves cells
+RESET
+RESIZE 25,80
+PUSH "AB\r\nCD"
+ ?screen_chars 0,0,1,80 = "AB"
+ ?screen_chars 1,0,2,80 = "CD"
+RESIZE 25,100
+ ?screen_chars 0,0,1,100 = "AB"
+ ?screen_chars 1,0,2,100 = "CD"
+
+!Resize wider allows print in new area
+RESET
+RESIZE 25,80
+PUSH "AB\e[79GCD"
+ ?screen_chars 0,0,1,2 = "AB"
+ ?screen_chars 0,78,1,80 = "CD"
+RESIZE 25,100
+ ?screen_chars 0,0,1,2 = "AB"
+ ?screen_chars 0,78,1,80 = "CD"
+PUSH "E"
+ ?screen_chars 0,78,1,81 = "CDE"
+
+!Resize shorter with blanks just truncates
+RESET
+RESIZE 25,80
+PUSH "Top\e[10HLine 10"
+ ?screen_row 0 = "Top"
+ ?screen_row 9 = "Line 10"
+ ?cursor = 9,7
+RESIZE 20,80
+ ?screen_row 0 = "Top"
+ ?screen_row 9 = "Line 10"
+ ?cursor = 9,7
+
+!Resize shorter with content must scroll
+RESET
+RESIZE 25,80
+PUSH "Top\e[25HLine 25\e[15H"
+ ?screen_row 0 = "Top"
+ ?screen_row 24 = "Line 25"
+ ?cursor = 14,0
+WANTSCREEN b
+RESIZE 20,80
+ sb_pushline 80 = 54 6F 70
+ sb_pushline 80 =
+ sb_pushline 80 =
+ sb_pushline 80 =
+ sb_pushline 80 =
+ ?screen_row 0 = ""
+ ?screen_row 19 = "Line 25"
+ ?cursor = 9,0
+
+!Resize shorter does not lose line with cursor
+# See also https://github.com/neovim/libvterm/commit/1b745d29d45623aa8d22a7b9288c7b0e331c7088
+RESET
+WANTSCREEN -b
+RESIZE 25,80
+WANTSCREEN b
+PUSH "\e[24HLine 24\r\nLine 25\r\n"
+ sb_pushline 80 =
+ ?screen_row 23 = "Line 25"
+ ?cursor = 24,0
+RESIZE 24,80
+ sb_pushline 80 =
+ ?screen_row 22 = "Line 25"
+ ?cursor = 23,0
+
+!Resize shorter does not send the cursor to a negative row
+# See also https://github.com/vim/vim/pull/6141
+RESET
+WANTSCREEN -b
+RESIZE 25,80
+WANTSCREEN b
+PUSH "\e[24HLine 24\r\nLine 25\e[H"
+ ?cursor = 0,0
+RESIZE 20,80
+ sb_pushline 80 =
+ sb_pushline 80 =
+ sb_pushline 80 =
+ sb_pushline 80 =
+ sb_pushline 80 =
+ ?cursor = 0,0
+
+!Resize taller attempts to pop scrollback
+RESET
+WANTSCREEN -b
+RESIZE 25,80
+PUSH "Line 1\e[25HBottom\e[15H"
+ ?screen_row 0 = "Line 1"
+ ?screen_row 24 = "Bottom"
+ ?cursor = 14,0
+WANTSCREEN b
+RESIZE 30,80
+ sb_popline 80
+ sb_popline 80
+ sb_popline 80
+ sb_popline 80
+ sb_popline 80
+ ?screen_row 0 = "ABCDE"
+ ?screen_row 5 = "Line 1"
+ ?screen_row 29 = "Bottom"
+ ?cursor = 19,0
+WANTSCREEN -b
+
+!Resize can operate on altscreen
+RESET
+WANTSCREEN a
+RESIZE 25,80
+PUSH "Main screen\e[?1049h\e[HAlt screen"
+RESIZE 30,80
+ ?screen_row 0 = "Alt screen"
+PUSH "\e[?1049l"
+ ?screen_row 0 = "Main screen"
diff --git a/src/libvterm/t/64screen_pen.test b/src/libvterm/t/64screen_pen.test
new file mode 100644
index 0000000..1cb6324
--- /dev/null
+++ b/src/libvterm/t/64screen_pen.test
@@ -0,0 +1,61 @@
+INIT
+WANTSCREEN
+
+RESET
+
+!Plain
+PUSH "A"
+ ?screen_cell 0,0 = {0x41} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)
+
+!Bold
+PUSH "\e[1mB"
+ ?screen_cell 0,1 = {0x42} width=1 attrs={B} fg=rgb(240,240,240) bg=rgb(0,0,0)
+
+!Italic
+PUSH "\e[3mC"
+ ?screen_cell 0,2 = {0x43} width=1 attrs={BI} fg=rgb(240,240,240) bg=rgb(0,0,0)
+
+!Underline
+PUSH "\e[4mD"
+ ?screen_cell 0,3 = {0x44} width=1 attrs={BU1I} fg=rgb(240,240,240) bg=rgb(0,0,0)
+
+!Reset
+PUSH "\e[mE"
+ ?screen_cell 0,4 = {0x45} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)
+
+!Font
+PUSH "\e[11mF\e[m"
+ ?screen_cell 0,5 = {0x46} width=1 attrs={F1} fg=rgb(240,240,240) bg=rgb(0,0,0)
+
+!Foreground
+PUSH "\e[31mG\e[m"
+ ?screen_cell 0,6 = {0x47} width=1 attrs={} fg=idx(1) bg=rgb(0,0,0)
+
+!Background
+PUSH "\e[42mH\e[m"
+ ?screen_cell 0,7 = {0x48} width=1 attrs={} fg=rgb(240,240,240) bg=idx(2)
+
+!Super/subscript
+PUSH "x\e[74m0\e[73m2\e[m"
+ ?screen_cell 0,8 = {0x78} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)
+ ?screen_cell 0,9 = {0x30} width=1 attrs={S_} fg=rgb(240,240,240) bg=rgb(0,0,0)
+ ?screen_cell 0,10 = {0x32} width=1 attrs={S^} fg=rgb(240,240,240) bg=rgb(0,0,0)
+
+!EL sets reverse and colours to end of line
+PUSH "\e[H\e[7;33;44m\e[K"
+ ?screen_cell 0,0 = {} width=1 attrs={R} fg=idx(3) bg=idx(4)
+ ?screen_cell 0,79 = {} width=1 attrs={R} fg=idx(3) bg=idx(4)
+
+!DECSCNM xors reverse for entire screen
+PUSH "\e[?5h"
+ ?screen_cell 0,0 = {} width=1 attrs={} fg=idx(3) bg=idx(4)
+ ?screen_cell 0,79 = {} width=1 attrs={} fg=idx(3) bg=idx(4)
+ ?screen_cell 1,0 = {} width=1 attrs={R} fg=rgb(240,240,240) bg=rgb(0,0,0)
+PUSH "\e[?5\$p"
+ output "\e[?5;1\$y"
+PUSH "\e[?5l"
+ ?screen_cell 0,0 = {} width=1 attrs={R} fg=idx(3) bg=idx(4)
+ ?screen_cell 0,79 = {} width=1 attrs={R} fg=idx(3) bg=idx(4)
+ ?screen_cell 1,0 = {} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)
+PUSH "\e[?5\$p"
+ output "\e[?5;2\$y"
diff --git a/src/libvterm/t/65screen_protect.test b/src/libvterm/t/65screen_protect.test
new file mode 100644
index 0000000..ec412a5
--- /dev/null
+++ b/src/libvterm/t/65screen_protect.test
@@ -0,0 +1,16 @@
+INIT
+WANTSCREEN
+
+!Selective erase
+RESET
+PUSH "A\e[1\"qB\e[\"qC"
+ ?screen_row 0 = "ABC"
+PUSH "\e[G\e[?J"
+ ?screen_row 0 = " B"
+
+!Non-selective erase
+RESET
+PUSH "A\e[1\"qB\e[\"qC"
+ ?screen_row 0 = "ABC"
+PUSH "\e[G\e[J"
+ ?screen_row 0 = ""
diff --git a/src/libvterm/t/66screen_extent.test b/src/libvterm/t/66screen_extent.test
new file mode 100644
index 0000000..a126cec
--- /dev/null
+++ b/src/libvterm/t/66screen_extent.test
@@ -0,0 +1,11 @@
+INIT
+WANTSCREEN
+
+!Bold extent
+RESET
+PUSH "AB\e[1mCD\e[mE"
+ ?screen_attrs_extent 0,0 = 0,0-1,1
+ ?screen_attrs_extent 0,1 = 0,0-1,1
+ ?screen_attrs_extent 0,2 = 0,2-1,3
+ ?screen_attrs_extent 0,3 = 0,2-1,3
+ ?screen_attrs_extent 0,4 = 0,4-1,79
diff --git a/src/libvterm/t/67screen_dbl_wh.test b/src/libvterm/t/67screen_dbl_wh.test
new file mode 100644
index 0000000..9c81e83
--- /dev/null
+++ b/src/libvterm/t/67screen_dbl_wh.test
@@ -0,0 +1,38 @@
+INIT
+WANTSCREEN
+
+RESET
+
+!Single Width, Single Height
+RESET
+PUSH "\e#5"
+PUSH "abcde"
+ ?screen_cell 0,0 = {0x61} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)
+
+!Double Width, Single Height
+RESET
+PUSH "\e#6"
+PUSH "abcde"
+ ?screen_cell 0,0 = {0x61} width=1 attrs={} dwl fg=rgb(240,240,240) bg=rgb(0,0,0)
+
+!Double Height
+RESET
+PUSH "\e#3"
+PUSH "abcde"
+PUSH "\r\n\e#4"
+PUSH "abcde"
+ ?screen_cell 0,0 = {0x61} width=1 attrs={} dwl dhl-top fg=rgb(240,240,240) bg=rgb(0,0,0)
+ ?screen_cell 1,0 = {0x61} width=1 attrs={} dwl dhl-bottom fg=rgb(240,240,240) bg=rgb(0,0,0)
+
+!Late change
+RESET
+PUSH "abcde"
+ ?screen_cell 0,0 = {0x61} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)
+PUSH "\e#6"
+ ?screen_cell 0,0 = {0x61} width=1 attrs={} dwl fg=rgb(240,240,240) bg=rgb(0,0,0)
+
+!DWL doesn't spill over on scroll
+RESET
+PUSH "\e[25H\e#6Final\r\n"
+ ?screen_cell 23,0 = {0x46} width=1 attrs={} dwl fg=rgb(240,240,240) bg=rgb(0,0,0)
+ ?screen_cell 24,0 = {} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)
diff --git a/src/libvterm/t/68screen_termprops.test b/src/libvterm/t/68screen_termprops.test
new file mode 100644
index 0000000..bba6660
--- /dev/null
+++ b/src/libvterm/t/68screen_termprops.test
@@ -0,0 +1,17 @@
+INIT
+WANTSCREEN p
+
+RESET
+ settermprop 1 true
+ settermprop 2 true
+ settermprop 7 1
+
+!Cursor visibility
+PUSH "\e[?25h"
+ settermprop 1 true
+PUSH "\e[?25l"
+ settermprop 1 false
+
+!Title
+PUSH "\e]2;Here is my title\a"
+ settermprop 4 ["Here is my title"]
diff --git a/src/libvterm/t/69screen_reflow.test b/src/libvterm/t/69screen_reflow.test
new file mode 100644
index 0000000..278cc5b
--- /dev/null
+++ b/src/libvterm/t/69screen_reflow.test
@@ -0,0 +1,79 @@
+INIT
+# Run these tests on a much smaller default screen, so debug output is
+# nowhere near as noisy
+RESIZE 5,10
+WANTSTATE
+WANTSCREEN r
+RESET
+
+!Resize wider reflows wide lines
+RESET
+PUSH "A"x12
+ ?screen_row 0 = "AAAAAAAAAA"
+ ?screen_row 1 = "AA"
+ ?lineinfo 1 = cont
+ ?cursor = 1,2
+RESIZE 5,15
+ ?screen_row 0 = "AAAAAAAAAAAA"
+ ?screen_row 1 =
+ ?lineinfo 1 =
+ ?cursor = 0,12
+RESIZE 5,20
+ ?screen_row 0 = "AAAAAAAAAAAA"
+ ?screen_row 1 =
+ ?lineinfo 1 =
+ ?cursor = 0,12
+
+!Resize narrower can create continuation lines
+RESET
+RESIZE 5,10
+PUSH "ABCDEFGHI"
+ ?screen_row 0 = "ABCDEFGHI"
+ ?screen_row 1 = ""
+ ?lineinfo 1 =
+ ?cursor = 0,9
+RESIZE 5,8
+ ?screen_row 0 = "ABCDEFGH"
+ ?screen_row 1 = "I"
+ ?lineinfo 1 = cont
+ ?cursor = 1,1
+RESIZE 5,6
+ ?screen_row 0 = "ABCDEF"
+ ?screen_row 1 = "GHI"
+ ?lineinfo 1 = cont
+ ?cursor = 1,3
+
+!Shell wrapped prompt behaviour
+RESET
+RESIZE 5,10
+PUSH "PROMPT GOES HERE\r\n> \r\n\r\nPROMPT GOES HERE\r\n> "
+ ?screen_row 0 = "> "
+ ?screen_row 1 = ""
+ ?screen_row 2 = "PROMPT GOE"
+ ?screen_row 3 = "S HERE"
+ ?lineinfo 3 = cont
+ ?screen_row 4 = "> "
+ ?cursor = 4,2
+RESIZE 5,11
+ ?screen_row 0 = "> "
+ ?screen_row 1 = ""
+ ?screen_row 2 = "PROMPT GOES"
+ ?screen_row 3 = " HERE"
+ ?lineinfo 3 = cont
+ ?screen_row 4 = "> "
+ ?cursor = 4,2
+RESIZE 5,12
+ ?screen_row 0 = "> "
+ ?screen_row 1 = ""
+ ?screen_row 2 = "PROMPT GOES "
+ ?screen_row 3 = "HERE"
+ ?lineinfo 3 = cont
+ ?screen_row 4 = "> "
+ ?cursor = 4,2
+RESIZE 5,16
+ ?screen_row 0 = "> "
+ ?screen_row 1 = ""
+ ?screen_row 2 = "PROMPT GOES HERE"
+ ?lineinfo 3 =
+ ?screen_row 3 = "> "
+ ?cursor = 3,2
diff --git a/src/libvterm/t/90vttest_01-movement-1.test b/src/libvterm/t/90vttest_01-movement-1.test
new file mode 100644
index 0000000..c1a8cb9
--- /dev/null
+++ b/src/libvterm/t/90vttest_01-movement-1.test
@@ -0,0 +1,87 @@
+INIT
+WANTSTATE
+WANTSCREEN
+
+RESET
+
+PUSH "\e#8"
+
+PUSH "\e[9;10H\e[1J"
+PUSH "\e[18;60H\e[0J\e[1K"
+PUSH "\e[9;71H\e[0K"
+
+$SEQ 10 16: PUSH "\e[\#;10H\e[1K\e[\#;71H\e[0K"
+
+PUSH "\e[17;30H\e[2K"
+
+$SEQ 1 80: PUSH "\e[24;\#f*\e[1;\#f*"
+
+PUSH "\e[2;2H"
+
+$REP 22: PUSH "+\e[1D\eD"
+
+PUSH "\e[23;79H"
+$REP 22: PUSH "+\e[1D\eM"
+
+PUSH "\e[2;1H"
+$SEQ 2 23: PUSH "*\e[\#;80H*\e[10D\eE"
+
+PUSH "\e[2;10H\e[42D\e[2C"
+$REP 76: PUSH "+\e[0C\e[2D\e[1C"
+
+PUSH "\e[23;70H\e[42C\e[2D"
+
+$REP 76: PUSH "+\e[1D\e[1C\e[0D\b"
+
+PUSH "\e[1;1H"
+PUSH "\e[10A"
+PUSH "\e[1A"
+PUSH "\e[0A"
+PUSH "\e[24;80H"
+PUSH "\e[10B"
+PUSH "\e[1B"
+PUSH "\e[0B"
+PUSH "\e[10;12H"
+
+$REP 58: PUSH " "
+PUSH "\e[1B\e[58D"
+
+$REP 58: PUSH " "
+PUSH "\e[1B\e[58D"
+
+$REP 58: PUSH " "
+PUSH "\e[1B\e[58D"
+
+$REP 58: PUSH " "
+PUSH "\e[1B\e[58D"
+
+$REP 58: PUSH " "
+PUSH "\e[1B\e[58D"
+
+$REP 58: PUSH " "
+PUSH "\e[1B\e[58D"
+
+PUSH "\e[5A\e[1CThe screen should be cleared, and have an unbroken bor-"
+PUSH "\e[12;13Hder of *'s and +'s around the edge, and exactly in the"
+PUSH "\e[13;13Hmiddle there should be a frame of E's around this text"
+PUSH "\e[14;13Hwith one (1) free position around it. Push <RETURN>"
+
+# And the result is...
+
+!Output
+ ?screen_row 0 = "********************************************************************************"
+ ?screen_row 1 = "*++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*"
+$SEQ 2 7: ?screen_row \# = "*+ +*"
+ ?screen_row 8 = "*+ EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE +*"
+ ?screen_row 9 = "*+ E E +*"
+ ?screen_row 10 = "*+ E The screen should be cleared, and have an unbroken bor- E +*"
+ ?screen_row 11 = "*+ E der of *'s and +'s around the edge, and exactly in the E +*"
+ ?screen_row 12 = "*+ E middle there should be a frame of E's around this text E +*"
+ ?screen_row 13 = "*+ E with one (1) free position around it. Push <RETURN> E +*"
+ ?screen_row 14 = "*+ E E +*"
+ ?screen_row 15 = "*+ EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE +*"
+$SEQ 16 21: ?screen_row \# = "*+ +*"
+ ?screen_row 22 = "*++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*"
+ ?screen_row 23 = "********************************************************************************"
+
+?cursor = 13,67
diff --git a/src/libvterm/t/90vttest_01-movement-2.test b/src/libvterm/t/90vttest_01-movement-2.test
new file mode 100644
index 0000000..3a515e3
--- /dev/null
+++ b/src/libvterm/t/90vttest_01-movement-2.test
@@ -0,0 +1,40 @@
+INIT
+WANTSTATE
+WANTSCREEN
+
+RESET
+
+PUSH "\e[3;21r"
+PUSH "\e[?6h"
+
+PUSH "\e[19;1HA\e[19;80Ha\x0a\e[18;80HaB\e[19;80HB\b b\x0a\e[19;80HC\b\b\t\tc\e[19;2H\bC\x0a\e[19;80H\x0a\e[18;1HD\e[18;80Hd"
+PUSH "\e[19;1HE\e[19;80He\x0a\e[18;80HeF\e[19;80HF\b f\x0a\e[19;80HG\b\b\t\tg\e[19;2H\bG\x0a\e[19;80H\x0a\e[18;1HH\e[18;80Hh"
+PUSH "\e[19;1HI\e[19;80Hi\x0a\e[18;80HiJ\e[19;80HJ\b j\x0a\e[19;80HK\b\b\t\tk\e[19;2H\bK\x0a\e[19;80H\x0a\e[18;1HL\e[18;80Hl"
+PUSH "\e[19;1HM\e[19;80Hm\x0a\e[18;80HmN\e[19;80HN\b n\x0a\e[19;80HO\b\b\t\to\e[19;2H\bO\x0a\e[19;80H\x0a\e[18;1HP\e[18;80Hp"
+PUSH "\e[19;1HQ\e[19;80Hq\x0a\e[18;80HqR\e[19;80HR\b r\x0a\e[19;80HS\b\b\t\ts\e[19;2H\bS\x0a\e[19;80H\x0a\e[18;1HT\e[18;80Ht"
+PUSH "\e[19;1HU\e[19;80Hu\x0a\e[18;80HuV\e[19;80HV\b v\x0a\e[19;80HW\b\b\t\tw\e[19;2H\bW\x0a\e[19;80H\x0a\e[18;1HX\e[18;80Hx"
+PUSH "\e[19;1HY\e[19;80Hy\x0a\e[18;80HyZ\e[19;80HZ\b z\x0a"
+
+!Output
+
+?screen_row 2 = "I i"
+?screen_row 3 = "J j"
+?screen_row 4 = "K k"
+?screen_row 5 = "L l"
+?screen_row 6 = "M m"
+?screen_row 7 = "N n"
+?screen_row 8 = "O o"
+?screen_row 9 = "P p"
+?screen_row 10 = "Q q"
+?screen_row 11 = "R r"
+?screen_row 12 = "S s"
+?screen_row 13 = "T t"
+?screen_row 14 = "U u"
+?screen_row 15 = "V v"
+?screen_row 16 = "W w"
+?screen_row 17 = "X x"
+?screen_row 18 = "Y y"
+?screen_row 19 = "Z z"
+?screen_row 20 = ""
+
+?cursor = 20,79
diff --git a/src/libvterm/t/90vttest_01-movement-3.test b/src/libvterm/t/90vttest_01-movement-3.test
new file mode 100644
index 0000000..f9a99bf
--- /dev/null
+++ b/src/libvterm/t/90vttest_01-movement-3.test
@@ -0,0 +1,21 @@
+# Test of cursor-control characters inside ESC sequences
+INIT
+WANTSTATE
+WANTSCREEN
+
+RESET
+
+PUSH "A B C D E F G H I"
+PUSH "\x0d\x0a"
+PUSH "A\e[2\bCB\e[2\bCC\e[2\bCD\e[2\bCE\e[2\bCF\e[2\bCG\e[2\bCH\e[2\bCI"
+PUSH "\x0d\x0a"
+PUSH "A \e[\x0d2CB\e[\x0d4CC\e[\x0d6CD\e[\x0d8CE\e[\x0d10CF\e[\x0d12CG\e[\x0d14CH\e[\x0d16CI"
+PUSH "\x0d\x0a"
+PUSH "A \e[1\x0bAB \e[1\x0bAC \e[1\x0bAD \e[1\x0bAE \e[1\x0bAF \e[1\x0bAG \e[1\x0bAH \e[1\x0bAI \e[1\x0bA"
+
+!Output
+
+$SEQ 0 2: ?screen_row \# = "A B C D E F G H I"
+ ?screen_row 3 = "A B C D E F G H I "
+
+?cursor = 3,18
diff --git a/src/libvterm/t/90vttest_01-movement-4.test b/src/libvterm/t/90vttest_01-movement-4.test
new file mode 100644
index 0000000..0dab3c7
--- /dev/null
+++ b/src/libvterm/t/90vttest_01-movement-4.test
@@ -0,0 +1,36 @@
+# Test of leading zeroes in ESC sequences
+INIT
+WANTSCREEN
+
+RESET
+
+PUSH "\e[00000000004;000000001HT"
+PUSH "\e[00000000004;000000002Hh"
+PUSH "\e[00000000004;000000003Hi"
+PUSH "\e[00000000004;000000004Hs"
+PUSH "\e[00000000004;000000005H "
+PUSH "\e[00000000004;000000006Hi"
+PUSH "\e[00000000004;000000007Hs"
+PUSH "\e[00000000004;000000008H "
+PUSH "\e[00000000004;000000009Ha"
+PUSH "\e[00000000004;0000000010H "
+PUSH "\e[00000000004;0000000011Hc"
+PUSH "\e[00000000004;0000000012Ho"
+PUSH "\e[00000000004;0000000013Hr"
+PUSH "\e[00000000004;0000000014Hr"
+PUSH "\e[00000000004;0000000015He"
+PUSH "\e[00000000004;0000000016Hc"
+PUSH "\e[00000000004;0000000017Ht"
+PUSH "\e[00000000004;0000000018H "
+PUSH "\e[00000000004;0000000019Hs"
+PUSH "\e[00000000004;0000000020He"
+PUSH "\e[00000000004;0000000021Hn"
+PUSH "\e[00000000004;0000000022Ht"
+PUSH "\e[00000000004;0000000023He"
+PUSH "\e[00000000004;0000000024Hn"
+PUSH "\e[00000000004;0000000025Hc"
+PUSH "\e[00000000004;0000000026He"
+
+!Output
+
+?screen_row 3 = "This is a correct sentence"
diff --git a/src/libvterm/t/90vttest_02-screen-1.test b/src/libvterm/t/90vttest_02-screen-1.test
new file mode 100644
index 0000000..003d56f
--- /dev/null
+++ b/src/libvterm/t/90vttest_02-screen-1.test
@@ -0,0 +1,18 @@
+# Test of WRAP AROUND mode setting.
+INIT
+WANTSCREEN
+
+RESET
+
+PUSH "\e[?7h"
+$REP 170: PUSH "*"
+
+PUSH "\e[?7l\e[3;1H"
+$REP 177: PUSH "*"
+
+PUSH "\e[?7h\e[5;1HOK"
+
+!Output
+$SEQ 0 2: ?screen_row \# = "********************************************************************************"
+ ?screen_row 3 = ""
+ ?screen_row 4 = "OK"
diff --git a/src/libvterm/t/90vttest_02-screen-2.test b/src/libvterm/t/90vttest_02-screen-2.test
new file mode 100644
index 0000000..1c3a6a7
--- /dev/null
+++ b/src/libvterm/t/90vttest_02-screen-2.test
@@ -0,0 +1,29 @@
+# TAB setting/resetting
+INIT
+WANTSTATE
+WANTSCREEN
+
+RESET
+
+PUSH "\e[2J\e[3g"
+
+PUSH "\e[1;1H"
+$REP 26: PUSH "\e[3C\eH"
+
+PUSH "\e[1;4H"
+$REP 13: PUSH "\e[0g\e[6C"
+
+PUSH "\e[1;7H"
+PUSH "\e[1g\e[2g"
+
+PUSH "\e[1;1H"
+$REP 13: PUSH "\t*"
+
+PUSH "\e[2;2H"
+$REP 13: PUSH " *"
+
+!Output
+?screen_row 0 = " * * * * * * * * * * * * *"
+?screen_row 1 = " * * * * * * * * * * * * *"
+
+?cursor = 1,79
diff --git a/src/libvterm/t/90vttest_02-screen-3.test b/src/libvterm/t/90vttest_02-screen-3.test
new file mode 100644
index 0000000..8cdf8df
--- /dev/null
+++ b/src/libvterm/t/90vttest_02-screen-3.test
@@ -0,0 +1,16 @@
+# Origin mode
+INIT
+WANTSCREEN
+
+RESET
+
+PUSH "\e[?6h"
+PUSH "\e[23;24r"
+PUSH "\n"
+PUSH "Bottom"
+PUSH "\e[1;1H"
+PUSH "Above"
+
+!Output
+?screen_row 22 = "Above"
+?screen_row 23 = "Bottom"
diff --git a/src/libvterm/t/90vttest_02-screen-4.test b/src/libvterm/t/90vttest_02-screen-4.test
new file mode 100644
index 0000000..44d51f1
--- /dev/null
+++ b/src/libvterm/t/90vttest_02-screen-4.test
@@ -0,0 +1,17 @@
+# Origin mode (2)
+INIT
+WANTSCREEN
+
+RESET
+
+PUSH "\e[?6l"
+PUSH "\e[23;24r"
+PUSH "\e[24;1H"
+PUSH "Bottom"
+PUSH "\e[1;1H"
+PUSH "Top"
+
+!Output
+?screen_row 23 = "Bottom"
+?screen_row 0 = "Top"
+
diff --git a/src/libvterm/t/92lp1640917.test b/src/libvterm/t/92lp1640917.test
new file mode 100644
index 0000000..70de439
--- /dev/null
+++ b/src/libvterm/t/92lp1640917.test
@@ -0,0 +1,13 @@
+INIT
+WANTSTATE
+
+!Mouse reporting should not break by idempotent DECSM 1002
+PUSH "\e[?1002h"
+MOUSEMOVE 0,0 0
+MOUSEBTN d 1 0
+ output "\e[M\x20\x21\x21"
+MOUSEMOVE 1,0 0
+ output "\e[M\x40\x21\x22"
+PUSH "\e[?1002h"
+MOUSEMOVE 2,0 0
+ output "\e[M\x40\x21\x23"
diff --git a/src/libvterm/t/harness.c b/src/libvterm/t/harness.c
new file mode 100644
index 0000000..d12c120
--- /dev/null
+++ b/src/libvterm/t/harness.c
@@ -0,0 +1,1233 @@
+#include "vterm.h"
+#include "../src/vterm_internal.h" // We pull in some internal bits too
+
+#include <assert.h>
+#include <stdio.h>
+#include <string.h>
+
+#define streq(a,b) (!strcmp(a,b))
+#define strstartswith(a,b) (!strncmp(a,b,strlen(b)))
+
+static size_t inplace_hex2bytes(char *s)
+{
+ char *inpos = s, *outpos = s;
+
+ while(*inpos) {
+ unsigned int ch;
+ if(sscanf(inpos, "%2x", &ch) < 1)
+ break;
+ *outpos = ch;
+ outpos += 1; inpos += 2;
+ }
+
+ return outpos - s;
+}
+
+static VTermModifier strpe_modifiers(char **strp)
+{
+ VTermModifier state = 0;
+
+ while((*strp)[0]) {
+ switch(((*strp)++)[0]) {
+ case 'S': state |= VTERM_MOD_SHIFT; break;
+ case 'C': state |= VTERM_MOD_CTRL; break;
+ case 'A': state |= VTERM_MOD_ALT; break;
+ default: return state;
+ }
+ }
+
+ return state;
+}
+
+static VTermKey strp_key(char *str)
+{
+ static struct {
+ char *name;
+ VTermKey key;
+ } keys[] = {
+ { "Up", VTERM_KEY_UP },
+ { "Tab", VTERM_KEY_TAB },
+ { "Enter", VTERM_KEY_ENTER },
+ { "KP0", VTERM_KEY_KP_0 },
+ { "F1", VTERM_KEY_FUNCTION(1) },
+ { NULL, VTERM_KEY_NONE },
+ };
+ int i;
+
+ for(i = 0; keys[i].name; i++) {
+ if(streq(str, keys[i].name))
+ return keys[i].key;
+ }
+
+ return VTERM_KEY_NONE;
+}
+
+static void print_color(const VTermColor *col)
+{
+ if (VTERM_COLOR_IS_RGB(col)) {
+ printf("rgb(%d,%d,%d", col->red, col->green, col->blue);
+ }
+ else if (VTERM_COLOR_IS_INDEXED(col)) {
+ printf("idx(%d", col->index);
+ }
+ else {
+ printf("invalid(%d", col->type);
+ }
+ if (VTERM_COLOR_IS_DEFAULT_FG(col)) {
+ printf(",is_default_fg");
+ }
+ if (VTERM_COLOR_IS_DEFAULT_BG(col)) {
+ printf(",is_default_bg");
+ }
+ printf(")");
+}
+
+static VTerm *vt;
+static VTermState *state;
+static VTermScreen *screen;
+
+static VTermEncodingInstance encoding;
+
+static void term_output(const char *s, size_t len, void *user UNUSED)
+{
+ size_t i;
+
+ printf("output ");
+ for(i = 0; i < len; i++)
+ printf("%x%s", (unsigned char)s[i], i < len-1 ? "," : "\n");
+}
+
+static void printhex(const char *s, size_t len)
+{
+ while(len--)
+ printf("%02x", (uint8_t)(s++)[0]);
+}
+
+static int parser_text(const char bytes[], size_t len, void *user UNUSED)
+{
+ size_t i;
+
+ printf("text ");
+ for(i = 0; i < len; i++) {
+ unsigned char b = bytes[i];
+ if(b < 0x20 || b == 0x7f || (b >= 0x80 && b < 0xa0))
+ break;
+ printf(i ? ",%x" : "%x", b);
+ }
+ printf("\n");
+
+ return i;
+}
+
+static int parser_control(unsigned char control, void *user UNUSED)
+{
+ printf("control %02x\n", control);
+
+ return 1;
+}
+
+static int parser_escape(const char bytes[], size_t len, void *user UNUSED)
+{
+ if(bytes[0] >= 0x20 && bytes[0] < 0x30) {
+ if(len < 2)
+ return -1;
+ len = 2;
+ }
+ else {
+ len = 1;
+ }
+
+ printf("escape ");
+ printhex(bytes, len);
+ printf("\n");
+
+ return len;
+}
+
+static int parser_csi(const char *leader, const long args[], int argcount, const char *intermed, char command, void *user UNUSED)
+{
+ int i;
+ printf("csi %02x", command);
+
+ if(leader && leader[0]) {
+ printf(" L=");
+ for(i = 0; leader[i]; i++)
+ printf("%02x", leader[i]);
+ }
+
+ for(i = 0; i < argcount; i++) {
+ char sep = i ? ',' : ' ';
+
+ if(args[i] == CSI_ARG_MISSING)
+ printf("%c*", sep);
+ else
+ printf("%c%ld%s", sep, CSI_ARG(args[i]), CSI_ARG_HAS_MORE(args[i]) ? "+" : "");
+ }
+
+ if(intermed && intermed[0]) {
+ printf(" I=");
+ for(i = 0; intermed[i]; i++)
+ printf("%02x", intermed[i]);
+ }
+
+ printf("\n");
+
+ return 1;
+}
+
+static int parser_osc(int command, VTermStringFragment frag, void *user UNUSED)
+{
+
+ printf("osc ");
+
+ if(frag.initial) {
+ if(command == -1)
+ printf("[");
+ else
+ printf("[%d;", command);
+ }
+
+ printhex(frag.str, frag.len);
+
+ if(frag.final)
+ printf("]");
+
+ printf("\n");
+
+ return 1;
+}
+
+static int parser_dcs(const char *command, size_t commandlen, VTermStringFragment frag, void *user UNUSED)
+{
+ printf("dcs ");
+
+ if(frag.initial) {
+ size_t i;
+ printf("[");
+ for(i = 0; i < commandlen; i++)
+ printf("%02x", command[i]);
+ }
+
+ printhex(frag.str, frag.len);
+
+ if(frag.final)
+ printf("]");
+
+ printf("\n");
+
+ return 1;
+}
+
+static int parser_apc(VTermStringFragment frag, void *user UNUSED)
+{
+ printf("apc ");
+
+ if(frag.initial)
+ printf("[");
+
+ printhex(frag.str, frag.len);
+
+ if(frag.final)
+ printf("]");
+
+ printf("\n");
+
+ return 1;
+}
+
+static int parser_pm(VTermStringFragment frag, void *user UNUSED)
+{
+ printf("pm ");
+
+ if(frag.initial)
+ printf("[");
+
+ printhex(frag.str, frag.len);
+
+ if(frag.final)
+ printf("]");
+
+ printf("\n");
+
+ return 1;
+}
+
+static int parser_sos(VTermStringFragment frag, void *user UNUSED)
+{
+ printf("sos ");
+
+ if(frag.initial)
+ printf("[");
+
+ printhex(frag.str, frag.len);
+
+ if(frag.final)
+ printf("]");
+
+ printf("\n");
+
+ return 1;
+}
+
+static VTermParserCallbacks parser_cbs = {
+ parser_text, // text
+ parser_control, // control
+ parser_escape, // escape
+ parser_csi, // csi
+ parser_osc, // osc
+ parser_dcs, // dcs
+ parser_apc, // apc
+ parser_pm, // pm
+ parser_sos, // sos
+ NULL // resize
+};
+
+static VTermStateFallbacks fallbacks = {
+ parser_control, // control
+ parser_csi, // csi
+ parser_osc, // osc
+ parser_dcs, // dcs
+ parser_apc, // dcs
+ parser_pm, // pm
+ parser_sos // sos
+};
+
+/* These callbacks are shared by State and Screen */
+
+static int want_movecursor = 0;
+static VTermPos state_pos;
+static int movecursor(VTermPos pos, VTermPos oldpos UNUSED, int visible UNUSED, void *user UNUSED)
+{
+ state_pos = pos;
+
+ if(want_movecursor)
+ printf("movecursor %d,%d\n", pos.row, pos.col);
+
+ return 1;
+}
+
+static int want_scrollrect = 0;
+static int scrollrect(VTermRect rect, int downward, int rightward, void *user UNUSED)
+{
+ if(!want_scrollrect)
+ return 0;
+
+ printf("scrollrect %d..%d,%d..%d => %+d,%+d\n",
+ rect.start_row, rect.end_row, rect.start_col, rect.end_col,
+ downward, rightward);
+
+ return 1;
+}
+
+static int want_moverect = 0;
+static int moverect(VTermRect dest, VTermRect src, void *user UNUSED)
+{
+ if(!want_moverect)
+ return 0;
+
+ printf("moverect %d..%d,%d..%d -> %d..%d,%d..%d\n",
+ src.start_row, src.end_row, src.start_col, src.end_col,
+ dest.start_row, dest.end_row, dest.start_col, dest.end_col);
+
+ return 1;
+}
+
+static int want_settermprop = 0;
+static int settermprop(VTermProp prop, VTermValue *val, void *user UNUSED)
+{
+ VTermValueType type;
+ if(!want_settermprop)
+ return 1;
+
+ type = vterm_get_prop_type(prop);
+ switch(type) {
+ case VTERM_VALUETYPE_BOOL:
+ printf("settermprop %d %s\n", prop, val->boolean ? "true" : "false");
+ return 1;
+ case VTERM_VALUETYPE_INT:
+ printf("settermprop %d %d\n", prop, val->number);
+ return 1;
+ case VTERM_VALUETYPE_STRING:
+ printf("settermprop %d %s\"%.*s\"%s\n", prop,
+ val->string.initial ? "[" : "", val->string.len, val->string.str, val->string.final ? "]" : "");
+ return 1;
+ case VTERM_VALUETYPE_COLOR:
+ printf("settermprop %d ", prop);
+ print_color(&val->color);
+ printf("\n");
+ return 1;
+
+ case VTERM_N_VALUETYPES:
+ return 0;
+ }
+
+ return 0;
+}
+
+// These callbacks are for State
+
+static int want_state_putglyph = 0;
+static int state_putglyph(VTermGlyphInfo *info, VTermPos pos, void *user UNUSED)
+{
+ int i;
+ if(!want_state_putglyph)
+ return 1;
+
+ printf("putglyph ");
+ for(i = 0; i < VTERM_MAX_CHARS_PER_CELL && info->chars[i]; i++)
+ printf(i ? ",%x" : "%x", info->chars[i]);
+ printf(" %d %d,%d", info->width, pos.row, pos.col);
+ if(info->protected_cell)
+ printf(" prot");
+ if(info->dwl)
+ printf(" dwl");
+ if(info->dhl)
+ printf(" dhl-%s", info->dhl == 1 ? "top" : info->dhl == 2 ? "bottom" : "?" );
+ printf("\n");
+
+ return 1;
+}
+
+static int want_state_erase = 0;
+static int state_erase(VTermRect rect, int selective, void *user UNUSED)
+{
+ if(!want_state_erase)
+ return 1;
+
+ printf("erase %d..%d,%d..%d%s\n",
+ rect.start_row, rect.end_row, rect.start_col, rect.end_col,
+ selective ? " selective" : "");
+
+ return 1;
+}
+
+static struct {
+ int bold;
+ int underline;
+ int italic;
+ int blink;
+ int reverse;
+ int conceal;
+ int strike;
+ int font;
+ int small;
+ int baseline;
+ VTermColor foreground;
+ VTermColor background;
+} state_pen;
+static int state_setpenattr(VTermAttr attr, VTermValue *val, void *user UNUSED)
+{
+ switch(attr) {
+ case VTERM_ATTR_BOLD:
+ state_pen.bold = val->boolean;
+ break;
+ case VTERM_ATTR_UNDERLINE:
+ state_pen.underline = val->number;
+ break;
+ case VTERM_ATTR_ITALIC:
+ state_pen.italic = val->boolean;
+ break;
+ case VTERM_ATTR_BLINK:
+ state_pen.blink = val->boolean;
+ break;
+ case VTERM_ATTR_REVERSE:
+ state_pen.reverse = val->boolean;
+ break;
+ case VTERM_ATTR_CONCEAL:
+ state_pen.conceal = val->boolean;
+ break;
+ case VTERM_ATTR_STRIKE:
+ state_pen.strike = val->boolean;
+ break;
+ case VTERM_ATTR_FONT:
+ state_pen.font = val->number;
+ break;
+ case VTERM_ATTR_SMALL:
+ state_pen.small = val->boolean;
+ break;
+ case VTERM_ATTR_BASELINE:
+ state_pen.baseline = val->number;
+ break;
+ case VTERM_ATTR_FOREGROUND:
+ state_pen.foreground = val->color;
+ break;
+ case VTERM_ATTR_BACKGROUND:
+ state_pen.background = val->color;
+ break;
+
+ case VTERM_N_ATTRS:
+ return 0;
+ }
+
+ return 1;
+}
+
+static int state_setlineinfo(int row UNUSED, const VTermLineInfo *newinfo UNUSED, const VTermLineInfo *oldinfo UNUSED, void *user UNUSED)
+{
+ return 1;
+}
+
+static int want_state_scrollback = 0;
+static int state_sb_clear(void *user UNUSED) {
+ if(!want_state_scrollback)
+ return 1;
+
+ printf("sb_clear\n");
+ return 0;
+}
+
+VTermStateCallbacks state_cbs = {
+ state_putglyph, // putglyph
+ movecursor, // movecursor
+ scrollrect, // scrollrect
+ moverect, // moverect
+ state_erase, // erase
+ NULL, // initpen
+ state_setpenattr, // setpenattr
+ settermprop, // settermprop
+ NULL, // bell
+ NULL, // resize
+ state_setlineinfo, // setlineinfo
+ state_sb_clear, // sb_clear
+};
+
+static int selection_set(VTermSelectionMask mask, VTermStringFragment frag, void *user UNUSED)
+{
+ printf("selection-set mask=%04X ", mask);
+ if(frag.initial)
+ printf("[");
+ printhex(frag.str, frag.len);
+ if(frag.final)
+ printf("]");
+ printf("\n");
+
+ return 1;
+}
+
+static int selection_query(VTermSelectionMask mask, void *user UNUSED)
+{
+ printf("selection-query mask=%04X\n", mask);
+
+ return 1;
+}
+
+VTermSelectionCallbacks selection_cbs = {
+ .set = selection_set,
+ .query = selection_query,
+};
+
+static int want_screen_damage = 0;
+static int want_screen_damage_cells = 0;
+static int screen_damage(VTermRect rect, void *user UNUSED)
+{
+ if(!want_screen_damage)
+ return 1;
+
+ printf("damage %d..%d,%d..%d",
+ rect.start_row, rect.end_row, rect.start_col, rect.end_col);
+
+ if(want_screen_damage_cells) {
+ int equals = FALSE;
+ int row;
+ int col;
+
+ for(row = rect.start_row; row < rect.end_row; row++) {
+ int eol = rect.end_col;
+ while(eol > rect.start_col) {
+ VTermScreenCell cell;
+ VTermPos pos;
+ pos.row = row;
+ pos.col = eol-1;
+ vterm_screen_get_cell(screen, pos, &cell);
+ if(cell.chars[0])
+ break;
+
+ eol--;
+ }
+
+ if(eol == rect.start_col)
+ break;
+
+ if(!equals)
+ printf(" ="), equals = TRUE;
+
+ printf(" %d<", row);
+ for(col = rect.start_col; col < eol; col++) {
+ VTermScreenCell cell;
+ VTermPos pos;
+ pos.row = row;
+ pos.col = col;
+ vterm_screen_get_cell(screen, pos, &cell);
+ printf(col == rect.start_col ? "%02X" : " %02X", cell.chars[0]);
+ }
+ printf(">");
+ }
+ }
+
+ printf("\n");
+
+ return 1;
+}
+
+static int want_screen_scrollback = 0;
+static int screen_sb_pushline(int cols, const VTermScreenCell *cells, void *user UNUSED)
+{
+ int eol;
+ int c;
+
+ if(!want_screen_scrollback)
+ return 1;
+
+ eol = cols;
+ while(eol && !cells[eol-1].chars[0])
+ eol--;
+
+ printf("sb_pushline %d =", cols);
+ for(c = 0; c < eol; c++)
+ printf(" %02X", cells[c].chars[0]);
+ printf("\n");
+
+ return 1;
+}
+
+static int screen_sb_popline(int cols, VTermScreenCell *cells, void *user UNUSED)
+{
+ int col;
+
+ if(!want_screen_scrollback)
+ return 0;
+
+ // All lines of scrollback contain "ABCDE"
+ for(col = 0; col < cols; col++) {
+ if(col < 5)
+ cells[col].chars[0] = 'A' + col;
+ else
+ cells[col].chars[0] = 0;
+
+ cells[col].width = 1;
+ }
+
+ printf("sb_popline %d\n", cols);
+ return 1;
+}
+
+static int screen_sb_clear(void *user UNUSED)
+{
+ if(!want_screen_scrollback)
+ return 1;
+
+ printf("sb_clear\n");
+ return 0;
+}
+
+VTermScreenCallbacks screen_cbs = {
+ screen_damage, // damage
+ moverect, // moverect
+ movecursor, // movecursor
+ settermprop, // settermprop
+ NULL, // bell
+ NULL, // resize
+ screen_sb_pushline, // sb_pushline
+ screen_sb_popline, // sb_popline
+ screen_sb_clear, // sb_clear
+};
+
+int main(int argc UNUSED, char **argv UNUSED)
+{
+ char line[1024] = {0};
+ int flag;
+
+ int err;
+
+ setvbuf(stdout, NULL, _IONBF, 0);
+
+ while(fgets(line, sizeof line, stdin)) {
+ char *nl;
+ size_t outlen;
+ err = 0;
+
+ if((nl = strchr(line, '\n')))
+ *nl = '\0';
+
+ if(streq(line, "INIT")) {
+ if(!vt)
+ vt = vterm_new(25, 80);
+
+ // Somehow this makes tests fail
+ // vterm_output_set_callback(vt, term_output, NULL);
+ }
+
+ else if(streq(line, "WANTPARSER")) {
+ assert(vt);
+ vterm_parser_set_callbacks(vt, &parser_cbs, NULL);
+ }
+
+ else if(strstartswith(line, "WANTSTATE") && (line[9] == '\0' || line[9] == ' ')) {
+ int i = 9;
+ int sense = 1;
+ assert(vt);
+ if(!state) {
+ state = vterm_obtain_state(vt);
+ vterm_state_set_callbacks(state, &state_cbs, NULL);
+ vterm_state_set_selection_callbacks(state, &selection_cbs, NULL, NULL, 1024);
+ vterm_state_set_bold_highbright(state, 1);
+ vterm_state_reset(state, 1);
+ }
+
+ while(line[i] == ' ')
+ i++;
+ for( ; line[i]; i++)
+ switch(line[i]) {
+ case '+':
+ sense = 1;
+ break;
+ case '-':
+ sense = 0;
+ break;
+ case 'g':
+ want_state_putglyph = sense;
+ break;
+ case 's':
+ want_scrollrect = sense;
+ break;
+ case 'm':
+ want_moverect = sense;
+ break;
+ case 'e':
+ want_state_erase = sense;
+ break;
+ case 'p':
+ want_settermprop = sense;
+ break;
+ case 'f':
+ vterm_state_set_unrecognised_fallbacks(state, sense ? &fallbacks : NULL, NULL);
+ break;
+ case 'b':
+ want_state_scrollback = sense;
+ break;
+ default:
+ fprintf(stderr, "Unrecognised WANTSTATE flag '%c'\n", line[i]);
+ }
+ }
+
+ else if(strstartswith(line, "WANTSCREEN") && (line[10] == '\0' || line[10] == ' ')) {
+ int i = 10;
+ int sense = 1;
+ assert(vt);
+ if(!screen)
+ screen = vterm_obtain_screen(vt);
+ vterm_screen_set_callbacks(screen, &screen_cbs, NULL);
+
+ while(line[i] == ' ')
+ i++;
+ for( ; line[i]; i++)
+ switch(line[i]) {
+ case '-':
+ sense = 0;
+ break;
+ case 'a':
+ vterm_screen_enable_altscreen(screen, 1);
+ break;
+ case 'd':
+ want_screen_damage = sense;
+ break;
+ case 'D':
+ want_screen_damage = sense;
+ want_screen_damage_cells = sense;
+ break;
+ case 'm':
+ want_moverect = sense;
+ break;
+ case 'c':
+ want_movecursor = sense;
+ break;
+ case 'p':
+ want_settermprop = 1;
+ break;
+ case 'b':
+ want_screen_scrollback = sense;
+ break;
+ case 'r':
+ vterm_screen_enable_reflow(screen, sense);
+ break;
+ default:
+ fprintf(stderr, "Unrecognised WANTSCREEN flag '%c'\n", line[i]);
+ }
+ }
+
+ else if(sscanf(line, "UTF8 %d", &flag)) {
+ vterm_set_utf8(vt, flag);
+ }
+
+ else if(streq(line, "RESET")) {
+ if(state) {
+ vterm_state_reset(state, 1);
+ vterm_state_get_cursorpos(state, &state_pos);
+ }
+ if(screen) {
+ vterm_screen_reset(screen, 1);
+ }
+ }
+
+ else if(strstartswith(line, "RESIZE ")) {
+ int rows, cols;
+ char *linep = line + 7;
+ while(linep[0] == ' ')
+ linep++;
+ sscanf(linep, "%d, %d", &rows, &cols);
+ vterm_set_size(vt, rows, cols);
+ }
+
+ else if(strstartswith(line, "PUSH ")) {
+ char *bytes = line + 5;
+ size_t len = inplace_hex2bytes(bytes);
+ assert(len);
+
+ size_t written = vterm_input_write(vt, bytes, len);
+ if(written < len)
+ fprintf(stderr, "! short write\n");
+ }
+
+ else if(streq(line, "WANTENCODING")) {
+ // This isn't really external API but it's hard to get this out any
+ // other way
+ encoding.enc = vterm_lookup_encoding(ENC_UTF8, 'u');
+ if(encoding.enc->init)
+ (*encoding.enc->init)(encoding.enc, encoding.data);
+ }
+
+ else if(strstartswith(line, "ENCIN ")) {
+ char *bytes = line + 6;
+ size_t len = inplace_hex2bytes(bytes);
+ assert(len);
+
+ uint32_t cp[1024];
+ int cpi = 0;
+ size_t pos = 0;
+
+ (*encoding.enc->decode)(encoding.enc, encoding.data,
+ cp, &cpi, len, bytes, &pos, len);
+
+ if(cpi > 0) {
+ int i;
+ printf("encout ");
+ for(i = 0; i < cpi; i++) {
+ printf(i ? ",%x" : "%x", cp[i]);
+ }
+ printf("\n");
+ }
+ }
+
+ else if(strstartswith(line, "INCHAR ")) {
+ char *linep = line + 7;
+ unsigned int c = 0;
+ VTermModifier mod;
+ while(linep[0] == ' ')
+ linep++;
+ mod = strpe_modifiers(&linep);
+ sscanf(linep, " %x", &c);
+
+ vterm_keyboard_unichar(vt, c, mod);
+ }
+
+ else if(strstartswith(line, "INKEY ")) {
+ VTermModifier mod;
+ VTermKey key;
+ char *linep = line + 6;
+ while(linep[0] == ' ')
+ linep++;
+ mod = strpe_modifiers(&linep);
+ while(linep[0] == ' ')
+ linep++;
+ key = strp_key(linep);
+
+ vterm_keyboard_key(vt, key, mod);
+ }
+
+ else if(strstartswith(line, "PASTE ")) {
+ char *linep = line + 6;
+ if(streq(linep, "START"))
+ vterm_keyboard_start_paste(vt);
+ else if(streq(linep, "END"))
+ vterm_keyboard_end_paste(vt);
+ else
+ goto abort_line;
+ }
+
+ else if(strstartswith(line, "FOCUS ")) {
+ assert(state);
+ char *linep = line + 6;
+ if(streq(linep, "IN"))
+ vterm_state_focus_in(state);
+ else if(streq(linep, "OUT"))
+ vterm_state_focus_out(state);
+ else
+ goto abort_line;
+ }
+
+ else if(strstartswith(line, "MOUSEMOVE ")) {
+ char *linep = line + 10;
+ int row, col, len;
+ VTermModifier mod;
+ while(linep[0] == ' ')
+ linep++;
+ sscanf(linep, "%d,%d%n", &row, &col, &len);
+ linep += len;
+ while(linep[0] == ' ')
+ linep++;
+ mod = strpe_modifiers(&linep);
+ vterm_mouse_move(vt, row, col, mod);
+ }
+
+ else if(strstartswith(line, "MOUSEBTN ")) {
+ char *linep = line + 9;
+ char press;
+ int button, len;
+ VTermModifier mod;
+ while(linep[0] == ' ')
+ linep++;
+ sscanf(linep, "%c %d%n", &press, &button, &len);
+ linep += len;
+ while(linep[0] == ' ')
+ linep++;
+ mod = strpe_modifiers(&linep);
+ vterm_mouse_button(vt, button, (press == 'd' || press == 'D'), mod);
+ }
+
+ else if(strstartswith(line, "SELECTION ")) {
+ char *linep = line + 10;
+ unsigned int mask;
+ int len;
+ VTermStringFragment frag = { 0 };
+ sscanf(linep, "%x%n", &mask, &len);
+ linep += len;
+ while(linep[0] == ' ')
+ linep++;
+ if(linep[0] == '[') {
+ frag.initial = TRUE;
+ linep++;
+ while(linep[0] == ' ')
+ linep++;
+ }
+ frag.len = inplace_hex2bytes(linep);
+ frag.str = linep;
+ assert(frag.len);
+
+ linep += frag.len * 2;
+ while(linep[0] == ' ')
+ linep++;
+ if(linep[0] == ']') {
+ frag.final = TRUE;
+ }
+ vterm_state_send_selection(state, mask, frag);
+ }
+
+ else if(strstartswith(line, "DAMAGEMERGE ")) {
+ assert(screen);
+ char *linep = line + 12;
+ while(linep[0] == ' ')
+ linep++;
+ if(streq(linep, "CELL"))
+ vterm_screen_set_damage_merge(screen, VTERM_DAMAGE_CELL);
+ else if(streq(linep, "ROW"))
+ vterm_screen_set_damage_merge(screen, VTERM_DAMAGE_ROW);
+ else if(streq(linep, "SCREEN"))
+ vterm_screen_set_damage_merge(screen, VTERM_DAMAGE_SCREEN);
+ else if(streq(linep, "SCROLL"))
+ vterm_screen_set_damage_merge(screen, VTERM_DAMAGE_SCROLL);
+ }
+
+ else if(strstartswith(line, "DAMAGEFLUSH")) {
+ assert(screen);
+ vterm_screen_flush_damage(screen);
+ }
+
+ else if(line[0] == '?') {
+ if(streq(line, "?cursor")) {
+ assert(state);
+ VTermPos pos;
+ vterm_state_get_cursorpos(state, &pos);
+ if(pos.row != state_pos.row)
+ printf("! row mismatch: state=%d,%d event=%d,%d\n",
+ pos.row, pos.col, state_pos.row, state_pos.col);
+ else if(pos.col != state_pos.col)
+ printf("! col mismatch: state=%d,%d event=%d,%d\n",
+ pos.row, pos.col, state_pos.row, state_pos.col);
+ else
+ printf("%d,%d\n", state_pos.row, state_pos.col);
+ }
+ else if(strstartswith(line, "?pen ")) {
+ assert(state);
+ VTermValue val;
+ char *linep = line + 5;
+ while(linep[0] == ' ')
+ linep++;
+
+#define BOOLSTR(v) ((v) ? "on" : "off")
+
+ if(streq(linep, "bold")) {
+ vterm_state_get_penattr(state, VTERM_ATTR_BOLD, &val);
+ if(val.boolean != state_pen.bold)
+ printf("! pen bold mismatch; state=%s, event=%s\n",
+ BOOLSTR(val.boolean), BOOLSTR(state_pen.bold));
+ else
+ printf("%s\n", BOOLSTR(state_pen.bold));
+ }
+ else if(streq(linep, "underline")) {
+ vterm_state_get_penattr(state, VTERM_ATTR_UNDERLINE, &val);
+ if(val.boolean != state_pen.underline)
+ printf("! pen underline mismatch; state=%d, event=%d\n",
+ val.boolean, state_pen.underline);
+ else
+ printf("%d\n", state_pen.underline);
+ }
+ else if(streq(linep, "italic")) {
+ vterm_state_get_penattr(state, VTERM_ATTR_ITALIC, &val);
+ if(val.boolean != state_pen.italic)
+ printf("! pen italic mismatch; state=%s, event=%s\n",
+ BOOLSTR(val.boolean), BOOLSTR(state_pen.italic));
+ else
+ printf("%s\n", BOOLSTR(state_pen.italic));
+ }
+ else if(streq(linep, "blink")) {
+ vterm_state_get_penattr(state, VTERM_ATTR_BLINK, &val);
+ if(val.boolean != state_pen.blink)
+ printf("! pen blink mismatch; state=%s, event=%s\n",
+ BOOLSTR(val.boolean), BOOLSTR(state_pen.blink));
+ else
+ printf("%s\n", BOOLSTR(state_pen.blink));
+ }
+ else if(streq(linep, "reverse")) {
+ vterm_state_get_penattr(state, VTERM_ATTR_REVERSE, &val);
+ if(val.boolean != state_pen.reverse)
+ printf("! pen reverse mismatch; state=%s, event=%s\n",
+ BOOLSTR(val.boolean), BOOLSTR(state_pen.reverse));
+ else
+ printf("%s\n", BOOLSTR(state_pen.reverse));
+ }
+ else if(streq(linep, "font")) {
+ vterm_state_get_penattr(state, VTERM_ATTR_FONT, &val);
+ if(val.boolean != state_pen.font)
+ printf("! pen font mismatch; state=%d, event=%d\n",
+ val.boolean, state_pen.font);
+ else
+ printf("%d\n", state_pen.font);
+ }
+ else if(streq(linep, "small")) {
+ vterm_state_get_penattr(state, VTERM_ATTR_SMALL, &val);
+ if(val.boolean != state_pen.small)
+ printf("! pen small mismatch; state=%s, event=%s\n",
+ BOOLSTR(val.boolean), BOOLSTR(state_pen.small));
+ else
+ printf("%s\n", BOOLSTR(state_pen.small));
+ }
+ else if(streq(linep, "baseline")) {
+ vterm_state_get_penattr(state, VTERM_ATTR_BASELINE, &val);
+ if(val.number != state_pen.baseline)
+ printf("! pen baseline mismatch: state=%d, event=%d\n",
+ val.number, state_pen.baseline);
+ else
+ printf("%s\n", state_pen.baseline == VTERM_BASELINE_RAISE ? "raise"
+ : state_pen.baseline == VTERM_BASELINE_LOWER ? "lower"
+ : "normal");
+ }
+ else if(streq(linep, "foreground")) {
+ print_color(&state_pen.foreground);
+ printf("\n");
+ }
+ else if(streq(linep, "background")) {
+ print_color(&state_pen.background);
+ printf("\n");
+ }
+ else
+ printf("?\n");
+ }
+ else if(strstartswith(line, "?lineinfo ")) {
+ assert(state);
+ char *linep = line + 10;
+ int row;
+ const VTermLineInfo *info;
+ while(linep[0] == ' ')
+ linep++;
+ if(sscanf(linep, "%d", &row) < 1) {
+ printf("! lineinfo unrecognised input\n");
+ goto abort_line;
+ }
+ info = vterm_state_get_lineinfo(state, row);
+ if(info->doublewidth)
+ printf("dwl ");
+ if(info->doubleheight)
+ printf("dhl ");
+ if(info->continuation)
+ printf("cont ");
+ printf("\n");
+ }
+ else if(strstartswith(line, "?screen_chars ")) {
+ assert(screen);
+ char *linep = line + 13;
+ VTermRect rect;
+ size_t len;
+ while(linep[0] == ' ')
+ linep++;
+ if(sscanf(linep, "%d,%d,%d,%d", &rect.start_row, &rect.start_col, &rect.end_row, &rect.end_col) == 4)
+ ; // fine
+ else if(sscanf(linep, "%d", &rect.start_row) == 1) {
+ rect.end_row = rect.start_row + 1;
+ rect.start_col = 0;
+ vterm_get_size(vt, NULL, &rect.end_col);
+ }
+ else {
+ printf("! screen_chars unrecognised input\n");
+ goto abort_line;
+ }
+ len = vterm_screen_get_chars(screen, NULL, 0, rect);
+ if(len == (size_t)-1)
+ printf("! screen_chars error\n");
+ else if(len == 0)
+ printf("\n");
+ else {
+ uint32_t *chars = malloc(sizeof(uint32_t) * len);
+ size_t i;
+ vterm_screen_get_chars(screen, chars, len, rect);
+ for(i = 0; i < len; i++) {
+ printf("0x%02x%s", chars[i], i < len-1 ? "," : "\n");
+ }
+ free(chars);
+ }
+ }
+ else if(strstartswith(line, "?screen_text ")) {
+ assert(screen);
+ char *linep = line + 12;
+ VTermRect rect;
+ size_t len;
+ while(linep[0] == ' ')
+ linep++;
+ if(sscanf(linep, "%d,%d,%d,%d", &rect.start_row, &rect.start_col, &rect.end_row, &rect.end_col) < 4) {
+ printf("! screen_text unrecognised input\n");
+ goto abort_line;
+ }
+ len = vterm_screen_get_text(screen, NULL, 0, rect);
+ if(len == (size_t)-1)
+ printf("! screen_text error\n");
+ else if(len == 0)
+ printf("\n");
+ else {
+ // Put an overwrite guard at both ends of the buffer
+ unsigned char *buffer = malloc(len + 4);
+ unsigned char *text = buffer + 2;
+ text[-2] = 0x55; text[-1] = 0xAA;
+ text[len] = 0x55; text[len+1] = 0xAA;
+
+ vterm_screen_get_text(screen, (char *)text, len, rect);
+
+ if(text[-2] != 0x55 || text[-1] != 0xAA)
+ printf("! screen_get_text buffer overrun left [%02x,%02x]\n", text[-2], text[-1]);
+ else if(text[len] != 0x55 || text[len+1] != 0xAA)
+ printf("! screen_get_text buffer overrun right [%02x,%02x]\n", text[len], text[len+1]);
+ else
+ {
+ size_t i;
+ for(i = 0; i < len; i++) {
+ printf("0x%02x%s", text[i], i < len-1 ? "," : "\n");
+ }
+ }
+
+ free(buffer);
+ }
+ }
+ else if(strstartswith(line, "?screen_cell ")) {
+ assert(screen);
+ char *linep = line + 12;
+ int i;
+ VTermPos pos;
+ VTermScreenCell cell;
+ while(linep[0] == ' ')
+ linep++;
+ if(sscanf(linep, "%d,%d\n", &pos.row, &pos.col) < 2) {
+ printf("! screen_cell unrecognised input\n");
+ goto abort_line;
+ }
+ if(!vterm_screen_get_cell(screen, pos, &cell))
+ goto abort_line;
+ printf("{");
+ for(i = 0; i < VTERM_MAX_CHARS_PER_CELL && cell.chars[i]; i++) {
+ printf("%s0x%x", i ? "," : "", cell.chars[i]);
+ }
+ printf("} width=%d attrs={", cell.width);
+ if(cell.attrs.bold) printf("B");
+ if(cell.attrs.underline) printf("U%d", cell.attrs.underline);
+ if(cell.attrs.italic) printf("I");
+ if(cell.attrs.blink) printf("K");
+ if(cell.attrs.reverse) printf("R");
+ if(cell.attrs.font) printf("F%d", cell.attrs.font);
+ if(cell.attrs.small) printf("S");
+ if(cell.attrs.baseline) printf(
+ cell.attrs.baseline == VTERM_BASELINE_RAISE ? "^" :
+ "_");
+ printf("} ");
+ if(cell.attrs.dwl) printf("dwl ");
+ if(cell.attrs.dhl) printf("dhl-%s ", cell.attrs.dhl == 2 ? "bottom" : "top");
+ printf("fg=");
+ vterm_screen_convert_color_to_rgb(screen, &cell.fg);
+ print_color(&cell.fg);
+ printf(" bg=");
+ vterm_screen_convert_color_to_rgb(screen, &cell.bg);
+ print_color(&cell.bg);
+ printf("\n");
+ }
+ else if(strstartswith(line, "?screen_eol ")) {
+ assert(screen);
+ VTermPos pos;
+ char *linep = line + 12;
+ while(linep[0] == ' ')
+ linep++;
+ if(sscanf(linep, "%d,%d\n", &pos.row, &pos.col) < 2) {
+ printf("! screen_eol unrecognised input\n");
+ goto abort_line;
+ }
+ printf("%d\n", vterm_screen_is_eol(screen, pos));
+ }
+ else if(strstartswith(line, "?screen_attrs_extent ")) {
+ assert(screen);
+ VTermPos pos;
+ VTermRect rect;
+ char *linep = line + 21;
+ while(linep[0] == ' ')
+ linep++;
+ if(sscanf(linep, "%d,%d\n", &pos.row, &pos.col) < 2) {
+ printf("! screen_attrs_extent unrecognised input\n");
+ goto abort_line;
+ }
+ rect.start_col = 0;
+ rect.end_col = -1;
+ if(!vterm_screen_get_attrs_extent(screen, &rect, pos, ~0)) {
+ printf("! screen_attrs_extent failed\n");
+ goto abort_line;
+ }
+ printf("%d,%d-%d,%d\n", rect.start_row, rect.start_col, rect.end_row, rect.end_col);
+ }
+ else
+ printf("?\n");
+
+ memset(line, 0, sizeof line);
+ continue;
+ }
+
+ else
+ abort_line: err = 1;
+
+ outlen = vterm_output_get_buffer_current(vt);
+ if(outlen > 0) {
+ char outbuff[1024];
+ vterm_output_read(vt, outbuff, outlen);
+
+ term_output(outbuff, outlen, NULL);
+ }
+
+ printf(err ? "?\n" : "DONE\n");
+ }
+
+ vterm_free(vt);
+
+ return 0;
+}
diff --git a/src/libvterm/t/run-test.pl b/src/libvterm/t/run-test.pl
new file mode 100644
index 0000000..3440465
--- /dev/null
+++ b/src/libvterm/t/run-test.pl
@@ -0,0 +1,233 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use Getopt::Long;
+use IO::Handle;
+use IPC::Open2 qw( open2 );
+use POSIX qw( WIFEXITED WEXITSTATUS WIFSIGNALED WTERMSIG );
+
+my $VALGRIND = 0;
+my $EXECUTABLE = "t/harness";
+GetOptions(
+ 'valgrind|v+' => \$VALGRIND,
+ 'executable|e=s' => \$EXECUTABLE,
+ 'fail-early|F' => \(my $FAIL_EARLY),
+) or exit 1;
+
+my ( $hin, $hout, $hpid );
+{
+ my @command = $EXECUTABLE;
+ unshift @command, "valgrind", "--tool=memcheck", "--leak-check=yes", "--num-callers=25", "--log-file=valgrind.out", "--error-exitcode=126" if $VALGRIND;
+
+ $hpid = open2 $hout, $hin, @command or die "Cannot open2 harness - $!";
+}
+
+my $exitcode = 0;
+
+my $command;
+my @expect;
+
+my $linenum = 0;
+
+sub do_onetest
+{
+ $hin->print( "$command\n" );
+ undef $command;
+
+ my $fail_printed = 0;
+
+ while( my $outline = <$hout> ) {
+ last if $outline eq "DONE\n" or $outline eq "?\n";
+
+ chomp $outline;
+
+ if( !@expect ) {
+ print "# line $linenum: Test failed\n" unless $fail_printed++;
+ print "# expected nothing more\n" .
+ "# Actual: $outline\n";
+ next;
+ }
+
+ my $expectation = shift @expect;
+
+ next if $expectation eq $outline;
+
+ print "# line $linenum: Test failed\n" unless $fail_printed++;
+ print "# Expected: $expectation\n" .
+ "# Actual: $outline\n";
+ }
+
+ if( @expect ) {
+ print "# line $linenum: Test failed\n" unless $fail_printed++;
+ print "# Expected: $_\n" .
+ "# didn't happen\n" for @expect;
+ }
+
+ $exitcode = 1 if $fail_printed;
+ exit $exitcode if $exitcode and $FAIL_EARLY;
+}
+
+sub do_line
+{
+ my ( $line ) = @_;
+
+ if( $line =~ m/^!(.*)/ ) {
+ do_onetest if defined $command;
+ print "> $1\n";
+ }
+
+ # Commands have capitals
+ elsif( $line =~ m/^([A-Z]+)/ ) {
+ # Some convenience formatting
+ if( $line =~ m/^(PUSH|ENCIN) (.*)$/ ) {
+ # we're evil
+ my $string = eval($2);
+ $line = "$1 " . unpack "H*", $string;
+ }
+ elsif( $line =~ m/^(SELECTION \d+) +(\[?)(.*?)(\]?)$/ ) {
+ # we're evil
+ my $string = eval($3);
+ $line = "$1 $2 " . unpack( "H*", $string ) . " $4";
+ }
+
+ do_onetest if defined $command;
+
+ $command = $line;
+ undef @expect;
+ }
+ # Expectations have lowercase
+ elsif( $line =~ m/^([a-z]+)/ ) {
+ # Convenience formatting
+ if( $line =~ m/^(text|encout) (.*)$/ ) {
+ $line = "$1 " . join ",", map sprintf("%x", $_), eval($2);
+ }
+ elsif( $line =~ m/^(output) (.*)$/ ) {
+ $line = "$1 " . join ",", map sprintf("%x", $_), unpack "C*", eval($2);
+ }
+ elsif( $line =~ m/^control (.*)$/ ) {
+ $line = sprintf "control %02x", eval($1);
+ }
+ elsif( $line =~ m/^csi (\S+) (.*)$/ ) {
+ $line = sprintf "csi %02x %s", eval($1), $2; # TODO
+ }
+ elsif( $line =~ m/^(osc) (\[\d+)? *(.*?)(\]?)$/ ) {
+ my ( $cmd, $initial, $data, $final ) = ( $1, $2, $3, $4 );
+ $initial //= "";
+ $initial .= ";" if $initial =~ m/\d+/;
+
+ $line = "$cmd $initial" . join( "", map sprintf("%02x", $_), unpack "C*", length $data ? eval($data) : "" ) . "$final";
+ }
+ elsif( $line =~ m/^(escape|dcs|apc|pm|sos) (\[?)(.*?)(\]?)$/ ) {
+ $line = "$1 $2" . join( "", map sprintf("%02x", $_), unpack "C*", length $3 ? eval($3) : "" ) . "$4";
+ }
+ elsif( $line =~ m/^putglyph (\S+) (.*)$/ ) {
+ $line = "putglyph " . join( ",", map sprintf("%x", $_), eval($1) ) . " $2";
+ }
+ elsif( $line =~ m/^(?:movecursor|scrollrect|moverect|erase|damage|sb_pushline|sb_popline|sb_clear|settermprop|setmousefunc|selection-query) ?/ ) {
+ # no conversion
+ }
+ elsif( $line =~ m/^(selection-set) (.*?) (\[?)(.*?)(\]?)$/ ) {
+ $line = "$1 $2 $3" . join( "", map sprintf("%02x", $_), unpack "C*", eval($4) ) . "$5";
+ }
+ else {
+ warn "Unrecognised test expectation '$line'\n";
+ }
+
+ push @expect, $line;
+ }
+ # ?screen_row assertion is emulated here
+ elsif( $line =~ s/^\?screen_row\s+(\d+)\s*=\s*// ) {
+ my $row = $1;
+ my $want;
+
+ if( $line =~ m/^"/ ) {
+ $want = eval($line);
+ }
+ else {
+ # Turn 0xDD,0xDD,... directly into bytes
+ $want = pack "C*", map { hex } split m/,/, $line;
+ }
+
+ do_onetest if defined $command;
+
+ $hin->print( "\?screen_chars $row\n" );
+ my $response = <$hout>;
+ chomp $response;
+
+ $response = pack "C*", map { hex } split m/,/, $response;
+ if( $response ne $want ) {
+ print "# line $linenum: Assert ?screen_row $row failed:\n" .
+ "# Expected: $want\n" .
+ "# Actual: $response\n";
+ $exitcode = 1;
+ exit $exitcode if $exitcode and $FAIL_EARLY;
+ }
+ }
+ # Assertions start with '?'
+ elsif( $line =~ s/^\?([a-z]+.*?=)\s*// ) {
+ do_onetest if defined $command;
+
+ my ( $assertion ) = $1 =~ m/^(.*)\s+=/;
+ my $expectation = $line;
+
+ $hin->print( "\?$assertion\n" );
+ my $response = <$hout>; defined $response or wait, die "Test harness failed - $?\n";
+ chomp $response; $response =~ s/^\s+|\s+$//g;
+
+ # Some convenience formatting
+ if( $assertion =~ m/^screen_chars/ and $expectation =~ m/^"/ ) {
+ $expectation = join ",", map sprintf("0x%02x", ord $_), split m//, eval($expectation);
+ }
+
+ if( $response ne $expectation ) {
+ print "# line $linenum: Assert $assertion failed:\n" .
+ "# Expected: $expectation\n" .
+ "# Actual: $response\n";
+ $exitcode = 1;
+ exit $exitcode if $exitcode and $FAIL_EARLY;
+ }
+ }
+ # Test controls start with '$'
+ elsif( $line =~ s/\$SEQ\s+(\d+)\s+(\d+):\s*// ) {
+ my ( $low, $high ) = ( $1, $2 );
+ foreach my $val ( $low .. $high ) {
+ ( my $inner = $line ) =~ s/\\#/$val/g;
+ do_line( $inner );
+ }
+ }
+ elsif( $line =~ s/\$REP\s+(\d+):\s*// ) {
+ my $count = $1;
+ do_line( $line ) for 1 .. $count;
+ }
+ else {
+ die "Unrecognised TEST line $line\n";
+ }
+}
+
+open my $test, "<", $ARGV[0] or die "Cannot open test script $ARGV[0] - $!";
+
+while( my $line = <$test> ) {
+ $linenum++;
+ $line =~ s/^\s+//;
+ chomp $line;
+
+ next if $line =~ m/^(?:#|$)/;
+ last if $line eq "__END__";
+
+ do_line( $line );
+}
+
+do_onetest if defined $command;
+
+close $hin;
+close $hout;
+
+waitpid $hpid, 0;
+if( $? ) {
+ printf STDERR "Harness exited %d\n", WEXITSTATUS($?) if WIFEXITED($?);
+ printf STDERR "Harness exit signal %d\n", WTERMSIG($?) if WIFSIGNALED($?);
+ $exitcode = WIFEXITED($?) ? WEXITSTATUS($?) : 125;
+}
+
+exit $exitcode;