changeset 30876:2d2758ffd959 v9.0.0772

patch 9.0.0772: the libvterm code is outdated Commit: https://github.com/vim/vim/commit/501e77766cd30d8f4bfeb04a67aff1865b5f5a41 Author: Bram Moolenaar <Bram@vim.org> Date: Sun Oct 16 14:35:46 2022 +0100 patch 9.0.0772: the libvterm code is outdated Problem: The libvterm code is outdated. Solution: Include libvterm changes from revision 790 to 801.
author Bram Moolenaar <Bram@vim.org>
date Sun, 16 Oct 2022 15:45:04 +0200
parents 3295247d97a5
children 096d7e00a7ea
files src/libvterm/README src/libvterm/doc/seqs.txt src/libvterm/include/vterm.h src/libvterm/src/screen.c src/libvterm/src/state.c src/libvterm/src/vterm.c src/libvterm/src/vterm_internal.h src/libvterm/t/26state_query.test src/libvterm/t/60screen_ascii.test src/libvterm/t/61screen_unicode.test src/libvterm/t/62screen_damage.test src/libvterm/t/63screen_resize.test src/libvterm/t/65screen_protect.test src/libvterm/t/harness.c src/libvterm/t/run-test.pl src/version.c
diffstat 16 files changed, 198 insertions(+), 99 deletions(-) [+]
line wrap: on
line diff
--- a/src/libvterm/README
+++ b/src/libvterm/README
@@ -8,7 +8,7 @@ The original can be found:
 
 Modifications:
 - Add a .gitignore file.
-- Convert from C99 to C90.
+- Convert some code from C99 to C90.
 - Other changes to support embedding in Vim.
 
 To get the latest version of libvterm you need the "bzr" command and do:
--- a/src/libvterm/doc/seqs.txt
+++ b/src/libvterm/doc/seqs.txt
@@ -132,6 +132,7 @@ 123x   CSI   n          = DSR, Device St
            1 or 2       =   block
            3 or 4       =   underline
            5 or 6       =   I-beam to left
+   x   CSI > q          = XTVERSION, request version string
  23x   CSI " q          = DECSCA, select character attributes
 123x   CSI r            = DECSTBM
    x   CSI s            = DECSLRM
--- a/src/libvterm/include/vterm.h
+++ b/src/libvterm/include/vterm.h
@@ -26,6 +26,11 @@ typedef unsigned int		uint32_t;
 #define VTERM_CHECK_VERSION \
         vterm_check_version(VTERM_VERSION_MAJOR, VTERM_VERSION_MINOR)
 
+/* Any cell can contain at most one basic printing character and 5 combining
+ * characters. This number could be changed but will be ABI-incompatible if
+ * you do */
+#define VTERM_MAX_CHARS_PER_CELL 6
+
 typedef struct VTerm VTerm;
 typedef struct VTermState VTermState;
 typedef struct VTermScreen VTermScreen;
