This commit is contained in:
Gregor Lohaus
2026-05-28 00:33:38 +02:00
parent 82cc7f4729
commit a3f4878fc7
72 changed files with 788 additions and 890 deletions

18
.classpath Normal file
View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="bin/main" path="src/main/java">
<attributes>
<attribute name="gradle_scope" value="main"/>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="bin/main" path="src/main/resources">
<attributes>
<attribute name="gradle_scope" value="main"/>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-21/"/>
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
<classpathentry kind="output" path="bin/default"/>
</classpath>

11
.gitignore vendored
View File

@@ -1 +1,12 @@
result
# Devenv
.devenv*
devenv.local.nix
devenv.local.yaml
# direnv
.direnv
# pre-commit
.pre-commit-config.yaml

Binary file not shown.

Binary file not shown.

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

Binary file not shown.

View File

@@ -0,0 +1,2 @@
#Wed May 27 23:44:22 CEST 2026
gradle.version=9.4.1

Binary file not shown.

BIN
.gradle/file-system.probe Normal file

Binary file not shown.

View File

34
.project Normal file
View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>jprototerm</name>
<comment>Project jprototerm created by Buildship.</comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
</natures>
<filteredResources>
<filter>
<id>1779917652126</id>
<name></name>
<type>30</type>
<matcher>
<id>org.eclipse.core.resources.regexFilterMatcher</id>
<arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
</matcher>
</filter>
</filteredResources>
</projectDescription>

View File

@@ -0,0 +1,13 @@
arguments=--init-script /home/anon/.eclipse/1927926929_linux_gtk_x86_64/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.project.dir=
eclipse.preferences.version=1
gradle.user.home=
java.home=/nix/store/c3pl7bqrx3d2rc3dh98z6yaj0mv1p52g-openjdk-21.0.10+7/lib/openjdk
jvm.arguments=
offline.mode=false
override.workspace.settings=true
show.console.view=true
show.executions.view=true

View File

@@ -8,17 +8,17 @@ JavaFX canvas terminal prototype using `jlibghostty` for terminal emulation, Nix
nix build
```
The package build compiles with Nix-provided OpenJFX 25, `jlibghostty`, JToml, and GraalVM Native Image directly so it does not depend on Gradle plugin resolution inside the Nix sandbox.
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.
For development:
```sh
nix develop
gradle -PjlibghosttyMavenRepo="$JLIBGHOSTTY_MAVEN_REPO" run
gradle -PjlibghosttyMavenRepo="$JLIBGHOSTTY_MAVEN_REPO" nativeCompile
gradle -PjlibghosttyMavenRepo="$JLIBGHOSTTY_MAVEN_REPO" -Pgluonfx.mainClassName=com.gregor.jprototerm.Main nativeExecutable
```
The Gradle project is kept for interactive development and IDE import.
The Gradle project is the source of truth for native JavaFX builds.
## Config
@@ -57,11 +57,17 @@ navigate_down = "ALT+J"
navigate_up = "ALT+K"
navigate_right = "ALT+L"
toggle_floating = "ALT+F"
new_floating = "ALT+SHIFT+F"
next_floating = "ALT+F12"
close_pane = "ALT+X"
```
## Defaults
- `Alt+h/j/k/l`: navigate panes
- `Alt+f`: open or close a floating pane
- Font default: `Symbols Nerd Font Mono`
- `Alt+f`: show or hide all floating panes
- `Alt+Shift+f`: create a new floating pane
- `Alt+F12`: cycle floating panes
- `Alt+x`: close the active floating pane
- Font default: `JetBrainsMono Nerd Font`
- Kitty graphics protocol parsing is enabled by default

31
build.gradle Normal file
View File

@@ -0,0 +1,31 @@
plugins {
id 'application'
id 'org.openjfx.javafxplugin' version '0.1.0'
}
repositories {
mavenCentral()
maven {
url = uri(System.getenv("JLIBGHOSTTY_MAVEN_REPO"))
}
}
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'
modules = [ 'javafx.controls', 'javafx.fxml' ]
}
application {
mainClass = 'com.gregor.jprototerm.Main'
}
run {
jvmArgs += ['--enable-native-access=ALL-UNNAMED']
}

View File

@@ -1,32 +0,0 @@
plugins {
application
id("org.openjfx.javafxplugin") version "0.1.0"
id("com.gluonhq.gluonfx-gradle-plugin") version "1.0.28"
}
group = "com.gregor"
version = "0.1.0"
dependencies {
implementation("dev.jlibghostty:jlibghostty:0.1.0-SNAPSHOT")
implementation("io.github.wasabithumb:jtoml:1.5.2")
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(25))
}
}
application {
mainClass.set("com.gregor.jprototerm.Main")
}
javafx {
version = "25"
modules = listOf("javafx.controls", "javafx.graphics")
}
gluonfx {
mainClassName = "com.gregor.jprototerm.Main"
}

Binary file not shown.

View File

@@ -18,3 +18,6 @@ navigate_down = "ALT+J"
navigate_up = "ALT+K"
navigate_right = "ALT+L"
toggle_floating = "ALT+F"
new_floating = "ALT+SHIFT+F"
next_floating = "ALT+F12"
close_pane = "ALT+X"

65
devenv.lock Normal file
View File

