Compare commits

...

2 Commits

Author SHA1 Message Date
Gregor Lohaus
0852e58086 expose cursor from render state 2026-05-27 19:47:28 +02:00
Gregor Lohaus
0a875910a0 expose color from render state 2026-05-27 19:44:03 +02:00
7 changed files with 154 additions and 2 deletions

View File

@@ -1,8 +1,18 @@
package dev.jlibghostty;
public record RenderCell(int column, int[] codepoints, boolean selected) {
import java.util.Optional;
public record RenderCell(
int column,
int[] codepoints,
Optional<RenderColor> foreground,
Optional<RenderColor> background,
boolean selected
) {
public RenderCell {
codepoints = codepoints.clone();
foreground = foreground == null ? Optional.empty() : foreground;
background = background == null ? Optional.empty() : background;
}
@Override

View File

@@ -0,0 +1,16 @@
package dev.jlibghostty;
public record RenderColor(int red, int green, int blue) {
public RenderColor {
red = checkChannel("red", red);
green = checkChannel("green", green);
blue = checkChannel("blue", blue);
}
private static int checkChannel(String name, int value) {
if (value < 0 || value > 255) {
throw new IllegalArgumentException(name + " must be between 0 and 255");
}
return value;
}
}

View File

@@ -0,0 +1,27 @@
package dev.jlibghostty;
public enum RenderCursorStyle {
BAR(0),
BLOCK(1),
UNDERLINE(2),
BLOCK_HOLLOW(3);
private final int nativeValue;
RenderCursorStyle(int nativeValue) {
this.nativeValue = nativeValue;
}
public int nativeValue() {
return nativeValue;
}
static RenderCursorStyle fromNative(int value) {
for (RenderCursorStyle style : values()) {
if (style.nativeValue == value) {
return style;
}
}
throw new IllegalArgumentException("Unknown render cursor style: " + value);
}
}

View File

@@ -24,10 +24,30 @@ public final class RenderState implements AutoCloseable {
public RenderStateSnapshot snapshot() {
ensureOpen();
boolean cursorViewportHasValue = library.renderStateGetBoolean(
handle,
GhosttyLibrary.RENDER_STATE_DATA_CURSOR_VIEWPORT_HAS_VALUE
);
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),
RenderCursorStyle.fromNative(
library.renderStateGetI32(handle, GhosttyLibrary.RENDER_STATE_DATA_CURSOR_VISUAL_STYLE)
),
library.renderStateGetBoolean(handle, GhosttyLibrary.RENDER_STATE_DATA_CURSOR_VISIBLE),
library.renderStateGetBoolean(handle, GhosttyLibrary.RENDER_STATE_DATA_CURSOR_BLINKING),
library.renderStateGetBoolean(handle, GhosttyLibrary.RENDER_STATE_DATA_CURSOR_PASSWORD_INPUT),
cursorViewportHasValue,
cursorViewportHasValue
? library.renderStateGetU16(handle, GhosttyLibrary.RENDER_STATE_DATA_CURSOR_VIEWPORT_X)
: -1,
cursorViewportHasValue
? library.renderStateGetU16(handle, GhosttyLibrary.RENDER_STATE_DATA_CURSOR_VIEWPORT_Y)
: -1,
cursorViewportHasValue
&& library.renderStateGetBoolean(handle, GhosttyLibrary.RENDER_STATE_DATA_CURSOR_VIEWPORT_WIDE_TAIL),
rows()
);
}

View File

@@ -2,7 +2,20 @@ package dev.jlibghostty;
import java.util.List;
public record RenderStateSnapshot(int columns, int rows, int dirty, List<RenderRow> renderRows) {
public record RenderStateSnapshot(
int columns,
int rows,
int dirty,
RenderCursorStyle cursorStyle,
boolean cursorVisible,
boolean cursorBlinking,
boolean cursorPasswordInput,
boolean cursorViewportHasValue,
int cursorViewportX,
int cursorViewportY,
boolean cursorViewportWideTail,
List<RenderRow> renderRows
) {
public RenderStateSnapshot {
renderRows = List.copyOf(renderRows);
}

View File

@@ -4,6 +4,7 @@ import dev.jlibghostty.GhosttyBuildInfo;
import dev.jlibghostty.GhosttyException;
import dev.jlibghostty.OptimizeMode;
import dev.jlibghostty.RenderCell;
import dev.jlibghostty.RenderColor;
import dev.jlibghostty.RenderRow;
import dev.jlibghostty.SizeReportSize;
import dev.jlibghostty.TerminalOptions;
@@ -22,6 +23,7 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import static java.lang.foreign.ValueLayout.JAVA_BYTE;
@@ -68,15 +70,26 @@ public final class GhosttyLibrary {
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_DATA_CURSOR_VISUAL_STYLE = 10;
public static final int RENDER_STATE_DATA_CURSOR_VISIBLE = 11;
public static final int RENDER_STATE_DATA_CURSOR_BLINKING = 12;
public static final int RENDER_STATE_DATA_CURSOR_PASSWORD_INPUT = 13;
public static final int RENDER_STATE_DATA_CURSOR_VIEWPORT_HAS_VALUE = 14;
public static final int RENDER_STATE_DATA_CURSOR_VIEWPORT_X = 15;
public static final int RENDER_STATE_DATA_CURSOR_VIEWPORT_Y = 16;
public static final int RENDER_STATE_DATA_CURSOR_VIEWPORT_WIDE_TAIL = 17;
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_BG_COLOR = 5;
public static final int RENDER_STATE_ROW_CELLS_DATA_FG_COLOR = 6;
public static final int RENDER_STATE_ROW_CELLS_DATA_SELECTED = 7;
private static final int GHOSTTY_SUCCESS = 0;
private static final int GHOSTTY_INVALID_VALUE = -2;
private static final int GHOSTTY_OUT_OF_SPACE = -3;
private static final int GHOSTTY_NO_VALUE = -4;
@@ -622,6 +635,17 @@ public final class GhosttyLibrary {
}
}
public boolean renderStateGetBoolean(MemorySegment state, int key) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment out = arena.allocate(C_BOOL);
int result = (int) renderStateGet.invoke(state, key, out);
checkResult("ghostty_render_state_get", result);
return out.get(C_BOOL, 0);
} catch (Throwable t) {
return rethrow(t);
}
}
public List<RenderRow> renderStateRows(MemorySegment state) {
MemorySegment iterator = renderStateRowIteratorNew();
try {
@@ -703,6 +727,8 @@ public final class GhosttyLibrary {
result.add(new RenderCell(
column,
renderStateRowCellGraphemes(cells),
renderStateRowCellColor(cells, RENDER_STATE_ROW_CELLS_DATA_FG_COLOR),
renderStateRowCellColor(cells, RENDER_STATE_ROW_CELLS_DATA_BG_COLOR),
renderStateRowCellsGetBoolean(cells, RENDER_STATE_ROW_CELLS_DATA_SELECTED)
));
column++;
@@ -799,6 +825,24 @@ public final class GhosttyLibrary {
}
}
private Optional<RenderColor> renderStateRowCellColor(MemorySegment cells, int key) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment out = arena.allocate(3, 1);
int result = (int) renderStateRowCellsGet.invoke(cells, key, out);
if (result == GHOSTTY_INVALID_VALUE) {
return Optional.empty();
}
checkResult("ghostty_render_state_row_cells_get", result);
return Optional.of(new RenderColor(
Byte.toUnsignedInt(out.get(JAVA_BYTE, 0)),
Byte.toUnsignedInt(out.get(JAVA_BYTE, 1)),
Byte.toUnsignedInt(out.get(JAVA_BYTE, 2))
));
} catch (Throwable t) {
return rethrow(t);
}
}
public void kittyGraphicsPopulatePlacementIterator(MemorySegment graphics, MemorySegment iterator) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment out = arena.allocate(C_POINTER);

View File

@@ -43,6 +43,20 @@ public final class GhosttySmokeTest {
if (!renderedHello) {
throw new AssertionError("render state should contain written text");
}
if (renderSnapshot.cursorViewportHasValue()) {
if (renderSnapshot.cursorViewportX() < 0 || renderSnapshot.cursorViewportX() >= renderSnapshot.columns()
|| renderSnapshot.cursorViewportY() < 0 || renderSnapshot.cursorViewportY() >= renderSnapshot.rows()) {
throw new AssertionError("cursor viewport position out of bounds: " + renderSnapshot);
}
} else if (renderSnapshot.cursorViewportX() != -1 || renderSnapshot.cursorViewportY() != -1) {
throw new AssertionError("cursor viewport position should be -1 when absent: " + renderSnapshot);
}
renderSnapshot.renderRows().stream()
.flatMap(row -> row.cells().stream())
.forEach(cell -> {
cell.foreground().ifPresent(GhosttySmokeTest::assertColor);
cell.background().ifPresent(GhosttySmokeTest::assertColor);
});
TerminalSnapshot snapshot = terminal.snapshot();
if (snapshot.columns() != 80 || snapshot.rows() != 24) {
throw new AssertionError("unexpected terminal size: " + snapshot);
@@ -50,4 +64,12 @@ public final class GhosttySmokeTest {
terminal.kittyGraphics().ifPresent(KittyGraphics::placements);
}
}
private static void assertColor(RenderColor color) {
if (color.red() < 0 || color.red() > 255
|| color.green() < 0 || color.green() > 255
|| color.blue() < 0 || color.blue() > 255) {
throw new AssertionError("invalid color: " + color);
}
}
}