From 6d7584cc382fd24e5da92baf3ac333b5dda12f3c Mon Sep 17 00:00:00 2001 From: Milos Nikic Date: Thu, 8 Jan 2026 22:04:25 -0800 Subject: [PATCH] st: alternative scrollback using ring buffer and view offset Implement scrollback as a fixed-size ring buffer and render history by offsetting the view instead of copying screen contents. Implement reflow of history and screen content on resizeif it is needed. Tradeoffs / differences: - Scrollback is disabled on the alternate screen - Simpler model than the existing scrollback patch set - Mouse wheel scrolling enabled by default - When using vim, mouse movement will no longer move the cursor. - There can be visual artifacts if width of the window is shrank to the size smaller than the shell promp. --- config.def.h | 5 + st.c | 710 ++++++++++++++++++++++++++++++++++++++++++++------- st.h | 5 + x.c | 17 ++ 4 files changed, 644 insertions(+), 93 deletions(-) diff --git a/config.def.h b/config.def.h index 2cd740a..a0b14e9 100644 --- a/config.def.h +++ b/config.def.h @@ -472,3 +472,8 @@ static char ascii_printable[] = " !\"#$%&'()*+,-./0123456789:;<=>?" "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" "`abcdefghijklmnopqrstuvwxyz{|}~"; + +/* + * The amount of lines scrollback can hold before it wraps around. + */ +unsigned int scrollback_lines = 5000; diff --git a/st.c b/st.c index e55e7b3..3f6b4a8 100644 --- a/st.c +++ b/st.c @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -178,7 +179,7 @@ static void tdeletechar(int); static void tdeleteline(int); static void tinsertblank(int); static void tinsertblankline(int); -static int tlinelen(int); +static int tlinelen(Line); static void tmoveto(int, int); static void tmoveato(int, int); static void tnewline(int); @@ -232,6 +233,379 @@ static const uchar utfmask[UTF_SIZ + 1] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8}; static const Rune utfmin[UTF_SIZ + 1] = { 0, 0, 0x80, 0x800, 0x10000}; static const Rune utfmax[UTF_SIZ + 1] = {0x10FFFF, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF}; +typedef struct +{ + Line *buf; /* ring of Line pointers */ + int cap; /* max number of lines */ + int len; /* current number of valid lines (<= cap) */ + int head; /* physical index of logical oldest (valid when len>0) */ + uint64_t base; /* Can overflow in the extreme */ + /* + * max_width tracks the widest line ever pushed to scrollback. + * It may be conservative (stale) if that line has since been + * evicted from the ring buffer, which is acceptable - it just + * means we might reflow when not strictly necessary, which is + * better than skipping a needed reflow. + */ + int max_width; + int view_offset; /* 0 means live screen */ +} Scrollback; + +static Scrollback sb; + +static int +sb_phys_index(int logical_idx) +{ + /* logical_idx: 0..sb.len-1 (0 = oldest) */ + return (sb.head + logical_idx) % sb.cap; +} + +static Line +lineclone(Line src) +{ + Line dst; + + if (!src) + return NULL; + + dst = xmalloc(term.col * sizeof(Glyph)); + memcpy(dst, src, term.col * sizeof(Glyph)); + return dst; +} + +static void +sb_init(int lines) +{ + int i; + + sb.buf = xmalloc(sizeof(Line) * lines); + sb.cap = lines; + sb.len = 0; + sb.head = 0; + sb.base = 0; + for (i = 0; i < sb.cap; i++) + sb.buf[i] = NULL; + + sb.view_offset = 0; + sb.max_width = 0; +} + +/* Push one screen line into scrollback. + * Overwrites oldest when full (ring buffer). + */ +static void +sb_push(Line line) +{ + Line copy; + int tail; + int width; + + if (sb.cap <= 0) + return; + + copy = lineclone(line); + + if (sb.len < sb.cap) { + tail = sb_phys_index(sb.len); + sb.buf[tail] = copy; + sb.len++; + } else { + /* We might've just evicted the widest line... */ + free(sb.buf[sb.head]); + sb.buf[sb.head] = copy; + sb.head = (sb.head + 1) % sb.cap; + sb.base++; + } + width = tlinelen(copy); + /* ...so max_width might be stale. */ + if (width > sb.max_width) + sb.max_width = width; +} + +static Line +sb_get(int idx) +{ + /* idx is logical: 0..sb.len-1 */ + if (idx < 0 || idx >= sb.len) + return NULL; + return sb.buf[sb_phys_index(idx)]; +} + +static void +sb_clear(void) +{ + int i; + int p; + + if (!sb.buf) + return; + + for (i = 0; i < sb.len; i++) { + p = sb_phys_index(i); + if (sb.buf[p]) { + free(sb.buf[p]); + sb.buf[p] = NULL; + } + } + + sb.len = 0; + sb.head = 0; + sb.base = 0; + sb.view_offset = 0; + sb.max_width = 0; +} + +/* + * Reflows the scrollback buffer to fit a new terminal width. + * + * The algorithm works in three steps: + * 1) Unwrap: It iterates through the existing history, joining physical lines + * marked with ATTR_WRAP into a single continuous 'logical' line. + * 2) Reflow: It slices this logical line into new chunks of size 'col'. + * - New wrap flags are applied where the text exceeds the new width. + * - Trailing spaces are trimmed to prevent ghost padding. + * 3) Rebuild: The new lines are pushed into a fresh ring buffer. + * - Uses O(1) ring insertion (updating head/tail) to avoid expensive + * memmoves during resize, but it is still O(N) where N is the existing + * history. + * + * Note: During reflow we reset sb to match the rebuilt buffer + * (head, base and len might change). + */ +static void +sb_resize(int col) +{ + Line *new_buf; + int i, j; + int new_len, logical_cap, logical_len, is_wrapped, cursor; + int copy_width, tail, current_width; + Line logical, line, nl; + uint64_t new_base = 0; + int new_head = 0; + int new_max_width = 0; + Glyph *g; + + new_len = 0; + + if (sb.len == 0) + return; + + new_buf = xmalloc(sizeof(Line) * sb.cap); + for (i = 0; i < sb.cap; i++) + new_buf[i] = NULL; + + logical_cap = term.col * 2; + logical = xmalloc(logical_cap * sizeof(Glyph)); + logical_len = 0; + + for (i = 0; i < sb.len; i++) { + /* Unwrap: Accumulate physical lines into one logical line. */ + line = sb_get(i); + is_wrapped = (line[term.col - 1].mode & ATTR_WRAP); + if (logical_len + term.col > logical_cap) { + logical_cap *= 2; + logical = xrealloc(logical, logical_cap * sizeof(Glyph)); + } + + memcpy(logical + logical_len, line, term.col * sizeof(Glyph)); + for (j = 0; j < term.col; j++) { + logical[logical_len + j].mode &= ~ATTR_WRAP; + } + logical_len += term.col; + /* If the line was wrapped, continue accumulating before reflowing. */ + if (is_wrapped) { + continue; + } + /* Trim trailing spaces from the fully unwrapped line. */ + while (logical_len > 0) { + g = &logical[logical_len - 1]; + if (g->u == ' ' && g->bg == defaultbg + && (g->mode & ATTR_BOLD) == 0) { + logical_len--; + } else { + break; + } + } + if (logical_len == 0) + logical_len = 1; + + /* Reflow: Split the logical line into new chunks. */ + cursor = 0; + while (cursor < logical_len) { + nl = xmalloc(col * sizeof(Glyph)); + for (j = 0; j < col; j++) { + nl[j].fg = defaultfg; + nl[j].bg = defaultbg; + nl[j].mode = 0; + nl[j].u = ' '; + } + + copy_width = logical_len - cursor; + if (copy_width > col) + copy_width = col; + + memcpy(nl, logical + cursor, copy_width * sizeof(Glyph)); + + for (j = 0; j < copy_width; j++) { + nl[j].mode &= ~ATTR_WRAP; + } + + if (cursor + copy_width < logical_len) { + nl[col - 1].mode |= ATTR_WRAP; + } else { + nl[col - 1].mode &= ~ATTR_WRAP; + } + + /* Rebuild: Push new lines into the ring buffer. */ + if (new_len < sb.cap) { + tail = (new_head + new_len) % sb.cap; + new_buf[tail] = nl; + new_len++; + } else { + free(new_buf[new_head]); + new_buf[new_head] = nl; + new_head = (new_head + 1) % sb.cap; + new_base++; + } + current_width = (cursor + copy_width < logical_len) ? col : copy_width; + if (current_width > new_max_width) + new_max_width = current_width; + cursor += copy_width; + } + logical_len = 0; + } + free(logical); + sb_clear(); + free(sb.buf); + sb.buf = new_buf; + sb.len = new_len; + sb.head = new_head; + sb.base = new_base; + sb.view_offset = 0; + sb.max_width = new_max_width; +} + +static void +sb_pop_screen(int loaded, int new_cols) +{ + int i, p; + int start_logical; + Line line; + + loaded = MIN(loaded, sb.len); + start_logical = sb.len - loaded; + new_cols = MIN(new_cols, term.col); + for (i = 0; i < loaded; i++) { + p = sb_phys_index(start_logical + i); + line = sb.buf[p]; + + memcpy(term.line[i], line, new_cols * sizeof(Glyph)); + + free(line); + sb.buf[p] = NULL; + } + + sb.len -= loaded; +} + +static uint64_t +sb_view_start(void) +{ + return sb.base + sb.len - sb.view_offset; +} + +static void +sb_view_changed(void) +{ + if (!term.dirty || term.row <= 0) + return; + tfulldirt(); +} + +static void +selscrollback(int delta) +{ + if (delta == 0) + return; + + if (sel.ob.x == -1 || sel.mode == SEL_EMPTY) + return; + + if (sel.alt != IS_SET(MODE_ALTSCREEN)) + return; + + sel.nb.y += delta; + sel.ne.y += delta; + sel.ob.y += delta; + sel.oe.y += delta; + + if (sel.ne.y < 0 || sel.nb.y >= term.row) + selclear(); + + sb_view_changed(); +} + +static Line +emptyline(void) +{ + static Line empty; + static int empty_cols; + int i = 0; + + if (empty_cols != term.col) { + free(empty); + empty = xmalloc(term.col * sizeof(Glyph)); + empty_cols = term.col; + } + + for (i = 0; i < term.col; i++) { + empty[i] = term.c.attr; + empty[i].u = ' '; + empty[i].mode = 0; + } + return empty; +} + +static Line +renderline(int y) +{ + int start, v; + + if (sb.view_offset <= 0) + return term.line[y]; + + start = sb.len - sb.view_offset; /* can be negative */ + v = start + y; + + if (v < 0) + return emptyline(); + + if (v < sb.len) + return sb_get(v); + + /* past scrollback -> into current screen */ + v -= sb.len; + if (v >= 0 && v < term.row) + return term.line[v]; + + return emptyline(); +} + +static void +sb_reset_on_clear(void) +{ + sb_clear(); + sb_view_changed(); + if (sel.ob.x != -1 && term.row > 0) + selclear(); +} + +int +tisaltscreen(void) +{ + return IS_SET(MODE_ALTSCREEN); +} + ssize_t xwrite(int fd, const char *s, size_t len) { @@ -404,20 +778,23 @@ selinit(void) sel.ob.x = -1; } -int -tlinelen(int y) +static int +tlinelen(Line line) { int i = term.col; - - if (term.line[y][i - 1].mode & ATTR_WRAP) + if (line[i - 1].mode & ATTR_WRAP) return i; - - while (i > 0 && term.line[y][i - 1].u == ' ') + while (i > 0 && line[i - 1].u == ' ') --i; - return i; } +static int +tlinelen_render(int y) +{ + return tlinelen(renderline(y)); +} + void selstart(int col, int row, int snap) { @@ -485,10 +862,10 @@ selnormalize(void) /* expand selection over line breaks */ if (sel.type == SEL_RECTANGULAR) return; - i = tlinelen(sel.nb.y); + i = tlinelen_render(sel.nb.y); if (i < sel.nb.x) sel.nb.x = i; - if (tlinelen(sel.ne.y) <= sel.ne.x) + if (tlinelen_render(sel.ne.y) <= sel.ne.x) sel.ne.x = term.col - 1; } @@ -514,6 +891,7 @@ selsnap(int *x, int *y, int direction) int newx, newy, xt, yt; int delim, prevdelim; const Glyph *gp, *prevgp; + Line line; switch (sel.snap) { case SNAP_WORD: @@ -521,7 +899,7 @@ selsnap(int *x, int *y, int direction) * Snap around if the word wraps around at the end or * beginning of a line. */ - prevgp = &term.line[*y][*x]; + prevgp = &renderline(*y)[*x]; prevdelim = ISDELIM(prevgp->u); for (;;) { newx = *x + direction; @@ -536,14 +914,15 @@ selsnap(int *x, int *y, int direction) yt = *y, xt = *x; else yt = newy, xt = newx; - if (!(term.line[yt][xt].mode & ATTR_WRAP)) + line = renderline(yt); + if (!(line[xt].mode & ATTR_WRAP)) break; } - if (newx >= tlinelen(newy)) + if (newx >= tlinelen_render(newy)) break; - gp = &term.line[newy][newx]; + gp = &renderline(newy)[newx]; delim = ISDELIM(gp->u); if (!(gp->mode & ATTR_WDUMMY) && (delim != prevdelim || (delim && gp->u != prevgp->u))) @@ -564,14 +943,14 @@ selsnap(int *x, int *y, int direction) *x = (direction < 0) ? 0 : term.col - 1; if (direction < 0) { for (; *y > 0; *y += direction) { - if (!(term.line[*y-1][term.col-1].mode + if (!(renderline(*y-1)[term.col-1].mode & ATTR_WRAP)) { break; } } } else if (direction > 0) { for (; *y < term.row-1; *y += direction) { - if (!(term.line[*y][term.col-1].mode + if (!(renderline(*y)[term.col-1].mode & ATTR_WRAP)) { break; } @@ -585,8 +964,9 @@ char * getsel(void) { char *str, *ptr; - int y, bufsize, lastx, linelen; + int y, bufsize, lastx, linelen, end_idx, insert_newline, is_wrapped; const Glyph *gp, *last; + Line line; if (sel.ob.x == -1) return NULL; @@ -596,29 +976,33 @@ getsel(void) /* append every set & selected glyph to the selection */ for (y = sel.nb.y; y <= sel.ne.y; y++) { - if ((linelen = tlinelen(y)) == 0) { + line = renderline(y); + linelen = tlinelen_render(y); + + if (linelen == 0) { *ptr++ = '\n'; continue; } if (sel.type == SEL_RECTANGULAR) { - gp = &term.line[y][sel.nb.x]; + gp = &line[sel.nb.x]; lastx = sel.ne.x; } else { - gp = &term.line[y][sel.nb.y == y ? sel.nb.x : 0]; + gp = &line[sel.nb.y == y ? sel.nb.x : 0]; lastx = (sel.ne.y == y) ? sel.ne.x : term.col-1; } - last = &term.line[y][MIN(lastx, linelen-1)]; - while (last >= gp && last->u == ' ') + end_idx = MIN(lastx, linelen-1); + is_wrapped = (line[end_idx].mode & ATTR_WRAP) != 0; + last = &line[end_idx]; + while (last >= gp && last->u == ' ') { --last; + } for ( ; gp <= last; ++gp) { if (gp->mode & ATTR_WDUMMY) continue; - ptr += utf8encode(gp->u, ptr); } - /* * Copy and pasting of line endings is inconsistent * in the inconsistent terminal and GUI world. @@ -628,8 +1012,13 @@ getsel(void) * st. * FIXME: Fix the computer world. */ + insert_newline = 0; if ((y < sel.ne.y || lastx >= linelen) && - (!(last->mode & ATTR_WRAP) || sel.type == SEL_RECTANGULAR)) + (!is_wrapped || sel.type == SEL_RECTANGULAR)) { + insert_newline = 1; + } + + if (insert_newline) *ptr++ = '\n'; } *ptr = 0; @@ -845,6 +1234,12 @@ ttywrite(const char *s, size_t n, int may_echo) { const char *next; + if (sb.view_offset > 0) { + selclear(); + sb.view_offset = 0; + sb_view_changed(); + } + if (may_echo && IS_SET(MODE_ECHO)) twrite(s, n, 1); @@ -965,6 +1360,8 @@ tsetdirt(int top, int bot) { int i; + if (term.row < 1) + return; LIMIT(top, 0, term.row-1); LIMIT(bot, 0, term.row-1); @@ -1030,15 +1427,21 @@ treset(void) for (i = 0; i < 2; i++) { tmoveto(0, 0); tcursor(CURSOR_SAVE); - tclearregion(0, 0, term.col-1, term.row-1); + if (term.col > 0 && term.row > 0 && term.line > 0) + tclearregion(0, 0, term.col-1, term.row-1); tswapscreen(); } + sb_clear(); + if (sel.ob.x != -1 && term.row > 0) + selclear(); } + void tnew(int col, int row) { term = (Term){ .c = { .attr = { .fg = defaultfg, .bg = defaultbg } } }; + sb_init(scrollback_lines); tresize(col, row); treset(); } @@ -1078,10 +1481,37 @@ void tscrollup(int orig, int n) { int i; + uint64_t newstart; + uint64_t oldstart; + + int attop; Line temp; + oldstart = sb_view_start(); LIMIT(n, 0, term.bot-orig+1); + if (!IS_SET(MODE_ALTSCREEN) && orig == term.top) { + /* At top of history only if history exists */ + attop = (sb.len != 0 && sb.view_offset == sb.len); + + if (sb.view_offset > 0 && !attop) + sb.view_offset += n; + + for (i = 0; i < n; i++) + sb_push(term.line[orig + i]); + + /* if at the top, keep me there */ + if (attop) + sb.view_offset = sb.len; + /* otherwise clamp me */ + else if (sb.view_offset > sb.len) + sb.view_offset = sb.len; + } + + newstart = sb_view_start(); + if (sb.view_offset > 0) + selscrollback(oldstart - newstart); + tclearregion(0, orig, term.col-1, orig+n-1); tsetdirt(orig+n, term.bot); @@ -1097,6 +1527,8 @@ tscrollup(int orig, int n) void selscroll(int orig, int n) { + if (sb.view_offset != 0) + return; if (sel.ob.x == -1 || sel.alt != IS_SET(MODE_ALTSCREEN)) return; @@ -1717,6 +2149,12 @@ csihandle(void) break; case 2: /* all */ tclearregion(0, 0, term.col-1, term.row-1); + if (!IS_SET(MODE_ALTSCREEN)) + sb_reset_on_clear(); + break; + case 3: + if (!IS_SET(MODE_ALTSCREEN)) + sb_reset_on_clear(); break; default: goto unknown; @@ -2106,7 +2544,7 @@ tdumpline(int n) const Glyph *bp, *end; bp = &term.line[n][0]; - end = &bp[MIN(tlinelen(n), term.col) - 1]; + end = &bp[MIN(tlinelen_render(n), term.col) - 1]; if (bp != end || bp->u != ' ') { for ( ; bp <= end; ++bp) tprinter(buf, utf8encode(bp->u, buf)); @@ -2163,6 +2601,36 @@ tdeftran(char ascii) } } +static void +kscroll(const Arg *arg) +{ + uint64_t oldstart; + uint64_t newstart; + + oldstart = sb_view_start(); + sb.view_offset += arg->i; + LIMIT(sb.view_offset, 0, sb.len); + newstart = sb_view_start(); + + selscrollback(oldstart - newstart); + redraw(); +} + +void +kscrolldown(const Arg *arg) +{ + Arg a; + + a.i = -arg->i; + kscroll(&a); +} + +void +kscrollup(const Arg *arg) +{ + kscroll(arg); +} + void tdectest(char c) { @@ -2569,83 +3037,136 @@ twrite(const char *buf, int buflen, int show_ctrl) void tresize(int col, int row) { - int i; + int i, j; + int min_limit; int minrow = MIN(row, term.row); - int mincol = MIN(col, term.col); - int *bp; - TCursor c; + int old_row = term.row; + int old_col = term.col; + int save_end = 0; /* Track effective pushed height */ + int loaded = 0; + int pop_width = 0; + int needs_reflow = 0; + int is_alt = IS_SET(MODE_ALTSCREEN); + Line *tmp; if (col < 1 || row < 1) { fprintf(stderr, - "tresize: error resizing to %dx%d\n", col, row); + "tresize: error resizing to %dx%d\n", col, row); return; } - /* - * slide screen to keep cursor where we expect it - - * tscrollup would work here, but we can optimize to - * memmove because we're freeing the earlier lines - */ - for (i = 0; i <= term.c.y - row; i++) { - free(term.line[i]); - free(term.alt[i]); - } - /* ensure that both src and dst are not NULL */ - if (i > 0) { - memmove(term.line, term.line + i, row * sizeof(Line)); - memmove(term.alt, term.alt + i, row * sizeof(Line)); - } - for (i += row; i < term.row; i++) { - free(term.line[i]); - free(term.alt[i]); + /* Operate on the currently visible screen buffer. */ + if (is_alt) { + tmp = term.line; + term.line = term.alt; + term.alt = tmp; } - /* resize to new height */ - term.line = xrealloc(term.line, row * sizeof(Line)); - term.alt = xrealloc(term.alt, row * sizeof(Line)); - term.dirty = xrealloc(term.dirty, row * sizeof(*term.dirty)); - term.tabs = xrealloc(term.tabs, col * sizeof(*term.tabs)); + save_end = term.row; + if (term.row != 0 && term.col != 0) { + if (!is_alt && term.c.y > 0 && term.c.y < term.row) { + term.line[term.c.y - 1][term.col - 1].mode &= ~ATTR_WRAP; + } + min_limit = is_alt ? 0 : term.c.y; - /* resize each row to new width, zero-pad if needed */ - for (i = 0; i < minrow; i++) { - term.line[i] = xrealloc(term.line[i], col * sizeof(Glyph)); - term.alt[i] = xrealloc(term.alt[i], col * sizeof(Glyph)); - } + for (i = term.row - 1; i > min_limit; i--) { + if (tlinelen(term.line[i]) > 0) + break; + } + save_end = i + 1; - /* allocate any new rows */ - for (/* i = minrow */; i < row; i++) { - term.line[i] = xmalloc(col * sizeof(Glyph)); - term.alt[i] = xmalloc(col * sizeof(Glyph)); + for (i = 0; i < save_end; i++) { + sb_push(term.line[i]); + } + /* Optimization: Only reflow if content doesn't fit in new width. + * This avoids expensive reflow operations when resizing doesn't + * affect line wrapping (e.g., when terminal is wide enough). */ + if (col > term.col) { + /* Growing: Only reflow if history was wrapped at old width */ + needs_reflow = sb.max_width >= term.col; + } else if (col < term.col) { + /* Shrinking: Only reflow if content is wider than new width. */ + if (sb.max_width > col) + needs_reflow = 1; + } + if (needs_reflow) { + sb_resize(col); + } else { + /* If we don't reflow, we still need to reset the view + * because sb_pop_screen() might change the history length. */ + sb.view_offset = 0; + } } - if (col > term.col) { - bp = term.tabs + term.col; - memset(bp, 0, sizeof(*term.tabs) * (col - term.col)); - while (--bp > term.tabs && !*bp) - /* nothing */ ; - for (bp += tabspaces; bp < term.tabs + col; bp += tabspaces) - *bp = 1; - } - /* update terminal size */ + if (term.line) { + for (i = 0; i < term.row; i++) { + free(term.line[i]); + free(term.alt[i]); + } + free(term.line); + free(term.alt); + free(term.dirty); + free(term.tabs); + } + term.col = col; term.row = row; - /* reset scrolling region */ - tsetscroll(0, row-1); - /* make use of the LIMIT in tmoveto */ - tmoveto(term.c.x, term.c.y); - /* Clearing both screens (it makes dirty all lines) */ - c = term.c; - for (i = 0; i < 2; i++) { - if (mincol < col && 0 < minrow) { - tclearregion(mincol, 0, col - 1, minrow - 1); - } - if (0 < col && minrow < row) { - tclearregion(0, minrow, col - 1, row - 1); + + term.line = xmalloc(term.row * sizeof(Line)); + term.alt = xmalloc(term.row * sizeof(Line)); + term.dirty = xmalloc(term.row * sizeof(int)); + term.tabs = xmalloc(term.col * sizeof(*term.tabs)); + + for (i = 0; i < term.row; i++) { + term.line[i] = xmalloc(term.col * sizeof(Glyph)); + term.alt[i] = xmalloc(term.col * sizeof(Glyph)); + term.dirty[i] = 1; + + for (j = 0; j < term.col; j++) { + term.line[i][j] = term.c.attr; + term.line[i][j].u = ' '; + term.line[i][j].mode = 0; + + term.alt[i][j] = term.c.attr; + term.alt[i][j].u = ' '; + term.alt[i][j].mode = 0; } - tswapscreen(); - tcursor(CURSOR_LOAD); } - term.c = c; + + memset(term.tabs, 0, term.col * sizeof(*term.tabs)); + for (i = 8; i < term.col; i += 8) + term.tabs[i] = 1; + + tsetscroll(0, term.row - 1); + + if (minrow > 0) { + loaded = MIN(sb.len, term.row); + pop_width = needs_reflow ? col : MIN(col, old_col); + sb_pop_screen(loaded, pop_width); + } + if (is_alt) { + tmp = term.line; + term.line = term.alt; + term.alt = tmp; + } + if (!is_alt && old_row > 0) { + term.c.y += (loaded - save_end); + } + if (term.c.y >= term.row) { + term.c.y = term.row - 1; + } + if (term.c.x >= term.col) { + term.c.x = term.col - 1; + } + if (term.c.y < 0) { + term.c.y = 0; + } + if (term.c.x < 0) { + term.c.x = 0; + } + + tfulldirt(); + sb_view_changed(); } void @@ -2659,12 +3180,13 @@ drawregion(int x1, int y1, int x2, int y2) { int y; + Line line; for (y = y1; y < y2; y++) { if (!term.dirty[y]) continue; - term.dirty[y] = 0; - xdrawline(term.line[y], x1, y, x2); + line = renderline(y); + xdrawline(line, x1, y, x2); } } @@ -2685,10 +3207,12 @@ draw(void) cx--; drawregion(0, 0, term.col, term.row); - xdrawcursor(cx, term.c.y, term.line[term.c.y][cx], - term.ocx, term.ocy, term.line[term.ocy][term.ocx]); - term.ocx = cx; - term.ocy = term.c.y; + if (sb.view_offset == 0) { + xdrawcursor(cx, term.c.y, term.line[term.c.y][cx], + term.ocx, term.ocy, term.line[term.ocy][term.ocx]); + term.ocx = cx; + term.ocy = term.c.y; + } xfinishdraw(); if (ocx != term.ocx || ocy != term.ocy) xximspot(term.ocx, term.ocy); diff --git a/st.h b/st.h index fd3b0d8..151d0c6 100644 --- a/st.h +++ b/st.h @@ -86,6 +86,7 @@ void printsel(const Arg *); void sendbreak(const Arg *); void toggleprinter(const Arg *); +int tisaltscreen(void); int tattrset(int); void tnew(int, int); void tresize(int, int); @@ -111,6 +112,9 @@ void *xmalloc(size_t); void *xrealloc(void *, size_t); char *xstrdup(const char *); +void kscrollup(const Arg *arg); +void kscrolldown(const Arg *arg); + /* config.h globals */ extern char *utmp; extern char *scroll; @@ -124,3 +128,4 @@ extern unsigned int tabspaces; extern unsigned int defaultfg; extern unsigned int defaultbg; extern unsigned int defaultcs; +extern unsigned int scrollback_lines; diff --git a/x.c b/x.c index d73152b..75f3db1 100644 --- a/x.c +++ b/x.c @@ -472,6 +472,23 @@ bpress(XEvent *e) struct timespec now; int snap; + if (btn == Button4 || btn == Button5) { + Arg a; + if (IS_SET(MODE_MOUSE) && !(e->xbutton.state & forcemousemod)) { + mousereport(e); + return; + } + if (!tisaltscreen()) { + a.i = 1; + if (btn == Button4) { + kscrollup(&a); + } else { + kscrolldown(&a); + } + } + return; + } + if (1 <= btn && btn <= 11) buttons |= 1 << (btn-1); -- 2.52.0