# 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-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// ``` 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().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 fg = cell.foreground(); Optional 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 image = placement.image(); Optional 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.