sites

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

st-scrollback-reflow-standalone-extended-0.9.31.diff (24796B)


      1 From 792cbb832839cb6981440356c26ce2836bc69427 Mon Sep 17 00:00:00 2001
      2 From: Milos Nikic <nikic.milos@gmail.com>
      3 Date: Thu, 8 Jan 2026 22:04:25 -0800
      4 Subject: [PATCH] st: alternative scrollback using ring buffer and view offset
      5 
      6 Implement scrollback as a fixed-size ring buffer and render history
      7 by offsetting the view instead of copying screen contents.
      8 
      9 Tradeoffs / differences:
     10 - Scrollback history is lost on resize
     11 - Scrollback is disabled on the alternate screen
     12 - Simpler model than the existing scrollback patch set
     13 - Mouse wheel scrolling enabled by default
     14 
     15 Note:
     16 When using vim, mouse movement will no longer move the cursor.
     17 
     18 Reminder:
     19 If applying this patch on top of others, ensure any changes to
     20 config.def.h are merged into config.h.
     21 ---
     22  config.def.h |   5 +
     23  st.c         | 713 ++++++++++++++++++++++++++++++++++++++++++++-------
     24  st.h         |   5 +
     25  x.c          |  17 ++
     26  4 files changed, 645 insertions(+), 95 deletions(-)
     27 
     28 diff --git a/config.def.h b/config.def.h
     29 index 2cd740a..a0b14e9 100644
     30 --- a/config.def.h
     31 +++ b/config.def.h
     32 @@ -472,3 +472,8 @@ static char ascii_printable[] =
     33  	" !\"#$%&'()*+,-./0123456789:;<=>?"
     34  	"@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_"
     35  	"`abcdefghijklmnopqrstuvwxyz{|}~";
     36 +
     37 +/*
     38 + * The amount of lines scrollback can hold before it wraps around.
     39 + */
     40 +unsigned int scrollback_lines = 5000;
     41 diff --git a/st.c b/st.c
     42 index 6f40e35..cf76c58 100644
     43 --- a/st.c
     44 +++ b/st.c
     45 @@ -5,6 +5,7 @@
     46  #include <limits.h>
     47  #include <pwd.h>
     48  #include <stdarg.h>
     49 +#include <stdint.h>
     50  #include <stdio.h>
     51  #include <stdlib.h>
     52  #include <string.h>
     53 @@ -178,7 +179,7 @@ static void tdeletechar(int);
     54  static void tdeleteline(int);
     55  static void tinsertblank(int);
     56  static void tinsertblankline(int);
     57 -static int tlinelen(int);
     58 +static int tlinelen(Line);
     59  static void tmoveto(int, int);
     60  static void tmoveato(int, int);
     61  static void tnewline(int);
     62 @@ -232,6 +233,379 @@ static const uchar utfmask[UTF_SIZ + 1] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8};
     63  static const Rune utfmin[UTF_SIZ + 1] = {       0,    0,  0x80,  0x800,  0x10000};
     64  static const Rune utfmax[UTF_SIZ + 1] = {0x10FFFF, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF};
     65  
     66 +typedef struct
     67 +{
     68 +	Line *buf;       /* ring of Line pointers */
     69 +	int cap;         /* max number of lines */
     70 +	int len;         /* current number of valid lines (<= cap) */
     71 +	int head;        /* physical index of logical oldest (valid when len>0) */
     72 +	uint64_t base;   /* Can overflow in the extreme */
     73 +	/*
     74 +	 * max_width tracks the widest line ever pushed to scrollback.
     75 +	 * It may be conservative (stale) if that line has since been
     76 +	 * evicted from the ring buffer, which is acceptable - it just
     77 +	 * means we might reflow when not strictly necessary, which is
     78 +	 * better than skipping a needed reflow.
     79 +	 */
     80 +	int max_width;
     81 +	int view_offset; /* 0 means live screen */
     82 +} Scrollback;
     83 +
     84 +static Scrollback sb;
     85 +
     86 +static int
     87 +sb_phys_index(int logical_idx)
     88 +{
     89 +	/* logical_idx: 0..sb.len-1 (0 = oldest) */
     90 +	return (sb.head + logical_idx) % sb.cap;
     91 +}
     92 +
     93 +static Line
     94 +lineclone(Line src)
     95 +{
     96 +	Line dst;
     97 +
     98 +	if (!src)
     99 +		return NULL;
    100 +
    101 +	dst = xmalloc(term.col * sizeof(Glyph));
    102 +	memcpy(dst, src, term.col * sizeof(Glyph));
    103 +	return dst;
    104 +}
    105 +
    106 +static void
    107 +sb_init(int lines)
    108 +{
    109 +	int i;
    110 +
    111 +	sb.buf  = xmalloc(sizeof(Line) * lines);
    112 +	sb.cap  = lines;
    113 +	sb.len  = 0;
    114 +	sb.head = 0;
    115 +	sb.base = 0;
    116 +	for (i = 0; i < sb.cap; i++)
    117 +		sb.buf[i] = NULL;
    118 +
    119 +	sb.view_offset = 0;
    120 +	sb.max_width = 0;
    121 +}
    122 +
    123 +/* Push one screen line into scrollback.
    124 + * Overwrites oldest when full (ring buffer).
    125 + */
    126 +static void
    127 +sb_push(Line line)
    128 +{
    129 +	Line copy;
    130 +	int tail;
    131 +	int width;
    132 +
    133 +	if (sb.cap <= 0)
    134 +		return;
    135 +
    136 +	copy = lineclone(line);
    137 +
    138 +	if (sb.len < sb.cap) {
    139 +		tail = sb_phys_index(sb.len);
    140 +		sb.buf[tail] = copy;
    141 +		sb.len++;
    142 +	} else {
    143 +		/* We might've just evicted the widest line... */
    144 +		free(sb.buf[sb.head]);
    145 +		sb.buf[sb.head] = copy;
    146 +		sb.head = (sb.head + 1) % sb.cap;
    147 +		sb.base++;
    148 +	}
    149 +	width = tlinelen(copy);
    150 +	/* ...so max_width might be stale. */
    151 +	if (width > sb.max_width)
    152 +		sb.max_width = width;
    153 +}
    154 +
    155 +static Line
    156 +sb_get(int idx)
    157 +{
    158 +	/* idx is logical: 0..sb.len-1 */
    159 +	if (idx < 0 || idx >= sb.len)
    160 +		return NULL;
    161 +	return sb.buf[sb_phys_index(idx)];
    162 +}
    163 +
    164 +static void
    165 +sb_clear(void)
    166 +{
    167 +	int i;
    168 +	int p;
    169 +
    170 +	if (!sb.buf)
    171 +		return;
    172 +
    173 +	for (i = 0; i < sb.len; i++) {
    174 +		p = sb_phys_index(i);
    175 +		if (sb.buf[p]) {
    176 +			free(sb.buf[p]);
    177 +			sb.buf[p] = NULL;
    178 +		}
    179 +	}
    180 +
    181 +	sb.len = 0;
    182 +	sb.head = 0;
    183 +	sb.base = 0;
    184 +	sb.view_offset = 0;
    185 +	sb.max_width = 0;
    186 +}
    187 +
    188 +/*
    189 + * Reflows the scrollback buffer to fit a new terminal width.
    190 + *
    191 + * The algorithm works in three steps:
    192 + * 1) Unwrap: It iterates through the existing history, joining physical lines
    193 + * marked with ATTR_WRAP into a single continuous 'logical' line.
    194 + * 2) Reflow: It slices this logical line into new chunks of size 'col'.
    195 + * - New wrap flags are applied where the text exceeds the new width.
    196 + * - Trailing spaces are trimmed to prevent ghost padding.
    197 + * 3) Rebuild: The new lines are pushed into a fresh ring buffer.
    198 + * - Uses O(1) ring insertion (updating head/tail) to avoid expensive
    199 + * memmoves during resize, but it is still O(N) where N is the existing
    200 + * history.
    201 + *
    202 + * Note: During reflow we reset sb to match the rebuilt buffer
    203 + * (head, base and len might change).
    204 + */
    205 +static void
    206 +sb_resize(int col)
    207 +{
    208 +	Line *new_buf;
    209 +	int i, j;
    210 +	int new_len, logical_cap, logical_len, is_wrapped, cursor;
    211 +	int copy_width, tail, current_width;
    212 +	Line logical, line, nl;
    213 +	uint64_t new_base = 0;
    214 +	int new_head = 0;
    215 +	int new_max_width = 0;
    216 +	Glyph *g;
    217 +
    218 +	new_len = 0;
    219 +
    220 +	if (sb.len == 0)
    221 +		return;
    222 +
    223 +	new_buf = xmalloc(sizeof(Line) * sb.cap);
    224 +	for (i = 0; i < sb.cap; i++)
    225 +		new_buf[i] = NULL;
    226 +
    227 +	logical_cap = term.col * 2;
    228 +	logical = xmalloc(logical_cap * sizeof(Glyph));
    229 +	logical_len = 0;
    230 +
    231 +	for (i = 0; i < sb.len; i++) {
    232 +		/* Unwrap: Accumulate physical lines into one logical line. */
    233 +		line = sb_get(i);
    234 +		is_wrapped = (line[term.col - 1].mode & ATTR_WRAP);
    235 +		if (logical_len + term.col > logical_cap) {
    236 +			logical_cap *= 2;
    237 +			logical = xrealloc(logical, logical_cap * sizeof(Glyph));
    238 +		}
    239 +
    240 +		memcpy(logical + logical_len, line, term.col * sizeof(Glyph));
    241 +		for (j = 0; j < term.col; j++) {
    242 +			logical[logical_len + j].mode &= ~ATTR_WRAP;
    243 +		}
    244 +		logical_len += term.col;
    245 +		/* If the line was wrapped, continue accumulating before reflowing. */
    246 +		if (is_wrapped) {
    247 +			continue;
    248 +		}
    249 +		/* Trim trailing spaces from the fully unwrapped line. */
    250 +		while (logical_len > 0) {
    251 +			g = &logical[logical_len - 1];
    252 +			if (g->u == ' ' && g->bg == defaultbg
    253 +					&& (g->mode & ATTR_BOLD) == 0) {
    254 +				logical_len--;
    255 +			} else {
    256 +				break;
    257 +			}
    258 +		}
    259 +		if (logical_len == 0)
    260 +			logical_len = 1;
    261 +
    262 +		/* Reflow: Split the logical line into new chunks. */
    263 +		cursor = 0;
    264 +		while (cursor < logical_len) {
    265 +			nl = xmalloc(col * sizeof(Glyph));
    266 +			for (j = 0; j < col; j++) {
    267 +				nl[j].fg = defaultfg;
    268 +				nl[j].bg = defaultbg;
    269 +				nl[j].mode = 0;
    270 +				nl[j].u = ' ';
    271 +			}
    272 +
    273 +			copy_width = logical_len - cursor;
    274 +			if (copy_width > col)
    275 +				copy_width = col;
    276 +
    277 +			memcpy(nl, logical + cursor, copy_width * sizeof(Glyph));
    278 +
    279 +			for (j = 0; j < copy_width; j++) {
    280 +				nl[j].mode &= ~ATTR_WRAP;
    281 +			}
    282 +
    283 +			if (cursor + copy_width < logical_len) {
    284 +				nl[col - 1].mode |= ATTR_WRAP;
    285 +			} else {
    286 +				nl[col - 1].mode &= ~ATTR_WRAP;
    287 +			}
    288 +
    289 +			/* Rebuild: Push new lines into the ring buffer. */
    290 +			if (new_len < sb.cap) {
    291 +				tail = (new_head + new_len) % sb.cap;
    292 +				new_buf[tail] = nl;
    293 +				new_len++;
    294 +			} else {
    295 +				free(new_buf[new_head]);
    296 +				new_buf[new_head] = nl;
    297 +				new_head = (new_head + 1) % sb.cap;
    298 +				new_base++;
    299 +			}
    300 +			current_width = (cursor + copy_width < logical_len) ? col : copy_width;
    301 +			if (current_width > new_max_width)
    302 +				new_max_width = current_width;
    303 +			cursor += copy_width;
    304 +		}
    305 +		logical_len = 0;
    306 +	}
    307 +	free(logical);
    308 +	sb_clear();
    309 +	free(sb.buf);
    310 +	sb.buf = new_buf;
    311 +	sb.len = new_len;
    312 +	sb.head = new_head;
    313 +	sb.base = new_base;
    314 +	sb.view_offset = 0;
    315 +	sb.max_width = new_max_width;
    316 +}
    317 +
    318 +static void
    319 +sb_pop_screen(int loaded, int new_cols)
    320 +{
    321 +	int i, p;
    322 +	int start_logical;
    323 +	Line line;
    324 +
    325 +	loaded = MIN(loaded, sb.len);
    326 +	start_logical = sb.len - loaded;
    327 +	new_cols = MIN(new_cols, term.col);
    328 +	for (i = 0; i < loaded; i++) {
    329 +		p = sb_phys_index(start_logical + i);
    330 +		line = sb.buf[p];
    331 +
    332 +		memcpy(term.line[i], line, new_cols * sizeof(Glyph));
    333 +
    334 +		free(line);
    335 +		sb.buf[p] = NULL;
    336 +	}
    337 +
    338 +	sb.len -= loaded;
    339 +}
    340 +
    341 +static uint64_t
    342 +sb_view_start(void)
    343 +{
    344 +	return sb.base + sb.len - sb.view_offset;
    345 +}
    346 +
    347 +static void
    348 +sb_view_changed(void)
    349 +{
    350 +	if (!term.dirty || term.row <= 0)
    351 +		return;
    352 +	tfulldirt();
    353 +}
    354 +
    355 +static void
    356 +selscrollback(int delta)
    357 +{
    358 +	if (delta == 0)
    359 +		return;
    360 +
    361 +	if (sel.ob.x == -1 || sel.mode == SEL_EMPTY)
    362 +		return;
    363 +
    364 +	if (sel.alt != IS_SET(MODE_ALTSCREEN))
    365 +		return;
    366 +
    367 +	sel.nb.y += delta;
    368 +	sel.ne.y += delta;
    369 +	sel.ob.y += delta;
    370 +	sel.oe.y += delta;
    371 +
    372 +	if (sel.ne.y < 0 || sel.nb.y >= term.row)
    373 +		selclear();
    374 +
    375 +	sb_view_changed();
    376 +}
    377 +
    378 +static Line
    379 +emptyline(void)
    380 +{
    381 +	static Line empty;
    382 +	static int empty_cols;
    383 +	int i = 0;
    384 +
    385 +	if (empty_cols != term.col) {
    386 +		free(empty);
    387 +		empty = xmalloc(term.col * sizeof(Glyph));
    388 +		empty_cols = term.col;
    389 +	}
    390 +
    391 +	for (i = 0; i < term.col; i++) {
    392 +		empty[i] = term.c.attr;
    393 +		empty[i].u = ' ';
    394 +		empty[i].mode = 0;
    395 +	}
    396 +	return empty;
    397 +}
    398 +
    399 +static Line
    400 +renderline(int y)
    401 +{
    402 +	int start, v;
    403 +
    404 +	if (sb.view_offset <= 0)
    405 +		return term.line[y];
    406 +
    407 +	start = sb.len - sb.view_offset; /* can be negative */
    408 +	v = start + y;
    409 +
    410 +	if (v < 0)
    411 +		return emptyline();
    412 +
    413 +	if (v < sb.len)
    414 +		return sb_get(v);
    415 +
    416 +	/* past scrollback -> into current screen */
    417 +	v -= sb.len;
    418 +	if (v >= 0 && v < term.row)
    419 +		return term.line[v];
    420 +
    421 +	return emptyline();
    422 +}
    423 +
    424 +static void
    425 +sb_reset_on_clear(void)
    426 +{
    427 +	sb_clear();
    428 +	sb_view_changed();
    429 +	if (sel.ob.x != -1 && term.row > 0)
    430 +		selclear();
    431 +}
    432 +
    433 +int
    434 +tisaltscreen(void)
    435 +{
    436 +	return IS_SET(MODE_ALTSCREEN);
    437 +}
    438 +
    439  ssize_t
    440  xwrite(int fd, const char *s, size_t len)
    441  {
    442 @@ -404,20 +778,23 @@ selinit(void)
    443  	sel.ob.x = -1;
    444  }
    445  
    446 -int
    447 -tlinelen(int y)
    448 +static int
    449 +tlinelen(Line line)
    450  {
    451  	int i = term.col;
    452 -
    453 -	if (term.line[y][i - 1].mode & ATTR_WRAP)
    454 +	if (line[i - 1].mode & ATTR_WRAP)
    455  		return i;
    456 -
    457 -	while (i > 0 && term.line[y][i - 1].u == ' ')
    458 +	while (i > 0 && line[i - 1].u == ' ')
    459  		--i;
    460 -
    461  	return i;
    462  }
    463  
    464 +static int
    465 +tlinelen_render(int y)
    466 +{
    467 +	return tlinelen(renderline(y));
    468 +}
    469 +
    470  void
    471  selstart(int col, int row, int snap)
    472  {
    473 @@ -485,10 +862,10 @@ selnormalize(void)
    474  	/* expand selection over line breaks */
    475  	if (sel.type == SEL_RECTANGULAR)
    476  		return;
    477 -	i = tlinelen(sel.nb.y);
    478 +	i = tlinelen_render(sel.nb.y);
    479  	if (i < sel.nb.x)
    480  		sel.nb.x = i;
    481 -	if (tlinelen(sel.ne.y) <= sel.ne.x)
    482 +	if (tlinelen_render(sel.ne.y) <= sel.ne.x)
    483  		sel.ne.x = term.col - 1;
    484  }
    485  
    486 @@ -514,6 +891,7 @@ selsnap(int *x, int *y, int direction)
    487  	int newx, newy, xt, yt;
    488  	int delim, prevdelim;
    489  	const Glyph *gp, *prevgp;
    490 +	Line line;
    491  
    492  	switch (sel.snap) {
    493  	case SNAP_WORD:
    494 @@ -521,7 +899,7 @@ selsnap(int *x, int *y, int direction)
    495  		 * Snap around if the word wraps around at the end or
    496  		 * beginning of a line.
    497  		 */
    498 -		prevgp = &term.line[*y][*x];
    499 +		prevgp = &renderline(*y)[*x];
    500  		prevdelim = ISDELIM(prevgp->u);
    501  		for (;;) {
    502  			newx = *x + direction;
    503 @@ -536,14 +914,15 @@ selsnap(int *x, int *y, int direction)
    504  					yt = *y, xt = *x;
    505  				else
    506  					yt = newy, xt = newx;
    507 -				if (!(term.line[yt][xt].mode & ATTR_WRAP))
    508 +				line = renderline(yt);
    509 +				if (!(line[xt].mode & ATTR_WRAP))
    510  					break;
    511  			}
    512  
    513 -			if (newx >= tlinelen(newy))
    514 +			if (newx >= tlinelen_render(newy))
    515  				break;
    516  
    517 -			gp = &term.line[newy][newx];
    518 +			gp = &renderline(newy)[newx];
    519  			delim = ISDELIM(gp->u);
    520  			if (!(gp->mode & ATTR_WDUMMY) && (delim != prevdelim
    521  					|| (delim && gp->u != prevgp->u)))
    522 @@ -564,14 +943,14 @@ selsnap(int *x, int *y, int direction)
    523  		*x = (direction < 0) ? 0 : term.col - 1;
    524  		if (direction < 0) {
    525  			for (; *y > 0; *y += direction) {
    526 -				if (!(term.line[*y-1][term.col-1].mode
    527 +				if (!(renderline(*y-1)[term.col-1].mode
    528  						& ATTR_WRAP)) {
    529  					break;
    530  				}
    531  			}
    532  		} else if (direction > 0) {
    533  			for (; *y < term.row-1; *y += direction) {
    534 -				if (!(term.line[*y][term.col-1].mode
    535 +				if (!(renderline(*y)[term.col-1].mode
    536  						& ATTR_WRAP)) {
    537  					break;
    538  				}
    539 @@ -585,8 +964,9 @@ char *
    540  getsel(void)
    541  {
    542  	char *str, *ptr;
    543 -	int y, bufsize, lastx, linelen;
    544 +	int y, bufsize, lastx, linelen, end_idx, insert_newline, is_wrapped;
    545  	const Glyph *gp, *last;
    546 +	Line line;
    547  
    548  	if (sel.ob.x == -1)
    549  		return NULL;
    550 @@ -596,29 +976,33 @@ getsel(void)
    551  
    552  	/* append every set & selected glyph to the selection */
    553  	for (y = sel.nb.y; y <= sel.ne.y; y++) {
    554 -		if ((linelen = tlinelen(y)) == 0) {
    555 +		line = renderline(y);
    556 +		linelen = tlinelen_render(y);
    557 +
    558 +		if (linelen == 0) {
    559  			*ptr++ = '\n';
    560  			continue;
    561  		}
    562  
    563  		if (sel.type == SEL_RECTANGULAR) {
    564 -			gp = &term.line[y][sel.nb.x];
    565 +			gp = &line[sel.nb.x];
    566  			lastx = sel.ne.x;
    567  		} else {
    568 -			gp = &term.line[y][sel.nb.y == y ? sel.nb.x : 0];
    569 +			gp = &line[sel.nb.y == y ? sel.nb.x : 0];
    570  			lastx = (sel.ne.y == y) ? sel.ne.x : term.col-1;
    571  		}
    572 -		last = &term.line[y][MIN(lastx, linelen-1)];
    573 -		while (last >= gp && last->u == ' ')
    574 +		end_idx = MIN(lastx, linelen-1);
    575 +		is_wrapped = (line[end_idx].mode & ATTR_WRAP) != 0;
    576 +		last = &line[end_idx];
    577 +		while (last >= gp && last->u == ' ') {
    578  			--last;
    579 +		}
    580  
    581  		for ( ; gp <= last; ++gp) {
    582  			if (gp->mode & ATTR_WDUMMY)
    583  				continue;
    584 -
    585  			ptr += utf8encode(gp->u, ptr);
    586  		}
    587 -
    588  		/*
    589  		 * Copy and pasting of line endings is inconsistent
    590  		 * in the inconsistent terminal and GUI world.
    591 @@ -628,8 +1012,13 @@ getsel(void)
    592  		 * st.
    593  		 * FIXME: Fix the computer world.
    594  		 */
    595 +		insert_newline = 0;
    596  		if ((y < sel.ne.y || lastx >= linelen) &&
    597 -		    (!(last->mode & ATTR_WRAP) || sel.type == SEL_RECTANGULAR))
    598 +			(!is_wrapped || sel.type == SEL_RECTANGULAR)) {
    599 +			insert_newline = 1;
    600 +		}
    601 +
    602 +		if (insert_newline)
    603  			*ptr++ = '\n';
    604  	}
    605  	*ptr = 0;
    606 @@ -845,6 +1234,12 @@ ttywrite(const char *s, size_t n, int may_echo)
    607  {
    608  	const char *next;
    609  
    610 +	if (sb.view_offset > 0) {
    611 +		selclear();
    612 +		sb.view_offset = 0;
    613 +		sb_view_changed();
    614 +	}
    615 +
    616  	if (may_echo && IS_SET(MODE_ECHO))
    617  		twrite(s, n, 1);
    618  
    619 @@ -965,9 +1360,8 @@ tsetdirt(int top, int bot)
    620  {
    621  	int i;
    622  
    623 -	if (term.row <= 0)
    624 +	if (term.row < 1)
    625  		return;
    626 -
    627  	LIMIT(top, 0, term.row-1);
    628  	LIMIT(bot, 0, term.row-1);
    629  
    630 @@ -1033,15 +1427,21 @@ treset(void)
    631  	for (i = 0; i < 2; i++) {
    632  		tmoveto(0, 0);
    633  		tcursor(CURSOR_SAVE);
    634 -		tclearregion(0, 0, term.col-1, term.row-1);
    635 +		if (term.col > 0 && term.row > 0 && term.line > 0)
    636 +			tclearregion(0, 0, term.col-1, term.row-1);
    637  		tswapscreen();
    638  	}
    639 +	sb_clear();
    640 +	if (sel.ob.x != -1 && term.row > 0)
    641 +		selclear();
    642  }
    643  
    644 +
    645  void
    646  tnew(int col, int row)
    647  {
    648  	term = (Term){ .c = { .attr = { .fg = defaultfg, .bg = defaultbg } } };
    649 +	sb_init(scrollback_lines);
    650  	tresize(col, row);
    651  	treset();
    652  }
    653 @@ -1081,10 +1481,37 @@ void
    654  tscrollup(int orig, int n)
    655  {
    656  	int i;
    657 +	uint64_t newstart;
    658 +	uint64_t oldstart;
    659 +
    660 +	int attop;
    661  	Line temp;
    662  
    663 +	oldstart = sb_view_start();
    664  	LIMIT(n, 0, term.bot-orig+1);
    665  
    666 +	if (!IS_SET(MODE_ALTSCREEN) && orig == term.top) {
    667 +		/* At top of history only if history exists */
    668 +		attop = (sb.len != 0 && sb.view_offset == sb.len);
    669 +
    670 +		if (sb.view_offset > 0 && !attop)
    671 +			sb.view_offset += n;
    672 +
    673 +		for (i = 0; i < n; i++)
    674 +			sb_push(term.line[orig + i]);
    675 +
    676 +		/* if at the top, keep me there */
    677 +		if (attop)
    678 +			sb.view_offset = sb.len;
    679 +		/* otherwise clamp me */
    680 +		else if (sb.view_offset > sb.len)
    681 +			sb.view_offset = sb.len;
    682 +	}
    683 +
    684 +	newstart = sb_view_start();
    685 +	if (sb.view_offset > 0)
    686 +		selscrollback(oldstart - newstart);
    687 +
    688  	tclearregion(0, orig, term.col-1, orig+n-1);
    689  	tsetdirt(orig+n, term.bot);
    690  
    691 @@ -1100,6 +1527,8 @@ tscrollup(int orig, int n)
    692  void
    693  selscroll(int orig, int n)
    694  {
    695 +	if (sb.view_offset != 0)
    696 +		return;
    697  	if (sel.ob.x == -1 || sel.alt != IS_SET(MODE_ALTSCREEN))
    698  		return;
    699  
    700 @@ -1720,6 +2149,12 @@ csihandle(void)
    701  			break;
    702  		case 2: /* all */
    703  			tclearregion(0, 0, term.col-1, term.row-1);
    704 +			if (!IS_SET(MODE_ALTSCREEN))
    705 +				sb_reset_on_clear();
    706 +			break;
    707 +		case 3:
    708 +			if (!IS_SET(MODE_ALTSCREEN))
    709 +				sb_reset_on_clear();
    710  			break;
    711  		default:
    712  			goto unknown;
    713 @@ -2109,7 +2544,7 @@ tdumpline(int n)
    714  	const Glyph *bp, *end;
    715  
    716  	bp = &term.line[n][0];
    717 -	end = &bp[MIN(tlinelen(n), term.col) - 1];
    718 +	end = &bp[MIN(tlinelen_render(n), term.col) - 1];
    719  	if (bp != end || bp->u != ' ') {
    720  		for ( ; bp <= end; ++bp)
    721  			tprinter(buf, utf8encode(bp->u, buf));
    722 @@ -2166,6 +2601,36 @@ tdeftran(char ascii)
    723  	}
    724  }
    725  
    726 +static void
    727 +kscroll(const Arg *arg)
    728 +{
    729 +	uint64_t oldstart;
    730 +	uint64_t newstart;
    731 +
    732 +	oldstart = sb_view_start();
    733 +	sb.view_offset += arg->i;
    734 +	LIMIT(sb.view_offset, 0, sb.len);
    735 +	newstart = sb_view_start();
    736 +
    737 +	selscrollback(oldstart - newstart);
    738 +	redraw();
    739 +}
    740 +
    741 +void
    742 +kscrolldown(const Arg *arg)
    743 +{
    744 +	Arg a;
    745 +
    746 +	a.i = -arg->i;
    747 +	kscroll(&a);
    748 +}
    749 +
    750 +void
    751 +kscrollup(const Arg *arg)
    752 +{
    753 +	kscroll(arg);
    754 +}
    755 +
    756  void
    757  tdectest(char c)
    758  {
    759 @@ -2572,83 +3037,138 @@ twrite(const char *buf, int buflen, int show_ctrl)
    760  void
    761  tresize(int col, int row)
    762  {
    763 -	int i;
    764 +	int i, j;
    765 +	int min_limit;
    766  	int minrow = MIN(row, term.row);
    767 -	int mincol = MIN(col, term.col);
    768 -	int *bp;
    769 -	TCursor c;
    770 +	int old_row = term.row;
    771 +	int old_col = term.col;
    772 +	int save_end = 0; /* Track effective pushed height */
    773 +	int loaded = 0;
    774 +	int pop_width = 0;
    775 +	int needs_reflow = 0;
    776 +	int is_alt = IS_SET(MODE_ALTSCREEN);
    777 +	Line *tmp;
    778  
    779  	if (col < 1 || row < 1) {
    780  		fprintf(stderr,
    781 -		        "tresize: error resizing to %dx%d\n", col, row);
    782 +			"tresize: error resizing to %dx%d\n", col, row);
    783  		return;
    784  	}
    785  
    786 -	/*
    787 -	 * slide screen to keep cursor where we expect it -
    788 -	 * tscrollup would work here, but we can optimize to
    789 -	 * memmove because we're freeing the earlier lines
    790 -	 */
    791 -	for (i = 0; i <= term.c.y - row; i++) {
    792 -		free(term.line[i]);
    793 -		free(term.alt[i]);
    794 -	}
    795 -	/* ensure that both src and dst are not NULL */
    796 -	if (i > 0) {
    797 -		memmove(term.line, term.line + i, row * sizeof(Line));
    798 -		memmove(term.alt, term.alt + i, row * sizeof(Line));
    799 -	}
    800 -	for (i += row; i < term.row; i++) {
    801 -		free(term.line[i]);
    802 -		free(term.alt[i]);
    803 +	/* Operate on the currently visible screen buffer. */
    804 +	if (is_alt) {
    805 +		tmp = term.line;
    806 +		term.line = term.alt;
    807 +		term.alt = tmp;
    808  	}
    809  
    810 -	/* resize to new height */
    811 -	term.line = xrealloc(term.line, row * sizeof(Line));
    812 -	term.alt  = xrealloc(term.alt,  row * sizeof(Line));
    813 -	term.dirty = xrealloc(term.dirty, row * sizeof(*term.dirty));
    814 -	term.tabs = xrealloc(term.tabs, col * sizeof(*term.tabs));
    815 +	save_end = term.row;
    816 +	if (term.row != 0 && term.col != 0) {
    817 +		if (!is_alt && term.c.y > 0 && term.c.y < term.row) {
    818 +			term.line[term.c.y - 1][term.col - 1].mode &= ~ATTR_WRAP;
    819 +		}
    820 +		min_limit = is_alt ? 0 : term.c.y;
    821  
    822 -	/* resize each row to new width, zero-pad if needed */
    823 -	for (i = 0; i < minrow; i++) {
    824 -		term.line[i] = xrealloc(term.line[i], col * sizeof(Glyph));
    825 -		term.alt[i]  = xrealloc(term.alt[i],  col * sizeof(Glyph));
    826 -	}
    827 +		for (i = term.row - 1; i > min_limit; i--) {
    828 +			if (tlinelen(term.line[i]) > 0)
    829 +				break;
    830 +		}
    831 +		save_end = i + 1;
    832  
    833 -	/* allocate any new rows */
    834 -	for (/* i = minrow */; i < row; i++) {
    835 -		term.line[i] = xmalloc(col * sizeof(Glyph));
    836 -		term.alt[i] = xmalloc(col * sizeof(Glyph));
    837 +		for (i = 0; i < save_end; i++) {
    838 +			sb_push(term.line[i]);
    839 +		}
    840 +		/* Optimization: Only reflow if content doesn't fit in new width.
    841 +		 * This avoids expensive reflow operations when resizing doesn't
    842 +		 * affect line wrapping (e.g., when terminal is wide enough). */
    843 +		if (col > term.col) {
    844 +			/* Growing: We MUST reflow. Even if the text doesn't need
    845 +			 * un-wrapping, the history lines must be physically reallocated
    846 +			 * to the new width to prevent heap-buffer-overflows on read. */
    847 +			needs_reflow = 1;
    848 +		} else if (col < term.col) {
    849 +			/* Shrinking: Only reflow if content is wider than new width. */
    850 +			if (sb.max_width > col)
    851 +				needs_reflow = 1;
    852 +		}
    853 +		if (needs_reflow) {
    854 +			sb_resize(col);
    855 +		} else {
    856 +			/* If we don't reflow, we still need to reset the view
    857 +			 * because sb_pop_screen() might change the history length. */
    858 +			sb.view_offset = 0;
    859 +		}
    860  	}
    861 -	if (col > term.col) {
    862 -		bp = term.tabs + term.col;
    863  
    864 -		memset(bp, 0, sizeof(*term.tabs) * (col - term.col));
    865 -		while (--bp > term.tabs && !*bp)
    866 -			/* nothing */ ;
    867 -		for (bp += tabspaces; bp < term.tabs + col; bp += tabspaces)
    868 -			*bp = 1;
    869 -	}
    870 -	/* update terminal size */
    871 +		if (term.line) {
    872 +			for (i = 0; i < term.row; i++) {
    873 +				free(term.line[i]);
    874 +				free(term.alt[i]);
    875 +			}
    876 +			free(term.line);
    877 +			free(term.alt);
    878 +			free(term.dirty);
    879 +			free(term.tabs);
    880 +		}
    881 +
    882  	term.col = col;
    883  	term.row = row;
    884 -	/* reset scrolling region */
    885 -	tsetscroll(0, row-1);
    886 -	/* make use of the LIMIT in tmoveto */
    887 -	tmoveto(term.c.x, term.c.y);
    888 -	/* Clearing both screens (it makes dirty all lines) */
    889 -	c = term.c;
    890 -	for (i = 0; i < 2; i++) {
    891 -		if (mincol < col && 0 < minrow) {
    892 -			tclearregion(mincol, 0, col - 1, minrow - 1);
    893 -		}
    894 -		if (0 < col && minrow < row) {
    895 -			tclearregion(0, minrow, col - 1, row - 1);
    896 +
    897 +	term.line  = xmalloc(term.row * sizeof(Line));
    898 +	term.alt   = xmalloc(term.row * sizeof(Line));
    899 +	term.dirty = xmalloc(term.row * sizeof(int));
    900 +	term.tabs  = xmalloc(term.col * sizeof(*term.tabs));
    901 +
    902 +	for (i = 0; i < term.row; i++) {
    903 +		term.line[i] = xmalloc(term.col * sizeof(Glyph));
    904 +		term.alt[i]  = xmalloc(term.col * sizeof(Glyph));
    905 +		term.dirty[i] = 1;
    906 +
    907 +		for (j = 0; j < term.col; j++) {
    908 +			term.line[i][j] = term.c.attr;
    909 +			term.line[i][j].u = ' ';
    910 +			term.line[i][j].mode = 0;
    911 +
    912 +			term.alt[i][j] = term.c.attr;
    913 +			term.alt[i][j].u = ' ';
    914 +			term.alt[i][j].mode = 0;
    915  		}
    916 -		tswapscreen();
    917 -		tcursor(CURSOR_LOAD);
    918  	}
    919 -	term.c = c;
    920 +
    921 +	memset(term.tabs, 0, term.col * sizeof(*term.tabs));
    922 +	for (i = 8; i < term.col; i += 8)
    923 +		term.tabs[i] = 1;
    924 +
    925 +	tsetscroll(0, term.row - 1);
    926 +
    927 +	if (minrow > 0) {
    928 +		loaded = MIN(sb.len, term.row);
    929 +		pop_width = needs_reflow ? col : MIN(col, old_col);
    930 +		sb_pop_screen(loaded, pop_width);
    931 +	}
    932 +	if (is_alt) {
    933 +		tmp = term.line;
    934 +		term.line = term.alt;
    935 +		term.alt = tmp;
    936 +	}
    937 +	if (!is_alt && old_row > 0) {
    938 +		term.c.y += (loaded - save_end);
    939 +	}
    940 +	if (term.c.y >= term.row) {
    941 +		term.c.y = term.row - 1;
    942 +	}
    943 +	if (term.c.x >= term.col) {
    944 +		term.c.x = term.col - 1;
    945 +	}
    946 +	if (term.c.y < 0) {
    947 +		term.c.y = 0;
    948 +	}
    949 +	if (term.c.x < 0) {
    950 +		term.c.x = 0;
    951 +	}
    952 +
    953 +	tfulldirt();
    954 +	sb_view_changed();
    955  }
    956  
    957  void
    958 @@ -2662,12 +3182,13 @@ drawregion(int x1, int y1, int x2, int y2)
    959  {
    960  	int y;
    961  
    962 +	Line line;
    963  	for (y = y1; y < y2; y++) {
    964  		if (!term.dirty[y])
    965  			continue;
    966 -
    967  		term.dirty[y] = 0;
    968 -		xdrawline(term.line[y], x1, y, x2);
    969 +		line = renderline(y);
    970 +		xdrawline(line, x1, y, x2);
    971  	}
    972  }
    973  
    974 @@ -2688,10 +3209,12 @@ draw(void)
    975  		cx--;
    976  
    977  	drawregion(0, 0, term.col, term.row);
    978 -	xdrawcursor(cx, term.c.y, term.line[term.c.y][cx],
    979 -			term.ocx, term.ocy, term.line[term.ocy][term.ocx]);
    980 -	term.ocx = cx;
    981 -	term.ocy = term.c.y;
    982 +	if (sb.view_offset == 0) {
    983 +		xdrawcursor(cx, term.c.y, term.line[term.c.y][cx],
    984 +		            term.ocx, term.ocy, term.line[term.ocy][term.ocx]);
    985 +		term.ocx = cx;
    986 +		term.ocy = term.c.y;
    987 +	}
    988  	xfinishdraw();
    989  	if (ocx != term.ocx || ocy != term.ocy)
    990  		xximspot(term.ocx, term.ocy);
    991 diff --git a/st.h b/st.h
    992 index fd3b0d8..151d0c6 100644
    993 --- a/st.h
    994 +++ b/st.h
    995 @@ -86,6 +86,7 @@ void printsel(const Arg *);
    996  void sendbreak(const Arg *);
    997  void toggleprinter(const Arg *);
    998  
    999 +int tisaltscreen(void);
   1000  int tattrset(int);
   1001  void tnew(int, int);
   1002  void tresize(int, int);
   1003 @@ -111,6 +112,9 @@ void *xmalloc(size_t);
   1004  void *xrealloc(void *, size_t);
   1005  char *xstrdup(const char *);
   1006  
   1007 +void kscrollup(const Arg *arg);
   1008 +void kscrolldown(const Arg *arg);
   1009 +
   1010  /* config.h globals */
   1011  extern char *utmp;
   1012  extern char *scroll;
   1013 @@ -124,3 +128,4 @@ extern unsigned int tabspaces;
   1014  extern unsigned int defaultfg;
   1015  extern unsigned int defaultbg;
   1016  extern unsigned int defaultcs;
   1017 +extern unsigned int scrollback_lines;
   1018 diff --git a/x.c b/x.c
   1019 index d73152b..75f3db1 100644
   1020 --- a/x.c
   1021 +++ b/x.c
   1022 @@ -472,6 +472,23 @@ bpress(XEvent *e)
   1023  	struct timespec now;
   1024  	int snap;
   1025  
   1026 +	if (btn == Button4 || btn == Button5) {
   1027 +		Arg a;
   1028 +		if (IS_SET(MODE_MOUSE) && !(e->xbutton.state & forcemousemod)) {
   1029 +			mousereport(e);
   1030 +			return;
   1031 +		}
   1032 +		if (!tisaltscreen()) {
   1033 +			a.i = 1;
   1034 +			if (btn == Button4) {
   1035 +				kscrollup(&a);
   1036 +			} else {
   1037 +				kscrolldown(&a);
   1038 +			}
   1039 +		}
   1040 +		return;
   1041 +	}
   1042 +
   1043  	if (1 <= btn && btn <= 11)
   1044  		buttons |= 1 << (btn-1);
   1045  
   1046 -- 
   1047 2.53.0
   1048