sites

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

st-scrollback-reflow-standalone-extended-0.9.3.diff (32374B)


      1 From c7cf4aae956ea0db9253b6592749c578d1546ce5 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: scrollback reflow and selection across scrollback
      5 
      6 Reflow the scrollback buffer on resize instead of dropping it: wrapped
      7 lines are joined into logical lines and re-split to the new width.
      8 
      9 Keep the selection in logical rows spanning history and screen (0 is
     10 the oldest scrollback line, sb.len + y is screen row y) instead of in
     11 window coordinates. Lines keep their row when pushed into scrollback,
     12 so the selection now survives scrolling the view and new output, and
     13 can cover more than one screen. Only evicting the oldest line from the
     14 ring shifts it.
     15 
     16 - shift+click extends the current selection: the end nearest to the
     17   click follows it, keeping word/line snapping. Disabled in MODE_MOUSE,
     18   where shift forces selection instead.
     19 - scrolling the wheel while dragging keeps the selection end under the
     20   pointer.
     21 - typing while scrolled back no longer clears the selection.
     22 - the selection is cleared when the width changes (reflow) or the alt
     23   screen is resized, and kept on height-only resizes.
     24 ---
     25  config.def.h |   5 +
     26  st.c         | 840 ++++++++++++++++++++++++++++++++++++++++++++-------
     27  st.h         |   6 +
     28  x.c          |  46 ++-
     29  4 files changed, 780 insertions(+), 117 deletions(-)
     30 
     31 diff --git a/config.def.h b/config.def.h
     32 index fd2bd87..0e885f1 100644
     33 --- a/config.def.h
     34 +++ b/config.def.h
     35 @@ -472,3 +472,8 @@ static char ascii_printable[] =
     36  	" !\"#$%&'()*+,-./0123456789:;<=>?"
     37  	"@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_"
     38  	"`abcdefghijklmnopqrstuvwxyz{|}~";
     39 +
     40 +/*
     41 + * The amount of lines scrollback can hold before it wraps around.
     42 + */
     43 +unsigned int scrollback_lines = 5000;
     44 diff --git a/st.c b/st.c
     45 index c289072..4d79f20 100644
     46 --- a/st.c
     47 +++ b/st.c
     48 @@ -5,6 +5,7 @@
     49  #include <limits.h>
     50  #include <pwd.h>
     51  #include <stdarg.h>
     52 +#include <stdint.h>
     53  #include <stdio.h>
     54  #include <stdlib.h>
     55  #include <string.h>
     56 @@ -178,7 +179,7 @@ static void tdeletechar(int);
     57  static void tdeleteline(int);
     58  static void tinsertblank(int);
     59  static void tinsertblankline(int);
     60 -static int tlinelen(int);
     61 +static int tlinelen(Line);
     62  static void tmoveto(int, int);
     63  static void tmoveato(int, int);
     64  static void tnewline(int);
     65 @@ -207,6 +208,9 @@ static void drawregion(int, int, int, int);
     66  static void selnormalize(void);
     67  static void selscroll(int, int);
     68  static void selsnap(int *, int *, int);
     69 +static void selevict(void);
     70 +static void seldirt(int, int);
     71 +static int isselected(int, int);
     72  
     73  static size_t utf8decode(const char *, Rune *, size_t);
     74  static Rune utf8decodebyte(char, size_t *);
     75 @@ -232,6 +236,366 @@ static const uchar utfmask[UTF_SIZ + 1] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8};
     76  static const Rune utfmin[UTF_SIZ + 1] = {       0,    0,  0x80,  0x800,  0x10000};
     77  static const Rune utfmax[UTF_SIZ + 1] = {0x10FFFF, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF};
     78  
     79 +typedef struct
     80 +{
     81 +	Line *buf;       /* ring of Line pointers */
     82 +	int cap;         /* max number of lines */
     83 +	int len;         /* current number of valid lines (<= cap) */
     84 +	int head;        /* physical index of logical oldest (valid when len>0) */
     85 +	uint64_t base;   /* Can overflow in the extreme */
     86 +	/*
     87 +	 * max_width tracks the widest line ever pushed to scrollback.
     88 +	 * It may be conservative (stale) if that line has since been
     89 +	 * evicted from the ring buffer, which is acceptable - it just
     90 +	 * means we might reflow when not strictly necessary, which is
     91 +	 * better than skipping a needed reflow.
     92 +	 */
     93 +	int max_width;
     94 +	int view_offset; /* 0 means live screen */
     95 +} Scrollback;
     96 +
     97 +static Scrollback sb;
     98 +
     99 +static int
    100 +sb_phys_index(int logical_idx)
    101 +{
    102 +	/* logical_idx: 0..sb.len-1 (0 = oldest) */
    103 +	return (sb.head + logical_idx) % sb.cap;
    104 +}
    105 +
    106 +static Line
    107 +lineclone(Line src)
    108 +{
    109 +	Line dst;
    110 +
    111 +	if (!src)
    112 +		return NULL;
    113 +
    114 +	dst = xmalloc(term.col * sizeof(Glyph));
    115 +	memcpy(dst, src, term.col * sizeof(Glyph));
    116 +	return dst;
    117 +}
    118 +
    119 +static void
    120 +sb_init(int lines)
    121 +{
    122 +	int i;
    123 +
    124 +	sb.buf  = xmalloc(sizeof(Line) * lines);
    125 +	sb.cap  = lines;
    126 +	sb.len  = 0;
    127 +	sb.head = 0;
    128 +	sb.base = 0;
    129 +	for (i = 0; i < sb.cap; i++)
    130 +		sb.buf[i] = NULL;
    131 +
    132 +	sb.view_offset = 0;
    133 +	sb.max_width = 0;
    134 +}
    135 +
    136 +/* Push one screen line into scrollback.
    137 + * Overwrites oldest when full (ring buffer).
    138 + */
    139 +static void
    140 +sb_push(Line line)
    141 +{
    142 +	Line copy;
    143 +	int tail;
    144 +	int width;
    145 +
    146 +	if (sb.cap <= 0)
    147 +		return;
    148 +
    149 +	copy = lineclone(line);
    150 +
    151 +	if (sb.len < sb.cap) {
    152 +		tail = sb_phys_index(sb.len);
    153 +		sb.buf[tail] = copy;
    154 +		sb.len++;
    155 +	} else {
    156 +		/* We might've just evicted the widest line... */
    157 +		free(sb.buf[sb.head]);
    158 +		sb.buf[sb.head] = copy;
    159 +		sb.head = (sb.head + 1) % sb.cap;
    160 +		sb.base++;
    161 +		selevict();
    162 +	}
    163 +	width = tlinelen(copy);
    164 +	/* ...so max_width might be stale. */
    165 +	if (width > sb.max_width)
    166 +		sb.max_width = width;
    167 +}
    168 +
    169 +static Line
    170 +sb_get(int idx)
    171 +{
    172 +	/* idx is logical: 0..sb.len-1 */
    173 +	if (idx < 0 || idx >= sb.len)
    174 +		return NULL;
    175 +	return sb.buf[sb_phys_index(idx)];
    176 +}
    177 +
    178 +static void
    179 +sb_clear(void)
    180 +{
    181 +	int i;
    182 +	int p;
    183 +
    184 +	if (!sb.buf)
    185 +		return;
    186 +
    187 +	for (i = 0; i < sb.len; i++) {
    188 +		p = sb_phys_index(i);
    189 +		if (sb.buf[p]) {
    190 +			free(sb.buf[p]);
    191 +			sb.buf[p] = NULL;
    192 +		}
    193 +	}
    194 +
    195 +	sb.len = 0;
    196 +	sb.head = 0;
    197 +	sb.base = 0;
    198 +	sb.view_offset = 0;
    199 +	sb.max_width = 0;
    200 +}
    201 +
    202 +/*
    203 + * Reflows the scrollback buffer to fit a new terminal width.
    204 + *
    205 + * The algorithm works in three steps:
    206 + * 1) Unwrap: It iterates through the existing history, joining physical lines
    207 + * marked with ATTR_WRAP into a single continuous 'logical' line.
    208 + * 2) Reflow: It slices this logical line into new chunks of size 'col'.
    209 + * - New wrap flags are applied where the text exceeds the new width.
    210 + * - Trailing spaces are trimmed to prevent ghost padding.
    211 + * 3) Rebuild: The new lines are pushed into a fresh ring buffer.
    212 + * - Uses O(1) ring insertion (updating head/tail) to avoid expensive
    213 + * memmoves during resize, but it is still O(N) where N is the existing
    214 + * history.
    215 + *
    216 + * Note: During reflow we reset sb to match the rebuilt buffer
    217 + * (head, base and len might change).
    218 + */
    219 +static void
    220 +sb_resize(int col)
    221 +{
    222 +	Line *new_buf;
    223 +	int i, j;
    224 +	int new_len, logical_cap, logical_len, is_wrapped, cursor;
    225 +	int copy_width, tail, current_width;
    226 +	Line logical, line, nl;
    227 +	uint64_t new_base = 0;
    228 +	int new_head = 0;
    229 +	int new_max_width = 0;
    230 +	Glyph *g;
    231 +
    232 +	new_len = 0;
    233 +
    234 +	if (sb.len == 0)
    235 +		return;
    236 +
    237 +	new_buf = xmalloc(sizeof(Line) * sb.cap);
    238 +	for (i = 0; i < sb.cap; i++)
    239 +		new_buf[i] = NULL;
    240 +
    241 +	logical_cap = term.col * 2;
    242 +	logical = xmalloc(logical_cap * sizeof(Glyph));
    243 +	logical_len = 0;
    244 +
    245 +	for (i = 0; i < sb.len; i++) {
    246 +		/* Unwrap: Accumulate physical lines into one logical line. */
    247 +		line = sb_get(i);
    248 +		is_wrapped = (line[term.col - 1].mode & ATTR_WRAP);
    249 +		if (logical_len + term.col > logical_cap) {
    250 +			logical_cap *= 2;
    251 +			logical = xrealloc(logical, logical_cap * sizeof(Glyph));
    252 +		}
    253 +
    254 +		memcpy(logical + logical_len, line, term.col * sizeof(Glyph));
    255 +		for (j = 0; j < term.col; j++) {
    256 +			logical[logical_len + j].mode &= ~ATTR_WRAP;
    257 +		}
    258 +		logical_len += term.col;
    259 +		/* If the line was wrapped, continue accumulating before reflowing. */
    260 +		if (is_wrapped) {
    261 +			continue;
    262 +		}
    263 +		/* Trim trailing spaces from the fully unwrapped line. */
    264 +		while (logical_len > 0) {
    265 +			g = &logical[logical_len - 1];
    266 +			if (g->u == ' ' && g->bg == defaultbg
    267 +					&& (g->mode & ATTR_BOLD) == 0) {
    268 +				logical_len--;
    269 +			} else {
    270 +				break;
    271 +			}
    272 +		}
    273 +		if (logical_len == 0)
    274 +			logical_len = 1;
    275 +
    276 +		/* Reflow: Split the logical line into new chunks. */
    277 +		cursor = 0;
    278 +		while (cursor < logical_len) {
    279 +			nl = xmalloc(col * sizeof(Glyph));
    280 +			for (j = 0; j < col; j++) {
    281 +				nl[j].fg = defaultfg;
    282 +				nl[j].bg = defaultbg;
    283 +				nl[j].mode = 0;
    284 +				nl[j].u = ' ';
    285 +			}
    286 +
    287 +			copy_width = logical_len - cursor;
    288 +			if (copy_width > col)
    289 +				copy_width = col;
    290 +
    291 +			memcpy(nl, logical + cursor, copy_width * sizeof(Glyph));
    292 +
    293 +			for (j = 0; j < copy_width; j++) {
    294 +				nl[j].mode &= ~ATTR_WRAP;
    295 +			}
    296 +
    297 +			if (cursor + copy_width < logical_len) {
    298 +				nl[col - 1].mode |= ATTR_WRAP;
    299 +			} else {
    300 +				nl[col - 1].mode &= ~ATTR_WRAP;
    301 +			}
    302 +
    303 +			/* Rebuild: Push new lines into the ring buffer. */
    304 +			if (new_len < sb.cap) {
    305 +				tail = (new_head + new_len) % sb.cap;
    306 +				new_buf[tail] = nl;
    307 +				new_len++;
    308 +			} else {
    309 +				free(new_buf[new_head]);
    310 +				new_buf[new_head] = nl;
    311 +				new_head = (new_head + 1) % sb.cap;
    312 +				new_base++;
    313 +			}
    314 +			current_width = (cursor + copy_width < logical_len) ? col : copy_width;
    315 +			if (current_width > new_max_width)
    316 +				new_max_width = current_width;
    317 +			cursor += copy_width;
    318 +		}
    319 +		logical_len = 0;
    320 +	}
    321 +	free(logical);
    322 +	sb_clear();
    323 +	free(sb.buf);
    324 +	sb.buf = new_buf;
    325 +	sb.len = new_len;
    326 +	sb.head = new_head;
    327 +	sb.base = new_base;
    328 +	sb.view_offset = 0;
    329 +	sb.max_width = new_max_width;
    330 +}
    331 +
    332 +static void
    333 +sb_pop_screen(int loaded, int new_cols)
    334 +{
    335 +	int i, p;
    336 +	int start_logical;
    337 +	Line line;
    338 +
    339 +	loaded = MIN(loaded, sb.len);
    340 +	start_logical = sb.len - loaded;
    341 +	new_cols = MIN(new_cols, term.col);
    342 +	for (i = 0; i < loaded; i++) {
    343 +		p = sb_phys_index(start_logical + i);
    344 +		line = sb.buf[p];
    345 +
    346 +		memcpy(term.line[i], line, new_cols * sizeof(Glyph));
    347 +
    348 +		free(line);
    349 +		sb.buf[p] = NULL;
    350 +	}
    351 +
    352 +	sb.len -= loaded;
    353 +}
    354 +
    355 +static void
    356 +sb_view_changed(void)
    357 +{
    358 +	if (!term.dirty || term.row <= 0)
    359 +		return;
    360 +	tfulldirt();
    361 +}
    362 +
    363 +static Line
    364 +emptyline(void)
    365 +{
    366 +	static Line empty;
    367 +	static int empty_cols;
    368 +	int i = 0;
    369 +
    370 +	if (empty_cols != term.col) {
    371 +		free(empty);
    372 +		empty = xmalloc(term.col * sizeof(Glyph));
    373 +		empty_cols = term.col;
    374 +	}
    375 +
    376 +	for (i = 0; i < term.col; i++) {
    377 +		empty[i] = term.c.attr;
    378 +		empty[i].u = ' ';
    379 +		empty[i].mode = 0;
    380 +	}
    381 +	return empty;
    382 +}
    383 +
    384 +/*
    385 + * Logical rows address history and screen as one continuous space:
    386 + * 0..sb.len-1 are scrollback lines (0 = oldest) and sb.len + y is
    387 + * screen row y. A line keeps its logical row when it is pushed into
    388 + * scrollback; only evicting the oldest line shifts everything by one.
    389 + * The selection lives in this space, so it survives scrolling.
    390 + */
    391 +static Line
    392 +logicalline(int v)
    393 +{
    394 +	if (v < 0)
    395 +		return emptyline();
    396 +
    397 +	if (v < sb.len)
    398 +		return sb_get(v);
    399 +
    400 +	/* past scrollback -> into current screen */
    401 +	v -= sb.len;
    402 +	if (v < term.row)
    403 +		return term.line[v];
    404 +
    405 +	return emptyline();
    406 +}
    407 +
    408 +/* first logical row shown at the top of the window */
    409 +static int
    410 +viewtop(void)
    411 +{
    412 +	return sb.len - sb.view_offset; /* can be negative */
    413 +}
    414 +
    415 +static Line
    416 +renderline(int y)
    417 +{
    418 +	if (sb.view_offset <= 0)
    419 +		return term.line[y];
    420 +
    421 +	return logicalline(viewtop() + y);
    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 +768,68 @@ 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 +/* range of logical rows a selection can cover */
    465 +static int
    466 +selminrow(void)
    467 +{
    468 +	/* the alternate screen has no history */
    469 +	return sel.alt ? sb.len : 0;
    470 +}
    471 +
    472 +static int
    473 +selmaxrow(void)
    474 +{
    475 +	return sb.len + term.row - 1;
    476 +}
    477 +
    478 +/* mark the visible part of logical rows a..b dirty */
    479 +static void
    480 +seldirt(int a, int b)
    481 +{
    482 +	int top = viewtop();
    483 +
    484 +	a -= top;
    485 +	b -= top;
    486 +	if (b < 0 || a >= term.row)
    487 +		return;
    488 +	tsetdirt(MAX(a, 0), MIN(b, term.row - 1));
    489 +}
    490 +
    491 +/* the oldest scrollback line was dropped: shift the selection with it */
    492 +static void
    493 +selevict(void)
    494 +{
    495 +	if (sel.ob.x == -1 || sel.alt)
    496 +		return;
    497 +
    498 +	sel.ob.y--;
    499 +	sel.oe.y--;
    500 +	sel.nb.y--;
    501 +	sel.ne.y--;
    502 +
    503 +	if (sel.ne.y < 0) {
    504 +		selclear();
    505 +		return;
    506 +	}
    507 +	if (sel.nb.y < 0)
    508 +		sel.nb.x = sel.nb.y = 0;
    509 +	if (sel.ob.y < 0)
    510 +		sel.ob.x = sel.ob.y = 0;
    511 +	if (sel.oe.y < 0)
    512 +		sel.oe.x = sel.oe.y = 0;
    513 +}
    514 +
    515  void
    516  selstart(int col, int row, int snap)
    517  {
    518 @@ -427,12 +839,12 @@ selstart(int col, int row, int snap)
    519  	sel.alt = IS_SET(MODE_ALTSCREEN);
    520  	sel.snap = snap;
    521  	sel.oe.x = sel.ob.x = col;
    522 -	sel.oe.y = sel.ob.y = row;
    523 +	sel.oe.y = sel.ob.y = viewtop() + row;
    524  	selnormalize();
    525  
    526  	if (sel.snap != 0)
    527  		sel.mode = SEL_READY;
    528 -	tsetdirt(sel.nb.y, sel.ne.y);
    529 +	seldirt(sel.nb.y, sel.ne.y);
    530  }
    531  
    532  void
    533 @@ -454,16 +866,48 @@ selextend(int col, int row, int type, int done)
    534  	oldtype = sel.type;
    535  
    536  	sel.oe.x = col;
    537 -	sel.oe.y = row;
    538 +	sel.oe.y = viewtop() + row;
    539  	selnormalize();
    540  	sel.type = type;
    541  
    542  	if (oldey != sel.oe.y || oldex != sel.oe.x || oldtype != sel.type || sel.mode == SEL_EMPTY)
    543 -		tsetdirt(MIN(sel.nb.y, oldsby), MAX(sel.ne.y, oldsey));
    544 +		seldirt(MIN(sel.nb.y, oldsby), MAX(sel.ne.y, oldsey));
    545  
    546  	sel.mode = done ? SEL_IDLE : SEL_READY;
    547  }
    548  
    549 +/*
    550 + * Extend the existing selection to (col, row): the end closest to the
    551 + * click follows it, the other end stays put. The selection can then be
    552 + * dragged further like a fresh one. Returns 0 if there is nothing to
    553 + * extend.
    554 + */
    555 +int
    556 +selextendto(int col, int row, int type)
    557 +{
    558 +	long pos, dbeg, dend;
    559 +
    560 +	if (sel.ob.x == -1 || sel.mode != SEL_IDLE ||
    561 +			sel.alt != IS_SET(MODE_ALTSCREEN))
    562 +		return 0;
    563 +
    564 +	row += viewtop();
    565 +	pos = (long)row * term.col + col;
    566 +	dbeg = labs(pos - ((long)sel.nb.y * term.col + sel.nb.x));
    567 +	dend = labs(pos - ((long)sel.ne.y * term.col + sel.ne.x));
    568 +
    569 +	seldirt(sel.nb.y, sel.ne.y);
    570 +	sel.ob = (dbeg < dend) ? sel.ne : sel.nb;
    571 +	sel.oe.x = col;
    572 +	sel.oe.y = row;
    573 +	sel.type = type;
    574 +	selnormalize();
    575 +	seldirt(sel.nb.y, sel.ne.y);
    576 +	sel.mode = SEL_READY;
    577 +
    578 +	return 1;
    579 +}
    580 +
    581  void
    582  selnormalize(void)
    583  {
    584 @@ -485,15 +929,16 @@ selnormalize(void)
    585  	/* expand selection over line breaks */
    586  	if (sel.type == SEL_RECTANGULAR)
    587  		return;
    588 -	i = tlinelen(sel.nb.y);
    589 +	i = tlinelen(logicalline(sel.nb.y));
    590  	if (i < sel.nb.x)
    591  		sel.nb.x = i;
    592 -	if (tlinelen(sel.ne.y) <= sel.ne.x)
    593 +	if (tlinelen(logicalline(sel.ne.y)) <= sel.ne.x)
    594  		sel.ne.x = term.col - 1;
    595  }
    596  
    597 -int
    598 -selected(int x, int y)
    599 +/* x, y in logical rows */
    600 +static int
    601 +isselected(int x, int y)
    602  {
    603  	if (sel.mode == SEL_EMPTY || sel.ob.x == -1 ||
    604  			sel.alt != IS_SET(MODE_ALTSCREEN))
    605 @@ -508,12 +953,21 @@ selected(int x, int y)
    606  	    && (y != sel.ne.y || x <= sel.ne.x);
    607  }
    608  
    609 +/* x, y in window coordinates */
    610 +int
    611 +selected(int x, int y)
    612 +{
    613 +	return isselected(x, viewtop() + y);
    614 +}
    615 +
    616  void
    617  selsnap(int *x, int *y, int direction)
    618  {
    619  	int newx, newy, xt, yt;
    620  	int delim, prevdelim;
    621 +	int miny = selminrow(), maxy = selmaxrow();
    622  	const Glyph *gp, *prevgp;
    623 +	Line line;
    624  
    625  	switch (sel.snap) {
    626  	case SNAP_WORD:
    627 @@ -521,7 +975,7 @@ selsnap(int *x, int *y, int direction)
    628  		 * Snap around if the word wraps around at the end or
    629  		 * beginning of a line.
    630  		 */
    631 -		prevgp = &term.line[*y][*x];
    632 +		prevgp = &logicalline(*y)[*x];
    633  		prevdelim = ISDELIM(prevgp->u);
    634  		for (;;) {
    635  			newx = *x + direction;
    636 @@ -529,21 +983,23 @@ selsnap(int *x, int *y, int direction)
    637  			if (!BETWEEN(newx, 0, term.col - 1)) {
    638  				newy += direction;
    639  				newx = (newx + term.col) % term.col;
    640 -				if (!BETWEEN(newy, 0, term.row - 1))
    641 +				if (!BETWEEN(newy, miny, maxy))
    642  					break;
    643  
    644  				if (direction > 0)
    645  					yt = *y, xt = *x;
    646  				else
    647  					yt = newy, xt = newx;
    648 -				if (!(term.line[yt][xt].mode & ATTR_WRAP))
    649 +				line = logicalline(yt);
    650 +				if (!(line[xt].mode & ATTR_WRAP))
    651  					break;
    652  			}
    653  
    654 -			if (newx >= tlinelen(newy))
    655 +			line = logicalline(newy);
    656 +			if (newx >= tlinelen(line))
    657  				break;
    658  
    659 -			gp = &term.line[newy][newx];
    660 +			gp = &line[newx];
    661  			delim = ISDELIM(gp->u);
    662  			if (!(gp->mode & ATTR_WDUMMY) && (delim != prevdelim
    663  					|| (delim && gp->u != prevgp->u)))
    664 @@ -563,15 +1019,15 @@ selsnap(int *x, int *y, int direction)
    665  		 */
    666  		*x = (direction < 0) ? 0 : term.col - 1;
    667  		if (direction < 0) {
    668 -			for (; *y > 0; *y += direction) {
    669 -				if (!(term.line[*y-1][term.col-1].mode
    670 +			for (; *y > miny; *y += direction) {
    671 +				if (!(logicalline(*y-1)[term.col-1].mode
    672  						& ATTR_WRAP)) {
    673  					break;
    674  				}
    675  			}
    676  		} else if (direction > 0) {
    677 -			for (; *y < term.row-1; *y += direction) {
    678 -				if (!(term.line[*y][term.col-1].mode
    679 +			for (; *y < maxy; *y += direction) {
    680 +				if (!(logicalline(*y)[term.col-1].mode
    681  						& ATTR_WRAP)) {
    682  					break;
    683  				}
    684 @@ -585,8 +1041,9 @@ char *
    685  getsel(void)
    686  {
    687  	char *str, *ptr;
    688 -	int y, bufsize, lastx, linelen;
    689 +	int y, bufsize, lastx, linelen, end_idx, insert_newline, is_wrapped;
    690  	const Glyph *gp, *last;
    691 +	Line line;
    692  
    693  	if (sel.ob.x == -1)
    694  		return NULL;
    695 @@ -596,29 +1053,33 @@ getsel(void)
    696  
    697  	/* append every set & selected glyph to the selection */
    698  	for (y = sel.nb.y; y <= sel.ne.y; y++) {
    699 -		if ((linelen = tlinelen(y)) == 0) {
    700 +		line = logicalline(y);
    701 +		linelen = tlinelen(line);
    702 +
    703 +		if (linelen == 0) {
    704  			*ptr++ = '\n';
    705  			continue;
    706  		}
    707  
    708  		if (sel.type == SEL_RECTANGULAR) {
    709 -			gp = &term.line[y][sel.nb.x];
    710 +			gp = &line[sel.nb.x];
    711  			lastx = sel.ne.x;
    712  		} else {
    713 -			gp = &term.line[y][sel.nb.y == y ? sel.nb.x : 0];
    714 +			gp = &line[sel.nb.y == y ? sel.nb.x : 0];
    715  			lastx = (sel.ne.y == y) ? sel.ne.x : term.col-1;
    716  		}
    717 -		last = &term.line[y][MIN(lastx, linelen-1)];
    718 -		while (last >= gp && last->u == ' ')
    719 +		end_idx = MIN(lastx, linelen-1);
    720 +		is_wrapped = (line[end_idx].mode & ATTR_WRAP) != 0;
    721 +		last = &line[end_idx];
    722 +		while (last >= gp && last->u == ' ') {
    723  			--last;
    724 +		}
    725  
    726  		for ( ; gp <= last; ++gp) {
    727  			if (gp->mode & ATTR_WDUMMY)
    728  				continue;
    729 -
    730  			ptr += utf8encode(gp->u, ptr);
    731  		}
    732 -
    733  		/*
    734  		 * Copy and pasting of line endings is inconsistent
    735  		 * in the inconsistent terminal and GUI world.
    736 @@ -628,8 +1089,13 @@ getsel(void)
    737  		 * st.
    738  		 * FIXME: Fix the computer world.
    739  		 */
    740 +		insert_newline = 0;
    741  		if ((y < sel.ne.y || lastx >= linelen) &&
    742 -		    (!(last->mode & ATTR_WRAP) || sel.type == SEL_RECTANGULAR))
    743 +			(!is_wrapped || sel.type == SEL_RECTANGULAR)) {
    744 +			insert_newline = 1;
    745 +		}
    746 +
    747 +		if (insert_newline)
    748  			*ptr++ = '\n';
    749  	}
    750  	*ptr = 0;
    751 @@ -643,7 +1109,7 @@ selclear(void)
    752  		return;
    753  	sel.mode = SEL_IDLE;
    754  	sel.ob.x = -1;
    755 -	tsetdirt(sel.nb.y, sel.ne.y);
    756 +	seldirt(sel.nb.y, sel.ne.y);
    757  }
    758  
    759  void
    760 @@ -850,6 +1316,11 @@ ttywrite(const char *s, size_t n, int may_echo)
    761  {
    762  	const char *next;
    763  
    764 +	if (sb.view_offset > 0) {
    765 +		sb.view_offset = 0;
    766 +		sb_view_changed();
    767 +	}
    768 +
    769  	if (may_echo && IS_SET(MODE_ECHO))
    770  		twrite(s, n, 1);
    771  
    772 @@ -970,9 +1441,8 @@ tsetdirt(int top, int bot)
    773  {
    774  	int i;
    775  
    776 -	if (term.row <= 0)
    777 +	if (term.row < 1)
    778  		return;
    779 -
    780  	LIMIT(top, 0, term.row-1);
    781  	LIMIT(bot, 0, term.row-1);
    782  
    783 @@ -1038,15 +1508,21 @@ treset(void)
    784  	for (i = 0; i < 2; i++) {
    785  		tmoveto(0, 0);
    786  		tcursor(CURSOR_SAVE);
    787 -		tclearregion(0, 0, term.col-1, term.row-1);
    788 +		if (term.col > 0 && term.row > 0 && term.line)
    789 +			tclearregion(0, 0, term.col-1, term.row-1);
    790  		tswapscreen();
    791  	}
    792 +	sb_clear();
    793 +	if (sel.ob.x != -1 && term.row > 0)
    794 +		selclear();
    795  }
    796  
    797 +
    798  void
    799  tnew(int col, int row)
    800  {
    801  	term = (Term){ .c = { .attr = { .fg = defaultfg, .bg = defaultbg } } };
    802 +	sb_init(scrollback_lines);
    803  	tresize(col, row);
    804  	treset();
    805  }
    806 @@ -1086,10 +1562,54 @@ void
    807  tscrollup(int orig, int n)
    808  {
    809  	int i;
    810 +	int attop;
    811 +	int pushed = 0;
    812  	Line temp;
    813  
    814  	LIMIT(n, 0, term.bot-orig+1);
    815  
    816 +	if (!IS_SET(MODE_ALTSCREEN) && orig == term.top) {
    817 +		/*
    818 +		 * Pushed lines keep their logical rows, but rows outside
    819 +		 * of a partial scroll region move along with the screen.
    820 +		 */
    821 +		if ((term.top > 0 || term.bot < term.row-1) &&
    822 +				sel.ob.x != -1 && !sel.alt && sel.ne.y >= sb.len)
    823 +			selclear();
    824 +
    825 +		/* At top of history only if history exists */
    826 +		attop = (sb.len != 0 && sb.view_offset == sb.len);
    827 +
    828 +		if (sb.view_offset > 0 && !attop)
    829 +			sb.view_offset += n;
    830 +
    831 +		for (i = 0; i < n; i++)
    832 +			sb_push(term.line[orig + i]);
    833 +		pushed = sb.cap > 0;
    834 +
    835 +		/* if at the top, keep me there */
    836 +		if (attop)
    837 +			sb.view_offset = sb.len;
    838 +		/* otherwise clamp me */
    839 +		else if (sb.view_offset > sb.len)
    840 +			sb.view_offset = sb.len;
    841 +	}
    842 +
    843 +	if (pushed) {
    844 +		/*
    845 +		 * The lines went to history, so the selection stays valid.
    846 +		 * Rotate before clearing, so clearing doesn't drop it.
    847 +		 */
    848 +		tsetdirt(orig, term.bot);
    849 +		for (i = orig; i <= term.bot-n; i++) {
    850 +			temp = term.line[i];
    851 +			term.line[i] = term.line[i+n];
    852 +			term.line[i+n] = temp;
    853 +		}
    854 +		tclearregion(0, term.bot-n+1, term.col-1, term.bot);
    855 +		return;
    856 +	}
    857 +
    858  	tclearregion(0, orig, term.col-1, orig+n-1);
    859  	tsetdirt(orig+n, term.bot);
    860  
    861 @@ -1105,16 +1625,23 @@ tscrollup(int orig, int n)
    862  void
    863  selscroll(int orig, int n)
    864  {
    865 +	int top, bot;
    866 +
    867  	if (sel.ob.x == -1 || sel.alt != IS_SET(MODE_ALTSCREEN))
    868  		return;
    869  
    870 -	if (BETWEEN(sel.nb.y, orig, term.bot) != BETWEEN(sel.ne.y, orig, term.bot)) {
    871 +	/* the scrolled screen region, in logical rows */
    872 +	orig += sb.len;
    873 +	top = sb.len + term.top;
    874 +	bot = sb.len + term.bot;
    875 +
    876 +	if (BETWEEN(sel.nb.y, orig, bot) != BETWEEN(sel.ne.y, orig, bot)) {
    877  		selclear();
    878 -	} else if (BETWEEN(sel.nb.y, orig, term.bot)) {
    879 +	} else if (BETWEEN(sel.nb.y, orig, bot)) {
    880  		sel.ob.y += n;
    881  		sel.oe.y += n;
    882 -		if (sel.ob.y < term.top || sel.ob.y > term.bot ||
    883 -		    sel.oe.y < term.top || sel.oe.y > term.bot) {
    884 +		if (sel.ob.y < top || sel.ob.y > bot ||
    885 +		    sel.oe.y < top || sel.oe.y > bot) {
    886  			selclear();
    887  		} else {
    888  			selnormalize();
    889 @@ -1250,7 +1777,7 @@ tclearregion(int x1, int y1, int x2, int y2)
    890  		term.dirty[y] = 1;
    891  		for (x = x1; x <= x2; x++) {
    892  			gp = &term.line[y][x];
    893 -			if (selected(x, y))
    894 +			if (isselected(x, sb.len + y))
    895  				selclear();
    896  			gp->fg = term.c.attr.fg;
    897  			gp->bg = term.c.attr.bg;
    898 @@ -1727,6 +2254,12 @@ csihandle(void)
    899  			break;
    900  		case 2: /* all */
    901  			tclearregion(0, 0, term.col-1, term.row-1);
    902 +			if (!IS_SET(MODE_ALTSCREEN))
    903 +				sb_reset_on_clear();
    904 +			break;
    905 +		case 3:
    906 +			if (!IS_SET(MODE_ALTSCREEN))
    907 +				sb_reset_on_clear();
    908  			break;
    909  		default:
    910  			goto unknown;
    911 @@ -2116,7 +2649,7 @@ tdumpline(int n)
    912  	const Glyph *bp, *end;
    913  
    914  	bp = &term.line[n][0];
    915 -	end = &bp[MIN(tlinelen(n), term.col) - 1];
    916 +	end = &bp[MIN(tlinelen(term.line[n]), term.col) - 1];
    917  	if (bp != end || bp->u != ' ') {
    918  		for ( ; bp <= end; ++bp)
    919  			tprinter(buf, utf8encode(bp->u, buf));
    920 @@ -2173,6 +2706,29 @@ tdeftran(char ascii)
    921  	}
    922  }
    923  
    924 +static void
    925 +kscroll(const Arg *arg)
    926 +{
    927 +	sb.view_offset += arg->i;
    928 +	LIMIT(sb.view_offset, 0, sb.len);
    929 +	redraw();
    930 +}
    931 +
    932 +void
    933 +kscrolldown(const Arg *arg)
    934 +{
    935 +	Arg a;
    936 +
    937 +	a.i = -arg->i;
    938 +	kscroll(&a);
    939 +}
    940 +
    941 +void
    942 +kscrollup(const Arg *arg)
    943 +{
    944 +	kscroll(arg);
    945 +}
    946 +
    947  void
    948  tdectest(char c)
    949  {
    950 @@ -2500,7 +3056,7 @@ check_control_code:
    951  		 */
    952  		return;
    953  	}
    954 -	if (selected(term.c.x, term.c.y))
    955 +	if (isselected(term.c.x, sb.len + term.c.y))
    956  		selclear();
    957  
    958  	gp = &term.line[term.c.y][term.c.x];
    959 @@ -2579,83 +3135,146 @@ twrite(const char *buf, int buflen, int show_ctrl)
    960  void
    961  tresize(int col, int row)
    962  {
    963 -	int i;
    964 +	int i, j;
    965 +	int min_limit;
    966  	int minrow = MIN(row, term.row);
    967 -	int mincol = MIN(col, term.col);
    968 -	int *bp;
    969 -	TCursor c;
    970 +	int old_row = term.row;
    971 +	int old_col = term.col;
    972 +	int save_end = 0; /* Track effective pushed height */
    973 +	int loaded = 0;
    974 +	int pop_width = 0;
    975 +	int needs_reflow = 0;
    976 +	int is_alt = IS_SET(MODE_ALTSCREEN);
    977 +	Line *tmp;
    978  
    979  	if (col < 1 || row < 1) {
    980  		fprintf(stderr,
    981 -		        "tresize: error resizing to %dx%d\n", col, row);
    982 +			"tresize: error resizing to %dx%d\n", col, row);
    983  		return;
    984  	}
    985  
    986  	/*
    987 -	 * slide screen to keep cursor where we expect it -
    988 -	 * tscrollup would work here, but we can optimize to
    989 -	 * memmove because we're freeing the earlier lines
    990 +	 * Reflow and truncation move text around, and the alt screen
    991 +	 * is rebuilt from scratch. A pure height change keeps logical
    992 +	 * rows intact (lines go through scrollback and back).
    993  	 */
    994 -	for (i = 0; i <= term.c.y - row; i++) {
    995 -		free(term.line[i]);
    996 -		free(term.alt[i]);
    997 -	}
    998 -	/* ensure that both src and dst are not NULL */
    999 -	if (i > 0) {
   1000 -		memmove(term.line, term.line + i, row * sizeof(Line));
   1001 -		memmove(term.alt, term.alt + i, row * sizeof(Line));
   1002 -	}
   1003 -	for (i += row; i < term.row; i++) {
   1004 -		free(term.line[i]);
   1005 -		free(term.alt[i]);
   1006 +	if (col != old_col || sel.alt)
   1007 +		selclear();
   1008 +
   1009 +	/* Operate on the currently visible screen buffer. */
   1010 +	if (is_alt) {
   1011 +		tmp = term.line;
   1012 +		term.line = term.alt;
   1013 +		term.alt = tmp;
   1014  	}
   1015  
   1016 -	/* resize to new height */
   1017 -	term.line = xrealloc(term.line, row * sizeof(Line));
   1018 -	term.alt  = xrealloc(term.alt,  row * sizeof(Line));
   1019 -	term.dirty = xrealloc(term.dirty, row * sizeof(*term.dirty));
   1020 -	term.tabs = xrealloc(term.tabs, col * sizeof(*term.tabs));
   1021 +	save_end = term.row;
   1022 +	if (term.row != 0 && term.col != 0) {
   1023 +		if (!is_alt && term.c.y > 0 && term.c.y < term.row) {
   1024 +			term.line[term.c.y - 1][term.col - 1].mode &= ~ATTR_WRAP;
   1025 +		}
   1026 +		min_limit = is_alt ? 0 : term.c.y;
   1027  
   1028 -	/* resize each row to new width, zero-pad if needed */
   1029 -	for (i = 0; i < minrow; i++) {
   1030 -		term.line[i] = xrealloc(term.line[i], col * sizeof(Glyph));
   1031 -		term.alt[i]  = xrealloc(term.alt[i],  col * sizeof(Glyph));
   1032 -	}
   1033 +		for (i = term.row - 1; i > min_limit; i--) {
   1034 +			if (tlinelen(term.line[i]) > 0)
   1035 +				break;
   1036 +		}
   1037 +		save_end = i + 1;
   1038  
   1039 -	/* allocate any new rows */
   1040 -	for (/* i = minrow */; i < row; i++) {
   1041 -		term.line[i] = xmalloc(col * sizeof(Glyph));
   1042 -		term.alt[i] = xmalloc(col * sizeof(Glyph));
   1043 +		for (i = 0; i < save_end; i++) {
   1044 +			sb_push(term.line[i]);
   1045 +		}
   1046 +		/* Optimization: Only reflow if content doesn't fit in new width.
   1047 +		 * This avoids expensive reflow operations when resizing doesn't
   1048 +		 * affect line wrapping (e.g., when terminal is wide enough). */
   1049 +		if (col > term.col) {
   1050 +			/* Growing: We MUST reflow. Even if the text doesn't need
   1051 +			 * un-wrapping, the history lines must be physically reallocated
   1052 +			 * to the new width to prevent heap-buffer-overflows on read. */
   1053 +			needs_reflow = 1;
   1054 +		} else if (col < term.col) {
   1055 +			/* Shrinking: Only reflow if content is wider than new width. */
   1056 +			if (sb.max_width > col)
   1057 +				needs_reflow = 1;
   1058 +		}
   1059 +		if (needs_reflow) {
   1060 +			sb_resize(col);
   1061 +		} else {
   1062 +			/* If we don't reflow, we still need to reset the view
   1063 +			 * because sb_pop_screen() might change the history length. */
   1064 +			sb.view_offset = 0;
   1065 +		}
   1066  	}
   1067 -	if (col > term.col) {
   1068 -		bp = term.tabs + term.col;
   1069  
   1070 -		memset(bp, 0, sizeof(*term.tabs) * (col - term.col));
   1071 -		while (--bp > term.tabs && !*bp)
   1072 -			/* nothing */ ;
   1073 -		for (bp += tabspaces; bp < term.tabs + col; bp += tabspaces)
   1074 -			*bp = 1;
   1075 -	}
   1076 -	/* update terminal size */
   1077 +		if (term.line) {
   1078 +			for (i = 0; i < term.row; i++) {
   1079 +				free(term.line[i]);
   1080 +				free(term.alt[i]);
   1081 +			}
   1082 +			free(term.line);
   1083 +			free(term.alt);
   1084 +			free(term.dirty);
   1085 +			free(term.tabs);
   1086 +		}
   1087 +
   1088  	term.col = col;
   1089  	term.row = row;
   1090 -	/* reset scrolling region */
   1091 -	tsetscroll(0, row-1);
   1092 -	/* make use of the LIMIT in tmoveto */
   1093 -	tmoveto(term.c.x, term.c.y);
   1094 -	/* Clearing both screens (it makes dirty all lines) */
   1095 -	c = term.c;
   1096 -	for (i = 0; i < 2; i++) {
   1097 -		if (mincol < col && 0 < minrow) {
   1098 -			tclearregion(mincol, 0, col - 1, minrow - 1);
   1099 -		}
   1100 -		if (0 < col && minrow < row) {
   1101 -			tclearregion(0, minrow, col - 1, row - 1);
   1102 +
   1103 +	term.line  = xmalloc(term.row * sizeof(Line));
   1104 +	term.alt   = xmalloc(term.row * sizeof(Line));
   1105 +	term.dirty = xmalloc(term.row * sizeof(int));
   1106 +	term.tabs  = xmalloc(term.col * sizeof(*term.tabs));
   1107 +
   1108 +	for (i = 0; i < term.row; i++) {
   1109 +		term.line[i] = xmalloc(term.col * sizeof(Glyph));
   1110 +		term.alt[i]  = xmalloc(term.col * sizeof(Glyph));
   1111 +		term.dirty[i] = 1;
   1112 +
   1113 +		for (j = 0; j < term.col; j++) {
   1114 +			term.line[i][j] = term.c.attr;
   1115 +			term.line[i][j].u = ' ';
   1116 +			term.line[i][j].mode = 0;
   1117 +
   1118 +			term.alt[i][j] = term.c.attr;
   1119 +			term.alt[i][j].u = ' ';
   1120 +			term.alt[i][j].mode = 0;
   1121  		}
   1122 -		tswapscreen();
   1123 -		tcursor(CURSOR_LOAD);
   1124  	}
   1125 -	term.c = c;
   1126 +
   1127 +	memset(term.tabs, 0, term.col * sizeof(*term.tabs));
   1128 +	for (i = 8; i < term.col; i += 8)
   1129 +		term.tabs[i] = 1;
   1130 +
   1131 +	tsetscroll(0, term.row - 1);
   1132 +
   1133 +	if (minrow > 0) {
   1134 +		loaded = MIN(sb.len, term.row);
   1135 +		pop_width = needs_reflow ? col : MIN(col, old_col);
   1136 +		sb_pop_screen(loaded, pop_width);
   1137 +	}
   1138 +	if (is_alt) {
   1139 +		tmp = term.line;
   1140 +		term.line = term.alt;
   1141 +		term.alt = tmp;
   1142 +	}
   1143 +	if (!is_alt && old_row > 0) {
   1144 +		term.c.y += (loaded - save_end);
   1145 +	}
   1146 +	if (term.c.y >= term.row) {
   1147 +		term.c.y = term.row - 1;
   1148 +	}
   1149 +	if (term.c.x >= term.col) {
   1150 +		term.c.x = term.col - 1;
   1151 +	}
   1152 +	if (term.c.y < 0) {
   1153 +		term.c.y = 0;
   1154 +	}
   1155 +	if (term.c.x < 0) {
   1156 +		term.c.x = 0;
   1157 +	}
   1158 +
   1159 +	tfulldirt();
   1160 +	sb_view_changed();
   1161  }
   1162  
   1163  void
   1164 @@ -2669,12 +3288,13 @@ drawregion(int x1, int y1, int x2, int y2)
   1165  {
   1166  	int y;
   1167  
   1168 +	Line line;
   1169  	for (y = y1; y < y2; y++) {
   1170  		if (!term.dirty[y])
   1171  			continue;
   1172 -
   1173  		term.dirty[y] = 0;
   1174 -		xdrawline(term.line[y], x1, y, x2);
   1175 +		line = renderline(y);
   1176 +		xdrawline(line, x1, y, x2);
   1177  	}
   1178  }
   1179  
   1180 @@ -2695,10 +3315,12 @@ draw(void)
   1181  		cx--;
   1182  
   1183  	drawregion(0, 0, term.col, term.row);
   1184 -	xdrawcursor(cx, term.c.y, term.line[term.c.y][cx],
   1185 -			term.ocx, term.ocy, term.line[term.ocy][term.ocx]);
   1186 -	term.ocx = cx;
   1187 -	term.ocy = term.c.y;
   1188 +	if (sb.view_offset == 0) {
   1189 +		xdrawcursor(cx, term.c.y, term.line[term.c.y][cx],
   1190 +		            term.ocx, term.ocy, term.line[term.ocy][term.ocx]);
   1191 +		term.ocx = cx;
   1192 +		term.ocy = term.c.y;
   1193 +	}
   1194  	xfinishdraw();
   1195  	if (ocx != term.ocx || ocy != term.ocy)
   1196  		xximspot(term.ocx, term.ocy);
   1197 diff --git a/st.h b/st.h
   1198 index fd3b0d8..66f1436 100644
   1199 --- a/st.h
   1200 +++ b/st.h
   1201 @@ -86,6 +86,7 @@ void printsel(const Arg *);
   1202  void sendbreak(const Arg *);
   1203  void toggleprinter(const Arg *);
   1204  
   1205 +int tisaltscreen(void);
   1206  int tattrset(int);
   1207  void tnew(int, int);
   1208  void tresize(int, int);
   1209 @@ -102,6 +103,7 @@ void selclear(void);
   1210  void selinit(void);
   1211  void selstart(int, int, int);
   1212  void selextend(int, int, int, int);
   1213 +int selextendto(int, int, int);
   1214  int selected(int, int);
   1215  char *getsel(void);
   1216  
   1217 @@ -111,6 +113,9 @@ void *xmalloc(size_t);
   1218  void *xrealloc(void *, size_t);
   1219  char *xstrdup(const char *);
   1220  
   1221 +void kscrollup(const Arg *arg);
   1222 +void kscrolldown(const Arg *arg);
   1223 +
   1224  /* config.h globals */
   1225  extern char *utmp;
   1226  extern char *scroll;
   1227 @@ -124,3 +129,4 @@ extern unsigned int tabspaces;
   1228  extern unsigned int defaultfg;
   1229  extern unsigned int defaultbg;
   1230  extern unsigned int defaultcs;
   1231 +extern unsigned int scrollback_lines;
   1232 diff --git a/x.c b/x.c
   1233 index 76f572d..8091e6f 100644
   1234 --- a/x.c
   1235 +++ b/x.c
   1236 @@ -181,6 +181,7 @@ static void selnotify(XEvent *);
   1237  static void selclear_(XEvent *);
   1238  static void selrequest(XEvent *);
   1239  static void setsel(char *, Time);
   1240 +static int mouseseltype(XEvent *);
   1241  static void mousesel(XEvent *, int);
   1242  static void mousereport(XEvent *);
   1243  static char *kmap(KeySym, uint);
   1244 @@ -344,19 +345,23 @@ evrow(XEvent *e)
   1245  	return y / win.ch;
   1246  }
   1247  
   1248 -void
   1249 -mousesel(XEvent *e, int done)
   1250 +int
   1251 +mouseseltype(XEvent *e)
   1252  {
   1253 -	int type, seltype = SEL_REGULAR;
   1254 +	int type;
   1255  	uint state = e->xbutton.state & ~(Button1Mask | forcemousemod);
   1256  
   1257  	for (type = 1; type < LEN(selmasks); ++type) {
   1258 -		if (match(selmasks[type], state)) {
   1259 -			seltype = type;
   1260 -			break;
   1261 -		}
   1262 +		if (match(selmasks[type], state))
   1263 +			return type;
   1264  	}
   1265 -	selextend(evcol(e), evrow(e), seltype, done);
   1266 +	return SEL_REGULAR;
   1267 +}
   1268 +
   1269 +void
   1270 +mousesel(XEvent *e, int done)
   1271 +{
   1272 +	selextend(evcol(e), evrow(e), mouseseltype(e), done);
   1273  	if (done)
   1274  		setsel(getsel(), e->xbutton.time);
   1275  }
   1276 @@ -472,6 +477,26 @@ bpress(XEvent *e)
   1277  	struct timespec now;
   1278  	int snap;
   1279  
   1280 +	if (btn == Button4 || btn == Button5) {
   1281 +		Arg a;
   1282 +		if (IS_SET(MODE_MOUSE) && !(e->xbutton.state & forcemousemod)) {
   1283 +			mousereport(e);
   1284 +			return;
   1285 +		}
   1286 +		if (!tisaltscreen()) {
   1287 +			a.i = 1;
   1288 +			if (btn == Button4) {
   1289 +				kscrollup(&a);
   1290 +			} else {
   1291 +				kscrolldown(&a);
   1292 +			}
   1293 +			/* keep a selection being dragged under the pointer */
   1294 +			if (buttons & 1)
   1295 +				mousesel(e, 0);
   1296 +		}
   1297 +		return;
   1298 +	}
   1299 +
   1300  	if (1 <= btn && btn <= 11)
   1301  		buttons |= 1 << (btn-1);
   1302  
   1303 @@ -484,6 +509,11 @@ bpress(XEvent *e)
   1304  		return;
   1305  
   1306  	if (btn == Button1) {
   1307 +		/* shift+click extends the current selection */
   1308 +		if ((e->xbutton.state & ShiftMask) && !IS_SET(MODE_MOUSE) &&
   1309 +		    selextendto(evcol(e), evrow(e), mouseseltype(e)))
   1310 +			return;
   1311 +
   1312  		/*
   1313  		 * If the user clicks below predefined timeouts specific
   1314  		 * snapping behaviour is exposed.
   1315 -- 
   1316 2.56.0
   1317