kitty placeholder mapping
This commit is contained in:
17
src/main/java/dev/jlibghostty/KittyPlaceholder.java
Normal file
17
src/main/java/dev/jlibghostty/KittyPlaceholder.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user