@@ -297,6 +302,7 @@ typedef struct {
  */
 typedef struct {
   VTermPos pos;                /* current cursor position */
+  VTermLineInfo *lineinfos[2]; /* [1] may be NULL */
 } VTermStateFields;
 
 typedef struct {
@@ -306,11 +312,30 @@ typedef struct {
   void  (*free)(void *ptr, void *allocdata);
 } VTermAllocatorFunctions;
 
+/* A convenient shortcut for default cases */
 void vterm_check_version(int major, int minor);
 
+struct VTermBuilder {
+  int ver; /* currently unused but reserved for some sort of ABI version flag */
+
+  int rows, cols;
+
+  const VTermAllocatorFunctions *allocator;
+  void *allocdata;
+
+  /* Override default sizes for various structures */
+  size_t outbuffer_len;  /* default: 4096 */
+  size_t tmpbuffer_len;  /* default: 4096 */
+};
+
+VTerm *vterm_build(const struct VTermBuilder *builder);
+
+/* A convenient shortcut for default cases */
 // Allocate and initialize a new terminal with default allocators.
 VTerm *vterm_new(int rows, int cols);
 
+/* These shortcuts are generally discouraged in favour of just using vterm_build() */
+
 // Allocate and initialize a new terminal with specified allocators.
 VTerm *vterm_new_with_allocator(int rows, int cols, VTermAllocatorFunctions *funcs, void *allocdata);
 
@@ -507,7 +532,6 @@ enum {
 };
 
 typedef struct {
-#define VTERM_MAX_CHARS_PER_CELL 6
   uint32_t chars[VTERM_MAX_CHARS_PER_CELL];
   char     width;
   VTermScreenCellAttrs attrs;
--- a/src/libvterm/src/screen.c
+++ b/src/libvterm/src/screen.c
@@ -502,7 +502,10 @@ static void resize_buffer(VTermScreen *s
   int old_cols = screen->cols;
 
   ScreenCell *old_buffer = screen->buffers[bufidx];
+  VTermLineInfo *old_lineinfo = statefields->lineinfos[bufidx];
+
   ScreenCell *new_buffer = vterm_allocator_malloc(screen->vt, sizeof(ScreenCell) * new_rows * new_cols);
+  VTermLineInfo *new_lineinfo = vterm_allocator_malloc(screen->vt, sizeof(new_lineinfo[0]) * new_rows);
 
   // Find the final row of old buffer content
   int old_row = old_rows - 1;
@@ -515,6 +518,8 @@ static void resize_buffer(VTermScreen *s
     for( ; col < new_cols; col++)
       clearcell(screen, &new_buffer[new_row * new_cols + col]);
 
+    new_lineinfo[new_row] = old_lineinfo[old_row];
+
     old_row--;
     new_row--;
 
@@ -584,15 +589,21 @@ static void resize_buffer(VTermScreen *s
     /* Scroll new rows back up to the top and fill in blanks at the bottom */
     int moverows = new_rows - new_row - 1;
     memmove(&new_buffer[0], &new_buffer[(new_row + 1) * new_cols], moverows * new_cols * sizeof(ScreenCell));
+    memmove(&new_lineinfo[0], &new_lineinfo[new_row + 1], moverows * sizeof(new_lineinfo[0]));
 
-    for(new_row = moverows; new_row < new_rows; new_row++)
+    for(new_row = moverows; new_row < new_rows; new_row++) {
       for(col = 0; col < new_cols; col++)
         clearcell(screen, &new_buffer[new_row * new_cols + col]);
+      new_lineinfo[new_row] = (VTermLineInfo){ 0 };
+    }
   }
 
   vterm_allocator_free(screen->vt, old_buffer);
   screen->buffers[bufidx] = new_buffer;
 
+  vterm_allocator_free(screen->vt, old_lineinfo);
+  statefields->lineinfos[bufidx] = new_lineinfo;
+
   return;
 
   /* REFLOW TODO:
@@ -606,6 +617,7 @@ static int resize(int new_rows, int new_
 
   int altscreen_active = (screen->buffers[BUFIDX_ALTSCREEN] && screen->buffer == screen->buffers[BUFIDX_ALTSCREEN]);
 
+  int old_rows = screen->rows;
   int old_cols = screen->cols;
 
   if(new_cols > old_cols) {
@@ -619,6 +631,17 @@ static int resize(int new_rows, int new_
   resize_buffer(screen, 0, new_rows, new_cols, !altscreen_active, fields);
   if(screen->buffers[BUFIDX_ALTSCREEN])
     resize_buffer(screen, 1, new_rows, new_cols, altscreen_active, fields);
+  else if(new_rows != old_rows) {
+    /* We don't need a full resize of the altscreen because it isn't enabled
+     * but we should at least keep the lineinfo the right size */
+    vterm_allocator_free(screen->vt, fields->lineinfos[BUFIDX_ALTSCREEN]);
+
+    VTermLineInfo *new_lineinfo = vterm_allocator_malloc(screen->vt, sizeof(new_lineinfo[0]) * new_rows);
+    for(int row = 0; row < new_rows; row++)
+      new_lineinfo[row] = (VTermLineInfo){ 0 };
+
+    fields->lineinfos[BUFIDX_ALTSCREEN] = new_lineinfo;
+  }
 
   screen->buffer = altscreen_active ? screen->buffers[BUFIDX_ALTSCREEN] : screen->buffers[BUFIDX_PRIMARY];
 
--- a/src/libvterm/src/state.c
+++ b/src/libvterm/src/state.c
@@ -280,7 +280,6 @@ static void set_lineinfo(VTermState *sta
 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;
@@ -290,9 +289,8 @@ static int on_text(const char bytes[], s
 
   // We'll have at most len codepoints, plus one from a previous incomplete
   // sequence.
-  codepoints = vterm_allocator_malloc(state->vt, (len + 1) * sizeof(uint32_t));
-  if (codepoints == NULL)
-    return 0;
+  uint32_t *codepoints = (uint32_t *)(state->vt->tmpbuffer);
+  size_t maxpoints = (state->vt->tmpbuffer_len) / sizeof(uint32_t);
 
   encoding =
     state->gsingle_set     ? &state->encoding[state->gsingle_set] :
@@ -301,7 +299,7 @@ static int on_text(const char bytes[], s
                              &state->encoding[state->gr_set];
 
   (*encoding->enc->decode)(encoding->enc, encoding->data,
-      codepoints, &npoints, state->gsingle_set ? 1 : (int)len,
+      codepoints, &npoints, state->gsingle_set ? 1 : (int)maxpoints,
       bytes, &eaten, len);
 
   /* There's a chance an encoding (e.g. UTF-8) hasn't found enough bytes yet
@@ -309,7 +307,6 @@ static int on_text(const char bytes[], s
    */
   if(!npoints)
   {
-    vterm_allocator_free(state->vt, codepoints);
     return (int)eaten;
   }
 
@@ -363,13 +360,14 @@ static int on_text(const char bytes[], s
     int glyph_starts = i;
     int glyph_ends;
     int width = 0;
-    uint32_t *chars;
-
-    for(glyph_ends = i + 1; glyph_ends < npoints; glyph_ends++)
+
+    for(glyph_ends = i + 1;
+        (glyph_ends < npoints) && (glyph_ends < glyph_starts + VTERM_MAX_CHARS_PER_CELL);
+        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));
+    uint32_t *chars = vterm_allocator_malloc(state->vt, (VTERM_MAX_CHARS_PER_CELL + 1) * sizeof(uint32_t));
     if (chars == NULL)
       break;
 
@@ -461,7 +459,6 @@ static int on_text(const char bytes[], s
   }
 #endif
 
-  vterm_allocator_free(state->vt, codepoints);
   return (int)eaten;
 }
 
@@ -952,6 +949,12 @@ static void request_dec_mode(VTermState 
   vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "?%d;%d$y", num, reply ? 1 : 2);
 }
 
+static void request_version_string(VTermState *state)
+{
+  vterm_push_output_sprintf_str(state->vt, C1_DCS, TRUE, ">|libvterm(%d.%d)",
+      VTERM_VERSION_MAJOR, VTERM_VERSION_MINOR);
+}
+
 static int on_csi(const char *leader, const long args[], int argcount, const char *intermed, char command, void *user)
 {
   VTermState *state = user;
@@ -1423,6 +1426,10 @@ static int on_csi(const char *leader, co
     request_dec_mode(state, CSI_ARG(args[0]));
     break;
 
+  case LEADER('>', 0x71): // XTVERSION - xterm query version string
+    request_version_string(state);
+    break;
+
   case INTERMED(' ', 0x71): // DECSCUSR - DEC set cursor shape
     val = CSI_ARG_OR(args[0], 1);
 
@@ -2018,33 +2025,6 @@ static int on_resize(int rows, int cols,
     state->tabstops = newtabstops;
   }
 
-  if(rows != state->rows) {
-    int bufidx;
-    for(bufidx = BUFIDX_PRIMARY; bufidx <= BUFIDX_ALTSCREEN; bufidx++) {
-      int row;
-      VTermLineInfo *oldlineinfo = state->lineinfos[bufidx];
-      VTermLineInfo *newlineinfo;
-      if(!oldlineinfo)
-        continue;
-
-      newlineinfo = vterm_allocator_malloc(state->vt, rows * sizeof(VTermLineInfo));
-
-      for(row = 0; row < state->rows && row < rows; row++) {
-        newlineinfo[row] = oldlineinfo[row];
-      }
-
-      for( ; row < rows; row++) {
-	VTermLineInfo lineInfo = {0x0};
-	newlineinfo[row] = lineInfo;
-      }
-
-      vterm_allocator_free(state->vt, state->lineinfos[bufidx]);
-      state->lineinfos[bufidx] = newlineinfo;
-    }
-
-    state->lineinfo = state->lineinfos[state->mode.alt_screen ? BUFIDX_ALTSCREEN : BUFIDX_PRIMARY];
-  }
-
   state->rows = rows;
   state->cols = cols;
 
@@ -2054,11 +2034,44 @@ static int on_resize(int rows, int cols,
     UBOUND(state->scrollregion_right, state->cols);
 
   fields.pos = state->pos;
-
-  if(state->callbacks && state->callbacks->resize)
+  fields.lineinfos[0] = state->lineinfos[0];
+  fields.lineinfos[1] = state->lineinfos[1];
+
+  if(state->callbacks && state->callbacks->resize) {
     (*state->callbacks->resize)(rows, cols, &fields, state->cbdata);
 
-  state->pos = fields.pos;
+    state->pos = fields.pos;
+
+    state->lineinfos[0] = fields.lineinfos[0];
+    state->lineinfos[1] = fields.lineinfos[1];
+  }
+  else {
+    if(rows != state->rows) {
+      for(int bufidx = BUFIDX_PRIMARY; bufidx <= BUFIDX_ALTSCREEN; bufidx++) {
+        VTermLineInfo *oldlineinfo = state->lineinfos[bufidx];
+        if(!oldlineinfo)
+          continue;
+
+        VTermLineInfo *newlineinfo = vterm_allocator_malloc(state->vt, rows * sizeof(VTermLineInfo));
+
+        int row;
+        for(row = 0; row < state->rows && row < rows; row++) {
+          newlineinfo[row] = oldlineinfo[row];
+        }
+
+        for( ; row < rows; row++) {
+          newlineinfo[row] = (VTermLineInfo){
+            .doublewidth = 0,
+          };
+        }
+
+        vterm_allocator_free(state->vt, state->lineinfos[bufidx]);
+        state->lineinfos[bufidx] = newlineinfo;
+      }
+    }
+  }
+
+  state->lineinfo = state->lineinfos[state->mode.alt_screen ? BUFIDX_ALTSCREEN : BUFIDX_PRIMARY];
 
   if(state->at_phantom && state->pos.col < cols-1) {
     state->at_phantom = 0;
--- a/src/libvterm/src/vterm.c
+++ b/src/libvterm/src/vterm.c
@@ -34,21 +34,39 @@ static VTermAllocatorFunctions default_a
 
 VTerm *vterm_new(int rows, int cols)
 {
-  return vterm_new_with_allocator(rows, cols, &default_allocator, NULL);
+  struct VTermBuilder builder;
+  memset(&builder, 0, sizeof(builder));
+  builder.rows = rows;
+  builder.cols = cols;
+  return vterm_build(&builder);
 }
 
 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);
+  struct VTermBuilder builder;
+  memset(&builder, 0, sizeof(builder));
+  builder.rows = rows;
+  builder.cols = cols;
+  builder.allocator = funcs;
+  builder.allocdata = allocdata;
+  return vterm_build(&builder);
+}
+
+/* A handy macro for defaulting values out of builder fields */
+#define DEFAULT(v, def)  ((v) ? (v) : (def))
 
-  if (vt == NULL)
-    return NULL;
-  vt->allocator = funcs;
-  vt->allocdata = allocdata;
+VTerm *vterm_build(const struct VTermBuilder *builder)
+{
+  const VTermAllocatorFunctions *allocator = DEFAULT(builder->allocator, &default_allocator);
 
-  vt->rows = rows;
-  vt->cols = cols;
+  /* Need to bootstrap using the allocator function directly */
+  VTerm *vt = (*allocator->malloc)(sizeof(VTerm), builder->allocdata);
+
+  vt->allocator = allocator;
+  vt->allocdata = builder->allocdata;
+
+  vt->rows = builder->rows;
+  vt->cols = builder->cols;
 
   vt->parser.state = NORMAL;
 
@@ -58,11 +76,11 @@ VTerm *vterm_new_with_allocator(int rows
   vt->outfunc = NULL;
   vt->outdata = NULL;
 
-  vt->outbuffer_len = 200;
+  vt->outbuffer_len = DEFAULT(builder->outbuffer_len, 4096);
   vt->outbuffer_cur = 0;
   vt->outbuffer = vterm_allocator_malloc(vt, vt->outbuffer_len);
 
-  vt->tmpbuffer_len = 64;
+  vt->tmpbuffer_len = DEFAULT(builder->tmpbuffer_len, 4096);
   vt->tmpbuffer = vterm_allocator_malloc(vt, vt->tmpbuffer_len);
 
   if (vt->tmpbuffer == NULL
--- a/src/libvterm/src/vterm_internal.h
+++ b/src/libvterm/src/vterm_internal.h
@@ -182,7 +182,7 @@ struct VTermState
 
 struct VTerm
 {
-  VTermAllocatorFunctions *allocator;
+  const VTermAllocatorFunctions *allocator;
   void *allocdata;
 
   int rows;
--- a/src/libvterm/t/26state_query.test
+++ b/src/libvterm/t/26state_query.test
@@ -6,6 +6,11 @@ RESET
 PUSH "\e[c"
   output "\e[?1;2c"
 
+!XTVERSION
+RESET
+PUSH "\e[>q"
+  output "\eP>|libvterm(0.2)\e\\"
+
 !DSR
 RESET
 PUSH "\e[5n"
@@ -57,6 +62,6 @@ PUSH "\e[5n"
   output "\x{9b}0n"
 PUSH "\e F"
 
-!Truncation on attempted buffer overflow
-PUSH "\e[6n" x 30
-  output "\e[10;10R" x 25
+#!Truncation on attempted buffer overflow
+#PUSH "\e[6n" x 30
+#  output "\e[10;10R" x 25
--- a/src/libvterm/t/60screen_ascii.test
+++ b/src/libvterm/t/60screen_ascii.test
@@ -18,11 +18,11 @@ PUSH "ABC"
   ?screen_eol 0,3 = 1
 PUSH "\e[H"
   movecursor 0,0
-  ?screen_chars 0,0,1,80 = "ABC"
+  ?screen_row 0 = "ABC"
   ?screen_text 0,0,1,80 = 0x41,0x42,0x43
 PUSH "E"
   movecursor 0,1
-  ?screen_chars 0,0,1,80 = "EBC"
+  ?screen_row 0 = "EBC"
   ?screen_text 0,0,1,80 = 0x45,0x42,0x43
 
 WANTSCREEN -c
@@ -30,14 +30,14 @@ WANTSCREEN -c
 !Erase
 RESET
 PUSH "ABCDE\e[H\e[K"
-  ?screen_chars 0,0,1,80 = 
+  ?screen_row 0 = ""
   ?screen_text 0,0,1,80 = 
 
 !Copycell
 RESET
 PUSH "ABC\e[H\e[@"
 PUSH "1"
-  ?screen_chars 0,0,1,80 = "1ABC"
+  ?screen_row 0 = "1ABC"
 
 RESET
 PUSH "ABC\e[H\e[P"
@@ -48,7 +48,7 @@ PUSH "ABC\e[H\e[P"
 !Space padding
 RESET
 PUSH "Hello\e[CWorld"
-  ?screen_chars 0,0,1,80 = "Hello World"
+  ?screen_row 0 = "Hello World"
   ?screen_text 0,0,1,80 = 0x48,0x65,0x6c,0x6c,0x6f,0x20,0x57,0x6f,0x72,0x6c,0x64
 
 !Linefeed padding
@@ -60,10 +60,10 @@ PUSH "Hello\r\nWorld"
 !Altscreen
 RESET
 PUSH "P"
-  ?screen_chars 0,0,1,80 = "P"
+  ?screen_row 0 = "P"
 PUSH "\e[?1049h"
-  ?screen_chars 0,0,1,80 = 
+  ?screen_row 0 = ""
 PUSH "\e[2K\e[HA"
-  ?screen_chars 0,0,1,80 = "A"
+  ?screen_row 0 = "A"
 PUSH "\e[?1049l"
-  ?screen_chars 0,0,1,80 = "P"
+  ?screen_row 0 = "P"
--- a/src/libvterm/t/61screen_unicode.test
+++ b/src/libvterm/t/61screen_unicode.test
@@ -7,7 +7,7 @@ WANTSCREEN
 # 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_row 0 = 0xc1,0xe9
   ?screen_text 0,0,1,80 = 0xc3,0x81,0xc3,0xa9
   ?screen_cell 0,0 = {0xc1} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)
 
@@ -16,7 +16,7 @@ PUSH "\xC3\x81\xC3\xA9"
 RESET
 PUSH "0123\e[H"
 PUSH "\xEF\xBC\x90"
-  ?screen_chars 0,0,1,80 = 0xff10,0x32,0x33
+  ?screen_row 0 = 0xff10,0x32,0x33
   ?screen_text 0,0,1,80 = 0xef,0xbc,0x90,0x32,0x33
   ?screen_cell 0,0 = {0xff10} width=2 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)
 
@@ -25,7 +25,7 @@ PUSH "\xEF\xBC\x90"
 RESET
 PUSH "0123\e[H"
 PUSH "e\xCC\x81"
-  ?screen_chars 0,0,1,80 = 0x65,0x301,0x31,0x32,0x33
+  ?screen_row 0 = 0x65,0x301,0x31,0x32,0x33
   ?screen_text 0,0,1,80 = 0x65,0xcc,0x81,0x31,0x32,0x33
   ?screen_cell 0,0 = {0x65,0x301} width=1 attrs={} fg=rgb(240,240,240) bg=rgb(0,0,0)
 
--- a/src/libvterm/t/62screen_damage.test
+++ b/src/libvterm/t/62screen_damage.test
@@ -152,4 +152,4 @@ PUSH "\e[25H\r\nABCDE\b\b\b\e[2P\r\n"
 DAMAGEFLUSH
   moverect 1..25,0..80 -> 0..24,0..80
   damage 24..25,0..80
-  ?screen_chars 23,0,24,5 = "ABE"
+  ?screen_row 23 = "ABE"
--- a/src/libvterm/t/63screen_resize.test
+++ b/src/libvterm/t/63screen_resize.test
@@ -28,20 +28,20 @@ PUSH "E"
 RESET
 RESIZE 25,80
 PUSH "Top\e[10HLine 10"
-  ?screen_chars 0,0,1,80 = "Top"
-  ?screen_chars 9,0,10,80 = "Line 10"
+  ?screen_row 0 = "Top"
+  ?screen_row 9 = "Line 10"
   ?cursor = 9,7
 RESIZE 20,80
-  ?screen_chars 0,0,1,80 = "Top"
-  ?screen_chars 9,0,10,80 = "Line 10"
+  ?screen_row 0 = "Top"
+  ?screen_row 9 = "Line 10"
   ?cursor = 9,7
 
 !Resize shorter with content must scroll
 RESET
 RESIZE 25,80
 PUSH "Top\e[25HLine 25\e[15H"
-  ?screen_chars 0,0,1,80 = "Top"
-  ?screen_chars 24,0,25,80 = "Line 25"
+  ?screen_row 0 = "Top"
+  ?screen_row 24 = "Line 25"
   ?cursor = 14,0
 WANTSCREEN b
 RESIZE 20,80
@@ -50,8 +50,8 @@ RESIZE 20,80
   sb_pushline 80 =
   sb_pushline 80 =
   sb_pushline 80 =
-  ?screen_chars 0,0,1,80 = 
-  ?screen_chars 19,0,20,80 = "Line 25"
+  ?screen_row 0  = ""
+  ?screen_row 19 = "Line 25"
   ?cursor = 9,0
 
 !Resize shorter does not lose line with cursor
@@ -62,11 +62,11 @@ RESIZE 25,80
 WANTSCREEN b
 PUSH "\e[24HLine 24\r\nLine 25\r\n"
   sb_pushline 80 =
-  ?screen_chars 23,0,24,10 = "Line 25"
+  ?screen_row 23 = "Line 25"
   ?cursor = 24,0
 RESIZE 24,80
   sb_pushline 80 =
-  ?screen_chars 22,0,23,10 = "Line 25"
+  ?screen_row 22 = "Line 25"
   ?cursor = 23,0
 
 !Resize shorter does not send the cursor to a negative row
@@ -90,8 +90,8 @@ RESET
 WANTSCREEN -b
 RESIZE 25,80
 PUSH "Line 1\e[25HBottom\e[15H"
-  ?screen_chars 0,0,1,80 = "Line 1"
-  ?screen_chars 24,0,25,80 = "Bottom"
+  ?screen_row 0  = "Line 1"
+  ?screen_row 24 = "Bottom"
   ?cursor = 14,0
 WANTSCREEN b
 RESIZE 30,80
@@ -100,9 +100,9 @@ RESIZE 30,80
   sb_popline 80
   sb_popline 80
   sb_popline 80
-  ?screen_chars 0,0,1,80 = "ABCDE"
-  ?screen_chars 5,0,6,80 = "Line 1"
-  ?screen_chars 29,0,30,80 = "Bottom"
+  ?screen_row 0  = "ABCDE"
+  ?screen_row 5  = "Line 1"
+  ?screen_row 29 = "Bottom"
   ?cursor = 19,0
 WANTSCREEN -b
 
@@ -112,6 +112,6 @@ WANTSCREEN a
 RESIZE 25,80
 PUSH "Main screen\e[?1049h\e[HAlt screen"
 RESIZE 30,80
-  ?screen_chars 0,0,1,3 = "Alt"
+  ?screen_row 0 = "Alt screen"
 PUSH "\e[?1049l"
-  ?screen_chars 0,0,1,3 = "Mai"
+  ?screen_row 0 = "Main screen"
--- a/src/libvterm/t/65screen_protect.test
+++ b/src/libvterm/t/65screen_protect.test
@@ -4,13 +4,13 @@ WANTSCREEN
 !Selective erase
 RESET
 PUSH "A\e[1\"qB\e[\"qC"
-  ?screen_chars 0,0,1,3 = 0x41,0x42,0x43
+  ?screen_row 0 = "ABC"
 PUSH "\e[G\e[?J"
-  ?screen_chars 0,0,1,3 = 0x20,0x42
+  ?screen_row 0 = " B"
 
 !Non-selective erase
 RESET
 PUSH "A\e[1\"qB\e[\"qC"
-  ?screen_chars 0,0,1,3 = 0x41,0x42,0x43
+  ?screen_row 0 = "ABC"
 PUSH "\e[G\e[J"
-  ?screen_chars 0,0,1,3 = 
+  ?screen_row 0 = ""
--- a/src/libvterm/t/harness.c
+++ b/src/libvterm/t/harness.c
@@ -1001,7 +1001,14 @@ int main(int argc UNUSED, char **argv UN
         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) {
+        if(sscanf(linep, "%d,%d,%d,%d", &rect.start_row, &rect.start_col, &rect.end_row, &rect.end_col) == 4)
+          ; // fine
+        else if(sscanf(linep, "%d", &rect.start_row) == 1) {
+          rect.end_row = rect.start_row + 1;
+          rect.start_col = 0;
+          vterm_get_size(vt, NULL, &rect.end_col);
+        }
+        else {
           printf("! screen_chars unrecognised input\n");
           goto abort_line;
         }
--- a/src/libvterm/t/run-test.pl
+++ b/src/libvterm/t/run-test.pl
@@ -139,17 +139,23 @@ sub do_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);
+      my $want;
+
+      if( $line =~ m/^"/ ) {
+         $want = eval($line);
+      }
+      else {
+         # Turn 0xDD,0xDD,... directly into bytes
+         $want = pack "C*", map { hex } split m/,/, $line;
+      }
 
       do_onetest if defined $command;
 
-      # TODO: may not be 80
-      $hin->print( "\?screen_chars $row,0,$row1,80\n" );
+      $hin->print( "\?screen_chars $row\n" );
       my $response = <$hout>;
       chomp $response;
 
-      $response = pack "C*", map hex, split m/,/, $response;
+      $response = pack "C*", map { hex } split m/,/, $response;
       if( $response ne $want ) {
          print "# line $linenum: Assert ?screen_row $row failed:\n" .
                "# Expected: $want\n" .
--- a/src/version.c
+++ b/src/version.c
@@ -696,6 +696,8 @@ static char *(features[]) =
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    772,
+/**/
     771,
 /**/
     770,