sites

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

st-scrollback-reflow-standalone-0.9.3.diff (12664B)


      1 From 79d001e06447448e1e0bcde47494196e61809cfa 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         | 335 ++++++++++++++++++++++++++++++++++++++++++++++++---
     24  st.h         |   5 +
     25  x.c          |  17 +++
     26  4 files changed, 344 insertions(+), 18 deletions(-)
     27 
     28 diff --git a/config.def.h b/config.def.h
     29 index fd2bd87..0e885f1 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 c289072..dc30082 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 @@ -232,6 +233,202 @@ static const uchar utfmask[UTF_SIZ + 1] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8};
     54  static const Rune utfmin[UTF_SIZ + 1] = {       0,    0,  0x80,  0x800,  0x10000};
     55  static const Rune utfmax[UTF_SIZ + 1] = {0x10FFFF, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF};
     56  
     57 +typedef struct {
     58 +	Line *buf;  /* ring of Line pointers */
     59 +	int cap;    /* max number of lines */
     60 +	int len;    /* current number of valid lines (<= cap) */
     61 +	int head;   /* physical index of logical oldest (valid when len>0) */
     62 +	uint64_t base;
     63 +} Scrollback;
     64 +
     65 +static Scrollback sb;
     66 +static int view_offset;
     67 +
     68 +static int
     69 +sb_phys_index(int logical_idx)
     70 +{
     71 +	/* logical_idx: 0..sb.len-1 (0 = oldest) */
     72 +	return (sb.head + logical_idx) % sb.cap;
     73 +}
     74 +
     75 +static Line
     76 +lineclone(Line src)
     77 +{
     78 +	Line dst = xmalloc(term.col * sizeof(Glyph));
     79 +	memcpy(dst, src, term.col * sizeof(Glyph));
     80 +	return dst;
     81 +}
     82 +
     83 +static void
     84 +sb_init(int lines)
     85 +{
     86 +	int i;
     87 +
     88 +	sb.buf  = xmalloc(sizeof(Line) * lines);
     89 +	sb.cap  = lines;
     90 +	sb.len  = 0;
     91 +	sb.head = 0;
     92 +	sb.base = 0;
     93 +	for (i = 0; i < sb.cap; i++)
     94 +		sb.buf[i] = NULL;
     95 +
     96 +	view_offset = 0;
     97 +}
     98 +
     99 +/* Push one screen line into scrollback.
    100 + * Overwrites oldest when full (ring buffer).
    101 + */
    102 +static void
    103 +sb_push(Line line)
    104 +{
    105 +	Line copy;
    106 +	int tail;
    107 +
    108 +	if (sb.cap <= 0)
    109 +		return;
    110 +
    111 +	copy = lineclone(line);
    112 +
    113 +	if (sb.len < sb.cap) {
    114 +		tail = sb_phys_index(sb.len);
    115 +		sb.buf[tail] = copy;
    116 +		sb.len++;
    117 +	} else {
    118 +		free(sb.buf[sb.head]);
    119 +		sb.buf[sb.head] = copy;
    120 +		sb.head = (sb.head + 1) % sb.cap;
    121 +		sb.base++;
    122 +	}
    123 +}
    124 +
    125 +static Line
    126 +sb_get(int idx)
    127 +{
    128 +	/* idx is logical: 0..sb.len-1 */
    129 +	if (idx < 0 || idx >= sb.len)
    130 +		return NULL;
    131 +	return sb.buf[sb_phys_index(idx)];
    132 +}
    133 +
    134 +static void
    135 +sb_clear(void)
    136 +{
    137 +	int i;
    138 +	int p;
    139 +
    140 +	if (!sb.buf)
    141 +		return;
    142 +
    143 +	for (i = 0; i < sb.len; i++) {
    144 +		p = sb_phys_index(i);
    145 +		if (sb.buf[p]) {
    146 +			free(sb.buf[p]);
    147 +			sb.buf[p] = NULL;
    148 +		}
    149 +	}
    150 +
    151 +	sb.len = 0;
    152 +	sb.head = 0;
    153 +	sb.base = 0;
    154 +	view_offset = 0;
    155 +	if (sel.ob.x != -1 && term.row > 0)
    156 +		selclear();
    157 +}
    158 +
    159 +static uint64_t
    160 +sb_view_start(void)
    161 +{
    162 +	return sb.base + sb.len - view_offset;
    163 +}
    164 +
    165 +static void
    166 +sb_view_changed(void)
    167 +{
    168 +	if (!term.dirty || term.row <= 0)
    169 +		return;
    170 +	tfulldirt();
    171 +}
    172 +
    173 +static void
    174 +selscrollback(int delta)
    175 +{
    176 +	if (delta == 0)
    177 +		return;
    178 +
    179 +	if (sel.ob.x == -1)
    180 +		return;
    181 +
    182 +	if (sel.alt != IS_SET(MODE_ALTSCREEN))
    183 +		return;
    184 +
    185 +	sel.nb.y += delta;
    186 +	sel.ne.y += delta;
    187 +	sel.ob.y += delta;
    188 +	sel.oe.y += delta;
    189 +
    190 +	/* the selection is in window coordinates: drop it once it leaves */
    191 +	if (sel.nb.y < 0 || sel.ne.y >= term.row)
    192 +		selclear();
    193 +
    194 +	sb_view_changed();
    195 +}
    196 +
    197 +static Line
    198 +emptyline(void)
    199 +{
    200 +	static Line empty;
    201 +	static int empty_cols;
    202 +	int i = 0;
    203 +
    204 +	if (empty_cols != term.col) {
    205 +		free(empty);
    206 +		empty = xmalloc(term.col * sizeof(Glyph));
    207 +		empty_cols = term.col;
    208 +	}
    209 +
    210 +	for (i = 0; i < term.col; i++) {
    211 +		empty[i] = term.c.attr;
    212 +		empty[i].u = ' ';
    213 +		empty[i].mode = 0;
    214 +	}
    215 +	return empty;
    216 +}
    217 +
    218 +static Line
    219 +getlineforrender(int y)
    220 +{
    221 +	int start, v;
    222 +
    223 +	start = sb.len - view_offset; /* can be negative */
    224 +	v = start + y;
    225 +
    226 +	if (v < 0)
    227 +		return emptyline();
    228 +
    229 +	if (v < sb.len)
    230 +		return sb_get(v);
    231 +
    232 +	/* past scrollback -> into current screen */
    233 +	v -= sb.len;
    234 +	if (v >= 0 && v < term.row)
    235 +		return term.line[v];
    236 +
    237 +	return emptyline();
    238 +}
    239 +
    240 +static void
    241 +sb_reset_on_clear(void)
    242 +{
    243 +	sb_clear();
    244 +	sb_view_changed();
    245 +}
    246 +
    247 +int
    248 +tisaltscreen(void)
    249 +{
    250 +	return IS_SET(MODE_ALTSCREEN);
    251 +}
    252 +
    253  ssize_t
    254  xwrite(int fd, const char *s, size_t len)
    255  {
    256 @@ -418,6 +615,27 @@ tlinelen(int y)
    257  	return i;
    258  }
    259  
    260 +static int
    261 +tlinelen_render(int y)
    262 +{
    263 +	int i;
    264 +	Line line;
    265 +
    266 +	if (term.col <= 0)
    267 +		return 0;
    268 +
    269 +	i = term.col;
    270 +	line = getlineforrender(y);
    271 +
    272 +	if (line[i - 1].mode & ATTR_WRAP)
    273 +		return i;
    274 +
    275 +	while (i > 0 && line[i - 1].u == ' ')
    276 +		--i;
    277 +
    278 +	return i;
    279 +}
    280 +
    281  void
    282  selstart(int col, int row, int snap)
    283  {
    284 @@ -485,10 +703,10 @@ selnormalize(void)
    285  	/* expand selection over line breaks */
    286  	if (sel.type == SEL_RECTANGULAR)
    287  		return;
    288 -	i = tlinelen(sel.nb.y);
    289 +	i = tlinelen_render(sel.nb.y);
    290  	if (i < sel.nb.x)
    291  		sel.nb.x = i;
    292 -	if (tlinelen(sel.ne.y) <= sel.ne.x)
    293 +	if (tlinelen_render(sel.ne.y) <= sel.ne.x)
    294  		sel.ne.x = term.col - 1;
    295  }
    296  
    297 @@ -514,6 +732,7 @@ selsnap(int *x, int *y, int direction)
    298  	int newx, newy, xt, yt;
    299  	int delim, prevdelim;
    300  	const Glyph *gp, *prevgp;
    301 +	Line line;
    302  
    303  	switch (sel.snap) {
    304  	case SNAP_WORD:
    305 @@ -521,7 +740,7 @@ selsnap(int *x, int *y, int direction)
    306  		 * Snap around if the word wraps around at the end or
    307  		 * beginning of a line.
    308  		 */
    309 -		prevgp = &term.line[*y][*x];
    310 +		prevgp = &getlineforrender(*y)[*x];
    311  		prevdelim = ISDELIM(prevgp->u);
    312  		for (;;) {
    313  			newx = *x + direction;
    314 @@ -536,14 +755,15 @@ selsnap(int *x, int *y, int direction)
    315  					yt = *y, xt = *x;
    316  				else
    317  					yt = newy, xt = newx;
    318 -				if (!(term.line[yt][xt].mode & ATTR_WRAP))
    319 +				line = getlineforrender(yt);
    320 +				if (!(line[xt].mode & ATTR_WRAP))
    321  					break;
    322  			}
    323  
    324 -			if (newx >= tlinelen(newy))
    325 +			if (newx >= tlinelen_render(newy))
    326  				break;
    327  
    328 -			gp = &term.line[newy][newx];
    329 +			gp = &getlineforrender(newy)[newx];
    330  			delim = ISDELIM(gp->u);
    331  			if (!(gp->mode & ATTR_WDUMMY) && (delim != prevdelim
    332  					|| (delim && gp->u != prevgp->u)))
    333 @@ -564,14 +784,14 @@ selsnap(int *x, int *y, int direction)
    334  		*x = (direction < 0) ? 0 : term.col - 1;
    335  		if (direction < 0) {
    336  			for (; *y > 0; *y += direction) {
    337 -				if (!(term.line[*y-1][term.col-1].mode
    338 +				if (!(getlineforrender(*y-1)[term.col-1].mode
    339  						& ATTR_WRAP)) {
    340  					break;
    341  				}
    342  			}
    343  		} else if (direction > 0) {
    344  			for (; *y < term.row-1; *y += direction) {
    345 -				if (!(term.line[*y][term.col-1].mode
    346 +				if (!(getlineforrender(*y)[term.col-1].mode
    347  						& ATTR_WRAP)) {
    348  					break;
    349  				}
    350 @@ -596,19 +816,20 @@ getsel(void)
    351  
    352  	/* append every set & selected glyph to the selection */
    353  	for (y = sel.nb.y; y <= sel.ne.y; y++) {
    354 -		if ((linelen = tlinelen(y)) == 0) {
    355 +		Line line = getlineforrender(y);
    356 +		if ((linelen = tlinelen_render(y)) == 0) {
    357  			*ptr++ = '\n';
    358  			continue;
    359  		}
    360  
    361  		if (sel.type == SEL_RECTANGULAR) {
    362 -			gp = &term.line[y][sel.nb.x];
    363 +			gp = &line[sel.nb.x];
    364  			lastx = sel.ne.x;
    365  		} else {
    366 -			gp = &term.line[y][sel.nb.y == y ? sel.nb.x : 0];
    367 +			gp = &line[sel.nb.y == y ? sel.nb.x : 0];
    368  			lastx = (sel.ne.y == y) ? sel.ne.x : term.col-1;
    369  		}
    370 -		last = &term.line[y][MIN(lastx, linelen-1)];
    371 +		last = &line[MIN(lastx, linelen-1)];
    372  		while (last >= gp && last->u == ' ')
    373  			--last;
    374  
    375 @@ -848,6 +1069,12 @@ ttyread(void)
    376  void
    377  ttywrite(const char *s, size_t n, int may_echo)
    378  {
    379 +	if (view_offset > 0) {
    380 +		selclear();
    381 +		view_offset = 0;
    382 +		sb_view_changed();
    383 +	}
    384 +
    385  	const char *next;
    386  
    387  	if (may_echo && IS_SET(MODE_ECHO))
    388 @@ -1041,12 +1268,14 @@ treset(void)
    389  		tclearregion(0, 0, term.col-1, term.row-1);
    390  		tswapscreen();
    391  	}
    392 +	sb_clear();
    393  }
    394  
    395  void
    396  tnew(int col, int row)
    397  {
    398  	term = (Term){ .c = { .attr = { .fg = defaultfg, .bg = defaultbg } } };
    399 +	sb_init(scrollback_lines);
    400  	tresize(col, row);
    401  	treset();
    402  }
    403 @@ -1086,10 +1315,36 @@ void
    404  tscrollup(int orig, int n)
    405  {
    406  	int i;
    407 +	uint64_t newstart;
    408 +	uint64_t oldstart;
    409 +
    410  	Line temp;
    411  
    412 +	oldstart = sb_view_start();
    413  	LIMIT(n, 0, term.bot-orig+1);
    414  
    415 +	if (!IS_SET(MODE_ALTSCREEN) && orig == term.top) {
    416 +		int attop;
    417 +
    418 +		/* At top of history only if history exists */
    419 +		attop = (sb.len != 0 && view_offset == sb.len);
    420 +
    421 +		if (view_offset > 0 && !attop)
    422 +			view_offset += n;
    423 +
    424 +		for (i = 0; i < n; i++)
    425 +			sb_push(term.line[orig + i]);
    426 +
    427 +		if (attop)
    428 +			view_offset = sb.len;
    429 +		else if (view_offset > sb.len)
    430 +			view_offset = sb.len;
    431 +	}
    432 +
    433 +	newstart = sb_view_start();
    434 +	if (view_offset > 0)
    435 +		selscrollback(oldstart - newstart);
    436 +
    437  	tclearregion(0, orig, term.col-1, orig+n-1);
    438  	tsetdirt(orig+n, term.bot);
    439  
    440 @@ -1105,6 +1360,8 @@ tscrollup(int orig, int n)
    441  void
    442  selscroll(int orig, int n)
    443  {
    444 +	if (view_offset != 0)
    445 +		return;
    446  	if (sel.ob.x == -1 || sel.alt != IS_SET(MODE_ALTSCREEN))
    447  		return;
    448  
    449 @@ -1727,6 +1984,12 @@ csihandle(void)
    450  			break;
    451  		case 2: /* all */
    452  			tclearregion(0, 0, term.col-1, term.row-1);
    453 +			if (!IS_SET(MODE_ALTSCREEN))
    454 +				sb_reset_on_clear();
    455 +			break;
    456 +		case 3:
    457 +			if (!IS_SET(MODE_ALTSCREEN))
    458 +				sb_reset_on_clear();
    459  			break;
    460  		default:
    461  			goto unknown;
    462 @@ -2173,6 +2436,36 @@ tdeftran(char ascii)
    463  	}
    464  }
    465  
    466 +static void
    467 +kscroll(const Arg *arg)
    468 +{
    469 +	uint64_t oldstart;
    470 +	uint64_t newstart; 
    471 +
    472 +	oldstart = sb_view_start();
    473 +	view_offset += arg->i;
    474 +	LIMIT(view_offset, 0, sb.len);
    475 +	newstart = sb_view_start();
    476 +
    477 +	selscrollback(oldstart - newstart);
    478 +	redraw();
    479 +}
    480 +
    481 +void
    482 +kscrolldown(const Arg *arg)
    483 +{
    484 +	Arg a;
    485 +
    486 +	a.i = -arg->i;
    487 +	kscroll(&a);
    488 +}
    489 +
    490 +void
    491 +kscrollup(const Arg *arg)
    492 +{
    493 +	kscroll(arg);
    494 +}
    495 +
    496  void
    497  tdectest(char c)
    498  {
    499 @@ -2585,6 +2878,9 @@ tresize(int col, int row)
    500  	int *bp;
    501  	TCursor c;
    502  
    503 +	sb_clear();
    504 +	sb_view_changed();
    505 +
    506  	if (col < 1 || row < 1) {
    507  		fprintf(stderr,
    508  		        "tresize: error resizing to %dx%d\n", col, row);
    509 @@ -2669,12 +2965,13 @@ drawregion(int x1, int y1, int x2, int y2)
    510  {
    511  	int y;
    512  
    513 +	Line line;
    514  	for (y = y1; y < y2; y++) {
    515  		if (!term.dirty[y])
    516  			continue;
    517 -
    518  		term.dirty[y] = 0;
    519 -		xdrawline(term.line[y], x1, y, x2);
    520 +		line = getlineforrender(y);
    521 +		xdrawline(line, x1, y, x2);
    522  	}
    523  }
    524  
    525 @@ -2695,10 +2992,12 @@ draw(void)
    526  		cx--;
    527  
    528  	drawregion(0, 0, term.col, term.row);
    529 -	xdrawcursor(cx, term.c.y, term.line[term.c.y][cx],
    530 -			term.ocx, term.ocy, term.line[term.ocy][term.ocx]);
    531 -	term.ocx = cx;
    532 -	term.ocy = term.c.y;
    533 +	if (view_offset == 0) {
    534 +		xdrawcursor(cx, term.c.y, term.line[term.c.y][cx],
    535 +		            term.ocx, term.ocy, term.line[term.ocy][term.ocx]);
    536 +		term.ocx = cx;
    537 +		term.ocy = term.c.y;
    538 +	}
    539  	xfinishdraw();
    540  	if (ocx != term.ocx || ocy != term.ocy)
    541  		xximspot(term.ocx, term.ocy);
    542 diff --git a/st.h b/st.h
    543 index fd3b0d8..151d0c6 100644
    544 --- a/st.h
    545 +++ b/st.h
    546 @@ -86,6 +86,7 @@ void printsel(const Arg *);
    547  void sendbreak(const Arg *);
    548  void toggleprinter(const Arg *);
    549  
    550 +int tisaltscreen(void);
    551  int tattrset(int);
    552  void tnew(int, int);
    553  void tresize(int, int);
    554 @@ -111,6 +112,9 @@ void *xmalloc(size_t);
    555  void *xrealloc(void *, size_t);
    556  char *xstrdup(const char *);
    557  
    558 +void kscrollup(const Arg *arg);
    559 +void kscrolldown(const Arg *arg);
    560 +
    561  /* config.h globals */
    562  extern char *utmp;
    563  extern char *scroll;
    564 @@ -124,3 +128,4 @@ extern unsigned int tabspaces;
    565  extern unsigned int defaultfg;
    566  extern unsigned int defaultbg;
    567  extern unsigned int defaultcs;
    568 +extern unsigned int scrollback_lines;
    569 diff --git a/x.c b/x.c
    570 index 76f572d..dfe416a 100644
    571 --- a/x.c
    572 +++ b/x.c
    573 @@ -472,6 +472,23 @@ bpress(XEvent *e)
    574  	struct timespec now;
    575  	int snap;
    576  
    577 +	if (btn == Button4 || btn == Button5) {
    578 +		Arg a;
    579 +		if (IS_SET(MODE_MOUSE) && !(e->xbutton.state & forcemousemod)) {
    580 +			mousereport(e);
    581 +			return;
    582 +		}
    583 +		if (!tisaltscreen()) {
    584 +			a.i = 1;
    585 +			if (btn == Button4) {
    586 +				kscrollup(&a);
    587 +			} else {
    588 +				kscrolldown(&a);
    589 +			}
    590 +		}
    591 +		return;
    592 +	}
    593 +
    594  	if (1 <= btn && btn <= 11)
    595  		buttons |= 1 << (btn-1);
    596  
    597 -- 
    598 2.56.0
    599