st-appsync-20200618-b27a383.diff (7468B)
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 5 6 See https://gitlab.com/gnachman/iterm2/-/wikis/synchronized-updates-spec 7 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). 12 13 The feature is supported and pioneered by iTerm2. There are probably 14 very few other terminals or applications which support this feature 15 currently. 16 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. 21 22 The terminfo string `Sync' added to `st.info' is also a tmux extension 23 which tmux detects automatically when `st.info` is installed. 24 25 Notes: 26 27 - Draw-suspension begins on BSU sequence (Begin-Synchronized-Update), 28 and ends on ESU sequence (End-Synchronized-Update). 29 30 - BSU, ESU are "\033P=1s\033\\", "\033P=2s\033\\" respectively (DCS). 31 32 - SU doesn't support nesting - BSU begins or extends, ESU always ends. 33 34 - ESU without BSU is ignored. 35 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. 40 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 49 draw. 50 --- 51 config.def.h | 6 ++++++ 52 st.c | 48 ++++++++++++++++++++++++++++++++++++++++++++++-- 53 st.info | 1 + 54 x.c | 22 +++++++++++++++++++--- 55 4 files changed, 72 insertions(+), 5 deletions(-) 56 57 diff --git a/config.def.h b/config.def.h 58 index 6f05dce..80d768e 100644 59 --- a/config.def.h 60 +++ b/config.def.h 61 @@ -56,6 +56,12 @@ int allowwindowops = 0; 62 static double minlatency = 8; 63 static double maxlatency = 33; 64 65 +/* 66 + * Synchronized-Update timeout in ms 67 + * https://gitlab.com/gnachman/iterm2/-/wikis/synchronized-updates-spec 68 + */ 69 +static uint su_timeout = 200; 70 + 71 /* 72 * blinking timeout (set to 0 to disable blinking) for the terminal blinking 73 * attribute. 74 diff --git a/st.c b/st.c 75 index 76b7e0d..0582e77 100644 76 --- a/st.c 77 +++ b/st.c 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}; 81 82 +#include <time.h> 83 +static int su = 0; 84 +struct timespec sutv; 85 + 86 +static void 87 +tsync_begin() 88 +{ 89 + clock_gettime(CLOCK_MONOTONIC, &sutv); 90 + su = 1; 91 +} 92 + 93 +static void 94 +tsync_end() 95 +{ 96 + su = 0; 97 +} 98 + 99 +int 100 +tinsync(uint timeout) 101 +{ 102 + struct timespec now; 103 + if (su && !clock_gettime(CLOCK_MONOTONIC, &now) 104 + && TIMEDIFF(now, sutv) >= timeout) 105 + su = 0; 106 + return su; 107 +} 108 + 109 ssize_t 110 xwrite(int fd, const char *s, size_t len) 111 { 112 @@ -818,6 +845,9 @@ ttynew(char *line, char *cmd, char *out, char **args) 113 return cmdfd; 114 } 115 116 +static int twrite_aborted = 0; 117 +int ttyread_pending() { return twrite_aborted; } 118 + 119 size_t 120 ttyread(void) 121 { 122 @@ -826,7 +856,7 @@ ttyread(void) 123 int ret, written; 124 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); 128 129 switch (ret) { 130 case 0: 131 @@ -834,7 +864,7 @@ ttyread(void) 132 case -1: 133 die("couldn't read from shell: %s\n", strerror(errno)); 134 default: 135 - buflen += ret; 136 + buflen += twrite_aborted ? 0 : ret; 137 written = twrite(buf, buflen, 0); 138 buflen -= written; 139 /* keep any incomplete UTF-8 byte sequence for the next call */ 140 @@ -994,6 +1024,7 @@ tsetdirtattr(int attr) 141 void 142 tfulldirt(void) 143 { 144 + tsync_end(); 145 tsetdirt(0, term.row-1); 146 } 147 148 @@ -1895,6 +1926,12 @@ strhandle(void) 149 xsettitle(strescseq.args[0]); 150 return; 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 */ 157 + return; 158 case '_': /* APC -- Application Program Command */ 159 case '^': /* PM -- Privacy Message */ 160 return; 161 @@ -2436,6 +2473,9 @@ twrite(const char *buf, int buflen, int show_ctrl) 162 Rune u; 163 int n; 164 165 + int su0 = su; 166 + twrite_aborted = 0; 167 + 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) 172 u = buf[n] & 0xFF; 173 charsize = 1; 174 } 175 + if (su0 && !su) { 176 + twrite_aborted = 1; 177 + break; // ESU - allow rendering before a new BSU 178 + } 179 if (show_ctrl && ISCONTROL(u)) { 180 if (u & 0x80) { 181 u &= 0x7f; 182 diff --git a/st.info b/st.info 183 index 8201ad6..b32b446 100644 184 --- a/st.info 185 +++ b/st.info 186 @@ -191,6 +191,7 @@ st-mono| simpleterm monocolor, 187 Ms=\E]52;%p1%s;%p2%s\007, 188 Se=\E[2 q, 189 Ss=\E[%p1%d q, 190 + Sync=\EP=%p1%ds\E\\, 191 192 st| simpleterm, 193 use=st-mono, 194 diff --git a/x.c b/x.c 195 index 210f184..27ff4e2 100644 196 --- a/x.c 197 +++ b/x.c 198 @@ -1861,6 +1861,9 @@ resize(XEvent *e) 199 cresize(e->xconfigure.width, e->xconfigure.height); 200 } 201 202 +int tinsync(uint); 203 +int ttyread_pending(); 204 + 205 void 206 run(void) 207 { 208 @@ -1895,7 +1898,7 @@ run(void) 209 FD_SET(ttyfd, &rfd); 210 FD_SET(xfd, &rfd); 211 212 - if (XPending(xw.dpy)) 213 + if (XPending(xw.dpy) || ttyread_pending()) 214 timeout = 0; /* existing events might not set xfd */ 215 216 seltv.tv_sec = timeout / 1E3; 217 @@ -1909,7 +1912,8 @@ run(void) 218 } 219 clock_gettime(CLOCK_MONOTONIC, &now); 220 221 - if (FD_ISSET(ttyfd, &rfd)) 222 + int ttyin = FD_ISSET(ttyfd, &rfd) || ttyread_pending(); 223 + if (ttyin) 224 ttyread(); 225 226 xev = 0; 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. 230 */ 231 - if (FD_ISSET(ttyfd, &rfd) || xev) { 232 + if (ttyin || xev) { 233 if (!drawing) { 234 trigger = now; 235 drawing = 1; 236 @@ -1944,6 +1948,18 @@ run(void) 237 continue; /* we have time, try to find idle */ 238 } 239 240 + if (tinsync(su_timeout)) { 241 + /* 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. 247 + */ 248 + timeout = minlatency; 249 + continue; 250 + } 251 + 252 /* idle detected or maxlatency exhausted -> draw */ 253 timeout = -1; 254 if (blinktimeout && tattrset(ATTR_BLINK)) { 255 256 base-commit: b27a383a3acc7decf00e6e889fca265430b5d329 257 -- 258 2.17.1 259