terminal effect callback

This commit is contained in:
Gregor Lohaus
2026-05-28 01:30:18 +02:00
parent 0852e58086
commit f44df36687
7 changed files with 256 additions and 1 deletions

View File

@@ -0,0 +1,68 @@
package dev.jlibghostty;
import java.util.Arrays;
public record DeviceAttributes(
int conformanceLevel,
int[] features,
int deviceType,
int firmwareVersion,
int romCartridge,
long unitId
) {
public static final int DA_CONFORMANCE_VT220 = 62;
public static final int DA_FEATURE_ANSI_COLOR = 22;
public static final int DA_FEATURE_CLIPBOARD = 52;
public static final int DA_FEATURE_SELECTIVE_ERASE = 6;
public static final int DA_DEVICE_TYPE_VT220 = 1;
public DeviceAttributes {
checkU16("conformanceLevel", conformanceLevel);
features = features.clone();
if (features.length > 64) {
throw new IllegalArgumentException("features may contain at most 64 entries");
}
for (int feature : features) {
checkU16("feature", feature);
}
checkU16("deviceType", deviceType);
checkU16("firmwareVersion", firmwareVersion);
checkU16("romCartridge", romCartridge);
if (unitId < 0 || unitId > 0xffffffffL) {
throw new IllegalArgumentException("unitId must fit in uint32_t");
}
}
public static DeviceAttributes xtermCompatible() {
return new DeviceAttributes(
DA_CONFORMANCE_VT220,
new int[] { DA_FEATURE_SELECTIVE_ERASE, DA_FEATURE_ANSI_COLOR, DA_FEATURE_CLIPBOARD },
DA_DEVICE_TYPE_VT220,
0,
0,
0
);
}
@Override
public int[] features() {
return features.clone();
}
@Override
public String toString() {
return "DeviceAttributes[conformanceLevel=" + conformanceLevel
+ ", features=" + Arrays.toString(features)
+ ", deviceType=" + deviceType
+ ", firmwareVersion=" + firmwareVersion
+ ", romCartridge=" + romCartridge
+ ", unitId=" + unitId
+ "]";
}
private static void checkU16(String name, int value) {
if (value < 0 || value > 65535) {
throw new IllegalArgumentException(name + " must fit in uint16_t");
}
}
}

View File

@@ -0,0 +1,6 @@
package dev.jlibghostty;
@FunctionalInterface
public interface DeviceAttributesProvider {
DeviceAttributes deviceAttributes();
}

View File

@@ -0,0 +1,6 @@
package dev.jlibghostty;
@FunctionalInterface
public interface PtyWriter {
void write(byte[] bytes);
}

View File

