374 lines
11 KiB
Markdown
374 lines
11 KiB
Markdown
# jlibghostty
|
|
|
|
Java FFM bindings for Ghostty's `libghostty-vt`.
|
|
|
|
This project targets Java 22+ 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
|
|
|
|
```sh
|
|
nix build
|
|
```
|
|
|
|
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:
|
|
|
|
```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
|
|
|
|
After `nix build`, another Gradle project can consume the generated Maven repository:
|
|
|
|
```kotlin
|
|
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`:
|
|
|
|
```sh
|
|
cd examples/gradle-consumer
|
|
JLIBGHOSTTY_MAVEN_REPO="$PWD/../../result/maven" gradle run
|
|
```
|
|
|
|
## GraalVM Native Image Consumer
|
|
|
|
`jlibghostty` ships GraalVM reachability metadata at:
|
|
|
|
```text
|
|
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:
|
|
|
|
```sh
|
|
native-image --enable-native-access=ALL-UNNAMED ...
|
|
```
|
|
|
|
For a Nix-built downstream project, the usual shape is:
|
|
|
|
```nix
|
|
{
|
|
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:
|
|
|
|
```kotlin
|
|
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:
|
|
|
|
```sh
|
|
java -Djlibghostty.library.path=/path/to/libghostty-vt.so ...
|
|
```
|
|
|
|
or:
|
|
|
|
```sh
|
|
export JLIBGHOSTTY_LIBRARY=/path/to/libghostty-vt.so
|
|
```
|
|
|
|
## Basic Terminal Usage
|
|
|
|
```java
|
|
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:
|
|
|
|
```java
|
|
try (Terminal terminal = Ghostty.open(new TerminalOptions(80, 24, 50_000))) {
|
|
terminal.write(outputBytes);
|
|
}
|
|
```
|
|
|
|
## Render State
|
|
|
|
Use `renderSnapshot()` for renderer-oriented state:
|
|
|
|
```java
|
|
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:
|
|
|
|
```java
|
|
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:
|
|
|
|
```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)
|
|
);
|
|
```
|
|
|
|
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.
|