expose render state
This commit is contained in:
20
src/main/java/dev/jlibghostty/RenderCell.java
Normal file
20
src/main/java/dev/jlibghostty/RenderCell.java
Normal 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();
|
||||
}
|
||||
}
|
||||
17
src/main/java/dev/jlibghostty/RenderRow.java
Normal file
17
src/main/java/dev/jlibghostty/RenderRow.java
Normal 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();
|
||||
}
|
||||
}
|
||||
52
src/main/java/dev/jlibghostty/RenderState.java
Normal file
52
src/main/java/dev/jlibghostty/RenderState.java
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/main/java/dev/jlibghostty/RenderStateSnapshot.java
Normal file
9
src/main/java/dev/jlibghostty/RenderStateSnapshot.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,14 @@ public final class Terminal implements AutoCloseable {
|
||||
return library.formatTerminal(handle, format.nativeValue());
|
||||
}
|
||||
|
||||
public RenderStateSnapshot renderSnapshot() {
|
||||
ensureOpen();
|
||||
try (RenderState renderState = new RenderState()) {
|
||||
renderState.update(this);
|
||||
return renderState.snapshot();
|
||||
}
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
ensureOpen();
|
||||
library.terminalReset(handle);
|
||||
@@ -115,4 +123,12 @@ public final class Terminal implements AutoCloseable {
|
||||
throw new IllegalStateException("Terminal is closed");
|
||||
}
|
||||
}
|
||||
|
||||
void ensureOpenForPackage() {
|
||||
ensureOpen();
|
||||
}
|
||||
|
||||
MemorySegment handleForPackage() {
|
||||
return handle;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package dev.jlibghostty.internal;
|
||||
import dev.jlibghostty.GhosttyBuildInfo;
|
||||
import dev.jlibghostty.GhosttyException;
|
||||
import dev.jlibghostty.OptimizeMode;
|
||||
import dev.jlibghostty.RenderCell;
|
||||
import dev.jlibghostty.RenderRow;
|
||||
import dev.jlibghostty.SizeReportSize;
|
||||
import dev.jlibghostty.TerminalOptions;
|
||||
|
||||
@@ -18,6 +20,8 @@ import java.lang.foreign.ValueLayout;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
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_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_OUT_OF_SPACE = -3;
|
||||
private static final int GHOSTTY_NO_VALUE = -4;
|
||||
@@ -169,6 +185,18 @@ public final class GhosttyLibrary {
|
||||
private final MethodHandle formatterTerminalNew;
|
||||
private final MethodHandle formatterFormatBuf;
|
||||
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 kittyGraphicsImage;
|
||||
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));
|
||||
formatterFree = downcall(symbols, "ghostty_formatter_free",
|
||||
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",
|
||||
FunctionDescriptor.of(C_INT, C_POINTER, C_INT, C_POINTER));
|
||||
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) {
|
||||
try (Arena arena = Arena.ofConfined()) {
|
||||
MemorySegment out = arena.allocate(C_POINTER);
|
||||
|
||||
@@ -37,6 +37,12 @@ public final class GhosttySmokeTest {
|
||||
if (!terminal.text().contains("hello")) {
|
||||
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();
|
||||
if (snapshot.columns() != 80 || snapshot.rows() != 24) {
|
||||
throw new AssertionError("unexpected terminal size: " + snapshot);
|
||||
|
||||
Reference in New Issue
Block a user