1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
|
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const renderer = @import("../../renderer.zig");
const point = @import("../point.zig");
const Terminal = @import("../Terminal.zig");
const command = @import("graphics_command.zig");
const image = @import("graphics_image.zig");
const Command = command.Command;
const Response = command.Response;
const LoadingImage = image.LoadingImage;
const Image = image.Image;
const ImageStorage = @import("graphics_storage.zig").ImageStorage;
const log = std.log.scoped(.kitty_gfx);
/// Execute a Kitty graphics command against the given terminal. This
/// will never fail, but the response may indicate an error and the
/// terminal state may not be updated to reflect the command. This will
/// never put the terminal in an unrecoverable state, however.
///
/// The allocator must be the same allocator that was used to build
/// the command.
pub fn execute(
alloc: Allocator,
terminal: *Terminal,
cmd: *const Command,
) ?Response {
// If storage is disabled then we disable the full protocol. This means
// we don't even respond to queries so the terminal completely acts as
// if this feature is not supported.
if (!terminal.screen.kitty_images.enabled()) {
log.debug("kitty graphics requested but disabled", .{});
return null;
}
log.debug("executing kitty graphics command: quiet={} control={}", .{
cmd.quiet,
cmd.control,
});
// The quiet settings used to control the response. We have to make this
// a var because in certain special cases (namely chunked transmissions)
// this can change.
var quiet = cmd.quiet;
const resp_: ?Response = switch (cmd.control) {
.query => query(alloc, cmd),
.display => display(alloc, terminal, cmd),
.delete => delete(alloc, terminal, cmd),
.transmit, .transmit_and_display => resp: {
// If we're transmitting, then our `q` setting value is complicated.
// The `q` setting inherits the value from the starting command
// unless `q` is set >= 1 on this command. If it is, then we save
// that as the new `q` setting.
const storage = &terminal.screen.kitty_images;
if (storage.loading) |loading| switch (cmd.quiet) {
// q=0 we use whatever the start command value is
.no => quiet = loading.quiet,
// q>=1 we use the new value, but we should already be set to it
inline .ok, .failures => |tag| {
assert(quiet == tag);
loading.quiet = tag;
},
};
break :resp transmit(alloc, terminal, cmd);
},
.transmit_animation_frame,
.control_animation,
.compose_animation,
=> .{ .message = "ERROR: unimplemented action" },
};
// Handle the quiet settings
if (resp_) |resp| {
if (!resp.ok()) {
log.warn("erroneous kitty graphics response: {s}", .{resp.message});
}
return switch (quiet) {
.no => if (resp.empty()) null else resp,
.ok => if (resp.ok()) null else resp,
.failures => null,
};
}
return null;
}
/// Execute a "query" command.
///
/// This command is used to attempt to load an image and respond with
/// success/error but does not persist any of the command to the terminal
/// state.
fn query(alloc: Allocator, cmd: *const Command) Response {
const t = cmd.control.query;
// Query requires image ID. We can't actually send a response without
// an image ID either but we return an error and this will be logged
// downstream.
if (t.image_id == 0) {
return .{ .message = "EINVAL: image ID required" };
}
// Build a partial response to start
var result: Response = .{
.id = t.image_id,
.image_number = t.image_number,
.placement_id = t.placement_id,
};
// Attempt to load the image. If we cannot, then set an appropriate error.
var loading = LoadingImage.init(alloc, cmd) catch |err| {
encodeError(&result, err);
return result;
};
loading.deinit(alloc);
return result;
}
/// Transmit image data.
///
/// This loads the image, validates it, and puts it into the terminal
/// screen storage. It does not display the image.
fn transmit(
alloc: Allocator,
terminal: *Terminal,
cmd: *const Command,
) Response {
const t = cmd.transmission().?;
var result: Response = .{
.id = t.image_id,
.image_number = t.image_number,
.placement_id = t.placement_id,
};
if (t.image_id > 0 and t.image_number > 0) {
return .{ .message = "EINVAL: image ID and number are mutually exclusive" };
}
const load = loadAndAddImage(alloc, terminal, cmd) catch |err| {
encodeError(&result, err);
return result;
};
errdefer load.image.deinit(alloc);
// If we're also displaying, then do that now. This function does
// both transmit and transmit and display. The display might also be
// deferred if it is multi-chunk.
if (load.display) |d| {
assert(!load.more);
var d_copy = d;
d_copy.image_id = load.image.id;
result = display(alloc, terminal, &.{
.control = .{ .display = d_copy },
.quiet = cmd.quiet,
});
}
// If there are more chunks expected we do not respond.
if (load.more) return .{};
// If the loaded image was assigned its ID automatically, not based
// on a number or explicitly specified ID, then we don't respond.
if (load.image.implicit_id) return .{};
// After the image is added, set the ID in case it changed.
// The resulting image number and placement ID never change.
result.id = load.image.id;
return result;
}
/// Display a previously transmitted image.
fn display(
alloc: Allocator,
terminal: *Terminal,
cmd: *const Command,
) Response {
const d = cmd.display().?;
// Display requires image ID or number.
if (d.image_id == 0 and d.image_number == 0) {
return .{ .message = "EINVAL: image ID or number required" };
}
// Build up our response
var result: Response = .{
.id = d.image_id,
.image_number = d.image_number,
.placement_id = d.placement_id,
};
// Verify the requested image exists if we have an ID
const storage = &terminal.screen.kitty_images;
const img_: ?Image = if (d.image_id != 0)
storage.imageById(d.image_id)
else
storage.imageByNumber(d.image_number);
const img = img_ orelse {
result.message = "ENOENT: image not found";
return result;
};
// Make sure our response has the image id in case we looked up by number
result.id = img.id;
// Location where the placement will go.
const location: ImageStorage.Placement.Location = location: {
// Virtual placements are not tracked
if (d.virtual_placement) {
if (d.parent_id > 0) {
result.message = "EINVAL: virtual placement cannot refer to a parent";
return result;
}
break :location .{ .virtual = {} };
}
// Track a new pin for our cursor. The cursor is always tracked but we
// don't want this one to move with the cursor.
const pin = terminal.screen.pages.trackPin(
terminal.screen.cursor.page_pin.*,
) catch |err| {
log.warn("failed to create pin for Kitty graphics err={}", .{err});
result.message = "EINVAL: failed to prepare terminal state";
return result;
};
break :location .{ .pin = pin };
};
// Add the placement
const p: ImageStorage.Placement = .{
.location = location,
.x_offset = d.x_offset,
.y_offset = d.y_offset,
.source_x = d.x,
.source_y = d.y,
.source_width = d.width,
.source_height = d.height,
.columns = d.columns,
.rows = d.rows,
.z = d.z,
};
storage.addPlacement(
alloc,
img.id,
result.placement_id,
p,
) catch |err| {
p.deinit(&terminal.screen);
encodeError(&result, err);
return result;
};
// Apply cursor movement setting. This only applies to pin placements.
switch (p.location) {
.virtual => {},
.pin => |pin| switch (d.cursor_movement) {
.none => {},
.after => {
// We use terminal.index to properly handle scroll regions.
const size = p.gridSize(img, terminal);
for (0..size.rows) |_| terminal.index() catch |err| {
log.warn("failed to move cursor: {}", .{err});
break;
};
terminal.setCursorPos(
terminal.screen.cursor.y,
pin.x + size.cols + 1,
);
},
},
}
return result;
}
/// Display a previously transmitted image.
fn delete(
alloc: Allocator,
terminal: *Terminal,
cmd: *const Command,
) Response {
const storage = &terminal.screen.kitty_images;
storage.delete(alloc, terminal, cmd.control.delete);
// Delete never responds on success
return .{};
}
fn loadAndAddImage(
alloc: Allocator,
terminal: *Terminal,
cmd: *const Command,
) !struct {
image: Image,
more: bool = false,
display: ?command.Display = null,
} {
const t = cmd.transmission().?;
const storage = &terminal.screen.kitty_images;
// Determine our image. This also handles chunking and early exit.
var loading: LoadingImage = if (storage.loading) |loading| loading: {
// Note: we do NOT want to call "cmd.toOwnedData" here because
// we're _copying_ the data. We want the command data to be freed.
try loading.addData(alloc, cmd.data);
// If we have more then we're done
if (t.more_chunks) return .{ .image = loading.image, .more = true };
// We have no more chunks. We're going to be completing the
// image so we want to destroy the pointer to the loading
// image and copy it out.
defer {
alloc.destroy(loading);
storage.loading = null;
}
break :loading loading.*;
} else try .init(alloc, cmd);
// We only want to deinit on error. If we're chunking, then we don't
// want to deinit at all. If we're not chunking, then we'll deinit
// after we've copied the image out.
errdefer loading.deinit(alloc);
// If the image has no ID, we assign one
if (loading.image.id == 0) {
loading.image.id = storage.next_image_id;
storage.next_image_id +%= 1;
// If the image also has no number then its auto-ID is "implicit".
// See the doc comment on the Image.implicit_id field for more detail.
if (loading.image.number == 0) loading.image.implicit_id = true;
}
// If this is chunked, this is the beginning of a new chunked transmission.
// (We checked for an in-progress chunk above.)
if (t.more_chunks) {
// We allocate the pointer on the heap because its rare and we
// don't want to always pay the memory cost to keep it around.
const loading_ptr = try alloc.create(LoadingImage);
errdefer alloc.destroy(loading_ptr);
loading_ptr.* = loading;
storage.loading = loading_ptr;
return .{ .image = loading.image, .more = true };
}
// Dump the image data before it is decompressed
// loading.debugDump() catch unreachable;
// Validate and store our image
var img = try loading.complete(alloc);
errdefer img.deinit(alloc);
try storage.addImage(alloc, img);
// Get our display settings
const display_ = loading.display;
// Ensure we deinit the loading state because we're done. The image
// won't be deinit because of "complete" above.
loading.deinit(alloc);
return .{ .image = img, .display = display_ };
}
const EncodeableError = Image.Error || Allocator.Error;
/// Encode an error code into a message for a response.
fn encodeError(r: *Response, err: EncodeableError) void {
switch (err) {
error.OutOfMemory => r.message = "ENOMEM: out of memory",
error.InternalError => r.message = "EINVAL: internal error",
error.InvalidData => r.message = "EINVAL: invalid data",
error.DecompressionFailed => r.message = "EINVAL: decompression failed",
error.FilePathTooLong => r.message = "EINVAL: file path too long",
error.TemporaryFileNotInTempDir => r.message = "EINVAL: temporary file not in temp dir",
error.TemporaryFileNotNamedCorrectly => r.message = "EINVAL: temporary file not named correctly",
error.UnsupportedFormat => r.message = "EINVAL: unsupported format",
error.UnsupportedMedium => r.message = "EINVAL: unsupported medium",
error.UnsupportedDepth => r.message = "EINVAL: unsupported pixel depth",
error.DimensionsRequired => r.message = "EINVAL: dimensions required",
error.DimensionsTooLarge => r.message = "EINVAL: dimensions too large",
}
}
test "kittygfx more chunks with q=1" {
const testing = std.testing;
const alloc = testing.allocator;
var t = try Terminal.init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
// Initial chunk has q=1
{
const cmd = try command.Parser.parseString(
alloc,
"a=T,f=24,t=d,i=1,s=1,v=2,c=10,r=1,m=1,q=1;////",
);
defer cmd.deinit(alloc);
const resp = execute(alloc, &t, &cmd);
try testing.expect(resp == null);
}
// Subsequent chunk has no q but should respect initial
{
const cmd = try command.Parser.parseString(
alloc,
"m=0;////",
);
defer cmd.deinit(alloc);
const resp = execute(alloc, &t, &cmd);
try testing.expect(resp == null);
}
}
test "kittygfx more chunks with q=0" {
const testing = std.testing;
const alloc = testing.allocator;
var t = try Terminal.init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
// Initial chunk has q=0
{
const cmd = try command.Parser.parseString(
alloc,
"a=t,f=24,t=d,s=1,v=2,c=10,r=1,m=1,i=1,q=0;////",
);
defer cmd.deinit(alloc);
const resp = execute(alloc, &t, &cmd);
try testing.expect(resp == null);
}
// Subsequent chunk has no q so should respond OK
{
const cmd = try command.Parser.parseString(
alloc,
"m=0;////",
);
defer cmd.deinit(alloc);
const resp = execute(alloc, &t, &cmd).?;
try testing.expect(resp.ok());
}
}
test "kittygfx more chunks with chunk increasing q" {
const testing = std.testing;
const alloc = testing.allocator;
var t = try Terminal.init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
// Initial chunk has q=0
{
const cmd = try command.Parser.parseString(
alloc,
"a=t,f=24,t=d,s=1,v=2,c=10,r=1,m=1,i=1,q=0;////",
);
defer cmd.deinit(alloc);
const resp = execute(alloc, &t, &cmd);
try testing.expect(resp == null);
}
// Subsequent chunk sets q=1 so should not respond
{
const cmd = try command.Parser.parseString(
alloc,
"m=0,q=1;////",
);
defer cmd.deinit(alloc);
const resp = execute(alloc, &t, &cmd);
try testing.expect(resp == null);
}
}
test "kittygfx default format is rgba" {
const testing = std.testing;
const alloc = testing.allocator;
var t = try Terminal.init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
const cmd = try command.Parser.parseString(
alloc,
"a=t,t=d,i=1,s=1,v=2,c=10,r=1;///////////",
);
defer cmd.deinit(alloc);
const resp = execute(alloc, &t, &cmd).?;
try testing.expect(resp.ok());
const storage = &t.screen.kitty_images;
const img = storage.imageById(1).?;
try testing.expectEqual(command.Transmission.Format.rgba, img.format);
}
test "kittygfx test valid u32 (expect invalid image ID)" {
const testing = std.testing;
const alloc = testing.allocator;
var t = try Terminal.init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
const cmd = try command.Parser.parseString(
alloc,
"a=p,i=4294967295",
);
defer cmd.deinit(alloc);
const resp = execute(alloc, &t, &cmd).?;
try testing.expect(!resp.ok());
try testing.expectEqual(resp.message, "ENOENT: image not found");
}
test "kittygfx test valid i32 (expect invalid image ID)" {
const testing = std.testing;
const alloc = testing.allocator;
var t = try Terminal.init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
const cmd = try command.Parser.parseString(
alloc,
"a=p,i=1,z=-2147483648",
);
defer cmd.deinit(alloc);
const resp = execute(alloc, &t, &cmd).?;
try testing.expect(!resp.ok());
try testing.expectEqual(resp.message, "ENOENT: image not found");
}
test "kittygfx no response with no image ID or number" {
const testing = std.testing;
const alloc = testing.allocator;
var t = try Terminal.init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
{
const cmd = try command.Parser.parseString(
alloc,
"a=t,f=24,t=d,s=1,v=2,c=10,r=1,i=0,I=0;////////",
);
defer cmd.deinit(alloc);
const resp = execute(alloc, &t, &cmd);
try testing.expect(resp == null);
}
}
test "kittygfx no response with no image ID or number load and display" {
const testing = std.testing;
const alloc = testing.allocator;
var t = try Terminal.init(alloc, .{ .rows = 5, .cols = 5 });
defer t.deinit(alloc);
{
const cmd = try command.Parser.parseString(
alloc,
"a=T,f=24,t=d,s=1,v=2,c=10,r=1,i=0,I=0;////////",
);
defer cmd.deinit(alloc);
const resp = execute(alloc, &t, &cmd);
try testing.expect(resp == null);
}
}
|