expose render state

This commit is contained in:
Gregor Lohaus
2026-05-27 19:38:17 +02:00
parent aee4954edf
commit 91e9ce5bb4
7 changed files with 404 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
package dev.jlibghostty;
public record RenderCell(int column, int[] codepoints, boolean selected) {
public RenderCell {
codepoints = codepoints.clone();
}
@Override
public int[] codepoints() {
return codepoints.clone();
}
public String text() {
StringBuilder builder = new StringBuilder();
for (int codepoint : codepoints) {
builder.appendCodePoint(codepoint);
}
return builder.toString();
}
}

View File

@@ -0,0 +1,17 @@
package dev.jlibghostty;
import java.util.List;
public record RenderRow(int row, boolean dirty, List<RenderCell> cells) {
public RenderRow {
cells = List.copyOf(cells);
}
public String text() {
StringBuilder builder = new StringBuilder();
for (RenderCell cell : cells) {
builder.append(cell.text());
}
return builder.toString();
}
}

View File

@@ -0,0 +1,52 @@
package dev.jlibghostty;
import dev.jlibghostty.internal.GhosttyLibrary;
import java.lang.foreign.MemorySegment;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
public final class RenderState implements AutoCloseable {
private final GhosttyLibrary library;
private final MemorySegment handle;
private final AtomicBoolean closed = new AtomicBoolean();
public RenderState() {
this.library = GhosttyLibrary.loadDefault();
this.handle = library.renderStateNew();
}
public void update(Terminal terminal) {
ensureOpen();
terminal.ensureOpenForPackage();
library.renderStateUpdate(handle, terminal.handleForPackage());
}
public RenderStateSnapshot snapshot() {
ensureOpen();
return new RenderStateSnapshot(
library.renderStateGetU16(handle, GhosttyLibrary.RENDER_STATE_DATA_COLS),
library.renderStateGetU16(handle, GhosttyLibrary.RENDER_STATE_DATA_ROWS),
library.renderStateGetI32(handle, GhosttyLibrary.RENDER_STATE_DATA_DIRTY),
rows()
);
}
public List<RenderRow> rows() {
ensureOpen();
return library.renderStateRows(handle);
}
@Override
public void close() {
if (closed.compareAndSet(false, true)) {
library.renderStateFree(handle);
}
}
private void ensureOpen() {
if (closed.get()) {
throw new IllegalStateException("RenderState is closed");
}
}
}

View File

@@ -0,0 +1,9 @@
package dev.jlibghostty;
import java.util.List;
public record RenderStateSnapshot(int columns, int rows, int dirty, List<RenderRow> renderRows) {
public RenderStateSnapshot {
renderRows = List.copyOf(renderRows);
}
}

View File

@@ -48,6 +48,14 @@ public final class Terminal implements AutoCloseable {
return library.formatTerminal(handle, format.nativeValue()); return library.formatTerminal(handle, format.nativeValue());
} }
public RenderStateSnapshot renderSnapshot() {
ensureOpen();
try (RenderState renderState = new RenderState()) {
renderState.update(this);
return renderState.snapshot();
}
}
public void reset() { public void reset() {
ensureOpen(); ensureOpen();
library.terminalReset(handle); library.terminalReset(handle);
@@ -115,4 +123,12 @@ public final class Terminal implements AutoCloseable {
throw new IllegalStateException("Terminal is closed"); throw new IllegalStateException("Terminal is closed");
} }
} }
void ensureOpenForPackage() {
ensureOpen();
}
MemorySegment handleForPackage() {
return handle;
}
} }

View File

