diff --git a/.classpath b/.classpath
new file mode 100644
index 0000000..be88c88
--- /dev/null
+++ b/.classpath
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.gitignore b/.gitignore
index b2be92b..ed72e43 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,12 @@
result
+
+# Devenv
+.devenv*
+devenv.local.nix
+devenv.local.yaml
+
+# direnv
+.direnv
+
+# pre-commit
+.pre-commit-config.yaml
diff --git a/.gradle/8.14.4/fileChanges/last-build.bin b/.gradle/8.14.4/fileChanges/last-build.bin
new file mode 100644
index 0000000..f76dd23
Binary files /dev/null and b/.gradle/8.14.4/fileChanges/last-build.bin differ
diff --git a/.gradle/8.14.4/fileHashes/fileHashes.lock b/.gradle/8.14.4/fileHashes/fileHashes.lock
new file mode 100644
index 0000000..71936ad
Binary files /dev/null and b/.gradle/8.14.4/fileHashes/fileHashes.lock differ
diff --git a/.gradle/8.14.4/gc.properties b/.gradle/8.14.4/gc.properties
new file mode 100644
index 0000000..e69de29
diff --git a/.gradle/8.9/checksums/checksums.lock b/.gradle/8.9/checksums/checksums.lock
new file mode 100644
index 0000000..6b42c4e
Binary files /dev/null and b/.gradle/8.9/checksums/checksums.lock differ
diff --git a/.gradle/8.9/checksums/md5-checksums.bin b/.gradle/8.9/checksums/md5-checksums.bin
new file mode 100644
index 0000000..f5f7e88
Binary files /dev/null and b/.gradle/8.9/checksums/md5-checksums.bin differ
diff --git a/.gradle/8.9/checksums/sha1-checksums.bin b/.gradle/8.9/checksums/sha1-checksums.bin
new file mode 100644
index 0000000..33d1488
Binary files /dev/null and b/.gradle/8.9/checksums/sha1-checksums.bin differ
diff --git a/.gradle/8.9/dependencies-accessors/gc.properties b/.gradle/8.9/dependencies-accessors/gc.properties
new file mode 100644
index 0000000..e69de29
diff --git a/.gradle/8.9/executionHistory/executionHistory.lock b/.gradle/8.9/executionHistory/executionHistory.lock
new file mode 100644
index 0000000..8afbfe1
Binary files /dev/null and b/.gradle/8.9/executionHistory/executionHistory.lock differ
diff --git a/.gradle/8.9/fileChanges/last-build.bin b/.gradle/8.9/fileChanges/last-build.bin
new file mode 100644
index 0000000..f76dd23
Binary files /dev/null and b/.gradle/8.9/fileChanges/last-build.bin differ
diff --git a/.gradle/8.9/fileHashes/fileHashes.lock b/.gradle/8.9/fileHashes/fileHashes.lock
new file mode 100644
index 0000000..1d4ce21
Binary files /dev/null and b/.gradle/8.9/fileHashes/fileHashes.lock differ
diff --git a/.gradle/8.9/gc.properties b/.gradle/8.9/gc.properties
new file mode 100644
index 0000000..e69de29
diff --git a/.gradle/9.4.1/checksums/checksums.lock b/.gradle/9.4.1/checksums/checksums.lock
new file mode 100644
index 0000000..98fc7a5
Binary files /dev/null and b/.gradle/9.4.1/checksums/checksums.lock differ
diff --git a/.gradle/9.4.1/checksums/md5-checksums.bin b/.gradle/9.4.1/checksums/md5-checksums.bin
new file mode 100644
index 0000000..b01b8c5
Binary files /dev/null and b/.gradle/9.4.1/checksums/md5-checksums.bin differ
diff --git a/.gradle/9.4.1/checksums/sha1-checksums.bin b/.gradle/9.4.1/checksums/sha1-checksums.bin
new file mode 100644
index 0000000..8651f4c
Binary files /dev/null and b/.gradle/9.4.1/checksums/sha1-checksums.bin differ
diff --git a/.gradle/9.4.1/executionHistory/executionHistory.bin b/.gradle/9.4.1/executionHistory/executionHistory.bin
new file mode 100644
index 0000000..1a9b6d6
Binary files /dev/null and b/.gradle/9.4.1/executionHistory/executionHistory.bin differ
diff --git a/.gradle/9.4.1/executionHistory/executionHistory.lock b/.gradle/9.4.1/executionHistory/executionHistory.lock
new file mode 100644
index 0000000..ac2b076
Binary files /dev/null and b/.gradle/9.4.1/executionHistory/executionHistory.lock differ
diff --git a/.gradle/9.4.1/fileChanges/last-build.bin b/.gradle/9.4.1/fileChanges/last-build.bin
new file mode 100644
index 0000000..f76dd23
Binary files /dev/null and b/.gradle/9.4.1/fileChanges/last-build.bin differ
diff --git a/.gradle/9.4.1/fileHashes/fileHashes.bin b/.gradle/9.4.1/fileHashes/fileHashes.bin
new file mode 100644
index 0000000..2e15714
Binary files /dev/null and b/.gradle/9.4.1/fileHashes/fileHashes.bin differ
diff --git a/.gradle/9.4.1/fileHashes/fileHashes.lock b/.gradle/9.4.1/fileHashes/fileHashes.lock
new file mode 100644
index 0000000..a2cbede
Binary files /dev/null and b/.gradle/9.4.1/fileHashes/fileHashes.lock differ
diff --git a/.gradle/9.4.1/fileHashes/resourceHashesCache.bin b/.gradle/9.4.1/fileHashes/resourceHashesCache.bin
new file mode 100644
index 0000000..48d03dd
Binary files /dev/null and b/.gradle/9.4.1/fileHashes/resourceHashesCache.bin differ
diff --git a/.gradle/9.4.1/gc.properties b/.gradle/9.4.1/gc.properties
new file mode 100644
index 0000000..e69de29
diff --git a/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/.gradle/buildOutputCleanup/buildOutputCleanup.lock
new file mode 100644
index 0000000..3413bb3
Binary files /dev/null and b/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ
diff --git a/.gradle/buildOutputCleanup/cache.properties b/.gradle/buildOutputCleanup/cache.properties
new file mode 100644
index 0000000..8ef438f
--- /dev/null
+++ b/.gradle/buildOutputCleanup/cache.properties
@@ -0,0 +1,2 @@
+#Wed May 27 23:44:22 CEST 2026
+gradle.version=9.4.1
diff --git a/.gradle/buildOutputCleanup/outputFiles.bin b/.gradle/buildOutputCleanup/outputFiles.bin
new file mode 100644
index 0000000..f25149a
Binary files /dev/null and b/.gradle/buildOutputCleanup/outputFiles.bin differ
diff --git a/.gradle/file-system.probe b/.gradle/file-system.probe
new file mode 100644
index 0000000..676e00e
Binary files /dev/null and b/.gradle/file-system.probe differ
diff --git a/.gradle/vcs-1/gc.properties b/.gradle/vcs-1/gc.properties
new file mode 100644
index 0000000..e69de29
diff --git a/.project b/.project
new file mode 100644
index 0000000..250acc4
--- /dev/null
+++ b/.project
@@ -0,0 +1,34 @@
+
+
+ jprototerm
+ Project jprototerm created by Buildship.
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.buildship.core.gradleprojectbuilder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.buildship.core.gradleprojectnature
+
+
+
+ 1779917652126
+
+ 30
+
+ org.eclipse.core.resources.regexFilterMatcher
+ node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__
+
+
+
+
diff --git a/.settings/org.eclipse.buildship.core.prefs b/.settings/org.eclipse.buildship.core.prefs
new file mode 100644
index 0000000..97aed87
--- /dev/null
+++ b/.settings/org.eclipse.buildship.core.prefs
@@ -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
diff --git a/README.md b/README.md
index 23d2aa0..43dd93f 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..3635554
--- /dev/null
+++ b/build.gradle
@@ -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']
+}
diff --git a/build.gradle.kts b/build.gradle.kts
deleted file mode 100644
index 72e980d..0000000
--- a/build.gradle.kts
+++ /dev/null
@@ -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"
-}
diff --git a/build/classes/java/main/com/gregor/jprototerm/AppConfig.class b/build/classes/java/main/com/gregor/jprototerm/AppConfig.class
new file mode 100644
index 0000000..24abfbb
Binary files /dev/null and b/build/classes/java/main/com/gregor/jprototerm/AppConfig.class differ
diff --git a/build/classes/java/main/com/gregor/jprototerm/Direction.class b/build/classes/java/main/com/gregor/jprototerm/Direction.class
new file mode 100644
index 0000000..e540aa0
Binary files /dev/null and b/build/classes/java/main/com/gregor/jprototerm/Direction.class differ
diff --git a/build/classes/java/main/com/gregor/jprototerm/KeyBinding.class b/build/classes/java/main/com/gregor/jprototerm/KeyBinding.class
new file mode 100644
index 0000000..26acc65
Binary files /dev/null and b/build/classes/java/main/com/gregor/jprototerm/KeyBinding.class differ
diff --git a/build/classes/java/main/com/gregor/jprototerm/KeyEncoder$1.class b/build/classes/java/main/com/gregor/jprototerm/KeyEncoder$1.class
new file mode 100644
index 0000000..09505a7
Binary files /dev/null and b/build/classes/java/main/com/gregor/jprototerm/KeyEncoder$1.class differ
diff --git a/build/classes/java/main/com/gregor/jprototerm/KeyEncoder.class b/build/classes/java/main/com/gregor/jprototerm/KeyEncoder.class
new file mode 100644
index 0000000..56c0172
Binary files /dev/null and b/build/classes/java/main/com/gregor/jprototerm/KeyEncoder.class differ
diff --git a/build/classes/java/main/com/gregor/jprototerm/Main$1.class b/build/classes/java/main/com/gregor/jprototerm/Main$1.class
new file mode 100644
index 0000000..068e051
Binary files /dev/null and b/build/classes/java/main/com/gregor/jprototerm/Main$1.class differ
diff --git a/build/classes/java/main/com/gregor/jprototerm/Main.class b/build/classes/java/main/com/gregor/jprototerm/Main.class
new file mode 100644
index 0000000..72439f5
Binary files /dev/null and b/build/classes/java/main/com/gregor/jprototerm/Main.class differ
diff --git a/build/classes/java/main/com/gregor/jprototerm/ShellSession.class b/build/classes/java/main/com/gregor/jprototerm/ShellSession.class
new file mode 100644
index 0000000..9a4d193
Binary files /dev/null and b/build/classes/java/main/com/gregor/jprototerm/ShellSession.class differ
diff --git a/build/classes/java/main/com/gregor/jprototerm/TerminalCanvasView$FontMetrics.class b/build/classes/java/main/com/gregor/jprototerm/TerminalCanvasView$FontMetrics.class
new file mode 100644
index 0000000..37dff2d
Binary files /dev/null and b/build/classes/java/main/com/gregor/jprototerm/TerminalCanvasView$FontMetrics.class differ
diff --git a/build/classes/java/main/com/gregor/jprototerm/TerminalCanvasView.class b/build/classes/java/main/com/gregor/jprototerm/TerminalCanvasView.class
new file mode 100644
index 0000000..de9420e
Binary files /dev/null and b/build/classes/java/main/com/gregor/jprototerm/TerminalCanvasView.class differ
diff --git a/build/classes/java/main/com/gregor/jprototerm/TerminalPane.class b/build/classes/java/main/com/gregor/jprototerm/TerminalPane.class
new file mode 100644
index 0000000..2b79a1b
Binary files /dev/null and b/build/classes/java/main/com/gregor/jprototerm/TerminalPane.class differ
diff --git a/build/classes/java/main/com/gregor/jprototerm/TerminalWorkspace$1.class b/build/classes/java/main/com/gregor/jprototerm/TerminalWorkspace$1.class
new file mode 100644
index 0000000..2bd6d75
Binary files /dev/null and b/build/classes/java/main/com/gregor/jprototerm/TerminalWorkspace$1.class differ
diff --git a/build/classes/java/main/com/gregor/jprototerm/TerminalWorkspace.class b/build/classes/java/main/com/gregor/jprototerm/TerminalWorkspace.class
new file mode 100644
index 0000000..b81a68f
Binary files /dev/null and b/build/classes/java/main/com/gregor/jprototerm/TerminalWorkspace.class differ
diff --git a/build/tmp/compileJava/compileTransaction/stash-dir/Main$1.class.uniqueId5 b/build/tmp/compileJava/compileTransaction/stash-dir/Main$1.class.uniqueId5
new file mode 100644
index 0000000..068e051
Binary files /dev/null and b/build/tmp/compileJava/compileTransaction/stash-dir/Main$1.class.uniqueId5 differ
diff --git a/build/tmp/compileJava/compileTransaction/stash-dir/Main.class.uniqueId3 b/build/tmp/compileJava/compileTransaction/stash-dir/Main.class.uniqueId3
new file mode 100644
index 0000000..72439f5
Binary files /dev/null and b/build/tmp/compileJava/compileTransaction/stash-dir/Main.class.uniqueId3 differ
diff --git a/build/tmp/compileJava/compileTransaction/stash-dir/TerminalCanvasView$FontMetrics.class.uniqueId4 b/build/tmp/compileJava/compileTransaction/stash-dir/TerminalCanvasView$FontMetrics.class.uniqueId4
new file mode 100644
index 0000000..37dff2d
Binary files /dev/null and b/build/tmp/compileJava/compileTransaction/stash-dir/TerminalCanvasView$FontMetrics.class.uniqueId4 differ
diff --git a/build/tmp/compileJava/compileTransaction/stash-dir/TerminalCanvasView.class.uniqueId1 b/build/tmp/compileJava/compileTransaction/stash-dir/TerminalCanvasView.class.uniqueId1
new file mode 100644
index 0000000..de9420e
Binary files /dev/null and b/build/tmp/compileJava/compileTransaction/stash-dir/TerminalCanvasView.class.uniqueId1 differ
diff --git a/build/tmp/compileJava/compileTransaction/stash-dir/TerminalWorkspace$1.class.uniqueId2 b/build/tmp/compileJava/compileTransaction/stash-dir/TerminalWorkspace$1.class.uniqueId2
new file mode 100644
index 0000000..cae4c9b
Binary files /dev/null and b/build/tmp/compileJava/compileTransaction/stash-dir/TerminalWorkspace$1.class.uniqueId2 differ
diff --git a/build/tmp/compileJava/compileTransaction/stash-dir/TerminalWorkspace.class.uniqueId0 b/build/tmp/compileJava/compileTransaction/stash-dir/TerminalWorkspace.class.uniqueId0
new file mode 100644
index 0000000..9774dde
Binary files /dev/null and b/build/tmp/compileJava/compileTransaction/stash-dir/TerminalWorkspace.class.uniqueId0 differ
diff --git a/build/tmp/compileJava/previous-compilation-data.bin b/build/tmp/compileJava/previous-compilation-data.bin
new file mode 100644
index 0000000..adba47d
Binary files /dev/null and b/build/tmp/compileJava/previous-compilation-data.bin differ
diff --git a/config.example.toml b/config.example.toml
index f6f5df2..cb450ee 100644
--- a/config.example.toml
+++ b/config.example.toml
@@ -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"
diff --git a/devenv.lock b/devenv.lock
new file mode 100644
index 0000000..5834f80
--- /dev/null
+++ b/devenv.lock
@@ -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
+}
diff --git a/devenv.nix b/devenv.nix
new file mode 100644
index 0000000..78379c6
--- /dev/null
+++ b/devenv.nix
@@ -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";
+}
diff --git a/devenv.yaml b/devenv.yaml
new file mode 100644
index 0000000..4da5c66
--- /dev/null
+++ b/devenv.yaml
@@ -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
diff --git a/flake.lock b/flake.lock
deleted file mode 100644
index 14956f4..0000000
--- a/flake.lock
+++ /dev/null
@@ -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
-}
diff --git a/flake.nix b/flake.nix
deleted file mode 100644
index 5e081d8..0000000
--- a/flake.nix
+++ /dev/null
@@ -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"
- '';
- };
- };
-}
diff --git a/settings.gradle.kts b/settings.gradle.kts
deleted file mode 100644
index 9ea7c35..0000000
--- a/settings.gradle.kts
+++ /dev/null
@@ -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"
diff --git a/src/main/java/com/gregor/jprototerm/AppConfig.java b/src/main/java/com/gregor/jprototerm/AppConfig.java
index 983f12b..cd07328 100644
--- a/src/main/java/com/gregor/jprototerm/AppConfig.java
+++ b/src/main/java/com/gregor/jprototerm/AppConfig.java
@@ -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) {
diff --git a/src/main/java/com/gregor/jprototerm/KeyBinding.java b/src/main/java/com/gregor/jprototerm/KeyBinding.java
index 996521d..1c0ebe9 100644
--- a/src/main/java/com/gregor/jprototerm/KeyBinding.java
+++ b/src/main/java/com/gregor/jprototerm/KeyBinding.java
@@ -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);
+ }
+ }
}
diff --git a/src/main/java/com/gregor/jprototerm/KeyEncoder.java b/src/main/java/com/gregor/jprototerm/KeyEncoder.java
index 795274c..765b1b3 100644
--- a/src/main/java/com/gregor/jprototerm/KeyEncoder.java
+++ b/src/main/java/com/gregor/jprototerm/KeyEncoder.java
@@ -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;
+ }
}
diff --git a/src/main/java/com/gregor/jprototerm/KittyGraphicsRegistry.java b/src/main/java/com/gregor/jprototerm/KittyGraphicsRegistry.java
deleted file mode 100644
index e35ab9e..0000000
--- a/src/main/java/com/gregor/jprototerm/KittyGraphicsRegistry.java
+++ /dev/null
@@ -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 chunks = new HashMap<>();
- private final List 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 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 parseControl(String text) {
- Map 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 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) {
- }
-}
diff --git a/src/main/java/com/gregor/jprototerm/Main.java b/src/main/java/com/gregor/jprototerm/Main.java
index adae33a..911c412 100644
--- a/src/main/java/com/gregor/jprototerm/Main.java
+++ b/src/main/java/com/gregor/jprototerm/Main.java
@@ -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);
}
}
diff --git a/src/main/java/com/gregor/jprototerm/ShellSession.java b/src/main/java/com/gregor/jprototerm/ShellSession.java
index 7f38311..64101cd 100644
--- a/src/main/java/com/gregor/jprototerm/ShellSession.java
+++ b/src/main/java/com/gregor/jprototerm/ShellSession.java
@@ -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 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);
}
});
}
diff --git a/src/main/java/com/gregor/jprototerm/TerminalCanvasView.java b/src/main/java/com/gregor/jprototerm/TerminalCanvasView.java
index 24ecc70..cf03466 100644
--- a/src/main/java/com/gregor/jprototerm/TerminalCanvasView.java
+++ b/src/main/java/com/gregor/jprototerm/TerminalCanvasView.java
@@ -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 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) {
+ }
}
diff --git a/src/main/java/com/gregor/jprototerm/TerminalPane.java b/src/main/java/com/gregor/jprototerm/TerminalPane.java
index 6c41cf0..b2b35d0 100644
--- a/src/main/java/com/gregor/jprototerm/TerminalPane.java
+++ b/src/main/java/com/gregor/jprototerm/TerminalPane.java
@@ -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 snapshotText = new AtomicReference<>("");
+ private final AtomicReference 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() {
+ 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
diff --git a/src/main/java/com/gregor/jprototerm/TerminalWorkspace.java b/src/main/java/com/gregor/jprototerm/TerminalWorkspace.java
index fc174c3..770dd45 100644
--- a/src/main/java/com/gregor/jprototerm/TerminalWorkspace.java
+++ b/src/main/java/com/gregor/jprototerm/TerminalWorkspace.java
@@ -8,6 +8,7 @@ public final class TerminalWorkspace implements AutoCloseable {
private final AppConfig config;
private final List 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 panes() {
- return List.copyOf(panes);
+ List visible = panes.stream().filter(TerminalPane::visible).toList();
+ TerminalPane active = activePane();
+ if (!active.visible() || !active.floating()) {
+ return visible;
+ }
+
+ List 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 tiled = panes.stream().filter(pane -> !pane.floating()).toList();
+ List 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 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 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 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;
}
diff --git a/src/main/resources/META-INF/native-image/com.gregor/jprototerm/jni-config.json b/src/main/resources/META-INF/native-image/com.gregor/jprototerm/jni-config.json
deleted file mode 100644
index 79fe92b..0000000
--- a/src/main/resources/META-INF/native-image/com.gregor/jprototerm/jni-config.json
+++ /dev/null
@@ -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
- }
-]
diff --git a/src/main/resources/META-INF/native-image/com.gregor/jprototerm/reflect-config.json b/src/main/resources/META-INF/native-image/com.gregor/jprototerm/reflect-config.json
deleted file mode 100644
index 2d05928..0000000
--- a/src/main/resources/META-INF/native-image/com.gregor/jprototerm/reflect-config.json
+++ /dev/null
@@ -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
- }
-]
diff --git a/src/main/resources/META-INF/native-image/com.gregor/jprototerm/resource-config.json b/src/main/resources/META-INF/native-image/com.gregor/jprototerm/resource-config.json
deleted file mode 100644
index f127be5..0000000
--- a/src/main/resources/META-INF/native-image/com.gregor/jprototerm/resource-config.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "resources": [
- { "pattern": ".*\\.css$" },
- { "pattern": ".*\\.toml$" }
- ]
-}