Skip to content

Commit af963fe

Browse files
committed
Provide Kitty capability for tmux using Unicode Placeholders
In general, this is a way to work around tmux that can't deal with graphics protocols directly. https://sw.kovidgoyal.net/kitty/graphics-protocol/#unicode-placeholders The needed workaround is not pretty, but that's all we got :/ This is a preparation to output properly tmux-escaped Unicode placeholders (not wired up yet). The default behavior without tmux of Kitty stays as-is (no Unicode Placeholders) to be compatible with terminals that just implement the image sub-set of the Kitty protocol. Issues: #95
1 parent 648ad81 commit af963fe

File tree

2 files changed

+168
-25
lines changed

2 files changed

+168
-25
lines changed

src/kitty-canvas.cc

+167-24
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,14 @@
2525
#define SCREEN_CURSOR_UP_FORMAT "\033[%dA" // Move cursor up given lines.
2626
#define SCREEN_CURSOR_RIGHT_FORMAT "\033[%dC" // Move cursor right given cols
2727

28+
#define TMUX_START_PASSTHROUGH "\ePtmux;"
29+
#define TMUX_END_PASSTHROUGH "\e\\"
30+
2831
static constexpr int kBase64EncodedChunkSize = 4096; // Max allowed: 4096.
2932
static constexpr int kByteChunk = kBase64EncodedChunkSize / 4 * 3;
3033

3134
namespace timg {
35+
static char *append_xy_msb(char *buffer, int x, int y, uint8_t msb);
3236

3337
// Create ID unique enough for our purposes.
3438
static uint32_t CreateId() {
@@ -38,23 +42,38 @@ static uint32_t CreateId() {
3842
return kStart + counter;
3943
}
4044

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+
4172
KittyGraphicsCanvas::KittyGraphicsCanvas(BufferedWriteSequencer *ws,
4273
ThreadPool *thread_pool,
4374
const DisplayOptions &opts)
4475
: TerminalCanvas(ws), options_(opts), executor_(thread_pool) {}
4576

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 :)
5877
void KittyGraphicsCanvas::Send(int x, int dy, const Framebuffer &fb_orig,
5978
SeqType seq_type, Duration end_of_frame) {
6079
if (dy < 0) {
@@ -70,7 +89,8 @@ void KittyGraphicsCanvas::Send(int x, int dy, const Framebuffer &fb_orig,
7089
const auto &opts = options_;
7190

7291
// 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.
7494
uint32_t id = 0;
7595
static uint32_t animation_id = 0;
7696
static uint8_t flip_buffer = 0;
@@ -100,7 +120,12 @@ void KittyGraphicsCanvas::Send(int x, int dy, const Framebuffer &fb_orig,
100120
}
101121
}
102122

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]() {
104129
std::unique_ptr<const Framebuffer> auto_delete(fb);
105130
const size_t png_buf_size = png::UpperBound(fb->width(), fb->height());
106131
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,
111136
: png::ColorEncoding::kRGBA_32,
112137
png_buf.get(), png_buf_size);
113138

114-
char *pos = offset;
139+
char *pos = offset; // Appending to the partially populated buffer.
140+
115141
// Need to send an image with an id (i=...) as some terminals interpet
116142
// no id with id=0 and just keep replacing one picture.
117143
// Also need q=2 to prevent getting terminal feedback back we don't
118144
// 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.
121155
const char *png_data = png_buf.get();
122156
while (png_size) {
123157
int chunk_bytes = std::min(png_size, kByteChunk);
124158
pos = timg::EncodeBase64(png_data, chunk_bytes, pos);
125159
png_data += chunk_bytes;
126160
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);
129169
}
130170
}
131-
*pos++ = '\e';
132-
*pos++ = '\\';
133-
134-
*pos++ = '\n'; // Need one final cursor movement.
171+
pos = AppendEscaped(pos, '\\', wrap_tmux);
135172

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+
}
136180
return OutBuffer(buffer, pos - buffer);
137181
};
138182

@@ -143,13 +187,17 @@ void KittyGraphicsCanvas::Send(int x, int dy, const Framebuffer &fb_orig,
143187
char *KittyGraphicsCanvas::RequestBuffer(int width, int height) {
144188
const size_t png_compressed_size = png::UpperBound(width, height);
145189
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+
146193
const size_t content_size =
147194
strlen(SCREEN_CURSOR_UP_FORMAT) + strlen(SCREEN_CURSOR_RIGHT_FORMAT) +
148195
encoded_base64_size //
149196
+ strlen("\e_Ga=T,f=XX,s=9999,v=9999,m=1;\e\\") +
150197
(encoded_base64_size / kBase64EncodedChunkSize) *
151198
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.
153201
return new char[content_size];
154202
}
155203

@@ -158,4 +206,99 @@ int KittyGraphicsCanvas::cell_height_for_pixels(int pixels) const {
158206
return -((-pixels + options_.cell_y_px - 1) / options_.cell_y_px);
159207
}
160208

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"\u10A0F", u8"\u10A38",
287+
u8"\u1D185", u8"\u1D186", u8"\u1D187", u8"\u1D188", u8"\u1D189",
288+
u8"\u1D1AA", u8"\u1D1AB", u8"\u1D1AC", u8"\u1D1AD", u8"\u1D242",
289+
u8"\u1D243", u8"\u1D244",
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+
}
161304
} // namespace timg

src/timg.cc

+1-1
Original file line numberDiff line numberDiff line change
@@ -888,7 +888,7 @@ int main(int argc, char *argv[]) {
888888

889889
ExitCode exit_code = ExitCode::kSuccess;
890890

891-
std::mutex errors_lock; // Collect any errors to display later.
891+
std::mutex errors_lock; // Collect any errors to display later.
892892
std::deque<std::string> errors;
893893

894894
// Async image loading, preparing them in a thread pool

0 commit comments

Comments
 (0)