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