wip
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
.gradle/
|
||||
build/
|
||||
result
|
||||
result-*
|
||||
*.class
|
||||
*.jar
|
||||
75
README.md
Normal file
75
README.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# jlibghostty
|
||||
|
||||
Java FFM bindings for Ghostty's `libghostty-vt`.
|
||||
|
||||
This targets Java 22+ and uses `java.lang.foreign`, not JNI. The public API is intentionally small while Ghostty's C API is still marked unstable upstream.
|
||||
|
||||
## Build
|
||||
|
||||
```sh
|
||||
nix build
|
||||
```
|
||||
|
||||
The default Nix package builds:
|
||||
|
||||
- `share/java/jlibghostty-0.1.0-SNAPSHOT.jar`
|
||||
- `maven/dev/jlibghostty/jlibghostty/0.1.0-SNAPSHOT/...`
|
||||
|
||||
The jar contains the host platform `libghostty-vt` under `dev/jlibghostty/native/<platform>/`.
|
||||
|
||||
## Gradle Consumer
|
||||
|
||||
After `nix build`, another Gradle project can consume the generated Maven repository:
|
||||
|
||||
```kotlin
|
||||
repositories {
|
||||
maven {
|
||||
url = uri("/home/anon/Dev/jlibghostty/result/maven")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("dev.jlibghostty:jlibghostty:0.1.0-SNAPSHOT")
|
||||
}
|
||||
|
||||
tasks.withType<JavaExec>().configureEach {
|
||||
jvmArgs("--enable-native-access=ALL-UNNAMED")
|
||||
}
|
||||
```
|
||||
|
||||
If the app runs on the module path, use:
|
||||
|
||||
```sh
|
||||
--enable-native-access=dev.jlibghostty
|
||||
```
|
||||
|
||||
## External Native Library
|
||||
|
||||
The library normally loads the bundled native `libghostty-vt`. To override it:
|
||||
|
||||
```sh
|
||||
java -Djlibghostty.library.path=/path/to/libghostty-vt.so ...
|
||||
```
|
||||
|
||||
or set:
|
||||
|
||||
```sh
|
||||
export JLIBGHOSTTY_LIBRARY=/path/to/libghostty-vt.so
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
```java
|
||||
try (Terminal terminal = Ghostty.open(TerminalOptions.of(80, 24))) {
|
||||
terminal.write("hello\r\n");
|
||||
System.out.println(terminal.snapshot());
|
||||
}
|
||||
```
|
||||
|
||||
## Development Shell
|
||||
|
||||
```sh
|
||||
nix develop
|
||||
```
|
||||
|
||||
The shell provides Java, Gradle, and `JLIBGHOSTTY_LIBRARY` pointing at the Nix-built `libghostty-vt`.
|
||||
68
build.gradle.kts
Normal file
68
build.gradle.kts
Normal file
@@ -0,0 +1,68 @@
|
||||
plugins {
|
||||
`java-library`
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
group = "dev.jlibghostty"
|
||||
version = "0.1.0-SNAPSHOT"
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(25))
|
||||
}
|
||||
|
||||
withSourcesJar()
|
||||
}
|
||||
|
||||
tasks.withType<JavaCompile>().configureEach {
|
||||
options.release.set(22)
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
jvmArgs("--enable-native-access=ALL-UNNAMED")
|
||||
}
|
||||
|
||||
fun currentPlatform(): String {
|
||||
val os = System.getProperty("os.name").lowercase()
|
||||
val arch = System.getProperty("os.arch").lowercase()
|
||||
|
||||
val normalizedOs = when {
|
||||
os.contains("linux") -> "linux"
|
||||
os.contains("mac") || os.contains("darwin") -> "macos"
|
||||
else -> error("Unsupported operating system for bundled libghostty-vt: $os")
|
||||
}
|
||||
|
||||
val normalizedArch = when (arch) {
|
||||
"amd64", "x86_64" -> "x86_64"
|
||||
"aarch64", "arm64" -> "aarch64"
|
||||
else -> error("Unsupported architecture for bundled libghostty-vt: $arch")
|
||||
}
|
||||
|
||||
return "$normalizedOs-$normalizedArch"
|
||||
}
|
||||
|
||||
val ghosttyNativeLib = providers
|
||||
.gradleProperty("ghosttyNativeLib")
|
||||
.orElse(providers.environmentVariable("JLIBGHOSTTY_LIBRARY"))
|
||||
|
||||
tasks.processResources {
|
||||
ghosttyNativeLib.orNull?.let { nativeLib ->
|
||||
from(nativeLib) {
|
||||
into("dev/jlibghostty/native/${currentPlatform()}")
|
||||
rename { System.mapLibraryName("ghostty-vt") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
create<MavenPublication>("mavenJava") {
|
||||
from(components["java"])
|
||||
|
||||
pom {
|
||||
name.set("jlibghostty")
|
||||
description.set("Java FFM bindings for libghostty-vt")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
examples/gradle-consumer/build.gradle.kts
Normal file
15
examples/gradle-consumer/build.gradle.kts
Normal file
@@ -0,0 +1,15 @@
|
||||
plugins {
|
||||
application
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("dev.jlibghostty:jlibghostty:0.1.0-SNAPSHOT")
|
||||
}
|
||||
|
||||
application {
|
||||
mainClass.set("example.Main")
|
||||
}
|
||||
|
||||
tasks.withType<JavaExec>().configureEach {
|
||||
jvmArgs("--enable-native-access=ALL-UNNAMED")
|
||||
}
|
||||
18
examples/gradle-consumer/settings.gradle.kts
Normal file
18
examples/gradle-consumer/settings.gradle.kts
Normal file
@@ -0,0 +1,18 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven {
|
||||
url = uri(System.getenv("JLIBGHOSTTY_MAVEN_REPO") ?: "../../result/maven")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "jlibghostty-consumer"
|
||||
19
examples/gradle-consumer/src/main/java/example/Main.java
Normal file
19
examples/gradle-consumer/src/main/java/example/Main.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package example;
|
||||
|
||||
import dev.jlibghostty.Ghostty;
|
||||
import dev.jlibghostty.Terminal;
|
||||
import dev.jlibghostty.TerminalOptions;
|
||||
|
||||
public final class Main {
|
||||
private Main() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("paste safe: " + Ghostty.pasteIsSafe("hello"));
|
||||
|
||||
try (Terminal terminal = Ghostty.open(TerminalOptions.of(80, 24))) {
|
||||
terminal.write("hello from libghostty-vt\r\n");
|
||||
System.out.println(terminal.snapshot());
|
||||
}
|
||||
}
|
||||
}
|
||||
180
flake.lock
generated
Normal file
180
flake.lock
generated
Normal file
@@ -0,0 +1,180 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1761588595,
|
||||
"narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"ghostty": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"home-manager": "home-manager",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"systems": "systems",
|
||||
"zig": "zig",
|
||||
"zon2nix": "zon2nix"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1779812402,
|
||||
"narHash": "sha256-gozJEyJHbaAyrbzODKeWJhxpUrGK6m4DIPDogfjz2BU=",
|
||||
"owner": "ghostty-org",
|
||||
"repo": "ghostty",
|
||||
"rev": "2e5ad917eb4e325a3dbb161c3f41208a8cd35e44",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "ghostty-org",
|
||||
"repo": "ghostty",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"home-manager": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"ghostty",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1770586272,
|
||||
"narHash": "sha256-Ucci8mu8QfxwzyfER2DQDbvW9t1BnTUJhBmY7ybralo=",
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"rev": "b1f916ba052341edc1f80d4b2399f1092a4873ca",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1779560665,
|
||||
"narHash": "sha256-tpyBcxPpcQb8ukyNF7DoCwfSY3VPsxHoYwj00Cayv5o=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "64c08a7ca051951c8eae34e3e3cb1e202fe36786",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"ghostty": "ghostty",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"zig": {
|
||||
"inputs": {
|
||||
"flake-compat": [
|
||||
"ghostty",
|
||||
"flake-compat"
|
||||
],
|
||||
"nixpkgs": [
|
||||
"ghostty",
|
||||
"nixpkgs"
|
||||
],
|
||||
"systems": [
|
||||
"ghostty",
|
||||
"systems"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1776789209,
|
||||
"narHash": "sha256-G6B7Q4TXn7MZ1mB+f9rymjsYF5PLWoSvmbxijb/99bw=",
|
||||
"owner": "mitchellh",
|
||||
"repo": "zig-overlay",
|
||||
"rev": "14fe971844e841297ddd2ce9783d6892b467af39",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "mitchellh",
|
||||
"repo": "zig-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"zig_2": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"ghostty",
|
||||
"zon2nix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1777234348,
|
||||
"narHash": "sha256-fKw44a4qbUuI5eTG8k0gPbqMV5TOrjYF35PBzsYgd2U=",
|
||||
"ref": "refs/heads/main",
|
||||
"rev": "2c781c0609ecda600ab98f98cca417bbd981bd53",
|
||||
"revCount": 1677,
|
||||
"type": "git",
|
||||
"url": "https://codeberg.org/jcollie/zig-overlay.git"
|
||||
},
|
||||
"original": {
|
||||
"type": "git",
|
||||
"url": "https://codeberg.org/jcollie/zig-overlay.git"
|
||||
}
|
||||
},
|
||||
"zon2nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"ghostty",
|
||||
"nixpkgs"
|
||||
],
|
||||
"zig": "zig_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1777314365,
|
||||
"narHash": "sha256-eLxQaD0wc96Neqkln8wHS0rNq/chPODifFkhwrwilEU=",
|
||||
"owner": "jcollie",
|
||||
"repo": "zon2nix",
|
||||
"rev": "a5a1d412ad1ab6305511997bbc92b3a9dd6cb784",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "jcollie",
|
||||
"ref": "main",
|
||||
"repo": "zon2nix",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
164
flake.nix
Normal file
164
flake.nix
Normal file
@@ -0,0 +1,164 @@
|
||||
{
|
||||
description = "Java FFM bindings for libghostty-vt";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
ghostty = {
|
||||
url = "github:ghostty-org/ghostty";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, ghostty }:
|
||||
let
|
||||
systems = [
|
||||
"x86_64-linux"
|
||||
"aarch64-linux"
|
||||
"x86_64-darwin"
|
||||
"aarch64-darwin"
|
||||
];
|
||||
|
||||
forAllSystems = nixpkgs.lib.genAttrs systems;
|
||||
in
|
||||
{
|
||||
packages = forAllSystems (system:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
jdk =
|
||||
if pkgs ? jdk25_headless then pkgs.jdk25_headless
|
||||
else if pkgs ? jdk25 then pkgs.jdk25
|
||||
else if pkgs ? jdk24_headless then pkgs.jdk24_headless
|
||||
else if pkgs ? jdk24 then pkgs.jdk24
|
||||
else pkgs.jdk;
|
||||
|
||||
version = "0.1.0-SNAPSHOT";
|
||||
groupPath = "dev/jlibghostty";
|
||||
artifactId = "jlibghostty";
|
||||
ghosttyVt = ghostty.packages.${system}.libghostty-vt;
|
||||
|
||||
platformName =
|
||||
if system == "x86_64-linux" then "linux-x86_64"
|
||||
else if system == "aarch64-linux" then "linux-aarch64"
|
||||
else if system == "x86_64-darwin" then "macos-x86_64"
|
||||
else if system == "aarch64-darwin" then "macos-aarch64"
|
||||
else throw "unsupported system: ${system}";
|
||||
|
||||
bundledLibraryName =
|
||||
if pkgs.stdenv.hostPlatform.isDarwin then "libghostty-vt.dylib"
|
||||
else "libghostty-vt.so";
|
||||
|
||||
sharedLibraryPattern =
|
||||
if pkgs.stdenv.hostPlatform.isDarwin then "libghostty-vt*.dylib"
|
||||
else "libghostty-vt.so*";
|
||||
|
||||
package = pkgs.stdenvNoCC.mkDerivation {
|
||||
pname = artifactId;
|
||||
inherit version;
|
||||
src = ./.;
|
||||
|
||||
nativeBuildInputs = [ jdk ];
|
||||
|
||||
dontConfigure = true;
|
||||
|
||||
buildPhase = ''
|
||||
runHook preBuild
|
||||
|
||||
mkdir -p build/classes build/test-classes build/resources/${groupPath}/native/${platformName}
|
||||
|
||||
ghostty_lib="$(find ${ghosttyVt} -type f -name '${sharedLibraryPattern}' -print -quit)"
|
||||
if [ -z "$ghostty_lib" ]; then
|
||||
echo "Could not find ${sharedLibraryPattern} in ${ghosttyVt}" >&2
|
||||
find ${ghosttyVt} -maxdepth 4 -type f >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
bundled_lib="build/resources/${groupPath}/native/${platformName}/${bundledLibraryName}"
|
||||
cp "$ghostty_lib" "$bundled_lib"
|
||||
|
||||
find src/main/java -name '*.java' | sort > build/sources.txt
|
||||
javac --release 22 -d build/classes @build/sources.txt
|
||||
|
||||
jar --create \
|
||||
--file build/${artifactId}-${version}.jar \
|
||||
-C build/classes . \
|
||||
-C build/resources .
|
||||
|
||||
jar --create \
|
||||
--file build/${artifactId}-${version}-sources.jar \
|
||||
-C src/main/java .
|
||||
|
||||
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
|
||||
java \
|
||||
--enable-native-access=ALL-UNNAMED \
|
||||
-Djlibghostty.library.path="$bundled_lib" \
|
||||
-cp build/classes:build/test-classes \
|
||||
dev.jlibghostty.GhosttySmokeTest
|
||||
fi
|
||||
|
||||
runHook postBuild
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
|
||||
mkdir -p "$out/share/java" "$out/maven/${groupPath}/${artifactId}/${version}"
|
||||
|
||||
cp build/${artifactId}-${version}.jar "$out/share/java/"
|
||||
cp build/${artifactId}-${version}.jar "$out/maven/${groupPath}/${artifactId}/${version}/"
|
||||
cp build/${artifactId}-${version}-sources.jar "$out/maven/${groupPath}/${artifactId}/${version}/"
|
||||
|
||||
cat > "$out/maven/${groupPath}/${artifactId}/${version}/${artifactId}-${version}.pom" <<'POM'
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>dev.jlibghostty</groupId>
|
||||
<artifactId>jlibghostty</artifactId>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
<name>jlibghostty</name>
|
||||
<description>Java FFM bindings for libghostty-vt</description>
|
||||
</project>
|
||||
POM
|
||||
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
meta = {
|
||||
description = "Java FFM bindings for libghostty-vt";
|
||||
platforms = systems;
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
default = package;
|
||||
jlibghostty = package;
|
||||
mavenRepository = package;
|
||||
});
|
||||
|
||||
devShells = forAllSystems (system:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
jdk =
|
||||
if pkgs ? jdk25_headless then pkgs.jdk25_headless
|
||||
else if pkgs ? jdk25 then pkgs.jdk25
|
||||
else if pkgs ? jdk24_headless then pkgs.jdk24_headless
|
||||
else if pkgs ? jdk24 then pkgs.jdk24
|
||||
else pkgs.jdk;
|
||||
ghosttyVt = ghostty.packages.${system}.libghostty-vt;
|
||||
in
|
||||
{
|
||||
default = pkgs.mkShell {
|
||||
packages =
|
||||
[ jdk pkgs.gradle ]
|
||||
++ pkgs.lib.optional (pkgs ? jextract) pkgs.jextract;
|
||||
|
||||
JLIBGHOSTTY_LIBRARY =
|
||||
if pkgs.stdenv.hostPlatform.isDarwin
|
||||
then "${ghosttyVt}/lib/libghostty-vt.dylib"
|
||||
else "${ghosttyVt}/lib/libghostty-vt.so";
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
15
settings.gradle.kts
Normal file
15
settings.gradle.kts
Normal file
@@ -0,0 +1,15 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "jlibghostty"
|
||||
31
src/main/java/dev/jlibghostty/Ghostty.java
Normal file
31
src/main/java/dev/jlibghostty/Ghostty.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package dev.jlibghostty;
|
||||
|
||||
import dev.jlibghostty.internal.GhosttyLibrary;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public final class Ghostty {
|
||||
private Ghostty() {
|
||||
}
|
||||
|
||||
public static Terminal open(TerminalOptions options) {
|
||||
return Terminal.open(options);
|
||||
}
|
||||
|
||||
public static boolean pasteIsSafe(String text) {
|
||||
return pasteIsSafe(text.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
public static boolean pasteIsSafe(byte[] data) {
|
||||
return GhosttyLibrary.loadDefault().pasteIsSafe(data);
|
||||
}
|
||||
|
||||
public static String encodePaste(String text, boolean bracketed) {
|
||||
byte[] encoded = encodePaste(text.getBytes(StandardCharsets.UTF_8), bracketed);
|
||||
return new String(encoded, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public static byte[] encodePaste(byte[] data, boolean bracketed) {
|
||||
return GhosttyLibrary.loadDefault().pasteEncode(data, bracketed);
|
||||
}
|
||||
}
|
||||
14
src/main/java/dev/jlibghostty/GhosttyException.java
Normal file
14
src/main/java/dev/jlibghostty/GhosttyException.java
Normal file
@@ -0,0 +1,14 @@
|
||||
package dev.jlibghostty;
|
||||
|
||||
public final class GhosttyException extends RuntimeException {
|
||||
private final int resultCode;
|
||||
|
||||
public GhosttyException(String message, int resultCode) {
|
||||
super(message + " failed with libghostty-vt result " + resultCode);
|
||||
this.resultCode = resultCode;
|
||||
}
|
||||
|
||||
public int resultCode() {
|
||||
return resultCode;
|
||||
}
|
||||
}
|
||||
68
src/main/java/dev/jlibghostty/Terminal.java
Normal file
68
src/main/java/dev/jlibghostty/Terminal.java
Normal file
@@ -0,0 +1,68 @@
|
||||
package dev.jlibghostty;
|
||||
|
||||
import dev.jlibghostty.internal.GhosttyLibrary;
|
||||
|
||||
import java.lang.foreign.MemorySegment;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public final class Terminal implements AutoCloseable {
|
||||
private final GhosttyLibrary library;
|
||||
private final MemorySegment handle;
|
||||
private final AtomicBoolean closed = new AtomicBoolean();
|
||||
|
||||
private Terminal(GhosttyLibrary library, MemorySegment handle) {
|
||||
this.library = library;
|
||||
this.handle = handle;
|
||||
}
|
||||
|
||||
public static Terminal open(TerminalOptions options) {
|
||||
GhosttyLibrary library = GhosttyLibrary.loadDefault();
|
||||
return new Terminal(library, library.terminalNew(options));
|
||||
}
|
||||
|
||||
public void write(String vtData) {
|
||||
write(vtData.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
public void write(byte[] vtData) {
|
||||
ensureOpen();
|
||||
library.terminalWrite(handle, vtData);
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
ensureOpen();
|
||||
library.terminalReset(handle);
|
||||
}
|
||||
|
||||
public void resize(int columns, int rows, int cellWidthPx, int cellHeightPx) {
|
||||
ensureOpen();
|
||||
library.terminalResize(handle, columns, rows, cellWidthPx, cellHeightPx);
|
||||
}
|
||||
|
||||
public TerminalSnapshot snapshot() {
|
||||
ensureOpen();
|
||||
return new TerminalSnapshot(
|
||||
library.terminalGetU16(handle, GhosttyLibrary.TERMINAL_DATA_COLS),
|
||||
library.terminalGetU16(handle, GhosttyLibrary.TERMINAL_DATA_ROWS),
|
||||
library.terminalGetU16(handle, GhosttyLibrary.TERMINAL_DATA_CURSOR_X),
|
||||
library.terminalGetU16(handle, GhosttyLibrary.TERMINAL_DATA_CURSOR_Y),
|
||||
library.terminalGetBoolean(handle, GhosttyLibrary.TERMINAL_DATA_CURSOR_VISIBLE),
|
||||
library.terminalGetString(handle, GhosttyLibrary.TERMINAL_DATA_TITLE),
|
||||
library.terminalGetString(handle, GhosttyLibrary.TERMINAL_DATA_PWD)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (closed.compareAndSet(false, true)) {
|
||||
library.terminalFree(handle);
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureOpen() {
|
||||
if (closed.get()) {
|
||||
throw new IllegalStateException("Terminal is closed");
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/main/java/dev/jlibghostty/TerminalOptions.java
Normal file
19
src/main/java/dev/jlibghostty/TerminalOptions.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package dev.jlibghostty;
|
||||
|
||||
public record TerminalOptions(int columns, int rows, long maxScrollback) {
|
||||
public TerminalOptions {
|
||||
if (columns < 1 || columns > 65535) {
|
||||
throw new IllegalArgumentException("columns must be between 1 and 65535");
|
||||
}
|
||||
if (rows < 1 || rows > 65535) {
|
||||
throw new IllegalArgumentException("rows must be between 1 and 65535");
|
||||
}
|
||||
if (maxScrollback < 0) {
|
||||
throw new IllegalArgumentException("maxScrollback must be non-negative");
|
||||
}
|
||||
}
|
||||
|
||||
public static TerminalOptions of(int columns, int rows) {
|
||||
return new TerminalOptions(columns, rows, 10_000);
|
||||
}
|
||||
}
|
||||
12
src/main/java/dev/jlibghostty/TerminalSnapshot.java
Normal file
12
src/main/java/dev/jlibghostty/TerminalSnapshot.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package dev.jlibghostty;
|
||||
|
||||
public record TerminalSnapshot(
|
||||
int columns,
|
||||
int rows,
|
||||
int cursorX,
|
||||
int cursorY,
|
||||
boolean cursorVisible,
|
||||
String title,
|
||||
String workingDirectory
|
||||
) {
|
||||
}
|
||||
297
src/main/java/dev/jlibghostty/internal/GhosttyLibrary.java
Normal file
297
src/main/java/dev/jlibghostty/internal/GhosttyLibrary.java
Normal file
@@ -0,0 +1,297 @@
|
||||
package dev.jlibghostty.internal;
|
||||
|
||||
import dev.jlibghostty.GhosttyException;
|
||||
import dev.jlibghostty.TerminalOptions;
|
||||
|
||||
import java.lang.foreign.Arena;
|
||||
import java.lang.foreign.FunctionDescriptor;
|
||||
import java.lang.foreign.GroupLayout;
|
||||
import java.lang.foreign.Linker;
|
||||
import java.lang.foreign.MemoryLayout;
|
||||
import java.lang.foreign.MemorySegment;
|
||||
import java.lang.foreign.SymbolLookup;
|
||||
import java.lang.foreign.ValueLayout;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static java.lang.foreign.ValueLayout.ADDRESS;
|
||||
import static java.lang.foreign.ValueLayout.JAVA_BOOLEAN;
|
||||
import static java.lang.foreign.ValueLayout.JAVA_BYTE;
|
||||
import static java.lang.foreign.ValueLayout.JAVA_INT;
|
||||
import static java.lang.foreign.ValueLayout.JAVA_SHORT;
|
||||
|
||||
public final class GhosttyLibrary {
|
||||
public static final int TERMINAL_DATA_COLS = 1;
|
||||
public static final int TERMINAL_DATA_ROWS = 2;
|
||||
public static final int TERMINAL_DATA_CURSOR_X = 3;
|
||||
public static final int TERMINAL_DATA_CURSOR_Y = 4;
|
||||
public static final int TERMINAL_DATA_CURSOR_VISIBLE = 7;
|
||||
public static final int TERMINAL_DATA_TITLE = 12;
|
||||
public static final int TERMINAL_DATA_PWD = 13;
|
||||
|
||||
private static final int GHOSTTY_SUCCESS = 0;
|
||||
private static final int GHOSTTY_OUT_OF_SPACE = -3;
|
||||
private static final int GHOSTTY_NO_VALUE = -4;
|
||||
|
||||
private static final Linker LINKER = Linker.nativeLinker();
|
||||
private static final ValueLayout.OfLong C_SIZE_T = sizeTLayout();
|
||||
|
||||
private static final GroupLayout TERMINAL_OPTIONS = MemoryLayout.structLayout(
|
||||
JAVA_SHORT.withName("cols"),
|
||||
JAVA_SHORT.withName("rows"),
|
||||
MemoryLayout.paddingLayout(4),
|
||||
C_SIZE_T.withName("max_scrollback")
|
||||
);
|
||||
|
||||
private static final GroupLayout GHOSTTY_STRING = MemoryLayout.structLayout(
|
||||
ADDRESS.withName("ptr"),
|
||||
C_SIZE_T.withName("len")
|
||||
);
|
||||
|
||||
private final MethodHandle terminalNew;
|
||||
private final MethodHandle terminalFree;
|
||||
private final MethodHandle terminalReset;
|
||||
private final MethodHandle terminalResize;
|
||||
private final MethodHandle terminalVtWrite;
|
||||
private final MethodHandle terminalGet;
|
||||
private final MethodHandle pasteIsSafe;
|
||||
private final MethodHandle pasteEncode;
|
||||
|
||||
private GhosttyLibrary(Path libraryPath) {
|
||||
try {
|
||||
SymbolLookup symbols = SymbolLookup.libraryLookup(libraryPath, Arena.global());
|
||||
|
||||
terminalNew = downcall(symbols, "ghostty_terminal_new",
|
||||
FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS, TERMINAL_OPTIONS));
|
||||
terminalFree = downcall(symbols, "ghostty_terminal_free",
|
||||
FunctionDescriptor.ofVoid(ADDRESS));
|
||||
terminalReset = downcall(symbols, "ghostty_terminal_reset",
|
||||
FunctionDescriptor.ofVoid(ADDRESS));
|
||||
terminalResize = downcall(symbols, "ghostty_terminal_resize",
|
||||
FunctionDescriptor.of(JAVA_INT, ADDRESS, JAVA_SHORT, JAVA_SHORT, JAVA_INT, JAVA_INT));
|
||||
terminalVtWrite = downcall(symbols, "ghostty_terminal_vt_write",
|
||||
FunctionDescriptor.ofVoid(ADDRESS, ADDRESS, C_SIZE_T));
|
||||
terminalGet = downcall(symbols, "ghostty_terminal_get",
|
||||
FunctionDescriptor.of(JAVA_INT, ADDRESS, JAVA_INT, ADDRESS));
|
||||
pasteIsSafe = downcall(symbols, "ghostty_paste_is_safe",
|
||||
FunctionDescriptor.of(JAVA_BOOLEAN, ADDRESS, C_SIZE_T));
|
||||
pasteEncode = downcall(symbols, "ghostty_paste_encode",
|
||||
FunctionDescriptor.of(JAVA_INT, ADDRESS, C_SIZE_T, JAVA_BOOLEAN, ADDRESS, C_SIZE_T, ADDRESS));
|
||||
} catch (IllegalCallerException e) {
|
||||
throw new IllegalStateException(
|
||||
"FFM native access is disabled. Run with --enable-native-access=dev.jlibghostty "
|
||||
+ "when using the module path, or --enable-native-access=ALL-UNNAMED on the classpath.",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static GhosttyLibrary loadDefault() {
|
||||
return Holder.INSTANCE;
|
||||
}
|
||||
|
||||
public MemorySegment terminalNew(TerminalOptions options) {
|
||||
try (Arena arena = Arena.ofConfined()) {
|
||||
MemorySegment out = arena.allocate(ADDRESS);
|
||||
MemorySegment nativeOptions = arena.allocate(TERMINAL_OPTIONS);
|
||||
|
||||
nativeOptions.set(JAVA_SHORT, 0, (short) options.columns());
|
||||
nativeOptions.set(JAVA_SHORT, 2, (short) options.rows());
|
||||
nativeOptions.set(C_SIZE_T, 8, options.maxScrollback());
|
||||
|
||||
int result = (int) terminalNew.invoke(MemorySegment.NULL, out, nativeOptions);
|
||||
checkResult("ghostty_terminal_new", result);
|
||||
|
||||
MemorySegment terminal = out.get(ADDRESS, 0);
|
||||
if (terminal.address() == 0) {
|
||||
throw new IllegalStateException("ghostty_terminal_new returned a null terminal handle");
|
||||
}
|
||||
return terminal;
|
||||
} catch (Throwable t) {
|
||||
return rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
public void terminalFree(MemorySegment terminal) {
|
||||
try {
|
||||
terminalFree.invoke(terminal);
|
||||
} catch (Throwable t) {
|
||||
rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
public void terminalReset(MemorySegment terminal) {
|
||||
try {
|
||||
terminalReset.invoke(terminal);
|
||||
} catch (Throwable t) {
|
||||
rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
public void terminalResize(MemorySegment terminal, int columns, int rows, int cellWidthPx, int cellHeightPx) {
|
||||
try {
|
||||
int result = (int) terminalResize.invoke(
|
||||
terminal,
|
||||
(short) columns,
|
||||
(short) rows,
|
||||
cellWidthPx,
|
||||
cellHeightPx
|
||||
);
|
||||
checkResult("ghostty_terminal_resize", result);
|
||||
} catch (Throwable t) {
|
||||
rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
public void terminalWrite(MemorySegment terminal, byte[] data) {
|
||||
if (data.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (Arena arena = Arena.ofConfined()) {
|
||||
MemorySegment nativeData = arena.allocate(data.length, 1);
|
||||
MemorySegment.copy(data, 0, nativeData, JAVA_BYTE, 0, data.length);
|
||||
terminalVtWrite.invoke(terminal, nativeData, (long) data.length);
|
||||
} catch (Throwable t) {
|
||||
rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
public int terminalGetU16(MemorySegment terminal, int key) {
|
||||
try (Arena arena = Arena.ofConfined()) {
|
||||
MemorySegment out = arena.allocate(JAVA_SHORT);
|
||||
int result = (int) terminalGet.invoke(terminal, key, out);
|
||||
checkResult("ghostty_terminal_get", result);
|
||||
return Short.toUnsignedInt(out.get(JAVA_SHORT, 0));
|
||||
} catch (Throwable t) {
|
||||
return rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean terminalGetBoolean(MemorySegment terminal, int key) {
|
||||
try (Arena arena = Arena.ofConfined()) {
|
||||
MemorySegment out = arena.allocate(JAVA_BOOLEAN);
|
||||
int result = (int) terminalGet.invoke(terminal, key, out);
|
||||
checkResult("ghostty_terminal_get", result);
|
||||
return out.get(JAVA_BOOLEAN, 0);
|
||||
} catch (Throwable t) {
|
||||
return rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
public String terminalGetString(MemorySegment terminal, int key) {
|
||||
try (Arena arena = Arena.ofConfined()) {
|
||||
MemorySegment out = arena.allocate(GHOSTTY_STRING);
|
||||
int result = (int) terminalGet.invoke(terminal, key, out);
|
||||
if (result == GHOSTTY_NO_VALUE) {
|
||||
return "";
|
||||
}
|
||||
checkResult("ghostty_terminal_get", result);
|
||||
|
||||
MemorySegment ptr = out.get(ADDRESS, 0);
|
||||
long len = out.get(C_SIZE_T, 8);
|
||||
if (ptr.address() == 0 || len == 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
byte[] bytes = ptr.reinterpret(len).toArray(JAVA_BYTE);
|
||||
return new String(bytes, StandardCharsets.UTF_8);
|
||||
} catch (Throwable t) {
|
||||
return rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean pasteIsSafe(byte[] data) {
|
||||
if (data.length == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try (Arena arena = Arena.ofConfined()) {
|
||||
MemorySegment nativeData = arena.allocate(data.length, 1);
|
||||
MemorySegment.copy(data, 0, nativeData, JAVA_BYTE, 0, data.length);
|
||||
return (boolean) pasteIsSafe.invoke(nativeData, (long) data.length);
|
||||
} catch (Throwable t) {
|
||||
return rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] pasteEncode(byte[] data, boolean bracketed) {
|
||||
try (Arena arena = Arena.ofConfined()) {
|
||||
MemorySegment nativeData = MemorySegment.NULL;
|
||||
if (data.length > 0) {
|
||||
nativeData = arena.allocate(data.length, 1);
|
||||
MemorySegment.copy(data, 0, nativeData, JAVA_BYTE, 0, data.length);
|
||||
}
|
||||
|
||||
MemorySegment outWritten = arena.allocate(C_SIZE_T);
|
||||
int result = (int) pasteEncode.invoke(
|
||||
nativeData,
|
||||
(long) data.length,
|
||||
bracketed,
|
||||
MemorySegment.NULL,
|
||||
0L,
|
||||
outWritten
|
||||
);
|
||||
if (result != GHOSTTY_SUCCESS && result != GHOSTTY_OUT_OF_SPACE) {
|
||||
checkResult("ghostty_paste_encode", result);
|
||||
}
|
||||
|
||||
long required = outWritten.get(C_SIZE_T, 0);
|
||||
if (required == 0) {
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
MemorySegment out = arena.allocate(required, 1);
|
||||
result = (int) pasteEncode.invoke(
|
||||
nativeData,
|
||||
(long) data.length,
|
||||
bracketed,
|
||||
out,
|
||||
required,
|
||||
outWritten
|
||||
);
|
||||
checkResult("ghostty_paste_encode", result);
|
||||
|
||||
long written = outWritten.get(C_SIZE_T, 0);
|
||||
return out.asSlice(0, written).toArray(JAVA_BYTE);
|
||||
} catch (Throwable t) {
|
||||
return rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
private static MethodHandle downcall(SymbolLookup symbols, String name, FunctionDescriptor descriptor) {
|
||||
MemorySegment symbol = symbols.find(name)
|
||||
.orElseThrow(() -> new UnsatisfiedLinkError("Missing libghostty-vt symbol: " + name));
|
||||
return LINKER.downcallHandle(symbol, descriptor);
|
||||
}
|
||||
|
||||
private static void checkResult(String operation, int result) {
|
||||
if (result != GHOSTTY_SUCCESS) {
|
||||
throw new GhosttyException(operation, result);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> T rethrow(Throwable t) {
|
||||
if (t instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
}
|
||||
if (t instanceof Error error) {
|
||||
throw error;
|
||||
}
|
||||
throw new IllegalStateException("Unexpected libghostty-vt FFM failure", t);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private static final class Holder {
|
||||
private static final GhosttyLibrary INSTANCE = new GhosttyLibrary(NativeLibraryLoader.resolve());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package dev.jlibghostty.internal;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.Locale;
|
||||
|
||||
public final class NativeLibraryLoader {
|
||||
private static final String PROPERTY = "jlibghostty.library.path";
|
||||
private static final String ENVIRONMENT = "JLIBGHOSTTY_LIBRARY";
|
||||
|
||||
private NativeLibraryLoader() {
|
||||
}
|
||||
|
||||
public static Path resolve() {
|
||||
String propertyOverride = System.getProperty(PROPERTY);
|
||||
if (propertyOverride != null && !propertyOverride.isBlank()) {
|
||||
return Path.of(propertyOverride).toAbsolutePath();
|
||||
}
|
||||
|
||||
String environmentOverride = System.getenv(ENVIRONMENT);
|
||||
if (environmentOverride != null && !environmentOverride.isBlank()) {
|
||||
return Path.of(environmentOverride).toAbsolutePath();
|
||||
}
|
||||
|
||||
String resource = "/dev/jlibghostty/native/" + platform() + "/" + System.mapLibraryName("ghostty-vt");
|
||||
URL url = NativeLibraryLoader.class.getResource(resource);
|
||||
if (url == null) {
|
||||
throw new UnsatisfiedLinkError(
|
||||
"Bundled libghostty-vt not found at " + resource
|
||||
+ ". Set -D" + PROPERTY + "=/path/to/" + System.mapLibraryName("ghostty-vt")
|
||||
+ " or " + ENVIRONMENT + " to load an external library."
|
||||
);
|
||||
}
|
||||
|
||||
return extract(resource);
|
||||
}
|
||||
|
||||
private static Path extract(String resource) {
|
||||
try (InputStream in = NativeLibraryLoader.class.getResourceAsStream(resource)) {
|
||||
if (in == null) {
|
||||
throw new UnsatisfiedLinkError("Bundled libghostty-vt resource disappeared: " + resource);
|
||||
}
|
||||
|
||||
Path dir = Files.createTempDirectory("jlibghostty-");
|
||||
Path library = dir.resolve(System.mapLibraryName("ghostty-vt"));
|
||||
Files.copy(in, library, StandardCopyOption.REPLACE_EXISTING);
|
||||
library.toFile().setReadable(true);
|
||||
library.toFile().setExecutable(true);
|
||||
library.toFile().deleteOnExit();
|
||||
dir.toFile().deleteOnExit();
|
||||
return library;
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Could not extract bundled libghostty-vt", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String platform() {
|
||||
return normalizeOs() + "-" + normalizeArch();
|
||||
}
|
||||
|
||||
private static String normalizeOs() {
|
||||
String os = System.getProperty("os.name").toLowerCase(Locale.ROOT);
|
||||
if (os.contains("linux")) {
|
||||
return "linux";
|
||||
}
|
||||
if (os.contains("mac") || os.contains("darwin")) {
|
||||
return "macos";
|
||||
}
|
||||
throw new UnsupportedOperationException("Unsupported operating system for libghostty-vt: " + os);
|
||||
}
|
||||
|
||||
private static String normalizeArch() {
|
||||
String arch = System.getProperty("os.arch").toLowerCase(Locale.ROOT);
|
||||
return switch (arch) {
|
||||
case "amd64", "x86_64" -> "x86_64";
|
||||
case "aarch64", "arm64" -> "aarch64";
|
||||
default -> throw new UnsupportedOperationException("Unsupported architecture for libghostty-vt: " + arch);
|
||||
};
|
||||
}
|
||||
}
|
||||
3
src/main/java/module-info.java
Normal file
3
src/main/java/module-info.java
Normal file
@@ -0,0 +1,3 @@
|
||||
module dev.jlibghostty {
|
||||
exports dev.jlibghostty;
|
||||
}
|
||||
26
src/test/java/dev/jlibghostty/GhosttySmokeTest.java
Normal file
26
src/test/java/dev/jlibghostty/GhosttySmokeTest.java
Normal file
@@ -0,0 +1,26 @@
|
||||
package dev.jlibghostty;
|
||||
|
||||
public final class GhosttySmokeTest {
|
||||
private GhosttySmokeTest() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (!Ghostty.pasteIsSafe("hello")) {
|
||||
throw new AssertionError("simple paste should be safe");
|
||||
}
|
||||
if (Ghostty.pasteIsSafe("rm -rf /\n")) {
|
||||
throw new AssertionError("newline paste should be unsafe");
|
||||
}
|
||||
if (!"hello".equals(Ghostty.encodePaste("hello", false))) {
|
||||
throw new AssertionError("simple paste encoding changed unexpectedly");
|
||||
}
|
||||
|
||||
try (Terminal terminal = Ghostty.open(TerminalOptions.of(80, 24))) {
|
||||
terminal.write("hello\r\n");
|
||||
TerminalSnapshot snapshot = terminal.snapshot();
|
||||
if (snapshot.columns() != 80 || snapshot.rows() != 24) {
|
||||
throw new AssertionError("unexpected terminal size: " + snapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user