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 dev.jlibghostty.internal.GhosttyLibrary;
|
||||||
|
|
||||||
|
import java.lang.foreign.Arena;
|
||||||
import java.lang.foreign.MemorySegment;
|
import java.lang.foreign.MemorySegment;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -10,7 +11,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
public final class Terminal implements AutoCloseable {
|
public final class Terminal implements AutoCloseable {
|
||||||
private final GhosttyLibrary library;
|
private final GhosttyLibrary library;
|
||||||
private final MemorySegment handle;
|
private final MemorySegment handle;
|
||||||
|
private final Arena callbackArena = Arena.ofShared();
|
||||||
private final AtomicBoolean closed = new AtomicBoolean();
|
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) {
|
private Terminal(GhosttyLibrary library, MemorySegment handle) {
|
||||||
this.library = library;
|
this.library = library;
|
||||||
@@ -31,6 +37,36 @@ public final class Terminal implements AutoCloseable {
|
|||||||
library.terminalWrite(handle, vtData);
|
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() {
|
public String text() {
|
||||||
return format(TerminalFormat.PLAIN);
|
return format(TerminalFormat.PLAIN);
|
||||||
}
|
}
|
||||||
@@ -115,6 +151,7 @@ public final class Terminal implements AutoCloseable {
|
|||||||
public void close() {
|
public void close() {
|
||||||
if (closed.compareAndSet(false, true)) {
|
if (closed.compareAndSet(false, true)) {
|
||||||
library.terminalFree(handle);
|
library.terminalFree(handle);
|
||||||
|
callbackArena.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ package dev.jlibghostty.internal;
|
|||||||
|
|
||||||
import dev.jlibghostty.GhosttyBuildInfo;
|
import dev.jlibghostty.GhosttyBuildInfo;
|
||||||
import dev.jlibghostty.GhosttyException;
|
import dev.jlibghostty.GhosttyException;
|
||||||
|
import dev.jlibghostty.DeviceAttributes;
|
||||||
|
import dev.jlibghostty.DeviceAttributesProvider;
|
||||||
import dev.jlibghostty.OptimizeMode;
|
import dev.jlibghostty.OptimizeMode;
|
||||||
|
import dev.jlibghostty.PtyWriter;
|
||||||
import dev.jlibghostty.RenderCell;
|
import dev.jlibghostty.RenderCell;
|
||||||
import dev.jlibghostty.RenderColor;
|
import dev.jlibghostty.RenderColor;
|
||||||
import dev.jlibghostty.RenderRow;
|
import dev.jlibghostty.RenderRow;
|
||||||
@@ -19,6 +22,8 @@ import java.lang.foreign.MemorySegment;
|
|||||||
import java.lang.foreign.SymbolLookup;
|
import java.lang.foreign.SymbolLookup;
|
||||||
import java.lang.foreign.ValueLayout;
|
import java.lang.foreign.ValueLayout;
|
||||||
import java.lang.invoke.MethodHandle;
|
import java.lang.invoke.MethodHandle;
|
||||||
|
import java.lang.invoke.MethodHandles;
|
||||||
|
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;
|
||||||
@@ -37,6 +42,8 @@ public final class GhosttyLibrary {
|
|||||||
public static final int TERMINAL_DATA_PWD = 13;
|
public static final int TERMINAL_DATA_PWD = 13;
|
||||||
public static final int TERMINAL_DATA_KITTY_GRAPHICS = 30;
|
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_STORAGE_LIMIT = 15;
|
||||||
public static final int TERMINAL_OPT_KITTY_IMAGE_MEDIUM_FILE = 16;
|
public static final int TERMINAL_OPT_KITTY_IMAGE_MEDIUM_FILE = 16;
|
||||||
public static final int TERMINAL_OPT_KITTY_IMAGE_MEDIUM_TEMP_FILE = 17;
|
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.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_LONG_LONG = (ValueLayout.OfLong) LINKER.canonicalLayouts().get("long long");
|
||||||
private static final ValueLayout.OfLong C_SIZE_T = sizeTLayout();
|
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(
|
private static final GroupLayout TERMINAL_OPTIONS = MemoryLayout.structLayout(
|
||||||
C_SHORT.withName("cols"),
|
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) {
|
public int terminalGetU16(MemorySegment terminal, int key) {
|
||||||
try (Arena arena = Arena.ofConfined()) {
|
try (Arena arena = Arena.ofConfined()) {
|
||||||
MemorySegment out = arena.allocate(C_SHORT);
|
MemorySegment out = arena.allocate(C_SHORT);
|
||||||
@@ -1006,6 +1048,71 @@ public final class GhosttyLibrary {
|
|||||||
return LINKER.downcallHandle(symbol, descriptor);
|
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) {
|
private static void checkResult(String operation, int result) {
|
||||||
if (result != GHOSTTY_SUCCESS) {
|
if (result != GHOSTTY_SUCCESS) {
|
||||||
throw new GhosttyException(operation, result);
|
throw new GhosttyException(operation, result);
|
||||||
|
|||||||
@@ -5,6 +5,25 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"foreign": {
|
"foreign": {
|
||||||
|
"upcalls": [
|
||||||
|
{
|
||||||
|
"returnType": "void",
|
||||||
|
"parameterTypes": [
|
||||||
|
"void*",
|
||||||
|
"void*",
|
||||||
|
"void*",
|
||||||
|
"size_t"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"returnType": "bool",
|
||||||
|
"parameterTypes": [
|
||||||
|
"void*",
|
||||||
|
"void*",
|
||||||
|
"void*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"downcalls": [
|
"downcalls": [
|
||||||
{
|
{
|
||||||
"returnType": "int",
|
"returnType": "int",
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package dev.jlibghostty;
|
package dev.jlibghostty;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
public final class GhosttySmokeTest {
|
public final class GhosttySmokeTest {
|
||||||
private GhosttySmokeTest() {
|
private GhosttySmokeTest() {
|
||||||
}
|
}
|
||||||
@@ -31,9 +34,18 @@ public final class GhosttySmokeTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try (Terminal terminal = Ghostty.open(TerminalOptions.of(80, 24))) {
|
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.setKittyImageStorageLimit(1024 * 1024);
|
||||||
terminal.setKittyImageMediumFile(true);
|
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")) {
|
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());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user