sites

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

st-scrollback-ringbuffer-20261108-fdaadd5.diff (20012B)


      1 From 3ac76861150ee604882cb95f444cdba3a1d8b366 Mon Sep 17 00:00:00 2001
      2 From: venus brock <venus@brock-v.dev>
      3 Date: Sun, 8 Nov 2026 18:16:26 +0000
      4 Subject: [PATCH] Terminal scrollback with ring buffer
      5 MIME-Version: 1.0
      6 Content-Type: text/plain; charset=UTF-8
      7 Content-Transfer-Encoding: 8bit
      8 
      9 This patch adds a ring buffer for scrollback to the terminal.  The
     10 advantage of using a ring buffer is that the common case, scrolling with
     11 no static screen content, can be achieved very efficiently by
     12 incrementing and decrementing the starting line (modulo buffer size).
     13 
     14 The scrollback buffer is limited to HISTSIZE lines in order to bound
     15 memory usage. As the lines are allocated on demand, it is possible to
     16 implement unlimited scrollback with few changes.  If the terminal is
     17 reset, the scroll back buffer is reset, too.
     18 
     19 Originally authored by Timo Röhling, updated for commit fdaadd5 by Venus
     20 Brock.
     21 ---
     22  config.def.h |   2 +
     23  st.c         | 359 ++++++++++++++++++++++++++++++++++-----------------
     24  st.h         |   1 +
     25  x.c          |   2 +
     26  4 files changed, 244 insertions(+), 120 deletions(-)
     27 
     28 diff --git a/config.def.h b/config.def.h
     29 index fd2bd87..089e265 100644
     30 --- a/config.def.h
     31 +++ b/config.def.h
     32 @@ -201,6 +201,8 @@ static Shortcut shortcuts[] = {
     33  	{ TERMMOD,              XK_Y,           selpaste,       {.i =  0} },
     34  	{ ShiftMask,            XK_Insert,      selpaste,       {.i =  0} },
     35  	{ TERMMOD,              XK_Num_Lock,    numlock,        {.i =  0} },
     36 +	{ ShiftMask,            XK_Page_Up,     kscrollup,      {.i = -1} },
     37 +	{ ShiftMask,            XK_Page_Down,   kscrolldown,    {.i = -1} },
     38  };
     39  
     40  /*
     41 diff --git a/st.c b/st.c
     42 index c289072..c701923 100644
     43 --- a/st.c
     44 +++ b/st.c
     45 @@ -43,6 +43,10 @@
     46  #define ISCONTROL(c)		(ISCONTROLC0(c) || ISCONTROLC1(c))
     47  #define ISDELIM(u)		(u && wcschr(worddelimiters, u))
     48  
     49 +#define TSCREEN term.screen[IS_SET(MODE_ALTSCREEN)]
     50 +#define TLINEOFFSET(y) (((y) + TSCREEN.cur - TSCREEN.off + TSCREEN.size) % TSCREEN.size)
     51 +#define TLINE(y) (TSCREEN.buffer[TLINEOFFSET(y)])
     52 +
     53  enum term_mode {
     54  	MODE_WRAP        = 1 << 0,
     55  	MODE_INSERT      = 1 << 1,
     56 @@ -109,12 +113,21 @@ typedef struct {
     57  	int alt;
     58  } Selection;
     59  
     60 +/* Screen lines */
     61 +typedef struct {
     62 +	Line* buffer;  /* ring buffer */
     63 +	int size;      /* size of buffer */
     64 +	int cur;       /* start of active screen */
     65 +	int off;       /* scrollback line offset */
     66 +	TCursor sc;    /* saved cursor */
     67 +} LineBuffer;
     68 +
     69  /* Internal representation of the screen */
     70  typedef struct {
     71  	int row;      /* nb row */
     72  	int col;      /* nb col */
     73 -	Line *line;   /* screen */
     74 -	Line *alt;    /* alternate screen */
     75 +	LineBuffer screen[2]; /* screen and alternate screen */
     76 +	int linelen;  /* allocated line length */
     77  	int *dirty;   /* dirtyness of lines */
     78  	TCursor c;    /* cursor */
     79  	int ocx;      /* old cursor col */
     80 @@ -203,6 +216,8 @@ static void tdeftran(char);
     81  static void tstrsequence(uchar);
     82  
     83  static void drawregion(int, int, int, int);
     84 +static void clearline(Line, Glyph, int, int);
     85 +static Line ensureline(Line);
     86  
     87  static void selnormalize(void);
     88  static void selscroll(int, int);
     89 @@ -408,11 +423,12 @@ int
     90  tlinelen(int y)
     91  {
     92  	int i = term.col;
     93 +	Line line = TLINE(y);
     94  
     95 -	if (term.line[y][i - 1].mode & ATTR_WRAP)
     96 +	if (line[i - 1].mode & ATTR_WRAP)
     97  		return i;
     98  
     99 -	while (i > 0 && term.line[y][i - 1].u == ' ')
    100 +	while (i > 0 && line[i - 1].u == ' ')
    101  		--i;
    102  
    103  	return i;
    104 @@ -521,7 +537,7 @@ selsnap(int *x, int *y, int direction)
    105  		 * Snap around if the word wraps around at the end or
    106  		 * beginning of a line.
    107  		 */
    108 -		prevgp = &term.line[*y][*x];
    109 +		prevgp = &TLINE(*y)[*x];
    110  		prevdelim = ISDELIM(prevgp->u);
    111  		for (;;) {
    112  			newx = *x + direction;
    113 @@ -536,14 +552,14 @@ selsnap(int *x, int *y, int direction)
    114  					yt = *y, xt = *x;
    115  				else
    116  					yt = newy, xt = newx;
    117 -				if (!(term.line[yt][xt].mode & ATTR_WRAP))
    118 +				if (!(TLINE(yt)[xt].mode & ATTR_WRAP))
    119  					break;
    120  			}
    121  
    122  			if (newx >= tlinelen(newy))
    123  				break;
    124  
    125 -			gp = &term.line[newy][newx];
    126 +			gp = &TLINE(newy)[newx];
    127  			delim = ISDELIM(gp->u);
    128  			if (!(gp->mode & ATTR_WDUMMY) && (delim != prevdelim
    129  					|| (delim && gp->u != prevgp->u)))
    130 @@ -564,14 +580,14 @@ selsnap(int *x, int *y, int direction)
    131  		*x = (direction < 0) ? 0 : term.col - 1;
    132  		if (direction < 0) {
    133  			for (; *y > 0; *y += direction) {
    134 -				if (!(term.line[*y-1][term.col-1].mode
    135 +				if (!(TLINE(*y-1)[term.col-1].mode
    136  						& ATTR_WRAP)) {
    137  					break;
    138  				}
    139  			}
    140  		} else if (direction > 0) {
    141  			for (; *y < term.row-1; *y += direction) {
    142 -				if (!(term.line[*y][term.col-1].mode
    143 +				if (!(TLINE(*y)[term.col-1].mode
    144  						& ATTR_WRAP)) {
    145  					break;
    146  				}
    147 @@ -602,13 +618,13 @@ getsel(void)
    148  		}
    149  
    150  		if (sel.type == SEL_RECTANGULAR) {
    151 -			gp = &term.line[y][sel.nb.x];
    152 +			gp = &TLINE(y)[sel.nb.x];
    153  			lastx = sel.ne.x;
    154  		} else {
    155 -			gp = &term.line[y][sel.nb.y == y ? sel.nb.x : 0];
    156 +			gp = &TLINE(y)[sel.nb.y == y ? sel.nb.x : 0];
    157  			lastx = (sel.ne.y == y) ? sel.ne.x : term.col-1;
    158  		}
    159 -		last = &term.line[y][MIN(lastx, linelen-1)];
    160 +		last = &TLINE(y)[MIN(lastx, linelen-1)];
    161  		while (last >= gp && last->u == ' ')
    162  			--last;
    163  
    164 @@ -954,12 +970,15 @@ int
    165  tattrset(int attr)
    166  {
    167  	int i, j;
    168 +	int y = TLINEOFFSET(0);
    169  
    170  	for (i = 0; i < term.row-1; i++) {
    171 +		Line line = TSCREEN.buffer[y];
    172  		for (j = 0; j < term.col-1; j++) {
    173 -			if (term.line[i][j].mode & attr)
    174 +			if (line[j].mode & attr)
    175  				return 1;
    176  		}
    177 +		y = (y+1) % TSCREEN.size;
    178  	}
    179  
    180  	return 0;
    181 @@ -984,14 +1003,17 @@ void
    182  tsetdirtattr(int attr)
    183  {
    184  	int i, j;
    185 +	int y = TLINEOFFSET(0);
    186  
    187  	for (i = 0; i < term.row-1; i++) {
    188 +		Line line = TSCREEN.buffer[y];
    189  		for (j = 0; j < term.col-1; j++) {
    190 -			if (term.line[i][j].mode & attr) {
    191 +			if (line[j].mode & attr) {
    192  				tsetdirt(i, i);
    193  				break;
    194  			}
    195  		}
    196 +		y = (y+1) % TSCREEN.size;
    197  	}
    198  }
    199  
    200 @@ -1004,27 +1026,19 @@ tfulldirt(void)
    201  void
    202  tcursor(int mode)
    203  {
    204 -	static TCursor c[2];
    205 -	int alt = IS_SET(MODE_ALTSCREEN);
    206 -
    207  	if (mode == CURSOR_SAVE) {
    208 -		c[alt] = term.c;
    209 +		TSCREEN.sc = term.c;
    210  	} else if (mode == CURSOR_LOAD) {
    211 -		term.c = c[alt];
    212 -		tmoveto(c[alt].x, c[alt].y);
    213 +		term.c = TSCREEN.sc;
    214 +		tmoveto(term.c.x, term.c.y);
    215  	}
    216  }
    217  
    218  void
    219  treset(void)
    220  {
    221 -	uint i;
    222 -
    223 -	term.c = (TCursor){{
    224 -		.mode = ATTR_NULL,
    225 -		.fg = defaultfg,
    226 -		.bg = defaultbg
    227 -	}, .x = 0, .y = 0, .state = CURSOR_DEFAULT};
    228 +	int i, j;
    229 +	Glyph g = (Glyph){ .fg = defaultfg, .bg = defaultbg};
    230  
    231  	memset(term.tabs, 0, term.col * sizeof(*term.tabs));
    232  	for (i = tabspaces; i < term.col; i += tabspaces)
    233 @@ -1036,17 +1050,37 @@ treset(void)
    234  	term.charset = 0;
    235  
    236  	for (i = 0; i < 2; i++) {
    237 -		tmoveto(0, 0);
    238 -		tcursor(CURSOR_SAVE);
    239 -		tclearregion(0, 0, term.col-1, term.row-1);
    240 -		tswapscreen();
    241 +		term.screen[i].sc = (TCursor){{
    242 +			.fg = defaultfg,
    243 +			.bg = defaultbg
    244 +		}};
    245 +		term.screen[i].cur = 0;
    246 +		term.screen[i].off = 0;
    247 +		for (j = 0; j < term.row; ++j) {
    248 +			if (term.col != term.linelen)
    249 +				term.screen[i].buffer[j] = xrealloc(term.screen[i].buffer[j], term.col * sizeof(Glyph));
    250 +			clearline(term.screen[i].buffer[j], g, 0, term.col);
    251 +		}
    252 +		for (j = term.row; j < term.screen[i].size; ++j) {
    253 +			free(term.screen[i].buffer[j]);
    254 +			term.screen[i].buffer[j] = NULL;
    255 +		}
    256  	}
    257 +	tcursor(CURSOR_LOAD);
    258 +	term.linelen = term.col;
    259 +	tfulldirt();
    260  }
    261  
    262  void
    263  tnew(int col, int row)
    264  {
    265 -	term = (Term){ .c = { .attr = { .fg = defaultfg, .bg = defaultbg } } };
    266 +	int i;
    267 +	term = (Term){};
    268 +	term.screen[0].buffer = xmalloc(HISTSIZE * sizeof(Line));
    269 +	term.screen[0].size = HISTSIZE;
    270 +	term.screen[1].buffer = NULL;
    271 +	for (i = 0; i < HISTSIZE; ++i) term.screen[0].buffer[i] = NULL;
    272 +
    273  	tresize(col, row);
    274  	treset();
    275  }
    276 @@ -1054,14 +1088,42 @@ tnew(int col, int row)
    277  void
    278  tswapscreen(void)
    279  {
    280 -	Line *tmp = term.line;
    281 -
    282 -	term.line = term.alt;
    283 -	term.alt = tmp;
    284  	term.mode ^= MODE_ALTSCREEN;
    285  	tfulldirt();
    286  }
    287  
    288 +void
    289 +kscrollup(const Arg *a)
    290 +{
    291 +	int n = a->i;
    292 +
    293 +	if (IS_SET(MODE_ALTSCREEN))
    294 +		return;
    295 +
    296 +	if (n < 0) n = (-n) * term.row;
    297 +	if (n > TSCREEN.size - term.row - TSCREEN.off) n = TSCREEN.size - term.row - TSCREEN.off;
    298 +	while (!TLINE(-n)) --n;
    299 +	TSCREEN.off += n;
    300 +	selscroll(0, n);
    301 +	tfulldirt();
    302 +}
    303 +
    304 +void
    305 +kscrolldown(const Arg *a)
    306 +{
    307 +
    308 +	int n = a->i;
    309 +
    310 +	if (IS_SET(MODE_ALTSCREEN))
    311 +		return;
    312 +
    313 +	if (n < 0) n = (-n) * term.row;
    314 +	if (n > TSCREEN.off) n = TSCREEN.off;
    315 +	TSCREEN.off -= n;
    316 +	selscroll(0, -n);
    317 +	tfulldirt();
    318 +}
    319 +
    320  void
    321  tscrolldown(int orig, int n)
    322  {
    323 @@ -1070,15 +1132,29 @@ tscrolldown(int orig, int n)
    324  
    325  	LIMIT(n, 0, term.bot-orig+1);
    326  
    327 -	tsetdirt(orig, term.bot-n);
    328 -	tclearregion(0, term.bot-n+1, term.col-1, term.bot);
    329 +	/* Ensure that lines are allocated */
    330 +	for (i = -n; i < 0; i++) {
    331 +		TLINE(i) = ensureline(TLINE(i));
    332 +	}
    333  
    334 -	for (i = term.bot; i >= orig+n; i--) {
    335 -		temp = term.line[i];
    336 -		term.line[i] = term.line[i-n];
    337 -		term.line[i-n] = temp;
    338 +	/* Shift non-scrolling areas in ring buffer */
    339 +	for (i = term.bot+1; i < term.row; i++) {
    340 +		temp = TLINE(i);
    341 +		TLINE(i) = TLINE(i-n);
    342 +		TLINE(i-n) = temp;
    343 +	}
    344 +	for (i = 0; i < orig; i++) {
    345 +		temp = TLINE(i);
    346 +		TLINE(i) = TLINE(i-n);
    347 +		TLINE(i-n) = temp;
    348  	}
    349  
    350 +	/* Scroll buffer */
    351 +	TSCREEN.cur = (TSCREEN.cur + TSCREEN.size - n) % TSCREEN.size;
    352 +	/* Clear lines that have entered the view */
    353 +	tclearregion(0, orig, term.linelen-1, orig+n-1);
    354 +	/* Redraw portion of the screen that has scrolled */
    355 +	tsetdirt(orig+n-1, term.bot);
    356  	selscroll(orig, n);
    357  }
    358  
    359 @@ -1090,15 +1166,29 @@ tscrollup(int orig, int n)
    360  
    361  	LIMIT(n, 0, term.bot-orig+1);
    362  
    363 -	tclearregion(0, orig, term.col-1, orig+n-1);
    364 -	tsetdirt(orig+n, term.bot);
    365 +	/* Ensure that lines are allocated */
    366 +	for (i = term.row; i < term.row + n; i++) {
    367 +		TLINE(i) = ensureline(TLINE(i));
    368 +	}
    369  
    370 -	for (i = orig; i <= term.bot-n; i++) {
    371 -		temp = term.line[i];
    372 -		term.line[i] = term.line[i+n];
    373 -		term.line[i+n] = temp;
    374 +	/* Shift non-scrolling areas in ring buffer */
    375 +	for (i = orig-1; i >= 0; i--) {
    376 +		temp = TLINE(i);
    377 +		TLINE(i) = TLINE(i+n);
    378 +		TLINE(i+n) = temp;
    379 +	}
    380 +	for (i = term.row-1; i >term.bot; i--) {
    381 +		temp = TLINE(i);
    382 +		TLINE(i) = TLINE(i+n);
    383 +		TLINE(i+n) = temp;
    384  	}
    385  
    386 +	/* Scroll buffer */
    387 +	TSCREEN.cur = (TSCREEN.cur + n) % TSCREEN.size;
    388 +	/* Clear lines that have entered the view */
    389 +	tclearregion(0, term.bot-n+1, term.linelen-1, term.bot);
    390 +	/* Redraw portion of the screen that has scrolled */
    391 +	tsetdirt(orig, term.bot-n+1);
    392  	selscroll(orig, -n);
    393  }
    394  
    395 @@ -1205,6 +1295,7 @@ tsetchar(Rune u, const Glyph *attr, int x, int y)
    396  		"⎻", "─", "⎼", "⎽", "├", "┤", "┴", "┬", /* p - w */
    397  		"│", "≤", "≥", "π", "≠", "£", "·", /* x - ~ */
    398  	};
    399 +	Line line = TLINE(y);
    400  
    401  	/*
    402  	 * The table is proudly stolen from rxvt.
    403 @@ -1213,27 +1304,27 @@ tsetchar(Rune u, const Glyph *attr, int x, int y)
    404  	   BETWEEN(u, 0x41, 0x7e) && vt100_0[u - 0x41])
    405  		utf8decode(vt100_0[u - 0x41], &u, UTF_SIZ);
    406  
    407 -	if (term.line[y][x].mode & ATTR_WIDE) {
    408 +	if (line[x].mode & ATTR_WIDE) {
    409  		if (x+1 < term.col) {
    410 -			term.line[y][x+1].u = ' ';
    411 -			term.line[y][x+1].mode &= ~ATTR_WDUMMY;
    412 +			line[x+1].u = ' ';
    413 +			line[x+1].mode &= ~ATTR_WDUMMY;
    414  		}
    415 -	} else if (term.line[y][x].mode & ATTR_WDUMMY) {
    416 +	} else if (line[x].mode & ATTR_WDUMMY) {
    417  		if (x > 0) {
    418 -			term.line[y][x-1].u = ' ';
    419 -			term.line[y][x-1].mode &= ~ATTR_WIDE;
    420 +			line[x-1].u = ' ';
    421 +			line[x-1].mode &= ~ATTR_WIDE;
    422  		}
    423  	}
    424  
    425  	term.dirty[y] = 1;
    426 -	term.line[y][x] = *attr;
    427 -	term.line[y][x].u = u;
    428 +	line[x] = *attr;
    429 +	line[x].u = u;
    430  }
    431  
    432  void
    433  tclearregion(int x1, int y1, int x2, int y2)
    434  {
    435 -	int x, y, temp;
    436 +	int x, y, L, S, temp;
    437  	Glyph *gp;
    438  
    439  	if (x1 > x2)
    440 @@ -1241,15 +1332,16 @@ tclearregion(int x1, int y1, int x2, int y2)
    441  	if (y1 > y2)
    442  		temp = y1, y1 = y2, y2 = temp;
    443  
    444 -	LIMIT(x1, 0, term.col-1);
    445 -	LIMIT(x2, 0, term.col-1);
    446 +	LIMIT(x1, 0, term.linelen-1);
    447 +	LIMIT(x2, 0, term.linelen-1);
    448  	LIMIT(y1, 0, term.row-1);
    449  	LIMIT(y2, 0, term.row-1);
    450  
    451 +	L = TLINEOFFSET(y1);
    452  	for (y = y1; y <= y2; y++) {
    453  		term.dirty[y] = 1;
    454  		for (x = x1; x <= x2; x++) {
    455 -			gp = &term.line[y][x];
    456 +			gp = &TSCREEN.buffer[L][x];
    457  			if (selected(x, y))
    458  				selclear();
    459  			gp->fg = term.c.attr.fg;
    460 @@ -1257,6 +1349,7 @@ tclearregion(int x1, int y1, int x2, int y2)
    461  			gp->mode = 0;
    462  			gp->u = ' ';
    463  		}
    464 +		L = (L + 1) % TSCREEN.size;
    465  	}
    466  }
    467  
    468 @@ -1271,7 +1364,7 @@ tdeletechar(int n)
    469  	dst = term.c.x;
    470  	src = term.c.x + n;
    471  	size = term.col - src;
    472 -	line = term.line[term.c.y];
    473 +	line = TLINE(term.c.y);
    474  
    475  	memmove(&line[dst], &line[src], size * sizeof(Glyph));
    476  	tclearregion(term.col-n, term.c.y, term.col-1, term.c.y);
    477 @@ -1288,7 +1381,7 @@ tinsertblank(int n)
    478  	dst = term.c.x + n;
    479  	src = term.c.x;
    480  	size = term.col - dst;
    481 -	line = term.line[term.c.y];
    482 +	line = TLINE(term.c.y);
    483  
    484  	memmove(&line[dst], &line[src], size * sizeof(Glyph));
    485  	tclearregion(src, term.c.y, dst - 1, term.c.y);
    486 @@ -2115,7 +2208,7 @@ tdumpline(int n)
    487  	char buf[UTF_SIZ];
    488  	const Glyph *bp, *end;
    489  
    490 -	bp = &term.line[n][0];
    491 +	bp = &TLINE(n)[0];
    492  	end = &bp[MIN(tlinelen(n), term.col) - 1];
    493  	if (bp != end || bp->u != ' ') {
    494  		for ( ; bp <= end; ++bp)
    495 @@ -2503,11 +2596,11 @@ check_control_code:
    496  	if (selected(term.c.x, term.c.y))
    497  		selclear();
    498  
    499 -	gp = &term.line[term.c.y][term.c.x];
    500 +	gp = &TLINE(term.c.y)[term.c.x];
    501  	if (IS_SET(MODE_WRAP) && (term.c.state & CURSOR_WRAPNEXT)) {
    502  		gp->mode |= ATTR_WRAP;
    503  		tnewline(1);
    504 -		gp = &term.line[term.c.y][term.c.x];
    505 +		gp = &TLINE(term.c.y)[term.c.x];
    506  	}
    507  
    508  	if (IS_SET(MODE_INSERT) && term.c.x+width < term.col) {
    509 @@ -2520,7 +2613,7 @@ check_control_code:
    510  			tnewline(1);
    511  		else
    512  			tmoveto(term.col - width, term.c.y);
    513 -		gp = &term.line[term.c.y][term.c.x];
    514 +		gp = &TLINE(term.c.y)[term.c.x];
    515  	}
    516  
    517  	tsetchar(u, &term.c.attr, term.c.x, term.c.y);
    518 @@ -2551,6 +2644,11 @@ twrite(const char *buf, int buflen, int show_ctrl)
    519  	Rune u;
    520  	int n;
    521  
    522 +	if (TSCREEN.off) {
    523 +		TSCREEN.off = 0;
    524 +		tfulldirt();
    525 +	}
    526 +
    527  	for (n = 0; n < buflen; n += charsize) {
    528  		if (IS_SET(MODE_UTF8)) {
    529  			/* process a complete utf8 char */
    530 @@ -2577,56 +2675,85 @@ twrite(const char *buf, int buflen, int show_ctrl)
    531  }
    532  
    533  void
    534 -tresize(int col, int row)
    535 +clearline(Line line, Glyph g, int x, int xend)
    536  {
    537  	int i;
    538 +	g.mode = 0;
    539 +	g.u = ' ';
    540 +	for (i = x; i < xend; ++i) {
    541 +		line[i] = g;
    542 +	}
    543 +}
    544 +
    545 +Line
    546 +ensureline(Line line)
    547 +{
    548 +	if (!line) {
    549 +		line = xmalloc(term.linelen * sizeof(Glyph));
    550 +	}
    551 +	return line;
    552 +}
    553 +
    554 +void
    555 +tresize(int col, int row)
    556 +{
    557 +	int i, j;
    558  	int minrow = MIN(row, term.row);
    559  	int mincol = MIN(col, term.col);
    560 +	int linelen = MAX(col, term.linelen);
    561  	int *bp;
    562 -	TCursor c;
    563  
    564 -	if (col < 1 || row < 1) {
    565 +	if (col < 1 || row < 1 || row > HISTSIZE) {
    566  		fprintf(stderr,
    567  		        "tresize: error resizing to %dx%d\n", col, row);
    568  		return;
    569  	}
    570  
    571 -	/*
    572 -	 * slide screen to keep cursor where we expect it -
    573 -	 * tscrollup would work here, but we can optimize to
    574 -	 * memmove because we're freeing the earlier lines
    575 -	 */
    576 -	for (i = 0; i <= term.c.y - row; i++) {
    577 -		free(term.line[i]);
    578 -		free(term.alt[i]);
    579 +	/* Shift buffer to keep the cursor where we expect it */
    580 +	if (row <= term.c.y) {
    581 +		term.screen[0].cur = (term.screen[0].cur - row + term.c.y + 1) % term.screen[0].size;
    582 +	}
    583 +
    584 +	/* Resize and clear line buffers as needed */
    585 +	if (linelen > term.linelen) {
    586 +		for (i = 0; i < term.screen[0].size; ++i) {
    587 +			if (term.screen[0].buffer[i]) {
    588 +				term.screen[0].buffer[i] = xrealloc(term.screen[0].buffer[i], linelen * sizeof(Glyph));
    589 +				clearline(term.screen[0].buffer[i], term.c.attr, term.linelen, linelen);
    590 +			}
    591 +		}
    592 +		for (i = 0; i < minrow; ++i) {
    593 +			term.screen[1].buffer[i] = xrealloc(term.screen[1].buffer[i], linelen * sizeof(Glyph));
    594 +			clearline(term.screen[1].buffer[i], term.c.attr, term.linelen, linelen);
    595 +		}
    596  	}
    597 -	/* ensure that both src and dst are not NULL */
    598 -	if (i > 0) {
    599 -		memmove(term.line, term.line + i, row * sizeof(Line));
    600 -		memmove(term.alt, term.alt + i, row * sizeof(Line));
    601 +	/* Allocate all visible lines for regular line buffer */
    602 +	for (j = term.screen[0].cur, i = 0; i < row; ++i, j = (j + 1) % term.screen[0].size)
    603 +	{
    604 +		if (!term.screen[0].buffer[j]) {
    605 +			term.screen[0].buffer[j] = xmalloc(linelen * sizeof(Glyph));
    606 +		}
    607 +		if (i >= term.row) {
    608 +			clearline(term.screen[0].buffer[j], term.c.attr, 0, linelen);
    609 +		}
    610  	}
    611 -	for (i += row; i < term.row; i++) {
    612 -		free(term.line[i]);
    613 -		free(term.alt[i]);
    614 +	/* Resize alt screen */
    615 +	term.screen[1].cur = 0;
    616 +	term.screen[1].size = row;
    617 +	for (i = row; i < term.row; ++i) {
    618 +		free(term.screen[1].buffer[i]);
    619 +	}
    620 +	term.screen[1].buffer = xrealloc(term.screen[1].buffer, row * sizeof(Line));
    621 +	for (i = term.row; i < row; ++i) {
    622 +		term.screen[1].buffer[i] = xmalloc(linelen * sizeof(Glyph));
    623 +		clearline(term.screen[1].buffer[i], term.c.attr, 0, linelen);
    624  	}
    625  
    626  	/* resize to new height */
    627 -	term.line = xrealloc(term.line, row * sizeof(Line));
    628 -	term.alt  = xrealloc(term.alt,  row * sizeof(Line));
    629  	term.dirty = xrealloc(term.dirty, row * sizeof(*term.dirty));
    630  	term.tabs = xrealloc(term.tabs, col * sizeof(*term.tabs));
    631  
    632 -	/* resize each row to new width, zero-pad if needed */
    633 -	for (i = 0; i < minrow; i++) {
    634 -		term.line[i] = xrealloc(term.line[i], col * sizeof(Glyph));
    635 -		term.alt[i]  = xrealloc(term.alt[i],  col * sizeof(Glyph));
    636 -	}
    637 -
    638 -	/* allocate any new rows */
    639 -	for (/* i = minrow */; i < row; i++) {
    640 -		term.line[i] = xmalloc(col * sizeof(Glyph));
    641 -		term.alt[i] = xmalloc(col * sizeof(Glyph));
    642 -	}
    643 +	/* fix tabstops */
    644  	if (col > term.col) {
    645  		bp = term.tabs + term.col;
    646  
    647 @@ -2636,26 +2763,16 @@ tresize(int col, int row)
    648  		for (bp += tabspaces; bp < term.tabs + col; bp += tabspaces)
    649  			*bp = 1;
    650  	}
    651 +
    652  	/* update terminal size */
    653  	term.col = col;
    654  	term.row = row;
    655 +	term.linelen = linelen;
    656  	/* reset scrolling region */
    657  	tsetscroll(0, row-1);
    658  	/* make use of the LIMIT in tmoveto */
    659  	tmoveto(term.c.x, term.c.y);
    660 -	/* Clearing both screens (it makes dirty all lines) */
    661 -	c = term.c;
    662 -	for (i = 0; i < 2; i++) {
    663 -		if (mincol < col && 0 < minrow) {
    664 -			tclearregion(mincol, 0, col - 1, minrow - 1);
    665 -		}
    666 -		if (0 < col && minrow < row) {
    667 -			tclearregion(0, minrow, col - 1, row - 1);
    668 -		}
    669 -		tswapscreen();
    670 -		tcursor(CURSOR_LOAD);
    671 -	}
    672 -	term.c = c;
    673 +	tfulldirt();
    674  }
    675  
    676  void
    677 @@ -2667,14 +2784,15 @@ resettitle(void)
    678  void
    679  drawregion(int x1, int y1, int x2, int y2)
    680  {
    681 -	int y;
    682 +	int y, L;
    683  
    684 +	L = TLINEOFFSET(y1);
    685  	for (y = y1; y < y2; y++) {
    686 -		if (!term.dirty[y])
    687 -			continue;
    688 -
    689 -		term.dirty[y] = 0;
    690 -		xdrawline(term.line[y], x1, y, x2);
    691 +		if (term.dirty[y]) {
    692 +			term.dirty[y] = 0;
    693 +			xdrawline(TSCREEN.buffer[L], x1, y, x2);
    694 +		}
    695 +		L = (L + 1) % TSCREEN.size;
    696  	}
    697  }
    698  
    699 @@ -2689,14 +2807,15 @@ draw(void)
    700  	/* adjust cursor position */
    701  	LIMIT(term.ocx, 0, term.col-1);
    702  	LIMIT(term.ocy, 0, term.row-1);
    703 -	if (term.ocx > 0 && (term.line[term.ocy][term.ocx].mode & ATTR_WDUMMY))
    704 +	if (term.ocx > 0 && (TLINE(term.ocy)[term.ocx].mode & ATTR_WDUMMY))
    705  		term.ocx--;
    706 -	if (cx > 0 && (term.line[term.c.y][cx].mode & ATTR_WDUMMY))
    707 +	if (cx > 0 && (TLINE(term.c.y)[cx].mode & ATTR_WDUMMY))
    708  		cx--;
    709  
    710  	drawregion(0, 0, term.col, term.row);
    711 -	xdrawcursor(cx, term.c.y, term.line[term.c.y][cx],
    712 -			term.ocx, term.ocy, term.line[term.ocy][term.ocx]);
    713 +	if (TSCREEN.off == 0)
    714 +		xdrawcursor(cx, term.c.y, TLINE(term.c.y)[cx],
    715 +			term.ocx, term.ocy, TLINE(term.ocy)[term.ocx]);
    716  	term.ocx = cx;
    717  	term.ocy = term.c.y;
    718  	xfinishdraw();
    719 diff --git a/st.h b/st.h
    720 index fd3b0d8..3cea73b 100644
    721 --- a/st.h
    722 +++ b/st.h
    723 @@ -19,6 +19,7 @@
    724  
    725  #define TRUECOLOR(r,g,b)	(1 << 24 | (r) << 16 | (g) << 8 | (b))
    726  #define IS_TRUECOL(x)		(1 << 24 & (x))
    727 +#define HISTSIZE            2000
    728  
    729  enum glyph_attribute {
    730  	ATTR_NULL       = 0,
    731 diff --git a/x.c b/x.c
    732 index 76f572d..c155e97 100644
    733 --- a/x.c
    734 +++ b/x.c
    735 @@ -59,6 +59,8 @@ static void zoom(const Arg *);
    736  static void zoomabs(const Arg *);
    737  static void zoomreset(const Arg *);
    738  static void ttysend(const Arg *);
    739 +void kscrollup(const Arg *);
    740 +void kscrolldown(const Arg *);
    741  
    742  /* config.h for applying patches and the configuration. */
    743  #include "config.h"
    744 -- 
    745 2.56.0
    746