expose formatter

This commit is contained in:
Gregor Lohaus
2026-05-27 18:50:45 +02:00
parent eea4384300
commit aee4954edf
5 changed files with 151 additions and 0 deletions

View File

@@ -31,6 +31,23 @@ public final class Terminal implements AutoCloseable {
library.terminalWrite(handle, vtData);
}
public String text() {
return format(TerminalFormat.PLAIN);
}
public String vtText() {
return format(TerminalFormat.VT);
}
public String html() {
return format(TerminalFormat.HTML);
}
public String format(TerminalFormat format) {
ensureOpen();
return library.formatTerminal(handle, format.nativeValue());
}
public void reset() {
ensureOpen();
library.terminalReset(handle);

View File

@@ -0,0 +1,17 @@
package dev.jlibghostty;
public enum TerminalFormat {
PLAIN(0),
VT(1),
HTML(2);
private final int nativeValue;
TerminalFormat(int nativeValue) {
this.nativeValue = nativeValue;
}
int nativeValue() {
return nativeValue;
}
}

View File

@@ -119,6 +119,39 @@ public final class GhosttyLibrary {
MemoryLayout.paddingLayout(4)
);
private static final GroupLayout FORMATTER_SCREEN_EXTRA = MemoryLayout.structLayout(
C_SIZE_T.withName("size"),
C_BOOL.withName("cursor"),
C_BOOL.withName("style"),
C_BOOL.withName("hyperlink"),
C_BOOL.withName("protection"),
C_BOOL.withName("kitty_keyboard"),
C_BOOL.withName("charsets"),
MemoryLayout.paddingLayout(2)
);
private static final GroupLayout FORMATTER_TERMINAL_EXTRA = MemoryLayout.structLayout(
C_SIZE_T.withName("size"),
C_BOOL.withName("palette"),
C_BOOL.withName("modes"),
C_BOOL.withName("scrolling_region"),
C_BOOL.withName("tabstops"),
C_BOOL.withName("pwd"),
C_BOOL.withName("keyboard"),
MemoryLayout.paddingLayout(2),
FORMATTER_SCREEN_EXTRA.withName("screen")
);
private static final GroupLayout FORMATTER_TERMINAL_OPTIONS = MemoryLayout.structLayout(
C_SIZE_T.withName("size"),
C_INT.withName("emit"),
C_BOOL.withName("unwrap"),
C_BOOL.withName("trim"),
MemoryLayout.paddingLayout(2),
FORMATTER_TERMINAL_EXTRA.withName("extra"),
C_POINTER.withName("selection")
);
private final MethodHandle terminalNew;
private final MethodHandle terminalFree;
private final MethodHandle terminalReset;
@@ -133,6 +166,9 @@ public final class GhosttyLibrary {
private final MethodHandle focusEncode;
private final MethodHandle modeReportEncode;
private final MethodHandle sizeReportEncode;
private final MethodHandle formatterTerminalNew;
private final MethodHandle formatterFormatBuf;
private final MethodHandle formatterFree;
private final MethodHandle kittyGraphicsGet;
private final MethodHandle kittyGraphicsImage;
private final MethodHandle kittyGraphicsImageGet;
@@ -175,6 +211,12 @@ public final class GhosttyLibrary {
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));
formatterTerminalNew = downcall(symbols, "ghostty_formatter_terminal_new",
FunctionDescriptor.of(C_INT, C_POINTER, C_POINTER, C_POINTER, FORMATTER_TERMINAL_OPTIONS));
formatterFormatBuf = downcall(symbols, "ghostty_formatter_format_buf",
FunctionDescriptor.of(C_INT, C_POINTER, C_POINTER, C_SIZE_T, C_POINTER));
formatterFree = downcall(symbols, "ghostty_formatter_free",
FunctionDescriptor.ofVoid(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",
@@ -461,6 +503,18 @@ public final class GhosttyLibrary {
});
}
public String formatTerminal(MemorySegment terminal, int format) {
MemorySegment formatter = formatterTerminalNew(terminal, format);
try {
byte[] bytes = encodeBuffer("ghostty_formatter_format_buf", (arena, out, outLen, outWritten) ->
(int) formatterFormatBuf.invoke(formatter, out, outLen, outWritten)
);
return new String(bytes, StandardCharsets.UTF_8);
} finally {
formatterFree(formatter);
}
}
public void kittyGraphicsPopulatePlacementIterator(MemorySegment graphics, MemorySegment iterator) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment out = arena.allocate(C_POINTER);
@@ -630,6 +684,48 @@ public final class GhosttyLibrary {
}
}
private MemorySegment formatterTerminalNew(MemorySegment terminal, int format) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment out = arena.allocate(C_POINTER);
MemorySegment options = arena.allocate(FORMATTER_TERMINAL_OPTIONS);
writeFormatterTerminalOptions(options, format);
int result = (int) formatterTerminalNew.invoke(MemorySegment.NULL, out, terminal, options);
checkResult("ghostty_formatter_terminal_new", result);
MemorySegment formatter = out.get(C_POINTER, 0);
if (formatter.address() == 0) {
throw new IllegalStateException("ghostty_formatter_terminal_new returned null");
}
return formatter;
} catch (Throwable t) {
return rethrow(t);
}
}
private void formatterFree(MemorySegment formatter) {
try {
formatterFree.invoke(formatter);
} catch (Throwable t) {
rethrow(t);
}
}
private void writeFormatterTerminalOptions(MemorySegment options, int format) {
options.set(C_SIZE_T, 0, FORMATTER_TERMINAL_OPTIONS.byteSize());
options.set(C_INT, 8, format);
options.set(C_BOOL, 12, false);
options.set(C_BOOL, 13, false);
long extra = 16;
options.set(C_SIZE_T, extra, FORMATTER_TERMINAL_EXTRA.byteSize());
long screen = extra + 16;
options.set(C_SIZE_T, screen, FORMATTER_SCREEN_EXTRA.byteSize());
options.set(C_POINTER, 48, MemorySegment.NULL);
}
private boolean buildInfoBoolean(int key) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment out = arena.allocate(C_BOOL);

View File

@@ -112,6 +112,24 @@
"void*"
]
},
{
"returnType": "int",
"parameterTypes": [
"void*",
"void*",
"void*",
"struct(size_t, int, bool, bool, padding(2), struct(size_t, bool, bool, bool, bool, bool, bool, padding(2), struct(size_t, bool, bool, bool, bool, bool, bool, padding(2))), void*)"
]
},
{
"returnType": "int",
"parameterTypes": [
"void*",
"void*",
"size_t",
"void*"
]
},
{
"returnType": "void*",
"parameterTypes": [

View File

@@ -34,6 +34,9 @@ public final class GhosttySmokeTest {
terminal.setKittyImageStorageLimit(1024 * 1024);
terminal.setKittyImageMediumFile(true);
terminal.write("hello\r\n");
if (!terminal.text().contains("hello")) {
throw new AssertionError("formatted terminal text should contain written text: " + terminal.text());
}
TerminalSnapshot snapshot = terminal.snapshot();
if (snapshot.columns() != 80 || snapshot.rows() != 24) {
throw new AssertionError("unexpected terminal size: " + snapshot);