Implement scrollback as a fixed-size ring buffer and render history
by offsetting the view instead of copying screen contents.

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 |   5 ++
 st.c         | 243 +++++++++++++++++++++++++++++++++++++++++++++++++--
 st.h         |   5 ++
 x.c          |  17 ++++
 4 files changed, 264 insertions(+), 6 deletions(-)

diff --git a/config.def.h b/config.def.h
index 2cd740a..a2d5182 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.
+ */
+int scrollback_lines = 5000;
diff --git a/st.c b/st.c
index e55e7b3..034d4b1 100644
--- a/st.c
+++ b/st.c
@@ -43,6 +43,7 @@
 #define ISCONTROL(c)           (ISCONTROLC0(c) || ISCONTROLC1(c))
 #define ISDELIM(u)             (u && wcschr(worddelimiters, u))
 
+
 enum term_mode {
        MODE_WRAP        = 1 << 0,
        MODE_INSERT      = 1 << 1,
@@ -232,6 +233,183 @@ 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) */
+} Scrollback;
+
+static Scrollback sb;
+static int view_offset;
+
+static inline int
+sb_phys_index(int logical_idx)
+{
+       /* logical_idx: 0..sb.len-1 (0 = oldest) */
+       return (sb.head + logical_idx) % sb.cap;
+}
+
+Line
+lineclone(Line src)
+{
+       Line dst = xmalloc(term.col * sizeof(Glyph));
+       memcpy(dst, src, term.col * sizeof(Glyph));
+       return dst;
+}
+
+void
+sb_init(int lines)
+{
+       sb.buf  = xmalloc(sizeof(Line) * lines);
+       sb.cap  = lines;
+       sb.len  = 0;
+       sb.head = 0;
+
+       for (int i = 0; i < sb.cap; i++)
+               sb.buf[i] = NULL;
+
+       view_offset = 0;
+}
+
+/* Push one screen line into scrollback.
+ * Overwrites oldest when full (ring buffer).
+ */
+void
+sb_push(Line line)
+{
+       if (sb.cap <= 0)
+               return;
+       int was_at_top = (view_offset == sb.len);
+       Line copy = lineclone(line);
+
+       if (sb.len < sb.cap) {
+               int tail = sb_phys_index(sb.len);
+               sb.buf[tail] = copy;
+               sb.len++;
+       } else {
+               if (sb.buf[sb.head])
+                       free(sb.buf[sb.head]);
+               sb.buf[sb.head] = copy;
+               sb.head = (sb.head + 1) % sb.cap;
+       }
+       if (was_at_top) {
+               view_offset = sb.len;
+       } else {
+               if (view_offset > sb.len)
+                       view_offset = sb.len;
+       }
+}
+
+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)];
+}
+
+int
+sb_len(void)
+{
+       return sb.len;
+}
+
+void
+sb_clear(void)
+{
+       if (!sb.buf)
+               return;
+
+       for (int i = 0; i < sb.len; i++) {
+               int p = sb_phys_index(i);
+               if (sb.buf[p]) {
+                       free(sb.buf[p]);
+                       sb.buf[p] = NULL;
+               }
+       }
+
+       sb.len = 0;
+       sb.head = 0;
+       view_offset = 0;
+}
+
+void
+sb_view_changed(void)
+{
+       if (!term.dirty || term.row < 0)
+               return;
+       tfulldirt();
+}
+
+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;
+}
+
+/* Render line selection with scrollback.
+ *
+ * When view_offset == 0: show live screen (term.line).
+ * When view_offset > 0: show a window that ends "view_offset" lines above the 
bottom.
+ *
+ * We treat the visible window as:
+ *   start = (sb.len + term.row) - view_offset - term.row = sb.len - 
view_offset
+ *   visible indices are [start .. start + term.row - 1] in the concatenation:
+ *      [ scrollback (0..sb.len-1) ][ screen (0..term.row-1) ]
+ */
+Line
+getlineforrender(int y)
+{
+       if (view_offset <= 0)
+               return term.line[y];
+
+       int start = sb.len - view_offset; /* can be negative */
+       int 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 NULL;
+}
+
+static void
+sb_reset_on_clear(void)
+{
+       sb_clear();
+       sb_view_changed();
+}
+
+int
+tisaltscreen(void)
+{
+       return IS_SET(MODE_ALTSCREEN);
+}
+
 ssize_t
 xwrite(int fd, const char *s, size_t len)
 {
@@ -843,6 +1021,11 @@ ttyread(void)
 void
 ttywrite(const char *s, size_t n, int may_echo)
 {
+       if (view_offset > 0) {
+               view_offset = 0;
+               sb_view_changed();
+       }
+
        const char *next;
 
        if (may_echo && IS_SET(MODE_ECHO))
@@ -1033,12 +1216,14 @@ treset(void)
                tclearregion(0, 0, term.col-1, term.row-1);
                tswapscreen();
        }
+       sb_clear();
 }
 
 void
 tnew(int col, int row)
 {
        term = (Term){ .c = { .attr = { .fg = defaultfg, .bg = defaultbg } } };
+       sb_init(scrollback_lines);
        tresize(col, row);
        treset();
 }
