sites

public wiki contents of suckless.org
git clone git://git.suckless.org/sites
Log | Files | Refs

commit 308bc705a0482ddc18020ab584c11a9f37723cf9
parent b68b11fb5b9332ecd1f1d5173a2294a43c73dbbb
Author: Milos Nikic <nikic.milos@gmail.com>
Date:   Tue, 13 Jan 2026 16:35:38 -0800

Alternative standalone scrollback, reflow 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 vs other scrollback patches:
    - One patch vs many to get the same functionality
    - Scrollback is disabled on the alternate screen
    - Simpler model than the existing scrollback patch set
    - Mouse wheel scrolling enabled by default
    - 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.

Diffstat:
Ast.suckless.org/patches/scrollback-reflow-standalone/index.md | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ast.suckless.org/patches/scrollback-reflow-standalone/st-scrollback-reflow-standalone-0.9.3.diff | 1039+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 1153 insertions(+), 0 deletions(-)

diff --git a/st.suckless.org/patches/scrollback-reflow-standalone/index.md b/st.suckless.org/patches/scrollback-reflow-standalone/index.md @@ -0,0 +1,114 @@ +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. + +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. + +--- + +Why another scrollback patch? +----------------------------- + +*TLDR* +Because one patch should be easier to integrate than many. + +Existing scrollback patches for st tend to 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. + +The goal is not to replace existing solutions, but to provide a standalone +alternative that emphasizes correctness and predictable behavior. + +--- + +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 + 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 + +--- + +Resize Reflow +------------- + +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. + +--- + +Patch Compatibility +------------------- + +This patch modifies `tresize()` and related resize handling to support scrollback +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. + +Patches that do not modify scrollback or resize behavior (e.g. transparency, +clipboard helpers, key bindings) are generally easier to integrate. + +--- + +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. + +* **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. + +--- + +Configuration +------------- + +* **Scrollback size:** Configurable via `scrollback_lines` in `config.h` + (default: 5000). + +* **Key bindings:** Defaults to `MouseWheel` + +--- + +Download +-------- + +* [st-scrollback-reflow-standalone-0.9.3.diff](st-scrollback-reflow-standalone-0.9.3.diff) + +--- + +Author +------ + +* Loshmi <nloshmi@gmail.com> diff --git a/st.suckless.org/patches/scrollback-reflow-standalone/st-scrollback-reflow-standalone-0.9.3.diff b/st.suckless.org/patches/scrollback-reflow-standalone/st-scrollback-reflow-standalone-0.9.3.diff @@ -0,0 +1,1039 @@ +From 6d7584cc382fd24e5da92baf3ac333b5dda12f3c 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. +Implement reflow of history and screen content on resizeif 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 +- 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 | 5 + + st.c | 710 ++++++++++++++++++++++++++++++++++++++++++++------- + st.h | 5 + + x.c | 17 ++ + 4 files changed, 644 insertions(+), 93 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 e55e7b3..3f6b4a8 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,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); + +@@ -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(); + } +@@ -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); + +@@ -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; + +@@ -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; +@@ -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)); +@@ -2163,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) + { +@@ -2569,83 +3037,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\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: 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 +3180,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 +3207,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 +