@@ -3,6 +3,8 @@ package dev.jlibghostty.internal;
import dev.jlibghostty.GhosttyBuildInfo; import dev.jlibghostty.GhosttyBuildInfo;
import dev.jlibghostty.GhosttyException; import dev.jlibghostty.GhosttyException;
import dev.jlibghostty.OptimizeMode; import dev.jlibghostty.OptimizeMode;
import dev.jlibghostty.RenderCell;
import dev.jlibghostty.RenderRow;
import dev.jlibghostty.SizeReportSize; import dev.jlibghostty.SizeReportSize;
import dev.jlibghostty.TerminalOptions; import dev.jlibghostty.TerminalOptions;
@@ -18,6 +20,8 @@ import java.lang.foreign.ValueLayout;
import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandle;
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.List;
import static java.lang.foreign.ValueLayout.JAVA_BYTE; import static java.lang.foreign.ValueLayout.JAVA_BYTE;
@@ -60,6 +64,18 @@ public final class GhosttyLibrary {
public static final int KITTY_IMAGE_DATA_DATA_PTR = 7; public static final int KITTY_IMAGE_DATA_DATA_PTR = 7;
public static final int KITTY_IMAGE_DATA_DATA_LEN = 8; public static final int KITTY_IMAGE_DATA_DATA_LEN = 8;
public static final int RENDER_STATE_DATA_COLS = 1;
public static final int RENDER_STATE_DATA_ROWS = 2;
public static final int RENDER_STATE_DATA_DIRTY = 3;
public static final int RENDER_STATE_DATA_ROW_ITERATOR = 4;
public static final int RENDER_STATE_ROW_DATA_DIRTY = 1;
public static final int RENDER_STATE_ROW_DATA_CELLS = 3;
public static final int RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_LEN = 3;
public static final int RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_BUF = 4;
public static final int RENDER_STATE_ROW_CELLS_DATA_SELECTED = 7;
private static final int GHOSTTY_SUCCESS = 0; private static final int GHOSTTY_SUCCESS = 0;
private static final int GHOSTTY_OUT_OF_SPACE = -3; private static final int GHOSTTY_OUT_OF_SPACE = -3;
private static final int GHOSTTY_NO_VALUE = -4; private static final int GHOSTTY_NO_VALUE = -4;
@@ -169,6 +185,18 @@ public final class GhosttyLibrary {
private final MethodHandle formatterTerminalNew; private final MethodHandle formatterTerminalNew;
private final MethodHandle formatterFormatBuf; private final MethodHandle formatterFormatBuf;
private final MethodHandle formatterFree; private final MethodHandle formatterFree;
private final MethodHandle renderStateNew;
private final MethodHandle renderStateFree;
private final MethodHandle renderStateUpdate;
private final MethodHandle renderStateGet;
private final MethodHandle renderStateRowIteratorNew;
private final MethodHandle renderStateRowIteratorFree;
private final MethodHandle renderStateRowIteratorNext;
private final MethodHandle renderStateRowGet;
private final MethodHandle renderStateRowCellsNew;
private final MethodHandle renderStateRowCellsFree;
private final MethodHandle renderStateRowCellsNext;
private final MethodHandle renderStateRowCellsGet;
private final MethodHandle kittyGraphicsGet; private final MethodHandle kittyGraphicsGet;
private final MethodHandle kittyGraphicsImage; private final MethodHandle kittyGraphicsImage;
private final MethodHandle kittyGraphicsImageGet; private final MethodHandle kittyGraphicsImageGet;
@@ -217,6 +245,30 @@ public final class GhosttyLibrary {
FunctionDescriptor.of(C_INT, C_POINTER, C_POINTER, C_SIZE_T, C_POINTER)); FunctionDescriptor.of(C_INT, C_POINTER, C_POINTER, C_SIZE_T, C_POINTER));
formatterFree = downcall(symbols, "ghostty_formatter_free", formatterFree = downcall(symbols, "ghostty_formatter_free",
FunctionDescriptor.ofVoid(C_POINTER)); FunctionDescriptor.ofVoid(C_POINTER));
renderStateNew = downcall(symbols, "ghostty_render_state_new",
FunctionDescriptor.of(C_INT, C_POINTER, C_POINTER));
renderStateFree = downcall(symbols, "ghostty_render_state_free",
FunctionDescriptor.ofVoid(C_POINTER));
renderStateUpdate = downcall(symbols, "ghostty_render_state_update",
FunctionDescriptor.of(C_INT, C_POINTER, C_POINTER));
renderStateGet = downcall(symbols, "ghostty_render_state_get",
FunctionDescriptor.of(C_INT, C_POINTER, C_INT, C_POINTER));
renderStateRowIteratorNew = downcall(symbols, "ghostty_render_state_row_iterator_new",
FunctionDescriptor.of(C_INT, C_POINTER, C_POINTER));
renderStateRowIteratorFree = downcall(symbols, "ghostty_render_state_row_iterator_free",
FunctionDescriptor.ofVoid(C_POINTER));
renderStateRowIteratorNext = downcall(symbols, "ghostty_render_state_row_iterator_next",
FunctionDescriptor.of(C_BOOL, C_POINTER));
renderStateRowGet = downcall(symbols, "ghostty_render_state_row_get",
FunctionDescriptor.of(C_INT, C_POINTER, C_INT, C_POINTER));
renderStateRowCellsNew = downcall(symbols, "ghostty_render_state_row_cells_new",
FunctionDescriptor.of(C_INT, C_POINTER, C_POINTER));
renderStateRowCellsFree = downcall(symbols, "ghostty_render_state_row_cells_free",
FunctionDescriptor.ofVoid(C_POINTER));
renderStateRowCellsNext = downcall(symbols, "ghostty_render_state_row_cells_next",
FunctionDescriptor.of(C_BOOL, C_POINTER));
renderStateRowCellsGet = downcall(symbols, "ghostty_render_state_row_cells_get",
FunctionDescriptor.of(C_INT, C_POINTER, C_INT, C_POINTER));
kittyGraphicsGet = downcall(symbols, "ghostty_kitty_graphics_get", kittyGraphicsGet = downcall(symbols, "ghostty_kitty_graphics_get",
FunctionDescriptor.of(C_INT, C_POINTER, C_INT, C_POINTER)); FunctionDescriptor.of(C_INT, C_POINTER, C_INT, C_POINTER));
kittyGraphicsImage = downcall(symbols, "ghostty_kitty_graphics_image", kittyGraphicsImage = downcall(symbols, "ghostty_kitty_graphics_image",
@@ -515,6 +567,238 @@ public final class GhosttyLibrary {
} }
} }
public MemorySegment renderStateNew() {
try (Arena arena = Arena.ofConfined()) {
MemorySegment out = arena.allocate(C_POINTER);
int result = (int) renderStateNew.invoke(MemorySegment.NULL, out);
checkResult("ghostty_render_state_new", result);
MemorySegment state = out.get(C_POINTER, 0);
if (state.address() == 0) {
throw new IllegalStateException("ghostty_render_state_new returned null");
}
return state;
} catch (Throwable t) {
return rethrow(t);
}
}
public void renderStateFree(MemorySegment state) {
try {
renderStateFree.invoke(state);
} catch (Throwable t) {
rethrow(t);
}
}
public void renderStateUpdate(MemorySegment state, MemorySegment terminal) {
try {
int result = (int) renderStateUpdate.invoke(state, terminal);
checkResult("ghostty_render_state_update", result);
} catch (Throwable t) {
rethrow(t);
}
}
public int renderStateGetU16(MemorySegment state, int key) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment out = arena.allocate(C_SHORT);
int result = (int) renderStateGet.invoke(state, key, out);
checkResult("ghostty_render_state_get", result);
return Short.toUnsignedInt(out.get(C_SHORT, 0));
} catch (Throwable t) {
return rethrow(t);
}
}
public int renderStateGetI32(MemorySegment state, int key) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment out = arena.allocate(C_INT);
int result = (int) renderStateGet.invoke(state, key, out);
checkResult("ghostty_render_state_get", result);
return out.get(C_INT, 0);
} catch (Throwable t) {
return rethrow(t);
}
}
public List<RenderRow> renderStateRows(MemorySegment state) {
MemorySegment iterator = renderStateRowIteratorNew();
try {
renderStatePopulateRowIterator(state, iterator);
List<RenderRow> rows = new ArrayList<>();
int rowIndex = 0;
while (renderStateRowIteratorNext(iterator)) {
boolean dirty = renderStateRowGetBoolean(iterator, RENDER_STATE_ROW_DATA_DIRTY);
rows.add(new RenderRow(rowIndex, dirty, renderStateRowCells(iterator)));
rowIndex++;
}
return List.copyOf(rows);
} finally {
renderStateRowIteratorFree(iterator);
}
}
private MemorySegment renderStateRowIteratorNew() {
try (Arena arena = Arena.ofConfined()) {
MemorySegment out = arena.allocate(C_POINTER);
int result = (int) renderStateRowIteratorNew.invoke(MemorySegment.NULL, out);
checkResult("ghostty_render_state_row_iterator_new", result);
MemorySegment iterator = out.get(C_POINTER, 0);
if (iterator.address() == 0) {
throw new IllegalStateException("ghostty_render_state_row_iterator_new returned null");
}
return iterator;
} catch (Throwable t) {
return rethrow(t);
}
}
private void renderStateRowIteratorFree(MemorySegment iterator) {
try {
renderStateRowIteratorFree.invoke(iterator);
} catch (Throwable t) {
rethrow(t);
}
}
private void renderStatePopulateRowIterator(MemorySegment state, MemorySegment iterator) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment out = arena.allocate(C_POINTER);
out.set(C_POINTER, 0, iterator);
int result = (int) renderStateGet.invoke(state, RENDER_STATE_DATA_ROW_ITERATOR, out);
checkResult("ghostty_render_state_get", result);
} catch (Throwable t) {
rethrow(t);
}
}
private boolean renderStateRowIteratorNext(MemorySegment iterator) {
try {
return (boolean) renderStateRowIteratorNext.invoke(iterator);
} catch (Throwable t) {
return rethrow(t);
}
}
private boolean renderStateRowGetBoolean(MemorySegment iterator, int key) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment out = arena.allocate(C_BOOL);
int result = (int) renderStateRowGet.invoke(iterator, key, out);
checkResult("ghostty_render_state_row_get", result);
return out.get(C_BOOL, 0);
} catch (Throwable t) {
return rethrow(t);
}
}
private List<RenderCell> renderStateRowCells(MemorySegment rowIterator) {
MemorySegment cells = renderStateRowCellsNew();
try {
renderStatePopulateRowCells(rowIterator, cells);
List<RenderCell> result = new ArrayList<>();
int column = 0;
while (renderStateRowCellsNext(cells)) {
result.add(new RenderCell(
column,
renderStateRowCellGraphemes(cells),
renderStateRowCellsGetBoolean(cells, RENDER_STATE_ROW_CELLS_DATA_SELECTED)
));
column++;
}
return List.copyOf(result);
} finally {
renderStateRowCellsFree(cells);
}
}
private MemorySegment renderStateRowCellsNew() {
try (Arena arena = Arena.ofConfined()) {
MemorySegment out = arena.allocate(C_POINTER);
int result = (int) renderStateRowCellsNew.invoke(MemorySegment.NULL, out);
checkResult("ghostty_render_state_row_cells_new", result);
MemorySegment cells = out.get(C_POINTER, 0);
if (cells.address() == 0) {
throw new IllegalStateException("ghostty_render_state_row_cells_new returned null");
}
return cells;
} catch (Throwable t) {
return rethrow(t);
}
}
private void renderStateRowCellsFree(MemorySegment cells) {
try {
renderStateRowCellsFree.invoke(cells);
} catch (Throwable t) {
rethrow(t);
}
}
private void renderStatePopulateRowCells(MemorySegment rowIterator, MemorySegment cells) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment out = arena.allocate(C_POINTER);
out.set(C_POINTER, 0, cells);
int result = (int) renderStateRowGet.invoke(rowIterator, RENDER_STATE_ROW_DATA_CELLS, out);
checkResult("ghostty_render_state_row_get", result);
} catch (Throwable t) {
rethrow(t);
}
}
private boolean renderStateRowCellsNext(MemorySegment cells) {
try {
return (boolean) renderStateRowCellsNext.invoke(cells);
} catch (Throwable t) {
return rethrow(t);
}
}
private int[] renderStateRowCellGraphemes(MemorySegment cells) {
int len = renderStateRowCellsGetI32(cells, RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_LEN);
if (len <= 0) {
return new int[0];
}
try (Arena arena = Arena.ofConfined()) {
MemorySegment out = arena.allocate(C_INT, len);
int result = (int) renderStateRowCellsGet.invoke(cells, RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_BUF, out);
checkResult("ghostty_render_state_row_cells_get", result);
int[] codepoints = new int[len];
for (int i = 0; i < len; i++) {
codepoints[i] = out.getAtIndex(C_INT, i);
}
return codepoints;
} catch (Throwable t) {
return rethrow(t);
}
}
private int renderStateRowCellsGetI32(MemorySegment cells, int key) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment out = arena.allocate(C_INT);
int result = (int) renderStateRowCellsGet.invoke(cells, key, out);
checkResult("ghostty_render_state_row_cells_get", result);
return out.get(C_INT, 0);
} catch (Throwable t) {
return rethrow(t);
}
}
private boolean renderStateRowCellsGetBoolean(MemorySegment cells, int key) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment out = arena.allocate(C_BOOL);
int result = (int) renderStateRowCellsGet.invoke(cells, key, out);
checkResult("ghostty_render_state_row_cells_get", result);
return out.get(C_BOOL, 0);
} catch (Throwable t) {
return rethrow(t);
}
}
public void kittyGraphicsPopulatePlacementIterator(MemorySegment graphics, MemorySegment iterator) { public void kittyGraphicsPopulatePlacementIterator(MemorySegment graphics, MemorySegment iterator) {
try (Arena arena = Arena.ofConfined()) { try (Arena arena = Arena.ofConfined()) {
MemorySegment out = arena.allocate(C_POINTER); MemorySegment out = arena.allocate(C_POINTER);

View File

@@ -37,6 +37,12 @@ public final class GhosttySmokeTest {
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());
} }
RenderStateSnapshot renderSnapshot = terminal.renderSnapshot();
boolean renderedHello = renderSnapshot.renderRows().stream()
.anyMatch(row -> row.text().contains("hello"));
if (!renderedHello) {
throw new AssertionError("render state should contain written text");
}
TerminalSnapshot snapshot = terminal.snapshot(); TerminalSnapshot snapshot = terminal.snapshot();
if (snapshot.columns() != 80 || snapshot.rows() != 24) { if (snapshot.columns() != 80 || snapshot.rows() != 24) {
throw new AssertionError("unexpected terminal size: " + snapshot); throw new AssertionError("unexpected terminal size: " + snapshot);