libghostty: C APIs for Kitty Graphics inspection (#12145)

This adds a C API for inspecting Kitty graphics image storage, images,
and placements from a terminal instance.

I think this is enough of the API surface area for a renderer to draw
images. But I'll have to add it to Ghostling to be sure.

## Example

```c
#include <stdint.h>
#include <stdio.h>
#include <ghostty/vt.h>

/* After creating a terminal and transmitting a Kitty graphics image... */

/* Get the kitty graphics storage from the terminal. */
GhosttyKittyGraphics graphics = NULL;
ghostty_terminal_get(terminal, GHOSTTY_TERMINAL_DATA_KITTY_GRAPHICS, &graphics);

/* Iterate over all placements. */
GhosttyKittyGraphicsPlacementIterator iter = NULL;
ghostty_kitty_graphics_placement_iterator_new(NULL, &iter);
ghostty_kitty_graphics_get(graphics,
    GHOSTTY_KITTY_GRAPHICS_DATA_PLACEMENT_ITERATOR, &iter);

while (ghostty_kitty_graphics_placement_next(iter)) {
  uint32_t image_id = 0;
  ghostty_kitty_graphics_placement_get(iter,
      GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IMAGE_ID, &image_id);

  /* Look up the image and query its properties. */
  GhosttyKittyGraphicsImage image = ghostty_kitty_graphics_image(graphics, image_id);
  uint32_t width = 0, height = 0;
  GhosttyKittyImageFormat format = 0;
  ghostty_kitty_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_WIDTH, &width);
  ghostty_kitty_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_HEIGHT, &height);
  ghostty_kitty_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_FORMAT, &format);
  printf("image %u: %ux%u format=%d\n", image_id, width, height, format);

  /* Compute rendered pixel size and grid size. */
  uint32_t px_w, px_h, cols, rows;
  ghostty_kitty_graphics_placement_pixel_size(iter, image, terminal, &px_w, &px_h);
  ghostty_kitty_graphics_placement_grid_size(iter, image, terminal, &cols, &rows);
  printf("  rendered: %ux%u px, %ux%u cells\n", px_w, px_h, cols, rows);
}

ghostty_kitty_graphics_placement_iterator_free(iter);
```

## API

### Functions

| Function | Description |
|----------|-------------|
| `ghostty_kitty_graphics_get` | Query data from a kitty graphics
storage (e.g. placement iterator) |
| `ghostty_kitty_graphics_image` | Look up an image by its image ID |
| `ghostty_kitty_graphics_image_get` | Query image properties (ID,
dimensions, format, compression, pixel data) |
| `ghostty_kitty_graphics_placement_iterator_new` | Create a new
placement iterator |
| `ghostty_kitty_graphics_placement_iterator_free` | Free a placement
iterator |
| `ghostty_kitty_graphics_placement_next` | Advance the iterator to the
next placement |
| `ghostty_kitty_graphics_placement_get` | Query placement properties
(image ID, offsets, source rect, z-index, etc.) |
| `ghostty_kitty_graphics_placement_rect` | Compute the bounding grid
rectangle for a placement |
| `ghostty_kitty_graphics_placement_pixel_size` | Compute the rendered
pixel dimensions of a placement |
| `ghostty_kitty_graphics_placement_grid_size` | Compute the grid cell
dimensions of a placement |

### Types

| Type | Description |
|------|-------------|
| `GhosttyKittyGraphics` | Opaque handle to image storage (borrowed from
terminal) |
| `GhosttyKittyGraphicsImage` | Opaque handle to a single image |
| `GhosttyKittyGraphicsPlacementIterator` | Opaque handle to a placement
iterator |
| `GhosttyKittyGraphicsData` | Enum for `ghostty_kitty_graphics_get`
data kinds |
| `GhosttyKittyGraphicsImageData` | Enum for `ghostty_kitty_image_get`
data kinds |
| `GhosttyKittyGraphicsPlacementData` | Enum for
`ghostty_kitty_graphics_placement_get` data kinds |
| `GhosttyKittyImageFormat` | Image pixel format (RGB, RGBA, PNG, gray,
gray+alpha) |
| `GhosttyKittyImageCompression` | Image compression (none, zlib) |
This commit is contained in:
Mitchell Hashimoto
2026-04-06 12:09:39 -07:00
committed by GitHub
18 changed files with 1625 additions and 103 deletions

View File

@@ -82,6 +82,9 @@ int main() {
return 1;
}
/* Set cell pixel dimensions so kitty graphics can compute grid sizes. */
ghostty_terminal_resize(terminal, 80, 24, 8, 16);
/* Set a storage limit to enable Kitty graphics. */
uint64_t storage_limit = 64 * 1024 * 1024; /* 64 MiB */
ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_STORAGE_LIMIT,
@@ -113,6 +116,83 @@ int main() {
printf("PNG decode calls: %d\n", decode_count);
/* Query the kitty graphics storage to verify the image was stored. */
GhosttyKittyGraphics graphics = NULL;
if (ghostty_terminal_get(terminal, GHOSTTY_TERMINAL_DATA_KITTY_GRAPHICS,
&graphics) != GHOSTTY_SUCCESS || !graphics) {
fprintf(stderr, "Failed to get kitty graphics storage\n");
return 1;
}
printf("\nKitty graphics storage is available.\n");
/* Iterate placements to find the image ID. */
GhosttyKittyGraphicsPlacementIterator iter = NULL;
if (ghostty_kitty_graphics_placement_iterator_new(NULL, &iter) != GHOSTTY_SUCCESS) {
fprintf(stderr, "Failed to create placement iterator\n");
return 1;
}
if (ghostty_kitty_graphics_get(graphics,
GHOSTTY_KITTY_GRAPHICS_DATA_PLACEMENT_ITERATOR, &iter) != GHOSTTY_SUCCESS) {
fprintf(stderr, "Failed to get placement iterator\n");
return 1;
}
int placement_count = 0;
while (ghostty_kitty_graphics_placement_next(iter)) {
placement_count++;
uint32_t image_id = 0;
uint32_t placement_id = 0;
bool is_virtual = false;
int32_t z = 0;
ghostty_kitty_graphics_placement_get(iter,
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IMAGE_ID, &image_id);
ghostty_kitty_graphics_placement_get(iter,
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_PLACEMENT_ID, &placement_id);
ghostty_kitty_graphics_placement_get(iter,
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IS_VIRTUAL, &is_virtual);
ghostty_kitty_graphics_placement_get(iter,
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_Z, &z);
printf(" placement #%d: image_id=%u placement_id=%u virtual=%s z=%d\n",
placement_count, image_id, placement_id,
is_virtual ? "true" : "false", z);
/* Look up the image and print its properties. */
GhosttyKittyGraphicsImage image =
ghostty_kitty_graphics_image(graphics, image_id);
if (!image) {
fprintf(stderr, "Failed to look up image %u\n", image_id);
return 1;
}
uint32_t width = 0, height = 0, number = 0;
GhosttyKittyImageFormat format = 0;
size_t data_len = 0;
ghostty_kitty_graphics_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_NUMBER, &number);
ghostty_kitty_graphics_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_WIDTH, &width);
ghostty_kitty_graphics_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_HEIGHT, &height);
ghostty_kitty_graphics_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_FORMAT, &format);
ghostty_kitty_graphics_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_DATA_LEN, &data_len);
printf(" image: number=%u size=%ux%u format=%d data_len=%zu\n",
number, width, height, format, data_len);
/* Compute the rendered pixel size and grid size. */
uint32_t px_w = 0, px_h = 0, cols = 0, rows = 0;
if (ghostty_kitty_graphics_placement_pixel_size(iter, image, terminal,
&px_w, &px_h) == GHOSTTY_SUCCESS) {
printf(" rendered pixel size: %ux%u\n", px_w, px_h);
}
if (ghostty_kitty_graphics_placement_grid_size(iter, image, terminal,
&cols, &rows) == GHOSTTY_SUCCESS) {
printf(" grid size: %u cols x %u rows\n", cols, rows);
}
}
printf("Total placements: %d\n", placement_count);
ghostty_kitty_graphics_placement_iterator_free(iter);
/* Clean up. */
ghostty_terminal_free(terminal);

