URI:
       [ST][PATCH] Bug fix in scrollback reflow standalone extended. - sites - public wiki contents of suckless.org
  HTML git clone git://git.suckless.org/sites
   DIR Log
   DIR Files
   DIR Refs
       ---
   DIR commit 04f6d9c4a2ab4449cac975afc2ea1fffc2f9c75c
   DIR parent 65946acfd6c946798e9f8a7babc62445fb4b505f
  HTML Author: Milos Nikic <nikic.milos@gmail.com>
       Date:   Mon, 23 Feb 2026 10:38:47 -0800
       
       [ST][PATCH] Bug fix in scrollback reflow standalone extended.
       
       Diffstat:
         M st.suckless.org/patches/scrollback… |       2 +-
         D st.suckless.org/patches/scrollback… |    1078 -------------------------------
         A st.suckless.org/patches/scrollback… |    1048 +++++++++++++++++++++++++++++++
       
       3 files changed, 1049 insertions(+), 1079 deletions(-)
       ---
   DIR diff --git a/st.suckless.org/patches/scrollback-reflow-standalone/index.md b/st.suckless.org/patches/scrollback-reflow-standalone/index.md
       @@ -126,7 +126,7 @@ No content is clipped or lost; only wrapping changes.
        Download
        --------
        * [st-scrollback-reflow-standalone-0.9.3.diff](st-scrollback-reflow-standalone-0.9.3.diff)
       -* [st-scrollback-reflow-standalone-extended-0.9.3.diff](st-scrollback-reflow-standalone-extended-0.9.3.diff)
       +* [st-scrollback-reflow-standalone-extended-0.9.31.diff](st-scrollback-reflow-standalone-extended-0.9.31.diff)
        
        Author
        ------
   DIR diff --git a/st.suckless.org/patches/scrollback-reflow-standalone/st-scrollback-reflow-standalone-extended-0.9.3.diff b/st.suckless.org/patches/scrollback-reflow-standalone/st-scrollback-reflow-standalone-extended-0.9.3.diff
       @@ -1,1078 +0,0 @@
       -From 55e224cf4d767db7d9184e70a0f3838935679a53 Mon Sep 17 00:00:00 2001
       -From: Milos Nikic <nikic.milos@gmail.com>
       -Date: Thu, 15 Jan 2026 16:08:59 -0800
       -Subject: [PATCH] st: alternative scrollback using ring buffer and view offset
       -
       -Implement scrollback as a fixed-size ring buffer and render history
       -by offsetting the view instead of copying screen contents.
       -Implement reflow of history and screen content on resize if it is needed.
       -
       -Tradeoffs / differences:
       -  - Scrollback is disabled on the alternate screen
       -  - Simpler model than the existing scrollback patch set
       -  - Mouse wheel scrolling enabled by default
       -  - Shift + page up/down and shift + end/home work as well.
       -  - When using vim, mouse movement will no longer move the cursor.
       -  - There can be visual artifacts if width of the window is shrank to the
       -    size smaller than the shell promp.
       -  - Mouse selection is persistent even if it goes off screen but it get
       -    reset on resize.
       ----
       - config.def.h |   9 +
       - st.c         | 727 ++++++++++++++++++++++++++++++++++++++++++++-------
       - st.h         |   5 +
       - x.c          |  17 ++
       - 4 files changed, 659 insertions(+), 99 deletions(-)
       -
       -diff --git a/config.def.h b/config.def.h
       -index 2cd740a..135a0b1 100644
       ---- a/config.def.h
       -+++ b/config.def.h
       -@@ -192,6 +192,10 @@ static Shortcut shortcuts[] = {
       -         { XK_ANY_MOD,           XK_Break,       sendbreak,      {.i =  0} },
       -         { ControlMask,          XK_Print,       toggleprinter,  {.i =  0} },
       -         { ShiftMask,            XK_Print,       printscreen,    {.i =  0} },
       -+        { ShiftMask,            XK_Page_Up,     kscrollup,      {.i = -1} },
       -+        { ShiftMask,            XK_Page_Down,   kscrolldown,    {.i = -1} },
       -+        { ShiftMask,            XK_Home,        kscrollup,      {.i = 1000000} },
       -+        { ShiftMask,            XK_End,         kscrolldown,    {.i = 1000000} },
       -         { XK_ANY_MOD,           XK_Print,       printsel,       {.i =  0} },
       -         { TERMMOD,              XK_Prior,       zoom,           {.f = +1} },
       -         { TERMMOD,              XK_Next,        zoom,           {.f = -1} },
       -@@ -472,3 +476,8 @@ static char ascii_printable[] =
       -         " !\"#$%&'()*+,-./0123456789:;<=>?"
       -         "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_"
       -         "`abcdefghijklmnopqrstuvwxyz{|}~";
       -+
       -+/*
       -+ * The amount of lines scrollback can hold before it wraps around.
       -+ */
       -+unsigned int scrollback_lines = 5000;
       -diff --git a/st.c b/st.c
       -index e55e7b3..9565003 100644
       ---- a/st.c
       -+++ b/st.c
       -@@ -5,6 +5,7 @@
       - #include <limits.h>
       - #include <pwd.h>
       - #include <stdarg.h>
       -+#include <stdint.h>
       - #include <stdio.h>
       - #include <stdlib.h>
       - #include <string.h>
       -@@ -178,7 +179,7 @@ static void tdeletechar(int);
       - static void tdeleteline(int);
       - static void tinsertblank(int);
       - static void tinsertblankline(int);
       --static int tlinelen(int);
       -+static int tlinelen(Line);
       - static void tmoveto(int, int);
       - static void tmoveato(int, int);
       - static void tnewline(int);
       -@@ -232,6 +233,376 @@ static const uchar utfmask[UTF_SIZ + 1] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8};
       - static const Rune utfmin[UTF_SIZ + 1] = {       0,    0,  0x80,  0x800,  0x10000};
       - static const Rune utfmax[UTF_SIZ + 1] = {0x10FFFF, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF};
       - 
       -+typedef struct
       -+{
       -+        Line *buf;       /* ring of Line pointers */
       -+        int cap;         /* max number of lines */
       -+        int len;         /* current number of valid lines (<= cap) */
       -+        int head;        /* physical index of logical oldest (valid when len>0) */
       -+        uint64_t base;   /* Can overflow in the extreme */
       -+        /*
       -+         * max_width tracks the widest line ever pushed to scrollback.
       -+         * It may be conservative (stale) if that line has since been
       -+         * evicted from the ring buffer, which is acceptable - it just
       -+         * means we might reflow when not strictly necessary, which is
       -+         * better than skipping a needed reflow.
       -+         */
       -+        int max_width;
       -+        int view_offset; /* 0 means live screen */
       -+} Scrollback;
       -+
       -+static Scrollback sb;
       -+
       -+static int
       -+sb_phys_index(int logical_idx)
       -+{
       -+        /* logical_idx: 0..sb.len-1 (0 = oldest) */
       -+        return (sb.head + logical_idx) % sb.cap;
       -+}
       -+
       -+static Line
       -+lineclone(Line src)
       -+{
       -+        Line dst;
       -+
       -+        if (!src)
       -+                return NULL;
       -+
       -+        dst = xmalloc(term.col * sizeof(Glyph));
       -+        memcpy(dst, src, term.col * sizeof(Glyph));
       -+        return dst;
       -+}
       -+
       -+static void
       -+sb_init(int lines)
       -+{
       -+        int i;
       -+
       -+        sb.buf  = xmalloc(sizeof(Line) * lines);
       -+        sb.cap  = lines;
       -+        sb.len  = 0;
       -+        sb.head = 0;
       -+        sb.base = 0;
       -+        for (i = 0; i < sb.cap; i++)
       -+                sb.buf[i] = NULL;
       -+
       -+        sb.view_offset = 0;
       -+        sb.max_width = 0;
       -+}
       -+
       -+/* Push one screen line into scrollback.
       -+ * Overwrites oldest when full (ring buffer).
       -+ */
       -+static void
       -+sb_push(Line line)
       -+{
       -+        Line copy;
       -+        int tail;
       -+        int width;
       -+
       -+        if (sb.cap <= 0)
       -+                return;
       -+
       -+        copy = lineclone(line);
       -+
       -+        if (sb.len < sb.cap) {
       -+                tail = sb_phys_index(sb.len);
       -+                sb.buf[tail] = copy;
       -+                sb.len++;
       -+        } else {
       -+                /* We might've just evicted the widest line... */
       -+                free(sb.buf[sb.head]);
       -+                sb.buf[sb.head] = copy;
       -+                sb.head = (sb.head + 1) % sb.cap;
       -+                sb.base++;
       -+        }
       -+        width = tlinelen(copy);
       -+        /* ...so max_width might be stale. */
       -+        if (width > sb.max_width)
       -+                sb.max_width = width;
       -+}
       -+
       -+static Line
       -+sb_get(int idx)
       -+{
       -+        /* idx is logical: 0..sb.len-1 */
       -+        if (idx < 0 || idx >= sb.len)
       -+                return NULL;
       -+        return sb.buf[sb_phys_index(idx)];
       -+}
       -+
       -+static void
       -+sb_clear(void)
       -+{
       -+        int i;
       -+        int p;
       -+
       -+        if (!sb.buf)
       -+                return;
       -+
       -+        for (i = 0; i < sb.len; i++) {
       -+                p = sb_phys_index(i);
       -+                if (sb.buf[p]) {
       -+                        free(sb.buf[p]);
       -+                        sb.buf[p] = NULL;
       -+                }
       -+        }
       -+
       -+        sb.len = 0;
       -+        sb.head = 0;
       -+        sb.base = 0;
       -+        sb.view_offset = 0;
       -+        sb.max_width = 0;
       -+}
       -+
       -+/*
       -+ * Reflows the scrollback buffer to fit a new terminal width.
       -+ *
       -+ * The algorithm works in three steps:
       -+ * 1) Unwrap: It iterates through the existing history, joining physical lines
       -+ * marked with ATTR_WRAP into a single continuous 'logical' line.
       -+ * 2) Reflow: It slices this logical line into new chunks of size 'col'.
       -+ * - New wrap flags are applied where the text exceeds the new width.
       -+ * - Trailing spaces are trimmed to prevent ghost padding.
       -+ * 3) Rebuild: The new lines are pushed into a fresh ring buffer.
       -+ * - Uses O(1) ring insertion (updating head/tail) to avoid expensive
       -+ * memmoves during resize, but it is still O(N) where N is the existing
       -+ * history.
       -+ *
       -+ * Note: During reflow we reset sb to match the rebuilt buffer
       -+ * (head, base and len might change).
       -+ */
       -+static void
       -+sb_resize(int col)
       -+{
       -+        Line *new_buf;
       -+        int i, j;
       -+        int new_len, logical_cap, logical_len, is_wrapped, cursor;
       -+        int copy_width, tail, current_width;
       -+        Line logical, line, nl;
       -+        uint64_t new_base = 0;
       -+        int new_head = 0;
       -+        int new_max_width = 0;
       -+        Glyph *g;
       -+
       -+        new_len = 0;
       -+
       -+        if (sb.len == 0)
       -+                return;
       -+
       -+        new_buf = xmalloc(sizeof(Line) * sb.cap);
       -+        for (i = 0; i < sb.cap; i++)
       -+                new_buf[i] = NULL;
       -+
       -+        logical_cap = term.col * 2;
       -+        logical = xmalloc(logical_cap * sizeof(Glyph));
       -+        logical_len = 0;
       -+
       -+        for (i = 0; i < sb.len; i++) {
       -+                /* Unwrap: Accumulate physical lines into one logical line. */
       -+                line = sb_get(i);
       -+                is_wrapped = (line[term.col - 1].mode & ATTR_WRAP);
       -+                if (logical_len + term.col > logical_cap) {
       -+                        logical_cap *= 2;
       -+                        logical = xrealloc(logical, logical_cap * sizeof(Glyph));
       -+                }
       -+
       -+                memcpy(logical + logical_len, line, term.col * sizeof(Glyph));
       -+                for (j = 0; j < term.col; j++) {
       -+                        logical[logical_len + j].mode &= ~ATTR_WRAP;
       -+                }
       -+                logical_len += term.col;
       -+                /* If the line was wrapped, continue accumulating before reflowing. */
       -+                if (is_wrapped) {
       -+                        continue;
       -+                }
       -+                /* Trim trailing spaces from the fully unwrapped line. */
       -+                while (logical_len > 0) {
       -+                        g = &logical[logical_len - 1];
       -+                        if (g->u == ' ' && g->bg == defaultbg
       -+                                        && (g->mode & ATTR_BOLD) == 0) {
       -+                                logical_len--;
       -+                        } else {
       -+                                break;
       -+                        }
       -+                }
       -+                if (logical_len == 0)
       -+                        logical_len = 1;
       -+
       -+                /* Reflow: Split the logical line into new chunks. */
       -+                cursor = 0;
       -+                while (cursor < logical_len) {
       -+                        nl = xmalloc(col * sizeof(Glyph));
       -+                        for (j = 0; j < col; j++) {
       -+                                nl[j].fg = defaultfg;
       -+                                nl[j].bg = defaultbg;
       -+                                nl[j].mode = 0;
       -+                                nl[j].u = ' ';
       -+                        }
       -+
       -+                        copy_width = logical_len - cursor;
       -+                        if (copy_width > col)
       -+                                copy_width = col;
       -+
       -+                        memcpy(nl, logical + cursor, copy_width * sizeof(Glyph));
       -+
       -+                        for (j = 0; j < copy_width; j++) {
       -+                                nl[j].mode &= ~ATTR_WRAP;
       -+                        }
       -+
       -+                        if (cursor + copy_width < logical_len) {
       -+                                nl[col - 1].mode |= ATTR_WRAP;
       -+                        } else {
       -+                                nl[col - 1].mode &= ~ATTR_WRAP;
       -+                        }
       -+
       -+                        /* Rebuild: Push new lines into the ring buffer. */
       -+                        if (new_len < sb.cap) {
       -+                                tail = (new_head + new_len) % sb.cap;
       -+                                new_buf[tail] = nl;
       -+                                new_len++;
       -+                        } else {
       -+                                free(new_buf[new_head]);
       -+                                new_buf[new_head] = nl;
       -+                                new_head = (new_head + 1) % sb.cap;
       -+                                new_base++;
       -+                        }
       -+                        current_width = (cursor + copy_width < logical_len) ? col : copy_width;
       -+                        if (current_width > new_max_width)
       -+                                new_max_width = current_width;
       -+                        cursor += copy_width;
       -+                }
       -+                logical_len = 0;
       -+        }
       -+        free(logical);
       -+        sb_clear();
       -+        free(sb.buf);
       -+        sb.buf = new_buf;
       -+        sb.len = new_len;
       -+        sb.head = new_head;
       -+        sb.base = new_base;
       -+        sb.view_offset = 0;
       -+        sb.max_width = new_max_width;
       -+}
       -+
       -+static void
       -+sb_pop_screen(int loaded, int new_cols)
       -+{
       -+        int i, p;
       -+        int start_logical;
       -+        Line line;
       -+
       -+        loaded = MIN(loaded, sb.len);
       -+        start_logical = sb.len - loaded;
       -+        new_cols = MIN(new_cols, term.col);
       -+        for (i = 0; i < loaded; i++) {
       -+                p = sb_phys_index(start_logical + i);
       -+                line = sb.buf[p];
       -+
       -+                memcpy(term.line[i], line, new_cols * sizeof(Glyph));
       -+
       -+                free(line);
       -+                sb.buf[p] = NULL;
       -+        }
       -+
       -+        sb.len -= loaded;
       -+}
       -+
       -+static uint64_t
       -+sb_view_start(void)
       -+{
       -+        return sb.base + sb.len - sb.view_offset;
       -+}
       -+
       -+static void
       -+sb_view_changed(void)
       -+{
       -+        if (!term.dirty || term.row <= 0)
       -+                return;
       -+        tfulldirt();
       -+}
       -+
       -+static void
       -+selscrollback(int delta)
       -+{
       -+        if (delta == 0)
       -+                return;
       -+
       -+        if (sel.ob.x == -1 || sel.mode == SEL_EMPTY)
       -+                return;
       -+
       -+        if (sel.alt != IS_SET(MODE_ALTSCREEN))
       -+                return;
       -+
       -+        sel.nb.y += delta;
       -+        sel.ne.y += delta;
       -+        sel.ob.y += delta;
       -+        sel.oe.y += delta;
       -+
       -+        sb_view_changed();
       -+}
       -+
       -+static Line
       -+emptyline(void)
       -+{
       -+        static Line empty;
       -+        static int empty_cols;
       -+        int i = 0;
       -+
       -+        if (empty_cols != term.col) {
       -+                free(empty);
       -+                empty = xmalloc(term.col * sizeof(Glyph));
       -+                empty_cols = term.col;
       -+        }
       -+
       -+        for (i = 0; i < term.col; i++) {
       -+                empty[i] = term.c.attr;
       -+                empty[i].u = ' ';
       -+                empty[i].mode = 0;
       -+        }
       -+        return empty;
       -+}
       -+
       -+static Line
       -+renderline(int y)
       -+{
       -+        int start, v;
       -+
       -+        if (sb.view_offset <= 0)
       -+                return term.line[y];
       -+
       -+        start = sb.len - sb.view_offset; /* can be negative */
       -+        v = start + y;
       -+
       -+        if (v < 0)
       -+                return emptyline();
       -+
       -+        if (v < sb.len)
       -+                return sb_get(v);
       -+
       -+        /* past scrollback -> into current screen */
       -+        v -= sb.len;
       -+        if (v >= 0 && v < term.row)
       -+                return term.line[v];
       -+
       -+        return emptyline();
       -+}
       -+
       -+static void
       -+sb_reset_on_clear(void)
       -+{
       -+        sb_clear();
       -+        sb_view_changed();
       -+        if (sel.ob.x != -1 && term.row > 0)
       -+                selclear();
       -+}
       -+
       -+int
       -+tisaltscreen(void)
       -+{
       -+        return IS_SET(MODE_ALTSCREEN);
       -+}
       -+
       - ssize_t
       - xwrite(int fd, const char *s, size_t len)
       - {
       -@@ -404,20 +775,23 @@ selinit(void)
       -         sel.ob.x = -1;
       - }
       - 
       --int
       --tlinelen(int y)
       -+static int
       -+tlinelen(Line line)
       - {
       -         int i = term.col;
       --
       --        if (term.line[y][i - 1].mode & ATTR_WRAP)
       -+        if (line[i - 1].mode & ATTR_WRAP)
       -                 return i;
       --
       --        while (i > 0 && term.line[y][i - 1].u == ' ')
       -+        while (i > 0 && line[i - 1].u == ' ')
       -                 --i;
       --
       -         return i;
       - }
       - 
       -+static int
       -+tlinelen_render(int y)
       -+{
       -+        return tlinelen(renderline(y));
       -+}
       -+
       - void
       - selstart(int col, int row, int snap)
       - {
       -@@ -485,10 +859,10 @@ selnormalize(void)
       -         /* expand selection over line breaks */
       -         if (sel.type == SEL_RECTANGULAR)
       -                 return;
       --        i = tlinelen(sel.nb.y);
       -+        i = tlinelen_render(sel.nb.y);
       -         if (i < sel.nb.x)
       -                 sel.nb.x = i;
       --        if (tlinelen(sel.ne.y) <= sel.ne.x)
       -+        if (tlinelen_render(sel.ne.y) <= sel.ne.x)
       -                 sel.ne.x = term.col - 1;
       - }
       - 
       -@@ -514,6 +888,7 @@ selsnap(int *x, int *y, int direction)
       -         int newx, newy, xt, yt;
       -         int delim, prevdelim;
       -         const Glyph *gp, *prevgp;
       -+        Line line;
       - 
       -         switch (sel.snap) {
       -         case SNAP_WORD:
       -@@ -521,7 +896,7 @@ selsnap(int *x, int *y, int direction)
       -                  * Snap around if the word wraps around at the end or
       -                  * beginning of a line.
       -                  */
       --                prevgp = &term.line[*y][*x];
       -+                prevgp = &renderline(*y)[*x];
       -                 prevdelim = ISDELIM(prevgp->u);
       -                 for (;;) {
       -                         newx = *x + direction;
       -@@ -536,14 +911,15 @@ selsnap(int *x, int *y, int direction)
       -                                         yt = *y, xt = *x;
       -                                 else
       -                                         yt = newy, xt = newx;
       --                                if (!(term.line[yt][xt].mode & ATTR_WRAP))
       -+                                line = renderline(yt);
       -+                                if (!(line[xt].mode & ATTR_WRAP))
       -                                         break;
       -                         }
       - 
       --                        if (newx >= tlinelen(newy))
       -+                        if (newx >= tlinelen_render(newy))
       -                                 break;
       - 
       --                        gp = &term.line[newy][newx];
       -+                        gp = &renderline(newy)[newx];
       -                         delim = ISDELIM(gp->u);
       -                         if (!(gp->mode & ATTR_WDUMMY) && (delim != prevdelim
       -                                         || (delim && gp->u != prevgp->u)))
       -@@ -564,14 +940,14 @@ selsnap(int *x, int *y, int direction)
       -                 *x = (direction < 0) ? 0 : term.col - 1;
       -                 if (direction < 0) {
       -                         for (; *y > 0; *y += direction) {
       --                                if (!(term.line[*y-1][term.col-1].mode
       -+                                if (!(renderline(*y-1)[term.col-1].mode
       -                                                 & ATTR_WRAP)) {
       -                                         break;
       -                                 }
       -                         }
       -                 } else if (direction > 0) {
       -                         for (; *y < term.row-1; *y += direction) {
       --                                if (!(term.line[*y][term.col-1].mode
       -+                                if (!(renderline(*y)[term.col-1].mode
       -                                                 & ATTR_WRAP)) {
       -                                         break;
       -                                 }
       -@@ -585,8 +961,9 @@ char *
       - getsel(void)
       - {
       -         char *str, *ptr;
       --        int y, bufsize, lastx, linelen;
       -+        int y, bufsize, lastx, linelen, end_idx, insert_newline, is_wrapped;
       -         const Glyph *gp, *last;
       -+        Line line;
       - 
       -         if (sel.ob.x == -1)
       -                 return NULL;
       -@@ -596,29 +973,33 @@ getsel(void)
       - 
       -         /* append every set & selected glyph to the selection */
       -         for (y = sel.nb.y; y <= sel.ne.y; y++) {
       --                if ((linelen = tlinelen(y)) == 0) {
       -+                line = renderline(y);
       -+                linelen = tlinelen_render(y);
       -+
       -+                if (linelen == 0) {
       -                         *ptr++ = '\n';
       -                         continue;
       -                 }
       - 
       -                 if (sel.type == SEL_RECTANGULAR) {
       --                        gp = &term.line[y][sel.nb.x];
       -+                        gp = &line[sel.nb.x];
       -                         lastx = sel.ne.x;
       -                 } else {
       --                        gp = &term.line[y][sel.nb.y == y ? sel.nb.x : 0];
       -+                        gp = &line[sel.nb.y == y ? sel.nb.x : 0];
       -                         lastx = (sel.ne.y == y) ? sel.ne.x : term.col-1;
       -                 }
       --                last = &term.line[y][MIN(lastx, linelen-1)];
       --                while (last >= gp && last->u == ' ')
       -+                end_idx = MIN(lastx, linelen-1);
       -+                is_wrapped = (line[end_idx].mode & ATTR_WRAP) != 0;
       -+                last = &line[end_idx];
       -+                while (last >= gp && last->u == ' ') {
       -                         --last;
       -+                }
       - 
       -                 for ( ; gp <= last; ++gp) {
       -                         if (gp->mode & ATTR_WDUMMY)
       -                                 continue;
       --
       -                         ptr += utf8encode(gp->u, ptr);
       -                 }
       --
       -                 /*
       -                  * Copy and pasting of line endings is inconsistent
       -                  * in the inconsistent terminal and GUI world.
       -@@ -628,8 +1009,13 @@ getsel(void)
       -                  * st.
       -                  * FIXME: Fix the computer world.
       -                  */
       -+                insert_newline = 0;
       -                 if ((y < sel.ne.y || lastx >= linelen) &&
       --                    (!(last->mode & ATTR_WRAP) || sel.type == SEL_RECTANGULAR))
       -+                        (!is_wrapped || sel.type == SEL_RECTANGULAR)) {
       -+                        insert_newline = 1;
       -+                }
       -+
       -+                if (insert_newline)
       -                         *ptr++ = '\n';
       -         }
       -         *ptr = 0;
       -@@ -845,6 +1231,12 @@ ttywrite(const char *s, size_t n, int may_echo)
       - {
       -         const char *next;
       - 
       -+        if (sb.view_offset > 0) {
       -+                selclear();
       -+                sb.view_offset = 0;
       -+                sb_view_changed();
       -+        }
       -+
       -         if (may_echo && IS_SET(MODE_ECHO))
       -                 twrite(s, n, 1);
       - 
       -@@ -965,6 +1357,8 @@ tsetdirt(int top, int bot)
       - {
       -         int i;
       - 
       -+        if (term.row < 1)
       -+                return;
       -         LIMIT(top, 0, term.row-1);
       -         LIMIT(bot, 0, term.row-1);
       - 
       -@@ -1030,15 +1424,21 @@ treset(void)
       -         for (i = 0; i < 2; i++) {
       -                 tmoveto(0, 0);
       -                 tcursor(CURSOR_SAVE);
       --                tclearregion(0, 0, term.col-1, term.row-1);
       -+                if (term.col > 0 && term.row > 0 && term.line > 0)
       -+                        tclearregion(0, 0, term.col-1, term.row-1);
       -                 tswapscreen();
       -         }
       -+        sb_clear();
       -+        if (sel.ob.x != -1 && term.row > 0)
       -+                selclear();
       - }
       - 
       -+
       - void
       - tnew(int col, int row)
       - {
       -         term = (Term){ .c = { .attr = { .fg = defaultfg, .bg = defaultbg } } };
       -+        sb_init(scrollback_lines);
       -         tresize(col, row);
       -         treset();
       - }
       -@@ -1078,10 +1478,37 @@ void
       - tscrollup(int orig, int n)
       - {
       -         int i;
       -+        uint64_t newstart;
       -+        uint64_t oldstart;
       -+
       -+        int attop;
       -         Line temp;
       - 
       -+        oldstart = sb_view_start();
       -         LIMIT(n, 0, term.bot-orig+1);
       - 
       -+        if (!IS_SET(MODE_ALTSCREEN) && orig == term.top) {
       -+                /* At top of history only if history exists */
       -+                attop = (sb.len != 0 && sb.view_offset == sb.len);
       -+
       -+                if (sb.view_offset > 0 && !attop)
       -+                        sb.view_offset += n;
       -+
       -+                for (i = 0; i < n; i++)
       -+                        sb_push(term.line[orig + i]);
       -+
       -+                /* if at the top, keep me there */
       -+                if (attop)
       -+                        sb.view_offset = sb.len;
       -+                /* otherwise clamp me */
       -+                else if (sb.view_offset > sb.len)
       -+                        sb.view_offset = sb.len;
       -+        }
       -+
       -+        newstart = sb_view_start();
       -+        if (sb.view_offset > 0)
       -+                selscrollback(oldstart - newstart);
       -+
       -         tclearregion(0, orig, term.col-1, orig+n-1);
       -         tsetdirt(orig+n, term.bot);
       - 
       -@@ -1097,6 +1524,8 @@ tscrollup(int orig, int n)
       - void
       - selscroll(int orig, int n)
       - {
       -+        if (sb.view_offset != 0)
       -+                return;
       -         if (sel.ob.x == -1 || sel.alt != IS_SET(MODE_ALTSCREEN))
       -                 return;
       - 
       -@@ -1105,12 +1534,7 @@ selscroll(int orig, int n)
       -         } else if (BETWEEN(sel.nb.y, orig, term.bot)) {
       -                 sel.ob.y += n;
       -                 sel.oe.y += n;
       --                if (sel.ob.y < term.top || sel.ob.y > term.bot ||
       --                    sel.oe.y < term.top || sel.oe.y > term.bot) {
       --                        selclear();
       --                } else {
       --                        selnormalize();
       --                }
       -+                selnormalize();
       -         }
       - }
       - 
       -@@ -1717,6 +2141,12 @@ csihandle(void)
       -                         break;
       -                 case 2: /* all */
       -                         tclearregion(0, 0, term.col-1, term.row-1);
       -+                        if (!IS_SET(MODE_ALTSCREEN))
       -+                                sb_reset_on_clear();
       -+                        break;
       -+                case 3:
       -+                        if (!IS_SET(MODE_ALTSCREEN))
       -+                                sb_reset_on_clear();
       -                         break;
       -                 default:
       -                         goto unknown;
       -@@ -2106,7 +2536,7 @@ tdumpline(int n)
       -         const Glyph *bp, *end;
       - 
       -         bp = &term.line[n][0];
       --        end = &bp[MIN(tlinelen(n), term.col) - 1];
       -+        end = &bp[MIN(tlinelen_render(n), term.col) - 1];
       -         if (bp != end || bp->u != ' ') {
       -                 for ( ; bp <= end; ++bp)
       -                         tprinter(buf, utf8encode(bp->u, buf));
       -@@ -2163,6 +2593,46 @@ tdeftran(char ascii)
       -         }
       - }
       - 
       -+static void
       -+kscroll(const Arg *arg)
       -+{
       -+        uint64_t oldstart;
       -+        uint64_t newstart;
       -+
       -+        oldstart = sb_view_start();
       -+        sb.view_offset += arg->i;
       -+        LIMIT(sb.view_offset, 0, sb.len);
       -+        newstart = sb_view_start();
       -+        selscrollback(oldstart - newstart);
       -+        redraw();
       -+}
       -+
       -+void
       -+kscrolldown(const Arg *arg)
       -+{
       -+        Arg a;
       -+
       -+        if (arg->i < 0)
       -+                a.i = -term.row;
       -+        else
       -+                a.i = -arg->i;
       -+
       -+        kscroll(&a);
       -+}
       -+
       -+void
       -+kscrollup(const Arg *arg)
       -+{
       -+        Arg a;
       -+
       -+        if (arg->i < 0)
       -+                a.i = term.row;
       -+        else
       -+                a.i = arg->i;
       -+
       -+        kscroll(&a);
       -+}
       -+
       - void
       - tdectest(char c)
       - {
       -@@ -2569,83 +3039,139 @@ twrite(const char *buf, int buflen, int show_ctrl)
       - void
       - tresize(int col, int row)
       - {
       --        int i;
       -+        int i, j;
       -+        int min_limit;
       -         int minrow = MIN(row, term.row);
       --        int mincol = MIN(col, term.col);
       --        int *bp;
       --        TCursor c;
       -+        int old_row = term.row;
       -+        int old_col = term.col;
       -+        int save_end = 0; /* Track effective pushed height */
       -+        int loaded = 0;
       -+        int pop_width = 0;
       -+        int needs_reflow = 0;
       -+        int is_alt = IS_SET(MODE_ALTSCREEN);
       -+        Line *tmp;
       - 
       -         if (col < 1 || row < 1) {
       -                 fprintf(stderr,
       --                        "tresize: error resizing to %dx%d\n", col, row);
       -+                        "tresize: error resizing to %dx%d\n", col, row);
       -                 return;
       -         }
       - 
       --        /*
       --         * slide screen to keep cursor where we expect it -
       --         * tscrollup would work here, but we can optimize to
       --         * memmove because we're freeing the earlier lines
       --         */
       --        for (i = 0; i <= term.c.y - row; i++) {
       --                free(term.line[i]);
       --                free(term.alt[i]);
       --        }
       --        /* ensure that both src and dst are not NULL */
       --        if (i > 0) {
       --                memmove(term.line, term.line + i, row * sizeof(Line));
       --                memmove(term.alt, term.alt + i, row * sizeof(Line));
       --        }
       --        for (i += row; i < term.row; i++) {
       --                free(term.line[i]);
       --                free(term.alt[i]);
       -+        if (sel.ob.x != -1)
       -+                selclear();
       -+
       -+        /* Operate on the currently visible screen buffer. */
       -+        if (is_alt) {
       -+                tmp = term.line;
       -+                term.line = term.alt;
       -+                term.alt = tmp;
       -         }
       - 
       --        /* resize to new height */
       --        term.line = xrealloc(term.line, row * sizeof(Line));
       --        term.alt  = xrealloc(term.alt,  row * sizeof(Line));
       --        term.dirty = xrealloc(term.dirty, row * sizeof(*term.dirty));
       --        term.tabs = xrealloc(term.tabs, col * sizeof(*term.tabs));
       -+        save_end = term.row;
       -+        if (term.row != 0 && term.col != 0) {
       -+                if (!is_alt && term.c.y > 0 && term.c.y < term.row) {
       -+                        term.line[term.c.y - 1][term.col - 1].mode &= ~ATTR_WRAP;
       -+                }
       -+                min_limit = is_alt ? 0 : term.c.y;
       - 
       --        /* resize each row to new width, zero-pad if needed */
       --        for (i = 0; i < minrow; i++) {
       --                term.line[i] = xrealloc(term.line[i], col * sizeof(Glyph));
       --                term.alt[i]  = xrealloc(term.alt[i],  col * sizeof(Glyph));
       --        }
       -+                for (i = term.row - 1; i > min_limit; i--) {
       -+                        if (tlinelen(term.line[i]) > 0)
       -+                                break;
       -+                }
       -+                save_end = i + 1;
       - 
       --        /* allocate any new rows */
       --        for (/* i = minrow */; i < row; i++) {
       --                term.line[i] = xmalloc(col * sizeof(Glyph));
       --                term.alt[i] = xmalloc(col * sizeof(Glyph));
       -+                for (i = 0; i < save_end; i++) {
       -+                        sb_push(term.line[i]);
       -+                }
       -+                /* Optimization: Only reflow if content doesn't fit in new width.
       -+                 * This avoids expensive reflow operations when resizing doesn't
       -+                 * affect line wrapping (e.g., when terminal is wide enough). */
       -+                if (col > term.col) {
       -+                        /* Growing: Only reflow if history was wrapped at old width */
       -+                        needs_reflow = sb.max_width >= term.col;
       -+                } else if (col < term.col) {
       -+                        /* Shrinking: Only reflow if content is wider than new width. */
       -+                        if (sb.max_width > col)
       -+                                needs_reflow = 1;
       -+                }
       -+                if (needs_reflow) {
       -+                        sb_resize(col);
       -+                } else {
       -+                        /* If we don't reflow, we still need to reset the view 
       -+                         * because sb_pop_screen() might change the history length. */
       -+                        sb.view_offset = 0;
       -+                }
       -         }
       --        if (col > term.col) {
       --                bp = term.tabs + term.col;
       - 
       --                memset(bp, 0, sizeof(*term.tabs) * (col - term.col));
       --                while (--bp > term.tabs && !*bp)
       --                        /* nothing */ ;
       --                for (bp += tabspaces; bp < term.tabs + col; bp += tabspaces)
       --                        *bp = 1;
       --        }
       --        /* update terminal size */
       -+                if (term.line) {
       -+                        for (i = 0; i < term.row; i++) {
       -+                                free(term.line[i]);
       -+                                free(term.alt[i]);
       -+                        }
       -+                        free(term.line);
       -+                        free(term.alt);
       -+                        free(term.dirty);
       -+                        free(term.tabs);
       -+                }
       -+
       -         term.col = col;
       -         term.row = row;
       --        /* reset scrolling region */
       --        tsetscroll(0, row-1);
       --        /* make use of the LIMIT in tmoveto */
       --        tmoveto(term.c.x, term.c.y);
       --        /* Clearing both screens (it makes dirty all lines) */
       --        c = term.c;
       --        for (i = 0; i < 2; i++) {
       --                if (mincol < col && 0 < minrow) {
       --                        tclearregion(mincol, 0, col - 1, minrow - 1);
       --                }
       --                if (0 < col && minrow < row) {
       --                        tclearregion(0, minrow, col - 1, row - 1);
       -+
       -+        term.line  = xmalloc(term.row * sizeof(Line));
       -+        term.alt   = xmalloc(term.row * sizeof(Line));
       -+        term.dirty = xmalloc(term.row * sizeof(int));
       -+        term.tabs  = xmalloc(term.col * sizeof(*term.tabs));
       -+
       -+        for (i = 0; i < term.row; i++) {
       -+                term.line[i] = xmalloc(term.col * sizeof(Glyph));
       -+                term.alt[i]  = xmalloc(term.col * sizeof(Glyph));
       -+                term.dirty[i] = 1;
       -+
       -+                for (j = 0; j < term.col; j++) {
       -+                        term.line[i][j] = term.c.attr;
       -+                        term.line[i][j].u = ' ';
       -+                        term.line[i][j].mode = 0;
       -+
       -+                        term.alt[i][j] = term.c.attr;
       -+                        term.alt[i][j].u = ' ';
       -+                        term.alt[i][j].mode = 0;
       -                 }
       --                tswapscreen();
       --                tcursor(CURSOR_LOAD);
       -         }
       --        term.c = c;
       -+
       -+        memset(term.tabs, 0, term.col * sizeof(*term.tabs));
       -+        for (i = 8; i < term.col; i += 8)
       -+                term.tabs[i] = 1;
       -+
       -+        tsetscroll(0, term.row - 1);
       -+
       -+        if (minrow > 0) {
       -+                loaded = MIN(sb.len, term.row);
       -+                pop_width = needs_reflow ? col : MIN(col, old_col);
       -+                sb_pop_screen(loaded, pop_width);
       -+        }
       -+        if (is_alt) {
       -+                tmp = term.line;
       -+                term.line = term.alt;
       -+                term.alt = tmp;
       -+        }
       -+        if (!is_alt && old_row > 0) {
       -+                term.c.y += (loaded - save_end);
       -+        }
       -+        if (term.c.y >= term.row) {
       -+                term.c.y = term.row - 1;
       -+        }
       -+        if (term.c.x >= term.col) {
       -+                term.c.x = term.col - 1;
       -+        }
       -+        if (term.c.y < 0) {
       -+                term.c.y = 0;
       -+        }
       -+        if (term.c.x < 0) {
       -+                term.c.x = 0;
       -+        }
       -+
       -+        tfulldirt();
       -+        sb_view_changed();
       - }
       - 
       - void
       -@@ -2659,12 +3185,13 @@ drawregion(int x1, int y1, int x2, int y2)
       - {
       -         int y;
       - 
       -+        Line line;
       -         for (y = y1; y < y2; y++) {
       -                 if (!term.dirty[y])
       -                         continue;
       --
       -                 term.dirty[y] = 0;
       --                xdrawline(term.line[y], x1, y, x2);
       -+                line = renderline(y);
       -+                xdrawline(line, x1, y, x2);
       -         }
       - }
       - 
       -@@ -2685,10 +3212,12 @@ draw(void)
       -                 cx--;
       - 
       -         drawregion(0, 0, term.col, term.row);
       --        xdrawcursor(cx, term.c.y, term.line[term.c.y][cx],
       --                        term.ocx, term.ocy, term.line[term.ocy][term.ocx]);
       --        term.ocx = cx;
       --        term.ocy = term.c.y;
       -+        if (sb.view_offset == 0) {
       -+                xdrawcursor(cx, term.c.y, term.line[term.c.y][cx],
       -+                            term.ocx, term.ocy, term.line[term.ocy][term.ocx]);
       -+                term.ocx = cx;
       -+                term.ocy = term.c.y;
       -+        }
       -         xfinishdraw();
       -         if (ocx != term.ocx || ocy != term.ocy)
       -                 xximspot(term.ocx, term.ocy);
       -diff --git a/st.h b/st.h
       -index fd3b0d8..151d0c6 100644
       ---- a/st.h
       -+++ b/st.h
       -@@ -86,6 +86,7 @@ void printsel(const Arg *);
       - void sendbreak(const Arg *);
       - void toggleprinter(const Arg *);
       - 
       -+int tisaltscreen(void);
       - int tattrset(int);
       - void tnew(int, int);
       - void tresize(int, int);
       -@@ -111,6 +112,9 @@ void *xmalloc(size_t);
       - void *xrealloc(void *, size_t);
       - char *xstrdup(const char *);
       - 
       -+void kscrollup(const Arg *arg);
       -+void kscrolldown(const Arg *arg);
       -+
       - /* config.h globals */
       - extern char *utmp;
       - extern char *scroll;
       -@@ -124,3 +128,4 @@ extern unsigned int tabspaces;
       - extern unsigned int defaultfg;
       - extern unsigned int defaultbg;
       - extern unsigned int defaultcs;
       -+extern unsigned int scrollback_lines;
       -diff --git a/x.c b/x.c
       -index d73152b..75f3db1 100644
       ---- a/x.c
       -+++ b/x.c
       -@@ -472,6 +472,23 @@ bpress(XEvent *e)
       -         struct timespec now;
       -         int snap;
       - 
       -+        if (btn == Button4 || btn == Button5) {
       -+                Arg a;
       -+                if (IS_SET(MODE_MOUSE) && !(e->xbutton.state & forcemousemod)) {
       -+                        mousereport(e);
       -+                        return;
       -+                }
       -+                if (!tisaltscreen()) {
       -+                        a.i = 1;
       -+                        if (btn == Button4) {
       -+                                kscrollup(&a);
       -+                        } else {
       -+                                kscrolldown(&a);
       -+                        }
       -+                }
       -+                return;
       -+        }
       -+
       -         if (1 <= btn && btn <= 11)
       -                 buttons |= 1 << (btn-1);
       - 
       --- 
       -2.52.0
       -
   DIR diff --git a/st.suckless.org/patches/scrollback-reflow-standalone/st-scrollback-reflow-standalone-extended-0.9.31.diff b/st.suckless.org/patches/scrollback-reflow-standalone/st-scrollback-reflow-standalone-extended-0.9.31.diff
       @@ -0,0 +1,1048 @@
       +From 792cbb832839cb6981440356c26ce2836bc69427 Mon Sep 17 00:00:00 2001
       +From: Milos Nikic <nikic.milos@gmail.com>
       +Date: Thu, 8 Jan 2026 22:04:25 -0800
       +Subject: [PATCH] st: alternative scrollback using ring buffer and view offset
       +
       +Implement scrollback as a fixed-size ring buffer and render history
       +by offsetting the view instead of copying screen contents.
       +
       +Tradeoffs / differences:
       +- Scrollback history is lost on resize
       +- Scrollback is disabled on the alternate screen
       +- Simpler model than the existing scrollback patch set
       +- Mouse wheel scrolling enabled by default
       +
       +Note:
       +When using vim, mouse movement will no longer move the cursor.
       +
       +Reminder:
       +If applying this patch on top of others, ensure any changes to
       +config.def.h are merged into config.h.
       +---
       + config.def.h |   5 +
       + st.c         | 713 ++++++++++++++++++++++++++++++++++++++++++++-------
       + st.h         |   5 +
       + x.c          |  17 ++
       + 4 files changed, 645 insertions(+), 95 deletions(-)
       +
       +diff --git a/config.def.h b/config.def.h
       +index 2cd740a..a0b14e9 100644
       +--- a/config.def.h
       ++++ b/config.def.h
       +@@ -472,3 +472,8 @@ static char ascii_printable[] =
       +         " !\"#$%&'()*+,-./0123456789:;<=>?"
       +         "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_"
       +         "`abcdefghijklmnopqrstuvwxyz{|}~";
       ++
       ++/*
       ++ * The amount of lines scrollback can hold before it wraps around.
       ++ */
       ++unsigned int scrollback_lines = 5000;
       +diff --git a/st.c b/st.c
       +index 6f40e35..cf76c58 100644
       +--- a/st.c
       ++++ b/st.c
       +@@ -5,6 +5,7 @@
       + #include <limits.h>
       + #include <pwd.h>
       + #include <stdarg.h>
       ++#include <stdint.h>
       + #include <stdio.h>
       + #include <stdlib.h>
       + #include <string.h>
       +@@ -178,7 +179,7 @@ static void tdeletechar(int);
       + static void tdeleteline(int);
       + static void tinsertblank(int);
       + static void tinsertblankline(int);
       +-static int tlinelen(int);
       ++static int tlinelen(Line);
       + static void tmoveto(int, int);
       + static void tmoveato(int, int);
       + static void tnewline(int);
       +@@ -232,6 +233,379 @@ static const uchar utfmask[UTF_SIZ + 1] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8};
       + static const Rune utfmin[UTF_SIZ + 1] = {       0,    0,  0x80,  0x800,  0x10000};
       + static const Rune utfmax[UTF_SIZ + 1] = {0x10FFFF, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF};
       + 
       ++typedef struct
       ++{
       ++        Line *buf;       /* ring of Line pointers */
       ++        int cap;         /* max number of lines */
       ++        int len;         /* current number of valid lines (<= cap) */
       ++        int head;        /* physical index of logical oldest (valid when len>0) */
       ++        uint64_t base;   /* Can overflow in the extreme */
       ++        /*
       ++         * max_width tracks the widest line ever pushed to scrollback.
       ++         * It may be conservative (stale) if that line has since been
       ++         * evicted from the ring buffer, which is acceptable - it just
       ++         * means we might reflow when not strictly necessary, which is
       ++         * better than skipping a needed reflow.
       ++         */
       ++        int max_width;
       ++        int view_offset; /* 0 means live screen */
       ++} Scrollback;
       ++
       ++static Scrollback sb;
       ++
       ++static int
       ++sb_phys_index(int logical_idx)
       ++{
       ++        /* logical_idx: 0..sb.len-1 (0 = oldest) */
       ++        return (sb.head + logical_idx) % sb.cap;
       ++}
       ++
       ++static Line
       ++lineclone(Line src)
       ++{
       ++        Line dst;
       ++
       ++        if (!src)
       ++                return NULL;
       ++
       ++        dst = xmalloc(term.col * sizeof(Glyph));
       ++        memcpy(dst, src, term.col * sizeof(Glyph));
       ++        return dst;
       ++}
       ++
       ++static void
       ++sb_init(int lines)
       ++{
       ++        int i;
       ++
       ++        sb.buf  = xmalloc(sizeof(Line) * lines);
       ++        sb.cap  = lines;
       ++        sb.len  = 0;
       ++        sb.head = 0;
       ++        sb.base = 0;
       ++        for (i = 0; i < sb.cap; i++)
       ++                sb.buf[i] = NULL;
       ++
       ++        sb.view_offset = 0;
       ++        sb.max_width = 0;
       ++}
       ++
       ++/* Push one screen line into scrollback.
       ++ * Overwrites oldest when full (ring buffer).
       ++ */
       ++static void
       ++sb_push(Line line)
       ++{
       ++        Line copy;
       ++        int tail;
       ++        int width;
       ++
       ++        if (sb.cap <= 0)
       ++                return;
       ++
       ++        copy = lineclone(line);
       ++
       ++        if (sb.len < sb.cap) {
       ++                tail = sb_phys_index(sb.len);
       ++                sb.buf[tail] = copy;
       ++                sb.len++;
       ++        } else {
       ++                /* We might've just evicted the widest line... */
       ++                free(sb.buf[sb.head]);
       ++                sb.buf[sb.head] = copy;
       ++                sb.head = (sb.head + 1) % sb.cap;
       ++                sb.base++;
       ++        }
       ++        width = tlinelen(copy);
       ++        /* ...so max_width might be stale. */
       ++        if (width > sb.max_width)
       ++                sb.max_width = width;
       ++}
       ++
       ++static Line
       ++sb_get(int idx)
       ++{
       ++        /* idx is logical: 0..sb.len-1 */
       ++        if (idx < 0 || idx >= sb.len)
       ++                return NULL;
       ++        return sb.buf[sb_phys_index(idx)];
       ++}
       ++
       ++static void
       ++sb_clear(void)
       ++{
       ++        int i;
       ++        int p;
       ++
       ++        if (!sb.buf)
       ++                return;
       ++
       ++        for (i = 0; i < sb.len; i++) {
       ++                p = sb_phys_index(i);
       ++                if (sb.buf[p]) {
       ++                        free(sb.buf[p]);
       ++                        sb.buf[p] = NULL;
       ++                }
       ++        }
       ++
       ++        sb.len = 0;
       ++        sb.head = 0;
       ++        sb.base = 0;
       ++        sb.view_offset = 0;
       ++        sb.max_width = 0;
       ++}
       ++
       ++/*
       ++ * Reflows the scrollback buffer to fit a new terminal width.
       ++ *
       ++ * The algorithm works in three steps:
       ++ * 1) Unwrap: It iterates through the existing history, joining physical lines
       ++ * marked with ATTR_WRAP into a single continuous 'logical' line.
       ++ * 2) Reflow: It slices this logical line into new chunks of size 'col'.
       ++ * - New wrap flags are applied where the text exceeds the new width.
       ++ * - Trailing spaces are trimmed to prevent ghost padding.
       ++ * 3) Rebuild: The new lines are pushed into a fresh ring buffer.
       ++ * - Uses O(1) ring insertion (updating head/tail) to avoid expensive
       ++ * memmoves during resize, but it is still O(N) where N is the existing
       ++ * history.
       ++ *
       ++ * Note: During reflow we reset sb to match the rebuilt buffer
       ++ * (head, base and len might change).
       ++ */
       ++static void
       ++sb_resize(int col)
       ++{
       ++        Line *new_buf;
       ++        int i, j;
       ++        int new_len, logical_cap, logical_len, is_wrapped, cursor;
       ++        int copy_width, tail, current_width;
       ++        Line logical, line, nl;
       ++        uint64_t new_base = 0;
       ++        int new_head = 0;
       ++        int new_max_width = 0;
       ++        Glyph *g;
       ++
       ++        new_len = 0;
       ++
       ++        if (sb.len == 0)
       ++                return;
       ++
       ++        new_buf = xmalloc(sizeof(Line) * sb.cap);
       ++        for (i = 0; i < sb.cap; i++)
       ++                new_buf[i] = NULL;
       ++
       ++        logical_cap = term.col * 2;
       ++        logical = xmalloc(logical_cap * sizeof(Glyph));
       ++        logical_len = 0;
       ++
       ++        for (i = 0; i < sb.len; i++) {
       ++                /* Unwrap: Accumulate physical lines into one logical line. */
       ++                line = sb_get(i);
       ++                is_wrapped = (line[term.col - 1].mode & ATTR_WRAP);
       ++                if (logical_len + term.col > logical_cap) {
       ++                        logical_cap *= 2;
       ++                        logical = xrealloc(logical, logical_cap * sizeof(Glyph));
       ++                }
       ++
       ++                memcpy(logical + logical_len, line, term.col * sizeof(Glyph));
       ++                for (j = 0; j < term.col; j++) {
       ++                        logical[logical_len + j].mode &= ~ATTR_WRAP;
       ++                }
       ++                logical_len += term.col;
       ++                /* If the line was wrapped, continue accumulating before reflowing. */
       ++                if (is_wrapped) {
       ++                        continue;
       ++                }
       ++                /* Trim trailing spaces from the fully unwrapped line. */
       ++                while (logical_len > 0) {
       ++                        g = &logical[logical_len - 1];
       ++                        if (g->u == ' ' && g->bg == defaultbg
       ++                                        && (g->mode & ATTR_BOLD) == 0) {
       ++                                logical_len--;
       ++                        } else {
       ++                                break;
       ++                        }
       ++                }
       ++                if (logical_len == 0)
       ++                        logical_len = 1;
       ++
       ++                /* Reflow: Split the logical line into new chunks. */
       ++                cursor = 0;
       ++                while (cursor < logical_len) {
       ++                        nl = xmalloc(col * sizeof(Glyph));
       ++                        for (j = 0; j < col; j++) {
       ++                                nl[j].fg = defaultfg;
       ++                                nl[j].bg = defaultbg;
       ++                                nl[j].mode = 0;
       ++                                nl[j].u = ' ';
       ++                        }
       ++
       ++                        copy_width = logical_len - cursor;
       ++                        if (copy_width > col)
       ++                                copy_width = col;
       ++
       ++                        memcpy(nl, logical + cursor, copy_width * sizeof(Glyph));
       ++
       ++                        for (j = 0; j < copy_width; j++) {
       ++                                nl[j].mode &= ~ATTR_WRAP;
       ++                        }
       ++
       ++                        if (cursor + copy_width < logical_len) {
       ++                                nl[col - 1].mode |= ATTR_WRAP;
       ++                        } else {
       ++                                nl[col - 1].mode &= ~ATTR_WRAP;
       ++                        }
       ++
       ++                        /* Rebuild: Push new lines into the ring buffer. */
       ++                        if (new_len < sb.cap) {
       ++                                tail = (new_head + new_len) % sb.cap;
       ++                                new_buf[tail] = nl;
       ++                                new_len++;
       ++                        } else {
       ++                                free(new_buf[new_head]);
       ++                                new_buf[new_head] = nl;
       ++                                new_head = (new_head + 1) % sb.cap;
       ++                                new_base++;
       ++                        }
       ++                        current_width = (cursor + copy_width < logical_len) ? col : copy_width;
       ++                        if (current_width > new_max_width)
       ++                                new_max_width = current_width;
       ++                        cursor += copy_width;
       ++                }
       ++                logical_len = 0;
       ++        }
       ++        free(logical);
       ++        sb_clear();
       ++        free(sb.buf);
       ++        sb.buf = new_buf;
       ++        sb.len = new_len;
       ++        sb.head = new_head;
       ++        sb.base = new_base;
       ++        sb.view_offset = 0;
       ++        sb.max_width = new_max_width;
       ++}
       ++
       ++static void
       ++sb_pop_screen(int loaded, int new_cols)
       ++{
       ++        int i, p;
       ++        int start_logical;
       ++        Line line;
       ++
       ++        loaded = MIN(loaded, sb.len);
       ++        start_logical = sb.len - loaded;
       ++        new_cols = MIN(new_cols, term.col);
       ++        for (i = 0; i < loaded; i++) {
       ++                p = sb_phys_index(start_logical + i);
       ++                line = sb.buf[p];
       ++
       ++                memcpy(term.line[i], line, new_cols * sizeof(Glyph));
       ++
       ++                free(line);
       ++                sb.buf[p] = NULL;
       ++        }
       ++
       ++        sb.len -= loaded;
       ++}
       ++
       ++static uint64_t
       ++sb_view_start(void)
       ++{
       ++        return sb.base + sb.len - sb.view_offset;
       ++}
       ++
       ++static void
       ++sb_view_changed(void)
       ++{
       ++        if (!term.dirty || term.row <= 0)
       ++                return;
       ++        tfulldirt();
       ++}
       ++
       ++static void
       ++selscrollback(int delta)
       ++{
       ++        if (delta == 0)
       ++                return;
       ++
       ++        if (sel.ob.x == -1 || sel.mode == SEL_EMPTY)
       ++                return;
       ++
       ++        if (sel.alt != IS_SET(MODE_ALTSCREEN))
       ++                return;
       ++
       ++        sel.nb.y += delta;
       ++        sel.ne.y += delta;
       ++        sel.ob.y += delta;
       ++        sel.oe.y += delta;
       ++
       ++        if (sel.ne.y < 0 || sel.nb.y >= term.row)
       ++                selclear();
       ++
       ++        sb_view_changed();
       ++}
       ++
       ++static Line
       ++emptyline(void)
       ++{
       ++        static Line empty;
       ++        static int empty_cols;
       ++        int i = 0;
       ++
       ++        if (empty_cols != term.col) {
       ++                free(empty);
       ++                empty = xmalloc(term.col * sizeof(Glyph));
       ++                empty_cols = term.col;
       ++        }
       ++
       ++        for (i = 0; i < term.col; i++) {
       ++                empty[i] = term.c.attr;
       ++                empty[i].u = ' ';
       ++                empty[i].mode = 0;
       ++        }
       ++        return empty;
       ++}
       ++
       ++static Line
       ++renderline(int y)
       ++{
       ++        int start, v;
       ++
       ++        if (sb.view_offset <= 0)
       ++                return term.line[y];
       ++
       ++        start = sb.len - sb.view_offset; /* can be negative */
       ++        v = start + y;
       ++
       ++        if (v < 0)
       ++                return emptyline();
       ++
       ++        if (v < sb.len)
       ++                return sb_get(v);
       ++
       ++        /* past scrollback -> into current screen */
       ++        v -= sb.len;
       ++        if (v >= 0 && v < term.row)
       ++                return term.line[v];
       ++
       ++        return emptyline();
       ++}
       ++
       ++static void
       ++sb_reset_on_clear(void)
       ++{
       ++        sb_clear();
       ++        sb_view_changed();
       ++        if (sel.ob.x != -1 && term.row > 0)
       ++                selclear();
       ++}
       ++
       ++int
       ++tisaltscreen(void)
       ++{
       ++        return IS_SET(MODE_ALTSCREEN);
       ++}
       ++
       + ssize_t
       + xwrite(int fd, const char *s, size_t len)
       + {
       +@@ -404,20 +778,23 @@ selinit(void)
       +         sel.ob.x = -1;
       + }
       + 
       +-int
       +-tlinelen(int y)
       ++static int
       ++tlinelen(Line line)
       + {
       +         int i = term.col;
       +-
       +-        if (term.line[y][i - 1].mode & ATTR_WRAP)
       ++        if (line[i - 1].mode & ATTR_WRAP)
       +                 return i;
       +-
       +-        while (i > 0 && term.line[y][i - 1].u == ' ')
       ++        while (i > 0 && line[i - 1].u == ' ')
       +                 --i;
       +-
       +         return i;
       + }
       + 
       ++static int
       ++tlinelen_render(int y)
       ++{
       ++        return tlinelen(renderline(y));
       ++}
       ++
       + void
       + selstart(int col, int row, int snap)
       + {
       +@@ -485,10 +862,10 @@ selnormalize(void)
       +         /* expand selection over line breaks */
       +         if (sel.type == SEL_RECTANGULAR)
       +                 return;
       +-        i = tlinelen(sel.nb.y);
       ++        i = tlinelen_render(sel.nb.y);
       +         if (i < sel.nb.x)
       +                 sel.nb.x = i;
       +-        if (tlinelen(sel.ne.y) <= sel.ne.x)
       ++        if (tlinelen_render(sel.ne.y) <= sel.ne.x)
       +                 sel.ne.x = term.col - 1;
       + }
       + 
       +@@ -514,6 +891,7 @@ selsnap(int *x, int *y, int direction)
       +         int newx, newy, xt, yt;
       +         int delim, prevdelim;
       +         const Glyph *gp, *prevgp;
       ++        Line line;
       + 
       +         switch (sel.snap) {
       +         case SNAP_WORD:
       +@@ -521,7 +899,7 @@ selsnap(int *x, int *y, int direction)
       +                  * Snap around if the word wraps around at the end or
       +                  * beginning of a line.
       +                  */
       +-                prevgp = &term.line[*y][*x];
       ++                prevgp = &renderline(*y)[*x];
       +                 prevdelim = ISDELIM(prevgp->u);
       +                 for (;;) {
       +                         newx = *x + direction;
       +@@ -536,14 +914,15 @@ selsnap(int *x, int *y, int direction)
       +                                         yt = *y, xt = *x;
       +                                 else
       +                                         yt = newy, xt = newx;
       +-                                if (!(term.line[yt][xt].mode & ATTR_WRAP))
       ++                                line = renderline(yt);
       ++                                if (!(line[xt].mode & ATTR_WRAP))
       +                                         break;
       +                         }
       + 
       +-                        if (newx >= tlinelen(newy))
       ++                        if (newx >= tlinelen_render(newy))
       +                                 break;
       + 
       +-                        gp = &term.line[newy][newx];
       ++                        gp = &renderline(newy)[newx];
       +                         delim = ISDELIM(gp->u);
       +                         if (!(gp->mode & ATTR_WDUMMY) && (delim != prevdelim
       +                                         || (delim && gp->u != prevgp->u)))
       +@@ -564,14 +943,14 @@ selsnap(int *x, int *y, int direction)
       +                 *x = (direction < 0) ? 0 : term.col - 1;
       +                 if (direction < 0) {
       +                         for (; *y > 0; *y += direction) {
       +-                                if (!(term.line[*y-1][term.col-1].mode
       ++                                if (!(renderline(*y-1)[term.col-1].mode
       +                                                 & ATTR_WRAP)) {
       +                                         break;
       +                                 }
       +                         }
       +                 } else if (direction > 0) {
       +                         for (; *y < term.row-1; *y += direction) {
       +-                                if (!(term.line[*y][term.col-1].mode
       ++                                if (!(renderline(*y)[term.col-1].mode
       +                                                 & ATTR_WRAP)) {
       +                                         break;
       +                                 }
       +@@ -585,8 +964,9 @@ char *
       + getsel(void)
       + {
       +         char *str, *ptr;
       +-        int y, bufsize, lastx, linelen;
       ++        int y, bufsize, lastx, linelen, end_idx, insert_newline, is_wrapped;
       +         const Glyph *gp, *last;
       ++        Line line;
       + 
       +         if (sel.ob.x == -1)
       +                 return NULL;
       +@@ -596,29 +976,33 @@ getsel(void)
       + 
       +         /* append every set & selected glyph to the selection */
       +         for (y = sel.nb.y; y <= sel.ne.y; y++) {
       +-                if ((linelen = tlinelen(y)) == 0) {
       ++                line = renderline(y);
       ++                linelen = tlinelen_render(y);
       ++
       ++                if (linelen == 0) {
       +                         *ptr++ = '\n';
       +                         continue;
       +                 }
       + 
       +                 if (sel.type == SEL_RECTANGULAR) {
       +-                        gp = &term.line[y][sel.nb.x];
       ++                        gp = &line[sel.nb.x];
       +                         lastx = sel.ne.x;
       +                 } else {
       +-                        gp = &term.line[y][sel.nb.y == y ? sel.nb.x : 0];
       ++                        gp = &line[sel.nb.y == y ? sel.nb.x : 0];
       +                         lastx = (sel.ne.y == y) ? sel.ne.x : term.col-1;
       +                 }
       +-                last = &term.line[y][MIN(lastx, linelen-1)];
       +-                while (last >= gp && last->u == ' ')
       ++                end_idx = MIN(lastx, linelen-1);
       ++                is_wrapped = (line[end_idx].mode & ATTR_WRAP) != 0;
       ++                last = &line[end_idx];
       ++                while (last >= gp && last->u == ' ') {
       +                         --last;
       ++                }
       + 
       +                 for ( ; gp <= last; ++gp) {
       +                         if (gp->mode & ATTR_WDUMMY)
       +                                 continue;
       +-
       +                         ptr += utf8encode(gp->u, ptr);
       +                 }
       +-
       +                 /*
       +                  * Copy and pasting of line endings is inconsistent
       +                  * in the inconsistent terminal and GUI world.
       +@@ -628,8 +1012,13 @@ getsel(void)
       +                  * st.
       +                  * FIXME: Fix the computer world.
       +                  */
       ++                insert_newline = 0;
       +                 if ((y < sel.ne.y || lastx >= linelen) &&
       +-                    (!(last->mode & ATTR_WRAP) || sel.type == SEL_RECTANGULAR))
       ++                        (!is_wrapped || sel.type == SEL_RECTANGULAR)) {
       ++                        insert_newline = 1;
       ++                }
       ++
       ++                if (insert_newline)
       +                         *ptr++ = '\n';
       +         }
       +         *ptr = 0;
       +@@ -845,6 +1234,12 @@ ttywrite(const char *s, size_t n, int may_echo)
       + {
       +         const char *next;
       + 
       ++        if (sb.view_offset > 0) {
       ++                selclear();
       ++                sb.view_offset = 0;
       ++                sb_view_changed();
       ++        }
       ++
       +         if (may_echo && IS_SET(MODE_ECHO))
       +                 twrite(s, n, 1);
       + 
       +@@ -965,9 +1360,8 @@ tsetdirt(int top, int bot)
       + {
       +         int i;
       + 
       +-        if (term.row <= 0)
       ++        if (term.row < 1)
       +                 return;
       +-
       +         LIMIT(top, 0, term.row-1);
       +         LIMIT(bot, 0, term.row-1);
       + 
       +@@ -1033,15 +1427,21 @@ treset(void)
       +         for (i = 0; i < 2; i++) {
       +                 tmoveto(0, 0);
       +                 tcursor(CURSOR_SAVE);
       +-                tclearregion(0, 0, term.col-1, term.row-1);
       ++                if (term.col > 0 && term.row > 0 && term.line > 0)
       ++                        tclearregion(0, 0, term.col-1, term.row-1);
       +                 tswapscreen();
       +         }
       ++        sb_clear();
       ++        if (sel.ob.x != -1 && term.row > 0)
       ++                selclear();
       + }
       + 
       ++
       + void
       + tnew(int col, int row)
       + {
       +         term = (Term){ .c = { .attr = { .fg = defaultfg, .bg = defaultbg } } };
       ++        sb_init(scrollback_lines);
       +         tresize(col, row);
       +         treset();
       + }
       +@@ -1081,10 +1481,37 @@ void
       + tscrollup(int orig, int n)
       + {
       +         int i;
       ++        uint64_t newstart;
       ++        uint64_t oldstart;
       ++
       ++        int attop;
       +         Line temp;
       + 
       ++        oldstart = sb_view_start();
       +         LIMIT(n, 0, term.bot-orig+1);
       + 
       ++        if (!IS_SET(MODE_ALTSCREEN) && orig == term.top) {
       ++                /* At top of history only if history exists */
       ++                attop = (sb.len != 0 && sb.view_offset == sb.len);
       ++
       ++                if (sb.view_offset > 0 && !attop)
       ++                        sb.view_offset += n;
       ++
       ++                for (i = 0; i < n; i++)
       ++                        sb_push(term.line[orig + i]);
       ++
       ++                /* if at the top, keep me there */
       ++                if (attop)
       ++                        sb.view_offset = sb.len;
       ++                /* otherwise clamp me */
       ++                else if (sb.view_offset > sb.len)
       ++                        sb.view_offset = sb.len;
       ++        }
       ++
       ++        newstart = sb_view_start();
       ++        if (sb.view_offset > 0)
       ++                selscrollback(oldstart - newstart);
       ++
       +         tclearregion(0, orig, term.col-1, orig+n-1);
       +         tsetdirt(orig+n, term.bot);
       + 
       +@@ -1100,6 +1527,8 @@ tscrollup(int orig, int n)
       + void
       + selscroll(int orig, int n)
       + {
       ++        if (sb.view_offset != 0)
       ++                return;
       +         if (sel.ob.x == -1 || sel.alt != IS_SET(MODE_ALTSCREEN))
       +                 return;
       + 
       +@@ -1720,6 +2149,12 @@ csihandle(void)
       +                         break;
       +                 case 2: /* all */
       +                         tclearregion(0, 0, term.col-1, term.row-1);
       ++                        if (!IS_SET(MODE_ALTSCREEN))
       ++                                sb_reset_on_clear();
       ++                        break;
       ++                case 3:
       ++                        if (!IS_SET(MODE_ALTSCREEN))
       ++                                sb_reset_on_clear();
       +                         break;
       +                 default:
       +                         goto unknown;
       +@@ -2109,7 +2544,7 @@ tdumpline(int n)
       +         const Glyph *bp, *end;
       + 
       +         bp = &term.line[n][0];
       +-        end = &bp[MIN(tlinelen(n), term.col) - 1];
       ++        end = &bp[MIN(tlinelen_render(n), term.col) - 1];
       +         if (bp != end || bp->u != ' ') {
       +                 for ( ; bp <= end; ++bp)
       +                         tprinter(buf, utf8encode(bp->u, buf));
       +@@ -2166,6 +2601,36 @@ tdeftran(char ascii)
       +         }
       + }
       + 
       ++static void
       ++kscroll(const Arg *arg)
       ++{
       ++        uint64_t oldstart;
       ++        uint64_t newstart;
       ++
       ++        oldstart = sb_view_start();
       ++        sb.view_offset += arg->i;
       ++        LIMIT(sb.view_offset, 0, sb.len);
       ++        newstart = sb_view_start();
       ++
       ++        selscrollback(oldstart - newstart);
       ++        redraw();
       ++}
       ++
       ++void
       ++kscrolldown(const Arg *arg)
       ++{
       ++        Arg a;
       ++
       ++        a.i = -arg->i;
       ++        kscroll(&a);
       ++}
       ++
       ++void
       ++kscrollup(const Arg *arg)
       ++{
       ++        kscroll(arg);
       ++}
       ++
       + void
       + tdectest(char c)
       + {
       +@@ -2572,83 +3037,138 @@ twrite(const char *buf, int buflen, int show_ctrl)
       + void
       + tresize(int col, int row)
       + {
       +-        int i;
       ++        int i, j;
       ++        int min_limit;
       +         int minrow = MIN(row, term.row);
       +-        int mincol = MIN(col, term.col);
       +-        int *bp;
       +-        TCursor c;
       ++        int old_row = term.row;
       ++        int old_col = term.col;
       ++        int save_end = 0; /* Track effective pushed height */
       ++        int loaded = 0;
       ++        int pop_width = 0;
       ++        int needs_reflow = 0;
       ++        int is_alt = IS_SET(MODE_ALTSCREEN);
       ++        Line *tmp;
       + 
       +         if (col < 1 || row < 1) {
       +                 fprintf(stderr,
       +-                        "tresize: error resizing to %dx%d\n", col, row);
       ++                        "tresize: error resizing to %dx%d\n", col, row);
       +                 return;
       +         }
       + 
       +-        /*
       +-         * slide screen to keep cursor where we expect it -
       +-         * tscrollup would work here, but we can optimize to
       +-         * memmove because we're freeing the earlier lines
       +-         */
       +-        for (i = 0; i <= term.c.y - row; i++) {
       +-                free(term.line[i]);
       +-                free(term.alt[i]);
       +-        }
       +-        /* ensure that both src and dst are not NULL */
       +-        if (i > 0) {
       +-                memmove(term.line, term.line + i, row * sizeof(Line));
       +-                memmove(term.alt, term.alt + i, row * sizeof(Line));
       +-        }
       +-        for (i += row; i < term.row; i++) {
       +-                free(term.line[i]);
       +-                free(term.alt[i]);
       ++        /* Operate on the currently visible screen buffer. */
       ++        if (is_alt) {
       ++                tmp = term.line;
       ++                term.line = term.alt;
       ++                term.alt = tmp;
       +         }
       + 
       +-        /* resize to new height */
       +-        term.line = xrealloc(term.line, row * sizeof(Line));
       +-        term.alt  = xrealloc(term.alt,  row * sizeof(Line));
       +-        term.dirty = xrealloc(term.dirty, row * sizeof(*term.dirty));
       +-        term.tabs = xrealloc(term.tabs, col * sizeof(*term.tabs));
       ++        save_end = term.row;
       ++        if (term.row != 0 && term.col != 0) {
       ++                if (!is_alt && term.c.y > 0 && term.c.y < term.row) {
       ++                        term.line[term.c.y - 1][term.col - 1].mode &= ~ATTR_WRAP;
       ++                }
       ++                min_limit = is_alt ? 0 : term.c.y;
       + 
       +-        /* resize each row to new width, zero-pad if needed */
       +-        for (i = 0; i < minrow; i++) {
       +-                term.line[i] = xrealloc(term.line[i], col * sizeof(Glyph));
       +-                term.alt[i]  = xrealloc(term.alt[i],  col * sizeof(Glyph));
       +-        }
       ++                for (i = term.row - 1; i > min_limit; i--) {
       ++                        if (tlinelen(term.line[i]) > 0)
       ++                                break;
       ++                }
       ++                save_end = i + 1;
       + 
       +-        /* allocate any new rows */
       +-        for (/* i = minrow */; i < row; i++) {
       +-                term.line[i] = xmalloc(col * sizeof(Glyph));
       +-                term.alt[i] = xmalloc(col * sizeof(Glyph));
       ++                for (i = 0; i < save_end; i++) {
       ++                        sb_push(term.line[i]);
       ++                }
       ++                /* Optimization: Only reflow if content doesn't fit in new width.
       ++                 * This avoids expensive reflow operations when resizing doesn't
       ++                 * affect line wrapping (e.g., when terminal is wide enough). */
       ++                if (col > term.col) {
       ++                        /* Growing: We MUST reflow. Even if the text doesn't need
       ++                         * un-wrapping, the history lines must be physically reallocated
       ++                         * to the new width to prevent heap-buffer-overflows on read. */
       ++                        needs_reflow = 1;
       ++                } else if (col < term.col) {
       ++                        /* Shrinking: Only reflow if content is wider than new width. */
       ++                        if (sb.max_width > col)
       ++                                needs_reflow = 1;
       ++                }
       ++                if (needs_reflow) {
       ++                        sb_resize(col);
       ++                } else {
       ++                        /* If we don't reflow, we still need to reset the view
       ++                         * because sb_pop_screen() might change the history length. */
       ++                        sb.view_offset = 0;
       ++                }
       +         }
       +-        if (col > term.col) {
       +-                bp = term.tabs + term.col;
       + 
       +-                memset(bp, 0, sizeof(*term.tabs) * (col - term.col));
       +-                while (--bp > term.tabs && !*bp)
       +-                        /* nothing */ ;
       +-                for (bp += tabspaces; bp < term.tabs + col; bp += tabspaces)
       +-                        *bp = 1;
       +-        }
       +-        /* update terminal size */
       ++                if (term.line) {
       ++                        for (i = 0; i < term.row; i++) {
       ++                                free(term.line[i]);
       ++                                free(term.alt[i]);
       ++                        }
       ++                        free(term.line);
       ++                        free(term.alt);
       ++                        free(term.dirty);
       ++                        free(term.tabs);
       ++                }
       ++
       +         term.col = col;
       +         term.row = row;
       +-        /* reset scrolling region */
       +-        tsetscroll(0, row-1);
       +-        /* make use of the LIMIT in tmoveto */
       +-        tmoveto(term.c.x, term.c.y);
       +-        /* Clearing both screens (it makes dirty all lines) */
       +-        c = term.c;
       +-        for (i = 0; i < 2; i++) {
       +-                if (mincol < col && 0 < minrow) {
       +-                        tclearregion(mincol, 0, col - 1, minrow - 1);
       +-                }
       +-                if (0 < col && minrow < row) {
       +-                        tclearregion(0, minrow, col - 1, row - 1);
       ++
       ++        term.line  = xmalloc(term.row * sizeof(Line));
       ++        term.alt   = xmalloc(term.row * sizeof(Line));
       ++        term.dirty = xmalloc(term.row * sizeof(int));
       ++        term.tabs  = xmalloc(term.col * sizeof(*term.tabs));
       ++
       ++        for (i = 0; i < term.row; i++) {
       ++                term.line[i] = xmalloc(term.col * sizeof(Glyph));
       ++                term.alt[i]  = xmalloc(term.col * sizeof(Glyph));
       ++                term.dirty[i] = 1;
       ++
       ++                for (j = 0; j < term.col; j++) {
       ++                        term.line[i][j] = term.c.attr;
       ++                        term.line[i][j].u = ' ';
       ++                        term.line[i][j].mode = 0;
       ++
       ++                        term.alt[i][j] = term.c.attr;
       ++                        term.alt[i][j].u = ' ';
       ++                        term.alt[i][j].mode = 0;
       +                 }
       +-                tswapscreen();
       +-                tcursor(CURSOR_LOAD);
       +         }
       +-        term.c = c;
       ++
       ++        memset(term.tabs, 0, term.col * sizeof(*term.tabs));
       ++        for (i = 8; i < term.col; i += 8)
       ++                term.tabs[i] = 1;
       ++
       ++        tsetscroll(0, term.row - 1);
       ++
       ++        if (minrow > 0) {
       ++                loaded = MIN(sb.len, term.row);
       ++                pop_width = needs_reflow ? col : MIN(col, old_col);
       ++                sb_pop_screen(loaded, pop_width);
       ++        }
       ++        if (is_alt) {
       ++                tmp = term.line;
       ++                term.line = term.alt;
       ++                term.alt = tmp;
       ++        }
       ++        if (!is_alt && old_row > 0) {
       ++                term.c.y += (loaded - save_end);
       ++        }
       ++        if (term.c.y >= term.row) {
       ++                term.c.y = term.row - 1;
       ++        }
       ++        if (term.c.x >= term.col) {
       ++                term.c.x = term.col - 1;
       ++        }
       ++        if (term.c.y < 0) {
       ++                term.c.y = 0;
       ++        }
       ++        if (term.c.x < 0) {
       ++                term.c.x = 0;
       ++        }
       ++
       ++        tfulldirt();
       ++        sb_view_changed();
       + }
       + 
       + void
       +@@ -2662,12 +3182,13 @@ drawregion(int x1, int y1, int x2, int y2)
       + {
       +         int y;
       + 
       ++        Line line;
       +         for (y = y1; y < y2; y++) {
       +                 if (!term.dirty[y])
       +                         continue;
       +-
       +                 term.dirty[y] = 0;
       +-                xdrawline(term.line[y], x1, y, x2);
       ++                line = renderline(y);
       ++                xdrawline(line, x1, y, x2);
       +         }
       + }
       + 
       +@@ -2688,10 +3209,12 @@ draw(void)
       +                 cx--;
       + 
       +         drawregion(0, 0, term.col, term.row);
       +-        xdrawcursor(cx, term.c.y, term.line[term.c.y][cx],
       +-                        term.ocx, term.ocy, term.line[term.ocy][term.ocx]);
       +-        term.ocx = cx;
       +-        term.ocy = term.c.y;
       ++        if (sb.view_offset == 0) {
       ++                xdrawcursor(cx, term.c.y, term.line[term.c.y][cx],
       ++                            term.ocx, term.ocy, term.line[term.ocy][term.ocx]);
       ++                term.ocx = cx;
       ++                term.ocy = term.c.y;
       ++        }
       +         xfinishdraw();
       +         if (ocx != term.ocx || ocy != term.ocy)
       +                 xximspot(term.ocx, term.ocy);
       +diff --git a/st.h b/st.h
       +index fd3b0d8..151d0c6 100644
       +--- a/st.h
       ++++ b/st.h
       +@@ -86,6 +86,7 @@ void printsel(const Arg *);
       + void sendbreak(const Arg *);
       + void toggleprinter(const Arg *);
       + 
       ++int tisaltscreen(void);
       + int tattrset(int);
       + void tnew(int, int);
       + void tresize(int, int);
       +@@ -111,6 +112,9 @@ void *xmalloc(size_t);
       + void *xrealloc(void *, size_t);
       + char *xstrdup(const char *);
       + 
       ++void kscrollup(const Arg *arg);
       ++void kscrolldown(const Arg *arg);
       ++
       + /* config.h globals */
       + extern char *utmp;
       + extern char *scroll;
       +@@ -124,3 +128,4 @@ extern unsigned int tabspaces;
       + extern unsigned int defaultfg;
       + extern unsigned int defaultbg;
       + extern unsigned int defaultcs;
       ++extern unsigned int scrollback_lines;
       +diff --git a/x.c b/x.c
       +index d73152b..75f3db1 100644
       +--- a/x.c
       ++++ b/x.c
       +@@ -472,6 +472,23 @@ bpress(XEvent *e)
       +         struct timespec now;
       +         int snap;
       + 
       ++        if (btn == Button4 || btn == Button5) {
       ++                Arg a;
       ++                if (IS_SET(MODE_MOUSE) && !(e->xbutton.state & forcemousemod)) {
       ++                        mousereport(e);
       ++                        return;
       ++                }
       ++                if (!tisaltscreen()) {
       ++                        a.i = 1;
       ++                        if (btn == Button4) {
       ++                                kscrollup(&a);
       ++                        } else {
       ++                                kscrolldown(&a);
       ++                        }
       ++                }
       ++                return;
       ++        }
       ++
       +         if (1 <= btn && btn <= 11)
       +                 buttons |= 1 << (btn-1);
       + 
       +-- 
       +2.53.0
       +