changeset 11621:b8299e742f41 v8.0.0693

patch 8.0.0693: no terminal emulator support commit https://github.com/vim/vim/commit/e4f25e4a8db2c8a8a71a4ba2a68540b3ab341e42 Author: Bram Moolenaar <Bram@vim.org> Date: Fri Jul 7 11:54:15 2017 +0200 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.
author Christian Brabandt <cb@256bit.org>
date Fri, 07 Jul 2017 12:00:04 +0200
parents fb788b3997c1
children 6b4fda4c25d2
files Filelist runtime/doc/Makefile runtime/doc/terminal.txt src/Makefile src/auto/configure src/config.h.in src/config.mk.in src/configure.ac src/evalfunc.c src/ex_cmdidxs.h src/ex_docmd.c src/feature.h src/libvterm/.bzrignore src/libvterm/.gitignore src/libvterm/LICENSE src/libvterm/Makefile src/libvterm/README src/libvterm/bin/unterm.c src/libvterm/bin/vterm-ctrl.c src/libvterm/bin/vterm-dump.c src/libvterm/doc/URLs src/libvterm/doc/seqs.txt src/libvterm/include/vterm.h src/libvterm/include/vterm_keycodes.h src/libvterm/src/encoding.c src/libvterm/src/encoding/DECdrawing.tbl src/libvterm/src/encoding/uk.tbl src/libvterm/src/keyboard.c src/libvterm/src/mouse.c src/libvterm/src/parser.c src/libvterm/src/pen.c src/libvterm/src/rect.h src/libvterm/src/screen.c src/libvterm/src/state.c src/libvterm/src/unicode.c src/libvterm/src/utf8.h src/libvterm/src/vterm.c src/libvterm/src/vterm_internal.h src/libvterm/t/02parser.test src/libvterm/t/03encoding_utf8.test src/libvterm/t/10state_putglyph.test src/libvterm/t/11state_movecursor.test src/libvterm/t/12state_scroll.test src/libvterm/t/13state_edit.test src/libvterm/t/14state_encoding.test src/libvterm/t/15state_mode.test src/libvterm/t/16state_resize.test src/libvterm/t/17state_mouse.test src/libvterm/t/18state_termprops.test src/libvterm/t/20state_wrapping.test src/libvterm/t/21state_tabstops.test src/libvterm/t/22state_save.test src/libvterm/t/25state_input.test src/libvterm/t/26state_query.test src/libvterm/t/27state_reset.test src/libvterm/t/28state_dbl_wh.test src/libvterm/t/29state_fallback.test src/libvterm/t/30pen.test src/libvterm/t/40screen_ascii.test src/libvterm/t/41screen_unicode.test src/libvterm/t/42screen_damage.test src/libvterm/t/43screen_resize.test src/libvterm/t/44screen_pen.test src/libvterm/t/45screen_protect.test src/libvterm/t/46screen_extent.test src/libvterm/t/47screen_dbl_wh.test src/libvterm/t/48screen_termprops.test src/libvterm/t/90vttest_01-movement-1.test src/libvterm/t/90vttest_01-movement-2.test src/libvterm/t/90vttest_01-movement-3.test src/libvterm/t/90vttest_01-movement-4.test src/libvterm/t/90vttest_02-screen-1.test src/libvterm/t/90vttest_02-screen-2.test src/libvterm/t/90vttest_02-screen-3.test src/libvterm/t/90vttest_02-screen-4.test src/libvterm/t/92lp1640917.test src/libvterm/t/harness.c src/libvterm/t/run-test.pl src/libvterm/tbl2inc_c.pl src/libvterm/vterm.pc.in src/option.c src/option.h src/proto.h src/proto/terminal.pro src/structs.h src/terminal.c src/version.c
diffstat 87 files changed, 11693 insertions(+), 13 deletions(-) [+]
line wrap: on
line diff
--- a/Filelist
+++ b/Filelist
@@ -85,6 +85,7 @@ SRC_ALL =	\
 		src/syntax.c \
 		src/tag.c \
 		src/term.c \
+		src/terminal.c \
 		src/term.h \
 		src/termlib.c \
 		src/ui.c \
@@ -187,6 +188,7 @@ SRC_ALL =	\
 		src/proto/syntax.pro \
 		src/proto/tag.pro \
 		src/proto/term.pro \
+		src/proto/terminal.pro \
 		src/proto/termlib.pro \
 		src/proto/ui.pro \
 		src/proto/undo.pro \
@@ -194,6 +196,76 @@ SRC_ALL =	\
 		src/proto/version.pro \
 		src/proto/winclip.pro \
 		src/proto/window.pro \
+		src/libvterm/.bzrignore \
+		src/libvterm/.gitignore \
+		src/libvterm/LICENSE \
+		src/libvterm/Makefile \
+		src/libvterm/README \
+		src/libvterm/tbl2inc_c.pl \
+		src/libvterm/vterm.pc.in \
+		src/libvterm/bin/unterm.c \
+		src/libvterm/bin/vterm-ctrl.c \
+		src/libvterm/bin/vterm-dump.c \
+		src/libvterm/doc/URLs \
+		src/libvterm/doc/seqs.txt \
+		src/libvterm/include/vterm.h \
+		src/libvterm/include/vterm_keycodes.h \
+		src/libvterm/src/encoding.c \
+		src/libvterm/src/encoding/DECdrawing.inc \
+		src/libvterm/src/encoding/DECdrawing.tbl \
+		src/libvterm/src/encoding/uk.inc \
+		src/libvterm/src/encoding/uk.tbl \
+		src/libvterm/src/keyboard.c \
+		src/libvterm/src/mouse.c \
+		src/libvterm/src/parser.c \
+		src/libvterm/src/pen.c \
+		src/libvterm/src/rect.h \
+		src/libvterm/src/screen.c \
+		src/libvterm/src/state.c \
+		src/libvterm/src/unicode.c \
+		src/libvterm/src/utf8.h \
+		src/libvterm/src/vterm.c \
+		src/libvterm/src/vterm_internal.h \
+		src/libvterm/t/02parser.test \
+		src/libvterm/t/03encoding_utf8.test \
+		src/libvterm/t/10state_putglyph.test \
+		src/libvterm/t/11state_movecursor.test \
+		src/libvterm/t/12state_scroll.test \
+		src/libvterm/t/13state_edit.test \
+		src/libvterm/t/14state_encoding.test \
+		src/libvterm/t/15state_mode.test \
+		src/libvterm/t/16state_resize.test \
+		src/libvterm/t/17state_mouse.test \
+		src/libvterm/t/18state_termprops.test \
+		src/libvterm/t/20state_wrapping.test \
+		src/libvterm/t/21state_tabstops.test \
+		src/libvterm/t/22state_save.test \
+		src/libvterm/t/25state_input.test \
+		src/libvterm/t/26state_query.test \
+		src/libvterm/t/27state_reset.test \
+		src/libvterm/t/28state_dbl_wh.test \
+		src/libvterm/t/29state_fallback.test \
+		src/libvterm/t/30pen.test \
+		src/libvterm/t/40screen_ascii.test \
+		src/libvterm/t/41screen_unicode.test \
+		src/libvterm/t/42screen_damage.test \
+		src/libvterm/t/43screen_resize.test \
+		src/libvterm/t/44screen_pen.test \
+		src/libvterm/t/45screen_protect.test \
+		src/libvterm/t/46screen_extent.test \
+		src/libvterm/t/47screen_dbl_wh.test \
+		src/libvterm/t/48screen_termprops.test \
+		src/libvterm/t/90vttest_01-movement-1.test \
+		src/libvterm/t/90vttest_01-movement-2.test \
+		src/libvterm/t/90vttest_01-movement-3.test \
+		src/libvterm/t/90vttest_01-movement-4.test \
+		src/libvterm/t/90vttest_02-screen-1.test \
+		src/libvterm/t/90vttest_02-screen-2.test \
+		src/libvterm/t/90vttest_02-screen-3.test \
+		src/libvterm/t/90vttest_02-screen-4.test \
+		src/libvterm/t/92lp1640917.test \
+		src/libvterm/t/harness.c \
+		src/libvterm/t/run-test.pl \
 
 
 # source files for Unix only
--- a/runtime/doc/Makefile
+++ b/runtime/doc/Makefile
@@ -101,6 +101,7 @@ DOCS = \
 	tabpage.txt \
 	tagsrch.txt \
 	term.txt \
+	terminal.txt \
 	tips.txt \
 	todo.txt \
 	uganda.txt \
@@ -236,6 +237,7 @@ HTMLS = \
 	tabpage.html \
 	tagsrch.html \
 	term.html \
+	terminal.html \
 	tips.html \
 	todo.html \
 	uganda.html \
new file mode 100644
--- /dev/null
+++ b/runtime/doc/terminal.txt
@@ -0,0 +1,130 @@
+*terminal.txt*	For Vim version 8.0.  Last change: 2017 Jul 04
+
+
+		  VIM REFERENCE MANUAL	  by Bram Moolenaar
+
+
+Terminal window support					*terminal*
+
+
+WARNING: THIS IS ONLY PARTLY IMPLEMENTED, ANYTHING CAN STILL CHANGE
+
+
+1. Basic use			|terminal-use|
+2. Remote testing		|terminal-testing|
+3. Debugging			|terminal-debug|
+
+{Vi does not have any of these commands}
+
+==============================================================================
+1. Basic use						*terminal-use*
+
+This feature is for running a terminal emulator in a Vim window.  A job can be
+started connected to the terminal emulator. For example, to run a shell: >
+     :term bash
+
+Or to run a debugger: >
+     :term gdb vim
+
+The job runs asynchronously from Vim, the window will be updated to show
+output from the job, also  while editing in any other window.
+
+When the keyboard focus is in the terminal window, typed keys will be send to
+the job.  This uses a pty when possible.
+
+Navigate between windows with CTRL-W commands (and mouse).
+E.g. CTRL-W CTRL-W moves focus to the next window.
+
+Option 'termkey'
+Specify key for Vim command in terminal window.  local to window.
+Default is CTRL-W.
+
+Option 'termsize'
+Specify terminal size. Local to window.
+When empty the terminal gets the size from the window.
+When set (e.g., "24x80") the terminal size is fixed.  If the window is smaller
+only the top-left part is displayed.  (TODO: scrolling?)
+
+Syntax ~
+						*:ter* *:terminal*
+:terminal[!] [command]	Open a new terminal window.
+
+			If [command] is provided run it as a job and connect
+			the input and output to the terminal.
+			If [command] is not given the 'shell' option is used.
+
+			A new buffer will be created, using [command] or
+			'shell' as the name.  If a buffer by this name already
+			exists a number is added in parenthesis.
+			E.g. if "gdb" exists the second terminal buffer will
+			use "gdb (1)".
+
+			The window can be closed, in which case the buffer
+			becomes hidden.  The command will not be stopped.  The
+			`:buffer` command can be used to turn the current
+			window into a terminal window, using the existing
+			buffer.  If there are unsaved changes this fails, use
+			! to force, as usual.
+
+Resizing ~
+
+The size of the terminal can be in one of three modes:
+
+1. The 'termsize' option is empty: The terminal size follows the window size.
+   The minimal size is 2 screen lines with 10 cells.
+
+2. The 'termsize' option is "rows*cols", where "rows" is the minimal number of
+   screen rows and "cols" is the minial number of cells.
+
+3. The 'termsize' option is "rowsXcols" (where the x is upper or lower case).
+   The terminal size is fixed to the specified number of screen lines and
+   cells.  If the window is bigger there will be unused empty space.
+
+If the window is smaller than the terminal size, only part of the terminal can
+be seen (the lower-left part).
+
+The |term_getsize()| function can be used to get the current size of the
+terminal.  |term_setsize()| can be used only when in the first or second mode,
+not when 'termsize' is "rowsXcols".
+
+==============================================================================
+2. Remote testing					*terminal-testing*
+
+Most Vim tests execute a script inside Vim.  For some tests this does not
+work, running the test interferes with the code being tested.  To avoid this
+Vim is executed in a terminal window.  The test sends keystrokes to it and
+inspects the resulting screen state.
+
+Functions ~
+
+term_sendkeys()		send keystrokes to a terminal
+term_wait()		wait for screen to be updated
+term_scrape()		inspect terminal screen
+
+
+==============================================================================
+3. Debugging					*terminal-debug*
+
+The Terminal debugging plugin can be used to debug a program with gdb and view
+the source code in a Vim window. For example: >
+
+	:TermDebug vim
+
+This opens three windows:
+- A terminal window in which "gdb vim" is executed.  Here you can directly
+  interact with gdb.
+- A terminal window for the executed program.  When "run" is used in gdb the
+  program I/O will happen in this window, so that it does not interfere with
+  controlling gdb.
+- A normal Vim window used to show the source code.  When gdb jumps to a
+  source file location this window will display the code, if possible.  Values
+  of variables can be inspected, breakpoints set and cleared, etc.
+
+This uses two terminal windows.  To open the gdb window: >
+	:term gdb [arguments]
+To open the terminal to run the tested program |term_open()| is used.
+
+TODO
+
+
+ vim:tw=78:ts=8:ft=help:norl:
--- a/src/Makefile
+++ b/src/Makefile
@@ -482,6 +482,11 @@ CClink = $(CC)
 # Uncomment this when you do not want inter process communication.
 #CONF_OPT_CHANNEL = --disable-channel
 
+# TERMINAL - Terminal emulator support, :terminal command.  Requires the
+# channel feature.
+# Uncomment this when you want terminal emulator support.
+#CONF_OPT_TERMINAL = --enable-terminal
+
 # MULTIBYTE - To edit multi-byte characters.
 # Uncomment this when you want to edit a multibyte language.
 # It's automatically enabled with normal features, GTK or IME support.
@@ -598,6 +603,9 @@ CClink = $(CC)
 
 # Use this with GCC to check for mistakes, unused arguments, etc.
 #CFLAGS = -g -Wall -Wextra -Wshadow -Wmissing-prototypes -Wunreachable-code -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=1
+# Add -Wpedantic to find // comments and other C99 constructs.
+# Better disable Perl and Python to avoid a lot of warnings.
+#CFLAGS = -g -Wall -Wextra -Wshadow -Wmissing-prototypes -Wpedantic -Wunreachable-code -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=1
 #CFLAGS = -g -O2 -Wall -Wextra -Wmissing-prototypes -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=1 -DU_DEBUG
 #PYTHON_CFLAGS_EXTRA = -Wno-missing-field-initializers
 #MZSCHEME_CFLAGS_EXTRA = -Wno-unreachable-code -Wno-unused-parameter
@@ -1371,6 +1379,13 @@ ALL_GUI_PRO  = gui.pro gui_gtk.pro gui_m
 
 # }}}
 
+TERM_DEPS = \
+	libvterm/include/vterm.h \
+	libvterm/include/vterm_keycodes.h \
+	libvterm/src/rect.h \
+	libvterm/src/utf8.h \
+	libvterm/src/vterm_internal.h
+
 ### Command to create dependencies based on #include "..."
 ### prototype headers are ignored due to -DPROTO, system
 ### headers #include <...> are ignored if we use the -MM option, as
@@ -1560,6 +1575,7 @@ BASIC_SRC = \
 	syntax.c \
 	tag.c \
 	term.c \
+	terminal.c \
 	ui.c \
 	undo.c \
 	userfunc.c \
@@ -1569,6 +1585,7 @@ BASIC_SRC = \
 
 SRC =	$(BASIC_SRC) \
 	$(GUI_SRC) \
+	$(TERM_SRC) \
 	$(HANGULIN_SRC) \
 	$(LUA_SRC) \
 	$(MZSCHEME_SRC) \