View File

@@ -125,6 +125,7 @@ extern "C" {
#include <ghostty/vt/style.h>
#include <ghostty/vt/sys.h>
#include <ghostty/vt/key.h>
#include <ghostty/vt/kitty_graphics.h>
#include <ghostty/vt/modes.h>
#include <ghostty/vt/mouse.h>
#include <ghostty/vt/paste.h>

View File

@@ -107,13 +107,6 @@ typedef struct {
GhosttyFormatterScreenExtra screen;
} GhosttyFormatterTerminalExtra;
/**
* Opaque handle to a formatter instance.
*
* @ingroup formatter
*/
typedef struct GhosttyFormatterImpl* GhosttyFormatter;
/**
* Options for creating a terminal formatter.
*

View File

@@ -0,0 +1,424 @@
/**
* @file kitty_graphics.h
*
* Kitty graphics protocol image storage.
*/
#ifndef GHOSTTY_VT_KITTY_GRAPHICS_H
#define GHOSTTY_VT_KITTY_GRAPHICS_H
#include <stdbool.h>
#include <stdint.h>
#include <ghostty/vt/allocator.h>
#include <ghostty/vt/selection.h>
#include <ghostty/vt/types.h>
#ifdef __cplusplus
extern "C" {
#endif
/** @defgroup kitty_graphics Kitty Graphics
*
* Opaque handle to the Kitty graphics image storage associated with a
* terminal screen, and an iterator for inspecting placements.
*
* @{
*/
/**
* Queryable data kinds for ghostty_kitty_graphics_get().
*
* @ingroup kitty_graphics
*/
typedef enum {
/** Invalid / sentinel value. */
GHOSTTY_KITTY_GRAPHICS_DATA_INVALID = 0,
/**
* Populate a pre-allocated placement iterator with placement data from
* the storage. Iterator data is only valid as long as the underlying
* terminal is not mutated.
*
* Output type: GhosttyKittyGraphicsPlacementIterator *
*/
GHOSTTY_KITTY_GRAPHICS_DATA_PLACEMENT_ITERATOR = 1,
} GhosttyKittyGraphicsData;
/**
* Queryable data kinds for ghostty_kitty_graphics_placement_get().
*
* @ingroup kitty_graphics
*/
typedef enum {
/** Invalid / sentinel value. */
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_INVALID = 0,
/**
* The image ID this placement belongs to.
*
* Output type: uint32_t *
*/
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IMAGE_ID = 1,
/**
* The placement ID.
*
* Output type: uint32_t *
*/
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_PLACEMENT_ID = 2,
/**
* Whether this is a virtual placement (unicode placeholder).
*
* Output type: bool *
*/
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IS_VIRTUAL = 3,
/**
* Pixel offset from the left edge of the cell.
*
* Output type: uint32_t *
*/
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_X_OFFSET = 4,
/**
* Pixel offset from the top edge of the cell.
*
* Output type: uint32_t *
*/
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_Y_OFFSET = 5,
/**
* Source rectangle x origin in pixels.
*
* Output type: uint32_t *
*/
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_X = 6,
/**
* Source rectangle y origin in pixels.
*
* Output type: uint32_t *
*/
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_Y = 7,
/**
* Source rectangle width in pixels (0 = full image width).
*
* Output type: uint32_t *
*/
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_WIDTH = 8,
/**
* Source rectangle height in pixels (0 = full image height).
*
* Output type: uint32_t *
*/
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_HEIGHT = 9,
/**
* Number of columns this placement occupies.
*
* Output type: uint32_t *
*/
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_COLUMNS = 10,
/**
* Number of rows this placement occupies.
*
* Output type: uint32_t *
*/
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_ROWS = 11,
/**
* Z-index for this placement.
*
* Output type: int32_t *
*/
GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_Z = 12,
} GhosttyKittyGraphicsPlacementData;
/**
* Pixel format of a Kitty graphics image.
*
* @ingroup kitty_graphics
*/
typedef enum {
GHOSTTY_KITTY_IMAGE_FORMAT_RGB = 0,
GHOSTTY_KITTY_IMAGE_FORMAT_RGBA = 1,
GHOSTTY_KITTY_IMAGE_FORMAT_PNG = 2,
GHOSTTY_KITTY_IMAGE_FORMAT_GRAY_ALPHA = 3,
GHOSTTY_KITTY_IMAGE_FORMAT_GRAY = 4,
} GhosttyKittyImageFormat;
/**
* Compression of a Kitty graphics image.
*
* @ingroup kitty_graphics
*/
typedef enum {
GHOSTTY_KITTY_IMAGE_COMPRESSION_NONE = 0,
GHOSTTY_KITTY_IMAGE_COMPRESSION_ZLIB_DEFLATE = 1,
} GhosttyKittyImageCompression;
/**
* Queryable data kinds for ghostty_kitty_graphics_image_get().
*
* @ingroup kitty_graphics
*/
typedef enum {
/** Invalid / sentinel value. */
GHOSTTY_KITTY_IMAGE_DATA_INVALID = 0,
/**
* The image ID.
*
* Output type: uint32_t *
*/
GHOSTTY_KITTY_IMAGE_DATA_ID = 1,
/**
* The image number.
*
* Output type: uint32_t *
*/
GHOSTTY_KITTY_IMAGE_DATA_NUMBER = 2,
/**
* Image width in pixels.
*
* Output type: uint32_t *
*/
GHOSTTY_KITTY_IMAGE_DATA_WIDTH = 3,
/**
* Image height in pixels.
*
* Output type: uint32_t *
*/
GHOSTTY_KITTY_IMAGE_DATA_HEIGHT = 4,
/**
* Pixel format of the image.
*
* Output type: GhosttyKittyImageFormat *
*/
GHOSTTY_KITTY_IMAGE_DATA_FORMAT = 5,
/**
* Compression of the image.
*
* Output type: GhosttyKittyImageCompression *
*/
GHOSTTY_KITTY_IMAGE_DATA_COMPRESSION = 6,
/**
* Borrowed pointer to the raw pixel data. Valid as long as the
* underlying terminal is not mutated.
*
* Output type: const uint8_t **
*/
GHOSTTY_KITTY_IMAGE_DATA_DATA_PTR = 7,
/**
* Length of the raw pixel data in bytes.
*
* Output type: size_t *
*/
GHOSTTY_KITTY_IMAGE_DATA_DATA_LEN = 8,
} GhosttyKittyGraphicsImageData;
/**
* Get data from a kitty graphics storage instance.
*
* The output pointer must be of the appropriate type for the requested
* data kind.
*
* Returns GHOSTTY_NO_VALUE when Kitty graphics are disabled at build time.
*
* @param graphics The kitty graphics handle
* @param data The type of data to extract
* @param[out] out Pointer to store the extracted data
* @return GHOSTTY_SUCCESS on success
*
* @ingroup kitty_graphics
*/
GHOSTTY_API GhosttyResult ghostty_kitty_graphics_get(
GhosttyKittyGraphics graphics,
GhosttyKittyGraphicsData data,
void* out);
/**
* Look up a Kitty graphics image by its image ID.
*
* Returns NULL if no image with the given ID exists or if Kitty graphics
* are disabled at build time.
*
* @param graphics The kitty graphics handle
* @param image_id The image ID to look up
* @return An opaque image handle, or NULL if not found
*
* @ingroup kitty_graphics
*/
GHOSTTY_API GhosttyKittyGraphicsImage ghostty_kitty_graphics_image(
GhosttyKittyGraphics graphics,
uint32_t image_id);
/**
* Get data from a Kitty graphics image.
*
* The output pointer must be of the appropriate type for the requested
* data kind.
*
* @param image The image handle (NULL returns GHOSTTY_INVALID_VALUE)
* @param data The data kind to query
* @param[out] out Pointer to receive the queried value
* @return GHOSTTY_SUCCESS on success
*
* @ingroup kitty_graphics
*/
GHOSTTY_API GhosttyResult ghostty_kitty_graphics_image_get(
GhosttyKittyGraphicsImage image,
GhosttyKittyGraphicsImageData data,
void* out);
/**
* Create a new placement iterator instance.
*
* All fields except the allocator are left undefined until populated
* via ghostty_kitty_graphics_get() with
* GHOSTTY_KITTY_GRAPHICS_DATA_PLACEMENT_ITERATOR.
*
* @param allocator Pointer to allocator, or NULL to use the default allocator
* @param[out] out_iterator On success, receives the created iterator handle
* @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY on allocation
* failure
*
* @ingroup kitty_graphics
*/
GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_iterator_new(
const GhosttyAllocator* allocator,
GhosttyKittyGraphicsPlacementIterator* out_iterator);
/**
* Free a placement iterator.
*
* @param iterator The iterator handle to free (may be NULL)
*
* @ingroup kitty_graphics
*/
GHOSTTY_API void ghostty_kitty_graphics_placement_iterator_free(
GhosttyKittyGraphicsPlacementIterator iterator);
/**
* Advance the placement iterator to the next placement.
*
* @param iterator The iterator handle (may be NULL)
* @return true if advanced to the next placement, false if at the end
*
* @ingroup kitty_graphics
*/
GHOSTTY_API bool ghostty_kitty_graphics_placement_next(
GhosttyKittyGraphicsPlacementIterator iterator);
/**
* Get data from the current placement in a placement iterator.
*
* Call ghostty_kitty_graphics_placement_next() at least once before
* calling this function.
*
* @param iterator The iterator handle (NULL returns GHOSTTY_INVALID_VALUE)
* @param data The data kind to query
* @param[out] out Pointer to receive the queried value
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the
* iterator is NULL or not positioned on a placement
*
* @ingroup kitty_graphics
*/
GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_get(
GhosttyKittyGraphicsPlacementIterator iterator,
GhosttyKittyGraphicsPlacementData data,
void* out);
/**
* Compute the grid rectangle occupied by the current placement.
*
* Uses the placement's pin, the image dimensions, and the terminal's
* cell/pixel geometry to calculate the bounding rectangle. Virtual
* placements (unicode placeholders) return GHOSTTY_NO_VALUE.
*
* @param terminal The terminal handle
* @param image The image handle for this placement's image
* @param iterator The placement iterator positioned on a placement
* @param[out] out_selection On success, receives the bounding rectangle
* as a selection with rectangle=true
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if any handle
* is NULL or the iterator is not positioned, GHOSTTY_NO_VALUE for
* virtual placements or when Kitty graphics are disabled
*
* @ingroup kitty_graphics
*/
GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_rect(
GhosttyKittyGraphicsPlacementIterator iterator,
GhosttyKittyGraphicsImage image,
GhosttyTerminal terminal,
GhosttySelection* out_selection);
/**
* Compute the rendered pixel size of the current placement.
*
* Takes into account the placement's source rectangle, specified
* columns/rows, and aspect ratio to calculate the final rendered
* pixel dimensions.
*
* @param iterator The placement iterator positioned on a placement
* @param image The image handle for this placement's image
* @param terminal The terminal handle
* @param[out] out_width On success, receives the width in pixels
* @param[out] out_height On success, receives the height in pixels
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if any handle
* is NULL or the iterator is not positioned, GHOSTTY_NO_VALUE when
* Kitty graphics are disabled
*
* @ingroup kitty_graphics
*/
GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_pixel_size(
GhosttyKittyGraphicsPlacementIterator iterator,
GhosttyKittyGraphicsImage image,
GhosttyTerminal terminal,
uint32_t* out_width,
uint32_t* out_height);
/**
* Compute the grid cell size of the current placement.
*
* Returns the number of columns and rows that the placement occupies
* in the terminal grid. If the placement specifies explicit columns
* and rows, those are returned directly; otherwise they are calculated
* from the pixel size and cell dimensions.
*
* @param iterator The placement iterator positioned on a placement
* @param image The image handle for this placement's image
* @param terminal The terminal handle
* @param[out] out_cols On success, receives the number of columns
* @param[out] out_rows On success, receives the number of rows
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if any handle
* is NULL or the iterator is not positioned, GHOSTTY_NO_VALUE when
* Kitty graphics are disabled
*
* @ingroup kitty_graphics
*/
GHOSTTY_API GhosttyResult ghostty_kitty_graphics_placement_grid_size(
GhosttyKittyGraphicsPlacementIterator iterator,
GhosttyKittyGraphicsImage image,
GhosttyTerminal terminal,
uint32_t* out_cols,
uint32_t* out_rows);
/** @} */
#ifdef __cplusplus
}
#endif
#endif /* GHOSTTY_VT_KITTY_GRAPHICS_H */

View File

@@ -13,26 +13,6 @@
#include <ghostty/vt/types.h>
#include <ghostty/vt/allocator.h>
/**
* Opaque handle to an OSC parser instance.
*
* This handle represents an OSC (Operating System Command) parser that can
* be used to parse the contents of OSC sequences.
*
* @ingroup osc
*/
typedef struct GhosttyOscParserImpl *GhosttyOscParser;
/**
* Opaque handle to a single OSC command.
*
* This handle represents a parsed OSC (Operating System Command) command.
* The command can be queried for its type and associated data.
*
* @ingroup osc
*/
typedef struct GhosttyOscCommandImpl *GhosttyOscCommand;
/** @defgroup osc OSC Parser
*
* OSC (Operating System Command) sequence parser and command handling.

View File

@@ -81,27 +81,6 @@ extern "C" {
* @{
*/
/**
* Opaque handle to a render state instance.
*
* @ingroup render
*/
typedef struct GhosttyRenderStateImpl* GhosttyRenderState;
/**
* Opaque handle to a render-state row iterator.
*
* @ingroup render
*/
typedef struct GhosttyRenderStateRowIteratorImpl* GhosttyRenderStateRowIterator;
/**
* Opaque handle to render-state row cells.
*
* @ingroup render
*/
typedef struct GhosttyRenderStateRowCellsImpl* GhosttyRenderStateRowCells;
/**
* Dirty state of a render state after update.
*

View File

@@ -47,16 +47,6 @@
extern "C" {
#endif
/**
* Opaque handle to an SGR parser instance.
*
* This handle represents an SGR (Select Graphic Rendition) parser that can
* be used to parse SGR sequences and extract individual text attributes.
*
* @ingroup sgr
*/
typedef struct GhosttySgrParserImpl* GhosttySgrParser;
/**
* SGR attribute tags.
*

View File

@@ -16,6 +16,7 @@
#include <ghostty/vt/modes.h>
#include <ghostty/vt/size_report.h>
#include <ghostty/vt/grid_ref.h>
#include <ghostty/vt/kitty_graphics.h>
#include <ghostty/vt/screen.h>
#include <ghostty/vt/point.h>
#include <ghostty/vt/style.h>
@@ -154,13 +155,6 @@ extern "C" {
* @{
*/
/**
* Opaque handle to a terminal instance.
*
* @ingroup terminal
*/
typedef struct GhosttyTerminalImpl* GhosttyTerminal;
/**
* Terminal initialization options.
*
@@ -839,6 +833,19 @@ typedef enum {
* Output type: bool *
*/
GHOSTTY_TERMINAL_DATA_KITTY_IMAGE_MEDIUM_SHARED_MEM = 29,
/**
* The Kitty graphics image storage for the active screen.
*
* Returns a borrowed pointer to the image storage. The pointer is valid
* until the next mutating terminal call (e.g. ghostty_terminal_vt_write()
* or ghostty_terminal_reset()).
*
* Returns GHOSTTY_NO_VALUE when Kitty graphics are disabled at build time.
*
* Output type: GhosttyKittyGraphics *
*/
GHOSTTY_TERMINAL_DATA_KITTY_GRAPHICS = 30,
} GhosttyTerminalData;
/**
@@ -1057,6 +1064,39 @@ GHOSTTY_API GhosttyResult ghostty_terminal_grid_ref(GhosttyTerminal terminal,
GhosttyPoint point,
GhosttyGridRef *out_ref);
/**
* Convert a grid reference back to a point in the given coordinate system.
*
* This is the inverse of ghostty_terminal_grid_ref(): given a grid reference,
* it returns the x/y coordinates in the requested coordinate system (active,
* viewport, screen, or history).
*
* The grid reference must have been obtained from the same terminal instance.
* Like all grid references, it is only valid until the next mutating terminal
* call.
*
* Not every grid reference is representable in every coordinate system. For
* example, a cell in scrollback history cannot be expressed in active
* coordinates, and a cell that has scrolled off the visible area cannot be
* expressed in viewport coordinates. In these cases, the function returns
* GHOSTTY_NO_VALUE.
*
* @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
* @param ref Pointer to the grid reference to convert
* @param tag The target coordinate system
* @param[out] out On success, set to the coordinate in the requested system (may be NULL)
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal
* or ref is NULL/invalid, GHOSTTY_NO_VALUE if the ref falls outside
* the requested coordinate system
*
* @ingroup terminal
*/
GHOSTTY_API GhosttyResult ghostty_terminal_point_from_grid_ref(
GhosttyTerminal terminal,
const GhosttyGridRef *ref,
GhosttyPointTag tag,
GhosttyPointCoordinate *out);
/** @} */
#ifdef __cplusplus

