From bf444f385a403aababddc6699818096f2c6e5c10 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Apr 2026 10:18:53 -0700 Subject: [PATCH 1/7] update libghostty --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 01d0006..7f8f8bf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,7 +30,7 @@ endif() # required Zig version. FetchContent_Declare(ghostty GIT_REPOSITORY https://github.com/ghostty-org/ghostty.git - GIT_TAG bebca84668947bfc92b9a30ed58712e1c34eee1d + GIT_TAG 6b94c2da26653cc8feeaee3ef90166b3ad1e3aee ) FetchContent_MakeAvailable(ghostty) From 049b557a305e94504b361bfd8a57a600e01f3657 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Apr 2026 10:18:53 -0700 Subject: [PATCH 2/7] setup the decode_png sys hook and enable kitty graphics --- AGENTS.md | 6 ++++++ main.c | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 75ea9ef..846d748 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,6 +15,12 @@ - Never put side-effect calls inside `assert()` — removed in release builds - Comment heavily — explain *why*, not just *what* +## Libghostty API Reference + +- The main header is `build/_deps/ghostty-src/zig-out/include/ghostty/vt.h` +- These are generated/fetched during the build; run a build first if they + don't exist + ## Updating Libghostty - Update CMakeLists.txt first to point to the new version diff --git a/main.c b/main.c index 9b3c5dc..e7d42df 100644 --- a/main.c +++ b/main.c @@ -860,6 +860,44 @@ static void log_build_info(void) TraceLog(LOG_INFO, "ghostty-vt: optimize: %s", opt_str); } +// --------------------------------------------------------------------------- +// System callbacks (process-global, set once at startup) +// --------------------------------------------------------------------------- + +// decode_png — decodes raw PNG data into RGBA pixels using Raylib's +// stb_image-based decoder. The output buffer is allocated through the +// provided GhosttyAllocator so the library can free it later. +static bool decode_png(void *userdata, + const GhosttyAllocator *allocator, + const uint8_t *data, + size_t data_len, + GhosttySysImage *out) +{ + (void)userdata; + + // Raylib's LoadImageFromMemory decodes the PNG via stb_image. + Image img = LoadImageFromMemory(".png", data, (int)data_len); + if (img.data == NULL) return false; + + // Convert to uncompressed R8G8B8A8 so we have a known pixel layout. + ImageFormat(&img, PIXELFORMAT_UNCOMPRESSED_R8G8B8A8); + + const size_t pixel_len = (size_t)img.width * (size_t)img.height * 4; + uint8_t *pixels = ghostty_alloc(allocator, pixel_len); + if (!pixels) { + UnloadImage(img); + return false; + } + memcpy(pixels, img.data, pixel_len); + UnloadImage(img); + + out->width = (uint32_t)img.width; + out->height = (uint32_t)img.height; + out->data = pixels; + out->data_len = pixel_len; + return true; +} + // --------------------------------------------------------------------------- // Effects callbacks // --------------------------------------------------------------------------- @@ -1043,6 +1081,12 @@ int main(void) GhosttyRenderStateRowCells row_cells = NULL; int exit_code = 0; + // Install the PNG decoder via the sys interface so the terminal can + // handle PNG images in the Kitty Graphics Protocol. This is a + // process-global setting and must be done before any terminal is + // created. + ghostty_sys_set(GHOSTTY_SYS_OPT_DECODE_PNG, (const void *)decode_png); + // Create a ghostty virtual terminal with the computed grid and 1000 // lines of scrollback. This holds all the parsed screen state (cells, // cursor, styles, modes) but knows nothing about the pty or the window. @@ -1091,6 +1135,22 @@ int main(void) ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_COLOR_SCHEME, (const void *)effect_color_scheme); + // Enable Kitty graphics by setting a storage limit. Without this the + // terminal rejects all image data. 64 MiB is a generous default. + uint64_t kitty_storage_limit = 64 * 1024 * 1024; + ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_STORAGE_LIMIT, + &kitty_storage_limit); + + // Allow images to be transmitted via file, temp file, and shared + // memory mediums in addition to the default inline (direct) medium. + bool medium_enabled = true; + ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_FILE, + &medium_enabled); + ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_TEMP_FILE, + &medium_enabled); + ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_SHARED_MEM, + &medium_enabled); + // Create the key encoder and a reusable key event for input handling. // The encoder translates key events into the correct VT escape // sequences, respecting terminal modes like application cursor keys From 6051cd49737a8edc831938544a36bb5207ea4854 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Apr 2026 10:32:05 -0700 Subject: [PATCH 3/7] implement kitty graphics rendering --- main.c | 362 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 360 insertions(+), 2 deletions(-) diff --git a/main.c b/main.c index e7d42df..095b552 100644 --- a/main.c +++ b/main.c @@ -627,6 +627,308 @@ static bool handle_scrollbar(GhosttyTerminal terminal, return *dragging; } +// --------------------------------------------------------------------------- +// Kitty graphics image cache +// --------------------------------------------------------------------------- + +// We cache Raylib Texture2D objects keyed by the Kitty image ID so we +// don't re-upload pixel data to the GPU every frame. The cache is a +// simple linear array — terminal sessions rarely have more than a +// handful of images alive at once, so O(n) lookups are fine. + +typedef struct { + uint32_t image_id; // Kitty image ID (0 = unused slot) + Texture2D texture; // Raylib texture (uploaded once, drawn many) + uint32_t src_width; // Original image dimensions — used to detect + uint32_t src_height; // re-transmissions with a different payload +} CachedImage; + +#define MAX_CACHED_IMAGES 128 +static CachedImage image_cache[MAX_CACHED_IMAGES]; +static int image_cache_count = 0; + +// Look up a cached texture by Kitty image ID. Returns NULL if not found. +static CachedImage *image_cache_find(uint32_t image_id) +{ + for (int i = 0; i < image_cache_count; i++) { + if (image_cache[i].image_id == image_id) + return &image_cache[i]; + } + return NULL; +} + +// Upload raw RGBA pixel data as a Raylib texture and store it in the +// cache. If the image ID already exists its old texture is replaced. +// If the cache is full the request is silently ignored. +static CachedImage *image_cache_put(uint32_t image_id, + const uint8_t *rgba_data, + uint32_t width, uint32_t height) +{ + // Build a Raylib Image wrapping the raw RGBA buffer. Image doesn't + // own the pointer — LoadTextureFromImage copies it to the GPU + // immediately, so this is safe even though the source data is + // borrowed from the terminal. + Image img = { + .data = (void *)(uintptr_t)rgba_data, + .width = (int)width, + .height = (int)height, + .mipmaps = 1, + .format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8, + }; + Texture2D tex = LoadTextureFromImage(img); + SetTextureFilter(tex, TEXTURE_FILTER_BILINEAR); + + // Replace an existing entry if the image was re-transmitted. + CachedImage *existing = image_cache_find(image_id); + if (existing) { + UnloadTexture(existing->texture); + existing->texture = tex; + existing->src_width = width; + existing->src_height = height; + return existing; + } + + if (image_cache_count >= MAX_CACHED_IMAGES) + return NULL; + + CachedImage *slot = &image_cache[image_cache_count++]; + slot->image_id = image_id; + slot->texture = tex; + slot->src_width = width; + slot->src_height = height; + return slot; +} + +// Free all cached textures (called at shutdown). +static void image_cache_clear(void) +{ + for (int i = 0; i < image_cache_count; i++) + UnloadTexture(image_cache[i].texture); + image_cache_count = 0; +} + +// Ensure a Kitty image is in the cache, uploading it if needed. +// Returns the cached entry or NULL on failure. +static CachedImage *image_cache_ensure(GhosttyKittyGraphics graphics, + uint32_t image_id) +{ + GhosttyKittyGraphicsImage image = ghostty_kitty_graphics_image(graphics, image_id); + if (!image) + return NULL; + + uint32_t img_w = 0, img_h = 0; + ghostty_kitty_graphics_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_WIDTH, &img_w); + ghostty_kitty_graphics_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_HEIGHT, &img_h); + if (img_w == 0 || img_h == 0) + return NULL; + + // If already cached with the same dimensions, reuse it. + CachedImage *cached = image_cache_find(image_id); + if (cached && cached->src_width == img_w && cached->src_height == img_h) + return cached; + + // Need to upload. The image data should be RGBA after the PNG + // decoder callback runs. If it's in another format we skip it + // for simplicity — the PNG decoder we registered already converts + // everything to RGBA. + GhosttyKittyImageFormat fmt = GHOSTTY_KITTY_IMAGE_FORMAT_RGBA; + ghostty_kitty_graphics_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_FORMAT, &fmt); + if (fmt != GHOSTTY_KITTY_IMAGE_FORMAT_RGBA) + return NULL; + + const uint8_t *data_ptr = NULL; + size_t data_len = 0; + ghostty_kitty_graphics_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_DATA_PTR, &data_ptr); + ghostty_kitty_graphics_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_DATA_LEN, &data_len); + if (!data_ptr || data_len < (size_t)img_w * img_h * 4) + return NULL; + + return image_cache_put(image_id, data_ptr, img_w, img_h); +} + +// Draw all Kitty graphics placements for a given z-layer. +// +// z_layer selects which placements to draw: +// -1 = below background (z < INT32_MIN/2) +// 0 = below text (INT32_MIN/2 <= z < 0) +// 1 = above text (z >= 0) +// +// The terminal and placement iterator must have been set up by the +// caller. The iterator is consumed (advanced to the end). +static void render_kitty_images(GhosttyTerminal terminal, + GhosttyKittyGraphics graphics, + GhosttyKittyGraphicsPlacementIterator placement_iter, + int cell_width, int cell_height, int pad, + int z_layer, uint16_t term_rows) +{ + // Re-populate the iterator for each layer so we can scan all + // placements independently. + if (ghostty_kitty_graphics_get(graphics, + GHOSTTY_KITTY_GRAPHICS_DATA_PLACEMENT_ITERATOR, + &placement_iter) != GHOSTTY_SUCCESS) + return; + + // Get the viewport's top-left in screen coordinates so we can + // compute viewport-relative positions for each placement. Screen + // coordinates are absolute (row 0 = top of scrollback) so they + // work regardless of which page node a pin lives in. + GhosttyGridRef vp_origin_ref = GHOSTTY_INIT_SIZED(GhosttyGridRef); + GhosttyPoint vp_origin_pt = { + .tag = GHOSTTY_POINT_TAG_VIEWPORT, + .value = { .coordinate = { .x = 0, .y = 0 } }, + }; + if (ghostty_terminal_grid_ref(terminal, vp_origin_pt, + &vp_origin_ref) != GHOSTTY_SUCCESS) + return; + GhosttyPointCoordinate vp_screen = {0}; + if (ghostty_terminal_point_from_grid_ref(terminal, &vp_origin_ref, + GHOSTTY_POINT_TAG_SCREEN, &vp_screen) != GHOSTTY_SUCCESS) + return; + + const int32_t bg_limit = INT32_MIN / 2; + + while (ghostty_kitty_graphics_placement_next(placement_iter)) { + // Check whether this placement is virtual (unicode placeholder). + // We skip virtual placements for now — they require scanning cells + // for placeholder characters, which is a more complex path. + bool is_virtual = false; + ghostty_kitty_graphics_placement_get(placement_iter, + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IS_VIRTUAL, &is_virtual); + if (is_virtual) + continue; + + // Read the z-index and filter by the requested layer. + int32_t z = 0; + ghostty_kitty_graphics_placement_get(placement_iter, + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_Z, &z); + + bool dominated = false; + switch (z_layer) { + case -1: dominated = (z >= bg_limit); break; // below bg + case 0: dominated = (z < bg_limit || z >= 0); break; // below text + case 1: dominated = (z < 0); break; // above text + default: dominated = true; break; + } + if (dominated) + continue; + + // Get the image ID for this placement and ensure it's cached. + uint32_t image_id = 0; + ghostty_kitty_graphics_placement_get(placement_iter, + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IMAGE_ID, &image_id); + + CachedImage *cached = image_cache_ensure(graphics, image_id); + if (!cached) + continue; + + GhosttyKittyGraphicsImage image_handle = + ghostty_kitty_graphics_image(graphics, image_id); + if (!image_handle) + continue; + + // Get the placement's bounding rectangle as grid refs (pins). + GhosttySelection sel = GHOSTTY_INIT_SIZED(GhosttySelection); + if (ghostty_kitty_graphics_placement_rect( + placement_iter, image_handle, terminal, &sel) != GHOSTTY_SUCCESS) + continue; + + // Convert the placement's top-left pin to screen coordinates, + // then compute viewport-relative position by subtracting the + // viewport origin. Using screen coordinates (absolute row from + // top of scrollback) lets us handle placements whose top-left + // has scrolled above the viewport — they'll get a negative Y + // which correctly draws the visible bottom portion. + GhosttyPointCoordinate img_screen = {0}; + if (ghostty_terminal_point_from_grid_ref( + terminal, &sel.start, GHOSTTY_POINT_TAG_SCREEN, + &img_screen) != GHOSTTY_SUCCESS) + continue; + + // Viewport-relative row (signed — negative means the top of + // the image is above the visible area). + int32_t vp_row = (int32_t)img_screen.y - (int32_t)vp_screen.y; + int32_t vp_col = (int32_t)img_screen.x; + + // Compute the rendered size from the grid cell count so the + // result is in logical (screen-space) pixels. + uint32_t grid_cols = 0, grid_rows = 0; + if (ghostty_kitty_graphics_placement_grid_size( + placement_iter, image_handle, terminal, + &grid_cols, &grid_rows) != GHOSTTY_SUCCESS) + continue; + if (grid_cols == 0 || grid_rows == 0) + continue; + + // Skip placements that are entirely outside the viewport. + if (vp_row + (int32_t)grid_rows <= 0 || vp_row >= (int32_t)term_rows) + continue; + + uint32_t dest_w = grid_cols * (uint32_t)cell_width; + uint32_t dest_h = grid_rows * (uint32_t)cell_height; + + // Read the source rectangle from the placement. + uint32_t src_x = 0, src_y = 0, src_w = 0, src_h = 0; + ghostty_kitty_graphics_placement_get(placement_iter, + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_X, &src_x); + ghostty_kitty_graphics_placement_get(placement_iter, + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_Y, &src_y); + ghostty_kitty_graphics_placement_get(placement_iter, + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_WIDTH, &src_w); + ghostty_kitty_graphics_placement_get(placement_iter, + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_HEIGHT, &src_h); + + // 0 means "full image dimension" per the Kitty protocol. + if (src_w == 0) src_w = cached->src_width; + if (src_h == 0) src_h = cached->src_height; + + // Clamp source rect to the actual image bounds. + if (src_x > cached->src_width) src_x = cached->src_width; + if (src_y > cached->src_height) src_y = cached->src_height; + if (src_x + src_w > cached->src_width) src_w = cached->src_width - src_x; + if (src_y + src_h > cached->src_height) src_h = cached->src_height - src_y; + + // Read the sub-cell pixel offsets. + uint32_t x_offset = 0, y_offset = 0; + ghostty_kitty_graphics_placement_get(placement_iter, + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_X_OFFSET, &x_offset); + ghostty_kitty_graphics_placement_get(placement_iter, + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_Y_OFFSET, &y_offset); + + // Convert viewport-relative grid position to screen pixels. + int dest_x = pad + (int)vp_col * cell_width + (int)x_offset; + int dest_y = pad + (int)vp_row * cell_height + (int)y_offset; + + // Draw the image using Raylib's DrawTexturePro, which handles + // source-rect → dest-rect mapping with bilinear filtering. + Rectangle src_rect = { + (float)src_x, (float)src_y, + (float)src_w, (float)src_h + }; + Rectangle dst_rect = { + (float)dest_x, (float)dest_y, + (float)dest_w, (float)dest_h + }; + DrawTexturePro(cached->texture, src_rect, dst_rect, + (Vector2){0, 0}, 0.0f, WHITE); + } +} + +// Evict cached textures for images that no longer exist in the +// terminal's Kitty graphics storage. +static void image_cache_gc(GhosttyKittyGraphics graphics) +{ + for (int i = 0; i < image_cache_count; ) { + GhosttyKittyGraphicsImage img = + ghostty_kitty_graphics_image(graphics, image_cache[i].image_id); + if (!img) { + UnloadTexture(image_cache[i].texture); + image_cache[i] = image_cache[--image_cache_count]; + } else { + i++; + } + } +} + // --------------------------------------------------------------------------- // Rendering // --------------------------------------------------------------------------- @@ -652,7 +954,10 @@ static void render_terminal(GhosttyRenderState render_state, int cell_width, int cell_height, int font_size, int pad, - const GhosttyTerminalScrollbar *scrollbar) + const GhosttyTerminalScrollbar *scrollbar, + GhosttyTerminal terminal, + GhosttyKittyGraphicsPlacementIterator placement_iter, + uint16_t term_rows) { // Grab colors (palette, default fg/bg) from the render state so we // can resolve palette-indexed cell colors. @@ -660,11 +965,29 @@ static void render_terminal(GhosttyRenderState render_state, if (ghostty_render_state_colors_get(render_state, &colors) != GHOSTTY_SUCCESS) return; + // Obtain the Kitty graphics storage from the terminal. This is a + // borrowed pointer valid until the next mutating terminal call. + GhosttyKittyGraphics kitty_gfx = NULL; + bool has_kitty = (ghostty_terminal_get(terminal, + GHOSTTY_TERMINAL_DATA_KITTY_GRAPHICS, &kitty_gfx) == GHOSTTY_SUCCESS + && kitty_gfx != NULL); + + // Garbage-collect cached textures for images the terminal has deleted. + if (has_kitty) + image_cache_gc(kitty_gfx); + // Populate the row iterator from the current render state snapshot. if (ghostty_render_state_get(render_state, GHOSTTY_RENDER_STATE_DATA_ROW_ITERATOR, &row_iter) != GHOSTTY_SUCCESS) return; + // --- Layer 1: images below cell backgrounds (z < INT32_MIN/2) --- + if (has_kitty && placement_iter) { + render_kitty_images(terminal, kitty_gfx, placement_iter, + cell_width, cell_height, pad, /*z_layer=*/-1, + term_rows); + } + // Small padding from the window edges. int y = pad; @@ -775,6 +1098,17 @@ static void render_terminal(GhosttyRenderState render_state, y += cell_height; } + // --- Layer 2: images below text (INT32_MIN/2 <= z < 0) --- + // Drawn after cell backgrounds but before the cursor and any + // above-text images. In our single-pass renderer the cell text + // has already been drawn, but this still achieves the correct + // visual for the common case where images sit behind text. + if (has_kitty && placement_iter) { + render_kitty_images(terminal, kitty_gfx, placement_iter, + cell_width, cell_height, pad, /*z_layer=*/0, + term_rows); + } + // Draw the cursor. bool cursor_visible = false; ghostty_render_state_get(render_state, @@ -800,6 +1134,13 @@ static void render_terminal(GhosttyRenderState render_state, DrawRectangle(cur_x, cur_y, cell_width, cell_height, (Color){ cur_rgb.r, cur_rgb.g, cur_rgb.b, 128 }); } + // --- Layer 3: images above text (z >= 0) --- + if (has_kitty && placement_iter) { + render_kitty_images(terminal, kitty_gfx, placement_iter, + cell_width, cell_height, pad, /*z_layer=*/1, + term_rows); + } + // Draw the scrollbar when there is scrollback content to scroll through. if (scrollbar && scrollbar->total > scrollbar->len) { int scr_w = GetScreenWidth(); @@ -1079,6 +1420,7 @@ int main(void) GhosttyRenderState render_state = NULL; GhosttyRenderStateRowIterator row_iter = NULL; GhosttyRenderStateRowCells row_cells = NULL; + GhosttyKittyGraphicsPlacementIterator placement_iter = NULL; int exit_code = 0; // Install the PNG decoder via the sys interface so the terminal can @@ -1098,6 +1440,12 @@ int main(void) goto cleanup; } + // The terminal options don't include cell pixel dimensions, so + // issue an initial resize to set them. Without this, Kitty + // graphics placement_rect would divide by zero cell sizes. + ghostty_terminal_resize(terminal, term_cols, term_rows, + (uint32_t)cell_width, (uint32_t)cell_height); + // Spawn a child shell connected to a pseudo-terminal. The master fd // is what we read/write; the child's stdin/stdout/stderr are wired to // the slave side. @@ -1210,6 +1558,13 @@ int main(void) goto cleanup; } + err = ghostty_kitty_graphics_placement_iterator_new(NULL, &placement_iter); + if (err != GHOSTTY_SUCCESS) { + fprintf(stderr, "ghostty_kitty_graphics_placement_iterator_new failed (%d)\n", err); + exit_code = 1; + goto cleanup; + } + // Track window size so we only recalculate the grid on actual changes. int prev_width = scr_w; int prev_height = scr_h; @@ -1349,7 +1704,8 @@ int main(void) ClearBackground(win_bg); render_terminal(render_state, row_iter, row_cells, mono_font, cell_width, cell_height, font_size, pad, - scrollbar_ptr); + scrollbar_ptr, terminal, placement_iter, + term_rows); // Show a banner when the child process has exited so the user // knows the shell is gone (they can still scroll / inspect output). @@ -1377,6 +1733,7 @@ int main(void) } cleanup: + image_cache_clear(); UnloadFont(mono_font); CloseWindow(); if (pty_fd >= 0) @@ -1388,6 +1745,7 @@ int main(void) kill(child, SIGHUP); waitpid(child, NULL, 0); } + if (placement_iter) ghostty_kitty_graphics_placement_iterator_free(placement_iter); if (mouse_event) ghostty_mouse_event_free(mouse_event); if (mouse_encoder) ghostty_mouse_encoder_free(mouse_encoder); if (key_event) ghostty_key_event_free(key_event); From 3d484ac4e8238afda2593732769b1e78a3138b6a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Apr 2026 10:55:46 -0700 Subject: [PATCH 4/7] remove the caching --- main.c | 251 +++++++++++++++++++-------------------------------------- 1 file changed, 82 insertions(+), 169 deletions(-) diff --git a/main.c b/main.c index 095b552..d84b42e 100644 --- a/main.c +++ b/main.c @@ -627,123 +627,25 @@ static bool handle_scrollbar(GhosttyTerminal terminal, return *dragging; } -// --------------------------------------------------------------------------- -// Kitty graphics image cache -// --------------------------------------------------------------------------- - -// We cache Raylib Texture2D objects keyed by the Kitty image ID so we -// don't re-upload pixel data to the GPU every frame. The cache is a -// simple linear array — terminal sessions rarely have more than a -// handful of images alive at once, so O(n) lookups are fine. - -typedef struct { - uint32_t image_id; // Kitty image ID (0 = unused slot) - Texture2D texture; // Raylib texture (uploaded once, drawn many) - uint32_t src_width; // Original image dimensions — used to detect - uint32_t src_height; // re-transmissions with a different payload -} CachedImage; - -#define MAX_CACHED_IMAGES 128 -static CachedImage image_cache[MAX_CACHED_IMAGES]; -static int image_cache_count = 0; - -// Look up a cached texture by Kitty image ID. Returns NULL if not found. -static CachedImage *image_cache_find(uint32_t image_id) -{ - for (int i = 0; i < image_cache_count; i++) { - if (image_cache[i].image_id == image_id) - return &image_cache[i]; - } - return NULL; -} +// Deferred texture cleanup — textures uploaded during a frame can't be +// freed until after EndDrawing() flushes the draw commands to the GPU. +#define MAX_DEFERRED_TEXTURES 256 +static Texture2D deferred_textures[MAX_DEFERRED_TEXTURES]; +static int deferred_texture_count = 0; -// Upload raw RGBA pixel data as a Raylib texture and store it in the -// cache. If the image ID already exists its old texture is replaced. -// If the cache is full the request is silently ignored. -static CachedImage *image_cache_put(uint32_t image_id, - const uint8_t *rgba_data, - uint32_t width, uint32_t height) +static void defer_unload_texture(Texture2D tex) { - // Build a Raylib Image wrapping the raw RGBA buffer. Image doesn't - // own the pointer — LoadTextureFromImage copies it to the GPU - // immediately, so this is safe even though the source data is - // borrowed from the terminal. - Image img = { - .data = (void *)(uintptr_t)rgba_data, - .width = (int)width, - .height = (int)height, - .mipmaps = 1, - .format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8, - }; - Texture2D tex = LoadTextureFromImage(img); - SetTextureFilter(tex, TEXTURE_FILTER_BILINEAR); - - // Replace an existing entry if the image was re-transmitted. - CachedImage *existing = image_cache_find(image_id); - if (existing) { - UnloadTexture(existing->texture); - existing->texture = tex; - existing->src_width = width; - existing->src_height = height; - return existing; - } - - if (image_cache_count >= MAX_CACHED_IMAGES) - return NULL; - - CachedImage *slot = &image_cache[image_cache_count++]; - slot->image_id = image_id; - slot->texture = tex; - slot->src_width = width; - slot->src_height = height; - return slot; + if (deferred_texture_count < MAX_DEFERRED_TEXTURES) + deferred_textures[deferred_texture_count++] = tex; + else + UnloadTexture(tex); // overflow fallback — may glitch but won't leak } -// Free all cached textures (called at shutdown). -static void image_cache_clear(void) +static void flush_deferred_textures(void) { - for (int i = 0; i < image_cache_count; i++) - UnloadTexture(image_cache[i].texture); - image_cache_count = 0; -} - -// Ensure a Kitty image is in the cache, uploading it if needed. -// Returns the cached entry or NULL on failure. -static CachedImage *image_cache_ensure(GhosttyKittyGraphics graphics, - uint32_t image_id) -{ - GhosttyKittyGraphicsImage image = ghostty_kitty_graphics_image(graphics, image_id); - if (!image) - return NULL; - - uint32_t img_w = 0, img_h = 0; - ghostty_kitty_graphics_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_WIDTH, &img_w); - ghostty_kitty_graphics_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_HEIGHT, &img_h); - if (img_w == 0 || img_h == 0) - return NULL; - - // If already cached with the same dimensions, reuse it. - CachedImage *cached = image_cache_find(image_id); - if (cached && cached->src_width == img_w && cached->src_height == img_h) - return cached; - - // Need to upload. The image data should be RGBA after the PNG - // decoder callback runs. If it's in another format we skip it - // for simplicity — the PNG decoder we registered already converts - // everything to RGBA. - GhosttyKittyImageFormat fmt = GHOSTTY_KITTY_IMAGE_FORMAT_RGBA; - ghostty_kitty_graphics_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_FORMAT, &fmt); - if (fmt != GHOSTTY_KITTY_IMAGE_FORMAT_RGBA) - return NULL; - - const uint8_t *data_ptr = NULL; - size_t data_len = 0; - ghostty_kitty_graphics_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_DATA_PTR, &data_ptr); - ghostty_kitty_graphics_image_get(image, GHOSTTY_KITTY_IMAGE_DATA_DATA_LEN, &data_len); - if (!data_ptr || data_len < (size_t)img_w * img_h * 4) - return NULL; - - return image_cache_put(image_id, data_ptr, img_w, img_h); + for (int i = 0; i < deferred_texture_count; i++) + UnloadTexture(deferred_textures[i]); + deferred_texture_count = 0; } // Draw all Kitty graphics placements for a given z-layer. @@ -753,8 +655,11 @@ static CachedImage *image_cache_ensure(GhosttyKittyGraphics graphics, // 0 = below text (INT32_MIN/2 <= z < 0) // 1 = above text (z >= 0) // -// The terminal and placement iterator must have been set up by the -// caller. The iterator is consumed (advanced to the end). +// WARNING: This is deliberately simple but very inefficient. Every +// visible image is re-uploaded to the GPU every frame and destroyed +// right after. A real implementation should cache Texture2D objects +// keyed by image ID and only re-upload when the image is re-transmitted +// or evicted from the terminal's storage. static void render_kitty_images(GhosttyTerminal terminal, GhosttyKittyGraphics graphics, GhosttyKittyGraphicsPlacementIterator placement_iter, @@ -788,9 +693,8 @@ static void render_kitty_images(GhosttyTerminal terminal, const int32_t bg_limit = INT32_MIN / 2; while (ghostty_kitty_graphics_placement_next(placement_iter)) { - // Check whether this placement is virtual (unicode placeholder). - // We skip virtual placements for now — they require scanning cells - // for placeholder characters, which is a more complex path. + // Skip virtual placements (unicode placeholders) — they require + // scanning cells for placeholder characters. bool is_virtual = false; ghostty_kitty_graphics_placement_get(placement_iter, GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IS_VIRTUAL, &is_virtual); @@ -802,55 +706,70 @@ static void render_kitty_images(GhosttyTerminal terminal, ghostty_kitty_graphics_placement_get(placement_iter, GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_Z, &z); - bool dominated = false; + bool skip = false; switch (z_layer) { - case -1: dominated = (z >= bg_limit); break; // below bg - case 0: dominated = (z < bg_limit || z >= 0); break; // below text - case 1: dominated = (z < 0); break; // above text - default: dominated = true; break; + case -1: skip = (z >= bg_limit); break; + case 0: skip = (z < bg_limit || z >= 0); break; + case 1: skip = (z < 0); break; + default: skip = true; break; } - if (dominated) + if (skip) continue; - // Get the image ID for this placement and ensure it's cached. + // Look up the image for this placement. uint32_t image_id = 0; ghostty_kitty_graphics_placement_get(placement_iter, GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IMAGE_ID, &image_id); - CachedImage *cached = image_cache_ensure(graphics, image_id); - if (!cached) - continue; - GhosttyKittyGraphicsImage image_handle = ghostty_kitty_graphics_image(graphics, image_id); if (!image_handle) continue; + // Read image dimensions and pixel data. We only handle RGBA + // (the PNG decoder we registered converts everything to RGBA). + uint32_t img_w = 0, img_h = 0; + ghostty_kitty_graphics_image_get(image_handle, + GHOSTTY_KITTY_IMAGE_DATA_WIDTH, &img_w); + ghostty_kitty_graphics_image_get(image_handle, + GHOSTTY_KITTY_IMAGE_DATA_HEIGHT, &img_h); + if (img_w == 0 || img_h == 0) + continue; + + GhosttyKittyImageFormat fmt = GHOSTTY_KITTY_IMAGE_FORMAT_RGBA; + ghostty_kitty_graphics_image_get(image_handle, + GHOSTTY_KITTY_IMAGE_DATA_FORMAT, &fmt); + if (fmt != GHOSTTY_KITTY_IMAGE_FORMAT_RGBA) + continue; + + const uint8_t *data_ptr = NULL; + size_t data_len = 0; + ghostty_kitty_graphics_image_get(image_handle, + GHOSTTY_KITTY_IMAGE_DATA_DATA_PTR, &data_ptr); + ghostty_kitty_graphics_image_get(image_handle, + GHOSTTY_KITTY_IMAGE_DATA_DATA_LEN, &data_len); + if (!data_ptr || data_len < (size_t)img_w * img_h * 4) + continue; + // Get the placement's bounding rectangle as grid refs (pins). GhosttySelection sel = GHOSTTY_INIT_SIZED(GhosttySelection); if (ghostty_kitty_graphics_placement_rect( placement_iter, image_handle, terminal, &sel) != GHOSTTY_SUCCESS) continue; - // Convert the placement's top-left pin to screen coordinates, - // then compute viewport-relative position by subtracting the - // viewport origin. Using screen coordinates (absolute row from - // top of scrollback) lets us handle placements whose top-left - // has scrolled above the viewport — they'll get a negative Y - // which correctly draws the visible bottom portion. + // Convert the placement's top-left to screen coordinates, then + // subtract the viewport origin to get viewport-relative position. + // A negative Y means the top of the image is above the viewport. GhosttyPointCoordinate img_screen = {0}; if (ghostty_terminal_point_from_grid_ref( terminal, &sel.start, GHOSTTY_POINT_TAG_SCREEN, &img_screen) != GHOSTTY_SUCCESS) continue; - // Viewport-relative row (signed — negative means the top of - // the image is above the visible area). int32_t vp_row = (int32_t)img_screen.y - (int32_t)vp_screen.y; int32_t vp_col = (int32_t)img_screen.x; - // Compute the rendered size from the grid cell count so the - // result is in logical (screen-space) pixels. + // Compute grid cell count for rendered size. uint32_t grid_cols = 0, grid_rows = 0; if (ghostty_kitty_graphics_placement_grid_size( placement_iter, image_handle, terminal, @@ -859,7 +778,7 @@ static void render_kitty_images(GhosttyTerminal terminal, if (grid_cols == 0 || grid_rows == 0) continue; - // Skip placements that are entirely outside the viewport. + // Skip placements entirely outside the viewport. if (vp_row + (int32_t)grid_rows <= 0 || vp_row >= (int32_t)term_rows) continue; @@ -878,14 +797,14 @@ static void render_kitty_images(GhosttyTerminal terminal, GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_HEIGHT, &src_h); // 0 means "full image dimension" per the Kitty protocol. - if (src_w == 0) src_w = cached->src_width; - if (src_h == 0) src_h = cached->src_height; + if (src_w == 0) src_w = img_w; + if (src_h == 0) src_h = img_h; // Clamp source rect to the actual image bounds. - if (src_x > cached->src_width) src_x = cached->src_width; - if (src_y > cached->src_height) src_y = cached->src_height; - if (src_x + src_w > cached->src_width) src_w = cached->src_width - src_x; - if (src_y + src_h > cached->src_height) src_h = cached->src_height - src_y; + if (src_x > img_w) src_x = img_w; + if (src_y > img_h) src_y = img_h; + if (src_x + src_w > img_w) src_w = img_w - src_x; + if (src_y + src_h > img_h) src_h = img_h - src_y; // Read the sub-cell pixel offsets. uint32_t x_offset = 0, y_offset = 0; @@ -894,12 +813,20 @@ static void render_kitty_images(GhosttyTerminal terminal, ghostty_kitty_graphics_placement_get(placement_iter, GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_Y_OFFSET, &y_offset); - // Convert viewport-relative grid position to screen pixels. + // Upload the RGBA data to a temporary texture, draw, and free. + Image img = { + .data = (void *)(uintptr_t)data_ptr, + .width = (int)img_w, + .height = (int)img_h, + .mipmaps = 1, + .format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8, + }; + Texture2D tex = LoadTextureFromImage(img); + SetTextureFilter(tex, TEXTURE_FILTER_BILINEAR); + int dest_x = pad + (int)vp_col * cell_width + (int)x_offset; int dest_y = pad + (int)vp_row * cell_height + (int)y_offset; - // Draw the image using Raylib's DrawTexturePro, which handles - // source-rect → dest-rect mapping with bilinear filtering. Rectangle src_rect = { (float)src_x, (float)src_y, (float)src_w, (float)src_h @@ -908,24 +835,10 @@ static void render_kitty_images(GhosttyTerminal terminal, (float)dest_x, (float)dest_y, (float)dest_w, (float)dest_h }; - DrawTexturePro(cached->texture, src_rect, dst_rect, + DrawTexturePro(tex, src_rect, dst_rect, (Vector2){0, 0}, 0.0f, WHITE); - } -} -// Evict cached textures for images that no longer exist in the -// terminal's Kitty graphics storage. -static void image_cache_gc(GhosttyKittyGraphics graphics) -{ - for (int i = 0; i < image_cache_count; ) { - GhosttyKittyGraphicsImage img = - ghostty_kitty_graphics_image(graphics, image_cache[i].image_id); - if (!img) { - UnloadTexture(image_cache[i].texture); - image_cache[i] = image_cache[--image_cache_count]; - } else { - i++; - } + defer_unload_texture(tex); } } @@ -972,10 +885,6 @@ static void render_terminal(GhosttyRenderState render_state, GHOSTTY_TERMINAL_DATA_KITTY_GRAPHICS, &kitty_gfx) == GHOSTTY_SUCCESS && kitty_gfx != NULL); - // Garbage-collect cached textures for images the terminal has deleted. - if (has_kitty) - image_cache_gc(kitty_gfx); - // Populate the row iterator from the current render state snapshot. if (ghostty_render_state_get(render_state, GHOSTTY_RENDER_STATE_DATA_ROW_ITERATOR, &row_iter) != GHOSTTY_SUCCESS) @@ -1730,10 +1639,14 @@ int main(void) } EndDrawing(); + + // Free any textures that were uploaded during this frame's + // kitty image rendering. Safe now that EndDrawing() has + // flushed all draw commands to the GPU. + flush_deferred_textures(); } cleanup: - image_cache_clear(); UnloadFont(mono_font); CloseWindow(); if (pty_fd >= 0) From cdd7a218d756b177e0bc3ba5d3ea71ee06feda0d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Apr 2026 12:10:36 -0700 Subject: [PATCH 5/7] update to merged --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7f8f8bf..daf60f5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,7 +30,7 @@ endif() # required Zig version. FetchContent_Declare(ghostty GIT_REPOSITORY https://github.com/ghostty-org/ghostty.git - GIT_TAG 6b94c2da26653cc8feeaee3ef90166b3ad1e3aee + GIT_TAG d712beff5b616f1f886937c6de8e8105b9f3956e ) FetchContent_MakeAvailable(ghostty) From de925018049f2bcc0f561b7c614c77e2778472a9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Apr 2026 12:43:17 -0700 Subject: [PATCH 6/7] simplify Kitty rendering with new libghostty placement APIs Use the new libghostty APIs from ghostty-org/ghostty#12147 to replace manual logic in render_kitty_images: The iterator layer filter (placement_iterator_set with a GhosttyKittyPlacementLayer) replaces the manual z-index read and switch/case filtering. The placement_viewport_pos call replaces a 3-step viewport origin lookup (grid_ref, point_from_grid_ref, manual subtraction) plus the off-screen and virtual-placement checks. The placement_source_rect call replaces four individual source field reads plus the "0 = full dimension" expansion and bounds clamping. This removes ~70 lines and drops the term_rows parameter from both render_kitty_images and render_terminal. --- main.c | 133 ++++++++++++++------------------------------------------- 1 file changed, 33 insertions(+), 100 deletions(-) diff --git a/main.c b/main.c index d84b42e..b50b7ae 100644 --- a/main.c +++ b/main.c @@ -650,10 +650,9 @@ static void flush_deferred_textures(void) // Draw all Kitty graphics placements for a given z-layer. // -// z_layer selects which placements to draw: -// -1 = below background (z < INT32_MIN/2) -// 0 = below text (INT32_MIN/2 <= z < 0) -// 1 = above text (z >= 0) +// The layer filter is applied by the iterator itself via +// ghostty_kitty_graphics_placement_iterator_set(), so we only see +// placements matching the requested layer. // // WARNING: This is deliberately simple but very inefficient. Every // visible image is re-uploaded to the GPU every frame and destroyed @@ -664,58 +663,20 @@ static void render_kitty_images(GhosttyTerminal terminal, GhosttyKittyGraphics graphics, GhosttyKittyGraphicsPlacementIterator placement_iter, int cell_width, int cell_height, int pad, - int z_layer, uint16_t term_rows) + GhosttyKittyPlacementLayer layer) { - // Re-populate the iterator for each layer so we can scan all - // placements independently. + // Configure the layer filter on the iterator so + // placement_next() only yields matching placements. + ghostty_kitty_graphics_placement_iterator_set(placement_iter, + GHOSTTY_KITTY_GRAPHICS_PLACEMENT_ITERATOR_OPTION_LAYER, &layer); + + // Re-populate the iterator for this layer scan. if (ghostty_kitty_graphics_get(graphics, GHOSTTY_KITTY_GRAPHICS_DATA_PLACEMENT_ITERATOR, &placement_iter) != GHOSTTY_SUCCESS) return; - // Get the viewport's top-left in screen coordinates so we can - // compute viewport-relative positions for each placement. Screen - // coordinates are absolute (row 0 = top of scrollback) so they - // work regardless of which page node a pin lives in. - GhosttyGridRef vp_origin_ref = GHOSTTY_INIT_SIZED(GhosttyGridRef); - GhosttyPoint vp_origin_pt = { - .tag = GHOSTTY_POINT_TAG_VIEWPORT, - .value = { .coordinate = { .x = 0, .y = 0 } }, - }; - if (ghostty_terminal_grid_ref(terminal, vp_origin_pt, - &vp_origin_ref) != GHOSTTY_SUCCESS) - return; - GhosttyPointCoordinate vp_screen = {0}; - if (ghostty_terminal_point_from_grid_ref(terminal, &vp_origin_ref, - GHOSTTY_POINT_TAG_SCREEN, &vp_screen) != GHOSTTY_SUCCESS) - return; - - const int32_t bg_limit = INT32_MIN / 2; - while (ghostty_kitty_graphics_placement_next(placement_iter)) { - // Skip virtual placements (unicode placeholders) — they require - // scanning cells for placeholder characters. - bool is_virtual = false; - ghostty_kitty_graphics_placement_get(placement_iter, - GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IS_VIRTUAL, &is_virtual); - if (is_virtual) - continue; - - // Read the z-index and filter by the requested layer. - int32_t z = 0; - ghostty_kitty_graphics_placement_get(placement_iter, - GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_Z, &z); - - bool skip = false; - switch (z_layer) { - case -1: skip = (z >= bg_limit); break; - case 0: skip = (z < bg_limit || z >= 0); break; - case 1: skip = (z < 0); break; - default: skip = true; break; - } - if (skip) - continue; - // Look up the image for this placement. uint32_t image_id = 0; ghostty_kitty_graphics_placement_get(placement_iter, @@ -726,6 +687,15 @@ static void render_kitty_images(GhosttyTerminal terminal, if (!image_handle) continue; + // Get viewport-relative position. Returns NO_VALUE when the + // placement is entirely off-screen or is a virtual (unicode + // placeholder) placement, so both cases are handled in one call. + int32_t vp_col = 0, vp_row = 0; + if (ghostty_kitty_graphics_placement_viewport_pos( + placement_iter, image_handle, terminal, + &vp_col, &vp_row) != GHOSTTY_SUCCESS) + continue; + // Read image dimensions and pixel data. We only handle RGBA // (the PNG decoder we registered converts everything to RGBA). uint32_t img_w = 0, img_h = 0; @@ -751,24 +721,6 @@ static void render_kitty_images(GhosttyTerminal terminal, if (!data_ptr || data_len < (size_t)img_w * img_h * 4) continue; - // Get the placement's bounding rectangle as grid refs (pins). - GhosttySelection sel = GHOSTTY_INIT_SIZED(GhosttySelection); - if (ghostty_kitty_graphics_placement_rect( - placement_iter, image_handle, terminal, &sel) != GHOSTTY_SUCCESS) - continue; - - // Convert the placement's top-left to screen coordinates, then - // subtract the viewport origin to get viewport-relative position. - // A negative Y means the top of the image is above the viewport. - GhosttyPointCoordinate img_screen = {0}; - if (ghostty_terminal_point_from_grid_ref( - terminal, &sel.start, GHOSTTY_POINT_TAG_SCREEN, - &img_screen) != GHOSTTY_SUCCESS) - continue; - - int32_t vp_row = (int32_t)img_screen.y - (int32_t)vp_screen.y; - int32_t vp_col = (int32_t)img_screen.x; - // Compute grid cell count for rendered size. uint32_t grid_cols = 0, grid_rows = 0; if (ghostty_kitty_graphics_placement_grid_size( @@ -778,33 +730,16 @@ static void render_kitty_images(GhosttyTerminal terminal, if (grid_cols == 0 || grid_rows == 0) continue; - // Skip placements entirely outside the viewport. - if (vp_row + (int32_t)grid_rows <= 0 || vp_row >= (int32_t)term_rows) - continue; - uint32_t dest_w = grid_cols * (uint32_t)cell_width; uint32_t dest_h = grid_rows * (uint32_t)cell_height; - // Read the source rectangle from the placement. + // Get the resolved source rectangle (handles "0 = full image" + // semantics and clamps to image bounds). uint32_t src_x = 0, src_y = 0, src_w = 0, src_h = 0; - ghostty_kitty_graphics_placement_get(placement_iter, - GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_X, &src_x); - ghostty_kitty_graphics_placement_get(placement_iter, - GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_Y, &src_y); - ghostty_kitty_graphics_placement_get(placement_iter, - GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_WIDTH, &src_w); - ghostty_kitty_graphics_placement_get(placement_iter, - GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_HEIGHT, &src_h); - - // 0 means "full image dimension" per the Kitty protocol. - if (src_w == 0) src_w = img_w; - if (src_h == 0) src_h = img_h; - - // Clamp source rect to the actual image bounds. - if (src_x > img_w) src_x = img_w; - if (src_y > img_h) src_y = img_h; - if (src_x + src_w > img_w) src_w = img_w - src_x; - if (src_y + src_h > img_h) src_h = img_h - src_y; + if (ghostty_kitty_graphics_placement_source_rect( + placement_iter, image_handle, + &src_x, &src_y, &src_w, &src_h) != GHOSTTY_SUCCESS) + continue; // Read the sub-cell pixel offsets. uint32_t x_offset = 0, y_offset = 0; @@ -869,8 +804,7 @@ static void render_terminal(GhosttyRenderState render_state, int pad, const GhosttyTerminalScrollbar *scrollbar, GhosttyTerminal terminal, - GhosttyKittyGraphicsPlacementIterator placement_iter, - uint16_t term_rows) + GhosttyKittyGraphicsPlacementIterator placement_iter) { // Grab colors (palette, default fg/bg) from the render state so we // can resolve palette-indexed cell colors. @@ -893,8 +827,8 @@ static void render_terminal(GhosttyRenderState render_state, // --- Layer 1: images below cell backgrounds (z < INT32_MIN/2) --- if (has_kitty && placement_iter) { render_kitty_images(terminal, kitty_gfx, placement_iter, - cell_width, cell_height, pad, /*z_layer=*/-1, - term_rows); + cell_width, cell_height, pad, + GHOSTTY_KITTY_PLACEMENT_LAYER_BELOW_BG); } // Small padding from the window edges. @@ -1014,8 +948,8 @@ static void render_terminal(GhosttyRenderState render_state, // visual for the common case where images sit behind text. if (has_kitty && placement_iter) { render_kitty_images(terminal, kitty_gfx, placement_iter, - cell_width, cell_height, pad, /*z_layer=*/0, - term_rows); + cell_width, cell_height, pad, + GHOSTTY_KITTY_PLACEMENT_LAYER_BELOW_TEXT); } // Draw the cursor. @@ -1046,8 +980,8 @@ static void render_terminal(GhosttyRenderState render_state, // --- Layer 3: images above text (z >= 0) --- if (has_kitty && placement_iter) { render_kitty_images(terminal, kitty_gfx, placement_iter, - cell_width, cell_height, pad, /*z_layer=*/1, - term_rows); + cell_width, cell_height, pad, + GHOSTTY_KITTY_PLACEMENT_LAYER_ABOVE_TEXT); } // Draw the scrollbar when there is scrollback content to scroll through. @@ -1613,8 +1547,7 @@ int main(void) ClearBackground(win_bg); render_terminal(render_state, row_iter, row_cells, mono_font, cell_width, cell_height, font_size, pad, - scrollbar_ptr, terminal, placement_iter, - term_rows); + scrollbar_ptr, terminal, placement_iter); // Show a banner when the child process has exited so the user // knows the shell is gone (they can still scroll / inspect output). From bfbc40b2fcf67048ae9e63886d4173170e012042 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Apr 2026 13:03:01 -0700 Subject: [PATCH 7/7] update to merged --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index daf60f5..7882fb1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,7 +30,7 @@ endif() # required Zig version. FetchContent_Declare(ghostty GIT_REPOSITORY https://github.com/ghostty-org/ghostty.git - GIT_TAG d712beff5b616f1f886937c6de8e8105b9f3956e + GIT_TAG fdb6e3d2c8543e2e756b7e07f44372efbc0fba4b ) FetchContent_MakeAvailable(ghostty)