25
25
#define SCREEN_CURSOR_UP_FORMAT " \033 [%dA" // Move cursor up given lines.
26
26
#define SCREEN_CURSOR_RIGHT_FORMAT " \033 [%dC" // Move cursor right given cols
27
27
28
+ #define TMUX_START_PASSTHROUGH " \ePtmux;"
29
+ #define TMUX_END_PASSTHROUGH " \e\\ "
30
+
28
31
static constexpr int kBase64EncodedChunkSize = 4096 ; // Max allowed: 4096.
29
32
static constexpr int kByteChunk = kBase64EncodedChunkSize / 4 * 3 ;
30
33
31
34
namespace timg {
35
+ static char *append_xy_msb (char *buffer, int x, int y, uint8_t msb);
32
36
33
37
// Create ID unique enough for our purposes.
34
38
static uint32_t CreateId () {
@@ -38,23 +42,38 @@ static uint32_t CreateId() {
38
42
return kStart + counter;
39
43
}
40
44
45
+ // Placehholder unicode characters to be used in tmux image output.
46
+ // https://sw.kovidgoyal.net/kitty/graphics-protocol/#unicode-placeholders
47
+ static char *AppendUnicodePicureTiles (char *pos, uint32_t id, int indent,
48
+ int rows, int cols) {
49
+ *pos++ = ' \r ' ;
50
+ for (int r = 0 ; r < rows; ++r) {
51
+ if (indent > 0 ) {
52
+ pos += sprintf (pos, SCREEN_CURSOR_RIGHT_FORMAT, indent);
53
+ }
54
+ pos += sprintf (pos, " \e[38:2:%u:%u:%um" , //
55
+ (id >> 16 ) & 0xff , (id >> 8 ) & 0xff , id & 0xff );
56
+ for (int c = 0 ; c < cols; ++c) {
57
+ pos += sprintf (pos, " \xf4\x8e\xbb\xae " ); // \u10ffff
58
+ pos = append_xy_msb (pos, r, c, (id >> 24 ) & 0xff );
59
+ }
60
+ pos += sprintf (pos, " \e[39m\n\r " );
61
+ }
62
+ return pos;
63
+ }
64
+
65
+ static char *AppendEscaped (char *pos, char c, bool wrap_tmux) {
66
+ *pos++ = ' \e ' ;
67
+ if (wrap_tmux) *pos++ = ' \e ' ; // in tmux: escape the escape
68
+ *pos++ = c;
69
+ return pos;
70
+ };
71
+
41
72
KittyGraphicsCanvas::KittyGraphicsCanvas (BufferedWriteSequencer *ws,
42
73
ThreadPool *thread_pool,
43
74
const DisplayOptions &opts)
44
75
: TerminalCanvas(ws), options_(opts), executor_(thread_pool) {}
45
76
46
- //
47
- // TODO: send image with an ID so that in an animation we can just replace
48
- // the image with the same ID.
49
- // First tests didn't work: sending them with a=t,i=<id>,q=1
50
- // right afterwards attempting to a=p,i=<id>, in the same write()
51
- // does not work at all (timing ? Maybe it first has to store it somewhere
52
- // and only then the ID is available ?)
53
- // Anyway, one of the bugs filed is already fixed upstream.
54
- //
55
- // For now, just a=T direct placement, and not using andy ID. Which
56
- // means, we probably cycle through a lot of memory in the Graphics adapter
57
- // when showing videos :)
58
77
void KittyGraphicsCanvas::Send (int x, int dy, const Framebuffer &fb_orig,
59
78
SeqType seq_type, Duration end_of_frame) {
60
79
if (dy < 0 ) {
@@ -70,7 +89,8 @@ void KittyGraphicsCanvas::Send(int x, int dy, const Framebuffer &fb_orig,
70
89
const auto &opts = options_;
71
90
72
91
// Creating a new ID. Some terminals store the images in a GPU texture
73
- // buffer and index by the ID, so we need to be economical with IDs.
92
+ // buffer (looking at you, wezterm) and index by the ID, so we need to be
93
+ // economical with IDs.
74
94
uint32_t id = 0 ;
75
95
static uint32_t animation_id = 0 ;
76
96
static uint8_t flip_buffer = 0 ;
@@ -100,7 +120,12 @@ void KittyGraphicsCanvas::Send(int x, int dy, const Framebuffer &fb_orig,
100
120
}
101
121
}
102
122
103
- std::function<OutBuffer ()> encode_fun = [opts, fb, id, buffer, offset]() {
123
+ const int cols = fb->width () / opts.cell_x_px ;
124
+ const int rows = -cell_height_for_pixels (-fb->height ());
125
+ const int indent = x / opts.cell_x_px ;
126
+ bool wrap_tmux = false ;
127
+ std::function<OutBuffer ()> encode_fun = [opts, fb, id, buffer, offset, rows,
128
+ cols, indent, wrap_tmux]() {
104
129
std::unique_ptr<const Framebuffer> auto_delete (fb);
105
130
const size_t png_buf_size = png::UpperBound (fb->width (), fb->height ());
106
131
std::unique_ptr<char []> png_buf (new char [png_buf_size]);
@@ -111,28 +136,47 @@ void KittyGraphicsCanvas::Send(int x, int dy, const Framebuffer &fb_orig,
111
136
: png::ColorEncoding::kRGBA_32 ,
112
137
png_buf.get (), png_buf_size);
113
138
114
- char *pos = offset;
139
+ char *pos = offset; // Appending to the partially populated buffer.
140
+
115
141
// Need to send an image with an id (i=...) as some terminals interpet
116
142
// no id with id=0 and just keep replacing one picture.
117
143
// Also need q=2 to prevent getting terminal feedback back we don't
118
144
// read.
119
- pos += sprintf (pos, " \e_Ga=T,i=%u,q=2,f=100,m=%d;" , id,
120
- png_size > kByteChunk );
145
+ if (wrap_tmux) pos += sprintf (pos, TMUX_START_PASSTHROUGH);
146
+ pos = AppendEscaped (pos, ' _' , wrap_tmux);
147
+ pos +=
148
+ sprintf (pos, " Ga=T,i=%u,q=2,f=100,m=%d" , id, png_size > kByteChunk );
149
+ if (wrap_tmux) {
150
+ pos += sprintf (pos, " ,U=1,c=%d,r=%d" , cols, rows);
151
+ }
152
+ *pos++ = ' ;' ; // End of Kitty command
153
+
154
+ // Write out binary data base64-encoded in chunks of limited size.
121
155
const char *png_data = png_buf.get ();
122
156
while (png_size) {
123
157
int chunk_bytes = std::min (png_size, kByteChunk );
124
158
pos = timg::EncodeBase64 (png_data, chunk_bytes, pos);
125
159
png_data += chunk_bytes;
126
160
png_size -= chunk_bytes;
127
- if (png_size) {
128
- pos += sprintf (pos, " \e\\ \e_Gm=%d;" , png_size > kByteChunk );
161
+ if (png_size) { // More to come. Finish chunk and start next.
162
+ pos = AppendEscaped (pos, ' \\ ' , wrap_tmux); // finish
163
+ if (wrap_tmux) {
164
+ pos += sprintf (pos,
165
+ TMUX_END_PASSTHROUGH TMUX_START_PASSTHROUGH);
166
+ }
167
+ pos = AppendEscaped (pos, ' _' , wrap_tmux);
168
+ pos += sprintf (pos, " Gq=2,m=%d;" , png_size > kByteChunk );
129
169
}
130
170
}
131
- *pos++ = ' \e ' ;
132
- *pos++ = ' \\ ' ;
133
-
134
- *pos++ = ' \n ' ; // Need one final cursor movement.
171
+ pos = AppendEscaped (pos, ' \\ ' , wrap_tmux);
135
172
173
+ if (wrap_tmux) {
174
+ pos += sprintf (pos, TMUX_END_PASSTHROUGH);
175
+ pos = AppendUnicodePicureTiles (pos, id, indent, rows, cols);
176
+ }
177
+ else {
178
+ *pos++ = ' \n ' ; // Need one final cursor movement.
179
+ }
136
180
return OutBuffer (buffer, pos - buffer);
137
181
};
138
182
@@ -143,13 +187,17 @@ void KittyGraphicsCanvas::Send(int x, int dy, const Framebuffer &fb_orig,
143
187
char *KittyGraphicsCanvas::RequestBuffer (int width, int height) {
144
188
const size_t png_compressed_size = png::UpperBound (width, height);
145
189
const int encoded_base64_size = png_compressed_size * 4 / 3 ;
190
+ const int cols = width / options_.cell_x_px ;
191
+ const int rows = -cell_height_for_pixels (-height);
192
+
146
193
const size_t content_size =
147
194
strlen (SCREEN_CURSOR_UP_FORMAT) + strlen (SCREEN_CURSOR_RIGHT_FORMAT) +
148
195
encoded_base64_size //
149
196
+ strlen (" \e_Ga=T,f=XX,s=9999,v=9999,m=1;\e\\ " ) +
150
197
(encoded_base64_size / kBase64EncodedChunkSize ) *
151
198
strlen (" \e_Gm=0;\e\\ " ) +
152
- 4 + 1 ; /* digit space for cursor up/right; \n */
199
+ 4 + 1 + // digit space for cursor up/right; \n
200
+ rows * cols * 16 ; // Some space for unicode tiles with diacritics.
153
201
return new char [content_size];
154
202
}
155
203
@@ -158,4 +206,99 @@ int KittyGraphicsCanvas::cell_height_for_pixels(int pixels) const {
158
206
return -((-pixels + options_.cell_y_px - 1 ) / options_.cell_y_px );
159
207
}
160
208
209
+ // Unicode diacritics to convey extra bytes.
210
+ //
211
+ // https://sw.kovidgoyal.net/kitty/graphics-protocol/#unicode-placeholders
212
+ // In general, this is a hack around tmux refusing to work with graphics.
213
+ //
214
+ // The Kitty terminal trick is to send real unicode characters that tmux
215
+ // can deal with, including scrolling etc.
216
+ // But these contain extra information which then refer to previously
217
+ // pass-throughed image information.
218
+ // A \u10ffff unicode is decorated with diacritics to convey x/y position
219
+ // and most significant byte.
220
+ static char *append_value_diacritic (char *buffer, int value) {
221
+ // Diacritics used are provided in
222
+ // https://sw.kovidgoyal.net/kitty/_downloads/1792bad15b12979994cd6ecc54c967a6/rowcolumn-diacritics.txt
223
+ #if 0
224
+ cat rowcolumn-diacritics.txt | awk -F";" '\
225
+ BEGIN { i=0; printf("static const char *const kRowColEncode[] = {"); } \
226
+ /^[0-9A-F]/ { if (i++ % 5 == 0) printf("\n"); printf("u8\"\\u%s\", ", $1); }\
227
+ END { printf("\n}; /* %d */\n", i); }'
228
+ #endif
229
+ static const char *const kRowColEncode [] = {
230
+ u8" \u0305 " , u8" \u030D " , u8" \u030E " , u8" \u0310 " , u8" \u0312 " ,
231
+ u8" \u033D " , u8" \u033E " , u8" \u033F " , u8" \u0346 " , u8" \u034A " ,
232
+ u8" \u034B " , u8" \u034C " , u8" \u0350 " , u8" \u0351 " , u8" \u0352 " ,
233
+ u8" \u0357 " , u8" \u035B " , u8" \u0363 " , u8" \u0364 " , u8" \u0365 " ,
234
+ u8" \u0366 " , u8" \u0367 " , u8" \u0368 " , u8" \u0369 " , u8" \u036A " ,
235
+ u8" \u036B " , u8" \u036C " , u8" \u036D " , u8" \u036E " , u8" \u036F " ,
236
+ u8" \u0483 " , u8" \u0484 " , u8" \u0485 " , u8" \u0486 " , u8" \u0487 " ,
237
+ u8" \u0592 " , u8" \u0593 " , u8" \u0594 " , u8" \u0595 " , u8" \u0597 " ,
238
+ u8" \u0598 " , u8" \u0599 " , u8" \u059C " , u8" \u059D " , u8" \u059E " ,
239
+ u8" \u059F " , u8" \u05A0 " , u8" \u05A1 " , u8" \u05A8 " , u8" \u05A9 " ,
240
+ u8" \u05AB " , u8" \u05AC " , u8" \u05AF " , u8" \u05C4 " , u8" \u0610 " ,
241
+ u8" \u0611 " , u8" \u0612 " , u8" \u0613 " , u8" \u0614 " , u8" \u0615 " ,
242
+ u8" \u0616 " , u8" \u0617 " , u8" \u0657 " , u8" \u0658 " , u8" \u0659 " ,
243
+ u8" \u065A " , u8" \u065B " , u8" \u065D " , u8" \u065E " , u8" \u06D6 " ,
244
+ u8" \u06D7 " , u8" \u06D8 " , u8" \u06D9 " , u8" \u06DA " , u8" \u06DB " ,
245
+ u8" \u06DC " , u8" \u06DF " , u8" \u06E0 " , u8" \u06E1 " , u8" \u06E2 " ,
246
+ u8" \u06E4 " , u8" \u06E7 " , u8" \u06E8 " , u8" \u06EB " , u8" \u06EC " ,
247
+ u8" \u0730 " , u8" \u0732 " , u8" \u0733 " , u8" \u0735 " , u8" \u0736 " ,
248
+ u8" \u073A " , u8" \u073D " , u8" \u073F " , u8" \u0740 " , u8" \u0741 " ,
249
+ u8" \u0743 " , u8" \u0745 " , u8" \u0747 " , u8" \u0749 " , u8" \u074A " ,
250
+ u8" \u07EB " , u8" \u07EC " , u8" \u07ED " , u8" \u07EE " , u8" \u07EF " ,
251
+ u8" \u07F0 " , u8" \u07F1 " , u8" \u07F3 " , u8" \u0816 " , u8" \u0817 " ,
252
+ u8" \u0818 " , u8" \u0819 " , u8" \u081B " , u8" \u081C " , u8" \u081D " ,
253
+ u8" \u081E " , u8" \u081F " , u8" \u0820 " , u8" \u0821 " , u8" \u0822 " ,
254
+ u8" \u0823 " , u8" \u0825 " , u8" \u0826 " , u8" \u0827 " , u8" \u0829 " ,
255
+ u8" \u082A " , u8" \u082B " , u8" \u082C " , u8" \u082D " , u8" \u0951 " ,
256
+ u8" \u0953 " , u8" \u0954 " , u8" \u0F82 " , u8" \u0F83 " , u8" \u0F86 " ,
257
+ u8" \u0F87 " , u8" \u135D " , u8" \u135E " , u8" \u135F " , u8" \u17DD " ,
258
+ u8" \u193A " , u8" \u1A17 " , u8" \u1A75 " , u8" \u1A76 " , u8" \u1A77 " ,
259
+ u8" \u1A78 " , u8" \u1A79 " , u8" \u1A7A " , u8" \u1A7B " , u8" \u1A7C " ,
260
+ u8" \u1B6B " , u8" \u1B6D " , u8" \u1B6E " , u8" \u1B6F " , u8" \u1B70 " ,
261
+ u8" \u1B71 " , u8" \u1B72 " , u8" \u1B73 " , u8" \u1CD0 " , u8" \u1CD1 " ,
262
+ u8" \u1CD2 " , u8" \u1CDA " , u8" \u1CDB " , u8" \u1CE0 " , u8" \u1DC0 " ,
263
+ u8" \u1DC1 " , u8" \u1DC3 " , u8" \u1DC4 " , u8" \u1DC5 " , u8" \u1DC6 " ,
264
+ u8" \u1DC7 " , u8" \u1DC8 " , u8" \u1DC9 " , u8" \u1DCB " , u8" \u1DCC " ,
265
+ u8" \u1DD1 " , u8" \u1DD2 " , u8" \u1DD3 " , u8" \u1DD4 " , u8" \u1DD5 " ,
266
+ u8" \u1DD6 " , u8" \u1DD7 " , u8" \u1DD8 " , u8" \u1DD9 " , u8" \u1DDA " ,
267
+ u8" \u1DDB " , u8" \u1DDC " , u8" \u1DDD " , u8" \u1DDE " , u8" \u1DDF " ,
268
+ u8" \u1DE0 " , u8" \u1DE1 " , u8" \u1DE2 " , u8" \u1DE3 " , u8" \u1DE4 " ,
269
+ u8" \u1DE5 " , u8" \u1DE6 " , u8" \u1DFE " , u8" \u20D0 " , u8" \u20D1 " ,
270
+ u8" \u20D4 " , u8" \u20D5 " , u8" \u20D6 " , u8" \u20D7 " , u8" \u20DB " ,
271
+ u8" \u20DC " , u8" \u20E1 " , u8" \u20E7 " , u8" \u20E9 " , u8" \u20F0 " ,
272
+ u8" \u2CEF " , u8" \u2CF0 " , u8" \u2CF1 " , u8" \u2DE0 " , u8" \u2DE1 " ,
273
+ u8" \u2DE2 " , u8" \u2DE3 " , u8" \u2DE4 " , u8" \u2DE5 " , u8" \u2DE6 " ,
274
+ u8" \u2DE7 " , u8" \u2DE8 " , u8" \u2DE9 " , u8" \u2DEA " , u8" \u2DEB " ,
275
+ u8" \u2DEC " , u8" \u2DED " , u8" \u2DEE " , u8" \u2DEF " , u8" \u2DF0 " ,
276
+ u8" \u2DF1 " , u8" \u2DF2 " , u8" \u2DF3 " , u8" \u2DF4 " , u8" \u2DF5 " ,
277
+ u8" \u2DF6 " , u8" \u2DF7 " , u8" \u2DF8 " , u8" \u2DF9 " , u8" \u2DFA " ,
278
+ u8" \u2DFB " , u8" \u2DFC " , u8" \u2DFD " , u8" \u2DFE " , u8" \u2DFF " ,
279
+ u8" \uA66F " , u8" \uA67C " , u8" \uA67D " , u8" \uA6F0 " , u8" \uA6F1 " ,
280
+ u8" \uA8E0 " , u8" \uA8E1 " , u8" \uA8E2 " , u8" \uA8E3 " , u8" \uA8E4 " ,
281
+ u8" \uA8E5 " , u8" \uA8E6 " , u8" \uA8E7 " , u8" \uA8E8 " , u8" \uA8E9 " ,
282
+ u8" \uA8EA " , u8" \uA8EB " , u8" \uA8EC " , u8" \uA8ED " , u8" \uA8EE " ,
283
+ u8" \uA8EF " , u8" \uA8F0 " , u8" \uA8F1 " , u8" \uAAB0 " , u8" \uAAB2 " ,
284
+ u8" \uAAB3 " , u8" \uAAB7 " , u8" \uAAB8 " , u8" \uAABE " , u8" \uAABF " ,
285
+ u8" \uAAC1 " , u8" \uFE20 " , u8" \uFE21 " , u8" \uFE22 " , u8" \uFE23 " ,
286
+ u8" \uFE24 " , u8" \uFE25 " , u8" \uFE26 " , u8" \u10A0 F" , u8" \u10A3 8" ,
287
+ u8" \u1D18 5" , u8" \u1D18 6" , u8" \u1D18 7" , u8" \u1D18 8" , u8" \u1D18 9" ,
288
+ u8" \u1D1A A" , u8" \u1D1A B" , u8" \u1D1A C" , u8" \u1D1A D" , u8" \u1D24 2" ,
289
+ u8" \u1D24 3" , u8" \u1D24 4" ,
290
+ }; /* 297 */
291
+
292
+ if (value < 0 || value >= 297 ) return buffer;
293
+ const char *src = kRowColEncode [value];
294
+ while ((*buffer++ = *src++)) {
295
+ /* */
296
+ }
297
+ return buffer - 1 ;
298
+ }
299
+ static char *append_xy_msb (char *buffer, int x, int y, uint8_t msb) {
300
+ buffer = append_value_diacritic (append_value_diacritic (buffer, x), y);
301
+ if (msb) buffer = append_value_diacritic (buffer, msb);
302
+ return buffer;
303
+ }
161
304
} // namespace timg
0 commit comments