libghostty-vt: add C API for snapshotting functions

Expose terminal snapshot through the libghostty-vt C API and add
a new C example that runs in CI to verify this stuff works!

## Example

```c
size_t continuation_limit = 1024;
assert(ghostty_terminal_set(
    terminal,
    GHOSTTY_TERMINAL_OPT_CONTINUATION_MAX_BYTES,
    &continuation_limit) == GHOSTTY_SUCCESS);

uint8_t *bytes = NULL;
size_t len = 0;
assert(ghostty_snapshot_encode_alloc(
    terminal, NULL, &bytes, &len) == GHOSTTY_SUCCESS);

GhosttySnapshotDecoder decoder = NULL;
assert(ghostty_snapshot_decoder_new_buf(
    NULL, &decoder, bytes, len) == GHOSTTY_SUCCESS);

GhosttyTerminal restored = NULL;
assert(ghostty_snapshot_decoder_decode(
    decoder, &restored) == GHOSTTY_SUCCESS);

ghostty_snapshot_decoder_free(decoder);
ghostty_free(NULL, bytes, len);
```

Streaming decode:

```c
GhosttyReader reader = {
    .read = read_snapshot,
    .userdata = source,
};
GhosttySnapshotDecoder decoder = NULL;
assert(ghostty_snapshot_decoder_new(
    NULL, &decoder, reader) == GHOSTTY_SUCCESS);

GhosttyTerminal terminal = NULL;
assert(ghostty_snapshot_decoder_ready(
    decoder, &terminal) == GHOSTTY_SUCCESS);

GhosttyResult result;
while ((result = ghostty_snapshot_decoder_next(decoder)) ==
       GHOSTTY_SUCCESS) {
  size_t rows = 0;
  assert(ghostty_snapshot_decoder_get(
      decoder,
      GHOSTTY_SNAPSHOT_DECODER_DATA_PROGRESS_ROWS,
      &rows) == GHOSTTY_SUCCESS);
  render(terminal);
}
assert(result == GHOSTTY_NO_VALUE);
```
This commit is contained in:
Mitchell Hashimoto
2026-08-03 12:38:08 -07:00
parent 7d748097a0
commit d7bb4b8639
19 changed files with 3652 additions and 62 deletions

View File

@@ -288,6 +288,9 @@ comptime {
@export(&c.terminal_mode_set, .{ .name = "ghostty_terminal_mode_set" });
@export(&c.terminal_get, .{ .name = "ghostty_terminal_get" });
@export(&c.terminal_get_multi, .{ .name = "ghostty_terminal_get_multi" });
@export(&c.terminal_continuation_write, .{ .name = "ghostty_terminal_continuation_write" });
@export(&c.terminal_continuation_buf, .{ .name = "ghostty_terminal_continuation_buf" });
@export(&c.terminal_continuation_alloc, .{ .name = "ghostty_terminal_continuation_alloc" });
@export(&c.terminal_select_word, .{ .name = "ghostty_terminal_select_word" });
@export(&c.terminal_select_word_between, .{ .name = "ghostty_terminal_select_word_between" });
@export(&c.terminal_select_line, .{ .name = "ghostty_terminal_select_line" });
@@ -310,6 +313,18 @@ comptime {
@export(&c.terminal_grid_ref, .{ .name = "ghostty_terminal_grid_ref" });
@export(&c.terminal_grid_ref_track, .{ .name = "ghostty_terminal_grid_ref_track" });
@export(&c.terminal_point_from_grid_ref, .{ .name = "ghostty_terminal_point_from_grid_ref" });
@export(&c.snapshot_encode, .{ .name = "ghostty_snapshot_encode" });
@export(&c.snapshot_encode_buf, .{ .name = "ghostty_snapshot_encode_buf" });
@export(&c.snapshot_encode_alloc, .{ .name = "ghostty_snapshot_encode_alloc" });
@export(&c.snapshot_decoder_new, .{ .name = "ghostty_snapshot_decoder_new" });
@export(&c.snapshot_decoder_new_buf, .{ .name = "ghostty_snapshot_decoder_new_buf" });
@export(&c.snapshot_decoder_free, .{ .name = "ghostty_snapshot_decoder_free" });
@export(&c.snapshot_decoder_set, .{ .name = "ghostty_snapshot_decoder_set" });
@export(&c.snapshot_decoder_get, .{ .name = "ghostty_snapshot_decoder_get" });
@export(&c.snapshot_decoder_get_multi, .{ .name = "ghostty_snapshot_decoder_get_multi" });
@export(&c.snapshot_decoder_ready, .{ .name = "ghostty_snapshot_decoder_ready" });
@export(&c.snapshot_decoder_next, .{ .name = "ghostty_snapshot_decoder_next" });
@export(&c.snapshot_decoder_decode, .{ .name = "ghostty_snapshot_decoder_decode" });
@export(&c.kitty_graphics_get, .{ .name = "ghostty_kitty_graphics_get" });
@export(&c.kitty_graphics_image, .{ .name = "ghostty_kitty_graphics_image" });
@export(&c.kitty_graphics_image_get, .{ .name = "ghostty_kitty_graphics_image_get" });