@@ -1610,7 +1627,7 @@ ALL_SRC = $(BASIC_SRC) $(ALL_GUI_SRC) $(
 LINT_SRC = $(BASIC_SRC) $(GUI_SRC) $(HANGULIN_SRC) \
 	   $(PYTHON_SRC) $(PYTHON3_SRC) $(TCL_SRC) \
 	   $(WORKSHOP_SRC) $(WSDEBUG_SRC) \
-	   $(NETBEANS_SRC) $(CHANNEL_SRC)
+	   $(NETBEANS_SRC) $(CHANNEL_SRC) $(TERM_SRC)
 #LINT_SRC = $(SRC)
 #LINT_SRC = $(ALL_SRC)
 #LINT_SRC = $(BASIC_SRC)
@@ -1665,12 +1682,14 @@ OBJ_COMMON = \
 	objects/syntax.o \
 	objects/tag.o \
 	objects/term.o \
+	objects/terminal.o \
 	objects/ui.o \
 	objects/undo.o \
 	objects/userfunc.o \
 	objects/version.o \
 	objects/window.o \
 	$(GUI_OBJ) \
+	$(TERM_OBJ) \
 	$(LUA_OBJ) \
 	$(MZSCHEME_OBJ) \
 	$(PERL_OBJ) \
@@ -1795,6 +1814,7 @@ PRO_AUTO = \
 	syntax.pro \
 	tag.pro \
 	term.pro \
+	terminal.pro \
 	termlib.pro \
 	ui.pro \
 	undo.pro \
@@ -1848,7 +1868,7 @@ config auto/config.mk: auto/configure co
 		$(CONF_OPT_OUTPUT) $(CONF_OPT_GPM) $(CONF_OPT_WORKSHOP) \
 		$(CONF_OPT_FEAT) $(CONF_TERM_LIB) \
 		$(CONF_OPT_COMPBY) $(CONF_OPT_ACL) $(CONF_OPT_NETBEANS) \
-		$(CONF_OPT_CHANNEL) \
+		$(CONF_OPT_CHANNEL) $(CONF_OPT_TERMINAL) \
 		$(CONF_ARGS) $(CONF_OPT_MZSCHEME) $(CONF_OPT_PLTHOME) \
 		$(CONF_OPT_LUA) $(CONF_OPT_LUA_PREFIX) \
 		$(CONF_OPT_SYSMOUSE); \
@@ -3228,6 +3248,9 @@ objects/tag.o: tag.c
 objects/term.o: term.c
 	$(CCC) -o $@ term.c
 
+objects/terminal.o: terminal.c $(TERM_DEPS)
+	$(CCC) -o $@ terminal.c
+
 objects/ui.o: ui.c
 	$(CCC) -o $@ ui.c
 
@@ -3255,6 +3278,34 @@ objects/channel.o: channel.c
 Makefile:
 	@echo The name of the makefile MUST be "Makefile" (with capital M)!!!!
 
+CCCTERM = $(CCC) -Ilibvterm/include -DINLINE=""
+objects/term_encoding.o: libvterm/src/encoding.c $(TERM_DEPS)
+	$(CCCTERM) -o $@ libvterm/src/encoding.c
+
+objects/term_keyboard.o: libvterm/src/keyboard.c $(TERM_DEPS)
+	$(CCCTERM) -o $@ libvterm/src/keyboard.c
+
+objects/term_mouse.o: libvterm/src/mouse.c $(TERM_DEPS)
+	$(CCCTERM) -o $@ libvterm/src/mouse.c
+
+objects/term_parser.o: libvterm/src/parser.c $(TERM_DEPS)
+	$(CCCTERM) -o $@ libvterm/src/parser.c
+
+objects/term_pen.o: libvterm/src/pen.c $(TERM_DEPS)
+	$(CCCTERM) -o $@ libvterm/src/pen.c
+
+objects/term_screen.o: libvterm/src/screen.c $(TERM_DEPS)
+	$(CCCTERM) -o $@ libvterm/src/screen.c
+
+objects/term_state.o: libvterm/src/state.c $(TERM_DEPS)
+	$(CCCTERM) -o $@ libvterm/src/state.c
+
+objects/term_unicode.o: libvterm/src/unicode.c $(TERM_DEPS)
+	$(CCCTERM) -o $@ libvterm/src/unicode.c
+
+objects/term_vterm.o: libvterm/src/vterm.c $(TERM_DEPS)
+	$(CCCTERM) -o $@ libvterm/src/vterm.c
+
 ###############################################################################
 ### MacOS X installation
 ###
@@ -3399,7 +3450,7 @@ objects/ex_cmds2.o: ex_cmds2.c vim.h aut
 objects/ex_docmd.o: ex_docmd.c vim.h auto/config.h feature.h os_unix.h \
  auto/osdef.h ascii.h keymap.h term.h macros.h option.h structs.h \
  regexp.h gui.h gui_beval.h proto/gui_beval.pro alloc.h ex_cmds.h spell.h \
- proto.h globals.h farsi.h arabic.h
+ proto.h globals.h farsi.h arabic.h ex_cmdidxs.h
 objects/ex_eval.o: ex_eval.c vim.h auto/config.h feature.h os_unix.h auto/osdef.h \
  ascii.h keymap.h term.h macros.h option.h structs.h regexp.h gui.h \
  gui_beval.h proto/gui_beval.pro alloc.h ex_cmds.h spell.h proto.h \
--- a/src/auto/configure
+++ b/src/auto/configure
@@ -655,6 +655,8 @@ X_PRE_LIBS
 X_CFLAGS
 XMKMF
 xmkmfpath
+TERM_OBJ
+TERM_SRC
 CHANNEL_OBJ
 CHANNEL_SRC
 NETBEANS_OBJ
@@ -814,6 +816,7 @@ enable_cscope
 enable_workshop
 enable_netbeans
 enable_channel
+enable_terminal
 enable_multibyte
 enable_hangulinput
 enable_xim
@@ -1491,6 +1494,7 @@ Optional Features:
   --enable-workshop       Include Sun Visual Workshop support.
   --disable-netbeans      Disable NetBeans integration support.
   --disable-channel      Disable process communication support.
+  --enable-terminal     Disable terminal emulation support.
   --enable-multibyte      Include multibyte editing support.
   --enable-hangulinput    Include Hangul input support.
   --enable-xim            Include XIM input support.
@@ -7464,6 +7468,35 @@ if test "$enable_channel" = "yes"; then
 
 fi
 
+{ $as_echo "$as_me:${as_lineno-$LINENO}: checking --enable-terminal argument" >&5
+$as_echo_n "checking --enable-terminal argument... " >&6; }
+# Check whether --enable-terminal was given.
+if test "${enable_terminal+set}" = set; then :
+  enableval=$enable_terminal; enable_terminal="yes"
+fi
+
+if test "$enable_terminal" = "yes"; then
+  if test "x$features" = "xtiny" -o "x$features" = "xsmall"; then
+    { $as_echo "$as_me:${as_lineno-$LINENO}: result: cannot use terminal emulator with tiny or small features" >&5
+$as_echo "cannot use terminal emulator with tiny or small features" >&6; }
+    enable_terminal="no"
+  else
+    { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5
+$as_echo "yes" >&6; }
+  fi
+else
+  { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5
+$as_echo "no" >&6; }
+fi
+if test "$enable_terminal" = "yes"; then
+  $as_echo "#define FEAT_TERMINAL 1" >>confdefs.h
+
+  TERM_SRC="libvterm/src/encoding.c libvterm/src/keyboard.c libvterm/src/mouse.c libvterm/src/parser.c libvterm/src/pen.c libvterm/src/screen.c libvterm/src/state.c libvterm/src/unicode.c libvterm/src/vterm.c"
+
+  TERM_OBJ="objects/term_encoding.o objects/term_keyboard.o objects/term_mouse.o objects/term_parser.o objects/term_pen.o objects/term_screen.o objects/term_state.o objects/term_unicode.o objects/term_vterm.o"
+
+fi
+
 { $as_echo "$as_me:${as_lineno-$LINENO}: checking --enable-multibyte argument" >&5
 $as_echo_n "checking --enable-multibyte argument... " >&6; }
 # Check whether --enable-multibyte was given.
--- a/src/config.h.in
+++ b/src/config.h.in
@@ -431,6 +431,9 @@
 /* Define if you want to include process communication. */
 #undef FEAT_JOB_CHANNEL
 
+/* Define if you want to include terminal emulator support. */
+#undef FEAT_TERMINAL
+
 /* Define default global runtime path */
 #undef RUNTIME_GLOBAL
 
--- a/src/config.mk.in
+++ b/src/config.mk.in
@@ -91,6 +91,8 @@ NETBEANS_SRC	= @NETBEANS_SRC@
 NETBEANS_OBJ	= @NETBEANS_OBJ@
 CHANNEL_SRC	= @CHANNEL_SRC@
 CHANNEL_OBJ	= @CHANNEL_OBJ@
+TERM_SRC	= @TERM_SRC@
+TERM_OBJ	= @TERM_OBJ@
 
 RUBY		= @vi_cv_path_ruby@
 RUBY_SRC	= @RUBY_SRC@
--- a/src/configure.ac
+++ b/src/configure.ac
@@ -2028,6 +2028,28 @@ if test "$enable_channel" = "yes"; then
   AC_SUBST(CHANNEL_OBJ)
 fi
 
+AC_MSG_CHECKING(--enable-terminal argument)
+AC_ARG_ENABLE(terminal,
+	[  --enable-terminal     Disable terminal emulation support.],
+	[enable_terminal="yes"], )
+if test "$enable_terminal" = "yes"; then
+  if test "x$features" = "xtiny" -o "x$features" = "xsmall"; then
+    AC_MSG_RESULT([cannot use terminal emulator with tiny or small features])
+    enable_terminal="no"
+  else
+    AC_MSG_RESULT(yes)
+  fi
+else
+  AC_MSG_RESULT(no)
+fi
+if test "$enable_terminal" = "yes"; then
+  AC_DEFINE(FEAT_TERMINAL)
+  TERM_SRC="libvterm/src/encoding.c libvterm/src/keyboard.c libvterm/src/mouse.c libvterm/src/parser.c libvterm/src/pen.c libvterm/src/screen.c libvterm/src/state.c libvterm/src/unicode.c libvterm/src/vterm.c"
+  AC_SUBST(TERM_SRC)
+  TERM_OBJ="objects/term_encoding.o objects/term_keyboard.o objects/term_mouse.o objects/term_parser.o objects/term_pen.o objects/term_screen.o objects/term_state.o objects/term_unicode.o objects/term_vterm.o"
+  AC_SUBST(TERM_OBJ)
+fi
+
 AC_MSG_CHECKING(--enable-multibyte argument)
 AC_ARG_ENABLE(multibyte,
 	[  --enable-multibyte      Include multibyte editing support.], ,
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -5870,6 +5870,9 @@ f_has(typval_T *argvars, typval_T *rettv
 #ifdef FEAT_TERMGUICOLORS
 	"termguicolors",
 #endif
+#ifdef FEAT_TERMINAL
+	"terminal",
+#endif
 #ifdef TERMINFO
 	"terminfo",
 #endif
--- a/src/ex_cmdidxs.h
+++ b/src/ex_cmdidxs.h
@@ -25,12 +25,12 @@ static const unsigned short cmdidxs1[26]
   /* r */ 351,
   /* s */ 370,
   /* t */ 437,
-  /* u */ 472,
-  /* v */ 483,
-  /* w */ 501,
-  /* x */ 516,
-  /* y */ 525,
-  /* z */ 526
+  /* u */ 473,
+  /* v */ 484,
+  /* w */ 502,
+  /* x */ 517,
+  /* y */ 526,
+  /* z */ 527
 };
 
 /*
@@ -60,7 +60,7 @@ static const unsigned char cmdidxs2[26][
   /* q */ {  2,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0 },
   /* r */ {  0,  0,  0,  0,  0,  0,  0,  0, 11,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 13, 18,  0,  0,  0,  0 },
   /* s */ {  2,  6, 15,  0, 18, 22,  0, 24, 25,  0,  0, 28, 30, 34, 38, 40,  0, 48,  0, 49,  0, 61, 62,  0, 63,  0 },
-  /* t */ {  2,  0, 19,  0, 22, 23,  0, 24,  0, 25,  0, 26, 27, 28, 29, 30,  0, 31, 33,  0, 34,  0,  0,  0,  0,  0 },
+  /* t */ {  2,  0, 19,  0, 22, 24,  0, 25,  0, 26,  0, 27, 28, 29, 30, 31,  0, 32, 34,  0, 35,  0,  0,  0,  0,  0 },
   /* u */ {  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 10,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0 },
   /* v */ {  0,  0,  0,  0,  1,  0,  0,  0,  4,  0,  0,  0,  9, 12,  0,  0,  0,  0, 15,  0, 16,  0,  0,  0,  0,  0 },
   /* w */ {  2,  0,  0,  0,  0,  0,  0,  3,  4,  0,  0,  0,  0,  8,  0,  9, 10,  0, 12,  0, 13, 14,  0,  0,  0,  0 },
@@ -69,4 +69,4 @@ static const unsigned char cmdidxs2[26][
   /* z */ {  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0 }
 };
 
-static const int command_count = 539;
+static const int command_count = 540;
--- a/src/ex_docmd.c
+++ b/src/ex_docmd.c
@@ -488,6 +488,9 @@ static void	ex_folddo(exarg_T *eap);
 #ifndef FEAT_PROFILE
 # define ex_profile		ex_ni
 #endif
+#ifndef FEAT_TERMINAL
+# define ex_terminal		ex_ni
+#endif
 
 /*
  * Declare cmdnames[].
--- a/src/feature.h
+++ b/src/feature.h
@@ -1268,6 +1268,13 @@
 #endif
 
 /*
+ * +terminal		":terminal" command.  Runs a terminal in a window.
+ */
+#if !defined(FEAT_JOB_CHANNEL) && defined(FEAT_TERMINAL)
+# undef FEAT_TERMINAL
+#endif
+
+/*
  * +signs		Allow signs to be displayed to the left of text lines.
  *			Adds the ":sign" command.
  */
new file mode 100644
--- /dev/null
+++ b/src/libvterm/.bzrignore
@@ -0,0 +1,13 @@
+.libs
+*.lo
+*.la
+
+bin/*
+!bin/*.c
+
+pangoterm
+t/test
+t/suites.h
+t/externs.h
+t/harness
+src/encoding/*.inc
new file mode 100644
--- /dev/null
+++ b/src/libvterm/.gitignore
@@ -0,0 +1,18 @@
+*~
+*.swp
+
+tags
+src/*.o
+src/*.lo
+src/encoding/*.inc
+
+libvterm.la
+bin/unterm
+bin/vterm-ctrl
+bin/vterm-dump
+
+t/harness
+t/harness.lo
+t/harness.o
+
+.libs/
new file mode 100644
--- /dev/null
+++ b/src/libvterm/LICENSE
@@ -0,0 +1,23 @@
+
+
+The MIT License
+
+Copyright (c) 2008 Paul Evans <leonerd@leonerd.org.uk>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
new file mode 100644
--- /dev/null
+++ b/src/libvterm/Makefile
@@ -0,0 +1,145 @@
+ifeq ($(shell uname),Darwin)
+  LIBTOOL ?= glibtool
+else
+  LIBTOOL ?= libtool
+endif
+
+ifneq ($(VERBOSE),1)
+  LIBTOOL +=--quiet
+endif
+
+# override CFLAGS +=-Wall -Iinclude -std=c99 -DINLINE="static inline" -DUSE_INLINE
+override CFLAGS +=-Wall -Iinclude -std=c90 -Wpedantic -DINLINE=""
+
+ifeq ($(shell uname),SunOS)
+  override CFLAGS +=-D__EXTENSIONS__ -D_XPG6 -D__XOPEN_OR_POSIX
+endif
+
+ifeq ($(DEBUG),1)
+  override CFLAGS +=-ggdb -DDEBUG
+endif
+
+ifeq ($(PROFILE),1)
+  override CFLAGS +=-pg
+  override LDFLAGS+=-pg
+endif
+
+CFILES=$(sort $(wildcard src/*.c))
+HFILES=$(sort $(wildcard include/*.h))
+OBJECTS=$(CFILES:.c=.lo)
+LIBRARY=libvterm.la
+
+BINFILES_SRC=$(sort $(wildcard bin/*.c))
+BINFILES=$(BINFILES_SRC:.c=)
+
+TBLFILES=$(sort $(wildcard src/encoding/*.tbl))
+INCFILES=$(TBLFILES:.tbl=.inc)
+
+HFILES_INT=$(sort $(wildcard src/*.h)) $(HFILES)
+
+VERSION_MAJOR=0
+VERSION_MINOR=0
+
+VERSION_CURRENT=0
+VERSION_REVISION=0
+VERSION_AGE=0
+
+VERSION=0
+
+PREFIX=/usr/local
+BINDIR=$(PREFIX)/bin
+LIBDIR=$(PREFIX)/lib
+INCDIR=$(PREFIX)/include
+MANDIR=$(PREFIX)/share/man
+MAN3DIR=$(MANDIR)/man3
+
+all: $(LIBRARY) $(BINFILES)
+
+$(LIBRARY): $(OBJECTS)
+	@echo LINK $@
+	@$(LIBTOOL) --mode=link --tag=CC $(CC) -rpath $(LIBDIR) -version-info $(VERSION_CURRENT):$(VERSION_REVISION):$(VERSION_AGE) -o $@ $^ $(LDFLAGS)
+
+src/%.lo: src/%.c $(HFILES_INT)
+	@echo CC $<
+	@$(LIBTOOL) --mode=compile --tag=CC $(CC) $(CFLAGS) -o $@ -c $<
+
+src/encoding/%.inc: src/encoding/%.tbl
+	@echo TBL $<
+	@perl -CSD tbl2inc_c.pl $< >$@
+
+src/encoding.lo: $(INCFILES)
+
+bin/%: bin/%.c $(LIBRARY)
+	@echo CC $<
+	@$(LIBTOOL) --mode=link --tag=CC $(CC) $(CFLAGS) -o $@ $< -lvterm $(LDFLAGS)
+
+t/harness.lo: t/harness.c $(HFILES)
+	@echo CC $<
+	@$(LIBTOOL) --mode=compile --tag=CC $(CC) $(CFLAGS) -o $@ -c $<
+
+t/harness: t/harness.lo $(LIBRARY)
+	@echo LINK $@
+	@$(LIBTOOL) --mode=link --tag=CC $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
+
+.PHONY: test
+test: $(LIBRARY) t/harness
+	for T in `ls t/[0-9]*.test`; do echo "** $$T **"; perl t/run-test.pl $$T $(if $(VALGRIND),--valgrind) || exit 1; done
+
+.PHONY: clean
+clean:
+	$(LIBTOOL) --mode=clean rm -f $(OBJECTS) $(INCFILES)
+	$(LIBTOOL) --mode=clean rm -f t/harness.lo t/harness
+	$(LIBTOOL) --mode=clean rm -f $(LIBRARY) $(BINFILES)
+
+.PHONY: install
+install: install-inc install-lib install-bin
+
+install-inc:
+	install -d $(DESTDIR)$(INCDIR)
+	install -m644 $(HFILES) $(DESTDIR)$(INCDIR)
+	install -d $(DESTDIR)$(LIBDIR)/pkgconfig
+	sed -e "s,@PREFIX@,$(PREFIX)," -e "s,@LIBDIR@,$(LIBDIR)," -e "s,@VERSION@,$(VERSION)," <vterm.pc.in >$(DESTDIR)$(LIBDIR)/pkgconfig/vterm.pc
+
+install-lib: $(LIBRARY)
+	install -d $(DESTDIR)$(LIBDIR)
+	$(LIBTOOL) --mode=install install $(LIBRARY) $(DESTDIR)$(LIBDIR)/$(LIBRARY)
+	$(LIBTOOL) --mode=finish $(DESTDIR)$(LIBDIR)
+
+install-bin: $(BINFILES)
+	install -d $(DESTDIR)$(BINDIR)
+	$(LIBTOOL) --mode=install install $(BINFILES) $(DESTDIR)$(BINDIR)/
+
+# DIST CUT
+
+VERSION=$(VERSION_MAJOR).$(VERSION_MINOR)
+
+DISTDIR=libvterm-$(VERSION)
+
+distdir: $(INCFILES)
+	mkdir __distdir
+	cp LICENSE __distdir
+	mkdir __distdir/src
+	cp src/*.c src/*.h __distdir/src
+	mkdir __distdir/src/encoding
+	cp src/encoding/*.inc __distdir/src/encoding
+	mkdir __distdir/include
+	cp include/*.h __distdir/include
+	mkdir __distdir/bin
+	cp bin/*.c __distdir/bin
+	mkdir __distdir/t
+	cp t/*.test t/harness.c t/run-test.pl __distdir/t
+	sed "s,@VERSION@,$(VERSION)," <vterm.pc.in >__distdir/vterm.pc.in
+	sed "/^# DIST CUT/Q" <Makefile >__distdir/Makefile
+	mv __distdir $(DISTDIR)
+
+TARBALL=$(DISTDIR).tar.gz
+
+dist: distdir
+	tar -czf $(TARBALL) $(DISTDIR)
+	rm -rf $(DISTDIR)
+
+dist+bzr:
+	$(MAKE) dist VERSION=$(VERSION)+bzr`bzr revno`
+
+distdir+bzr:
+	$(MAKE) distdir VERSION=$(VERSION)+bzr`bzr revno`
new file mode 100644
--- /dev/null
+++ b/src/libvterm/README
@@ -0,0 +1,13 @@
+This is a MODIFIED version of libvterm.
+
+The original can be found:
+On the original site (tar archive and Bazaar repository):
+	http://www.leonerd.org.uk/code/libvterm/
+Cloned on Github:
+	https://github.com/neovim/libvterm
+
+Modifications:
+- Add a .gitignore file.
+- Convert from C99 to C90.
+- Other changes to support embedding in Vim.
+- 
new file mode 100644
--- /dev/null
+++ b/src/libvterm/bin/unterm.c
@@ -0,0 +1,287 @@
+#include <stdio.h>
+#include <string.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <unistd.h>
+
+#include "vterm.h"
+
+#define DEFINE_INLINES
+#include "../src/utf8.h" /* fill_utf8 */
+
+#define streq(a,b) (!strcmp(a,b))
+
+static VTerm *vt;
+static VTermScreen *vts;
+
+static int cols;
+static int rows;
+
+static enum {
+  FORMAT_PLAIN,
+  FORMAT_SGR
+} format = FORMAT_PLAIN;
+
+static int col2index(VTermColor target)
+{
+  int index;
+
+  for(index = 0; index < 256; index++) {
+    VTermColor col;
+    vterm_state_get_palette_color(NULL, index, &col);
+    if(col.red == target.red && col.green == target.green && col.blue == target.blue)
+      return index;
+  }
+  return -1;
+}
+
+static void dump_cell(const VTermScreenCell *cell, const VTermScreenCell *prevcell)
+{
+  switch(format) {
+    case FORMAT_PLAIN:
+      break;
+    case FORMAT_SGR:
+      {
+        /* If all 7 attributes change, that means 7 SGRs max */
+        /* Each colour could consume up to 3 */
+        int sgr[7 + 2*3]; int sgri = 0;
+
+        if(!prevcell->attrs.bold && cell->attrs.bold)
+          sgr[sgri++] = 1;
+        if(prevcell->attrs.bold && !cell->attrs.bold)
+          sgr[sgri++] = 22;
+
+        if(!prevcell->attrs.underline && cell->attrs.underline)
+          sgr[sgri++] = 4;
+        if(prevcell->attrs.underline && !cell->attrs.underline)
+          sgr[sgri++] = 24;
+
+        if(!prevcell->attrs.italic && cell->attrs.italic)
+          sgr[sgri++] = 3;
+        if(prevcell->attrs.italic && !cell->attrs.italic)
+          sgr[sgri++] = 23;
+
+        if(!prevcell->attrs.blink && cell->attrs.blink)
+          sgr[sgri++] = 5;
+        if(prevcell->attrs.blink && !cell->attrs.blink)
+          sgr[sgri++] = 25;
+
+        if(!prevcell->attrs.reverse && cell->attrs.reverse)
+          sgr[sgri++] = 7;
+        if(prevcell->attrs.reverse && !cell->attrs.reverse)
+          sgr[sgri++] = 27;
+
+        if(!prevcell->attrs.strike && cell->attrs.strike)
+          sgr[sgri++] = 9;
+        if(prevcell->attrs.strike && !cell->attrs.strike)
+          sgr[sgri++] = 29;
+
+        if(!prevcell->attrs.font && cell->attrs.font)
+          sgr[sgri++] = 10 + cell->attrs.font;
+        if(prevcell->attrs.font && !cell->attrs.font)
+          sgr[sgri++] = 10;
+
+        if(prevcell->fg.red   != cell->fg.red   ||
+            prevcell->fg.green != cell->fg.green ||
+            prevcell->fg.blue  != cell->fg.blue) {
+          int index = col2index(cell->fg);
+          if(index == -1)
+            sgr[sgri++] = 39;
+          else if(index < 8)
+            sgr[sgri++] = 30 + index;
+          else if(index < 16)
+            sgr[sgri++] = 90 + (index - 8);
+          else {
+            sgr[sgri++] = 38;
+            sgr[sgri++] = 5 | (1<<31);
+            sgr[sgri++] = index | (1<<31);
+          }
+        }
+
+        if(prevcell->bg.red   != cell->bg.red   ||
+            prevcell->bg.green != cell->bg.green ||
+            prevcell->bg.blue  != cell->bg.blue) {
+          int index = col2index(cell->bg);
+          if(index == -1)
+            sgr[sgri++] = 49;
+          else if(index < 8)
+            sgr[sgri++] = 40 + index;
+          else if(index < 16)
+            sgr[sgri++] = 100 + (index - 8);
+          else {
+            sgr[sgri++] = 48;
+            sgr[sgri++] = 5 | (1<<31);
+            sgr[sgri++] = index | (1<<31);
+          }
+        }
+
+        if(!sgri)
+          break;
+
+        printf("\x1b[");
+	{
+	  int i;
+	  for(i = 0; i < sgri; i++)
+	    printf(!i               ? "%d" :
+		sgr[i] & (1<<31) ? ":%d" :
+		";%d",
+		sgr[i] & ~(1<<31));
+	}
+        printf("m");
+      }
+      break;
+  }
+
+  {
+    int i;
+    for(i = 0; i < VTERM_MAX_CHARS_PER_CELL && cell->chars[i]; i++) {
+      char bytes[6];
+      bytes[fill_utf8(cell->chars[i], bytes)] = 0;
+      printf("%s", bytes);
+    }
+  }
+}
+
+static void dump_eol(const VTermScreenCell *prevcell)
+{
+  switch(format) {
+    case FORMAT_PLAIN:
+      break;
+    case FORMAT_SGR:
+      if(prevcell->attrs.bold || prevcell->attrs.underline || prevcell->attrs.italic ||
+         prevcell->attrs.blink || prevcell->attrs.reverse || prevcell->attrs.strike ||
+         prevcell->attrs.font)
+        printf("\x1b[m");
+      break;
+  }
+
+  printf("\n");
+}
+
+void dump_row(int row)
+{
+  VTermPos pos;
+  VTermScreenCell prevcell;
+  pos.row = row;
+  pos.col = 0;
+  memset(&prevcell, 0, sizeof(prevcell));
+  vterm_state_get_default_colors(vterm_obtain_state(vt), &prevcell.fg, &prevcell.bg);
+
+  while(pos.col < cols) {
+    VTermScreenCell cell;
+    vterm_screen_get_cell(vts, pos, &cell);
+
+    dump_cell(&cell, &prevcell);
+
+    pos.col += cell.width;
+    prevcell = cell;
+  }
+
+  dump_eol(&prevcell);
+}
+
+static int screen_sb_pushline(int cols, const VTermScreenCell *cells, void *user)
+{
+  VTermScreenCell prevcell;
+  int col;
+
+  memset(&prevcell, 0, sizeof(prevcell));
+  vterm_state_get_default_colors(vterm_obtain_state(vt), &prevcell.fg, &prevcell.bg);
+
+  for(col = 0; col < cols; col++) {
+    dump_cell(cells + col, &prevcell);
+    prevcell = cells[col];
+  }
+
+  dump_eol(&prevcell);
+
+  return 1;
+}
+
+static int screen_resize(int new_rows, int new_cols, void *user)
+{
+  rows = new_rows;
+  cols = new_cols;
+  return 1;
+}
+
+static VTermScreenCallbacks cb_screen = {
+  NULL, /* damage */
+  NULL, /* moverect */
+  NULL, /* movecursor */
+  NULL, /* settermprop */
+  NULL, /* bell */
+  &screen_resize, /* resize */
+  &screen_sb_pushline, /* sb_pushline */
+  NULL, /* popline */
+};
+
+int main(int argc, char *argv[])
+{
+  int opt;
+  const char *file;
+  int fd;
+  int len;
+  char buffer[1024];
+  int row;
+
+  rows = 25;
+  cols = 80;
+
+  while((opt = getopt(argc, argv, "f:l:c:")) != -1) {
+    switch(opt) {
+      case 'f':
+        if(streq(optarg, "plain"))
+          format = FORMAT_PLAIN;
+        else if(streq(optarg, "sgr"))
+          format = FORMAT_SGR;
+        else {
+          fprintf(stderr, "Unrecognised format '%s'\n", optarg);
+          exit(1);
+        }
+        break;
+
+      case 'l':
+        rows = atoi(optarg);
+        if(!rows)
+          rows = 25;
+        break;
+
+      case 'c':
+        cols = atoi(optarg);
+        if(!cols)
+          cols = 80;
+        break;
+    }
+  }
+
+  file = argv[optind++];
+  fd = open(file, O_RDONLY);
+  if(fd == -1) {
+    fprintf(stderr, "Cannot open %s - %s\n", file, strerror(errno));
+    exit(1);
+  }
+
+  vt = vterm_new(rows, cols);
+  vterm_set_utf8(vt, true);
+
+  vts = vterm_obtain_screen(vt);
+  vterm_screen_set_callbacks(vts, &cb_screen, NULL);
+
+  vterm_screen_reset(vts, 1);
+
+  while((len = read(fd, buffer, sizeof(buffer))) > 0) {
+    vterm_input_write(vt, buffer, len);
+  }
+
+  for(row = 0; row < rows; row++) {
+    dump_row(row);
+  }
+
+  close(fd);
+
+  vterm_free(vt);
+  return 0;
+}
new file mode 100644
--- /dev/null
+++ b/src/libvterm/bin/vterm-ctrl.c
@@ -0,0 +1,362 @@
+#define _XOPEN_SOURCE 500  /* strdup */
+
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#define streq(a,b) (strcmp(a,b)==0)
+
+#include <termios.h>
+
+static char *getvalue(int *argip, int argc, char *argv[])
+{
+  if(*argip >= argc) {
+    fprintf(stderr, "Expected an option value\n");
+    exit(1);
+  }
+
+  return argv[(*argip)++];
+}
+
+static int getchoice(int *argip, int argc, char *argv[], const char *options[])
+{
+  const char *arg = getvalue(argip, argc, argv);
+
+  int value = -1;
+  while(options[++value])
+    if(streq(arg, options[value]))
+      return value;
+
+  fprintf(stderr, "Unrecognised option value %s\n", arg);
+  exit(1);
+}
+
+typedef enum {
+  OFF,
+  ON,
+  QUERY
+} BoolQuery;
+
+static BoolQuery getboolq(int *argip, int argc, char *argv[])
+{
+  const char *choices[] = {"off", "on", "query", NULL};
+  return getchoice(argip, argc, argv, choices);
+}
+
+static char *helptext[] = {
+  "reset",
+  "s8c1t [off|on]",
+  "keypad [app|num]",
+  "screen [off|on|query]",
+  "cursor [off|on|query]",
+  "curblink [off|on|query]",
+  "curshape [block|under|bar|query]",
+  "mouse [off|click|clickdrag|motion]",
+  "altscreen [off|on|query]",
+  "bracketpaste [off|on|query]",
+  "icontitle [STR]",
+  "icon [STR]",
+  "title [STR]",
+  NULL
+};
+
+static bool seticanon(bool icanon, bool echo)
+{
+  struct termios termios;
+
+  tcgetattr(0, &termios);
+
+  bool ret = (termios.c_lflag & ICANON);
+
+  if(icanon) termios.c_lflag |=  ICANON;
+  else       termios.c_lflag &= ~ICANON;
+
+  if(echo) termios.c_lflag |=  ECHO;
+  else     termios.c_lflag &= ~ECHO;
+
+  tcsetattr(0, TCSANOW, &termios);
+
+  return ret;
+}
+
+static void await_c1(int c1)
+{
+  int c;
+
+  /* await CSI - 8bit or 2byte 7bit form */
+  bool in_esc = false;
+  while((c = getchar())) {
+    if(c == c1)
+      break;
+    if(in_esc && c == (char)(c1 - 0x40))
+      break;
+    if(!in_esc && c == 0x1b)
+      in_esc = true;
+    else
+      in_esc = false;
+  }
+}
+
+static char *read_csi()
+{
+  unsigned char csi[32];
+  int i = 0;
+
+  await_c1(0x9B); /* CSI */
+
+  /* TODO: This really should be a more robust CSI parser
+   */
+  for(; i < sizeof(csi)-1; i++) {
+    int c = csi[i] = getchar();
+    if(c >= 0x40 && c <= 0x7e)
+      break;
+  }
+  csi[++i] = 0;
+
+  /* TODO: returns longer than 32? */
+
+  return strdup((char *)csi);
+}
+
+static char *read_dcs()
+{
+  unsigned char dcs[32];
+  bool in_esc = false;
+  int i;
+
+  await_c1(0x90);
+
+  for(i = 0; i < sizeof(dcs)-1; ) {
+    char c = getchar();
+    if(c == 0x9c) /* ST */
+      break;
+    if(in_esc && c == 0x5c)
+      break;
+    if(!in_esc && c == 0x1b)
+      in_esc = true;
+    else {
+      dcs[i++] = c;
+      in_esc = false;
+    }
+  }
+  dcs[++i] = 0;
+
+  return strdup((char *)dcs);
+}
+
+static void usage(int exitcode)
+{
+  char **p;
+
+  fprintf(stderr, "Control a libvterm-based terminal\n"
+      "\n"
+      "Options:\n");
+
+  for(p = helptext; *p; p++)
+    fprintf(stderr, "  %s\n", *p);
+
+  exit(exitcode);
+}
+
+static bool query_dec_mode(int mode)
+{
+  char *s = NULL;
+
+  printf("\x1b[?%d$p", mode);
+
+  do {
+    int reply_mode, reply_value;
+    char reply_cmd;
+
+    if(s)
+      free(s);
+    s = read_csi();
+
+    /* expect "?" mode ";" value "$y" */
+
+    /* If the sscanf format string ends in a literal, we can't tell from
+     * its return value if it matches. Hence we'll %c the cmd and check it
+     * explicitly
+     */
+    if(sscanf(s, "?%d;%d$%c", &reply_mode, &reply_value, &reply_cmd) < 3)
+      continue;
+    if(reply_cmd != 'y')
+      continue;
+
+    if(reply_mode != mode)
+      continue;
+
+    free(s);
+
+    if(reply_value == 1 || reply_value == 3)
+      return true;
+    if(reply_value == 2 || reply_value == 4)
+      return false;
+
+    printf("Unrecognised reply to DECRQM: %d\n", reply_value);
+    return false;
+  } while(1);
+}
+
+static void do_dec_mode(int mode, BoolQuery val, const char *name)
+{
+  switch(val) {
+    case OFF:
+    case ON:
+      printf("\x1b[?%d%c", mode, val == ON ? 'h' : 'l');
+      break;
+
+    case QUERY:
+      if(query_dec_mode(mode))
+        printf("%s on\n", name);
+      else
+        printf("%s off\n", name);
+      break;
+  }
+}
+
+static int query_rqss_numeric(char *cmd)
+{
+  char *s = NULL;
+
+  printf("\x1bP$q%s\x1b\\", cmd);
+
+  do {
+    int num;
+
+    if(s)
+      free(s);
+    s = read_dcs();
+
+    if(!s)
+      return -1;
+    if(strlen(s) < strlen(cmd))
+      return -1;
+    if(strcmp(s + strlen(s) - strlen(cmd), cmd) != 0) {
+      printf("No match\n");
+      continue;
+    }
+
+    if(s[0] != '1' || s[1] != '$' || s[2] != 'r')
+      return -1;
+
+    if(sscanf(s + 3, "%d", &num) != 1)
+      return -1;
+
+    return num;
+  } while(1);
+}
+
+bool wasicanon;
+
+void restoreicanon(void)
+{
+  seticanon(wasicanon, true);
+}
+
+int main(int argc, char *argv[])
+{
+  int argi = 1;
+
+  if(argc == 1)
+    usage(0);
+
+  wasicanon = seticanon(false, false);
+  atexit(restoreicanon);
+
+  while(argi < argc) {
+    const char *arg = argv[argi++];
+
+    if(streq(arg, "reset")) {
+      printf("\x1b" "c");
+    }
+    else if(streq(arg, "s8c1t")) {
+      const char *choices[] = {"off", "on", NULL};
+      switch(getchoice(&argi, argc, argv, choices)) {
+      case 0:
+        printf("\x1b F"); break;
+      case 1:
+        printf("\x1b G"); break;
+      }
+    }
+    else if(streq(arg, "keypad")) {
+      const char *choices[] = {"app", "num", NULL};
+      switch(getchoice(&argi, argc, argv, choices)) {
+      case 0:
+        printf("\x1b="); break;
+      case 1:
+        printf("\x1b>"); break;
+      }
+    }
+    else if(streq(arg, "screen")) {
+      do_dec_mode(5, getboolq(&argi, argc, argv), "screen");
+    }
+    else if(streq(arg, "cursor")) {
+      do_dec_mode(25, getboolq(&argi, argc, argv), "cursor");
+    }
+    else if(streq(arg, "curblink")) {
+      do_dec_mode(12, getboolq(&argi, argc, argv), "curblink");
+    }
+    else if(streq(arg, "curshape")) {
+      /* TODO: This ought to query the current value of DECSCUSR because it */
+      /*   may need blinking on or off */
+      const char *choices[] = {"block", "under", "bar", "query", NULL};
+      int shape = getchoice(&argi, argc, argv, choices);
+      switch(shape) {
+        case 3: /* query */
+          shape = query_rqss_numeric(" q");
+          switch(shape) {
+            case 1: case 2:
+              printf("curshape block\n");
+              break;
+            case 3: case 4:
+              printf("curshape under\n");
+              break;
+            case 5: case 6:
+              printf("curshape bar\n");
+              break;
+          }
+          break;
+
+        case 0:
+        case 1:
+        case 2:
+          printf("\x1b[%d q", 1 + (shape * 2));
+          break;
+      }
+    }
+    else if(streq(arg, "mouse")) {
+      const char *choices[] = {"off", "click", "clickdrag", "motion", NULL};
+      switch(getchoice(&argi, argc, argv, choices)) {
+      case 0:
+        printf("\x1b[?1000l"); break;
+      case 1:
+        printf("\x1b[?1000h"); break;
+      case 2:
+        printf("\x1b[?1002h"); break;
+      case 3:
+        printf("\x1b[?1003h"); break;
+      }
+    }
+    else if(streq(arg, "altscreen")) {
+      do_dec_mode(1049, getboolq(&argi, argc, argv), "altscreen");
+    }
+    else if(streq(arg, "bracketpaste")) {
+      do_dec_mode(2004, getboolq(&argi, argc, argv), "bracketpaste");
+    }
+    else if(streq(arg, "icontitle")) {
+      printf("\x1b]0;%s\a", getvalue(&argi, argc, argv));
+    }
+    else if(streq(arg, "icon")) {
+      printf("\x1b]1;%s\a", getvalue(&argi, argc, argv));
+    }
+    else if(streq(arg, "title")) {
+      printf("\x1b]2;%s\a", getvalue(&argi, argc, argv));
+    }
+    else {
+      fprintf(stderr, "Unrecognised command %s\n", arg);
+      exit(1);
+    }
+  }
+  return 0;
+}
new file mode 100644
--- /dev/null
+++ b/src/libvterm/bin/vterm-dump.c
@@ -0,0 +1,231 @@
+/* Require getopt(3) */
+#define _XOPEN_SOURCE
+
+#include <stdio.h>
+#include <string.h>
+#define streq(a,b) (strcmp(a,b)==0)
+
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "vterm.h"
+
+static const char *special_begin = "{";
+static const char *special_end   = "}";
+
+static int parser_text(const char bytes[], size_t len, void *user)
+{
+  unsigned char *b = (unsigned char *)bytes;
+
+  int i;
+  for(i = 0; i < len; /* none */) {
+    if(b[i] < 0x20)        /* C0 */
+      break;
+    else if(b[i] < 0x80)   /* ASCII */
+      i++;
+    else if(b[i] < 0xa0)   /* C1 */
+      break;
+    else if(b[i] < 0xc0)   /* UTF-8 continuation */
+      break;
+    else if(b[i] < 0xe0) { /* UTF-8 2-byte */
+      /* 2-byte UTF-8 */
+      if(len < i+2) break;
+      i += 2;
+    }
+    else if(b[i] < 0xf0) { /* UTF-8 3-byte */
+      if(len < i+3) break;
+      i += 3;
+    }
+    else if(b[i] < 0xf8) { /* UTF-8 4-byte */
+      if(len < i+4) break;
+      i += 4;
+    }
+    else                   /* otherwise invalid */
+      break;
+  }
+
+  printf("%.*s", i, b);
+  return i;
+}
+
+/* 0     1      2      3       4     5      6      7      8      9      A      B      C      D      E      F    */
+static const char *name_c0[] = {
+  "NUL", "SOH", "STX", "ETX", "EOT", "ENQ", "ACK", "BEL", "BS",  "HT",  "LF",  "VT",  "FF",  "CR",  "LS0", "LS1",
+  "DLE", "DC1", "DC2", "DC3", "DC4", "NAK", "SYN", "ETB", "CAN", "EM",  "SUB", "ESC", "FS",  "GS",  "RS",  "US",
+};
+static const char *name_c1[] = {
+  NULL,  NULL,  "BPH", "NBH", NULL,  "NEL", "SSA", "ESA", "HTS", "HTJ", "VTS", "PLD", "PLU", "RI",  "SS2", "SS3",
+  "DCS", "PU1", "PU2", "STS", "CCH", "MW",  "SPA", "EPA", "SOS", NULL,  "SCI", "CSI", "ST",  "OSC", "PM",  "APC",
+};
+
+static int parser_control(unsigned char control, void *user)
+{
+  if(control < 0x20)
+    printf("%s%s%s", special_begin, name_c0[control], special_end);
+  else if(control >= 0x80 && control < 0xa0 && name_c1[control - 0x80])
+    printf("%s%s%s", special_begin, name_c1[control - 0x80], special_end);
+  else
+    printf("%sCONTROL 0x%02x%s", special_begin, control, special_end);
+
+  if(control == 0x0a)
+    printf("\n");
+  return 1;
+}
+
+static int parser_escape(const char bytes[], size_t len, void *user)
+{
+  if(bytes[0] >= 0x20 && bytes[0] < 0x30) {
+    if(len < 2)
+      return -1;
+    len = 2;
+  }
+  else {
+    len = 1;
+  }
+
+  printf("%sESC %.*s%s", special_begin, (int)len, bytes, special_end);
+
+  return len;
+}
+
+/* 0     1      2      3       4     5      6      7      8      9      A      B      C      D      E      F    */
+static const char *name_csi_plain[] = {
+  "ICH", "CUU", "CUD", "CUF", "CUB", "CNL", "CPL", "CHA", "CUP", "CHT", "ED",  "EL",  "IL",  "DL",  "EF",  "EA",
+  "DCH", "SSE", "CPR", "SU",  "SD",  "NP",  "PP",  "CTC", "ECH", "CVT", "CBT", "SRS", "PTX", "SDS", "SIMD",NULL,
+  "HPA", "HPR", "REP", "DA",  "VPA", "VPR", "HVP", "TBC", "SM",  "MC",  "HPB", "VPB", "RM",  "SGR", "DSR", "DAQ",
+};
+
+/*0           4           8           B         */
+static const int newline_csi_plain[] = {
+  0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0,
+  0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0,
+};
+
+static int parser_csi(const char *leader, const long args[], int argcount, const char *intermed, char command, void *user)
+{
+  const char *name = NULL;
+  if(!leader && !intermed && command < 0x70)
+    name = name_csi_plain[command - 0x40];
+  else if(leader && streq(leader, "?") && !intermed) {
+    /* DEC */
+    switch(command) {
+      case 'h': name = "DECSM"; break;
+      case 'l': name = "DECRM"; break;
+    }
+    if(name)
+      leader = NULL;
+  }
+
+  if(!leader && !intermed && command < 0x70 && newline_csi_plain[command - 0x40])
+    printf("\n");
+
+  if(name)
+    printf("%s%s", special_begin, name);
+  else
+    printf("%sCSI", special_begin);
+
+  if(leader && leader[0])
+    printf(" %s", leader);
+
+  {
+    int i;
+    for(i = 0; i < argcount; i++) {
+      printf(i ? "," : " ");
+  }
+
+    if(args[i] == CSI_ARG_MISSING)
+      printf("*");
+    else {
+      while(CSI_ARG_HAS_MORE(args[i]))
+        printf("%ld+", CSI_ARG(args[i++]));
+      printf("%ld", CSI_ARG(args[i]));
+    }
+  }
+
+  if(intermed && intermed[0])
+    printf(" %s", intermed);
+
+  if(name)
+    printf("%s", special_end);
+  else
+    printf(" %c%s", command, special_end);
+
+  return 1;
+}
+
+static int parser_osc(const char *command, size_t cmdlen, void *user)
+{
+  printf("%sOSC %.*s%s", special_begin, (int)cmdlen, command, special_end);
+
+  return 1;
+}
+
+static int parser_dcs(const char *command, size_t cmdlen, void *user)
+{
+  printf("%sDCS %.*s%s", special_begin, (int)cmdlen, command, special_end);
+
+  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 */
+};
+
+int main(int argc, char *argv[])
+{
+  int use_colour = isatty(1);
+  const char *file;
+  int fd;
+  VTerm *vt;
+  int len;
+  char buffer[1024];
+
+  int opt;
+  while((opt = getopt(argc, argv, "c")) != -1) {
+    switch(opt) {
+      case 'c': use_colour = 1; break;
+    }
+  }
+
+  file = argv[optind++];
+
+  if(!file || streq(file, "-"))
+    fd = 0; /* stdin */
+  else {
+    fd = open(file, O_RDONLY);
+    if(fd == -1) {
+      fprintf(stderr, "Cannot open %s - %s\n", file, strerror(errno));
+      exit(1);
+    }
+  }
+
+  if(use_colour) {
+    special_begin = "\x1b[7m{";
+    special_end   = "}\x1b[m";
+  }
+
+  /* Size matters not for the parser */
+  vt = vterm_new(25, 80);
+  vterm_set_utf8(vt, 1);
+  vterm_parser_set_callbacks(vt, &parser_cbs, NULL);
+
+  while((len = read(fd, buffer, sizeof(buffer))) > 0) {
+    vterm_input_write(vt, buffer, len);
+  }
+
+  printf("\n");
+
+  close(fd);
+  vterm_free(vt);
+  return 0;
+}
new file mode 100644
--- /dev/null
+++ b/src/libvterm/doc/URLs
@@ -0,0 +1,11 @@
+ECMA-48:
+  http://www.ecma-international.org/publications/standards/Ecma-048.htm
+
+Xterm Control Sequences:
+  http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
+
+Digital VT100 User Guide:
+  http://vt100.net/docs/vt100-ug/
+
+Digital VT220 Programmer Reference Manual
+  http://vt100.net/docs/vt220-rm/
new file mode 100644
--- /dev/null
+++ b/src/libvterm/doc/seqs.txt
@@ -0,0 +1,226 @@
+Sequences documented in parens are implicit ones from parser.c, which move
+between states.
+
+1 = VT100
+2 = VT220
+3 = VT320
+
+   C0 controls
+
+123   0x00             = NUL
+123   0x07             = BEL
+123   0x08             = BS
+123   0x09             = HT
+123   0x0A             = LF
+123   0x0B             = VT
+123   0x0C             = FF
+123   0x0D             = CR
+123   0x0E             = LS1
+123   0x0F             = LS0
+     (0x18             = CAN)
+     (0x1A             = SUB)
+     (0x1B             = ESC)
+
+123   0x7f             = DEL (ignored)
+
+   C1 controls
+
+123   0x84             = IND
+123   0x85             = NEL
+123   0x88             = HTS
+123   0x8D             = RI
+ 23   0x8e             = SS2
+ 23   0x8f             = SS3
+     (0x90             = DCS)
+     (0x9B             = CSI)
+     (0x9C             = ST)
+     (0x9D             = OSC)
+
+   Escape sequences
+    - excluding sequences that are C1 aliases
+
+123   ESC ()           = SCS, select character set (G0, G1)
+ 23   ESC *+           = SCS, select character set (G2, G3)
+123   ESC 7            = DECSC - save cursor
+123   ESC 8            = DECRC - restore cursor
+123   ESC # 3          = DECDHL, double-height line (top half)
+123   ESC # 4          = DECDHL, double-height line (bottom half)
+123   ESC # 5          = DECSWL, single-width single-height line
+123   ESC # 6          = DECDWL, double-width single-height line
+123   ESC # 8          = DECALN
+123   ESC <            = Ignored (used by VT100 to exit VT52 mode)
+123   ESC =            = DECKPAM, keypad application mode
+123   ESC >            = DECKPNM, keypad numeric mode
+ 23   ESC Sp F         = S7C1T
+ 23   ESC Sp G         = S8C1T
+     (ESC P            = DCS)
+     (ESC [            = CSI)
+     (ESC \            = ST)
+     (ESC ]            = OSC)
+123   ESC c            = RIS, reset initial state
+  3   ESC n            = LS2
+  3   ESC o            = LS3
+  3   ESC ~            = LS1R
+  3   ESC }            = LS2R
+  3   ESC |            = LS3R
+
+   DCSes
+
+  3   DCS $ q      ST  = DECRQSS
+  3           m        =   Request SGR
+              Sp q     =   Request DECSCUSR
+  3           " q      =   Request DECSCA
+  3           r        =   Request DECSTBM
+              s        =   Request DECSLRM
+
+   CSIs
+ 23   CSI @            = ICH
+123   CSI A            = CUU
+123   CSI B            = CUD
+123   CSI C            = CUF
+123   CSI D            = CUB
+      CSI E            = CNL
+      CSI F            = CPL
+      CSI G            = CHA
+123   CSI H            = CUP
+      CSI I            = CHT
+123   CSI J            = ED
+ 23   CSI ? J          = DECSED, selective erase in display
+123   CSI K            = EL
+ 23   CSI ? K          = DECSEL, selective erase in line
+ 23   CSI L            = IL
+ 23   CSI M            = DL
+ 23   CSI P            = DCH
+      CSI S            = SU
+      CSI T            = SD
+ 23   CSI X            = ECH
+      CSI Z            = CBT
+      CSI `            = HPA
+      CSI a            = HPR
+123   CSI   c          = DA, device attributes
+123       0            =   DA
+ 23   CSI >   c        = DECSDA
+ 23         0          =   SDA
+      CSI d            = VPA
+      CSI e            = VPR
+123   CSI f            = HVP
+123   CSI g            = TBC
+123   CSI h            = SM, Set mode
+123   CSI ? h          = DECSM, DEC set mode
+      CSI j            = HPB
+      CSI k            = VPB
+123   CSI l            = RM, Reset mode
+123   CSI ? l          = DECRM, DEC reset mode
+123   CSI m            = SGR, Set Graphic Rendition
+123   CSI   n          = DSR, Device Status Report
+ 23       5            =   operating status
+ 23       6            =   CPR = cursor position
+ 23   CSI ? n          = DECDSR; behaves as DSR but uses CSI ? instead of CSI to respond
+ 23   CSI ! p          = DECSTR, soft terminal reset
+  3   CSI ? $ p        = DECRQM, request mode
+      CSI   Sp q       = DECSCUSR (odd numbers blink, even numbers solid)
+          1 or 2       =   block
+          3 or 4       =   underline
+          5 or 6       =   I-beam to left
+ 23   CSI " q          = DECSCA, select character attributes
+123   CSI r            = DECSTBM
+      CSI s            = DECSLRM
+      CSI ' }          = DECIC
+      CSI ' ~          = DECDC
+
+   OSCs
+
+      OSC 0;           = Set icon name and title
+      OSC 1;           = Set icon name
+      OSC 2;           = Set title
+
+   Standard modes
+
+ 23   SM 4             = IRM
+123   SM 20            = NLM, linefeed/newline
+
+   DEC modes
+
+123   DECSM 1          = DECCKM, cursor keys
+123   DECSM 5          = DECSCNM, screen
+123   DECSM 6          = DECOM, origin
+123   DECSM 7          = DECAWM, autowrap
+      DECSM 12         = Cursor blink
+ 23   DECSM 25         = DECTCEM, text cursor enable
+      DECSM 69         = DECVSSM, vertical screen split
+      DECSM 1000       = Mouse click/release tracking
+      DECSM 1002       = Mouse click/release/drag tracking
+      DECSM 1003       = Mouse all movements tracking
+      DECSM 1005       = Mouse protocol extended (UTF-8) - not recommended
+      DECSM 1006       = Mouse protocol SGR
+      DECSM 1015       = Mouse protocol rxvt
+      DECSM 1047       = Altscreen
+      DECSM 1048       = Save cursor
+      DECSM 1049       = 1047 + 1048
+      DECSM 2004       = Bracketed paste
+
+   Graphic Renditions
+
+123   SGR 0            = Reset
+123   SGR 1            = Bold on
+      SGR 3            = Italic on
+123   SGR 4            = Underline single
+123   SGR 5            = Blink on
+123   SGR 7            = Reverse on
+      SGR 9            = Strikethrough on
+      SGR 10-19        = Select font
+      SGR 21           = Underline double
+ 23   SGR 22           = Bold off
+      SGR 23           = Italic off
+ 23   SGR 24           = Underline off
+ 23   SGR 25           = Blink off
+ 23   SGR 27           = Reverse off
+      SGR 29           = Strikethrough off
+      SGR 30-37        = Foreground ANSI
+      SGR 38           = Foreground alternative palette
+      SGR 39           = Foreground default
+      SGR 40-47        = Background ANSI
+      SGR 48           = Background alternative palette
+      SGR 49           = Background default
+      SGR 90-97        = Foreground ANSI high-intensity
+      SGR 100-107      = Background ANSI high-intensity
+
+The state storage used by ESC 7 and DECSM 1048/1049 is shared.
+
+   Unimplemented sequences:
+
+The following sequences are not recognised by libvterm.
+
+123   0x05             = ENQ
+  3   0x11             = DC1 (XON)
+  3   0x13             = DC3 (XOFF)
+12    ESC Z            = DECID, identify terminal
+      DCS $ q          = [DECRQSS]
+  3           " p      =   Request DECSCL
+  3           $ }      =   Request DECSASD
+  3           $ ~      =   Request DECSSDT
+ 23   DCS {            = DECDLD, down-line-loadable character set
+ 23   DCS |            = DECUDK, user-defined key
+ 23   CSI i            = DEC printer control
+ 23   CSI " p          = DECSCL, set compatibility level
+1     CSI q            = DECLL, load LEDs
+  3   CSI   $ u        = DECRQTSR, request terminal state report
+  3       1            =   terminal state report
+  3   CSI & u          = DECRQUPSS, request user-preferred supplemental set
+  3   CSI   $ w        = DECRQPSR, request presentation state report
+  3       1            =   cursor information report
+  3       2            =   tab stop report
+1     CSI x            = DECREQTPARM, request terminal parameters
+123   CSI y            = DECTST, invoke confidence test
+  3   CSI $ }          = DECSASD, select active status display
+  3   CSI $ ~          = DECSSDT, select status line type
+ 23   SM 2             = KAM, keyboard action
+123   SM 12            = SRM, send/receive
+123   DECSM 2          = DECANM, ANSI/VT52
+123   DECSM 3          = DECCOLM, 132 column
+123   DECSM 4          = DECSCLM, scrolling
+123   DECSM 8          = DECARM, auto-repeat
+12    DECSM 9          = DECINLM, interlace
+ 23   DECSM 18         = DECPFF, print form feed
+ 23   DECSM 19         = DECPEX, print extent
+ 23   DECSM 42         = DECNRCM, national/multinational character
new file mode 100644
--- /dev/null
+++ b/src/libvterm/include/vterm.h
@@ -0,0 +1,370 @@
+/*
+ * NOTE: This is a MODIFIED version of libvterm, see the README file.
+ */
+#ifndef __VTERM_H__
+#define __VTERM_H__
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdint.h>
+#include <stdlib.h>
+#include <stdbool.h>
+
+#include "vterm_keycodes.h"
+
+typedef struct VTerm VTerm;
+typedef struct VTermState VTermState;
+typedef struct VTermScreen VTermScreen;
+
+/* Specifies a screen point. */
+typedef struct {
+  int row;
+  int col;
+} VTermPos;
+
+/*
+ * Some small utility functions; we can just keep these static here.
+ */
+
+/*
+ * Order points by on-screen flow order:
+ * Return < 0 if "a" is before "b"
+ * Return  0  if "a" and "b" are equal
+ * Return > 0 if "a" is after "b".
+ */
+int vterm_pos_cmp(VTermPos a, VTermPos b);
+
+#if defined(DEFINE_INLINES) || USE_INLINE
+INLINE int vterm_pos_cmp(VTermPos a, VTermPos b)
+{
+  return (a.row == b.row) ? a.col - b.col : a.row - b.row;
+}
+#endif
+
+/* Specifies a rectangular screen area. */
+typedef struct {
+  int start_row;
+  int end_row;
+  int start_col;
+  int end_col;
+} VTermRect;
+
+/* Return true if the rect "r" contains the point "p". */
+int vterm_rect_contains(VTermRect r, VTermPos p);
+
+#if defined(DEFINE_INLINES) || USE_INLINE
+INLINE int vterm_rect_contains(VTermRect r, VTermPos p)
+{
+  return p.row >= r.start_row && p.row < r.end_row &&
+         p.col >= r.start_col && p.col < r.end_col;
+}
+#endif
+
+/* Move "rect" "row_delta" down and "col_delta" right.
+ * Does not check boundaries. */
+void vterm_rect_move(VTermRect *rect, int row_delta, int col_delta);
+
+#if defined(DEFINE_INLINES) || USE_INLINE
+INLINE void vterm_rect_move(VTermRect *rect, int row_delta, int col_delta)
+{
+  rect->start_row += row_delta; rect->end_row += row_delta;
+  rect->start_col += col_delta; rect->end_col += col_delta;
+}
+#endif
+
+typedef struct {
+  uint8_t red, green, blue;
+} VTermColor;
+
+typedef enum {
+  /* VTERM_VALUETYPE_NONE = 0 */
+  VTERM_VALUETYPE_BOOL = 1,
+  VTERM_VALUETYPE_INT,
+  VTERM_VALUETYPE_STRING,
+  VTERM_VALUETYPE_COLOR
+} VTermValueType;
+
+typedef union {
+  int boolean;
+  int number;
+  char *string;
+  VTermColor color;
+} VTermValue;
+
+typedef enum {
+  /* VTERM_ATTR_NONE = 0 */
+  VTERM_ATTR_BOLD = 1,   /* bool:   1, 22 */
+  VTERM_ATTR_UNDERLINE,  /* number: 4, 21, 24 */
+  VTERM_ATTR_ITALIC,     /* bool:   3, 23 */
+  VTERM_ATTR_BLINK,      /* bool:   5, 25 */
+  VTERM_ATTR_REVERSE,    /* bool:   7, 27 */
+  VTERM_ATTR_STRIKE,     /* bool:   9, 29 */
+  VTERM_ATTR_FONT,       /* number: 10-19 */
+  VTERM_ATTR_FOREGROUND, /* color:  30-39 90-97 */
+  VTERM_ATTR_BACKGROUND  /* color:  40-49 100-107 */
+} VTermAttr;
+
+typedef enum {
+  /* VTERM_PROP_NONE = 0 */
+  VTERM_PROP_CURSORVISIBLE = 1, /* bool */
+  VTERM_PROP_CURSORBLINK,       /* bool */
+  VTERM_PROP_ALTSCREEN,         /* bool */
+  VTERM_PROP_TITLE,             /* string */
+  VTERM_PROP_ICONNAME,          /* string */
+  VTERM_PROP_REVERSE,           /* bool */
+  VTERM_PROP_CURSORSHAPE,       /* number */
+  VTERM_PROP_MOUSE              /* number */
+} VTermProp;
+
+enum {
+  VTERM_PROP_CURSORSHAPE_BLOCK = 1,
+  VTERM_PROP_CURSORSHAPE_UNDERLINE,
+  VTERM_PROP_CURSORSHAPE_BAR_LEFT
+};
+
+enum {
+  VTERM_PROP_MOUSE_NONE = 0,
+  VTERM_PROP_MOUSE_CLICK,
+  VTERM_PROP_MOUSE_DRAG,
+  VTERM_PROP_MOUSE_MOVE
+};
+
+typedef struct {
+  const uint32_t *chars;
+  int             width;
+  unsigned int    protected_cell:1;  /* DECSCA-protected against DECSEL/DECSED */
+  unsigned int    dwl:1;             /* DECDWL or DECDHL double-width line */
+  unsigned int    dhl:2;             /* DECDHL double-height line (1=top 2=bottom) */
+} VTermGlyphInfo;
+
+typedef struct {
+  unsigned int    doublewidth:1;     /* DECDWL or DECDHL line */
+  unsigned int    doubleheight:2;    /* DECDHL line (1=top 2=bottom) */
+} VTermLineInfo;
+
+typedef struct {
+  /* libvterm relies on the allocated memory to be zeroed out before it is
+   * returned by the allocator. */
+  void *(*malloc)(size_t size, void *allocdata);
+  void  (*free)(void *ptr, void *allocdata);
+} VTermAllocatorFunctions;
+
+/* Allocate and initialize a new terminal with default allocators. */
+VTerm *vterm_new(int rows, int cols);
+
+/* Allocate and initialize a new terminal with specified allocators. */
+VTerm *vterm_new_with_allocator(int rows, int cols, VTermAllocatorFunctions *funcs, void *allocdata);
+
+/* Free and cleanup a terminal and all its data. */
+void   vterm_free(VTerm* vt);
+
+void vterm_get_size(const VTerm *vt, int *rowsp, int *colsp);
+void vterm_set_size(VTerm *vt, int rows, int cols);
+
+int  vterm_get_utf8(const VTerm *vt);
+void vterm_set_utf8(VTerm *vt, int is_utf8);
+
+size_t vterm_input_write(VTerm *vt, const char *bytes, size_t len);
+
+size_t vterm_output_get_buffer_size(const VTerm *vt);
+size_t vterm_output_get_buffer_current(const VTerm *vt);
+size_t vterm_output_get_buffer_remaining(const VTerm *vt);
+
+size_t vterm_output_read(VTerm *vt, char *buffer, size_t len);
+
+void vterm_keyboard_unichar(VTerm *vt, uint32_t c, VTermModifier mod);
+void vterm_keyboard_key(VTerm *vt, VTermKey key, VTermModifier mod);
+
+void vterm_keyboard_start_paste(VTerm *vt);
+void vterm_keyboard_end_paste(VTerm *vt);
+
+void vterm_mouse_move(VTerm *vt, int row, int col, VTermModifier mod);
+void vterm_mouse_button(VTerm *vt, int button, bool pressed, VTermModifier mod);
+
+/* ------------
+ * Parser layer
+ * ------------ */
+
+/* Flag to indicate non-final subparameters in a single CSI parameter.
+ * Consider
+ *   CSI 1;2:3:4;5a
+ * 1 4 and 5 are final.
+ * 2 and 3 are non-final and will have this bit set
+ *
+ * Don't confuse this with the final byte of the CSI escape; 'a' in this case.
+ */
+#define CSI_ARG_FLAG_MORE (1<<31)
+#define CSI_ARG_MASK      (~(1<<31))
+
+#define CSI_ARG_HAS_MORE(a) ((a) & CSI_ARG_FLAG_MORE)
+#define CSI_ARG(a)          ((a) & CSI_ARG_MASK)
+
+/* Can't use -1 to indicate a missing argument; use this instead */
+#define CSI_ARG_MISSING ((1UL<<31)-1)
+
+#define CSI_ARG_IS_MISSING(a) (CSI_ARG(a) == CSI_ARG_MISSING)
+#define CSI_ARG_OR(a,def)     (CSI_ARG(a) == CSI_ARG_MISSING ? (def) : CSI_ARG(a))
+#define CSI_ARG_COUNT(a)      (CSI_ARG(a) == CSI_ARG_MISSING || CSI_ARG(a) == 0 ? 1 : CSI_ARG(a))
+
+typedef struct {
+  int (*text)(const char *bytes, size_t len, void *user);
+  int (*control)(unsigned char control, void *user);
+  int (*escape)(const char *bytes, size_t len, void *user);
+  int (*csi)(const char *leader, const long args[], int argcount, const char *intermed, char command, void *user);
+  int (*osc)(const char *command, size_t cmdlen, void *user);
+  int (*dcs)(const char *command, size_t cmdlen, void *user);
+  int (*resize)(int rows, int cols, void *user);
+} VTermParserCallbacks;
+
+void  vterm_parser_set_callbacks(VTerm *vt, const VTermParserCallbacks *callbacks, void *user);
+void *vterm_parser_get_cbdata(VTerm *vt);
+
+/* -----------
+ * State layer
+ * ----------- */
+
+typedef struct {
+  int (*putglyph)(VTermGlyphInfo *info, VTermPos pos, void *user);
+  int (*movecursor)(VTermPos pos, VTermPos oldpos, int visible, void *user);
+  int (*scrollrect)(VTermRect rect, int downward, int rightward, void *user);
+  int (*moverect)(VTermRect dest, VTermRect src, void *user);
+  int (*erase)(VTermRect rect, int selective, void *user);
+  int (*initpen)(void *user);
+  int (*setpenattr)(VTermAttr attr, VTermValue *val, void *user);
+  int (*settermprop)(VTermProp prop, VTermValue *val, void *user);
+  int (*bell)(void *user);
+  int (*resize)(int rows, int cols, VTermPos *delta, void *user);
+  int (*setlineinfo)(int row, const VTermLineInfo *newinfo, const VTermLineInfo *oldinfo, void *user);
+} VTermStateCallbacks;
+
+VTermState *vterm_obtain_state(VTerm *vt);
+
+void  vterm_state_set_callbacks(VTermState *state, const VTermStateCallbacks *callbacks, void *user);
+void *vterm_state_get_cbdata(VTermState *state);
+
+/* Only invokes control, csi, osc, dcs */
+void  vterm_state_set_unrecognised_fallbacks(VTermState *state, const VTermParserCallbacks *fallbacks, void *user);
+void *vterm_state_get_unrecognised_fbdata(VTermState *state);
+
+void vterm_state_reset(VTermState *state, int hard);
+void vterm_state_get_cursorpos(const VTermState *state, VTermPos *cursorpos);
+void vterm_state_get_default_colors(const VTermState *state, VTermColor *default_fg, VTermColor *default_bg);
+void vterm_state_get_palette_color(const VTermState *state, int index, VTermColor *col);
+void vterm_state_set_default_colors(VTermState *state, const VTermColor *default_fg, const VTermColor *default_bg);
+void vterm_state_set_palette_color(VTermState *state, int index, const VTermColor *col);
+void vterm_state_set_bold_highbright(VTermState *state, int bold_is_highbright);
+int  vterm_state_get_penattr(const VTermState *state, VTermAttr attr, VTermValue *val);
+int  vterm_state_set_termprop(VTermState *state, VTermProp prop, VTermValue *val);
+const VTermLineInfo *vterm_state_get_lineinfo(const VTermState *state, int row);
+
+/* ------------
+ * Screen layer
+ * ------------ */
+
+typedef struct {
+    unsigned int bold      : 1;
+    unsigned int underline : 2;
+    unsigned int italic    : 1;
+    unsigned int blink     : 1;
+    unsigned int reverse   : 1;
+    unsigned int strike    : 1;
+    unsigned int font      : 4; /* 0 to 9 */
+    unsigned int dwl       : 1; /* On a DECDWL or DECDHL line */
+    unsigned int dhl       : 2; /* On a DECDHL line (1=top 2=bottom) */
+} VTermScreenCellAttrs;
+
+typedef struct {
+#define VTERM_MAX_CHARS_PER_CELL 6
+  uint32_t chars[VTERM_MAX_CHARS_PER_CELL];
+  char     width;
+  VTermScreenCellAttrs attrs;
+  VTermColor fg, bg;
+} VTermScreenCell;
+
+/* All fields are optional, NULL when not used. */
+typedef struct {
+  int (*damage)(VTermRect rect, void *user);
+  int (*moverect)(VTermRect dest, VTermRect src, void *user);
+  int (*movecursor)(VTermPos pos, VTermPos oldpos, int visible, void *user);
+  int (*settermprop)(VTermProp prop, VTermValue *val, void *user);
+  int (*bell)(void *user);
+  int (*resize)(int rows, int cols, void *user);
+  int (*sb_pushline)(int cols, const VTermScreenCell *cells, void *user);
+  int (*sb_popline)(int cols, VTermScreenCell *cells, void *user);
+} VTermScreenCallbacks;
+
+VTermScreen *vterm_obtain_screen(VTerm *vt);
+
+/*
+ * Install screen callbacks.  These are invoked when the screen contents is
+ * changed.  "user" is passed into to the callback.
+ */
+void  vterm_screen_set_callbacks(VTermScreen *screen, const VTermScreenCallbacks *callbacks, void *user);
+void *vterm_screen_get_cbdata(VTermScreen *screen);
+
+/* Only invokes control, csi, osc, dcs */
+void  vterm_screen_set_unrecognised_fallbacks(VTermScreen *screen, const VTermParserCallbacks *fallbacks, void *user);
+void *vterm_screen_get_unrecognised_fbdata(VTermScreen *screen);
+
+void vterm_screen_enable_altscreen(VTermScreen *screen, int altscreen);
+
+typedef enum {
+  VTERM_DAMAGE_CELL,    /* every cell */
+  VTERM_DAMAGE_ROW,     /* entire rows */
+  VTERM_DAMAGE_SCREEN,  /* entire screen */
+  VTERM_DAMAGE_SCROLL   /* entire screen + scrollrect */
+} VTermDamageSize;
+
+void vterm_screen_flush_damage(VTermScreen *screen);
+void vterm_screen_set_damage_merge(VTermScreen *screen, VTermDamageSize size);
+
+void   vterm_screen_reset(VTermScreen *screen, int hard);
+
+/* Neither of these functions NUL-terminate the buffer */
+size_t vterm_screen_get_chars(const VTermScreen *screen, uint32_t *chars, size_t len, const VTermRect rect);
+size_t vterm_screen_get_text(const VTermScreen *screen, char *str, size_t len, const VTermRect rect);
+
+typedef enum {
+  VTERM_ATTR_BOLD_MASK       = 1 << 0,
+  VTERM_ATTR_UNDERLINE_MASK  = 1 << 1,
+  VTERM_ATTR_ITALIC_MASK     = 1 << 2,
+  VTERM_ATTR_BLINK_MASK      = 1 << 3,
+  VTERM_ATTR_REVERSE_MASK    = 1 << 4,
+  VTERM_ATTR_STRIKE_MASK     = 1 << 5,
+  VTERM_ATTR_FONT_MASK       = 1 << 6,
+  VTERM_ATTR_FOREGROUND_MASK = 1 << 7,
+  VTERM_ATTR_BACKGROUND_MASK = 1 << 8
+} VTermAttrMask;
+
+int vterm_screen_get_attrs_extent(const VTermScreen *screen, VTermRect *extent, VTermPos pos, VTermAttrMask attrs);
+
+int vterm_screen_get_cell(const VTermScreen *screen, VTermPos pos, VTermScreenCell *cell);
+
+int vterm_screen_is_eol(const VTermScreen *screen, VTermPos pos);
+
+/* ---------
+ * Utilities
+ * --------- */
+
+VTermValueType vterm_get_attr_type(VTermAttr attr);
+VTermValueType vterm_get_prop_type(VTermProp prop);
+
+void vterm_scroll_rect(VTermRect rect,
+                       int downward,
+                       int rightward,
+                       int (*moverect)(VTermRect src, VTermRect dest, void *user),
+                       int (*eraserect)(VTermRect rect, int selective, void *user),
+                       void *user);
+
+void vterm_copy_cells(VTermRect dest,
+                      VTermRect src,
+                      void (*copycell)(VTermPos dest, VTermPos src, void *user),
+                      void *user);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
new file mode 100644
--- /dev/null
+++ b/src/libvterm/include/vterm_keycodes.h
@@ -0,0 +1,58 @@
+#ifndef __VTERM_INPUT_H__
+#define __VTERM_INPUT_H__
+
+typedef enum {
+  VTERM_MOD_NONE  = 0x00,
+  VTERM_MOD_SHIFT = 0x01,
+  VTERM_MOD_ALT   = 0x02,
+  VTERM_MOD_CTRL  = 0x04
+} VTermModifier;
+
+typedef enum {
+  VTERM_KEY_NONE,
+
+  VTERM_KEY_ENTER,
+  VTERM_KEY_TAB,
+  VTERM_KEY_BACKSPACE,
+  VTERM_KEY_ESCAPE,
+
+  VTERM_KEY_UP,
+  VTERM_KEY_DOWN,
+  VTERM_KEY_LEFT,
+  VTERM_KEY_RIGHT,
+
+  VTERM_KEY_INS,
+  VTERM_KEY_DEL,
+  VTERM_KEY_HOME,
+  VTERM_KEY_END,
+  VTERM_KEY_PAGEUP,
+  VTERM_KEY_PAGEDOWN,
+
+  VTERM_KEY_FUNCTION_0   = 256,
+  VTERM_KEY_FUNCTION_MAX = VTERM_KEY_FUNCTION_0 + 255,
+
+  VTERM_KEY_KP_0,
+  VTERM_KEY_KP_1,
+  VTERM_KEY_KP_2,
+  VTERM_KEY_KP_3,
+  VTERM_KEY_KP_4,
+  VTERM_KEY_KP_5,
+  VTERM_KEY_KP_6,
+  VTERM_KEY_KP_7,
+  VTERM_KEY_KP_8,
+  VTERM_KEY_KP_9,
+  VTERM_KEY_KP_MULT,
+  VTERM_KEY_KP_PLUS,
+  VTERM_KEY_KP_COMMA,
+  VTERM_KEY_KP_MINUS,
+  VTERM_KEY_KP_PERIOD,
+  VTERM_KEY_KP_DIVIDE,
+  VTERM_KEY_KP_ENTER,
+  VTERM_KEY_KP_EQUAL,
+
+  VTERM_KEY_MAX /* Must be last */
+} VTermKey;
+
+#define VTERM_KEY_FUNCTION(n) (VTERM_KEY_FUNCTION_0+(n))
+
+#endif
new file mode 100644
--- /dev/null
+++ b/src/libvterm/src/encoding.c
@@ -0,0 +1,232 @@
+#include "vterm_internal.h"
+
+#define UNICODE_INVALID 0xFFFD
+
+#if defined(DEBUG) && DEBUG > 1
+# define DEBUG_PRINT_UTF8
+#endif
+
+struct UTF8DecoderData {
+  /* number of bytes remaining in this codepoint */
+  int bytes_remaining;
+
+  /* number of bytes total in this codepoint once it's finished
+     (for detecting overlongs) */
+  int bytes_total;
+
+  int this_cp;
+};
+
+static void init_utf8(VTermEncoding *enc UNUSED, void *data_)
+{
+  struct UTF8DecoderData *data = data_;
+
+  data->bytes_remaining = 0;
+  data->bytes_total     = 0;
+}
+
+static void decode_utf8(VTermEncoding *enc UNUSED, void *data_,
+                        uint32_t cp[], int *cpi, int cplen,
+                        const char bytes[], size_t *pos, size_t bytelen)
+{
+  struct UTF8DecoderData *data = data_;
+
+#ifdef DEBUG_PRINT_UTF8
+  printf("BEGIN UTF-8\n");
+#endif
+
+  for(; *pos < bytelen && *cpi < cplen; (*pos)++) {
+    unsigned char c = bytes[*pos];
+
+#ifdef DEBUG_PRINT_UTF8
+    printf(" pos=%zd c=%02x rem=%d\n", *pos, c, data->bytes_remaining);
+#endif
+
+    if(c < 0x20) /* C0 */
+      return;
+
+    else if(c >= 0x20 && c < 0x7f) {
+      if(data->bytes_remaining)
+        cp[(*cpi)++] = UNICODE_INVALID;
+
+      cp[(*cpi)++] = c;
+#ifdef DEBUG_PRINT_UTF8
+      printf(" UTF-8 char: U+%04x\n", c);
+#endif
+      data->bytes_remaining = 0;
+    }
+
+    else if(c == 0x7f) /* DEL */
+      return;
+
+    else if(c >= 0x80 && c < 0xc0) {
+      if(!data->bytes_remaining) {
+        cp[(*cpi)++] = UNICODE_INVALID;
+        continue;
+      }
+
+      data->this_cp <<= 6;
+      data->this_cp |= c & 0x3f;
+      data->bytes_remaining--;
+
+      if(!data->bytes_remaining) {
+#ifdef DEBUG_PRINT_UTF8
+        printf(" UTF-8 raw char U+%04x bytelen=%d ", data->this_cp, data->bytes_total);
+#endif
+        /* Check for overlong sequences */
+        switch(data->bytes_total) {
+        case 2:
+          if(data->this_cp <  0x0080) data->this_cp = UNICODE_INVALID;
+          break;
+        case 3:
+          if(data->this_cp <  0x0800) data->this_cp = UNICODE_INVALID;
+          break;
+        case 4:
+          if(data->this_cp < 0x10000) data->this_cp = UNICODE_INVALID;
+          break;
+        case 5:
+          if(data->this_cp < 0x200000) data->this_cp = UNICODE_INVALID;
+          break;
+        case 6:
+          if(data->this_cp < 0x4000000) data->this_cp = UNICODE_INVALID;
+          break;
+        }
+        /* Now look for plain invalid ones */
+        if((data->this_cp >= 0xD800 && data->this_cp <= 0xDFFF) ||
+           data->this_cp == 0xFFFE ||
+           data->this_cp == 0xFFFF)
+          data->this_cp = UNICODE_INVALID;
+#ifdef DEBUG_PRINT_UTF8
+        printf(" char: U+%04x\n", data->this_cp);
+#endif
+        cp[(*cpi)++] = data->this_cp;
+      }
+    }
+
+    else if(c >= 0xc0 && c < 0xe0) {
+      if(data->bytes_remaining)
+        cp[(*cpi)++] = UNICODE_INVALID;
+
+      data->this_cp = c & 0x1f;
+      data->bytes_total = 2;
+      data->bytes_remaining = 1;
+    }
+
+    else if(c >= 0xe0 && c < 0xf0) {
+      if(data->bytes_remaining)
+        cp[(*cpi)++] = UNICODE_INVALID;
+
+      data->this_cp = c & 0x0f;
+      data->bytes_total = 3;
+      data->bytes_remaining = 2;
+    }
+
+    else if(c >= 0xf0 && c < 0xf8) {
+      if(data->bytes_remaining)
+        cp[(*cpi)++] = UNICODE_INVALID;
+
+      data->this_cp = c & 0x07;
+      data->bytes_total = 4;
+      data->bytes_remaining = 3;
+    }
+
+    else if(c >= 0xf8 && c < 0xfc) {
+      if(data->bytes_remaining)
+        cp[(*cpi)++] = UNICODE_INVALID;
+
+      data->this_cp = c & 0x03;
+      data->bytes_total = 5;
+      data->bytes_remaining = 4;
+    }
+
+    else if(c >= 0xfc && c < 0xfe) {
+      if(data->bytes_remaining)
+        cp[(*cpi)++] = UNICODE_INVALID;
+
+      data->this_cp = c & 0x01;
+      data->bytes_total = 6;
+      data->bytes_remaining = 5;
+    }
+
+    else {
+      cp[(*cpi)++] = UNICODE_INVALID;
+    }
+  }
+}
+
+static VTermEncoding encoding_utf8 = {
+  &init_utf8,  /* init */
+  &decode_utf8 /* decode */
+};
+
+static void decode_usascii(VTermEncoding *enc UNUSED, void *data UNUSED,
+                           uint32_t cp[], int *cpi, int cplen,
+                           const char bytes[], size_t *pos, size_t bytelen)
+{
+  int is_gr = bytes[*pos] & 0x80;
+
+  for(; *pos < bytelen && *cpi < cplen; (*pos)++) {
+    unsigned char c = bytes[*pos] ^ is_gr;
+
+    if(c < 0x20 || c == 0x7f || c >= 0x80)
+      return;
+
+    cp[(*cpi)++] = c;
+  }
+}
+
+static VTermEncoding encoding_usascii = {
+  NULL,           /* init */
+  &decode_usascii /* decode */
+};
+
+struct StaticTableEncoding {
+  const VTermEncoding enc;
+  const uint32_t chars[128];
+};
+
+static void decode_table(VTermEncoding *enc, void *data UNUSED,
+                         uint32_t cp[], int *cpi, int cplen,
+                         const char bytes[], size_t *pos, size_t bytelen)
+{
+  struct StaticTableEncoding *table = (struct StaticTableEncoding *)enc;
+  int is_gr = bytes[*pos] & 0x80;
+
+  for(; *pos < bytelen && *cpi < cplen; (*pos)++) {
+    unsigned char c = bytes[*pos] ^ is_gr;
+
+    if(c < 0x20 || c == 0x7f || c >= 0x80)
+      return;
+
+    if(table->chars[c])
+      cp[(*cpi)++] = table->chars[c];
+    else
+      cp[(*cpi)++] = c;
+  }
+}
+
+#include "encoding/DECdrawing.inc"
+#include "encoding/uk.inc"
+
+static struct {
+  VTermEncodingType type;
+  char designation;
+  VTermEncoding *enc;
+}
+encodings[] = {
+  { ENC_UTF8,      'u', &encoding_utf8 },
+  { ENC_SINGLE_94, '0', (VTermEncoding*)&encoding_DECdrawing },
+  { ENC_SINGLE_94, 'A', (VTermEncoding*)&encoding_uk },
+  { ENC_SINGLE_94, 'B', &encoding_usascii },
+  { 0 },
+};
+
+/* This ought to be INTERNAL but isn't because it's used by unit testing */
+VTermEncoding *vterm_lookup_encoding(VTermEncodingType type, char designation)
+{
+  int i;
+  for(i = 0; encodings[i].designation; i++)
+    if(encodings[i].type == type && encodings[i].designation == designation)
+      return encodings[i].enc;
+  return NULL;
+}
new file mode 100644
--- /dev/null
+++ b/src/libvterm/src/encoding/DECdrawing.tbl
@@ -0,0 +1,31 @@
+6/0 = U+25C6 # BLACK DIAMOND
+6/1 = U+2592 # MEDIUM SHADE (checkerboard)
+6/2 = U+2409 # SYMBOL FOR HORIZONTAL TAB
+6/3 = U+240C # SYMBOL FOR FORM FEED
+6/4 = U+240D # SYMBOL FOR CARRIAGE RETURN
+6/5 = U+240A # SYMBOL FOR LINE FEED
+6/6 = U+00B0 # DEGREE SIGN
+6/7 = U+00B1 # PLUS-MINUS SIGN (plus or minus)
+6/8 = U+2424 # SYMBOL FOR NEW LINE
+6/9 = U+240B # SYMBOL FOR VERTICAL TAB
+6/10 = U+2518 # BOX DRAWINGS LIGHT UP AND LEFT (bottom-right corner)
+6/11 = U+2510 # BOX DRAWINGS LIGHT DOWN AND LEFT (top-right corner)
+6/12 = U+250C # BOX DRAWINGS LIGHT DOWN AND RIGHT (top-left corner)
+6/13 = U+2514 # BOX DRAWINGS LIGHT UP AND RIGHT (bottom-left corner)
+6/14 = U+253C # BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL (crossing lines)
+6/15 = U+23BA # HORIZONTAL SCAN LINE-1
+7/0 = U+23BB # HORIZONTAL SCAN LINE-3
+7/1 = U+2500 # BOX DRAWINGS LIGHT HORIZONTAL
+7/2 = U+23BC # HORIZONTAL SCAN LINE-7
+7/3 = U+23BD # HORIZONTAL SCAN LINE-9
+7/4 = U+251C # BOX DRAWINGS LIGHT VERTICAL AND RIGHT
+7/5 = U+2524 # BOX DRAWINGS LIGHT VERTICAL AND LEFT
+7/6 = U+2534 # BOX DRAWINGS LIGHT UP AND HORIZONTAL
+7/7 = U+252C # BOX DRAWINGS LIGHT DOWN AND HORIZONTAL
+7/8 = U+2502 # BOX DRAWINGS LIGHT VERTICAL
+7/9 = U+2A7D # LESS-THAN OR SLANTED EQUAL-TO
+7/10 = U+2A7E # GREATER-THAN OR SLANTED EQUAL-TO
+7/11 = U+03C0 # GREEK SMALL LETTER PI
+7/12 = U+2260 # NOT EQUAL TO
+7/13 = U+00A3 # POUND SIGN
+7/14 = U+00B7 # MIDDLE DOT
new file mode 100644
--- /dev/null
+++ b/src/libvterm/src/encoding/uk.tbl
@@ -0,0 +1,1 @@
+2/3 = "£"
new file mode 100644
--- /dev/null
+++ b/src/libvterm/src/keyboard.c
@@ -0,0 +1,228 @@
+#include "vterm_internal.h"
+
+#include <stdio.h>
+
+#include "utf8.h"
+
+void vterm_keyboard_unichar(VTerm *vt, uint32_t c, VTermModifier mod)
+{
+  int needs_CSIu;
+
+  /* The shift modifier is never important for Unicode characters
+   * apart from Space
+   */
+  if(c != ' ')
+    mod &= ~VTERM_MOD_SHIFT;
+
+  if(mod == 0) {
+    /* Normal text - ignore just shift */
+    char str[6];
+    int seqlen = fill_utf8(c, str);
+    vterm_push_output_bytes(vt, str, seqlen);
+    return;
+  }
+
+  switch(c) {
+    /* Special Ctrl- letters that can't be represented elsewise */
+    case 'i': case 'j': case 'm': case '[':
+      needs_CSIu = 1;
+      break;
+    /* Ctrl-\ ] ^ _ don't need CSUu */
+    case '\\': case ']': case '^': case '_':
+      needs_CSIu = 0;
+      break;
+    /* Shift-space needs CSIu */
+    case ' ':
+      needs_CSIu = !!(mod & VTERM_MOD_SHIFT);
+      break;
+    /* All other characters needs CSIu except for letters a-z */
+    default:
+      needs_CSIu = (c < 'a' || c > 'z');
+  }
+
+  /* ALT we can just prefix with ESC; anything else requires CSI u */
+  if(needs_CSIu && (mod & ~VTERM_MOD_ALT)) {
+    vterm_push_output_sprintf_ctrl(vt, C1_CSI, "%d;%du", c, mod+1);
+    return;
+  }
+
+  if(mod & VTERM_MOD_CTRL)
+    c &= 0x1f;
+
+  vterm_push_output_sprintf(vt, "%s%c", mod & VTERM_MOD_ALT ? ESC_S : "", c);
+}
+
+typedef struct {
+  enum {
+    KEYCODE_NONE,
+    KEYCODE_LITERAL,
+    KEYCODE_TAB,
+    KEYCODE_ENTER,
+    KEYCODE_SS3,
+    KEYCODE_CSI,
+    KEYCODE_CSI_CURSOR,
+    KEYCODE_CSINUM,
+    KEYCODE_KEYPAD
+  } type;
+  char literal;
+  int csinum;
+} keycodes_s;
+
+static keycodes_s keycodes[] = {
+  { KEYCODE_NONE,       0, 0 }, /* NONE */
+
+  { KEYCODE_ENTER,      '\r', 0 }, /* ENTER */
+  { KEYCODE_TAB,        '\t',  0 }, /* TAB */
+  { KEYCODE_LITERAL,    '\x7f', 0 }, /* BACKSPACE == ASCII DEL */
+  { KEYCODE_LITERAL,    '\x1b', 0 }, /* ESCAPE */
+
+  { KEYCODE_CSI_CURSOR, 'A', 0 }, /* UP */
+  { KEYCODE_CSI_CURSOR, 'B', 0 }, /* DOWN */
+  { KEYCODE_CSI_CURSOR, 'D', 0 }, /* LEFT */
+  { KEYCODE_CSI_CURSOR, 'C', 0 }, /* RIGHT */
+
+  { KEYCODE_CSINUM,     '~', 2 },  /* INS */
+  { KEYCODE_CSINUM,     '~', 3 },  /* DEL */
+  { KEYCODE_CSI_CURSOR, 'H', 0 }, /* HOME */
+  { KEYCODE_CSI_CURSOR, 'F', 0 }, /* END */
+  { KEYCODE_CSINUM,     '~', 5 },  /* PAGEUP */
+  { KEYCODE_CSINUM,     '~', 6 },  /* PAGEDOWN */
+};
+
+static keycodes_s keycodes_fn[] = {
+  { KEYCODE_NONE,       0, 0 },   /* F0 - shouldn't happen */
+  { KEYCODE_CSI_CURSOR, 'P', 0 }, /* F1 */
+  { KEYCODE_CSI_CURSOR, 'Q', 0 }, /* F2 */
+  { KEYCODE_CSI_CURSOR, 'R', 0 }, /* F3 */
+  { KEYCODE_CSI_CURSOR, 'S', 0 }, /* F4 */
+  { KEYCODE_CSINUM,     '~', 15 }, /* F5 */
+  { KEYCODE_CSINUM,     '~', 17 }, /* F6 */
+  { KEYCODE_CSINUM,     '~', 18 }, /* F7 */
+  { KEYCODE_CSINUM,     '~', 19 }, /* F8 */
+  { KEYCODE_CSINUM,     '~', 20 }, /* F9 */
+  { KEYCODE_CSINUM,     '~', 21 }, /* F10 */
+  { KEYCODE_CSINUM,     '~', 23 }, /* F11 */
+  { KEYCODE_CSINUM,     '~', 24 }, /* F12 */
+};
+
+static keycodes_s keycodes_kp[] = {
+  { KEYCODE_KEYPAD, '0', 'p' }, /* KP_0 */
+  { KEYCODE_KEYPAD, '1', 'q' }, /* KP_1 */
+  { KEYCODE_KEYPAD, '2', 'r' }, /* KP_2 */
+  { KEYCODE_KEYPAD, '3', 's' }, /* KP_3 */
+  { KEYCODE_KEYPAD, '4', 't' }, /* KP_4 */
+  { KEYCODE_KEYPAD, '5', 'u' }, /* KP_5 */
+  { KEYCODE_KEYPAD, '6', 'v' }, /* KP_6 */
+  { KEYCODE_KEYPAD, '7', 'w' }, /* KP_7 */
+  { KEYCODE_KEYPAD, '8', 'x' }, /* KP_8 */
+  { KEYCODE_KEYPAD, '9', 'y' }, /* KP_9 */
+  { KEYCODE_KEYPAD, '*', 'j' }, /* KP_MULT */
+  { KEYCODE_KEYPAD, '+', 'k' }, /* KP_PLUS */
+  { KEYCODE_KEYPAD, ',', 'l' }, /* KP_COMMA */
+  { KEYCODE_KEYPAD, '-', 'm' }, /* KP_MINUS */
+  { KEYCODE_KEYPAD, '.', 'n' }, /* KP_PERIOD */
+  { KEYCODE_KEYPAD, '/', 'o' }, /* KP_DIVIDE */
+  { KEYCODE_KEYPAD, '\n', 'M' }, /* KP_ENTER */
+  { KEYCODE_KEYPAD, '=', 'X' }, /* KP_EQUAL */
+};
+
+void vterm_keyboard_key(VTerm *vt, VTermKey key, VTermModifier mod)
+{
+  keycodes_s k;
+
+  if(key == VTERM_KEY_NONE)
+    return;
+
+  if(key < VTERM_KEY_FUNCTION_0) {
+    if(key >= sizeof(keycodes)/sizeof(keycodes[0]))
+      return;
+    k = keycodes[key];
+  }
+  else if(key >= VTERM_KEY_FUNCTION_0 && key <= VTERM_KEY_FUNCTION_MAX) {
+    if((key - VTERM_KEY_FUNCTION_0) >= sizeof(keycodes_fn)/sizeof(keycodes_fn[0]))
+      return;
+    k = keycodes_fn[key - VTERM_KEY_FUNCTION_0];
+  }
+  else if(key >= VTERM_KEY_KP_0) {
+    if((key - VTERM_KEY_KP_0) >= sizeof(keycodes_kp)/sizeof(keycodes_kp[0]))
+      return;
+    k = keycodes_kp[key - VTERM_KEY_KP_0];
+  }
+
+  switch(k.type) {
+  case KEYCODE_NONE:
+    break;
+
+  case KEYCODE_TAB:
+    /* Shift-Tab is CSI Z but plain Tab is 0x09 */
+    if(mod == VTERM_MOD_SHIFT)
+      vterm_push_output_sprintf_ctrl(vt, C1_CSI, "Z");
+    else if(mod & VTERM_MOD_SHIFT)
+      vterm_push_output_sprintf_ctrl(vt, C1_CSI, "1;%dZ", mod+1);
+    else
+      goto case_LITERAL;
+    break;
+
+  case KEYCODE_ENTER:
+    /* Enter is CRLF in newline mode, but just LF in linefeed */
+    if(vt->state->mode.newline)
+      vterm_push_output_sprintf(vt, "\r\n");
+    else
+      goto case_LITERAL;
+    break;
+
+  case KEYCODE_LITERAL: case_LITERAL:
+    if(mod & (VTERM_MOD_SHIFT|VTERM_MOD_CTRL))
+      vterm_push_output_sprintf_ctrl(vt, C1_CSI, "%d;%du", k.literal, mod+1);
+    else
+      vterm_push_output_sprintf(vt, mod & VTERM_MOD_ALT ? ESC_S "%c" : "%c", k.literal);
+    break;
+
+  case KEYCODE_SS3: case_SS3:
+    if(mod == 0)
+      vterm_push_output_sprintf_ctrl(vt, C1_SS3, "%c", k.literal);
+    else
+      goto case_CSI;
+    break;
+
+  case KEYCODE_CSI: case_CSI:
+    if(mod == 0)
+      vterm_push_output_sprintf_ctrl(vt, C1_CSI, "%c", k.literal);
+    else
+      vterm_push_output_sprintf_ctrl(vt, C1_CSI, "1;%d%c", mod + 1, k.literal);
+    break;
+
+  case KEYCODE_CSINUM:
+    if(mod == 0)
+      vterm_push_output_sprintf_ctrl(vt, C1_CSI, "%d%c", k.csinum, k.literal);
+    else
+      vterm_push_output_sprintf_ctrl(vt, C1_CSI, "%d;%d%c", k.csinum, mod + 1, k.literal);
+    break;
+
+  case KEYCODE_CSI_CURSOR:
+    if(vt->state->mode.cursor)
+      goto case_SS3;
+    else
+      goto case_CSI;
+
+  case KEYCODE_KEYPAD:
+    if(vt->state->mode.keypad) {
+      k.literal = k.csinum;
+      goto case_SS3;
+    }
+    else
+      goto case_LITERAL;
+  }
+}
+
+void vterm_keyboard_start_paste(VTerm *vt)
+{
+  if(vt->state->mode.bracketpaste)
+    vterm_push_output_sprintf_ctrl(vt, C1_CSI, "200~");
+}
+
+void vterm_keyboard_end_paste(VTerm *vt)
+{
+  if(vt->state->mode.bracketpaste)
+    vterm_push_output_sprintf_ctrl(vt, C1_CSI, "201~");
+}
new file mode 100644
--- /dev/null
+++ b/src/libvterm/src/mouse.c
@@ -0,0 +1,96 @@
+#include "vterm_internal.h"
+
+#include "utf8.h"
+
+static void output_mouse(VTermState *state, int code, int pressed, int modifiers, int col, int row)
+{
+  modifiers <<= 2;
+
+  switch(state->mouse_protocol) {
+  case MOUSE_X10:
+    if(col + 0x21 > 0xff)
+      col = 0xff - 0x21;
+    if(row + 0x21 > 0xff)
+      row = 0xff - 0x21;
+
+    if(!pressed)
+      code = 3;
+
+    vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "M%c%c%c",
+        (code | modifiers) + 0x20, col + 0x21, row + 0x21);
+    break;
+
+  case MOUSE_UTF8:
+    {
+      char utf8[18]; size_t len = 0;
+
+      if(!pressed)
+        code = 3;
+
+      len += fill_utf8((code | modifiers) + 0x20, utf8 + len);
+      len += fill_utf8(col + 0x21, utf8 + len);
+      len += fill_utf8(row + 0x21, utf8 + len);
+      utf8[len] = 0;
+
+      vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "M%s", utf8);
+    }
+    break;
+
+  case MOUSE_SGR:
+    vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "<%d;%d;%d%c",
+        code | modifiers, col + 1, row + 1, pressed ? 'M' : 'm');
+    break;
+
+  case MOUSE_RXVT:
+    if(!pressed)
+      code = 3;
+
+    vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "%d;%d;%dM",
+        code | modifiers, col + 1, row + 1);
+    break;
+  }
+}
+
+void vterm_mouse_move(VTerm *vt, int row, int col, VTermModifier mod)
+{
+  VTermState *state = vt->state;
+
+  if(col == state->mouse_col && row == state->mouse_row)
+    return;
+
+  state->mouse_col = col;
+  state->mouse_row = row;
+
+  if((state->mouse_flags & MOUSE_WANT_DRAG && state->mouse_buttons) ||
+     (state->mouse_flags & MOUSE_WANT_MOVE)) {
+    int button = state->mouse_buttons & 0x01 ? 1 :
+                 state->mouse_buttons & 0x02 ? 2 :
+                 state->mouse_buttons & 0x04 ? 3 : 4;
+    output_mouse(state, button-1 + 0x20, 1, mod, col, row);
+  }
+}
+
+void vterm_mouse_button(VTerm *vt, int button, bool pressed, VTermModifier mod)
+{
+  VTermState *state = vt->state;
+
+  int old_buttons = state->mouse_buttons;
+
+  if(button > 0 && button <= 3) {
+    if(pressed)
+      state->mouse_buttons |= (1 << (button-1));
+    else
+      state->mouse_buttons &= ~(1 << (button-1));
+  }
+
+  /* Most of the time we don't get button releases from 4/5 */
+  if(state->mouse_buttons == old_buttons && button < 4)
+    return;
+
+  if(button < 4) {
+    output_mouse(state, button-1, pressed, mod, state->mouse_col, state->mouse_row);
+  }
+  else if(button < 6) {
+    output_mouse(state, button-4 + 0x40, pressed, mod, state->mouse_col, state->mouse_row);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/src/libvterm/src/parser.c
@@ -0,0 +1,346 @@
+#include "vterm_internal.h"
+
+#include <stdio.h>
+#include <string.h>
+
+#define CSI_ARGS_MAX 16
+#define CSI_LEADER_MAX 16
+#define CSI_INTERMED_MAX 16
+
+static void do_control(VTerm *vt, unsigned char control)
+{
+  if(vt->parser_callbacks && vt->parser_callbacks->control)
+    if((*vt->parser_callbacks->control)(control, vt->cbdata))
+      return;
+
+  DEBUG_LOG1("libvterm: Unhandled control 0x%02x\n", control);
+}
+
+static void do_string_csi(VTerm *vt, const char *args, size_t arglen, char command)
+{
+  int i = 0;
+
+  int leaderlen = 0;
+  char leader[CSI_LEADER_MAX];
+  int argcount = 1; /* Always at least 1 arg */
+  long csi_args[CSI_ARGS_MAX];
+  int argi;
+  int intermedlen = 0;
+  char intermed[CSI_INTERMED_MAX];
+
+  /* Extract leader bytes 0x3c to 0x3f */
+  for( ; i < (int)arglen; i++) {
+    if(args[i] < 0x3c || args[i] > 0x3f)
+      break;
+    if(leaderlen < CSI_LEADER_MAX-1)
+      leader[leaderlen++] = args[i];
+  }
+
+  leader[leaderlen] = 0;
+
+  for( ; i < (int)arglen; i++)
+    if(args[i] == 0x3b || args[i] == 0x3a) /* ; or : */
+      argcount++;
+
+  /* TODO: Consider if these buffers should live in the VTerm struct itself */
+  if(argcount > CSI_ARGS_MAX)
+    argcount = CSI_ARGS_MAX;
+
+  for(argi = 0; argi < argcount; argi++)
+    csi_args[argi] = CSI_ARG_MISSING;
+
+  argi = 0;
+  for(i = leaderlen; i < (int)arglen && argi < argcount; i++) {
+    switch(args[i]) {
+    case 0x30: case 0x31: case 0x32: case 0x33: case 0x34:
+    case 0x35: case 0x36: case 0x37: case 0x38: case 0x39:
+      if(csi_args[argi] == CSI_ARG_MISSING)
+        csi_args[argi] = 0;
+      csi_args[argi] *= 10;
+      csi_args[argi] += args[i] - '0';
+      break;
+    case 0x3a:
+      csi_args[argi] |= CSI_ARG_FLAG_MORE;
+      /* FALLTHROUGH */
+    case 0x3b:
+      argi++;
+      break;
+    default:
+      goto done_leader;
+    }
+  }
+done_leader: ;
+
+  for( ; i < (int)arglen; i++) {
+    if((args[i] & 0xf0) != 0x20)
+      break;
+
+    if(intermedlen < CSI_INTERMED_MAX-1)
+      intermed[intermedlen++] = args[i];
+  }
+
+  intermed[intermedlen] = 0;
+
+  if(i < (int)arglen) {
+    DEBUG_LOG2("libvterm: TODO unhandled CSI bytes \"%.*s\"\n", (int)(arglen - i), args + i);
+  }
+
+#if 0
+  printf("Parsed CSI args %.*s as:\n", arglen, args);
+  printf(" leader: %s\n", leader);
+  for(argi = 0; argi < argcount; argi++) {
+    printf(" %lu", CSI_ARG(csi_args[argi]));
+    if(!CSI_ARG_HAS_MORE(csi_args[argi]))
+      printf("\n");
+  printf(" intermed: %s\n", intermed);
+  }
+#endif
+
+  if(vt->parser_callbacks && vt->parser_callbacks->csi)
+    if((*vt->parser_callbacks->csi)(leaderlen ? leader : NULL, csi_args, argcount, intermedlen ? intermed : NULL, command, vt->cbdata))
+      return;
+
+  DEBUG_LOG3("libvterm: Unhandled CSI %.*s %c\n", (int)arglen, args, command);
+}
+
+static void append_strbuffer(VTerm *vt, const char *str, size_t len)
+{
+  if(len > vt->strbuffer_len - vt->strbuffer_cur) {
+    len = vt->strbuffer_len - vt->strbuffer_cur;
+    DEBUG_LOG1("Truncating strbuffer preserve to %zd bytes\n", len);
+  }
+
+  if(len > 0) {
+    strncpy(vt->strbuffer + vt->strbuffer_cur, str, len);
+    vt->strbuffer_cur += len;
+  }
+}
+
+static size_t do_string(VTerm *vt, const char *str_frag, size_t len)
+{
+  size_t eaten;
+
+  if(vt->strbuffer_cur) {
+    if(str_frag)
+      append_strbuffer(vt, str_frag, len);
+
+    str_frag = vt->strbuffer;
+    len = vt->strbuffer_cur;
+  }
+  else if(!str_frag) {
+    DEBUG_LOG("parser.c: TODO: No strbuffer _and_ no final fragment???\n");
+    len = 0;
+  }
+
+  vt->strbuffer_cur = 0;
+
+  switch(vt->parser_state) {
+  case NORMAL:
+    if(vt->parser_callbacks && vt->parser_callbacks->text)
+      if((eaten = (*vt->parser_callbacks->text)(str_frag, len, vt->cbdata)))
+        return eaten;
+
+    DEBUG_LOG1("libvterm: Unhandled text (%zu chars)\n", len);
+    return 0;
+
+  case ESC:
+    if(len == 1 && str_frag[0] >= 0x40 && str_frag[0] < 0x60) {
+      /* C1 emulations using 7bit clean */
+      /* ESC 0x40 == 0x80 */
+      do_control(vt, str_frag[0] + 0x40);
+      return 0;
+    }
+
+    if(vt->parser_callbacks && vt->parser_callbacks->escape)
+      if((*vt->parser_callbacks->escape)(str_frag, len, vt->cbdata))
+        return 0;
+
+    DEBUG_LOG1("libvterm: Unhandled escape ESC 0x%02x\n", str_frag[len-1]);
+    return 0;
+
+  case CSI:
+    do_string_csi(vt, str_frag, len - 1, str_frag[len - 1]);
+    return 0;
+
+  case OSC:
+    if(vt->parser_callbacks && vt->parser_callbacks->osc)
+      if((*vt->parser_callbacks->osc)(str_frag, len, vt->cbdata))
+        return 0;
+
+    DEBUG_LOG2("libvterm: Unhandled OSC %.*s\n", (int)len, str_frag);
+    return 0;
+
+  case DCS:
+    if(vt->parser_callbacks && vt->parser_callbacks->dcs)
+      if((*vt->parser_callbacks->dcs)(str_frag, len, vt->cbdata))
+        return 0;
+
+    DEBUG_LOG2("libvterm: Unhandled DCS %.*s\n", (int)len, str_frag);
+    return 0;
+
+  case ESC_IN_OSC:
+  case ESC_IN_DCS:
+    DEBUG_LOG("libvterm: ARGH! Should never do_string() in ESC_IN_{OSC,DCS}\n");
+    return 0;
+  }
+
+  return 0;
+}
+
+size_t vterm_input_write(VTerm *vt, const char *bytes, size_t len)
+{
+  size_t pos = 0;
+  const char *string_start;
+
+  switch(vt->parser_state) {
+  case NORMAL:
+    string_start = NULL;
+    break;
+  case ESC:
+  case ESC_IN_OSC:
+  case ESC_IN_DCS:
+  case CSI:
+  case OSC:
+  case DCS:
+    string_start = bytes;
+    break;
+  }
+
+#define ENTER_STRING_STATE(st) do { vt->parser_state = st; string_start = bytes + pos + 1; } while(0)
+#define ENTER_NORMAL_STATE()   do { vt->parser_state = NORMAL; string_start = NULL; } while(0)
+
+  for( ; pos < len; pos++) {
+    unsigned char c = bytes[pos];
+
+    if(c == 0x00 || c == 0x7f) { /* NUL, DEL */
+      if(vt->parser_state != NORMAL) {
+        append_strbuffer(vt, string_start, bytes + pos - string_start);
+        string_start = bytes + pos + 1;
+      }
+      continue;
+    }
+    if(c == 0x18 || c == 0x1a) { /* CAN, SUB */
+      ENTER_NORMAL_STATE();
+      continue;
+    }
+    else if(c == 0x1b) { /* ESC */
+      if(vt->parser_state == OSC)
+        vt->parser_state = ESC_IN_OSC;
+      else if(vt->parser_state == DCS)
+        vt->parser_state = ESC_IN_DCS;
+      else
+        ENTER_STRING_STATE(ESC);
+      continue;
+    }
+    else if(c == 0x07 &&  /* BEL, can stand for ST in OSC or DCS state */
+            (vt->parser_state == OSC || vt->parser_state == DCS)) {
+      /* fallthrough */
+    }
+    else if(c < 0x20) { /* other C0 */
+      if(vt->parser_state != NORMAL)
+        append_strbuffer(vt, string_start, bytes + pos - string_start);
+      do_control(vt, c);
+      if(vt->parser_state != NORMAL)
+        string_start = bytes + pos + 1;
+      continue;
+    }
+    /* else fallthrough */
+
+    switch(vt->parser_state) {
+    case ESC_IN_OSC:
+    case ESC_IN_DCS:
+      if(c == 0x5c) { /* ST */
+        switch(vt->parser_state) {
+          case ESC_IN_OSC: vt->parser_state = OSC; break;
+          case ESC_IN_DCS: vt->parser_state = DCS; break;
+          default: break;
+        }
+        do_string(vt, string_start, bytes + pos - string_start - 1);
+        ENTER_NORMAL_STATE();
+        break;
+      }
+      vt->parser_state = ESC;
+      string_start = bytes + pos;
+      /* else fallthrough */
+
+    case ESC:
+      switch(c) {
+      case 0x50: /* DCS */
+        ENTER_STRING_STATE(DCS);
+        break;
+      case 0x5b: /* CSI */
+        ENTER_STRING_STATE(CSI);
+        break;
+      case 0x5d: /* OSC */
+        ENTER_STRING_STATE(OSC);
+        break;
+      default:
+        if(c >= 0x30 && c < 0x7f) {
+          /* +1 to pos because we want to include this command byte as well */
+          do_string(vt, string_start, bytes + pos - string_start + 1);
+          ENTER_NORMAL_STATE();
+        }
+        else if(c >= 0x20 && c < 0x30) {
+          /* intermediate byte */
+        }
+        else {
+          DEBUG_LOG1("TODO: Unhandled byte %02x in Escape\n", c);
+        }
+      }
+      break;
+
+    case CSI:
+      if(c >= 0x40 && c <= 0x7f) {
+        /* +1 to pos because we want to include this command byte as well */
+        do_string(vt, string_start, bytes + pos - string_start + 1);
+        ENTER_NORMAL_STATE();
+      }
+      break;
+
+    case OSC:
+    case DCS:
+      if(c == 0x07 || (c == 0x9c && !vt->mode.utf8)) {
+        do_string(vt, string_start, bytes + pos - string_start);
+        ENTER_NORMAL_STATE();
+      }
+      break;
+
+    case NORMAL:
+      if(c >= 0x80 && c < 0xa0 && !vt->mode.utf8) {
+        switch(c) {
+        case 0x90: /* DCS */
+          ENTER_STRING_STATE(DCS);
+          break;
+        case 0x9b: /* CSI */
+          ENTER_STRING_STATE(CSI);
+          break;
+        case 0x9d: /* OSC */
+          ENTER_STRING_STATE(OSC);
+          break;
+        default:
+          do_control(vt, c);
+          break;
+        }
+      }
+      else {
+        size_t text_eaten = do_string(vt, bytes + pos, len - pos);
+
+        if(text_eaten == 0) {
+          string_start = bytes + pos;
+          goto pause;
+        }
+
+        pos += (text_eaten - 1); /* we'll ++ it again in a moment */
+      }
+      break;
+    }
+  }
+
+pause:
+  if(string_start && string_start < len + bytes) {
+    size_t remaining = len - (string_start - bytes);
+    append_strbuffer(vt, string_start, remaining);
+  }
+
+  return len;
+}
new file mode 100644
--- /dev/null
+++ b/src/libvterm/src/pen.c
@@ -0,0 +1,504 @@
+#include "vterm_internal.h"
+
+#include <stdio.h>
+
+static const VTermColor ansi_colors[] = {
+  /* R    G    B */
+  {   0,   0,   0 }, /* black */
+  { 224,   0,   0 }, /* red */
+  {   0, 224,   0 }, /* green */
+  { 224, 224,   0 }, /* yellow */
+  {   0,   0, 224 }, /* blue */
+  { 224,   0, 224 }, /* magenta */
+  {   0, 224, 224 }, /* cyan */
+  { 224, 224, 224 }, /* white == light grey */
+
+  /* high intensity */
+  { 128, 128, 128 }, /* black */
+  { 255,  64,  64 }, /* red */
+  {  64, 255,  64 }, /* green */
+  { 255, 255,  64 }, /* yellow */
+  {  64,  64, 255 }, /* blue */
+  { 255,  64, 255 }, /* magenta */
+  {  64, 255, 255 }, /* cyan */
+  { 255, 255, 255 }, /* white for real */
+};
+
+static int ramp6[] = {
+  0x00, 0x33, 0x66, 0x99, 0xCC, 0xFF,
+};
+
+static int ramp24[] = {
+  0x00, 0x0B, 0x16, 0x21, 0x2C, 0x37, 0x42, 0x4D, 0x58, 0x63, 0x6E, 0x79,
+  0x85, 0x90, 0x9B, 0xA6, 0xB1, 0xBC, 0xC7, 0xD2, 0xDD, 0xE8, 0xF3, 0xFF,
+};
+
+static bool lookup_colour_ansi(const VTermState *state, long index, VTermColor *col)
+{
+  if(index >= 0 && index < 16) {
+    *col = state->colors[index];
+    return true;
+  }
+
+  return false;
+}
+
+static bool lookup_colour_palette(const VTermState *state, long index, VTermColor *col)
+{
+  if(index >= 0 && index < 16) {
+    /* Normal 8 colours or high intensity - parse as palette 0 */
+    return lookup_colour_ansi(state, index, col);
+  }
+  else if(index >= 16 && index < 232) {
+    /* 216-colour cube */
+    index -= 16;
+
+    col->blue  = ramp6[index     % 6];
+    col->green = ramp6[index/6   % 6];
+    col->red   = ramp6[index/6/6 % 6];
+
+    return true;
+  }
+  else if(index >= 232 && index < 256) {
+    /* 24 greyscales */
+    index -= 232;
+
+    col->blue  = ramp24[index];
+    col->green = ramp24[index];
+    col->red   = ramp24[index];
+
+    return true;
+  }
+
+  return false;
+}
+
+static int lookup_colour(const VTermState *state, int palette, const long args[], int argcount, VTermColor *col, int *index)
+{
+  switch(palette) {
+  case 2: /* RGB mode - 3 args contain colour values directly */
+    if(argcount < 3)
+      return argcount;
+
+    col->red   = CSI_ARG(args[0]);
+    col->green = CSI_ARG(args[1]);
+    col->blue  = CSI_ARG(args[2]);
+
+    return 3;
+
+  case 5: /* XTerm 256-colour mode */
+    if(index)
+      *index = CSI_ARG_OR(args[0], -1);
+
+    lookup_colour_palette(state, argcount ? CSI_ARG_OR(args[0], -1) : -1, col);
+
+    return argcount ? 1 : 0;
+
+  default:
+    DEBUG_LOG1("Unrecognised colour palette %d\n", palette);
+    return 0;
+  }
+}
+
+/* Some conveniences */
+
+static void setpenattr(VTermState *state, VTermAttr attr, VTermValueType type UNUSED, VTermValue *val)
+{
+#ifdef DEBUG
+  if(type != vterm_get_attr_type(attr)) {
+    DEBUG_LOG("Cannot set attr %d as it has type %d, not type %d\n",
+        attr, vterm_get_attr_type(attr), type);
+    return;
+  }
+#endif
+  if(state->callbacks && state->callbacks->setpenattr)
+    (*state->callbacks->setpenattr)(attr, val, state->cbdata);
+}
+
+static void setpenattr_bool(VTermState *state, VTermAttr attr, int boolean)
+{
+  VTermValue val;
+  val.boolean = boolean;
+  setpenattr(state, attr, VTERM_VALUETYPE_BOOL, &val);
+}
+
+static void setpenattr_int(VTermState *state, VTermAttr attr, int number)
+{
+  VTermValue val;
+  val.number = number;
+  setpenattr(state, attr, VTERM_VALUETYPE_INT, &val);
+}
+
+static void setpenattr_col(VTermState *state, VTermAttr attr, VTermColor color)
+{
+  VTermValue val;
+  val.color = color;
+  setpenattr(state, attr, VTERM_VALUETYPE_COLOR, &val);
+}
+
+static void set_pen_col_ansi(VTermState *state, VTermAttr attr, long col)
+{
+  VTermColor *colp = (attr == VTERM_ATTR_BACKGROUND) ? &state->pen.bg : &state->pen.fg;
+
+  lookup_colour_ansi(state, col, colp);
+
+  setpenattr_col(state, attr, *colp);
+}
+
+INTERNAL void vterm_state_newpen(VTermState *state)
+{
+  int col;
+
+  /* 90% grey so that pure white is brighter */
+  state->default_fg.red = state->default_fg.green = state->default_fg.blue = 240;
+  state->default_bg.red = state->default_bg.green = state->default_bg.blue = 0;
+
+  for(col = 0; col < 16; col++)
+    state->colors[col] = ansi_colors[col];
+}
+
+INTERNAL void vterm_state_resetpen(VTermState *state)
+{
+  state->pen.bold = 0;      setpenattr_bool(state, VTERM_ATTR_BOLD, 0);
+  state->pen.underline = 0; setpenattr_int( state, VTERM_ATTR_UNDERLINE, 0);
+  state->pen.italic = 0;    setpenattr_bool(state, VTERM_ATTR_ITALIC, 0);
+  state->pen.blink = 0;     setpenattr_bool(state, VTERM_ATTR_BLINK, 0);
+  state->pen.reverse = 0;   setpenattr_bool(state, VTERM_ATTR_REVERSE, 0);
+  state->pen.strike = 0;    setpenattr_bool(state, VTERM_ATTR_STRIKE, 0);
+  state->pen.font = 0;      setpenattr_int( state, VTERM_ATTR_FONT, 0);
+
+  state->fg_index = -1;
+  state->bg_index = -1;
+  state->pen.fg = state->default_fg;  setpenattr_col(state, VTERM_ATTR_FOREGROUND, state->default_fg);
+  state->pen.bg = state->default_bg;  setpenattr_col(state, VTERM_ATTR_BACKGROUND, state->default_bg);
+}
+
+INTERNAL void vterm_state_savepen(VTermState *state, int save)
+{
+  if(save) {
+    state->saved.pen = state->pen;
+  }
+  else {
+    state->pen = state->saved.pen;
+
+    setpenattr_bool(state, VTERM_ATTR_BOLD,       state->pen.bold);
+    setpenattr_int( state, VTERM_ATTR_UNDERLINE,  state->pen.underline);
+    setpenattr_bool(state, VTERM_ATTR_ITALIC,     state->pen.italic);
+    setpenattr_bool(state, VTERM_ATTR_BLINK,      state->pen.blink);
+    setpenattr_bool(state, VTERM_ATTR_REVERSE,    state->pen.reverse);
+    setpenattr_bool(state, VTERM_ATTR_STRIKE,     state->pen.strike);
+    setpenattr_int( state, VTERM_ATTR_FONT,       state->pen.font);
+    setpenattr_col( state, VTERM_ATTR_FOREGROUND, state->pen.fg);
+    setpenattr_col( state, VTERM_ATTR_BACKGROUND, state->pen.bg);
+  }
+}
+
+void vterm_state_get_default_colors(const VTermState *state, VTermColor *default_fg, VTermColor *default_bg)
+{
+  *default_fg = state->default_fg;
+  *default_bg = state->default_bg;
+}
+
+void vterm_state_get_palette_color(const VTermState *state, int index, VTermColor *col)
+{
+  lookup_colour_palette(state, index, col);
+}
+
+void vterm_state_set_default_colors(VTermState *state, const VTermColor *default_fg, const VTermColor *default_bg)
+{
+  state->default_fg = *default_fg;
+  state->default_bg = *default_bg;
+}
+
+void vterm_state_set_palette_color(VTermState *state, int index, const VTermColor *col)
+{
+  if(index >= 0 && index < 16)
+    state->colors[index] = *col;
+}
+
+void vterm_state_set_bold_highbright(VTermState *state, int bold_is_highbright)
+{
+  state->bold_is_highbright = bold_is_highbright;
+}
+
+INTERNAL void vterm_state_setpen(VTermState *state, const long args[], int argcount)
+{
+  /* SGR - ECMA-48 8.3.117 */
+
+  int argi = 0;
+  int value;
+
+  while(argi < argcount) {
+    /* This logic is easier to do 'done' backwards; set it true, and make it
+       false again in the 'default' case */
+    int done = 1;
+
+    long arg;
+    switch(arg = CSI_ARG(args[argi])) {
+    case CSI_ARG_MISSING:
+    case 0: /* Reset */
+      vterm_state_resetpen(state);
+      break;
+
+    case 1: /* Bold on */
+      state->pen.bold = 1;
+      setpenattr_bool(state, VTERM_ATTR_BOLD, 1);
+      if(state->fg_index > -1 && state->fg_index < 8 && state->bold_is_highbright)
+        set_pen_col_ansi(state, VTERM_ATTR_FOREGROUND, state->fg_index + (state->pen.bold ? 8 : 0));
+      break;
+
+    case 3: /* Italic on */
+      state->pen.italic = 1;
+      setpenattr_bool(state, VTERM_ATTR_ITALIC, 1);
+      break;
+
+    case 4: /* Underline single */
+      state->pen.underline = 1;
+      setpenattr_int(state, VTERM_ATTR_UNDERLINE, 1);
+      break;
+
+    case 5: /* Blink */
+      state->pen.blink = 1;
+      setpenattr_bool(state, VTERM_ATTR_BLINK, 1);
+      break;
+
+    case 7: /* Reverse on */
+      state->pen.reverse = 1;
+      setpenattr_bool(state, VTERM_ATTR_REVERSE, 1);
+      break;
+
+    case 9: /* Strikethrough on */
+      state->pen.strike = 1;
+      setpenattr_bool(state, VTERM_ATTR_STRIKE, 1);
+      break;
+
+    case 10: case 11: case 12: case 13: case 14:
+    case 15: case 16: case 17: case 18: case 19: /* Select font */
+      state->pen.font = CSI_ARG(args[argi]) - 10;
+      setpenattr_int(state, VTERM_ATTR_FONT, state->pen.font);
+      break;
+
+    case 21: /* Underline double */
+      state->pen.underline = 2;
+      setpenattr_int(state, VTERM_ATTR_UNDERLINE, 2);
+      break;
+
+    case 22: /* Bold off */
+      state->pen.bold = 0;
+      setpenattr_bool(state, VTERM_ATTR_BOLD, 0);
+      break;
+
+    case 23: /* Italic and Gothic (currently unsupported) off */
+      state->pen.italic = 0;
+      setpenattr_bool(state, VTERM_ATTR_ITALIC, 0);
+      break;
+
+    case 24: /* Underline off */
+      state->pen.underline = 0;
+      setpenattr_int(state, VTERM_ATTR_UNDERLINE, 0);
+      break;
+
+    case 25: /* Blink off */
+      state->pen.blink = 0;
+      setpenattr_bool(state, VTERM_ATTR_BLINK, 0);
+      break;
+
+    case 27: /* Reverse off */
+      state->pen.reverse = 0;
+      setpenattr_bool(state, VTERM_ATTR_REVERSE, 0);
+      break;
+
+    case 29: /* Strikethrough off */
+      state->pen.strike = 0;
+      setpenattr_bool(state, VTERM_ATTR_STRIKE, 0);
+      break;
+
+    case 30: case 31: case 32: case 33:
+    case 34: case 35: case 36: case 37: /* Foreground colour palette */
+      value = CSI_ARG(args[argi]) - 30;
+      state->fg_index = value;
+      if(state->pen.bold && state->bold_is_highbright)
+        value += 8;
+      set_pen_col_ansi(state, VTERM_ATTR_FOREGROUND, value);
+      break;
+
+    case 38: /* Foreground colour alternative palette */
+      state->fg_index = -1;
+      if(argcount - argi < 1)
+        return;
+      argi += 1 + lookup_colour(state, CSI_ARG(args[argi+1]), args+argi+2, argcount-argi-2, &state->pen.fg, &state->fg_index);
+      setpenattr_col(state, VTERM_ATTR_FOREGROUND, state->pen.fg);
+      break;
+
+    case 39: /* Foreground colour default */
+      state->fg_index = -1;
+      state->pen.fg = state->default_fg;
+      setpenattr_col(state, VTERM_ATTR_FOREGROUND, state->pen.fg);
+      break;
+
+    case 40: case 41: case 42: case 43:
+    case 44: case 45: case 46: case 47: /* Background colour palette */
+      value = CSI_ARG(args[argi]) - 40;
+      state->bg_index = value;
+      set_pen_col_ansi(state, VTERM_ATTR_BACKGROUND, value);
+      break;
+
+    case 48: /* Background colour alternative palette */
+      state->bg_index = -1;
+      if(argcount - argi < 1)
+        return;
+      argi += 1 + lookup_colour(state, CSI_ARG(args[argi+1]), args+argi+2, argcount-argi-2, &state->pen.bg, &state->bg_index);
+      setpenattr_col(state, VTERM_ATTR_BACKGROUND, state->pen.bg);
+      break;
+
+    case 49: /* Default background */
+      state->bg_index = -1;
+      state->pen.bg = state->default_bg;
+      setpenattr_col(state, VTERM_ATTR_BACKGROUND, state->pen.bg);
+      break;
+
+    case 90: case 91: case 92: case 93:
+    case 94: case 95: case 96: case 97: /* Foreground colour high-intensity palette */
+      value = CSI_ARG(args[argi]) - 90 + 8;
+      state->fg_index = value;
+      set_pen_col_ansi(state, VTERM_ATTR_FOREGROUND, value);
+      break;
+
+    case 100: case 101: case 102: case 103:
+    case 104: case 105: case 106: case 107: /* Background colour high-intensity palette */
+      value = CSI_ARG(args[argi]) - 100 + 8;
+      state->bg_index = value;
+      set_pen_col_ansi(state, VTERM_ATTR_BACKGROUND, value);
+      break;
+
+    default:
+      done = 0;
+      break;
+    }
+
+    if(!done)
+    {
+      DEBUG_LOG1("libvterm: Unhandled CSI SGR %lu\n", arg);
+    }
+
+    while(CSI_ARG_HAS_MORE(args[argi++]));
+  }
+}
+
+INTERNAL int vterm_state_getpen(VTermState *state, long args[], int argcount UNUSED)
+{
+  int argi = 0;
+
+  if(state->pen.bold)
+    args[argi++] = 1;
+
+  if(state->pen.italic)
+    args[argi++] = 3;
+
+  if(state->pen.underline == 1)
+    args[argi++] = 4;
+
+  if(state->pen.blink)
+    args[argi++] = 5;
+
+  if(state->pen.reverse)
+    args[argi++] = 7;
+
+  if(state->pen.strike)
+    args[argi++] = 9;
+
+  if(state->pen.font)
+    args[argi++] = 10 + state->pen.font;
+
+  if(state->pen.underline == 2)
+    args[argi++] = 21;
+
+  if(state->fg_index >= 0 && state->fg_index < 8)
+    args[argi++] = 30 + state->fg_index;
+  else if(state->fg_index >= 8 && state->fg_index < 16)
+    args[argi++] = 90 + state->fg_index - 8;
+  else if(state->fg_index >= 16 && state->fg_index < 256) {
+    args[argi++] = CSI_ARG_FLAG_MORE|38;
+    args[argi++] = CSI_ARG_FLAG_MORE|5;
+    args[argi++] = state->fg_index;
+  }
+  else if(state->fg_index == -1) {
+    /* Send palette 2 if the actual FG colour is not default */
+    if(state->pen.fg.red   != state->default_fg.red   ||
+       state->pen.fg.green != state->default_fg.green ||
+       state->pen.fg.blue  != state->default_fg.blue  ) {
+      args[argi++] = CSI_ARG_FLAG_MORE|38;
+      args[argi++] = CSI_ARG_FLAG_MORE|2;
+      args[argi++] = CSI_ARG_FLAG_MORE | state->pen.fg.red;
+      args[argi++] = CSI_ARG_FLAG_MORE | state->pen.fg.green;
+      args[argi++] = state->pen.fg.blue;
+    }
+  }
+
+  if(state->bg_index >= 0 && state->bg_index < 8)
+    args[argi++] = 40 + state->bg_index;
+  else if(state->bg_index >= 8 && state->bg_index < 16)
+    args[argi++] = 100 + state->bg_index - 8;
+  else if(state->bg_index >= 16 && state->bg_index < 256) {
+    args[argi++] = CSI_ARG_FLAG_MORE|48;
+    args[argi++] = CSI_ARG_FLAG_MORE|5;
+    args[argi++] = state->bg_index;
+  }
+  else if(state->bg_index == -1) {
+    /* Send palette 2 if the actual BG colour is not default */
+    if(state->pen.bg.red   != state->default_bg.red   ||
+       state->pen.bg.green != state->default_bg.green ||
+       state->pen.bg.blue  != state->default_bg.blue  ) {
+      args[argi++] = CSI_ARG_FLAG_MORE|48;
+      args[argi++] = CSI_ARG_FLAG_MORE|2;
+      args[argi++] = CSI_ARG_FLAG_MORE | state->pen.bg.red;
+      args[argi++] = CSI_ARG_FLAG_MORE | state->pen.bg.green;
+      args[argi++] = state->pen.bg.blue;
+    }
+  }
+
+  return argi;
+}
+
+int vterm_state_get_penattr(const VTermState *state, VTermAttr attr, VTermValue *val)
+{
+  switch(attr) {
+  case VTERM_ATTR_BOLD:
+    val->boolean = state->pen.bold;
+    return 1;
+
+  case VTERM_ATTR_UNDERLINE:
+    val->number = state->pen.underline;
+    return 1;
+
+  case VTERM_ATTR_ITALIC:
+    val->boolean = state->pen.italic;
+    return 1;
+
+  case VTERM_ATTR_BLINK:
+    val->boolean = state->pen.blink;
+    return 1;
+
+  case VTERM_ATTR_REVERSE:
+    val->boolean = state->pen.reverse;
+    return 1;
+
+  case VTERM_ATTR_STRIKE:
+    val->boolean = state->pen.strike;
+    return 1;
+
+  case VTERM_ATTR_FONT:
+    val->number = state->pen.font;
+    return 1;
+
+  case VTERM_ATTR_FOREGROUND:
+    val->color = state->pen.fg;
+    return 1;
+
+  case VTERM_ATTR_BACKGROUND:
+    val->color = state->pen.bg;
+    return 1;
+  }
+
+  return 0;
+}
new file mode 100644
--- /dev/null
+++ b/src/libvterm/src/rect.h
@@ -0,0 +1,56 @@
+/*
+ * Some utility functions on VTermRect structures
+ */
+
+#define STRFrect "(%d,%d-%d,%d)"
+#define ARGSrect(r) (r).start_row, (r).start_col, (r).end_row, (r).end_col
+
+/* Expand dst to contain src as well */
+static void rect_expand(VTermRect *dst, VTermRect *src)
+{
+  if(dst->start_row > src->start_row) dst->start_row = src->start_row;
+  if(dst->start_col > src->start_col) dst->start_col = src->start_col;
+  if(dst->end_row   < src->end_row)   dst->end_row   = src->end_row;
+  if(dst->end_col   < src->end_col)   dst->end_col   = src->end_col;
+}
+
+/* Clip the dst to ensure it does not step outside of bounds */
+static void rect_clip(VTermRect *dst, VTermRect *bounds)
+{
+  if(dst->start_row < bounds->start_row) dst->start_row = bounds->start_row;
+  if(dst->start_col < bounds->start_col) dst->start_col = bounds->start_col;
+  if(dst->end_row   > bounds->end_row)   dst->end_row   = bounds->end_row;
+  if(dst->end_col   > bounds->end_col)   dst->end_col   = bounds->end_col;
+  /* Ensure it doesn't end up negatively-sized */
+  if(dst->end_row < dst->start_row) dst->end_row = dst->start_row;
+  if(dst->end_col < dst->start_col) dst->end_col = dst->start_col;
+}
+
+/* True if the two rectangles are equal */
+static int rect_equal(VTermRect *a, VTermRect *b)
+{
+  return (a->start_row == b->start_row) &&
+         (a->start_col == b->start_col) &&
+         (a->end_row   == b->end_row)   &&
+         (a->end_col   == b->end_col);
+}
+
+/* True if small is contained entirely within big */
+static int rect_contains(VTermRect *big, VTermRect *small)
+{
+  if(small->start_row < big->start_row) return 0;
+  if(small->start_col < big->start_col) return 0;
+  if(small->end_row   > big->end_row)   return 0;
+  if(small->end_col   > big->end_col)   return 0;
+  return 1;
+}
+
+/* True if the rectangles overlap at all */
+static int rect_intersects(VTermRect *a, VTermRect *b)
+{
+  if(a->start_row > b->end_row || b->start_row > a->end_row)
+    return 0;
+  if(a->start_col > b->end_col || b->start_col > a->end_col)
+    return 0;
+  return 1;
+}
new file mode 100644
--- /dev/null
+++ b/src/libvterm/src/screen.c
@@ -0,0 +1,937 @@
+#include "vterm_internal.h"
+
+#include <stdio.h>
+#include <string.h>
+
+#include "rect.h"
+#include "utf8.h"
+
+#define UNICODE_SPACE 0x20
+#define UNICODE_LINEFEED 0x0a
+
+/* State of the pen at some moment in time, also used in a cell */
+typedef struct
+{
+  /* After the bitfield */
+  VTermColor   fg, bg;
+
+  unsigned int bold      : 1;
+  unsigned int underline : 2;
+  unsigned int italic    : 1;
+  unsigned int blink     : 1;
+  unsigned int reverse   : 1;
+  unsigned int strike    : 1;
+  unsigned int font      : 4; /* 0 to 9 */
+
+  /* Extra state storage that isn't strictly pen-related */
+  unsigned int protected_cell : 1;
+  unsigned int dwl            : 1; /* on a DECDWL or DECDHL line */
+  unsigned int dhl            : 2; /* on a DECDHL line (1=top 2=bottom) */
+} ScreenPen;
+
+/* Internal representation of a screen cell */
+typedef struct
+{
+  uint32_t chars[VTERM_MAX_CHARS_PER_CELL];
+  ScreenPen pen;
+} ScreenCell;
+
+static int vterm_screen_set_cell(VTermScreen *screen, VTermPos pos, const VTermScreenCell *cell);
+
+struct VTermScreen
+{
+  VTerm *vt;
+  VTermState *state;
+
+  const VTermScreenCallbacks *callbacks;
+  void *cbdata;
+
+  VTermDamageSize damage_merge;
+  /* start_row == -1 => no damage */
+  VTermRect damaged;
+  VTermRect pending_scrollrect;
+  int pending_scroll_downward, pending_scroll_rightward;
+
+  int rows;
+  int cols;
+  int global_reverse;
+
+  /* Primary and Altscreen. buffers[1] is lazily allocated as needed */
+  ScreenCell *buffers[2];
+
+  /* buffer will == buffers[0] or buffers[1], depending on altscreen */
+  ScreenCell *buffer;
+
+  /* buffer for a single screen row used in scrollback storage callbacks */
+  VTermScreenCell *sb_buffer;
+
+  ScreenPen pen;
+};
+
+static ScreenCell *getcell(const VTermScreen *screen, int row, int col)
+{
+  if(row < 0 || row >= screen->rows)
+    return NULL;
+  if(col < 0 || col >= screen->cols)
+    return NULL;
+  return screen->buffer + (screen->cols * row) + col;
+}
+
+static ScreenCell *realloc_buffer(VTermScreen *screen, ScreenCell *buffer, int new_rows, int new_cols)
+{
+  ScreenCell *new_buffer = vterm_allocator_malloc(screen->vt, sizeof(ScreenCell) * new_rows * new_cols);
+  int row, col;
+
+  for(row = 0; row < new_rows; row++) {
+    for(col = 0; col < new_cols; col++) {
+      ScreenCell *new_cell = new_buffer + row*new_cols + col;
+
+      if(buffer && row < screen->rows && col < screen->cols)
+        *new_cell = buffer[row * screen->cols + col];
+      else {
+        new_cell->chars[0] = 0;
+        new_cell->pen = screen->pen;
+      }
+    }
+  }
+
+  if(buffer)
+    vterm_allocator_free(screen->vt, buffer);
+
+  return new_buffer;
+}
+
+static void damagerect(VTermScreen *screen, VTermRect rect)
+{
+  VTermRect emit;
+
+  switch(screen->damage_merge) {
+  case VTERM_DAMAGE_CELL:
+    /* Always emit damage event */
+    emit = rect;
+    break;
+
+  case VTERM_DAMAGE_ROW:
+    /* Emit damage longer than one row. Try to merge with existing damage in
+     * the same row */
+    if(rect.end_row > rect.start_row + 1) {
+      /* Bigger than 1 line - flush existing, emit this */
+      vterm_screen_flush_damage(screen);
+      emit = rect;
+    }
+    else if(screen->damaged.start_row == -1) {
+      /* None stored yet */
+      screen->damaged = rect;
+      return;
+    }
+    else if(rect.start_row == screen->damaged.start_row) {
+      /* Merge with the stored line */
+      if(screen->damaged.start_col > rect.start_col)
+        screen->damaged.start_col = rect.start_col;
+      if(screen->damaged.end_col < rect.end_col)
+        screen->damaged.end_col = rect.end_col;
+      return;
+    }
+    else {
+      /* Emit the currently stored line, store a new one */
+      emit = screen->damaged;
+      screen->damaged = rect;
+    }
+    break;
+
+  case VTERM_DAMAGE_SCREEN:
+  case VTERM_DAMAGE_SCROLL:
+    /* Never emit damage event */
+    if(screen->damaged.start_row == -1)
+      screen->damaged = rect;
+    else {
+      rect_expand(&screen->damaged, &rect);
+    }
+    return;
+
+  default:
+    DEBUG_LOG1("TODO: Maybe merge damage for level %d\n", screen->damage_merge);
+    return;
+  }
+
+  if(screen->callbacks && screen->callbacks->damage)
+    (*screen->callbacks->damage)(emit, screen->cbdata);
+}
+
+static void damagescreen(VTermScreen *screen)
+{
+  VTermRect rect = {0,0,0,0};
+  rect.end_row = screen->rows;
+  rect.end_col = screen->cols;
+
+  damagerect(screen, rect);
+}
+
+static int putglyph(VTermGlyphInfo *info, VTermPos pos, void *user)
+{
+  int i;
+  int col;
+  VTermRect rect;
+
+  VTermScreen *screen = user;
+  ScreenCell *cell = getcell(screen, pos.row, pos.col);
+
+  if(!cell)
+    return 0;
+
+  for(i = 0; i < VTERM_MAX_CHARS_PER_CELL && info->chars[i]; i++) {
+    cell->chars[i] = info->chars[i];
+    cell->pen = screen->pen;
+  }
+  if(i < VTERM_MAX_CHARS_PER_CELL)
+    cell->chars[i] = 0;
+
+  for(col = 1; col < info->width; col++)
+    getcell(screen, pos.row, pos.col + col)->chars[0] = (uint32_t)-1;
+
+  rect.start_row = pos.row;
+  rect.end_row   = pos.row+1;
+  rect.start_col = pos.col;
+  rect.end_col   = pos.col+info->width;
+
+  cell->pen.protected_cell = info->protected_cell;
+  cell->pen.dwl            = info->dwl;
+  cell->pen.dhl            = info->dhl;
+
+  damagerect(screen, rect);
+
+  return 1;
+}
+
+static int moverect_internal(VTermRect dest, VTermRect src, void *user)
+{
+  VTermScreen *screen = user;
+
+  if(screen->callbacks && screen->callbacks->sb_pushline &&
+     dest.start_row == 0 && dest.start_col == 0 &&  /* starts top-left corner */
+     dest.end_col == screen->cols &&                /* full width */
+     screen->buffer == screen->buffers[0]) {        /* not altscreen */
+    VTermPos pos;
+    for(pos.row = 0; pos.row < src.start_row; pos.row++) {
+      for(pos.col = 0; pos.col < screen->cols; pos.col++)
+        vterm_screen_get_cell(screen, pos, screen->sb_buffer + pos.col);
+
+      (screen->callbacks->sb_pushline)(screen->cols, screen->sb_buffer, screen->cbdata);
+    }
+  }
+
+  {
+    int cols = src.end_col - src.start_col;
+    int downward = src.start_row - dest.start_row;
+    int init_row, test_row, inc_row;
+    int row;
+
+    if(downward < 0) {
+      init_row = dest.end_row - 1;
+      test_row = dest.start_row - 1;
+      inc_row  = -1;
+    }
+    else {
+      init_row = dest.start_row;
+      test_row = dest.end_row;
+      inc_row  = +1;
+    }
+
+    for(row = init_row; row != test_row; row += inc_row)
+      memmove(getcell(screen, row, dest.start_col),
+	      getcell(screen, row + downward, src.start_col),
+	      cols * sizeof(ScreenCell));
+  }
+
+  return 1;
+}
+
+static int moverect_user(VTermRect dest, VTermRect src, void *user)
+{
+  VTermScreen *screen = user;
+
+  if(screen->callbacks && screen->callbacks->moverect) {
+    if(screen->damage_merge != VTERM_DAMAGE_SCROLL)
+      /* Avoid an infinite loop */
+      vterm_screen_flush_damage(screen);
+
+    if((*screen->callbacks->moverect)(dest, src, screen->cbdata))
+      return 1;
+  }
+
+  damagerect(screen, dest);
+
+  return 1;
+}
+
+static int erase_internal(VTermRect rect, int selective, void *user)
+{
+  VTermScreen *screen = user;
+  int row, col;
+
+  for(row = rect.start_row; row < screen->state->rows && row < rect.end_row; row++) {
+    const VTermLineInfo *info = vterm_state_get_lineinfo(screen->state, row);
+
+    for(col = rect.start_col; col < rect.end_col; col++) {
+      ScreenCell *cell = getcell(screen, row, col);
+
+      if(selective && cell->pen.protected_cell)
+        continue;
+
+      cell->chars[0] = 0;
+      cell->pen = screen->pen;
+      cell->pen.dwl = info->doublewidth;
+      cell->pen.dhl = info->doubleheight;
+    }
+  }
+
+  return 1;
+}
+
+static int erase_user(VTermRect rect, int selective UNUSED, void *user)
+{
+  VTermScreen *screen = user;
+
+  damagerect(screen, rect);
+
+  return 1;
+}
+
+static int erase(VTermRect rect, int selective, void *user)
+{
+  erase_internal(rect, selective, user);
+  return erase_user(rect, 0, user);
+}
+
+static int scrollrect(VTermRect rect, int downward, int rightward, void *user)
+{
+  VTermScreen *screen = user;
+
+  if(screen->damage_merge != VTERM_DAMAGE_SCROLL) {
+    vterm_scroll_rect(rect, downward, rightward,
+        moverect_internal, erase_internal, screen);
+
+    vterm_screen_flush_damage(screen);
+
+    vterm_scroll_rect(rect, downward, rightward,
+        moverect_user, erase_user, screen);
+
+    return 1;
+  }
+
+  if(screen->damaged.start_row != -1 &&
+     !rect_intersects(&rect, &screen->damaged)) {
+    vterm_screen_flush_damage(screen);
+  }
+
+  if(screen->pending_scrollrect.start_row == -1) {
+    screen->pending_scrollrect = rect;
+    screen->pending_scroll_downward  = downward;
+    screen->pending_scroll_rightward = rightward;
+  }
+  else if(rect_equal(&screen->pending_scrollrect, &rect) &&
+     ((screen->pending_scroll_downward  == 0 && downward  == 0) ||
+      (screen->pending_scroll_rightward == 0 && rightward == 0))) {
+    screen->pending_scroll_downward  += downward;
+    screen->pending_scroll_rightward += rightward;
+  }
+  else {
+    vterm_screen_flush_damage(screen);
+
+    screen->pending_scrollrect = rect;
+    screen->pending_scroll_downward  = downward;
+    screen->pending_scroll_rightward = rightward;
+  }
+
+  vterm_scroll_rect(rect, downward, rightward,
+      moverect_internal, erase_internal, screen);
+
+  if(screen->damaged.start_row == -1)
+    return 1;
+
+  if(rect_contains(&rect, &screen->damaged)) {
+    /* Scroll region entirely contains the damage; just move it */
+    vterm_rect_move(&screen->damaged, -downward, -rightward);
+    rect_clip(&screen->damaged, &rect);
+  }
+  /* There are a number of possible cases here, but lets restrict this to only
+   * the common case where we might actually gain some performance by
+   * optimising it. Namely, a vertical scroll that neatly cuts the damage
+   * region in half.
+   */
+  else if(rect.start_col <= screen->damaged.start_col &&
+          rect.end_col   >= screen->damaged.end_col &&
+          rightward == 0) {
+    if(screen->damaged.start_row >= rect.start_row &&
+       screen->damaged.start_row  < rect.end_row) {
+      screen->damaged.start_row -= downward;
+      if(screen->damaged.start_row < rect.start_row)
+        screen->damaged.start_row = rect.start_row;
+      if(screen->damaged.start_row > rect.end_row)
+        screen->damaged.start_row = rect.end_row;
+    }
+    if(screen->damaged.end_row >= rect.start_row &&
+       screen->damaged.end_row  < rect.end_row) {
+      screen->damaged.end_row -= downward;
+      if(screen->damaged.end_row < rect.start_row)
+        screen->damaged.end_row = rect.start_row;
+      if(screen->damaged.end_row > rect.end_row)
+        screen->damaged.end_row = rect.end_row;
+    }
+  }
+  else {
+    DEBUG_LOG2("TODO: Just flush and redo damaged=" STRFrect " rect=" STRFrect "\n",
+        ARGSrect(screen->damaged), ARGSrect(rect));
+  }
+
+  return 1;
+}
+
+static int movecursor(VTermPos pos, VTermPos oldpos, int visible, void *user)
+{
+  VTermScreen *screen = user;
+
+  if(screen->callbacks && screen->callbacks->movecursor)
+    return (*screen->callbacks->movecursor)(pos, oldpos, visible, screen->cbdata);
+
+  return 0;
+}
+
+static int setpenattr(VTermAttr attr, VTermValue *val, void *user)
+{
+  VTermScreen *screen = user;
+
+  switch(attr) {
+  case VTERM_ATTR_BOLD:
+    screen->pen.bold = val->boolean;
+    return 1;
+  case VTERM_ATTR_UNDERLINE:
+    screen->pen.underline = val->number;
+    return 1;
+  case VTERM_ATTR_ITALIC:
+    screen->pen.italic = val->boolean;
+    return 1;
+  case VTERM_ATTR_BLINK:
+    screen->pen.blink = val->boolean;
+    return 1;
+  case VTERM_ATTR_REVERSE:
+    screen->pen.reverse = val->boolean;
+    return 1;
+  case VTERM_ATTR_STRIKE:
+    screen->pen.strike = val->boolean;
+    return 1;
+  case VTERM_ATTR_FONT:
+    screen->pen.font = val->number;
+    return 1;
+  case VTERM_ATTR_FOREGROUND:
+    screen->pen.fg = val->color;
+    return 1;
+  case VTERM_ATTR_BACKGROUND:
+    screen->pen.bg = val->color;
+    return 1;
+  }
+
+  return 0;
+}
+
+static int settermprop(VTermProp prop, VTermValue *val, void *user)
+{
+  VTermScreen *screen = user;
+
+  switch(prop) {
+  case VTERM_PROP_ALTSCREEN:
+    if(val->boolean && !screen->buffers[1])
+      return 0;
+
+    screen->buffer = val->boolean ? screen->buffers[1] : screen->buffers[0];
+    /* only send a damage event on disable; because during enable there's an
+     * erase that sends a damage anyway
+     */
+    if(!val->boolean)
+      damagescreen(screen);
+    break;
+  case VTERM_PROP_REVERSE:
+    screen->global_reverse = val->boolean;
+    damagescreen(screen);
+    break;
+  default:
+    ; /* ignore */
+  }
+
+  if(screen->callbacks && screen->callbacks->settermprop)
+    return (*screen->callbacks->settermprop)(prop, val, screen->cbdata);
+
+  return 1;
+}
+
+static int bell(void *user)
+{
+  VTermScreen *screen = user;
+
+  if(screen->callbacks && screen->callbacks->bell)
+    return (*screen->callbacks->bell)(screen->cbdata);
+
+  return 0;
+}
+
+static int resize(int new_rows, int new_cols, VTermPos *delta, void *user)
+{
+  VTermScreen *screen = user;
+
+  int is_altscreen = (screen->buffers[1] && screen->buffer == screen->buffers[1]);
+
+  int old_rows = screen->rows;
+  int old_cols = screen->cols;
+  int first_blank_row;
+
+  if(!is_altscreen && new_rows < old_rows) {
+    /* Fewer rows - determine if we're going to scroll at all, and if so, push
+       those lines to scrollback */
+    VTermPos pos = { 0, 0 };
+    VTermPos cursor = screen->state->pos;
+    /* Find the first blank row after the cursor. */
+    for(pos.row = old_rows - 1; pos.row >= new_rows; pos.row--)
+      if(!vterm_screen_is_eol(screen, pos) || cursor.row == pos.row)
+        break;
+
+    first_blank_row = pos.row + 1;
+    if(first_blank_row > new_rows) {
+      VTermRect rect = {0,0,0,0};
+      rect.end_row   = old_rows;
+      rect.end_col   = old_cols;
+      scrollrect(rect, first_blank_row - new_rows, 0, user);
+      vterm_screen_flush_damage(screen);
+
+      delta->row -= first_blank_row - new_rows;
+    }
+  }
+
+  screen->buffers[0] = realloc_buffer(screen, screen->buffers[0], new_rows, new_cols);
+  if(screen->buffers[1])
+    screen->buffers[1] = realloc_buffer(screen, screen->buffers[1], new_rows, new_cols);
+
+  screen->buffer = is_altscreen ? screen->buffers[1] : screen->buffers[0];
+
+  screen->rows = new_rows;
+  screen->cols = new_cols;
+
+  if(screen->sb_buffer)
+    vterm_allocator_free(screen->vt, screen->sb_buffer);
+
+  screen->sb_buffer = vterm_allocator_malloc(screen->vt, sizeof(VTermScreenCell) * new_cols);
+
+  if(new_cols > old_cols) {
+    VTermRect rect;
+    rect.start_row = 0;
+    rect.end_row   = old_rows;
+    rect.start_col = old_cols;
+    rect.end_col   = new_cols;
+    damagerect(screen, rect);
+  }
+
+  if(new_rows > old_rows) {
+    if(!is_altscreen && screen->callbacks && screen->callbacks->sb_popline) {
+      int rows = new_rows - old_rows;
+      while(rows) {
+        VTermRect rect = {0,0,0,0};
+        VTermPos pos = { 0, 0 };
+        if(!(screen->callbacks->sb_popline(screen->cols, screen->sb_buffer, screen->cbdata)))
+          break;
+
+	rect.end_row   = screen->rows;
+	rect.end_col   = screen->cols;
+        scrollrect(rect, -1, 0, user);
+
+        for(pos.col = 0; pos.col < screen->cols; pos.col += screen->sb_buffer[pos.col].width)
+          vterm_screen_set_cell(screen, pos, screen->sb_buffer + pos.col);
+
+        rect.end_row = 1;
+        damagerect(screen, rect);
+
+        vterm_screen_flush_damage(screen);
+
+        rows--;
+        delta->row++;
+      }
+    }
+
+    {
+      VTermRect rect;
+      rect.start_row = old_rows;
+      rect.end_row   = new_rows;
+      rect.start_col = 0;
+      rect.end_col   = new_cols;
+      damagerect(screen, rect);
+    }
+  }
+
+  if(screen->callbacks && screen->callbacks->resize)
+    return (*screen->callbacks->resize)(new_rows, new_cols, screen->cbdata);
+
+  return 1;
+}
+
+static int setlineinfo(int row, const VTermLineInfo *newinfo, const VTermLineInfo *oldinfo, void *user)
+{
+  VTermScreen *screen = user;
+  int col;
+  VTermRect rect;
+
+  if(newinfo->doublewidth != oldinfo->doublewidth ||
+     newinfo->doubleheight != oldinfo->doubleheight) {
+    for(col = 0; col < screen->cols; col++) {
+      ScreenCell *cell = getcell(screen, row, col);
+      cell->pen.dwl = newinfo->doublewidth;
+      cell->pen.dhl = newinfo->doubleheight;
+    }
+
+    rect.start_row = row;
+    rect.end_row   = row + 1;
+    rect.start_col = 0;
+    rect.end_col   = newinfo->doublewidth ? screen->cols / 2 : screen->cols;
+    damagerect(screen, rect);
+
+    if(newinfo->doublewidth) {
+      rect.start_col = screen->cols / 2;
+      rect.end_col   = screen->cols;
+
+      erase_internal(rect, 0, user);
+    }
+  }
+
+  return 1;
+}
+
+static VTermStateCallbacks state_cbs = {
+  &putglyph, /* putglyph */
+  &movecursor, /* movecursor */
+  &scrollrect, /* scrollrect */
+  NULL, /* moverect */
+  &erase, /* erase */
+  NULL, /* initpen */
+  &setpenattr, /* setpenattr */
+  &settermprop, /* settermprop */
+  &bell, /* bell */
+  &resize, /* resize */
+  &setlineinfo /* setlineinfo */
+};
+
+static VTermScreen *screen_new(VTerm *vt)
+{
+  VTermState *state = vterm_obtain_state(vt);
+  VTermScreen *screen;
+  int rows, cols;
+
+  if(!state)
+    return NULL;
+
+  screen = vterm_allocator_malloc(vt, sizeof(VTermScreen));
+
+  vterm_get_size(vt, &rows, &cols);
+
+  screen->vt = vt;
+  screen->state = state;
+
+  screen->damage_merge = VTERM_DAMAGE_CELL;
+  screen->damaged.start_row = -1;
+  screen->pending_scrollrect.start_row = -1;
+
+  screen->rows = rows;
+  screen->cols = cols;
+
+  screen->callbacks = NULL;
+  screen->cbdata    = NULL;
+
+  screen->buffers[0] = realloc_buffer(screen, NULL, rows, cols);
+
+  screen->buffer = screen->buffers[0];
+
+  screen->sb_buffer = vterm_allocator_malloc(screen->vt, sizeof(VTermScreenCell) * cols);
+
+  vterm_state_set_callbacks(screen->state, &state_cbs, screen);
+
+  return screen;
+}
+
+INTERNAL void vterm_screen_free(VTermScreen *screen)
+{
+  vterm_allocator_free(screen->vt, screen->buffers[0]);
+  if(screen->buffers[1])
+    vterm_allocator_free(screen->vt, screen->buffers[1]);
+
+  vterm_allocator_free(screen->vt, screen->sb_buffer);
+
+  vterm_allocator_free(screen->vt, screen);
+}
+
+void vterm_screen_reset(VTermScreen *screen, int hard)
+{
+  screen->damaged.start_row = -1;
+  screen->pending_scrollrect.start_row = -1;
+  vterm_state_reset(screen->state, hard);
+  vterm_screen_flush_damage(screen);
+}
+
+static size_t _get_chars(const VTermScreen *screen, const int utf8, void *buffer, size_t len, const VTermRect rect)
+{
+  size_t outpos = 0;
+  int padding = 0;
+  int row, col;
+
+#define PUT(c)                                             \
+  if(utf8) {                                               \
+    size_t thislen = utf8_seqlen(c);                       \
+    if(buffer && outpos + thislen <= len)                  \
+      outpos += fill_utf8((c), (char *)buffer + outpos);   \
+    else                                                   \
+      outpos += thislen;                                   \
+  }                                                        \
+  else {                                                   \
+    if(buffer && outpos + 1 <= len)                        \
+      ((uint32_t*)buffer)[outpos++] = (c);                 \
+    else                                                   \
+      outpos++;                                            \
+  }
+
+  for(row = rect.start_row; row < rect.end_row; row++) {
+    for(col = rect.start_col; col < rect.end_col; col++) {
+      ScreenCell *cell = getcell(screen, row, col);
+      int i;
+
+      if(cell->chars[0] == 0)
+        /* Erased cell, might need a space */
+        padding++;
+      else if(cell->chars[0] == (uint32_t)-1)
+        /* Gap behind a double-width char, do nothing */
+        ;
+      else {
+        while(padding) {
+          PUT(UNICODE_SPACE);
+          padding--;
+        }
+        for(i = 0; i < VTERM_MAX_CHARS_PER_CELL && cell->chars[i]; i++) {
+          PUT(cell->chars[i]);
+        }
+      }
+    }
+
+    if(row < rect.end_row - 1) {
+      PUT(UNICODE_LINEFEED);
+      padding = 0;
+    }
+  }
+
+  return outpos;
+}
+
+size_t vterm_screen_get_chars(const VTermScreen *screen, uint32_t *chars, size_t len, const VTermRect rect)
+{
+  return _get_chars(screen, 0, chars, len, rect);
+}
+
+size_t vterm_screen_get_text(const VTermScreen *screen, char *str, size_t len, const VTermRect rect)
+{
+  return _get_chars(screen, 1, str, len, rect);
+}
+
+/* Copy internal to external representation of a screen cell */
+int vterm_screen_get_cell(const VTermScreen *screen, VTermPos pos, VTermScreenCell *cell)
+{
+  ScreenCell *intcell = getcell(screen, pos.row, pos.col);
+  int i;
+
+  if(!intcell)
+    return 0;
+
+  for(i = 0; ; i++) {
+    cell->chars[i] = intcell->chars[i];
+    if(!intcell->chars[i])
+      break;
+  }
+
+  cell->attrs.bold      = intcell->pen.bold;
+  cell->attrs.underline = intcell->pen.underline;
+  cell->attrs.italic    = intcell->pen.italic;
+  cell->attrs.blink     = intcell->pen.blink;
+  cell->attrs.reverse   = intcell->pen.reverse ^ screen->global_reverse;
+  cell->attrs.strike    = intcell->pen.strike;
+  cell->attrs.font      = intcell->pen.font;
+
+  cell->attrs.dwl = intcell->pen.dwl;
+  cell->attrs.dhl = intcell->pen.dhl;
+
+  cell->fg = intcell->pen.fg;
+  cell->bg = intcell->pen.bg;
+
+  if(pos.col < (screen->cols - 1) &&
+     getcell(screen, pos.row, pos.col + 1)->chars[0] == (uint32_t)-1)
+    cell->width = 2;
+  else
+    cell->width = 1;
+
+  return 1;
+}
+
+/* Copy external to internal representation of a screen cell */
+/* static because it's only used internally for sb_popline during resize */
+static int vterm_screen_set_cell(VTermScreen *screen, VTermPos pos, const VTermScreenCell *cell)
+{
+  ScreenCell *intcell = getcell(screen, pos.row, pos.col);
+  int i;
+
+  if(!intcell)
+    return 0;
+
+  for(i = 0; ; i++) {
+    intcell->chars[i] = cell->chars[i];
+    if(!cell->chars[i])
+      break;
+  }
+
+  intcell->pen.bold      = cell->attrs.bold;
+  intcell->pen.underline = cell->attrs.underline;
+  intcell->pen.italic    = cell->attrs.italic;
+  intcell->pen.blink     = cell->attrs.blink;
+  intcell->pen.reverse   = cell->attrs.reverse ^ screen->global_reverse;
+  intcell->pen.strike    = cell->attrs.strike;
+  intcell->pen.font      = cell->attrs.font;
+
+  intcell->pen.fg = cell->fg;
+  intcell->pen.bg = cell->bg;
+
+  if(cell->width == 2)
+    getcell(screen, pos.row, pos.col + 1)->chars[0] = (uint32_t)-1;
+
+  return 1;
+}
+
+int vterm_screen_is_eol(const VTermScreen *screen, VTermPos pos)
+{
+  /* This cell is EOL if this and every cell to the right is black */
+  for(; pos.col < screen->cols; pos.col++) {
+    ScreenCell *cell = getcell(screen, pos.row, pos.col);
+    if(cell->chars[0] != 0)
+      return 0;
+  }
+
+  return 1;
+}
+
+VTermScreen *vterm_obtain_screen(VTerm *vt)
+{
+  VTermScreen *screen;
+  if(vt->screen)
+    return vt->screen;
+
+  screen = screen_new(vt);
+  vt->screen = screen;
+
+  return screen;
+}
+
+void vterm_screen_enable_altscreen(VTermScreen *screen, int altscreen)
+{
+
+  if(!screen->buffers[1] && altscreen) {
+    int rows, cols;
+    vterm_get_size(screen->vt, &rows, &cols);
+
+    screen->buffers[1] = realloc_buffer(screen, NULL, rows, cols);
+  }
+}
+
+void vterm_screen_set_callbacks(VTermScreen *screen, const VTermScreenCallbacks *callbacks, void *user)
+{
+  screen->callbacks = callbacks;
+  screen->cbdata = user;
+}
+
+void *vterm_screen_get_cbdata(VTermScreen *screen)
+{
+  return screen->cbdata;
+}
+
+void vterm_screen_set_unrecognised_fallbacks(VTermScreen *screen, const VTermParserCallbacks *fallbacks, void *user)
+{
+  vterm_state_set_unrecognised_fallbacks(screen->state, fallbacks, user);
+}
+
+void *vterm_screen_get_unrecognised_fbdata(VTermScreen *screen)
+{
+  return vterm_state_get_unrecognised_fbdata(screen->state);
+}
+
+void vterm_screen_flush_damage(VTermScreen *screen)
+{
+  if(screen->pending_scrollrect.start_row != -1) {
+    vterm_scroll_rect(screen->pending_scrollrect, screen->pending_scroll_downward, screen->pending_scroll_rightward,
+        moverect_user, erase_user, screen);
+
+    screen->pending_scrollrect.start_row = -1;
+  }
+
+  if(screen->damaged.start_row != -1) {
+    if(screen->callbacks && screen->callbacks->damage)
+      (*screen->callbacks->damage)(screen->damaged, screen->cbdata);
+
+    screen->damaged.start_row = -1;
+  }
+}
+
+void vterm_screen_set_damage_merge(VTermScreen *screen, VTermDamageSize size)
+{
+  vterm_screen_flush_damage(screen);
+  screen->damage_merge = size;
+}
+
+static int attrs_differ(VTermAttrMask attrs, ScreenCell *a, ScreenCell *b)
+{
+  if((attrs & VTERM_ATTR_BOLD_MASK)       && (a->pen.bold != b->pen.bold))
+    return 1;
+  if((attrs & VTERM_ATTR_UNDERLINE_MASK)  && (a->pen.underline != b->pen.underline))
+    return 1;
+  if((attrs & VTERM_ATTR_ITALIC_MASK)     && (a->pen.italic != b->pen.italic))
+    return 1;
+  if((attrs & VTERM_ATTR_BLINK_MASK)      && (a->pen.blink != b->pen.blink))
+    return 1;
+  if((attrs & VTERM_ATTR_REVERSE_MASK)    && (a->pen.reverse != b->pen.reverse))
+    return 1;
+  if((attrs & VTERM_ATTR_STRIKE_MASK)     && (a->pen.strike != b->pen.strike))
+    return 1;
+  if((attrs & VTERM_ATTR_FONT_MASK)       && (a->pen.font != b->pen.font))
+    return 1;
+  if((attrs & VTERM_ATTR_FOREGROUND_MASK) && !vterm_color_equal(a->pen.fg, b->pen.fg))
+    return 1;
+  if((attrs & VTERM_ATTR_BACKGROUND_MASK) && !vterm_color_equal(a->pen.bg, b->pen.bg))
+    return 1;
+
+  return 0;
+}
+
+int vterm_screen_get_attrs_extent(const VTermScreen *screen, VTermRect *extent, VTermPos pos, VTermAttrMask attrs)
+{
+  int col;
+
+  ScreenCell *target = getcell(screen, pos.row, pos.col);
+
+  /* TODO: bounds check */
+  extent->start_row = pos.row;
+  extent->end_row   = pos.row + 1;
+
+  if(extent->start_col < 0)
+    extent->start_col = 0;
+  if(extent->end_col < 0)
+    extent->end_col = screen->cols;
+
+  for(col = pos.col - 1; col >= extent->start_col; col--)
+    if(attrs_differ(attrs, target, getcell(screen, pos.row, col)))
+      break;
+  extent->start_col = col + 1;
+
+  for(col = pos.col + 1; col < extent->end_col; col++)
+    if(attrs_differ(attrs, target, getcell(screen, pos.row, col)))
+      break;
+  extent->end_col = col - 1;
+
+  return 1;
+}
new file mode 100644
--- /dev/null
+++ b/src/libvterm/src/state.c
@@ -0,0 +1,1851 @@
+#include "vterm_internal.h"
+
+#include <stdio.h>
+#include <string.h>
+
+#define strneq(a,b,n) (strncmp(a,b,n)==0)
+
+#if defined(DEBUG) && DEBUG > 1
+# define DEBUG_GLYPH_COMBINE
+#endif
+
+/* Some convenient wrappers to make callback functions easier */
+
+static void putglyph(VTermState *state, const uint32_t chars[], int width, VTermPos pos)
+{
+  VTermGlyphInfo info;
+  info.chars = chars;
+  info.width = width;
+  info.protected_cell = state->protected_cell;
+  info.dwl = state->lineinfo[pos.row].doublewidth;
+  info.dhl = state->lineinfo[pos.row].doubleheight;
+
+  if(state->callbacks && state->callbacks->putglyph)
+    if((*state->callbacks->putglyph)(&info, pos, state->cbdata))
+      return;
+
+  DEBUG_LOG3("libvterm: Unhandled putglyph U+%04x at (%d,%d)\n", chars[0], pos.col, pos.row);
+}
+
+static void updatecursor(VTermState *state, VTermPos *oldpos, int cancel_phantom)
+{
+  if(state->pos.col == oldpos->col && state->pos.row == oldpos->row)
+    return;
+
+  if(cancel_phantom)
+    state->at_phantom = 0;
+
+  if(state->callbacks && state->callbacks->movecursor)
+    if((*state->callbacks->movecursor)(state->pos, *oldpos, state->mode.cursor_visible, state->cbdata))
+      return;
+}
+
+static void erase(VTermState *state, VTermRect rect, int selective)
+{
+  if(state->callbacks && state->callbacks->erase)
+    if((*state->callbacks->erase)(rect, selective, state->cbdata))
+      return;
+}
+
+static VTermState *vterm_state_new(VTerm *vt)
+{
+  VTermState *state = vterm_allocator_malloc(vt, sizeof(VTermState));
+
+  state->vt = vt;
+
+  state->rows = vt->rows;
+  state->cols = vt->cols;
+
+  state->mouse_col     = 0;
+  state->mouse_row     = 0;
+  state->mouse_buttons = 0;
+
+  state->mouse_protocol = MOUSE_X10;
+
+  state->callbacks = NULL;
+  state->cbdata    = NULL;
+
+  vterm_state_newpen(state);
+
+  state->bold_is_highbright = 0;
+
+  return state;
+}
+
+INTERNAL void vterm_state_free(VTermState *state)
+{
+  vterm_allocator_free(state->vt, state->tabstops);
+  vterm_allocator_free(state->vt, state->lineinfo);
+  vterm_allocator_free(state->vt, state->combine_chars);
+  vterm_allocator_free(state->vt, state);
+}
+
+static void scroll(VTermState *state, VTermRect rect, int downward, int rightward)
+{
+  int rows;
+  int cols;
+  if(!downward && !rightward)
+    return;
+
+  rows = rect.end_row - rect.start_row;
+  if(downward > rows)
+    downward = rows;
+  else if(downward < -rows)
+    downward = -rows;
+
+  cols = rect.end_col - rect.start_col;
+  if(rightward > cols)
+    rightward = cols;
+  else if(rightward < -cols)
+    rightward = -cols;
+
+  /* Update lineinfo if full line */
+  if(rect.start_col == 0 && rect.end_col == state->cols && rightward == 0) {
+    int height = rect.end_row - rect.start_row - abs(downward);
+
+    if(downward > 0)
+      memmove(state->lineinfo + rect.start_row,
+              state->lineinfo + rect.start_row + downward,
+              height * sizeof(state->lineinfo[0]));
+    else
+      memmove(state->lineinfo + rect.start_row - downward,
+              state->lineinfo + rect.start_row,
+              height * sizeof(state->lineinfo[0]));
+  }
+
+  if(state->callbacks && state->callbacks->scrollrect)
+    if((*state->callbacks->scrollrect)(rect, downward, rightward, state->cbdata))
+      return;
+
+  if(state->callbacks)
+    vterm_scroll_rect(rect, downward, rightward,
+        state->callbacks->moverect, state->callbacks->erase, state->cbdata);
+}
+
+static void linefeed(VTermState *state)
+{
+  if(state->pos.row == SCROLLREGION_BOTTOM(state) - 1) {
+    VTermRect rect;
+    rect.start_row = state->scrollregion_top;
+    rect.end_row   = SCROLLREGION_BOTTOM(state);
+    rect.start_col = SCROLLREGION_LEFT(state);
+    rect.end_col   = SCROLLREGION_RIGHT(state);
+
+    scroll(state, rect, 1, 0);
+  }
+  else if(state->pos.row < state->rows-1)
+    state->pos.row++;
+}
+
+static void grow_combine_buffer(VTermState *state)
+{
+  size_t    new_size = state->combine_chars_size * 2;
+  uint32_t *new_chars = vterm_allocator_malloc(state->vt, new_size * sizeof(new_chars[0]));
+
+  memcpy(new_chars, state->combine_chars, state->combine_chars_size * sizeof(new_chars[0]));
+
+  vterm_allocator_free(state->vt, state->combine_chars);
+
+  state->combine_chars = new_chars;
+  state->combine_chars_size = new_size;
+}
+
+static void set_col_tabstop(VTermState *state, int col)
+{
+  unsigned char mask = 1 << (col & 7);
+  state->tabstops[col >> 3] |= mask;
+}
+
+static void clear_col_tabstop(VTermState *state, int col)
+{
+  unsigned char mask = 1 << (col & 7);
+  state->tabstops[col >> 3] &= ~mask;
+}
+
+static int is_col_tabstop(VTermState *state, int col)
+{
+  unsigned char mask = 1 << (col & 7);
+  return state->tabstops[col >> 3] & mask;
+}
+
+static int is_cursor_in_scrollregion(const VTermState *state)
+{
+  if(state->pos.row < state->scrollregion_top ||
+     state->pos.row >= SCROLLREGION_BOTTOM(state))
+    return 0;
+  if(state->pos.col < SCROLLREGION_LEFT(state) ||
+     state->pos.col >= SCROLLREGION_RIGHT(state))
+    return 0;
+
+  return 1;
+}
+
+static void tab(VTermState *state, int count, int direction)
+{
+  while(count > 0) {
+    if(direction > 0) {
+      if(state->pos.col >= THISROWWIDTH(state)-1)
+        return;
+
+      state->pos.col++;
+    }
+    else if(direction < 0) {
+      if(state->pos.col < 1)
+        return;
+
+      state->pos.col--;
+    }
+
+    if(is_col_tabstop(state, state->pos.col))
+      count--;
+  }
+}
+
+#define NO_FORCE 0
+#define FORCE    1
+
+#define DWL_OFF 0
+#define DWL_ON  1
+
+#define DHL_OFF    0
+#define DHL_TOP    1
+#define DHL_BOTTOM 2
+
+static void set_lineinfo(VTermState *state, int row, int force, int dwl, int dhl)
+{
+  VTermLineInfo info = state->lineinfo[row];
+
+  if(dwl == DWL_OFF)
+    info.doublewidth = DWL_OFF;
+  else if(dwl == DWL_ON)
+    info.doublewidth = DWL_ON;
+  /* else -1 to ignore */
+
+  if(dhl == DHL_OFF)
+    info.doubleheight = DHL_OFF;
+  else if(dhl == DHL_TOP)
+    info.doubleheight = DHL_TOP;
+  else if(dhl == DHL_BOTTOM)
+    info.doubleheight = DHL_BOTTOM;
+
+  if((state->callbacks &&
+      state->callbacks->setlineinfo &&
+      (*state->callbacks->setlineinfo)(row, &info, state->lineinfo + row, state->cbdata))
+      || force)
+    state->lineinfo[row] = info;
+}
+
+static int on_text(const char bytes[], size_t len, void *user)
+{
+  VTermState *state = user;
+  uint32_t *codepoints;
+  int npoints = 0;
+  size_t eaten = 0;
+  VTermEncodingInstance *encoding;
+  int i = 0;
+
+  VTermPos oldpos = state->pos;
+
+  /* We'll have at most len codepoints */
+  codepoints = vterm_allocator_malloc(state->vt, len * sizeof(uint32_t));
+
+  encoding =
+    state->gsingle_set     ? &state->encoding[state->gsingle_set] :
+    !(bytes[eaten] & 0x80) ? &state->encoding[state->gl_set] :
+    state->vt->mode.utf8   ? &state->encoding_utf8 :
+                             &state->encoding[state->gr_set];
+
+  (*encoding->enc->decode)(encoding->enc, encoding->data,
+      codepoints, &npoints, state->gsingle_set ? 1 : len,
+      bytes, &eaten, len);
+
+  /* There's a chance an encoding (e.g. UTF-8) hasn't found enough bytes yet
+   * for even a single codepoint
+   */
+  if(!npoints)
+  {
+    vterm_allocator_free(state->vt, codepoints);
+    return 0;
+  }
+
+  if(state->gsingle_set && npoints)
+    state->gsingle_set = 0;
+
+  /* This is a combining char. that needs to be merged with the previous
+   * glyph output */
+  if(vterm_unicode_is_combining(codepoints[i])) {
+    /* See if the cursor has moved since */
+    if(state->pos.row == state->combine_pos.row && state->pos.col == state->combine_pos.col + state->combine_width) {
+#ifdef DEBUG_GLYPH_COMBINE
+      int printpos;
+      printf("DEBUG: COMBINING SPLIT GLYPH of chars {");
+      for(printpos = 0; state->combine_chars[printpos]; printpos++)
+        printf("U+%04x ", state->combine_chars[printpos]);
+      printf("} + {");
+#endif
+
+      /* Find where we need to append these combining chars */
+      int saved_i = 0;
+      while(state->combine_chars[saved_i])
+        saved_i++;
+
+      /* Add extra ones */
+      while(i < npoints && vterm_unicode_is_combining(codepoints[i])) {
+        if(saved_i >= (int)state->combine_chars_size)
+          grow_combine_buffer(state);
+        state->combine_chars[saved_i++] = codepoints[i++];
+      }
+      if(saved_i >= (int)state->combine_chars_size)
+        grow_combine_buffer(state);
+      state->combine_chars[saved_i] = 0;
+
+#ifdef DEBUG_GLYPH_COMBINE
+      for(; state->combine_chars[printpos]; printpos++)
+        printf("U+%04x ", state->combine_chars[printpos]);
+      printf("}\n");
+#endif
+
+      /* Now render it */
+      putglyph(state, state->combine_chars, state->combine_width, state->combine_pos);
+    }
+    else {
+      DEBUG_LOG("libvterm: TODO: Skip over split char+combining\n");
+    }
+  }
+
+  for(; i < npoints; i++) {
+    /* Try to find combining characters following this */
+    int glyph_starts = i;
+    int glyph_ends;
+    int width = 0;
+    uint32_t *chars;
+
+    for(glyph_ends = i + 1; glyph_ends < npoints; glyph_ends++)
+      if(!vterm_unicode_is_combining(codepoints[glyph_ends]))
+        break;
+
+    chars = vterm_allocator_malloc(state->vt, (glyph_ends - glyph_starts + 1) * sizeof(uint32_t));
+
+    for( ; i < glyph_ends; i++) {
+      int this_width;
+      chars[i - glyph_starts] = codepoints[i];
+      this_width = vterm_unicode_width(codepoints[i]);
+#ifdef DEBUG
+      if(this_width < 0) {
+        fprintf(stderr, "Text with negative-width codepoint U+%04x\n", codepoints[i]);
+        abort();
+      }
+#endif
+      width += this_width;
+    }
+
+    chars[glyph_ends - glyph_starts] = 0;
+    i--;
+
+#ifdef DEBUG_GLYPH_COMBINE
+    int printpos;
+    printf("DEBUG: COMBINED GLYPH of %d chars {", glyph_ends - glyph_starts);
+    for(printpos = 0; printpos < glyph_ends - glyph_starts; printpos++)
+      printf("U+%04x ", chars[printpos]);
+    printf("}, onscreen width %d\n", width);
+#endif
+
+    if(state->at_phantom || state->pos.col + width > THISROWWIDTH(state)) {
+      linefeed(state);
+      state->pos.col = 0;
+      state->at_phantom = 0;
+    }
+
+    if(state->mode.insert) {
+      /* TODO: This will be a little inefficient for large bodies of text, as
+       * it'll have to 'ICH' effectively before every glyph. We should scan
+       * ahead and ICH as many times as required
+       */
+      VTermRect rect;
+      rect.start_row = state->pos.row;
+      rect.end_row   = state->pos.row + 1;
+      rect.start_col = state->pos.col;
+      rect.end_col   = THISROWWIDTH(state);
+      scroll(state, rect, 0, -1);
+    }
+
+    putglyph(state, chars, width, state->pos);
+
+    if(i == npoints - 1) {
+      /* End of the buffer. Save the chars in case we have to combine with
+       * more on the next call */
+      int save_i;
+      for(save_i = 0; chars[save_i]; save_i++) {
+        if(save_i >= (int)state->combine_chars_size)
+          grow_combine_buffer(state);
+        state->combine_chars[save_i] = chars[save_i];
+      }
+      if(save_i >= (int)state->combine_chars_size)
+        grow_combine_buffer(state);
+      state->combine_chars[save_i] = 0;
+      state->combine_width = width;
+      state->combine_pos = state->pos;
+    }
+
+    if(state->pos.col + width >= THISROWWIDTH(state)) {
+      if(state->mode.autowrap)
+        state->at_phantom = 1;
+    }
+    else {
+      state->pos.col += width;
+    }
+    vterm_allocator_free(state->vt, chars);
+  }
+
+  updatecursor(state, &oldpos, 0);
+
+#ifdef DEBUG
+  if(state->pos.row < 0 || state->pos.row >= state->rows ||
+     state->pos.col < 0 || state->pos.col >= state->cols) {
+    fprintf(stderr, "Position out of bounds after text: (%d,%d)\n",
+        state->pos.row, state->pos.col);
+    abort();
+  }
+#endif
+
+  vterm_allocator_free(state->vt, codepoints);
+  return eaten;
+}
+
+static int on_control(unsigned char control, void *user)
+{
+  VTermState *state = user;
+
+  VTermPos oldpos = state->pos;
+
+  switch(control) {
+  case 0x07: /* BEL - ECMA-48 8.3.3 */
+    if(state->callbacks && state->callbacks->bell)
+      (*state->callbacks->bell)(state->cbdata);
+    break;
+
+  case 0x08: /* BS - ECMA-48 8.3.5 */
+    if(state->pos.col > 0)
+      state->pos.col--;
+    break;
+
+  case 0x09: /* HT - ECMA-48 8.3.60 */
+    tab(state, 1, +1);
+    break;
+
+  case 0x0a: /* LF - ECMA-48 8.3.74 */
+  case 0x0b: /* VT */
+  case 0x0c: /* FF */
+    linefeed(state);
+    if(state->mode.newline)
+      state->pos.col = 0;
+    break;
+
+  case 0x0d: /* CR - ECMA-48 8.3.15 */
+    state->pos.col = 0;
+    break;
+
+  case 0x0e: /* LS1 - ECMA-48 8.3.76 */
+    state->gl_set = 1;
+    break;
+
+  case 0x0f: /* LS0 - ECMA-48 8.3.75 */
+    state->gl_set = 0;
+    break;
+
+  case 0x84: /* IND - DEPRECATED but implemented for completeness */
+    linefeed(state);
+    break;
+
+  case 0x85: /* NEL - ECMA-48 8.3.86 */
+    linefeed(state);
+    state->pos.col = 0;
+    break;
+
+  case 0x88: /* HTS - ECMA-48 8.3.62 */
+    set_col_tabstop(state, state->pos.col);
+    break;
+
+  case 0x8d: /* RI - ECMA-48 8.3.104 */
+    if(state->pos.row == state->scrollregion_top) {
+      VTermRect rect;
+      rect.start_row = state->scrollregion_top;
+      rect.end_row   = SCROLLREGION_BOTTOM(state);
+      rect.start_col = SCROLLREGION_LEFT(state);
+      rect.end_col   = SCROLLREGION_RIGHT(state);
+
+      scroll(state, rect, -1, 0);
+    }
+    else if(state->pos.row > 0)
+        state->pos.row--;
+    break;
+
+  case 0x8e: /* SS2 - ECMA-48 8.3.141 */
+    state->gsingle_set = 2;
+    break;
+
+  case 0x8f: /* SS3 - ECMA-48 8.3.142 */
+    state->gsingle_set = 3;
+    break;
+
+  default:
+    if(state->fallbacks && state->fallbacks->control)
+      if((*state->fallbacks->control)(control, state->fbdata))
+        return 1;
+
+    return 0;
+  }
+
+  updatecursor(state, &oldpos, 1);
+
+#ifdef DEBUG
+  if(state->pos.row < 0 || state->pos.row >= state->rows ||
+     state->pos.col < 0 || state->pos.col >= state->cols) {
+    fprintf(stderr, "Position out of bounds after Ctrl %02x: (%d,%d)\n",
+        control, state->pos.row, state->pos.col);
+    abort();
+  }
+#endif
+
+  return 1;
+}
+
+static int settermprop_bool(VTermState *state, VTermProp prop, int v)
+{
+  VTermValue val;
+  val.boolean = v;
+  return vterm_state_set_termprop(state, prop, &val);
+}
+
+static int settermprop_int(VTermState *state, VTermProp prop, int v)
+{
+  VTermValue val;
+  val.number = v;
+  return vterm_state_set_termprop(state, prop, &val);
+}
+
+static int settermprop_string(VTermState *state, VTermProp prop, const char *str, size_t len)
+{
+  char *strvalue;
+  int r;
+  VTermValue val;
+  strvalue = vterm_allocator_malloc(state->vt, (len+1) * sizeof(char));
+  strncpy(strvalue, str, len);
+  strvalue[len] = 0;
+
+  val.string = strvalue;
+  r = vterm_state_set_termprop(state, prop, &val);
+  vterm_allocator_free(state->vt, strvalue);
+  return r;
+}
+
+static void savecursor(VTermState *state, int save)
+{
+  if(save) {
+    state->saved.pos = state->pos;
+    state->saved.mode.cursor_visible = state->mode.cursor_visible;
+    state->saved.mode.cursor_blink   = state->mode.cursor_blink;
+    state->saved.mode.cursor_shape   = state->mode.cursor_shape;
+
+    vterm_state_savepen(state, 1);
+  }
+  else {
+    VTermPos oldpos = state->pos;
+
+    state->pos = state->saved.pos;
+
+    settermprop_bool(state, VTERM_PROP_CURSORVISIBLE, state->saved.mode.cursor_visible);
+    settermprop_bool(state, VTERM_PROP_CURSORBLINK,   state->saved.mode.cursor_blink);
+    settermprop_int (state, VTERM_PROP_CURSORSHAPE,   state->saved.mode.cursor_shape);
+
+    vterm_state_savepen(state, 0);
+
+    updatecursor(state, &oldpos, 1);
+  }
+}
+
+static int on_escape(const char *bytes, size_t len, void *user)
+{
+  VTermState *state = user;
+
+  /* Easier to decode this from the first byte, even though the final
+   * byte terminates it
+   */
+  switch(bytes[0]) {
+  case ' ':
+    if(len != 2)
+      return 0;
+
+    switch(bytes[1]) {
+      case 'F': /* S7C1T */
+        state->vt->mode.ctrl8bit = 0;
+        break;
+
+      case 'G': /* S8C1T */
+        state->vt->mode.ctrl8bit = 1;
+        break;
+
+      default:
+        return 0;
+    }
+    return 2;
+
+  case '#':
+    if(len != 2)
+      return 0;
+
+    switch(bytes[1]) {
+      case '3': /* DECDHL top */
+        if(state->mode.leftrightmargin)
+          break;
+        set_lineinfo(state, state->pos.row, NO_FORCE, DWL_ON, DHL_TOP);
+        break;
+
+      case '4': /* DECDHL bottom */
+        if(state->mode.leftrightmargin)
+          break;
+        set_lineinfo(state, state->pos.row, NO_FORCE, DWL_ON, DHL_BOTTOM);
+        break;
+
+      case '5': /* DECSWL */
+        if(state->mode.leftrightmargin)
+          break;
+        set_lineinfo(state, state->pos.row, NO_FORCE, DWL_OFF, DHL_OFF);
+        break;
+
+      case '6': /* DECDWL */
+        if(state->mode.leftrightmargin)
+          break;
+        set_lineinfo(state, state->pos.row, NO_FORCE, DWL_ON, DHL_OFF);
+        break;
+
+      case '8': /* DECALN */
+      {
+        VTermPos pos;
+        uint32_t E[] = { 'E', 0 };
+        for(pos.row = 0; pos.row < state->rows; pos.row++)
+          for(pos.col = 0; pos.col < ROWWIDTH(state, pos.row); pos.col++)
+            putglyph(state, E, 1, pos);
+        break;
+      }
+
+      default:
+        return 0;
+    }
+    return 2;
+
+  case '(': case ')': case '*': case '+': /* SCS */
+    if(len != 2)
+      return 0;
+
+    {
+      int setnum = bytes[0] - 0x28;
+      VTermEncoding *newenc = vterm_lookup_encoding(ENC_SINGLE_94, bytes[1]);
+
+      if(newenc) {
+        state->encoding[setnum].enc = newenc;
+
+        if(newenc->init)
+          (*newenc->init)(newenc, state->encoding[setnum].data);
+      }
+    }
+
+    return 2;
+
+  case '7': /* DECSC */
+    savecursor(state, 1);
+    return 1;
+
+  case '8': /* DECRC */
+    savecursor(state, 0);
+    return 1;
+
+  case '<': /* Ignored by VT100. Used in VT52 mode to switch up to VT100 */
+    return 1;
+
+  case '=': /* DECKPAM */
+    state->mode.keypad = 1;
+    return 1;
+
+  case '>': /* DECKPNM */
+    state->mode.keypad = 0;
+    return 1;
+
+  case 'c': /* RIS - ECMA-48 8.3.105 */
+  {
+    VTermPos oldpos = state->pos;
+    vterm_state_reset(state, 1);
+    if(state->callbacks && state->callbacks->movecursor)
+      (*state->callbacks->movecursor)(state->pos, oldpos, state->mode.cursor_visible, state->cbdata);
+    return 1;
+  }
+
+  case 'n': /* LS2 - ECMA-48 8.3.78 */
+    state->gl_set = 2;
+    return 1;
+
+  case 'o': /* LS3 - ECMA-48 8.3.80 */
+    state->gl_set = 3;
+    return 1;
+
+  case '~': /* LS1R - ECMA-48 8.3.77 */
+    state->gr_set = 1;
+    return 1;
+
+  case '}': /* LS2R - ECMA-48 8.3.79 */
+    state->gr_set = 2;
+    return 1;
+
+  case '|': /* LS3R - ECMA-48 8.3.81 */
+    state->gr_set = 3;
+    return 1;
+
+  default:
+    return 0;
+  }
+}
+
+static void set_mode(VTermState *state, int num, int val)
+{
+  switch(num) {
+  case 4: /* IRM - ECMA-48 7.2.10 */
+    state->mode.insert = val;
+    break;
+
+  case 20: /* LNM - ANSI X3.4-1977 */
+    state->mode.newline = val;
+    break;
+
+  default:
+    DEBUG_LOG1("libvterm: Unknown mode %d\n", num);
+    return;
+  }
+}
+
+static void set_dec_mode(VTermState *state, int num, int val)
+{
+  switch(num) {
+  case 1:
+    state->mode.cursor = val;
+    break;
+
+  case 5: /* DECSCNM - screen mode */
+    settermprop_bool(state, VTERM_PROP_REVERSE, val);
+    break;
+
+  case 6: /* DECOM - origin mode */
+    {
+      VTermPos oldpos = state->pos;
+      state->mode.origin = val;
+      state->pos.row = state->mode.origin ? state->scrollregion_top : 0;
+      state->pos.col = state->mode.origin ? SCROLLREGION_LEFT(state) : 0;
+      updatecursor(state, &oldpos, 1);
+    }
+    break;
+
+  case 7:
+    state->mode.autowrap = val;
+    break;
+
+  case 12:
+    settermprop_bool(state, VTERM_PROP_CURSORBLINK, val);
+    break;
+
+  case 25:
+    settermprop_bool(state, VTERM_PROP_CURSORVISIBLE, val);
+    break;
+
+  case 69: /* DECVSSM - vertical split screen mode */
+           /* DECLRMM - left/right margin mode */
+    state->mode.leftrightmargin = val;
+    if(val) {
+      int row;
+
+      /* Setting DECVSSM must clear doublewidth/doubleheight state of every line */
+      for(row = 0; row < state->rows; row++)
+        set_lineinfo(state, row, FORCE, DWL_OFF, DHL_OFF);
+    }
+
+    break;
+
+  case 1000:
+  case 1002:
+  case 1003:
+    settermprop_int(state, VTERM_PROP_MOUSE,
+        !val          ? VTERM_PROP_MOUSE_NONE  :
+        (num == 1000) ? VTERM_PROP_MOUSE_CLICK :
+        (num == 1002) ? VTERM_PROP_MOUSE_DRAG  :
+                        VTERM_PROP_MOUSE_MOVE);
+    break;
+
+  case 1005:
+    state->mouse_protocol = val ? MOUSE_UTF8 : MOUSE_X10;
+    break;
+
+  case 1006:
+    state->mouse_protocol = val ? MOUSE_SGR : MOUSE_X10;
+    break;
+
+  case 1015:
+    state->mouse_protocol = val ? MOUSE_RXVT : MOUSE_X10;
+    break;
+
+  case 1047:
+    settermprop_bool(state, VTERM_PROP_ALTSCREEN, val);
+    break;
+
+  case 1048:
+    savecursor(state, val);
+    break;
+
+  case 1049:
+    settermprop_bool(state, VTERM_PROP_ALTSCREEN, val);
+    savecursor(state, val);
+    break;
+
+  case 2004:
+    state->mode.bracketpaste = val;
+    break;
+
+  default:
+    DEBUG_LOG1("libvterm: Unknown DEC mode %d\n", num);
+    return;
+  }
+}
+
+static void request_dec_mode(VTermState *state, int num)
+{
+  int reply;
+
+  switch(num) {
+    case 1:
+      reply = state->mode.cursor;
+      break;
+
+    case 5:
+      reply = state->mode.screen;
+      break;
+
+    case 6:
+      reply = state->mode.origin;
+      break;
+
+    case 7:
+      reply = state->mode.autowrap;
+      break;
+
+    case 12:
+      reply = state->mode.cursor_blink;
+      break;
+
+    case 25:
+      reply = state->mode.cursor_visible;
+      break;
+
+    case 69:
+      reply = state->mode.leftrightmargin;
+      break;
+
+    case 1000:
+      reply = state->mouse_flags == MOUSE_WANT_CLICK;
+      break;
+
+    case 1002:
+      reply = state->mouse_flags == (MOUSE_WANT_CLICK|MOUSE_WANT_DRAG);
+      break;
+
+    case 1003:
+      reply = state->mouse_flags == (MOUSE_WANT_CLICK|MOUSE_WANT_MOVE);
+      break;
+
+    case 1005:
+      reply = state->mouse_protocol == MOUSE_UTF8;
+      break;
+
+    case 1006:
+      reply = state->mouse_protocol == MOUSE_SGR;
+      break;
+
+    case 1015:
+      reply = state->mouse_protocol == MOUSE_RXVT;
+      break;
+
+    case 1047:
+      reply = state->mode.alt_screen;
+      break;
+
+    case 2004:
+      reply = state->mode.bracketpaste;
+
+    default:
+      vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "?%d;%d$y", num, 0);
+      return;
+  }
+
+  vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "?%d;%d$y", num, reply ? 1 : 2);
+}
+
+static int on_csi(const char *leader, const long args[], int argcount, const char *intermed, char command, void *user)
+{
+  VTermState *state = user;
+  int leader_byte = 0;
+  int intermed_byte = 0;
+  VTermPos oldpos = state->pos;
+
+  /* Some temporaries for later code */
+  int count, val;
+  int row, col;
+  VTermRect rect;
+  int selective;
+
+  if(leader && leader[0]) {
+    if(leader[1]) /* longer than 1 char */
+      return 0;
+
+    switch(leader[0]) {
+    case '?':
+    case '>':
+      leader_byte = leader[0];
+      break;
+    default:
+      return 0;
+    }
+  }
+
+  if(intermed && intermed[0]) {
+    if(intermed[1]) /* longer than 1 char */
+      return 0;
+
+    switch(intermed[0]) {
+    case ' ':
+    case '"':
+    case '$':
+    case '\'':
+      intermed_byte = intermed[0];
+      break;
+    default:
+      return 0;
+    }
+  }
+
+  oldpos = state->pos;
+
+#define LBOUND(v,min) if((v) < (min)) (v) = (min)
+#define UBOUND(v,max) if((v) > (max)) (v) = (max)
+
+#define LEADER(l,b) ((l << 8) | b)
+#define INTERMED(i,b) ((i << 16) | b)
+
+  switch(intermed_byte << 16 | leader_byte << 8 | command) {
+  case 0x40: /* ICH - ECMA-48 8.3.64 */
+    count = CSI_ARG_COUNT(args[0]);
+
+    if(!is_cursor_in_scrollregion(state))
+      break;
+
+    rect.start_row = state->pos.row;
+    rect.end_row   = state->pos.row + 1;
+    rect.start_col = state->pos.col;
+    if(state->mode.leftrightmargin)
+      rect.end_col = SCROLLREGION_RIGHT(state);
+    else
+      rect.end_col = THISROWWIDTH(state);
+
+    scroll(state, rect, 0, -count);
+
+    break;
+
+  case 0x41: /* CUU - ECMA-48 8.3.22 */
+    count = CSI_ARG_COUNT(args[0]);
+    state->pos.row -= count;
+    state->at_phantom = 0;
+    break;
+
+  case 0x42: /* CUD - ECMA-48 8.3.19 */
+    count = CSI_ARG_COUNT(args[0]);
+    state->pos.row += count;
+    state->at_phantom = 0;
+    break;
+
+  case 0x43: /* CUF - ECMA-48 8.3.20 */
+    count = CSI_ARG_COUNT(args[0]);
+    state->pos.col += count;
+    state->at_phantom = 0;
+    break;
+
+  case 0x44: /* CUB - ECMA-48 8.3.18 */
+    count = CSI_ARG_COUNT(args[0]);
+    state->pos.col -= count;
+    state->at_phantom = 0;
+    break;
+
+  case 0x45: /* CNL - ECMA-48 8.3.12 */
+    count = CSI_ARG_COUNT(args[0]);
+    state->pos.col = 0;
+    state->pos.row += count;
+    state->at_phantom = 0;
+    break;
+
+  case 0x46: /* CPL - ECMA-48 8.3.13 */
+    count = CSI_ARG_COUNT(args[0]);
+    state->pos.col = 0;
+    state->pos.row -= count;
+    state->at_phantom = 0;
+    break;
+
+  case 0x47: /* CHA - ECMA-48 8.3.9 */
+    val = CSI_ARG_OR(args[0], 1);
+    state->pos.col = val-1;
+    state->at_phantom = 0;
+    break;
+
+  case 0x48: /* CUP - ECMA-48 8.3.21 */
+    row = CSI_ARG_OR(args[0], 1);
+    col = argcount < 2 || CSI_ARG_IS_MISSING(args[1]) ? 1 : CSI_ARG(args[1]);
+    /* zero-based */
+    state->pos.row = row-1;
+    state->pos.col = col-1;
+    if(state->mode.origin) {
+      state->pos.row += state->scrollregion_top;
+      state->pos.col += SCROLLREGION_LEFT(state);
+    }
+    state->at_phantom = 0;
+    break;
+
+  case 0x49: /* CHT - ECMA-48 8.3.10 */
+    count = CSI_ARG_COUNT(args[0]);
+    tab(state, count, +1);
+    break;
+
+  case 0x4a: /* ED - ECMA-48 8.3.39 */
+  case LEADER('?', 0x4a): /* DECSED - Selective Erase in Display */
+    selective = (leader_byte == '?');
+    switch(CSI_ARG(args[0])) {
+    case CSI_ARG_MISSING:
+    case 0:
+      rect.start_row = state->pos.row; rect.end_row = state->pos.row + 1;
+      rect.start_col = state->pos.col; rect.end_col = state->cols;
+      if(rect.end_col > rect.start_col)
+        erase(state, rect, selective);
+
+      rect.start_row = state->pos.row + 1; rect.end_row = state->rows;
+      rect.start_col = 0;
+      for(row = rect.start_row; row < rect.end_row; row++)
+	set_lineinfo(state, row, FORCE, DWL_OFF, DHL_OFF);
+      if(rect.end_row > rect.start_row)
+        erase(state, rect, selective);
+      break;
+
+    case 1:
+      rect.start_row = 0; rect.end_row = state->pos.row;
+      rect.start_col = 0; rect.end_col = state->cols;
+      for(row = rect.start_row; row < rect.end_row; row++)
+	set_lineinfo(state, row, FORCE, DWL_OFF, DHL_OFF);
+      if(rect.end_col > rect.start_col)
+        erase(state, rect, selective);
+
+      rect.start_row = state->pos.row; rect.end_row = state->pos.row + 1;
+                          rect.end_col = state->pos.col + 1;
+      if(rect.end_row > rect.start_row)
+        erase(state, rect, selective);
+      break;
+
+    case 2:
+      rect.start_row = 0; rect.end_row = state->rows;
+      rect.start_col = 0; rect.end_col = state->cols;
+      for(row = rect.start_row; row < rect.end_row; row++)
+	set_lineinfo(state, row, FORCE, DWL_OFF, DHL_OFF);
+      erase(state, rect, selective);
+      break;
+    }
+    break;
+
+  case 0x4b: /* EL - ECMA-48 8.3.41 */
+  case LEADER('?', 0x4b): /* DECSEL - Selective Erase in Line */
+    selective = (leader_byte == '?');
+    rect.start_row = state->pos.row;
+    rect.end_row   = state->pos.row + 1;
+
+    switch(CSI_ARG(args[0])) {
+    case CSI_ARG_MISSING:
+    case 0:
+      rect.start_col = state->pos.col; rect.end_col = THISROWWIDTH(state); break;
+    case 1:
+      rect.start_col = 0; rect.end_col = state->pos.col + 1; break;
+    case 2:
+      rect.start_col = 0; rect.end_col = THISROWWIDTH(state); break;
+    default:
+      return 0;
+    }
+
+    if(rect.end_col > rect.start_col)
+      erase(state, rect, selective);
+
+    break;
+
+  case 0x4c: /* IL - ECMA-48 8.3.67 */
+    count = CSI_ARG_COUNT(args[0]);
+
+    if(!is_cursor_in_scrollregion(state))
+      break;
+
+    rect.start_row = state->pos.row;
+    rect.end_row   = SCROLLREGION_BOTTOM(state);
+    rect.start_col = SCROLLREGION_LEFT(state);
+    rect.end_col   = SCROLLREGION_RIGHT(state);
+
+    scroll(state, rect, -count, 0);
+
+    break;
+
+  case 0x4d: /* DL - ECMA-48 8.3.32 */
+    count = CSI_ARG_COUNT(args[0]);
+
+    if(!is_cursor_in_scrollregion(state))
+      break;
+
+    rect.start_row = state->pos.row;
+    rect.end_row   = SCROLLREGION_BOTTOM(state);
+    rect.start_col = SCROLLREGION_LEFT(state);
+    rect.end_col   = SCROLLREGION_RIGHT(state);
+
+    scroll(state, rect, count, 0);
+
+    break;
+
+  case 0x50: /* DCH - ECMA-48 8.3.26 */
+    count = CSI_ARG_COUNT(args[0]);
+
+    if(!is_cursor_in_scrollregion(state))
+      break;
+
+    rect.start_row = state->pos.row;
+    rect.end_row   = state->pos.row + 1;
+    rect.start_col = state->pos.col;
+    if(state->mode.leftrightmargin)
+      rect.end_col = SCROLLREGION_RIGHT(state);
+    else
+      rect.end_col = THISROWWIDTH(state);
+
+    scroll(state, rect, 0, count);
+
+    break;
+
+  case 0x53: /* SU - ECMA-48 8.3.147 */
+    count = CSI_ARG_COUNT(args[0]);
+
+    rect.start_row = state->scrollregion_top;
+    rect.end_row   = SCROLLREGION_BOTTOM(state);
+    rect.start_col = SCROLLREGION_LEFT(state);
+    rect.end_col   = SCROLLREGION_RIGHT(state);
+
+    scroll(state, rect, count, 0);
+
+    break;
+
+  case 0x54: /* SD - ECMA-48 8.3.113 */
+    count = CSI_ARG_COUNT(args[0]);
+
+    rect.start_row = state->scrollregion_top;
+    rect.end_row   = SCROLLREGION_BOTTOM(state);
+    rect.start_col = SCROLLREGION_LEFT(state);
+    rect.end_col   = SCROLLREGION_RIGHT(state);
+
+    scroll(state, rect, -count, 0);
+
+    break;
+
+  case 0x58: /* ECH - ECMA-48 8.3.38 */
+    count = CSI_ARG_COUNT(args[0]);
+
+    rect.start_row = state->pos.row;
+    rect.end_row   = state->pos.row + 1;
+    rect.start_col = state->pos.col;
+    rect.end_col   = state->pos.col + count;
+    UBOUND(rect.end_col, THISROWWIDTH(state));
+
+    erase(state, rect, 0);
+    break;
+
+  case 0x5a: /* CBT - ECMA-48 8.3.7 */
+    count = CSI_ARG_COUNT(args[0]);
+    tab(state, count, -1);
+    break;
+
+  case 0x60: /* HPA - ECMA-48 8.3.57 */
+    col = CSI_ARG_OR(args[0], 1);
+    state->pos.col = col-1;
+    state->at_phantom = 0;
+    break;
+
+  case 0x61: /* HPR - ECMA-48 8.3.59 */
+    count = CSI_ARG_COUNT(args[0]);
+    state->pos.col += count;
+    state->at_phantom = 0;
+    break;
+
+  case 0x63: /* DA - ECMA-48 8.3.24 */
+    val = CSI_ARG_OR(args[0], 0);
+    if(val == 0)
+      /* DEC VT100 response */
+      vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "?1;2c");
+    break;
+
+  case LEADER('>', 0x63): /* DEC secondary Device Attributes */
+    vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, ">%d;%d;%dc", 0, 100, 0);
+    break;
+
+  case 0x64: /* VPA - ECMA-48 8.3.158 */
+    row = CSI_ARG_OR(args[0], 1);
+    state->pos.row = row-1;
+    if(state->mode.origin)
+      state->pos.row += state->scrollregion_top;
+    state->at_phantom = 0;
+    break;
+
+  case 0x65: /* VPR - ECMA-48 8.3.160 */
+    count = CSI_ARG_COUNT(args[0]);
+    state->pos.row += count;
+    state->at_phantom = 0;
+    break;
+
+  case 0x66: /* HVP - ECMA-48 8.3.63 */
+    row = CSI_ARG_OR(args[0], 1);
+    col = argcount < 2 || CSI_ARG_IS_MISSING(args[1]) ? 1 : CSI_ARG(args[1]);
+    /* zero-based */
+    state->pos.row = row-1;
+    state->pos.col = col-1;
+    if(state->mode.origin) {
+      state->pos.row += state->scrollregion_top;
+      state->pos.col += SCROLLREGION_LEFT(state);
+    }
+    state->at_phantom = 0;
+    break;
+
+  case 0x67: /* TBC - ECMA-48 8.3.154 */
+    val = CSI_ARG_OR(args[0], 0);
+
+    switch(val) {
+    case 0:
+      clear_col_tabstop(state, state->pos.col);
+      break;
+    case 3:
+    case 5:
+      for(col = 0; col < state->cols; col++)
+        clear_col_tabstop(state, col);
+      break;
+    case 1:
+    case 2:
+    case 4:
+      break;
+    /* TODO: 1, 2 and 4 aren't meaningful yet without line tab stops */
+    default:
+      return 0;
+    }
+    break;
+
+  case 0x68: /* SM - ECMA-48 8.3.125 */
+    if(!CSI_ARG_IS_MISSING(args[0]))
+      set_mode(state, CSI_ARG(args[0]), 1);
+    break;
+
+  case LEADER('?', 0x68): /* DEC private mode set */
+    if(!CSI_ARG_IS_MISSING(args[0]))
+      set_dec_mode(state, CSI_ARG(args[0]), 1);
+    break;
+
+  case 0x6a: /* HPB - ECMA-48 8.3.58 */
+    count = CSI_ARG_COUNT(args[0]);
+    state->pos.col -= count;
+    state->at_phantom = 0;
+    break;
+
+  case 0x6b: /* VPB - ECMA-48 8.3.159 */
+    count = CSI_ARG_COUNT(args[0]);
+    state->pos.row -= count;
+    state->at_phantom = 0;
+    break;
+
+  case 0x6c: /* RM - ECMA-48 8.3.106 */
+    if(!CSI_ARG_IS_MISSING(args[0]))
+      set_mode(state, CSI_ARG(args[0]), 0);
+    break;
+
+  case LEADER('?', 0x6c): /* DEC private mode reset */
+    if(!CSI_ARG_IS_MISSING(args[0]))
+      set_dec_mode(state, CSI_ARG(args[0]), 0);
+    break;
+
+  case 0x6d: /* SGR - ECMA-48 8.3.117 */
+    vterm_state_setpen(state, args, argcount);
+    break;
+
+  case 0x6e: /* DSR - ECMA-48 8.3.35 */
+  case LEADER('?', 0x6e): /* DECDSR */
+    val = CSI_ARG_OR(args[0], 0);
+
+    {
+      char *qmark = (leader_byte == '?') ? "?" : "";
+
+      switch(val) {
+      case 0: case 1: case 2: case 3: case 4:
+        /* ignore - these are replies */
+        break;
+      case 5:
+        vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "%s0n", qmark);
+        break;
+      case 6: /* CPR - cursor position report */
+        vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "%s%d;%dR", qmark, state->pos.row + 1, state->pos.col + 1);
+        break;
+      }
+    }
+    break;
+
+
+  case LEADER('!', 0x70): /* DECSTR - DEC soft terminal reset */
+    vterm_state_reset(state, 0);
+    break;
+
+  case LEADER('?', INTERMED('$', 0x70)):
+    request_dec_mode(state, CSI_ARG(args[0]));
+    break;
+
+  case INTERMED(' ', 0x71): /* DECSCUSR - DEC set cursor shape */
+    val = CSI_ARG_OR(args[0], 1);
+
+    switch(val) {
+    case 0: case 1:
+      settermprop_bool(state, VTERM_PROP_CURSORBLINK, 1);
+      settermprop_int (state, VTERM_PROP_CURSORSHAPE, VTERM_PROP_CURSORSHAPE_BLOCK);
+      break;
+    case 2:
+      settermprop_bool(state, VTERM_PROP_CURSORBLINK, 0);
+      settermprop_int (state, VTERM_PROP_CURSORSHAPE, VTERM_PROP_CURSORSHAPE_BLOCK);
+      break;
+    case 3:
+      settermprop_bool(state, VTERM_PROP_CURSORBLINK, 1);
+      settermprop_int (state, VTERM_PROP_CURSORSHAPE, VTERM_PROP_CURSORSHAPE_UNDERLINE);
+      break;
+    case 4:
+      settermprop_bool(state, VTERM_PROP_CURSORBLINK, 0);
+      settermprop_int (state, VTERM_PROP_CURSORSHAPE, VTERM_PROP_CURSORSHAPE_UNDERLINE);
+      break;
+    case 5:
+      settermprop_bool(state, VTERM_PROP_CURSORBLINK, 1);
+      settermprop_int (state, VTERM_PROP_CURSORSHAPE, VTERM_PROP_CURSORSHAPE_BAR_LEFT);
+      break;
+    case 6:
+      settermprop_bool(state, VTERM_PROP_CURSORBLINK, 0);
+      settermprop_int (state, VTERM_PROP_CURSORSHAPE, VTERM_PROP_CURSORSHAPE_BAR_LEFT);
+      break;
+    }
+
+    break;
+
+  case INTERMED('"', 0x71): /* DECSCA - DEC select character protection attribute */
+    val = CSI_ARG_OR(args[0], 0);
+
+    switch(val) {
+    case 0: case 2:
+      state->protected_cell = 0;
+      break;
+    case 1:
+      state->protected_cell = 1;
+      break;
+    }
+
+    break;
+
+  case 0x72: /* DECSTBM - DEC custom */
+    state->scrollregion_top = CSI_ARG_OR(args[0], 1) - 1;
+    state->scrollregion_bottom = argcount < 2 || CSI_ARG_IS_MISSING(args[1]) ? -1 : CSI_ARG(args[1]);
+    LBOUND(state->scrollregion_top, 0);
+    UBOUND(state->scrollregion_top, state->rows);
+    LBOUND(state->scrollregion_bottom, -1);
+    if(state->scrollregion_top == 0 && state->scrollregion_bottom == state->rows)
+      state->scrollregion_bottom = -1;
+    else
+      UBOUND(state->scrollregion_bottom, state->rows);
+
+    if(SCROLLREGION_BOTTOM(state) <= state->scrollregion_top) {
+      /* Invalid */
+      state->scrollregion_top    = 0;
+      state->scrollregion_bottom = -1;
+    }
+
+    break;
+
+  case 0x73: /* DECSLRM - DEC custom */
+    /* Always allow setting these margins, just they won't take effect without DECVSSM */
+    state->scrollregion_left = CSI_ARG_OR(args[0], 1) - 1;
+    state->scrollregion_right = argcount < 2 || CSI_ARG_IS_MISSING(args[1]) ? -1 : CSI_ARG(args[1]);
+    LBOUND(state->scrollregion_left, 0);
+    UBOUND(state->scrollregion_left, state->cols);
+    LBOUND(state->scrollregion_right, -1);
+    if(state->scrollregion_left == 0 && state->scrollregion_right == state->cols)
+      state->scrollregion_right = -1;
+    else
+      UBOUND(state->scrollregion_right, state->cols);
+
+    if(state->scrollregion_right > -1 &&
+       state->scrollregion_right <= state->scrollregion_left) {
+      /* Invalid */
+      state->scrollregion_left  = 0;
+      state->scrollregion_right = -1;
+    }
+
+    break;
+
+  case INTERMED('\'', 0x7D): /* DECIC */
+    count = CSI_ARG_COUNT(args[0]);
+
+    if(!is_cursor_in_scrollregion(state))
+      break;
+
+    rect.start_row = state->scrollregion_top;
+    rect.end_row   = SCROLLREGION_BOTTOM(state);
+    rect.start_col = state->pos.col;
+    rect.end_col   = SCROLLREGION_RIGHT(state);
+
+    scroll(state, rect, 0, -count);
+
+    break;
+
+  case INTERMED('\'', 0x7E): /* DECDC */
+    count = CSI_ARG_COUNT(args[0]);
+
+    if(!is_cursor_in_scrollregion(state))
+      break;
+
+    rect.start_row = state->scrollregion_top;
+    rect.end_row   = SCROLLREGION_BOTTOM(state);
+    rect.start_col = state->pos.col;
+    rect.end_col   = SCROLLREGION_RIGHT(state);
+
+    scroll(state, rect, 0, count);
+
+    break;
+
+  default:
+    if(state->fallbacks && state->fallbacks->csi)
+      if((*state->fallbacks->csi)(leader, args, argcount, intermed, command, state->fbdata))
+        return 1;
+
+    return 0;
+  }
+
+  if(state->mode.origin) {
+    LBOUND(state->pos.row, state->scrollregion_top);
+    UBOUND(state->pos.row, SCROLLREGION_BOTTOM(state)-1);
+    LBOUND(state->pos.col, SCROLLREGION_LEFT(state));
+    UBOUND(state->pos.col, SCROLLREGION_RIGHT(state)-1);
+  }
+  else {
+    LBOUND(state->pos.row, 0);
+    UBOUND(state->pos.row, state->rows-1);
+    LBOUND(state->pos.col, 0);
+    UBOUND(state->pos.col, THISROWWIDTH(state)-1);
+  }
+
+  updatecursor(state, &oldpos, 1);
+
+#ifdef DEBUG
+  if(state->pos.row < 0 || state->pos.row >= state->rows ||
+     state->pos.col < 0 || state->pos.col >= state->cols) {
+    fprintf(stderr, "Position out of bounds after CSI %c: (%d,%d)\n",
+        command, state->pos.row, state->pos.col);
+    abort();
+  }
+
+  if(SCROLLREGION_BOTTOM(state) <= state->scrollregion_top) {
+    fprintf(stderr, "Scroll region height out of bounds after CSI %c: %d <= %d\n",
+        command, SCROLLREGION_BOTTOM(state), state->scrollregion_top);
+    abort();
+  }
+
+  if(SCROLLREGION_RIGHT(state) <= SCROLLREGION_LEFT(state)) {
+    fprintf(stderr, "Scroll region width out of bounds after CSI %c: %d <= %d\n",
+        command, SCROLLREGION_RIGHT(state), SCROLLREGION_LEFT(state));
+    abort();
+  }
+#endif
+
+  return 1;
+}
+
+static int on_osc(const char *command, size_t cmdlen, void *user)
+{
+  VTermState *state = user;
+
+  if(cmdlen < 2)
+    return 0;
+
+  if(strneq(command, "0;", 2)) {
+    settermprop_string(state, VTERM_PROP_ICONNAME, command + 2, cmdlen - 2);
+    settermprop_string(state, VTERM_PROP_TITLE, command + 2, cmdlen - 2);
+    return 1;
+  }
+  else if(strneq(command, "1;", 2)) {
+    settermprop_string(state, VTERM_PROP_ICONNAME, command + 2, cmdlen - 2);
+    return 1;
+  }
+  else if(strneq(command, "2;", 2)) {
+    settermprop_string(state, VTERM_PROP_TITLE, command + 2, cmdlen - 2);
+    return 1;
+  }
+  else if(state->fallbacks && state->fallbacks->osc)
+    if((*state->fallbacks->osc)(command, cmdlen, state->fbdata))
+      return 1;
+
+  return 0;
+}
+
+static void request_status_string(VTermState *state, const char *command, size_t cmdlen)
+{
+  if(cmdlen == 1)
+    switch(command[0]) {
+      case 'm': /* Query SGR */
+        {
+          long args[20];
+          int argc = vterm_state_getpen(state, args, sizeof(args)/sizeof(args[0]));
+	  int argi;
+          vterm_push_output_sprintf_ctrl(state->vt, C1_DCS, "1$r");
+          for(argi = 0; argi < argc; argi++)
+            vterm_push_output_sprintf(state->vt,
+                argi == argc - 1             ? "%d" :
+                CSI_ARG_HAS_MORE(args[argi]) ? "%d:" :
+                                               "%d;",
+                CSI_ARG(args[argi]));
+          vterm_push_output_sprintf(state->vt, "m");
+          vterm_push_output_sprintf_ctrl(state->vt, C1_ST, "");
+        }
+        return;
+      case 'r': /* Query DECSTBM */
+        vterm_push_output_sprintf_dcs(state->vt, "1$r%d;%dr", state->scrollregion_top+1, SCROLLREGION_BOTTOM(state));
+        return;
+      case 's': /* Query DECSLRM */
+        vterm_push_output_sprintf_dcs(state->vt, "1$r%d;%ds", SCROLLREGION_LEFT(state)+1, SCROLLREGION_RIGHT(state));
+        return;
+    }
+
+  if(cmdlen == 2) {
+    if(strneq(command, " q", 2)) {
+      int reply;
+      switch(state->mode.cursor_shape) {
+        case VTERM_PROP_CURSORSHAPE_BLOCK:     reply = 2; break;
+        case VTERM_PROP_CURSORSHAPE_UNDERLINE: reply = 4; break;
+        case VTERM_PROP_CURSORSHAPE_BAR_LEFT:  reply = 6; break;
+      }
+      if(state->mode.cursor_blink)
+        reply--;
+      vterm_push_output_sprintf_dcs(state->vt, "1$r%d q", reply);
+      return;
+    }
+    else if(strneq(command, "\"q", 2)) {
+      vterm_push_output_sprintf_dcs(state->vt, "1$r%d\"q", state->protected_cell ? 1 : 2);
+      return;
+    }
+  }
+
+  vterm_push_output_sprintf_dcs(state->vt, "0$r%.s", (int)cmdlen, command);
+}
+
+static int on_dcs(const char *command, size_t cmdlen, void *user)
+{
+  VTermState *state = user;
+
+  if(cmdlen >= 2 && strneq(command, "$q", 2)) {
+    request_status_string(state, command+2, cmdlen-2);
+    return 1;
+  }
+  else if(state->fallbacks && state->fallbacks->dcs)
+    if((*state->fallbacks->dcs)(command, cmdlen, state->fbdata))
+      return 1;
+
+  return 0;
+}
+
+static int on_resize(int rows, int cols, void *user)
+{
+  VTermState *state = user;
+  VTermPos oldpos = state->pos;
+  VTermPos delta = { 0, 0 };
+
+  if(cols != state->cols) {
+    unsigned char *newtabstops = vterm_allocator_malloc(state->vt, (cols + 7) / 8);
+
+    /* TODO: This can all be done much more efficiently bytewise */
+    int col;
+    for(col = 0; col < state->cols && col < cols; col++) {
+      unsigned char mask = 1 << (col & 7);
+      if(state->tabstops[col >> 3] & mask)
+        newtabstops[col >> 3] |= mask;
+      else
+        newtabstops[col >> 3] &= ~mask;
+      }
+
+    for( ; col < cols; col++) {
+      unsigned char mask = 1 << (col & 7);
+      if(col % 8 == 0)
+        newtabstops[col >> 3] |= mask;
+      else
+        newtabstops[col >> 3] &= ~mask;
+    }
+
+    vterm_allocator_free(state->vt, state->tabstops);
+    state->tabstops = newtabstops;
+  }
+
+  if(rows != state->rows) {
+    VTermLineInfo *newlineinfo = vterm_allocator_malloc(state->vt, rows * sizeof(VTermLineInfo));
+
+    int row;
+    for(row = 0; row < state->rows && row < rows; row++) {
+      newlineinfo[row] = state->lineinfo[row];
+    }
+
+    for( ; row < rows; row++) {
+      newlineinfo[row].doublewidth = 0;
+      newlineinfo[row].doubleheight = 0;
+    }
+
+    vterm_allocator_free(state->vt, state->lineinfo);
+    state->lineinfo = newlineinfo;
+  }
+
+  state->rows = rows;
+  state->cols = cols;
+
+  if(state->scrollregion_bottom > -1)
+    UBOUND(state->scrollregion_bottom, state->rows);
+  if(state->scrollregion_right > -1)
+    UBOUND(state->scrollregion_right, state->cols);
+
+  if(state->callbacks && state->callbacks->resize)
+    (*state->callbacks->resize)(rows, cols, &delta, state->cbdata);
+
+  if(state->at_phantom && state->pos.col < cols-1) {
+    state->at_phantom = 0;
+    state->pos.col++;
+  }
+
+  state->pos.row += delta.row;
+  state->pos.col += delta.col;
+
+  if(state->pos.row >= rows)
+    state->pos.row = rows - 1;
+  if(state->pos.col >= cols)
+    state->pos.col = cols - 1;
+
+  updatecursor(state, &oldpos, 1);
+
+  return 1;
+}
+
+static const VTermParserCallbacks parser_callbacks = {
+  on_text, /* text */
+  on_control, /* control */
+  on_escape, /* escape */
+  on_csi, /* csi */
+  on_osc, /* osc */
+  on_dcs, /* dcs */
+  on_resize /* resize */
+};
+
+VTermState *vterm_obtain_state(VTerm *vt)
+{
+  VTermState *state;
+  if(vt->state)
+    return vt->state;
+
+  state = vterm_state_new(vt);
+  vt->state = state;
+
+  state->combine_chars_size = 16;
+  state->combine_chars = vterm_allocator_malloc(state->vt, state->combine_chars_size * sizeof(state->combine_chars[0]));
+
+  state->tabstops = vterm_allocator_malloc(state->vt, (state->cols + 7) / 8);
+
+  state->lineinfo = vterm_allocator_malloc(state->vt, state->rows * sizeof(VTermLineInfo));
+
+  state->encoding_utf8.enc = vterm_lookup_encoding(ENC_UTF8, 'u');
+  if(*state->encoding_utf8.enc->init)
+    (*state->encoding_utf8.enc->init)(state->encoding_utf8.enc, state->encoding_utf8.data);
+
+  vterm_parser_set_callbacks(vt, &parser_callbacks, state);
+
+  return state;
+}
+
+void vterm_state_reset(VTermState *state, int hard)
+{
+  VTermEncoding *default_enc;
+
+  state->scrollregion_top = 0;
+  state->scrollregion_bottom = -1;
+  state->scrollregion_left = 0;
+  state->scrollregion_right = -1;
+
+  state->mode.keypad          = 0;
+  state->mode.cursor          = 0;
+  state->mode.autowrap        = 1;
+  state->mode.insert          = 0;
+  state->mode.newline         = 0;
+  state->mode.alt_screen      = 0;
+  state->mode.origin          = 0;
+  state->mode.leftrightmargin = 0;
+  state->mode.bracketpaste    = 0;
+
+  state->vt->mode.ctrl8bit   = 0;
+
+  {
+    int col;
+    for(col = 0; col < state->cols; col++)
+      if(col % 8 == 0)
+	set_col_tabstop(state, col);
+      else
+	clear_col_tabstop(state, col);
+  }
+
+  {
+    int row;
+    for(row = 0; row < state->rows; row++)
+      set_lineinfo(state, row, FORCE, DWL_OFF, DHL_OFF);
+  }
+
+  if(state->callbacks && state->callbacks->initpen)
+    (*state->callbacks->initpen)(state->cbdata);
+
+  vterm_state_resetpen(state);
+
+  default_enc = state->vt->mode.utf8 ?
+      vterm_lookup_encoding(ENC_UTF8,      'u') :
+      vterm_lookup_encoding(ENC_SINGLE_94, 'B');
+
+  {
+    int i;
+    for(i = 0; i < 4; i++) {
+      state->encoding[i].enc = default_enc;
+      if(default_enc->init)
+	(*default_enc->init)(default_enc, state->encoding[i].data);
+    }
+  }
+
+  state->gl_set = 0;
+  state->gr_set = 1;
+  state->gsingle_set = 0;
+
+  state->protected_cell = 0;
+
+  /* Initialise the props */
+  settermprop_bool(state, VTERM_PROP_CURSORVISIBLE, 1);
+  settermprop_bool(state, VTERM_PROP_CURSORBLINK,   1);
+  settermprop_int (state, VTERM_PROP_CURSORSHAPE,   VTERM_PROP_CURSORSHAPE_BLOCK);
+
+  if(hard) {
+    VTermRect rect = { 0, 0, 0, 0 };
+
+    state->pos.row = 0;
+    state->pos.col = 0;
+    state->at_phantom = 0;
+
+    rect.end_row = state->rows;
+    rect.end_col =  state->cols;
+    erase(state, rect, 0);
+  }
+}
+
+void vterm_state_get_cursorpos(const VTermState *state, VTermPos *cursorpos)
+{
+  *cursorpos = state->pos;
+}
+
+void vterm_state_set_callbacks(VTermState *state, const VTermStateCallbacks *callbacks, void *user)
+{
+  if(callbacks) {
+    state->callbacks = callbacks;
+    state->cbdata = user;
+
+    if(state->callbacks && state->callbacks->initpen)
+      (*state->callbacks->initpen)(state->cbdata);
+  }
+  else {
+    state->callbacks = NULL;
+    state->cbdata = NULL;
+  }
+}
+
+void *vterm_state_get_cbdata(VTermState *state)
+{
+  return state->cbdata;
+}
+
+void vterm_state_set_unrecognised_fallbacks(VTermState *state, const VTermParserCallbacks *fallbacks, void *user)
+{
+  if(fallbacks) {
+    state->fallbacks = fallbacks;
+    state->fbdata = user;
+  }
+  else {
+    state->fallbacks = NULL;
+    state->fbdata = NULL;
+  }
+}
+
+void *vterm_state_get_unrecognised_fbdata(VTermState *state)
+{
+  return state->fbdata;
+}
+
+int vterm_state_set_termprop(VTermState *state, VTermProp prop, VTermValue *val)
+{
+  /* Only store the new value of the property if usercode said it was happy.
+   * This is especially important for altscreen switching */
+  if(state->callbacks && state->callbacks->settermprop)
+    if(!(*state->callbacks->settermprop)(prop, val, state->cbdata))
+      return 0;
+
+  switch(prop) {
+  case VTERM_PROP_TITLE:
+  case VTERM_PROP_ICONNAME:
+    /* we don't store these, just transparently pass through */
+    return 1;
+  case VTERM_PROP_CURSORVISIBLE:
+    state->mode.cursor_visible = val->boolean;
+    return 1;
+  case VTERM_PROP_CURSORBLINK:
+    state->mode.cursor_blink = val->boolean;
+    return 1;
+  case VTERM_PROP_CURSORSHAPE:
+    state->mode.cursor_shape = val->number;
+    return 1;
+  case VTERM_PROP_REVERSE:
+    state->mode.screen = val->boolean;
+    return 1;
+  case VTERM_PROP_ALTSCREEN:
+    state->mode.alt_screen = val->boolean;
+    if(state->mode.alt_screen) {
+      VTermRect rect = {0, 0, 0, 0};
+      rect.end_row = state->rows;
+      rect.end_col = state->cols;
+      erase(state, rect, 0);
+    }
+    return 1;
+  case VTERM_PROP_MOUSE:
+    state->mouse_flags = 0;
+    if(val->number)
+      state->mouse_flags |= MOUSE_WANT_CLICK;
+    if(val->number == VTERM_PROP_MOUSE_DRAG)
+      state->mouse_flags |= MOUSE_WANT_DRAG;
+    if(val->number == VTERM_PROP_MOUSE_MOVE)
+      state->mouse_flags |= MOUSE_WANT_MOVE;
+    return 1;
+  }
+
+  return 0;
+}
+
+const VTermLineInfo *vterm_state_get_lineinfo(const VTermState *state, int row)
+{
+  return state->lineinfo + row;
+}
new file mode 100644
--- /dev/null
+++ b/src/libvterm/src/unicode.c
@@ -0,0 +1,331 @@
+#include "vterm_internal.h"
+
+/* ### The following from http://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c
+ * With modifications:
+ *   made functions static
+ *   moved 'combining' table to file scope, so other functions can see it
+ * ###################################################################
+ */
+
+/*
+ * This is an implementation of wcwidth() and wcswidth() (defined in
+ * IEEE Std 1002.1-2001) for Unicode.
+ *
+ * http://www.opengroup.org/onlinepubs/007904975/functions/wcwidth.html
+ * http://www.opengroup.org/onlinepubs/007904975/functions/wcswidth.html
+ *
+ * In fixed-width output devices, Latin characters all occupy a single
+ * "cell" position of equal width, whereas ideographic CJK characters
+ * occupy two such cells. Interoperability between terminal-line
+ * applications and (teletype-style) character terminals using the
+ * UTF-8 encoding requires agreement on which character should advance
+ * the cursor by how many cell positions. No established formal
+ * standards exist at present on which Unicode character shall occupy
+ * how many cell positions on character terminals. These routines are
+ * a first attempt of defining such behavior based on simple rules
+ * applied to data provided by the Unicode Consortium.
+ *
+ * For some graphical characters, the Unicode standard explicitly
+ * defines a character-cell width via the definition of the East Asian
+ * FullWidth (F), Wide (W), Half-width (H), and Narrow (Na) classes.
+ * In all these cases, there is no ambiguity about which width a
+ * terminal shall use. For characters in the East Asian Ambiguous (A)
+ * class, the width choice depends purely on a preference of backward
+ * compatibility with either historic CJK or Western practice.
+ * Choosing single-width for these characters is easy to justify as
+ * the appropriate long-term solution, as the CJK practice of
+ * displaying these characters as double-width comes from historic
+ * implementation simplicity (8-bit encoded characters were displayed
+ * single-width and 16-bit ones double-width, even for Greek,
+ * Cyrillic, etc.) and not any typographic considerations.
+ *
+ * Much less clear is the choice of width for the Not East Asian
+ * (Neutral) class. Existing practice does not dictate a width for any
+ * of these characters. It would nevertheless make sense
+ * typographically to allocate two character cells to characters such
+ * as for instance EM SPACE or VOLUME INTEGRAL, which cannot be
+ * represented adequately with a single-width glyph. The following
+ * routines at present merely assign a single-cell width to all
+ * neutral characters, in the interest of simplicity. This is not
+ * entirely satisfactory and should be reconsidered before
+ * establishing a formal standard in this area. At the moment, the
+ * decision which Not East Asian (Neutral) characters should be
+ * represented by double-width glyphs cannot yet be answered by
+ * applying a simple rule from the Unicode database content. Setting
+ * up a proper standard for the behavior of UTF-8 character terminals
+ * will require a careful analysis not only of each Unicode character,
+ * but also of each presentation form, something the author of these
+ * routines has avoided to do so far.
+ *
+ * http://www.unicode.org/unicode/reports/tr11/
+ *
+ * Markus Kuhn -- 2007-05-26 (Unicode 5.0)
+ *
+ * Permission to use, copy, modify, and distribute this software
+ * for any purpose and without fee is hereby granted. The author
+ * disclaims all warranties with regard to this software.
+ *
+ * Latest version: http://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c
+ */
+
+struct interval {
+  int first;
+  int last;
+};
+
+/* sorted list of non-overlapping intervals of non-spacing characters */
+/* generated by "uniset +cat=Me +cat=Mn +cat=Cf -00AD +1160-11FF +200B c" */
+static const struct interval combining[] = {
+  { 0x0300, 0x036F }, { 0x0483, 0x0486 }, { 0x0488, 0x0489 },
+  { 0x0591, 0x05BD }, { 0x05BF, 0x05BF }, { 0x05C1, 0x05C2 },
+  { 0x05C4, 0x05C5 }, { 0x05C7, 0x05C7 }, { 0x0600, 0x0603 },
+  { 0x0610, 0x0615 }, { 0x064B, 0x065E }, { 0x0670, 0x0670 },
+  { 0x06D6, 0x06E4 }, { 0x06E7, 0x06E8 }, { 0x06EA, 0x06ED },
+  { 0x070F, 0x070F }, { 0x0711, 0x0711 }, { 0x0730, 0x074A },
+  { 0x07A6, 0x07B0 }, { 0x07EB, 0x07F3 }, { 0x0901, 0x0902 },
+  { 0x093C, 0x093C }, { 0x0941, 0x0948 }, { 0x094D, 0x094D },
+  { 0x0951, 0x0954 }, { 0x0962, 0x0963 }, { 0x0981, 0x0981 },
+  { 0x09BC, 0x09BC }, { 0x09C1, 0x09C4 }, { 0x09CD, 0x09CD },
+  { 0x09E2, 0x09E3 }, { 0x0A01, 0x0A02 }, { 0x0A3C, 0x0A3C },
+  { 0x0A41, 0x0A42 }, { 0x0A47, 0x0A48 }, { 0x0A4B, 0x0A4D },
+  { 0x0A70, 0x0A71 }, { 0x0A81, 0x0A82 }, { 0x0ABC, 0x0ABC },
+  { 0x0AC1, 0x0AC5 }, { 0x0AC7, 0x0AC8 }, { 0x0ACD, 0x0ACD },
+  { 0x0AE2, 0x0AE3 }, { 0x0B01, 0x0B01 }, { 0x0B3C, 0x0B3C },
+  { 0x0B3F, 0x0B3F }, { 0x0B41, 0x0B43 }, { 0x0B4D, 0x0B4D },
+  { 0x0B56, 0x0B56 }, { 0x0B82, 0x0B82 }, { 0x0BC0, 0x0BC0 },
+  { 0x0BCD, 0x0BCD }, { 0x0C3E, 0x0C40 }, { 0x0C46, 0x0C48 },
+  { 0x0C4A, 0x0C4D }, { 0x0C55, 0x0C56 }, { 0x0CBC, 0x0CBC },
+  { 0x0CBF, 0x0CBF }, { 0x0CC6, 0x0CC6 }, { 0x0CCC, 0x0CCD },
+  { 0x0CE2, 0x0CE3 }, { 0x0D41, 0x0D43 }, { 0x0D4D, 0x0D4D },
+  { 0x0DCA, 0x0DCA }, { 0x0DD2, 0x0DD4 }, { 0x0DD6, 0x0DD6 },
+  { 0x0E31, 0x0E31 }, { 0x0E34, 0x0E3A }, { 0x0E47, 0x0E4E },
+  { 0x0EB1, 0x0EB1 }, { 0x0EB4, 0x0EB9 }, { 0x0EBB, 0x0EBC },
+  { 0x0EC8, 0x0ECD }, { 0x0F18, 0x0F19 }, { 0x0F35, 0x0F35 },
+  { 0x0F37, 0x0F37 }, { 0x0F39, 0x0F39 }, { 0x0F71, 0x0F7E },
+  { 0x0F80, 0x0F84 }, { 0x0F86, 0x0F87 }, { 0x0F90, 0x0F97 },
+  { 0x0F99, 0x0FBC }, { 0x0FC6, 0x0FC6 }, { 0x102D, 0x1030 },
+  { 0x1032, 0x1032 }, { 0x1036, 0x1037 }, { 0x1039, 0x1039 },
+  { 0x1058, 0x1059 }, { 0x1160, 0x11FF }, { 0x135F, 0x135F },
+  { 0x1712, 0x1714 }, { 0x1732, 0x1734 }, { 0x1752, 0x1753 },
+  { 0x1772, 0x1773 }, { 0x17B4, 0x17B5 }, { 0x17B7, 0x17BD },
+  { 0x17C6, 0x17C6 }, { 0x17C9, 0x17D3 }, { 0x17DD, 0x17DD },
+  { 0x180B, 0x180D }, { 0x18A9, 0x18A9 }, { 0x1920, 0x1922 },
+  { 0x1927, 0x1928 }, { 0x1932, 0x1932 }, { 0x1939, 0x193B },
+  { 0x1A17, 0x1A18 }, { 0x1B00, 0x1B03 }, { 0x1B34, 0x1B34 },
+  { 0x1B36, 0x1B3A }, { 0x1B3C, 0x1B3C }, { 0x1B42, 0x1B42 },
+  { 0x1B6B, 0x1B73 }, { 0x1DC0, 0x1DCA }, { 0x1DFE, 0x1DFF },
+  { 0x200B, 0x200F }, { 0x202A, 0x202E }, { 0x2060, 0x2063 },
+  { 0x206A, 0x206F }, { 0x20D0, 0x20EF }, { 0x302A, 0x302F },
+  { 0x3099, 0x309A }, { 0xA806, 0xA806 }, { 0xA80B, 0xA80B },
+  { 0xA825, 0xA826 }, { 0xFB1E, 0xFB1E }, { 0xFE00, 0xFE0F },
+  { 0xFE20, 0xFE23 }, { 0xFEFF, 0xFEFF }, { 0xFFF9, 0xFFFB },
+  { 0x10A01, 0x10A03 }, { 0x10A05, 0x10A06 }, { 0x10A0C, 0x10A0F },
+  { 0x10A38, 0x10A3A }, { 0x10A3F, 0x10A3F }, { 0x1D167, 0x1D169 },
+  { 0x1D173, 0x1D182 }, { 0x1D185, 0x1D18B }, { 0x1D1AA, 0x1D1AD },
+  { 0x1D242, 0x1D244 }, { 0xE0001, 0xE0001 }, { 0xE0020, 0xE007F },
+  { 0xE0100, 0xE01EF }
+};
+
+
+/* auxiliary function for binary search in interval table */
+static int bisearch(uint32_t ucs, const struct interval *table, int max) {
+  int min = 0;
+  int mid;
+
+  if ((int)ucs < table[0].first || (int)ucs > table[max].last)
+    return 0;
+  while (max >= min) {
+    mid = (min + max) / 2;
+    if ((int)ucs > table[mid].last)
+      min = mid + 1;
+    else if ((int)ucs < table[mid].first)
+      max = mid - 1;
+    else
+      return 1;
+  }
+
+  return 0;
+}
+
+
+/* The following two functions define the column width of an ISO 10646
+ * character as follows:
+ *
+ *    - The null character (U+0000) has a column width of 0.
+ *
+ *    - Other C0/C1 control characters and DEL will lead to a return
+ *      value of -1.
+ *
+ *    - Non-spacing and enclosing combining characters (general
+ *      category code Mn or Me in the Unicode database) have a
+ *      column width of 0.
+ *
+ *    - SOFT HYPHEN (U+00AD) has a column width of 1.
+ *
+ *    - Other format characters (general category code Cf in the Unicode
+ *      database) and ZERO WIDTH SPACE (U+200B) have a column width of 0.
+ *
+ *    - Hangul Jamo medial vowels and final consonants (U+1160-U+11FF)
+ *      have a column width of 0.
+ *
+ *    - Spacing characters in the East Asian Wide (W) or East Asian
+ *      Full-width (F) category as defined in Unicode Technical
+ *      Report #11 have a column width of 2.
+ *
+ *    - All remaining characters (including all printable
+ *      ISO 8859-1 and WGL4 characters, Unicode control characters,
+ *      etc.) have a column width of 1.
+ *
+ * This implementation assumes that uint32_t characters are encoded
+ * in ISO 10646.
+ */
+
+
+static int mk_wcwidth(uint32_t ucs)
+{
+  /* test for 8-bit control characters */
+  if (ucs == 0)
+    return 0;
+  if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0))
+    return -1;
+
+  /* binary search in table of non-spacing characters */
+  if (bisearch(ucs, combining,
+               sizeof(combining) / sizeof(struct interval) - 1))
+    return 0;
+
+  /* if we arrive here, ucs is not a combining or C0/C1 control character */
+
+  return 1 + 
+    (ucs >= 0x1100 &&
+     (ucs <= 0x115f ||                    /* Hangul Jamo init. consonants */
+      ucs == 0x2329 || ucs == 0x232a ||
+      (ucs >= 0x2e80 && ucs <= 0xa4cf &&
+       ucs != 0x303f) ||                  /* CJK ... Yi */
+      (ucs >= 0xac00 && ucs <= 0xd7a3) || /* Hangul Syllables */
+      (ucs >= 0xf900 && ucs <= 0xfaff) || /* CJK Compatibility Ideographs */
+      (ucs >= 0xfe10 && ucs <= 0xfe19) || /* Vertical forms */
+      (ucs >= 0xfe30 && ucs <= 0xfe6f) || /* CJK Compatibility Forms */
+      (ucs >= 0xff00 && ucs <= 0xff60) || /* Fullwidth Forms */
+      (ucs >= 0xffe0 && ucs <= 0xffe6) ||
+      (ucs >= 0x20000 && ucs <= 0x2fffd) ||
+      (ucs >= 0x30000 && ucs <= 0x3fffd)));
+}
+
+#if 0 /* unused */
+static int mk_wcswidth(const uint32_t *pwcs, size_t n)
+{
+  int w, width = 0;
+
+  for (;*pwcs && n-- > 0; pwcs++)
+    if ((w = mk_wcwidth(*pwcs)) < 0)
+      return -1;
+    else
+      width += w;
+
+  return width;
+}
+
+
+/*
+ * The following functions are the same as mk_wcwidth() and
+ * mk_wcswidth(), except that spacing characters in the East Asian
+ * Ambiguous (A) category as defined in Unicode Technical Report #11
+ * have a column width of 2. This variant might be useful for users of
+ * CJK legacy encodings who want to migrate to UCS without changing
+ * the traditional terminal character-width behaviour. It is not
+ * otherwise recommended for general use.
+ */
+static int mk_wcwidth_cjk(uint32_t ucs)
+{
+  /* sorted list of non-overlapping intervals of East Asian Ambiguous
+   * characters, generated by "uniset +WIDTH-A -cat=Me -cat=Mn -cat=Cf c" */
+  static const struct interval ambiguous[] = {
+    { 0x00A1, 0x00A1 }, { 0x00A4, 0x00A4 }, { 0x00A7, 0x00A8 },
+    { 0x00AA, 0x00AA }, { 0x00AE, 0x00AE }, { 0x00B0, 0x00B4 },
+    { 0x00B6, 0x00BA }, { 0x00BC, 0x00BF }, { 0x00C6, 0x00C6 },
+    { 0x00D0, 0x00D0 }, { 0x00D7, 0x00D8 }, { 0x00DE, 0x00E1 },
+    { 0x00E6, 0x00E6 }, { 0x00E8, 0x00EA }, { 0x00EC, 0x00ED },
+    { 0x00F0, 0x00F0 }, { 0x00F2, 0x00F3 }, { 0x00F7, 0x00FA },
+    { 0x00FC, 0x00FC }, { 0x00FE, 0x00FE }, { 0x0101, 0x0101 },
+    { 0x0111, 0x0111 }, { 0x0113, 0x0113 }, { 0x011B, 0x011B },
+    { 0x0126, 0x0127 }, { 0x012B, 0x012B }, { 0x0131, 0x0133 },
+    { 0x0138, 0x0138 }, { 0x013F, 0x0142 }, { 0x0144, 0x0144 },
+    { 0x0148, 0x014B }, { 0x014D, 0x014D }, { 0x0152, 0x0153 },
+    { 0x0166, 0x0167 }, { 0x016B, 0x016B }, { 0x01CE, 0x01CE },
+    { 0x01D0, 0x01D0 }, { 0x01D2, 0x01D2 }, { 0x01D4, 0x01D4 },
+    { 0x01D6, 0x01D6 }, { 0x01D8, 0x01D8 }, { 0x01DA, 0x01DA },
+    { 0x01DC, 0x01DC }, { 0x0251, 0x0251 }, { 0x0261, 0x0261 },
+    { 0x02C4, 0x02C4 }, { 0x02C7, 0x02C7 }, { 0x02C9, 0x02CB },
+    { 0x02CD, 0x02CD }, { 0x02D0, 0x02D0 }, { 0x02D8, 0x02DB },
+    { 0x02DD, 0x02DD }, { 0x02DF, 0x02DF }, { 0x0391, 0x03A1 },
+    { 0x03A3, 0x03A9 }, { 0x03B1, 0x03C1 }, { 0x03C3, 0x03C9 },
+    { 0x0401, 0x0401 }, { 0x0410, 0x044F }, { 0x0451, 0x0451 },
+    { 0x2010, 0x2010 }, { 0x2013, 0x2016 }, { 0x2018, 0x2019 },
+    { 0x201C, 0x201D }, { 0x2020, 0x2022 }, { 0x2024, 0x2027 },
+    { 0x2030, 0x2030 }, { 0x2032, 0x2033 }, { 0x2035, 0x2035 },
+    { 0x203B, 0x203B }, { 0x203E, 0x203E }, { 0x2074, 0x2074 },
+    { 0x207F, 0x207F }, { 0x2081, 0x2084 }, { 0x20AC, 0x20AC },
+    { 0x2103, 0x2103 }, { 0x2105, 0x2105 }, { 0x2109, 0x2109 },
+    { 0x2113, 0x2113 }, { 0x2116, 0x2116 }, { 0x2121, 0x2122 },
+    { 0x2126, 0x2126 }, { 0x212B, 0x212B }, { 0x2153, 0x2154 },
+    { 0x215B, 0x215E }, { 0x2160, 0x216B }, { 0x2170, 0x2179 },
+    { 0x2190, 0x2199 }, { 0x21B8, 0x21B9 }, { 0x21D2, 0x21D2 },
+    { 0x21D4, 0x21D4 }, { 0x21E7, 0x21E7 }, { 0x2200, 0x2200 },
+    { 0x2202, 0x2203 }, { 0x2207, 0x2208 }, { 0x220B, 0x220B },
+    { 0x220F, 0x220F }, { 0x2211, 0x2211 }, { 0x2215, 0x2215 },
+    { 0x221A, 0x221A }, { 0x221D, 0x2220 }, { 0x2223, 0x2223 },
+    { 0x2225, 0x2225 }, { 0x2227, 0x222C }, { 0x222E, 0x222E },
+    { 0x2234, 0x2237 }, { 0x223C, 0x223D }, { 0x2248, 0x2248 },
+    { 0x224C, 0x224C }, { 0x2252, 0x2252 }, { 0x2260, 0x2261 },
+    { 0x2264, 0x2267 }, { 0x226A, 0x226B }, { 0x226E, 0x226F },
+    { 0x2282, 0x2283 }, { 0x2286, 0x2287 }, { 0x2295, 0x2295 },
+    { 0x2299, 0x2299 }, { 0x22A5, 0x22A5 }, { 0x22BF, 0x22BF },
+    { 0x2312, 0x2312 }, { 0x2460, 0x24E9 }, { 0x24EB, 0x254B },
+    { 0x2550, 0x2573 }, { 0x2580, 0x258F }, { 0x2592, 0x2595 },
+    { 0x25A0, 0x25A1 }, { 0x25A3, 0x25A9 }, { 0x25B2, 0x25B3 },
+    { 0x25B6, 0x25B7 }, { 0x25BC, 0x25BD }, { 0x25C0, 0x25C1 },
+    { 0x25C6, 0x25C8 }, { 0x25CB, 0x25CB }, { 0x25CE, 0x25D1 },
+    { 0x25E2, 0x25E5 }, { 0x25EF, 0x25EF }, { 0x2605, 0x2606 },
+    { 0x2609, 0x2609 }, { 0x260E, 0x260F }, { 0x2614, 0x2615 },
+    { 0x261C, 0x261C }, { 0x261E, 0x261E }, { 0x2640, 0x2640 },
+    { 0x2642, 0x2642 }, { 0x2660, 0x2661 }, { 0x2663, 0x2665 },
+    { 0x2667, 0x266A }, { 0x266C, 0x266D }, { 0x266F, 0x266F },
+    { 0x273D, 0x273D }, { 0x2776, 0x277F }, { 0xE000, 0xF8FF },
+    { 0xFFFD, 0xFFFD }, { 0xF0000, 0xFFFFD }, { 0x100000, 0x10FFFD }
+  };
+
+  /* binary search in table of non-spacing characters */
+  if (bisearch(ucs, ambiguous,
+               sizeof(ambiguous) / sizeof(struct interval) - 1))
+    return 2;
+
+  return mk_wcwidth(ucs);
+}
+
+static int mk_wcswidth_cjk(const uint32_t *pwcs, size_t n)
+{
+  int w, width = 0;
+
+  for (;*pwcs && n-- > 0; pwcs++)
+    if ((w = mk_wcwidth_cjk(*pwcs)) < 0)
+      return -1;
+    else
+      width += w;
+
+  return width;
+}
+#endif
+
+/* ################################
+ * ### The rest added by Paul Evans */
+
+INTERNAL int vterm_unicode_width(uint32_t codepoint)
+{
+  return mk_wcwidth(codepoint);
+}
+
+INTERNAL int vterm_unicode_is_combining(uint32_t codepoint)
+{
+  return bisearch(codepoint, combining, sizeof(combining) / sizeof(struct interval) - 1);
+}
new file mode 100644
--- /dev/null
+++ b/src/libvterm/src/utf8.h
@@ -0,0 +1,47 @@
+/* The following functions copied and adapted from libtermkey
+ *
+ * http://www.leonerd.org.uk/code/libtermkey/
+ */
+unsigned int utf8_seqlen(long codepoint);
+
+#if defined(DEFINE_INLINES) || USE_INLINE
+INLINE unsigned int utf8_seqlen(long codepoint)
+{
+  if(codepoint < 0x0000080) return 1;
+  if(codepoint < 0x0000800) return 2;
+  if(codepoint < 0x0010000) return 3;
+  if(codepoint < 0x0200000) return 4;
+  if(codepoint < 0x4000000) return 5;
+  return 6;
+}
+#endif
+
+/* Does NOT NUL-terminate the buffer */
+int fill_utf8(long codepoint, char *str);
+
+#if defined(DEFINE_INLINES) || USE_INLINE
+INLINE int fill_utf8(long codepoint, char *str)
+{
+  int nbytes = utf8_seqlen(codepoint);
+
+  /* This is easier done backwards */
+  int b = nbytes;
+  while(b > 1) {
+    b--;
+    str[b] = 0x80 | (codepoint & 0x3f);
+    codepoint >>= 6;
+  }
+
+  switch(nbytes) {
+    case 1: str[0] =        (codepoint & 0x7f); break;
+    case 2: str[0] = 0xc0 | (codepoint & 0x1f); break;
+    case 3: str[0] = 0xe0 | (codepoint & 0x0f); break;
+    case 4: str[0] = 0xf0 | (codepoint & 0x07); break;
+    case 5: str[0] = 0xf8 | (codepoint & 0x03); break;
+    case 6: str[0] = 0xfc | (codepoint & 0x01); break;
+  }
+
+  return nbytes;
+}
+#endif
+/* end copy */
new file mode 100644
--- /dev/null
+++ b/src/libvterm/src/vterm.c
@@ -0,0 +1,385 @@
+#define DEFINE_INLINES
+
+#include "vterm_internal.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <string.h>
+
+#include "utf8.h"
+
+/*****************
+ * API functions *
+ *****************/
+
+static void *default_malloc(size_t size, void *allocdata UNUSED)
+{
+  void *ptr = malloc(size);
+  if(ptr)
+    memset(ptr, 0, size);
+  return ptr;
+}
+
+static void default_free(void *ptr, void *allocdata UNUSED)
+{
+  free(ptr);
+}
+
+static VTermAllocatorFunctions default_allocator = {
+  &default_malloc, /* malloc */
+  &default_free /* free */
+};
+
+VTerm *vterm_new(int rows, int cols)
+{
+  return vterm_new_with_allocator(rows, cols, &default_allocator, NULL);
+}
+
+VTerm *vterm_new_with_allocator(int rows, int cols, VTermAllocatorFunctions *funcs, void *allocdata)
+{
+  /* Need to bootstrap using the allocator function directly */
+  VTerm *vt = (*funcs->malloc)(sizeof(VTerm), allocdata);
+
+  vt->allocator = funcs;
+  vt->allocdata = allocdata;
+
+  vt->rows = rows;
+  vt->cols = cols;
+
+  vt->parser_state = NORMAL;
+
+  vt->parser_callbacks = NULL;
+  vt->cbdata           = NULL;
+
+  vt->strbuffer_len = 64;
+  vt->strbuffer_cur = 0;
+  vt->strbuffer = vterm_allocator_malloc(vt, vt->strbuffer_len);
+
+  vt->outbuffer_len = 64;
+  vt->outbuffer_cur = 0;
+  vt->outbuffer = vterm_allocator_malloc(vt, vt->outbuffer_len);
+
+  return vt;
+}
+
+void vterm_free(VTerm *vt)
+{
+  if(vt->screen)
+    vterm_screen_free(vt->screen);
+
+  if(vt->state)
+    vterm_state_free(vt->state);
+
+  vterm_allocator_free(vt, vt->strbuffer);
+  vterm_allocator_free(vt, vt->outbuffer);
+
+  vterm_allocator_free(vt, vt);
+}
+
+INTERNAL void *vterm_allocator_malloc(VTerm *vt, size_t size)
+{
+  return (*vt->allocator->malloc)(size, vt->allocdata);
+}
+
+INTERNAL void vterm_allocator_free(VTerm *vt, void *ptr)
+{
+  (*vt->allocator->free)(ptr, vt->allocdata);
+}
+
+void vterm_get_size(const VTerm *vt, int *rowsp, int *colsp)
+{
+  if(rowsp)
+    *rowsp = vt->rows;
+  if(colsp)
+    *colsp = vt->cols;
+}
+
+void vterm_set_size(VTerm *vt, int rows, int cols)
+{
+  vt->rows = rows;
+  vt->cols = cols;
+
+  if(vt->parser_callbacks && vt->parser_callbacks->resize)
+    (*vt->parser_callbacks->resize)(rows, cols, vt->cbdata);
+}
+
+int vterm_get_utf8(const VTerm *vt)
+{
+  return vt->mode.utf8;
+}
+
+void vterm_set_utf8(VTerm *vt, int is_utf8)
+{
+  vt->mode.utf8 = is_utf8;
+}
+
+INTERNAL void vterm_push_output_bytes(VTerm *vt, const char *bytes, size_t len)
+{
+  if(len > vt->outbuffer_len - vt->outbuffer_cur) {
+    DEBUG_LOG("vterm_push_output(): buffer overflow; truncating output\n");
+    len = vt->outbuffer_len - vt->outbuffer_cur;
+  }
+
+  memcpy(vt->outbuffer + vt->outbuffer_cur, bytes, len);
+  vt->outbuffer_cur += len;
+}
+
+static int outbuffer_is_full(VTerm *vt)
+{
+  return vt->outbuffer_cur >= vt->outbuffer_len - 1;
+}
+
+INTERNAL void vterm_push_output_vsprintf(VTerm *vt, const char *format, va_list args)
+{
+  int written;
+  char buffer[1024]; /* 1Kbyte is enough for everybody, right? */
+
+  if(outbuffer_is_full(vt)) {
+    DEBUG_LOG("vterm_push_output(): buffer overflow; truncating output\n");
+    return;
+  }
+
+  written = vsprintf(buffer, format, args);
+
+  if(written >= (int)(vt->outbuffer_len - vt->outbuffer_cur)) {
+    /* output was truncated */
+    written = vt->outbuffer_len - vt->outbuffer_cur;
+  }
+  if (written > 0)
+  {
+    strncpy(vt->outbuffer + vt->outbuffer_cur, buffer, written + 1);
+    vt->outbuffer_cur += written;
+  }
+}
+
+INTERNAL void vterm_push_output_sprintf(VTerm *vt, const char *format, ...)
+{
+  va_list args;
+  va_start(args, format);
+  vterm_push_output_vsprintf(vt, format, args);
+  va_end(args);
+}
+
+INTERNAL void vterm_push_output_sprintf_ctrl(VTerm *vt, unsigned char ctrl, const char *fmt, ...)
+{
+  size_t orig_cur = vt->outbuffer_cur;
+  va_list args;
+
+  if(ctrl >= 0x80 && !vt->mode.ctrl8bit)
+    vterm_push_output_sprintf(vt, ESC_S "%c", ctrl - 0x40);
+  else
+    vterm_push_output_sprintf(vt, "%c", ctrl);
+
+  va_start(args, fmt);
+  vterm_push_output_vsprintf(vt, fmt, args);
+  va_end(args);
+
+  if(outbuffer_is_full(vt))
+    vt->outbuffer_cur = orig_cur;
+}
+
+INTERNAL void vterm_push_output_sprintf_dcs(VTerm *vt, const char *fmt, ...)
+{
+  size_t orig_cur = vt->outbuffer_cur;
+  va_list args;
+
+  if(!vt->mode.ctrl8bit)
+    vterm_push_output_sprintf(vt, ESC_S "%c", C1_DCS - 0x40);
+  else
+    vterm_push_output_sprintf(vt, "%c", C1_DCS);
+
+  va_start(args, fmt);
+  vterm_push_output_vsprintf(vt, fmt, args);
+  va_end(args);
+
+  vterm_push_output_sprintf_ctrl(vt, C1_ST, "");
+
+  if(outbuffer_is_full(vt))
+    vt->outbuffer_cur = orig_cur;
+}
+
+size_t vterm_output_get_buffer_size(const VTerm *vt)
+{
+  return vt->outbuffer_len;
+}
+
+size_t vterm_output_get_buffer_current(const VTerm *vt)
+{
+  return vt->outbuffer_cur;
+}
+
+size_t vterm_output_get_buffer_remaining(const VTerm *vt)
+{
+  return vt->outbuffer_len - vt->outbuffer_cur;
+}
+
+size_t vterm_output_read(VTerm *vt, char *buffer, size_t len)
+{
+  if(len > vt->outbuffer_cur)
+    len = vt->outbuffer_cur;
+
+  memcpy(buffer, vt->outbuffer, len);
+
+  if(len < vt->outbuffer_cur)
+    memmove(vt->outbuffer, vt->outbuffer + len, vt->outbuffer_cur - len);
+
+  vt->outbuffer_cur -= len;
+
+  return len;
+}
+
+void vterm_parser_set_callbacks(VTerm *vt, const VTermParserCallbacks *callbacks, void *user)
+{
+  vt->parser_callbacks = callbacks;
+  vt->cbdata = user;
+}
+
+void *vterm_parser_get_cbdata(VTerm *vt)
+{
+  return vt->cbdata;
+}
+
+VTermValueType vterm_get_attr_type(VTermAttr attr)
+{
+  switch(attr) {
+    case VTERM_ATTR_BOLD:       return VTERM_VALUETYPE_BOOL;
+    case VTERM_ATTR_UNDERLINE:  return VTERM_VALUETYPE_INT;
+    case VTERM_ATTR_ITALIC:     return VTERM_VALUETYPE_BOOL;
+    case VTERM_ATTR_BLINK:      return VTERM_VALUETYPE_BOOL;
+    case VTERM_ATTR_REVERSE:    return VTERM_VALUETYPE_BOOL;
+    case VTERM_ATTR_STRIKE:     return VTERM_VALUETYPE_BOOL;
+    case VTERM_ATTR_FONT:       return VTERM_VALUETYPE_INT;
+    case VTERM_ATTR_FOREGROUND: return VTERM_VALUETYPE_COLOR;
+    case VTERM_ATTR_BACKGROUND: return VTERM_VALUETYPE_COLOR;
+  }
+  return 0; /* UNREACHABLE */
+}
+
+VTermValueType vterm_get_prop_type(VTermProp prop)
+{
+  switch(prop) {
+    case VTERM_PROP_CURSORVISIBLE: return VTERM_VALUETYPE_BOOL;
+    case VTERM_PROP_CURSORBLINK:   return VTERM_VALUETYPE_BOOL;
+    case VTERM_PROP_ALTSCREEN:     return VTERM_VALUETYPE_BOOL;
+    case VTERM_PROP_TITLE:         return VTERM_VALUETYPE_STRING;
+    case VTERM_PROP_ICONNAME:      return VTERM_VALUETYPE_STRING;
+    case VTERM_PROP_REVERSE:       return VTERM_VALUETYPE_BOOL;
+    case VTERM_PROP_CURSORSHAPE:   return VTERM_VALUETYPE_INT;
+    case VTERM_PROP_MOUSE:         return VTERM_VALUETYPE_INT;
+  }
+  return 0; /* UNREACHABLE */
+}
+
+void vterm_scroll_rect(VTermRect rect,
+    int downward,
+    int rightward,
+    int (*moverect)(VTermRect src, VTermRect dest, void *user),
+    int (*eraserect)(VTermRect rect, int selective, void *user),
+    void *user)
+{
+  VTermRect src;
+  VTermRect dest;
+
+  if(abs(downward)  >= rect.end_row - rect.start_row ||
+     abs(rightward) >= rect.end_col - rect.start_col) {
+    /* Scroll more than area; just erase the lot */
+    (*eraserect)(rect, 0, user);
+    return;
+  }
+
+  if(rightward >= 0) {
+    /* rect: [XXX................]
+     * src:     [----------------]
+     * dest: [----------------]
+     */
+    dest.start_col = rect.start_col;
+    dest.end_col   = rect.end_col   - rightward;
+    src.start_col  = rect.start_col + rightward;
+    src.end_col    = rect.end_col;
+  }
+  else {
+    /* rect: [................XXX]
+     * src:  [----------------]
+     * dest:    [----------------]
+     */
+    int leftward = -rightward;
+    dest.start_col = rect.start_col + leftward;
+    dest.end_col   = rect.end_col;
+    src.start_col  = rect.start_col;
+    src.end_col    = rect.end_col - leftward;
+  }
+
+  if(downward >= 0) {
+    dest.start_row = rect.start_row;
+    dest.end_row   = rect.end_row   - downward;
+    src.start_row  = rect.start_row + downward;
+    src.end_row    = rect.end_row;
+  }
+  else {
+    int upward = -downward;
+    dest.start_row = rect.start_row + upward;
+    dest.end_row   = rect.end_row;
+    src.start_row  = rect.start_row;
+    src.end_row    = rect.end_row - upward;
+  }
+
+  if(moverect)
+    (*moverect)(dest, src, user);
+
+  if(downward > 0)
+    rect.start_row = rect.end_row - downward;
+  else if(downward < 0)
+    rect.end_row = rect.start_row - downward;
+
+  if(rightward > 0)
+    rect.start_col = rect.end_col - rightward;
+  else if(rightward < 0)
+    rect.end_col = rect.start_col - rightward;
+
+  (*eraserect)(rect, 0, user);
+}
+
+void vterm_copy_cells(VTermRect dest,
+    VTermRect src,
+    void (*copycell)(VTermPos dest, VTermPos src, void *user),
+    void *user)
+{
+  int downward  = src.start_row - dest.start_row;
+  int rightward = src.start_col - dest.start_col;
+
+  int init_row, test_row, init_col, test_col;
+  int inc_row, inc_col;
+
+  VTermPos pos;
+
+  if(downward < 0) {
+    init_row = dest.end_row - 1;
+    test_row = dest.start_row - 1;
+    inc_row = -1;
+  }
+  else /* downward >= 0 */ {
+    init_row = dest.start_row;
+    test_row = dest.end_row;
+    inc_row = +1;
+  }
+
+  if(rightward < 0) {
+    init_col = dest.end_col - 1;
+    test_col = dest.start_col - 1;
+    inc_col = -1;
+  }
+  else /* rightward >= 0 */ {
+    init_col = dest.start_col;
+    test_col = dest.end_col;
+    inc_col = +1;
+  }
+
+  for(pos.row = init_row; pos.row != test_row; pos.row += inc_row)
+    for(pos.col = init_col; pos.col != test_col; pos.col += inc_col) {
+      VTermPos srcpos;
+      srcpos.row = pos.row + downward;
+      srcpos.col = pos.col + rightward;
+      (*copycell)(pos, srcpos, user);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/src/libvterm/src/vterm_internal.h
@@ -0,0 +1,237 @@
+#ifndef __VTERM_INTERNAL_H__
+#define __VTERM_INTERNAL_H__
+
+#include "vterm.h"
+
+#include <stdarg.h>
+
+#if defined(__GNUC__)
+# define INTERNAL __attribute__((visibility("internal")))
+# define UNUSED __attribute__((unused))
+#else
+# define INTERNAL
+# define UNUSED
+#endif
+
+#ifdef DEBUG
+# define DEBUG_LOG(s) fprintf(stderr, s)
+# define DEBUG_LOG1(s, a) fprintf(stderr, s, a)
+# define DEBUG_LOG2(s, a, b) fprintf(stderr, s, a, b)
+# define DEBUG_LOG3(s, a, b, c) fprintf(stderr, s, a, b, c)
+#else
+# define DEBUG_LOG(s)
+# define DEBUG_LOG1(s, a)
+# define DEBUG_LOG2(s, a, b)
+# define DEBUG_LOG3(s, a, b, c)
+#endif
+
+#define ESC_S "\x1b"
+
+typedef struct VTermEncoding VTermEncoding;
+
+typedef struct {
+  VTermEncoding *enc;
+
+  /* This size should be increased if required by other stateful encodings */
+  char           data[4*sizeof(uint32_t)];
+} VTermEncodingInstance;
+
+struct VTermPen
+{
+  VTermColor fg;
+  VTermColor bg;
+  unsigned int bold:1;
+  unsigned int underline:2;
+  unsigned int italic:1;
+  unsigned int blink:1;
+  unsigned int reverse:1;
+  unsigned int strike:1;
+  unsigned int font:4; /* To store 0-9 */
+};
+
+int vterm_color_equal(VTermColor a, VTermColor b);
+
+#if defined(DEFINE_INLINES) || USE_INLINE
+INLINE int vterm_color_equal(VTermColor a, VTermColor b)
+{
+  return a.red == b.red && a.green == b.green && a.blue == b.blue;
+}
+#endif
+
+struct VTermState
+{
+  VTerm *vt;
+
+  const VTermStateCallbacks *callbacks;
+  void *cbdata;
+
+  const VTermParserCallbacks *fallbacks;
+  void *fbdata;
+
+  int rows;
+  int cols;
+
+  /* Current cursor position */
+  VTermPos pos;
+
+  int at_phantom; /* True if we're on the "81st" phantom column to defer a wraparound */
+
+  int scrollregion_top;
+  int scrollregion_bottom; /* -1 means unbounded */
+#define SCROLLREGION_BOTTOM(state) ((state)->scrollregion_bottom > -1 ? (state)->scrollregion_bottom : (state)->rows)
+  int scrollregion_left;
+#define SCROLLREGION_LEFT(state)  ((state)->mode.leftrightmargin ? (state)->scrollregion_left : 0)
+  int scrollregion_right; /* -1 means unbounded */
+#define SCROLLREGION_RIGHT(state) ((state)->mode.leftrightmargin && (state)->scrollregion_right > -1 ? (state)->scrollregion_right : (state)->cols)
+
+  /* Bitvector of tab stops */
+  unsigned char *tabstops;
+
+  VTermLineInfo *lineinfo;
+#define ROWWIDTH(state,row) ((state)->lineinfo[(row)].doublewidth ? ((state)->cols / 2) : (state)->cols)
+#define THISROWWIDTH(state) ROWWIDTH(state, (state)->pos.row)
+
+  /* Mouse state */
+  int mouse_col, mouse_row;
+  int mouse_buttons;
+  int mouse_flags;
+#define MOUSE_WANT_CLICK 0x01
+#define MOUSE_WANT_DRAG  0x02
+#define MOUSE_WANT_MOVE  0x04
+
+  enum { MOUSE_X10, MOUSE_UTF8, MOUSE_SGR, MOUSE_RXVT } mouse_protocol;
+
+  /* Last glyph output, for Unicode recombining purposes */
+  uint32_t *combine_chars;
+  size_t combine_chars_size; /* Number of ELEMENTS in the above */
+  int combine_width; /* The width of the glyph above */
+  VTermPos combine_pos;   /* Position before movement */
+
+  struct {
+    unsigned int keypad:1;
+    unsigned int cursor:1;
+    unsigned int autowrap:1;
+    unsigned int insert:1;
+    unsigned int newline:1;
+    unsigned int cursor_visible:1;
+    unsigned int cursor_blink:1;
+    unsigned int cursor_shape:2;
+    unsigned int alt_screen:1;
+    unsigned int origin:1;
+    unsigned int screen:1;
+    unsigned int leftrightmargin:1;
+    unsigned int bracketpaste:1;
+  } mode;
+
+  VTermEncodingInstance encoding[4], encoding_utf8;
+  int gl_set, gr_set, gsingle_set;
+
+  struct VTermPen pen;
+
+  VTermColor default_fg;
+  VTermColor default_bg;
+  VTermColor colors[16]; /* Store the 8 ANSI and the 8 ANSI high-brights only */
+
+  int fg_index;
+  int bg_index;
+  int bold_is_highbright;
+
+  unsigned int protected_cell : 1;
+
+  /* Saved state under DEC mode 1048/1049 */
+  struct {
+    VTermPos pos;
+    struct VTermPen pen;
+
+    struct {
+      int cursor_visible:1;
+      int cursor_blink:1;
+      unsigned int cursor_shape:2;
+    } mode;
+  } saved;
+};
+
+struct VTerm
+{
+  VTermAllocatorFunctions *allocator;
+  void *allocdata;
+
+  int rows;
+  int cols;
+
+  struct {
+    unsigned int utf8:1;
+    unsigned int ctrl8bit:1;
+  } mode;
+
+  enum VTermParserState {
+    NORMAL,
+    CSI,
+    OSC,
+    DCS,
+    ESC,
+    ESC_IN_OSC,
+    ESC_IN_DCS
+  } parser_state;
+  const VTermParserCallbacks *parser_callbacks;
+  void *cbdata;
+
+  /* len == malloc()ed size; cur == number of valid bytes */
+  char  *strbuffer;
+  size_t strbuffer_len;
+  size_t strbuffer_cur;
+
+  char  *outbuffer;
+  size_t outbuffer_len;
+  size_t outbuffer_cur;
+
+  VTermState *state;
+  VTermScreen *screen;
+};
+
+struct VTermEncoding {
+  void (*init) (VTermEncoding *enc, void *data);
+  void (*decode)(VTermEncoding *enc, void *data,
+                 uint32_t cp[], int *cpi, int cplen,
+                 const char bytes[], size_t *pos, size_t len);
+};
+
+typedef enum {
+  ENC_UTF8,
+  ENC_SINGLE_94
+} VTermEncodingType;
+
+void *vterm_allocator_malloc(VTerm *vt, size_t size);
+void  vterm_allocator_free(VTerm *vt, void *ptr);
+
+void vterm_push_output_bytes(VTerm *vt, const char *bytes, size_t len);
+void vterm_push_output_vsprintf(VTerm *vt, const char *format, va_list args);
+void vterm_push_output_sprintf(VTerm *vt, const char *format, ...);
+void vterm_push_output_sprintf_ctrl(VTerm *vt, unsigned char ctrl, const char *fmt, ...);
+void vterm_push_output_sprintf_dcs(VTerm *vt, const char *fmt, ...);
+
+void vterm_state_free(VTermState *state);
+
+void vterm_state_newpen(VTermState *state);
+void vterm_state_resetpen(VTermState *state);
+void vterm_state_setpen(VTermState *state, const long args[], int argcount);
+int  vterm_state_getpen(VTermState *state, long args[], int argcount);
+void vterm_state_savepen(VTermState *state, int save);
+
+enum {
+  C1_SS3 = 0x8f,
+  C1_DCS = 0x90,
+  C1_CSI = 0x9b,
+  C1_ST  = 0x9c
+};
+
+void vterm_state_push_output_sprintf_CSI(VTermState *vts, const char *format, ...);
+
+void vterm_screen_free(VTermScreen *screen);
+
+VTermEncoding *vterm_lookup_encoding(VTermEncodingType type, char designation);
+
+int vterm_unicode_width(uint32_t codepoint);
+int vterm_unicode_is_combining(uint32_t codepoint);
+
+#endif
new file mode 100644
--- /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
new file mode 100644
--- /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
new file mode 100644
--- /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
new file mode 100644
--- /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
new file mode 100644
--- /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
new file mode 100644
--- /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
new file mode 100644
--- /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
new file mode 100644
--- /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
new file mode 100644
--- /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
new file mode 100644
--- /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"
new file mode 100644
--- /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"
new file mode 100644
--- /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
new file mode 100644
--- /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
new file mode 100644
--- /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
new file mode 100644
--- /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~"
new file mode 100644
--- /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
new file mode 100644
--- /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
new file mode 100644
--- /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
new file mode 100644
--- /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"
new file mode 100644
--- /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)
new file mode 100644
--- /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
new file mode 100644
--- /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)
new file mode 100644
--- /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
new file mode 100644
--- /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
new file mode 100644
--- /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"
new file mode 100644
--- /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 = 
new file mode 100644
--- /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
new file mode 100644
--- /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)
new file mode 100644
--- /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"
new file mode 100644
--- /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
new file mode 100644
--- /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
new file mode 100644
--- /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
new file mode 100644
--- /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"
new file mode 100644
--- /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"
new file mode 100644
--- /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
new file mode 100644
--- /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"
new file mode 100644
--- /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"
+
new file mode 100644
--- /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"
new file mode 100644
--- /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;
+}
new file mode 100644
--- /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;
new file mode 100644
--- /dev/null
+++ b/src/libvterm/tbl2inc_c.pl
@@ -0,0 +1,51 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+my ( $encname ) = $ARGV[0] =~ m{/([^/.]+).tbl}
+   or die "Cannot parse encoding name out of $ARGV[0]\n";
+
+print <<"EOF";
+static const struct StaticTableEncoding encoding_$encname = {
+  {
+    NULL, /* init */
+    &decode_table /* decode */
+  },
+  {
+EOF
+
+my $row = 0;
+while( <> ) {
+   s/\s*#.*//; # strip comment
+
+   if ($_ =~ m{^\d+/\d+}) {
+     my ($up, $low) = ($_ =~ m{^(\d+)/(\d+)});
+     my $thisrow = $up * 16 + $low;
+     while ($row < $thisrow) {
+	print "    0x0, /* $row */\n";
+	++$row;
+     }
+   }
+
+   s{^(\d+)/(\d+)}{""}e;                     # Remove 3/1
+   s{ = }{""}e;                            # Remove " = "
+   s{"(.)"}{sprintf "0x%04x", ord $1}e;      # Convert "A" to 0x41
+   s{U\+}{0x};                               # Convert U+0041 to 0x0041
+
+   s{$}{, /* $row */}; # append comma and index
+
+   print "    $_";
+
+   ++$row;
+}
+
+while ($row < 128) {
+   print "    0x0, /* $row */\n";
+   ++$row;
+}
+
+print <<"EOF";
+  }
+};
+EOF
new file mode 100644
--- /dev/null
+++ b/src/libvterm/vterm.pc.in
@@ -0,0 +1,9 @@
+prefix=@PREFIX@
+libdir=@LIBDIR@
+includedir=${prefix}/include
+
+Name: vterm
+Description: Abstract VT220/Xterm/ECMA-48 emulation library
+Version: @VERSION@
+Libs: -L${libdir} -lvterm
+Cflags: -I${includedir}
--- a/src/option.c
+++ b/src/option.c
@@ -257,6 +257,9 @@
 # define PV_COCU	OPT_WIN(WV_COCU)
 # define PV_COLE	OPT_WIN(WV_COLE)
 #endif
+#ifdef FEAT_TERMINAL
+# define PV_TMS		OPT_WIN(WV_TMS)
+#endif
 #ifdef FEAT_SIGNS
 # define PV_SCL		OPT_WIN(WV_SCL)
 #endif
@@ -2778,6 +2781,15 @@ static struct vimoption options[] =
 			    {(char_u *)FALSE, (char_u *)FALSE}
 #endif
 			    SCRIPTID_INIT},
+    {"termsize", "tms",	    P_STRING|P_ALLOCED|P_RWIN|P_VI_DEF,
+#ifdef FEAT_TERMINAL
+			    (char_u *)VAR_WIN, PV_TMS,
+			    {(char_u *)"", (char_u *)NULL}
+#else
+			    (char_u *)NULL, PV_NONE,
+			    {(char_u *)NULL, (char_u *)0L}
+#endif
+			    SCRIPTID_INIT},
     {"terse",	    NULL,   P_BOOL|P_VI_DEF,
 			    (char_u *)&p_terse, PV_NONE,
 			    {(char_u *)FALSE, (char_u *)0L} SCRIPTID_INIT},
@@ -10662,8 +10674,11 @@ get_varp(struct vimoption *p)
 	case PV_CRBIND: return (char_u *)&(curwin->w_p_crb);
 #endif
 #ifdef FEAT_CONCEAL
-	case PV_COCU:    return (char_u *)&(curwin->w_p_cocu);
-	case PV_COLE:    return (char_u *)&(curwin->w_p_cole);
+	case PV_COCU:   return (char_u *)&(curwin->w_p_cocu);
+	case PV_COLE:   return (char_u *)&(curwin->w_p_cole);
+#endif
+#ifdef FEAT_TERMINAL
+	case PV_TMS:    return (char_u *)&(curwin->w_p_tms);
 #endif
 
 	case PV_AI:	return (char_u *)&(curbuf->b_p_ai);
@@ -10871,6 +10886,9 @@ copy_winopt(winopt_T *from, winopt_T *to
     to->wo_cocu = vim_strsave(from->wo_cocu);
     to->wo_cole = from->wo_cole;
 #endif
+#ifdef FEAT_TERMINAL
+    to->wo_tms = vim_strsave(from->wo_tms);
+#endif
 #ifdef FEAT_FOLDING
     to->wo_fdc = from->wo_fdc;
     to->wo_fdc_save = from->wo_fdc_save;
@@ -10937,6 +10955,9 @@ check_winopt(winopt_T *wop UNUSED)
 #ifdef FEAT_CONCEAL
     check_string_option(&wop->wo_cocu);
 #endif
+#ifdef FEAT_TERMINAL
+    check_string_option(&wop->wo_tms);
+#endif
 #ifdef FEAT_LINEBREAK
     check_string_option(&wop->wo_briopt);
 #endif
@@ -10976,6 +10997,9 @@ clear_winopt(winopt_T *wop UNUSED)
 #ifdef FEAT_CONCEAL
     clear_string_option(&wop->wo_cocu);
 #endif
+#ifdef FEAT_TERMINAL
+    clear_string_option(&wop->wo_tms);
+#endif
 }
 
 /*
--- a/src/option.h
+++ b/src/option.h
@@ -1130,6 +1130,9 @@ enum
     , WV_COCU
     , WV_COLE
 #endif
+#ifdef FEAT_TERMINAL
+    , WV_TMS
+#endif
 #ifdef FEAT_CURSORBIND
     , WV_CRBIND
 #endif
--- a/src/proto.h
+++ b/src/proto.h
@@ -162,6 +162,9 @@ void qsort(void *base, size_t elm_count,
 # include "syntax.pro"
 # include "tag.pro"
 # include "term.pro"
+# ifdef FEAT_TERMINAL
+#  include "terminal.pro"
+# endif
 # if defined(HAVE_TGETENT) && (defined(AMIGA) || defined(VMS))
 #  include "termlib.pro"
 # endif
new file mode 100644
--- /dev/null
+++ b/src/proto/terminal.pro
@@ -0,0 +1,3 @@
+/* terminal.c */
+void ex_terminal(exarg_T *eap);
+/* vim: set ft=c : */
--- a/src/structs.h
+++ b/src/structs.h
@@ -68,6 +68,7 @@ typedef struct wininfo_S	wininfo_T;
 typedef struct frame_S		frame_T;
 typedef int			scid_T;		/* script ID */
 typedef struct file_buffer	buf_T;  /* forward declaration */
+typedef struct terminal_S	term_T;
 
 /*
  * Reference to a buffer that stores the value of buf_free_count.
@@ -268,6 +269,10 @@ typedef struct
     char_u	*wo_scl;
 # define w_p_scl w_onebuf_opt.wo_scl	/* 'signcolumn' */
 #endif
+#ifdef FEAT_TERMINAL
+    char_u	*wo_tms;
+#define w_p_tms w_onebuf_opt.wo_tms	/* 'termsize' */
+#endif
 
 #ifdef FEAT_EVAL
     int		wo_scriptID[WV_COUNT];	/* SIDs for window-local options */
@@ -2351,6 +2356,11 @@ struct file_buffer
 #endif
     int		b_mapped_ctrl_c; /* modes where CTRL-C is mapped */
 
+#ifdef FEAT_TERMINAL
+    term_T	*b_term;	/* When not NULL this buffer is for a terminal
+				 * window. */
+#endif
+
 }; /* file_buffer */
 
 
new file mode 100644
--- /dev/null
+++ b/src/terminal.c
@@ -0,0 +1,211 @@
+/* vi:set ts=8 sts=4 sw=4 noet:
+ *
+ * VIM - Vi IMproved	by Bram Moolenaar
+ *
+ * Do ":help uganda"  in Vim to read copying and usage conditions.
+ * Do ":help credits" in Vim to see a list of people who contributed.
+ * See README.txt for an overview of the Vim source code.
+ */
+
+/*
+ * Terminal window support, see ":help :terminal".
+ *
+ * For a terminal one VTerm is constructed.  This uses libvterm.  A copy of
+ * that library is in the libvterm directory.
+ *
+ * The VTerm invokes callbacks when its screen contents changes.  The line
+ * range is stored in tl_dirty_row_start and tl_dirty_row_end.  Once in a
+ * while, if the window is visible, the screen contents is drawn.
+ *
+ * If the terminal window has keyboard focus, typed keys are converted to the
+ * terminal encoding and writting to the job over a channel.
+ *
+ * If the job produces output, it is written to the VTerm.
+ * This will result in screen updates.
+ *
+ * TODO:
+ * - +terminal in features[] in version.c
+ * - free b_term when closing terminal.
+ * - remove term from first_term list when closing terminal.
+ * - set buffer options to be scratch, hidden, nomodifiable, etc.
+ * - set buffer name to command, add (1) to avoid duplicates.
+ * - command line completion (command name)
+ * - support fixed size when 'termsize' is "rowsXcols".
+ * - support minimal size when 'termsize' is "rows*cols".
+ * - support minimal size when 'termsize' is empty.
+ * - implement ":buf {term-buf-name}"
+ * - implement term_getsize()
+ * - implement term_setsize()
+ * - implement term_sendkeys()		send keystrokes to a terminal
+ * - implement term_wait()		wait for screen to be updated
+ * - implement term_scrape()		inspect terminal screen
+ * - implement term_open()		open terminal window
+ * - implement term_getjob()
+ */
+
+#include "vim.h"
+
+#ifdef FEAT_TERMINAL
+
+#include "libvterm/include/vterm.h"
+
+/* typedef term_T in structs.h */
+struct terminal_S {
+    term_T	*tl_next;
+
+    VTerm	*tl_vterm;
+    job_T	*tl_job;
+
+    /* Range of screen rows to update.  Zero based. */
+    int		tl_dirty_row_start; /* -1 if nothing dirty */
+    int		tl_dirty_row_end;   /* row below last one to update */
+
+    pos_T	tl_cursor;
+};
+
+#define MAX_ROW 999999	    /* used for tl_dirty_row_end to update all rows */
+
+/*
+ * List of all active terminals.
+ */
+static term_T *first_term = NULL;
+
+static int handle_damage(VTermRect rect, void *user);
+static int handle_moverect(VTermRect dest, VTermRect src, void *user);
+static int handle_movecursor(VTermPos pos, VTermPos oldpos, int visible, void *user);
+static int handle_resize(int rows, int cols, void *user);
+
+static VTermScreenCallbacks screen_callbacks = {
+  handle_damage,	/* damage */
+  handle_moverect,	/* moverect */
+  handle_movecursor,	/* movecursor */
+  NULL,			/* settermprop */
+  NULL,			/* bell */
+  handle_resize,	/* resize */
+  NULL,			/* sb_pushline */
+  NULL			/* sb_popline */
+};
+
+/*
+ * ":terminal": open a terminal window and execute a job in it.
+ */
+    void
+ex_terminal(exarg_T *eap)
+{
+    int		rows;
+    int		cols;
+    exarg_T	split_ea;
+    win_T	*old_curwin = curwin;
+    typval_T	argvars[2];
+    term_T	*term;
+    VTerm	*vterm;
+    VTermScreen *screen;
+
+    if (check_restricted() || check_secure())
+	return;
+
+    term = (term_T *)alloc_clear(sizeof(term_T));
+    if (term == NULL)
+	return;
+    term->tl_dirty_row_end = MAX_ROW;
+
+    /* Open a new window or tab. */
+    vim_memset(&split_ea, 0, sizeof(split_ea));
+    split_ea.cmdidx = CMD_new;
+    split_ea.cmd = (char_u *)"new";
+    split_ea.arg = (char_u *)"";
+    ex_splitview(&split_ea);
+    if (curwin == old_curwin)
+    {
+	/* split failed */
+	vim_free(term);
+	return;
+    }
+
+    curbuf->b_term = term;
+    term->tl_next = first_term;
+    first_term = term;
+
+    /* TODO: set buffer type, hidden, etc. */
+
+    if (*curwin->w_p_tms != NUL)
+    {
+	char_u *p = vim_strchr(curwin->w_p_tms, 'x') + 1;
+
+	rows = atoi((char *)curwin->w_p_tms);
+	cols = atoi((char *)p);
+	/* TODO: resize window if possible. */
+    }
+    else
+    {
+	rows = curwin->w_height;
+	cols = curwin->w_width;
+    }
+
+    vterm = vterm_new(rows, cols);
+    term->tl_vterm = vterm;
+    screen = vterm_obtain_screen(vterm);
+    vterm_screen_set_callbacks(screen, &screen_callbacks, term);
+
+    argvars[0].v_type = VAR_STRING;
+    argvars[0].vval.v_string = eap->arg;
+    argvars[1].v_type = VAR_UNKNOWN;
+    term->tl_job = job_start(argvars);
+
+    /* TODO: setup channels to/from job */
+    /* Setup pty, see mch_call_shell(). */
+}
+
+    static int
+handle_damage(VTermRect rect, void *user)
+{
+    term_T *term = (term_T *)user;
+
+    term->tl_dirty_row_start = MIN(term->tl_dirty_row_start, rect.start_row);
+    term->tl_dirty_row_end = MAX(term->tl_dirty_row_end, rect.end_row);
+    return 1;
+}
+
+    static int
+handle_moverect(VTermRect dest, VTermRect src, void *user)
+{
+    /* TODO */
+    return 1;
+}
+
+  static int
+handle_movecursor(VTermPos pos, VTermPos oldpos, int visible, void *user)
+{
+    /* TODO: handle moving the cursor. */
+    return 1;
+}
+
+    static int
+handle_resize(int rows, int cols, void *user)
+{
+    /* TODO: handle terminal resize. */
+    return 1;
+}
+
+/* TODO: Use win_del_lines() to make scroll up efficient. */
+
+/* TODO: function to update the window.
+ * Get the screen contents from vterm with vterm_screen_get_cell().
+ * put in current_ScreenLine and call screen_line().
+ */
+
+/* TODO: function to wait for input and send it to the job.
+ * Return when a CTRL-W command is typed that moves to another window.
+ * Convert special keys to vterm keys:
+ * - Write keys to vterm: vterm_keyboard_key()
+ * - read the output (xterm escape sequences): vterm_output_read()
+ * - Write output to channel.
+ */
+
+/* TODO: function to read job output from the channel.
+ * write to vterm: vterm_input_write()
+ * This will invoke screen callbacks.
+ * call vterm_screen_flush_damage()
+ */
+
+#endif /* FEAT_TERMINAL */
--- a/src/version.c
+++ b/src/version.c
@@ -765,6 +765,8 @@ static char *(features[]) =
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    693,
+/**/
     692,
 /**/
     691,