terminal effect callback
This commit is contained in:
68
src/main/java/dev/jlibghostty/DeviceAttributes.java
Normal file
68
src/main/java/dev/jlibghostty/DeviceAttributes.java
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package dev.jlibghostty;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface DeviceAttributesProvider {
|
||||
DeviceAttributes deviceAttributes();
|
||||
}
|
||||
6
src/main/java/dev/jlibghostty/PtyWriter.java
Normal file
6
src/main/java/dev/jlibghostty/PtyWriter.java
Normal file
@@ -0,0 +1,6 @@
|
||||
package dev.jlibghostty;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface PtyWriter {
|
||||
void write(byte[] bytes);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -5,6 +5,25 @@
|
||||
}
|
||||
],
|
||||
"foreign": {
|
||||
"upcalls": [
|
||||
{
|
||||
"returnType": "void",
|
||||
"parameterTypes": [
|
||||
"void*",
|
||||
"void*",
|
||||
"void*",
|
||||
"size_t"
|
||||
]
|
||||
},
|
||||
{
|
||||
"returnType": "bool",
|
||||
"parameterTypes": [
|
||||
"void*",
|
||||
"void*",
|
||||
"void*"
|
||||
]
|
||||
}
|
||||
],
|
||||
"downcalls": [
|
||||
{
|
||||
"returnType": "int",
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user