[wiki] [sites] [st][patches] scrollback-reflow added two separate patches || Milos Nikic

From: <git_AT_suckless.org>
Date: Fri, 16 Jan 2026 03:15:26 +0100

commit 5a0d54b3726895b7b58975fa0a6676ce5fad7b5b
Author: Milos Nikic <nikic.milos_AT_gmail.com>
Date: Thu Jan 15 17:58:49 2026 -0800

    [st][patches] scrollback-reflow added two separate patches
    
    One is Normal and the other extended
    Also added example pictures

diff --git a/st.suckless.org/patches/scrollback-reflow-standalone/index.md b/st.suckless.org/patches/scrollback-reflow-standalone/index.md
index 21662d8b..36b19e8e 100644
--- a/st.suckless.org/patches/scrollback-reflow-standalone/index.md
+++ b/st.suckless.org/patches/scrollback-reflow-standalone/index.md
_AT_@ -3,32 +3,39 @@ scrollback-reflow-standalone
 
 Description
 -----------
-A standalone scrollback and reflow implementation for st that integrates history storage,
-mouse scrolling, selection handling, and resize reflow into a single cohesive
-patch.
+A standalone scrollback and reflow implementation for st that integrates history
+storage, mouse scrolling, selection handling, and resize reflow into a single
+cohesive patch.
 
-This implementation achieves this by modifying st’s resize and screen buffer handling.
-It is intended as an alternative design, not as a patch stack to be combined
-with other scrollback-related patches.
+st does not provide scrollback by default. This patch adds scrollback support by
+modifying resize and screen buffer handling. It is intended as an alternative
+design, not as a patch stack to be combined with other scrollback-related
+patches.
+
+Two variants are provided:
+
+* **Normal**: scrollback, reflow, mouse and keyboard scrolling, and screen-bound
+ selection.
+* **Extended**: identical to the basic variant, but with persistent selection
+ that remains valid even when scrolled outside the visible screen.
 
 ---
 
 Why another scrollback patch?
 -----------------------------
 
-*TLDR*
-Because one patch should be easier to integrate than many.
+One patch is easier to handle than many.
 
-Existing scrollback patches for st tend to address individual features in
+Existing scrollback patches for st typically address individual features in
 isolation (history storage, mouse scrolling, resize behavior, or selection).
 When combined, these patches often interact in subtle ways, particularly during
 terminal resize operations.
 
 This patch explores a unified approach where scrollback storage, rendering,
-selection, and resize handling are driven through a single abstraction.
-By routing all visible content through the same view layer, it avoids common
-issues such as selection drift, incorrect wrapping after resize, and conflicting
-mouse behavior.
+selection, and resize handling are driven through a single abstraction. By
+routing all visible content through the same view layer, it avoids common issues
+such as selection drift, incorrect wrapping after resize, and conflicting mouse
+behavior.
 
 The goal is not to replace existing solutions, but to provide a standalone
 alternative that emphasizes correctness and predictable behavior.
_AT_@ -41,11 +48,12 @@ Key Features
 * Ring buffer scrollback with O(1) insertion
 * Text reflow on resize (O(N) in scrollback size)
 * Mouse wheel and touchpad scrolling