@@ -1082,6 +1267,20 @@ tscrollup(int orig, int n)
 
        LIMIT(n, 0, term.bot-orig+1);
 
+       if (!IS_SET(MODE_ALTSCREEN) && orig == term.top) {
+               if (view_offset > 0) {
+                       view_offset += n;
+                       if (view_offset > sb.len)
+                               view_offset = sb.len;
+               }
+
+               for (i = 0; i < n; i++)
+                       sb_push(term.line[orig + i]);
+
+               if (view_offset > sb.len)
+                       view_offset = sb.len;
+       }
+
        tclearregion(0, orig, term.col-1, orig+n-1);
        tsetdirt(orig+n, term.bot);
 
@@ -1717,7 +1916,13 @@ 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;
                }
@@ -2163,6 +2368,24 @@ tdeftran(char ascii)
        }
 }
 
+void
+kscrollup(const Arg *arg)
+{
+       view_offset += arg->i;
+       if(view_offset > sb.len)
+               view_offset = sb.len;
+       redraw ();
+}
+
+void
+kscrolldown(const Arg *arg)
+{
+       view_offset -= arg->i;
+       if(view_offset < 0)
+               view_offset = 0;
+       redraw ();
+}
+
 void
 tdectest(char c)
 {
@@ -2575,6 +2798,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);
@@ -2662,9 +2888,12 @@ drawregion(int x1, int y1, int x2, int y2)
        for (y = y1; y < y2; y++) {
                if (!term.dirty[y])
                        continue;
-
+               
                term.dirty[y] = 0;
-               xdrawline(term.line[y], x1, y, x2);
+               Line line = getlineforrender(y);
+               if (!line)
+                       continue;
+               xdrawline(line, x1, y, x2);
        }
 }
 
@@ -2685,10 +2914,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 (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..db94fa1 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 int scrollback_lines;
diff --git a/x.c b/x.c
index d73152b..07ad9b1 100644
--- a/x.c
+++ b/x.c
@@ -4,6 +4,7 @@
 #include <limits.h>
 #include <locale.h>
 #include <signal.h>
+#include <stdio.h>
 #include <sys/select.h>
 #include <time.h>
 #include <unistd.h>
@@ -471,6 +472,22 @@ bpress(XEvent *e)
        int btn = e->xbutton.button;
        struct timespec now;
        int snap;
+       if (btn == Button4 || btn == Button5) {
+                       if (IS_SET(MODE_MOUSE) && !(e->xbutton.state & 
forcemousemod)) {
+                                       mousereport(e);
+                                       return;
+                       }
+
+                       if (!tisaltscreen()) {
+                                       Arg a = {.i = 1};
+                                       if (btn == Button4) {
+                                                       kscrollup(&a);
+                                       } else {
+                                                       kscrolldown(&a);
+                                       }
+                       }
+                       return;
+       }
 
        if (1 <= btn && btn <= 11)
                buttons |= 1 << (btn-1);
-- 
2.52.0


Reply via email to