@@ -0,0 +1,65 @@
{
"nodes": {
"devenv": {
"locked": {
"dir": "src/modules",
"lastModified": 1779749056,
"narHash": "sha256-AtocdrunzuxTvSDn+82RntEhrs6TicM6Z4/zNQS9KKg=",
"owner": "cachix",
"repo": "devenv",
"rev": "099ac65fcef79e88127bdc06adbd1ea94255274a",
"type": "github"
},
"original": {
"dir": "src/modules",
"owner": "cachix",
"repo": "devenv",
"type": "github"
}
},
"nixpkgs": {
"inputs": {
"nixpkgs-src": "nixpkgs-src"
},
"locked": {
"lastModified": 1778507786,
"narHash": "sha256-HzSQCKMsMr8r55LwM1JuzIOB+8bzk0FEv6sItKvsfoY=",
"owner": "cachix",
"repo": "devenv-nixpkgs",
"rev": "8f24a228a782e24576b155d1e39f0d914b380691",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "rolling",
"repo": "devenv-nixpkgs",
"type": "github"
}
},
"nixpkgs-src": {
"flake": false,
"locked": {
"lastModified": 1778274207,
"narHash": "sha256-I4puXmX1iovcCHZlRmztO3vW0mAbbRvq4F8wgIMQ1MM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b3da656039dc7a6240f27b2ef8cc6a3ef3bccae7",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"devenv": "devenv",
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

46
devenv.nix Normal file
View File

@@ -0,0 +1,46 @@
{ pkgs, lib, config, inputs, ... }:
let
system = pkgs.stdenv.hostPlatform.system;
jlibghostty = builtins.getFlake
"git+https://gitea.gregorlohaus.com/gregor/jlibghostty.git";
jlib = jlibghostty.packages.${system}.jlibghostty;
in
{
packages = [
pkgs.git
pkgs.gradle_9
pkgs.jdk25
pkgs.jdt-language-server
pkgs.openjfx
pkgs.glib
pkgs.xorg.libXxf86vm
pkgs.xorg.libXrender
pkgs.xorg.libXtst
pkgs.xorg.libXi
pkgs.xorg.libXrandr
pkgs.libGL
pkgs.gtk3
pkgs.alsa-lib
];
env.LD_LIBRARY_PATH = lib.makeLibraryPath [
pkgs.openjfx
pkgs.glib
pkgs.xorg.libXxf86vm
pkgs.xorg.libXrender
pkgs.xorg.libXtst
pkgs.xorg.libXi
pkgs.xorg.libXrandr
pkgs.libGL
pkgs.gtk3
pkgs.alsa-lib
] + ":/usr/lib/x86_64-linux-gnu/nvidia/current";
env.JLIBGHOSTTY_MAVEN_REPO = "${jlib}/maven";
}

18
devenv.yaml Normal file
View File

@@ -0,0 +1,18 @@
# yaml-language-server: $schema=https://devenv.sh/devenv.schema.json
inputs:
nixpkgs:
url: github:cachix/devenv-nixpkgs/rolling
# If you're using non-OSS software, you can set allowUnfree to true.
# allowUnfree: true
# If you're not willing to allow unsupported packages:
# allowUnsupportedSystem: false
# If you're willing to use a package that's vulnerable
# permittedInsecurePackages:
# - "openssl-1.1.1w"
# If you have more than one devenv you can merge them
#imports:
# - ./backend

274
flake.lock generated
View File

@@ -1,274 +0,0 @@
{
"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"
}
},
"javafx-base": {
"flake": false,
"locked": {
"narHash": "sha256-96fttJUts/rFwKB7u5G8NWkK2NjJ3a6eIKbe1RTWkmM=",
"type": "file",
"url": "https://repo.maven.apache.org/maven2/org/openjfx/javafx-base/25/javafx-base-25-linux.jar"
},
"original": {
"type": "file",
"url": "https://repo.maven.apache.org/maven2/org/openjfx/javafx-base/25/javafx-base-25-linux.jar"
}
},
"javafx-controls": {
"flake": false,
"locked": {
"narHash": "sha256-2Cdc2/hPOjJmQidDjXu9vnlwAuawLn0cg/tLhzFfkUs=",
"type": "file",
"url": "https://repo.maven.apache.org/maven2/org/openjfx/javafx-controls/25/javafx-controls-25-linux.jar"
},
"original": {
"type": "file",
"url": "https://repo.maven.apache.org/maven2/org/openjfx/javafx-controls/25/javafx-controls-25-linux.jar"
}
},
"javafx-graphics": {
"flake": false,
"locked": {
"narHash": "sha256-w01IhRAQzcfTvwkqIQkjrI8ZPXT0VTEeijfzbqp3G0k=",
"type": "file",
"url": "https://repo.maven.apache.org/maven2/org/openjfx/javafx-graphics/25/javafx-graphics-25-linux.jar"
},
"original": {
"type": "file",
"url": "https://repo.maven.apache.org/maven2/org/openjfx/javafx-graphics/25/javafx-graphics-25-linux.jar"
}
},
"jlibghostty": {
"inputs": {
"ghostty": "ghostty",
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1779889299,
"narHash": "sha256-B82MyhTvlfeszdcuM3F8YDSZYaxUom+m59oQKSoWjmQ=",
"ref": "refs/heads/main",
"rev": "eea43843002f8fae4fa4cb1c46b64339124bf6b2",
"revCount": 6,
"type": "git",
"url": "https://gitea.gregorlohaus.com/gregor/jlibghostty.git"
},
"original": {
"type": "git",
"url": "https://gitea.gregorlohaus.com/gregor/jlibghostty.git"
}
},
"jtoml-all": {
"flake": false,
"locked": {
"narHash": "sha256-KWrUaDVmnWzdkQxjgPFFNl8DOEvkCqWW3OmXU2sZHKw=",
"type": "file",
"url": "https://repo.maven.apache.org/maven2/io/github/wasabithumb/jtoml-all/1.5.2/jtoml-all-1.5.2.jar"
},
"original": {
"type": "file",
"url": "https://repo.maven.apache.org/maven2/io/github/wasabithumb/jtoml-all/1.5.2/jtoml-all-1.5.2.jar"
}
},
"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": {
"javafx-base": "javafx-base",
"javafx-controls": "javafx-controls",
"javafx-graphics": "javafx-graphics",
"jlibghostty": "jlibghostty",
"jtoml-all": "jtoml-all",
"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
}

182
flake.nix
View File

@@ -1,182 +0,0 @@
{
description = "JavaFX terminal using jlibghostty and GraalVM Native Image";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
jlibghostty.url = "git+https://gitea.gregorlohaus.com/gregor/jlibghostty.git";
jtoml-all = {
url = "https://repo.maven.apache.org/maven2/io/github/wasabithumb/jtoml-all/1.5.2/jtoml-all-1.5.2.jar";
flake = false;
};
javafx-base = {
url = "https://repo.maven.apache.org/maven2/org/openjfx/javafx-base/25/javafx-base-25-linux.jar";
flake = false;
};
javafx-controls = {
url = "https://repo.maven.apache.org/maven2/org/openjfx/javafx-controls/25/javafx-controls-25-linux.jar";
flake = false;
};
javafx-graphics = {
url = "https://repo.maven.apache.org/maven2/org/openjfx/javafx-graphics/25/javafx-graphics-25-linux.jar";
flake = false;
};
};
outputs = {
self,
nixpkgs,
jlibghostty,
jtoml-all,
javafx-base,
javafx-controls,
javafx-graphics
}:
let
system = "x86_64-linux";
pkgs = import nixpkgs { inherit system; };
jlib = jlibghostty.packages.${system}.jlibghostty;
graalvm = pkgs.graalvmPackages.graalvm-ce;
gradle = if pkgs ? gradle_9 then pkgs.gradle_9 else pkgs.gradle;
openjfx = pkgs.javaPackages.openjfx25;
javafxNativeLibraryPath = pkgs.lib.concatStringsSep ":" [
"${openjfx}/modules_libs/javafx.base"
"${openjfx}/modules_libs/javafx.graphics"
"${openjfx}/modules_libs/javafx.media"
];
x11 = name: oldName: pkgs.${name} or pkgs.xorg.${oldName};
mesaDrivers = pkgs.mesa;
runtimeLibraryPath = pkgs.lib.makeLibraryPath ([
openjfx
jlib
pkgs.gtk3
pkgs.glib
pkgs.pango
pkgs.cairo
pkgs.gdk-pixbuf
pkgs.harfbuzz
pkgs.freetype
pkgs.fontconfig
pkgs.libxkbcommon
pkgs.zlib
pkgs.stdenv.cc.cc.lib
pkgs.libglvnd
(x11 "libx11" "libX11")
(x11 "libxext" "libXext")
(x11 "libxrender" "libXrender")
(x11 "libxtst" "libXtst")
(x11 "libxi" "libXi")
(x11 "libxcursor" "libXcursor")
(x11 "libxrandr" "libXrandr")
(x11 "libxinerama" "libXinerama")
(x11 "libxcb" "libxcb")
]
++ pkgs.lib.optionals (pkgs ? atk) [ pkgs.atk ]
++ pkgs.lib.optionals (pkgs ? libxxf86vm || pkgs.xorg ? libXxf86vm) [ (x11 "libxxf86vm" "libXxf86vm") ]
++ pkgs.lib.optionals (pkgs ? libGL) [ pkgs.libGL ]
++ pkgs.lib.optionals (pkgs ? mesa) [ pkgs.mesa ]);
openglDriverPath = pkgs.lib.concatStringsSep ":" [
"/run/opengl-driver/lib"
"/run/opengl-driver-32/lib"
"${mesaDrivers}/lib"
];
driDriverPath = pkgs.lib.concatStringsSep ":" [
"/run/opengl-driver/lib/dri"
"/run/opengl-driver-32/lib/dri"
"${mesaDrivers}/lib/dri"
];
in {
packages.${system}.default = pkgs.stdenvNoCC.mkDerivation {
pname = "jprototerm";
version = "0.1.0";
src = ./.;
nativeBuildInputs = [
graalvm
pkgs.makeWrapper
];
buildPhase = ''
runHook preBuild
mkdir -p build/classes build/native-image build/lib build/javafx-modules
find src/main/java -name '*.java' | sort > build/sources.txt
cp ${jtoml-all} build/lib/jtoml-all.jar
cp ${javafx-base} build/javafx-modules/javafx-base.jar
cp ${javafx-controls} build/javafx-modules/javafx-controls.jar
cp ${javafx-graphics} build/javafx-modules/javafx-graphics.jar
javafx_module_path="build/javafx-modules"
jlib_classpath="$(
find ${jlib}/maven -type f -name '*.jar' \
! -name '*-sources.jar' \
! -name '*-javadoc.jar' \
| sort \
| paste -sd: -
)"
app_classpath="build/classes:build/lib/jtoml-all.jar:$jlib_classpath:build/javafx-modules/javafx-base.jar:build/javafx-modules/javafx-controls.jar:build/javafx-modules/javafx-graphics.jar"
javac \
--release 25 \
--module-path "$javafx_module_path" \
--add-modules javafx.controls,javafx.graphics \
-cp "build/lib/jtoml-all.jar:$jlib_classpath" \
-d build/classes \
@build/sources.txt
if [ -d src/main/resources ]; then
cp -R src/main/resources/. build/classes/
fi
native-image \
--no-fallback \
--enable-native-access=javafx.graphics \
-Djava.library.path=${javafxNativeLibraryPath} \
--module-path "$javafx_module_path" \
--add-modules javafx.controls,javafx.graphics \
-cp "$app_classpath" \
-H:Class=com.gregor.jprototerm.Main \
-o build/native-image/jprototerm
runHook postBuild
'';
installPhase = ''
runHook preInstall
mkdir -p $out/bin
cp build/native-image/jprototerm $out/bin/jprototerm
wrapProgram $out/bin/jprototerm \
--set GDK_BACKEND x11 \
--set LIBGL_DRIVERS_PATH ${driDriverPath} \
--set JAVA_TOOL_OPTIONS "-Dprism.order=es2,sw -Dprism.verbose=true" \
--add-flags "-Djava.library.path=${javafxNativeLibraryPath}" \
--add-flags "-Dprism.order=es2,sw" \
--add-flags "-Dprism.verbose=true" \
--prefix LD_LIBRARY_PATH : ${javafxNativeLibraryPath}:${runtimeLibraryPath}:${openglDriverPath} \
--prefix PATH : ${pkgs.lib.makeBinPath [ pkgs.util-linux pkgs.bash ]}
runHook postInstall
'';
};
devShells.${system}.default = pkgs.mkShell {
packages = [
graalvm
gradle
pkgs.util-linux
];
shellHook = ''
export JLIBGHOSTTY_MAVEN_REPO=${jlib}/maven
echo "Use: gradle -PjlibghosttyMavenRepo=$JLIBGHOSTTY_MAVEN_REPO run"
'';
};
};
}

View File

@@ -1,22 +0,0 @@
pluginManagement {
repositories {
gradlePluginPortal()
mavenCentral()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenCentral()
maven {
url = uri(
providers.gradleProperty("jlibghosttyMavenRepo")
.orElse("../jlibghostty/result/maven")
.get()
)
}
}
}
rootProject.name = "jprototerm"

View File

@@ -45,7 +45,10 @@ public record AppConfig(
"navigate_down", binding(document, "keybindings.navigate_down", defaults.keybindings.get("navigate_down")),
"navigate_up", binding(document, "keybindings.navigate_up", defaults.keybindings.get("navigate_up")),
"navigate_right", binding(document, "keybindings.navigate_right", defaults.keybindings.get("navigate_right")),
"toggle_floating", binding(document, "keybindings.toggle_floating", defaults.keybindings.get("toggle_floating"))
"toggle_floating", binding(document, "keybindings.toggle_floating", defaults.keybindings.get("toggle_floating")),
"new_floating", binding(document, "keybindings.new_floating", defaults.keybindings.get("new_floating")),
"next_floating", binding(document, "keybindings.next_floating", defaults.keybindings.get("next_floating")),
"close_pane", binding(document, "keybindings.close_pane", defaults.keybindings.get("close_pane"))
)
);
} catch (TomlException ex) {
@@ -59,7 +62,7 @@ public record AppConfig(
100,
30,
defaultShell(),
"Symbols Nerd Font Mono",
"JetBrainsMono Nerd Font",
15.0,
1200.0,
760.0,
@@ -69,7 +72,10 @@ public record AppConfig(
"navigate_down", KeyBinding.parse("ALT+J"),
"navigate_up", KeyBinding.parse("ALT+K"),
"navigate_right", KeyBinding.parse("ALT+L"),
"toggle_floating", KeyBinding.parse("ALT+F")
"toggle_floating", KeyBinding.parse("ALT+F"),
"new_floating", KeyBinding.parse("ALT+SHIFT+F"),
"next_floating", KeyBinding.parse("ALT+F12"),
"close_pane", KeyBinding.parse("ALT+X")
)
);
}
@@ -83,8 +89,7 @@ public record AppConfig(
}
private static String defaultShell() {
String shell = System.getenv("SHELL");
return shell == null || shell.isBlank() ? "/bin/sh" : shell;
return "/bin/bash";
}
private static KeyBinding binding(TomlTable table, String key, KeyBinding fallback) {

View File

@@ -18,7 +18,7 @@ public record KeyBinding(boolean alt, boolean control, boolean shift, KeyCode co
case "ALT", "META" -> alt = true;
case "CTRL", "CONTROL" -> control = true;
case "SHIFT" -> shift = true;
default -> code = KeyCode.getKeyCode(token);
default -> code = keyCode(token);
}
}
@@ -34,4 +34,19 @@ public record KeyBinding(boolean alt, boolean control, boolean shift, KeyCode co
&& event.isShiftDown() == shift
&& event.getCode() == code;
}
private static KeyCode keyCode(String token) {
KeyCode alias = switch (token) {
case "GRAVE", "BACKTICK", "BACK_QUOTE", "`" -> KeyCode.BACK_QUOTE;
default -> null;
};
if (alias != null) {
return alias;
}
try {
return KeyCode.valueOf(token);
} catch (IllegalArgumentException ex) {
return KeyCode.getKeyCode(token);
}
}
}

