commit 0663bdf11a409961da5b1120741a69814da8ce65
Author: Timo Röhling <timo@gaussglocke.de>
Date:   Tue Nov 23 19:45:33 2021 +0100

    Terminal scrollback with ring buffer
    
    This patch adds a ring buffer for scrollback to the terminal.  The
    advantage of using a ring buffer is that the common case, scrolling with
    no static screen content, can be achieved very efficiently by
    incrementing and decrementing the starting line (modulo buffer size).
    
    The scrollback buffer is limited to HISTSIZE lines in order to bound
    memory usage. As the lines are allocated on demand, it is possible to
    implement unlimited scrollback with few changes.  If the terminal is
    reset, the scroll back buffer is reset, too.

diff --git a/config.def.h b/config.def.h
index 2cd740a..8b25d40 100644
--- a/config.def.h
+++ b/config.def.h
@@ -201,6 +201,8 @@ static Shortcut shortcuts[] = {
 	{ TERMMOD,              XK_Y,           selpaste,       {.i =  0} },
 	{ ShiftMask,            XK_Insert,      selpaste,       {.i =  0} },
 	{ TERMMOD,              XK_Num_Lock,    numlock,        {.i =  0} },
+	{ ShiftMask,            XK_Page_Up,     kscrollup,      {.i = -1} },
+	{ ShiftMask,            XK_Page_Down,   kscrolldown,    {.i = -1} },
 };
 
 /*
diff --git a/st.c b/st.c
index b9f66e7..d9b163e 100644
--- a/st.c
+++ b/st.c
@@ -43,6 +43,10 @@
 #define ISCONTROL(c)		(ISCONTROLC0(c) || ISCONTROLC1(c))
 #define ISDELIM(u)		(u && wcschr(worddelimiters, u))
 
+#define TSCREEN term.screen[IS_SET(MODE_ALTSCREEN)]
+#define TLINEOFFSET(y) (((y) + TSCREEN.cur - TSCREEN.off + TSCREEN.size) % TSCREEN.size)
+#define TLINE(y) (TSCREEN.buffer[TLINEOFFSET(y)])
+
 enum term_mode {
 	MODE_WRAP        = 1 << 0,
 	MODE_INSERT      = 1 << 1,
@@ -109,12 +113,21 @@ typedef struct {
 	int alt;
 } Selection;
 
+/* Screen lines */
+typedef struct {
+	Line* buffer;  /* ring buffer */
+	int size;      /* size of buffer */
+	int cur;       /* start of active screen */
+	int off;       /* scrollback line offset */
+	TCursor sc;    /* saved cursor */
+} LineBuffer;
+
 /* Internal representation of the screen */
 typedef struct {
 	int row;      /* nb row */
 	int col;      /* nb col */
-	Line *line;   /* screen */
-	Line *alt;    /* alternate screen */
+	LineBuffer screen[2]; /* screen and alternate screen */
+	int linelen;  /* allocated line length */
 	int *dirty;   /* dirtyness of lines */
 	TCursor c;    /* cursor */
 	int ocx;      /* old cursor col */
@@ -203,6 +216,8 @@ static void tdeftran(char);
 static void tstrsequence(uchar);
 
 static void drawregion(int, int, int, int);
+static void clearline(Line, Glyph, int, int);
+static Line ensureline(Line);
 
 static void selnormalize(void);
 static void selscroll(int, int);
@@ -408,11 +423,12 @@ int
 tlinelen(int y)
 {
 	int i = term.col;
+	Line line = TLINE(y);
 
-	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;
@@ -521,7 +537,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 = &TLINE(*y)[*x];
 		prevdelim = ISDELIM(prevgp->u);
 		for (;;) {
 			newx = *x + direction;
@@ -536,14 +552,14 @@ selsnap(int *x, int *y, int direction)
 					yt = *y, xt = *x;
 				else
 					yt = newy, xt = newx;
-				if (!(term.line[yt][xt].mode & ATTR_WRAP))
+				if (!(TLINE(yt)[xt].mode & ATTR_WRAP))
 					break;
 			}
 
 			if (newx >= tlinelen(newy))
 				break;
 
-			gp = &term.line[newy][newx];
+			gp = &TLINE(newy)[newx];
 			delim = ISDELIM(gp->u);
 			if (!(gp->mode & ATTR_WDUMMY) && (delim != prevdelim
 					|| (delim && gp->u != prevgp->u)))
@@ -564,14 +580,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 (!(TLINE(*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 (!(TLINE(*y)[term.col-1].mode
 						& ATTR_WRAP)) {
 					break;
 				}
@@ -602,13 +618,13 @@ getsel(void)
 		}
 
 		if (sel.type == SEL_RECTANGULAR) {
-			gp = &term.line[y][sel.nb.x];
+			gp = &TLINE(y)[sel.nb.x];
 			lastx = sel.ne.x;
 		} else {
-			gp = &term.line[y][sel.nb.y == y ? sel.nb.x : 0];
+			gp = &TLINE(y)[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 = &TLINE(y)[MIN(lastx, linelen-1)];
 		while (last >= gp && last->u == ' ')
 			--last;
 
@@ -949,12 +965,15 @@ int
 tattrset(int attr)
 {
 	int i, j;
+	int y = TLINEOFFSET(0);
 
 	for (i = 0; i < term.row-1; i++) {
+		Line line = TSCREEN.buffer[y];
 		for (j = 0; j < term.col-1; j++) {
-			if (term.line[i][j].mode & attr)
+			if (line[j].mode & attr)
 				return 1;
 		}
+		y = (y+1) % TSCREEN.size;
 	}
 
 	return 0;
@@ -976,14 +995,17 @@ void
 tsetdirtattr(int attr)
 {
 	int i, j;
+	int y = TLINEOFFSET(0);
 
 	for (i = 0; i < term.row-1; i++) {
+		Line line = TSCREEN.buffer[y];
 		for (j = 0; j < term.col-1; j++) {
-			if (term.line[i][j].mode & attr) {
+			if (line[j].mode & attr) {
 				tsetdirt(i, i);
 				break;
 			}
 		}
+		y = (y+1) % TSCREEN.size;
 	}
 }
 
@@ -996,27 +1018,19 @@ tfulldirt(void)
 void
 tcursor(int mode)
 {
-	static TCursor c[2];
-	int alt = IS_SET(MODE_ALTSCREEN);
-
 	if (mode == CURSOR_SAVE) {
-		c[alt] = term.c;
+		TSCREEN.sc = term.c;
 	} else if (mode == CURSOR_LOAD) {
-		term.c = c[alt];
-		tmoveto(c[alt].x, c[alt].y);
+		term.c = TSCREEN.sc;
+		tmoveto(term.c.x, term.c.y);
 	}
 }
 
 void
 treset(void)
 {
-	uint i;
-
-	term.c = (TCursor){{
-		.mode = ATTR_NULL,
-		.fg = defaultfg,
-		.bg = defaultbg
-	}, .x = 0, .y = 0, .state = CURSOR_DEFAULT};
+	int i, j;
+	Glyph g = (Glyph){ .fg = defaultfg, .bg = defaultbg};
 
 	memset(term.tabs, 0, term.col * sizeof(*term.tabs));
 	for (i = tabspaces; i < term.col; i += tabspaces)
@@ -1028,17 +1042,37 @@ treset(void)
 	term.charset = 0;
 
 	for (i = 0; i < 2; i++) {
-		tmoveto(0, 0);
-		tcursor(CURSOR_SAVE);
-		tclearregion(0, 0, term.col-1, term.row-1);
-		tswapscreen();
+		term.screen[i].sc = (TCursor){{
+			.fg = defaultfg,
+			.bg = defaultbg
+		}};
+		term.screen[i].cur = 0;
+		term.screen[i].off = 0;
+		for (j = 0; j < term.row; ++j) {
+			if (term.col != term.linelen)
+				term.screen[i].buffer[j] = xrealloc(term.screen[i].buffer[j], term.col * sizeof(Glyph));
+			clearline(term.screen[i].buffer[j], g, 0, term.col);
+		}
+		for (j = term.row; j < term.screen[i].size; ++j) {
+			free(term.screen[i].buffer[j]);
+			term.screen[i].buffer[j] = NULL;
+		}
 	}
+	tcursor(CURSOR_LOAD);
+	term.linelen = term.col;
+	tfulldirt();
 }
 
 void
 tnew(int col, int row)
 {
-	term = (Term){ .c = { .attr = { .fg = defaultfg, .bg = defaultbg } } };
+	int i;
+	term = (Term){};
+	term.screen[0].buffer = xmalloc(HISTSIZE * sizeof(Line));
+	term.screen[0].size = HISTSIZE;
+	term.screen[1].buffer = NULL;
+	for (i = 0; i < HISTSIZE; ++i) term.screen[0].buffer[i] = NULL;
+
 	tresize(col, row);
 	treset();
 }
@@ -1046,14 +1080,42 @@ tnew(int col, int row)
 void
 tswapscreen(void)
 {
-	Line *tmp = term.line;
-
-	term.line = term.alt;
-	term.alt = tmp;
 	term.mode ^= MODE_ALTSCREEN;
 	tfulldirt();
 }
 
+void
+kscrollup(const Arg *a)
+{
+	int n = a->i;
+
+	if (IS_SET(MODE_ALTSCREEN))
+		return;
+
+	if (n < 0) n = (-n) * term.row;
+	if (n > TSCREEN.size - term.row - TSCREEN.off) n = TSCREEN.size - term.row - TSCREEN.off;
+	while (!TLINE(-n)) --n;
+	TSCREEN.off += n;
+	selscroll(0, n);
+	tfulldirt();
+}
+
+void
+kscrolldown(const Arg *a)
+{
+
+	int n = a->i;
+
+	if (IS_SET(MODE_ALTSCREEN))
+		return;
+
+	if (n < 0) n = (-n) * term.row;
+	if (n > TSCREEN.off) n = TSCREEN.off;
+	TSCREEN.off -= n;
+	selscroll(0, -n);
+	tfulldirt();
+}
+
 void
 tscrolldown(int orig, int n)
 {
@@ -1062,15 +1124,29 @@ tscrolldown(int orig, int n)
 
 	LIMIT(n, 0, term.bot-orig+1);
 
-	tsetdirt(orig, term.bot-n);
-	tclearregion(0, term.bot-n+1, term.col-1, term.bot);
+	/* Ensure that lines are allocated */
+	for (i = -n; i < 0; i++) {
+		TLINE(i) = ensureline(TLINE(i));
+	}
 
-	for (i = term.bot; i >= orig+n; i--) {
-		temp = term.line[i];
-		term.line[i] = term.line[i-n];
-		term.line[i-n] = temp;
+	/* Shift non-scrolling areas in ring buffer */
+	for (i = term.bot+1; i < term.row; i++) {
+		temp = TLINE(i);
+		TLINE(i) = TLINE(i-n);
+		TLINE(i-n) = temp;
+	}
+	for (i = 0; i < orig; i++) {
+		temp = TLINE(i);
+		TLINE(i) = TLINE(i-n);
+		TLINE(i-n) = temp;
 	}
 
+	/* Scroll buffer */
+	TSCREEN.cur = (TSCREEN.cur + TSCREEN.size - n) % TSCREEN.size;
+	/* Clear lines that have entered the view */
+	tclearregion(0, orig, term.linelen-1, orig+n-1);
+	/* Redraw portion of the screen that has scrolled */
+	tsetdirt(orig+n-1, term.bot);
 	selscroll(orig, n);
 }
 
@@ -1082,15 +1158,29 @@ tscrollup(int orig, int n)
 
 	LIMIT(n, 0, term.bot-orig+1);
 
-	tclearregion(0, orig, term.col-1, orig+n-1);
-	tsetdirt(orig+n, term.bot);
+	/* Ensure that lines are allocated */
+	for (i = term.row; i < term.row + n; i++) {
+		TLINE(i) = ensureline(TLINE(i));
+	}
 
-	for (i = orig; i <= term.bot-n; i++) {
-		temp = term.line[i];
-		term.line[i] = term.line[i+n];
-		term.line[i+n] = temp;
+	/* Shift non-scrolling areas in ring buffer */
+	for (i = orig-1; i >= 0; i--) {
+		temp = TLINE(i);
+		TLINE(i) = TLINE(i+n);
+		TLINE(i+n) = temp;
+	}
+	for (i = term.row-1; i >term.bot; i--) {
+		temp = TLINE(i);
+		TLINE(i) = TLINE(i+n);
+		TLINE(i+n) = temp;
 	}
 
+	/* Scroll buffer */
+	TSCREEN.cur = (TSCREEN.cur + n) % TSCREEN.size;
+	/* Clear lines that have entered the view */
+	tclearregion(0, term.bot-n+1, term.linelen-1, term.bot);
+	/* Redraw portion of the screen that has scrolled */
+	tsetdirt(orig, term.bot-n+1);
 	selscroll(orig, -n);
 }
 
@@ -1194,6 +1284,7 @@ tsetchar(Rune u, const Glyph *attr, int x, int y)
 		"⎻", "─", "⎼", "⎽", "├", "┤", "┴", "┬", /* p - w */
 		"│", "≤", "≥", "π", "≠", "£", "·", /* x - ~ */
 	};
+	Line line = TLINE(y);
 
 	/*
 	 * The table is proudly stolen from rxvt.
@@ -1202,25 +1293,25 @@ tsetchar(Rune u, const Glyph *attr, int x, int y)
 	   BETWEEN(u, 0x41, 0x7e) && vt100_0[u - 0x41])
 		utf8decode(vt100_0[u - 0x41], &u, UTF_SIZ);
 
-	if (term.line[y][x].mode & ATTR_WIDE) {
+	if (line[x].mode & ATTR_WIDE) {
 		if (x+1 < term.col) {
-			term.line[y][x+1].u = ' ';
-			term.line[y][x+1].mode &= ~ATTR_WDUMMY;
+			line[x+1].u = ' ';
+			line[x+1].mode &= ~ATTR_WDUMMY;
 		}
-	} else if (term.line[y][x].mode & ATTR_WDUMMY) {
-		term.line[y][x-1].u = ' ';
-		term.line[y][x-1].mode &= ~ATTR_WIDE;
+	} else if (line[x].mode & ATTR_WDUMMY) {
+		line[x-1].u = ' ';
+		line[x-1].mode &= ~ATTR_WIDE;
 	}
 
 	term.dirty[y] = 1;
-	term.line[y][x] = *attr;
-	term.line[y][x].u = u;
+	line[x] = *attr;
+	line[x].u = u;
 }
 
 void
 tclearregion(int x1, int y1, int x2, int y2)
 {
-	int x, y, temp;
+	int x, y, L, S, temp;
 	Glyph *gp;
 
 	if (x1 > x2)
@@ -1228,15 +1319,16 @@ tclearregion(int x1, int y1, int x2, int y2)
 	if (y1 > y2)
 		temp = y1, y1 = y2, y2 = temp;
 
-	LIMIT(x1, 0, term.col-1);
-	LIMIT(x2, 0, term.col-1);
+	LIMIT(x1, 0, term.linelen-1);
+	LIMIT(x2, 0, term.linelen-1);
 	LIMIT(y1, 0, term.row-1);
 	LIMIT(y2, 0, term.row-1);
 
+	L = TLINEOFFSET(y1);
 	for (y = y1; y <= y2; y++) {
 		term.dirty[y] = 1;
 		for (x = x1; x <= x2; x++) {
-			gp = &term.line[y][x];
+			gp = &TSCREEN.buffer[L][x];
 			if (selected(x, y))
 				selclear();
 			gp->fg = term.c.attr.fg;
@@ -1244,6 +1336,7 @@ tclearregion(int x1, int y1, int x2, int y2)
 			gp->mode = 0;
 			gp->u = ' ';
 		}
+		L = (L + 1) % TSCREEN.size;
 	}
 }
 
@@ -1258,7 +1351,7 @@ tdeletechar(int n)
 	dst = term.c.x;
 	src = term.c.x + n;
 	size = term.col - src;
-	line = term.line[term.c.y];
+	line = TLINE(term.c.y);
 
 	memmove(&line[dst], &line[src], size * sizeof(Glyph));
 	tclearregion(term.col-n, term.c.y, term.col-1, term.c.y);
@@ -1275,7 +1368,7 @@ tinsertblank(int n)
 	dst = term.c.x + n;
 	src = term.c.x;
 	size = term.col - dst;
-	line = term.line[term.c.y];
+	line = TLINE(term.c.y);
 
 	memmove(&line[dst], &line[src], size * sizeof(Glyph));
 	tclearregion(src, term.c.y, dst - 1, term.c.y);
@@ -2079,7 +2172,7 @@ tdumpline(int n)
 	char buf[UTF_SIZ];
 	const Glyph *bp, *end;
 
-	bp = &term.line[n][0];
+	bp = &TLINE(n)[0];
 	end = &bp[MIN(tlinelen(n), term.col) - 1];
 	if (bp != end || bp->u != ' ') {
 		for ( ; bp <= end; ++bp)
@@ -2466,11 +2559,11 @@ check_control_code:
 	if (selected(term.c.x, term.c.y))
 		selclear();
 
-	gp = &term.line[term.c.y][term.c.x];
+	gp = &TLINE(term.c.y)[term.c.x];
 	if (IS_SET(MODE_WRAP) && (term.c.state & CURSOR_WRAPNEXT)) {
 		gp->mode |= ATTR_WRAP;
 		tnewline(1);
-		gp = &term.line[term.c.y][term.c.x];
+		gp = &TLINE(term.c.y)[term.c.x];
 	}
 
 	if (IS_SET(MODE_INSERT) && term.c.x+width < term.col) {
@@ -2483,7 +2576,7 @@ check_control_code:
 			tnewline(1);
 		else
 			tmoveto(term.col - width, term.c.y);
-		gp = &term.line[term.c.y][term.c.x];
+		gp = &TLINE(term.c.y)[term.c.x];
 	}
 
 	tsetchar(u, &term.c.attr, term.c.x, term.c.y);
@@ -2514,6 +2607,11 @@ twrite(const char *buf, int buflen, int show_ctrl)
 	Rune u;
 	int n;
 
+	if (TSCREEN.off) {
+		TSCREEN.off = 0;
+		tfulldirt();
+	}
+
 	for (n = 0; n < buflen; n += charsize) {
 		if (IS_SET(MODE_UTF8)) {
 			/* process a complete utf8 char */
@@ -2540,56 +2638,85 @@ twrite(const char *buf, int buflen, int show_ctrl)
 }
 
 void
-tresize(int col, int row)
+clearline(Line line, Glyph g, int x, int xend)
 {
 	int i;
+	g.mode = 0;
+	g.u = ' ';
+	for (i = x; i < xend; ++i) {
+		line[i] = g;
+	}
+}
+
+Line
+ensureline(Line line)
+{
+	if (!line) {
+		line = xmalloc(term.linelen * sizeof(Glyph));
+	}
+	return line;
+}
+
+void
+tresize(int col, int row)
+{
+	int i, j;
 	int minrow = MIN(row, term.row);
 	int mincol = MIN(col, term.col);
+	int linelen = MAX(col, term.linelen);
 	int *bp;
-	TCursor c;
 
-	if (col < 1 || row < 1) {
+	if (col < 1 || row < 1 || row > HISTSIZE) {
 		fprintf(stderr,
 		        "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]);
+	/* Shift buffer to keep the cursor where we expect it */
+	if (row <= term.c.y) {
+		term.screen[0].cur = (term.screen[0].cur - row + term.c.y + 1) % term.screen[0].size;
+	}
+
+	/* Resize and clear line buffers as needed */
+	if (linelen > term.linelen) {
+		for (i = 0; i < term.screen[0].size; ++i) {
+			if (term.screen[0].buffer[i]) {
+				term.screen[0].buffer[i] = xrealloc(term.screen[0].buffer[i], linelen * sizeof(Glyph));
+				clearline(term.screen[0].buffer[i], term.c.attr, term.linelen, linelen);
+			}
+		}
+		for (i = 0; i < minrow; ++i) {
+			term.screen[1].buffer[i] = xrealloc(term.screen[1].buffer[i], linelen * sizeof(Glyph));
+			clearline(term.screen[1].buffer[i], term.c.attr, term.linelen, linelen);
+		}
 	}
-	/* 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));
+	/* Allocate all visible lines for regular line buffer */
+	for (j = term.screen[0].cur, i = 0; i < row; ++i, j = (j + 1) % term.screen[0].size)
+	{
+		if (!term.screen[0].buffer[j]) {
+			term.screen[0].buffer[j] = xmalloc(linelen * sizeof(Glyph));
+		}
+		if (i >= term.row) {
+			clearline(term.screen[0].buffer[j], term.c.attr, 0, linelen);
+		}
 	}
-	for (i += row; i < term.row; i++) {
-		free(term.line[i]);
-		free(term.alt[i]);
+	/* Resize alt screen */
+	term.screen[1].cur = 0;
+	term.screen[1].size = row;
+	for (i = row; i < term.row; ++i) {
+		free(term.screen[1].buffer[i]);
+	}
+	term.screen[1].buffer = xrealloc(term.screen[1].buffer, row * sizeof(Line));
+	for (i = term.row; i < row; ++i) {
+		term.screen[1].buffer[i] = xmalloc(linelen * sizeof(Glyph));
+		clearline(term.screen[1].buffer[i], term.c.attr, 0, linelen);
 	}
 
 	/* 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));
 
-	/* 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));
-	}
-
-	/* allocate any new rows */
-	for (/* i = minrow */; i < row; i++) {
-		term.line[i] = xmalloc(col * sizeof(Glyph));
-		term.alt[i] = xmalloc(col * sizeof(Glyph));
-	}
+	/* fix tabstops */
 	if (col > term.col) {
 		bp = term.tabs + term.col;
 
@@ -2599,26 +2726,16 @@ tresize(int col, int row)
 		for (bp += tabspaces; bp < term.tabs + col; bp += tabspaces)
 			*bp = 1;
 	}
+
 	/* update terminal size */
 	term.col = col;
 	term.row = row;
+	term.linelen = linelen;
 	/* 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);
-		}
-		tswapscreen();
-		tcursor(CURSOR_LOAD);
-	}
-	term.c = c;
+	tfulldirt();
 }
 
 void
@@ -2630,14 +2747,15 @@ resettitle(void)
 void
 drawregion(int x1, int y1, int x2, int y2)
 {
-	int y;
+	int y, L;
 
+	L = TLINEOFFSET(y1);
 	for (y = y1; y < y2; y++) {
-		if (!term.dirty[y])
-			continue;
-
-		term.dirty[y] = 0;
-		xdrawline(term.line[y], x1, y, x2);
+		if (term.dirty[y]) {
+			term.dirty[y] = 0;
+			xdrawline(TSCREEN.buffer[L], x1, y, x2);
+		}
+		L = (L + 1) % TSCREEN.size;
 	}
 }
 
@@ -2652,14 +2770,15 @@ draw(void)
 	/* adjust cursor position */
 	LIMIT(term.ocx, 0, term.col-1);
 	LIMIT(term.ocy, 0, term.row-1);
-	if (term.line[term.ocy][term.ocx].mode & ATTR_WDUMMY)
+	if (TLINE(term.ocy)[term.ocx].mode & ATTR_WDUMMY)
 		term.ocx--;
-	if (term.line[term.c.y][cx].mode & ATTR_WDUMMY)
+	if (TLINE(term.c.y)[cx].mode & ATTR_WDUMMY)
 		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]);
+	if (TSCREEN.off == 0)
+		xdrawcursor(cx, term.c.y, TLINE(term.c.y)[cx],
+				term.ocx, term.ocy, TLINE(term.ocy)[term.ocx]);
 	term.ocx = cx;
 	term.ocy = term.c.y;
 	xfinishdraw();
diff --git a/st.h b/st.h
index fd3b0d8..3cea73b 100644
--- a/st.h
+++ b/st.h
@@ -19,6 +19,7 @@
 
 #define TRUECOLOR(r,g,b)	(1 << 24 | (r) << 16 | (g) << 8 | (b))
 #define IS_TRUECOL(x)		(1 << 24 & (x))
+#define HISTSIZE            2000
 
 enum glyph_attribute {
 	ATTR_NULL       = 0,
diff --git a/x.c b/x.c
index bd23686..25785a6 100644
--- a/x.c
+++ b/x.c
@@ -59,6 +59,8 @@ static void zoom(const Arg *);
 static void zoomabs(const Arg *);
 static void zoomreset(const Arg *);
 static void ttysend(const Arg *);
+void kscrollup(const Arg *);
+void kscrolldown(const Arg *);
 
 /* config.h for applying patches and the configuration. */
 #include "config.h"
