]> Skullheadx's Git Forge - st.git/commitdiff
st: alternative scrollback using ring buffer and view offset
authorMilos Nikic <nikic.milos@gmail.com>
Fri, 16 Jan 2026 00:08:59 +0000 (16:08 -0800)
committerSkullheadx <admonty1@protonmail.com>
Wed, 29 Apr 2026 08:22:40 +0000 (04:22 -0400)
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
st.c
st.h
x.c

index 6625ecac151caf893096e902b66d0bc8659e5033..382197fd4a7510314e7495bb4e5712c823b09fa6 100644 (file)
@@ -207,6 +207,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} },
@@ -487,3 +491,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 fb7327bba364cee7ca1766d359a13a3ec621ba79..ce8eff8d0b17b0fe1dde17c45ecbe941e5ed3888 100644 (file)
--- 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,9 +1357,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 +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();
 }
@@ -1081,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);
 
@@ -1100,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;
 
@@ -1108,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();
        }
 }
 
@@ -1723,6 +2144,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;
@@ -2112,7 +2539,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));
@@ -2169,6 +2596,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)
 {
@@ -2575,83 +3042,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
@@ -2665,12 +3188,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);
        }
 }
 
@@ -2691,10 +3215,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 07a7c660cf54a0b01f4a07f598be6e12f89f7881..02d9f8f337eabd0b19fc1f9bb3aad083644a2133 100644 (file)
--- a/st.h
+++ b/st.h
@@ -87,6 +87,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);
@@ -121,6 +122,8 @@ ushort boxdrawindex(const Glyph *);
 void boxdraw_xinit(Display *, Colormap, XftDraw *, Visual *);
 void drawboxes(int, int, int, int, XftColor *, XftColor *, const XftGlyphFontSpec *, int);
 #endif
+void kscrollup(const Arg *arg);
+void kscrolldown(const Arg *arg);
 
 /* config.h globals */
 extern char *utmp;
@@ -136,3 +139,4 @@ extern unsigned int defaultfg;
 extern unsigned int defaultbg;
 extern unsigned int defaultcs;
 extern const int boxdraw, boxdraw_bold, boxdraw_braille;
+extern unsigned int scrollback_lines;
diff --git a/x.c b/x.c
index 28bf0152505d401bbcc3a94d8fb88441619019c8..b2e8ceea77eac95cae02ced192f7357c4cdd3bcb 100644 (file)
--- a/x.c
+++ b/x.c
@@ -474,6 +474,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);