View File

@@ -8,6 +8,20 @@ final class KeyEncoder {
}
static String encode(KeyEvent event) {
if (event.isControlDown() && !event.isAltDown() && !event.isMetaDown()) {
String control = controlSequence(event);
if (control != null) {
return control;
}
}
if (event.isAltDown() && !event.isControlDown() && !event.isMetaDown()) {
String alt = altSequence(event);
if (alt != null) {
return alt;
}
}
KeyCode code = event.getCode();
return switch (code) {
case ENTER -> "\r";
@@ -23,7 +37,43 @@ final class KeyEncoder {
case DELETE -> "\u001b[3~";
case PAGE_UP -> "\u001b[5~";
case PAGE_DOWN -> "\u001b[6~";
case F1 -> "\u001bOP";
case F2 -> "\u001bOQ";
case F3 -> "\u001bOR";
case F4 -> "\u001bOS";
case F5 -> "\u001b[15~";
case F6 -> "\u001b[17~";
case F7 -> "\u001b[18~";
case F8 -> "\u001b[19~";
case F9 -> "\u001b[20~";
case F10 -> "\u001b[21~";
case F11 -> "\u001b[23~";
case F12 -> "\u001b[24~";
default -> null;
};
}
private static String controlSequence(KeyEvent event) {
KeyCode code = event.getCode();
if (code.isLetterKey()) {
return String.valueOf((char) (Character.toUpperCase(code.getName().charAt(0)) - '@'));
}
return switch (code) {
case SPACE -> "\u0000";
case OPEN_BRACKET -> "\u001b";
case BACK_SLASH -> "\u001c";
case CLOSE_BRACKET -> "\u001d";
case DIGIT6 -> "\u001e";
case MINUS -> "\u001f";
default -> null;
};
}
private static String altSequence(KeyEvent event) {
KeyCode code = event.getCode();
if (code.isLetterKey() || code.isDigitKey()) {
return "\u001b" + code.getName().toLowerCase();
}
return null;
}
}

