From 5285fb68c91d43fccc558d3f3085de6380e94371 Mon Sep 17 00:00:00 2001 From: Gregor Lohaus Date: Wed, 27 May 2026 14:33:41 +0200 Subject: [PATCH] kitty graphics support --- README.md | 25 ++ .../java/dev/jlibghostty/KittyGraphics.java | 63 ++++ src/main/java/dev/jlibghostty/KittyImage.java | 31 ++ .../jlibghostty/KittyImageCompression.java | 25 ++ .../dev/jlibghostty/KittyImageFormat.java | 28 ++ .../dev/jlibghostty/KittyImageSnapshot.java | 20 ++ .../java/dev/jlibghostty/KittyPlacement.java | 21 ++ .../jlibghostty/KittyPlacementIterator.java | 56 ++++ .../dev/jlibghostty/KittyPlacementLayer.java | 18 ++ .../java/dev/jlibghostty/KittyRenderInfo.java | 16 ++ src/main/java/dev/jlibghostty/Terminal.java | 33 +++ .../jlibghostty/internal/GhosttyLibrary.java | 271 ++++++++++++++++++ .../jlibghostty/reachability-metadata.json | 37 +++ .../dev/jlibghostty/GhosttySmokeTest.java | 7 + 14 files changed, 651 insertions(+) create mode 100644 src/main/java/dev/jlibghostty/KittyGraphics.java create mode 100644 src/main/java/dev/jlibghostty/KittyImage.java create mode 100644 src/main/java/dev/jlibghostty/KittyImageCompression.java create mode 100644 src/main/java/dev/jlibghostty/KittyImageFormat.java create mode 100644 src/main/java/dev/jlibghostty/KittyImageSnapshot.java create mode 100644 src/main/java/dev/jlibghostty/KittyPlacement.java create mode 100644 src/main/java/dev/jlibghostty/KittyPlacementIterator.java create mode 100644 src/main/java/dev/jlibghostty/KittyPlacementLayer.java create mode 100644 src/main/java/dev/jlibghostty/KittyRenderInfo.java diff --git a/README.md b/README.md index fba5415..4a21a8d 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,31 @@ try (Terminal terminal = Ghostty.open(TerminalOptions.of(80, 24))) { } ``` +## Kitty Graphics + +Kitty graphics storage can be enabled and inspected: + +```java +try (Terminal terminal = Ghostty.open(TerminalOptions.of(80, 24))) { + terminal.setKittyImageStorageLimit(64 * 1024 * 1024); + terminal.setKittyImageMediumFile(true); + terminal.setKittyImageMediumTemporaryFile(true); + terminal.setKittyImageMediumSharedMemory(true); + + terminal.write(kittyGraphicsSequenceBytes); + + for (KittyPlacement placement : terminal.kittyGraphics().orElseThrow().placements()) { + placement.image().ifPresent(image -> { + // Hand image.data() and placement.renderInfo() to your renderer. + }); + } +} +``` + +The Kitty handles returned by `libghostty-vt` are borrowed from the terminal and are invalidated by mutating terminal calls. The Java API returns snapshots for images and placements to make renderer handoff simpler. + +PNG decode callbacks from `ghostty_sys_set(GHOSTTY_SYS_OPT_DECODE_PNG, ...)` are not exposed yet. Raw Kitty image formats can be inspected; PNG image ingestion will need a Java callback bridge or a small native helper that allocates decoded RGBA data through Ghostty's allocator. + ## Development Shell ```sh diff --git a/src/main/java/dev/jlibghostty/KittyGraphics.java b/src/main/java/dev/jlibghostty/KittyGraphics.java new file mode 100644 index 0000000..ec99535 --- /dev/null +++ b/src/main/java/dev/jlibghostty/KittyGraphics.java @@ -0,0 +1,63 @@ +package dev.jlibghostty; + +import dev.jlibghostty.internal.GhosttyLibrary; + +import java.lang.foreign.MemorySegment; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public final class KittyGraphics { + private final GhosttyLibrary library; + private final MemorySegment terminal; + private final MemorySegment graphics; + + KittyGraphics(GhosttyLibrary library, MemorySegment terminal, MemorySegment graphics) { + this.library = library; + this.terminal = terminal; + this.graphics = graphics; + } + + public Optional image(long imageId) { + MemorySegment image = library.kittyGraphicsImage(graphics, imageId); + if (image.address() == 0) { + return Optional.empty(); + } + return Optional.of(new KittyImage(library, image)); + } + + public List placements() { + return placements(KittyPlacementLayer.ALL); + } + + public List placements(KittyPlacementLayer layer) { + try (KittyPlacementIterator iterator = KittyPlacementIterator.open(library, graphics, layer)) { + List placements = new ArrayList<>(); + while (iterator.next()) { + long imageId = iterator.getU32(GhosttyLibrary.KITTY_GRAPHICS_PLACEMENT_DATA_IMAGE_ID); + Optional image = image(imageId); + KittyRenderInfo renderInfo = image + .flatMap(value -> library.kittyPlacementRenderInfo(iterator.handle(), value.handle(), terminal)) + .orElse(null); + + placements.add(new KittyPlacement( + imageId, + iterator.getU32(GhosttyLibrary.KITTY_GRAPHICS_PLACEMENT_DATA_PLACEMENT_ID), + iterator.getBoolean(GhosttyLibrary.KITTY_GRAPHICS_PLACEMENT_DATA_IS_VIRTUAL), + iterator.getU32(GhosttyLibrary.KITTY_GRAPHICS_PLACEMENT_DATA_X_OFFSET), + iterator.getU32(GhosttyLibrary.KITTY_GRAPHICS_PLACEMENT_DATA_Y_OFFSET), + iterator.getU32(GhosttyLibrary.KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_X), + iterator.getU32(GhosttyLibrary.KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_Y), + iterator.getU32(GhosttyLibrary.KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_WIDTH), + iterator.getU32(GhosttyLibrary.KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_HEIGHT), + iterator.getU32(GhosttyLibrary.KITTY_GRAPHICS_PLACEMENT_DATA_COLUMNS), + iterator.getU32(GhosttyLibrary.KITTY_GRAPHICS_PLACEMENT_DATA_ROWS), + iterator.getI32(GhosttyLibrary.KITTY_GRAPHICS_PLACEMENT_DATA_Z), + image.map(KittyImage::snapshot), + Optional.ofNullable(renderInfo) + )); + } + return List.copyOf(placements); + } + } +} diff --git a/src/main/java/dev/jlibghostty/KittyImage.java b/src/main/java/dev/jlibghostty/KittyImage.java new file mode 100644 index 0000000..da2192a --- /dev/null +++ b/src/main/java/dev/jlibghostty/KittyImage.java @@ -0,0 +1,31 @@ +package dev.jlibghostty; + +import dev.jlibghostty.internal.GhosttyLibrary; + +import java.lang.foreign.MemorySegment; + +public final class KittyImage { + private final GhosttyLibrary library; + private final MemorySegment handle; + + KittyImage(GhosttyLibrary library, MemorySegment handle) { + this.library = library; + this.handle = handle; + } + + public KittyImageSnapshot snapshot() { + return new KittyImageSnapshot( + library.kittyImageGetU32(handle, GhosttyLibrary.KITTY_IMAGE_DATA_ID), + library.kittyImageGetU32(handle, GhosttyLibrary.KITTY_IMAGE_DATA_NUMBER), + library.kittyImageGetU32(handle, GhosttyLibrary.KITTY_IMAGE_DATA_WIDTH), + library.kittyImageGetU32(handle, GhosttyLibrary.KITTY_IMAGE_DATA_HEIGHT), + KittyImageFormat.fromNative(library.kittyImageGetI32(handle, GhosttyLibrary.KITTY_IMAGE_DATA_FORMAT)), + KittyImageCompression.fromNative(library.kittyImageGetI32(handle, GhosttyLibrary.KITTY_IMAGE_DATA_COMPRESSION)), + library.kittyImageData(handle) + ); + } + + MemorySegment handle() { + return handle; + } +} diff --git a/src/main/java/dev/jlibghostty/KittyImageCompression.java b/src/main/java/dev/jlibghostty/KittyImageCompression.java new file mode 100644 index 0000000..b9b74cb --- /dev/null +++ b/src/main/java/dev/jlibghostty/KittyImageCompression.java @@ -0,0 +1,25 @@ +package dev.jlibghostty; + +public enum KittyImageCompression { + NONE(0), + ZLIB_DEFLATE(1); + + private final int nativeValue; + + KittyImageCompression(int nativeValue) { + this.nativeValue = nativeValue; + } + + public int nativeValue() { + return nativeValue; + } + + static KittyImageCompression fromNative(int value) { + for (KittyImageCompression compression : values()) { + if (compression.nativeValue == value) { + return compression; + } + } + throw new IllegalArgumentException("Unknown Kitty image compression: " + value); + } +} diff --git a/src/main/java/dev/jlibghostty/KittyImageFormat.java b/src/main/java/dev/jlibghostty/KittyImageFormat.java new file mode 100644 index 0000000..6fa3c29 --- /dev/null +++ b/src/main/java/dev/jlibghostty/KittyImageFormat.java @@ -0,0 +1,28 @@ +package dev.jlibghostty; + +public enum KittyImageFormat { + RGB(0), + RGBA(1), + PNG(2), + GRAY_ALPHA(3), + GRAY(4); + + private final int nativeValue; + + KittyImageFormat(int nativeValue) { + this.nativeValue = nativeValue; + } + + public int nativeValue() { + return nativeValue; + } + + static KittyImageFormat fromNative(int value) { + for (KittyImageFormat format : values()) { + if (format.nativeValue == value) { + return format; + } + } + throw new IllegalArgumentException("Unknown Kitty image format: " + value); + } +} diff --git a/src/main/java/dev/jlibghostty/KittyImageSnapshot.java b/src/main/java/dev/jlibghostty/KittyImageSnapshot.java new file mode 100644 index 0000000..79fd144 --- /dev/null +++ b/src/main/java/dev/jlibghostty/KittyImageSnapshot.java @@ -0,0 +1,20 @@ +package dev.jlibghostty; + +public record KittyImageSnapshot( + long id, + long number, + long width, + long height, + KittyImageFormat format, + KittyImageCompression compression, + byte[] data +) { + public KittyImageSnapshot { + data = data.clone(); + } + + @Override + public byte[] data() { + return data.clone(); + } +} diff --git a/src/main/java/dev/jlibghostty/KittyPlacement.java b/src/main/java/dev/jlibghostty/KittyPlacement.java new file mode 100644 index 0000000..708e344 --- /dev/null +++ b/src/main/java/dev/jlibghostty/KittyPlacement.java @@ -0,0 +1,21 @@ +package dev.jlibghostty; + +import java.util.Optional; + +public record KittyPlacement( + long imageId, + long placementId, + boolean virtual, + long xOffset, + long yOffset, + long sourceX, + long sourceY, + long sourceWidth, + long sourceHeight, + long columns, + long rows, + int z, + Optional image, + Optional renderInfo +) { +} diff --git a/src/main/java/dev/jlibghostty/KittyPlacementIterator.java b/src/main/java/dev/jlibghostty/KittyPlacementIterator.java new file mode 100644 index 0000000..fba9433 --- /dev/null +++ b/src/main/java/dev/jlibghostty/KittyPlacementIterator.java @@ -0,0 +1,56 @@ +package dev.jlibghostty; + +import dev.jlibghostty.internal.GhosttyLibrary; + +import java.lang.foreign.MemorySegment; + +final class KittyPlacementIterator implements AutoCloseable { + private final GhosttyLibrary library; + private final MemorySegment handle; + private boolean closed; + + private KittyPlacementIterator(GhosttyLibrary library, MemorySegment handle) { + this.library = library; + this.handle = handle; + } + + static KittyPlacementIterator open(GhosttyLibrary library, MemorySegment graphics, KittyPlacementLayer layer) { + MemorySegment handle = library.kittyPlacementIteratorNew(); + try { + library.kittyGraphicsPopulatePlacementIterator(graphics, handle); + library.kittyPlacementIteratorSetLayer(handle, layer.nativeValue()); + return new KittyPlacementIterator(library, handle); + } catch (RuntimeException | Error e) { + library.kittyPlacementIteratorFree(handle); + throw e; + } + } + + boolean next() { + return library.kittyPlacementNext(handle); + } + + long getU32(int key) { + return library.kittyPlacementGetU32(handle, key); + } + + int getI32(int key) { + return library.kittyPlacementGetI32(handle, key); + } + + boolean getBoolean(int key) { + return library.kittyPlacementGetBoolean(handle, key); + } + + MemorySegment handle() { + return handle; + } + + @Override + public void close() { + if (!closed) { + closed = true; + library.kittyPlacementIteratorFree(handle); + } + } +} diff --git a/src/main/java/dev/jlibghostty/KittyPlacementLayer.java b/src/main/java/dev/jlibghostty/KittyPlacementLayer.java new file mode 100644 index 0000000..f231cfd --- /dev/null +++ b/src/main/java/dev/jlibghostty/KittyPlacementLayer.java @@ -0,0 +1,18 @@ +package dev.jlibghostty; + +public enum KittyPlacementLayer { + ALL(0), + BELOW_BACKGROUND(1), + BELOW_TEXT(2), + ABOVE_TEXT(3); + + private final int nativeValue; + + KittyPlacementLayer(int nativeValue) { + this.nativeValue = nativeValue; + } + + int nativeValue() { + return nativeValue; + } +} diff --git a/src/main/java/dev/jlibghostty/KittyRenderInfo.java b/src/main/java/dev/jlibghostty/KittyRenderInfo.java new file mode 100644 index 0000000..c0733ab --- /dev/null +++ b/src/main/java/dev/jlibghostty/KittyRenderInfo.java @@ -0,0 +1,16 @@ +package dev.jlibghostty; + +public record KittyRenderInfo( + long pixelWidth, + long pixelHeight, + long gridColumns, + long gridRows, + int viewportColumn, + int viewportRow, + boolean viewportVisible, + long sourceX, + long sourceY, + long sourceWidth, + long sourceHeight +) { +} diff --git a/src/main/java/dev/jlibghostty/Terminal.java b/src/main/java/dev/jlibghostty/Terminal.java index 5c8387b..1f4f590 100644 --- a/src/main/java/dev/jlibghostty/Terminal.java +++ b/src/main/java/dev/jlibghostty/Terminal.java @@ -4,6 +4,7 @@ import dev.jlibghostty.internal.GhosttyLibrary; import java.lang.foreign.MemorySegment; import java.nio.charset.StandardCharsets; +import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; public final class Terminal implements AutoCloseable { @@ -40,6 +41,38 @@ public final class Terminal implements AutoCloseable { library.terminalResize(handle, columns, rows, cellWidthPx, cellHeightPx); } + public void setKittyImageStorageLimit(long bytes) { + ensureOpen(); + library.terminalSetU64(handle, GhosttyLibrary.TERMINAL_OPT_KITTY_IMAGE_STORAGE_LIMIT, bytes); + } + + public void setKittyImageMediumFile(boolean enabled) { + ensureOpen(); + library.terminalSetBoolean(handle, GhosttyLibrary.TERMINAL_OPT_KITTY_IMAGE_MEDIUM_FILE, enabled); + } + + public void setKittyImageMediumTemporaryFile(boolean enabled) { + ensureOpen(); + library.terminalSetBoolean(handle, GhosttyLibrary.TERMINAL_OPT_KITTY_IMAGE_MEDIUM_TEMP_FILE, enabled); + } + + public void setKittyImageMediumSharedMemory(boolean enabled) { + ensureOpen(); + library.terminalSetBoolean(handle, GhosttyLibrary.TERMINAL_OPT_KITTY_IMAGE_MEDIUM_SHARED_MEM, enabled); + } + + public Optional kittyGraphics() { + ensureOpen(); + MemorySegment graphics = library.terminalGetPointerOrNull( + handle, + GhosttyLibrary.TERMINAL_DATA_KITTY_GRAPHICS + ); + if (graphics.address() == 0) { + return Optional.empty(); + } + return Optional.of(new KittyGraphics(library, handle, graphics)); + } + public TerminalSnapshot snapshot() { ensureOpen(); return new TerminalSnapshot( diff --git a/src/main/java/dev/jlibghostty/internal/GhosttyLibrary.java b/src/main/java/dev/jlibghostty/internal/GhosttyLibrary.java index e3d2fbd..fe4bd24 100644 --- a/src/main/java/dev/jlibghostty/internal/GhosttyLibrary.java +++ b/src/main/java/dev/jlibghostty/internal/GhosttyLibrary.java @@ -26,6 +26,36 @@ public final class GhosttyLibrary { public static final int TERMINAL_DATA_CURSOR_VISIBLE = 7; public static final int TERMINAL_DATA_TITLE = 12; public static final int TERMINAL_DATA_PWD = 13; + public static final int TERMINAL_DATA_KITTY_GRAPHICS = 30; + + public static final int TERMINAL_OPT_KITTY_IMAGE_STORAGE_LIMIT = 15; + public static final int TERMINAL_OPT_KITTY_IMAGE_MEDIUM_FILE = 16; + public static final int TERMINAL_OPT_KITTY_IMAGE_MEDIUM_TEMP_FILE = 17; + public static final int TERMINAL_OPT_KITTY_IMAGE_MEDIUM_SHARED_MEM = 18; + + public static final int KITTY_GRAPHICS_DATA_PLACEMENT_ITERATOR = 1; + + public static final int KITTY_GRAPHICS_PLACEMENT_DATA_IMAGE_ID = 1; + public static final int KITTY_GRAPHICS_PLACEMENT_DATA_PLACEMENT_ID = 2; + public static final int KITTY_GRAPHICS_PLACEMENT_DATA_IS_VIRTUAL = 3; + public static final int KITTY_GRAPHICS_PLACEMENT_DATA_X_OFFSET = 4; + public static final int KITTY_GRAPHICS_PLACEMENT_DATA_Y_OFFSET = 5; + public static final int KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_X = 6; + public static final int KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_Y = 7; + public static final int KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_WIDTH = 8; + public static final int KITTY_GRAPHICS_PLACEMENT_DATA_SOURCE_HEIGHT = 9; + public static final int KITTY_GRAPHICS_PLACEMENT_DATA_COLUMNS = 10; + public static final int KITTY_GRAPHICS_PLACEMENT_DATA_ROWS = 11; + public static final int KITTY_GRAPHICS_PLACEMENT_DATA_Z = 12; + + public static final int KITTY_IMAGE_DATA_ID = 1; + public static final int KITTY_IMAGE_DATA_NUMBER = 2; + public static final int KITTY_IMAGE_DATA_WIDTH = 3; + public static final int KITTY_IMAGE_DATA_HEIGHT = 4; + public static final int KITTY_IMAGE_DATA_FORMAT = 5; + public static final int KITTY_IMAGE_DATA_COMPRESSION = 6; + public static final int KITTY_IMAGE_DATA_DATA_PTR = 7; + public static final int KITTY_IMAGE_DATA_DATA_LEN = 8; private static final int GHOSTTY_SUCCESS = 0; private static final int GHOSTTY_OUT_OF_SPACE = -3; @@ -36,6 +66,7 @@ public final class GhosttyLibrary { private static final ValueLayout.OfBoolean C_BOOL = (ValueLayout.OfBoolean) LINKER.canonicalLayouts().get("bool"); private static final ValueLayout.OfShort C_SHORT = (ValueLayout.OfShort) LINKER.canonicalLayouts().get("short"); private static final ValueLayout.OfInt C_INT = (ValueLayout.OfInt) LINKER.canonicalLayouts().get("int"); + private static final ValueLayout.OfLong C_LONG_LONG = (ValueLayout.OfLong) LINKER.canonicalLayouts().get("long long"); private static final ValueLayout.OfLong C_SIZE_T = sizeTLayout(); private static final GroupLayout TERMINAL_OPTIONS = MemoryLayout.structLayout( @@ -50,14 +81,41 @@ public final class GhosttyLibrary { C_SIZE_T.withName("len") ); + private static final GroupLayout KITTY_RENDER_INFO = MemoryLayout.structLayout( + C_SIZE_T.withName("size"), + C_INT.withName("pixel_width"), + C_INT.withName("pixel_height"), + C_INT.withName("grid_cols"), + C_INT.withName("grid_rows"), + C_INT.withName("viewport_col"), + C_INT.withName("viewport_row"), + C_BOOL.withName("viewport_visible"), + MemoryLayout.paddingLayout(3), + C_INT.withName("source_x"), + C_INT.withName("source_y"), + C_INT.withName("source_width"), + C_INT.withName("source_height"), + MemoryLayout.paddingLayout(4) + ); + private final MethodHandle terminalNew; private final MethodHandle terminalFree; private final MethodHandle terminalReset; private final MethodHandle terminalResize; private final MethodHandle terminalVtWrite; + private final MethodHandle terminalSet; private final MethodHandle terminalGet; private final MethodHandle pasteIsSafe; private final MethodHandle pasteEncode; + private final MethodHandle kittyGraphicsGet; + private final MethodHandle kittyGraphicsImage; + private final MethodHandle kittyGraphicsImageGet; + private final MethodHandle kittyGraphicsPlacementIteratorNew; + private final MethodHandle kittyGraphicsPlacementIteratorFree; + private final MethodHandle kittyGraphicsPlacementIteratorSet; + private final MethodHandle kittyGraphicsPlacementNext; + private final MethodHandle kittyGraphicsPlacementGet; + private final MethodHandle kittyGraphicsPlacementRenderInfo; private GhosttyLibrary(Path libraryPath) { try { @@ -73,12 +131,32 @@ public final class GhosttyLibrary { FunctionDescriptor.of(C_INT, C_POINTER, C_SHORT, C_SHORT, C_INT, C_INT)); terminalVtWrite = downcall(symbols, "ghostty_terminal_vt_write", FunctionDescriptor.ofVoid(C_POINTER, C_POINTER, C_SIZE_T)); + terminalSet = downcall(symbols, "ghostty_terminal_set", + FunctionDescriptor.of(C_INT, C_POINTER, C_INT, C_POINTER)); terminalGet = downcall(symbols, "ghostty_terminal_get", FunctionDescriptor.of(C_INT, C_POINTER, C_INT, C_POINTER)); pasteIsSafe = downcall(symbols, "ghostty_paste_is_safe", FunctionDescriptor.of(C_BOOL, C_POINTER, C_SIZE_T)); pasteEncode = downcall(symbols, "ghostty_paste_encode", FunctionDescriptor.of(C_INT, C_POINTER, C_SIZE_T, C_BOOL, C_POINTER, C_SIZE_T, C_POINTER)); + kittyGraphicsGet = downcall(symbols, "ghostty_kitty_graphics_get", + FunctionDescriptor.of(C_INT, C_POINTER, C_INT, C_POINTER)); + kittyGraphicsImage = downcall(symbols, "ghostty_kitty_graphics_image", + FunctionDescriptor.of(C_POINTER, C_POINTER, C_INT)); + kittyGraphicsImageGet = downcall(symbols, "ghostty_kitty_graphics_image_get", + FunctionDescriptor.of(C_INT, C_POINTER, C_INT, C_POINTER)); + kittyGraphicsPlacementIteratorNew = downcall(symbols, "ghostty_kitty_graphics_placement_iterator_new", + FunctionDescriptor.of(C_INT, C_POINTER, C_POINTER)); + kittyGraphicsPlacementIteratorFree = downcall(symbols, "ghostty_kitty_graphics_placement_iterator_free", + FunctionDescriptor.ofVoid(C_POINTER)); + kittyGraphicsPlacementIteratorSet = downcall(symbols, "ghostty_kitty_graphics_placement_iterator_set", + FunctionDescriptor.of(C_INT, C_POINTER, C_INT, C_POINTER)); + kittyGraphicsPlacementNext = downcall(symbols, "ghostty_kitty_graphics_placement_next", + FunctionDescriptor.of(C_BOOL, C_POINTER)); + kittyGraphicsPlacementGet = downcall(symbols, "ghostty_kitty_graphics_placement_get", + FunctionDescriptor.of(C_INT, C_POINTER, C_INT, C_POINTER)); + kittyGraphicsPlacementRenderInfo = downcall(symbols, "ghostty_kitty_graphics_placement_render_info", + FunctionDescriptor.of(C_INT, C_POINTER, C_POINTER, C_POINTER, C_POINTER)); } catch (IllegalCallerException e) { throw new IllegalStateException( "FFM native access is disabled. Run with --enable-native-access=dev.jlibghostty " @@ -159,6 +237,28 @@ public final class GhosttyLibrary { } } + public void terminalSetU64(MemorySegment terminal, int key, long value) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment nativeValue = arena.allocate(C_LONG_LONG); + nativeValue.set(C_LONG_LONG, 0, value); + int result = (int) terminalSet.invoke(terminal, key, nativeValue); + checkResult("ghostty_terminal_set", result); + } catch (Throwable t) { + rethrow(t); + } + } + + public void terminalSetBoolean(MemorySegment terminal, int key, boolean value) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment nativeValue = arena.allocate(C_BOOL); + nativeValue.set(C_BOOL, 0, value); + int result = (int) terminalSet.invoke(terminal, key, nativeValue); + checkResult("ghostty_terminal_set", result); + } catch (Throwable t) { + rethrow(t); + } + } + public int terminalGetU16(MemorySegment terminal, int key) { try (Arena arena = Arena.ofConfined()) { MemorySegment out = arena.allocate(C_SHORT); @@ -203,6 +303,20 @@ public final class GhosttyLibrary { } } + public MemorySegment terminalGetPointerOrNull(MemorySegment terminal, int key) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment out = arena.allocate(C_POINTER); + int result = (int) terminalGet.invoke(terminal, key, out); + if (result == GHOSTTY_NO_VALUE) { + return MemorySegment.NULL; + } + checkResult("ghostty_terminal_get", result); + return out.get(C_POINTER, 0); + } catch (Throwable t) { + return rethrow(t); + } + } + public boolean pasteIsSafe(byte[] data) { if (data.length == 0) { return true; @@ -261,6 +375,163 @@ public final class GhosttyLibrary { } } + public void kittyGraphicsPopulatePlacementIterator(MemorySegment graphics, MemorySegment iterator) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment out = arena.allocate(C_POINTER); + out.set(C_POINTER, 0, iterator); + int result = (int) kittyGraphicsGet.invoke(graphics, KITTY_GRAPHICS_DATA_PLACEMENT_ITERATOR, out); + checkResult("ghostty_kitty_graphics_get", result); + } catch (Throwable t) { + rethrow(t); + } + } + + public MemorySegment kittyGraphicsImage(MemorySegment graphics, long imageId) { + try { + return (MemorySegment) kittyGraphicsImage.invoke(graphics, (int) imageId); + } catch (Throwable t) { + return rethrow(t); + } + } + + public long kittyImageGetU32(MemorySegment image, int key) { + return Integer.toUnsignedLong(kittyImageGetI32(image, key)); + } + + public int kittyImageGetI32(MemorySegment image, int key) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment out = arena.allocate(C_INT); + int result = (int) kittyGraphicsImageGet.invoke(image, key, out); + checkResult("ghostty_kitty_graphics_image_get", result); + return out.get(C_INT, 0); + } catch (Throwable t) { + return rethrow(t); + } + } + + public byte[] kittyImageData(MemorySegment image) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment outPtr = arena.allocate(C_POINTER); + MemorySegment outLen = arena.allocate(C_SIZE_T); + + int result = (int) kittyGraphicsImageGet.invoke(image, KITTY_IMAGE_DATA_DATA_PTR, outPtr); + checkResult("ghostty_kitty_graphics_image_get", result); + result = (int) kittyGraphicsImageGet.invoke(image, KITTY_IMAGE_DATA_DATA_LEN, outLen); + checkResult("ghostty_kitty_graphics_image_get", result); + + MemorySegment ptr = outPtr.get(C_POINTER, 0); + long len = outLen.get(C_SIZE_T, 0); + if (ptr.address() == 0 || len == 0) { + return new byte[0]; + } + return ptr.reinterpret(len).toArray(JAVA_BYTE); + } catch (Throwable t) { + return rethrow(t); + } + } + + public MemorySegment kittyPlacementIteratorNew() { + try (Arena arena = Arena.ofConfined()) { + MemorySegment out = arena.allocate(C_POINTER); + int result = (int) kittyGraphicsPlacementIteratorNew.invoke(MemorySegment.NULL, out); + checkResult("ghostty_kitty_graphics_placement_iterator_new", result); + + MemorySegment iterator = out.get(C_POINTER, 0); + if (iterator.address() == 0) { + throw new IllegalStateException("ghostty_kitty_graphics_placement_iterator_new returned null"); + } + return iterator; + } catch (Throwable t) { + return rethrow(t); + } + } + + public void kittyPlacementIteratorFree(MemorySegment iterator) { + try { + kittyGraphicsPlacementIteratorFree.invoke(iterator); + } catch (Throwable t) { + rethrow(t); + } + } + + public void kittyPlacementIteratorSetLayer(MemorySegment iterator, int layer) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment nativeLayer = arena.allocate(C_INT); + nativeLayer.set(C_INT, 0, layer); + int result = (int) kittyGraphicsPlacementIteratorSet.invoke(iterator, 0, nativeLayer); + checkResult("ghostty_kitty_graphics_placement_iterator_set", result); + } catch (Throwable t) { + rethrow(t); + } + } + + public boolean kittyPlacementNext(MemorySegment iterator) { + try { + return (boolean) kittyGraphicsPlacementNext.invoke(iterator); + } catch (Throwable t) { + return rethrow(t); + } + } + + public long kittyPlacementGetU32(MemorySegment iterator, int key) { + return Integer.toUnsignedLong(kittyPlacementGetI32(iterator, key)); + } + + public int kittyPlacementGetI32(MemorySegment iterator, int key) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment out = arena.allocate(C_INT); + int result = (int) kittyGraphicsPlacementGet.invoke(iterator, key, out); + checkResult("ghostty_kitty_graphics_placement_get", result); + return out.get(C_INT, 0); + } catch (Throwable t) { + return rethrow(t); + } + } + + public boolean kittyPlacementGetBoolean(MemorySegment iterator, int key) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment out = arena.allocate(C_BOOL); + int result = (int) kittyGraphicsPlacementGet.invoke(iterator, key, out); + checkResult("ghostty_kitty_graphics_placement_get", result); + return out.get(C_BOOL, 0); + } catch (Throwable t) { + return rethrow(t); + } + } + + public java.util.Optional kittyPlacementRenderInfo( + MemorySegment iterator, + MemorySegment image, + MemorySegment terminal + ) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment out = arena.allocate(KITTY_RENDER_INFO); + out.set(C_SIZE_T, 0, KITTY_RENDER_INFO.byteSize()); + + int result = (int) kittyGraphicsPlacementRenderInfo.invoke(iterator, image, terminal, out); + if (result == GHOSTTY_NO_VALUE) { + return java.util.Optional.empty(); + } + checkResult("ghostty_kitty_graphics_placement_render_info", result); + + return java.util.Optional.of(new dev.jlibghostty.KittyRenderInfo( + Integer.toUnsignedLong(out.get(C_INT, 8)), + Integer.toUnsignedLong(out.get(C_INT, 12)), + Integer.toUnsignedLong(out.get(C_INT, 16)), + Integer.toUnsignedLong(out.get(C_INT, 20)), + out.get(C_INT, 24), + out.get(C_INT, 28), + out.get(C_BOOL, 32), + Integer.toUnsignedLong(out.get(C_INT, 36)), + Integer.toUnsignedLong(out.get(C_INT, 40)), + Integer.toUnsignedLong(out.get(C_INT, 44)), + Integer.toUnsignedLong(out.get(C_INT, 48)) + )); + } catch (Throwable t) { + return rethrow(t); + } + } + private static MethodHandle downcall(SymbolLookup symbols, String name, FunctionDescriptor descriptor) { MemorySegment symbol = symbols.find(name) .orElseThrow(() -> new UnsatisfiedLinkError("Missing libghostty-vt symbol: " + name)); diff --git a/src/main/resources/META-INF/native-image/dev.jlibghostty/jlibghostty/reachability-metadata.json b/src/main/resources/META-INF/native-image/dev.jlibghostty/jlibghostty/reachability-metadata.json index 7d096be..c02d604 100644 --- a/src/main/resources/META-INF/native-image/dev.jlibghostty/jlibghostty/reachability-metadata.json +++ b/src/main/resources/META-INF/native-image/dev.jlibghostty/jlibghostty/reachability-metadata.json @@ -46,6 +46,14 @@ "void*" ] }, + { + "returnType": "int", + "parameterTypes": [ + "void*", + "int", + "void*" + ] + }, { "returnType": "bool", "parameterTypes": [ @@ -63,6 +71,35 @@ "size_t", "void*" ] + }, + { + "returnType": "void*", + "parameterTypes": [ + "void*", + "int" + ] + }, + { + "returnType": "int", + "parameterTypes": [ + "void*", + "void*" + ] + }, + { + "returnType": "bool", + "parameterTypes": [ + "void*" + ] + }, + { + "returnType": "int", + "parameterTypes": [ + "void*", + "void*", + "void*", + "void*" + ] } ] } diff --git a/src/test/java/dev/jlibghostty/GhosttySmokeTest.java b/src/test/java/dev/jlibghostty/GhosttySmokeTest.java index 3ccc847..32798aa 100644 --- a/src/test/java/dev/jlibghostty/GhosttySmokeTest.java +++ b/src/test/java/dev/jlibghostty/GhosttySmokeTest.java @@ -16,11 +16,18 @@ public final class GhosttySmokeTest { } try (Terminal terminal = Ghostty.open(TerminalOptions.of(80, 24))) { + terminal.setKittyImageStorageLimit(1024 * 1024); + terminal.setKittyImageMediumFile(true); terminal.write("hello\r\n"); TerminalSnapshot snapshot = terminal.snapshot(); if (snapshot.columns() != 80 || snapshot.rows() != 24) { throw new AssertionError("unexpected terminal size: " + snapshot); } + KittyGraphics graphics = terminal.kittyGraphics() + .orElseThrow(() -> new AssertionError("expected kitty graphics handle")); + if (graphics.placements().size() != 1) { + throw new AssertionError("expected one kitty placement"); + } } } }