kitty placeholder mapping

This commit is contained in:
Gregor Lohaus
2026-05-28 13:19:27 +02:00
parent acdda73c02
commit 39468748fd
4 changed files with 194 additions and 1 deletions

View File

@@ -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;
}
}

View File

@@ -7,12 +7,24 @@ public record RenderCell(
int[] codepoints, int[] codepoints,
Optional<RenderColor> foreground, Optional<RenderColor> foreground,
Optional<RenderColor> background, Optional<RenderColor> background,
Optional<KittyPlaceholder> kittyPlaceholder,
boolean selected boolean selected
) { ) {
public RenderCell(
int column,
int[] codepoints,
Optional<RenderColor> foreground,
Optional<RenderColor> background,
boolean selected
) {
this(column, codepoints, foreground, background, Optional.empty(), selected);
}
public RenderCell { public RenderCell {
codepoints = codepoints.clone(); codepoints = codepoints.clone();
foreground = foreground == null ? Optional.empty() : foreground; foreground = foreground == null ? Optional.empty() : foreground;
background = background == null ? Optional.empty() : background; background = background == null ? Optional.empty() : background;
kittyPlaceholder = kittyPlaceholder == null ? Optional.empty() : kittyPlaceholder;
} }
@Override @Override

View File

@@ -4,6 +4,7 @@ import dev.jlibghostty.GhosttyBuildInfo;
import dev.jlibghostty.GhosttyException; import dev.jlibghostty.GhosttyException;
import dev.jlibghostty.DeviceAttributes; import dev.jlibghostty.DeviceAttributes;
import dev.jlibghostty.DeviceAttributesProvider; import dev.jlibghostty.DeviceAttributesProvider;
import dev.jlibghostty.KittyPlaceholder;
import dev.jlibghostty.MouseEncoderSize; import dev.jlibghostty.MouseEncoderSize;
import dev.jlibghostty.MouseInput; import dev.jlibghostty.MouseInput;
import dev.jlibghostty.OptimizeMode; import dev.jlibghostty.OptimizeMode;
@@ -30,6 +31,7 @@ import java.lang.invoke.MethodType;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Optional; 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_DIRTY = 1;
public static final int RENDER_STATE_ROW_DATA_CELLS = 3; 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_LEN = 3;
public static final int RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_BUF = 4; public static final int RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_BUF = 4;
public static final int RENDER_STATE_ROW_CELLS_DATA_BG_COLOR = 5; 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_PATCH = 8;
private static final int BUILD_INFO_VERSION_PRE = 9; private static final int BUILD_INFO_VERSION_PRE = 9;
private static final int BUILD_INFO_VERSION_BUILD = 10; 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 Linker LINKER = Linker.nativeLinker();
private static final AddressLayout C_POINTER = (AddressLayout) LINKER.canonicalLayouts().get("void*"); private static final AddressLayout C_POINTER = (AddressLayout) LINKER.canonicalLayouts().get("void*");
@@ -153,6 +199,39 @@ public final class GhosttyLibrary {
C_INT.withName("cell_height") 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( private static final GroupLayout SCROLL_VIEWPORT = MemoryLayout.structLayout(
C_INT.withName("tag"), C_INT.withName("tag"),
MemoryLayout.paddingLayout(4), MemoryLayout.paddingLayout(4),
@@ -853,11 +932,13 @@ public final class GhosttyLibrary {
List<RenderCell> result = new ArrayList<>(); List<RenderCell> result = new ArrayList<>();
int column = 0; int column = 0;
while (renderStateRowCellsNext(cells)) { while (renderStateRowCellsNext(cells)) {
int[] codepoints = renderStateRowCellGraphemes(cells);
result.add(new RenderCell( result.add(new RenderCell(
column, column,
renderStateRowCellGraphemes(cells), codepoints,
renderStateRowCellColor(cells, RENDER_STATE_ROW_CELLS_DATA_FG_COLOR), renderStateRowCellColor(cells, RENDER_STATE_ROW_CELLS_DATA_FG_COLOR),
renderStateRowCellColor(cells, RENDER_STATE_ROW_CELLS_DATA_BG_COLOR), renderStateRowCellColor(cells, RENDER_STATE_ROW_CELLS_DATA_BG_COLOR),
renderStateRowCellKittyPlaceholder(cells, codepoints),
renderStateRowCellsGetBoolean(cells, RENDER_STATE_ROW_CELLS_DATA_SELECTED) renderStateRowCellsGetBoolean(cells, RENDER_STATE_ROW_CELLS_DATA_SELECTED)
)); ));
column++; column++;
@@ -1095,6 +1176,58 @@ public final class GhosttyLibrary {
} }
} }
private Optional<KittyPlaceholder> 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) { public void kittyGraphicsPopulatePlacementIterator(MemorySegment graphics, MemorySegment iterator) {
try (Arena arena = Arena.ofConfined()) { try (Arena arena = Arena.ofConfined()) {
MemorySegment out = arena.allocate(C_POINTER); MemorySegment out = arena.allocate(C_POINTER);

View File

@@ -2,6 +2,7 @@ package dev.jlibghostty;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Optional;
public final class GhosttySmokeTest { public final class GhosttySmokeTest {
private GhosttySmokeTest() { private GhosttySmokeTest() {
@@ -62,6 +63,22 @@ public final class GhosttySmokeTest {
if (!terminal.text().contains("hello")) { if (!terminal.text().contains("hello")) {
throw new AssertionError("formatted terminal text should contain written text: " + terminal.text()); throw new AssertionError("formatted terminal text should contain written text: " + terminal.text());
} }
terminal.write(kittyPlaceholderFixture());
Optional<KittyPlaceholder> 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.write(scrollbackFixture());
terminal.scrollViewport(ScrollViewport.delta(-10)); terminal.scrollViewport(ScrollViewport.delta(-10));
String scrolledText = renderText(terminal.renderSnapshot()); String scrolledText = renderText(terminal.renderSnapshot());
@@ -125,4 +142,18 @@ public final class GhosttySmokeTest {
} }
return text.toString(); 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));
}
} }