@@ -2,6 +2,7 @@ package dev.jlibghostty;
import dev.jlibghostty.internal.GhosttyLibrary;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
@@ -10,7 +11,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
public final class Terminal implements AutoCloseable {
private final GhosttyLibrary library;
private final MemorySegment handle;
private final Arena callbackArena = Arena.ofShared();
private final AtomicBoolean closed = new AtomicBoolean();
private MemorySegment ptyWriterStub = MemorySegment.NULL;
private MemorySegment deviceAttributesStub = MemorySegment.NULL;
private PtyWriter ptyWriter;
private DeviceAttributesProvider deviceAttributesProvider;
private Terminal(GhosttyLibrary library, MemorySegment handle) {
this.library = library;
@@ -31,6 +37,36 @@ public final class Terminal implements AutoCloseable {
library.terminalWrite(handle, vtData);
}
public void setPtyWriter(PtyWriter writer) {
ensureOpen();
ptyWriter = writer;
if (writer == null) {
ptyWriterStub = MemorySegment.NULL;
library.terminalSetPointer(handle, GhosttyLibrary.TERMINAL_OPT_WRITE_PTY, MemorySegment.NULL);
return;
}
ptyWriterStub = library.upcallPtyWriter(writer, callbackArena);
library.terminalSetPointer(handle, GhosttyLibrary.TERMINAL_OPT_WRITE_PTY, ptyWriterStub);
}
public void setDeviceAttributesProvider(DeviceAttributesProvider provider) {
ensureOpen();
deviceAttributesProvider = provider;
if (provider == null) {
deviceAttributesStub = MemorySegment.NULL;
library.terminalSetPointer(handle, GhosttyLibrary.TERMINAL_OPT_DEVICE_ATTRIBUTES, MemorySegment.NULL);
return;
}
deviceAttributesStub = library.upcallDeviceAttributesProvider(provider, callbackArena);
library.terminalSetPointer(
handle,
GhosttyLibrary.TERMINAL_OPT_DEVICE_ATTRIBUTES,
deviceAttributesStub
);
}
public String text() {
return format(TerminalFormat.PLAIN);
}
@@ -115,6 +151,7 @@ public final class Terminal implements AutoCloseable {
public void close() {
if (closed.compareAndSet(false, true)) {
library.terminalFree(handle);
callbackArena.close();
}
}

View File

@@ -2,7 +2,10 @@ package dev.jlibghostty.internal;
import dev.jlibghostty.GhosttyBuildInfo;
import dev.jlibghostty.GhosttyException;
import dev.jlibghostty.DeviceAttributes;
import dev.jlibghostty.DeviceAttributesProvider;
import dev.jlibghostty.OptimizeMode;
import dev.jlibghostty.PtyWriter;
import dev.jlibghostty.RenderCell;
import dev.jlibghostty.RenderColor;
import dev.jlibghostty.RenderRow;
@@ -19,6 +22,8 @@ import java.lang.foreign.MemorySegment;
import java.lang.foreign.SymbolLookup;
import java.lang.foreign.ValueLayout;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
@@ -37,6 +42,8 @@ public final class GhosttyLibrary {
public static final int TERMINAL_DATA_PWD = 13;
public static final int TERMINAL_DATA_KITTY_GRAPHICS = 30;
public static final int TERMINAL_OPT_WRITE_PTY = 1;
public static final int TERMINAL_OPT_DEVICE_ATTRIBUTES = 8;
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;
@@ -111,6 +118,14 @@ public final class GhosttyLibrary {
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 MethodHandle PTY_WRITER_UPCALL = staticMethod(
"ptyWriterUpcall",
MethodType.methodType(void.class, PtyWriter.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, long.class)
);
private static final MethodHandle DEVICE_ATTRIBUTES_UPCALL = staticMethod(
"deviceAttributesUpcall",
MethodType.methodType(boolean.class, DeviceAttributesProvider.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)
);
private static final GroupLayout TERMINAL_OPTIONS = MemoryLayout.structLayout(
C_SHORT.withName("cols"),
@@ -402,6 +417,33 @@ public final class GhosttyLibrary {
}
}
public void terminalSetPointer(MemorySegment terminal, int key, MemorySegment value) {
try {
int result = (int) terminalSet.invoke(terminal, key, value);
checkResult("ghostty_terminal_set", result);
} catch (Throwable t) {
rethrow(t);
}
}
public MemorySegment upcallPtyWriter(PtyWriter writer, Arena arena) {
MethodHandle handle = MethodHandles.insertArguments(PTY_WRITER_UPCALL, 0, writer);
return LINKER.upcallStub(
handle,
FunctionDescriptor.ofVoid(C_POINTER, C_POINTER, C_POINTER, C_SIZE_T),
arena
);
}
public MemorySegment upcallDeviceAttributesProvider(DeviceAttributesProvider provider, Arena arena) {
MethodHandle handle = MethodHandles.insertArguments(DEVICE_ATTRIBUTES_UPCALL, 0, provider);
return LINKER.upcallStub(
handle,
FunctionDescriptor.of(C_BOOL, C_POINTER, C_POINTER, C_POINTER),
arena
);
}
public int terminalGetU16(MemorySegment terminal, int key) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment out = arena.allocate(C_SHORT);
@@ -1006,6 +1048,71 @@ public final class GhosttyLibrary {
return LINKER.downcallHandle(symbol, descriptor);
}
private static MethodHandle staticMethod(String name, MethodType type) {
try {
return MethodHandles.lookup().findStatic(GhosttyLibrary.class, name, type);
} catch (NoSuchMethodException | IllegalAccessException e) {
throw new ExceptionInInitializerError(e);
}
}
private static void ptyWriterUpcall(
PtyWriter writer,
MemorySegment terminal,
MemorySegment userdata,
MemorySegment data,
long len
) {
try {
if (data.address() == 0 || len == 0) {
writer.write(new byte[0]);
return;
}
writer.write(data.reinterpret(len).toArray(JAVA_BYTE));
} catch (Throwable ignored) {
}
}
private static boolean deviceAttributesUpcall(
DeviceAttributesProvider provider,
MemorySegment terminal,
MemorySegment userdata,
MemorySegment outAttrs
) {
try {
if (outAttrs.address() == 0) {
return false;
}
DeviceAttributes attrs = provider.deviceAttributes();
if (attrs == null) {
return false;
}
writeDeviceAttributes(outAttrs, attrs);
return true;
} catch (Throwable ignored) {
return false;
}
}
private static void writeDeviceAttributes(MemorySegment out, DeviceAttributes attrs) {
out.reinterpret(160).fill((byte) 0);
out.set(C_SHORT, 0, (short) attrs.conformanceLevel());
int[] features = attrs.features();
for (int i = 0; i < features.length; i++) {
out.set(C_SHORT, 2L + (long) i * Short.BYTES, (short) features[i]);
}
out.set(C_SIZE_T, 136, features.length);
out.set(C_SHORT, 144, (short) attrs.deviceType());
out.set(C_SHORT, 146, (short) attrs.firmwareVersion());
out.set(C_SHORT, 148, (short) attrs.romCartridge());
out.set(C_INT, 152, (int) attrs.unitId());
}
private static void checkResult(String operation, int result) {
if (result != GHOSTTY_SUCCESS) {
throw new GhosttyException(operation, result);

View File

@@ -5,6 +5,25 @@
}
],
"foreign": {
"upcalls": [
{
"returnType": "void",
"parameterTypes": [
"void*",
"void*",
"void*",
"size_t"
]
},
{
"returnType": "bool",
"parameterTypes": [
"void*",
"void*",
"void*"
]
}
],
"downcalls": [
{
"returnType": "int",

View File

@@ -1,5 +1,8 @@
package dev.jlibghostty;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
public final class GhosttySmokeTest {
private GhosttySmokeTest() {
}
@@ -31,9 +34,18 @@ public final class GhosttySmokeTest {
}
try (Terminal terminal = Ghostty.open(TerminalOptions.of(80, 24))) {
ByteArrayOutputStream ptyResponses = new ByteArrayOutputStream();
terminal.setPtyWriter(ptyResponses::writeBytes);
terminal.setDeviceAttributesProvider(DeviceAttributes::xtermCompatible);
terminal.setKittyImageStorageLimit(1024 * 1024);
terminal.setKittyImageMediumFile(true);
terminal.write("hello\r\n");
terminal.write("hello\r\n\u001b[5n\u001b[6n\u001b[c");
String responses = ptyResponses.toString(StandardCharsets.UTF_8);
if (!responses.contains("\u001b[0n")
|| !responses.contains("\u001b[")
|| !responses.contains("c")) {
throw new AssertionError("expected PTY query responses, got: " + responses);
}
if (!terminal.text().contains("hello")) {
throw new AssertionError("formatted terminal text should contain written text: " + terminal.text());
}