Compare commits

..

17 Commits

Author SHA1 Message Date
1cd908e5d0 fix memory leak 2026-06-02 03:26:24 +02:00
06a9d5d3ec improve kitty image data handling 2026-06-01 02:15:54 +02:00
6a3d5aa0b0 gitignore 2026-05-31 22:20:14 +02:00
5fdae1e7d5 avoid redundant copies in render cell marshalling
- store codepoints by reference in RenderCell (accessor still clones on
  read); the constructor clone double-allocated every cell and defeated
  the shared-empty-array optimization
- return rows as an unmodifiable view instead of List.copyOf; the list is
  a fresh local that never escapes
- preallocate row/cell lists to known rows/cols capacity

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 22:16:43 +02:00
Gregor Lohaus
db5ee5d20d reuse scratch buffers when marshalling render cells
Each per-field cell getter wrapped its own Arena.ofConfined() (a native
alloc/free plus session bookkeeping) to hold a few output bytes. With ~6
downcalls per cell that is tens of thousands of confined arenas per frame on a
full screen, which profiling in jprototerm pinned as ~6-7ms/frame and the
dominant render cost.

Allocate the scalar out-segments (int, bool, color, style) plus a growable
graphemes buffer once per snapshot in a single Scratch, confined to the
single marshalling thread, and thread it through the cell getters. Also drop a
redundant List.copyOf of the per-row cell list (RenderRow's constructor already
makes the immutable copy) and reuse a shared empty-codepoints array.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 21:36:21 +02:00
Gregor Lohaus
68121d50b5 styling stuff 2026-05-29 20:32:09 +02:00
Gregor Lohaus
5bbba354ab finalize 2026-05-29 19:40:30 +02:00
Gregor Lohaus
a534914a9b expose dirty on render state for incremental rendering 2026-05-29 13:33:58 +02:00
Gregor Lohaus
d558d554b3 bytecode v25 2026-05-29 12:29:36 +02:00
Gregor Lohaus
482305a1af fix reachability metadata drift 2026-05-29 10:31:44 +02:00
Gregor Lohaus
eeac1d20d6 fix reachability metadata drift 2026-05-29 09:16:10 +02:00
Gregor Lohaus
c90ed9f9a5 readme update 2026-05-28 13:48:30 +02:00
Gregor Lohaus
39468748fd kitty placeholder mapping 2026-05-28 13:19:27 +02:00
Gregor Lohaus
acdda73c02 scrollback, mouse 2026-05-28 02:13:47 +02:00
Gregor Lohaus
f44df36687 terminal effect callback 2026-05-28 01:30:18 +02:00
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
31 changed files with 1853 additions and 94 deletions

6
.gitignore vendored
View File

@@ -4,3 +4,9 @@ result
result-*
*.class
*.jar
.devenv
BUG.md
.classpath
.project
.settings
bin

1
.ignore Normal file
View File

@@ -0,0 +1 @@
.devenv

252
README.md
View File

