Compare commits
36 Commits
80cd318c1c
...
b98a18b49f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b98a18b49f | ||
|
|
08ad025f76 | ||
|
|
c9fb8b5f0a | ||
|
|
3b26a8d12c | ||
|
|
f545375957 | ||
|
|
224e8d0273 | ||
|
|
422b672dec | ||
|
|
2c7f71064c | ||
|
|
829eb66bbc | ||
|
|
8d36fbae35 | ||
|
|
f9454b28db | ||
|
|
f75b8c29e0 | ||
|
|
2816d99ce4 | ||
|
|
a1e0c2b2d4 | ||
|
|
3612de46bd | ||
|
|
d588eb75a6 | ||
|
|
7b8d30a058 | ||
|
|
96a752566b | ||
|
|
6ebf710031 | ||
|
|
3f102a9ede | ||
|
|
57f97e4119 | ||
|
|
163c7b7279 | ||
|
|
8669de2d32 | ||
|
|
a1717438e4 | ||
|
|
d14fa5c1cb | ||
|
|
6e1aff6200 | ||
|
|
6e4ddbf9df | ||
|
|
ee127fd006 | ||
|
|
34ae351431 | ||
|
|
191ec6d0e0 | ||
|
|
19f20a4039 | ||
|
|
0698016a65 | ||
|
|
0a84ec720a | ||
|
|
e3405fee48 | ||
|
|
380996fe50 | ||
|
|
c7f734bf64 |
1
.codexsession
Normal file
1
.codexsession
Normal file
@@ -0,0 +1 @@
|
||||
019e6999-b7c8-7591-a8aa-ea51b89a7f7e
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
.gradle/9.5.1/checksums/checksums.lock
Normal file
BIN
.gradle/9.5.1/checksums/checksums.lock
Normal file
Binary file not shown.
BIN
.gradle/9.5.1/checksums/md5-checksums.bin
Normal file
BIN
.gradle/9.5.1/checksums/md5-checksums.bin
Normal file
Binary file not shown.
BIN
.gradle/9.5.1/checksums/sha1-checksums.bin
Normal file
BIN
.gradle/9.5.1/checksums/sha1-checksums.bin
Normal file
Binary file not shown.
BIN
.gradle/9.5.1/executionHistory/executionHistory.bin
Normal file
BIN
.gradle/9.5.1/executionHistory/executionHistory.bin
Normal file
Binary file not shown.
BIN
.gradle/9.5.1/executionHistory/executionHistory.lock
Normal file
BIN
.gradle/9.5.1/executionHistory/executionHistory.lock
Normal file
Binary file not shown.
BIN
.gradle/9.5.1/expanded/expanded.lock
Normal file
BIN
.gradle/9.5.1/expanded/expanded.lock
Normal file
Binary file not shown.
BIN
.gradle/9.5.1/fileChanges/last-build.bin
Normal file
BIN
.gradle/9.5.1/fileChanges/last-build.bin
Normal file
Binary file not shown.
BIN
.gradle/9.5.1/fileHashes/fileHashes.bin
Normal file
BIN
.gradle/9.5.1/fileHashes/fileHashes.bin
Normal file
Binary file not shown.
BIN
.gradle/9.5.1/fileHashes/fileHashes.lock
Normal file
BIN
.gradle/9.5.1/fileHashes/fileHashes.lock
Normal file
Binary file not shown.
BIN
.gradle/9.5.1/fileHashes/resourceHashesCache.bin
Normal file
BIN
.gradle/9.5.1/fileHashes/resourceHashesCache.bin
Normal file
Binary file not shown.
0
.gradle/9.5.1/gc.properties
Normal file
0
.gradle/9.5.1/gc.properties
Normal file
Binary file not shown.
@@ -1,2 +1,2 @@
|
||||
#Wed May 27 23:44:22 CEST 2026
|
||||
gradle.version=9.4.1
|
||||
#Thu May 28 14:41:41 CEST 2026
|
||||
gradle.version=9.5.1
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,7 +1,7 @@
|
||||
arguments=--init-script /home/anon/.eclipse/1927926929_linux_gtk_x86_64/configuration/org.eclipse.osgi/58/0/.cp/gradle/init/init.gradle
|
||||
arguments=--init-script /home/anon/Src/eclipse.jdt.ls/org.eclipse.jdt.ls.product/target/repository/configuration/org.eclipse.osgi/58/0/.cp/gradle/init/init.gradle
|
||||
auto.sync=false
|
||||
build.scans.enabled=false
|
||||
connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(8.9))
|
||||
connection.gradle.distribution=GRADLE_DISTRIBUTION(LOCAL_INSTALLATION(/home/anon/.sdkman/candidates/gradle/current))
|
||||
connection.project.dir=
|
||||
eclipse.preferences.version=1
|
||||
gradle.user.home=
|
||||
|
||||
34
README.md
34
README.md
@@ -1,24 +1,45 @@
|
||||
# jprototerm
|
||||
|
||||
JavaFX canvas terminal prototype using `jlibghostty` for terminal emulation, Nix for the build environment, and GluonFX/GraalVM Native Image for the Linux binary.
|
||||
JavaFX canvas terminal prototype using `jlibghostty` for terminal emulation and Nix for
|
||||
the build environment. It builds a plain JavaFX application (JDK 25, JavaFX 25 via Gradle)
|
||||
packaged as a Nix derivation — no GraalVM/GluonFX native image.
|
||||
|
||||
## Build
|
||||
|
||||
```sh
|
||||
nix build
|
||||
./result/bin/jprototerm
|
||||
```
|
||||
|
||||
The package build uses GluonFX through Gradle so JavaFX native-image metadata is generated by the toolchain that is designed for it. In a strict pure Nix sandbox, Gradle dependencies must be vendored first with `gradle2nix` or a checked-in Maven/Gradle cache.
|
||||
Install it into a profile (works on NixOS and on a plain Debian box with Nix installed):
|
||||
|
||||
```sh
|
||||
nix profile add .
|
||||
jprototerm
|
||||
```
|
||||
|
||||
The flake bundles everything the app needs — the JDK 25 runtime, the Maven JavaFX modules
|
||||
and their native libraries, and the gtk/glib/freetype/X11 libraries they load — **except**
|
||||
the system OpenGL/graphics drivers. `libGL` is supplied by the host at runtime through a GL
|
||||
shim in the launcher wrapper, so the same closure runs against NixOS, Mesa, or vendor
|
||||
(e.g. NVIDIA) GPU drivers.
|
||||
|
||||
Gradle dependencies are vendored in `deps.json` for the pure Nix sandbox. Regenerate it
|
||||
after changing dependencies in `build.gradle` (the update script writes `deps.json` in the
|
||||
current directory):
|
||||
|
||||
```sh
|
||||
$(nix build .#gradleDepsUpdateScript --no-link --print-out-paths)
|
||||
```
|
||||
|
||||
For development:
|
||||
|
||||
```sh
|
||||
nix develop
|
||||
gradle -PjlibghosttyMavenRepo="$JLIBGHOSTTY_MAVEN_REPO" run
|
||||
gradle -PjlibghosttyMavenRepo="$JLIBGHOSTTY_MAVEN_REPO" -Pgluonfx.mainClassName=com.gregor.jprototerm.Main nativeExecutable
|
||||
gradle run
|
||||
```
|
||||
|
||||
The Gradle project is the source of truth for native JavaFX builds.
|
||||
The Gradle project is the source of truth for the JavaFX build.
|
||||
|
||||
## Config
|
||||
|
||||
@@ -56,6 +77,9 @@ enabled = true
|
||||
[scrollback]
|
||||
editor_command = "vi {file}"
|
||||
|
||||
[env.override]
|
||||
ZELLIJ_SESSION_NAME = ""
|
||||
|
||||
[keybindings]
|
||||
navigate_left = "ALT+H"
|
||||
navigate_down = "ALT+J"
|
||||
|
||||
2
TODOS.md
Normal file
2
TODOS.md
Normal file
@@ -0,0 +1,2 @@
|
||||
jlibghostty - why downcall metadata not propagated ?
|
||||
jlibghostty - how need to change flake so consuming flakes dont have to depend on same ghostty flake ?
|
||||
@@ -14,11 +14,10 @@ repositories {
|
||||
dependencies {
|
||||
implementation 'io.github.wasabithumb:jtoml:1.5.2'
|
||||
implementation 'dev.jlibghostty:jlibghostty:0.1.0-SNAPSHOT'
|
||||
implementation 'org.jetbrains.pty4j:pty4j:0.13.11'
|
||||
}
|
||||
|
||||
javafx {
|
||||
version = '22'
|
||||
version = '25'
|
||||
modules = [ 'javafx.controls', 'javafx.fxml' ]
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,9 @@ enabled = true
|
||||
[scrollback]
|
||||
editor_command = "vi {file}"
|
||||
|
||||
[env.override]
|
||||
ZELLIJ_SESSION_NAME = ""
|
||||
|
||||
[keybindings]
|
||||
navigate_left = "ALT+H"
|
||||
navigate_down = "ALT+J"
|
||||
|
||||
76
deps.json
Normal file
76
deps.json
Normal file
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"!comment": "This is a nixpkgs Gradle dependency lockfile. For more details, refer to the Gradle section in the nixpkgs manual.",
|
||||
"!version": 1,
|
||||
"https://plugins.gradle.org/m2": {
|
||||
"com/google/code/findbugs#jsr305/3.0.2": {
|
||||
"jar": "sha256-dmrSoHg/JoeWLIrXTO7MOKKLn3Ki0IXuQ4t4E+ko0Mc=",
|
||||
"pom": "sha256-GYidvfGyVLJgGl7mRbgUepdGRIgil2hMeYr+XWPXjf4="
|
||||
},
|
||||
"com/google/gradle#osdetector-gradle-plugin/1.7.3": {
|
||||
"jar": "sha256-a0aS+ROiGx+2Axae54uo8+SrKvnXYq+cqIt5EmwcCtE=",
|
||||
"pom": "sha256-hGDJUBJ8o1mHZhYeOLT/jWO01p+4MQoW4As1E1ABDBE="
|
||||
},
|
||||
"kr/motd/maven#os-maven-plugin/1.7.1": {
|
||||
"jar": "sha256-9Hru+Ggh5SsrGHWJeL0EXwPXIikuMudHCCEixiKJUuA=",
|
||||
"pom": "sha256-S3WABEIrljPdMY8p54Tx0YC9ilkgzVCvGTCGH21qVHY="
|
||||
},
|
||||
"org/openjfx#javafx-plugin/0.1.0": {
|
||||
"jar": "sha256-Xq7sB5m0QGRrDKTP2iGaMttr4rpXktAyoNpKOlw4j6s=",
|
||||
"module": "sha256-rf+3RA0kntF8BJOD1nBp+UU7F3gncMAFtoKkNBbYNmE=",
|
||||
"pom": "sha256-NMjfVSfrWjXl8AmjzeH3oInEzkoOclgC8uy+UDu9PLY="
|
||||
},
|
||||
"org/openjfx/javafxplugin#org.openjfx.javafxplugin.gradle.plugin/0.1.0": {
|
||||
"pom": "sha256-1tASf/Q2PQAXPDV6mByec+/wPDCl0Ohq2CtgVPrvqEE="
|
||||
},
|
||||
"org/sonatype/oss#oss-parent/7": {
|
||||
"pom": "sha256-tR+IZ8kranIkmVV/w6H96ne9+e9XRyL+kM5DailVlFQ="
|
||||
},
|
||||
"org/sonatype/oss#oss-parent/9": {
|
||||
"pom": "sha256-+0AmX5glSCEv+C42LllzKyGH7G8NgBgohcFO8fmCgno="
|
||||
}
|
||||
},
|
||||
"https://repo.maven.apache.org/maven2": {
|
||||
"io/github/wasabithumb#jtoml-api/1.5.2": {
|
||||
"jar": "sha256-3MvElpV+cmdqWO+SAHVKCWDYs+KMUXwIYYk7ax85yWo=",
|
||||
"module": "sha256-RUZYGHsxZfuGBi5TogMHhWcbRcCUBTUAPN8FZpPszlE=",
|
||||
"pom": "sha256-VOaIPO8w4z9saTR8smoLTSoih2PUFOBhBio9eqpoqo8="
|
||||
},
|
||||
"io/github/wasabithumb#jtoml-internals/1.5.2": {
|
||||
"jar": "sha256-k4z/Uxzugk2hXUIdeeNLTz//NeynHzPfHCDXzDHP1Ys=",
|
||||
"module": "sha256-OsnYjM8Tylw/MNdw0/HRUWvXgrQHdnT09O5vYlaZENU=",
|
||||
"pom": "sha256-qXtmwHMJBNIWgrewEoUq1FBZjs2eRuRO1wMLndgsndg="
|
||||
},
|
||||
"io/github/wasabithumb#jtoml/1.5.2": {
|
||||
"jar": "sha256-zDtf6VVGSrvC8StneL9fKq9LyaJSfiDb86w52s+bRYs=",
|
||||
"module": "sha256-TRoE8nqf0ULuQ4J1/u2+voUNf421lTOJ1SajE07F8/Y=",
|
||||
"pom": "sha256-sG4IDPD+ItRgyQcfDiLqdd+wCd40JHcSLocA+jWX1p0="
|
||||
},
|
||||
"org/openjfx#javafx-base/25": {
|
||||
"pom": "sha256-XFYpcqK673qkB7J9Wc4XOl6lCht7dRgEO3/I92/v5Tc="
|
||||
},
|
||||
"org/openjfx#javafx-base/25/linux": {
|
||||
"jar": "sha256-MkJZRruLjbBxfPovsuAOIc1InzW5ZitvrKGLYVpKlmk="
|
||||
},
|
||||
"org/openjfx#javafx-controls/25": {
|
||||
"pom": "sha256-74cad6gX7nuDrKWKKe6yv5h2AvRseKHRXEYAgzpq1uM="
|
||||
},
|
||||
"org/openjfx#javafx-controls/25/linux": {
|
||||
"jar": "sha256-NzVeTZHGfoj9mBX2AeasW1Xd3p9em5P8j0qgRXfmkdM="
|
||||
},
|
||||
"org/openjfx#javafx-fxml/25": {
|
||||
"pom": "sha256-RopsFNQeVHnwNK4v4FPwyJEpfqJoo8dtf/047zyrsio="
|
||||
},
|
||||
"org/openjfx#javafx-fxml/25/linux": {
|
||||
"jar": "sha256-OUrjL2TBIsFPvRDvSb3efbsFVpt6uOf58XDIGSS5Wis="
|
||||
},
|
||||
"org/openjfx#javafx-graphics/25": {
|
||||
"pom": "sha256-zB2jY7Id7uvymRWBk9qmIB+USw+Setv13DhL62qDOfQ="
|
||||
},
|
||||
"org/openjfx#javafx-graphics/25/linux": {
|
||||
"jar": "sha256-PlGLwX7lWFWaKsWKX3/UUmFRCNnVCI9lsTvuk5nDcis="
|
||||
},
|
||||
"org/openjfx#javafx/25": {
|
||||
"pom": "sha256-55IzCPyt1/LGiwcgQfR9jnNVIj2EZVnutceA3EuivxM="
|
||||
}
|
||||
}
|
||||
}
|
||||
226
flake.lock
generated
Normal file
226
flake.lock
generated
Normal file
@@ -0,0 +1,226 @@
|
||||
{
|
||||
"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": [
|
||||
"jlibghostty",
|
||||
"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": [
|
||||
"jlibghostty",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"jlibghostty": {
|
||||
"inputs": {
|
||||
"ghostty": "ghostty",
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1780050576,
|
||||
"narHash": "sha256-u06xuO3QnLDpajIOZwDdhwI0HGzMuXG7x1pR+4Zb+RA=",
|
||||
"ref": "refs/heads/main",
|
||||
"rev": "d558d554b360a76d03c2fc09d327e3ec4aade878",
|
||||
"revCount": 17,
|
||||
"type": "git",
|
||||
"url": "https://gitea.gregorlohaus.com/gregor/jlibghostty.git"
|
||||
},
|
||||
"original": {
|
||||
"type": "git",
|
||||
"url": "https://gitea.gregorlohaus.com/gregor/jlibghostty.git"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"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": [
|
||||
"jlibghostty",
|
||||
"ghostty"
|
||||
],
|
||||
"jlibghostty": "jlibghostty",
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
}
|
||||
},
|
||||
"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": [
|
||||
"jlibghostty",
|
||||
"ghostty",
|
||||
"flake-compat"
|
||||
],
|
||||
"nixpkgs": [
|
||||
"jlibghostty",
|
||||
"ghostty",
|
||||
"nixpkgs"
|
||||
],
|
||||
"systems": [
|
||||
"jlibghostty",
|
||||
"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": [
|
||||
"jlibghostty",
|
||||
"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": [
|
||||
"jlibghostty",
|
||||
"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
|
||||
}
|
||||
149
flake.nix
Normal file
149
flake.nix
Normal file
@@ -0,0 +1,149 @@
|
||||
{
|
||||
description = "jprototerm";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
jlibghostty.url = "git+https://gitea.gregorlohaus.com/gregor/jlibghostty.git";
|
||||
ghostty.follows = "jlibghostty/ghostty";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, jlibghostty, ghostty }:
|
||||
let
|
||||
supportedSystems = [ "x86_64-linux" ];
|
||||
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
|
||||
|
||||
# Everything the JavaFX natives (and jlibghostty) dlopen at runtime, EXCEPT the
|
||||
# system OpenGL/graphics drivers. libGL is intentionally left out: it is supplied
|
||||
# by the host at runtime via the GL shim in the wrapper below, so the same closure
|
||||
# works on NixOS and on a plain Debian box with vendor GPU drivers installed.
|
||||
runtimeLibsFor = pkgs: ghosttyVt: [
|
||||
pkgs.glib
|
||||
pkgs.gtk3
|
||||
pkgs.pango
|
||||
pkgs.cairo
|
||||
pkgs.gdk-pixbuf
|
||||
pkgs.harfbuzz
|
||||
pkgs.freetype
|
||||
pkgs.fontconfig.lib
|
||||
pkgs.libx11
|
||||
pkgs.libxext
|
||||
pkgs.libxrender
|
||||
pkgs.libxtst
|
||||
pkgs.libxi
|
||||
pkgs.libxcursor
|
||||
pkgs.libxrandr
|
||||
pkgs.libxinerama
|
||||
pkgs.libxcb
|
||||
pkgs.libxxf86vm
|
||||
pkgs.zlib
|
||||
ghosttyVt
|
||||
];
|
||||
in {
|
||||
packages = forAllSystems (system:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
|
||||
jlib = jlibghostty.packages.${system}.jlibghostty;
|
||||
ghosttyVt = ghostty.packages.${system}.libghostty-vt;
|
||||
|
||||
runtimeLibs = runtimeLibsFor pkgs ghosttyVt;
|
||||
|
||||
jprototerm = pkgs.stdenv.mkDerivation (finalAttrs: {
|
||||
pname = "jprototerm";
|
||||
version = "0.1.0";
|
||||
src = ./.;
|
||||
|
||||
nativeBuildInputs = [
|
||||
pkgs.jdk25
|
||||
pkgs.gradle_9
|
||||
pkgs.makeWrapper
|
||||
];
|
||||
|
||||
buildInputs = runtimeLibs;
|
||||
|
||||
mitmCache = pkgs.gradle_9.fetchDeps {
|
||||
pkg = finalAttrs.finalPackage;
|
||||
data = ./deps.json;
|
||||
useBwrap = false;
|
||||
};
|
||||
|
||||
# Builds build/install/jprototerm/{bin,lib} with every runtime jar, including
|
||||
# the maven javafx-*-linux jars that carry the platform natives.
|
||||
gradleBuildTask = "installDist";
|
||||
gradleFlags = [
|
||||
"--no-build-cache"
|
||||
"--stacktrace"
|
||||
"-Dorg.gradle.java.home=${pkgs.jdk25}"
|
||||
];
|
||||
|
||||
JAVA_HOME = "${pkgs.jdk25}";
|
||||
JLIBGHOSTTY_MAVEN_REPO = "${jlib}/maven";
|
||||
|
||||
preBuild = ''
|
||||
export HOME="$TMPDIR/home"
|
||||
export GRADLE_OPTS="-Duser.home=$HOME ''${GRADLE_OPTS:-}"
|
||||
'';
|
||||
|
||||
preGradleUpdate = ''
|
||||
export HOME="$TMPDIR/home"
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
|
||||
mkdir -p "$out/share/jprototerm"
|
||||
cp -a build/install/jprototerm/lib "$out/share/jprototerm/lib"
|
||||
|
||||
# JavaFX is a set of proper modular jars: put them on the module path and
|
||||
# keep the application + plain dependency jars on the classpath, so the two
|
||||
# worlds do not collide.
|
||||
mkdir -p "$out/share/jprototerm/javafx"
|
||||
mv "$out/share/jprototerm/lib"/javafx-*.jar "$out/share/jprototerm/javafx/"
|
||||
|
||||
# Build an explicit colon-separated classpath. A "lib/*" glob would be
|
||||
# expanded by the wrapper's shell before java sees it, breaking -cp.
|
||||
classpath=""
|
||||
for jar in "$out"/share/jprototerm/lib/*.jar; do
|
||||
classpath="$classpath''${classpath:+:}$jar"
|
||||
done
|
||||
|
||||
makeWrapper "${pkgs.jdk25}/bin/java" "$out/bin/jprototerm" \
|
||||
--add-flags "--enable-native-access=ALL-UNNAMED,javafx.graphics" \
|
||||
--add-flags "--module-path $out/share/jprototerm/javafx" \
|
||||
--add-flags "--add-modules javafx.controls,javafx.fxml" \
|
||||
--add-flags "-cp $classpath" \
|
||||
--add-flags "com.gregor.jprototerm.Main" \
|
||||
--prefix LD_LIBRARY_PATH : "${pkgs.lib.makeLibraryPath runtimeLibs}" \
|
||||
--run 'glShimDir="''${XDG_RUNTIME_DIR:-/tmp}/jprototerm-gl"; mkdir -p "$glShimDir"; for lib in /lib/x86_64-linux-gnu/libGL.so.1 /lib/x86_64-linux-gnu/libGLX.so.0 /lib/x86_64-linux-gnu/libGLdispatch.so.0 /usr/lib/x86_64-linux-gnu/libGLX_nvidia.so* /usr/lib/x86_64-linux-gnu/libEGL_nvidia.so* /usr/lib/x86_64-linux-gnu/libnvidia*.so* /usr/lib/x86_64-linux-gnu/nvidia/current/lib*.so*; do [ -e "$lib" ] && ln -sfn "$lib" "$glShimDir/$(basename "$lib")"; done; export LD_LIBRARY_PATH="$glShimDir''${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"; export __GLX_VENDOR_LIBRARY_NAME="''${__GLX_VENDOR_LIBRARY_NAME:-nvidia}"; if [ -e /usr/share/glvnd/egl_vendor.d/10_nvidia.json ]; then export __EGL_VENDOR_LIBRARY_FILENAMES="''${__EGL_VENDOR_LIBRARY_FILENAMES:-/usr/share/glvnd/egl_vendor.d/10_nvidia.json}"; fi' \
|
||||
--set JLIBGHOSTTY_LIBRARY "${ghosttyVt}/lib/libghostty-vt.so" \
|
||||
--set GDK_BACKEND x11
|
||||
|
||||
runHook postInstall
|
||||
'';
|
||||
});
|
||||
in {
|
||||
default = jprototerm;
|
||||
gradleDepsUpdateScript = jprototerm.mitmCache.updateScript;
|
||||
});
|
||||
|
||||
devShells = forAllSystems (system:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
jlib = jlibghostty.packages.${system}.jlibghostty;
|
||||
ghosttyVt = ghostty.packages.${system}.libghostty-vt;
|
||||
runtimeLibs = runtimeLibsFor pkgs ghosttyVt;
|
||||
in {
|
||||
default = pkgs.mkShell {
|
||||
packages = [
|
||||
pkgs.gradle_9
|
||||
pkgs.jdk25
|
||||
pkgs.jdt-language-server
|
||||
] ++ runtimeLibs;
|
||||
|
||||
JLIBGHOSTTY_MAVEN_REPO = "${jlib}/maven";
|
||||
JLIBGHOSTTY_LIBRARY = "${ghosttyVt}/lib/libghostty-vt.so";
|
||||
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath runtimeLibs;
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
8
settings.gradle
Normal file
8
settings.gradle
Normal file
@@ -0,0 +1,8 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = 'jprototerm'
|
||||
@@ -3,6 +3,7 @@ package com.gregor.jprototerm;
|
||||
import io.github.wasabithumb.jtoml.JToml;
|
||||
import io.github.wasabithumb.jtoml.document.TomlDocument;
|
||||
import io.github.wasabithumb.jtoml.except.TomlException;
|
||||
import io.github.wasabithumb.jtoml.key.TomlKey;
|
||||
import io.github.wasabithumb.jtoml.value.TomlValue;
|
||||
import io.github.wasabithumb.jtoml.value.primitive.TomlPrimitive;
|
||||
import io.github.wasabithumb.jtoml.value.table.TomlTable;
|
||||
@@ -26,6 +27,7 @@ public record AppConfig(
|
||||
double windowHeight,
|
||||
boolean kittyGraphics,
|
||||
String scrollbackEditorCommand,
|
||||
Map<String, String> envOverride,
|
||||
Map<String, KeyBinding> keybindings
|
||||
) {
|
||||
private static final List<String> KEYBINDING_KEYS = List.of(
|
||||
@@ -62,6 +64,7 @@ public record AppConfig(
|
||||
doubleValue(document, "window.height", defaults.windowHeight),
|
||||
booleanValue(document, "kitty_graphics.enabled", defaults.kittyGraphics),
|
||||
stringValue(document, "scrollback.editor_command", defaults.scrollbackEditorCommand),
|
||||
envOverride(document, defaults.envOverride),
|
||||
keybindings(document, defaults)
|
||||
);
|
||||
} catch (TomlException ex) {
|
||||
@@ -82,6 +85,7 @@ public record AppConfig(
|
||||
760.0,
|
||||
true,
|
||||
defaultScrollbackEditorCommand(),
|
||||
Map.of(),
|
||||
Map.of(
|
||||
"navigate_left", KeyBinding.parse("ALT+H"),
|
||||
"navigate_down", KeyBinding.parse("ALT+J"),
|
||||
@@ -109,6 +113,7 @@ public record AppConfig(
|
||||
windowHeight,
|
||||
kittyGraphics,
|
||||
scrollbackEditorCommand,
|
||||
envOverride,
|
||||
keybindings
|
||||
);
|
||||
}
|
||||
@@ -183,6 +188,11 @@ public record AppConfig(
|
||||
builder.append("enabled = ").append(kittyGraphics).append("\n\n");
|
||||
builder.append("[scrollback]\n");
|
||||
builder.append("editor_command = ").append(quoted(scrollbackEditorCommand)).append("\n\n");
|
||||
builder.append("[env.override]\n");
|
||||
for (Map.Entry<String, String> entry : envOverride.entrySet()) {
|
||||
builder.append(entry.getKey()).append(" = ").append(quoted(entry.getValue())).append('\n');
|
||||
}
|
||||
builder.append('\n');
|
||||
builder.append("[keybindings]\n");
|
||||
for (String key : KEYBINDING_KEYS) {
|
||||
KeyBinding binding = keybindings.get(key);
|
||||
@@ -222,6 +232,31 @@ public record AppConfig(
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, String> envOverride(TomlTable table, Map<String, String> fallback) {
|
||||
TomlValue value = table.get("env.override");
|
||||
if (value == null || !value.isTable()) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
Map<String, String> result = new LinkedHashMap<>();
|
||||
TomlTable overrides = value.asTable();
|
||||
for (TomlKey key : overrides.keys(false)) {
|
||||
if (key.size() != 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
TomlValue override = overrides.get(key);
|
||||
if (override != null && override.isPrimitive()) {
|
||||
try {
|
||||
result.put(key.get(0), override.asPrimitive().asString());
|
||||
} catch (RuntimeException ignored) {
|
||||
// Ignore non-string values; environment values are strings.
|
||||
}
|
||||
}
|
||||
}
|
||||
return Map.copyOf(result);
|
||||
}
|
||||
|
||||
private static String stringValue(TomlTable table, String key, String fallback) {
|
||||
TomlPrimitive primitive = primitive(table, key);
|
||||
return primitive == null ? fallback : primitive.asString();
|
||||
|
||||
401
src/main/java/com/gregor/jprototerm/LinuxPty.java
Normal file
401
src/main/java/com/gregor/jprototerm/LinuxPty.java
Normal file
@@ -0,0 +1,401 @@
|
||||
package com.gregor.jprototerm;
|
||||
|
||||
import java.lang.foreign.AddressLayout;
|
||||
import java.lang.foreign.Arena;
|
||||
import java.lang.foreign.FunctionDescriptor;
|
||||
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.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A Linux PTY backed by libc via the Foreign Function & Memory API.
|
||||
*
|
||||
* <p>This replaces pty4j (which loads a JNA JNI shim). It uses
|
||||
* {@code posix_openpt}/{@code posix_spawnp} rather than {@code fork}/{@code forkpty}:
|
||||
* doing work between {@code fork} and {@code exec} inside a multithreaded JVM is unsafe
|
||||
* (only async-signal-safe calls are permitted), whereas {@code posix_spawn} performs the
|
||||
* dangerous part in libc with no Java on the stack.
|
||||
*
|
||||
* <p>The child gets a fresh session via {@code POSIX_SPAWN_SETSID}; it then opens the slave
|
||||
* PTY itself (as fd 0, without {@code O_NOCTTY}) so the slave becomes its controlling
|
||||
* terminal. glibc applies attribute flags (the setsid) before file actions, so the open
|
||||
* happens in the new session.
|
||||
*/
|
||||
public final class LinuxPty implements AutoCloseable {
|
||||
static final Linker LINKER = Linker.nativeLinker();
|
||||
private static final SymbolLookup LIBC = LINKER.defaultLookup();
|
||||
|
||||
static final AddressLayout C_POINTER = (AddressLayout) LINKER.canonicalLayouts().get("void*");
|
||||
static final ValueLayout.OfShort C_SHORT = (ValueLayout.OfShort) LINKER.canonicalLayouts().get("short");
|
||||
static final ValueLayout.OfInt C_INT = (ValueLayout.OfInt) LINKER.canonicalLayouts().get("int");
|
||||
static final ValueLayout.OfLong C_LONG = (ValueLayout.OfLong) LINKER.canonicalLayouts().get("long");
|
||||
static final ValueLayout.OfLong C_SIZE_T = (ValueLayout.OfLong) LINKER.canonicalLayouts().get("size_t");
|
||||
|
||||
// Function descriptors.
|
||||
static final FunctionDescriptor FD_INT_INT = FunctionDescriptor.of(C_INT, C_INT);
|
||||
static final FunctionDescriptor FD_PTSNAME_R = FunctionDescriptor.of(C_INT, C_INT, C_POINTER, C_SIZE_T);
|
||||
static final FunctionDescriptor FD_RW = FunctionDescriptor.of(C_LONG, C_INT, C_POINTER, C_SIZE_T);
|
||||
static final FunctionDescriptor FD_IOCTL = FunctionDescriptor.of(C_INT, C_INT, C_LONG, C_POINTER);
|
||||
static final FunctionDescriptor FD_KILL = FunctionDescriptor.of(C_INT, C_INT, C_INT);
|
||||
static final FunctionDescriptor FD_WAITPID = FunctionDescriptor.of(C_INT, C_INT, C_POINTER, C_INT);
|
||||
static final FunctionDescriptor FD_SPAWN = FunctionDescriptor.of(
|
||||
C_INT, C_POINTER, C_POINTER, C_POINTER, C_POINTER, C_POINTER, C_POINTER);
|
||||
static final FunctionDescriptor FD_FA_INIT = FunctionDescriptor.of(C_INT, C_POINTER);
|
||||
static final FunctionDescriptor FD_FA_ADDCLOSE = FunctionDescriptor.of(C_INT, C_POINTER, C_INT);
|
||||
static final FunctionDescriptor FD_FA_ADDDUP2 = FunctionDescriptor.of(C_INT, C_POINTER, C_INT, C_INT);
|
||||
static final FunctionDescriptor FD_FA_ADDOPEN =
|
||||
FunctionDescriptor.of(C_INT, C_POINTER, C_INT, C_POINTER, C_INT, C_INT);
|
||||
static final FunctionDescriptor FD_FA_ADDCHDIR = FunctionDescriptor.of(C_INT, C_POINTER, C_POINTER);
|
||||
static final FunctionDescriptor FD_ATTR_SETFLAGS = FunctionDescriptor.of(C_INT, C_POINTER, C_SHORT);
|
||||
|
||||
// Linux constants (x86-64 / arm64).
|
||||
private static final int O_RDWR = 0x0002;
|
||||
private static final int O_NOCTTY = 0x0100;
|
||||
private static final long TIOCSWINSZ = 0x5414L;
|
||||
private static final short POSIX_SPAWN_SETSID = 0x80;
|
||||
private static final int SIGHUP = 1;
|
||||
private static final int SIGKILL = 9;
|
||||
private static final int WNOHANG = 1;
|
||||
|
||||
// struct winsize { unsigned short ws_row, ws_col, ws_xpixel, ws_ypixel; }
|
||||
private static final MemoryLayout WINSIZE = MemoryLayout.structLayout(
|
||||
C_SHORT.withName("ws_row"),
|
||||
C_SHORT.withName("ws_col"),
|
||||
C_SHORT.withName("ws_xpixel"),
|
||||
C_SHORT.withName("ws_ypixel"));
|
||||
|
||||
// posix_spawn_file_actions_t / posix_spawnattr_t are opaque; over-allocate generously.
|
||||
private static final long SPAWN_ACTIONS_SIZE = 256;
|
||||
private static final long SPAWN_ATTR_SIZE = 512;
|
||||
|
||||
private static final MethodHandle POSIX_OPENPT = handle("posix_openpt", FD_INT_INT);
|
||||
private static final MethodHandle GRANTPT = handle("grantpt", FD_INT_INT);
|
||||
private static final MethodHandle UNLOCKPT = handle("unlockpt", FD_INT_INT);
|
||||
private static final MethodHandle PTSNAME_R = handle("ptsname_r", FD_PTSNAME_R);
|
||||
private static final MethodHandle CLOSE = handle("close", FD_INT_INT);
|
||||
private static final MethodHandle READ = handle("read", FD_RW);
|
||||
private static final MethodHandle WRITE = handle("write", FD_RW);
|
||||
private static final MethodHandle IOCTL = handle("ioctl", FD_IOCTL, Linker.Option.firstVariadicArg(2));
|
||||
private static final MethodHandle KILL = handle("kill", FD_KILL);
|
||||
private static final MethodHandle WAITPID = handle("waitpid", FD_WAITPID);
|
||||
private static final MethodHandle POSIX_SPAWNP = handle("posix_spawnp", FD_SPAWN);
|
||||
private static final MethodHandle FA_INIT = handle("posix_spawn_file_actions_init", FD_FA_INIT);
|
||||
private static final MethodHandle FA_DESTROY = handle("posix_spawn_file_actions_destroy", FD_FA_INIT);
|
||||
private static final MethodHandle FA_ADDCLOSE = handle("posix_spawn_file_actions_addclose", FD_FA_ADDCLOSE);
|
||||
private static final MethodHandle FA_ADDDUP2 = handle("posix_spawn_file_actions_adddup2", FD_FA_ADDDUP2);
|
||||
private static final MethodHandle FA_ADDOPEN = handle("posix_spawn_file_actions_addopen", FD_FA_ADDOPEN);
|
||||
private static final MethodHandle FA_ADDCHDIR = handle("posix_spawn_file_actions_addchdir_np", FD_FA_ADDCHDIR);
|
||||
private static final MethodHandle ATTR_INIT = handle("posix_spawnattr_init", FD_FA_INIT);
|
||||
private static final MethodHandle ATTR_DESTROY = handle("posix_spawnattr_destroy", FD_FA_INIT);
|
||||
private static final MethodHandle ATTR_SETFLAGS = handle("posix_spawnattr_setflags", FD_ATTR_SETFLAGS);
|
||||
|
||||
private final Arena arena = Arena.ofShared();
|
||||
private final MemorySegment readBuffer = arena.allocate(65536);
|
||||
private final Object writeLock = new Object();
|
||||
private final int masterFd;
|
||||
private final int pid;
|
||||
private volatile boolean closed;
|
||||
|
||||
private LinuxPty(int masterFd, int pid) {
|
||||
this.masterFd = masterFd;
|
||||
this.pid = pid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a PTY and spawns {@code argv} attached to its slave end.
|
||||
*
|
||||
* @param argv command and arguments (e.g. {@code {"/bin/zsh", "-i"}})
|
||||
* @param environment environment for the child, as KEY=VALUE pairs
|
||||
* @param workingDirectory directory the child starts in, or {@code null} to inherit
|
||||
*/
|
||||
public static LinuxPty spawn(String[] argv, Map<String, String> environment, String workingDirectory) {
|
||||
Arena setup = Arena.ofConfined();
|
||||
try {
|
||||
int master = check(callInt(POSIX_OPENPT, O_RDWR | O_NOCTTY), "posix_openpt");
|
||||
try {
|
||||
check(callInt(GRANTPT, master), "grantpt");
|
||||
check(callInt(UNLOCKPT, master), "unlockpt");
|
||||
|
||||
MemorySegment nameBuf = setup.allocate(256);
|
||||
check(callPtsnameR(master, nameBuf), "ptsname_r");
|
||||
String slavePath = nameBuf.getString(0);
|
||||
|
||||
MemorySegment actions = setup.allocate(SPAWN_ACTIONS_SIZE);
|
||||
MemorySegment attr = setup.allocate(SPAWN_ATTR_SIZE);
|
||||
check(callInt(FA_INIT, actions), "posix_spawn_file_actions_init");
|
||||
check(callInt(ATTR_INIT, attr), "posix_spawnattr_init");
|
||||
try {
|
||||
check(callInt(ATTR_SETFLAGS, attr, POSIX_SPAWN_SETSID), "posix_spawnattr_setflags");
|
||||
|
||||
if (workingDirectory != null) {
|
||||
MemorySegment dir = setup.allocateFrom(workingDirectory);
|
||||
check(callAddChdir(actions, dir), "posix_spawn_file_actions_addchdir_np");
|
||||
}
|
||||
// Open the slave as fd 0 in the new session -> controlling terminal, then fan out.
|
||||
MemorySegment slave = setup.allocateFrom(slavePath);
|
||||
check(callAddOpen(actions, 0, slave, O_RDWR, 0), "posix_spawn_file_actions_addopen");
|
||||
check(callAddDup2(actions, 0, 1), "posix_spawn_file_actions_adddup2");
|
||||
check(callAddDup2(actions, 0, 2), "posix_spawn_file_actions_adddup2");
|
||||
check(callAddClose(actions, master), "posix_spawn_file_actions_addclose");
|
||||
|
||||
MemorySegment argvSeg = cStringArray(setup, List.of(argv));
|
||||
MemorySegment envpSeg = cStringArray(setup, toEnvList(environment));
|
||||
MemorySegment path = setup.allocateFrom(argv[0]);
|
||||
MemorySegment pidOut = setup.allocate(C_INT);
|
||||
|
||||
int rc = callSpawn(pidOut, path, actions, attr, argvSeg, envpSeg);
|
||||
if (rc != 0) {
|
||||
throw new IllegalStateException("posix_spawnp failed for " + argv[0] + " (rc=" + rc + ")");
|
||||
}
|
||||
return new LinuxPty(master, pidOut.get(C_INT, 0));
|
||||
} finally {
|
||||
callInt(ATTR_DESTROY, attr);
|
||||
callInt(FA_DESTROY, actions);
|
||||
}
|
||||
} catch (RuntimeException ex) {
|
||||
callInt(CLOSE, master);
|
||||
throw ex;
|
||||
}
|
||||
} finally {
|
||||
setup.close();
|
||||
}
|
||||
}
|
||||
|
||||
/** Reads available output into {@code dst}; returns bytes read, or -1 at EOF. */
|
||||
public int read(byte[] dst) {
|
||||
if (closed) {
|
||||
return -1;
|
||||
}
|
||||
long n = callLong(READ, masterFd, readBuffer, Math.min(dst.length, readBuffer.byteSize()));
|
||||
if (n <= 0) {
|
||||
return -1;
|
||||
}
|
||||
MemorySegment.copy(readBuffer, ValueLayout.JAVA_BYTE, 0, dst, 0, (int) n);
|
||||
return (int) n;
|
||||
}
|
||||
|
||||
/** Writes all of {@code data} to the master end. */
|
||||
public void write(byte[] data) {
|
||||
if (closed || data.length == 0) {
|
||||
return;
|
||||
}
|
||||
synchronized (writeLock) {
|
||||
try (Arena a = Arena.ofConfined()) {
|
||||
MemorySegment buf = a.allocate(data.length);
|
||||
MemorySegment.copy(data, 0, buf, ValueLayout.JAVA_BYTE, 0, data.length);
|
||||
long offset = 0;
|
||||
while (offset < data.length) {
|
||||
long n = callLong(WRITE, masterFd, buf.asSlice(offset), data.length - offset);
|
||||
if (n < 0) {
|
||||
throw new IllegalStateException("write to pty failed");
|
||||
}
|
||||
offset += n;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Resizes the terminal window. */
|
||||
public void setWinSize(int columns, int rows) {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
try (Arena a = Arena.ofConfined()) {
|
||||
MemorySegment ws = a.allocate(WINSIZE);
|
||||
ws.set(C_SHORT, 0, (short) rows);
|
||||
ws.set(C_SHORT, 2, (short) columns);
|
||||
ws.set(C_SHORT, 4, (short) 0);
|
||||
ws.set(C_SHORT, 6, (short) 0);
|
||||
callIoctl(masterFd, TIOCSWINSZ, ws);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
closed = true;
|
||||
callKill(pid, SIGHUP);
|
||||
callInt(CLOSE, masterFd);
|
||||
reap();
|
||||
arena.close();
|
||||
}
|
||||
|
||||
private void reap() {
|
||||
try (Arena a = Arena.ofConfined()) {
|
||||
MemorySegment status = a.allocate(C_INT);
|
||||
// Closing the master sends EOF/SIGHUP; an interactive shell exits promptly.
|
||||
for (int attempt = 0; attempt < 50; attempt++) {
|
||||
int r = callWaitpid(pid, status, WNOHANG);
|
||||
if (r != 0) {
|
||||
return; // reaped, or no such child
|
||||
}
|
||||
if (attempt == 25) {
|
||||
callKill(pid, SIGKILL);
|
||||
}
|
||||
try {
|
||||
Thread.sleep(2);
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- typed invokeExact wrappers ---------------------------------------------------------
|
||||
|
||||
private static int callInt(MethodHandle handle, int arg) {
|
||||
try {
|
||||
return (int) handle.invokeExact(arg);
|
||||
} catch (Throwable t) {
|
||||
throw sneaky(t);
|
||||
}
|
||||
}
|
||||
|
||||
private static int callInt(MethodHandle handle, MemorySegment arg) {
|
||||
try {
|
||||
return (int) handle.invokeExact(arg);
|
||||
} catch (Throwable t) {
|
||||
throw sneaky(t);
|
||||
}
|
||||
}
|
||||
|
||||
private static int callInt(MethodHandle handle, MemorySegment a, short b) {
|
||||
try {
|
||||
return (int) handle.invokeExact(a, b);
|
||||
} catch (Throwable t) {
|
||||
throw sneaky(t);
|
||||
}
|
||||
}
|
||||
|
||||
private static long callLong(MethodHandle handle, int fd, MemorySegment buf, long len) {
|
||||
try {
|
||||
return (long) handle.invokeExact(fd, buf, len);
|
||||
} catch (Throwable t) {
|
||||
throw sneaky(t);
|
||||
}
|
||||
}
|
||||
|
||||
private static int callPtsnameR(int fd, MemorySegment buf) {
|
||||
try {
|
||||
return (int) PTSNAME_R.invokeExact(fd, buf, buf.byteSize());
|
||||
} catch (Throwable t) {
|
||||
throw sneaky(t);
|
||||
}
|
||||
}
|
||||
|
||||
private static int callAddChdir(MemorySegment actions, MemorySegment path) {
|
||||
try {
|
||||
return (int) FA_ADDCHDIR.invokeExact(actions, path);
|
||||
} catch (Throwable t) {
|
||||
throw sneaky(t);
|
||||
}
|
||||
}
|
||||
|
||||
private static int callAddOpen(MemorySegment actions, int fd, MemorySegment path, int oflag, int mode) {
|
||||
try {
|
||||
return (int) FA_ADDOPEN.invokeExact(actions, fd, path, oflag, mode);
|
||||
} catch (Throwable t) {
|
||||
throw sneaky(t);
|
||||
}
|
||||
}
|
||||
|
||||
private static int callAddDup2(MemorySegment actions, int fd, int newFd) {
|
||||
try {
|
||||
return (int) FA_ADDDUP2.invokeExact(actions, fd, newFd);
|
||||
} catch (Throwable t) {
|
||||
throw sneaky(t);
|
||||
}
|
||||
}
|
||||
|
||||
private static int callAddClose(MemorySegment actions, int fd) {
|
||||
try {
|
||||
return (int) FA_ADDCLOSE.invokeExact(actions, fd);
|
||||
} catch (Throwable t) {
|
||||
throw sneaky(t);
|
||||
}
|
||||
}
|
||||
|
||||
private static int callSpawn(MemorySegment pid, MemorySegment path, MemorySegment actions,
|
||||
MemorySegment attr, MemorySegment argv, MemorySegment envp) {
|
||||
try {
|
||||
return (int) POSIX_SPAWNP.invokeExact(pid, path, actions, attr, argv, envp);
|
||||
} catch (Throwable t) {
|
||||
throw sneaky(t);
|
||||
}
|
||||
}
|
||||
|
||||
private static void callIoctl(int fd, long request, MemorySegment arg) {
|
||||
try {
|
||||
int unused = (int) IOCTL.invokeExact(fd, request, arg);
|
||||
} catch (Throwable t) {
|
||||
throw sneaky(t);
|
||||
}
|
||||
}
|
||||
|
||||
private static void callKill(int pid, int signal) {
|
||||
try {
|
||||
int unused = (int) KILL.invokeExact(pid, signal);
|
||||
} catch (Throwable t) {
|
||||
throw sneaky(t);
|
||||
}
|
||||
}
|
||||
|
||||
private static int callWaitpid(int pid, MemorySegment status, int options) {
|
||||
try {
|
||||
return (int) WAITPID.invokeExact(pid, status, options);
|
||||
} catch (Throwable t) {
|
||||
throw sneaky(t);
|
||||
}
|
||||
}
|
||||
|
||||
// --- helpers ----------------------------------------------------------------------------
|
||||
|
||||
private static MethodHandle handle(String symbol, FunctionDescriptor descriptor, Linker.Option... options) {
|
||||
MemorySegment address = LIBC.find(symbol)
|
||||
.orElseThrow(() -> new IllegalStateException("libc symbol not found: " + symbol));
|
||||
return LINKER.downcallHandle(address, descriptor, options);
|
||||
}
|
||||
|
||||
private static MemorySegment cStringArray(Arena arena, List<String> values) {
|
||||
MemorySegment array = arena.allocate(C_POINTER, values.size() + 1L);
|
||||
for (int i = 0; i < values.size(); i++) {
|
||||
array.setAtIndex(C_POINTER, i, arena.allocateFrom(values.get(i)));
|
||||
}
|
||||
array.setAtIndex(C_POINTER, values.size(), MemorySegment.NULL);
|
||||
return array;
|
||||
}
|
||||
|
||||
private static List<String> toEnvList(Map<String, String> environment) {
|
||||
List<String> out = new ArrayList<>(environment.size());
|
||||
for (Map.Entry<String, String> entry : environment.entrySet()) {
|
||||
out.add(entry.getKey() + "=" + entry.getValue());
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static int check(int rc, String what) {
|
||||
if (rc < 0) {
|
||||
throw new IllegalStateException(what + " failed (rc=" + rc + ")");
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
private static RuntimeException sneaky(Throwable t) {
|
||||
if (t instanceof RuntimeException re) {
|
||||
return re;
|
||||
}
|
||||
if (t instanceof Error e) {
|
||||
throw e;
|
||||
}
|
||||
return new IllegalStateException(t);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,7 @@
|
||||
package com.gregor.jprototerm;
|
||||
|
||||
import com.pty4j.PtyProcess;
|
||||
import com.pty4j.PtyProcessBuilder;
|
||||
import com.pty4j.WinSize;
|
||||
import javafx.application.Platform;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@@ -14,14 +9,12 @@ import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public final class ShellSession implements AutoCloseable {
|
||||
private final PtyProcess process;
|
||||
private final OutputStream stdin;
|
||||
private final LinuxPty pty;
|
||||
private final ExecutorService reader;
|
||||
private volatile boolean closed;
|
||||
|
||||
private ShellSession(PtyProcess process) {
|
||||
this.process = process;
|
||||
this.stdin = process.getOutputStream();
|
||||
private ShellSession(LinuxPty pty) {
|
||||
this.pty = pty;
|
||||
this.reader = Executors.newSingleThreadExecutor(runnable -> {
|
||||
Thread thread = new Thread(runnable, "shell-output-reader");
|
||||
thread.setDaemon(true);
|
||||
@@ -29,20 +22,21 @@ public final class ShellSession implements AutoCloseable {
|
||||
});
|
||||
}
|
||||
|
||||
public static ShellSession start(String shell, TerminalPane pane, int columns, int rows) {
|
||||
public static ShellSession start(String shell, Map<String, String> envOverride, TerminalPane pane, int columns, int rows) {
|
||||
try {
|
||||
Map<String, String> environment = new HashMap<>(System.getenv());
|
||||
environment.put("TERM", "xterm-kitty");
|
||||
environment.put("COLORTERM", "truecolor");
|
||||
environment.putAll(envOverride);
|
||||
|
||||
PtyProcess process = new PtyProcessBuilder(new String[] {shell, "-i"})
|
||||
.setEnvironment(environment)
|
||||
.setInitialColumns(columns)
|
||||
.setInitialRows(rows)
|
||||
.setDirectory(System.getProperty("user.home"))
|
||||
.start();
|
||||
return new ShellSession(process);
|
||||
} catch (IOException ex) {
|
||||
LinuxPty pty = LinuxPty.spawn(
|
||||
new String[] {shell, "-i"},
|
||||
environment,
|
||||
System.getProperty("user.home"));
|
||||
ShellSession session = new ShellSession(pty);
|
||||
session.resize(columns, rows);
|
||||
return session;
|
||||
} catch (RuntimeException ex) {
|
||||
pane.write("failed to start shell: " + ex.getMessage() + "\r\n");
|
||||
throw new IllegalStateException("Could not start shell " + shell, ex);
|
||||
}
|
||||
@@ -56,7 +50,7 @@ public final class ShellSession implements AutoCloseable {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
process.setWinSize(new WinSize(columns, rows));
|
||||
pty.setWinSize(columns, rows);
|
||||
}
|
||||
|
||||
public void send(String text) {
|
||||
@@ -68,9 +62,8 @@ public final class ShellSession implements AutoCloseable {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
stdin.write(bytes);
|
||||
stdin.flush();
|
||||
} catch (IOException ex) {
|
||||
pty.write(bytes);
|
||||
} catch (RuntimeException ex) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
@@ -79,7 +72,7 @@ public final class ShellSession implements AutoCloseable {
|
||||
byte[] buffer = new byte[8192];
|
||||
try {
|
||||
int read;
|
||||
while ((read = process.getInputStream().read(buffer)) != -1) {
|
||||
while ((read = pty.read(buffer)) != -1) {
|
||||
if (!closed) {
|
||||
byte[] bytes = new byte[read];
|
||||
System.arraycopy(buffer, 0, bytes, 0, read);
|
||||
@@ -90,7 +83,7 @@ public final class ShellSession implements AutoCloseable {
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
} catch (RuntimeException ex) {
|
||||
if (!closed) {
|
||||
Platform.runLater(() -> pane.write("\r\nshell output stopped: " + ex.getMessage() + "\r\n"));
|
||||
}
|
||||
@@ -101,6 +94,6 @@ public final class ShellSession implements AutoCloseable {
|
||||
public void close() {
|
||||
closed = true;
|
||||
reader.shutdownNow();
|
||||
process.destroy();
|
||||
pty.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,13 +121,24 @@ public final class TerminalCanvasView {
|
||||
String cacheKey = paneCacheKey(pane, metrics);
|
||||
int imageWidth = Math.max(1, (int) Math.ceil(pane.width()));
|
||||
int imageHeight = Math.max(1, (int) Math.ceil(pane.height()));
|
||||
if (cache.image == null || cache.canvas == null || cache.imageWidth != imageWidth || cache.imageHeight != imageHeight || !cacheKey.equals(cache.key)) {
|
||||
|
||||
// Allocate the offscreen buffers only when the pane size changes. Reallocating a
|
||||
// full-pane Canvas + WritableImage on every content change churns ~20 MB per frame,
|
||||
// which the native image's serial GC turns into Full-GC frame drops.
|
||||
if (cache.canvas == null || cache.image == null || cache.imageWidth != imageWidth || cache.imageHeight != imageHeight) {
|
||||
cache.canvas = new Canvas(imageWidth, imageHeight);
|
||||
drawPaneContent(cache.canvas.getGraphicsContext2D(), pane, font, metrics, 0.0, 0.0, imageWidth, imageHeight, true);
|
||||
cache.image = new WritableImage(imageWidth, imageHeight);
|
||||
cache.canvas.snapshot(null, cache.image);
|
||||
cache.imageWidth = imageWidth;
|
||||
cache.imageHeight = imageHeight;
|
||||
cache.key = null;
|
||||
}
|
||||
|
||||
// Redraw and re-snapshot into the existing buffers only when content changed.
|
||||
if (!cacheKey.equals(cache.key)) {
|
||||
GraphicsContext cacheGc = cache.canvas.getGraphicsContext2D();
|
||||
cacheGc.clearRect(0, 0, imageWidth, imageHeight);
|
||||
drawPaneContent(cacheGc, pane, font, metrics, 0.0, 0.0, imageWidth, imageHeight, true);
|
||||
cache.canvas.snapshot(null, cache.image);
|
||||
cache.key = cacheKey;
|
||||
}
|
||||
|
||||
|
||||
@@ -259,7 +259,7 @@ public final class TerminalWorkspace implements AutoCloseable {
|
||||
private TerminalPane openPane(boolean floating) {
|
||||
TerminalPane pane = TerminalPane.create(config.columns(), config.rows(), config.maxScrollback());
|
||||
pane.setFloating(floating);
|
||||
pane.attach(ShellSession.start(config.shell(), pane, config.columns(), config.rows()));
|
||||
pane.attach(ShellSession.start(config.shell(), config.envOverride(), pane, config.columns(), config.rows()));
|
||||
return pane;
|
||||
}
|
||||
|
||||
|
||||
0
src/main/resources/.gitkeep
Normal file
0
src/main/resources/.gitkeep
Normal file
Reference in New Issue
Block a user