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