everything

This commit is contained in:
Gregor Lohaus
2026-05-27 14:56:04 +02:00
parent ff5c4f4ff9
commit c077bcc6c0
12 changed files with 551 additions and 0 deletions

View File

@@ -145,6 +145,26 @@ try (Terminal terminal = Ghostty.open(TerminalOptions.of(80, 24))) {
}
```
## Full Native Surface
The public Java API covers the common terminal, paste, build-info, focus, mode-report, size-report, and Kitty graphics paths. For libghostty-vt APIs that do not yet have ergonomic Java wrappers, use `GhosttyNative`:
```java
MethodHandle handle = GhosttyNative.downcall(
"ghostty_render_state_new",
FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS)
);
```
`GhosttyNative.symbolNames()` lists every exported symbol found in the current public headers. Some entries are target-specific, such as WASM helpers, so use `GhosttyNative.findSymbol(name)` when probing optional symbols.
Build metadata and C struct layout JSON are exposed directly:
```java
GhosttyBuildInfo info = Ghostty.buildInfo();
String typeJson = Ghostty.typeJson();
```
## Kitty Graphics
Kitty graphics storage can be enabled and inspected:

View File

@@ -0,0 +1,16 @@
package dev.jlibghostty;
public enum FocusEvent {
GAINED(0),
LOST(1);
private final int nativeValue;
FocusEvent(int nativeValue) {
this.nativeValue = nativeValue;
}
int nativeValue() {
return nativeValue;
}
}

View File

@@ -12,6 +12,14 @@ public final class Ghostty {
return Terminal.open(options);
}
public static GhosttyBuildInfo buildInfo() {
return GhosttyLibrary.loadDefault().buildInfo();
}
public static String typeJson() {
return GhosttyLibrary.loadDefault().typeJson();
}
public static boolean pasteIsSafe(String text) {
return pasteIsSafe(text.getBytes(StandardCharsets.UTF_8));
}
@@ -28,4 +36,28 @@ public final class Ghostty {
public static byte[] encodePaste(byte[] data, boolean bracketed) {
return GhosttyLibrary.loadDefault().pasteEncode(data, bracketed);
}
public static String encodeFocus(FocusEvent event) {
return new String(encodeFocusBytes(event), StandardCharsets.UTF_8);
}
public static byte[] encodeFocusBytes(FocusEvent event) {
return GhosttyLibrary.loadDefault().focusEncode(event.nativeValue());
}
public static String encodeModeReport(int mode, ModeReportState state) {
return new String(encodeModeReportBytes(mode, state), StandardCharsets.UTF_8);
}
public static byte[] encodeModeReportBytes(int mode, ModeReportState state) {
return GhosttyLibrary.loadDefault().modeReportEncode(mode, state.nativeValue());
}
public static String encodeSizeReport(SizeReportStyle style, SizeReportSize size) {
return new String(encodeSizeReportBytes(style, size), StandardCharsets.UTF_8);
}
public static byte[] encodeSizeReportBytes(SizeReportStyle style, SizeReportSize size) {
return GhosttyLibrary.loadDefault().sizeReportEncode(style.nativeValue(), size);
}
}

View File

@@ -0,0 +1,15 @@
package dev.jlibghostty;
public record GhosttyBuildInfo(
boolean simd,
boolean kittyGraphics,
boolean tmuxControlMode,
OptimizeMode optimizeMode,
String version,
long versionMajor,
long versionMinor,
long versionPatch,
String versionPre,
String versionBuild
) {
}

View File

@@ -0,0 +1,165 @@
package dev.jlibghostty;
import dev.jlibghostty.internal.NativeLibraryLoader;
import java.lang.foreign.Arena;
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.Linker;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.SymbolLookup;
import java.lang.invoke.MethodHandle;
import java.util.List;
import java.util.Optional;
public final class GhosttyNative {
private static final SymbolLookup SYMBOLS = SymbolLookup.libraryLookup(
NativeLibraryLoader.resolve(),
Arena.global()
);
private GhosttyNative() {
}
public static List<String> symbolNames() {
return Symbols.ALL;
}
public static Optional<MemorySegment> findSymbol(String name) {
return SYMBOLS.find(name);
}
public static MemorySegment requireSymbol(String name) {
return findSymbol(name)
.orElseThrow(() -> new UnsatisfiedLinkError("Missing libghostty-vt symbol: " + name));
}
public static MethodHandle downcall(String name, FunctionDescriptor descriptor) {
return Linker.nativeLinker().downcallHandle(requireSymbol(name), descriptor);
}
private static final class Symbols {
private static final List<String> ALL = List.of(
"ghostty_alloc",
"ghostty_build_info",
"ghostty_cell_get",
"ghostty_cell_get_multi",
"ghostty_color_rgb_get",
"ghostty_focus_encode",
"ghostty_formatter_format_alloc",
"ghostty_formatter_format_buf",
"ghostty_formatter_free",
"ghostty_formatter_terminal_new",
"ghostty_free",
"ghostty_grid_ref_cell",
"ghostty_grid_ref_graphemes",
"ghostty_grid_ref_hyperlink_uri",
"ghostty_grid_ref_row",
"ghostty_grid_ref_style",
"ghostty_kitty_graphics_get",
"ghostty_kitty_graphics_image",
"ghostty_kitty_graphics_image_get",
"ghostty_kitty_graphics_image_get_multi",
"ghostty_kitty_graphics_placement_get",
"ghostty_kitty_graphics_placement_get_multi",
"ghostty_kitty_graphics_placement_grid_size",
"ghostty_kitty_graphics_placement_iterator_free",
"ghostty_kitty_graphics_placement_iterator_new",
"ghostty_kitty_graphics_placement_iterator_set",
"ghostty_kitty_graphics_placement_next",
"ghostty_kitty_graphics_placement_pixel_size",
"ghostty_kitty_graphics_placement_rect",
"ghostty_kitty_graphics_placement_render_info",
"ghostty_kitty_graphics_placement_source_rect",
"ghostty_kitty_graphics_placement_viewport_pos",
"ghostty_mode_report_encode",
"ghostty_osc_command_data",
"ghostty_osc_command_type",
"ghostty_osc_end",
"ghostty_osc_free",
"ghostty_osc_new",
"ghostty_osc_next",
"ghostty_osc_reset",
"ghostty_paste_encode",
"ghostty_paste_is_safe",
"ghostty_render_state_colors_get",
"ghostty_render_state_free",
"ghostty_render_state_get",
"ghostty_render_state_get_multi",
"ghostty_render_state_new",
"ghostty_render_state_row_cells_free",
"ghostty_render_state_row_cells_get",
"ghostty_render_state_row_cells_get_multi",
"ghostty_render_state_row_cells_new",
"ghostty_render_state_row_cells_next",
"ghostty_render_state_row_cells_select",
"ghostty_render_state_row_get",
"ghostty_render_state_row_get_multi",
"ghostty_render_state_row_iterator_free",
"ghostty_render_state_row_iterator_new",
"ghostty_render_state_row_iterator_next",
"ghostty_render_state_row_set",
"ghostty_render_state_set",
"ghostty_render_state_update",
"ghostty_row_get",
"ghostty_row_get_multi",
"ghostty_sgr_attribute_tag",
"ghostty_sgr_attribute_value",
"ghostty_sgr_free",
"ghostty_sgr_new",
"ghostty_sgr_next",
"ghostty_sgr_reset",
"ghostty_sgr_set_params",
"ghostty_sgr_unknown_full",
"ghostty_sgr_unknown_partial",
"ghostty_size_report_encode",
"ghostty_style_default",
"ghostty_style_is_default",
"ghostty_sys_log_stderr",
"ghostty_sys_set",
"ghostty_terminal_free",
"ghostty_terminal_get",
"ghostty_terminal_get_multi",
"ghostty_terminal_grid_ref",
"ghostty_terminal_grid_ref_track",
"ghostty_terminal_mode_get",
"ghostty_terminal_mode_set",
"ghostty_terminal_new",
"ghostty_terminal_point_from_grid_ref",
"ghostty_terminal_reset",
"ghostty_terminal_resize",
"ghostty_terminal_scroll_viewport",
"ghostty_terminal_select_all",
"ghostty_terminal_select_line",
"ghostty_terminal_select_output",
"ghostty_terminal_select_word",
"ghostty_terminal_select_word_between",
"ghostty_terminal_selection_adjust",
"ghostty_terminal_selection_contains",
"ghostty_terminal_selection_equal",
"ghostty_terminal_selection_format_alloc",
"ghostty_terminal_selection_format_buf",
"ghostty_terminal_selection_order",
"ghostty_terminal_selection_ordered",
"ghostty_terminal_set",
"ghostty_terminal_vt_write",
"ghostty_tracked_grid_ref_free",
"ghostty_tracked_grid_ref_has_value",
"ghostty_tracked_grid_ref_point",
"ghostty_tracked_grid_ref_set",
"ghostty_tracked_grid_ref_snapshot",
"ghostty_type_json",
"ghostty_wasm_alloc_opaque",
"ghostty_wasm_alloc_sgr_attribute",
"ghostty_wasm_alloc_u16_array",
"ghostty_wasm_alloc_u8",
"ghostty_wasm_alloc_u8_array",
"ghostty_wasm_alloc_usize",
"ghostty_wasm_free_opaque",
"ghostty_wasm_free_sgr_attribute",
"ghostty_wasm_free_u16_array",
"ghostty_wasm_free_u8",
"ghostty_wasm_free_u8_array",
"ghostty_wasm_free_usize"
);
}
}

View File

@@ -0,0 +1,19 @@
package dev.jlibghostty;
public enum ModeReportState {
NOT_RECOGNIZED(0),
SET(1),
RESET(2),
PERMANENTLY_SET(3),
PERMANENTLY_RESET(4);
private final int nativeValue;
ModeReportState(int nativeValue) {
this.nativeValue = nativeValue;
}
int nativeValue() {
return nativeValue;
}
}

View File

@@ -0,0 +1,23 @@
package dev.jlibghostty;
public enum OptimizeMode {
DEBUG(0),
RELEASE_SAFE(1),
RELEASE_SMALL(2),
RELEASE_FAST(3);
private final int nativeValue;
OptimizeMode(int nativeValue) {
this.nativeValue = nativeValue;
}
public static OptimizeMode fromNative(int value) {
for (OptimizeMode mode : values()) {
if (mode.nativeValue == value) {
return mode;
}
}
throw new IllegalArgumentException("Unknown Ghostty optimize mode: " + value);
}
}

View File

@@ -0,0 +1,18 @@
package dev.jlibghostty;
public record SizeReportSize(int rows, int columns, long cellWidth, long cellHeight) {
public SizeReportSize {
if (rows < 1 || rows > 65535) {
throw new IllegalArgumentException("rows must be between 1 and 65535");
}
if (columns < 1 || columns > 65535) {
throw new IllegalArgumentException("columns must be between 1 and 65535");
}
if (cellWidth < 0 || cellWidth > 0xffffffffL) {
throw new IllegalArgumentException("cellWidth must fit in uint32_t");
}
if (cellHeight < 0 || cellHeight > 0xffffffffL) {
throw new IllegalArgumentException("cellHeight must fit in uint32_t");
}
}
}

View File

@@ -0,0 +1,18 @@
package dev.jlibghostty;
public enum SizeReportStyle {
MODE_2048(0),
CSI_14_T(1),
CSI_16_T(2),
CSI_18_T(3);
private final int nativeValue;
SizeReportStyle(int nativeValue) {
this.nativeValue = nativeValue;
}
int nativeValue() {
return nativeValue;
}
}

View File

@@ -1,6 +1,9 @@
package dev.jlibghostty.internal;
import dev.jlibghostty.GhosttyBuildInfo;
import dev.jlibghostty.GhosttyException;
import dev.jlibghostty.OptimizeMode;
import dev.jlibghostty.SizeReportSize;
import dev.jlibghostty.TerminalOptions;
import java.lang.foreign.Arena;
@@ -61,6 +64,17 @@ public final class GhosttyLibrary {
private static final int GHOSTTY_OUT_OF_SPACE = -3;
private static final int GHOSTTY_NO_VALUE = -4;
private static final int BUILD_INFO_SIMD = 1;
private static final int BUILD_INFO_KITTY_GRAPHICS = 2;
private static final int BUILD_INFO_TMUX_CONTROL_MODE = 3;
private static final int BUILD_INFO_OPTIMIZE = 4;
private static final int BUILD_INFO_VERSION_STRING = 5;
private static final int BUILD_INFO_VERSION_MAJOR = 6;
private static final int BUILD_INFO_VERSION_MINOR = 7;
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 Linker LINKER = Linker.nativeLinker();
private static final AddressLayout C_POINTER = (AddressLayout) LINKER.canonicalLayouts().get("void*");
private static final ValueLayout.OfBoolean C_BOOL = (ValueLayout.OfBoolean) LINKER.canonicalLayouts().get("bool");
@@ -81,6 +95,13 @@ public final class GhosttyLibrary {
C_SIZE_T.withName("len")
);
private static final GroupLayout SIZE_REPORT_SIZE = MemoryLayout.structLayout(
C_SHORT.withName("rows"),
C_SHORT.withName("columns"),
C_INT.withName("cell_width"),
C_INT.withName("cell_height")
);
private static final GroupLayout KITTY_RENDER_INFO = MemoryLayout.structLayout(
C_SIZE_T.withName("size"),
C_INT.withName("pixel_width"),
@@ -107,6 +128,11 @@ public final class GhosttyLibrary {
private final MethodHandle terminalGet;
private final MethodHandle pasteIsSafe;
private final MethodHandle pasteEncode;
private final MethodHandle buildInfo;
private final MethodHandle typeJson;
private final MethodHandle focusEncode;
private final MethodHandle modeReportEncode;
private final MethodHandle sizeReportEncode;
private final MethodHandle kittyGraphicsGet;
private final MethodHandle kittyGraphicsImage;
private final MethodHandle kittyGraphicsImageGet;
@@ -139,6 +165,16 @@ public final class GhosttyLibrary {
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));
buildInfo = downcall(symbols, "ghostty_build_info",
FunctionDescriptor.of(C_INT, C_INT, C_POINTER));
typeJson = downcall(symbols, "ghostty_type_json",
FunctionDescriptor.of(C_POINTER));
focusEncode = downcall(symbols, "ghostty_focus_encode",
FunctionDescriptor.of(C_INT, C_INT, C_POINTER, C_SIZE_T, C_POINTER));
modeReportEncode = downcall(symbols, "ghostty_mode_report_encode",
FunctionDescriptor.of(C_INT, C_INT, C_INT, C_POINTER, C_SIZE_T, C_POINTER));
sizeReportEncode = downcall(symbols, "ghostty_size_report_encode",
FunctionDescriptor.of(C_INT, C_INT, SIZE_REPORT_SIZE, 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",
@@ -375,6 +411,56 @@ public final class GhosttyLibrary {
}
}
public GhosttyBuildInfo buildInfo() {
return new GhosttyBuildInfo(
buildInfoBoolean(BUILD_INFO_SIMD),
buildInfoBoolean(BUILD_INFO_KITTY_GRAPHICS),
buildInfoBoolean(BUILD_INFO_TMUX_CONTROL_MODE),
OptimizeMode.fromNative(buildInfoInt(BUILD_INFO_OPTIMIZE)),
buildInfoString(BUILD_INFO_VERSION_STRING),
buildInfoSizeT(BUILD_INFO_VERSION_MAJOR),
buildInfoSizeT(BUILD_INFO_VERSION_MINOR),
buildInfoSizeT(BUILD_INFO_VERSION_PATCH),
buildInfoString(BUILD_INFO_VERSION_PRE),
buildInfoString(BUILD_INFO_VERSION_BUILD)
);
}
public String typeJson() {
try {
MemorySegment ptr = (MemorySegment) typeJson.invoke();
if (ptr.address() == 0) {
return "";
}
return ptr.reinterpret(16L * 1024L * 1024L).getString(0);
} catch (Throwable t) {
return rethrow(t);
}
}
public byte[] focusEncode(int event) {
return encodeBuffer("ghostty_focus_encode", (arena, out, outLen, outWritten) ->
(int) focusEncode.invoke(event, out, outLen, outWritten)
);
}
public byte[] modeReportEncode(int mode, int state) {
return encodeBuffer("ghostty_mode_report_encode", (arena, out, outLen, outWritten) ->
(int) modeReportEncode.invoke(mode, state, out, outLen, outWritten)
);
}
public byte[] sizeReportEncode(int style, SizeReportSize size) {
return encodeBuffer("ghostty_size_report_encode", (arena, out, outLen, outWritten) -> {
MemorySegment nativeSize = arena.allocate(SIZE_REPORT_SIZE);
nativeSize.set(C_SHORT, 0, (short) size.rows());
nativeSize.set(C_SHORT, 2, (short) size.columns());
nativeSize.set(C_INT, 4, (int) size.cellWidth());
nativeSize.set(C_INT, 8, (int) size.cellHeight());
return (int) sizeReportEncode.invoke(style, nativeSize, out, outLen, outWritten);
});
}
public void kittyGraphicsPopulatePlacementIterator(MemorySegment graphics, MemorySegment iterator) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment out = arena.allocate(C_POINTER);
@@ -544,6 +630,90 @@ public final class GhosttyLibrary {
}
}
private boolean buildInfoBoolean(int key) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment out = arena.allocate(C_BOOL);
int result = (int) buildInfo.invoke(key, out);
checkResult("ghostty_build_info", result);
return out.get(C_BOOL, 0);
} catch (Throwable t) {
return rethrow(t);
}
}
private int buildInfoInt(int key) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment out = arena.allocate(C_INT);
int result = (int) buildInfo.invoke(key, out);
checkResult("ghostty_build_info", result);
return out.get(C_INT, 0);
} catch (Throwable t) {
return rethrow(t);
}
}
private long buildInfoSizeT(int key) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment out = arena.allocate(C_SIZE_T);
int result = (int) buildInfo.invoke(key, out);
checkResult("ghostty_build_info", result);
return out.get(C_SIZE_T, 0);
} catch (Throwable t) {
return rethrow(t);
}
}
private String buildInfoString(int key) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment out = arena.allocate(GHOSTTY_STRING);
int result = (int) buildInfo.invoke(key, out);
checkResult("ghostty_build_info", result);
return readGhosttyString(out);
} catch (Throwable t) {
return rethrow(t);
}
}
private String readGhosttyString(MemorySegment string) {
MemorySegment ptr = string.get(C_POINTER, 0);
long len = string.get(C_SIZE_T, 8);
if (ptr.address() == 0 || len == 0) {
return "";
}
byte[] bytes = ptr.reinterpret(len).toArray(JAVA_BYTE);
return new String(bytes, StandardCharsets.UTF_8);
}
private byte[] encodeBuffer(String operation, Encoder encoder) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment outWritten = arena.allocate(C_SIZE_T);
int result = encoder.invoke(arena, MemorySegment.NULL, 0L, outWritten);
if (result != GHOSTTY_SUCCESS && result != GHOSTTY_OUT_OF_SPACE) {
checkResult(operation, result);
}
long required = outWritten.get(C_SIZE_T, 0);
if (required == 0) {
return new byte[0];
}
MemorySegment out = arena.allocate(required, 1);
result = encoder.invoke(arena, out, required, outWritten);
checkResult(operation, result);
long written = outWritten.get(C_SIZE_T, 0);
return out.asSlice(0, written).toArray(JAVA_BYTE);
} catch (Throwable t) {
return rethrow(t);
}
}
@FunctionalInterface
private interface Encoder {
int invoke(Arena arena, MemorySegment out, long outLen, MemorySegment outWritten) throws Throwable;
}
@SuppressWarnings("unchecked")
private static <T> T rethrow(Throwable t) {
if (t instanceof RuntimeException runtimeException) {

View File

@@ -72,6 +72,46 @@
"void*"
]
},
{
"returnType": "int",
"parameterTypes": [
"int",
"void*"
]
},
{
"returnType": "void*",
"parameterTypes": []
},
{
"returnType": "int",
"parameterTypes": [
"int",
"void*",
"size_t",
"void*"
]
},
{
"returnType": "int",
"parameterTypes": [
"int",
"int",
"void*",
"size_t",
"void*"
]
},
{
"returnType": "int",
"parameterTypes": [
"int",
"struct(short, short, int, int)",
"void*",
"size_t",
"void*"
]
},
{
"returnType": "void*",
"parameterTypes": [

View File

@@ -14,6 +14,21 @@ public final class GhosttySmokeTest {
if (!"hello".equals(Ghostty.encodePaste("hello", false))) {
throw new AssertionError("simple paste encoding changed unexpectedly");
}
if (Ghostty.buildInfo().version().isEmpty()) {
throw new AssertionError("build info version should not be empty");
}
if (Ghostty.typeJson().isEmpty()) {
throw new AssertionError("type JSON should not be empty");
}
if (Ghostty.encodeFocus(FocusEvent.GAINED).isEmpty()) {
throw new AssertionError("focus encoding should not be empty");
}
if (Ghostty.encodeModeReport(25, ModeReportState.SET).isEmpty()) {
throw new AssertionError("mode report encoding should not be empty");
}
if (Ghostty.encodeSizeReport(SizeReportStyle.CSI_18_T, new SizeReportSize(24, 80, 8, 16)).isEmpty()) {
throw new AssertionError("size report encoding should not be empty");
}
try (Terminal terminal = Ghostty.open(TerminalOptions.of(80, 24))) {
terminal.setKittyImageStorageLimit(1024 * 1024);