Milos Nikic
unread,Jan 11, 2026, 9:19:40 AM (7 days ago) Jan 11Sign in to reply to author
Sign in to forward
You do not have permission to delete messages in this group
Either email addresses are anonymous for this group or you need the view member email addresses permission to view the original message
to d...@suckless.org, Milos Nikic
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