-* Altscreen-aware mouse handling (applications like vim and tmux receive mouse
+* Keyboard scrolling via `Shift + PageUp/PageDown` and `Shift + Home/End`
+* Altscreen-aware mouse handling (applications such as vim and tmux receive mouse
   events when requested)
-* No more text clipping when shrinking st's window
-* Visual cursor is hidden while viewing scrollback history
-* Stable selection while scrolling through history
+* Cursor hidden while viewing scrollback history
+* Stable selection while scrolling
+* Optional persistent selection (extended variant)
 
 ---
 
_AT_@ -56,7 +64,20 @@ When the terminal width changes, scrollback content is reflowed to match the new
 column width. Wrapped lines are flattened and rewrapped so that text is neither
 clipped nor lost when shrinking the window.
 
-To reduce overhead, reflow is skipped when not needed.
+To reduce overhead, reflow is skipped when it is not required.
+
+---
+Which patch should I choose?
+------------
+
+Choose the **extended** variant if you prefer selection behavior similar to
+most modern terminal emulators, where a selection remains valid even when
+scrolled outside the visible screen. Be mindful that this might interact
+poorly with more patches than the **basic** version.
+
+Choose the **basic** variant if you prefer st’s traditional selection behavior,
+where selection is limited to the visible screen, or if you plan to combine
+this patch with other modifications that interact with selection logic.
 
 ---
 
_AT_@ -67,44 +88,66 @@ This patch modifies `tresize()` and related resize handling to support scrollbac
 reflow and history preservation across window size changes.
 
 As a result, it is **not intended to be combined** with other scrollback patches
-or patches that modify `tresize()` or rely on its stock side effects.
-It should be treated as a standalone alternative for the existing scrollback
-model.
+or patches that modify `tresize()` or rely on its stock side effects. It should
+be treated as a standalone scrollback implementation.
 
 Patches that do not modify scrollback or resize behavior (e.g. transparency,
 clipboard helpers, key bindings) are generally easier to integrate.
 
+The extended variant relaxes certain selection bounds checks to allow selection
+to persist outside the visible screen. This may interact poorly with patches
+that make assumptions about screen-local selection coordinates.
+
 ---
 
 Notes & Caveats
 ---------------
 
-* **Screen-bound selection:** Selection is tied to visible screen coordinates.
- If a selection is scrolled completely off-screen while viewing history, it is
- cleared. This is a deliberate design choice to avoid rewriting the selection
- engine.
+* **Selection behavior**
+ * In the **Normal** variant, selection is tied to visible screen coordinates.
+ If a selection is scrolled completely off-screen, it is cleared.
+ * In the **extended** variant, selection persists even when scrolled outside
+ the visible screen.
 
-* **Shell prompt artifacts:** During resize in which the window becomes very narrow,
- shell prompts may appear duplicated or briefly "ghosted".
- This is normal behavior caused by the shell reacting to `SIGWINCH` while st
- simultaneously reflows historical content.
+* **Shell prompt artifacts**
+ When resizing to very narrow widths, shell prompts may briefly appear
+ duplicated or "ghosted". This is normal behavior caused by the shell reacting
+ to `SIGWINCH` while st simultaneously reflows historical content.
 
 ---
 
 Configuration
 -------------
 
-* **Scrollback size:** Configurable via `scrollback_lines` in `config.h`
+* **Scrollback size:** configurable via `scrollback_lines` in `config.h`
   (default: 5000).
 
-* **Key bindings:** Defaults to `MouseWheel`
+* **Key bindings:** defaults to `MouseWheel`, `Shift + PageUp/PageDown`, and
+ `Shift + Home/End`.
 
 ---
 
+Example
+-------
+
+The following example demonstrates resize reflow using a single line of
+colored blocks:
+
+ for i in {41..46}; do printf "\e[${i}m "; done; echo -e "\e[0m"
+
+<img src="st-full.png" alt="Wide terminal with color blocks on one line" />
+
+<img src="st-shrunk.png" alt="Narrow terminal with color blocks reflowed across multiple lines" />
+
+In the narrow case, the original line is reflowed into multiple logical lines.
+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)
+* Normal: [st-scrollback-reflow-standalone-0.9.31.diff](st-scrollback-reflow-standalone-0.9.31.diff)
+* Extended: [st-scrollback-reflow-standalone-0.9.31-extended.diff](st-scrollback-reflow-standalone-0.9.31-extended.diff)
 
 ---
 
_AT_@ -112,3 +155,5 @@ Author
 ------
 
 * Loshmi <nloshmi_AT_gmail.com>
