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