summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--config.def.h6
-rw-r--r--patches/st-appsync-20200618-b27a383.diff259
-rw-r--r--st.c48
-rw-r--r--st.info1
-rw-r--r--x.c22
5 files changed, 331 insertions, 5 deletions
diff --git a/config.def.h b/config.def.h
index bfd6cb9..5acd6d5 100644
--- a/config.def.h
+++ b/config.def.h
@@ -57,6 +57,12 @@ static double minlatency = 8;
static double maxlatency = 33;
/*
+ * Synchronized-Update timeout in ms
+ * https://gitlab.com/gnachman/iterm2/-/wikis/synchronized-updates-spec
+ */
+static uint su_timeout = 200;
+
+/*
* blinking timeout (set to 0 to disable blinking) for the terminal blinking
* attribute.
*/
diff --git a/patches/st-appsync-20200618-b27a383.diff b/patches/st-appsync-20200618-b27a383.diff
new file mode 100644
index 0000000..4736325
--- /dev/null
+++ b/patches/st-appsync-20200618-b27a383.diff
@@ -0,0 +1,259 @@
+From 8c9c920325fa10440a96736ba58ec647a0365e22 Mon Sep 17 00:00:00 2001
+From: "Avi Halachmi (:avih)" <avihpit@yahoo.com>
+Date: Sat, 18 Apr 2020 13:56:11 +0300
+Subject: [PATCH] application-sync: support Synchronized-Updates
+
+See https://gitlab.com/gnachman/iterm2/-/wikis/synchronized-updates-spec
+
+In a nutshell: allow an application to suspend drawing until it has
+completed some output - so that the terminal will not flicker/tear by
+rendering partial content. If the end-of-suspension sequence doesn't
+arrive, the terminal bails out after a timeout (default: 200 ms).
+
+The feature is supported and pioneered by iTerm2. There are probably
+very few other terminals or applications which support this feature
+currently.
+
+One notable application which does support it is tmux (master as of
+2020-04-18) - where cursor flicker is completely avoided when a pane
+has new content. E.g. run in one pane: `while :; do cat x.c; done'
+while the cursor is at another pane.
+
+The terminfo string `Sync' added to `st.info' is also a tmux extension
+which tmux detects automatically when `st.info` is installed.
+
+Notes:
+
+- Draw-suspension begins on BSU sequence (Begin-Synchronized-Update),
+ and ends on ESU sequence (End-Synchronized-Update).
+
+- BSU, ESU are "\033P=1s\033\\", "\033P=2s\033\\" respectively (DCS).
+
+- SU doesn't support nesting - BSU begins or extends, ESU always ends.
+
+- ESU without BSU is ignored.
+
+- BSU after BSU extends (resets the timeout), so an application could
+ send BSU in a loop and keep drawing suspended - exactly like it can
+ not-draw anything in a loop. But as soon as it exits/aborted then
+ drawing is resumed according to the timeout even without ESU.
+
+- This implementation focuses on ESU and doesn't really care about BSU
+ in the sense that it tries hard to draw exactly once ESU arrives (if
+ it's not too soon after the last draw - according to minlatency),
+ and doesn't try to draw the content just up-to BSU. These two sides
+ complement eachother - not-drawing on BSU increases the chance that
+ ESU is not too soon after the last draw. This approach was chosen
+ because the application's main focus is that ESU indicates to the
+ terminal that the content is now ready - and that's when we try to
+ draw.
+---
+ config.def.h | 6 ++++++
+ st.c | 48 ++++++++++++++++++++++++++++++++++++++++++++++--
+ st.info | 1 +
+ x.c | 22 +++++++++++++++++++---
+ 4 files changed, 72 insertions(+), 5 deletions(-)
+
+diff --git a/config.def.h b/config.def.h
+index 6f05dce..80d768e 100644
+--- a/config.def.h
++++ b/config.def.h
+@@ -56,6 +56,12 @@ int allowwindowops = 0;
+ static double minlatency = 8;
+ static double maxlatency = 33;
+
++/*
++ * Synchronized-Update timeout in ms
++ * https://gitlab.com/gnachman/iterm2/-/wikis/synchronized-updates-spec
++ */
++static uint su_timeout = 200;
++
+ /*
+ * blinking timeout (set to 0 to disable blinking) for the terminal blinking
+ * attribute.
+diff --git a/st.c b/st.c
+index 76b7e0d..0582e77 100644
+--- a/st.c
++++ b/st.c
+@@ -231,6 +231,33 @@ static uchar utfmask[UTF_SIZ + 1] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8};
+ static Rune utfmin[UTF_SIZ + 1] = { 0, 0, 0x80, 0x800, 0x10000};
+ static Rune utfmax[UTF_SIZ + 1] = {0x10FFFF, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF};
+
++#include <time.h>
++static int su = 0;
++struct timespec sutv;
++
++static void
++tsync_begin()
++{
++ clock_gettime(CLOCK_MONOTONIC, &sutv);
++ su = 1;
++}
++
++static void
++tsync_end()
++{
++ su = 0;
++}
++
++int
++tinsync(uint timeout)
++{
++ struct timespec now;
++ if (su && !clock_gettime(CLOCK_MONOTONIC, &now)
++ && TIMEDIFF(now, sutv) >= timeout)
++ su = 0;
++ return su;
++}
++
+ ssize_t
+ xwrite(int fd, const char *s, size_t len)
+ {
+@@ -818,6 +845,9 @@ ttynew(char *line, char *cmd, char *out, char **args)
+ return cmdfd;
+ }
+
++static int twrite_aborted = 0;
++int ttyread_pending() { return twrite_aborted; }
++
+ size_t
+ ttyread(void)
+ {
+@@ -826,7 +856,7 @@ ttyread(void)
+ int ret, written;
+
+ /* append read bytes to unprocessed bytes */
+- ret = read(cmdfd, buf+buflen, LEN(buf)-buflen);
++ ret = twrite_aborted ? 1 : read(cmdfd, buf+buflen, LEN(buf)-buflen);
+
+ switch (ret) {
+ case 0:
+@@ -834,7 +864,7 @@ ttyread(void)
+ case -1:
+ die("couldn't read from shell: %s\n", strerror(errno));
+ default:
+- buflen += ret;
++ buflen += twrite_aborted ? 0 : ret;
+ written = twrite(buf, buflen, 0);
+ buflen -= written;
+ /* keep any incomplete UTF-8 byte sequence for the next call */
+@@ -994,6 +1024,7 @@ tsetdirtattr(int attr)
+ void
+ tfulldirt(void)
+ {
++ tsync_end();
+ tsetdirt(0, term.row-1);
+ }
+
+@@ -1895,6 +1926,12 @@ strhandle(void)
+ xsettitle(strescseq.args[0]);
+ return;
+ case 'P': /* DCS -- Device Control String */
++ /* https://gitlab.com/gnachman/iterm2/-/wikis/synchronized-updates-spec */
++ if (strstr(strescseq.buf, "=1s") == strescseq.buf)
++ tsync_begin(); /* BSU */
++ else if (strstr(strescseq.buf, "=2s") == strescseq.buf)
++ tsync_end(); /* ESU */
++ return;
+ case '_': /* APC -- Application Program Command */
+ case '^': /* PM -- Privacy Message */
+ return;
+@@ -2436,6 +2473,9 @@ twrite(const char *buf, int buflen, int show_ctrl)
+ Rune u;
+ int n;
+
++ int su0 = su;
++ twrite_aborted = 0;
++
+ for (n = 0; n < buflen; n += charsize) {
+ if (IS_SET(MODE_UTF8)) {
+ /* process a complete utf8 char */
+@@ -2446,6 +2486,10 @@ twrite(const char *buf, int buflen, int show_ctrl)
+ u = buf[n] & 0xFF;
+ charsize = 1;
+ }
++ if (su0 && !su) {
++ twrite_aborted = 1;
++ break; // ESU - allow rendering before a new BSU
++ }
+ if (show_ctrl && ISCONTROL(u)) {
+ if (u & 0x80) {
+ u &= 0x7f;
+diff --git a/st.info b/st.info
+index 8201ad6..b32b446 100644
+--- a/st.info
++++ b/st.info
+@@ -191,6 +191,7 @@ st-mono| simpleterm monocolor,
+ Ms=\E]52;%p1%s;%p2%s\007,
+ Se=\E[2 q,
+ Ss=\E[%p1%d q,
++ Sync=\EP=%p1%ds\E\\,
+
+ st| simpleterm,
+ use=st-mono,
+diff --git a/x.c b/x.c
+index 210f184..27ff4e2 100644
+--- a/x.c
++++ b/x.c
+@@ -1861,6 +1861,9 @@ resize(XEvent *e)
+ cresize(e->xconfigure.width, e->xconfigure.height);
+ }
+
++int tinsync(uint);
++int ttyread_pending();
++
+ void
+ run(void)
+ {
+@@ -1895,7 +1898,7 @@ run(void)
+ FD_SET(ttyfd, &rfd);
+ FD_SET(xfd, &rfd);
+
+- if (XPending(xw.dpy))
++ if (XPending(xw.dpy) || ttyread_pending())
+ timeout = 0; /* existing events might not set xfd */
+
+ seltv.tv_sec = timeout / 1E3;
+@@ -1909,7 +1912,8 @@ run(void)
+ }
+ clock_gettime(CLOCK_MONOTONIC, &now);
+
+- if (FD_ISSET(ttyfd, &rfd))
++ int ttyin = FD_ISSET(ttyfd, &rfd) || ttyread_pending();
++ if (ttyin)
+ ttyread();
+
+ xev = 0;
+@@ -1933,7 +1937,7 @@ run(void)
+ * maximum latency intervals during `cat huge.txt`, and perfect
+ * sync with periodic updates from animations/key-repeats/etc.
+ */
+- if (FD_ISSET(ttyfd, &rfd) || xev) {
++ if (ttyin || xev) {
+ if (!drawing) {
+ trigger = now;
+ drawing = 1;
+@@ -1944,6 +1948,18 @@ run(void)
+ continue; /* we have time, try to find idle */
+ }
+
++ if (tinsync(su_timeout)) {
++ /*
++ * on synchronized-update draw-suspension: don't reset
++ * drawing so that we draw ASAP once we can (just after
++ * ESU). it won't be too soon because we already can
++ * draw now but we skip. we set timeout > 0 to draw on
++ * SU-timeout even without new content.
++ */
++ timeout = minlatency;
++ continue;
++ }
++
+ /* idle detected or maxlatency exhausted -> draw */
+ timeout = -1;
+ if (blinktimeout && tattrset(ATTR_BLINK)) {
+
+base-commit: b27a383a3acc7decf00e6e889fca265430b5d329
+--
+2.17.1
+
diff --git a/st.c b/st.c
index ebdf360..c05fd49 100644
--- a/st.c
+++ b/st.c
@@ -231,6 +231,33 @@ static const uchar utfmask[UTF_SIZ + 1] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8};
static const Rune utfmin[UTF_SIZ + 1] = { 0, 0, 0x80, 0x800, 0x10000};
static const Rune utfmax[UTF_SIZ + 1] = {0x10FFFF, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF};
+#include <time.h>
+static int su = 0;
+struct timespec sutv;
+
+static void
+tsync_begin()
+{
+ clock_gettime(CLOCK_MONOTONIC, &sutv);
+ su = 1;
+}
+
+static void
+tsync_end()
+{
+ su = 0;
+}
+
+int
+tinsync(uint timeout)
+{
+ struct timespec now;
+ if (su && !clock_gettime(CLOCK_MONOTONIC, &now)
+ && TIMEDIFF(now, sutv) >= timeout)
+ su = 0;
+ return su;
+}
+
ssize_t
xwrite(int fd, const char *s, size_t len)
{
@@ -820,6 +847,9 @@ ttynew(const char *line, char *cmd, const char *out, char **args)
return cmdfd;
}
+static int twrite_aborted = 0;
+int ttyread_pending() { return twrite_aborted; }
+
size_t
ttyread(void)
{
@@ -828,7 +858,7 @@ ttyread(void)
int ret, written;
/* append read bytes to unprocessed bytes */
- ret = read(cmdfd, buf+buflen, LEN(buf)-buflen);
+ ret = twrite_aborted ? 1 : read(cmdfd, buf+buflen, LEN(buf)-buflen);
switch (ret) {
case 0:
@@ -836,7 +866,7 @@ ttyread(void)
case -1:
die("couldn't read from shell: %s\n", strerror(errno));
default:
- buflen += ret;
+ buflen += twrite_aborted ? 0 : ret;
written = twrite(buf, buflen, 0);
buflen -= written;
/* keep any incomplete UTF-8 byte sequence for the next call */
@@ -996,6 +1026,7 @@ tsetdirtattr(int attr)
void
tfulldirt(void)
{
+ tsync_end();
tsetdirt(0, term.row-1);
}
@@ -1905,6 +1936,12 @@ strhandle(void)
xsettitle(strescseq.args[0]);
return;
case 'P': /* DCS -- Device Control String */
+ /* https://gitlab.com/gnachman/iterm2/-/wikis/synchronized-updates-spec */
+ if (strstr(strescseq.buf, "=1s") == strescseq.buf)
+ tsync_begin(); /* BSU */
+ else if (strstr(strescseq.buf, "=2s") == strescseq.buf)
+ tsync_end(); /* ESU */
+ return;
case '_': /* APC -- Application Program Command */
case '^': /* PM -- Privacy Message */
return;
@@ -2446,6 +2483,9 @@ twrite(const char *buf, int buflen, int show_ctrl)
Rune u;
int n;
+ int su0 = su;
+ twrite_aborted = 0;
+
for (n = 0; n < buflen; n += charsize) {
if (IS_SET(MODE_UTF8)) {
/* process a complete utf8 char */
@@ -2456,6 +2496,10 @@ twrite(const char *buf, int buflen, int show_ctrl)
u = buf[n] & 0xFF;
charsize = 1;
}
+ if (su0 && !su) {
+ twrite_aborted = 1;
+ break; // ESU - allow rendering before a new BSU
+ }
if (show_ctrl && ISCONTROL(u)) {
if (u & 0x80) {
u &= 0x7f;
diff --git a/st.info b/st.info
index 8201ad6..b32b446 100644
--- a/st.info
+++ b/st.info
@@ -191,6 +191,7 @@ st-mono| simpleterm monocolor,
Ms=\E]52;%p1%s;%p2%s\007,
Se=\E[2 q,
Ss=\E[%p1%d q,
+ Sync=\EP=%p1%ds\E\\,
st| simpleterm,
use=st-mono,
diff --git a/x.c b/x.c
index 6c8a1d8..e0fa998 100644
--- a/x.c
+++ b/x.c
@@ -1889,6 +1889,9 @@ resize(XEvent *e)
cresize(e->xconfigure.width, e->xconfigure.height);
}
+int tinsync(uint);
+int ttyread_pending();
+
void
run(void)
{
@@ -1923,7 +1926,7 @@ run(void)
FD_SET(ttyfd, &rfd);
FD_SET(xfd, &rfd);
- if (XPending(xw.dpy))
+ if (XPending(xw.dpy) || ttyread_pending())
timeout = 0; /* existing events might not set xfd */
seltv.tv_sec = timeout / 1E3;
@@ -1937,7 +1940,8 @@ run(void)
}
clock_gettime(CLOCK_MONOTONIC, &now);
- if (FD_ISSET(ttyfd, &rfd))
+ int ttyin = FD_ISSET(ttyfd, &rfd) || ttyread_pending();
+ if (ttyin)
ttyread();
xev = 0;
@@ -1961,7 +1965,7 @@ run(void)
* maximum latency intervals during `cat huge.txt`, and perfect
* sync with periodic updates from animations/key-repeats/etc.
*/
- if (FD_ISSET(ttyfd, &rfd) || xev) {
+ if (ttyin || xev) {
if (!drawing) {
trigger = now;
drawing = 1;
@@ -1972,6 +1976,18 @@ run(void)
continue; /* we have time, try to find idle */
}
+ if (tinsync(su_timeout)) {
+ /*
+ * on synchronized-update draw-suspension: don't reset
+ * drawing so that we draw ASAP once we can (just after
+ * ESU). it won't be too soon because we already can
+ * draw now but we skip. we set timeout > 0 to draw on
+ * SU-timeout even without new content.
+ */
+ timeout = minlatency;
+ continue;
+ }
+
/* idle detected or maxlatency exhausted -> draw */
timeout = -1;
if (blinktimeout && tattrset(ATTR_BLINK)) {