+* [GitHub](https://github.com/mnikic/st)
+
diff --git a/st.suckless.org/patches/scrollback-reflow-standalone/st-full.png b/st.suckless.org/patches/scrollback-reflow-standalone/st-full.png
new file mode 100644
index 00000000..c51c2c4a
Binary files /dev/null and b/st.suckless.org/patches/scrollback-reflow-standalone/st-full.png differ
diff --git a/st.suckless.org/patches/scrollback-reflow-standalone/st-scrollback-reflow-standalone-0.9.31-extended.diff b/st.suckless.org/patches/scrollback-reflow-standalone/st-scrollback-reflow-standalone-0.9.31-extended.diff
new file mode 100644
index 00000000..1da54de3
--- /dev/null
+++ b/st.suckless.org/patches/scrollback-reflow-standalone/st-scrollback-reflow-standalone-0.9.31-extended.diff
_AT_@ -0,0 +1,1078 @@
+From 55e224cf4d767db7d9184e70a0f3838935679a53 Mon Sep 17 00:00:00 2001
+From: Milos Nikic <nikic.milos_AT_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
+_AT_@ -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} },
+_AT_@ -472,3 +476,8 @@ static char ascii_printable[] =
+ " !\"#$%&'()*+,-./0123456789:;<=>?"
+ "_AT_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
+_AT_@ -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>
+_AT_@ -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);
+_AT_@ -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)
+ {
+_AT_@ -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)
+ {
+_AT_@ -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;
+ }
+
+_AT_@ -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:
+_AT_@ -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;
+_AT_@ -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)))
+_AT_@ -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;
+ }
+_AT_@ -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;
+_AT_@ -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++ = '
';
+ 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.
+_AT_@ -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++ = '
';
+ }
+ *ptr = 0;
+_AT_@ -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);
+
+_AT_@ -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);
+
+_AT_@ -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();
+ }
+_AT_@ -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);
+
+_AT_@ -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;
+
+_AT_@ -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();
+ }
+ }
+
+_AT_@ -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;
+_AT_@ -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));
+_AT_@ -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)
+ {
+_AT_@ -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
", col, row);
++ "tresize: error resizing to %dx%d
", 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
+_AT_@ -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);
+ }
+ }
+
+_AT_@ -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
+_AT_@ -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);
+_AT_@ -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;
+_AT_@ -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
+_AT_@ -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-0.9.31.diff b/st.suckless.org/patches/scrollback-reflow-standalone/st-scrollback-reflow-standalone-0.9.31.diff
new file mode 100644
index 00000000..7394721a
--- /dev/null
+++ b/st.suckless.org/patches/scrollback-reflow-standalone/st-scrollback-reflow-standalone-0.9.31.diff
_AT_@ -0,0 +1,1062 @@
+From 3f41a815d3a0527274b1e83238e821bd286c0905 Mon Sep 17 00:00:00 2001
+From: Milos Nikic <nikic.milos_AT_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.
+---
+ config.def.h | 9 +
+ st.c | 720 ++++++++++++++++++++++++++++++++++++++++++++-------
+ st.h | 5 +
+ x.c | 17 ++
+ 4 files changed, 658 insertions(+), 93 deletions(-)
+
+diff --git a/config.def.h b/config.def.h
+index 2cd740a..135a0b1 100644
+--- a/config.def.h
++++ b/config.def.h
+_AT_@ -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} },
+_AT_@ -472,3 +476,8 @@ static char ascii_printable[] =
+ " !\"#$%&'()*+,-./0123456789:;<=>?"
+ "_AT_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..3b0803f 100644
+--- a/st.c
++++ b/st.c
+_AT_@ -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>
+_AT_@ -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);
+_AT_@ -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)
+ {
+_AT_@ -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)
+ {
+_AT_@ -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;
+ }
+
+_AT_@ -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:
+_AT_@ -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;
+_AT_@ -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)))
+_AT_@ -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;
+ }
+_AT_@ -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;
+_AT_@ -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++ = '
';
+ 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.
+_AT_@ -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++ = '
';
+ }
+ *ptr = 0;
+_AT_@ -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);
+
+_AT_@ -965,6 +1360,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);
+
+_AT_@ -1030,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();
+ }
+_AT_@ -1078,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);
+
+_AT_@ -1097,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;
+
+_AT_@ -1717,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;
+_AT_@ -2106,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));
+_AT_@ -2163,6 +2601,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)
+ {
+_AT_@ -2569,83 +3047,136 @@ 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
", col, row);
++ "tresize: error resizing to %dx%d
", 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: 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
+_AT_@ -2659,12 +3190,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);
+ }
+ }
+
+_AT_@ -2685,10 +3217,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
+_AT_@ -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);
+_AT_@ -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;
+_AT_@ -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
+_AT_@ -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-shrunk.png b/st.suckless.org/patches/scrollback-reflow-standalone/st-shrunk.png
new file mode 100644
index 00000000..98ab1a44
Binary files /dev/null and b/st.suckless.org/patches/scrollback-reflow-standalone/st-shrunk.png differ
Received on Fri Jan 16 2026 - 03:15:26 CET

This archive was generated by hypermail 2.3.0 : Fri Jan 16 2026 - 03:24:45 CET