aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.adoc (renamed from README)28
-rw-r--r--patches/st-anysize-20220718-baa9357.diff164
-rw-r--r--patches/st-blinking_cursor-20211116-2f6e597.diff153
-rw-r--r--patches/st-clipboard-0.8.3.diff12
-rw-r--r--patches/st-copyurl-multiline-20220221-0.8.5.diff156
-rw-r--r--patches/st-ligatures-20221120-0.9.diff526
-rw-r--r--patches/st-ligatures-scrollback-20221120-0.9.diff526
-rw-r--r--patches/st-scrollback-0.8.5.diff350
-rw-r--r--patches/st-scrollback-mouse-20220127-2c5edf2.diff25
-rw-r--r--patches/st-scrollback-mouse-altscreen-20220127-2c5edf2.diff78
-rw-r--r--patches/st-scrollback-mouse-increment-0.8.2.diff34
-rw-r--r--patches/st-scrollback-reflow-0.8.5.diff1478
-rw-r--r--patches/st-title_parsing_fix-0.8.5.diff145
-rw-r--r--patches/st-visualbell2-basic-2020-05-13-045a0fa.diff105
14 files changed, 3770 insertions, 10 deletions
diff --git a/README b/README.adoc
index 6a846ed..6c46243 100644
--- a/README
+++ b/README.adoc
@@ -1,15 +1,24 @@
-st - simple terminal
---------------------
+= st - simple terminal
+
st is a simple terminal emulator for X which sucks less.
+Patched with:
+
+- anysize
+- blinking cursor
+- clipboard
+- copyurl
+- ligatures
+- scrollback
+- title parsing fix
+- visualbell2
+
+= Requirements
-Requirements
-------------
In order to build st you need the Xlib header files.
+= Installation
-Installation
-------------
Edit config.mk to match your local setup (st is installed into
the /usr/local namespace by default).
@@ -18,9 +27,8 @@ necessary as root):
make clean install
+= Running
-Running st
-----------
If you did not install st with make clean install, you must compile
the st terminfo entry with the following command:
@@ -28,7 +36,7 @@ the st terminfo entry with the following command:
See the man page for additional details.
-Credits
--------
+= Credits
+
Based on Aurélien APTEL <aurelien dot aptel at gmail dot com> bt source code.
diff --git a/patches/st-anysize-20220718-baa9357.diff b/patches/st-anysize-20220718-baa9357.diff
new file mode 100644
index 0000000..675ffdf
--- /dev/null
+++ b/patches/st-anysize-20220718-baa9357.diff
@@ -0,0 +1,164 @@
+From 8dcdc4b21a73268e167d98aa30f24315c7f3b7ff Mon Sep 17 00:00:00 2001
+From: Bakkeby <bakkeby@gmail.com>
+Date: Mon, 18 Jul 2022 16:52:03 +0200
+Subject: [PATCH] Adding anysize patch
+
+---
+ x.c | 56 ++++++++++++++++++++++++++++++--------------------------
+ 1 file changed, 30 insertions(+), 26 deletions(-)
+
+diff --git a/x.c b/x.c
+index 2a3bd38..f534347 100644
+--- a/x.c
++++ b/x.c
+@@ -81,6 +81,7 @@ typedef XftGlyphFontSpec GlyphFontSpec;
+ typedef struct {
+ int tw, th; /* tty width and height */
+ int w, h; /* window width and height */
++ int hborderpx, vborderpx;
+ int ch; /* char height */
+ int cw; /* char width */
+ int mode; /* window state/mode flags */
+@@ -331,7 +332,7 @@ ttysend(const Arg *arg)
+ int
+ evcol(XEvent *e)
+ {
+- int x = e->xbutton.x - borderpx;
++ int x = e->xbutton.x - win.hborderpx;
+ LIMIT(x, 0, win.tw - 1);
+ return x / win.cw;
+ }
+@@ -339,7 +340,7 @@ evcol(XEvent *e)
+ int
+ evrow(XEvent *e)
+ {
+- int y = e->xbutton.y - borderpx;
++ int y = e->xbutton.y - win.vborderpx;
+ LIMIT(y, 0, win.th - 1);
+ return y / win.ch;
+ }
+@@ -739,6 +740,9 @@ cresize(int width, int height)
+ col = MAX(1, col);
+ row = MAX(1, row);
+
++ win.hborderpx = (win.w - col * win.cw) / 2;
++ win.vborderpx = (win.h - row * win.ch) / 2;
++
+ tresize(col, row);
+ xresize(col, row);
+ ttyresize(win.tw, win.th);
+@@ -869,8 +873,8 @@ xhints(void)
+ sizeh->flags = PSize | PResizeInc | PBaseSize | PMinSize;
+ sizeh->height = win.h;
+ sizeh->width = win.w;
+- sizeh->height_inc = win.ch;
+- sizeh->width_inc = win.cw;
++ sizeh->height_inc = 1;
++ sizeh->width_inc = 1;
+ sizeh->base_height = 2 * borderpx;
+ sizeh->base_width = 2 * borderpx;
+ sizeh->min_height = win.ch + 2 * borderpx;
+@@ -1152,8 +1156,8 @@ xinit(int cols, int rows)
+ xloadcols();
+
+ /* adjust fixed window geometry */
+- win.w = 2 * borderpx + cols * win.cw;
+- win.h = 2 * borderpx + rows * win.ch;
++ win.w = 2 * win.hborderpx + 2 * borderpx + cols * win.cw;
++ win.h = 2 * win.vborderpx + 2 * borderpx + rows * win.ch;
+ if (xw.gm & XNegative)
+ xw.l += DisplayWidth(xw.dpy, xw.scr) - win.w - 2;
+ if (xw.gm & YNegative)
+@@ -1242,7 +1246,7 @@ xinit(int cols, int rows)
+ int
+ xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x, int y)
+ {
+- float winx = borderpx + x * win.cw, winy = borderpx + y * win.ch, xp, yp;
++ float winx = win.hborderpx + x * win.cw, winy = win.vborderpx + y * win.ch, xp, yp;
+ ushort mode, prevmode = USHRT_MAX;
+ Font *font = &dc.font;
+ int frcflags = FRC_NORMAL;
+@@ -1375,7 +1379,7 @@ void
+ xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, int y)
+ {
+ int charlen = len * ((base.mode & ATTR_WIDE) ? 2 : 1);
+- int winx = borderpx + x * win.cw, winy = borderpx + y * win.ch,
++ int winx = win.hborderpx + x * win.cw, winy = win.vborderpx + y * win.ch,
+ width = charlen * win.cw;
+ Color *fg, *bg, *temp, revfg, revbg, truefg, truebg;
+ XRenderColor colfg, colbg;
+@@ -1465,17 +1469,17 @@ xdrawglyphfontspecs(const XftGlyphFontSpec *specs, Glyph base, int len, int x, i
+
+ /* Intelligent cleaning up of the borders. */
+ if (x == 0) {
+- xclear(0, (y == 0)? 0 : winy, borderpx,
++ xclear(0, (y == 0)? 0 : winy, win.hborderpx,
+ winy + win.ch +
+- ((winy + win.ch >= borderpx + win.th)? win.h : 0));
++ ((winy + win.ch >= win.vborderpx + win.th)? win.h : 0));
+ }
+- if (winx + width >= borderpx + win.tw) {
++ if (winx + width >= win.hborderpx + win.tw) {
+ xclear(winx + width, (y == 0)? 0 : winy, win.w,
+- ((winy + win.ch >= borderpx + win.th)? win.h : (winy + win.ch)));
++ ((winy + win.ch >= win.vborderpx + win.th)? win.h : (winy + win.ch)));
+ }
+ if (y == 0)
+- xclear(winx, 0, winx + width, borderpx);
+- if (winy + win.ch >= borderpx + win.th)
++ xclear(winx, 0, winx + width, win.vborderpx);
++ if (winy + win.ch >= win.vborderpx + win.th)
+ xclear(winx, winy + win.ch, winx + width, win.h);
+
+ /* Clean up the region we want to draw to. */
+@@ -1569,35 +1573,35 @@ xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og)
+ case 3: /* Blinking Underline */
+ case 4: /* Steady Underline */
+ XftDrawRect(xw.draw, &drawcol,
+- borderpx + cx * win.cw,
+- borderpx + (cy + 1) * win.ch - \
++ win.hborderpx + cx * win.cw,
++ win.vborderpx + (cy + 1) * win.ch - \
+ cursorthickness,
+ win.cw, cursorthickness);
+ break;
+ case 5: /* Blinking bar */
+ case 6: /* Steady bar */
+ XftDrawRect(xw.draw, &drawcol,
+- borderpx + cx * win.cw,
+- borderpx + cy * win.ch,
++ win.hborderpx + cx * win.cw,
++ win.vborderpx + cy * win.ch,
+ cursorthickness, win.ch);
+ break;
+ }
+ } else {
+ XftDrawRect(xw.draw, &drawcol,
+- borderpx + cx * win.cw,
+- borderpx + cy * win.ch,
++ win.hborderpx + cx * win.cw,
++ win.vborderpx + cy * win.ch,
+ win.cw - 1, 1);
+ XftDrawRect(xw.draw, &drawcol,
+- borderpx + cx * win.cw,
+- borderpx + cy * win.ch,
++ win.hborderpx + cx * win.cw,
++ win.vborderpx + cy * win.ch,
+ 1, win.ch - 1);
+ XftDrawRect(xw.draw, &drawcol,
+- borderpx + (cx + 1) * win.cw - 1,
+- borderpx + cy * win.ch,
++ win.hborderpx + (cx + 1) * win.cw - 1,
++ win.vborderpx + cy * win.ch,
+ 1, win.ch - 1);
+ XftDrawRect(xw.draw, &drawcol,
+- borderpx + cx * win.cw,
+- borderpx + (cy + 1) * win.ch - 1,
++ win.hborderpx + cx * win.cw,
++ win.vborderpx + (cy + 1) * win.ch - 1,
+ win.cw, 1);
+ }
+ }
+--
+2.37.1
+
diff --git a/patches/st-blinking_cursor-20211116-2f6e597.diff b/patches/st-blinking_cursor-20211116-2f6e597.diff
new file mode 100644
index 0000000..91c871a
--- /dev/null
+++ b/patches/st-blinking_cursor-20211116-2f6e597.diff
@@ -0,0 +1,153 @@
+From a3cdd0753bf578cd4e6db7c6507481f3b5c38aea Mon Sep 17 00:00:00 2001
+From: Steve Ward <planet36@gmail.com>
+Date: Tue, 16 Nov 2021 14:15:06 -0500
+Subject: [PATCH] Allow blinking cursor
+
+---
+ config.def.h | 19 +++++++++++++------
+ x.c | 47 +++++++++++++++++++++++++++++++++++------------
+ 2 files changed, 48 insertions(+), 18 deletions(-)
+
+diff --git a/config.def.h b/config.def.h
+index 6f05dce..1a5fed0 100644
+--- a/config.def.h
++++ b/config.def.h
+@@ -133,13 +133,20 @@ static unsigned int defaultcs = 256;
+ static unsigned int defaultrcs = 257;
+
+ /*
+- * Default shape of cursor
+- * 2: Block ("█")
+- * 4: Underline ("_")
+- * 6: Bar ("|")
+- * 7: Snowman ("☃")
++ * https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h4-Functions-using-CSI-_-ordered-by-the-final-character-lparen-s-rparen:CSI-Ps-SP-q.1D81
++ * Default style of cursor
++ * 0: blinking block
++ * 1: blinking block (default)
++ * 2: steady block ("█")
++ * 3: blinking underline
++ * 4: steady underline ("_")
++ * 5: blinking bar
++ * 6: steady bar ("|")
++ * 7: blinking st cursor
++ * 8: steady st cursor
+ */
+-static unsigned int cursorshape = 2;
++static unsigned int cursorstyle = 1;
++static Rune stcursor = 0x2603; /* snowman ("☃") */
+
+ /*
+ * Default columns and rows numbers
+diff --git a/x.c b/x.c
+index 89786b8..7d2447d 100644
+--- a/x.c
++++ b/x.c
+@@ -253,6 +253,7 @@ static char *opt_name = NULL;
+ static char *opt_title = NULL;
+
+ static int oldbutton = 3; /* button event on startup: 3 = release */
++static int cursorblinks = 0;
+
+ void
+ clipcopy(const Arg *dummy)
+@@ -1529,29 +1530,44 @@ xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og)
+ /* draw the new one */
+ if (IS_SET(MODE_FOCUSED)) {
+ switch (win.cursor) {
+- case 7: /* st extension */
+- g.u = 0x2603; /* snowman (U+2603) */
++ default:
++ case 0: /* blinking block */
++ case 1: /* blinking block (default) */
++ if (IS_SET(MODE_BLINK))
++ break;
+ /* FALLTHROUGH */
+- case 0: /* Blinking Block */
+- case 1: /* Blinking Block (Default) */
+- case 2: /* Steady Block */
++ case 2: /* steady block */
+ xdrawglyph(g, cx, cy);
+ break;
+- case 3: /* Blinking Underline */
+- case 4: /* Steady Underline */
++ case 3: /* blinking underline */
++ if (IS_SET(MODE_BLINK))
++ break;
++ /* FALLTHROUGH */
++ case 4: /* steady underline */
+ XftDrawRect(xw.draw, &drawcol,
+ borderpx + cx * win.cw,
+ borderpx + (cy + 1) * win.ch - \
+ cursorthickness,
+ win.cw, cursorthickness);
+ break;
+- case 5: /* Blinking bar */
+- case 6: /* Steady bar */
++ case 5: /* blinking bar */
++ if (IS_SET(MODE_BLINK))
++ break;
++ /* FALLTHROUGH */
++ case 6: /* steady bar */
+ XftDrawRect(xw.draw, &drawcol,
+ borderpx + cx * win.cw,
+ borderpx + cy * win.ch,
+ cursorthickness, win.ch);
+ break;
++ case 7: /* blinking st cursor */
++ if (IS_SET(MODE_BLINK))
++ break;
++ /* FALLTHROUGH */
++ case 8: /* steady st cursor */
++ g.u = stcursor;
++ xdrawglyph(g, cx, cy);
++ break;
+ }
+ } else {
+ XftDrawRect(xw.draw, &drawcol,
+@@ -1708,9 +1724,12 @@ xsetmode(int set, unsigned int flags)
+ int
+ xsetcursor(int cursor)
+ {
+- if (!BETWEEN(cursor, 0, 7)) /* 7: st extension */
++ if (!BETWEEN(cursor, 0, 8)) /* 7-8: st extensions */
+ return 1;
+ win.cursor = cursor;
++ cursorblinks = win.cursor == 0 || win.cursor == 1 ||
++ win.cursor == 3 || win.cursor == 5 ||
++ win.cursor == 7;
+ return 0;
+ }
+
+@@ -1954,6 +1973,10 @@ run(void)
+ if (FD_ISSET(ttyfd, &rfd) || xev) {
+ if (!drawing) {
+ trigger = now;
++ if (IS_SET(MODE_BLINK)) {
++ win.mode ^= MODE_BLINK;
++ }
++ lastblink = now;
+ drawing = 1;
+ }
+ timeout = (maxlatency - TIMEDIFF(now, trigger)) \
+@@ -1964,7 +1987,7 @@ run(void)
+
+ /* idle detected or maxlatency exhausted -> draw */
+ timeout = -1;
+- if (blinktimeout && tattrset(ATTR_BLINK)) {
++ if (blinktimeout && (cursorblinks || tattrset(ATTR_BLINK))) {
+ timeout = blinktimeout - TIMEDIFF(now, lastblink);
+ if (timeout <= 0) {
+ if (-timeout > blinktimeout) /* start visible */
+@@ -2000,7 +2023,7 @@ main(int argc, char *argv[])
+ {
+ xw.l = xw.t = 0;
+ xw.isfixed = False;
+- xsetcursor(cursorshape);
++ xsetcursor(cursorstyle);
+
+ ARGBEGIN {
+ case 'a':
+--
+2.34.0
+
diff --git a/patches/st-clipboard-0.8.3.diff b/patches/st-clipboard-0.8.3.diff
new file mode 100644
index 0000000..c1e0e9e
--- /dev/null
+++ b/patches/st-clipboard-0.8.3.diff
@@ -0,0 +1,12 @@
+diff --git a/x.c b/x.c
+index e5f1737..5cabd60 100644
+--- a/x.c
++++ b/x.c
+@@ -673,6 +673,7 @@ setsel(char *str, Time t)
+ XSetSelectionOwner(xw.dpy, XA_PRIMARY, xw.win, t);
+ if (XGetSelectionOwner(xw.dpy, XA_PRIMARY) != xw.win)
+ selclear();
++ clipcopy(NULL);
+ }
+
+ void
diff --git a/patches/st-copyurl-multiline-20220221-0.8.5.diff b/patches/st-copyurl-multiline-20220221-0.8.5.diff
new file mode 100644
index 0000000..7cd26e2
--- /dev/null
+++ b/patches/st-copyurl-multiline-20220221-0.8.5.diff
@@ -0,0 +1,156 @@
+From 30a04d9ecb3998953bdbe42e5617d00d6002869b Mon Sep 17 00:00:00 2001
+From: Santtu Lakkala <inz@inz.fi>
+Date: Wed, 16 Feb 2022 20:34:20 +0200
+Subject: [PATCH] Loop through urls on screen and copy to clipboard
+
+Replace url detection heuristics with a DFA, enabling urls that span
+multiple lines. Also fix the selection not to use snapping so that urls
+are selected exactly.
+---
+ config.def.h | 1 +
+ st.c | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++++
+ st.h | 1 +
+ 3 files changed, 95 insertions(+)
+
+diff --git a/config.def.h b/config.def.h
+index 91ab8ca..3f365c7 100644
+--- a/config.def.h
++++ b/config.def.h
+@@ -201,6 +201,7 @@ static Shortcut shortcuts[] = {
+ { TERMMOD, XK_Y, selpaste, {.i = 0} },
+ { ShiftMask, XK_Insert, selpaste, {.i = 0} },
+ { TERMMOD, XK_Num_Lock, numlock, {.i = 0} },
++ { MODKEY, XK_l, copyurl, {.i = 0} },
+ };
+
+ /*
+diff --git a/st.c b/st.c
+index 51049ba..5b6d919 100644
+--- a/st.c
++++ b/st.c
+@@ -152,6 +152,11 @@ typedef struct {
+ int narg; /* nb of args */
+ } STREscape;
+
++typedef struct {
++ int state;
++ size_t length;
++} URLdfa;
++
+ static void execsh(char *, char **);
+ static void stty(char **);
+ static void sigchld(int);
+@@ -200,6 +205,7 @@ static void tdefutf8(char);
+ static int32_t tdefcolor(const int *, int *, int);
+ static void tdeftran(char);
+ static void tstrsequence(uchar);
++static int daddch(URLdfa *, char);
+
+ static void drawregion(int, int, int, int);
+
+@@ -2688,3 +2694,90 @@ redraw(void)
+ tfulldirt();
+ draw();
+ }
++
++int
++daddch(URLdfa *dfa, char c)
++{
++ /* () and [] can appear in urls, but excluding them here will reduce false
++ * positives when figuring out where a given url ends.
++ */
++ static const char URLCHARS[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
++ "abcdefghijklmnopqrstuvwxyz"
++ "0123456789-._~:/?#@!$&'*+,;=%";
++ static const char RPFX[] = "//:sptth";
++
++ if (!strchr(URLCHARS, c)) {
++ dfa->length = 0;
++ dfa->state = 0;
++
++ return 0;
++ }
++
++ dfa->length++;
++
++ if (dfa->state == 2 && c == '/') {
++ dfa->state = 0;
++ } else if (dfa->state == 3 && c == 'p') {
++ dfa->state++;
++ } else if (c != RPFX[dfa->state]) {
++ dfa->state = 0;
++ return 0;
++ }
++
++ if (dfa->state++ == 7) {
++ dfa->state = 0;
++ return 1;
++ }
++
++ return 0;
++}
++
++/*
++** Select and copy the previous url on screen (do nothing if there's no url).
++*/
++void
++copyurl(const Arg *arg) {
++ int row = 0, /* row of current URL */
++ col = 0, /* column of current URL start */
++ colend = 0, /* column of last occurrence */
++ passes = 0; /* how many rows have been scanned */
++
++ const char *c = NULL,
++ *match = NULL;
++ URLdfa dfa = { 0 };
++
++ row = (sel.ob.x >= 0 && sel.nb.y > 0) ? sel.nb.y : term.bot;
++ LIMIT(row, term.top, term.bot);
++
++ colend = (sel.ob.x >= 0 && sel.nb.y > 0) ? sel.nb.x : term.col;
++ LIMIT(colend, 0, term.col);
++
++ /*
++ ** Scan from (term.row - 1,term.col - 1) to (0,0) and find
++ ** next occurrance of a URL
++ */
++ for (passes = 0; passes < term.row; passes++) {
++ /* Read in each column of every row until
++ ** we hit previous occurrence of URL
++ */
++ for (col = colend; col--;)
++ if (daddch(&dfa, term.line[row][col].u < 128 ? term.line[row][col].u : ' '))
++ break;
++
++ if (col >= 0)
++ break;
++
++ if (--row < 0)
++ row = term.row - 1;
++
++ colend = term.col;
++ }
++
++ if (passes < term.row) {
++ selstart(col, row, 0);
++ selextend((col + dfa.length - 1) % term.col, row + (col + dfa.length - 1) / term.col, SEL_REGULAR, 0);
++ selextend((col + dfa.length - 1) % term.col, row + (col + dfa.length - 1) / term.col, SEL_REGULAR, 1);
++ xsetsel(getsel());
++ xclipcopy();
++ }
++}
+diff --git a/st.h b/st.h
+index 519b9bd..0458005 100644
+--- a/st.h
++++ b/st.h
+@@ -85,6 +85,7 @@ void printscreen(const Arg *);
+ void printsel(const Arg *);
+ void sendbreak(const Arg *);
+ void toggleprinter(const Arg *);
++void copyurl(const Arg *);
+
+ int tattrset(int);
+ void tnew(int, int);
+--
+2.32.0
+
diff --git a/patches/st-ligatures-20221120-0.9.diff b/patches/st-ligatures-20221120-0.9.diff
new file mode 100644
index 0000000..ab1e0fa
--- /dev/null
+++ b/patches/st-ligatures-20221120-0.9.diff
@@ -0,0 +1,526 @@
+diff --git a/Makefile b/Makefile
+index 470ac86..38240da 100644
+--- a/Makefile
++++ b/Makefile
+@@ -4,7 +4,7 @@
+
+ include config.mk
+
+-SRC = st.c x.c
++SRC = st.c x.c hb.c
+ OBJ = $(SRC:.c=.o)
+
+ all: options st
+@@ -22,7 +22,8 @@ config.h:
+ $(CC) $(STCFLAGS) -c $<
+
+ st.o: config.h st.h win.h
+-x.o: arg.h config.h st.h win.h
++x.o: arg.h config.h st.h win.h hb.h
++hb.o: st.h
+
+ $(OBJ): config.h config.mk
+
+diff --git a/config.mk b/config.mk
+index 1e306f8..3e13e53 100644
+--- a/config.mk
++++ b/config.mk
+@@ -15,10 +15,12 @@ PKG_CONFIG = pkg-config
+ # includes and libs
+ INCS = -I$(X11INC) \
+ `$(PKG_CONFIG) --cflags fontconfig` \
+- `$(PKG_CONFIG) --cflags freetype2`
++ `$(PKG_CONFIG) --cflags freetype2` \
++ `$(PKG_CONFIG) --cflags harfbuzz`
+ LIBS = -L$(X11LIB) -lm -lrt -lX11 -lutil -lXft \
+ `$(PKG_CONFIG) --libs fontconfig` \
+- `$(PKG_CONFIG) --libs freetype2`
++ `$(PKG_CONFIG) --libs freetype2` \
++ `$(PKG_CONFIG) --libs harfbuzz`
+
+ # flags
+ STCPPFLAGS = -DVERSION=\"$(VERSION)\" -D_XOPEN_SOURCE=600
+diff --git a/hb.c b/hb.c
+new file mode 100644
+index 0000000..bacec05
+--- /dev/null
++++ b/hb.c
+@@ -0,0 +1,107 @@
++#include <stdlib.h>
++#include <stdio.h>
++#include <math.h>
++#include <X11/Xft/Xft.h>
++#include <X11/cursorfont.h>
++#include <hb.h>
++#include <hb-ft.h>
++
++#include "st.h"
++#include "hb.h"
++
++#define FEATURE(c1,c2,c3,c4) { .tag = HB_TAG(c1,c2,c3,c4), .value = 1, .start = HB_FEATURE_GLOBAL_START, .end = HB_FEATURE_GLOBAL_END }
++
++hb_font_t *hbfindfont(XftFont *match);
++
++typedef struct {
++ XftFont *match;
++ hb_font_t *font;
++} HbFontMatch;
++
++static int hbfontslen = 0;
++static HbFontMatch *hbfontcache = NULL;
++
++/*
++ * Poplulate the array with a list of font features, wrapped in FEATURE macro,
++ * e. g.
++ * FEATURE('c', 'a', 'l', 't'), FEATURE('d', 'l', 'i', 'g')
++ */
++hb_feature_t features[] = { };
++
++void
++hbunloadfonts()
++{
++ for (int i = 0; i < hbfontslen; i++) {
++ hb_font_destroy(hbfontcache[i].font);
++ XftUnlockFace(hbfontcache[i].match);
++ }
++
++ if (hbfontcache != NULL) {
++ free(hbfontcache);
++ hbfontcache = NULL;
++ }
++ hbfontslen = 0;
++}
++
++hb_font_t *
++hbfindfont(XftFont *match)
++{
++ for (int i = 0; i < hbfontslen; i++) {
++ if (hbfontcache[i].match == match)
++ return hbfontcache[i].font;
++ }
++
++ /* Font not found in cache, caching it now. */
++ hbfontcache = realloc(hbfontcache, sizeof(HbFontMatch) * (hbfontslen + 1));
++ FT_Face face = XftLockFace(match);
++ hb_font_t *font = hb_ft_font_create(face, NULL);
++ if (font == NULL)
++ die("Failed to load Harfbuzz font.");
++
++ hbfontcache[hbfontslen].match = match;
++ hbfontcache[hbfontslen].font = font;
++ hbfontslen += 1;
++
++ return font;
++}
++
++void hbtransform(HbTransformData *data, XftFont *xfont, const Glyph *glyphs, int start, int length) {
++ Rune rune;
++ ushort mode = USHRT_MAX;
++ unsigned int glyph_count;
++ int i, end = start + length;
++
++ hb_font_t *font = hbfindfont(xfont);
++ if (font == NULL)
++ return;
++
++ hb_buffer_t *buffer = hb_buffer_create();
++ hb_buffer_set_direction(buffer, HB_DIRECTION_LTR);
++
++ /* Fill buffer with codepoints. */
++ for (i = start; i < end; i++) {
++ rune = glyphs[i].u;
++ mode = glyphs[i].mode;
++ if (mode & ATTR_WDUMMY)
++ rune = 0x0020;
++ hb_buffer_add_codepoints(buffer, &rune, 1, 0, 1);
++ }
++
++ /* Shape the segment. */
++ hb_shape(font, buffer, features, sizeof(features)/sizeof(hb_feature_t));
++
++ /* Get new glyph info. */
++ hb_glyph_info_t *info = hb_buffer_get_glyph_infos(buffer, &glyph_count);
++ hb_glyph_position_t *pos = hb_buffer_get_glyph_positions(buffer, &glyph_count);
++
++ /** Fill the output. */
++ data->buffer = buffer;
++ data->glyphs = info;
++ data->positions = pos;
++ data->count = glyph_count;
++}
++
++void hbcleanup(HbTransformData *data) {
++ hb_buffer_destroy(data->buffer);
++ memset(data, 0, sizeof(HbTransformData));
++}
+diff --git a/hb.h b/hb.h
+new file mode 100644
+index 0000000..88de9bd
+--- /dev/null
++++ b/hb.h
+@@ -0,0 +1,14 @@
++#include <X11/Xft/Xft.h>
++#include <hb.h>
++#include <hb-ft.h>
++
++typedef struct {
++ hb_buffer_t *buffer;
++ hb_glyph_info_t *glyphs;
++ hb_glyph_position_t *positions;
++ unsigned int count;
++} HbTransformData;
++
++void hbunloadfonts();
++void hbtransform(HbTransformData *, XftFont *, const Glyph *, int, int);
++void hbcleanup(HbTransformData *);
+diff --git a/st.c b/st.c
+index 62def59..041c6d8 100644
+--- a/st.c
++++ b/st.c
+@@ -2640,7 +2640,8 @@ draw(void)
+
+ 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, term.ocy, term.line[term.ocy][term.ocx],
++ term.line[term.ocy], term.col);
+ term.ocx = cx;
+ term.ocy = term.c.y;
+ xfinishdraw();
+diff --git a/st.h b/st.h
+index fd3b0d8..142fdfe 100644
+--- a/st.h
++++ b/st.h
+@@ -11,7 +11,8 @@
+ #define DIVCEIL(n, d) (((n) + ((d) - 1)) / (d))
+ #define DEFAULT(a, b) (a) = (a) ? (a) : (b)
+ #define LIMIT(x, a, b) (x) = (x) < (a) ? (a) : (x) > (b) ? (b) : (x)
+-#define ATTRCMP(a, b) ((a).mode != (b).mode || (a).fg != (b).fg || \
++#define ATTRCMP(a, b) (((a).mode & (~ATTR_WRAP)) != ((b).mode & (~ATTR_WRAP)) || \
++ (a).fg != (b).fg || \
+ (a).bg != (b).bg)
+ #define TIMEDIFF(t1, t2) ((t1.tv_sec-t2.tv_sec)*1000 + \
+ (t1.tv_nsec-t2.tv_nsec)/1E6)
+diff --git a/win.h b/win.h
+index 6de960d..94679e4 100644
+--- a/win.h
++++ b/win.h
+@@ -25,7 +25,7 @@ enum win_mode {
+
+ void xbell(void);
+ void xclipcopy(void);
+-void xdrawcursor(int, int, Glyph, int, int, Glyph);
++void xdrawcursor(int, int, Glyph, int, int, Glyph, Line, int);
+ void xdrawline(Line, int, int, int);
+ void xfinishdraw(void);
+ void xloadcols(void);
+diff --git a/x.c b/x.c
+index 2a3bd38..5feac09 100644
+--- a/x.c
++++ b/x.c
+@@ -19,6 +19,7 @@ char *argv0;
+ #include "arg.h"
+ #include "st.h"
+ #include "win.h"
++#include "hb.h"
+
+ /* types used in config.h */
+ typedef struct {
+@@ -141,6 +142,7 @@ typedef struct {
+ } DC;
+
+ static inline ushort sixd_to_16bit(int);
++static void xresetfontsettings(ushort mode, Font **font, int *frcflags);
+ static int xmakeglyphfontspecs(XftGlyphFontSpec *, const Glyph *, int, int, int);
+ static void xdrawglyphfontspecs(const XftGlyphFontSpec *, Glyph, int, int, int);
+ static void xdrawglyph(Glyph, int, int);
+@@ -1062,6 +1064,9 @@ xunloadfont(Font *f)
+ void
+ xunloadfonts(void)
+ {
++ /* Clear Harfbuzz font cache. */
++ hbunloadfonts();
++
+ /* Free the loaded fonts in the font cache. */
+ while (frclen > 0)
+ XftFontClose(xw.dpy, frc[--frclen].font);
+@@ -1239,6 +1244,22 @@ xinit(int cols, int rows)
+ xsel.xtarget = XA_STRING;
+ }
+
++void
++xresetfontsettings(ushort mode, Font **font, int *frcflags)
++{
++ *font = &dc.font;
++ if ((mode & ATTR_ITALIC) && (mode & ATTR_BOLD)) {
++ *font = &dc.ibfont;
++ *frcflags = FRC_ITALICBOLD;
++ } else if (mode & ATTR_ITALIC) {
++ *font = &dc.ifont;
++ *frcflags = FRC_ITALIC;
++ } else if (mode & ATTR_BOLD) {
++ *font = &dc.bfont;
++ *frcflags = FRC_BOLD;
++ }
++}
++
+ int
+ xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x, int y)
+ {
+@@ -1253,119 +1274,137 @@ xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x
+ FcPattern *fcpattern, *fontpattern;
+ FcFontSet *fcsets[] = { NULL };
+ FcCharSet *fccharset;
+- int i, f, numspecs = 0;
++ int i, f, length = 0, start = 0, numspecs = 0;
++ HbTransformData shaped = { 0 };
++
++ /* Initial values. */
++ mode = prevmode = glyphs[0].mode;
++ xresetfontsettings(mode, &font, &frcflags);
+
+ for (i = 0, xp = winx, yp = winy + font->ascent; i < len; ++i) {
+- /* Fetch rune and mode for current glyph. */
+- rune = glyphs[i].u;
+ mode = glyphs[i].mode;
+
+ /* Skip dummy wide-character spacing. */
+- if (mode == ATTR_WDUMMY)
++ if (mode & ATTR_WDUMMY)
+ continue;
+
+- /* Determine font for glyph if different from previous glyph. */
+- if (prevmode != mode) {
+- prevmode = mode;
+- font = &dc.font;
+- frcflags = FRC_NORMAL;
+- runewidth = win.cw * ((mode & ATTR_WIDE) ? 2.0f : 1.0f);
+- if ((mode & ATTR_ITALIC) && (mode & ATTR_BOLD)) {
+- font = &dc.ibfont;
+- frcflags = FRC_ITALICBOLD;
+- } else if (mode & ATTR_ITALIC) {
+- font = &dc.ifont;
+- frcflags = FRC_ITALIC;
+- } else if (mode & ATTR_BOLD) {
+- font = &dc.bfont;
+- frcflags = FRC_BOLD;
++ if (
++ prevmode != mode
++ || ATTRCMP(glyphs[start], glyphs[i])
++ || selected(x + i, y) != selected(x + start, y)
++ || i == (len - 1)
++ ) {
++ /* Handle 1-character wide segments and end of line */
++ length = i - start;
++ if (i == start) {
++ length = 1;
++ } else if (i == (len - 1)) {
++ length = (i - start + 1);
+ }
+- yp = winy + font->ascent;
+- }
+-
+- /* Lookup character index with default font. */
+- glyphidx = XftCharIndex(xw.dpy, font->match, rune);
+- if (glyphidx) {
+- specs[numspecs].font = font->match;
+- specs[numspecs].glyph = glyphidx;
+- specs[numspecs].x = (short)xp;
+- specs[numspecs].y = (short)yp;
+- xp += runewidth;
+- numspecs++;
+- continue;
+- }
+
+- /* Fallback on font cache, search the font cache for match. */
+- for (f = 0; f < frclen; f++) {
+- glyphidx = XftCharIndex(xw.dpy, frc[f].font, rune);
+- /* Everything correct. */
+- if (glyphidx && frc[f].flags == frcflags)
+- break;
+- /* We got a default font for a not found glyph. */
+- if (!glyphidx && frc[f].flags == frcflags
+- && frc[f].unicodep == rune) {
+- break;
++ /* Shape the segment. */
++ hbtransform(&shaped, font->match, glyphs, start, length);
++ for (int code_idx = 0; code_idx < shaped.count; code_idx++) {
++ rune = glyphs[start + code_idx].u;
++ runewidth = win.cw * ((glyphs[start + code_idx].mode & ATTR_WIDE) ? 2.0f : 1.0f);
++
++ if (glyphs[start + code_idx].mode & ATTR_WDUMMY)
++ continue;
++
++ if (shaped.glyphs[code_idx].codepoint != 0) {
++ /* If symbol is found, put it into the specs. */
++ specs[numspecs].font = font->match;
++ specs[numspecs].glyph = shaped.glyphs[code_idx].codepoint;
++ specs[numspecs].x = xp + (short)shaped.positions[code_idx].x_offset;
++ specs[numspecs].y = yp + (short)shaped.positions[code_idx].y_offset;
++ xp += runewidth;
++ numspecs++;
++ } else {
++ /* If it's not found, try to fetch it through the font cache. */
++ for (f = 0; f < frclen; f++) {
++ glyphidx = XftCharIndex(xw.dpy, frc[f].font, rune);
++ /* Everything correct. */
++ if (glyphidx && frc[f].flags == frcflags)
++ break;
++ /* We got a default font for a not found glyph. */
++ if (!glyphidx && frc[f].flags == frcflags
++ && frc[f].unicodep == rune) {
++ break;
++ }
++ }
++
++ /* Nothing was found. Use fontconfig to find matching font. */
++ if (f >= frclen) {
++ if (!font->set)
++ font->set = FcFontSort(0, font->pattern,
++ 1, 0, &fcres);
++ fcsets[0] = font->set;
++
++ /*
++ * Nothing was found in the cache. Now use
++ * some dozen of Fontconfig calls to get the
++ * font for one single character.
++ *
++ * Xft and fontconfig are design failures.
++ */
++ fcpattern = FcPatternDuplicate(font->pattern);
++ fccharset = FcCharSetCreate();
++
++ FcCharSetAddChar(fccharset, rune);
++ FcPatternAddCharSet(fcpattern, FC_CHARSET,
++ fccharset);
++ FcPatternAddBool(fcpattern, FC_SCALABLE, 1);
++
++ FcConfigSubstitute(0, fcpattern,
++ FcMatchPattern);
++ FcDefaultSubstitute(fcpattern);
++
++ fontpattern = FcFontSetMatch(0, fcsets, 1,
++ fcpattern, &fcres);
++
++ /* Allocate memory for the new cache entry. */
++ if (frclen >= frccap) {
++ frccap += 16;
++ frc = xrealloc(frc, frccap * sizeof(Fontcache));
++ }
++
++ frc[frclen].font = XftFontOpenPattern(xw.dpy,
++ fontpattern);
++ if (!frc[frclen].font)
++ die("XftFontOpenPattern failed seeking fallback font: %s\n",
++ strerror(errno));
++ frc[frclen].flags = frcflags;
++ frc[frclen].unicodep = rune;
++
++ glyphidx = XftCharIndex(xw.dpy, frc[frclen].font, rune);
++
++ f = frclen;
++ frclen++;
++
++ FcPatternDestroy(fcpattern);
++ FcCharSetDestroy(fccharset);
++ }
++
++ specs[numspecs].font = frc[f].font;
++ specs[numspecs].glyph = glyphidx;
++ specs[numspecs].x = (short)xp;
++ specs[numspecs].y = (short)yp;
++ xp += runewidth;
++ numspecs++;
++ }
+ }
+- }
+
+- /* Nothing was found. Use fontconfig to find matching font. */
+- if (f >= frclen) {
+- if (!font->set)
+- font->set = FcFontSort(0, font->pattern,
+- 1, 0, &fcres);
+- fcsets[0] = font->set;
++ /* Cleanup and get ready for next segment. */
++ hbcleanup(&shaped);
++ start = i;
+
+- /*
+- * Nothing was found in the cache. Now use
+- * some dozen of Fontconfig calls to get the
+- * font for one single character.
+- *
+- * Xft and fontconfig are design failures.
+- */
+- fcpattern = FcPatternDuplicate(font->pattern);
+- fccharset = FcCharSetCreate();
+-
+- FcCharSetAddChar(fccharset, rune);
+- FcPatternAddCharSet(fcpattern, FC_CHARSET,
+- fccharset);
+- FcPatternAddBool(fcpattern, FC_SCALABLE, 1);
+-
+- FcConfigSubstitute(0, fcpattern,
+- FcMatchPattern);
+- FcDefaultSubstitute(fcpattern);
+-
+- fontpattern = FcFontSetMatch(0, fcsets, 1,
+- fcpattern, &fcres);
+-
+- /* Allocate memory for the new cache entry. */
+- if (frclen >= frccap) {
+- frccap += 16;
+- frc = xrealloc(frc, frccap * sizeof(Fontcache));
++ /* Determine font for glyph if different from previous glyph. */
++ if (prevmode != mode) {
++ prevmode = mode;
++ xresetfontsettings(mode, &font, &frcflags);
++ yp = winy + font->ascent;
+ }
+-
+- frc[frclen].font = XftFontOpenPattern(xw.dpy,
+- fontpattern);
+- if (!frc[frclen].font)
+- die("XftFontOpenPattern failed seeking fallback font: %s\n",
+- strerror(errno));
+- frc[frclen].flags = frcflags;
+- frc[frclen].unicodep = rune;
+-
+- glyphidx = XftCharIndex(xw.dpy, frc[frclen].font, rune);
+-
+- f = frclen;
+- frclen++;
+-
+- FcPatternDestroy(fcpattern);
+- FcCharSetDestroy(fccharset);
+ }
+-
+- specs[numspecs].font = frc[f].font;
+- specs[numspecs].glyph = glyphidx;
+- specs[numspecs].x = (short)xp;
+- specs[numspecs].y = (short)yp;
+- xp += runewidth;
+- numspecs++;
+ }
+
+ return numspecs;
+@@ -1517,14 +1556,17 @@ xdrawglyph(Glyph g, int x, int y)
+ }
+
+ void
+-xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og)
++xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og, Line line, int len)
+ {
+ Color drawcol;
+
+ /* remove the old cursor */
+ if (selected(ox, oy))
+ og.mode ^= ATTR_REVERSE;
+- xdrawglyph(og, ox, oy);
++
++ /* Redraw the line where cursor was previously.
++ * It will restore the ligatures broken by the cursor. */
++ xdrawline(line, 0, oy, len);
+
+ if (IS_SET(MODE_HIDE))
+ return;
diff --git a/patches/st-ligatures-scrollback-20221120-0.9.diff b/patches/st-ligatures-scrollback-20221120-0.9.diff
new file mode 100644
index 0000000..9a5686f
--- /dev/null
+++ b/patches/st-ligatures-scrollback-20221120-0.9.diff
@@ -0,0 +1,526 @@
+diff --git a/Makefile b/Makefile
+index 470ac86..38240da 100644
+--- a/Makefile
++++ b/Makefile
+@@ -4,7 +4,7 @@
+
+ include config.mk
+
+-SRC = st.c x.c
++SRC = st.c x.c hb.c
+ OBJ = $(SRC:.c=.o)
+
+ all: options st
+@@ -22,7 +22,8 @@ config.h:
+ $(CC) $(STCFLAGS) -c $<
+
+ st.o: config.h st.h win.h
+-x.o: arg.h config.h st.h win.h
++x.o: arg.h config.h st.h win.h hb.h
++hb.o: st.h
+
+ $(OBJ): config.h config.mk
+
+diff --git a/config.mk b/config.mk
+index 1e306f8..3e13e53 100644
+--- a/config.mk
++++ b/config.mk
+@@ -15,10 +15,12 @@ PKG_CONFIG = pkg-config
+ # includes and libs
+ INCS = -I$(X11INC) \
+ `$(PKG_CONFIG) --cflags fontconfig` \
+- `$(PKG_CONFIG) --cflags freetype2`
++ `$(PKG_CONFIG) --cflags freetype2` \
++ `$(PKG_CONFIG) --cflags harfbuzz`
+ LIBS = -L$(X11LIB) -lm -lrt -lX11 -lutil -lXft \
+ `$(PKG_CONFIG) --libs fontconfig` \
+- `$(PKG_CONFIG) --libs freetype2`
++ `$(PKG_CONFIG) --libs freetype2` \
++ `$(PKG_CONFIG) --libs harfbuzz`
+
+ # flags
+ STCPPFLAGS = -DVERSION=\"$(VERSION)\" -D_XOPEN_SOURCE=600
+diff --git a/hb.c b/hb.c
+new file mode 100644
+index 0000000..59b9200
+--- /dev/null
++++ b/hb.c
+@@ -0,0 +1,107 @@
++#include <stdlib.h>
++#include <stdio.h>
++#include <math.h>
++#include <X11/Xft/Xft.h>
++#include <X11/cursorfont.h>
++#include <hb.h>
++#include <hb-ft.h>
++
++#include "st.h"
++#include "hb.h"
++
++#define FEATURE(c1,c2,c3,c4) { .tag = HB_TAG(c1,c2,c3,c4), .value = 1, .start = HB_FEATURE_GLOBAL_START, .end = HB_FEATURE_GLOBAL_END }
++
++hb_font_t *hbfindfont(XftFont *match);
++
++typedef struct {
++ XftFont *match;
++ hb_font_t *font;
++} HbFontMatch;
++
++static int hbfontslen = 0;
++static HbFontMatch *hbfontcache = NULL;
++
++/*
++ * Poplulate the array with a list of font features, wrapped in FEATURE macro,
++ * e. g.
++ * FEATURE('c', 'a', 'l', 't'), FEATURE('d', 'l', 'i', 'g')
++ */
++hb_feature_t features[] = { };
++
++void
++hbunloadfonts()
++{
++ for (int i = 0; i < hbfontslen; i++) {
++ hb_font_destroy(hbfontcache[i].font);
++ XftUnlockFace(hbfontcache[i].match);
++ }
++
++ if (hbfontcache != NULL) {
++ free(hbfontcache);
++ hbfontcache = NULL;
++ }
++ hbfontslen = 0;
++}
++
++hb_font_t *
++hbfindfont(XftFont *match)
++{
++ for (int i = 0; i < hbfontslen; i++) {
++ if (hbfontcache[i].match == match)
++ return hbfontcache[i].font;
++ }
++
++ /* Font not found in cache, caching it now. */
++ hbfontcache = realloc(hbfontcache, sizeof(HbFontMatch) * (hbfontslen + 1));
++ FT_Face face = XftLockFace(match);
++ hb_font_t *font = hb_ft_font_create(face, NULL);
++ if (font == NULL)
++ die("Failed to load Harfbuzz font.");
++
++ hbfontcache[hbfontslen].match = match;
++ hbfontcache[hbfontslen].font = font;
++ hbfontslen += 1;
++
++ return font;
++}
++
++void hbtransform(HbTransformData *data, XftFont *xfont, const Glyph *glyphs, int start, int length) {
++ Rune rune;
++ ushort mode = USHRT_MAX;
++ unsigned int glyph_count;
++ int i, end = start + length;
++
++ hb_font_t *font = hbfindfont(xfont);
++ if (font == NULL)
++ return;
++
++ hb_buffer_t *buffer = hb_buffer_create();
++ hb_buffer_set_direction(buffer, HB_DIRECTION_LTR);
++
++ /* Fill buffer with codepoints. */
++ for (i = start; i < end; i++) {
++ rune = glyphs[i].u;
++ mode = glyphs[i].mode;
++ if (mode & ATTR_WDUMMY)
++ rune = 0x0020;
++ hb_buffer_add_codepoints(buffer, &rune, 1, 0, 1);
++ }
++
++ /* Shape the segment. */
++ hb_shape(font, buffer, features, sizeof(features)/sizeof(hb_feature_t));
++
++ /* Get new glyph info. */
++ hb_glyph_info_t *info = hb_buffer_get_glyph_infos(buffer, &glyph_count);
++ hb_glyph_position_t *pos = hb_buffer_get_glyph_positions(buffer, &glyph_count);
++
++ /** Fill the output. */
++ data->buffer = buffer;
++ data->glyphs = info;
++ data->positions = pos;
++ data->count = glyph_count;
++}
++
++void hbcleanup(HbTransformData *data) {
++ hb_buffer_destroy(data->buffer);
++ memset(data, 0, sizeof(HbTransformData));
++}
+diff --git a/hb.h b/hb.h
+new file mode 100644
+index 0000000..88de9bd
+--- /dev/null
++++ b/hb.h
+@@ -0,0 +1,14 @@
++#include <X11/Xft/Xft.h>
++#include <hb.h>
++#include <hb-ft.h>
++
++typedef struct {
++ hb_buffer_t *buffer;
++ hb_glyph_info_t *glyphs;
++ hb_glyph_position_t *positions;
++ unsigned int count;
++} HbTransformData;
++
++void hbunloadfonts();
++void hbtransform(HbTransformData *, XftFont *, const Glyph *, int, int);
++void hbcleanup(HbTransformData *);
+diff --git a/st.c b/st.c
+index 79ee9ba..7675db6 100644
+--- a/st.c
++++ b/st.c
+@@ -2711,7 +2711,8 @@ draw(void)
+ drawregion(0, 0, term.col, term.row);
+ if (term.scr == 0)
+ xdrawcursor(cx, term.c.y, term.line[term.c.y][cx],
+- term.ocx, term.ocy, term.line[term.ocy][term.ocx]);
++ term.ocx, term.ocy, term.line[term.ocy][term.ocx],
++ term.line[term.ocy], term.col);
+ term.ocx = cx;
+ term.ocy = term.c.y;
+ xfinishdraw();
+diff --git a/st.h b/st.h
+index 818a6f8..4e584b6 100644
+--- a/st.h
++++ b/st.h
+@@ -11,7 +11,8 @@
+ #define DIVCEIL(n, d) (((n) + ((d) - 1)) / (d))
+ #define DEFAULT(a, b) (a) = (a) ? (a) : (b)
+ #define LIMIT(x, a, b) (x) = (x) < (a) ? (a) : (x) > (b) ? (b) : (x)
+-#define ATTRCMP(a, b) ((a).mode != (b).mode || (a).fg != (b).fg || \
++#define ATTRCMP(a, b) (((a).mode & (~ATTR_WRAP)) != ((b).mode & (~ATTR_WRAP)) || \
++ (a).fg != (b).fg || \
+ (a).bg != (b).bg)
+ #define TIMEDIFF(t1, t2) ((t1.tv_sec-t2.tv_sec)*1000 + \
+ (t1.tv_nsec-t2.tv_nsec)/1E6)
+diff --git a/win.h b/win.h
+index 6de960d..94679e4 100644
+--- a/win.h
++++ b/win.h
+@@ -25,7 +25,7 @@ enum win_mode {
+
+ void xbell(void);
+ void xclipcopy(void);
+-void xdrawcursor(int, int, Glyph, int, int, Glyph);
++void xdrawcursor(int, int, Glyph, int, int, Glyph, Line, int);
+ void xdrawline(Line, int, int, int);
+ void xfinishdraw(void);
+ void xloadcols(void);
+diff --git a/x.c b/x.c
+index 2a3bd38..5feac09 100644
+--- a/x.c
++++ b/x.c
+@@ -19,6 +19,7 @@ char *argv0;
+ #include "arg.h"
+ #include "st.h"
+ #include "win.h"
++#include "hb.h"
+
+ /* types used in config.h */
+ typedef struct {
+@@ -141,6 +142,7 @@ typedef struct {
+ } DC;
+
+ static inline ushort sixd_to_16bit(int);
++static void xresetfontsettings(ushort mode, Font **font, int *frcflags);
+ static int xmakeglyphfontspecs(XftGlyphFontSpec *, const Glyph *, int, int, int);
+ static void xdrawglyphfontspecs(const XftGlyphFontSpec *, Glyph, int, int, int);
+ static void xdrawglyph(Glyph, int, int);
+@@ -1062,6 +1064,9 @@ xunloadfont(Font *f)
+ void
+ xunloadfonts(void)
+ {
++ /* Clear Harfbuzz font cache. */
++ hbunloadfonts();
++
+ /* Free the loaded fonts in the font cache. */
+ while (frclen > 0)
+ XftFontClose(xw.dpy, frc[--frclen].font);
+@@ -1239,6 +1244,22 @@ xinit(int cols, int rows)
+ xsel.xtarget = XA_STRING;
+ }
+
++void
++xresetfontsettings(ushort mode, Font **font, int *frcflags)
++{
++ *font = &dc.font;
++ if ((mode & ATTR_ITALIC) && (mode & ATTR_BOLD)) {
++ *font = &dc.ibfont;
++ *frcflags = FRC_ITALICBOLD;
++ } else if (mode & ATTR_ITALIC) {
++ *font = &dc.ifont;
++ *frcflags = FRC_ITALIC;
++ } else if (mode & ATTR_BOLD) {
++ *font = &dc.bfont;
++ *frcflags = FRC_BOLD;
++ }
++}
++
+ int
+ xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x, int y)
+ {
+@@ -1253,119 +1274,137 @@ xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x
+ FcPattern *fcpattern, *fontpattern;
+ FcFontSet *fcsets[] = { NULL };
+ FcCharSet *fccharset;
+- int i, f, numspecs = 0;
++ int i, f, length = 0, start = 0, numspecs = 0;
++ HbTransformData shaped = { 0 };
++
++ /* Initial values. */
++ mode = prevmode = glyphs[0].mode;
++ xresetfontsettings(mode, &font, &frcflags);
+
+ for (i = 0, xp = winx, yp = winy + font->ascent; i < len; ++i) {
+- /* Fetch rune and mode for current glyph. */
+- rune = glyphs[i].u;
+ mode = glyphs[i].mode;
+
+ /* Skip dummy wide-character spacing. */
+- if (mode == ATTR_WDUMMY)
++ if (mode & ATTR_WDUMMY)
+ continue;
+
+- /* Determine font for glyph if different from previous glyph. */
+- if (prevmode != mode) {
+- prevmode = mode;
+- font = &dc.font;
+- frcflags = FRC_NORMAL;
+- runewidth = win.cw * ((mode & ATTR_WIDE) ? 2.0f : 1.0f);
+- if ((mode & ATTR_ITALIC) && (mode & ATTR_BOLD)) {
+- font = &dc.ibfont;
+- frcflags = FRC_ITALICBOLD;
+- } else if (mode & ATTR_ITALIC) {
+- font = &dc.ifont;
+- frcflags = FRC_ITALIC;
+- } else if (mode & ATTR_BOLD) {
+- font = &dc.bfont;
+- frcflags = FRC_BOLD;
++ if (
++ prevmode != mode
++ || ATTRCMP(glyphs[start], glyphs[i])
++ || selected(x + i, y) != selected(x + start, y)
++ || i == (len - 1)
++ ) {
++ /* Handle 1-character wide segments and end of line */
++ length = i - start;
++ if (i == start) {
++ length = 1;
++ } else if (i == (len - 1)) {
++ length = (i - start + 1);
+ }
+- yp = winy + font->ascent;
+- }
+-
+- /* Lookup character index with default font. */
+- glyphidx = XftCharIndex(xw.dpy, font->match, rune);
+- if (glyphidx) {
+- specs[numspecs].font = font->match;
+- specs[numspecs].glyph = glyphidx;
+- specs[numspecs].x = (short)xp;
+- specs[numspecs].y = (short)yp;
+- xp += runewidth;
+- numspecs++;
+- continue;
+- }
+
+- /* Fallback on font cache, search the font cache for match. */
+- for (f = 0; f < frclen; f++) {
+- glyphidx = XftCharIndex(xw.dpy, frc[f].font, rune);
+- /* Everything correct. */
+- if (glyphidx && frc[f].flags == frcflags)
+- break;
+- /* We got a default font for a not found glyph. */
+- if (!glyphidx && frc[f].flags == frcflags
+- && frc[f].unicodep == rune) {
+- break;
++ /* Shape the segment. */
++ hbtransform(&shaped, font->match, glyphs, start, length);
++ for (int code_idx = 0; code_idx < shaped.count; code_idx++) {
++ rune = glyphs[start + code_idx].u;
++ runewidth = win.cw * ((glyphs[start + code_idx].mode & ATTR_WIDE) ? 2.0f : 1.0f);
++
++ if (glyphs[start + code_idx].mode & ATTR_WDUMMY)
++ continue;
++
++ if (shaped.glyphs[code_idx].codepoint != 0) {
++ /* If symbol is found, put it into the specs. */
++ specs[numspecs].font = font->match;
++ specs[numspecs].glyph = shaped.glyphs[code_idx].codepoint;
++ specs[numspecs].x = xp + (short)shaped.positions[code_idx].x_offset;
++ specs[numspecs].y = yp + (short)shaped.positions[code_idx].y_offset;
++ xp += runewidth;
++ numspecs++;
++ } else {
++ /* If it's not found, try to fetch it through the font cache. */
++ for (f = 0; f < frclen; f++) {
++ glyphidx = XftCharIndex(xw.dpy, frc[f].font, rune);
++ /* Everything correct. */
++ if (glyphidx && frc[f].flags == frcflags)
++ break;
++ /* We got a default font for a not found glyph. */
++ if (!glyphidx && frc[f].flags == frcflags
++ && frc[f].unicodep == rune) {
++ break;
++ }
++ }
++
++ /* Nothing was found. Use fontconfig to find matching font. */
++ if (f >= frclen) {
++ if (!font->set)
++ font->set = FcFontSort(0, font->pattern,
++ 1, 0, &fcres);
++ fcsets[0] = font->set;
++
++ /*
++ * Nothing was found in the cache. Now use
++ * some dozen of Fontconfig calls to get the
++ * font for one single character.
++ *
++ * Xft and fontconfig are design failures.
++ */
++ fcpattern = FcPatternDuplicate(font->pattern);
++ fccharset = FcCharSetCreate();
++
++ FcCharSetAddChar(fccharset, rune);
++ FcPatternAddCharSet(fcpattern, FC_CHARSET,
++ fccharset);
++ FcPatternAddBool(fcpattern, FC_SCALABLE, 1);
++
++ FcConfigSubstitute(0, fcpattern,
++ FcMatchPattern);
++ FcDefaultSubstitute(fcpattern);
++
++ fontpattern = FcFontSetMatch(0, fcsets, 1,
++ fcpattern, &fcres);
++
++ /* Allocate memory for the new cache entry. */
++ if (frclen >= frccap) {
++ frccap += 16;
++ frc = xrealloc(frc, frccap * sizeof(Fontcache));
++ }
++
++ frc[frclen].font = XftFontOpenPattern(xw.dpy,
++ fontpattern);
++ if (!frc[frclen].font)
++ die("XftFontOpenPattern failed seeking fallback font: %s\n",
++ strerror(errno));
++ frc[frclen].flags = frcflags;
++ frc[frclen].unicodep = rune;
++
++ glyphidx = XftCharIndex(xw.dpy, frc[frclen].font, rune);
++
++ f = frclen;
++ frclen++;
++
++ FcPatternDestroy(fcpattern);
++ FcCharSetDestroy(fccharset);
++ }
++
++ specs[numspecs].font = frc[f].font;
++ specs[numspecs].glyph = glyphidx;
++ specs[numspecs].x = (short)xp;
++ specs[numspecs].y = (short)yp;
++ xp += runewidth;
++ numspecs++;
++ }
+ }
+- }
+
+- /* Nothing was found. Use fontconfig to find matching font. */
+- if (f >= frclen) {
+- if (!font->set)
+- font->set = FcFontSort(0, font->pattern,
+- 1, 0, &fcres);
+- fcsets[0] = font->set;
++ /* Cleanup and get ready for next segment. */
++ hbcleanup(&shaped);
++ start = i;
+
+- /*
+- * Nothing was found in the cache. Now use
+- * some dozen of Fontconfig calls to get the
+- * font for one single character.
+- *
+- * Xft and fontconfig are design failures.
+- */
+- fcpattern = FcPatternDuplicate(font->pattern);
+- fccharset = FcCharSetCreate();
+-
+- FcCharSetAddChar(fccharset, rune);
+- FcPatternAddCharSet(fcpattern, FC_CHARSET,
+- fccharset);
+- FcPatternAddBool(fcpattern, FC_SCALABLE, 1);
+-
+- FcConfigSubstitute(0, fcpattern,
+- FcMatchPattern);
+- FcDefaultSubstitute(fcpattern);
+-
+- fontpattern = FcFontSetMatch(0, fcsets, 1,
+- fcpattern, &fcres);
+-
+- /* Allocate memory for the new cache entry. */
+- if (frclen >= frccap) {
+- frccap += 16;
+- frc = xrealloc(frc, frccap * sizeof(Fontcache));
++ /* Determine font for glyph if different from previous glyph. */
++ if (prevmode != mode) {
++ prevmode = mode;
++ xresetfontsettings(mode, &font, &frcflags);
++ yp = winy + font->ascent;
+ }
+-
+- frc[frclen].font = XftFontOpenPattern(xw.dpy,
+- fontpattern);
+- if (!frc[frclen].font)
+- die("XftFontOpenPattern failed seeking fallback font: %s\n",
+- strerror(errno));
+- frc[frclen].flags = frcflags;
+- frc[frclen].unicodep = rune;
+-
+- glyphidx = XftCharIndex(xw.dpy, frc[frclen].font, rune);
+-
+- f = frclen;
+- frclen++;
+-
+- FcPatternDestroy(fcpattern);
+- FcCharSetDestroy(fccharset);
+ }
+-
+- specs[numspecs].font = frc[f].font;
+- specs[numspecs].glyph = glyphidx;
+- specs[numspecs].x = (short)xp;
+- specs[numspecs].y = (short)yp;
+- xp += runewidth;
+- numspecs++;
+ }
+
+ return numspecs;
+@@ -1517,14 +1556,17 @@ xdrawglyph(Glyph g, int x, int y)
+ }
+
+ void
+-xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og)
++xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og, Line line, int len)
+ {
+ Color drawcol;
+
+ /* remove the old cursor */
+ if (selected(ox, oy))
+ og.mode ^= ATTR_REVERSE;
+- xdrawglyph(og, ox, oy);
++
++ /* Redraw the line where cursor was previously.
++ * It will restore the ligatures broken by the cursor. */
++ xdrawline(line, 0, oy, len);
+
+ if (IS_SET(MODE_HIDE))
+ return;
diff --git a/patches/st-scrollback-0.8.5.diff b/patches/st-scrollback-0.8.5.diff
new file mode 100644
index 0000000..750111d
--- /dev/null
+++ b/patches/st-scrollback-0.8.5.diff
@@ -0,0 +1,350 @@
+diff --git a/config.def.h b/config.def.h
+index 91ab8ca..e3b469b 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 51049ba..cd750f2 100644
+--- a/st.c
++++ b/st.c
+@@ -35,6 +35,7 @@
+ #define ESC_ARG_SIZ 16
+ #define STR_BUF_SIZ ESC_BUF_SIZ
+ #define STR_ARG_SIZ ESC_ARG_SIZ
++#define HISTSIZE 2000
+
+ /* macros */
+ #define IS_SET(flag) ((term.mode & (flag)) != 0)
+@@ -42,6 +43,9 @@
+ #define ISCONTROLC1(c) (BETWEEN(c, 0x80, 0x9f))
+ #define ISCONTROL(c) (ISCONTROLC0(c) || ISCONTROLC1(c))
+ #define ISDELIM(u) (u && wcschr(worddelimiters, u))
++#define TLINE(y) ((y) < term.scr ? term.hist[((y) + term.histi - \
++ term.scr + HISTSIZE + 1) % HISTSIZE] : \
++ term.line[(y) - term.scr])
+
+ enum term_mode {
+ MODE_WRAP = 1 << 0,
+@@ -115,6 +119,9 @@ typedef struct {
+ int col; /* nb col */
+ Line *line; /* screen */
+ Line *alt; /* alternate screen */
++ Line hist[HISTSIZE]; /* history buffer */
++ int histi; /* history index */
++ int scr; /* scroll back */
+ int *dirty; /* dirtyness of lines */
+ TCursor c; /* cursor */
+ int ocx; /* old cursor col */
+@@ -184,8 +191,8 @@ static void tnewline(int);
+ static void tputtab(int);
+ static void tputc(Rune);
+ static void treset(void);
+-static void tscrollup(int, int);
+-static void tscrolldown(int, int);
++static void tscrollup(int, int, int);
++static void tscrolldown(int, int, int);
+ static void tsetattr(const int *, int);
+ static void tsetchar(Rune, const Glyph *, int, int);
+ static void tsetdirt(int, int);
+@@ -416,10 +423,10 @@ tlinelen(int y)
+ {
+ int i = term.col;
+
+- if (term.line[y][i - 1].mode & ATTR_WRAP)
++ if (TLINE(y)[i - 1].mode & ATTR_WRAP)
+ return i;
+
+- while (i > 0 && term.line[y][i - 1].u == ' ')
++ while (i > 0 && TLINE(y)[i - 1].u == ' ')
+ --i;
+
+ return i;
+@@ -528,7 +535,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;
+@@ -543,14 +550,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)))
+@@ -571,14 +578,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;
+ }
+@@ -609,13 +616,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;
+
+@@ -851,6 +858,9 @@ void
+ ttywrite(const char *s, size_t n, int may_echo)
+ {
+ const char *next;
++ Arg arg = (Arg) { .i = term.scr };
++
++ kscrolldown(&arg);
+
+ if (may_echo && IS_SET(MODE_ECHO))
+ twrite(s, n, 1);
+@@ -1062,12 +1072,52 @@ tswapscreen(void)
+ }
+
+ void
+-tscrolldown(int orig, int n)
++kscrolldown(const Arg* a)
++{
++ int n = a->i;
++
++ if (n < 0)
++ n = term.row + n;
++
++ if (n > term.scr)
++ n = term.scr;
++
++ if (term.scr > 0) {
++ term.scr -= n;
++ selscroll(0, -n);
++ tfulldirt();
++ }
++}
++
++void
++kscrollup(const Arg* a)
++{
++ int n = a->i;
++
++ if (n < 0)
++ n = term.row + n;
++
++ if (term.scr <= HISTSIZE-n) {
++ term.scr += n;
++ selscroll(0, n);
++ tfulldirt();
++ }
++}
++
++void
++tscrolldown(int orig, int n, int copyhist)
+ {
+ int i;
+ Line temp;
+
+ LIMIT(n, 0, term.bot-orig+1);
++ if (copyhist) {
++ term.histi = (term.histi - 1 + HISTSIZE) % HISTSIZE;
++ temp = term.hist[term.histi];
++ term.hist[term.histi] = term.line[term.bot];
++ term.line[term.bot] = temp;
++ }
++
+
+ tsetdirt(orig, term.bot-n);
+ tclearregion(0, term.bot-n+1, term.col-1, term.bot);
+@@ -1078,17 +1128,28 @@ tscrolldown(int orig, int n)
+ term.line[i-n] = temp;
+ }
+
+- selscroll(orig, n);
++ if (term.scr == 0)
++ selscroll(orig, n);
+ }
+
+ void
+-tscrollup(int orig, int n)
++tscrollup(int orig, int n, int copyhist)
+ {
+ int i;
+ Line temp;
+
+ LIMIT(n, 0, term.bot-orig+1);
+
++ if (copyhist) {
++ term.histi = (term.histi + 1) % HISTSIZE;
++ temp = term.hist[term.histi];
++ term.hist[term.histi] = term.line[orig];
++ term.line[orig] = temp;
++ }
++
++ if (term.scr > 0 && term.scr < HISTSIZE)
++ term.scr = MIN(term.scr + n, HISTSIZE-1);
++
+ tclearregion(0, orig, term.col-1, orig+n-1);
+ tsetdirt(orig+n, term.bot);
+
+@@ -1098,7 +1159,8 @@ tscrollup(int orig, int n)
+ term.line[i+n] = temp;
+ }
+
+- selscroll(orig, -n);
++ if (term.scr == 0)
++ selscroll(orig, -n);
+ }
+
+ void
+@@ -1127,7 +1189,7 @@ tnewline(int first_col)
+ int y = term.c.y;
+
+ if (y == term.bot) {
+- tscrollup(term.top, 1);
++ tscrollup(term.top, 1, 1);
+ } else {
+ y++;
+ }
+@@ -1292,14 +1354,14 @@ void
+ tinsertblankline(int n)
+ {
+ if (BETWEEN(term.c.y, term.top, term.bot))
+- tscrolldown(term.c.y, n);
++ tscrolldown(term.c.y, n, 0);
+ }
+
+ void
+ tdeleteline(int n)
+ {
+ if (BETWEEN(term.c.y, term.top, term.bot))
+- tscrollup(term.c.y, n);
++ tscrollup(term.c.y, n, 0);
+ }
+
+ int32_t
+@@ -1736,11 +1798,11 @@ csihandle(void)
+ break;
+ case 'S': /* SU -- Scroll <n> line up */
+ DEFAULT(csiescseq.arg[0], 1);
+- tscrollup(term.top, csiescseq.arg[0]);
++ tscrollup(term.top, csiescseq.arg[0], 0);
+ break;
+ case 'T': /* SD -- Scroll <n> line down */
+ DEFAULT(csiescseq.arg[0], 1);
+- tscrolldown(term.top, csiescseq.arg[0]);
++ tscrolldown(term.top, csiescseq.arg[0], 0);
+ break;
+ case 'L': /* IL -- Insert <n> blank lines */
+ DEFAULT(csiescseq.arg[0], 1);
+@@ -2330,7 +2392,7 @@ eschandle(uchar ascii)
+ return 0;
+ case 'D': /* IND -- Linefeed */
+ if (term.c.y == term.bot) {
+- tscrollup(term.top, 1);
++ tscrollup(term.top, 1, 1);
+ } else {
+ tmoveto(term.c.x, term.c.y+1);
+ }
+@@ -2343,7 +2405,7 @@ eschandle(uchar ascii)
+ break;
+ case 'M': /* RI -- Reverse index */
+ if (term.c.y == term.top) {
+- tscrolldown(term.top, 1);
++ tscrolldown(term.top, 1, 1);
+ } else {
+ tmoveto(term.c.x, term.c.y-1);
+ }
+@@ -2557,7 +2619,7 @@ twrite(const char *buf, int buflen, int show_ctrl)
+ void
+ tresize(int col, int row)
+ {
+- int i;
++ int i, j;
+ int minrow = MIN(row, term.row);
+ int mincol = MIN(col, term.col);
+ int *bp;
+@@ -2594,6 +2656,14 @@ tresize(int col, int row)
+ term.dirty = xrealloc(term.dirty, row * sizeof(*term.dirty));
+ term.tabs = xrealloc(term.tabs, col * sizeof(*term.tabs));
+
++ for (i = 0; i < HISTSIZE; i++) {
++ term.hist[i] = xrealloc(term.hist[i], col * sizeof(Glyph));
++ for (j = mincol; j < col; j++) {
++ term.hist[i][j] = term.c.attr;
++ term.hist[i][j].u = ' ';
++ }
++ }
++
+ /* 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));
+@@ -2652,7 +2722,7 @@ drawregion(int x1, int y1, int x2, int y2)
+ continue;
+
+ term.dirty[y] = 0;
+- xdrawline(term.line[y], x1, y, x2);
++ xdrawline(TLINE(y), x1, y, x2);
+ }
+ }
+
+@@ -2673,8 +2743,9 @@ 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]);
++ if (term.scr == 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();
+diff --git a/st.h b/st.h
+index 519b9bd..da36b34 100644
+--- a/st.h
++++ b/st.h
+@@ -81,6 +81,8 @@ void die(const char *, ...);
+ void redraw(void);
+ void draw(void);
+
++void kscrolldown(const Arg *);
++void kscrollup(const Arg *);
+ void printscreen(const Arg *);
+ void printsel(const Arg *);
+ void sendbreak(const Arg *);
diff --git a/patches/st-scrollback-mouse-20220127-2c5edf2.diff b/patches/st-scrollback-mouse-20220127-2c5edf2.diff
new file mode 100644
index 0000000..5c47abc
--- /dev/null
+++ b/patches/st-scrollback-mouse-20220127-2c5edf2.diff
@@ -0,0 +1,25 @@
+From b5d3351a21442a842e01e8c0317603b6890b379c Mon Sep 17 00:00:00 2001
+From: asparagii <michele.lambertucci1@gmail.com>
+Date: Thu, 27 Jan 2022 15:44:02 +0100
+Subject: [PATCH] st-scrollback-mouse
+
+---
+ config.def.h | 2 ++
+ 1 file changed, 2 insertions(+)
+
+diff --git a/config.def.h b/config.def.h
+index e3b469b..c217315 100644
+--- a/config.def.h
++++ b/config.def.h
+@@ -176,6 +176,8 @@ static uint forcemousemod = ShiftMask;
+ */
+ static MouseShortcut mshortcuts[] = {
+ /* mask button function argument release */
++ { ShiftMask, Button4, kscrollup, {.i = 1} },
++ { ShiftMask, Button5, kscrolldown, {.i = 1} },
+ { XK_ANY_MOD, Button2, selpaste, {.i = 0}, 1 },
+ { ShiftMask, Button4, ttysend, {.s = "\033[5;2~"} },
+ { XK_ANY_MOD, Button4, ttysend, {.s = "\031"} },
+--
+2.34.1
+
diff --git a/patches/st-scrollback-mouse-altscreen-20220127-2c5edf2.diff b/patches/st-scrollback-mouse-altscreen-20220127-2c5edf2.diff
new file mode 100644
index 0000000..6a8722b
--- /dev/null
+++ b/patches/st-scrollback-mouse-altscreen-20220127-2c5edf2.diff
@@ -0,0 +1,78 @@
+From 580e3f386e9215707100e9ba44797701943fd927 Mon Sep 17 00:00:00 2001
+From: asparagii <michele.lambertucci1@gmail.com>
+Date: Thu, 27 Jan 2022 15:49:27 +0100
+Subject: [PATCH] st-scrollback-mouse-altscreen
+
+---
+ config.def.h | 4 ++--
+ st.c | 5 +++++
+ st.h | 1 +
+ x.c | 2 ++
+ 4 files changed, 10 insertions(+), 2 deletions(-)
+
+diff --git a/config.def.h b/config.def.h
+index c217315..c223706 100644
+--- a/config.def.h
++++ b/config.def.h
+@@ -176,8 +176,8 @@ static uint forcemousemod = ShiftMask;
+ */
+ static MouseShortcut mshortcuts[] = {
+ /* mask button function argument release */
+- { ShiftMask, Button4, kscrollup, {.i = 1} },
+- { ShiftMask, Button5, kscrolldown, {.i = 1} },
++ { XK_ANY_MOD, Button4, kscrollup, {.i = 1}, 0, /* !alt */ -1 },
++ { XK_ANY_MOD, Button5, kscrolldown, {.i = 1}, 0, /* !alt */ -1 },
+ { XK_ANY_MOD, Button2, selpaste, {.i = 0}, 1 },
+ { ShiftMask, Button4, ttysend, {.s = "\033[5;2~"} },
+ { XK_ANY_MOD, Button4, ttysend, {.s = "\031"} },
+diff --git a/st.c b/st.c
+index f3af82b..876a6bf 100644
+--- a/st.c
++++ b/st.c
+@@ -1060,6 +1060,11 @@ tnew(int col, int row)
+ treset();
+ }
+
++int tisaltscr(void)
++{
++ return IS_SET(MODE_ALTSCREEN);
++}
++
+ void
+ tswapscreen(void)
+ {
+diff --git a/st.h b/st.h
+index da36b34..e95c6f8 100644
+--- a/st.h
++++ b/st.h
+@@ -89,6 +89,7 @@ void sendbreak(const Arg *);
+ void toggleprinter(const Arg *);
+
+ int tattrset(int);
++int tisaltscr(void);
+ void tnew(int, int);
+ void tresize(int, int);
+ void tsetdirtattr(int);
+diff --git a/x.c b/x.c
+index cd96575..9274556 100644
+--- a/x.c
++++ b/x.c
+@@ -34,6 +34,7 @@ typedef struct {
+ void (*func)(const Arg *);
+ const Arg arg;
+ uint release;
++ int altscrn; /* 0: don't care, -1: not alt screen, 1: alt screen */
+ } MouseShortcut;
+
+ typedef struct {
+@@ -455,6 +456,7 @@ mouseaction(XEvent *e, uint release)
+ for (ms = mshortcuts; ms < mshortcuts + LEN(mshortcuts); ms++) {
+ if (ms->release == release &&
+ ms->button == e->xbutton.button &&
++ (!ms->altscrn || (ms->altscrn == (tisaltscr() ? 1 : -1))) &&
+ (match(ms->mod, state) || /* exact or forced */
+ match(ms->mod, state & ~forcemousemod))) {
+ ms->func(&(ms->arg));
+--
+2.34.1
+
diff --git a/patches/st-scrollback-mouse-increment-0.8.2.diff b/patches/st-scrollback-mouse-increment-0.8.2.diff
new file mode 100644
index 0000000..9556a9d
--- /dev/null
+++ b/patches/st-scrollback-mouse-increment-0.8.2.diff
@@ -0,0 +1,34 @@
+From 63e717e51dcd2f59c7a3aa75b659926aa92e08f3 Mon Sep 17 00:00:00 2001
+From: Jacob Louis Prosser <geriatricjacob@cumallover.me>
+Date: Mon, 5 Aug 2019 18:20:25 +1000
+Subject: [st] [patch] Exposed variable to easily change mouse scroll increment.
+
+---
+ config.def.h | 5 +++--
+ 1 file changed, 3 insertions(+), 2 deletions(-)
+
+diff --git a/config.def.h b/config.def.h
+index ad20c4c..47e4b66 100644
+--- a/config.def.h
++++ b/config.def.h
+@@ -154,6 +154,7 @@ static unsigned int defaultattr = 11;
+ * Internal mouse shortcuts.
+ * Beware that overloading Button1 will disable the selection.
+ */
++const unsigned int mousescrollincrement = 1;
+ static MouseShortcut mshortcuts[] = {
+ /* button mask string */
+ { Button4, XK_NO_MOD, "\031" },
+@@ -162,8 +163,8 @@ static MouseShortcut mshortcuts[] = {
+
+ MouseKey mkeys[] = {
+ /* button mask function argument */
+- { Button4, ShiftMask, kscrollup, {.i = 1} },
+- { Button5, ShiftMask, kscrolldown, {.i = 1} },
++ { Button4, ShiftMask, kscrollup, {.i = mousescrollincrement} },
++ { Button5, ShiftMask, kscrolldown, {.i = mousescrollincrement} },
+ };
+
+ /* Internal keyboard shortcuts. */
+--
+2.22.0
diff --git a/patches/st-scrollback-reflow-0.8.5.diff b/patches/st-scrollback-reflow-0.8.5.diff
new file mode 100644
index 0000000..085c828
--- /dev/null
+++ b/patches/st-scrollback-reflow-0.8.5.diff
@@ -0,0 +1,1478 @@
+diff --git a/st.c b/st.c
+index 91e7077..a76d983 100644
+--- a/st.c
++++ b/st.c
+@@ -36,6 +36,7 @@
+ #define STR_BUF_SIZ ESC_BUF_SIZ
+ #define STR_ARG_SIZ ESC_ARG_SIZ
+ #define HISTSIZE 2000
++#define RESIZEBUFFER 1000
+
+ /* macros */
+ #define IS_SET(flag) ((term.mode & (flag)) != 0)
+@@ -43,9 +44,22 @@
+ #define ISCONTROLC1(c) (BETWEEN(c, 0x80, 0x9f))
+ #define ISCONTROL(c) (ISCONTROLC0(c) || ISCONTROLC1(c))
+ #define ISDELIM(u) (u && wcschr(worddelimiters, u))
+-#define TLINE(y) ((y) < term.scr ? term.hist[((y) + term.histi - \
+- term.scr + HISTSIZE + 1) % HISTSIZE] : \
+- term.line[(y) - term.scr])
++
++#define TLINE(y) ( \
++ (y) < term.scr ? term.hist[(term.histi + (y) - term.scr + 1 + HISTSIZE) % HISTSIZE] \
++ : term.line[(y) - term.scr] \
++)
++
++#define TLINEABS(y) ( \
++ (y) < 0 ? term.hist[(term.histi + (y) + 1 + HISTSIZE) % HISTSIZE] : term.line[(y)] \
++)
++
++#define UPDATEWRAPNEXT(alt, col) do { \
++ if ((term.c.state & CURSOR_WRAPNEXT) && term.c.x + term.wrapcwidth[alt] < col) { \
++ term.c.x += term.wrapcwidth[alt]; \
++ term.c.state &= ~CURSOR_WRAPNEXT; \
++ } \
++} while (0);
+
+ enum term_mode {
+ MODE_WRAP = 1 << 0,
+@@ -57,6 +71,12 @@ enum term_mode {
+ MODE_UTF8 = 1 << 6,
+ };
+
++enum scroll_mode {
++ SCROLL_RESIZE = -1,
++ SCROLL_NOSAVEHIST = 0,
++ SCROLL_SAVEHIST = 1
++};
++
+ enum cursor_movement {
+ CURSOR_SAVE,
+ CURSOR_LOAD
+@@ -118,10 +138,11 @@ typedef struct {
+ int row; /* nb row */
+ int col; /* nb col */
+ Line *line; /* screen */
+- Line *alt; /* alternate screen */
+ Line hist[HISTSIZE]; /* history buffer */
+- int histi; /* history index */
+- int scr; /* scroll back */
++ int histi; /* history index */
++ int histf; /* nb history available */
++ int scr; /* scroll back */
++ int wrapcwidth[2]; /* used in updating WRAPNEXT when resizing */
+ int *dirty; /* dirtyness of lines */
+ TCursor c; /* cursor */
+ int ocx; /* old cursor col */
+@@ -179,26 +200,37 @@ static void tprinter(char *, size_t);
+ static void tdumpsel(void);
+ static void tdumpline(int);
+ static void tdump(void);
+-static void tclearregion(int, int, int, int);
++static void tclearregion(int, int, int, int, int);
+ static void tcursor(int);
++static void tclearglyph(Glyph *, int);
++static void tresetcursor(void);
+ static void tdeletechar(int);
+ static void tdeleteline(int);
+ static void tinsertblank(int);
+ static void tinsertblankline(int);
+-static int tlinelen(int);
++static int tlinelen(Line len);
++static int tiswrapped(Line line);
++static char *tgetglyphs(char *, const Glyph *, const Glyph *);
++static size_t tgetline(char *, const Glyph *);
+ static void tmoveto(int, int);
+ static void tmoveato(int, int);
+ static void tnewline(int);
+ static void tputtab(int);
+ static void tputc(Rune);
+ static void treset(void);
+-static void tscrollup(int, int, int);
+-static void tscrolldown(int, int, int);
++static void tscrollup(int, int, int, int);
++static void tscrolldown(int, int);
++static void treflow(int, int);
++static void rscrolldown(int);
++static void tresizedef(int, int);
++static void tresizealt(int, int);
+ static void tsetattr(const int *, int);
+ static void tsetchar(Rune, const Glyph *, int, int);
+ static void tsetdirt(int, int);
+ static void tsetscroll(int, int);
+ static void tswapscreen(void);
++static void tloaddefscreen(int, int);
++static void tloadaltscreen(int, int);
+ static void tsetmode(int, int, const int *, int);
+ static int twrite(const char *, int, int);
+ static void tfulldirt(void);
+@@ -212,7 +244,10 @@ static void tstrsequence(uchar);
+ static void drawregion(int, int, int, int);
+
+ static void selnormalize(void);
+-static void selscroll(int, int);
++static void selscroll(int, int, int);
++static void selmove(int);
++static void selremove(void);
++static int regionselected(int, int, int, int);
+ static void selsnap(int *, int *, int);
+
+ static size_t utf8decode(const char *, Rune *, size_t);
+@@ -412,17 +447,46 @@ selinit(void)
+ }
+
+ int
+-tlinelen(int y)
++tlinelen(Line line)
+ {
+- int i = term.col;
++ int i = term.col - 1;
++
++ for (; i >= 0 && !(line[i].mode & (ATTR_SET | ATTR_WRAP)); i--);
++ return i + 1;
++}
+
+- if (TLINE(y)[i - 1].mode & ATTR_WRAP)
+- return i;
++int
++tiswrapped(Line line)
++{
++ int len = tlinelen(line);
+
+- while (i > 0 && TLINE(y)[i - 1].u == ' ')
+- --i;
++ return len > 0 && (line[len - 1].mode & ATTR_WRAP);
++}
+
+- return i;
++char *
++tgetglyphs(char *buf, const Glyph *gp, const Glyph *lgp)
++{
++ while (gp <= lgp)
++ if (gp->mode & ATTR_WDUMMY) {
++ gp++;
++ } else {
++ buf += utf8encode((gp++)->u, buf);
++ }
++ return buf;
++}
++
++size_t
++tgetline(char *buf, const Glyph *fgp)
++{
++ char *ptr;
++ const Glyph *lgp = &fgp[term.col - 1];
++
++ while (lgp > fgp && !(lgp->mode & (ATTR_SET | ATTR_WRAP)))
++ lgp--;
++ ptr = tgetglyphs(buf, fgp, lgp);
++ if (!(lgp->mode & ATTR_WRAP))
++ *(ptr++) = '\n';
++ return ptr - buf;
+ }
+
+ void
+@@ -462,10 +526,11 @@ selextend(int col, int row, int type, int done)
+
+ sel.oe.x = col;
+ sel.oe.y = row;
+- selnormalize();
+ sel.type = type;
++ selnormalize();
+
+- if (oldey != sel.oe.y || oldex != sel.oe.x || oldtype != sel.type || sel.mode == SEL_EMPTY)
++ if (oldey != sel.oe.y || oldex != sel.oe.x ||
++ oldtype != sel.type || sel.mode == SEL_EMPTY)
+ tsetdirt(MIN(sel.nb.y, oldsby), MAX(sel.ne.y, oldsey));
+
+ sel.mode = done ? SEL_IDLE : SEL_READY;
+@@ -492,36 +557,43 @@ selnormalize(void)
+ /* expand selection over line breaks */
+ if (sel.type == SEL_RECTANGULAR)
+ return;
+- i = tlinelen(sel.nb.y);
+- if (i < sel.nb.x)
++
++ i = tlinelen(TLINE(sel.nb.y));
++ if (sel.nb.x > i)
+ sel.nb.x = i;
+- if (tlinelen(sel.ne.y) <= sel.ne.x)
+- sel.ne.x = term.col - 1;
++ if (sel.ne.x >= tlinelen(TLINE(sel.ne.y)))
++ sel.ne.x = term.col - 1;
+ }
+
+ int
+-selected(int x, int y)
++regionselected(int x1, int y1, int x2, int y2)
+ {
+- if (sel.mode == SEL_EMPTY || sel.ob.x == -1 ||
+- sel.alt != IS_SET(MODE_ALTSCREEN))
++ if (sel.ob.x == -1 || sel.mode == SEL_EMPTY ||
++ sel.alt != IS_SET(MODE_ALTSCREEN) || sel.nb.y > y2 || sel.ne.y < y1)
+ return 0;
+
+- if (sel.type == SEL_RECTANGULAR)
+- return BETWEEN(y, sel.nb.y, sel.ne.y)
+- && BETWEEN(x, sel.nb.x, sel.ne.x);
++ return (sel.type == SEL_RECTANGULAR) ? sel.nb.x <= x2 && sel.ne.x >= x1
++ : (sel.nb.y != y2 || sel.nb.x <= x2) &&
++ (sel.ne.y != y1 || sel.ne.x >= x1);
++}
+
+- return BETWEEN(y, sel.nb.y, sel.ne.y)
+- && (y != sel.nb.y || x >= sel.nb.x)
+- && (y != sel.ne.y || x <= sel.ne.x);
++int
++selected(int x, int y)
++{
++ return regionselected(x, y, x, y);
+ }
+
+ void
+ selsnap(int *x, int *y, int direction)
+ {
+ int newx, newy, xt, yt;
++ int rtop = 0, rbot = term.row - 1;
+ int delim, prevdelim;
+ const Glyph *gp, *prevgp;
+
++ if (!IS_SET(MODE_ALTSCREEN))
++ rtop += -term.histf + term.scr, rbot += term.scr;
++
+ switch (sel.snap) {
+ case SNAP_WORD:
+ /*
+@@ -536,7 +608,7 @@ selsnap(int *x, int *y, int direction)
+ if (!BETWEEN(newx, 0, term.col - 1)) {
+ newy += direction;
+ newx = (newx + term.col) % term.col;
+- if (!BETWEEN(newy, 0, term.row - 1))
++ if (!BETWEEN(newy, rtop, rbot))
+ break;
+
+ if (direction > 0)
+@@ -547,13 +619,13 @@ selsnap(int *x, int *y, int direction)
+ break;
+ }
+
+- if (newx >= tlinelen(newy))
++ if (newx >= tlinelen(TLINE(newy)))
+ break;
+
+ gp = &TLINE(newy)[newx];
+ delim = ISDELIM(gp->u);
+- if (!(gp->mode & ATTR_WDUMMY) && (delim != prevdelim
+- || (delim && gp->u != prevgp->u)))
++ if (!(gp->mode & ATTR_WDUMMY) && (delim != prevdelim ||
++ (delim && !(gp->u == ' ' && prevgp->u == ' '))))
+ break;
+
+ *x = newx;
+@@ -570,18 +642,14 @@ selsnap(int *x, int *y, int direction)
+ */
+ *x = (direction < 0) ? 0 : term.col - 1;
+ if (direction < 0) {
+- for (; *y > 0; *y += direction) {
+- if (!(TLINE(*y-1)[term.col-1].mode
+- & ATTR_WRAP)) {
++ for (; *y > rtop; *y -= 1) {
++ if (!tiswrapped(TLINE(*y-1)))
+ break;
+- }
+ }
+ } else if (direction > 0) {
+- for (; *y < term.row-1; *y += direction) {
+- if (!(TLINE(*y)[term.col-1].mode
+- & ATTR_WRAP)) {
++ for (; *y < rbot; *y += 1) {
++ if (!tiswrapped(TLINE(*y)))
+ break;
+- }
+ }
+ }
+ break;
+@@ -592,40 +660,34 @@ char *
+ getsel(void)
+ {
+ char *str, *ptr;
+- int y, bufsize, lastx, linelen;
+- const Glyph *gp, *last;
++ int y, lastx, linelen;
++ const Glyph *gp, *lgp;
+
+- if (sel.ob.x == -1)
++ if (sel.ob.x == -1 || sel.alt != IS_SET(MODE_ALTSCREEN))
+ return NULL;
+
+- bufsize = (term.col+1) * (sel.ne.y-sel.nb.y+1) * UTF_SIZ;
+- ptr = str = xmalloc(bufsize);
++ str = xmalloc((term.col + 1) * (sel.ne.y - sel.nb.y + 1) * UTF_SIZ);
++ ptr = str;
+
+ /* append every set & selected glyph to the selection */
+ for (y = sel.nb.y; y <= sel.ne.y; y++) {
+- if ((linelen = tlinelen(y)) == 0) {
++ Line line = TLINE(y);
++
++ if ((linelen = tlinelen(line)) == 0) {
+ *ptr++ = '\n';
+ continue;
+ }
+
+ if (sel.type == SEL_RECTANGULAR) {
+- gp = &TLINE(y)[sel.nb.x];
++ gp = &line[sel.nb.x];
+ lastx = sel.ne.x;
+ } else {
+- gp = &TLINE(y)[sel.nb.y == y ? sel.nb.x : 0];
++ gp = &line[sel.nb.y == y ? sel.nb.x : 0];
+ lastx = (sel.ne.y == y) ? sel.ne.x : term.col-1;
+ }
+- last = &TLINE(y)[MIN(lastx, linelen-1)];
+- while (last >= gp && last->u == ' ')
+- --last;
+-
+- for ( ; gp <= last; ++gp) {
+- if (gp->mode & ATTR_WDUMMY)
+- continue;
+-
+- ptr += utf8encode(gp->u, ptr);
+- }
++ lgp = &line[MIN(lastx, linelen-1)];
+
++ ptr = tgetglyphs(ptr, gp, lgp);
+ /*
+ * Copy and pasting of line endings is inconsistent
+ * in the inconsistent terminal and GUI world.
+@@ -636,10 +698,10 @@ getsel(void)
+ * FIXME: Fix the computer world.
+ */
+ if ((y < sel.ne.y || lastx >= linelen) &&
+- (!(last->mode & ATTR_WRAP) || sel.type == SEL_RECTANGULAR))
++ (!(lgp->mode & ATTR_WRAP) || sel.type == SEL_RECTANGULAR))
+ *ptr++ = '\n';
+ }
+- *ptr = 0;
++ *ptr = '\0';
+ return str;
+ }
+
+@@ -648,9 +710,15 @@ selclear(void)
+ {
+ if (sel.ob.x == -1)
+ return;
++ selremove();
++ tsetdirt(sel.nb.y, sel.ne.y);
++}
++
++void
++selremove(void)
++{
+ sel.mode = SEL_IDLE;
+ sel.ob.x = -1;
+- tsetdirt(sel.nb.y, sel.ne.y);
+ }
+
+ void
+@@ -851,10 +919,8 @@ void
+ ttywrite(const char *s, size_t n, int may_echo)
+ {
+ const char *next;
+- Arg arg = (Arg) { .i = term.scr };
+-
+- kscrolldown(&arg);
+
++ kscrolldown(&((Arg){ .i = term.scr }));
+ if (may_echo && IS_SET(MODE_ECHO))
+ twrite(s, n, 1);
+
+@@ -990,7 +1056,7 @@ tsetdirtattr(int attr)
+ for (i = 0; i < term.row-1; i++) {
+ for (j = 0; j < term.col-1; j++) {
+ if (term.line[i][j].mode & attr) {
+- tsetdirt(i, i);
++ term.dirty[i] = 1;
+ break;
+ }
+ }
+@@ -1000,7 +1066,8 @@ tsetdirtattr(int attr)
+ void
+ tfulldirt(void)
+ {
+- tsetdirt(0, term.row-1);
++ for (int i = 0; i < term.row; i++)
++ term.dirty[i] = 1;
+ }
+
+ void
+@@ -1017,51 +1084,116 @@ tcursor(int mode)
+ }
+ }
+
++void
++tresetcursor(void)
++{
++ term.c = (TCursor){ { .mode = ATTR_NULL, .fg = defaultfg, .bg = defaultbg },
++ .x = 0, .y = 0, .state = CURSOR_DEFAULT };
++}
++
+ void
+ treset(void)
+ {
+ uint i;
++ int x, y;
+
+- term.c = (TCursor){{
+- .mode = ATTR_NULL,
+- .fg = defaultfg,
+- .bg = defaultbg
+- }, .x = 0, .y = 0, .state = CURSOR_DEFAULT};
++ tresetcursor();
+
+ memset(term.tabs, 0, term.col * sizeof(*term.tabs));
+ for (i = tabspaces; i < term.col; i += tabspaces)
+ term.tabs[i] = 1;
+ term.top = 0;
++ term.histf = 0;
++ term.scr = 0;
+ term.bot = term.row - 1;
+ term.mode = MODE_WRAP|MODE_UTF8;
+ memset(term.trantbl, CS_USA, sizeof(term.trantbl));
+ term.charset = 0;
+
++ selremove();
+ for (i = 0; i < 2; i++) {
+- tmoveto(0, 0);
+- tcursor(CURSOR_SAVE);
+- tclearregion(0, 0, term.col-1, term.row-1);
++ tcursor(CURSOR_SAVE); /* reset saved cursor */
++ for (y = 0; y < term.row; y++)
++ for (x = 0; x < term.col; x++)
++ tclearglyph(&term.line[y][x], 0);
+ tswapscreen();
+ }
++ tfulldirt();
+ }
+
+ void
+ tnew(int col, int row)
+ {
+- term = (Term){ .c = { .attr = { .fg = defaultfg, .bg = defaultbg } } };
+- tresize(col, row);
+- treset();
++ int i, j;
++
++ for (i = 0; i < 2; i++) {
++ term.line = xmalloc(row * sizeof(Line));
++ for (j = 0; j < row; j++)
++ term.line[j] = xmalloc(col * sizeof(Glyph));
++ term.col = col, term.row = row;
++ tswapscreen();
++ }
++ term.dirty = xmalloc(row * sizeof(*term.dirty));
++ term.tabs = xmalloc(col * sizeof(*term.tabs));
++ for (i = 0; i < HISTSIZE; i++)
++ term.hist[i] = xmalloc(col * sizeof(Glyph));
++ treset();
+ }
+
++/* handle it with care */
+ void
+ tswapscreen(void)
+ {
+- Line *tmp = term.line;
++ static Line *altline;
++ static int altcol, altrow;
++ Line *tmpline = term.line;
++ int tmpcol = term.col, tmprow = term.row;
+
+- term.line = term.alt;
+- term.alt = tmp;
++ term.line = altline;
++ term.col = altcol, term.row = altrow;
++ altline = tmpline;
++ altcol = tmpcol, altrow = tmprow;
+ term.mode ^= MODE_ALTSCREEN;
+- tfulldirt();
++}
++
++void
++tloaddefscreen(int clear, int loadcursor)
++{
++ int col, row, alt = IS_SET(MODE_ALTSCREEN);
++
++ if (alt) {
++ if (clear)
++ tclearregion(0, 0, term.col-1, term.row-1, 1);
++ col = term.col, row = term.row;
++ tswapscreen();
++ }
++ if (loadcursor)
++ tcursor(CURSOR_LOAD);
++ if (alt)
++ tresizedef(col, row);
++}
++
++void
++tloadaltscreen(int clear, int savecursor)
++{
++ int col, row, def = !IS_SET(MODE_ALTSCREEN);
++
++ if (savecursor)
++ tcursor(CURSOR_SAVE);
++ if (def) {
++ col = term.col, row = term.row;
++ tswapscreen();
++ term.scr = 0;
++ tresizealt(col, row);
++ }
++ if (clear)
++ tclearregion(0, 0, term.col-1, term.row-1, 1);
++}
++
++int
++tisaltscreen(void)
++{
++ return IS_SET(MODE_ALTSCREEN);
+ }
+
+ void
+@@ -1069,17 +1201,22 @@ kscrolldown(const Arg* a)
+ {
+ int n = a->i;
+
+- if (n < 0)
+- n = term.row + n;
++ if (!term.scr || IS_SET(MODE_ALTSCREEN))
++ return;
+
+- if (n > term.scr)
+- n = term.scr;
++ if (n < 0)
++ n = MAX(term.row / -n, 1);
+
+- if (term.scr > 0) {
++ if (n <= term.scr) {
+ term.scr -= n;
+- selscroll(0, -n);
+- tfulldirt();
++ } else {
++ n = term.scr;
++ term.scr = 0;
+ }
++
++ if (sel.ob.x != -1 && !sel.alt)
++ selmove(-n); /* negate change in term.scr */
++ tfulldirt();
+ }
+
+ void
+@@ -1087,92 +1224,118 @@ kscrollup(const Arg* a)
+ {
+ int n = a->i;
+
++ if (!term.histf || IS_SET(MODE_ALTSCREEN))
++ return;
++
+ if (n < 0)
+- n = term.row + n;
++ n = MAX(term.row / -n, 1);
+
+- if (term.scr <= HISTSIZE-n) {
++ if (term.scr + n <= term.histf) {
+ term.scr += n;
+- selscroll(0, n);
+- tfulldirt();
++ } else {
++ n = term.histf - term.scr;
++ term.scr = term.histf;
+ }
++
++ if (sel.ob.x != -1 && !sel.alt)
++ selmove(n); /* negate change in term.scr */
++ tfulldirt();
+ }
+
+ void
+-tscrolldown(int orig, int n, int copyhist)
++tscrolldown(int top, int n)
+ {
+- int i;
++ int i, bot = term.bot;
+ Line temp;
+
+- LIMIT(n, 0, term.bot-orig+1);
+- if (copyhist) {
+- term.histi = (term.histi - 1 + HISTSIZE) % HISTSIZE;
+- temp = term.hist[term.histi];
+- term.hist[term.histi] = term.line[term.bot];
+- term.line[term.bot] = temp;
+- }
+-
++ if (n <= 0)
++ return;
++ n = MIN(n, bot-top+1);
+
+- tsetdirt(orig, term.bot-n);
+- tclearregion(0, term.bot-n+1, term.col-1, term.bot);
++ tsetdirt(top, bot-n);
++ tclearregion(0, bot-n+1, term.col-1, bot, 1);
+
+- for (i = term.bot; i >= orig+n; i--) {
++ for (i = bot; i >= top+n; i--) {
+ temp = term.line[i];
+ term.line[i] = term.line[i-n];
+ term.line[i-n] = temp;
+ }
+
+- if (term.scr == 0)
+- selscroll(orig, n);
++ if (sel.ob.x != -1 && sel.alt == IS_SET(MODE_ALTSCREEN))
++ selscroll(top, bot, n);
+ }
+
+ void
+-tscrollup(int orig, int n, int copyhist)
++tscrollup(int top, int bot, int n, int mode)
+ {
+- int i;
++ int i, j, s;
++ int alt = IS_SET(MODE_ALTSCREEN);
++ int savehist = !alt && top == 0 && mode != SCROLL_NOSAVEHIST;
+ Line temp;
+
+- LIMIT(n, 0, term.bot-orig+1);
+-
+- if (copyhist) {
+- term.histi = (term.histi + 1) % HISTSIZE;
+- temp = term.hist[term.histi];
+- term.hist[term.histi] = term.line[orig];
+- term.line[orig] = temp;
++ if (n <= 0)
++ return;
++ n = MIN(n, bot-top+1);
++
++ if (savehist) {
++ for (i = 0; i < n; i++) {
++ term.histi = (term.histi + 1) % HISTSIZE;
++ temp = term.hist[term.histi];
++ for (j = 0; j < term.col; j++)
++ tclearglyph(&temp[j], 1);
++ term.hist[term.histi] = term.line[i];
++ term.line[i] = temp;
++ }
++ term.histf = MIN(term.histf + n, HISTSIZE);
++ s = n;
++ if (term.scr) {
++ j = term.scr;
++ term.scr = MIN(j + n, HISTSIZE);
++ s = j + n - term.scr;
++ }
++ if (mode != SCROLL_RESIZE)
++ tfulldirt();
++ } else {
++ tclearregion(0, top, term.col-1, top+n-1, 1);
++ tsetdirt(top+n, bot);
+ }
+
+- if (term.scr > 0 && term.scr < HISTSIZE)
+- term.scr = MIN(term.scr + n, HISTSIZE-1);
+-
+- tclearregion(0, orig, term.col-1, orig+n-1);
+- tsetdirt(orig+n, term.bot);
+-
+- for (i = orig; i <= term.bot-n; i++) {
++ for (i = top; i <= bot-n; i++) {
+ temp = term.line[i];
+ term.line[i] = term.line[i+n];
+ term.line[i+n] = temp;
+ }
+
+- if (term.scr == 0)
+- selscroll(orig, -n);
++ if (sel.ob.x != -1 && sel.alt == alt) {
++ if (!savehist) {
++ selscroll(top, bot, -n);
++ } else if (s > 0) {
++ selmove(-s);
++ if (-term.scr + sel.nb.y < -term.histf)
++ selremove();
++ }
++ }
+ }
+
+ void
+-selscroll(int orig, int n)
++selmove(int n)
+ {
+- if (sel.ob.x == -1)
+- return;
++ sel.ob.y += n, sel.nb.y += n;
++ sel.oe.y += n, sel.ne.y += n;
++}
++
++void
++selscroll(int top, int bot, int n)
++{
++ /* turn absolute coordinates into relative */
++ top += term.scr, bot += term.scr;
+
+- if (BETWEEN(sel.nb.y, orig, term.bot) != BETWEEN(sel.ne.y, orig, term.bot)) {
++ if (BETWEEN(sel.nb.y, top, bot) != BETWEEN(sel.ne.y, top, bot)) {
+ selclear();
+- } else if (BETWEEN(sel.nb.y, orig, term.bot)) {
+- sel.ob.y += n;
+- sel.oe.y += n;
+- if (sel.ob.y < term.top || sel.ob.y > term.bot ||
+- sel.oe.y < term.top || sel.oe.y > term.bot) {
++ } else if (BETWEEN(sel.nb.y, top, bot)) {
++ selmove(n);
++ if (sel.nb.y < top || sel.ne.y > bot)
+ selclear();
+- } else {
+- selnormalize();
+- }
+ }
+ }
+
+@@ -1182,7 +1345,7 @@ tnewline(int first_col)
+ int y = term.c.y;
+
+ if (y == term.bot) {
+- tscrollup(term.top, 1, 1);
++ tscrollup(term.top, term.bot, 1, SCROLL_SAVEHIST);
+ } else {
+ y++;
+ }
+@@ -1272,89 +1435,93 @@ tsetchar(Rune u, const Glyph *attr, int x, int y)
+ } else if (term.line[y][x].mode & ATTR_WDUMMY) {
+ term.line[y][x-1].u = ' ';
+ term.line[y][x-1].mode &= ~ATTR_WIDE;
+- }
++ }
+
+ term.dirty[y] = 1;
+ term.line[y][x] = *attr;
+ term.line[y][x].u = u;
++ term.line[y][x].mode |= ATTR_SET;
+ }
+
+ void
+-tclearregion(int x1, int y1, int x2, int y2)
++tclearglyph(Glyph *gp, int usecurattr)
+ {
+- int x, y, temp;
+- Glyph *gp;
++ if (usecurattr) {
++ gp->fg = term.c.attr.fg;
++ gp->bg = term.c.attr.bg;
++ } else {
++ gp->fg = defaultfg;
++ gp->bg = defaultbg;
++ }
++ gp->mode = ATTR_NULL;
++ gp->u = ' ';
++}
+
+- if (x1 > x2)
+- temp = x1, x1 = x2, x2 = temp;
+- if (y1 > y2)
+- temp = y1, y1 = y2, y2 = temp;
++void
++tclearregion(int x1, int y1, int x2, int y2, int usecurattr)
++{
++ int x, y;
+
+- LIMIT(x1, 0, term.col-1);
+- LIMIT(x2, 0, term.col-1);
+- LIMIT(y1, 0, term.row-1);
+- LIMIT(y2, 0, term.row-1);
++ /* regionselected() takes relative coordinates */
++ if (regionselected(x1+term.scr, y1+term.scr, x2+term.scr, y2+term.scr))
++ selremove();
+
+ for (y = y1; y <= y2; y++) {
+ term.dirty[y] = 1;
+- for (x = x1; x <= x2; x++) {
+- gp = &term.line[y][x];
+- if (selected(x, y))
+- selclear();
+- gp->fg = term.c.attr.fg;
+- gp->bg = term.c.attr.bg;
+- gp->mode = 0;
+- gp->u = ' ';
+- }
++ for (x = x1; x <= x2; x++)
++ tclearglyph(&term.line[y][x], usecurattr);
+ }
+ }
+
+ void
+ tdeletechar(int n)
+ {
+- int dst, src, size;
+- Glyph *line;
+-
+- LIMIT(n, 0, term.col - term.c.x);
++ int src, dst, size;
++ Line line;
+
++ if (n <= 0)
++ return;
+ dst = term.c.x;
+- src = term.c.x + n;
++ src = MIN(term.c.x + n, term.col);
+ size = term.col - src;
+- line = term.line[term.c.y];
+-
+- memmove(&line[dst], &line[src], size * sizeof(Glyph));
+- tclearregion(term.col-n, term.c.y, term.col-1, term.c.y);
++ if (size > 0) { /* otherwise src would point beyond the array
++ https://stackoverflow.com/questions/29844298 */
++ line = term.line[term.c.y];
++ memmove(&line[dst], &line[src], size * sizeof(Glyph));
++ }
++ tclearregion(dst + size, term.c.y, term.col - 1, term.c.y, 1);
+ }
+
+ void
+ tinsertblank(int n)
+ {
+- int dst, src, size;
+- Glyph *line;
++ int src, dst, size;
++ Line line;
+
+- LIMIT(n, 0, term.col - term.c.x);
+-
+- dst = term.c.x + n;
++ if (n <= 0)
++ return;
++ dst = MIN(term.c.x + n, term.col);
+ src = term.c.x;
+ size = term.col - dst;
+- line = term.line[term.c.y];
+-
+- memmove(&line[dst], &line[src], size * sizeof(Glyph));
+- tclearregion(src, term.c.y, dst - 1, term.c.y);
++ if (size > 0) { /* otherwise dst would point beyond the array */
++ line = term.line[term.c.y];
++ memmove(&line[dst], &line[src], size * sizeof(Glyph));
++ }
++ tclearregion(src, term.c.y, dst - 1, term.c.y, 1);
+ }
+
+ void
+ tinsertblankline(int n)
+ {
+ if (BETWEEN(term.c.y, term.top, term.bot))
+- tscrolldown(term.c.y, n, 0);
++ tscrolldown(term.c.y, n);
+ }
+
+ void
+ tdeleteline(int n)
+ {
+ if (BETWEEN(term.c.y, term.top, term.bot))
+- tscrollup(term.c.y, n, 0);
++ tscrollup(term.c.y, term.bot, n, SCROLL_NOSAVEHIST);
+ }
+
+ int32_t
+@@ -1528,7 +1695,7 @@ tsetscroll(int t, int b)
+ void
+ tsetmode(int priv, int set, const int *args, int narg)
+ {
+- int alt; const int *lim;
++ const int *lim;
+
+ for (lim = args + narg; args < lim; ++args) {
+ if (priv) {
+@@ -1589,25 +1756,18 @@ tsetmode(int priv, int set, const int *args, int narg)
+ xsetmode(set, MODE_8BIT);
+ break;
+ case 1049: /* swap screen & set/restore cursor as xterm */
+- if (!allowaltscreen)
+- break;
+- tcursor((set) ? CURSOR_SAVE : CURSOR_LOAD);
+- /* FALLTHROUGH */
+ case 47: /* swap screen */
+- case 1047:
++ case 1047: /* swap screen, clearing alternate screen */
+ if (!allowaltscreen)
+ break;
+- alt = IS_SET(MODE_ALTSCREEN);
+- if (alt) {
+- tclearregion(0, 0, term.col-1,
+- term.row-1);
+- }
+- if (set ^ alt) /* set is always 1 or 0 */
+- tswapscreen();
+- if (*args != 1049)
+- break;
+- /* FALLTHROUGH */
++ if (set)
++ tloadaltscreen(*args == 1049, *args == 1049);
++ else
++ tloaddefscreen(*args == 1047, *args == 1049);
++ break;
+ case 1048:
++ if (!allowaltscreen)
++ break;
+ tcursor((set) ? CURSOR_SAVE : CURSOR_LOAD);
+ break;
+ case 2004: /* 2004: bracketed paste mode */
+@@ -1659,7 +1819,7 @@ void
+ csihandle(void)
+ {
+ char buf[40];
+- int len;
++ int n, x;
+
+ switch (csiescseq.mode[0]) {
+ default:
+@@ -1757,20 +1917,30 @@ csihandle(void)
+ case 'J': /* ED -- Clear screen */
+ switch (csiescseq.arg[0]) {
+ case 0: /* below */
+- tclearregion(term.c.x, term.c.y, term.col-1, term.c.y);
++ tclearregion(term.c.x, term.c.y, term.col-1, term.c.y, 1);
+ if (term.c.y < term.row-1) {
+- tclearregion(0, term.c.y+1, term.col-1,
+- term.row-1);
++ tclearregion(0, term.c.y+1, term.col-1, term.row-1, 1);
+ }
+ break;
+ case 1: /* above */
+- if (term.c.y > 1)
+- tclearregion(0, 0, term.col-1, term.c.y-1);
+- tclearregion(0, term.c.y, term.c.x, term.c.y);
++ if (term.c.y >= 1)
++ tclearregion(0, 0, term.col-1, term.c.y-1, 1);
++ tclearregion(0, term.c.y, term.c.x, term.c.y, 1);
+ break;
+ case 2: /* all */
+- tclearregion(0, 0, term.col-1, term.row-1);
+- break;
++ if (IS_SET(MODE_ALTSCREEN)) {
++ tclearregion(0, 0, term.col-1, term.row-1, 1);
++ break;
++ }
++ /* vte does this:
++ tscrollup(0, term.row-1, term.row, SCROLL_SAVEHIST); */
++
++ /* alacritty does this: */
++ for (n = term.row-1; n >= 0 && tlinelen(term.line[n]) == 0; n--);
++ if (n >= 0)
++ tscrollup(0, term.row-1, n+1, SCROLL_SAVEHIST);
++ tscrollup(0, term.row-1, term.row-n-1, SCROLL_NOSAVEHIST);
++ break;
+ default:
+ goto unknown;
+ }
+@@ -1778,24 +1948,24 @@ csihandle(void)
+ case 'K': /* EL -- Clear line */
+ switch (csiescseq.arg[0]) {
+ case 0: /* right */
+- tclearregion(term.c.x, term.c.y, term.col-1,
+- term.c.y);
++ tclearregion(term.c.x, term.c.y, term.col-1, term.c.y, 1);
+ break;
+ case 1: /* left */
+- tclearregion(0, term.c.y, term.c.x, term.c.y);
++ tclearregion(0, term.c.y, term.c.x, term.c.y, 1);
+ break;
+ case 2: /* all */
+- tclearregion(0, term.c.y, term.col-1, term.c.y);
++ tclearregion(0, term.c.y, term.col-1, term.c.y, 1);
+ break;
+ }
+ break;
+ case 'S': /* SU -- Scroll <n> line up */
+ DEFAULT(csiescseq.arg[0], 1);
+- tscrollup(term.top, csiescseq.arg[0], 0);
++ /* xterm, urxvt, alacritty save this in history */
++ tscrollup(term.top, term.bot, csiescseq.arg[0], SCROLL_SAVEHIST);
+ break;
+ case 'T': /* SD -- Scroll <n> line down */
+ DEFAULT(csiescseq.arg[0], 1);
+- tscrolldown(term.top, csiescseq.arg[0], 0);
++ tscrolldown(term.top, csiescseq.arg[0]);
+ break;
+ case 'L': /* IL -- Insert <n> blank lines */
+ DEFAULT(csiescseq.arg[0], 1);
+@@ -1809,9 +1979,11 @@ csihandle(void)
+ tdeleteline(csiescseq.arg[0]);
+ break;
+ case 'X': /* ECH -- Erase <n> char */
++ if (csiescseq.arg[0] < 0)
++ return;
+ DEFAULT(csiescseq.arg[0], 1);
+- tclearregion(term.c.x, term.c.y,
+- term.c.x + csiescseq.arg[0] - 1, term.c.y);
++ x = MIN(term.c.x + csiescseq.arg[0], term.col) - 1;
++ tclearregion(term.c.x, term.c.y, x, term.c.y, 1);
+ break;
+ case 'P': /* DCH -- Delete <n> char */
+ DEFAULT(csiescseq.arg[0], 1);
+@@ -1833,9 +2005,9 @@ csihandle(void)
+ break;
+ case 'n': /* DSR – Device Status Report (cursor position) */
+ if (csiescseq.arg[0] == 6) {
+- len = snprintf(buf, sizeof(buf), "\033[%i;%iR",
++ n = snprintf(buf, sizeof(buf), "\033[%i;%iR",
+ term.c.y+1, term.c.x+1);
+- ttywrite(buf, len, 0);
++ ttywrite(buf, n, 0);
+ }
+ break;
+ case 'r': /* DECSTBM -- Set Scrolling Region */
+@@ -2128,16 +2300,8 @@ tdumpsel(void)
+ void
+ tdumpline(int n)
+ {
+- char buf[UTF_SIZ];
+- const Glyph *bp, *end;
+-
+- bp = &term.line[n][0];
+- end = &bp[MIN(tlinelen(n), term.col) - 1];
+- if (bp != end || bp->u != ' ') {
+- for ( ; bp <= end; ++bp)
+- tprinter(buf, utf8encode(bp->u, buf));
+- }
+- tprinter("\n", 1);
++ char str[(term.col + 1) * UTF_SIZ];
++ tprinter(str, tgetline(str, &term.line[n][0]));
+ }
+
+ void
+@@ -2358,7 +2522,7 @@ eschandle(uchar ascii)
+ return 0;
+ case 'D': /* IND -- Linefeed */
+ if (term.c.y == term.bot) {
+- tscrollup(term.top, 1, 1);
++ tscrollup(term.top, term.bot, 1, SCROLL_SAVEHIST);
+ } else {
+ tmoveto(term.c.x, term.c.y+1);
+ }
+@@ -2371,7 +2535,7 @@ eschandle(uchar ascii)
+ break;
+ case 'M': /* RI -- Reverse index */
+ if (term.c.y == term.top) {
+- tscrolldown(term.top, 1, 1);
++ tscrolldown(term.top, 1);
+ } else {
+ tmoveto(term.c.x, term.c.y-1);
+ }
+@@ -2511,7 +2675,8 @@ check_control_code:
+ */
+ return;
+ }
+- if (selected(term.c.x, term.c.y))
++ /* selected() takes relative coordinates */
++ if (selected(term.c.x + term.scr, term.c.y + term.scr))
+ selclear();
+
+ gp = &term.line[term.c.y][term.c.x];
+@@ -2546,6 +2711,7 @@ check_control_code:
+ if (term.c.x+width < term.col) {
+ tmoveto(term.c.x+width, term.c.y);
+ } else {
++ term.wrapcwidth[IS_SET(MODE_ALTSCREEN)] = width;
+ term.c.state |= CURSOR_WRAPNEXT;
+ }
+ }
+@@ -2583,93 +2749,275 @@ twrite(const char *buf, int buflen, int show_ctrl)
+ }
+
+ void
+-tresize(int col, int row)
++treflow(int col, int row)
+ {
+ int i, j;
+- int minrow = MIN(row, term.row);
+- int mincol = MIN(col, term.col);
+- int *bp;
+- TCursor c;
+-
+- if (col < 1 || row < 1) {
+- fprintf(stderr,
+- "tresize: error resizing to %dx%d\n", col, row);
+- return;
++ int oce, nce, bot, scr;
++ int ox = 0, oy = -term.histf, nx = 0, ny = -1, len;
++ int cy = -1; /* proxy for new y coordinate of cursor */
++ int nlines;
++ Line *buf, line;
++
++ /* y coordinate of cursor line end */
++ for (oce = term.c.y; oce < term.row - 1 &&
++ tiswrapped(term.line[oce]); oce++);
++
++ nlines = term.histf + oce + 1;
++ if (col < term.col) {
++ /* each line can take this many lines after reflow */
++ j = (term.col + col - 1) / col;
++ nlines = j * nlines;
++ if (nlines > HISTSIZE + RESIZEBUFFER + row) {
++ nlines = HISTSIZE + RESIZEBUFFER + row;
++ oy = -(nlines / j - oce - 1);
++ }
+ }
++ buf = xmalloc(nlines * sizeof(Line));
++ do {
++ if (!nx)
++ buf[++ny] = xmalloc(col * sizeof(Glyph));
++ if (!ox) {
++ line = TLINEABS(oy);
++ len = tlinelen(line);
++ }
++ if (oy == term.c.y) {
++ if (!ox)
++ len = MAX(len, term.c.x + 1);
++ /* update cursor */
++ if (cy < 0 && term.c.x - ox < col - nx) {
++ term.c.x = nx + term.c.x - ox, cy = ny;
++ UPDATEWRAPNEXT(0, col);
++ }
++ }
++ /* get reflowed lines in buf */
++ if (col - nx > len - ox) {
++ memcpy(&buf[ny][nx], &line[ox], (len-ox) * sizeof(Glyph));
++ nx += len - ox;
++ if (len == 0 || !(line[len - 1].mode & ATTR_WRAP)) {
++ for (j = nx; j < col; j++)
++ tclearglyph(&buf[ny][j], 0);
++ nx = 0;
++ } else if (nx > 0) {
++ buf[ny][nx - 1].mode &= ~ATTR_WRAP;
++ }
++ ox = 0, oy++;
++ } else if (col - nx == len - ox) {
++ memcpy(&buf[ny][nx], &line[ox], (col-nx) * sizeof(Glyph));
++ ox = 0, oy++, nx = 0;
++ } else/* if (col - nx < len - ox) */ {
++ memcpy(&buf[ny][nx], &line[ox], (col-nx) * sizeof(Glyph));
++ ox += col - nx;
++ buf[ny][col - 1].mode |= ATTR_WRAP;
++ nx = 0;
++ }
++ } while (oy <= oce);
++ if (nx)
++ for (j = nx; j < col; j++)
++ tclearglyph(&buf[ny][j], 0);
+
+- /*
+- * 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 extra lines */
++ for (i = row; i < term.row; i++)
+ free(term.line[i]);
+- free(term.alt[i]);
++ /* resize to new height */
++ term.line = xrealloc(term.line, row * sizeof(Line));
++
++ bot = MIN(ny, row - 1);
++ scr = MAX(row - term.row, 0);
++ /* update y coordinate of cursor line end */
++ nce = MIN(oce + scr, bot);
++ /* update cursor y coordinate */
++ term.c.y = nce - (ny - cy);
++ if (term.c.y < 0) {
++ j = nce, nce = MIN(nce + -term.c.y, bot);
++ term.c.y += nce - j;
++ while (term.c.y < 0) {
++ free(buf[ny--]);
++ term.c.y++;
++ }
+ }
+- /* 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 new rows */
++ for (i = row - 1; i > nce; i--) {
++ term.line[i] = xmalloc(col * sizeof(Glyph));
++ for (j = 0; j < col; j++)
++ tclearglyph(&term.line[i][j], 0);
+ }
+- for (i += row; i < term.row; i++) {
++ /* fill visible area */
++ for (/*i = nce */; i >= term.row; i--, ny--)
++ term.line[i] = buf[ny];
++ for (/*i = term.row - 1 */; i >= 0; i--, ny--) {
+ free(term.line[i]);
+- free(term.alt[i]);
++ term.line[i] = buf[ny];
++ }
++ /* fill lines in history buffer and update term.histf */
++ for (/*i = -1 */; ny >= 0 && i >= -HISTSIZE; i--, ny--) {
++ j = (term.histi + i + 1 + HISTSIZE) % HISTSIZE;
++ free(term.hist[j]);
++ term.hist[j] = buf[ny];
+ }
++ term.histf = -i - 1;
++ term.scr = MIN(term.scr, term.histf);
++ /* resize rest of the history lines */
++ for (/*i = -term.histf - 1 */; i >= -HISTSIZE; i--) {
++ j = (term.histi + i + 1 + HISTSIZE) % HISTSIZE;
++ term.hist[j] = xrealloc(term.hist[j], col * sizeof(Glyph));
++ }
++ free(buf);
++}
+
+- /* 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));
++void
++rscrolldown(int n)
++{
++ int i;
++ Line temp;
+
+- for (i = 0; i < HISTSIZE; i++) {
+- term.hist[i] = xrealloc(term.hist[i], col * sizeof(Glyph));
+- for (j = mincol; j < col; j++) {
+- term.hist[i][j] = term.c.attr;
+- term.hist[i][j].u = ' ';
+- }
+- }
++ /* can never be true as of now
++ if (IS_SET(MODE_ALTSCREEN))
++ return; */
+
+- /* 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));
+- }
++ if ((n = MIN(n, term.histf)) <= 0)
++ return;
+
+- /* allocate any new rows */
+- for (/* i = minrow */; i < row; i++) {
+- term.line[i] = xmalloc(col * sizeof(Glyph));
+- term.alt[i] = xmalloc(col * sizeof(Glyph));
++ for (i = term.c.y + n; i >= n; i--) {
++ temp = term.line[i];
++ term.line[i] = term.line[i-n];
++ term.line[i-n] = temp;
+ }
++ for (/*i = n - 1 */; i >= 0; i--) {
++ temp = term.line[i];
++ term.line[i] = term.hist[term.histi];
++ term.hist[term.histi] = temp;
++ term.histi = (term.histi - 1 + HISTSIZE) % HISTSIZE;
++ }
++ term.c.y += n;
++ term.histf -= n;
++ if ((i = term.scr - n) >= 0) {
++ term.scr = i;
++ } else {
++ term.scr = 0;
++ if (sel.ob.x != -1 && !sel.alt)
++ selmove(-i);
++ }
++}
++
++void
++tresize(int col, int row)
++{
++ int *bp;
++
++ /* col and row are always MAX(_, 1)
++ if (col < 1 || row < 1) {
++ fprintf(stderr, "tresize: error resizing to %dx%d\n", col, row);
++ return;
++ } */
++
++ term.dirty = xrealloc(term.dirty, row * sizeof(*term.dirty));
++ term.tabs = xrealloc(term.tabs, col * sizeof(*term.tabs));
+ if (col > term.col) {
+ bp = term.tabs + term.col;
+-
+ memset(bp, 0, sizeof(*term.tabs) * (col - term.col));
+ while (--bp > term.tabs && !*bp)
+ /* nothing */ ;
+ for (bp += tabspaces; bp < term.tabs + col; bp += tabspaces)
+ *bp = 1;
+ }
+- /* update terminal size */
+- term.col = col;
+- term.row = row;
+- /* 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 (IS_SET(MODE_ALTSCREEN))
++ tresizealt(col, row);
++ else
++ tresizedef(col, row);
++}
++
++void
++tresizedef(int col, int row)
++{
++ int i, j;
++
++ /* return if dimensions haven't changed */
++ if (term.col == col && term.row == row) {
++ tfulldirt();
++ return;
++ }
++ if (col != term.col) {
++ if (!sel.alt)
++ selremove();
++ treflow(col, row);
++ } else {
++ /* slide screen up if otherwise cursor would get out of the screen */
++ if (term.c.y >= row) {
++ tscrollup(0, term.row - 1, term.c.y - row + 1, SCROLL_RESIZE);
++ term.c.y = row - 1;
+ }
+- if (0 < col && minrow < row) {
+- tclearregion(0, minrow, col - 1, row - 1);
++ for (i = row; i < term.row; i++)
++ free(term.line[i]);
++
++ /* resize to new height */
++ term.line = xrealloc(term.line, row * sizeof(Line));
++ /* allocate any new rows */
++ for (i = term.row; i < row; i++) {
++ term.line[i] = xmalloc(col * sizeof(Glyph));
++ for (j = 0; j < col; j++)
++ tclearglyph(&term.line[i][j], 0);
+ }
+- tswapscreen();
+- tcursor(CURSOR_LOAD);
++ /* scroll down as much as height has increased */
++ rscrolldown(row - term.row);
++ }
++ /* update terminal size */
++ term.col = col, term.row = row;
++ /* reset scrolling region */
++ term.top = 0, term.bot = row - 1;
++ /* dirty all lines */
++ tfulldirt();
++}
++
++void
++tresizealt(int col, int row)
++{
++ int i, j;
++
++ /* return if dimensions haven't changed */
++ if (term.col == col && term.row == row) {
++ tfulldirt();
++ return;
+ }
+- term.c = c;
++ if (sel.alt)
++ selremove();
++ /* slide screen up if otherwise cursor would get out of the screen */
++ for (i = 0; i <= term.c.y - row; i++)
++ free(term.line[i]);
++ if (i > 0) {
++ /* ensure that both src and dst are not NULL */
++ memmove(term.line, term.line + i, row * sizeof(Line));
++ term.c.y = row - 1;
++ }
++ for (i += row; i < term.row; i++)
++ free(term.line[i]);
++ /* resize to new height */
++ term.line = xrealloc(term.line, row * sizeof(Line));
++ /* resize to new width */
++ for (i = 0; i < MIN(row, term.row); i++) {
++ term.line[i] = xrealloc(term.line[i], col * sizeof(Glyph));
++ for (j = term.col; j < col; j++)
++ tclearglyph(&term.line[i][j], 0);
++ }
++ /* allocate any new rows */
++ for (/*i = MIN(row, term.row) */; i < row; i++) {
++ term.line[i] = xmalloc(col * sizeof(Glyph));
++ for (j = 0; j < col; j++)
++ tclearglyph(&term.line[i][j], 0);
++ }
++ /* update cursor */
++ if (term.c.x >= col) {
++ term.c.state &= ~CURSOR_WRAPNEXT;
++ term.c.x = col - 1;
++ } else {
++ UPDATEWRAPNEXT(1, col);
++ }
++ /* update terminal size */
++ term.col = col, term.row = row;
++ /* reset scrolling region */
++ term.top = 0, term.bot = row - 1;
++ /* dirty all lines */
++ tfulldirt();
+ }
+
+ void
+@@ -2709,9 +3057,8 @@ draw(void)
+ cx--;
+
+ drawregion(0, 0, term.col, term.row);
+- if (term.scr == 0)
+- xdrawcursor(cx, term.c.y, term.line[term.c.y][cx],
+- term.ocx, term.ocy, term.line[term.ocy][term.ocx]);
++ 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();
+diff --git a/st.h b/st.h
+index 818a6f8..514ec08 100644
+--- a/st.h
++++ b/st.h
+@@ -22,17 +22,19 @@
+
+ enum glyph_attribute {
+ ATTR_NULL = 0,
+- ATTR_BOLD = 1 << 0,
+- ATTR_FAINT = 1 << 1,
+- ATTR_ITALIC = 1 << 2,
+- ATTR_UNDERLINE = 1 << 3,
+- ATTR_BLINK = 1 << 4,
+- ATTR_REVERSE = 1 << 5,
+- ATTR_INVISIBLE = 1 << 6,
+- ATTR_STRUCK = 1 << 7,
+- ATTR_WRAP = 1 << 8,
+- ATTR_WIDE = 1 << 9,
+- ATTR_WDUMMY = 1 << 10,
++ ATTR_SET = 1 << 0,
++ ATTR_BOLD = 1 << 1,
++ ATTR_FAINT = 1 << 2,
++ ATTR_ITALIC = 1 << 3,
++ ATTR_UNDERLINE = 1 << 4,
++ ATTR_BLINK = 1 << 5,
++ ATTR_REVERSE = 1 << 6,
++ ATTR_INVISIBLE = 1 << 7,
++ ATTR_STRUCK = 1 << 8,
++ ATTR_WRAP = 1 << 9,
++ ATTR_WIDE = 1 << 10,
++ ATTR_WDUMMY = 1 << 11,
++ ATTR_SELECTED = 1 << 12,
+ ATTR_BOLD_FAINT = ATTR_BOLD | ATTR_FAINT,
+ };
+
+@@ -90,6 +92,7 @@ void toggleprinter(const Arg *);
+
+ int tattrset(int);
+ void tnew(int, int);
++int tisaltscreen(void);
+ void tresize(int, int);
+ void tsetdirtattr(int);
+ void ttyhangup(void);
diff --git a/patches/st-title_parsing_fix-0.8.5.diff b/patches/st-title_parsing_fix-0.8.5.diff
new file mode 100644
index 0000000..a5cee40
--- /dev/null
+++ b/patches/st-title_parsing_fix-0.8.5.diff
@@ -0,0 +1,145 @@
+From 8446609384429bfc4342ede4b52ff42214ad2b77 Mon Sep 17 00:00:00 2001
+From: Rizqi Nur Assyaufi <bandithijo@gmail.com>
+Date: Mon, 18 Jul 2022 00:35:26 +0800
+Subject: [PATCH] Fixes title parsing for st-0.8.5
+
+In st, titles get truncated after the first ';' character, this patch
+fixes that.
+---
+ st.c | 38 ++++++++++++++++++++------------------
+ 1 file changed, 20 insertions(+), 18 deletions(-)
+
+diff --git a/st.c b/st.c
+index 51049ba..a2e595c 100644
+--- a/st.c
++++ b/st.c
+@@ -42,6 +42,8 @@
+ #define ISCONTROLC1(c) (BETWEEN(c, 0x80, 0x9f))
+ #define ISCONTROL(c) (ISCONTROLC0(c) || ISCONTROLC1(c))
+ #define ISDELIM(u) (u && wcschr(worddelimiters, u))
++#define STRESCARGREST(n) ((n) == 0 ? strescseq.buf : strescseq.argp[(n)-1] + 1)
++#define STRESCARGJUST(n) (*(strescseq.argp[n]) = '\0', STRESCARGREST(n))
+
+ enum term_mode {
+ MODE_WRAP = 1 << 0,
+@@ -148,7 +150,7 @@ typedef struct {
+ char *buf; /* allocated raw string */
+ size_t siz; /* allocation size */
+ size_t len; /* raw string length */
+- char *args[STR_ARG_SIZ];
++ char *argp[STR_ARG_SIZ]; /* pointers to the end of nth argument */
+ int narg; /* nb of args */
+ } STREscape;
+
+@@ -1885,29 +1887,30 @@ strhandle(void)
+ int j, narg, par;
+
+ term.esc &= ~(ESC_STR_END|ESC_STR);
+- strparse();
+- par = (narg = strescseq.narg) ? atoi(strescseq.args[0]) : 0;
++ strescseq.buf[strescseq.len] = '\0';
+
+ switch (strescseq.type) {
+ case ']': /* OSC -- Operating System Command */
++ strparse();
++ par = (narg = strescseq.narg) ? atoi(STRESCARGJUST(0)) : 0;
+ switch (par) {
+ case 0:
+ if (narg > 1) {
+- xsettitle(strescseq.args[1]);
+- xseticontitle(strescseq.args[1]);
++ xsettitle(STRESCARGREST(1));
++ xseticontitle(STRESCARGREST(1));
+ }
+ return;
+ case 1:
+ if (narg > 1)
+- xseticontitle(strescseq.args[1]);
++ xseticontitle(STRESCARGREST(1));
+ return;
+ case 2:
+ if (narg > 1)
+- xsettitle(strescseq.args[1]);
++ xsettitle(STRESCARGREST(1));
+ return;
+ case 52:
+ if (narg > 2 && allowwindowops) {
+- dec = base64dec(strescseq.args[2]);
++ dec = base64dec(STRESCARGJUST(2));
+ if (dec) {
+ xsetsel(dec);
+ xclipcopy();
+@@ -1920,7 +1923,7 @@ strhandle(void)
+ if (narg < 2)
+ break;
+
+- p = strescseq.args[1];
++ p = STRESCARGREST(1);
+
+ if (!strcmp(p, "?"))
+ osc_color_response(defaultfg, 10);
+@@ -1933,7 +1936,7 @@ strhandle(void)
+ if (narg < 2)
+ break;
+
+- p = strescseq.args[1];
++ p = STRESCARGREST(1);
+
+ if (!strcmp(p, "?"))
+ osc_color_response(defaultbg, 11);
+@@ -1946,7 +1949,7 @@ strhandle(void)
+ if (narg < 2)
+ break;
+
+- p = strescseq.args[1];
++ p = STRESCARGREST(1);
+
+ if (!strcmp(p, "?"))
+ osc_color_response(defaultcs, 12);
+@@ -1958,10 +1961,10 @@ strhandle(void)
+ case 4: /* color set */
+ if (narg < 3)
+ break;
+- p = strescseq.args[2];
++ p = STRESCARGJUST(2);
+ /* FALLTHROUGH */
+ case 104: /* color reset */
+- j = (narg > 1) ? atoi(strescseq.args[1]) : -1;
++ j = (narg > 1) ? atoi(STRESCARGJUST(1)) : -1;
+
+ if (p && !strcmp(p, "?"))
+ osc4_color_response(j);
+@@ -1981,7 +1984,7 @@ strhandle(void)
+ }
+ break;
+ case 'k': /* old title set compatibility */
+- xsettitle(strescseq.args[0]);
++ xsettitle(strescseq.buf);
+ return;
+ case 'P': /* DCS -- Device Control String */
+ case '_': /* APC -- Application Program Command */
+@@ -2000,18 +2003,17 @@ strparse(void)
+ char *p = strescseq.buf;
+
+ strescseq.narg = 0;
+- strescseq.buf[strescseq.len] = '\0';
+
+ if (*p == '\0')
+ return;
+
+ while (strescseq.narg < STR_ARG_SIZ) {
+- strescseq.args[strescseq.narg++] = p;
+ while ((c = *p) != ';' && c != '\0')
+- ++p;
++ p++;
++ strescseq.argp[strescseq.narg++] = p;
+ if (c == '\0')
+ return;
+- *p++ = '\0';
++ p++;
+ }
+ }
+
+--
+2.37.1
+
diff --git a/patches/st-visualbell2-basic-2020-05-13-045a0fa.diff b/patches/st-visualbell2-basic-2020-05-13-045a0fa.diff
new file mode 100644
index 0000000..a87e243
--- /dev/null
+++ b/patches/st-visualbell2-basic-2020-05-13-045a0fa.diff
@@ -0,0 +1,105 @@
+From 18fc7793b0bb2f9a93d39fe69a72d40122e151eb Mon Sep 17 00:00:00 2001
+From: "Avi Halachmi (:avih)" <avihpit@yahoo.com>
+Date: Mon, 15 Oct 2018 01:06:01 +0300
+Subject: [PATCH] add visual bell with few rendering modes
+
+- Inverse the whole terminal - "standard" visual-bell, a bit jarring.
+- Inverse outer (border) cells - much less jarring, yet plenty visible.
+- Inverse the bottom-right corner only.
+- Inverse cells according to custom logic.
+---
+ config.def.h | 8 ++++++++
+ x.c | 35 +++++++++++++++++++++++++++++++++++
+ 2 files changed, 43 insertions(+)
+
+diff --git a/config.def.h b/config.def.h
+index fdbacfd..fe07204 100644
+--- a/config.def.h
++++ b/config.def.h
+@@ -69,6 +69,14 @@ static unsigned int cursorthickness = 2;
+ */
+ static int bellvolume = 0;
+
++/* visual-bell timeout in ms (0 to disable visual-bell) */
++static int vbelltimeout = 150;
++
++/* choose predefined visual-bell cells to inverse, or define your own logic */
++#define VBCELL x==0 || x==right || y==0 || y==bottom /* border */
++// #define VBCELL 1 /* all cells - whole screen */
++// #define VBCELL y==bottom && x>right-2 /* bottom-right */
++
+ /* default TERM value */
+ char *termname = "st-256color";
+
+diff --git a/x.c b/x.c
+index 1dc44d6..44d5a8d 100644
+--- a/x.c
++++ b/x.c
+@@ -1592,6 +1592,27 @@ xsettitle(char *p)
+ XFree(prop.value);
+ }
+
++
++static int vbellset = 0; /* 1 during visual bell, 0 otherwise */
++static struct timespec lastvbell = {0};
++
++static int
++isvbellcell(int x, int y)
++{
++ int right = win.tw / win.cw - 1, bottom = win.th / win.ch - 1;
++ return VBCELL; /* logic condition defined at config.h */
++}
++
++static void
++vbellbegin() {
++ clock_gettime(CLOCK_MONOTONIC, &lastvbell);
++ if (vbellset)
++ return;
++ vbellset = 1;
++ redraw();
++ XFlush(xw.dpy);
++}
++
+ int
+ xstartdraw(void)
+ {
+@@ -1613,6 +1634,8 @@ xdrawline(Line line, int x1, int y1, int x2)
+ continue;
+ if (selected(x, y1))
+ new.mode ^= ATTR_REVERSE;
++ if (vbellset && isvbellcell(x, y1))
++ new.mode ^= ATTR_REVERSE;
+ if (i > 0 && ATTRCMP(base, new)) {
+ xdrawglyphfontspecs(specs, base, i, ox, y1);
+ specs += i;
+@@ -1714,6 +1737,8 @@ xbell(void)
+ xseturgency(1);
+ if (bellvolume)
+ XkbBell(xw.dpy, xw.win, bellvolume, (Atom)NULL);
++ if (vbelltimeout)
++ vbellbegin();
+ }
+
+ void
+@@ -1959,6 +1984,16 @@ run(void)
+ }
+ }
+
++ if (vbellset) {
++ double remain = vbelltimeout - TIMEDIFF(now, lastvbell);
++ if (remain <= 0) {
++ vbellset = 0;
++ redraw();
++ } else if (timeout < 0 || remain < timeout) {
++ timeout = remain;
++ }
++ }
++
+ draw();
+ XFlush(xw.dpy);
+ drawing = 0;
+
+base-commit: 045a0fab4f80b57f4a982ae6bc5f33fe21d66111
+--
+2.17.1
+