From 39468748fd945256b3ab460e915235a7d126e2d4 Mon Sep 17 00:00:00 2001 From: Gregor Lohaus Date: Thu, 28 May 2026 13:19:27 +0200 Subject: [PATCH] kitty placeholder mapping --- .../dev/jlibghostty/KittyPlaceholder.java | 17 +++ src/main/java/dev/jlibghostty/RenderCell.java | 12 ++ .../jlibghostty/internal/GhosttyLibrary.java | 135 +++++++++++++++++- .../dev/jlibghostty/GhosttySmokeTest.java | 31 ++++ 4 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 src/main/java/dev/jlibghostty/KittyPlaceholder.java diff --git a/src/main/java/dev/jlibghostty/KittyPlaceholder.java b/src/main/java/dev/jlibghostty/KittyPlaceholder.java new file mode 100644 index 0000000..ded788d --- /dev/null +++ b/src/main/java/dev/jlibghostty/KittyPlaceholder.java @@ -0,0 +1,17 @@ +package dev.jlibghostty; + +/** + * Decoded Kitty graphics unicode placeholder metadata for a rendered cell. + */ +public record KittyPlaceholder( + long imageId, + long placementId, + long sourceRow, + long sourceColumn +) { + public static final int CODEPOINT = 0x10EEEE; + + public boolean hasPlacementId() { + return placementId != 0; + } +} diff --git a/src/main/java/dev/jlibghostty/RenderCell.java b/src/main/java/dev/jlibghostty/RenderCell.java index 64683a1..c65c2c7 100644 --- a/src/main/java/dev/jlibghostty/RenderCell.java +++ b/src/main/java/dev/jlibghostty/RenderCell.java @@ -7,12 +7,24 @@ public record RenderCell( int[] codepoints, Optional foreground, Optional background, + Optional kittyPlaceholder, boolean selected ) { + public RenderCell( + int column, + int[] codepoints, + Optional foreground, + Optional background, + boolean selected + ) { + this(column, codepoints, foreground, background, Optional.empty(), selected); + } + public RenderCell { codepoints = codepoints.clone(); foreground = foreground == null ? Optional.empty() : foreground; background = background == null ? Optional.empty() : background; + kittyPlaceholder = kittyPlaceholder == null ? Optional.empty() : kittyPlaceholder; } @Override diff --git a/src/main/java/dev/jlibghostty/internal/GhosttyLibrary.java b/src/main/java/dev/jlibghostty/internal/GhosttyLibrary.java index 96070e6..9bea9be 100644 --- a/src/main/java/dev/jlibghostty/internal/GhosttyLibrary.java +++ b/src/main/java/dev/jlibghostty/internal/GhosttyLibrary.java @@ -4,6 +4,7 @@ import dev.jlibghostty.GhosttyBuildInfo; import dev.jlibghostty.GhosttyException; import dev.jlibghostty.DeviceAttributes; import dev.jlibghostty.DeviceAttributesProvider; +import dev.jlibghostty.KittyPlaceholder; import dev.jlibghostty.MouseEncoderSize; import dev.jlibghostty.MouseInput; import dev.jlibghostty.OptimizeMode; @@ -30,6 +31,7 @@ import java.lang.invoke.MethodType; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Optional; @@ -92,6 +94,7 @@ public final class GhosttyLibrary { public static final int RENDER_STATE_ROW_DATA_DIRTY = 1; public static final int RENDER_STATE_ROW_DATA_CELLS = 3; + public static final int RENDER_STATE_ROW_CELLS_DATA_STYLE = 2; public static final int RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_LEN = 3; public static final int RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_BUF = 4; public static final int RENDER_STATE_ROW_CELLS_DATA_BG_COLOR = 5; @@ -117,6 +120,49 @@ public final class GhosttyLibrary { private static final int BUILD_INFO_VERSION_PATCH = 8; private static final int BUILD_INFO_VERSION_PRE = 9; private static final int BUILD_INFO_VERSION_BUILD = 10; + private static final int STYLE_COLOR_TAG_PALETTE = 1; + private static final int STYLE_COLOR_TAG_RGB = 2; + + private static final int[] KITTY_PLACEHOLDER_DIACRITICS = { + 0x0305, 0x030D, 0x030E, 0x0310, 0x0312, 0x033D, 0x033E, 0x033F, + 0x0346, 0x034A, 0x034B, 0x034C, 0x0350, 0x0351, 0x0352, 0x0357, + 0x035B, 0x0363, 0x0364, 0x0365, 0x0366, 0x0367, 0x0368, 0x0369, + 0x036A, 0x036B, 0x036C, 0x036D, 0x036E, 0x036F, 0x0483, 0x0484, + 0x0485, 0x0486, 0x0487, 0x0592, 0x0593, 0x0594, 0x0595, 0x0597, + 0x0598, 0x0599, 0x059C, 0x059D, 0x059E, 0x059F, 0x05A0, 0x05A1, + 0x05A8, 0x05A9, 0x05AB, 0x05AC, 0x05AF, 0x05C4, 0x0610, 0x0611, + 0x0612, 0x0613, 0x0614, 0x0615, 0x0616, 0x0617, 0x0657, 0x0658, + 0x0659, 0x065A, 0x065B, 0x065D, 0x065E, 0x06D6, 0x06D7, 0x06D8, + 0x06D9, 0x06DA, 0x06DB, 0x06DC, 0x06DF, 0x06E0, 0x06E1, 0x06E2, + 0x06E4, 0x06E7, 0x06E8, 0x06EB, 0x06EC, 0x0730, 0x0732, 0x0733, + 0x0735, 0x0736, 0x073A, 0x073D, 0x073F, 0x0740, 0x0741, 0x0743, + 0x0745, 0x0747, 0x0749, 0x074A, 0x07EB, 0x07EC, 0x07ED, 0x07EE, + 0x07EF, 0x07F0, 0x07F1, 0x07F3, 0x0816, 0x0817, 0x0818, 0x0819, + 0x081B, 0x081C, 0x081D, 0x081E, 0x081F, 0x0820, 0x0821, 0x0822, + 0x0823, 0x0825, 0x0826, 0x0827, 0x0829, 0x082A, 0x082B, 0x082C, + 0x082D, 0x0951, 0x0953, 0x0954, 0x0F82, 0x0F83, 0x0F86, 0x0F87, + 0x135D, 0x135E, 0x135F, 0x17DD, 0x193A, 0x1A17, 0x1A75, 0x1A76, + 0x1A77, 0x1A78, 0x1A79, 0x1A7A, 0x1A7B, 0x1A7C, 0x1B6B, 0x1B6D, + 0x1B6E, 0x1B6F, 0x1B70, 0x1B71, 0x1B72, 0x1B73, 0x1CD0, 0x1CD1, + 0x1CD2, 0x1CDA, 0x1CDB, 0x1CE0, 0x1DC0, 0x1DC1, 0x1DC3, 0x1DC4, + 0x1DC5, 0x1DC6, 0x1DC7, 0x1DC8, 0x1DC9, 0x1DCB, 0x1DCC, 0x1DD1, + 0x1DD2, 0x1DD3, 0x1DD4, 0x1DD5, 0x1DD6, 0x1DD7, 0x1DD8, 0x1DD9, + 0x1DDA, 0x1DDB, 0x1DDC, 0x1DDD, 0x1DDE, 0x1DDF, 0x1DE0, 0x1DE1, + 0x1DE2, 0x1DE3, 0x1DE4, 0x1DE5, 0x1DE6, 0x1DFE, 0x20D0, 0x20D1, + 0x20D4, 0x20D5, 0x20D6, 0x20D7, 0x20DB, 0x20DC, 0x20E1, 0x20E7, + 0x20E9, 0x20F0, 0x2CEF, 0x2CF0, 0x2CF1, 0x2DE0, 0x2DE1, 0x2DE2, + 0x2DE3, 0x2DE4, 0x2DE5, 0x2DE6, 0x2DE7, 0x2DE8, 0x2DE9, 0x2DEA, + 0x2DEB, 0x2DEC, 0x2DED, 0x2DEE, 0x2DEF, 0x2DF0, 0x2DF1, 0x2DF2, + 0x2DF3, 0x2DF4, 0x2DF5, 0x2DF6, 0x2DF7, 0x2DF8, 0x2DF9, 0x2DFA, + 0x2DFB, 0x2DFC, 0x2DFD, 0x2DFE, 0x2DFF, 0xA66F, 0xA67C, 0xA67D, + 0xA6F0, 0xA6F1, 0xA8E0, 0xA8E1, 0xA8E2, 0xA8E3, 0xA8E4, 0xA8E5, + 0xA8E6, 0xA8E7, 0xA8E8, 0xA8E9, 0xA8EA, 0xA8EB, 0xA8EC, 0xA8ED, + 0xA8EE, 0xA8EF, 0xA8F0, 0xA8F1, 0xAAB0, 0xAAB2, 0xAAB3, 0xAAB7, + 0xAAB8, 0xAABE, 0xAABF, 0xAAC1, 0xFE20, 0xFE21, 0xFE22, 0xFE23, + 0xFE24, 0xFE25, 0xFE26, 0x10A0F, 0x10A38, 0x1D185, 0x1D186, + 0x1D187, 0x1D188, 0x1D189, 0x1D1AA, 0x1D1AB, 0x1D1AC, 0x1D1AD, + 0x1D242, 0x1D243, 0x1D244 + }; private static final Linker LINKER = Linker.nativeLinker(); private static final AddressLayout C_POINTER = (AddressLayout) LINKER.canonicalLayouts().get("void*"); @@ -153,6 +199,39 @@ public final class GhosttyLibrary { C_INT.withName("cell_height") ); + private static final GroupLayout STYLE_COLOR_VALUE = MemoryLayout.unionLayout( + JAVA_BYTE.withName("palette"), + MemoryLayout.structLayout( + JAVA_BYTE.withName("r"), + JAVA_BYTE.withName("g"), + JAVA_BYTE.withName("b") + ).withName("rgb"), + C_LONG_LONG.withName("padding") + ); + + private static final GroupLayout STYLE_COLOR = MemoryLayout.structLayout( + C_INT.withName("tag"), + MemoryLayout.paddingLayout(4), + STYLE_COLOR_VALUE.withName("value") + ); + + private static final GroupLayout STYLE = MemoryLayout.structLayout( + C_SIZE_T.withName("size"), + STYLE_COLOR.withName("fg_color"), + STYLE_COLOR.withName("bg_color"), + STYLE_COLOR.withName("underline_color"), + C_BOOL.withName("bold"), + C_BOOL.withName("italic"), + C_BOOL.withName("faint"), + C_BOOL.withName("blink"), + C_BOOL.withName("inverse"), + C_BOOL.withName("invisible"), + C_BOOL.withName("strikethrough"), + C_BOOL.withName("overline"), + C_INT.withName("underline"), + MemoryLayout.paddingLayout(4) + ); + private static final GroupLayout SCROLL_VIEWPORT = MemoryLayout.structLayout( C_INT.withName("tag"), MemoryLayout.paddingLayout(4), @@ -853,11 +932,13 @@ public final class GhosttyLibrary { List result = new ArrayList<>(); int column = 0; while (renderStateRowCellsNext(cells)) { + int[] codepoints = renderStateRowCellGraphemes(cells); result.add(new RenderCell( column, - renderStateRowCellGraphemes(cells), + codepoints, renderStateRowCellColor(cells, RENDER_STATE_ROW_CELLS_DATA_FG_COLOR), renderStateRowCellColor(cells, RENDER_STATE_ROW_CELLS_DATA_BG_COLOR), + renderStateRowCellKittyPlaceholder(cells, codepoints), renderStateRowCellsGetBoolean(cells, RENDER_STATE_ROW_CELLS_DATA_SELECTED) )); column++; @@ -1095,6 +1176,58 @@ public final class GhosttyLibrary { } } + private Optional renderStateRowCellKittyPlaceholder(MemorySegment cells, int[] codepoints) { + if (codepoints.length == 0 || codepoints[0] != KittyPlaceholder.CODEPOINT) { + return Optional.empty(); + } + + try (Arena arena = Arena.ofConfined()) { + MemorySegment style = arena.allocate(STYLE); + style.set(C_SIZE_T, 0, STYLE.byteSize()); + int result = (int) renderStateRowCellsGet.invoke(cells, RENDER_STATE_ROW_CELLS_DATA_STYLE, style); + checkResult("ghostty_render_state_row_cells_get", result); + + long imageIdLow = styleColorToKittyId(style, 8); + long placementId = styleColorToKittyId(style, 40); + long sourceRow = kittyPlaceholderDiacriticIndex(codepoints, 1); + long sourceColumn = kittyPlaceholderDiacriticIndex(codepoints, 2); + long imageIdHigh = kittyPlaceholderDiacriticIndex(codepoints, 3); + if (imageIdHigh > 255) { + imageIdHigh = 0; + } + + return Optional.of(new KittyPlaceholder( + imageIdLow | (imageIdHigh << 24), + placementId, + sourceRow, + sourceColumn + )); + } catch (Throwable t) { + return rethrow(t); + } + } + + private static long styleColorToKittyId(MemorySegment style, long colorOffset) { + int tag = style.get(C_INT, colorOffset); + long valueOffset = colorOffset + 8; + return switch (tag) { + case STYLE_COLOR_TAG_PALETTE -> Byte.toUnsignedLong(style.get(JAVA_BYTE, valueOffset)); + case STYLE_COLOR_TAG_RGB -> (long) Byte.toUnsignedInt(style.get(JAVA_BYTE, valueOffset)) << 16 + | (long) Byte.toUnsignedInt(style.get(JAVA_BYTE, valueOffset + 1)) << 8 + | Byte.toUnsignedLong(style.get(JAVA_BYTE, valueOffset + 2)); + default -> 0; + }; + } + + private static long kittyPlaceholderDiacriticIndex(int[] codepoints, int index) { + if (codepoints.length <= index) { + return 0; + } + + int result = Arrays.binarySearch(KITTY_PLACEHOLDER_DIACRITICS, codepoints[index]); + return result >= 0 ? result : 0; + } + public void kittyGraphicsPopulatePlacementIterator(MemorySegment graphics, MemorySegment iterator) { try (Arena arena = Arena.ofConfined()) { MemorySegment out = arena.allocate(C_POINTER); diff --git a/src/test/java/dev/jlibghostty/GhosttySmokeTest.java b/src/test/java/dev/jlibghostty/GhosttySmokeTest.java index e4fe1eb..609d996 100644 --- a/src/test/java/dev/jlibghostty/GhosttySmokeTest.java +++ b/src/test/java/dev/jlibghostty/GhosttySmokeTest.java @@ -2,6 +2,7 @@ package dev.jlibghostty; import java.io.ByteArrayOutputStream; import java.nio.charset.StandardCharsets; +import java.util.Optional; public final class GhosttySmokeTest { private GhosttySmokeTest() { @@ -62,6 +63,22 @@ public final class GhosttySmokeTest { if (!terminal.text().contains("hello")) { throw new AssertionError("formatted terminal text should contain written text: " + terminal.text()); } + terminal.write(kittyPlaceholderFixture()); + Optional placeholder = terminal.renderSnapshot().renderRows().stream() + .flatMap(row -> row.cells().stream()) + .map(RenderCell::kittyPlaceholder) + .flatMap(Optional::stream) + .findFirst(); + if (placeholder.isEmpty()) { + throw new AssertionError("expected decoded Kitty unicode placeholder"); + } + KittyPlaceholder kittyPlaceholder = placeholder.orElseThrow(); + if (kittyPlaceholder.imageId() != 0x010203 + || kittyPlaceholder.placementId() != 0x040506 + || kittyPlaceholder.sourceRow() != 1 + || kittyPlaceholder.sourceColumn() != 2) { + throw new AssertionError("unexpected Kitty placeholder metadata: " + kittyPlaceholder); + } terminal.write(scrollbackFixture()); terminal.scrollViewport(ScrollViewport.delta(-10)); String scrolledText = renderText(terminal.renderSnapshot()); @@ -125,4 +142,18 @@ public final class GhosttySmokeTest { } return text.toString(); } + + private static String kittyPlaceholderFixture() { + return "\u001b[2027h" + + "\u001b[38;2;1;2;3m" + + "\u001b[58;2;4;5;6m" + + codepoint(KittyPlaceholder.CODEPOINT) + + codepoint(0x030D) + + codepoint(0x030E) + + "\u001b[0m\r\n"; + } + + private static String codepoint(int codepoint) { + return new String(Character.toChars(codepoint)); + } }