commit 04f6d9c4a2ab4449cac975afc2ea1fffc2f9c75c
parent 65946acfd6c946798e9f8a7babc62445fb4b505f
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:
3 files changed, 1049 insertions(+), 1079 deletions(-)
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
------
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
-
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
+