@@ -2,7 +2,31 @@
Java FFM bindings for Ghostty's `libghostty-vt`.
This targets Java 22+ and uses `java.lang.foreign`, not JNI. The public API is intentionally small while Ghostty's C API is still marked unstable upstream.
This project targets Java 25+ and uses `java.lang.foreign`, not JNI. The build is Nix-first, but the output is a normal Maven repository that can be consumed from Gradle.
Ghostty's C API is still evolving upstream, so the ergonomic Java API is intentionally focused on the parts needed to embed the VT parser and renderer state in Java applications. The raw native symbol surface is still available through `GhosttyNative`.
## Current Coverage
The public Java API currently wraps:
- Terminal creation, resize, reset, VT input, snapshots, and scrollback viewport movement.
- Terminal text formatting as plain text, VT text, or HTML.
- PTY output callbacks for terminal-generated responses such as DSR and DA.
- Device attributes callbacks.
- Render-state snapshots, including rows, cells, codepoints, fg/bg colors, selection state, cursor state, and cursor viewport position.
- Kitty graphics image and placement inspection, including image data, placement render info, and decoded unicode-placeholder metadata on `RenderCell`.
- Mouse event encoding through Ghostty's mouse encoder.
- Paste safety and paste encoding.
- Focus, mode-report, and size-report encoding.
- Build metadata and Ghostty type/layout JSON.
- Raw exported symbol lookup/downcalls through `GhosttyNative`.
Notable gaps:
- PNG decode callbacks via `ghostty_sys_set(GHOSTTY_SYS_OPT_DECODE_PNG, ...)` are not wrapped yet.
- Some lower-level grid, selection, OSC, SGR, style, and sys APIs are only available through `GhosttyNative`.
- The wrapper follows the pinned `libghostty-vt` ABI from the Nix flake. Recheck layouts when updating Ghostty.
## Build
@@ -15,7 +39,51 @@ The default Nix package builds:
- `share/java/jlibghostty-0.1.0-SNAPSHOT.jar`
- `maven/dev/jlibghostty/jlibghostty/0.1.0-SNAPSHOT/...`
The jar contains the host platform `libghostty-vt` under `dev/jlibghostty/native/<platform>/`.
The jar contains the host platform `libghostty-vt` under:
```text
dev/jlibghostty/native/<platform>/
```
Supported Nix systems are:
- `x86_64-linux`
- `aarch64-linux`
- `x86_64-darwin`
- `aarch64-darwin`
Because this is a flake build, new source files must be tracked by git before Nix sees them:
```sh
git add src/main/java src/test/java README.md
nix build
```
## Development Shell
```sh
nix develop
```
The shell provides Java, Gradle, and `JLIBGHOSTTY_LIBRARY` pointing at the Nix-built `libghostty-vt`.
For a local Gradle build outside Nix:
```sh
./gradlew build -PghosttyNativeLib="$JLIBGHOSTTY_LIBRARY"
```
Runtime native access is required when running on the classpath:
```sh
--enable-native-access=ALL-UNNAMED
```
If your app runs on the module path, use:
```sh
--enable-native-access=dev.jlibghostty
```
## Gradle Consumer
@@ -23,6 +91,7 @@ After `nix build`, another Gradle project can consume the generated Maven reposi
```kotlin
repositories {
mavenCentral()
maven {
url = uri("/home/anon/Dev/jlibghostty/result/maven")
}
@@ -37,6 +106,13 @@ tasks.withType<JavaExec>().configureEach {
}
```
There is also a small consumer example in `examples/gradle-consumer`. After `nix build`:
```sh
cd examples/gradle-consumer
JLIBGHOSTTY_MAVEN_REPO="$PWD/../../result/maven" gradle run
```
## GraalVM Native Image Consumer
`jlibghostty` ships GraalVM reachability metadata at:
@@ -45,25 +121,19 @@ tasks.withType<JavaExec>().configureEach {
META-INF/native-image/dev.jlibghostty/jlibghostty/reachability-metadata.json
```
That metadata registers the FFM downcalls used by this library and includes the bundled native library resource. GraalVM 25 enables Native Image FFM support by default, but the build still needs native access:
That metadata registers the FFM downcalls/upcalls used by this library and includes the bundled native library resource. GraalVM 25 enables Native Image FFM support by default, but the build still needs native access:
```sh
native-image --enable-native-access=ALL-UNNAMED ...
```
If your app uses the module path, prefer:
```sh
native-image --enable-native-access=dev.jlibghostty ...
```
For a Nix-built downstream project, the usual shape is:
```nix
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
jlibghostty.url = "path:/home/anon/Dev/jlibghostty";
jlibghostty.url = "git+https://gitea.gregorlohaus.com/gregor/jlibghostty";
};
outputs = { nixpkgs, jlibghostty, ... }:
@@ -116,55 +186,133 @@ dependencies {
}
```
If the app runs on the module path, use:
## Loading Native Libraries
```sh
--enable-native-access=dev.jlibghostty
```
The library normally loads the bundled native `libghostty-vt` resource from the jar.
## External Native Library
The library normally loads the bundled native `libghostty-vt`. To override it:
To override it with an external library:
```sh
java -Djlibghostty.library.path=/path/to/libghostty-vt.so ...
```
or set:
or:
```sh
export JLIBGHOSTTY_LIBRARY=/path/to/libghostty-vt.so
```
## Example
## Basic Terminal Usage
```java
try (Terminal terminal = Ghostty.open(TerminalOptions.of(80, 24))) {
terminal.write("hello\r\n");
System.out.println(terminal.snapshot());
TerminalSnapshot snapshot = terminal.snapshot();
String plainText = terminal.text();
String vtText = terminal.vtText();
String html = terminal.html();
System.out.println(snapshot);
System.out.println(plainText);
}
```
## Full Native Surface
The public Java API covers the common terminal, paste, build-info, focus, mode-report, size-report, and Kitty graphics paths. For libghostty-vt APIs that do not yet have ergonomic Java wrappers, use `GhosttyNative`:
`TerminalOptions.of(columns, rows)` defaults to `10_000` scrollback lines. Use the full constructor to choose a different limit:
```java
MethodHandle handle = GhosttyNative.downcall(
"ghostty_render_state_new",
FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS)
);
try (Terminal terminal = Ghostty.open(new TerminalOptions(80, 24, 50_000))) {
terminal.write(outputBytes);
}
```
`GhosttyNative.symbolNames()` lists every exported symbol found in the current public headers. Some entries are target-specific, such as WASM helpers, so use `GhosttyNative.findSymbol(name)` when probing optional symbols.
## Render State
Build metadata and C struct layout JSON are exposed directly:
Use `renderSnapshot()` for renderer-oriented state:
```java
GhosttyBuildInfo info = Ghostty.buildInfo();
String typeJson = Ghostty.typeJson();
try (Terminal terminal = Ghostty.open(TerminalOptions.of(80, 24))) {
terminal.write("hello\r\n");
RenderStateSnapshot snapshot = terminal.renderSnapshot();
for (RenderRow row : snapshot.renderRows()) {
for (RenderCell cell : row.cells()) {
int column = cell.column();
int[] codepoints = cell.codepoints();
Optional<RenderColor> fg = cell.foreground();
Optional<RenderColor> bg = cell.background();
boolean selected = cell.selected();
}
}
}
```
`RenderStateSnapshot` also exposes cursor visual style, cursor visibility/blinking, password-input state, and cursor viewport coordinates. If `cursorViewportHasValue()` is false, the cursor viewport coordinates are `-1`.
Cells can include Kitty unicode-placeholder metadata:
```java
cell.kittyPlaceholder().ifPresent(placeholder -> {
long imageId = placeholder.imageId();
long placementId = placeholder.placementId();
long sourceRow = placeholder.sourceRow();
long sourceColumn = placeholder.sourceColumn();
});
```
That is the cell-to-image mapping needed for exact virtual Kitty placement rendering.
## Scrollback Viewport
Scrollback is stored by `libghostty-vt` according to `TerminalOptions.maxScrollback()`. Move the viewport with:
```java
terminal.scrollViewport(ScrollViewport.delta(-3)); // up 3 rows
terminal.scrollViewport(ScrollViewport.delta(3)); // down 3 rows
terminal.scrollViewport(ScrollViewport.top());
terminal.scrollViewport(ScrollViewport.bottom());
```
After scrolling, `terminal.renderSnapshot()` renders the current viewport.
## PTY Responses And Device Attributes
Install a PTY writer if the embedded terminal should answer terminal queries back to the child process:
```java
try (Terminal terminal = Ghostty.open(TerminalOptions.of(80, 24))) {
terminal.setPtyWriter(bytes -> shellSession.send(bytes));
terminal.setDeviceAttributesProvider(DeviceAttributes::xtermCompatible);
terminal.write(childOutputBytes);
}
```
This lets libghostty handle query semantics such as DSR and DA instead of duplicating VT parsing in Java.
## Mouse Input
`MouseEncoder` converts UI mouse events into terminal input bytes according to the mouse modes currently enabled in the terminal:
```java
try (MouseEncoder mouse = new MouseEncoder()) {
mouse.syncFromTerminal(terminal);
mouse.setSize(MouseEncoderSize.of(widthPx, heightPx, cellWidthPx, cellHeightPx));
KeyModifiers mods = KeyModifiers.of(
event.isShiftDown(),
event.isControlDown(),
event.isAltDown(),
event.isMetaDown()
);
byte[] bytes = mouse.encode(MouseInput.press(MouseButton.LEFT, event.getX(), event.getY(), mods));
shellSession.send(bytes);
}
```
Call `syncFromTerminal(terminal)` after terminal mode changes, or before encoding a batch of UI events.
## Kitty Graphics
Kitty graphics storage can be enabled and inspected:
@@ -179,21 +327,47 @@ try (Terminal terminal = Ghostty.open(TerminalOptions.of(80, 24))) {
terminal.write(kittyGraphicsSequenceBytes);
for (KittyPlacement placement : terminal.kittyGraphics().orElseThrow().placements()) {
placement.image().ifPresent(image -> {
// Hand image.data() and placement.renderInfo() to your renderer.
});
Optional<KittyImageSnapshot> image = placement.image();
Optional<KittyRenderInfo> renderInfo = placement.renderInfo();
}
}
```
The Kitty handles returned by `libghostty-vt` are borrowed from the terminal and are invalidated by mutating terminal calls. The Java API returns snapshots for images and placements to make renderer handoff simpler.
PNG decode callbacks from `ghostty_sys_set(GHOSTTY_SYS_OPT_DECODE_PNG, ...)` are not exposed yet. Raw Kitty image formats can be inspected; PNG image ingestion will need a Java callback bridge or a small native helper that allocates decoded RGBA data through Ghostty's allocator.
For virtual Kitty placements, combine `KittyGraphics.placements()` with `RenderCell.kittyPlaceholder()` when you need exact per-cell placeholder mapping.
## Development Shell
## Utility Encoders
```sh
nix develop
The static `Ghostty` facade exposes small helpers:
```java
boolean safe = Ghostty.pasteIsSafe("hello");
String paste = Ghostty.encodePaste("hello", true);
String focus = Ghostty.encodeFocus(FocusEvent.GAINED);
String mode = Ghostty.encodeModeReport(25, ModeReportState.SET);
String size = Ghostty.encodeSizeReport(
SizeReportStyle.CSI_18_T,
new SizeReportSize(24, 80, 8, 16)
);
```
The shell provides Java, Gradle, and `JLIBGHOSTTY_LIBRARY` pointing at the Nix-built `libghostty-vt`.
Build metadata and C struct layout JSON are exposed directly:
```java
GhosttyBuildInfo info = Ghostty.buildInfo();
String typeJson = Ghostty.typeJson();
```
## Raw Native Surface
For libghostty-vt APIs that do not yet have ergonomic Java wrappers, use `GhosttyNative`:
```java
MethodHandle handle = GhosttyNative.downcall(
"ghostty_render_state_new",
FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS)
);
```
`GhosttyNative.symbolNames()` lists exported symbols from the current public headers. Some entries are target-specific, such as WASM helpers, so use `GhosttyNative.findSymbol(name)` when probing optional symbols.

View File

@@ -14,8 +14,18 @@ java {
withSourcesJar()
}
val graalvmHome = providers.environmentVariable("GRAALVM_HOME")
.orElse(providers.systemProperty("java.home"))
val graalNativeImageJmod = graalvmHome.map {
file("$it/jmods/org.graalvm.nativeimage.jmod")
}
dependencies {
compileOnly(files(graalNativeImageJmod))
}
tasks.withType<JavaCompile>().configureEach {
options.release.set(22)
options.release.set(25)
}
tasks.test {

View File

@@ -30,6 +30,7 @@
else if pkgs ? jdk24_headless then pkgs.jdk24_headless
else if pkgs ? jdk24 then pkgs.jdk24
else pkgs.jdk;
graalvm = pkgs.graalvmPackages.graalvm-ce;
version = "0.1.0-SNAPSHOT";
groupPath = "dev/jlibghostty";
@@ -79,7 +80,7 @@
cp "$ghostty_lib" "$bundled_lib"
find src/main/java -name '*.java' | sort > build/sources.txt
javac --release 22 -d build/classes @build/sources.txt
javac --release 25 --module-path ${graalvm}/jmods -d build/classes @build/sources.txt
jar --create \
--file build/${artifactId}-${version}.jar \
@@ -92,7 +93,7 @@
find src/test/java -name '*.java' | sort > build/test-sources.txt
if [ -s build/test-sources.txt ]; then
javac --release 22 -cp build/classes -d build/test-classes @build/test-sources.txt
javac --release 25 -cp build/classes -d build/test-classes @build/test-sources.txt
java \
--enable-native-access=ALL-UNNAMED \
-Djlibghostty.library.path="$bundled_lib" \
@@ -149,14 +150,17 @@ POM
else if pkgs ? jdk24_headless then pkgs.jdk24_headless
else if pkgs ? jdk24 then pkgs.jdk24
else pkgs.jdk;
graalvm = pkgs.graalvmPackages.graalvm-ce;
ghosttyVt = ghostty.packages.${system}.libghostty-vt;
in
{
default = pkgs.mkShell {
packages =
[ jdk pkgs.gradle ]
[ jdk pkgs.gradle graalvm ]
++ pkgs.lib.optional (pkgs ? jextract) pkgs.jextract;
GRAALVM_HOME = "${graalvm}";
JLIBGHOSTTY_LIBRARY =
if pkgs.stdenv.hostPlatform.isDarwin
then "${ghosttyVt}/lib/libghostty-vt.dylib"

View File

@@ -0,0 +1,68 @@
package dev.jlibghostty;
import java.util.Arrays;
public record DeviceAttributes(
int conformanceLevel,
int[] features,
int deviceType,
int firmwareVersion,
int romCartridge,
long unitId
) {
public static final int DA_CONFORMANCE_VT220 = 62;
public static final int DA_FEATURE_ANSI_COLOR = 22;
public static final int DA_FEATURE_CLIPBOARD = 52;
public static final int DA_FEATURE_SELECTIVE_ERASE = 6;
public static final int DA_DEVICE_TYPE_VT220 = 1;
public DeviceAttributes {
checkU16("conformanceLevel", conformanceLevel);
features = features.clone();
if (features.length > 64) {
throw new IllegalArgumentException("features may contain at most 64 entries");
}
for (int feature : features) {
checkU16("feature", feature);
}
checkU16("deviceType", deviceType);
checkU16("firmwareVersion", firmwareVersion);
checkU16("romCartridge", romCartridge);
if (unitId < 0 || unitId > 0xffffffffL) {
throw new IllegalArgumentException("unitId must fit in uint32_t");
}
}
public static DeviceAttributes xtermCompatible() {
return new DeviceAttributes(
DA_CONFORMANCE_VT220,
new int[] { DA_FEATURE_SELECTIVE_ERASE, DA_FEATURE_ANSI_COLOR, DA_FEATURE_CLIPBOARD },
DA_DEVICE_TYPE_VT220,
0,
0,
0
);
}
@Override
public int[] features() {
return features.clone();
}
@Override
public String toString() {
return "DeviceAttributes[conformanceLevel=" + conformanceLevel
+ ", features=" + Arrays.toString(features)
+ ", deviceType=" + deviceType
+ ", firmwareVersion=" + firmwareVersion
+ ", romCartridge=" + romCartridge
+ ", unitId=" + unitId
+ "]";
}
private static void checkU16(String name, int value) {
if (value < 0 || value > 65535) {
throw new IllegalArgumentException(name + " must fit in uint16_t");
}
}
}

View File

@@ -0,0 +1,6 @@
package dev.jlibghostty;
@FunctionalInterface
public interface DeviceAttributesProvider {
DeviceAttributes deviceAttributes();
}

View File

@@ -0,0 +1,134 @@
package dev.jlibghostty;
import org.graalvm.nativeimage.hosted.Feature;
import org.graalvm.nativeimage.hosted.RuntimeForeignAccess;
import java.lang.foreign.AddressLayout;
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.GroupLayout;
import java.lang.foreign.Linker;
import java.lang.foreign.MemoryLayout;
import java.lang.foreign.ValueLayout;
/**
* Registers FFM descriptors for GraalVM releases that do not consume
* foreign.downcalls from reachability-metadata.json.
*/
public final class GhosttyForeignRegistrationFeature implements Feature {
private static final Linker LINKER = Linker.nativeLinker();
private static final AddressLayout C_POINTER = (AddressLayout) LINKER.canonicalLayouts().get("void*");
private static final ValueLayout.OfBoolean C_BOOL = (ValueLayout.OfBoolean) LINKER.canonicalLayouts().get("bool");
private static final ValueLayout.OfShort C_SHORT = (ValueLayout.OfShort) LINKER.canonicalLayouts().get("short");
private static final ValueLayout.OfInt C_INT = (ValueLayout.OfInt) LINKER.canonicalLayouts().get("int");
private static final ValueLayout.OfLong C_LONG_LONG = (ValueLayout.OfLong) LINKER.canonicalLayouts().get("long long");
private static final ValueLayout.OfLong C_SIZE_T = sizeTLayout();
private static final GroupLayout TERMINAL_OPTIONS = MemoryLayout.structLayout(
C_SHORT.withName("cols"),
C_SHORT.withName("rows"),
MemoryLayout.paddingLayout(4),
C_SIZE_T.withName("max_scrollback")
);
private static final GroupLayout SIZE_REPORT_SIZE = MemoryLayout.structLayout(
C_SHORT.withName("rows"),
C_SHORT.withName("columns"),
C_INT.withName("cell_width"),
C_INT.withName("cell_height")
);
private static final GroupLayout SCROLL_VIEWPORT = MemoryLayout.structLayout(
C_INT.withName("tag"),
MemoryLayout.paddingLayout(4),
MemoryLayout.structLayout(
C_LONG_LONG.withName("delta"),
C_LONG_LONG.withName("padding")
).withName("value")
);
private static final GroupLayout MOUSE_POSITION = MemoryLayout.structLayout(
ValueLayout.JAVA_FLOAT.withName("x"),
ValueLayout.JAVA_FLOAT.withName("y")
);
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")
);
@Override
public void duringSetup(DuringSetupAccess access) {
downcall(FunctionDescriptor.of(C_INT, C_POINTER, C_POINTER, TERMINAL_OPTIONS));
downcall(FunctionDescriptor.ofVoid(C_POINTER));
downcall(FunctionDescriptor.of(C_INT, C_POINTER, C_SHORT, C_SHORT, C_INT, C_INT));
downcall(FunctionDescriptor.ofVoid(C_POINTER, SCROLL_VIEWPORT));
downcall(FunctionDescriptor.ofVoid(C_POINTER, C_POINTER, C_SIZE_T));
downcall(FunctionDescriptor.of(C_INT, C_POINTER, C_INT, C_POINTER));
downcall(FunctionDescriptor.of(C_BOOL, C_POINTER, C_SIZE_T));
downcall(FunctionDescriptor.of(C_INT, C_POINTER, C_SIZE_T, C_BOOL, C_POINTER, C_SIZE_T, C_POINTER));
downcall(FunctionDescriptor.of(C_INT, C_INT, C_POINTER));
downcall(FunctionDescriptor.of(C_POINTER));
downcall(FunctionDescriptor.of(C_INT, C_INT, C_POINTER, C_SIZE_T, C_POINTER));
downcall(FunctionDescriptor.of(C_INT, C_INT, C_INT, C_POINTER, C_SIZE_T, C_POINTER));
downcall(FunctionDescriptor.of(C_INT, C_INT, SIZE_REPORT_SIZE, C_POINTER, C_SIZE_T, C_POINTER));
downcall(FunctionDescriptor.of(C_INT, C_POINTER, C_POINTER, C_POINTER, FORMATTER_TERMINAL_OPTIONS));
downcall(FunctionDescriptor.of(C_INT, C_POINTER, C_POINTER, C_SIZE_T, C_POINTER));
downcall(FunctionDescriptor.of(C_INT, C_POINTER, C_POINTER));
downcall(FunctionDescriptor.of(C_BOOL, C_POINTER));
downcall(FunctionDescriptor.of(C_INT, C_POINTER, C_POINTER, C_POINTER, C_POINTER));
downcall(FunctionDescriptor.ofVoid(C_POINTER, C_INT));
downcall(FunctionDescriptor.ofVoid(C_POINTER, C_SHORT));
downcall(FunctionDescriptor.ofVoid(C_POINTER, MOUSE_POSITION));
downcall(FunctionDescriptor.ofVoid(C_POINTER, C_INT, C_POINTER));
downcall(FunctionDescriptor.ofVoid(C_POINTER, C_POINTER));
downcall(FunctionDescriptor.of(C_INT, C_POINTER, C_POINTER, C_POINTER, C_SIZE_T, C_POINTER));
downcall(FunctionDescriptor.of(C_POINTER, C_POINTER, C_INT));
upcall(FunctionDescriptor.ofVoid(C_POINTER, C_POINTER, C_POINTER, C_SIZE_T));
upcall(FunctionDescriptor.of(C_BOOL, C_POINTER, C_POINTER, C_POINTER));
}
private static void downcall(FunctionDescriptor descriptor) {
RuntimeForeignAccess.registerForDowncall(descriptor);
}
private static void upcall(FunctionDescriptor descriptor) {
RuntimeForeignAccess.registerForUpcall(descriptor);
}
private static ValueLayout.OfLong sizeTLayout() {
ValueLayout layout = (ValueLayout) LINKER.canonicalLayouts().get("size_t");
if (layout.byteSize() != Long.BYTES) {
throw new UnsupportedOperationException("jlibghostty currently supports 64-bit platforms only");
}
return (ValueLayout.OfLong) layout;
}
}

View File

@@ -0,0 +1,37 @@
package dev.jlibghostty;
public record KeyModifiers(int mask) {
public static final int SHIFT = 1 << 0;
public static final int CTRL = 1 << 1;
public static final int ALT = 1 << 2;
public static final int SUPER = 1 << 3;
public static final int CAPS_LOCK = 1 << 4;
public static final int NUM_LOCK = 1 << 5;
public KeyModifiers {
if (mask < 0 || mask > 0xffff) {
throw new IllegalArgumentException("modifier mask must fit in uint16_t");
}
}
public static KeyModifiers none() {
return new KeyModifiers(0);
}
public static KeyModifiers of(boolean shift, boolean ctrl, boolean alt, boolean superKey) {
int mask = 0;
if (shift) {
mask |= SHIFT;
}
if (ctrl) {
mask |= CTRL;
}
if (alt) {
mask |= ALT;
}
if (superKey) {
mask |= SUPER;
}
return new KeyModifiers(mask);
}
}

View File

@@ -30,6 +30,14 @@ public final class KittyGraphics {
return placements(KittyPlacementLayer.ALL);
}
// Whether any placement exists, without materializing the placement list or its per-image
// render info. Cheap enough to call every frame to decide the render path.
public boolean isEmpty() {
try (KittyPlacementIterator iterator = KittyPlacementIterator.open(library, graphics, KittyPlacementLayer.ALL)) {
return !iterator.next();
}
}
public List<KittyPlacement> placements(KittyPlacementLayer layer) {
try (KittyPlacementIterator iterator = KittyPlacementIterator.open(library, graphics, layer)) {
List<KittyPlacement> placements = new ArrayList<>();
@@ -53,7 +61,7 @@ public final class KittyGraphics {
iterator.getU32(GhosttyLibrary.KITTY_GRAPHICS_PLACEMENT_DATA_COLUMNS),
iterator.getU32(GhosttyLibrary.KITTY_GRAPHICS_PLACEMENT_DATA_ROWS),
iterator.getI32(GhosttyLibrary.KITTY_GRAPHICS_PLACEMENT_DATA_Z),
image.map(KittyImage::snapshot),
image,
Optional.ofNullable(renderInfo)
));
}

View File

@@ -13,16 +13,46 @@ public final class KittyImage {
this.handle = handle;
}
// Cheap metadata accessors: a single native field read each, no pixel-buffer copy. Use these
// to build cache keys; only call data() once you've decided you actually need the bytes.
public long id() {
return library.kittyImageGetU32(handle, GhosttyLibrary.KITTY_IMAGE_DATA_ID);
}
public long number() {
return library.kittyImageGetU32(handle, GhosttyLibrary.KITTY_IMAGE_DATA_NUMBER);
}
public long width() {
return library.kittyImageGetU32(handle, GhosttyLibrary.KITTY_IMAGE_DATA_WIDTH);
}
public long height() {
return library.kittyImageGetU32(handle, GhosttyLibrary.KITTY_IMAGE_DATA_HEIGHT);
}
public KittyImageFormat format() {
return KittyImageFormat.fromNative(library.kittyImageGetI32(handle, GhosttyLibrary.KITTY_IMAGE_DATA_FORMAT));
}
public KittyImageCompression compression() {
return KittyImageCompression.fromNative(library.kittyImageGetI32(handle, GhosttyLibrary.KITTY_IMAGE_DATA_COMPRESSION));
}
// Byte length of the pixel buffer, read without copying it.
public long dataLength() {
return library.kittyImageDataLength(handle);
}
// Copies the pixel buffer out of native memory. Expensive for large images; call only when
// the decoded image isn't already cached.
public byte[] data() {
return library.kittyImageData(handle);
}
public KittyImageSnapshot snapshot() {
return new KittyImageSnapshot(
library.kittyImageGetU32(handle, GhosttyLibrary.KITTY_IMAGE_DATA_ID),
library.kittyImageGetU32(handle, GhosttyLibrary.KITTY_IMAGE_DATA_NUMBER),
library.kittyImageGetU32(handle, GhosttyLibrary.KITTY_IMAGE_DATA_WIDTH),
library.kittyImageGetU32(handle, GhosttyLibrary.KITTY_IMAGE_DATA_HEIGHT),
KittyImageFormat.fromNative(library.kittyImageGetI32(handle, GhosttyLibrary.KITTY_IMAGE_DATA_FORMAT)),
KittyImageCompression.fromNative(library.kittyImageGetI32(handle, GhosttyLibrary.KITTY_IMAGE_DATA_COMPRESSION)),
library.kittyImageData(handle)
);
return new KittyImageSnapshot(id(), number(), width(), height(), format(), compression(), data());
}
MemorySegment handle() {

View File

@@ -0,0 +1,17 @@
package dev.jlibghostty;
/**
* Decoded Kitty graphics unicode placeholder metadata for a rendered cell.
*/
public record KittyPlaceholder(
long imageId,
long placementId,
long sourceRow,
long sourceColumn
) {
public static final int CODEPOINT = 0x10EEEE;
public boolean hasPlacementId() {
return placementId != 0;
}
}

View File

@@ -15,7 +15,7 @@ public record KittyPlacement(
long columns,
long rows,
int z,
Optional<KittyImageSnapshot> image,
Optional<KittyImage> image,
Optional<KittyRenderInfo> renderInfo
) {
}

View File

@@ -0,0 +1,17 @@
package dev.jlibghostty;
public enum MouseAction {
PRESS(0),
RELEASE(1),
MOTION(2);
private final int nativeValue;
MouseAction(int nativeValue) {
this.nativeValue = nativeValue;
}
public int nativeValue() {
return nativeValue;
}
}

View File

@@ -0,0 +1,26 @@
package dev.jlibghostty;
public enum MouseButton {
UNKNOWN(0),
LEFT(1),
RIGHT(2),
MIDDLE(3),
FOUR(4),
FIVE(5),
SIX(6),
SEVEN(7),
EIGHT(8),
NINE(9),
TEN(10),
ELEVEN(11);
private final int nativeValue;
MouseButton(int nativeValue) {
this.nativeValue = nativeValue;
}
public int nativeValue() {
return nativeValue;
}
}

View File

@@ -0,0 +1,61 @@
package dev.jlibghostty;
import dev.jlibghostty.internal.GhosttyLibrary;
import java.lang.foreign.MemorySegment;
import java.util.concurrent.atomic.AtomicBoolean;
public final class MouseEncoder implements AutoCloseable {
private final GhosttyLibrary library;
private final MemorySegment handle;
private final AtomicBoolean closed = new AtomicBoolean();
public MouseEncoder() {
library = GhosttyLibrary.loadDefault();
handle = library.mouseEncoderNew();
}
public void syncFromTerminal(Terminal terminal) {
ensureOpen();
terminal.ensureOpenForPackage();
library.mouseEncoderSetOptFromTerminal(handle, terminal.handleForPackage());
}
public void setSize(MouseEncoderSize size) {
ensureOpen();
library.mouseEncoderSetSize(handle, size);
}
public void setAnyButtonPressed(boolean pressed) {
ensureOpen();
library.mouseEncoderSetBoolean(handle, GhosttyLibrary.MOUSE_ENCODER_OPT_ANY_BUTTON_PRESSED, pressed);
}
public void setTrackLastCell(boolean enabled) {
ensureOpen();
library.mouseEncoderSetBoolean(handle, GhosttyLibrary.MOUSE_ENCODER_OPT_TRACK_LAST_CELL, enabled);
}
public void reset() {
ensureOpen();
library.mouseEncoderReset(handle);
}
public byte[] encode(MouseInput input) {
ensureOpen();
return library.mouseEncoderEncode(handle, input);
}
@Override
public void close() {
if (closed.compareAndSet(false, true)) {
library.mouseEncoderFree(handle);
}
}
private void ensureOpen() {
if (closed.get()) {
throw new IllegalStateException("MouseEncoder is closed");
}
}
}

View File

@@ -0,0 +1,36 @@
package dev.jlibghostty;
public record MouseEncoderSize(
long screenWidth,
long screenHeight,
long cellWidth,
long cellHeight,
long paddingTop,
long paddingBottom,
long paddingRight,
long paddingLeft
) {
public MouseEncoderSize {
checkU32("screenWidth", screenWidth);
checkU32("screenHeight", screenHeight);
checkU32("cellWidth", cellWidth);
checkU32("cellHeight", cellHeight);
checkU32("paddingTop", paddingTop);
checkU32("paddingBottom", paddingBottom);
checkU32("paddingRight", paddingRight);
checkU32("paddingLeft", paddingLeft);
if (cellWidth == 0 || cellHeight == 0) {
throw new IllegalArgumentException("cell dimensions must be non-zero");
}
}
public static MouseEncoderSize of(long screenWidth, long screenHeight, long cellWidth, long cellHeight) {
return new MouseEncoderSize(screenWidth, screenHeight, cellWidth, cellHeight, 0, 0, 0, 0);
}
private static void checkU32(String name, long value) {
if (value < 0 || value > 0xffffffffL) {
throw new IllegalArgumentException(name + " must fit in uint32_t");
}
}
}

View File

@@ -0,0 +1,32 @@
package dev.jlibghostty;
import java.util.Optional;
public record MouseInput(
MouseAction action,
Optional<MouseButton> button,
KeyModifiers modifiers,
double x,
double y
) {
public MouseInput {
button = button == null ? Optional.empty() : button;
modifiers = modifiers == null ? KeyModifiers.none() : modifiers;
}
public static MouseInput press(MouseButton button, double x, double y, KeyModifiers modifiers) {
return new MouseInput(MouseAction.PRESS, Optional.of(button), modifiers, x, y);
}
public static MouseInput release(MouseButton button, double x, double y, KeyModifiers modifiers) {
return new MouseInput(MouseAction.RELEASE, Optional.of(button), modifiers, x, y);
}
public static MouseInput motion(double x, double y, KeyModifiers modifiers) {
return new MouseInput(MouseAction.MOTION, Optional.empty(), modifiers, x, y);
}
public static MouseInput drag(MouseButton button, double x, double y, KeyModifiers modifiers) {
return new MouseInput(MouseAction.MOTION, Optional.of(button), modifiers, x, y);
}
}

View File

@@ -0,0 +1,6 @@
package dev.jlibghostty;
@FunctionalInterface
public interface PtyWriter {
void write(byte[] bytes);
}

View File

@@ -1,8 +1,34 @@
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,
Optional<KittyPlaceholder> kittyPlaceholder,
boolean selected,
boolean inverse
) {
public RenderCell(
int column,
int[] codepoints,
Optional<RenderColor> foreground,
Optional<RenderColor> background,
boolean selected
) {
this(column, codepoints, foreground, background, Optional.empty(), selected, false);
}
public RenderCell {
codepoints = codepoints.clone();
// Stored by reference: the marshaller hands us freshly allocated (or the shared
// empty) arrays it never mutates, so cloning here would double-allocate every
// cell and defeat the shared-empty optimization. The codepoints() accessor still
// clones on read to protect external callers.
foreground = foreground == null ? Optional.empty() : foreground;
background = background == null ? Optional.empty() : background;
kittyPlaceholder = kittyPlaceholder == null ? Optional.empty() : kittyPlaceholder;
}
@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

@@ -22,13 +22,48 @@ public final class RenderState implements AutoCloseable {
library.renderStateUpdate(handle, terminal.handleForPackage());
}
/** Snapshot with every row's cells fully populated. */
public RenderStateSnapshot snapshot() {
return buildSnapshot(false);
}
/**
* Snapshot in which cells are marshalled only for rows ghostty flagged dirty; clean
* rows get an empty cell list. Intended for a render state reused across frames (paired
* with {@link #resetDirty}) so unchanged rows skip the per-cell marshalling cost. The
* global {@link RenderStateSnapshot#dirty()} still reports FALSE / PARTIAL / FULL.
*/
public RenderStateSnapshot snapshotIncremental() {
return buildSnapshot(true);
}
private RenderStateSnapshot buildSnapshot(boolean dirtyRowsOnly) {
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),
rows()
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),
library.renderStateRows(handle, dirtyRowsOnly)
);
}
@@ -37,6 +72,16 @@ public final class RenderState implements AutoCloseable {
return library.renderStateRows(handle);
}
/**
* Clears the global and per-row dirty flags. Call after rendering a frame so the next
* {@link #update} reports only the rows that change after this point. Required for
* incremental rendering with a render state reused across frames.
*/
public void resetDirty() {
ensureOpen();
library.renderStateResetDirty(handle);
}
@Override
public void close() {
if (closed.compareAndSet(false, true)) {

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

@@ -0,0 +1,47 @@
package dev.jlibghostty;
/**
* Describes how libghostty should move the terminal viewport through scrollback.
*/
public record ScrollViewport(Type type, long delta) {
public ScrollViewport {
if (type == null) {
throw new NullPointerException("type");
}
if (type != Type.DELTA && delta != 0) {
throw new IllegalArgumentException("delta is only valid for DELTA scroll viewport behavior");
}
}
public static ScrollViewport top() {
return new ScrollViewport(Type.TOP, 0);
}
public static ScrollViewport bottom() {
return new ScrollViewport(Type.BOTTOM, 0);
}
public static ScrollViewport delta(long rows) {
return new ScrollViewport(Type.DELTA, rows);
}
public int nativeTag() {
return type.nativeValue();
}
public enum Type {
TOP(0),
BOTTOM(1),
DELTA(2);
private final int nativeValue;
Type(int nativeValue) {
this.nativeValue = nativeValue;
}
int nativeValue() {
return nativeValue;
}
}
}

View File

@@ -2,6 +2,7 @@ package dev.jlibghostty;
import dev.jlibghostty.internal.GhosttyLibrary;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
@@ -11,6 +12,12 @@ public final class Terminal implements AutoCloseable {
private final GhosttyLibrary library;
private final MemorySegment handle;
private final AtomicBoolean closed = new AtomicBoolean();
private Arena ptyWriterArena;
private Arena deviceAttributesArena;
private MemorySegment ptyWriterStub = MemorySegment.NULL;
private MemorySegment deviceAttributesStub = MemorySegment.NULL;
private PtyWriter ptyWriter;
private DeviceAttributesProvider deviceAttributesProvider;
private Terminal(GhosttyLibrary library, MemorySegment handle) {
this.library = library;
@@ -31,6 +38,56 @@ public final class Terminal implements AutoCloseable {
library.terminalWrite(handle, vtData);
}
public void setPtyWriter(PtyWriter writer) {
ensureOpen();
ptyWriter = writer;
// Each upcall stub lives in its own arena so that reassigning (or clearing) the
// writer can free the previous stub. The native pointer is repointed before the old
// arena is closed, so native never holds a dangling stub.
Arena previous = ptyWriterArena;
if (writer == null) {
ptyWriterStub = MemorySegment.NULL;
ptyWriterArena = null;
library.terminalSetPointer(handle, GhosttyLibrary.TERMINAL_OPT_WRITE_PTY, MemorySegment.NULL);
} else {
Arena arena = Arena.ofShared();
ptyWriterStub = library.upcallPtyWriter(writer, arena);
ptyWriterArena = arena;
library.terminalSetPointer(handle, GhosttyLibrary.TERMINAL_OPT_WRITE_PTY, ptyWriterStub);
}
if (previous != null) {
previous.close();
}
}
public void setDeviceAttributesProvider(DeviceAttributesProvider provider) {
ensureOpen();
deviceAttributesProvider = provider;
// See setPtyWriter: a per-stub arena lets reassignment free the previous stub.
Arena previous = deviceAttributesArena;
if (provider == null) {
deviceAttributesStub = MemorySegment.NULL;
deviceAttributesArena = null;
library.terminalSetPointer(handle, GhosttyLibrary.TERMINAL_OPT_DEVICE_ATTRIBUTES, MemorySegment.NULL);
} else {
Arena arena = Arena.ofShared();
deviceAttributesStub = library.upcallDeviceAttributesProvider(provider, arena);
deviceAttributesArena = arena;
library.terminalSetPointer(
handle,
GhosttyLibrary.TERMINAL_OPT_DEVICE_ATTRIBUTES,
deviceAttributesStub
);
}
if (previous != null) {
previous.close();
}
}
public String text() {
return format(TerminalFormat.PLAIN);
}
@@ -66,6 +123,11 @@ public final class Terminal implements AutoCloseable {
library.terminalResize(handle, columns, rows, cellWidthPx, cellHeightPx);
}
public void scrollViewport(ScrollViewport behavior) {
ensureOpen();
library.terminalScrollViewport(handle, behavior);
}
public void setKittyImageStorageLimit(long bytes) {
ensureOpen();
library.terminalSetU64(handle, GhosttyLibrary.TERMINAL_OPT_KITTY_IMAGE_STORAGE_LIMIT, bytes);
@@ -115,6 +177,14 @@ public final class Terminal implements AutoCloseable {
public void close() {
if (closed.compareAndSet(false, true)) {
library.terminalFree(handle);
if (ptyWriterArena != null) {
ptyWriterArena.close();
ptyWriterArena = null;
}
if (deviceAttributesArena != null) {
deviceAttributesArena.close();
deviceAttributesArena = null;
}
}
}

View File

@@ -2,9 +2,17 @@ package dev.jlibghostty.internal;
import dev.jlibghostty.GhosttyBuildInfo;
import dev.jlibghostty.GhosttyException;
import dev.jlibghostty.DeviceAttributes;
import dev.jlibghostty.DeviceAttributesProvider;
import dev.jlibghostty.KittyPlaceholder;
import dev.jlibghostty.MouseEncoderSize;
import dev.jlibghostty.MouseInput;
import dev.jlibghostty.OptimizeMode;
import dev.jlibghostty.PtyWriter;
import dev.jlibghostty.RenderCell;
import dev.jlibghostty.RenderColor;
import dev.jlibghostty.RenderRow;
import dev.jlibghostty.ScrollViewport;
import dev.jlibghostty.SizeReportSize;
import dev.jlibghostty.TerminalOptions;
@@ -18,10 +26,15 @@ import java.lang.foreign.MemorySegment;
import java.lang.foreign.SymbolLookup;
import java.lang.foreign.ValueLayout;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import static java.lang.foreign.ValueLayout.JAVA_BYTE;
@@ -35,6 +48,8 @@ public final class GhosttyLibrary {
public static final int TERMINAL_DATA_PWD = 13;
public static final int TERMINAL_DATA_KITTY_GRAPHICS = 30;
public static final int TERMINAL_OPT_WRITE_PTY = 1;
public static final int TERMINAL_OPT_DEVICE_ATTRIBUTES = 8;
public static final int TERMINAL_OPT_KITTY_IMAGE_STORAGE_LIMIT = 15;
public static final int TERMINAL_OPT_KITTY_IMAGE_MEDIUM_FILE = 16;
public static final int TERMINAL_OPT_KITTY_IMAGE_MEDIUM_TEMP_FILE = 17;
@@ -68,15 +83,40 @@ 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;
// ghostty_render_state_set / _row_set option selectors (both DIRTY == 0).
public static final int RENDER_STATE_OPTION_DIRTY = 0;
public static final int RENDER_STATE_ROW_OPTION_DIRTY = 0;
// GhosttyRenderStateDirty values returned by RENDER_STATE_DATA_DIRTY.
public static final int RENDER_STATE_DIRTY_FALSE = 0;
public static final int RENDER_STATE_DIRTY_PARTIAL = 1;
public static final int RENDER_STATE_DIRTY_FULL = 2;
public static final int RENDER_STATE_ROW_CELLS_DATA_STYLE = 2;
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;
public static final int MOUSE_ENCODER_OPT_SIZE = 2;
public static final int MOUSE_ENCODER_OPT_ANY_BUTTON_PRESSED = 3;
public static final int MOUSE_ENCODER_OPT_TRACK_LAST_CELL = 4;
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;
@@ -90,6 +130,49 @@ public final class GhosttyLibrary {
private static final int BUILD_INFO_VERSION_PATCH = 8;
private static final int BUILD_INFO_VERSION_PRE = 9;
private static final int BUILD_INFO_VERSION_BUILD = 10;
private static final int STYLE_COLOR_TAG_PALETTE = 1;
private static final int STYLE_COLOR_TAG_RGB = 2;
private static final int[] KITTY_PLACEHOLDER_DIACRITICS = {
0x0305, 0x030D, 0x030E, 0x0310, 0x0312, 0x033D, 0x033E, 0x033F,
0x0346, 0x034A, 0x034B, 0x034C, 0x0350, 0x0351, 0x0352, 0x0357,
0x035B, 0x0363, 0x0364, 0x0365, 0x0366, 0x0367, 0x0368, 0x0369,
0x036A, 0x036B, 0x036C, 0x036D, 0x036E, 0x036F, 0x0483, 0x0484,
0x0485, 0x0486, 0x0487, 0x0592, 0x0593, 0x0594, 0x0595, 0x0597,
0x0598, 0x0599, 0x059C, 0x059D, 0x059E, 0x059F, 0x05A0, 0x05A1,
0x05A8, 0x05A9, 0x05AB, 0x05AC, 0x05AF, 0x05C4, 0x0610, 0x0611,
0x0612, 0x0613, 0x0614, 0x0615, 0x0616, 0x0617, 0x0657, 0x0658,
0x0659, 0x065A, 0x065B, 0x065D, 0x065E, 0x06D6, 0x06D7, 0x06D8,
0x06D9, 0x06DA, 0x06DB, 0x06DC, 0x06DF, 0x06E0, 0x06E1, 0x06E2,
0x06E4, 0x06E7, 0x06E8, 0x06EB, 0x06EC, 0x0730, 0x0732, 0x0733,
0x0735, 0x0736, 0x073A, 0x073D, 0x073F, 0x0740, 0x0741, 0x0743,
0x0745, 0x0747, 0x0749, 0x074A, 0x07EB, 0x07EC, 0x07ED, 0x07EE,
0x07EF, 0x07F0, 0x07F1, 0x07F3, 0x0816, 0x0817, 0x0818, 0x0819,
0x081B, 0x081C, 0x081D, 0x081E, 0x081F, 0x0820, 0x0821, 0x0822,
0x0823, 0x0825, 0x0826, 0x0827, 0x0829, 0x082A, 0x082B, 0x082C,
0x082D, 0x0951, 0x0953, 0x0954, 0x0F82, 0x0F83, 0x0F86, 0x0F87,
0x135D, 0x135E, 0x135F, 0x17DD, 0x193A, 0x1A17, 0x1A75, 0x1A76,
0x1A77, 0x1A78, 0x1A79, 0x1A7A, 0x1A7B, 0x1A7C, 0x1B6B, 0x1B6D,
0x1B6E, 0x1B6F, 0x1B70, 0x1B71, 0x1B72, 0x1B73, 0x1CD0, 0x1CD1,
0x1CD2, 0x1CDA, 0x1CDB, 0x1CE0, 0x1DC0, 0x1DC1, 0x1DC3, 0x1DC4,
0x1DC5, 0x1DC6, 0x1DC7, 0x1DC8, 0x1DC9, 0x1DCB, 0x1DCC, 0x1DD1,
0x1DD2, 0x1DD3, 0x1DD4, 0x1DD5, 0x1DD6, 0x1DD7, 0x1DD8, 0x1DD9,
0x1DDA, 0x1DDB, 0x1DDC, 0x1DDD, 0x1DDE, 0x1DDF, 0x1DE0, 0x1DE1,
0x1DE2, 0x1DE3, 0x1DE4, 0x1DE5, 0x1DE6, 0x1DFE, 0x20D0, 0x20D1,
0x20D4, 0x20D5, 0x20D6, 0x20D7, 0x20DB, 0x20DC, 0x20E1, 0x20E7,
0x20E9, 0x20F0, 0x2CEF, 0x2CF0, 0x2CF1, 0x2DE0, 0x2DE1, 0x2DE2,
0x2DE3, 0x2DE4, 0x2DE5, 0x2DE6, 0x2DE7, 0x2DE8, 0x2DE9, 0x2DEA,
0x2DEB, 0x2DEC, 0x2DED, 0x2DEE, 0x2DEF, 0x2DF0, 0x2DF1, 0x2DF2,
0x2DF3, 0x2DF4, 0x2DF5, 0x2DF6, 0x2DF7, 0x2DF8, 0x2DF9, 0x2DFA,
0x2DFB, 0x2DFC, 0x2DFD, 0x2DFE, 0x2DFF, 0xA66F, 0xA67C, 0xA67D,
0xA6F0, 0xA6F1, 0xA8E0, 0xA8E1, 0xA8E2, 0xA8E3, 0xA8E4, 0xA8E5,
0xA8E6, 0xA8E7, 0xA8E8, 0xA8E9, 0xA8EA, 0xA8EB, 0xA8EC, 0xA8ED,
0xA8EE, 0xA8EF, 0xA8F0, 0xA8F1, 0xAAB0, 0xAAB2, 0xAAB3, 0xAAB7,
0xAAB8, 0xAABE, 0xAABF, 0xAAC1, 0xFE20, 0xFE21, 0xFE22, 0xFE23,
0xFE24, 0xFE25, 0xFE26, 0x10A0F, 0x10A38, 0x1D185, 0x1D186,
0x1D187, 0x1D188, 0x1D189, 0x1D1AA, 0x1D1AB, 0x1D1AC, 0x1D1AD,
0x1D242, 0x1D243, 0x1D244
};
private static final Linker LINKER = Linker.nativeLinker();
private static final AddressLayout C_POINTER = (AddressLayout) LINKER.canonicalLayouts().get("void*");
@@ -98,6 +181,14 @@ public final class GhosttyLibrary {
private static final ValueLayout.OfInt C_INT = (ValueLayout.OfInt) LINKER.canonicalLayouts().get("int");
private static final ValueLayout.OfLong C_LONG_LONG = (ValueLayout.OfLong) LINKER.canonicalLayouts().get("long long");
private static final ValueLayout.OfLong C_SIZE_T = sizeTLayout();
private static final MethodHandle PTY_WRITER_UPCALL = staticMethod(
"ptyWriterUpcall",
MethodType.methodType(void.class, PtyWriter.class, MemorySegment.class, MemorySegment.class, MemorySegment.class, long.class)
);
private static final MethodHandle DEVICE_ATTRIBUTES_UPCALL = staticMethod(
"deviceAttributesUpcall",
MethodType.methodType(boolean.class, DeviceAttributesProvider.class, MemorySegment.class, MemorySegment.class, MemorySegment.class)
);
private static final GroupLayout TERMINAL_OPTIONS = MemoryLayout.structLayout(
C_SHORT.withName("cols"),
@@ -118,6 +209,65 @@ public final class GhosttyLibrary {
C_INT.withName("cell_height")
);
private static final GroupLayout STYLE_COLOR_VALUE = MemoryLayout.unionLayout(
JAVA_BYTE.withName("palette"),
MemoryLayout.structLayout(
JAVA_BYTE.withName("r"),
JAVA_BYTE.withName("g"),
JAVA_BYTE.withName("b")
).withName("rgb"),
C_LONG_LONG.withName("padding")
);
private static final GroupLayout STYLE_COLOR = MemoryLayout.structLayout(
C_INT.withName("tag"),
MemoryLayout.paddingLayout(4),
STYLE_COLOR_VALUE.withName("value")
);
private static final GroupLayout STYLE = MemoryLayout.structLayout(
C_SIZE_T.withName("size"),
STYLE_COLOR.withName("fg_color"),
STYLE_COLOR.withName("bg_color"),
STYLE_COLOR.withName("underline_color"),
C_BOOL.withName("bold"),
C_BOOL.withName("italic"),
C_BOOL.withName("faint"),
C_BOOL.withName("blink"),
C_BOOL.withName("inverse"),
C_BOOL.withName("invisible"),
C_BOOL.withName("strikethrough"),
C_BOOL.withName("overline"),
C_INT.withName("underline"),
MemoryLayout.paddingLayout(4)
);
private static final GroupLayout SCROLL_VIEWPORT = MemoryLayout.structLayout(
C_INT.withName("tag"),
MemoryLayout.paddingLayout(4),
MemoryLayout.structLayout(
C_LONG_LONG.withName("delta"),
C_LONG_LONG.withName("padding")
).withName("value")
);
private static final GroupLayout MOUSE_POSITION = MemoryLayout.structLayout(
ValueLayout.JAVA_FLOAT.withName("x"),
ValueLayout.JAVA_FLOAT.withName("y")
);
private static final GroupLayout MOUSE_ENCODER_SIZE = MemoryLayout.structLayout(
C_SIZE_T.withName("size"),
C_INT.withName("screen_width"),
C_INT.withName("screen_height"),
C_INT.withName("cell_width"),
C_INT.withName("cell_height"),
C_INT.withName("padding_top"),
C_INT.withName("padding_bottom"),
C_INT.withName("padding_right"),
C_INT.withName("padding_left")
);
private static final GroupLayout KITTY_RENDER_INFO = MemoryLayout.structLayout(
C_SIZE_T.withName("size"),
C_INT.withName("pixel_width"),
@@ -135,6 +285,35 @@ public final class GhosttyLibrary {
MemoryLayout.paddingLayout(4)
);
// GhosttyStyleColor: { GhosttyStyleColorTag tag; union value; } (see ghostty/vt/style.h).
private static final GroupLayout GHOSTTY_STYLE_COLOR = MemoryLayout.structLayout(
C_INT.withName("tag"),
MemoryLayout.paddingLayout(4),
C_LONG_LONG.withName("value")
);
// GhosttyStyle sized struct. We only read the `inverse` flag, but lay out the whole
// struct so the offset and total size match the C ABI exactly.
private static final GroupLayout GHOSTTY_STYLE = MemoryLayout.structLayout(
C_SIZE_T.withName("size"),
GHOSTTY_STYLE_COLOR.withName("fg_color"),
GHOSTTY_STYLE_COLOR.withName("bg_color"),
GHOSTTY_STYLE_COLOR.withName("underline_color"),
C_BOOL.withName("bold"),
C_BOOL.withName("italic"),
C_BOOL.withName("faint"),
C_BOOL.withName("blink"),
C_BOOL.withName("inverse"),
C_BOOL.withName("invisible"),
C_BOOL.withName("strikethrough"),
C_BOOL.withName("overline"),
C_INT.withName("underline"),
MemoryLayout.paddingLayout(4)
);
private static final long STYLE_INVERSE_OFFSET =
GHOSTTY_STYLE.byteOffset(java.lang.foreign.MemoryLayout.PathElement.groupElement("inverse"));
private static final GroupLayout FORMATTER_SCREEN_EXTRA = MemoryLayout.structLayout(
C_SIZE_T.withName("size"),
C_BOOL.withName("cursor"),
@@ -172,6 +351,7 @@ public final class GhosttyLibrary {
private final MethodHandle terminalFree;
private final MethodHandle terminalReset;
private final MethodHandle terminalResize;
private final MethodHandle terminalScrollViewport;
private final MethodHandle terminalVtWrite;
private final MethodHandle terminalSet;
private final MethodHandle terminalGet;
@@ -189,6 +369,8 @@ public final class GhosttyLibrary {
private final MethodHandle renderStateFree;
private final MethodHandle renderStateUpdate;
private final MethodHandle renderStateGet;
private final MethodHandle renderStateSet;
private final MethodHandle renderStateRowSet;
private final MethodHandle renderStateRowIteratorNew;
private final MethodHandle renderStateRowIteratorFree;
private final MethodHandle renderStateRowIteratorNext;
@@ -197,6 +379,19 @@ public final class GhosttyLibrary {
private final MethodHandle renderStateRowCellsFree;
private final MethodHandle renderStateRowCellsNext;
private final MethodHandle renderStateRowCellsGet;
private final MethodHandle mouseEventNew;
private final MethodHandle mouseEventFree;
private final MethodHandle mouseEventSetAction;
private final MethodHandle mouseEventSetButton;
private final MethodHandle mouseEventClearButton;
private final MethodHandle mouseEventSetMods;
private final MethodHandle mouseEventSetPosition;
private final MethodHandle mouseEncoderNew;
private final MethodHandle mouseEncoderFree;
private final MethodHandle mouseEncoderSetOpt;
private final MethodHandle mouseEncoderSetOptFromTerminal;
private final MethodHandle mouseEncoderReset;
private final MethodHandle mouseEncoderEncode;
private final MethodHandle kittyGraphicsGet;
private final MethodHandle kittyGraphicsImage;
private final MethodHandle kittyGraphicsImageGet;
@@ -219,6 +414,8 @@ public final class GhosttyLibrary {
FunctionDescriptor.ofVoid(C_POINTER));
terminalResize = downcall(symbols, "ghostty_terminal_resize",
FunctionDescriptor.of(C_INT, C_POINTER, C_SHORT, C_SHORT, C_INT, C_INT));
terminalScrollViewport = downcall(symbols, "ghostty_terminal_scroll_viewport",
FunctionDescriptor.ofVoid(C_POINTER, SCROLL_VIEWPORT));
terminalVtWrite = downcall(symbols, "ghostty_terminal_vt_write",
FunctionDescriptor.ofVoid(C_POINTER, C_POINTER, C_SIZE_T));
terminalSet = downcall(symbols, "ghostty_terminal_set",
@@ -253,6 +450,10 @@ public final class GhosttyLibrary {
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));
renderStateSet = downcall(symbols, "ghostty_render_state_set",
FunctionDescriptor.of(C_INT, C_POINTER, C_INT, C_POINTER));
renderStateRowSet = downcall(symbols, "ghostty_render_state_row_set",
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",
@@ -269,6 +470,32 @@ public final class GhosttyLibrary {
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));
mouseEventNew = downcall(symbols, "ghostty_mouse_event_new",
FunctionDescriptor.of(C_INT, C_POINTER, C_POINTER));
mouseEventFree = downcall(symbols, "ghostty_mouse_event_free",
FunctionDescriptor.ofVoid(C_POINTER));
mouseEventSetAction = downcall(symbols, "ghostty_mouse_event_set_action",
FunctionDescriptor.ofVoid(C_POINTER, C_INT));
mouseEventSetButton = downcall(symbols, "ghostty_mouse_event_set_button",
FunctionDescriptor.ofVoid(C_POINTER, C_INT));
mouseEventClearButton = downcall(symbols, "ghostty_mouse_event_clear_button",
FunctionDescriptor.ofVoid(C_POINTER));
mouseEventSetMods = downcall(symbols, "ghostty_mouse_event_set_mods",
FunctionDescriptor.ofVoid(C_POINTER, C_SHORT));
mouseEventSetPosition = downcall(symbols, "ghostty_mouse_event_set_position",
FunctionDescriptor.ofVoid(C_POINTER, MOUSE_POSITION));
mouseEncoderNew = downcall(symbols, "ghostty_mouse_encoder_new",
FunctionDescriptor.of(C_INT, C_POINTER, C_POINTER));
mouseEncoderFree = downcall(symbols, "ghostty_mouse_encoder_free",
FunctionDescriptor.ofVoid(C_POINTER));
mouseEncoderSetOpt = downcall(symbols, "ghostty_mouse_encoder_setopt",
FunctionDescriptor.ofVoid(C_POINTER, C_INT, C_POINTER));
mouseEncoderSetOptFromTerminal = downcall(symbols, "ghostty_mouse_encoder_setopt_from_terminal",
FunctionDescriptor.ofVoid(C_POINTER, C_POINTER));
mouseEncoderReset = downcall(symbols, "ghostty_mouse_encoder_reset",
FunctionDescriptor.ofVoid(C_POINTER));
mouseEncoderEncode = downcall(symbols, "ghostty_mouse_encoder_encode",
FunctionDescriptor.of(C_INT, C_POINTER, C_POINTER, C_POINTER, C_SIZE_T, 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",
@@ -353,6 +580,18 @@ public final class GhosttyLibrary {
}
}
public void terminalScrollViewport(MemorySegment terminal, ScrollViewport behavior) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment nativeBehavior = arena.allocate(SCROLL_VIEWPORT);
nativeBehavior.set(C_INT, 0, behavior.nativeTag());
nativeBehavior.set(C_LONG_LONG, 8, behavior.delta());
nativeBehavior.set(C_LONG_LONG, 16, 0);
terminalScrollViewport.invoke(terminal, nativeBehavior);
} catch (Throwable t) {
rethrow(t);
}
}
public void terminalWrite(MemorySegment terminal, byte[] data) {
if (data.length == 0) {
return;
@@ -389,6 +628,33 @@ public final class GhosttyLibrary {
}
}
public void terminalSetPointer(MemorySegment terminal, int key, MemorySegment value) {
try {
int result = (int) terminalSet.invoke(terminal, key, value);
checkResult("ghostty_terminal_set", result);
} catch (Throwable t) {
rethrow(t);
}
}
public MemorySegment upcallPtyWriter(PtyWriter writer, Arena arena) {
MethodHandle handle = MethodHandles.insertArguments(PTY_WRITER_UPCALL, 0, writer);
return LINKER.upcallStub(
handle,
FunctionDescriptor.ofVoid(C_POINTER, C_POINTER, C_POINTER, C_SIZE_T),
arena
);
}
public MemorySegment upcallDeviceAttributesProvider(DeviceAttributesProvider provider, Arena arena) {
MethodHandle handle = MethodHandles.insertArguments(DEVICE_ATTRIBUTES_UPCALL, 0, provider);
return LINKER.upcallStub(
handle,
FunctionDescriptor.of(C_BOOL, C_POINTER, C_POINTER, C_POINTER),
arena
);
}
public int terminalGetU16(MemorySegment terminal, int key) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment out = arena.allocate(C_SHORT);
@@ -622,23 +888,123 @@ 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);
}
}
private static final int[] EMPTY_CODEPOINTS = new int[0];
/**
* Per-snapshot scratch buffers. A snapshot makes ~6 native downcalls per cell, and each
* field getter used to wrap its own {@link Arena#ofConfined()} (a native alloc/free plus
* session bookkeeping) just to hold a few output bytes — tens of thousands of arenas per
* frame on a full screen. These buffers are allocated once per snapshot and reused for
* every cell. Safe because a snapshot is marshalled on a single thread.
*/
private static final class Scratch {
private final Arena arena;
private final MemorySegment outInt;
private final MemorySegment outBool;
private final MemorySegment outColor;
private final MemorySegment outStyle;
private MemorySegment graphemes;
private int graphemesCapacity;
private Scratch(Arena arena) {
this.arena = arena;
this.outInt = arena.allocate(C_INT);
this.outBool = arena.allocate(C_BOOL);
this.outColor = arena.allocate(3, 1);
this.outStyle = arena.allocate(GHOSTTY_STYLE);
this.graphemesCapacity = 8;
this.graphemes = arena.allocate(C_INT, graphemesCapacity);
}
private MemorySegment graphemes(int len) {
if (len > graphemesCapacity) {
graphemesCapacity = Math.max(len, graphemesCapacity * 2);
graphemes = arena.allocate(C_INT, graphemesCapacity);
}
return graphemes;
}
}
public List<RenderRow> renderStateRows(MemorySegment state) {
return renderStateRows(state, false);
}
/**
* Builds the row list. When {@code dirtyRowsOnly} is true, cells are marshalled only
* for rows ghostty flagged dirty; clean rows get an empty cell list. This lets callers
* that keep a persistent render state skip the (expensive) per-cell marshalling for
* rows that have not changed since the last {@link #renderStateResetDirty}.
*/
public List<RenderRow> renderStateRows(MemorySegment state, boolean dirtyRowsOnly) {
MemorySegment iterator = renderStateRowIteratorNew();
try {
try (Arena scratchArena = Arena.ofConfined()) {
Scratch scratch = new Scratch(scratchArena);
renderStatePopulateRowIterator(state, iterator);
List<RenderRow> rows = new ArrayList<>();
int cols = renderStateGetU16(state, RENDER_STATE_DATA_COLS);
List<RenderRow> rows = new ArrayList<>(renderStateGetU16(state, RENDER_STATE_DATA_ROWS));
int rowIndex = 0;
while (renderStateRowIteratorNext(iterator)) {
boolean dirty = renderStateRowGetBoolean(iterator, RENDER_STATE_ROW_DATA_DIRTY);
rows.add(new RenderRow(rowIndex, dirty, renderStateRowCells(iterator)));
boolean dirty = renderStateRowGetBoolean(iterator, RENDER_STATE_ROW_DATA_DIRTY, scratch);
List<RenderCell> cells = (dirtyRowsOnly && !dirty)
? List.of()
: renderStateRowCells(iterator, scratch, cols);
rows.add(new RenderRow(rowIndex, dirty, cells));
rowIndex++;
}
return List.copyOf(rows);
// rows is a fresh local that never escapes, so an unmodifiable wrapper gives
// the same immutability as List.copyOf without re-copying every row per frame.
return Collections.unmodifiableList(rows);
} finally {
renderStateRowIteratorFree(iterator);
}
}
public void renderStateSetDirty(MemorySegment state, int dirtyValue) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment value = arena.allocate(C_INT);
value.set(C_INT, 0, dirtyValue);
int result = (int) renderStateSet.invoke(state, RENDER_STATE_OPTION_DIRTY, value);
checkResult("ghostty_render_state_set", result);
} catch (Throwable t) {
rethrow(t);
}
}
/**
* Clears the global dirty flag and every per-row dirty flag. The {@code update} call
* does not reset dirty state, so a caller reusing a render state across frames must
* call this after consuming a frame; the next {@code update} then reports only what
* changed since now.
*/
public void renderStateResetDirty(MemorySegment state) {
MemorySegment iterator = renderStateRowIteratorNew();
try (Arena arena = Arena.ofConfined()) {
renderStatePopulateRowIterator(state, iterator);
MemorySegment falseValue = arena.allocate(C_BOOL);
falseValue.set(C_BOOL, 0, false);
while (renderStateRowIteratorNext(iterator)) {
int result = (int) renderStateRowSet.invoke(iterator, RENDER_STATE_ROW_OPTION_DIRTY, falseValue);
checkResult("ghostty_render_state_row_set", result);
}
} catch (Throwable t) {
rethrow(t);
} finally {
renderStateRowIteratorFree(iterator);
}
renderStateSetDirty(state, RENDER_STATE_DIRTY_FALSE);
}
private MemorySegment renderStateRowIteratorNew() {
try (Arena arena = Arena.ofConfined()) {
MemorySegment out = arena.allocate(C_POINTER);
@@ -682,32 +1048,38 @@ public final class GhosttyLibrary {
}
}
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);
private boolean renderStateRowGetBoolean(MemorySegment iterator, int key, Scratch scratch) {
try {
int result = (int) renderStateRowGet.invoke(iterator, key, scratch.outBool);
checkResult("ghostty_render_state_row_get", result);
return out.get(C_BOOL, 0);
return scratch.outBool.get(C_BOOL, 0);
} catch (Throwable t) {
return rethrow(t);
}
}
private List<RenderCell> renderStateRowCells(MemorySegment rowIterator) {
private List<RenderCell> renderStateRowCells(MemorySegment rowIterator, Scratch scratch, int cols) {
MemorySegment cells = renderStateRowCellsNew();
try {
renderStatePopulateRowCells(rowIterator, cells);
List<RenderCell> result = new ArrayList<>();
// Returned raw: RenderRow's constructor already wraps this in an immutable copy,
// so a List.copyOf here would copy the per-cell list a second time every row.
List<RenderCell> result = new ArrayList<>(cols);
int column = 0;
while (renderStateRowCellsNext(cells)) {
int[] codepoints = renderStateRowCellGraphemes(cells, scratch);
result.add(new RenderCell(
column,
renderStateRowCellGraphemes(cells),
renderStateRowCellsGetBoolean(cells, RENDER_STATE_ROW_CELLS_DATA_SELECTED)
codepoints,
renderStateRowCellColor(cells, RENDER_STATE_ROW_CELLS_DATA_FG_COLOR, scratch),
renderStateRowCellColor(cells, RENDER_STATE_ROW_CELLS_DATA_BG_COLOR, scratch),
renderStateRowCellKittyPlaceholder(cells, codepoints),
renderStateRowCellsGetBoolean(cells, RENDER_STATE_ROW_CELLS_DATA_SELECTED, scratch),
renderStateRowCellInverse(cells, scratch)
));
column++;
}
return List.copyOf(result);
return result;
} finally {
renderStateRowCellsFree(cells);
}
@@ -756,14 +1128,14 @@ public final class GhosttyLibrary {
}
}
private int[] renderStateRowCellGraphemes(MemorySegment cells) {
int len = renderStateRowCellsGetI32(cells, RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_LEN);
private int[] renderStateRowCellGraphemes(MemorySegment cells, Scratch scratch) {
int len = renderStateRowCellsGetI32(cells, RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_LEN, scratch);
if (len <= 0) {
return new int[0];
return EMPTY_CODEPOINTS;
}
try (Arena arena = Arena.ofConfined()) {
MemorySegment out = arena.allocate(C_INT, len);
try {
MemorySegment out = scratch.graphemes(len);
int result = (int) renderStateRowCellsGet.invoke(cells, RENDER_STATE_ROW_CELLS_DATA_GRAPHEMES_BUF, out);
checkResult("ghostty_render_state_row_cells_get", result);
@@ -777,28 +1149,236 @@ public final class GhosttyLibrary {
}
}
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);
private int renderStateRowCellsGetI32(MemorySegment cells, int key, Scratch scratch) {
try {
int result = (int) renderStateRowCellsGet.invoke(cells, key, scratch.outInt);
checkResult("ghostty_render_state_row_cells_get", result);
return out.get(C_INT, 0);
return scratch.outInt.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);
private boolean renderStateRowCellsGetBoolean(MemorySegment cells, int key, Scratch scratch) {
try {
int result = (int) renderStateRowCellsGet.invoke(cells, key, scratch.outBool);
checkResult("ghostty_render_state_row_cells_get", result);
return out.get(C_BOOL, 0);
return scratch.outBool.get(C_BOOL, 0);
} catch (Throwable t) {
return rethrow(t);
}
}
public MemorySegment mouseEncoderNew() {
try (Arena arena = Arena.ofConfined()) {
MemorySegment out = arena.allocate(C_POINTER);
int result = (int) mouseEncoderNew.invoke(MemorySegment.NULL, out);
checkResult("ghostty_mouse_encoder_new", result);
MemorySegment encoder = out.get(C_POINTER, 0);
if (encoder.address() == 0) {
throw new IllegalStateException("ghostty_mouse_encoder_new returned null");
}
return encoder;
} catch (Throwable t) {
return rethrow(t);
}
}
public void mouseEncoderFree(MemorySegment encoder) {
try {
mouseEncoderFree.invoke(encoder);
} catch (Throwable t) {
rethrow(t);
}
}
public void mouseEncoderSetOptFromTerminal(MemorySegment encoder, MemorySegment terminal) {
try {
mouseEncoderSetOptFromTerminal.invoke(encoder, terminal);
} catch (Throwable t) {
rethrow(t);
}
}
public void mouseEncoderSetSize(MemorySegment encoder, MouseEncoderSize size) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment nativeSize = arena.allocate(MOUSE_ENCODER_SIZE);
nativeSize.set(C_SIZE_T, 0, MOUSE_ENCODER_SIZE.byteSize());
nativeSize.set(C_INT, 8, (int) size.screenWidth());
nativeSize.set(C_INT, 12, (int) size.screenHeight());
nativeSize.set(C_INT, 16, (int) size.cellWidth());
nativeSize.set(C_INT, 20, (int) size.cellHeight());
nativeSize.set(C_INT, 24, (int) size.paddingTop());
nativeSize.set(C_INT, 28, (int) size.paddingBottom());
nativeSize.set(C_INT, 32, (int) size.paddingRight());
nativeSize.set(C_INT, 36, (int) size.paddingLeft());
mouseEncoderSetOpt.invoke(encoder, MOUSE_ENCODER_OPT_SIZE, nativeSize);
} catch (Throwable t) {
rethrow(t);
}
}
public void mouseEncoderSetBoolean(MemorySegment encoder, int option, boolean value) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment nativeValue = arena.allocate(C_BOOL);
nativeValue.set(C_BOOL, 0, value);
mouseEncoderSetOpt.invoke(encoder, option, nativeValue);
} catch (Throwable t) {
rethrow(t);
}
}
public void mouseEncoderReset(MemorySegment encoder) {
try {
mouseEncoderReset.invoke(encoder);
} catch (Throwable t) {
rethrow(t);
}
}
public byte[] mouseEncoderEncode(MemorySegment encoder, MouseInput input) {
MemorySegment event = mouseEventNew();
try {
configureMouseEvent(event, input);
return encodeBuffer("ghostty_mouse_encoder_encode", (arena, out, outLen, outWritten) ->
(int) mouseEncoderEncode.invoke(encoder, event, out, outLen, outWritten)
);
} finally {
mouseEventFree(event);
}
}
private MemorySegment mouseEventNew() {
try (Arena arena = Arena.ofConfined()) {
MemorySegment out = arena.allocate(C_POINTER);
int result = (int) mouseEventNew.invoke(MemorySegment.NULL, out);
checkResult("ghostty_mouse_event_new", result);
MemorySegment event = out.get(C_POINTER, 0);
if (event.address() == 0) {
throw new IllegalStateException("ghostty_mouse_event_new returned null");
}
return event;
} catch (Throwable t) {
return rethrow(t);
}
}
private void mouseEventFree(MemorySegment event) {
try {
mouseEventFree.invoke(event);
} catch (Throwable t) {
rethrow(t);
}
}
private void configureMouseEvent(MemorySegment event, MouseInput input) {
try (Arena arena = Arena.ofConfined()) {
mouseEventSetAction.invoke(event, input.action().nativeValue());
if (input.button().isPresent()) {
mouseEventSetButton.invoke(event, input.button().orElseThrow().nativeValue());
} else {
mouseEventClearButton.invoke(event);
}
mouseEventSetMods.invoke(event, (short) input.modifiers().mask());
MemorySegment position = arena.allocate(MOUSE_POSITION);
position.set(ValueLayout.JAVA_FLOAT, 0, (float) input.x());
position.set(ValueLayout.JAVA_FLOAT, 4, (float) input.y());
mouseEventSetPosition.invoke(event, position);
} catch (Throwable t) {
rethrow(t);
}
}
private Optional<RenderColor> renderStateRowCellColor(MemorySegment cells, int key, Scratch scratch) {
try {
MemorySegment out = scratch.outColor;
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);
}
}
// Reads the cell's reverse/inverse flag from its GhosttyStyle. The resolved fg/bg
// colors do NOT account for inverse, so a renderer must read this and swap fg/bg.
private boolean renderStateRowCellInverse(MemorySegment cells, Scratch scratch) {
try {
MemorySegment out = scratch.outStyle;
out.set(C_SIZE_T, 0, GHOSTTY_STYLE.byteSize());
int result = (int) renderStateRowCellsGet.invoke(cells, RENDER_STATE_ROW_CELLS_DATA_STYLE, out);
if (result == GHOSTTY_INVALID_VALUE) {
return false;
}
checkResult("ghostty_render_state_row_cells_get", result);
return out.get(C_BOOL, STYLE_INVERSE_OFFSET);
} catch (Throwable t) {
return rethrow(t);
}
}
private Optional<KittyPlaceholder> renderStateRowCellKittyPlaceholder(MemorySegment cells, int[] codepoints) {
if (codepoints.length == 0 || codepoints[0] != KittyPlaceholder.CODEPOINT) {
return Optional.empty();
}
try (Arena arena = Arena.ofConfined()) {
MemorySegment style = arena.allocate(STYLE);
style.set(C_SIZE_T, 0, STYLE.byteSize());
int result = (int) renderStateRowCellsGet.invoke(cells, RENDER_STATE_ROW_CELLS_DATA_STYLE, style);
checkResult("ghostty_render_state_row_cells_get", result);
long imageIdLow = styleColorToKittyId(style, 8);
long placementId = styleColorToKittyId(style, 40);
long sourceRow = kittyPlaceholderDiacriticIndex(codepoints, 1);
long sourceColumn = kittyPlaceholderDiacriticIndex(codepoints, 2);
long imageIdHigh = kittyPlaceholderDiacriticIndex(codepoints, 3);
if (imageIdHigh > 255) {
imageIdHigh = 0;
}
return Optional.of(new KittyPlaceholder(
imageIdLow | (imageIdHigh << 24),
placementId,
sourceRow,
sourceColumn
));
} catch (Throwable t) {
return rethrow(t);
}
}
private static long styleColorToKittyId(MemorySegment style, long colorOffset) {
int tag = style.get(C_INT, colorOffset);
long valueOffset = colorOffset + 8;
return switch (tag) {
case STYLE_COLOR_TAG_PALETTE -> Byte.toUnsignedLong(style.get(JAVA_BYTE, valueOffset));
case STYLE_COLOR_TAG_RGB -> (long) Byte.toUnsignedInt(style.get(JAVA_BYTE, valueOffset)) << 16
| (long) Byte.toUnsignedInt(style.get(JAVA_BYTE, valueOffset + 1)) << 8
| Byte.toUnsignedLong(style.get(JAVA_BYTE, valueOffset + 2));
default -> 0;
};
}
private static long kittyPlaceholderDiacriticIndex(int[] codepoints, int index) {
if (codepoints.length <= index) {
return 0;
}
int result = Arrays.binarySearch(KITTY_PLACEHOLDER_DIACRITICS, codepoints[index]);
return result >= 0 ? result : 0;
}
public void kittyGraphicsPopulatePlacementIterator(MemorySegment graphics, MemorySegment iterator) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment out = arena.allocate(C_POINTER);
@@ -854,6 +1434,19 @@ public final class GhosttyLibrary {
}
}
// Length of the image's pixel buffer without copying it out of native memory. Lets callers
// build a cache key cheaply and only pull the bytes (kittyImageData) on a cache miss.
public long kittyImageDataLength(MemorySegment image) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment outLen = arena.allocate(C_SIZE_T);
int result = (int) kittyGraphicsImageGet.invoke(image, KITTY_IMAGE_DATA_DATA_LEN, outLen);
checkResult("ghostty_kitty_graphics_image_get", result);
return outLen.get(C_SIZE_T, 0);
} catch (Throwable t) {
return rethrow(t);
}
}
public MemorySegment kittyPlacementIteratorNew() {
try (Arena arena = Arena.ofConfined()) {
MemorySegment out = arena.allocate(C_POINTER);
@@ -962,6 +1555,71 @@ public final class GhosttyLibrary {
return LINKER.downcallHandle(symbol, descriptor);
}
private static MethodHandle staticMethod(String name, MethodType type) {
try {
return MethodHandles.lookup().findStatic(GhosttyLibrary.class, name, type);
} catch (NoSuchMethodException | IllegalAccessException e) {
throw new ExceptionInInitializerError(e);
}
}
private static void ptyWriterUpcall(
PtyWriter writer,
MemorySegment terminal,
MemorySegment userdata,
MemorySegment data,
long len
) {
try {
if (data.address() == 0 || len == 0) {
writer.write(new byte[0]);
return;
}
writer.write(data.reinterpret(len).toArray(JAVA_BYTE));
} catch (Throwable ignored) {
}
}
private static boolean deviceAttributesUpcall(
DeviceAttributesProvider provider,
MemorySegment terminal,
MemorySegment userdata,
MemorySegment outAttrs
) {
try {
if (outAttrs.address() == 0) {
return false;
}
DeviceAttributes attrs = provider.deviceAttributes();
if (attrs == null) {
return false;
}
writeDeviceAttributes(outAttrs, attrs);
return true;
} catch (Throwable ignored) {
return false;
}
}
private static void writeDeviceAttributes(MemorySegment out, DeviceAttributes attrs) {
out.reinterpret(160).fill((byte) 0);
out.set(C_SHORT, 0, (short) attrs.conformanceLevel());
int[] features = attrs.features();
for (int i = 0; i < features.length; i++) {
out.set(C_SHORT, 2L + (long) i * Short.BYTES, (short) features[i]);
}
out.set(C_SIZE_T, 136, features.length);
out.set(C_SHORT, 144, (short) attrs.deviceType());
out.set(C_SHORT, 146, (short) attrs.firmwareVersion());
out.set(C_SHORT, 148, (short) attrs.romCartridge());
out.set(C_INT, 152, (int) attrs.unitId());
}
private static void checkResult(String operation, int result) {
if (result != GHOSTTY_SUCCESS) {
throw new GhosttyException(operation, result);

View File

@@ -1,3 +1,5 @@
module dev.jlibghostty {
requires static org.graalvm.nativeimage;
exports dev.jlibghostty;
}

View File

@@ -0,0 +1,4 @@
Args = --features=dev.jlibghostty.GhosttyForeignRegistrationFeature \
-H:+UnlockExperimentalVMOptions \
-H:+ForeignAPISupport \
--enable-native-access=ALL-UNNAMED,dev.jlibghostty

View File

@@ -5,6 +5,25 @@
}
],
"foreign": {
"upcalls": [
{
"returnType": "void",
"parameterTypes": [
"void*",
"void*",
"void*",
"size_t"
]
},
{
"returnType": "bool",
"parameterTypes": [
"void*",
"void*",
"void*"
]
}
],
"downcalls": [
{
"returnType": "int",
@@ -38,6 +57,13 @@
"size_t"
]
},
{
"returnType": "void",
"parameterTypes": [
"void*",
"struct(int, padding(4), struct(long long, long long))"
]
},
{
"returnType": "int",
"parameterTypes": [
@@ -158,6 +184,52 @@
"void*",
"void*"
]
},
{
"returnType": "void",
"parameterTypes": [
"void*",
"int"
]
},
{
"returnType": "void",
"parameterTypes": [
"void*",
"short"
]
},
{
"returnType": "void",
"parameterTypes": [
"void*",
"struct(float, float)"
]
},
{
"returnType": "void",
"parameterTypes": [
"void*",
"int",
"void*"
]
},
{
"returnType": "void",
"parameterTypes": [
"void*",
"void*"
]
},
{
"returnType": "int",
"parameterTypes": [
"void*",
"void*",
"void*",
"size_t",
"void*"
]
}
]
}

View File

@@ -1,5 +1,9 @@
package dev.jlibghostty;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
public final class GhosttySmokeTest {
private GhosttySmokeTest() {
}
@@ -31,18 +35,82 @@ public final class GhosttySmokeTest {
}
try (Terminal terminal = Ghostty.open(TerminalOptions.of(80, 24))) {
ByteArrayOutputStream ptyResponses = new ByteArrayOutputStream();
terminal.setPtyWriter(ptyResponses::writeBytes);
terminal.setDeviceAttributesProvider(DeviceAttributes::xtermCompatible);
terminal.setKittyImageStorageLimit(1024 * 1024);
terminal.setKittyImageMediumFile(true);
terminal.write("hello\r\n");
terminal.write("hello\r\n\u001b[?1000h\u001b[?1006h\u001b[5n\u001b[6n\u001b[c");
String responses = ptyResponses.toString(StandardCharsets.UTF_8);
if (!responses.contains("\u001b[0n")
|| !responses.contains("\u001b[")
|| !responses.contains("c")) {
throw new AssertionError("expected PTY query responses, got: " + responses);
}
try (MouseEncoder mouseEncoder = new MouseEncoder()) {
mouseEncoder.syncFromTerminal(terminal);
mouseEncoder.setSize(MouseEncoderSize.of(640, 384, 8, 16));
byte[] mouseBytes = mouseEncoder.encode(MouseInput.press(
MouseButton.LEFT,
12,
20,
KeyModifiers.none()
));
if (mouseBytes.length == 0) {
throw new AssertionError("mouse encoder should emit bytes when tracking is enabled");
}
}
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");
terminal.write(kittyPlaceholderFixture());
Optional<KittyPlaceholder> placeholder = terminal.renderSnapshot().renderRows().stream()
.flatMap(row -> row.cells().stream())
.map(RenderCell::kittyPlaceholder)
.flatMap(Optional::stream)
.findFirst();
if (placeholder.isEmpty()) {
throw new AssertionError("expected decoded Kitty unicode placeholder");
}
KittyPlaceholder kittyPlaceholder = placeholder.orElseThrow();
if (kittyPlaceholder.imageId() != 0x010203
|| kittyPlaceholder.placementId() != 0x040506
|| kittyPlaceholder.sourceRow() != 1
|| kittyPlaceholder.sourceColumn() != 2) {
throw new AssertionError("unexpected Kitty placeholder metadata: " + kittyPlaceholder);
}
terminal.write(scrollbackFixture());
terminal.scrollViewport(ScrollViewport.delta(-10));
String scrolledText = renderText(terminal.renderSnapshot());
if (scrolledText.contains("scroll-line-39")) {
throw new AssertionError("scrolled viewport should move away from bottom");
}
terminal.scrollViewport(ScrollViewport.bottom());
if (!renderText(terminal.renderSnapshot()).contains("scroll-line-39")) {
throw new AssertionError("bottom viewport should show latest text");
}
terminal.scrollViewport(ScrollViewport.top());
terminal.scrollViewport(ScrollViewport.bottom());
RenderStateSnapshot renderSnapshot = terminal.renderSnapshot();
boolean renderedLatestLine = renderSnapshot.renderRows().stream()
.anyMatch(row -> row.text().contains("scroll-line-39"));
if (!renderedLatestLine) {
throw new AssertionError("render state should contain latest 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 +118,42 @@ 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);
}
}
private static String renderText(RenderStateSnapshot snapshot) {
StringBuilder text = new StringBuilder();
for (RenderRow row : snapshot.renderRows()) {
text.append(row.text()).append('\n');
}
return text.toString();
}
private static String scrollbackFixture() {
StringBuilder text = new StringBuilder();
for (int i = 0; i < 40; i++) {
text.append("scroll-line-").append(i).append("\r\n");
}
return text.toString();
}
private static String kittyPlaceholderFixture() {
return "\u001b[2027h"
+ "\u001b[38;2;1;2;3m"
+ "\u001b[58;2;4;5;6m"
+ codepoint(KittyPlaceholder.CODEPOINT)
+ codepoint(0x030D)
+ codepoint(0x030E)
+ "\u001b[0m\r\n";
}
private static String codepoint(int codepoint) {
return new String(Character.toChars(codepoint));
}
}