Compare commits
10 Commits
194a6556c0
...
cf218e2afd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf218e2afd | ||
|
|
f07e524fbb | ||
|
|
a3f4878fc7 | ||
|
|
82cc7f4729 | ||
|
|
9f8767bc88 | ||
|
|
1ae1548db0 | ||
|
|
4e9f1487cb | ||
|
|
b60dcd5918 | ||
|
|
addeed6f30 | ||
|
|
a2de5118c1 |
18
.classpath
Normal file
18
.classpath
Normal 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>
|
||||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -1 +1,14 @@
|
|||||||
result
|
result
|
||||||
|
|
||||||
|
# Devenv
|
||||||
|
.devenv*
|
||||||
|
devenv.local.nix
|
||||||
|
devenv.local.yaml
|
||||||
|
|
||||||
|
# direnv
|
||||||
|
.direnv
|
||||||
|
|
||||||
|
# pre-commit
|
||||||
|
.pre-commit-config.yaml
|
||||||
|
build
|
||||||
|
build
|
||||||
|
|||||||
BIN
.gradle/8.14.4/fileChanges/last-build.bin
Normal file
BIN
.gradle/8.14.4/fileChanges/last-build.bin
Normal file
Binary file not shown.
BIN
.gradle/8.14.4/fileHashes/fileHashes.lock
Normal file
BIN
.gradle/8.14.4/fileHashes/fileHashes.lock
Normal file
Binary file not shown.
0
.gradle/8.14.4/gc.properties
Normal file
0
.gradle/8.14.4/gc.properties
Normal file
BIN
.gradle/8.9/checksums/checksums.lock
Normal file
BIN
.gradle/8.9/checksums/checksums.lock
Normal file
Binary file not shown.
BIN
.gradle/8.9/checksums/md5-checksums.bin
Normal file
BIN
.gradle/8.9/checksums/md5-checksums.bin
Normal file
Binary file not shown.
BIN
.gradle/8.9/checksums/sha1-checksums.bin
Normal file
BIN
.gradle/8.9/checksums/sha1-checksums.bin
Normal file
Binary file not shown.
0
.gradle/8.9/dependencies-accessors/gc.properties
Normal file
0
.gradle/8.9/dependencies-accessors/gc.properties
Normal file
BIN
.gradle/8.9/executionHistory/executionHistory.lock
Normal file
BIN
.gradle/8.9/executionHistory/executionHistory.lock
Normal file
Binary file not shown.
BIN
.gradle/8.9/fileChanges/last-build.bin
Normal file
BIN
.gradle/8.9/fileChanges/last-build.bin
Normal file
Binary file not shown.
BIN
.gradle/8.9/fileHashes/fileHashes.lock
Normal file
BIN
.gradle/8.9/fileHashes/fileHashes.lock
Normal file
Binary file not shown.
0
.gradle/8.9/gc.properties
Normal file
0
.gradle/8.9/gc.properties
Normal file
BIN
.gradle/9.4.1/checksums/checksums.lock
Normal file
BIN
.gradle/9.4.1/checksums/checksums.lock
Normal file
Binary file not shown.
BIN
.gradle/9.4.1/checksums/md5-checksums.bin
Normal file
BIN
.gradle/9.4.1/checksums/md5-checksums.bin
Normal file
Binary file not shown.
BIN
.gradle/9.4.1/checksums/sha1-checksums.bin
Normal file
BIN
.gradle/9.4.1/checksums/sha1-checksums.bin
Normal file
Binary file not shown.
BIN
.gradle/9.4.1/executionHistory/executionHistory.bin
Normal file
BIN
.gradle/9.4.1/executionHistory/executionHistory.bin
Normal file
Binary file not shown.
BIN
.gradle/9.4.1/executionHistory/executionHistory.lock
Normal file
BIN
.gradle/9.4.1/executionHistory/executionHistory.lock
Normal file
Binary file not shown.
BIN
.gradle/9.4.1/fileChanges/last-build.bin
Normal file
BIN
.gradle/9.4.1/fileChanges/last-build.bin
Normal file
Binary file not shown.
BIN
.gradle/9.4.1/fileHashes/fileHashes.bin
Normal file
BIN
.gradle/9.4.1/fileHashes/fileHashes.bin
Normal file
Binary file not shown.
BIN
.gradle/9.4.1/fileHashes/fileHashes.lock
Normal file
BIN
.gradle/9.4.1/fileHashes/fileHashes.lock
Normal file
Binary file not shown.
BIN
.gradle/9.4.1/fileHashes/resourceHashesCache.bin
Normal file
BIN
.gradle/9.4.1/fileHashes/resourceHashesCache.bin
Normal file
Binary file not shown.
0
.gradle/9.4.1/gc.properties
Normal file
0
.gradle/9.4.1/gc.properties
Normal file
BIN
.gradle/buildOutputCleanup/buildOutputCleanup.lock
Normal file
BIN
.gradle/buildOutputCleanup/buildOutputCleanup.lock
Normal file
Binary file not shown.
2
.gradle/buildOutputCleanup/cache.properties
Normal file
2
.gradle/buildOutputCleanup/cache.properties
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#Wed May 27 23:44:22 CEST 2026
|
||||||
|
gradle.version=9.4.1
|
||||||
BIN
.gradle/buildOutputCleanup/outputFiles.bin
Normal file
BIN
.gradle/buildOutputCleanup/outputFiles.bin
Normal file
Binary file not shown.
BIN
.gradle/file-system.probe
Normal file
BIN
.gradle/file-system.probe
Normal file
Binary file not shown.
0
.gradle/vcs-1/gc.properties
Normal file
0
.gradle/vcs-1/gc.properties
Normal file
34
.project
Normal file
34
.project
Normal 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>
|
||||||
13
.settings/org.eclipse.buildship.core.prefs
Normal file
13
.settings/org.eclipse.buildship.core.prefs
Normal 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
|
||||||
20
README.md
20
README.md
@@ -8,17 +8,17 @@ JavaFX canvas terminal prototype using `jlibghostty` for terminal emulation, Nix
|
|||||||
nix build
|
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:
|
For development:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
nix develop
|
nix develop
|
||||||
gradle -PjlibghosttyMavenRepo="$JLIBGHOSTTY_MAVEN_REPO" run
|
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
|
## Config
|
||||||
|
|
||||||
@@ -34,6 +34,8 @@ If `XDG_CONFIG_HOME` is unset, the fallback is:
|
|||||||
$HOME/.config/jprototerm/config.toml
|
$HOME/.config/jprototerm/config.toml
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If no config file exists, jprototerm writes the default config on startup.
|
||||||
|
|
||||||
Example, also available in `config.example.toml`:
|
Example, also available in `config.example.toml`:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
@@ -57,11 +59,19 @@ navigate_down = "ALT+J"
|
|||||||
navigate_up = "ALT+K"
|
navigate_up = "ALT+K"
|
||||||
navigate_right = "ALT+L"
|
navigate_right = "ALT+L"
|
||||||
toggle_floating = "ALT+F"
|
toggle_floating = "ALT+F"
|
||||||
|
new_floating = "ALT+SHIFT+F"
|
||||||
|
next_floating = "ALT+F12"
|
||||||
|
close_pane = "ALT+X"
|
||||||
|
open_font_selector = "ALT+T"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Defaults
|
## Defaults
|
||||||
|
|
||||||
- `Alt+h/j/k/l`: navigate panes
|
- `Alt+h/j/k/l`: navigate panes
|
||||||
- `Alt+f`: open or close a floating pane
|
- `Alt+f`: show or hide all floating panes
|
||||||
- Font default: `Symbols Nerd Font Mono`
|
- `Alt+Shift+f`: create a new floating pane
|
||||||
|
- `Alt+F12`: cycle floating panes
|
||||||
|
- `Alt+x`: close the active floating pane
|
||||||
|
- `Alt+t`: open the font selector
|
||||||
|
- Font default: `JetBrainsMono Nerd Font`
|
||||||
- Kitty graphics protocol parsing is enabled by default
|
- Kitty graphics protocol parsing is enabled by default
|
||||||
|
|||||||
31
build.gradle
Normal file
31
build.gradle
Normal 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']
|
||||||
|
}
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
[terminal]
|
[terminal]
|
||||||
columns = 100
|
columns = 100
|
||||||
rows = 30
|
rows = 30
|
||||||
|
max_scrollback = 100000
|
||||||
shell = "/bin/bash"
|
shell = "/bin/bash"
|
||||||
font_family = "JetBrainsMono Nerd Font"
|
font_family = "JetBrainsMono Nerd Font"
|
||||||
font_size = 15
|
font_size = 15
|
||||||
@@ -18,3 +19,7 @@ navigate_down = "ALT+J"
|
|||||||
navigate_up = "ALT+K"
|
navigate_up = "ALT+K"
|
||||||
navigate_right = "ALT+L"
|
navigate_right = "ALT+L"
|
||||||
toggle_floating = "ALT+F"
|
toggle_floating = "ALT+F"
|
||||||
|
new_floating = "ALT+SHIFT+F"
|
||||||
|
next_floating = "ALT+F12"
|
||||||
|
close_pane = "ALT+X"
|
||||||
|
open_font_selector = "ALT+T"
|
||||||
|
|||||||
65
devenv.lock
Normal file
65
devenv.lock
Normal 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
|
||||||
|
}
|
||||||
66
devenv.nix
Normal file
66
devenv.nix
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
{ 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;
|
||||||
|
hostNvidiaLibs = ".devenv/host-nvidia-libs";
|
||||||
|
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
|
||||||
|
pkgs.mesa-demos
|
||||||
|
];
|
||||||
|
|
||||||
|
env.LD_LIBRARY_PATH = "${hostNvidiaLibs}:" + 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.__GLX_VENDOR_LIBRARY_NAME = "nvidia";
|
||||||
|
env.__EGL_VENDOR_LIBRARY_FILENAMES = "/usr/share/glvnd/egl_vendor.d/10_nvidia.json";
|
||||||
|
env.JLIBGHOSTTY_MAVEN_REPO = "${jlib}/maven";
|
||||||
|
|
||||||
|
enterShell = ''
|
||||||
|
mkdir -p ${hostNvidiaLibs}
|
||||||
|
for lib in \
|
||||||
|
/usr/lib/x86_64-linux-gnu/libnvidia*.so* \
|
||||||
|
/usr/lib/x86_64-linux-gnu/libGLX_nvidia.so* \
|
||||||
|
/usr/lib/x86_64-linux-gnu/libEGL_nvidia.so* \
|
||||||
|
/usr/lib/x86_64-linux-gnu/nvidia/current/libnvidia*.so* \
|
||||||
|
/usr/lib/x86_64-linux-gnu/nvidia/current/libGLX_nvidia.so* \
|
||||||
|
/usr/lib/x86_64-linux-gnu/nvidia/current/libEGL_nvidia.so*
|
||||||
|
do
|
||||||
|
if [ -e "$lib" ]; then
|
||||||
|
ln -sfn "$lib" ${hostNvidiaLibs}/"$(basename "$lib")"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
'';
|
||||||
|
}
|
||||||
18
devenv.yaml
Normal file
18
devenv.yaml
Normal 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
274
flake.lock
generated
@@ -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
|
|
||||||
}
|
|
||||||
131
flake.nix
131
flake.nix
@@ -1,131 +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;
|
|
||||||
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 \
|
|
||||||
--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 JAVA_TOOL_OPTIONS "-Dprism.order=sw" \
|
|
||||||
--prefix LD_LIBRARY_PATH : ${pkgs.lib.makeLibraryPath [ openjfx jlib ]}:${openjfx}/modules_libs/javafx.graphics \
|
|
||||||
--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"
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
@@ -8,12 +8,17 @@ import io.github.wasabithumb.jtoml.value.primitive.TomlPrimitive;
|
|||||||
import io.github.wasabithumb.jtoml.value.table.TomlTable;
|
import io.github.wasabithumb.jtoml.value.table.TomlTable;
|
||||||
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public record AppConfig(
|
public record AppConfig(
|
||||||
int columns,
|
int columns,
|
||||||
int rows,
|
int rows,
|
||||||
|
long maxScrollback,
|
||||||
String shell,
|
String shell,
|
||||||
String fontFamily,
|
String fontFamily,
|
||||||
double fontSize,
|
double fontSize,
|
||||||
@@ -22,10 +27,23 @@ public record AppConfig(
|
|||||||
boolean kittyGraphics,
|
boolean kittyGraphics,
|
||||||
Map<String, KeyBinding> keybindings
|
Map<String, KeyBinding> keybindings
|
||||||
) {
|
) {
|
||||||
|
private static final List<String> KEYBINDING_KEYS = List.of(
|
||||||
|
"navigate_left",
|
||||||
|
"navigate_down",
|
||||||
|
"navigate_up",
|
||||||
|
"navigate_right",
|
||||||
|
"toggle_floating",
|
||||||
|
"new_floating",
|
||||||
|
"next_floating",
|
||||||
|
"close_pane",
|
||||||
|
"open_font_selector"
|
||||||
|
);
|
||||||
|
|
||||||
public static AppConfig load() {
|
public static AppConfig load() {
|
||||||
AppConfig defaults = defaults();
|
AppConfig defaults = defaults();
|
||||||
Path path = configPath();
|
Path path = configPath();
|
||||||
if (!Files.isRegularFile(path)) {
|
if (!Files.isRegularFile(path)) {
|
||||||
|
writeDefaultConfig(path, defaults);
|
||||||
return defaults;
|
return defaults;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,19 +52,14 @@ public record AppConfig(
|
|||||||
return new AppConfig(
|
return new AppConfig(
|
||||||
intValue(document, "terminal.columns", defaults.columns),
|
intValue(document, "terminal.columns", defaults.columns),
|
||||||
intValue(document, "terminal.rows", defaults.rows),
|
intValue(document, "terminal.rows", defaults.rows),
|
||||||
|
longValue(document, "terminal.max_scrollback", defaults.maxScrollback),
|
||||||
stringValue(document, "terminal.shell", defaults.shell),
|
stringValue(document, "terminal.shell", defaults.shell),
|
||||||
stringValue(document, "terminal.font_family", defaults.fontFamily),
|
stringValue(document, "terminal.font_family", defaults.fontFamily),
|
||||||
doubleValue(document, "terminal.font_size", defaults.fontSize),
|
doubleValue(document, "terminal.font_size", defaults.fontSize),
|
||||||
doubleValue(document, "window.width", defaults.windowWidth),
|
doubleValue(document, "window.width", defaults.windowWidth),
|
||||||
doubleValue(document, "window.height", defaults.windowHeight),
|
doubleValue(document, "window.height", defaults.windowHeight),
|
||||||
booleanValue(document, "kitty_graphics.enabled", defaults.kittyGraphics),
|
booleanValue(document, "kitty_graphics.enabled", defaults.kittyGraphics),
|
||||||
Map.of(
|
keybindings(document, defaults)
|
||||||
"navigate_left", binding(document, "keybindings.navigate_left", defaults.keybindings.get("navigate_left")),
|
|
||||||
"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"))
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
} catch (TomlException ex) {
|
} catch (TomlException ex) {
|
||||||
System.err.println("Could not parse " + path + ": " + ex.getMessage());
|
System.err.println("Could not parse " + path + ": " + ex.getMessage());
|
||||||
@@ -58,8 +71,9 @@ public record AppConfig(
|
|||||||
return new AppConfig(
|
return new AppConfig(
|
||||||
100,
|
100,
|
||||||
30,
|
30,
|
||||||
|
100_000,
|
||||||
defaultShell(),
|
defaultShell(),
|
||||||
"Symbols Nerd Font Mono",
|
"JetBrainsMono Nerd Font",
|
||||||
15.0,
|
15.0,
|
||||||
1200.0,
|
1200.0,
|
||||||
760.0,
|
760.0,
|
||||||
@@ -69,11 +83,34 @@ public record AppConfig(
|
|||||||
"navigate_down", KeyBinding.parse("ALT+J"),
|
"navigate_down", KeyBinding.parse("ALT+J"),
|
||||||
"navigate_up", KeyBinding.parse("ALT+K"),
|
"navigate_up", KeyBinding.parse("ALT+K"),
|
||||||
"navigate_right", KeyBinding.parse("ALT+L"),
|
"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"),
|
||||||
|
"open_font_selector", KeyBinding.parse("ALT+T")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AppConfig withFont(String family, double size) {
|
||||||
|
return new AppConfig(
|
||||||
|
columns,
|
||||||
|
rows,
|
||||||
|
maxScrollback,
|
||||||
|
shell,
|
||||||
|
family,
|
||||||
|
size,
|
||||||
|
windowWidth,
|
||||||
|
windowHeight,
|
||||||
|
kittyGraphics,
|
||||||
|
keybindings
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void save() {
|
||||||
|
save(configPath(), this);
|
||||||
|
}
|
||||||
|
|
||||||
public static Path configPath() {
|
public static Path configPath() {
|
||||||
String configHome = System.getenv("XDG_CONFIG_HOME");
|
String configHome = System.getenv("XDG_CONFIG_HOME");
|
||||||
if (configHome != null && !configHome.isBlank()) {
|
if (configHome != null && !configHome.isBlank()) {
|
||||||
@@ -83,8 +120,78 @@ public record AppConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static String defaultShell() {
|
private static String defaultShell() {
|
||||||
String shell = System.getenv("SHELL");
|
return "/bin/bash";
|
||||||
return shell == null || shell.isBlank() ? "/bin/sh" : shell;
|
}
|
||||||
|
|
||||||
|
private static Map<String, KeyBinding> keybindings(TomlTable table, AppConfig defaults) {
|
||||||
|
Map<String, KeyBinding> parsed = new LinkedHashMap<>();
|
||||||
|
for (String key : KEYBINDING_KEYS) {
|
||||||
|
parsed.put(key, binding(table, "keybindings." + key, defaults.keybindings.get(key)));
|
||||||
|
}
|
||||||
|
return Map.copyOf(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeDefaultConfig(Path path, AppConfig defaults) {
|
||||||
|
save(path, defaults);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void save(Path path, AppConfig config) {
|
||||||
|
try {
|
||||||
|
Path parent = path.getParent();
|
||||||
|
if (parent != null) {
|
||||||
|
Files.createDirectories(parent);
|
||||||
|
}
|
||||||
|
Files.writeString(
|
||||||
|
path,
|
||||||
|
config.toToml(),
|
||||||
|
StandardOpenOption.CREATE,
|
||||||
|
StandardOpenOption.TRUNCATE_EXISTING,
|
||||||
|
StandardOpenOption.WRITE
|
||||||
|
);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
System.err.println("Could not write " + path + ": " + ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String toToml() {
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
builder.append("[terminal]\n");
|
||||||
|
builder.append("columns = ").append(columns).append('\n');
|
||||||
|
builder.append("rows = ").append(rows).append('\n');
|
||||||
|
builder.append("max_scrollback = ").append(maxScrollback).append('\n');
|
||||||
|
builder.append("shell = ").append(quoted(shell)).append('\n');
|
||||||
|
builder.append("font_family = ").append(quoted(fontFamily)).append('\n');
|
||||||
|
builder.append("font_size = ").append(trimDouble(fontSize)).append("\n\n");
|
||||||
|
builder.append("[window]\n");
|
||||||
|
builder.append("width = ").append(trimDouble(windowWidth)).append('\n');
|
||||||
|
builder.append("height = ").append(trimDouble(windowHeight)).append("\n\n");
|
||||||
|
builder.append("[kitty_graphics]\n");
|
||||||
|
builder.append("enabled = ").append(kittyGraphics).append("\n\n");
|
||||||
|
builder.append("[keybindings]\n");
|
||||||
|
for (String key : KEYBINDING_KEYS) {
|
||||||
|
KeyBinding binding = keybindings.get(key);
|
||||||
|
if (binding != null) {
|
||||||
|
builder.append(key).append(" = ").append(quoted(binding.toString())).append('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String quoted(String value) {
|
||||||
|
return "\"" + value
|
||||||
|
.replace("\\", "\\\\")
|
||||||
|
.replace("\"", "\\\"")
|
||||||
|
.replace("\n", "\\n")
|
||||||
|
.replace("\r", "\\r")
|
||||||
|
.replace("\t", "\\t")
|
||||||
|
+ "\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String trimDouble(double value) {
|
||||||
|
if (value == Math.rint(value)) {
|
||||||
|
return Long.toString((long) value);
|
||||||
|
}
|
||||||
|
return Double.toString(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static KeyBinding binding(TomlTable table, String key, KeyBinding fallback) {
|
private static KeyBinding binding(TomlTable table, String key, KeyBinding fallback) {
|
||||||
@@ -116,6 +223,18 @@ public record AppConfig(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static long longValue(TomlTable table, String key, long fallback) {
|
||||||
|
TomlPrimitive primitive = primitive(table, key);
|
||||||
|
if (primitive == null) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return primitive.asInteger();
|
||||||
|
} catch (RuntimeException ex) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static double doubleValue(TomlTable table, String key, double fallback) {
|
private static double doubleValue(TomlTable table, String key, double fallback) {
|
||||||
TomlPrimitive primitive = primitive(table, key);
|
TomlPrimitive primitive = primitive(table, key);
|
||||||
if (primitive == null) {
|
if (primitive == null) {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ public record KeyBinding(boolean alt, boolean control, boolean shift, KeyCode co
|
|||||||
case "ALT", "META" -> alt = true;
|
case "ALT", "META" -> alt = true;
|
||||||
case "CTRL", "CONTROL" -> control = true;
|
case "CTRL", "CONTROL" -> control = true;
|
||||||
case "SHIFT" -> shift = true;
|
case "SHIFT" -> shift = true;
|
||||||
default -> code = KeyCode.getKeyCode(token);
|
default -> code = keyCode(token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,4 +34,35 @@ public record KeyBinding(boolean alt, boolean control, boolean shift, KeyCode co
|
|||||||
&& event.isShiftDown() == shift
|
&& event.isShiftDown() == shift
|
||||||
&& event.getCode() == code;
|
&& event.getCode() == code;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
if (control) {
|
||||||
|
builder.append("CTRL+");
|
||||||
|
}
|
||||||
|
if (alt) {
|
||||||
|
builder.append("ALT+");
|
||||||
|
}
|
||||||
|
if (shift) {
|
||||||
|
builder.append("SHIFT+");
|
||||||
|
}
|
||||||
|
builder.append(code.getName().toUpperCase(Locale.ROOT).replace(' ', '_'));
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,20 @@ final class KeyEncoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static String encode(KeyEvent event) {
|
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();
|
KeyCode code = event.getCode();
|
||||||
return switch (code) {
|
return switch (code) {
|
||||||
case ENTER -> "\r";
|
case ENTER -> "\r";
|
||||||
@@ -23,7 +37,43 @@ final class KeyEncoder {
|
|||||||
case DELETE -> "\u001b[3~";
|
case DELETE -> "\u001b[3~";
|
||||||
case PAGE_UP -> "\u001b[5~";
|
case PAGE_UP -> "\u001b[5~";
|
||||||
case PAGE_DOWN -> "\u001b[6~";
|
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;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,26 +3,36 @@ package com.gregor.jprototerm;
|
|||||||
import javafx.animation.AnimationTimer;
|
import javafx.animation.AnimationTimer;
|
||||||
import javafx.application.Application;
|
import javafx.application.Application;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.control.ButtonType;
|
||||||
|
import javafx.scene.control.ComboBox;
|
||||||
|
import javafx.scene.control.Dialog;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.Spinner;
|
||||||
|
import javafx.scene.control.SpinnerValueFactory;
|
||||||
import javafx.scene.input.KeyEvent;
|
import javafx.scene.input.KeyEvent;
|
||||||
|
import javafx.scene.layout.GridPane;
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
|
import javafx.scene.text.Font;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
public final class Main extends Application {
|
public final class Main extends Application {
|
||||||
private TerminalWorkspace workspace;
|
private TerminalWorkspace workspace;
|
||||||
|
private TerminalCanvasView terminalView;
|
||||||
|
private AppConfig config;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void start(Stage stage) {
|
public void start(Stage stage) {
|
||||||
AppConfig config = AppConfig.load();
|
config = AppConfig.load();
|
||||||
|
|
||||||
workspace = new TerminalWorkspace(config);
|
workspace = new TerminalWorkspace(config);
|
||||||
TerminalCanvasView terminalView = new TerminalCanvasView(workspace, config);
|
terminalView = new TerminalCanvasView(workspace, config);
|
||||||
|
|
||||||
StackPane root = new StackPane(terminalView.canvas());
|
StackPane root = new StackPane(terminalView.canvas());
|
||||||
terminalView.canvas().widthProperty().bind(root.widthProperty());
|
terminalView.canvas().widthProperty().bind(root.widthProperty());
|
||||||
terminalView.canvas().heightProperty().bind(root.heightProperty());
|
terminalView.canvas().heightProperty().bind(root.heightProperty());
|
||||||
|
|
||||||
Scene scene = new Scene(root, config.windowWidth(), config.windowHeight());
|
Scene scene = new Scene(root, config.windowWidth(), config.windowHeight());
|
||||||
scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> handlePressed(config, event));
|
scene.addEventFilter(KeyEvent.KEY_PRESSED, this::handlePressed);
|
||||||
scene.addEventFilter(KeyEvent.KEY_TYPED, event -> handleTyped(event));
|
scene.addEventFilter(KeyEvent.KEY_TYPED, event -> handleTyped(event));
|
||||||
|
|
||||||
new AnimationTimer() {
|
new AnimationTimer() {
|
||||||
@@ -38,9 +48,10 @@ public final class Main extends Application {
|
|||||||
workspace.close();
|
workspace.close();
|
||||||
});
|
});
|
||||||
stage.show();
|
stage.show();
|
||||||
|
terminalView.canvas().requestFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handlePressed(AppConfig config, KeyEvent event) {
|
private void handlePressed(KeyEvent event) {
|
||||||
if (config.keybindings().get("navigate_left").matches(event)) {
|
if (config.keybindings().get("navigate_left").matches(event)) {
|
||||||
workspace.navigate(Direction.LEFT);
|
workspace.navigate(Direction.LEFT);
|
||||||
event.consume();
|
event.consume();
|
||||||
@@ -56,6 +67,18 @@ public final class Main extends Application {
|
|||||||
} else if (config.keybindings().get("toggle_floating").matches(event)) {
|
} else if (config.keybindings().get("toggle_floating").matches(event)) {
|
||||||
workspace.toggleFloating();
|
workspace.toggleFloating();
|
||||||
event.consume();
|
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 if (config.keybindings().get("open_font_selector").matches(event)) {
|
||||||
|
openFontSelector();
|
||||||
|
event.consume();
|
||||||
} else {
|
} else {
|
||||||
String encoded = KeyEncoder.encode(event);
|
String encoded = KeyEncoder.encode(event);
|
||||||
if (encoded != null) {
|
if (encoded != null) {
|
||||||
@@ -77,9 +100,51 @@ public final class Main extends Application {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void openFontSelector() {
|
||||||
|
Dialog<ButtonType> dialog = new Dialog<>();
|
||||||
|
dialog.setTitle("Font");
|
||||||
|
dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||||
|
|
||||||
|
ComboBox<String> family = new ComboBox<>();
|
||||||
|
family.getItems().setAll(Font.getFamilies());
|
||||||
|
family.setEditable(true);
|
||||||
|
family.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
family.setValue(config.fontFamily());
|
||||||
|
|
||||||
|
Spinner<Double> size = new Spinner<>();
|
||||||
|
size.setEditable(true);
|
||||||
|
size.setValueFactory(new SpinnerValueFactory.DoubleSpinnerValueFactory(6.0, 48.0, config.fontSize(), 0.5));
|
||||||
|
|
||||||
|
GridPane content = new GridPane();
|
||||||
|
content.setHgap(10.0);
|
||||||
|
content.setVgap(10.0);
|
||||||
|
content.add(new Label("Family"), 0, 0);
|
||||||
|
content.add(family, 1, 0);
|
||||||
|
content.add(new Label("Size"), 0, 1);
|
||||||
|
content.add(size, 1, 1);
|
||||||
|
dialog.getDialogPane().setContent(content);
|
||||||
|
|
||||||
|
dialog.showAndWait()
|
||||||
|
.filter(button -> button == ButtonType.OK)
|
||||||
|
.ifPresent(ignored -> {
|
||||||
|
String selectedFamily = family.getEditor().getText();
|
||||||
|
if (selectedFamily == null || selectedFamily.isBlank()) {
|
||||||
|
selectedFamily = family.getValue();
|
||||||
|
}
|
||||||
|
if (selectedFamily == null || selectedFamily.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
double selectedSize = size.getValue();
|
||||||
|
config = config.withFont(selectedFamily.trim(), selectedSize);
|
||||||
|
config.save();
|
||||||
|
terminalView.setFont(config.fontFamily(), config.fontSize());
|
||||||
|
terminalView.canvas().requestFocus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
System.setProperty("prism.order", "sw");
|
System.setProperty("prism.order", System.getProperty("prism.order", "es2,sw"));
|
||||||
System.setProperty("prism.verbose", "true");
|
|
||||||
launch(Main.class, args);
|
launch(Main.class, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,25 @@
|
|||||||
package com.gregor.jprototerm;
|
package com.gregor.jprototerm;
|
||||||
|
|
||||||
|
import com.pty4j.PtyProcess;
|
||||||
|
import com.pty4j.PtyProcessBuilder;
|
||||||
|
import com.pty4j.WinSize;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
public final class ShellSession implements AutoCloseable {
|
public final class ShellSession implements AutoCloseable {
|
||||||
private final Process process;
|
private final PtyProcess process;
|
||||||
private final OutputStream stdin;
|
private final OutputStream stdin;
|
||||||
private final ExecutorService reader;
|
private final ExecutorService reader;
|
||||||
private volatile boolean closed;
|
private volatile boolean closed;
|
||||||
|
|
||||||
private ShellSession(Process process, TerminalPane pane, KittyGraphicsRegistry graphicsRegistry) {
|
private ShellSession(PtyProcess process) {
|
||||||
this.process = process;
|
this.process = process;
|
||||||
this.stdin = process.getOutputStream();
|
this.stdin = process.getOutputStream();
|
||||||
this.reader = Executors.newSingleThreadExecutor(runnable -> {
|
this.reader = Executors.newSingleThreadExecutor(runnable -> {
|
||||||
@@ -22,50 +27,65 @@ public final class ShellSession implements AutoCloseable {
|
|||||||
thread.setDaemon(true);
|
thread.setDaemon(true);
|
||||||
return thread;
|
return thread;
|
||||||
});
|
});
|
||||||
reader.submit(() -> readOutput(pane, graphicsRegistry));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ShellSession start(String shell, TerminalPane pane, KittyGraphicsRegistry graphicsRegistry) {
|
public static ShellSession start(String shell, TerminalPane pane, int columns, int rows) {
|
||||||
try {
|
try {
|
||||||
ProcessBuilder processBuilder = new ProcessBuilder(
|
Map<String, String> environment = new HashMap<>(System.getenv());
|
||||||
"script",
|
environment.put("TERM", "xterm-kitty");
|
||||||
"-qfec",
|
environment.put("COLORTERM", "truecolor");
|
||||||
shell + " -i",
|
|
||||||
"/dev/null"
|
PtyProcess process = new PtyProcessBuilder(new String[] {shell, "-i"})
|
||||||
).redirectErrorStream(true);
|
.setEnvironment(environment)
|
||||||
processBuilder.environment().put("TERM", "xterm-kitty");
|
.setInitialColumns(columns)
|
||||||
processBuilder.environment().put("COLORTERM", "truecolor");
|
.setInitialRows(rows)
|
||||||
Process process = processBuilder.start();
|
.setDirectory(System.getProperty("user.home"))
|
||||||
return new ShellSession(process, pane, graphicsRegistry);
|
.start();
|
||||||
|
return new ShellSession(process);
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
pane.write("failed to start shell: " + ex.getMessage() + "\r\n");
|
pane.write("failed to start shell: " + ex.getMessage() + "\r\n");
|
||||||
throw new IllegalStateException("Could not start shell " + shell, ex);
|
throw new IllegalStateException("Could not start shell " + shell, ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void startReading(TerminalPane pane) {
|
||||||
|
reader.submit(() -> readOutput(pane));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resize(int columns, int rows) {
|
||||||
|
if (closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
process.setWinSize(new WinSize(columns, rows));
|
||||||
|
}
|
||||||
|
|
||||||
public void send(String text) {
|
public void send(String text) {
|
||||||
|
send(text.getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void send(byte[] bytes) {
|
||||||
if (closed) {
|
if (closed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
stdin.write(text.getBytes(StandardCharsets.UTF_8));
|
stdin.write(bytes);
|
||||||
stdin.flush();
|
stdin.flush();
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void readOutput(TerminalPane pane, KittyGraphicsRegistry graphicsRegistry) {
|
private void readOutput(TerminalPane pane) {
|
||||||
byte[] buffer = new byte[8192];
|
byte[] buffer = new byte[8192];
|
||||||
try {
|
try {
|
||||||
int read;
|
int read;
|
||||||
while ((read = process.getInputStream().read(buffer)) != -1) {
|
while ((read = process.getInputStream().read(buffer)) != -1) {
|
||||||
String text = new String(buffer, 0, read, StandardCharsets.UTF_8);
|
|
||||||
if (!closed) {
|
if (!closed) {
|
||||||
graphicsRegistry.accept(text);
|
byte[] bytes = new byte[read];
|
||||||
|
System.arraycopy(buffer, 0, bytes, 0, read);
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
if (!closed) {
|
if (!closed) {
|
||||||
pane.write(text);
|
pane.write(bytes);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,72 @@
|
|||||||
package com.gregor.jprototerm;
|
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.KeyModifiers;
|
||||||
|
import dev.jlibghostty.MouseButton;
|
||||||
|
import dev.jlibghostty.MouseEncoderSize;
|
||||||
|
import dev.jlibghostty.MouseInput;
|
||||||
|
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.Canvas;
|
||||||
import javafx.scene.canvas.GraphicsContext;
|
import javafx.scene.canvas.GraphicsContext;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.image.PixelFormat;
|
||||||
|
import javafx.scene.image.WritableImage;
|
||||||
|
import javafx.scene.input.InputEvent;
|
||||||
|
import javafx.scene.input.MouseEvent;
|
||||||
|
import javafx.scene.input.ScrollEvent;
|
||||||
|
import javafx.scene.input.ScrollEvent.VerticalTextScrollUnits;
|
||||||
import javafx.scene.paint.Color;
|
import javafx.scene.paint.Color;
|
||||||
import javafx.scene.text.Font;
|
import javafx.scene.text.Font;
|
||||||
import javafx.scene.text.FontSmoothingType;
|
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 {
|
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 Canvas canvas = new Canvas();
|
||||||
private final TerminalWorkspace workspace;
|
private final TerminalWorkspace workspace;
|
||||||
private final AppConfig config;
|
private final AppConfig config;
|
||||||
|
private final Map<Long, Image> kittyImageCache = new HashMap<>();
|
||||||
|
private String fontFamily;
|
||||||
|
private double fontSize;
|
||||||
|
private boolean mouseButtonPressed;
|
||||||
|
private MouseButton pressedButton = MouseButton.UNKNOWN;
|
||||||
|
|
||||||
public TerminalCanvasView(TerminalWorkspace workspace, AppConfig config) {
|
public TerminalCanvasView(TerminalWorkspace workspace, AppConfig config) {
|
||||||
this.workspace = workspace;
|
this.workspace = workspace;
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
this.fontFamily = config.fontFamily();
|
||||||
|
this.fontSize = config.fontSize();
|
||||||
canvas.setFocusTraversable(true);
|
canvas.setFocusTraversable(true);
|
||||||
|
canvas.setOnMousePressed(this::handleMousePressed);
|
||||||
|
canvas.setOnMouseReleased(this::handleMouseReleased);
|
||||||
|
canvas.setOnMouseDragged(this::handleMouseDragged);
|
||||||
|
canvas.setOnMouseMoved(this::handleMouseMoved);
|
||||||
|
canvas.setOnScroll(this::handleScroll);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Canvas canvas() {
|
public Canvas canvas() {
|
||||||
return canvas;
|
return canvas;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setFont(String family, double size) {
|
||||||
|
this.fontFamily = family;
|
||||||
|
this.fontSize = size;
|
||||||
|
}
|
||||||
|
|
||||||
public void render() {
|
public void render() {
|
||||||
double width = canvas.getWidth();
|
double width = canvas.getWidth();
|
||||||
double height = canvas.getHeight();
|
double height = canvas.getHeight();
|
||||||
@@ -29,7 +75,7 @@ public final class TerminalCanvasView {
|
|||||||
GraphicsContext gc = canvas.getGraphicsContext2D();
|
GraphicsContext gc = canvas.getGraphicsContext2D();
|
||||||
gc.setFill(Color.rgb(16, 16, 18));
|
gc.setFill(Color.rgb(16, 16, 18));
|
||||||
gc.fillRect(0, 0, width, height);
|
gc.fillRect(0, 0, width, height);
|
||||||
gc.setFontSmoothingType(FontSmoothingType.GRAY);
|
gc.setFontSmoothingType(FontSmoothingType.LCD);
|
||||||
|
|
||||||
for (TerminalPane pane : workspace.panes()) {
|
for (TerminalPane pane : workspace.panes()) {
|
||||||
drawPane(gc, pane);
|
drawPane(gc, pane);
|
||||||
@@ -53,22 +99,359 @@ public final class TerminalCanvasView {
|
|||||||
gc.setLineWidth(workspace.isActive(pane) ? 2.0 : 1.0);
|
gc.setLineWidth(workspace.isActive(pane) ? 2.0 : 1.0);
|
||||||
gc.strokeRect(pane.x() + 0.5, pane.y() + 0.5, pane.width() - 1.0, pane.height() - 1.0);
|
gc.strokeRect(pane.x() + 0.5, pane.y() + 0.5, pane.width() - 1.0, pane.height() - 1.0);
|
||||||
|
|
||||||
Font font = Font.font(config.fontFamily(), config.fontSize());
|
Font font = Font.font(fontFamily, fontSize);
|
||||||
gc.setFont(font);
|
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 left = pane.x() + 12.0;
|
||||||
double baseline = pane.y() + 18.0;
|
double top = pane.y() + 12.0;
|
||||||
int maxLines = Math.max(1, (int) ((pane.height() - 24.0) / lineHeight));
|
double baseline = top + metrics.baselineOffset;
|
||||||
|
|
||||||
String[] lines = pane.snapshotText().split("\\R", -1);
|
RenderStateSnapshot snapshot = pane.renderSnapshot();
|
||||||
int start = Math.max(0, lines.length - maxLines);
|
if (snapshot != null) {
|
||||||
for (int i = start; i < lines.length; i++) {
|
for (RenderRow row : snapshot.renderRows()) {
|
||||||
gc.fillText(lines[i], left, baseline + ((i - start) * lineHeight));
|
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();
|
gc.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static FontMetrics measureFontMetrics(Font font) {
|
||||||
|
Text text = new Text("┃MgÅjy");
|
||||||
|
text.setFont(font);
|
||||||
|
double lineHeight = Math.max(1.0, text.getLayoutBounds().getHeight());
|
||||||
|
double baselineOffset = -text.getLayoutBounds().getMinY();
|
||||||
|
|
||||||
|
String sample = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||||
|
Text cell = new Text(sample);
|
||||||
|
cell.setFont(font);
|
||||||
|
double cellWidth = Math.max(1.0, cell.getLayoutBounds().getWidth() / sample.length());
|
||||||
|
return new FontMetrics(cellWidth, lineHeight, baselineOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleMousePressed(MouseEvent event) {
|
||||||
|
canvas.requestFocus();
|
||||||
|
TerminalPane pane = paneAt(event.getX(), event.getY());
|
||||||
|
if (pane == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
workspace.focus(pane);
|
||||||
|
pressedButton = mouseButton(event);
|
||||||
|
mouseButtonPressed = true;
|
||||||
|
sendMouse(pane, MouseInput.press(pressedButton, eventX(pane, event.getX()), eventY(pane, event.getY()), modifiers(event)), true, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleMouseReleased(MouseEvent event) {
|
||||||
|
TerminalPane pane = paneAt(event.getX(), event.getY());
|
||||||
|
if (pane == null) {
|
||||||
|
pane = workspace.activePane();
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseButton button = pressedButton == MouseButton.UNKNOWN ? mouseButton(event) : pressedButton;
|
||||||
|
sendMouse(pane, MouseInput.release(button, eventX(pane, event.getX()), eventY(pane, event.getY()), modifiers(event)), false, event);
|
||||||
|
mouseButtonPressed = false;
|
||||||
|
pressedButton = MouseButton.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleMouseDragged(MouseEvent event) {
|
||||||
|
TerminalPane pane = paneAt(event.getX(), event.getY());
|
||||||
|
if (pane == null) {
|
||||||
|
pane = workspace.activePane();
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseButton button = pressedButton == MouseButton.UNKNOWN ? mouseButton(event) : pressedButton;
|
||||||
|
sendMouse(pane, MouseInput.drag(button, eventX(pane, event.getX()), eventY(pane, event.getY()), modifiers(event)), true, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleMouseMoved(MouseEvent event) {
|
||||||
|
TerminalPane pane = paneAt(event.getX(), event.getY());
|
||||||
|
if (pane == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMouse(pane, MouseInput.motion(eventX(pane, event.getX()), eventY(pane, event.getY()), modifiers(event)), mouseButtonPressed, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleScroll(ScrollEvent event) {
|
||||||
|
TerminalPane pane = paneAt(event.getX(), event.getY());
|
||||||
|
if (pane == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.requestFocus();
|
||||||
|
workspace.focus(pane);
|
||||||
|
int direction = scrollDirection(event);
|
||||||
|
if (direction == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseButton wheelButton = direction > 0 ? MouseButton.FOUR : MouseButton.FIVE;
|
||||||
|
int rows = scrollRows(event);
|
||||||
|
boolean sent = false;
|
||||||
|
for (int i = 0; i < rows; i++) {
|
||||||
|
sent |= sendMouse(
|
||||||
|
pane,
|
||||||
|
MouseInput.press(wheelButton, eventX(pane, event.getX()), eventY(pane, event.getY()), modifiers(event)),
|
||||||
|
mouseButtonPressed,
|
||||||
|
event
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!sent) {
|
||||||
|
pane.scrollViewport(direction > 0 ? -rows : rows);
|
||||||
|
event.consume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean sendMouse(TerminalPane pane, MouseInput input, boolean anyButtonPressed, InputEvent event) {
|
||||||
|
MouseTarget target = mouseTarget(pane);
|
||||||
|
if (target == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean sent = pane.sendMouse(input, target.size(), anyButtonPressed);
|
||||||
|
if (sent) {
|
||||||
|
event.consume();
|
||||||
|
}
|
||||||
|
return sent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TerminalPane paneAt(double x, double y) {
|
||||||
|
java.util.List<TerminalPane> panes = workspace.panes();
|
||||||
|
for (int i = panes.size() - 1; i >= 0; i--) {
|
||||||
|
TerminalPane pane = panes.get(i);
|
||||||
|
if (x >= pane.x() && x < pane.x() + pane.width() && y >= pane.y() && y < pane.y() + pane.height()) {
|
||||||
|
return pane;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MouseTarget mouseTarget(TerminalPane pane) {
|
||||||
|
if (pane.width() <= 24.0 || pane.height() <= 24.0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
FontMetrics metrics = measureFontMetrics(Font.font(fontFamily, fontSize));
|
||||||
|
int columns = Math.max(1, (int) ((pane.width() - 24.0) / metrics.cellWidth));
|
||||||
|
int rows = Math.max(1, (int) ((pane.height() - 24.0) / metrics.lineHeight));
|
||||||
|
long cellWidth = Math.max(1L, Math.round(metrics.cellWidth));
|
||||||
|
long cellHeight = Math.max(1L, Math.round(metrics.lineHeight));
|
||||||
|
long screenWidth = Math.max(1L, Math.round(columns * metrics.cellWidth));
|
||||||
|
long screenHeight = Math.max(1L, Math.round(rows * metrics.lineHeight));
|
||||||
|
return new MouseTarget(MouseEncoderSize.of(screenWidth, screenHeight, cellWidth, cellHeight), screenWidth, screenHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double eventX(TerminalPane pane, double canvasX) {
|
||||||
|
MouseTarget target = mouseTarget(pane);
|
||||||
|
if (target == null) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
return clamp(canvasX - pane.x() - 12.0, 0.0, target.screenWidth() - 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double eventY(TerminalPane pane, double canvasY) {
|
||||||
|
MouseTarget target = mouseTarget(pane);
|
||||||
|
if (target == null) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
return clamp(canvasY - pane.y() - 12.0, 0.0, target.screenHeight() - 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double clamp(double value, double min, double max) {
|
||||||
|
return Math.max(min, Math.min(max, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static KeyModifiers modifiers(MouseEvent event) {
|
||||||
|
return KeyModifiers.of(event.isShiftDown(), event.isControlDown(), event.isAltDown(), event.isMetaDown());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static KeyModifiers modifiers(ScrollEvent event) {
|
||||||
|
return KeyModifiers.of(event.isShiftDown(), event.isControlDown(), event.isAltDown(), event.isMetaDown());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int scrollRows(ScrollEvent event) {
|
||||||
|
double rows;
|
||||||
|
if (event.getTextDeltaYUnits() == VerticalTextScrollUnits.LINES && event.getTextDeltaY() != 0.0) {
|
||||||
|
rows = Math.abs(event.getTextDeltaY());
|
||||||
|
} else if (event.getTextDeltaYUnits() == VerticalTextScrollUnits.PAGES && event.getTextDeltaY() != 0.0) {
|
||||||
|
rows = Math.abs(event.getTextDeltaY()) * 24.0;
|
||||||
|
} else if (event.getMultiplierY() > 0.0) {
|
||||||
|
rows = Math.abs(event.getDeltaY()) / event.getMultiplierY();
|
||||||
|
} else {
|
||||||
|
rows = Math.abs(event.getDeltaY()) / 40.0;
|
||||||
|
}
|
||||||
|
return Math.max(1, Math.min(64, (int) Math.ceil(rows)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int scrollDirection(ScrollEvent event) {
|
||||||
|
if (event.getDeltaY() != 0.0) {
|
||||||
|
return event.getDeltaY() > 0.0 ? 1 : -1;
|
||||||
|
}
|
||||||
|
if (event.getTextDeltaYUnits() != VerticalTextScrollUnits.NONE && event.getTextDeltaY() != 0.0) {
|
||||||
|
return event.getTextDeltaY() > 0.0 ? 1 : -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MouseButton mouseButton(MouseEvent event) {
|
||||||
|
return switch (event.getButton()) {
|
||||||
|
case PRIMARY -> MouseButton.LEFT;
|
||||||
|
case SECONDARY -> MouseButton.RIGHT;
|
||||||
|
case MIDDLE -> MouseButton.MIDDLE;
|
||||||
|
default -> MouseButton.UNKNOWN;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
fillCellRect(gc, x, cellTop, cellWidth, lineHeight);
|
||||||
|
});
|
||||||
|
if (cell.selected()) {
|
||||||
|
gc.setFill(SELECTED_BACKGROUND);
|
||||||
|
fillCellRect(gc, 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 void fillCellRect(GraphicsContext gc, double x, double y, double width, double height) {
|
||||||
|
double x1 = Math.floor(x);
|
||||||
|
double y1 = Math.floor(y);
|
||||||
|
double x2 = Math.ceil(x + width);
|
||||||
|
double y2 = Math.ceil(y + height);
|
||||||
|
gc.fillRect(x1, y1, Math.max(1.0, x2 - x1), Math.max(1.0, y2 - y1));
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record MouseTarget(MouseEncoderSize size, long screenWidth, long screenHeight) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,46 @@
|
|||||||
package com.gregor.jprototerm;
|
package com.gregor.jprototerm;
|
||||||
|
|
||||||
import dev.jlibghostty.Ghostty;
|
import dev.jlibghostty.Ghostty;
|
||||||
|
import dev.jlibghostty.KittyGraphics;
|
||||||
|
import dev.jlibghostty.MouseAction;
|
||||||
|
import dev.jlibghostty.MouseEncoder;
|
||||||
|
import dev.jlibghostty.MouseEncoderSize;
|
||||||
|
import dev.jlibghostty.MouseInput;
|
||||||
|
import dev.jlibghostty.RenderStateSnapshot;
|
||||||
|
import dev.jlibghostty.ScrollViewport;
|
||||||
import dev.jlibghostty.Terminal;
|
import dev.jlibghostty.Terminal;
|
||||||
import dev.jlibghostty.TerminalOptions;
|
import dev.jlibghostty.TerminalOptions;
|
||||||
|
import dev.jlibghostty.DeviceAttributes;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
public final class TerminalPane implements AutoCloseable {
|
public final class TerminalPane implements AutoCloseable {
|
||||||
private final Terminal terminal;
|
private final Terminal terminal;
|
||||||
private final KittyGraphicsRegistry graphicsRegistry;
|
private final MouseEncoder mouseEncoder = new MouseEncoder();
|
||||||
private final AtomicReference<String> snapshotText = new AtomicReference<>("");
|
private final AtomicReference<RenderStateSnapshot> renderSnapshot = new AtomicReference<>();
|
||||||
private ShellSession session;
|
private ShellSession session;
|
||||||
private boolean floating;
|
private boolean floating;
|
||||||
|
private boolean visible = true;
|
||||||
private double x;
|
private double x;
|
||||||
private double y;
|
private double y;
|
||||||
private double width;
|
private double width;
|
||||||
private double height;
|
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.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, long maxScrollback) {
|
||||||
Terminal terminal = Ghostty.open(TerminalOptions.of(columns, rows));
|
Terminal terminal = Ghostty.open(new TerminalOptions(columns, rows, maxScrollback));
|
||||||
TerminalPane pane = new TerminalPane(terminal, new KittyGraphicsRegistry(kittyGraphics));
|
terminal.setDeviceAttributesProvider(DeviceAttributes::xtermCompatible);
|
||||||
|
TerminalPane pane = new TerminalPane(terminal, columns, rows);
|
||||||
pane.refresh();
|
pane.refresh();
|
||||||
return pane;
|
return pane;
|
||||||
}
|
}
|
||||||
@@ -36,22 +52,72 @@ public final class TerminalPane implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void write(byte[] bytes) {
|
||||||
|
synchronized (terminal) {
|
||||||
|
terminal.write(bytes);
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void attach(ShellSession session) {
|
public void attach(ShellSession session) {
|
||||||
this.session = session;
|
this.session = session;
|
||||||
|
terminal.setPtyWriter(bytes -> {
|
||||||
|
ShellSession current = this.session;
|
||||||
|
if (current != null) {
|
||||||
|
current.send(bytes);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
session.startReading(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void send(String text) {
|
public void send(String text) {
|
||||||
|
scrollViewportToBottom();
|
||||||
if (session != null) {
|
if (session != null) {
|
||||||
session.send(text);
|
session.send(text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public String snapshotText() {
|
public boolean sendMouse(MouseInput input, MouseEncoderSize size, boolean anyButtonPressed) {
|
||||||
return snapshotText.get();
|
synchronized (terminal) {
|
||||||
|
mouseEncoder.syncFromTerminal(terminal);
|
||||||
|
mouseEncoder.setSize(size);
|
||||||
|
mouseEncoder.setAnyButtonPressed(anyButtonPressed);
|
||||||
|
mouseEncoder.setTrackLastCell(input.action() == MouseAction.MOTION && input.button().isEmpty());
|
||||||
|
|
||||||
|
byte[] encoded = mouseEncoder.encode(input);
|
||||||
|
if (encoded.length == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session != null) {
|
||||||
|
session.send(encoded);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public KittyGraphicsRegistry graphicsRegistry() {
|
public void scrollViewport(long rows) {
|
||||||
return graphicsRegistry;
|
synchronized (terminal) {
|
||||||
|
terminal.scrollViewport(ScrollViewport.delta(rows));
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void scrollViewportToBottom() {
|
||||||
|
synchronized (terminal) {
|
||||||
|
terminal.scrollViewport(ScrollViewport.bottom());
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public RenderStateSnapshot renderSnapshot() {
|
||||||
|
return renderSnapshot.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<KittyGraphics> kittyGraphics() {
|
||||||
|
synchronized (terminal) {
|
||||||
|
return terminal.kittyGraphics();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean floating() {
|
public boolean floating() {
|
||||||
@@ -62,6 +128,14 @@ public final class TerminalPane implements AutoCloseable {
|
|||||||
this.floating = floating;
|
this.floating = floating;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean visible() {
|
||||||
|
return visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVisible(boolean visible) {
|
||||||
|
this.visible = visible;
|
||||||
|
}
|
||||||
|
|
||||||
public double x() {
|
public double x() {
|
||||||
return x;
|
return x;
|
||||||
}
|
}
|
||||||
@@ -85,8 +159,29 @@ public final class TerminalPane implements AutoCloseable {
|
|||||||
this.height = height;
|
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() {
|
private void refresh() {
|
||||||
snapshotText.set(String.valueOf(terminal.snapshot()));
|
renderSnapshot.set(terminal.renderSnapshot());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -95,6 +190,7 @@ public final class TerminalPane implements AutoCloseable {
|
|||||||
session.close();
|
session.close();
|
||||||
session = null;
|
session = null;
|
||||||
}
|
}
|
||||||
|
mouseEncoder.close();
|
||||||
terminal.close();
|
terminal.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ public final class TerminalWorkspace implements AutoCloseable {
|
|||||||
private final AppConfig config;
|
private final AppConfig config;
|
||||||
private final List<TerminalPane> panes = new ArrayList<>();
|
private final List<TerminalPane> panes = new ArrayList<>();
|
||||||
private int activeIndex;
|
private int activeIndex;
|
||||||
|
private int hiddenFloatingFocusIndex = -1;
|
||||||
|
|
||||||
public TerminalWorkspace(AppConfig config) {
|
public TerminalWorkspace(AppConfig config) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
@@ -19,28 +20,55 @@ public final class TerminalWorkspace implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<TerminalPane> panes() {
|
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) {
|
public boolean isActive(TerminalPane pane) {
|
||||||
return activePane() == pane;
|
return activePane() == pane;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void focus(TerminalPane pane) {
|
||||||
|
int index = panes.indexOf(pane);
|
||||||
|
if (index >= 0 && pane.visible()) {
|
||||||
|
activeIndex = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void layout(double width, double height) {
|
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());
|
int tileCount = Math.max(1, tiled.size());
|
||||||
double tileWidth = width / tileCount;
|
double tileWidth = width / tileCount;
|
||||||
for (int i = 0; i < tiled.size(); i++) {
|
for (int i = 0; i < tiled.size(); i++) {
|
||||||
tiled.get(i).bounds(i * tileWidth, 0, tileWidth, height);
|
tiled.get(i).bounds(i * tileWidth, 0, tileWidth, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (TerminalPane pane : panes) {
|
List<TerminalPane> floating = panes.stream()
|
||||||
if (pane.floating()) {
|
.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 floatingWidth = Math.max(420, width * 0.58);
|
||||||
double floatingHeight = Math.max(260, height * 0.58);
|
double floatingHeight = Math.max(260, height * 0.58);
|
||||||
|
double offset = i * 28.0;
|
||||||
pane.bounds(
|
pane.bounds(
|
||||||
(width - floatingWidth) / 2.0,
|
Math.min(width - floatingWidth - 12.0, ((width - floatingWidth) / 2.0) + offset),
|
||||||
(height - floatingHeight) / 2.0,
|
Math.min(height - floatingHeight - 12.0, ((height - floatingHeight) / 2.0) + offset),
|
||||||
floatingWidth,
|
floatingWidth,
|
||||||
floatingHeight
|
floatingHeight
|
||||||
);
|
);
|
||||||
@@ -50,7 +78,12 @@ public final class TerminalWorkspace implements AutoCloseable {
|
|||||||
|
|
||||||
public void navigate(Direction direction) {
|
public void navigate(Direction direction) {
|
||||||
TerminalPane current = activePane();
|
TerminalPane current = activePane();
|
||||||
|
if (current.floating() && navigateFloatingStack(direction)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
panes.stream()
|
panes.stream()
|
||||||
|
.filter(TerminalPane::visible)
|
||||||
.filter(pane -> pane != current)
|
.filter(pane -> pane != current)
|
||||||
.filter(pane -> directionFilter(direction, current, pane))
|
.filter(pane -> directionFilter(direction, current, pane))
|
||||||
.min(Comparator.comparingDouble(pane -> distance(current, pane)))
|
.min(Comparator.comparingDouble(pane -> distance(current, pane)))
|
||||||
@@ -58,23 +91,160 @@ public final class TerminalWorkspace implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void toggleFloating() {
|
public void toggleFloating() {
|
||||||
TerminalPane active = activePane();
|
List<TerminalPane> floating = panes.stream()
|
||||||
if (active.floating()) {
|
.filter(TerminalPane::floating)
|
||||||
panes.remove(activeIndex);
|
.toList();
|
||||||
active.close();
|
if (floating.isEmpty()) {
|
||||||
activeIndex = Math.max(0, activeIndex - 1);
|
createFloatingPane();
|
||||||
return;
|
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);
|
TerminalPane pane = openPane(true);
|
||||||
panes.add(pane);
|
panes.add(pane);
|
||||||
activeIndex = panes.size() - 1;
|
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) {
|
private TerminalPane openPane(boolean floating) {
|
||||||
TerminalPane pane = TerminalPane.create(config.columns(), config.rows(), config.kittyGraphics());
|
TerminalPane pane = TerminalPane.create(config.columns(), config.rows(), config.maxScrollback());
|
||||||
pane.setFloating(floating);
|
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;
|
return pane;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +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
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "com.sun.prism.es2.X11GLFactory",
|
|
||||||
"allDeclaredConstructors": true,
|
|
||||||
"allPublicConstructors": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "com.sun.prism.sw.SWPipeline",
|
|
||||||
"allDeclaredConstructors": true,
|
|
||||||
"allPublicConstructors": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"resources": [
|
|
||||||
{ "pattern": ".*\\.css$" },
|
|
||||||
{ "pattern": ".*\\.toml$" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user