1 From 8c9c920325fa10440a96736ba58ec647a0365e22 Mon Sep 17 00:00:00 2001
2 From: "Avi Halachmi (:avih)" <avihpit@yahoo.com>
3 Date: Sat, 18 Apr 2020 13:56:11 +0300
4 Subject: [PATCH] application-sync: support Synchronized-Updates
6 See https://gitlab.com/gnachman/iterm2/-/wikis/synchronized-updates-spec
8 In a nutshell: allow an application to suspend drawing until it has
9 completed some output - so that the terminal will not flicker/tear by
10 rendering partial content. If the end-of-suspension sequence doesn't
11 arrive, the terminal bails out after a timeout (default: 200 ms).
13 The feature is supported and pioneered by iTerm2. There are probably
14 very few other terminals or applications which support this feature
17 One notable application which does support it is tmux (master as of
18 2020-04-18) - where cursor flicker is completely avoided when a pane
19 has new content. E.g. run in one pane: `while :; do cat x.c; done'
20 while the cursor is at another pane.
22 The terminfo string `Sync' added to `st.info' is also a tmux extension
23 which tmux detects automatically when `st.info` is installed.
27 - Draw-suspension begins on BSU sequence (Begin-Synchronized-Update),
28 and ends on ESU sequence (End-Synchronized-Update).
30 - BSU, ESU are "\033P=1s\033\\", "\033P=2s\033\\" respectively (DCS).
32 - SU doesn't support nesting - BSU begins or extends, ESU always ends.
34 - ESU without BSU is ignored.
36 - BSU after BSU extends (resets the timeout), so an application could
37 send BSU in a loop and keep drawing suspended - exactly like it can
38 not-draw anything in a loop. But as soon as it exits/aborted then
39 drawing is resumed according to the timeout even without ESU.
41 - This implementation focuses on ESU and doesn't really care about BSU
42 in the sense that it tries hard to draw exactly once ESU arrives (if
43 it's not too soon after the last draw - according to minlatency),
44 and doesn't try to draw the content just up-to BSU. These two sides
45 complement eachother - not-drawing on BSU increases the chance that
46 ESU is not too soon after the last draw. This approach was chosen
47 because the application's main focus is that ESU indicates to the
48 terminal that the content is now ready - and that's when we try to
51 config.def.h | 6 ++++++
52 st.c | 48 ++++++++++++++++++++++++++++++++++++++++++++++--
54 x.c | 22 +++++++++++++++++++---
55 4 files changed, 72 insertions(+), 5 deletions(-)
57 diff --git a/config.def.h b/config.def.h
58 index 6f05dce..80d768e 100644
61 @@ -56,6 +56,12 @@ int allowwindowops = 0;
62 static double minlatency = 8;
63 static double maxlatency = 33;
66 + * Synchronized-Update timeout in ms
67 + * https://gitlab.com/gnachman/iterm2/-/wikis/synchronized-updates-spec
69 +static uint su_timeout = 200;
72 * blinking timeout (set to 0 to disable blinking) for the terminal blinking
74 diff --git a/st.c b/st.c
75 index 76b7e0d..0582e77 100644
78 @@ -231,6 +231,33 @@ static uchar utfmask[UTF_SIZ + 1] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8};
79 static Rune utfmin[UTF_SIZ + 1] = { 0, 0, 0x80, 0x800, 0x10000};
80 static Rune utfmax[UTF_SIZ + 1] = {0x10FFFF, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF};
84 +struct timespec sutv;
89 + clock_gettime(CLOCK_MONOTONIC, &sutv);
100 +tinsync(uint timeout)
102 + struct timespec now;
103 + if (su && !clock_gettime(CLOCK_MONOTONIC, &now)
104 + && TIMEDIFF(now, sutv) >= timeout)
110 xwrite(int fd, const char *s, size_t len)
112 @@ -818,6 +845,9 @@ ttynew(char *line, char *cmd, char *out, char **args)
116 +static int twrite_aborted = 0;
117 +int ttyread_pending() { return twrite_aborted; }
122 @@ -826,7 +856,7 @@ ttyread(void)
125 /* append read bytes to unprocessed bytes */
126 - ret = read(cmdfd, buf+buflen, LEN(buf)-buflen);
127 + ret = twrite_aborted ? 1 : read(cmdfd, buf+buflen, LEN(buf)-buflen);
131 @@ -834,7 +864,7 @@ ttyread(void)
133 die("couldn't read from shell: %s\n", strerror(errno));
136 + buflen += twrite_aborted ? 0 : ret;
137 written = twrite(buf, buflen, 0);
139 /* keep any incomplete UTF-8 byte sequence for the next call */
140 @@ -994,6 +1024,7 @@ tsetdirtattr(int attr)
145 tsetdirt(0, term.row-1);
148 @@ -1895,6 +1926,12 @@ strhandle(void)
149 xsettitle(strescseq.args[0]);
151 case 'P': /* DCS -- Device Control String */
152 + /* https://gitlab.com/gnachman/iterm2/-/wikis/synchronized-updates-spec */
153 + if (strstr(strescseq.buf, "=1s") == strescseq.buf)
154 + tsync_begin(); /* BSU */
155 + else if (strstr(strescseq.buf, "=2s") == strescseq.buf)
156 + tsync_end(); /* ESU */
158 case '_': /* APC -- Application Program Command */
159 case '^': /* PM -- Privacy Message */
161 @@ -2436,6 +2473,9 @@ twrite(const char *buf, int buflen, int show_ctrl)
166 + twrite_aborted = 0;
168 for (n = 0; n < buflen; n += charsize) {
169 if (IS_SET(MODE_UTF8)) {
170 /* process a complete utf8 char */
171 @@ -2446,6 +2486,10 @@ twrite(const char *buf, int buflen, int show_ctrl)
176 + twrite_aborted = 1;
177 + break; // ESU - allow rendering before a new BSU
179 if (show_ctrl && ISCONTROL(u)) {
182 diff --git a/st.info b/st.info
183 index 8201ad6..b32b446 100644
186 @@ -191,6 +191,7 @@ st-mono| simpleterm monocolor,
187 Ms=\E]52;%p1%s;%p2%s\007,
190 + Sync=\EP=%p1%ds\E\\,
194 diff --git a/x.c b/x.c
195 index 210f184..27ff4e2 100644
198 @@ -1861,6 +1861,9 @@ resize(XEvent *e)
199 cresize(e->xconfigure.width, e->xconfigure.height);
203 +int ttyread_pending();
208 @@ -1895,7 +1898,7 @@ run(void)
212 - if (XPending(xw.dpy))
213 + if (XPending(xw.dpy) || ttyread_pending())
214 timeout = 0; /* existing events might not set xfd */
216 seltv.tv_sec = timeout / 1E3;
217 @@ -1909,7 +1912,8 @@ run(void)
219 clock_gettime(CLOCK_MONOTONIC, &now);
221 - if (FD_ISSET(ttyfd, &rfd))
222 + int ttyin = FD_ISSET(ttyfd, &rfd) || ttyread_pending();
227 @@ -1933,7 +1937,7 @@ run(void)
228 * maximum latency intervals during `cat huge.txt`, and perfect
229 * sync with periodic updates from animations/key-repeats/etc.
231 - if (FD_ISSET(ttyfd, &rfd) || xev) {
232 + if (ttyin || xev) {
236 @@ -1944,6 +1948,18 @@ run(void)
237 continue; /* we have time, try to find idle */
240 + if (tinsync(su_timeout)) {
242 + * on synchronized-update draw-suspension: don't reset
243 + * drawing so that we draw ASAP once we can (just after
244 + * ESU). it won't be too soon because we already can
245 + * draw now but we skip. we set timeout > 0 to draw on
246 + * SU-timeout even without new content.
248 + timeout = minlatency;
252 /* idle detected or maxlatency exhausted -> draw */
254 if (blinktimeout && tattrset(ATTR_BLINK)) {
256 base-commit: b27a383a3acc7decf00e6e889fca265430b5d329