View File

@@ -48,6 +48,105 @@ typedef enum {
GHOSTTY_NO_VALUE = -4,
} GhosttyResult;
/* ---- Opaque handles ---- */
/**
* Opaque handle to a terminal instance.
*
* @ingroup terminal
*/
typedef struct GhosttyTerminalImpl* GhosttyTerminal;
/**
* Opaque handle to a Kitty graphics image storage.
*
* Obtained via ghostty_terminal_get() with
* GHOSTTY_TERMINAL_DATA_KITTY_GRAPHICS. The pointer is borrowed from
* the terminal and remains valid until the next mutating terminal call
* (e.g. ghostty_terminal_vt_write() or ghostty_terminal_reset()).
*
* @ingroup kitty_graphics
*/
typedef struct GhosttyKittyGraphicsImpl* GhosttyKittyGraphics;
/**
* Opaque handle to a Kitty graphics image.
*
* Obtained via ghostty_kitty_graphics_image() with an image ID. The
* pointer is borrowed from the storage and remains valid until the next
* mutating terminal call.
*
* @ingroup kitty_graphics
*/
typedef const struct GhosttyKittyGraphicsImageImpl* GhosttyKittyGraphicsImage;
/**
* Opaque handle to a Kitty graphics placement iterator.
*
* @ingroup kitty_graphics
*/
typedef struct GhosttyKittyGraphicsPlacementIteratorImpl* GhosttyKittyGraphicsPlacementIterator;
/**
* Opaque handle to a render state instance.
*
* @ingroup render
*/
typedef struct GhosttyRenderStateImpl* GhosttyRenderState;
/**
* Opaque handle to a render-state row iterator.
*
* @ingroup render
*/
typedef struct GhosttyRenderStateRowIteratorImpl* GhosttyRenderStateRowIterator;
/**
* Opaque handle to render-state row cells.
*
* @ingroup render
*/
typedef struct GhosttyRenderStateRowCellsImpl* GhosttyRenderStateRowCells;
/**
* Opaque handle to an SGR parser instance.
*
* This handle represents an SGR (Select Graphic Rendition) parser that can
* be used to parse SGR sequences and extract individual text attributes.
*
* @ingroup sgr
*/
typedef struct GhosttySgrParserImpl* GhosttySgrParser;
/**
* Opaque handle to a formatter instance.
*
* @ingroup formatter
*/
typedef struct GhosttyFormatterImpl* GhosttyFormatter;
/**
* Opaque handle to an OSC parser instance.
*
* This handle represents an OSC (Operating System Command) parser that can
* be used to parse the contents of OSC sequences.
*
* @ingroup osc
*/
typedef struct GhosttyOscParserImpl* GhosttyOscParser;
/**
* Opaque handle to a single OSC command.
*
* This handle represents a parsed OSC (Operating System Command) command.
* The command can be queried for its type and associated data.
*
* @ingroup osc
*/
typedef struct GhosttyOscCommandImpl* GhosttyOscCommand;
/* ---- Common value types ---- */
/**
* A borrowed byte string (pointer + length).
*

View File

@@ -233,6 +233,17 @@ comptime {
@export(&c.terminal_mode_set, .{ .name = "ghostty_terminal_mode_set" });
@export(&c.terminal_get, .{ .name = "ghostty_terminal_get" });
@export(&c.terminal_grid_ref, .{ .name = "ghostty_terminal_grid_ref" });
@export(&c.terminal_point_from_grid_ref, .{ .name = "ghostty_terminal_point_from_grid_ref" });
@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" });
@export(&c.kitty_graphics_placement_iterator_new, .{ .name = "ghostty_kitty_graphics_placement_iterator_new" });
@export(&c.kitty_graphics_placement_iterator_free, .{ .name = "ghostty_kitty_graphics_placement_iterator_free" });
@export(&c.kitty_graphics_placement_next, .{ .name = "ghostty_kitty_graphics_placement_next" });
@export(&c.kitty_graphics_placement_get, .{ .name = "ghostty_kitty_graphics_placement_get" });
@export(&c.kitty_graphics_placement_rect, .{ .name = "ghostty_kitty_graphics_placement_rect" });
@export(&c.kitty_graphics_placement_pixel_size, .{ .name = "ghostty_kitty_graphics_placement_pixel_size" });
@export(&c.kitty_graphics_placement_grid_size, .{ .name = "ghostty_kitty_graphics_placement_grid_size" });
@export(&c.grid_ref_cell, .{ .name = "ghostty_grid_ref_cell" });
@export(&c.grid_ref_row, .{ .name = "ghostty_grid_ref_row" });
@export(&c.grid_ref_graphemes, .{ .name = "ghostty_grid_ref_graphemes" });

View File

@@ -426,7 +426,7 @@ pub const State = struct {
// Calculate the dimensions of our image, taking in to
// account the rows / columns specified by the placement.
const dest_size = p.calculatedSize(image.*, t);
const dest_size = p.pixelSize(image.*, t);
// Calculate the source rectangle
const source_x = @min(image.width, p.source_x);

View File

@@ -5,7 +5,12 @@
via `lib.TaggedUnion`.
- Any functions must be updated all the way through from here to
`src/terminal/c/main.zig` to `src/lib_vt.zig` and the headers
in `include/ghostty/vt.h`.
in `include/ghostty/vt.h`. Specifically:
1. Define the function in `src/terminal/c/<module>.zig`.
2. Re-export it via a `pub const` in `src/terminal/c/main.zig`.
3. Add an `@export` call in `src/lib_vt.zig` with the
`ghostty_` prefixed symbol name.
4. Declare it in the corresponding header under `include/ghostty/vt/`.
- In `include/ghostty/vt.h`, always sort the header contents by:
(1) macros, (2) forward declarations, (3) types, (4) functions

View File

@@ -0,0 +1,786 @@
const std = @import("std");
const testing = std.testing;
const build_options = @import("terminal_options");
const lib = @import("../lib.zig");
const CAllocator = lib.alloc.Allocator;
const kitty_storage = @import("../kitty/graphics_storage.zig");
const kitty_cmd = @import("../kitty/graphics_command.zig");
const Image = @import("../kitty/graphics_image.zig").Image;
const grid_ref = @import("grid_ref.zig");
const selection_c = @import("selection.zig");
const terminal_c = @import("terminal.zig");
const Result = @import("result.zig").Result;
/// C: GhosttyKittyGraphics
pub const KittyGraphics = if (build_options.kitty_graphics)
*kitty_storage.ImageStorage
else
*anyopaque;
/// C: GhosttyKittyGraphicsImage
pub const ImageHandle = if (build_options.kitty_graphics)
?*const Image
else
?*const anyopaque;
/// C: GhosttyKittyGraphicsPlacementIterator
pub const PlacementIterator = if (build_options.kitty_graphics)
?*PlacementIteratorWrapper
else
?*anyopaque;
const PlacementMap = if (build_options.kitty_graphics)
std.AutoHashMapUnmanaged(
kitty_storage.ImageStorage.PlacementKey,
kitty_storage.ImageStorage.Placement,
)
else
void;
const PlacementIteratorWrapper = if (build_options.kitty_graphics)
struct {
alloc: std.mem.Allocator,
inner: PlacementMap.Iterator = undefined,
entry: ?PlacementMap.Entry = null,
}
else
void;
/// C: GhosttyKittyGraphicsData
pub const Data = enum(c_int) {
invalid = 0,
placement_iterator = 1,
pub fn OutType(comptime self: Data) type {
return switch (self) {
.invalid => void,
.placement_iterator => PlacementIterator,
};
}
};
/// C: GhosttyKittyGraphicsPlacementData
pub const PlacementData = enum(c_int) {
invalid = 0,
image_id = 1,
placement_id = 2,
is_virtual = 3,
x_offset = 4,
y_offset = 5,
source_x = 6,
source_y = 7,
source_width = 8,
source_height = 9,
columns = 10,
rows = 11,
z = 12,
pub fn OutType(comptime self: PlacementData) type {
return switch (self) {
.invalid => void,
.image_id, .placement_id => u32,
.is_virtual => bool,
.x_offset,
.y_offset,
.source_x,
.source_y,
.source_width,
.source_height,
.columns,
.rows,
=> u32,
.z => i32,
};
}
};
pub fn get(
graphics_: KittyGraphics,
data: Data,
out: ?*anyopaque,
) callconv(lib.calling_conv) Result {
if (comptime !build_options.kitty_graphics) return .no_value;
return switch (data) {
.invalid => .invalid_value,
inline else => |comptime_data| getTyped(
graphics_,
comptime_data,
@ptrCast(@alignCast(out)),
),
};
}
fn getTyped(
graphics_: KittyGraphics,
comptime data: Data,
out: *data.OutType(),
) Result {
const storage = graphics_;
switch (data) {
.invalid => return .invalid_value,
.placement_iterator => {
const it = out.* orelse return .invalid_value;
it.* = .{
.alloc = it.alloc,
.inner = storage.placements.iterator(),
};
},
}
return .success;
}
/// C: GhosttyKittyImageFormat
pub const ImageFormat = kitty_cmd.Transmission.Format;
/// C: GhosttyKittyImageCompression
pub const ImageCompression = kitty_cmd.Transmission.Compression;
/// C: GhosttyKittyGraphicsImageData
pub const ImageData = enum(c_int) {
invalid = 0,
id = 1,
number = 2,
width = 3,
height = 4,
format = 5,
compression = 6,
data_ptr = 7,
data_len = 8,
pub fn OutType(comptime self: ImageData) type {
return switch (self) {
.invalid => void,
.id, .number, .width, .height => u32,
.format => ImageFormat,
.compression => ImageCompression,
.data_ptr => [*]const u8,
.data_len => usize,
};
}
};
pub fn image_get_handle(
graphics_: KittyGraphics,
image_id: u32,
) callconv(lib.calling_conv) ImageHandle {
if (comptime !build_options.kitty_graphics) return null;
const storage = graphics_;
return storage.images.getPtr(image_id);
}
pub fn image_get(
image_: ImageHandle,
data: ImageData,
out: ?*anyopaque,
) callconv(lib.calling_conv) Result {
if (comptime !build_options.kitty_graphics) return .no_value;
return switch (data) {
.invalid => .invalid_value,
inline else => |comptime_data| imageGetTyped(
image_,
comptime_data,
@ptrCast(@alignCast(out)),
),
};
}
fn imageGetTyped(
image_: ImageHandle,
comptime data: ImageData,
out: *data.OutType(),
) Result {
const image = image_ orelse return .invalid_value;
switch (data) {
.invalid => return .invalid_value,
.id => out.* = image.id,
.number => out.* = image.number,
.width => out.* = image.width,
.height => out.* = image.height,
.format => out.* = image.format,
.compression => out.* = image.compression,
.data_ptr => out.* = image.data.ptr,
.data_len => out.* = image.data.len,
}
return .success;
}
pub fn placement_iterator_new(
alloc_: ?*const CAllocator,
out: *PlacementIterator,
) callconv(lib.calling_conv) Result {
if (comptime !build_options.kitty_graphics) {
out.* = null;
return .no_value;
}
const alloc = lib.alloc.default(alloc_);
const ptr = alloc.create(PlacementIteratorWrapper) catch {
out.* = null;
return .out_of_memory;
};
ptr.* = .{ .alloc = alloc };
out.* = ptr;
return .success;
}
pub fn placement_iterator_free(iter_: PlacementIterator) callconv(lib.calling_conv) void {
if (comptime !build_options.kitty_graphics) return;
const iter = iter_ orelse return;
iter.alloc.destroy(iter);
}
pub fn placement_iterator_next(iter_: PlacementIterator) callconv(lib.calling_conv) bool {
if (comptime !build_options.kitty_graphics) return false;
const iter = iter_ orelse return false;
iter.entry = iter.inner.next() orelse return false;
return true;
}
pub fn placement_get(
iter_: PlacementIterator,
data: PlacementData,
out: ?*anyopaque,
) callconv(lib.calling_conv) Result {
if (comptime !build_options.kitty_graphics) return .no_value;
return switch (data) {
.invalid => .invalid_value,
inline else => |comptime_data| placementGetTyped(
iter_,
comptime_data,
@ptrCast(@alignCast(out)),
),
};
}
fn placementGetTyped(
iter_: PlacementIterator,
comptime data: PlacementData,
out: *data.OutType(),
) Result {
const iter = iter_ orelse return .invalid_value;
const entry = iter.entry orelse return .invalid_value;
switch (data) {
.invalid => return .invalid_value,
.image_id => out.* = entry.key_ptr.image_id,
.placement_id => out.* = entry.key_ptr.placement_id.id,
.is_virtual => out.* = entry.value_ptr.location == .virtual,
.x_offset => out.* = entry.value_ptr.x_offset,
.y_offset => out.* = entry.value_ptr.y_offset,
.source_x => out.* = entry.value_ptr.source_x,
.source_y => out.* = entry.value_ptr.source_y,
.source_width => out.* = entry.value_ptr.source_width,
.source_height => out.* = entry.value_ptr.source_height,
.columns => out.* = entry.value_ptr.columns,
.rows => out.* = entry.value_ptr.rows,
.z => out.* = entry.value_ptr.z,
}
return .success;
}
pub fn placement_rect(
iter_: PlacementIterator,
image_: ImageHandle,
terminal_: terminal_c.Terminal,
out: *selection_c.CSelection,
) callconv(lib.calling_conv) Result {
if (comptime !build_options.kitty_graphics) return .no_value;
const wrapper = terminal_ orelse return .invalid_value;
const image = image_ orelse return .invalid_value;
const iter = iter_ orelse return .invalid_value;
const entry = iter.entry orelse return .invalid_value;
const r = entry.value_ptr.rect(
image.*,
wrapper.terminal,
) orelse return .no_value;
out.* = .{
.start = grid_ref.CGridRef.fromPin(r.top_left),
.end = grid_ref.CGridRef.fromPin(r.bottom_right),
.rectangle = true,
};
return .success;
}
pub fn placement_pixel_size(
iter_: PlacementIterator,
image_: ImageHandle,
terminal_: terminal_c.Terminal,
out_width: *u32,
out_height: *u32,
) callconv(lib.calling_conv) Result {
if (comptime !build_options.kitty_graphics) return .no_value;
const wrapper = terminal_ orelse return .invalid_value;
const image = image_ orelse return .invalid_value;
const iter = iter_ orelse return .invalid_value;
const entry = iter.entry orelse return .invalid_value;
const s = entry.value_ptr.pixelSize(image.*, wrapper.terminal);
out_width.* = s.width;
out_height.* = s.height;
return .success;
}
pub fn placement_grid_size(
iter_: PlacementIterator,
image_: ImageHandle,
terminal_: terminal_c.Terminal,
out_cols: *u32,
out_rows: *u32,
) callconv(lib.calling_conv) Result {
if (comptime !build_options.kitty_graphics) return .no_value;
const wrapper = terminal_ orelse return .invalid_value;
const image = image_ orelse return .invalid_value;
const iter = iter_ orelse return .invalid_value;
const entry = iter.entry orelse return .invalid_value;
const s = entry.value_ptr.gridSize(image.*, wrapper.terminal);
out_cols.* = s.cols;
out_rows.* = s.rows;
return .success;
}
test "placement_iterator new/free" {
var iter: PlacementIterator = null;
try testing.expectEqual(Result.success, placement_iterator_new(
&lib.alloc.test_allocator,
&iter,
));
try testing.expect(iter != null);
placement_iterator_free(iter);
}
test "placement_iterator free null" {
placement_iterator_free(null);
}
test "placement_iterator next on empty storage" {
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
var t: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib.alloc.test_allocator,
&t,
.{ .cols = 80, .rows = 24, .max_scrollback = 0 },
));
defer terminal_c.free(t);
var graphics: KittyGraphics = undefined;
try testing.expectEqual(Result.success, terminal_c.get(
t,
.kitty_graphics,
@ptrCast(&graphics),
));
var iter: PlacementIterator = null;
try testing.expectEqual(Result.success, placement_iterator_new(
&lib.alloc.test_allocator,
&iter,
));
defer placement_iterator_free(iter);
try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter)));
try testing.expect(!placement_iterator_next(iter));
}
test "placement_iterator get before next returns invalid" {
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
var t: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib.alloc.test_allocator,
&t,
.{ .cols = 80, .rows = 24, .max_scrollback = 0 },
));
defer terminal_c.free(t);
var graphics: KittyGraphics = undefined;
try testing.expectEqual(Result.success, terminal_c.get(
t,
.kitty_graphics,
@ptrCast(&graphics),
));
var iter: PlacementIterator = null;
try testing.expectEqual(Result.success, placement_iterator_new(
&lib.alloc.test_allocator,
&iter,
));
defer placement_iterator_free(iter);
try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter)));
var image_id: u32 = undefined;
try testing.expectEqual(Result.invalid_value, placement_get(iter, .image_id, @ptrCast(&image_id)));
}
test "placement_iterator with transmit and display" {
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
var t: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib.alloc.test_allocator,
&t,
.{ .cols = 80, .rows = 24, .max_scrollback = 0 },
));
defer terminal_c.free(t);
// Transmit and display a 1x2 RGB image (image_id=1, placement_id=1).
// a=T (transmit+display), t=d (direct), f=24 (RGB), i=1, p=1
// s=1,v=2 (1x2 pixels), c=10,r=1 (10 cols, 1 row)
// //////// = 8 base64 chars = 6 bytes = 1*2*3 RGB bytes
const cmd = "\x1b_Ga=T,t=d,f=24,i=1,p=1,s=1,v=2,c=10,r=1;////////\x1b\\";
terminal_c.vt_write(t, cmd.ptr, cmd.len);
var graphics: KittyGraphics = undefined;
try testing.expectEqual(Result.success, terminal_c.get(
t,
.kitty_graphics,
@ptrCast(&graphics),
));
var iter: PlacementIterator = null;
try testing.expectEqual(Result.success, placement_iterator_new(
&lib.alloc.test_allocator,
&iter,
));
defer placement_iterator_free(iter);
try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter)));
// Should have exactly one placement.
try testing.expect(placement_iterator_next(iter));
var image_id: u32 = undefined;
try testing.expectEqual(Result.success, placement_get(iter, .image_id, @ptrCast(&image_id)));
try testing.expectEqual(1, image_id);
var placement_id: u32 = undefined;
try testing.expectEqual(Result.success, placement_get(iter, .placement_id, @ptrCast(&placement_id)));
try testing.expectEqual(1, placement_id);
var is_virtual: bool = undefined;
try testing.expectEqual(Result.success, placement_get(iter, .is_virtual, @ptrCast(&is_virtual)));
try testing.expect(!is_virtual);
// No more placements.
try testing.expect(!placement_iterator_next(iter));
}
test "placement_iterator with multiple placements" {
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
var t: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib.alloc.test_allocator,
&t,
.{ .cols = 80, .rows = 24, .max_scrollback = 0 },
));
defer terminal_c.free(t);
// Transmit image 1 then display it twice with different placement IDs.
const transmit = "\x1b_Ga=t,t=d,f=24,i=1,s=1,v=2;////////\x1b\\";
const display1 = "\x1b_Ga=p,i=1,p=1,c=10,r=1;\x1b\\";
const display2 = "\x1b_Ga=p,i=1,p=2,c=5,r=1;\x1b\\";
terminal_c.vt_write(t, transmit.ptr, transmit.len);
terminal_c.vt_write(t, display1.ptr, display1.len);
terminal_c.vt_write(t, display2.ptr, display2.len);
var graphics: KittyGraphics = undefined;
try testing.expectEqual(Result.success, terminal_c.get(
t,
.kitty_graphics,
@ptrCast(&graphics),
));
var iter: PlacementIterator = null;
try testing.expectEqual(Result.success, placement_iterator_new(
&lib.alloc.test_allocator,
&iter,
));
defer placement_iterator_free(iter);
try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter)));
// Count placements and collect image IDs.
var count: usize = 0;
var seen_p1 = false;
var seen_p2 = false;
while (placement_iterator_next(iter)) {
count += 1;
var image_id: u32 = undefined;
try testing.expectEqual(Result.success, placement_get(iter, .image_id, @ptrCast(&image_id)));
try testing.expectEqual(1, image_id);
var placement_id: u32 = undefined;
try testing.expectEqual(Result.success, placement_get(iter, .placement_id, @ptrCast(&placement_id)));
if (placement_id == 1) seen_p1 = true;
if (placement_id == 2) seen_p2 = true;
}
try testing.expectEqual(2, count);
try testing.expect(seen_p1);
try testing.expect(seen_p2);
}
test "image_get_handle returns null for missing id" {
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
var t: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib.alloc.test_allocator,
&t,
.{ .cols = 80, .rows = 24, .max_scrollback = 0 },
));
defer terminal_c.free(t);
var graphics: KittyGraphics = undefined;
try testing.expectEqual(Result.success, terminal_c.get(
t,
.kitty_graphics,
@ptrCast(&graphics),
));
try testing.expectEqual(@as(ImageHandle, null), image_get_handle(graphics, 999));
}
test "image_get_handle and image_get with transmitted image" {
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
var t: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib.alloc.test_allocator,
&t,
.{ .cols = 80, .rows = 24, .max_scrollback = 0 },
));
defer terminal_c.free(t);
// Transmit a 1x2 RGB image with image_id=1.
const cmd = "\x1b_Ga=T,t=d,f=24,i=1,p=1,s=1,v=2;////////\x1b\\";
terminal_c.vt_write(t, cmd.ptr, cmd.len);
var graphics: KittyGraphics = undefined;
try testing.expectEqual(Result.success, terminal_c.get(
t,
.kitty_graphics,
@ptrCast(&graphics),
));
const img = image_get_handle(graphics, 1);
try testing.expect(img != null);
var id: u32 = undefined;
try testing.expectEqual(Result.success, image_get(img, .id, @ptrCast(&id)));
try testing.expectEqual(1, id);
var w: u32 = undefined;
try testing.expectEqual(Result.success, image_get(img, .width, @ptrCast(&w)));
try testing.expectEqual(1, w);
var h: u32 = undefined;
try testing.expectEqual(Result.success, image_get(img, .height, @ptrCast(&h)));
try testing.expectEqual(2, h);
var fmt: ImageFormat = undefined;
try testing.expectEqual(Result.success, image_get(img, .format, @ptrCast(&fmt)));
try testing.expectEqual(.rgb, fmt);
var comp: ImageCompression = undefined;
try testing.expectEqual(Result.success, image_get(img, .compression, @ptrCast(&comp)));
try testing.expectEqual(.none, comp);
var data_len: usize = undefined;
try testing.expectEqual(Result.success, image_get(img, .data_len, @ptrCast(&data_len)));
try testing.expect(data_len > 0);
}
test "placement_rect with transmit and display" {
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
var t: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib.alloc.test_allocator,
&t,
.{ .cols = 80, .rows = 24, .max_scrollback = 0 },
));
defer terminal_c.free(t);
// Set cell size so grid calculations are deterministic.
// 80 cols * 10px = 800px, 24 rows * 20px = 480px.
try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 24, 10, 20));
// Transmit and display a 1x2 RGB image at cursor (0,0).
// c=10,r=1 => 10 columns, 1 row.
const cmd = "\x1b_Ga=T,t=d,f=24,i=1,p=1,s=1,v=2,c=10,r=1;////////\x1b\\";
terminal_c.vt_write(t, cmd.ptr, cmd.len);
var graphics: KittyGraphics = undefined;
try testing.expectEqual(Result.success, terminal_c.get(
t,
.kitty_graphics,
@ptrCast(&graphics),
));
const img = image_get_handle(graphics, 1);
try testing.expect(img != null);
var iter: PlacementIterator = null;
try testing.expectEqual(Result.success, placement_iterator_new(
&lib.alloc.test_allocator,
&iter,
));
defer placement_iterator_free(iter);
try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter)));
try testing.expect(placement_iterator_next(iter));
var sel: selection_c.CSelection = undefined;
try testing.expectEqual(Result.success, placement_rect(iter, img, t, &sel));
// Placement starts at cursor origin (0,0).
try testing.expectEqual(0, sel.start.x);
try testing.expectEqual(0, sel.start.y);
// 10 columns wide, 1 row tall => bottom-right is (9, 0).
try testing.expectEqual(9, sel.end.x);
try testing.expectEqual(0, sel.end.y);
try testing.expect(sel.rectangle);
}
test "placement_rect null args return invalid_value" {
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
var sel: selection_c.CSelection = undefined;
try testing.expectEqual(Result.invalid_value, placement_rect(null, null, null, &sel));
}
test "placement_pixel_size with transmit and display" {
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
var t: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib.alloc.test_allocator,
&t,
.{ .cols = 80, .rows = 24, .max_scrollback = 0 },
));
defer terminal_c.free(t);
// 80 cols * 10px = 800px, 24 rows * 20px = 480px.
try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 24, 10, 20));
// Transmit and display a 1x2 RGB image with c=10,r=1.
// 10 cols * 10px = 100px width, 1 row * 20px = 20px height.
const cmd = "\x1b_Ga=T,t=d,f=24,i=1,p=1,s=1,v=2,c=10,r=1;////////\x1b\\";
terminal_c.vt_write(t, cmd.ptr, cmd.len);
var graphics: KittyGraphics = undefined;
try testing.expectEqual(Result.success, terminal_c.get(
t,
.kitty_graphics,
@ptrCast(&graphics),
));
const img = image_get_handle(graphics, 1);
try testing.expect(img != null);
var iter: PlacementIterator = null;
try testing.expectEqual(Result.success, placement_iterator_new(
&lib.alloc.test_allocator,
&iter,
));
defer placement_iterator_free(iter);
try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter)));
try testing.expect(placement_iterator_next(iter));
var w: u32 = undefined;
var h: u32 = undefined;
try testing.expectEqual(Result.success, placement_pixel_size(iter, img, t, &w, &h));
try testing.expectEqual(100, w);
try testing.expectEqual(20, h);
}
test "placement_pixel_size null args return invalid_value" {
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
var w: u32 = undefined;
var h: u32 = undefined;
try testing.expectEqual(Result.invalid_value, placement_pixel_size(null, null, null, &w, &h));
}
test "placement_grid_size with transmit and display" {
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
var t: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib.alloc.test_allocator,
&t,
.{ .cols = 80, .rows = 24, .max_scrollback = 0 },
));
defer terminal_c.free(t);
// 80 cols * 10px = 800px, 24 rows * 20px = 480px.
try testing.expectEqual(Result.success, terminal_c.resize(t, 80, 24, 10, 20));
// Transmit and display a 1x2 RGB image with c=10,r=1.
const cmd = "\x1b_Ga=T,t=d,f=24,i=1,p=1,s=1,v=2,c=10,r=1;////////\x1b\\";
terminal_c.vt_write(t, cmd.ptr, cmd.len);
var graphics: KittyGraphics = undefined;
try testing.expectEqual(Result.success, terminal_c.get(
t,
.kitty_graphics,
@ptrCast(&graphics),
));
const img = image_get_handle(graphics, 1);
try testing.expect(img != null);
var iter: PlacementIterator = null;
try testing.expectEqual(Result.success, placement_iterator_new(
&lib.alloc.test_allocator,
&iter,
));
defer placement_iterator_free(iter);
try testing.expectEqual(Result.success, get(graphics, .placement_iterator, @ptrCast(&iter)));
try testing.expect(placement_iterator_next(iter));
var cols: u32 = undefined;
var rows: u32 = undefined;
try testing.expectEqual(Result.success, placement_grid_size(iter, img, t, &cols, &rows));
try testing.expectEqual(10, cols);
try testing.expectEqual(1, rows);
}
test "placement_grid_size null args return invalid_value" {
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
var cols: u32 = undefined;
var rows: u32 = undefined;
try testing.expectEqual(Result.invalid_value, placement_grid_size(null, null, null, &cols, &rows));
}
test "image_get on null returns invalid_value" {
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
var id: u32 = undefined;
try testing.expectEqual(Result.invalid_value, image_get(null, .id, @ptrCast(&id)));
}

View File

@@ -8,6 +8,17 @@ pub const color = @import("color.zig");
pub const focus = @import("focus.zig");
pub const formatter = @import("formatter.zig");
pub const grid_ref = @import("grid_ref.zig");
pub const kitty_graphics = @import("kitty_graphics.zig");
pub const kitty_graphics_get = kitty_graphics.get;
pub const kitty_graphics_image = kitty_graphics.image_get_handle;
pub const kitty_graphics_image_get = kitty_graphics.image_get;
pub const kitty_graphics_placement_iterator_new = kitty_graphics.placement_iterator_new;
pub const kitty_graphics_placement_iterator_free = kitty_graphics.placement_iterator_free;
pub const kitty_graphics_placement_next = kitty_graphics.placement_iterator_next;
pub const kitty_graphics_placement_get = kitty_graphics.placement_get;
pub const kitty_graphics_placement_rect = kitty_graphics.placement_rect;
pub const kitty_graphics_placement_pixel_size = kitty_graphics.placement_pixel_size;
pub const kitty_graphics_placement_grid_size = kitty_graphics.placement_grid_size;
pub const types = @import("types.zig");
pub const modes = @import("modes.zig");
pub const osc = @import("osc.zig");
@@ -146,6 +157,7 @@ pub const terminal_mode_get = terminal.mode_get;
pub const terminal_mode_set = terminal.mode_set;
pub const terminal_get = terminal.get;
pub const terminal_grid_ref = terminal.grid_ref;
pub const terminal_point_from_grid_ref = terminal.point_from_grid_ref;
pub const type_json = types.get_json;
@@ -161,6 +173,7 @@ test {
_ = cell;
_ = color;
_ = grid_ref;
_ = kitty_graphics;
_ = row;
_ = focus;
_ = formatter;

View File

@@ -8,6 +8,7 @@ const Stream = @import("../stream_terminal.zig").Stream;
const ScreenSet = @import("../ScreenSet.zig");
const PageList = @import("../PageList.zig");
const kitty = @import("../kitty/key.zig");
const kitty_gfx_c = @import("kitty_graphics.zig");
const modes = @import("../modes.zig");
const point = @import("../point.zig");
const size = @import("../size.zig");
@@ -515,6 +516,9 @@ pub fn mode_set(
return .success;
}
/// C: GhosttyKittyGraphics
pub const KittyGraphics = kitty_gfx_c.KittyGraphics;
/// C: GhosttyTerminalScreen
pub const TerminalScreen = ScreenSet.Key;
@@ -553,6 +557,7 @@ pub const TerminalData = enum(c_int) {
kitty_image_medium_file = 27,
kitty_image_medium_temp_file = 28,
kitty_image_medium_shared_mem = 29,
kitty_graphics = 30,
/// Output type expected for querying the data of the given kind.
pub fn OutType(comptime self: TerminalData) type {
@@ -580,6 +585,7 @@ pub const TerminalData = enum(c_int) {
.kitty_image_medium_temp_file,
.kitty_image_medium_shared_mem,
=> bool,
.kitty_graphics => KittyGraphics,
};
}
};
@@ -664,6 +670,10 @@ fn getTyped(
if (comptime !build_options.kitty_graphics) return .no_value;
out.* = t.screens.active.kitty_images.image_limits.shared_memory;
},
.kitty_graphics => {
if (comptime !build_options.kitty_graphics) return .no_value;
out.* = &t.screens.active.kitty_images;
},
}
return .success;
@@ -687,6 +697,20 @@ pub fn grid_ref(
return .success;
}
pub fn point_from_grid_ref(
terminal_: Terminal,
ref: *const grid_ref_c.CGridRef,
tag: point.Tag,
out: ?*point.Coordinate,
) callconv(lib.calling_conv) Result {
const t: *ZigTerminal = (terminal_ orelse return .invalid_value).terminal;
const p = ref.toPin() orelse return .invalid_value;
const pt = t.screens.active.pages.pointFromPin(tag, p) orelse
return .no_value;
if (out) |o| o.* = pt.coord();
return .success;
}
pub fn free(terminal_: Terminal) callconv(lib.calling_conv) void {
const wrapper = terminal_ orelse return;
const t = wrapper.terminal;
@@ -1251,6 +1275,102 @@ test "grid_ref null terminal" {
}, &out_ref));
}
test "point_from_grid_ref roundtrip active" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib.alloc.test_allocator,
&t,
.{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
));
defer free(t);
vt_write(t, "Hello", 5);
// Get a grid ref at (2, 0) in active coords
var ref: grid_ref_c.CGridRef = .{};
try testing.expectEqual(Result.success, grid_ref(t, .{
.tag = .active,
.value = .{ .active = .{ .x = 2, .y = 0 } },
}, &ref));
// Convert back to active coords
var coord: point.Coordinate = undefined;
try testing.expectEqual(Result.success, point_from_grid_ref(t, &ref, .active, &coord));
try testing.expectEqual(@as(size.CellCountInt, 2), coord.x);
try testing.expectEqual(@as(u32, 0), coord.y);
}
test "point_from_grid_ref roundtrip viewport" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib.alloc.test_allocator,
&t,
.{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
));
defer free(t);
vt_write(t, "Hello", 5);
var ref: grid_ref_c.CGridRef = .{};
try testing.expectEqual(Result.success, grid_ref(t, .{
.tag = .viewport,
.value = .{ .viewport = .{ .x = 0, .y = 0 } },
}, &ref));
var coord: point.Coordinate = undefined;
try testing.expectEqual(Result.success, point_from_grid_ref(t, &ref, .viewport, &coord));
try testing.expectEqual(@as(size.CellCountInt, 0), coord.x);
try testing.expectEqual(@as(u32, 0), coord.y);
}
test "point_from_grid_ref history ref to active returns no_value" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib.alloc.test_allocator,
&t,
.{ .cols = 80, .rows = 4, .max_scrollback = 10_000 },
));
defer free(t);
// Write enough lines to push content into scrollback
for (0..10) |_| {
vt_write(t, "line\n", 5);
}
// Get a ref to the first line (now in scrollback)
var ref: grid_ref_c.CGridRef = .{};
try testing.expectEqual(Result.success, grid_ref(t, .{
.tag = .screen,
.value = .{ .screen = .{ .x = 0, .y = 0 } },
}, &ref));
// Should succeed for screen coords
var coord: point.Coordinate = undefined;
try testing.expectEqual(Result.success, point_from_grid_ref(t, &ref, .screen, &coord));
try testing.expectEqual(@as(u32, 0), coord.y);
// Should fail for active coords (it's in scrollback)
try testing.expectEqual(Result.no_value, point_from_grid_ref(t, &ref, .active, &coord));
}
test "point_from_grid_ref null terminal" {
var ref: grid_ref_c.CGridRef = .{};
try testing.expectEqual(Result.invalid_value, point_from_grid_ref(null, &ref, .active, null));
}
test "point_from_grid_ref null node" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib.alloc.test_allocator,
&t,
.{ .cols = 80, .rows = 24, .max_scrollback = 0 },
));
defer free(t);
const ref: grid_ref_c.CGridRef = .{};
try testing.expectEqual(Result.invalid_value, point_from_grid_ref(t, &ref, .active, null));
}
test "set write_pty callback" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(

View File

@@ -3,6 +3,7 @@ const assert = @import("../../quirks.zig").inlineAssert;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const simd = @import("../../simd/main.zig");
const lib = @import("../lib.zig");
const log = std.log.scoped(.kitty_gfx);
@@ -394,39 +395,38 @@ pub const Transmission = struct {
compression: Compression = .none, // o
more_chunks: bool = false, // m
pub const Format = enum {
rgb, // 24
rgba, // 32
png, // 100
pub const Format = lib.Enum(lib.target, &.{
"rgb", // 24
"rgba", // 32
"png", // 100
// The following are not supported directly via the protocol
// but they are formats that a png may decode to that we
// support.
gray_alpha,
gray,
"gray_alpha",
"gray",
});
pub fn bpp(self: Format) u8 {
return switch (self) {
.gray => 1,
.gray_alpha => 2,
.rgb => 3,
.rgba => 4,
.png => unreachable, // Must be validated before
};
}
};
pub const Medium = lib.Enum(lib.target, &.{
"direct", // d
"file", // f
"temporary_file", // t
"shared_memory", // s
});
pub const Medium = enum {
direct, // d
file, // f
temporary_file, // t
shared_memory, // s
};
pub const Compression = lib.Enum(lib.target, &.{
"none",
"zlib_deflate", // z
});
pub const Compression = enum {
none,
zlib_deflate, // z
};
pub fn formatBpp(format: Format) u8 {
return switch (format) {
.gray => 1,
.gray_alpha => 2,
.rgb => 3,
.rgba => 4,
.png => unreachable, // Must be validated before
};
}
fn parse(kv: KV) !Transmission {
var result: Transmission = .{};

View File

@@ -202,8 +202,8 @@ pub const LoadingImage = struct {
.png => stat_size,
// For these formats we have a size we must have.
.gray, .gray_alpha, .rgb, .rgba => |f| size: {
const bpp = f.bpp();
.gray, .gray_alpha, .rgb, .rgba => size: {
const bpp = command.Transmission.formatBpp(self.image.format);
break :size self.image.width * self.image.height * bpp;
},
};
@@ -390,7 +390,7 @@ pub const LoadingImage = struct {
if (img.width > max_dimension or img.height > max_dimension) return error.DimensionsTooLarge;
// Data length must be what we expect
const bpp = img.format.bpp();
const bpp = command.Transmission.formatBpp(img.format);
const expected_len = img.width * img.height * bpp;
const actual_len = self.data.items.len;
if (actual_len != expected_len) {

View File

@@ -662,9 +662,10 @@ pub const ImageStorage = struct {
}
}
/// Calculates the size of this placement's image in pixels,
/// taking in to account the specified rows and columns.
pub fn calculatedSize(
/// Returns the size of this placement's image in pixels,
/// taking into account the source rectangle, specified
/// rows/columns, and aspect ratio.
pub fn pixelSize(
self: Placement,
image: Image,
t: *const terminal.Terminal,
@@ -759,7 +760,7 @@ pub const ImageStorage = struct {
// Otherwise we calculate the pixel size, divide by
// cell size, and round up to the nearest integer.
const calc_size = self.calculatedSize(image, t);
const calc_size = self.pixelSize(image, t);
return .{
.cols = std.math.divCeil(
u32,
@@ -1338,7 +1339,7 @@ test "storage: aspect ratio calculation when only columns or rows specified" {
// that's 100px width. 100px * (9 / 16) = 56.25, which should round
// to a height of 56px.
const calc_size = placement.calculatedSize(image, &t);
const calc_size = placement.pixelSize(image, &t);
try testing.expectEqual(@as(u32, 100), calc_size.width);
try testing.expectEqual(@as(u32, 56), calc_size.height);
}
@@ -1356,7 +1357,7 @@ test "storage: aspect ratio calculation when only columns or rows specified" {
// 100px height. 100px * (16 / 9) = 177.77..., which should round to
// a width of 178px.
const calc_size = placement.calculatedSize(image, &t);
const calc_size = placement.pixelSize(image, &t);
try testing.expectEqual(@as(u32, 178), calc_size.width);
try testing.expectEqual(@as(u32, 100), calc_size.height);
}