sites

public wiki contents of suckless.org
git clone git://git.suckless.org/sites
Log | Files | Refs

commit 5ed94a02a3338548633c8d745bf14fe38d3e3ffb
parent 9db0b2ad8b822f9babba7b0d472a3d20da3baf12
Author: Milos Nikic <nikic.milos@gmail.com>
Date:   Sun, 18 Oct 2026 17:44:04 -0700

[ST][PATCH] Another bug fix in scrollback reflow standalone extended.

Diffstat:
Mst.suckless.org/patches/scrollback-reflow-standalone/st-scrollback-reflow-standalone-0.9.3.diff | 697++++++++++++++-----------------------------------------------------------------
Mst.suckless.org/patches/scrollback-reflow-standalone/st-scrollback-reflow-standalone-extended-0.9.3.diff | 1037++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
2 files changed, 718 insertions(+), 1016 deletions(-)

diff --git a/st.suckless.org/patches/scrollback-reflow-standalone/st-scrollback-reflow-standalone-0.9.3.diff b/st.suckless.org/patches/scrollback-reflow-standalone/st-scrollback-reflow-standalone-0.9.3.diff @@ -1,43 +1,35 @@ -From 3f41a815d3a0527274b1e83238e821bd286c0905 Mon Sep 17 00:00:00 2001 +From 79d001e06447448e1e0bcde47494196e61809cfa Mon Sep 17 00:00:00 2001 From: Milos Nikic <nikic.milos@gmail.com> -Date: Thu, 15 Jan 2026 16:08:59 -0800 -Subject: [PATCH] st: alternative scrollback using ring buffer and view - offset +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 resize if it is needed. +Implement scrollback as a fixed-size ring buffer and render history +by offsetting the view instead of copying screen contents. - Tradeoffs / differences: - - Scrollback is disabled on the alternate screen - - Simpler model than the existing scrollback patch set - - Mouse wheel scrolling enabled by default - - Shift + page up/down and shift + end/home work as well. - - 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. +Tradeoffs / differences: +- Scrollback history is lost on resize +- Scrollback is disabled on the alternate screen +- Simpler model than the existing scrollback patch set +- Mouse wheel scrolling enabled by default + +Note: +When using vim, mouse movement will no longer move the cursor. + +Reminder: +If applying this patch on top of others, ensure any changes to +config.def.h are merged into config.h. --- - config.def.h | 9 + - st.c | 720 ++++++++++++++++++++++++++++++++++++++++++++------- + config.def.h | 5 + + st.c | 335 ++++++++++++++++++++++++++++++++++++++++++++++++--- st.h | 5 + - x.c | 17 ++ - 4 files changed, 658 insertions(+), 93 deletions(-) + x.c | 17 +++ + 4 files changed, 344 insertions(+), 18 deletions(-) diff --git a/config.def.h b/config.def.h -index 2cd740a..135a0b1 100644 +index fd2bd87..0e885f1 100644 --- a/config.def.h +++ b/config.def.h -@@ -192,6 +192,10 @@ static Shortcut shortcuts[] = { - { XK_ANY_MOD, XK_Break, sendbreak, {.i = 0} }, - { ControlMask, XK_Print, toggleprinter, {.i = 0} }, - { ShiftMask, XK_Print, printscreen, {.i = 0} }, -+ { ShiftMask, XK_Page_Up, kscrollup, {.i = -1} }, -+ { ShiftMask, XK_Page_Down, kscrolldown, {.i = -1} }, -+ { ShiftMask, XK_Home, kscrollup, {.i = 1000000} }, -+ { ShiftMask, XK_End, kscrolldown, {.i = 1000000} }, - { XK_ANY_MOD, XK_Print, printsel, {.i = 0} }, - { TERMMOD, XK_Prior, zoom, {.f = +1} }, - { TERMMOD, XK_Next, zoom, {.f = -1} }, -@@ -472,3 +476,8 @@ static char ascii_printable[] = +@@ -472,3 +472,8 @@ static char ascii_printable[] = " !\"#$%&'()*+,-./0123456789:;<=>?" "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" "`abcdefghijklmnopqrstuvwxyz{|}~"; @@ -47,7 +39,7 @@ index 2cd740a..135a0b1 100644 + */ +unsigned int scrollback_lines = 5000; diff --git a/st.c b/st.c -index e55e7b3..3b0803f 100644 +index c289072..dc30082 100644 --- a/st.c +++ b/st.c @@ -5,6 +5,7 @@ @@ -58,38 +50,20 @@ index e55e7b3..3b0803f 100644 #include <stdio.h> #include <stdlib.h> #include <string.h> -@@ -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}; +@@ -232,6 +233,202 @@ 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 */ ++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; +} Scrollback; + +static Scrollback sb; ++static int view_offset; + +static int +sb_phys_index(int logical_idx) @@ -101,12 +75,7 @@ index e55e7b3..3b0803f 100644 +static Line +lineclone(Line src) +{ -+ Line dst; -+ -+ if (!src) -+ return NULL; -+ -+ dst = xmalloc(term.col * sizeof(Glyph)); ++ Line dst = xmalloc(term.col * sizeof(Glyph)); + memcpy(dst, src, term.col * sizeof(Glyph)); + return dst; +} @@ -124,8 +93,7 @@ index e55e7b3..3b0803f 100644 + for (i = 0; i < sb.cap; i++) + sb.buf[i] = NULL; + -+ sb.view_offset = 0; -+ sb.max_width = 0; ++ view_offset = 0; +} + +/* Push one screen line into scrollback. @@ -136,7 +104,6 @@ index e55e7b3..3b0803f 100644 +{ + Line copy; + int tail; -+ int width; + + if (sb.cap <= 0) + return; @@ -148,16 +115,11 @@ index e55e7b3..3b0803f 100644 + 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 @@ -189,167 +151,15 @@ index e55e7b3..3b0803f 100644 + 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; ++ view_offset = 0; ++ if (sel.ob.x != -1 && term.row > 0) ++ selclear(); +} + +static uint64_t +sb_view_start(void) +{ -+ return sb.base + sb.len - sb.view_offset; ++ return sb.base + sb.len - view_offset; +} + +static void @@ -366,7 +176,7 @@ index e55e7b3..3b0803f 100644 + if (delta == 0) + return; + -+ if (sel.ob.x == -1 || sel.mode == SEL_EMPTY) ++ if (sel.ob.x == -1) + return; + + if (sel.alt != IS_SET(MODE_ALTSCREEN)) @@ -377,7 +187,8 @@ index e55e7b3..3b0803f 100644 + sel.ob.y += delta; + sel.oe.y += delta; + -+ if (sel.ne.y < 0 || sel.nb.y >= term.row) ++ /* the selection is in window coordinates: drop it once it leaves */ ++ if (sel.nb.y < 0 || sel.ne.y >= term.row) + selclear(); + + sb_view_changed(); @@ -405,14 +216,11 @@ index e55e7b3..3b0803f 100644 +} + +static Line -+renderline(int y) ++getlineforrender(int y) +{ + int start, v; + -+ if (sb.view_offset <= 0) -+ return term.line[y]; -+ -+ start = sb.len - sb.view_offset; /* can be negative */ ++ start = sb.len - view_offset; /* can be negative */ + v = start + y; + + if (v < 0) @@ -434,8 +242,6 @@ index e55e7b3..3b0803f 100644 +{ + sb_clear(); + sb_view_changed(); -+ if (sel.ob.x != -1 && term.row > 0) -+ selclear(); +} + +int @@ -447,38 +253,35 @@ index e55e7b3..3b0803f 100644 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; -- +@@ -418,6 +615,27 @@ tlinelen(int y) return i; } +static int +tlinelen_render(int y) +{ -+ return tlinelen(renderline(y)); ++ int i; ++ Line line; ++ ++ if (term.col <= 0) ++ return 0; ++ ++ i = term.col; ++ line = getlineforrender(y); ++ ++ if (line[i - 1].mode & ATTR_WRAP) ++ return i; ++ ++ while (i > 0 && line[i - 1].u == ' ') ++ --i; ++ ++ return i; +} + void selstart(int col, int row, int snap) { -@@ -485,10 +862,10 @@ selnormalize(void) +@@ -485,10 +703,10 @@ selnormalize(void) /* expand selection over line breaks */ if (sel.type == SEL_RECTANGULAR) return; @@ -491,7 +294,7 @@ index e55e7b3..3b0803f 100644 sel.ne.x = term.col - 1; } -@@ -514,6 +891,7 @@ selsnap(int *x, int *y, int direction) +@@ -514,6 +732,7 @@ selsnap(int *x, int *y, int direction) int newx, newy, xt, yt; int delim, prevdelim; const Glyph *gp, *prevgp; @@ -499,21 +302,21 @@ index e55e7b3..3b0803f 100644 switch (sel.snap) { case SNAP_WORD: -@@ -521,7 +899,7 @@ selsnap(int *x, int *y, int direction) +@@ -521,7 +740,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]; ++ prevgp = &getlineforrender(*y)[*x]; prevdelim = ISDELIM(prevgp->u); for (;;) { newx = *x + direction; -@@ -536,14 +914,15 @@ selsnap(int *x, int *y, int direction) +@@ -536,14 +755,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); ++ line = getlineforrender(yt); + if (!(line[xt].mode & ATTR_WRAP)) break; } @@ -523,16 +326,16 @@ index e55e7b3..3b0803f 100644 break; - gp = &term.line[newy][newx]; -+ gp = &renderline(newy)[newx]; ++ gp = &getlineforrender(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) +@@ -564,14 +784,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 ++ if (!(getlineforrender(*y-1)[term.col-1].mode & ATTR_WRAP)) { break; } @@ -540,30 +343,17 @@ index e55e7b3..3b0803f 100644 } 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 ++ if (!(getlineforrender(*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) +@@ -596,19 +816,20 @@ 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) { ++ Line line = getlineforrender(y); ++ if ((linelen = tlinelen_render(y)) == 0) { *ptr++ = '\n'; continue; } @@ -578,76 +368,30 @@ index e55e7b3..3b0803f 100644 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 = &line[MIN(lastx, linelen-1)]; + 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) +@@ -848,6 +1069,12 @@ ttyread(void) + void + ttywrite(const char *s, size_t n, int may_echo) { - const char *next; - -+ if (sb.view_offset > 0) { ++ if (view_offset > 0) { + selclear(); -+ sb.view_offset = 0; ++ 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); + const char *next; -@@ -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); + if (may_echo && IS_SET(MODE_ECHO)) +@@ -1041,12 +1268,14 @@ treset(void) + 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) { @@ -656,54 +400,53 @@ index e55e7b3..3b0803f 100644 tresize(col, row); treset(); } -@@ -1078,10 +1481,37 @@ void +@@ -1086,10 +1315,36 @@ 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) { ++ int attop; ++ + /* At top of history only if history exists */ -+ attop = (sb.len != 0 && sb.view_offset == sb.len); ++ attop = (sb.len != 0 && view_offset == sb.len); + -+ if (sb.view_offset > 0 && !attop) -+ sb.view_offset += n; ++ if (view_offset > 0 && !attop) ++ 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; ++ view_offset = sb.len; ++ else if (view_offset > sb.len) ++ view_offset = sb.len; + } + + newstart = sb_view_start(); -+ if (sb.view_offset > 0) ++ if (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) +@@ -1105,6 +1360,8 @@ tscrollup(int orig, int n) void selscroll(int orig, int n) { -+ if (sb.view_offset != 0) ++ if (view_offset != 0) + return; if (sel.ob.x == -1 || sel.alt != IS_SET(MODE_ALTSCREEN)) return; -@@ -1717,6 +2149,12 @@ csihandle(void) +@@ -1727,6 +1984,12 @@ csihandle(void) break; case 2: /* all */ tclearregion(0, 0, term.col-1, term.row-1); @@ -716,16 +459,7 @@ index e55e7b3..3b0803f 100644 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,46 @@ tdeftran(char ascii) +@@ -2173,6 +2436,36 @@ tdeftran(char ascii) } } @@ -733,12 +467,13 @@ index e55e7b3..3b0803f 100644 +kscroll(const Arg *arg) +{ + uint64_t oldstart; -+ uint64_t newstart; ++ uint64_t newstart; + + oldstart = sb_view_start(); -+ sb.view_offset += arg->i; -+ LIMIT(sb.view_offset, 0, sb.len); ++ view_offset += arg->i; ++ LIMIT(view_offset, 0, sb.len); + newstart = sb_view_start(); ++ + selscrollback(oldstart - newstart); + redraw(); +} @@ -748,228 +483,30 @@ index e55e7b3..3b0803f 100644 +{ + Arg a; + -+ if (arg->i < 0) -+ a.i = -term.row; -+ else -+ a.i = -arg->i; -+ ++ a.i = -arg->i; + kscroll(&a); +} + +void +kscrollup(const Arg *arg) +{ -+ Arg a; -+ -+ if (arg->i < 0) -+ a.i = term.row; -+ else -+ a.i = arg->i; -+ -+ kscroll(&a); ++ kscroll(arg); +} + void tdectest(char c) { -@@ -2569,83 +3047,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; +@@ -2585,6 +2878,9 @@ tresize(int col, int row) + int *bp; + TCursor c; ++ sb_clear(); ++ sb_view_changed(); ++ 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 +3190,13 @@ drawregion(int x1, int y1, int x2, int y2) + "tresize: error resizing to %dx%d\n", col, row); +@@ -2669,12 +2965,13 @@ drawregion(int x1, int y1, int x2, int y2) { int y; @@ -980,12 +517,12 @@ index e55e7b3..3b0803f 100644 - term.dirty[y] = 0; - xdrawline(term.line[y], x1, y, x2); -+ line = renderline(y); ++ line = getlineforrender(y); + xdrawline(line, x1, y, x2); } } -@@ -2685,10 +3217,12 @@ draw(void) +@@ -2695,10 +2992,12 @@ draw(void) cx--; drawregion(0, 0, term.col, term.row); @@ -993,7 +530,7 @@ index e55e7b3..3b0803f 100644 - term.ocx, term.ocy, term.line[term.ocy][term.ocx]); - term.ocx = cx; - term.ocy = term.c.y; -+ if (sb.view_offset == 0) { ++ if (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; @@ -1030,7 +567,7 @@ index fd3b0d8..151d0c6 100644 extern unsigned int defaultcs; +extern unsigned int scrollback_lines; diff --git a/x.c b/x.c -index d73152b..75f3db1 100644 +index 76f572d..dfe416a 100644 --- a/x.c +++ b/x.c @@ -472,6 +472,23 @@ bpress(XEvent *e) @@ -1058,5 +595,5 @@ index d73152b..75f3db1 100644 buttons |= 1 << (btn-1); -- -2.52.0 +2.56.0 diff --git a/st.suckless.org/patches/scrollback-reflow-standalone/st-scrollback-reflow-standalone-extended-0.9.3.diff b/st.suckless.org/patches/scrollback-reflow-standalone/st-scrollback-reflow-standalone-extended-0.9.3.diff @@ -1,56 +1,39 @@ -From 792cbb832839cb6981440356c26ce2836bc69427 Mon Sep 17 00:00:00 2001 +From 5fbf33797d8d286b94a69babbfee014f58dfe87e Mon Sep 17 00:00:00 2001 From: Milos Nikic <nikic.milos@gmail.com> -Date: Thu, 8 Jan 2026 22:04:25 -0800 -Subject: [PATCH] st: alternative scrollback using ring buffer and view offset +Date: Sun, 18 Oct 2026 11:58:07 -0700 +Subject: [PATCH] st: scrollback reflow and selection across scrollback -Implement scrollback as a fixed-size ring buffer and render history -by offsetting the view instead of copying screen contents. +Reflow the scrollback buffer on resize instead of dropping it: wrapped +lines are joined into logical lines and re-split to the new width. -Tradeoffs / differences: -- Scrollback history is lost on resize -- Scrollback is disabled on the alternate screen -- Simpler model than the existing scrollback patch set -- Mouse wheel scrolling enabled by default +Keep the selection in logical rows spanning history and screen (0 is +the oldest scrollback line, sb.len + y is screen row y) instead of in +window coordinates. Lines keep their row when pushed into scrollback, +so the selection now survives scrolling the view and new output, and +can cover more than one screen. Only evicting the oldest line from the +ring shifts it. -Note: -When using vim, mouse movement will no longer move the cursor. +- shift+click extends the current selection: the end nearest to the + click follows it, keeping word/line snapping. Disabled in MODE_MOUSE, + where shift forces selection instead. +- scrolling the wheel while dragging keeps the selection end under the + pointer. +- typing while scrolled back no longer clears the selection. +- the selection is cleared when the width changes (reflow) or the alt + screen is resized, and kept on height-only resizes. -Reminder: -If applying this patch on top of others, ensure any changes to -config.def.h are merged into config.h. +Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- - config.def.h | 5 + - st.c | 713 ++++++++++++++++++++++++++++++++++++++++++++------- - st.h | 5 + - x.c | 17 ++ - 4 files changed, 645 insertions(+), 95 deletions(-) + st.c | 707 +++++++++++++++++++++++++++++++++++++++++++---------------- + st.h | 1 + + x.c | 29 ++- + 3 files changed, 537 insertions(+), 200 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 6f40e35..cf76c58 100644 +index dc30082..4d79f20 100644 --- a/st.c +++ b/st.c -@@ -5,6 +5,7 @@ - #include <limits.h> - #include <pwd.h> - #include <stdarg.h> -+#include <stdint.h> - #include <stdio.h> - #include <stdlib.h> - #include <string.h> -@@ -178,7 +179,7 @@ static void tdeletechar(int); +@@ -179,7 +179,7 @@ static void tdeletechar(int); static void tdeleteline(int); static void tinsertblank(int); static void tinsertblankline(int); @@ -59,10 +42,26 @@ index 6f40e35..cf76c58 100644 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}; +@@ -208,6 +208,9 @@ static void drawregion(int, int, int, int); + static void selnormalize(void); + static void selscroll(int, int); + static void selsnap(int *, int *, int); ++static void selevict(void); ++static void seldirt(int, int); ++static int isselected(int, int); + + static size_t utf8decode(const char *, Rune *, size_t); + static Rune utf8decodebyte(char, size_t *); +@@ -233,16 +236,25 @@ 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; +typedef struct +{ + Line *buf; /* ring of Line pointers */ @@ -79,112 +78,80 @@ index 6f40e35..cf76c58 100644 + */ + 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) -+{ + } Scrollback; + + static Scrollback sb; +-static int view_offset; + + static int + sb_phys_index(int logical_idx) +@@ -254,7 +266,12 @@ sb_phys_index(int logical_idx) + static Line + lineclone(Line src) + { +- Line dst = xmalloc(term.col * sizeof(Glyph)); + 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; -+ + memcpy(dst, src, term.col * sizeof(Glyph)); + return dst; + } +@@ -272,7 +289,8 @@ sb_init(int lines) + for (i = 0; i < sb.cap; i++) + sb.buf[i] = NULL; + +- view_offset = 0; + 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; + } + + /* Push one screen line into scrollback. +@@ -283,6 +301,7 @@ 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 { + + if (sb.cap <= 0) + return; +@@ -294,11 +313,17 @@ sb_push(Line line) + 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++; -+ } + free(sb.buf[sb.head]); + sb.buf[sb.head] = copy; + sb.head = (sb.head + 1) % sb.cap; + sb.base++; ++ selevict(); + } + 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; + } + + static Line +@@ -330,47 +355,169 @@ sb_clear(void) + sb.len = 0; + sb.head = 0; + sb.base = 0; +- view_offset = 0; +- if (sel.ob.x != -1 && term.row > 0) +- selclear(); +-} +- +-static uint64_t +-sb_view_start(void) +-{ +- return sb.base + sb.len - view_offset; + sb.view_offset = 0; + sb.max_width = 0; -+} -+ + } + +/* + * Reflows the scrollback buffer to fit a new terminal width. + * @@ -202,9 +169,11 @@ index 6f40e35..cf76c58 100644 + * Note: During reflow we reset sb to match the rebuilt buffer + * (head, base and len might change). + */ -+static void + static void +-sb_view_changed(void) +sb_resize(int col) -+{ + { +- if (!term.dirty || term.row <= 0) + Line *new_buf; + int i, j; + int new_len, logical_cap, logical_len, is_wrapped, cursor; @@ -218,7 +187,8 @@ index 6f40e35..cf76c58 100644 + new_len = 0; + + if (sb.len == 0) -+ return; + return; +- tfulldirt(); + + new_buf = xmalloc(sizeof(Line) * sb.cap); + for (i = 0; i < sb.cap; i++) @@ -313,133 +283,116 @@ index 6f40e35..cf76c58 100644 + sb.base = new_base; + sb.view_offset = 0; + sb.max_width = new_max_width; -+} -+ -+static void + } + + static void +-selscrollback(int delta) +sb_pop_screen(int loaded, int new_cols) -+{ + { +- if (delta == 0) +- return; + int i, p; + int start_logical; + Line line; -+ + +- if (sel.ob.x == -1) +- return; + 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]; -+ + +- if (sel.alt != IS_SET(MODE_ALTSCREEN)) +- return; + memcpy(term.line[i], line, new_cols * sizeof(Glyph)); -+ + +- sel.nb.y += delta; +- sel.ne.y += delta; +- sel.ob.y += delta; +- sel.oe.y += delta; + free(line); + sb.buf[p] = NULL; + } -+ + +- /* the selection is in window coordinates: drop it once it leaves */ +- if (sel.nb.y < 0 || sel.ne.y >= term.row) +- selclear(); + sb.len -= loaded; +} -+ -+static uint64_t -+sb_view_start(void) -+{ -+ return sb.base + sb.len - sb.view_offset; -+} -+ + +- sb_view_changed(); +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 +@@ -394,14 +541,16 @@ emptyline(void) + return empty; + } + ++/* ++ * Logical rows address history and screen as one continuous space: ++ * 0..sb.len-1 are scrollback lines (0 = oldest) and sb.len + y is ++ * screen row y. A line keeps its logical row when it is pushed into ++ * scrollback; only evicting the oldest line shifts everything by one. ++ * The selection lives in this space, so it survives scrolling. ++ */ + static Line +-getlineforrender(int y) ++logicalline(int v) + { +- int start, v; +- +- start = sb.len - view_offset; /* can be negative */ +- v = start + y; +- + if (v < 0) + return emptyline(); + +@@ -410,17 +559,35 @@ getlineforrender(int y) + + /* past scrollback -> into current screen */ + v -= sb.len; +- if (v >= 0 && v < term.row) ++ if (v < term.row) + return term.line[v]; + + return emptyline(); + } + ++/* first logical row shown at the top of the window */ ++static int ++viewtop(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; ++ return sb.len - sb.view_offset; /* can be negative */ +} + +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(); ++ return logicalline(viewtop() + y); +} + -+static void -+sb_reset_on_clear(void) -+{ -+ sb_clear(); -+ sb_view_changed(); + 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) + } + + int +@@ -601,39 +768,66 @@ selinit(void) sel.ob.x = -1; } @@ -461,82 +414,235 @@ index 6f40e35..cf76c58 100644 return i; } ++/* range of logical rows a selection can cover */ + static int +-tlinelen_render(int y) ++selminrow(void) + { +- int i; +- Line line; ++ /* the alternate screen has no history */ ++ return sel.alt ? sb.len : 0; ++} + +- if (term.col <= 0) +- return 0; +static int -+tlinelen_render(int y) ++selmaxrow(void) +{ -+ return tlinelen(renderline(y)); ++ return sb.len + term.row - 1; ++} + +- i = term.col; +- line = getlineforrender(y); ++/* mark the visible part of logical rows a..b dirty */ ++static void ++seldirt(int a, int b) ++{ ++ int top = viewtop(); + +- if (line[i - 1].mode & ATTR_WRAP) +- return i; ++ a -= top; ++ b -= top; ++ if (b < 0 || a >= term.row) ++ return; ++ tsetdirt(MAX(a, 0), MIN(b, term.row - 1)); ++} + +- while (i > 0 && line[i - 1].u == ' ') +- --i; ++/* the oldest scrollback line was dropped: shift the selection with it */ ++static void ++selevict(void) ++{ ++ if (sel.ob.x == -1 || sel.alt) ++ return; + +- return i; ++ sel.ob.y--; ++ sel.oe.y--; ++ sel.nb.y--; ++ sel.ne.y--; ++ ++ if (sel.ne.y < 0) { ++ selclear(); ++ return; ++ } ++ if (sel.nb.y < 0) ++ sel.nb.x = sel.nb.y = 0; ++ if (sel.ob.y < 0) ++ sel.ob.x = sel.ob.y = 0; ++ if (sel.oe.y < 0) ++ sel.oe.x = sel.oe.y = 0; + } + + void +@@ -645,12 +839,12 @@ selstart(int col, int row, int snap) + sel.alt = IS_SET(MODE_ALTSCREEN); + sel.snap = snap; + sel.oe.x = sel.ob.x = col; +- sel.oe.y = sel.ob.y = row; ++ sel.oe.y = sel.ob.y = viewtop() + row; + selnormalize(); + + if (sel.snap != 0) + sel.mode = SEL_READY; +- tsetdirt(sel.nb.y, sel.ne.y); ++ seldirt(sel.nb.y, sel.ne.y); + } + + void +@@ -672,16 +866,48 @@ selextend(int col, int row, int type, int done) + oldtype = sel.type; + + sel.oe.x = col; +- sel.oe.y = row; ++ sel.oe.y = viewtop() + row; + selnormalize(); + sel.type = type; + + if (oldey != sel.oe.y || oldex != sel.oe.x || oldtype != sel.type || sel.mode == SEL_EMPTY) +- tsetdirt(MIN(sel.nb.y, oldsby), MAX(sel.ne.y, oldsey)); ++ seldirt(MIN(sel.nb.y, oldsby), MAX(sel.ne.y, oldsey)); + + sel.mode = done ? SEL_IDLE : SEL_READY; + } + ++/* ++ * Extend the existing selection to (col, row): the end closest to the ++ * click follows it, the other end stays put. The selection can then be ++ * dragged further like a fresh one. Returns 0 if there is nothing to ++ * extend. ++ */ ++int ++selextendto(int col, int row, int type) ++{ ++ long pos, dbeg, dend; ++ ++ if (sel.ob.x == -1 || sel.mode != SEL_IDLE || ++ sel.alt != IS_SET(MODE_ALTSCREEN)) ++ return 0; ++ ++ row += viewtop(); ++ pos = (long)row * term.col + col; ++ dbeg = labs(pos - ((long)sel.nb.y * term.col + sel.nb.x)); ++ dend = labs(pos - ((long)sel.ne.y * term.col + sel.ne.x)); ++ ++ seldirt(sel.nb.y, sel.ne.y); ++ sel.ob = (dbeg < dend) ? sel.ne : sel.nb; ++ sel.oe.x = col; ++ sel.oe.y = row; ++ sel.type = type; ++ selnormalize(); ++ seldirt(sel.nb.y, sel.ne.y); ++ sel.mode = SEL_READY; ++ ++ return 1; +} + void - selstart(int col, int row, int snap) + selnormalize(void) { -@@ -485,10 +862,10 @@ selnormalize(void) +@@ -703,15 +929,16 @@ selnormalize(void) /* expand selection over line breaks */ if (sel.type == SEL_RECTANGULAR) return; -- i = tlinelen(sel.nb.y); -+ i = tlinelen_render(sel.nb.y); +- i = tlinelen_render(sel.nb.y); ++ i = tlinelen(logicalline(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) +- if (tlinelen_render(sel.ne.y) <= sel.ne.x) ++ if (tlinelen(logicalline(sel.ne.y)) <= sel.ne.x) sel.ne.x = term.col - 1; } -@@ -514,6 +891,7 @@ selsnap(int *x, int *y, int direction) +-int +-selected(int x, int y) ++/* x, y in logical rows */ ++static int ++isselected(int x, int y) + { + if (sel.mode == SEL_EMPTY || sel.ob.x == -1 || + sel.alt != IS_SET(MODE_ALTSCREEN)) +@@ -726,11 +953,19 @@ selected(int x, int y) + && (y != sel.ne.y || x <= sel.ne.x); + } + ++/* x, y in window coordinates */ ++int ++selected(int x, int y) ++{ ++ return isselected(x, viewtop() + y); ++} ++ + void + selsnap(int *x, int *y, int direction) + { int newx, newy, xt, yt; int delim, prevdelim; ++ int miny = selminrow(), maxy = selmaxrow(); const Glyph *gp, *prevgp; -+ Line line; + Line line; - switch (sel.snap) { - case SNAP_WORD: -@@ -521,7 +899,7 @@ selsnap(int *x, int *y, int direction) +@@ -740,7 +975,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]; +- prevgp = &getlineforrender(*y)[*x]; ++ prevgp = &logicalline(*y)[*x]; prevdelim = ISDELIM(prevgp->u); for (;;) { newx = *x + direction; -@@ -536,14 +914,15 @@ selsnap(int *x, int *y, int direction) +@@ -748,22 +983,23 @@ selsnap(int *x, int *y, int direction) + if (!BETWEEN(newx, 0, term.col - 1)) { + newy += direction; + newx = (newx + term.col) % term.col; +- if (!BETWEEN(newy, 0, term.row - 1)) ++ if (!BETWEEN(newy, miny, maxy)) + break; + + if (direction > 0) 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)) +- line = getlineforrender(yt); ++ line = logicalline(yt); + if (!(line[xt].mode & ATTR_WRAP)) break; } -- if (newx >= tlinelen(newy)) -+ if (newx >= tlinelen_render(newy)) +- if (newx >= tlinelen_render(newy)) ++ line = logicalline(newy); ++ if (newx >= tlinelen(line)) break; -- gp = &term.line[newy][newx]; -+ gp = &renderline(newy)[newx]; +- gp = &getlineforrender(newy)[newx]; ++ gp = &line[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) +@@ -783,15 +1019,15 @@ 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 +- for (; *y > 0; *y += direction) { +- if (!(getlineforrender(*y-1)[term.col-1].mode ++ for (; *y > miny; *y += direction) { ++ if (!(logicalline(*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 +- for (; *y < term.row-1; *y += direction) { +- if (!(getlineforrender(*y)[term.col-1].mode ++ for (; *y < maxy; *y += direction) { ++ if (!(logicalline(*y)[term.col-1].mode & ATTR_WRAP)) { break; } -@@ -585,8 +964,9 @@ char * +@@ -805,8 +1041,9 @@ char * getsel(void) { char *str, *ptr; @@ -547,29 +653,24 @@ index 6f40e35..cf76c58 100644 if (sel.ob.x == -1) return NULL; -@@ -596,29 +976,33 @@ getsel(void) +@@ -816,8 +1053,10 @@ 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); +- Line line = getlineforrender(y); +- if ((linelen = tlinelen_render(y)) == 0) { ++ line = logicalline(y); ++ linelen = tlinelen(line); + + 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]; +@@ -829,17 +1068,18 @@ getsel(void) + 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)]; +- last = &line[MIN(lastx, linelen-1)]; - while (last >= gp && last->u == ' ') + end_idx = MIN(lastx, linelen-1); + is_wrapped = (line[end_idx].mode & ATTR_WRAP) != 0; @@ -588,7 +689,7 @@ index 6f40e35..cf76c58 100644 /* * Copy and pasting of line endings is inconsistent * in the inconsistent terminal and GUI world. -@@ -628,8 +1012,13 @@ getsel(void) +@@ -849,8 +1089,13 @@ getsel(void) * st. * FIXME: Fix the computer world. */ @@ -603,20 +704,35 @@ index 6f40e35..cf76c58 100644 *ptr++ = '\n'; } *ptr = 0; -@@ -845,6 +1234,12 @@ ttywrite(const char *s, size_t n, int may_echo) - { - const char *next; +@@ -864,7 +1109,7 @@ selclear(void) + return; + sel.mode = SEL_IDLE; + sel.ob.x = -1; +- tsetdirt(sel.nb.y, sel.ne.y); ++ seldirt(sel.nb.y, sel.ne.y); + } + void +@@ -1069,14 +1314,13 @@ ttyread(void) + void + ttywrite(const char *s, size_t n, int may_echo) + { +- if (view_offset > 0) { +- selclear(); +- view_offset = 0; ++ const char *next; ++ + if (sb.view_offset > 0) { -+ selclear(); + sb.view_offset = 0; -+ sb_view_changed(); -+ } -+ + sb_view_changed(); + } + +- const char *next; +- if (may_echo && IS_SET(MODE_ECHO)) twrite(s, n, 1); -@@ -965,9 +1360,8 @@ tsetdirt(int top, int bot) +@@ -1197,9 +1441,8 @@ tsetdirt(int top, int bot) { int i; @@ -627,16 +743,16 @@ index 6f40e35..cf76c58 100644 LIMIT(top, 0, term.row-1); LIMIT(bot, 0, term.row-1); -@@ -1033,15 +1427,21 @@ treset(void) +@@ -1265,12 +1508,16 @@ 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) ++ if (term.col > 0 && term.row > 0 && term.line) + tclearregion(0, 0, term.col-1, term.row-1); tswapscreen(); } -+ sb_clear(); + sb_clear(); + if (sel.ob.x != -1 && term.row > 0) + selclear(); } @@ -645,118 +761,150 @@ index 6f40e35..cf76c58 100644 void tnew(int col, int row) { - term = (Term){ .c = { .attr = { .fg = defaultfg, .bg = defaultbg } } }; -+ sb_init(scrollback_lines); - tresize(col, row); - treset(); - } -@@ -1081,10 +1481,37 @@ void +@@ -1315,35 +1562,53 @@ void tscrollup(int orig, int n) { int i; -+ uint64_t newstart; -+ uint64_t oldstart; -+ +- uint64_t newstart; +- uint64_t oldstart; +- + int attop; ++ int pushed = 0; Line temp; -+ oldstart = sb_view_start(); +- 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 */ + if (!IS_SET(MODE_ALTSCREEN) && orig == term.top) { +- int attop; ++ /* ++ * Pushed lines keep their logical rows, but rows outside ++ * of a partial scroll region move along with the screen. ++ */ ++ if ((term.top > 0 || term.bot < term.row-1) && ++ sel.ob.x != -1 && !sel.alt && sel.ne.y >= sb.len) ++ selclear(); + + /* At top of history only if history exists */ +- attop = (sb.len != 0 && view_offset == sb.len); + attop = (sb.len != 0 && sb.view_offset == sb.len); -+ + +- if (view_offset > 0 && !attop) +- view_offset += n; + if (sb.view_offset > 0 && !attop) + sb.view_offset += n; -+ -+ for (i = 0; i < n; i++) -+ sb_push(term.line[orig + i]); -+ + + for (i = 0; i < n; i++) + sb_push(term.line[orig + i]); ++ pushed = sb.cap > 0; + + /* if at the top, keep me there */ -+ if (attop) + if (attop) +- view_offset = sb.len; +- else if (view_offset > sb.len) +- view_offset = sb.len; + 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 (view_offset > 0) +- selscrollback(oldstart - newstart); ++ if (pushed) { ++ /* ++ * The lines went to history, so the selection stays valid. ++ * Rotate before clearing, so clearing doesn't drop it. ++ */ ++ tsetdirt(orig, term.bot); ++ for (i = orig; i <= term.bot-n; i++) { ++ temp = term.line[i]; ++ term.line[i] = term.line[i+n]; ++ term.line[i+n] = temp; ++ } ++ tclearregion(0, term.bot-n+1, term.col-1, term.bot); ++ return; + } -+ -+ 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); - -@@ -1100,6 +1527,8 @@ tscrollup(int orig, int n) +@@ -1360,18 +1625,23 @@ tscrollup(int orig, int n) void selscroll(int orig, int n) { -+ if (sb.view_offset != 0) -+ return; +- if (view_offset != 0) +- return; ++ int top, bot; ++ if (sel.ob.x == -1 || sel.alt != IS_SET(MODE_ALTSCREEN)) return; -@@ -1720,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; -@@ -2109,7 +2544,7 @@ tdumpline(int n) +- if (BETWEEN(sel.nb.y, orig, term.bot) != BETWEEN(sel.ne.y, orig, term.bot)) { ++ /* the scrolled screen region, in logical rows */ ++ orig += sb.len; ++ top = sb.len + term.top; ++ bot = sb.len + term.bot; ++ ++ if (BETWEEN(sel.nb.y, orig, bot) != BETWEEN(sel.ne.y, orig, bot)) { + selclear(); +- } else if (BETWEEN(sel.nb.y, orig, term.bot)) { ++ } else if (BETWEEN(sel.nb.y, orig, bot)) { + sel.ob.y += n; + sel.oe.y += n; +- if (sel.ob.y < term.top || sel.ob.y > term.bot || +- sel.oe.y < term.top || sel.oe.y > term.bot) { ++ if (sel.ob.y < top || sel.ob.y > bot || ++ sel.oe.y < top || sel.oe.y > bot) { + selclear(); + } else { + selnormalize(); +@@ -1507,7 +1777,7 @@ tclearregion(int x1, int y1, int x2, int y2) + term.dirty[y] = 1; + for (x = x1; x <= x2; x++) { + gp = &term.line[y][x]; +- if (selected(x, y)) ++ if (isselected(x, sb.len + y)) + selclear(); + gp->fg = term.c.attr.fg; + gp->bg = term.c.attr.bg; +@@ -2379,7 +2649,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]; ++ end = &bp[MIN(tlinelen(term.line[n]), term.col) - 1]; if (bp != end || bp->u != ' ') { for ( ; bp <= end; ++bp) tprinter(buf, utf8encode(bp->u, buf)); -@@ -2166,6 +2601,36 @@ tdeftran(char ascii) - } - } - -+static void -+kscroll(const Arg *arg) -+{ -+ uint64_t oldstart; -+ uint64_t newstart; -+ -+ oldstart = sb_view_start(); +@@ -2439,15 +2709,8 @@ tdeftran(char ascii) + static void + kscroll(const Arg *arg) + { +- uint64_t oldstart; +- uint64_t newstart; +- +- oldstart = sb_view_start(); +- view_offset += arg->i; +- LIMIT(view_offset, 0, sb.len); +- newstart = sb_view_start(); +- +- selscrollback(oldstart - newstart); + 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) - { -@@ -2572,83 +3037,138 @@ twrite(const char *buf, int buflen, int show_ctrl) + redraw(); + } + +@@ -2793,7 +3056,7 @@ check_control_code: + */ + return; + } +- if (selected(term.c.x, term.c.y)) ++ if (isselected(term.c.x, sb.len + term.c.y)) + selclear(); + + gp = &term.line[term.c.y][term.c.x]; +@@ -2872,86 +3135,146 @@ twrite(const char *buf, int buflen, int show_ctrl) void tresize(int col, int row) { @@ -767,6 +915,9 @@ index 6f40e35..cf76c58 100644 - int mincol = MIN(col, term.col); - int *bp; - TCursor c; +- +- sb_clear(); +- sb_view_changed(); + int old_row = term.row; + int old_col = term.col; + int save_end = 0; /* Track effective pushed height */ @@ -783,11 +934,14 @@ index 6f40e35..cf76c58 100644 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 -- */ ++ * Reflow and truncation move text around, and the alt screen ++ * is rebuilt from scratch. A pure height change keeps logical ++ * rows intact (lines go through scrollback and back). + */ - for (i = 0; i <= term.c.y - row; i++) { - free(term.line[i]); - free(term.alt[i]); @@ -800,6 +954,9 @@ index 6f40e35..cf76c58 100644 - for (i += row; i < term.row; i++) { - free(term.line[i]); - free(term.alt[i]); ++ if (col != old_col || sel.alt) ++ selclear(); ++ + /* Operate on the currently visible screen buffer. */ + if (is_alt) { + tmp = term.line; @@ -955,94 +1112,102 @@ index 6f40e35..cf76c58 100644 } void -@@ -2662,12 +3182,13 @@ drawregion(int x1, int y1, int x2, int y2) - { - int y; - -+ Line line; - for (y = y1; y < y2; y++) { +@@ -2970,7 +3293,7 @@ drawregion(int x1, int y1, int x2, int y2) if (!term.dirty[y]) continue; -- term.dirty[y] = 0; -- xdrawline(term.line[y], x1, y, x2); +- line = getlineforrender(y); + line = renderline(y); -+ xdrawline(line, x1, y, x2); + xdrawline(line, x1, y, x2); } } - -@@ -2688,10 +3209,12 @@ draw(void) +@@ -2992,7 +3315,7 @@ 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 (view_offset == 0) { + 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); + xdrawcursor(cx, term.c.y, term.line[term.c.y][cx], + term.ocx, term.ocy, term.line[term.ocy][term.ocx]); + term.ocx = cx; diff --git a/st.h b/st.h -index fd3b0d8..151d0c6 100644 +index 151d0c6..66f1436 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; +@@ -103,6 +103,7 @@ void selclear(void); + void selinit(void); + void selstart(int, int, int); + void selextend(int, int, int, int); ++int selextendto(int, int, int); + int selected(int, int); + char *getsel(void); + diff --git a/x.c b/x.c -index d73152b..75f3db1 100644 +index dfe416a..8091e6f 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; -+ } +@@ -181,6 +181,7 @@ static void selnotify(XEvent *); + static void selclear_(XEvent *); + static void selrequest(XEvent *); + static void setsel(char *, Time); ++static int mouseseltype(XEvent *); + static void mousesel(XEvent *, int); + static void mousereport(XEvent *); + static char *kmap(KeySym, uint); +@@ -344,19 +345,23 @@ evrow(XEvent *e) + return y / win.ch; + } + +-void +-mousesel(XEvent *e, int done) ++int ++mouseseltype(XEvent *e) + { +- int type, seltype = SEL_REGULAR; ++ int type; + uint state = e->xbutton.state & ~(Button1Mask | forcemousemod); + + for (type = 1; type < LEN(selmasks); ++type) { +- if (match(selmasks[type], state)) { +- seltype = type; +- break; +- } ++ if (match(selmasks[type], state)) ++ return type; + } +- selextend(evcol(e), evrow(e), seltype, done); ++ return SEL_REGULAR; ++} + - if (1 <= btn && btn <= 11) - buttons |= 1 << (btn-1); ++void ++mousesel(XEvent *e, int done) ++{ ++ selextend(evcol(e), evrow(e), mouseseltype(e), done); + if (done) + setsel(getsel(), e->xbutton.time); + } +@@ -485,6 +490,9 @@ bpress(XEvent *e) + } else { + kscrolldown(&a); + } ++ /* keep a selection being dragged under the pointer */ ++ if (buttons & 1) ++ mousesel(e, 0); + } + return; + } +@@ -501,6 +509,11 @@ bpress(XEvent *e) + return; + if (btn == Button1) { ++ /* shift+click extends the current selection */ ++ if ((e->xbutton.state & ShiftMask) && !IS_SET(MODE_MOUSE) && ++ selextendto(evcol(e), evrow(e), mouseseltype(e))) ++ return; ++ + /* + * If the user clicks below predefined timeouts specific + * snapping behaviour is exposed. -- -2.53.0 +2.56.0