View File

@@ -1,143 +0,0 @@
package com.gregor.jprototerm;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.Image;
import java.io.ByteArrayInputStream;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public final class KittyGraphicsRegistry {
private final boolean enabled;
private final StringBuilder stream = new StringBuilder();
private final Map<Integer, StringBuilder> chunks = new HashMap<>();
private final List<Placement> placements = new ArrayList<>();
public KittyGraphicsRegistry(boolean enabled) {
this.enabled = enabled;
}
public synchronized void accept(String text) {
if (!enabled) {
return;
}
stream.append(text);
parseBufferedCommands();
}
public synchronized void draw(GraphicsContext gc, double originX, double originY, double cellWidth, double lineHeight) {
if (!enabled) {
return;
}
for (Placement placement : placements) {
double x = originX + placement.column * cellWidth;
double y = originY + placement.row * lineHeight;
double width = placement.columns <= 0 ? placement.image.getWidth() : placement.columns * cellWidth;
double height = placement.rows <= 0 ? placement.image.getHeight() : placement.rows * lineHeight;
gc.drawImage(placement.image, x, y, width, height);
}
}
public synchronized void clear() {
chunks.clear();
placements.clear();
stream.setLength(0);
}
private void parseBufferedCommands() {
int start;
while ((start = stream.indexOf("\u001b_G")) >= 0) {
int end = commandEnd(start + 3);
if (end < 0) {
if (start > 0) {
stream.delete(0, start);
}
return;
}
String command = stream.substring(start + 3, end);
handleCommand(command);
stream.delete(0, end + terminatorLength(end));
}
if (stream.length() > 16384) {
stream.delete(0, stream.length() - 4096);
}
}
private int commandEnd(int from) {
int bell = stream.indexOf("\u0007", from);
int st = stream.indexOf("\u001b\\", from);
if (bell < 0) {
return st;
}
if (st < 0) {
return bell;
}
return Math.min(bell, st);
}
private int terminatorLength(int end) {
return stream.charAt(end) == '\u0007' ? 1 : 2;
}
private void handleCommand(String command) {
int separator = command.indexOf(';');
if (separator < 0) {
return;
}
Map<String, String> control = parseControl(command.substring(0, separator));
String payload = command.substring(separator + 1).replace("\n", "").replace("\r", "");
int id = intControl(control, "i", 1);
boolean more = intControl(control, "m", 0) == 1;
chunks.computeIfAbsent(id, ignored -> new StringBuilder()).append(payload);
if (more) {
return;
}
String data = chunks.remove(id).toString();
try {
byte[] bytes = Base64.getDecoder().decode(data);
Image image = new Image(new ByteArrayInputStream(bytes));
if (!image.isError()) {
placements.add(new Placement(
image,
intControl(control, "x", 0),
intControl(control, "y", 0),
intControl(control, "c", 0),
intControl(control, "r", 0)
));
}
} catch (IllegalArgumentException ignored) {
chunks.remove(id);
}
}
private static Map<String, String> parseControl(String text) {
Map<String, String> result = new HashMap<>();
for (String part : text.split(",")) {
int equals = part.indexOf('=');
if (equals > 0) {
result.put(part.substring(0, equals), part.substring(equals + 1));
}
}
return result;
}
private static int intControl(Map<String, String> control, String key, int fallback) {
try {
return Integer.parseInt(control.getOrDefault(key, String.valueOf(fallback)));
} catch (NumberFormatException ex) {
return fallback;
}
}
private record Placement(Image image, int column, int row, int columns, int rows) {
}
}

View File

