jlibghostty
Java FFM bindings for Ghostty's libghostty-vt.
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-vtABI from the Nix flake. Recheck layouts when updating Ghostty.
Build
nix build
The default Nix package builds:
share/java/jlibghostty-0.1.0-SNAPSHOT.jarmaven/dev/jlibghostty/jlibghostty/0.1.0-SNAPSHOT/...
The jar contains the host platform libghostty-vt under:
dev/jlibghostty/native/<platform>/
Supported Nix systems are:
x86_64-linuxaarch64-linuxx86_64-darwinaarch64-darwin
Because this is a flake build, new source files must be tracked by git before Nix sees them:
git add src/main/java src/test/java README.md
nix build
Development Shell
nix develop
The shell provides Java, Gradle, and JLIBGHOSTTY_LIBRARY pointing at the Nix-built libghostty-vt.
For a local Gradle build outside Nix:
./gradlew build -PghosttyNativeLib="$JLIBGHOSTTY_LIBRARY"
Runtime native access is required when running on the classpath:
--enable-native-access=ALL-UNNAMED
If your app runs on the module path, use:
--enable-native-access=dev.jlibghostty
Gradle Consumer
After nix build, another Gradle project can consume the generated Maven repository:
repositories {
mavenCentral()
maven {
url = uri("/home/anon/Dev/jlibghostty/result/maven")
}
}
dependencies {
implementation("dev.jlibghostty:jlibghostty:0.1.0-SNAPSHOT")
}
tasks.withType<JavaExec>().configureEach {
jvmArgs("--enable-native-access=ALL-UNNAMED")
}
There is also a small consumer example in examples/gradle-consumer. After nix build:
cd examples/gradle-consumer
JLIBGHOSTTY_MAVEN_REPO="$PWD/../../result/maven" gradle run
GraalVM Native Image Consumer
jlibghostty ships GraalVM reachability metadata at:
META-INF/native-image/dev.jlibghostty/jlibghostty/reachability-metadata.json
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:
native-image --enable-native-access=ALL-UNNAMED ...
For a Nix-built downstream project, the usual shape is:
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
jlibghostty.url = "git+https://gitea.gregorlohaus.com/gregor/jlibghostty";
};
outputs = { nixpkgs, jlibghostty, ... }:
let
system = "x86_64-linux";
pkgs = import nixpkgs { inherit system; };
jlib = jlibghostty.packages.${system}.jlibghostty;
in {
packages.${system}.default = pkgs.stdenvNoCC.mkDerivation {
pname = "my-native-app";
version = "0.1.0";
src = ./.;
nativeBuildInputs = [
pkgs.graalvmPackages.graalvm-ce
pkgs.gradle
];
buildPhase = ''
export GRADLE_USER_HOME=$TMPDIR/gradle
gradle --offline -PjlibghosttyMavenRepo=${jlib}/maven installDist
native-image \
--enable-native-access=ALL-UNNAMED \
-cp build/install/my-app/lib/'*' \
-o my-app \
com.example.Main
'';
installPhase = ''
mkdir -p "$out/bin"
cp my-app "$out/bin/"
'';
};
};
}
In that downstream Gradle build:
repositories {
mavenCentral()
maven {
url = uri(providers.gradleProperty("jlibghosttyMavenRepo").get())
}
}
dependencies {
implementation("dev.jlibghostty:jlibghostty:0.1.0-SNAPSHOT")
}
Loading Native Libraries
The library normally loads the bundled native libghostty-vt resource from the jar.
To override it with an external library:
java -Djlibghostty.library.path=/path/to/libghostty-vt.so ...
or:
export JLIBGHOSTTY_LIBRARY=/path/to/libghostty-vt.so
Basic Terminal Usage
try (Terminal terminal = Ghostty.open(TerminalOptions.of(80, 24))) {
terminal.write("hello\r\n");
TerminalSnapshot snapshot = terminal.snapshot();
String plainText = terminal.text();
String vtText = terminal.vtText();
String html = terminal.html();
System.out.println(snapshot);
System.out.println(plainText);
}
TerminalOptions.of(columns, rows) defaults to 10_000 scrollback lines. Use the full constructor to choose a different limit:
try (Terminal terminal = Ghostty.open(new TerminalOptions(80, 24, 50_000))) {
terminal.write(outputBytes);
}
Render State
Use renderSnapshot() for renderer-oriented state:
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:
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:
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:
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:
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:
try (Terminal terminal = Ghostty.open(TerminalOptions.of(80, 24))) {
terminal.setKittyImageStorageLimit(64 * 1024 * 1024);
terminal.setKittyImageMediumFile(true);
terminal.setKittyImageMediumTemporaryFile(true);
terminal.setKittyImageMediumSharedMemory(true);
terminal.write(kittyGraphicsSequenceBytes);
for (KittyPlacement placement : terminal.kittyGraphics().orElseThrow().placements()) {
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.
For virtual Kitty placements, combine KittyGraphics.placements() with RenderCell.kittyPlaceholder() when you need exact per-cell placeholder mapping.
Utility Encoders
The static Ghostty facade exposes small helpers:
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)
);
Build metadata and C struct layout JSON are exposed directly:
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:
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.