From aee4954edfee75110bf6ffd11bfe46cd8a42497e Mon Sep 17 00:00:00 2001 From: Gregor Lohaus Date: Wed, 27 May 2026 18:50:45 +0200 Subject: [PATCH] expose formatter --- src/main/java/dev/jlibghostty/Terminal.java | 17 ++++ .../java/dev/jlibghostty/TerminalFormat.java | 17 ++++ .../jlibghostty/internal/GhosttyLibrary.java | 96 +++++++++++++++++++ .../jlibghostty/reachability-metadata.json | 18 ++++ .../dev/jlibghostty/GhosttySmokeTest.java | 3 + 5 files changed, 151 insertions(+) create mode 100644 src/main/java/dev/jlibghostty/TerminalFormat.java diff --git a/src/main/java/dev/jlibghostty/Terminal.java b/src/main/java/dev/jlibghostty/Terminal.java index 1f4f590..fd23fe8 100644 --- a/src/main/java/dev/jlibghostty/Terminal.java +++ b/src/main/java/dev/jlibghostty/Terminal.java @@ -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); diff --git a/src/main/java/dev/jlibghostty/TerminalFormat.java b/src/main/java/dev/jlibghostty/TerminalFormat.java new file mode 100644 index 0000000..fcbb94c --- /dev/null +++ b/src/main/java/dev/jlibghostty/TerminalFormat.java @@ -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; + } +} diff --git a/src/main/java/dev/jlibghostty/internal/GhosttyLibrary.java b/src/main/java/dev/jlibghostty/internal/GhosttyLibrary.java index 030761c..700dd26 100644 --- a/src/main/java/dev/jlibghostty/internal/GhosttyLibrary.java +++ b/src/main/java/dev/jlibghostty/internal/GhosttyLibrary.java @@ -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); diff --git a/src/main/resources/META-INF/native-image/dev.jlibghostty/jlibghostty/reachability-metadata.json b/src/main/resources/META-INF/native-image/dev.jlibghostty/jlibghostty/reachability-metadata.json index e1c8337..3a09f8f 100644 --- a/src/main/resources/META-INF/native-image/dev.jlibghostty/jlibghostty/reachability-metadata.json +++ b/src/main/resources/META-INF/native-image/dev.jlibghostty/jlibghostty/reachability-metadata.json @@ -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": [ diff --git a/src/test/java/dev/jlibghostty/GhosttySmokeTest.java b/src/test/java/dev/jlibghostty/GhosttySmokeTest.java index e313e13..eaeacbb 100644 --- a/src/test/java/dev/jlibghostty/GhosttySmokeTest.java +++ b/src/test/java/dev/jlibghostty/GhosttySmokeTest.java @@ -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);