@@ -20,6 +20,7 @@ public final class Main extends Application {
StackPane root = new StackPane(terminalView.canvas());
terminalView.canvas().widthProperty().bind(root.widthProperty());
terminalView.canvas().heightProperty().bind(root.heightProperty());
terminalView.canvas().setOnMousePressed(event -> terminalView.canvas().requestFocus());
Scene scene = new Scene(root, config.windowWidth(), config.windowHeight());
scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> handlePressed(config, event));
@@ -38,6 +39,7 @@ public final class Main extends Application {
workspace.close();
});
stage.show();
terminalView.canvas().requestFocus();
}
private void handlePressed(AppConfig config, KeyEvent event) {
@@ -56,6 +58,15 @@ public final class Main extends Application {
} else if (config.keybindings().get("toggle_floating").matches(event)) {
workspace.toggleFloating();
event.consume();
} else if (config.keybindings().get("new_floating").matches(event)) {
workspace.createFloatingPane();
event.consume();
} else if (config.keybindings().get("next_floating").matches(event)) {
workspace.nextFloatingPane();
event.consume();
} else if (config.keybindings().get("close_pane").matches(event)) {
workspace.closeActivePane();
event.consume();
} else {
String encoded = KeyEncoder.encode(event);
if (encoded != null) {
@@ -78,8 +89,7 @@ public final class Main extends Application {
}
public static void main(String[] args) {
System.setProperty("prism.order", "es2,sw");
System.setProperty("prism.verbose", "true");
System.setProperty("prism.order", System.getProperty("prism.order", "es2,sw"));
launch(Main.class, args);
}
}

View File

@@ -1,20 +1,25 @@
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;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public final class ShellSession implements AutoCloseable {
private final Process process;
private final PtyProcess process;
private final OutputStream stdin;
private final ExecutorService reader;
private volatile boolean closed;
private ShellSession(Process process, TerminalPane pane, KittyGraphicsRegistry graphicsRegistry) {
private ShellSession(PtyProcess process, TerminalPane pane) {
this.process = process;
this.stdin = process.getOutputStream();
this.reader = Executors.newSingleThreadExecutor(runnable -> {
@@ -22,27 +27,35 @@ public final class ShellSession implements AutoCloseable {
thread.setDaemon(true);
return thread;
});
reader.submit(() -> readOutput(pane, graphicsRegistry));
reader.submit(() -> readOutput(pane));
}
public static ShellSession start(String shell, TerminalPane pane, KittyGraphicsRegistry graphicsRegistry) {
public static ShellSession start(String shell, TerminalPane pane, int columns, int rows) {
try {
ProcessBuilder processBuilder = new ProcessBuilder(
"script",
"-qfec",
shell + " -i",
"/dev/null"
).redirectErrorStream(true);
processBuilder.environment().put("TERM", "xterm-kitty");
processBuilder.environment().put("COLORTERM", "truecolor");
Process process = processBuilder.start();
return new ShellSession(process, pane, graphicsRegistry);
Map<String, String> environment = new HashMap<>(System.getenv());
environment.put("TERM", "xterm-kitty");
environment.put("COLORTERM", "truecolor");
PtyProcess process = new PtyProcessBuilder(new String[] {shell, "-i"})
.setEnvironment(environment)
.setInitialColumns(columns)
.setInitialRows(rows)
.setDirectory(System.getProperty("user.home"))
.start();
return new ShellSession(process, pane);
} catch (IOException ex) {
pane.write("failed to start shell: " + ex.getMessage() + "\r\n");
throw new IllegalStateException("Could not start shell " + shell, ex);
}
}
public void resize(int columns, int rows) {
if (closed) {
return;
}
process.setWinSize(new WinSize(columns, rows));
}
public void send(String text) {
if (closed) {
return;
@@ -55,17 +68,17 @@ public final class ShellSession implements AutoCloseable {
}
}
private void readOutput(TerminalPane pane, KittyGraphicsRegistry graphicsRegistry) {
private void readOutput(TerminalPane pane) {
byte[] buffer = new byte[8192];
try {
int read;
while ((read = process.getInputStream().read(buffer)) != -1) {
String text = new String(buffer, 0, read, StandardCharsets.UTF_8);
if (!closed) {
graphicsRegistry.accept(text);
byte[] bytes = new byte[read];
System.arraycopy(buffer, 0, bytes, 0, read);
Platform.runLater(() -> {
if (!closed) {
pane.write(text);
pane.write(bytes);
}
});
}

View File

@@ -1,15 +1,37 @@
package com.gregor.jprototerm;
import dev.jlibghostty.KittyImageCompression;
import dev.jlibghostty.KittyImageFormat;
import dev.jlibghostty.KittyImageSnapshot;
import dev.jlibghostty.KittyPlacement;
import dev.jlibghostty.KittyRenderInfo;
import dev.jlibghostty.RenderCell;
import dev.jlibghostty.RenderColor;
import dev.jlibghostty.RenderCursorStyle;
import dev.jlibghostty.RenderRow;
import dev.jlibghostty.RenderStateSnapshot;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.Image;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.WritableImage;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontSmoothingType;
import javafx.scene.text.Text;
import java.io.ByteArrayInputStream;
import java.util.HashMap;
import java.util.Map;
public final class TerminalCanvasView {
private static final Color DEFAULT_FOREGROUND = Color.rgb(225, 229, 235);
private static final Color SELECTED_BACKGROUND = Color.rgb(52, 92, 140);
private final Canvas canvas = new Canvas();
private final TerminalWorkspace workspace;
private final AppConfig config;
private final Map<Long, Image> kittyImageCache = new HashMap<>();
public TerminalCanvasView(TerminalWorkspace workspace, AppConfig config) {
this.workspace = workspace;
@@ -29,7 +51,7 @@ public final class TerminalCanvasView {
GraphicsContext gc = canvas.getGraphicsContext2D();
gc.setFill(Color.rgb(16, 16, 18));
gc.fillRect(0, 0, width, height);
gc.setFontSmoothingType(FontSmoothingType.GRAY);
gc.setFontSmoothingType(FontSmoothingType.LCD);
for (TerminalPane pane : workspace.panes()) {
drawPane(gc, pane);
@@ -55,20 +77,172 @@ public final class TerminalCanvasView {
Font font = Font.font(config.fontFamily(), config.fontSize());
gc.setFont(font);
gc.setFill(Color.rgb(225, 229, 235));
double lineHeight = Math.ceil(config.fontSize() * 1.35);
FontMetrics metrics = measureFontMetrics(font);
int columns = Math.max(1, (int) ((pane.width() - 24.0) / metrics.cellWidth));
int rows = Math.max(1, (int) ((pane.height() - 24.0) / metrics.lineHeight));
pane.resize(columns, rows, (int) Math.round(metrics.cellWidth), (int) Math.round(metrics.lineHeight));
double left = pane.x() + 12.0;
double baseline = pane.y() + 18.0;
int maxLines = Math.max(1, (int) ((pane.height() - 24.0) / lineHeight));
double top = pane.y() + 12.0;
double baseline = top + metrics.baselineOffset;
String[] lines = pane.snapshotText().split("\\R", -1);
int start = Math.max(0, lines.length - maxLines);
for (int i = start; i < lines.length; i++) {
gc.fillText(lines[i], left, baseline + ((i - start) * lineHeight));
RenderStateSnapshot snapshot = pane.renderSnapshot();
if (snapshot != null) {
for (RenderRow row : snapshot.renderRows()) {
drawRow(gc, row, left, top, baseline, metrics.cellWidth, metrics.lineHeight);
}
}
pane.graphicsRegistry().draw(gc, pane.x() + 12.0, pane.y() + 12.0, config.fontSize() * 0.62, lineHeight);
if (snapshot != null) {
drawCursor(gc, snapshot, left, top, metrics.cellWidth, metrics.lineHeight);
}
if (config.kittyGraphics()) {
drawKittyGraphics(gc, pane, left, top, metrics.cellWidth, metrics.lineHeight);
}
gc.restore();
}
private static FontMetrics measureFontMetrics(Font font) {
Text text = new Text("Mg");
text.setFont(font);
double textHeight = text.getLayoutBounds().getHeight();
double lineHeight = Math.max(1.0, Math.ceil(textHeight * 1.2));
double baselineOffset = -text.getLayoutBounds().getMinY() + ((lineHeight - textHeight) / 2.0);
Text cell = new Text("M");
cell.setFont(font);
double cellWidth = Math.max(1.0, Math.ceil(cell.getLayoutBounds().getWidth()));
return new FontMetrics(cellWidth, lineHeight, baselineOffset);
}
private static void drawRow(
GraphicsContext gc,
RenderRow row,
double left,
double top,
double baseline,
double cellWidth,
double lineHeight
) {
for (RenderCell cell : row.cells()) {
double x = left + (cell.column() * cellWidth);
double cellTop = top + (row.row() * lineHeight);
cell.background().ifPresent(background -> {
gc.setFill(toFxColor(background));
gc.fillRect(x, cellTop, cellWidth, lineHeight);
});
if (cell.selected()) {
gc.setFill(SELECTED_BACKGROUND);
gc.fillRect(x, cellTop, cellWidth, lineHeight);
}
if (cell.codepoints().length == 0) {
continue;
}
double y = baseline + (row.row() * lineHeight);
Color foreground = cell.foreground().map(TerminalCanvasView::toFxColor).orElse(DEFAULT_FOREGROUND);
gc.setFill(foreground);
gc.fillText(cell.text(), x, y);
}
}
private static Color toFxColor(RenderColor color) {
return Color.rgb(color.red(), color.green(), color.blue());
}
private static void drawCursor(GraphicsContext gc, RenderStateSnapshot snapshot, double left, double top, double cellWidth, double lineHeight) {
if (!snapshot.cursorVisible() || !snapshot.cursorViewportHasValue()) {
return;
}
double x = left + (snapshot.cursorViewportX() * cellWidth);
double y = top + (snapshot.cursorViewportY() * lineHeight);
gc.setStroke(Color.rgb(225, 229, 235));
gc.setFill(Color.rgb(225, 229, 235, 0.28));
gc.setLineWidth(1.5);
RenderCursorStyle style = snapshot.cursorStyle();
if (style == RenderCursorStyle.BAR) {
gc.strokeLine(x + 0.5, y + 2.0, x + 0.5, y + lineHeight - 2.0);
} else if (style == RenderCursorStyle.UNDERLINE) {
gc.strokeLine(x + 1.0, y + lineHeight - 2.0, x + cellWidth - 1.0, y + lineHeight - 2.0);
} else if (style == RenderCursorStyle.BLOCK) {
gc.fillRect(x + 0.5, y + 1.0, Math.max(1.0, cellWidth - 1.0), Math.max(1.0, lineHeight - 2.0));
} else {
gc.strokeRect(x + 0.5, y + 1.0, Math.max(1.0, cellWidth - 1.0), Math.max(1.0, lineHeight - 2.0));
}
}
private void drawKittyGraphics(GraphicsContext gc, TerminalPane pane, double originX, double originY, double cellWidth, double lineHeight) {
pane.kittyGraphics().ifPresent(graphics -> {
for (KittyPlacement placement : graphics.placements()) {
Image image = imageFor(placement);
if (image == null) {
continue;
}
KittyRenderInfo renderInfo = placement.renderInfo().orElse(null);
double x = originX;
double y = originY;
double width = image.getWidth();
double height = image.getHeight();
if (renderInfo != null) {
x += renderInfo.viewportColumn() * cellWidth;
y += renderInfo.viewportRow() * lineHeight;
width = renderInfo.gridColumns() > 0 ? renderInfo.gridColumns() * cellWidth : renderInfo.pixelWidth();
height = renderInfo.gridRows() > 0 ? renderInfo.gridRows() * lineHeight : renderInfo.pixelHeight();
} else {
width = placement.columns() > 0 ? placement.columns() * cellWidth : width;
height = placement.rows() > 0 ? placement.rows() * lineHeight : height;
}
gc.drawImage(image, x + placement.xOffset(), y + placement.yOffset(), width, height);
}
});
}
private Image imageFor(KittyPlacement placement) {
return placement.image()
.map(snapshot -> kittyImageCache.computeIfAbsent(snapshot.id(), ignored -> decodeImage(snapshot)))
.orElse(null);
}
private Image decodeImage(KittyImageSnapshot snapshot) {
if (snapshot.compression() != KittyImageCompression.NONE) {
return null;
}
if (snapshot.format() == KittyImageFormat.PNG) {
return new Image(new ByteArrayInputStream(snapshot.data()));
}
int width = Math.toIntExact(snapshot.width());
int height = Math.toIntExact(snapshot.height());
WritableImage image = new WritableImage(width, height);
byte[] data = snapshot.data();
if (snapshot.format() == KittyImageFormat.RGBA) {
image.getPixelWriter().setPixels(0, 0, width, height, PixelFormat.getByteBgraInstance(), rgbaToBgra(data), 0, width * 4);
} else if (snapshot.format() == KittyImageFormat.RGB) {
image.getPixelWriter().setPixels(0, 0, width, height, PixelFormat.getByteRgbInstance(), data, 0, width * 3);
}
return image;
}
private static byte[] rgbaToBgra(byte[] rgba) {
byte[] bgra = new byte[rgba.length];
for (int i = 0; i + 3 < rgba.length; i += 4) {
bgra[i] = rgba[i + 2];
bgra[i + 1] = rgba[i + 1];
bgra[i + 2] = rgba[i];
bgra[i + 3] = rgba[i + 3];
}
return bgra;
}
private record FontMetrics(double cellWidth, double lineHeight, double baselineOffset) {
}
}

View File

@@ -1,30 +1,38 @@
package com.gregor.jprototerm;
import dev.jlibghostty.Ghostty;
import dev.jlibghostty.KittyGraphics;
import dev.jlibghostty.RenderStateSnapshot;
import dev.jlibghostty.Terminal;
import dev.jlibghostty.TerminalOptions;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
public final class TerminalPane implements AutoCloseable {
private final Terminal terminal;
private final KittyGraphicsRegistry graphicsRegistry;
private final AtomicReference<String> snapshotText = new AtomicReference<>("");
private final AtomicReference<RenderStateSnapshot> renderSnapshot = new AtomicReference<>();
private ShellSession session;
private boolean floating;
private boolean visible = true;
private double x;
private double y;
private double width;
private double height;
private int columns;
private int rows;
private int pixelWidth;
private int pixelHeight;
private TerminalPane(Terminal terminal, KittyGraphicsRegistry graphicsRegistry) {
private TerminalPane(Terminal terminal, int columns, int rows) {
this.terminal = terminal;
this.graphicsRegistry = graphicsRegistry;
this.columns = columns;
this.rows = rows;
}
public static TerminalPane create(int columns, int rows, boolean kittyGraphics) {
public static TerminalPane create(int columns, int rows) {
Terminal terminal = Ghostty.open(TerminalOptions.of(columns, rows));
TerminalPane pane = new TerminalPane(terminal, new KittyGraphicsRegistry(kittyGraphics));
TerminalPane pane = new TerminalPane(terminal, columns, rows);
pane.refresh();
return pane;
}
@@ -36,6 +44,13 @@ public final class TerminalPane implements AutoCloseable {
}
}
public void write(byte[] bytes) {
synchronized (terminal) {
terminal.write(bytes);
refresh();
}
}
public void attach(ShellSession session) {
this.session = session;
}
@@ -46,12 +61,14 @@ public final class TerminalPane implements AutoCloseable {
}
}
public String snapshotText() {
return snapshotText.get();
public RenderStateSnapshot renderSnapshot() {
return renderSnapshot.get();
}
public KittyGraphicsRegistry graphicsRegistry() {
return graphicsRegistry;
public Optional<KittyGraphics> kittyGraphics() {
synchronized (terminal) {
return terminal.kittyGraphics();
}
}
public boolean floating() {
@@ -62,6 +79,14 @@ public final class TerminalPane implements AutoCloseable {
this.floating = floating;
}
public boolean visible() {
return visible;
}
public void setVisible(boolean visible) {
this.visible = visible;
}
public double x() {
return x;
}
@@ -85,8 +110,29 @@ public final class TerminalPane implements AutoCloseable {
this.height = height;
}
public void resize(int columns, int rows, int pixelWidth, int pixelHeight) {
if (columns <= 0 || rows <= 0 || pixelWidth <= 0 || pixelHeight <= 0) {
return;
}
if (this.columns == columns && this.rows == rows && this.pixelWidth == pixelWidth && this.pixelHeight == pixelHeight) {
return;
}
synchronized (terminal) {
terminal.resize(columns, rows, pixelWidth, pixelHeight);
if (session != null) {
session.resize(columns, rows);
}
this.columns = columns;
this.rows = rows;
this.pixelWidth = pixelWidth;
this.pixelHeight = pixelHeight;
refresh();
}
}
private void refresh() {
snapshotText.set(String.valueOf(terminal.snapshot()));
renderSnapshot.set(terminal.renderSnapshot());
}
@Override

View File

@@ -8,6 +8,7 @@ public final class TerminalWorkspace implements AutoCloseable {
private final AppConfig config;
private final List<TerminalPane> panes = new ArrayList<>();
private int activeIndex;
private int hiddenFloatingFocusIndex = -1;
public TerminalWorkspace(AppConfig config) {
this.config = config;
@@ -19,7 +20,18 @@ public final class TerminalWorkspace implements AutoCloseable {
}
public List<TerminalPane> panes() {
return List.copyOf(panes);
List<TerminalPane> visible = panes.stream().filter(TerminalPane::visible).toList();
TerminalPane active = activePane();
if (!active.visible() || !active.floating()) {
return visible;
}
List<TerminalPane> ordered = new ArrayList<>(visible.size());
visible.stream()
.filter(pane -> pane != active)
.forEach(ordered::add);
ordered.add(active);
return List.copyOf(ordered);
}
public boolean isActive(TerminalPane pane) {
@@ -27,20 +39,29 @@ public final class TerminalWorkspace implements AutoCloseable {
}
public void layout(double width, double height) {
List<TerminalPane> tiled = panes.stream().filter(pane -> !pane.floating()).toList();
List<TerminalPane> tiled = panes.stream()
.filter(TerminalPane::visible)
.filter(pane -> !pane.floating())
.toList();
int tileCount = Math.max(1, tiled.size());
double tileWidth = width / tileCount;
for (int i = 0; i < tiled.size(); i++) {
tiled.get(i).bounds(i * tileWidth, 0, tileWidth, height);
}
for (TerminalPane pane : panes) {
if (pane.floating()) {
List<TerminalPane> floating = panes.stream()
.filter(TerminalPane::visible)
.filter(TerminalPane::floating)
.toList();
for (int i = 0; i < floating.size(); i++) {
TerminalPane pane = floating.get(i);
if (pane.visible() && pane.floating()) {
double floatingWidth = Math.max(420, width * 0.58);
double floatingHeight = Math.max(260, height * 0.58);
double offset = i * 28.0;
pane.bounds(
(width - floatingWidth) / 2.0,
(height - floatingHeight) / 2.0,
Math.min(width - floatingWidth - 12.0, ((width - floatingWidth) / 2.0) + offset),
Math.min(height - floatingHeight - 12.0, ((height - floatingHeight) / 2.0) + offset),
floatingWidth,
floatingHeight
);
@@ -50,7 +71,12 @@ public final class TerminalWorkspace implements AutoCloseable {
public void navigate(Direction direction) {
TerminalPane current = activePane();
if (current.floating() && navigateFloatingStack(direction)) {
return;
}
panes.stream()
.filter(TerminalPane::visible)
.filter(pane -> pane != current)
.filter(pane -> directionFilter(direction, current, pane))
.min(Comparator.comparingDouble(pane -> distance(current, pane)))
@@ -58,23 +84,160 @@ public final class TerminalWorkspace implements AutoCloseable {
}
public void toggleFloating() {
TerminalPane active = activePane();
if (active.floating()) {
panes.remove(activeIndex);
active.close();
activeIndex = Math.max(0, activeIndex - 1);
List<TerminalPane> floating = panes.stream()
.filter(TerminalPane::floating)
.toList();
if (floating.isEmpty()) {
createFloatingPane();
return;
}
boolean anyVisible = floating.stream().anyMatch(TerminalPane::visible);
if (anyVisible) {
TerminalPane active = activePane();
hiddenFloatingFocusIndex = active.floating() ? activeIndex : firstVisibleFloatingIndex();
floating.forEach(pane -> pane.setVisible(false));
activeIndex = firstVisibleNonFloatingIndex();
} else {
floating.forEach(pane -> pane.setVisible(true));
activeIndex = visibleIndexOrFallback(hiddenFloatingFocusIndex, panes.indexOf(floating.get(floating.size() - 1)));
hiddenFloatingFocusIndex = -1;
}
}
public void createFloatingPane() {
TerminalPane pane = openPane(true);
panes.add(pane);
activeIndex = panes.size() - 1;
}
public void nextFloatingPane() {
TerminalPane next = nextFloatingAfter(activeIndex);
next.setVisible(true);
activeIndex = panes.indexOf(next);
}
public void closeActivePane() {
TerminalPane active = activePane();
if (!active.floating() || panes.stream().filter(pane -> !pane.floating()).count() == 0) {
return;
}
int removed = activeIndex;
int previous = previousVisibleIndex(removed);
panes.remove(removed);
active.close();
activeIndex = adjustIndexAfterRemoval(previous, removed);
hiddenFloatingFocusIndex = adjustHiddenFocusAfterRemoval(hiddenFloatingFocusIndex, removed);
}
private TerminalPane nextFloatingAfter(int index) {
for (int i = index + 1; i < panes.size(); i++) {
TerminalPane pane = panes.get(i);
if (pane.floating()) {
return pane;
}
}
for (int i = 0; i <= index && i < panes.size(); i++) {
TerminalPane pane = panes.get(i);
if (pane.floating()) {
return pane;
}
}
return createAndReturnFloatingPane();
}
private TerminalPane createAndReturnFloatingPane() {
TerminalPane pane = openPane(true);
panes.add(pane);
return pane;
}
private boolean navigateFloatingStack(Direction direction) {
List<TerminalPane> floating = panes.stream()
.filter(TerminalPane::visible)
.filter(TerminalPane::floating)
.toList();
if (floating.size() < 2) {
return false;
}
int current = floating.indexOf(activePane());
if (current < 0) {
return false;
}
int next = switch (direction) {
case LEFT, UP -> current - 1;
case DOWN, RIGHT -> current + 1;
};
if (next < 0 || next >= floating.size()) {
return false;
}
activeIndex = panes.indexOf(floating.get(next));
return true;
}
private int firstVisibleFloatingIndex() {
for (int i = 0; i < panes.size(); i++) {
TerminalPane pane = panes.get(i);
if (pane.visible() && pane.floating()) {
return i;
}
}
return -1;
}
private int firstVisibleNonFloatingIndex() {
for (int i = 0; i < panes.size(); i++) {
TerminalPane pane = panes.get(i);
if (pane.visible() && !pane.floating()) {
return i;
}
}
return 0;
}
private int previousVisibleIndex(int index) {
for (int i = index - 1; i >= 0; i--) {
if (panes.get(i).visible()) {
return i;
}
}
for (int i = index + 1; i < panes.size(); i++) {
if (panes.get(i).visible()) {
return i;
}
}
return firstVisibleNonFloatingIndex();
}
private int visibleIndexOrFallback(int index, int fallback) {
if (index >= 0 && index < panes.size() && panes.get(index).visible()) {
return index;
}
return fallback;
}
private static int adjustIndexAfterRemoval(int index, int removedIndex) {
if (index < 0) {
return 0;
}
return index > removedIndex ? index - 1 : index;
}
private static int adjustHiddenFocusAfterRemoval(int index, int removedIndex) {
if (index < 0 || index == removedIndex) {
return -1;
}
return index > removedIndex ? index - 1 : index;
}
private TerminalPane openPane(boolean floating) {
TerminalPane pane = TerminalPane.create(config.columns(), config.rows(), config.kittyGraphics());
TerminalPane pane = TerminalPane.create(config.columns(), config.rows());
pane.setFloating(floating);
pane.attach(ShellSession.start(config.shell(), pane, pane.graphicsRegistry()));
pane.attach(ShellSession.start(config.shell(), pane, config.columns(), config.rows()));
return pane;
}

View File

@@ -1,123 +0,0 @@
[
{
"name": "java.lang.Object",
"allPublicMethods": true,
"allDeclaredMethods": true
},
{
"name": "java.lang.String",
"allPublicMethods": true,
"allDeclaredMethods": true
},
{
"name": "java.lang.Runnable",
"allPublicMethods": true,
"allDeclaredMethods": true
},
{
"name": "java.lang.Thread",
"allPublicMethods": true,
"allDeclaredMethods": true
},
{
"name": "java.nio.Buffer",
"allPublicMethods": true,
"allDeclaredMethods": true
},
{
"name": "java.nio.ByteBuffer",
"allPublicMethods": true,
"allDeclaredMethods": true
},
{
"name": "java.nio.DirectByteBuffer",
"allPublicMethods": true,
"allDeclaredMethods": true
},
{
"name": "java.nio.IntBuffer",
"allPublicMethods": true,
"allDeclaredMethods": true
},
{
"name": "java.nio.FloatBuffer",
"allPublicMethods": true,
"allDeclaredMethods": true
},
{
"name": "java.nio.LongBuffer",
"allPublicMethods": true,
"allDeclaredMethods": true
},
{
"name": "java.nio.ShortBuffer",
"allPublicMethods": true,
"allDeclaredMethods": true
},
{
"name": "com.sun.glass.ui.gtk.GtkApplication",
"allDeclaredConstructors": true,
"allPublicMethods": true,
"allDeclaredMethods": true
},
{
"name": "com.sun.glass.ui.gtk.GtkWindow",
"allDeclaredConstructors": true,
"allPublicMethods": true,
"allDeclaredMethods": true
},
{
"name": "com.sun.glass.ui.gtk.GtkView",
"allDeclaredConstructors": true,
"allPublicMethods": true,
"allDeclaredMethods": true
},
{
"name": "com.sun.glass.ui.Application",
"allDeclaredConstructors": true,
"allPublicMethods": true,
"allDeclaredMethods": true
},
{
"name": "com.sun.glass.ui.Window",
"allDeclaredConstructors": true,
"allPublicMethods": true,
"allDeclaredMethods": true
},
{
"name": "com.sun.glass.ui.View",
"allDeclaredConstructors": true,
"allPublicMethods": true,
"allDeclaredMethods": true
},
{
"name": "com.sun.glass.ui.Screen",
"allDeclaredConstructors": true,
"allPublicMethods": true,
"allDeclaredMethods": true
},
{
"name": "com.sun.glass.ui.Pixels",
"allDeclaredConstructors": true,
"allPublicMethods": true,
"allDeclaredMethods": true
},
{
"name": "com.sun.glass.ui.Clipboard",
"allDeclaredConstructors": true,
"allPublicMethods": true,
"allDeclaredMethods": true
},
{
"name": "com.sun.glass.ui.Cursor",
"allDeclaredConstructors": true,
"allPublicMethods": true,
"allDeclaredMethods": true
},
{
"name": "com.sun.glass.ui.CommonDialogs",
"allDeclaredConstructors": true,
"allPublicMethods": true,
"allDeclaredMethods": true
}
]

View File

@@ -1,43 +0,0 @@
[
{
"name": "com.gregor.jprototerm.Main",
"allDeclaredConstructors": true,
"allPublicConstructors": true
},
{
"name": "com.sun.javafx.tk.quantum.QuantumToolkit",
"allDeclaredConstructors": true,
"allPublicConstructors": true
},
{
"name": "com.sun.glass.ui.gtk.GtkPlatformFactory",
"allDeclaredConstructors": true,
"allPublicConstructors": true
},
{
"name": "com.sun.glass.ui.gtk.GtkApplication",
"allDeclaredConstructors": true,
"allPublicConstructors": true
},
{
"name": "com.sun.prism.es2.ES2Pipeline",
"allDeclaredConstructors": true,
"allPublicConstructors": true,
"allDeclaredMethods": true,
"allPublicMethods": true
},
{
"name": "com.sun.prism.es2.X11GLFactory",
"allDeclaredConstructors": true,
"allPublicConstructors": true,
"allDeclaredMethods": true,
"allPublicMethods": true
},
{
"name": "com.sun.prism.sw.SWPipeline",
"allDeclaredConstructors": true,
"allPublicConstructors": true,
"allDeclaredMethods": true,
"allPublicMethods": true
}
]

View File

@@ -1,6 +0,0 @@
{
"resources": [
{ "pattern": ".*\\.css$" },
{ "pattern": ".*\\.toml$" }
]
}