Compare commits
15 Commits
0852e58086
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1cd908e5d0 | |||
| 06a9d5d3ec | |||
| 6a3d5aa0b0 | |||
| 5fdae1e7d5 | |||
|
|
db5ee5d20d | ||
|
|
68121d50b5 | ||
|
|
5bbba354ab | ||
|
|
a534914a9b | ||
|
|
d558d554b3 | ||
|
|
482305a1af | ||
|
|
eeac1d20d6 | ||
|
|
c90ed9f9a5 | ||
|
|
39468748fd | ||
|
|
acdda73c02 | ||
|
|
f44df36687 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -4,3 +4,9 @@ result
|
||||
result-*
|
||||
*.class
|
||||
*.jar
|
||||
.devenv
|
||||
BUG.md
|
||||
.classpath
|
||||
.project
|
||||
.settings
|
||||
bin
|
||||
|
||||
252
README.md
252
README.md
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
10
flake.nix
10
flake.nix
@@ -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"
|
||||
|
||||
68
src/main/java/dev/jlibghostty/DeviceAttributes.java
Normal file
68
src/main/java/dev/jlibghostty/DeviceAttributes.java
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package dev.jlibghostty;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface DeviceAttributesProvider {
|
||||
DeviceAttributes deviceAttributes();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
37
src/main/java/dev/jlibghostty/KeyModifiers.java
Normal file
37
src/main/java/dev/jlibghostty/KeyModifiers.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
));
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
17
src/main/java/dev/jlibghostty/KittyPlaceholder.java
Normal file
17
src/main/java/dev/jlibghostty/KittyPlaceholder.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ public record KittyPlacement(
|
||||
long columns,
|
||||
long rows,
|
||||
int z,
|
||||
Optional<KittyImageSnapshot> image,
|
||||
Optional<KittyImage> image,
|
||||
Optional<KittyRenderInfo> renderInfo
|
||||
) {
|
||||
}
|
||||
|
||||
17
src/main/java/dev/jlibghostty/MouseAction.java
Normal file
17
src/main/java/dev/jlibghostty/MouseAction.java
Normal 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;
|
||||
}
|
||||
}
|
||||
26
src/main/java/dev/jlibghostty/MouseButton.java
Normal file
26
src/main/java/dev/jlibghostty/MouseButton.java
Normal 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;
|
||||
}
|
||||
}
|
||||
61
src/main/java/dev/jlibghostty/MouseEncoder.java
Normal file
61
src/main/java/dev/jlibghostty/MouseEncoder.java
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/main/java/dev/jlibghostty/MouseEncoderSize.java
Normal file
36
src/main/java/dev/jlibghostty/MouseEncoderSize.java
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
32
src/main/java/dev/jlibghostty/MouseInput.java
Normal file
32
src/main/java/dev/jlibghostty/MouseInput.java
Normal 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);
|
||||
}
|
||||
}
|
||||
6
src/main/java/dev/jlibghostty/PtyWriter.java
Normal file
6
src/main/java/dev/jlibghostty/PtyWriter.java
Normal file
@@ -0,0 +1,6 @@
|
||||
package dev.jlibghostty;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface PtyWriter {
|
||||
void write(byte[] bytes);
|
||||
}
|
||||
@@ -7,12 +7,28 @@ public record RenderCell(
|
||||
int[] codepoints,
|
||||
Optional<RenderColor> foreground,
|
||||
Optional<RenderColor> background,
|
||||
boolean selected
|
||||
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
|
||||
|
||||
@@ -22,7 +22,22 @@ 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,
|
||||
@@ -48,7 +63,7 @@ public final class RenderState implements AutoCloseable {
|
||||
: -1,
|
||||
cursorViewportHasValue
|
||||
&& library.renderStateGetBoolean(handle, GhosttyLibrary.RENDER_STATE_DATA_CURSOR_VIEWPORT_WIDE_TAIL),
|
||||
rows()
|
||||
library.renderStateRows(handle, dirtyRowsOnly)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -57,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)) {
|
||||
|
||||
47
src/main/java/dev/jlibghostty/ScrollViewport.java
Normal file
47
src/main/java/dev/jlibghostty/ScrollViewport.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +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;
|
||||
|
||||
@@ -19,9 +26,13 @@ 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;
|
||||
|
||||
@@ -37,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;
|
||||
@@ -82,12 +95,26 @@ public final class GhosttyLibrary {
|
||||
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;
|
||||
@@ -103,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*");
|
||||
@@ -111,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"),
|
||||
@@ -131,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"),
|
||||
@@ -148,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"),
|
||||
@@ -185,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;
|
||||
@@ -202,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;
|
||||
@@ -210,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;
|
||||
@@ -232,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",
|
||||
@@ -266,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",
|
||||
@@ -282,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",
|
||||
@@ -366,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;
|
||||
@@ -402,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);
|
||||
@@ -646,23 +899,112 @@ public final class GhosttyLibrary {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -706,34 +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),
|
||||
renderStateRowCellColor(cells, RENDER_STATE_ROW_CELLS_DATA_FG_COLOR),
|
||||
renderStateRowCellColor(cells, RENDER_STATE_ROW_CELLS_DATA_BG_COLOR),
|
||||
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);
|
||||
}
|
||||
@@ -782,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);
|
||||
|
||||
@@ -803,31 +1149,152 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<RenderColor> renderStateRowCellColor(MemorySegment cells, int key) {
|
||||
public MemorySegment mouseEncoderNew() {
|
||||
try (Arena arena = Arena.ofConfined()) {
|
||||
MemorySegment out = arena.allocate(3, 1);
|
||||
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();
|
||||
@@ -843,6 +1310,75 @@ public final class GhosttyLibrary {
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
@@ -898,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);
|
||||
@@ -1006,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);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
module dev.jlibghostty {
|
||||
requires static org.graalvm.nativeimage;
|
||||
|
||||
exports dev.jlibghostty;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
Args = --features=dev.jlibghostty.GhosttyForeignRegistrationFeature \
|
||||
-H:+UnlockExperimentalVMOptions \
|
||||
-H:+ForeignAPISupport \
|
||||
--enable-native-access=ALL-UNNAMED,dev.jlibghostty
|
||||
@@ -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*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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,17 +35,67 @@ 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());
|
||||
}
|
||||
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 renderedHello = renderSnapshot.renderRows().stream()
|
||||
.anyMatch(row -> row.text().contains("hello"));
|
||||
if (!renderedHello) {
|
||||
throw new AssertionError("render state should contain written text");
|
||||
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()
|
||||
@@ -72,4 +126,34 @@ public final class GhosttySmokeTest {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user