patch 8.0.0693: no terminal emulator support

Problem:    No terminal emulator support.  Cannot properly run commands in the
            GUI.  Cannot run a job interactively with an ssh connection.
Solution:   Very early implementation of the :terminal command.  Includes
            libvterm converted to ANSI C.  Many parts still missing.
diff --git a/src/libvterm/t/02parser.test b/src/libvterm/t/02parser.test
new file mode 100644
index 0000000..66d487d
--- /dev/null
+++ b/src/libvterm/t/02parser.test
@@ -0,0 +1,200 @@
+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 "\x9f"
+  control 0x9f
+
+!C1 7bit
+PUSH "\e\x43"
+  control 0x83
+
+PUSH "\e\x5f"
+  control 0x9f
+
+!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"
+
+!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"
+  control 10
+  osc "2;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"
+
+!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"
+  control 10
+  dcs "Bye"
+
+!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..5665bce
--- /dev/null
+++ b/src/libvterm/t/10state_putglyph.test
@@ -0,0 +1,55 @@
+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 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 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
+
+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..c1d72b2
--- /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 Horizonal 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..ca305d4
--- /dev/null
+++ b/src/libvterm/t/12state_scroll.test
@@ -0,0 +1,150 @@
+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[10H"
+PUSH "\e[9;10r"
+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
diff --git a/src/libvterm/t/13state_edit.test b/src/libvterm/t/13state_edit.test
new file mode 100644
index 0000000..b435655
--- /dev/null
+++ b/src/libvterm/t/13state_edit.test
@@ -0,0 +1,300 @@
+INIT
+UTF8 1
+WANTSTATE se
+
+!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
+
+!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..c39f56b
--- /dev/null
+++ b/src/libvterm/t/17state_mouse.test
@@ -0,0 +1,172 @@
+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"
diff --git a/src/libvterm/t/18state_termprops.test b/src/libvterm/t/18state_termprops.test
new file mode 100644
index 0000000..9e6928a
--- /dev/null
+++ b/src/libvterm/t/18state_termprops.test
@@ -0,0 +1,36 @@
+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"
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..d54de83
--- /dev/null
+++ b/src/libvterm/t/25state_input.test
@@ -0,0 +1,132 @@
+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"
+
+!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~"
diff --git a/src/libvterm/t/26state_query.test b/src/libvterm/t/26state_query.test
new file mode 100644
index 0000000..bfe8f69
--- /dev/null
+++ b/src/libvterm/t/26state_query.test
@@ -0,0 +1,62 @@
+INIT
+WANTSTATE
+
+!DA
+RESET
+PUSH "\e[c"
+  output "\e[?1;2c"
+
+!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 20
+  output "\e[10;10R" x 7
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..adf1c23
--- /dev/null
+++ b/src/libvterm/t/29state_fallback.test
@@ -0,0 +1,19 @@
+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"
diff --git a/src/libvterm/t/30pen.test b/src/libvterm/t/30pen.test
new file mode 100644
index 0000000..7a671e7
--- /dev/null
+++ b/src/libvterm/t/30pen.test
@@ -0,0 +1,106 @@
+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)
+  ?pen background = rgb(0,0,0)
+
+!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[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 = rgb(224,0,0)
+PUSH "\e[32m"
+  ?pen foreground = rgb(0,224,0)
+PUSH "\e[34m"
+  ?pen foreground = rgb(0,0,224)
+PUSH "\e[91m"
+  ?pen foreground = rgb(255,64,64)
+PUSH "\e[38:2:10:20:30m"
+  ?pen foreground = rgb(10,20,30)
+PUSH "\e[38:5:1m"
+  ?pen foreground = rgb(224,0,0)
+PUSH "\e[39m"
+  ?pen foreground = rgb(240,240,240)
+
+!Background
+PUSH "\e[41m"
+  ?pen background = rgb(224,0,0)
+PUSH "\e[42m"
+  ?pen background = rgb(0,224,0)
+PUSH "\e[44m"
+  ?pen background = rgb(0,0,224)
+PUSH "\e[101m"
+  ?pen background = rgb(255,64,64)
+PUSH "\e[48:2:10:20:30m"
+  ?pen background = rgb(10,20,30)
+PUSH "\e[48:5:1m"
+  ?pen background = rgb(224,0,0)
+PUSH "\e[49m"
+  ?pen background = rgb(0,0,0)
+
+!Bold+ANSI colour == highbright
+PUSH "\e[m\e[1;37m"
+  ?pen bold = on
+  ?pen foreground = rgb(255,255,255)
+PUSH "\e[m\e[37;1m"
+  ?pen bold = on
+  ?pen foreground = rgb(255,255,255)
diff --git a/src/libvterm/t/40screen_ascii.test b/src/libvterm/t/40screen_ascii.test
new file mode 100644
index 0000000..c2f48fa
--- /dev/null
+++ b/src/libvterm/t/40screen_ascii.test
@@ -0,0 +1,69 @@
+INIT
+WANTSCREEN c
+
+!Get
+RESET
+PUSH "ABC"
+  movecursor 0,3
+  ?screen_chars 0,0,1,3 = 0x41,0x42,0x43
+  ?screen_chars 0,0,1,80 = 0x41,0x42,0x43
+  ?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_chars 0,0,1,80 = 0x41,0x42,0x43
+  ?screen_text 0,0,1,80 = 0x41,0x42,0x43
+PUSH "E"
+  movecursor 0,1
+  ?screen_chars 0,0,1,80 = 0x45,0x42,0x43
+  ?screen_text 0,0,1,80 = 0x45,0x42,0x43
+
+WANTSCREEN -c
+
+!Erase
+RESET
+PUSH "ABCDE\e[H\e[K"
+  ?screen_chars 0,0,1,80 = 
+  ?screen_text 0,0,1,80 = 
+
+!Copycell
+RESET
+PUSH "ABC\e[H\e[@"
+PUSH "1"
+  ?screen_chars 0,0,1,80 = 0x31,0x41,0x42,0x43
+
+RESET
+PUSH "ABC\e[H\e[P"
+  ?screen_chars 0,0,1,1 = 0x42
+  ?screen_chars 0,1,1,2 = 0x43
+  ?screen_chars 0,0,1,80 = 0x42,0x43
+
+!Space padding
+RESET
+PUSH "Hello\e[CWorld"
+  ?screen_chars 0,0,1,80 = 0x48,0x65,0x6c,0x6c,0x6f,0x20,0x57,0x6f,0x72,0x6c,0x64
+  ?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 = 0x48,0x65,0x6c,0x6c,0x6f,0x0a,0x57,0x6f,0x72,0x6c,0x64
+  ?screen_text 0,0,2,80 = 0x48,0x65,0x6c,0x6c,0x6f,0x0a,0x57,0x6f,0x72,0x6c,0x64
+
+!Altscreen
+RESET
+PUSH "P"
+  ?screen_chars 0,0,1,80 = 0x50
+PUSH "\e[?1049h"
+  ?screen_chars 0,0,1,80 = 
+PUSH "\e[2K\e[HA"
+  ?screen_chars 0,0,1,80 = 0x41
+PUSH "\e[?1049l"
+  ?screen_chars 0,0,1,80 = 0x50
diff --git a/src/libvterm/t/41screen_unicode.test b/src/libvterm/t/41screen_unicode.test
new file mode 100644
index 0000000..79dcb68
--- /dev/null
+++ b/src/libvterm/t/41screen_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_chars 0,0,1,80 = 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_chars 0,0,1,80 = 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_chars 0,0,1,80 = 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)
+
+!Outputing 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/42screen_damage.test b/src/libvterm/t/42screen_damage.test
new file mode 100644
index 0000000..791a96c
--- /dev/null
+++ b/src/libvterm/t/42screen_damage.test
@@ -0,0 +1,155 @@
+INIT
+WANTSCREEN Db
+
+!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_chars 23,0,24,5 = 0x41,0x42,0x45
diff --git a/src/libvterm/t/43screen_resize.test b/src/libvterm/t/43screen_resize.test
new file mode 100644
index 0000000..9e5e5b2
--- /dev/null
+++ b/src/libvterm/t/43screen_resize.test
@@ -0,0 +1,90 @@
+INIT
+WANTSTATE
+WANTSCREEN
+
+!Resize wider preserves cells
+RESET
+RESIZE 25,80
+PUSH "AB\r\nCD"
+  ?screen_chars 0,0,1,80 = 0x41,0x42
+  ?screen_chars 1,0,2,80 = 0x43,0x44
+RESIZE 25,100
+  ?screen_chars 0,0,1,100 = 0x41,0x42
+  ?screen_chars 1,0,2,100 = 0x43,0x44
+
+!Resize wider allows print in new area
+RESET
+RESIZE 25,80
+PUSH "AB\e[79GCD"
+  ?screen_chars 0,0,1,2 = 0x41,0x42
+  ?screen_chars 0,78,1,80 = 0x43,0x44
+RESIZE 25,100
+  ?screen_chars 0,0,1,2 = 0x41,0x42
+  ?screen_chars 0,78,1,80 = 0x43,0x44
+PUSH "E"
+  ?screen_chars 0,78,1,81 = 0x43,0x44,0x45
+
+!Resize shorter with blanks just truncates
+RESET
+RESIZE 25,80
+PUSH "Top\e[10HLine 10"
+  ?screen_chars 0,0,1,80 = 0x54,0x6f,0x70
+  ?screen_chars 9,0,10,80 = 0x4c,0x69,0x6e,0x65,0x20,0x31,0x30
+  ?cursor = 9,7
+RESIZE 20,80
+  ?screen_chars 0,0,1,80 = 0x54,0x6f,0x70
+  ?screen_chars 9,0,10,80 = 0x4c,0x69,0x6e,0x65,0x20,0x31,0x30
+  ?cursor = 9,7
+
+!Resize shorter with content must scroll
+RESET
+RESIZE 25,80
+PUSH "Top\e[25HLine 25\e[15H"
+  ?screen_chars 0,0,1,80 = 0x54,0x6f,0x70
+  ?screen_chars 24,0,25,80 = 0x4c,0x69,0x6e,0x65,0x20,0x32,0x35
+  ?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_chars 0,0,1,80 = 
+  ?screen_chars 19,0,20,80 = 0x4c,0x69,0x6e,0x65,0x20,0x32,0x35
+  ?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_chars 23,0,24,10 = 0x4c,0x69,0x6e,0x65,0x20,0x32,0x35
+  ?cursor = 24,0
+RESIZE 24,80
+  sb_pushline 80 =
+  ?screen_chars 22,0,23,10 = 0x4c,0x69,0x6e,0x65,0x20,0x32,0x35
+  ?cursor = 23,0
+
+!Resize taller attempts to pop scrollback
+RESET
+WANTSCREEN -b
+RESIZE 25,80
+PUSH "Line 1\e[25HBottom\e[15H"
+  ?screen_chars 0,0,1,80 = 0x4c,0x69,0x6e,0x65,0x20,0x31
+  ?screen_chars 24,0,25,80 = 0x42,0x6f,0x74,0x74,0x6f,0x6d
+  ?cursor = 14,0
+WANTSCREEN b
+RESIZE 30,80
+  sb_popline 80
+  sb_popline 80
+  sb_popline 80
+  sb_popline 80
+  sb_popline 80
+  ?screen_chars 0,0,1,80 = 0x41,0x42,0x43,0x44,0x45
+  ?screen_chars 5,0,6,80 = 0x4c,0x69,0x6e,0x65,0x20,0x31
+  ?screen_chars 29,0,30,80 = 0x42,0x6f,0x74,0x74,0x6f,0x6d
+  ?cursor = 19,0
diff --git a/src/libvterm/t/44screen_pen.test b/src/libvterm/t/44screen_pen.test
new file mode 100644
index 0000000..f1ee639
--- /dev/null
+++ b/src/libvterm/t/44screen_pen.test
@@ -0,0 +1,55 @@
+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=rgb(224,0,0) 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=rgb(0,224,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=rgb(224,224,0) bg=rgb(0,0,224)
+  ?screen_cell 0,79 = {} width=1 attrs={R} fg=rgb(224,224,0) bg=rgb(0,0,224)
+
+!DECSCNM xors reverse for entire screen
+PUSH "\e[?5h"
+  ?screen_cell 0,0  = {} width=1 attrs={} fg=rgb(224,224,0) bg=rgb(0,0,224)
+  ?screen_cell 0,79 = {} width=1 attrs={} fg=rgb(224,224,0) bg=rgb(0,0,224)
+  ?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=rgb(224,224,0) bg=rgb(0,0,224)
+  ?screen_cell 0,79 = {} width=1 attrs={R} fg=rgb(224,224,0) bg=rgb(0,0,224)
+  ?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/45screen_protect.test b/src/libvterm/t/45screen_protect.test
new file mode 100644
index 0000000..718f853
--- /dev/null
+++ b/src/libvterm/t/45screen_protect.test
@@ -0,0 +1,16 @@
+INIT
+WANTSCREEN 
+
+!Selective erase
+RESET
+PUSH "A\e[1\"qB\e[\"qC"
+  ?screen_chars 0,0,1,3 = 0x41,0x42,0x43
+PUSH "\e[G\e[?J"
+  ?screen_chars 0,0,1,3 = 0x20,0x42
+
+!Non-selective erase
+RESET
+PUSH "A\e[1\"qB\e[\"qC"
+  ?screen_chars 0,0,1,3 = 0x41,0x42,0x43
+PUSH "\e[G\e[J"
+  ?screen_chars 0,0,1,3 = 
diff --git a/src/libvterm/t/46screen_extent.test b/src/libvterm/t/46screen_extent.test
new file mode 100644
index 0000000..a126cec
--- /dev/null
+++ b/src/libvterm/t/46screen_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/47screen_dbl_wh.test b/src/libvterm/t/47screen_dbl_wh.test
new file mode 100644
index 0000000..7d17d9a
--- /dev/null
+++ b/src/libvterm/t/47screen_dbl_wh.test
@@ -0,0 +1,32 @@
+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)
diff --git a/src/libvterm/t/48screen_termprops.test b/src/libvterm/t/48screen_termprops.test
new file mode 100644
index 0000000..adf7ec2
--- /dev/null
+++ b/src/libvterm/t/48screen_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/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..2b22464
--- /dev/null
+++ b/src/libvterm/t/harness.c
@@ -0,0 +1,929 @@
+#include "vterm.h"
+#include "../src/vterm_internal.h" /* We pull in some internal bits too */
+
+#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;
+    sscanf(inpos, "%2x", &ch);
+    *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 },
+    { 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 VTerm *vt;
+static VTermState *state;
+static VTermScreen *screen;
+
+static VTermEncodingInstance encoding;
+
+static int parser_text(const char bytes[], size_t len, void *user)
+{
+  int 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)
+{
+  printf("control %02x\n", control);
+
+  return 1;
+}
+
+static int parser_escape(const char bytes[], size_t len, void *user)
+{
+  int i;
+
+  if(bytes[0] >= 0x20 && bytes[0] < 0x30) {
+    if(len < 2)
+      return -1;
+    len = 2;
+  }
+  else {
+    len = 1;
+  }
+
+  printf("escape ");
+  for(i = 0; i < len; i++)
+    printf("%02x", bytes[i]);
+  printf("\n");
+
+  return len;
+}
+
+static int parser_csi(const char *leader, const long args[], int argcount, const char *intermed, char command, void *user)
+{
+  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(const char *command, size_t cmdlen, void *user)
+{
+  int i;
+  printf("osc ");
+  for(i = 0; i < cmdlen; i++)
+    printf("%02x", command[i]);
+  printf("\n");
+
+  return 1;
+}
+
+static int parser_dcs(const char *command, size_t cmdlen, void *user)
+{
+  int i;
+  printf("dcs ");
+  for(i = 0; i < cmdlen; i++)
+    printf("%02x", command[i]);
+  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 */
+  NULL /* resize */
+};
+
+/* These callbacks are shared by State and Screen */
+
+static int want_movecursor = 0;
+static VTermPos state_pos;
+static int movecursor(VTermPos pos, VTermPos oldpos, int visible, void *user)
+{
+  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)
+{
+  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)
+{
+  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)
+{
+  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\"\n", prop, val->string);
+    return 1;
+  case VTERM_VALUETYPE_COLOR:
+    printf("settermprop %d rgb(%d,%d,%d)\n", prop, val->color.red, val->color.green, val->color.blue);
+    return 1;
+  }
+
+  return 0;
+}
+
+/* These callbacks are for State */
+
+static int want_state_putglyph = 0;
+static int state_putglyph(VTermGlyphInfo *info, VTermPos pos, void *user)
+{
+  int i;
+  if(!want_state_putglyph)
+    return 1;
+
+  printf("putglyph ");
+  for(i = 0; 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)
+{
+  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 strike;
+  int font;
+  VTermColor foreground;
+  VTermColor background;
+} state_pen;
+static int state_setpenattr(VTermAttr attr, VTermValue *val, void *user)
+{
+  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_STRIKE:
+    state_pen.strike = val->boolean;
+    break;
+  case VTERM_ATTR_FONT:
+    state_pen.font = val->number;
+    break;
+  case VTERM_ATTR_FOREGROUND:
+    state_pen.foreground = val->color;
+    break;
+  case VTERM_ATTR_BACKGROUND:
+    state_pen.background = val->color;
+    break;
+  }
+
+  return 1;
+}
+
+static int state_setlineinfo(int row, const VTermLineInfo *newinfo, const VTermLineInfo *oldinfo, void *user)
+{
+  return 1;
+}
+
+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 */
+};
+
+static int want_screen_damage = 0;
+static int want_screen_damage_cells = 0;
+static int screen_damage(VTermRect rect, void *user)
+{
+  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) {
+    bool 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)
+{
+  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)
+{
+  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;
+}
+
+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 */
+};
+
+int main(int argc, char **argv)
+{
+  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);
+    }
+
+    else if(streq(line, "WANTPARSER")) {
+      vterm_parser_set_callbacks(vt, &parser_cbs, NULL);
+    }
+
+    else if(strstartswith(line, "WANTSTATE") && (line[9] == '\0' || line[9] == ' ')) {
+      int i = 9;
+      int sense = 1;
+      if(!state) {
+        state = vterm_obtain_state(vt);
+        vterm_state_set_callbacks(state, &state_cbs, NULL);
+        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 ? &parser_cbs : NULL, NULL);
+          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;
+      if(!screen)
+        screen = vterm_obtain_screen(vt);
+      vterm_screen_enable_altscreen(screen, 1);
+      vterm_screen_set_callbacks(screen, &screen_cbs, NULL);
+
+      while(line[i] == ' ')
+        i++;
+      for( ; line[i]; i++)
+        switch(line[i]) {
+        case '-':
+          sense = 0;
+          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;
+        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);
+      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);
+
+      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, "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, "DAMAGEMERGE ")) {
+      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")) {
+      vterm_screen_flush_damage(screen);
+    }
+
+    else if(line[0] == '?') {
+      if(streq(line, "?cursor")) {
+        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 ")) {
+        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, "foreground")) {
+          printf("rgb(%d,%d,%d)\n", state_pen.foreground.red, state_pen.foreground.green, state_pen.foreground.blue);
+        }
+        else if(streq(linep, "background")) {
+          printf("rgb(%d,%d,%d)\n", state_pen.background.red, state_pen.background.green, state_pen.background.blue);
+        }
+        else
+          printf("?\n");
+      }
+      else if(strstartswith(line, "?screen_chars ")) {
+        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) {
+          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 ")) {
+        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 ")) {
+        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);
+        printf("} ");
+        if(cell.attrs.dwl)       printf("dwl ");
+        if(cell.attrs.dhl)       printf("dhl-%s ", cell.attrs.dhl == 2 ? "bottom" : "top");
+        printf("fg=rgb(%d,%d,%d) ",  cell.fg.red, cell.fg.green, cell.fg.blue);
+        printf("bg=rgb(%d,%d,%d)\n", cell.bg.red, cell.bg.green, cell.bg.blue);
+      }
+      else if(strstartswith(line, "?screen_eol ")) {
+        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 ")) {
+        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) {
+      int i;
+      char outbuff[1024];
+      vterm_output_read(vt, outbuff, outlen);
+
+      printf("output ");
+      for(i = 0; i < outlen; i++)
+        printf("%x%s", (unsigned char)outbuff[i], i < outlen-1 ? "," : "\n");
+    }
+
+    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..4ef0f26
--- /dev/null
+++ b/src/libvterm/t/run-test.pl
@@ -0,0 +1,196 @@
+#!/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;
+GetOptions(
+   'valgrind|v+' => \$VALGRIND,
+) or exit 1;
+
+my ( $hin, $hout, $hpid );
+{
+   local $ENV{LD_LIBRARY_PATH} = ".libs";
+   my @command = "t/.libs/harness";
+   unshift @command, "valgrind", "--quiet", "--error-exitcode=126" if $VALGRIND;
+
+   $hpid = open2 $hout, $hin, @command or die "Cannot open2 harness - $!";
+}
+
+my $exitcode = 0;
+
+my $command;
+my @expect;
+
+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 "# 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 "# Test failed\n" unless $fail_printed++;
+      print "#   Expected: $expectation\n" .
+            "#   Actual:   $outline\n";
+   }
+
+   if( @expect ) {
+      print "# Test failed\n" unless $fail_printed++;
+      print "#   Expected: $_\n" .
+            "#    didn't happen\n" for @expect;
+   }
+
+   $exitcode = 1 if $fail_printed;
+}
+
+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;
+      }
+
+      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/^(escape|osc|dcs) (.*)$/ ) {
+         $line = "$1 " . join "", map sprintf("%02x", $_), unpack "C*", eval($2);
+      }
+      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|settermprop|setmousefunc) / ) {
+         # no conversion
+      }
+      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 $row1 = $row + 1;
+      my $want = eval($line);
+
+      do_onetest if defined $command;
+
+      # TODO: may not be 80
+      $hin->print( "\?screen_chars $row,0,$row1,80\n" );
+      my $response = <$hout>;
+      chomp $response;
+
+      $response = pack "C*", map hex, split m/,/, $response;
+      if( $response ne $want ) {
+         print "# Assert ?screen_row $row failed:\n" .
+               "# Expected: $want\n" .
+               "# Actual:   $response\n";
+         $exitcode = 1;
+      }
+   }
+   # Assertions start with '?'
+   elsif( $line =~ s/^\?([a-z]+.*?=)\s+// ) {
+      do_onetest if defined $command;
+
+      my ( $assertion ) = $1 =~ m/^(.*)\s+=/;
+
+      $hin->print( "\?$assertion\n" );
+      my $response = <$hout>; defined $response or wait, die "Test harness failed - $?\n";
+      chomp $response;
+
+      if( $response ne $line ) {
+         print "# Assert $assertion failed:\n" .
+               "# Expected: $line\n" .
+               "# Actual:   $response\n";
+         $exitcode = 1;
+      }
+   }
+   # 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> ) {
+   $line =~ s/^\s+//;
+   next if $line =~ m/^(?:#|$)/;
+
+   chomp $line;
+   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;