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