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