jlibghostty
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.
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>/.
Gradle Consumer
After nix build, another Gradle project can consume the generated Maven repository:
repositories {
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")
}
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 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 ...
If your app uses the module path, prefer:
native-image --enable-native-access=dev.jlibghostty ...
For a Nix-built downstream project, the usual shape is:
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
jlibghostty.url = "path:/home/anon/Dev/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")
}
If the app runs on the module path, use:
--enable-native-access=dev.jlibghostty
External Native Library
The library normally loads the bundled native libghostty-vt. To override it:
java -Djlibghostty.library.path=/path/to/libghostty-vt.so ...
or set:
export JLIBGHOSTTY_LIBRARY=/path/to/libghostty-vt.so
Example
try (Terminal terminal = Ghostty.open(TerminalOptions.of(80, 24))) {
terminal.write("hello\r\n");
System.out.println(terminal.snapshot());
}
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()) {
placement.image().ifPresent(image -> {
// Hand image.data() and placement.renderInfo() to your renderer.
});
}
}
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.
Development Shell
nix develop
The shell provides Java, Gradle, and JLIBGHOSTTY_LIBRARY pointing at the Nix-built libghostty-vt.