diff --git a/.gitignore b/.gitignore index ed72e43..7f46a51 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ devenv.local.yaml # pre-commit .pre-commit-config.yaml +build diff --git a/.gradle/9.4.1/checksums/checksums.lock b/.gradle/9.4.1/checksums/checksums.lock index 98fc7a5..21b1625 100644 Binary files a/.gradle/9.4.1/checksums/checksums.lock and b/.gradle/9.4.1/checksums/checksums.lock differ diff --git a/.gradle/9.4.1/checksums/md5-checksums.bin b/.gradle/9.4.1/checksums/md5-checksums.bin index b01b8c5..6c6e39e 100644 Binary files a/.gradle/9.4.1/checksums/md5-checksums.bin and b/.gradle/9.4.1/checksums/md5-checksums.bin differ diff --git a/.gradle/9.4.1/checksums/sha1-checksums.bin b/.gradle/9.4.1/checksums/sha1-checksums.bin index 8651f4c..163b8e6 100644 Binary files a/.gradle/9.4.1/checksums/sha1-checksums.bin and b/.gradle/9.4.1/checksums/sha1-checksums.bin differ diff --git a/.gradle/9.4.1/executionHistory/executionHistory.bin b/.gradle/9.4.1/executionHistory/executionHistory.bin index 1a9b6d6..b89587b 100644 Binary files a/.gradle/9.4.1/executionHistory/executionHistory.bin and b/.gradle/9.4.1/executionHistory/executionHistory.bin differ diff --git a/.gradle/9.4.1/executionHistory/executionHistory.lock b/.gradle/9.4.1/executionHistory/executionHistory.lock index ac2b076..e066b43 100644 Binary files a/.gradle/9.4.1/executionHistory/executionHistory.lock and b/.gradle/9.4.1/executionHistory/executionHistory.lock differ diff --git a/.gradle/9.4.1/fileHashes/fileHashes.bin b/.gradle/9.4.1/fileHashes/fileHashes.bin index 2e15714..6ca9c89 100644 Binary files a/.gradle/9.4.1/fileHashes/fileHashes.bin and b/.gradle/9.4.1/fileHashes/fileHashes.bin differ diff --git a/.gradle/9.4.1/fileHashes/fileHashes.lock b/.gradle/9.4.1/fileHashes/fileHashes.lock index a2cbede..547b775 100644 Binary files a/.gradle/9.4.1/fileHashes/fileHashes.lock and b/.gradle/9.4.1/fileHashes/fileHashes.lock differ diff --git a/.gradle/9.4.1/fileHashes/resourceHashesCache.bin b/.gradle/9.4.1/fileHashes/resourceHashesCache.bin index 48d03dd..139d13a 100644 Binary files a/.gradle/9.4.1/fileHashes/resourceHashesCache.bin and b/.gradle/9.4.1/fileHashes/resourceHashesCache.bin differ diff --git a/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/.gradle/buildOutputCleanup/buildOutputCleanup.lock index 3413bb3..1b09cf3 100644 Binary files a/.gradle/buildOutputCleanup/buildOutputCleanup.lock and b/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/README.md b/README.md index 43dd93f..a9269bb 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,8 @@ If `XDG_CONFIG_HOME` is unset, the fallback is: $HOME/.config/jprototerm/config.toml ``` +If no config file exists, jprototerm writes the default config on startup. + Example, also available in `config.example.toml`: ```toml @@ -60,6 +62,7 @@ toggle_floating = "ALT+F" new_floating = "ALT+SHIFT+F" next_floating = "ALT+F12" close_pane = "ALT+X" +open_font_selector = "ALT+T" ``` ## Defaults @@ -69,5 +72,6 @@ close_pane = "ALT+X" - `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 diff --git a/build/classes/java/main/com/gregor/jprototerm/AppConfig.class b/build/classes/java/main/com/gregor/jprototerm/AppConfig.class index 24abfbb..96dce22 100644 Binary files a/build/classes/java/main/com/gregor/jprototerm/AppConfig.class and b/build/classes/java/main/com/gregor/jprototerm/AppConfig.class differ diff --git a/build/classes/java/main/com/gregor/jprototerm/KeyBinding.class b/build/classes/java/main/com/gregor/jprototerm/KeyBinding.class index 26acc65..017b2da 100644 Binary files a/build/classes/java/main/com/gregor/jprototerm/KeyBinding.class and b/build/classes/java/main/com/gregor/jprototerm/KeyBinding.class differ diff --git a/build/classes/java/main/com/gregor/jprototerm/Main$1.class b/build/classes/java/main/com/gregor/jprototerm/Main$1.class index 068e051..fc23ee6 100644 Binary files a/build/classes/java/main/com/gregor/jprototerm/Main$1.class and b/build/classes/java/main/com/gregor/jprototerm/Main$1.class differ diff --git a/build/classes/java/main/com/gregor/jprototerm/Main.class b/build/classes/java/main/com/gregor/jprototerm/Main.class index 72439f5..e05e6bd 100644 Binary files a/build/classes/java/main/com/gregor/jprototerm/Main.class and b/build/classes/java/main/com/gregor/jprototerm/Main.class differ diff --git a/build/classes/java/main/com/gregor/jprototerm/ShellSession.class b/build/classes/java/main/com/gregor/jprototerm/ShellSession.class index 9a4d193..95437f4 100644 Binary files a/build/classes/java/main/com/gregor/jprototerm/ShellSession.class and b/build/classes/java/main/com/gregor/jprototerm/ShellSession.class differ diff --git a/build/classes/java/main/com/gregor/jprototerm/TerminalCanvasView$FontMetrics.class b/build/classes/java/main/com/gregor/jprototerm/TerminalCanvasView$FontMetrics.class index 37dff2d..9f0f6d1 100644 Binary files a/build/classes/java/main/com/gregor/jprototerm/TerminalCanvasView$FontMetrics.class and b/build/classes/java/main/com/gregor/jprototerm/TerminalCanvasView$FontMetrics.class differ diff --git a/build/classes/java/main/com/gregor/jprototerm/TerminalCanvasView.class b/build/classes/java/main/com/gregor/jprototerm/TerminalCanvasView.class index de9420e..51ad634 100644 Binary files a/build/classes/java/main/com/gregor/jprototerm/TerminalCanvasView.class and b/build/classes/java/main/com/gregor/jprototerm/TerminalCanvasView.class differ diff --git a/build/classes/java/main/com/gregor/jprototerm/TerminalPane.class b/build/classes/java/main/com/gregor/jprototerm/TerminalPane.class index 2b79a1b..b860482 100644 Binary files a/build/classes/java/main/com/gregor/jprototerm/TerminalPane.class and b/build/classes/java/main/com/gregor/jprototerm/TerminalPane.class differ diff --git a/build/tmp/compileJava/compileTransaction/stash-dir/Main$1.class.uniqueId5 b/build/tmp/compileJava/compileTransaction/stash-dir/Main$1.class.uniqueId5 deleted file mode 100644 index 068e051..0000000 Binary files a/build/tmp/compileJava/compileTransaction/stash-dir/Main$1.class.uniqueId5 and /dev/null differ diff --git a/build/tmp/compileJava/compileTransaction/stash-dir/Main.class.uniqueId3 b/build/tmp/compileJava/compileTransaction/stash-dir/Main.class.uniqueId3 deleted file mode 100644 index 72439f5..0000000 Binary files a/build/tmp/compileJava/compileTransaction/stash-dir/Main.class.uniqueId3 and /dev/null differ diff --git a/build/tmp/compileJava/compileTransaction/stash-dir/TerminalCanvasView$FontMetrics.class.uniqueId4 b/build/tmp/compileJava/compileTransaction/stash-dir/TerminalCanvasView$FontMetrics.class.uniqueId4 deleted file mode 100644 index 37dff2d..0000000 Binary files a/build/tmp/compileJava/compileTransaction/stash-dir/TerminalCanvasView$FontMetrics.class.uniqueId4 and /dev/null differ diff --git a/build/tmp/compileJava/compileTransaction/stash-dir/TerminalCanvasView.class.uniqueId1 b/build/tmp/compileJava/compileTransaction/stash-dir/TerminalCanvasView.class.uniqueId1 deleted file mode 100644 index de9420e..0000000 Binary files a/build/tmp/compileJava/compileTransaction/stash-dir/TerminalCanvasView.class.uniqueId1 and /dev/null differ diff --git a/build/tmp/compileJava/compileTransaction/stash-dir/TerminalWorkspace$1.class.uniqueId2 b/build/tmp/compileJava/compileTransaction/stash-dir/TerminalWorkspace$1.class.uniqueId2 deleted file mode 100644 index cae4c9b..0000000 Binary files a/build/tmp/compileJava/compileTransaction/stash-dir/TerminalWorkspace$1.class.uniqueId2 and /dev/null differ diff --git a/build/tmp/compileJava/compileTransaction/stash-dir/TerminalWorkspace.class.uniqueId0 b/build/tmp/compileJava/compileTransaction/stash-dir/TerminalWorkspace.class.uniqueId0 deleted file mode 100644 index 9774dde..0000000 Binary files a/build/tmp/compileJava/compileTransaction/stash-dir/TerminalWorkspace.class.uniqueId0 and /dev/null differ diff --git a/build/tmp/compileJava/previous-compilation-data.bin b/build/tmp/compileJava/previous-compilation-data.bin index adba47d..50a8bb1 100644 Binary files a/build/tmp/compileJava/previous-compilation-data.bin and b/build/tmp/compileJava/previous-compilation-data.bin differ diff --git a/config.example.toml b/config.example.toml index cb450ee..6289750 100644 --- a/config.example.toml +++ b/config.example.toml @@ -21,3 +21,4 @@ toggle_floating = "ALT+F" new_floating = "ALT+SHIFT+F" next_floating = "ALT+F12" close_pane = "ALT+X" +open_font_selector = "ALT+T" diff --git a/devenv.nix b/devenv.nix index 78379c6..15b5c3c 100644 --- a/devenv.nix +++ b/devenv.nix @@ -7,6 +7,7 @@ let "git+https://gitea.gregorlohaus.com/gregor/jlibghostty.git"; jlib = jlibghostty.packages.${system}.jlibghostty; + hostNvidiaLibs = ".devenv/host-nvidia-libs"; in { packages = [ @@ -26,9 +27,10 @@ in pkgs.libGL pkgs.gtk3 pkgs.alsa-lib + pkgs.mesa-demos ]; - env.LD_LIBRARY_PATH = lib.makeLibraryPath [ + env.LD_LIBRARY_PATH = "${hostNvidiaLibs}:" + lib.makeLibraryPath [ pkgs.openjfx pkgs.glib @@ -42,5 +44,23 @@ in 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 + ''; } diff --git a/src/main/java/com/gregor/jprototerm/AppConfig.java b/src/main/java/com/gregor/jprototerm/AppConfig.java index cd07328..2684e78 100644 --- a/src/main/java/com/gregor/jprototerm/AppConfig.java +++ b/src/main/java/com/gregor/jprototerm/AppConfig.java @@ -8,7 +8,11 @@ import io.github.wasabithumb.jtoml.value.primitive.TomlPrimitive; import io.github.wasabithumb.jtoml.value.table.TomlTable; import java.nio.file.Files; +import java.io.IOException; import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; public record AppConfig( @@ -22,10 +26,23 @@ public record AppConfig( boolean kittyGraphics, Map keybindings ) { + private static final List 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() { AppConfig defaults = defaults(); Path path = configPath(); if (!Files.isRegularFile(path)) { + writeDefaultConfig(path, defaults); return defaults; } @@ -40,16 +57,7 @@ public record AppConfig( doubleValue(document, "window.width", defaults.windowWidth), doubleValue(document, "window.height", defaults.windowHeight), booleanValue(document, "kitty_graphics.enabled", defaults.kittyGraphics), - Map.of( - "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")), - "new_floating", binding(document, "keybindings.new_floating", defaults.keybindings.get("new_floating")), - "next_floating", binding(document, "keybindings.next_floating", defaults.keybindings.get("next_floating")), - "close_pane", binding(document, "keybindings.close_pane", defaults.keybindings.get("close_pane")) - ) + keybindings(document, defaults) ); } catch (TomlException ex) { System.err.println("Could not parse " + path + ": " + ex.getMessage()); @@ -75,11 +83,30 @@ public record AppConfig( "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") + "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, + shell, + family, + size, + windowWidth, + windowHeight, + kittyGraphics, + keybindings + ); + } + + public void save() { + save(configPath(), this); + } + public static Path configPath() { String configHome = System.getenv("XDG_CONFIG_HOME"); if (configHome != null && !configHome.isBlank()) { @@ -92,6 +119,76 @@ public record AppConfig( return "/bin/bash"; } + private static Map keybindings(TomlTable table, AppConfig defaults) { + Map 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("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) { String value = stringValue(table, key, null); if (value == null) { diff --git a/src/main/java/com/gregor/jprototerm/KeyBinding.java b/src/main/java/com/gregor/jprototerm/KeyBinding.java index 1c0ebe9..f7c5cab 100644 --- a/src/main/java/com/gregor/jprototerm/KeyBinding.java +++ b/src/main/java/com/gregor/jprototerm/KeyBinding.java @@ -35,6 +35,22 @@ public record KeyBinding(boolean alt, boolean control, boolean shift, KeyCode co && 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; diff --git a/src/main/java/com/gregor/jprototerm/Main.java b/src/main/java/com/gregor/jprototerm/Main.java index 911c412..351871c 100644 --- a/src/main/java/com/gregor/jprototerm/Main.java +++ b/src/main/java/com/gregor/jprototerm/Main.java @@ -3,19 +3,29 @@ package com.gregor.jprototerm; import javafx.animation.AnimationTimer; import javafx.application.Application; 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.layout.GridPane; import javafx.scene.layout.StackPane; +import javafx.scene.text.Font; import javafx.stage.Stage; public final class Main extends Application { private TerminalWorkspace workspace; + private TerminalCanvasView terminalView; + private AppConfig config; @Override public void start(Stage stage) { - AppConfig config = AppConfig.load(); + config = AppConfig.load(); workspace = new TerminalWorkspace(config); - TerminalCanvasView terminalView = new TerminalCanvasView(workspace, config); + terminalView = new TerminalCanvasView(workspace, config); StackPane root = new StackPane(terminalView.canvas()); terminalView.canvas().widthProperty().bind(root.widthProperty()); @@ -23,7 +33,7 @@ public final class Main extends Application { terminalView.canvas().setOnMousePressed(event -> terminalView.canvas().requestFocus()); Scene scene = new Scene(root, config.windowWidth(), config.windowHeight()); - scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> handlePressed(config, event)); + scene.addEventFilter(KeyEvent.KEY_PRESSED, this::handlePressed); scene.addEventFilter(KeyEvent.KEY_TYPED, event -> handleTyped(event)); new AnimationTimer() { @@ -42,7 +52,7 @@ public final class Main extends Application { terminalView.canvas().requestFocus(); } - private void handlePressed(AppConfig config, KeyEvent event) { + private void handlePressed(KeyEvent event) { if (config.keybindings().get("navigate_left").matches(event)) { workspace.navigate(Direction.LEFT); event.consume(); @@ -67,6 +77,9 @@ public final class Main extends Application { } 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 { String encoded = KeyEncoder.encode(event); if (encoded != null) { @@ -88,6 +101,49 @@ public final class Main extends Application { } } + private void openFontSelector() { + Dialog dialog = new Dialog<>(); + dialog.setTitle("Font"); + dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); + + ComboBox family = new ComboBox<>(); + family.getItems().setAll(Font.getFamilies()); + family.setEditable(true); + family.setMaxWidth(Double.MAX_VALUE); + family.setValue(config.fontFamily()); + + Spinner 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) { System.setProperty("prism.order", System.getProperty("prism.order", "es2,sw")); launch(Main.class, args); diff --git a/src/main/java/com/gregor/jprototerm/ShellSession.java b/src/main/java/com/gregor/jprototerm/ShellSession.java index 64101cd..e70a6fb 100644 --- a/src/main/java/com/gregor/jprototerm/ShellSession.java +++ b/src/main/java/com/gregor/jprototerm/ShellSession.java @@ -19,7 +19,7 @@ public final class ShellSession implements AutoCloseable { private final ExecutorService reader; private volatile boolean closed; - private ShellSession(PtyProcess process, TerminalPane pane) { + private ShellSession(PtyProcess process) { this.process = process; this.stdin = process.getOutputStream(); this.reader = Executors.newSingleThreadExecutor(runnable -> { @@ -27,7 +27,6 @@ public final class ShellSession implements AutoCloseable { thread.setDaemon(true); return thread; }); - reader.submit(() -> readOutput(pane)); } public static ShellSession start(String shell, TerminalPane pane, int columns, int rows) { @@ -42,13 +41,17 @@ public final class ShellSession implements AutoCloseable { .setInitialRows(rows) .setDirectory(System.getProperty("user.home")) .start(); - return new ShellSession(process, pane); + return new ShellSession(process); } catch (IOException ex) { pane.write("failed to start shell: " + ex.getMessage() + "\r\n"); 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; @@ -57,11 +60,15 @@ public final class ShellSession implements AutoCloseable { } public void send(String text) { + send(text.getBytes(StandardCharsets.UTF_8)); + } + + public void send(byte[] bytes) { if (closed) { return; } try { - stdin.write(text.getBytes(StandardCharsets.UTF_8)); + stdin.write(bytes); stdin.flush(); } catch (IOException ex) { close(); diff --git a/src/main/java/com/gregor/jprototerm/TerminalCanvasView.java b/src/main/java/com/gregor/jprototerm/TerminalCanvasView.java index cf03466..9b1a0d0 100644 --- a/src/main/java/com/gregor/jprototerm/TerminalCanvasView.java +++ b/src/main/java/com/gregor/jprototerm/TerminalCanvasView.java @@ -32,10 +32,14 @@ public final class TerminalCanvasView { private final TerminalWorkspace workspace; private final AppConfig config; private final Map kittyImageCache = new HashMap<>(); + private String fontFamily; + private double fontSize; public TerminalCanvasView(TerminalWorkspace workspace, AppConfig config) { this.workspace = workspace; this.config = config; + this.fontFamily = config.fontFamily(); + this.fontSize = config.fontSize(); canvas.setFocusTraversable(true); } @@ -43,6 +47,11 @@ public final class TerminalCanvasView { return canvas; } + public void setFont(String family, double size) { + this.fontFamily = family; + this.fontSize = size; + } + public void render() { double width = canvas.getWidth(); double height = canvas.getHeight(); @@ -75,7 +84,7 @@ public final class TerminalCanvasView { 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); - Font font = Font.font(config.fontFamily(), config.fontSize()); + Font font = Font.font(fontFamily, fontSize); gc.setFont(font); FontMetrics metrics = measureFontMetrics(font); @@ -105,15 +114,14 @@ public final class TerminalCanvasView { } private static FontMetrics measureFontMetrics(Font font) { - Text text = new Text("Mg"); + Text text = new Text("┃MgÅjy"); text.setFont(font); - double textHeight = text.getLayoutBounds().getHeight(); - double lineHeight = Math.max(1.0, Math.ceil(textHeight * 1.2)); - double baselineOffset = -text.getLayoutBounds().getMinY() + ((lineHeight - textHeight) / 2.0); + double lineHeight = Math.max(1.0, text.getLayoutBounds().getHeight()); + double baselineOffset = -text.getLayoutBounds().getMinY(); Text cell = new Text("M"); cell.setFont(font); - double cellWidth = Math.max(1.0, Math.ceil(cell.getLayoutBounds().getWidth())); + double cellWidth = Math.max(1.0, cell.getLayoutBounds().getWidth()); return new FontMetrics(cellWidth, lineHeight, baselineOffset); } diff --git a/src/main/java/com/gregor/jprototerm/TerminalPane.java b/src/main/java/com/gregor/jprototerm/TerminalPane.java index b2b35d0..2ce4e00 100644 --- a/src/main/java/com/gregor/jprototerm/TerminalPane.java +++ b/src/main/java/com/gregor/jprototerm/TerminalPane.java @@ -5,6 +5,7 @@ import dev.jlibghostty.KittyGraphics; import dev.jlibghostty.RenderStateSnapshot; import dev.jlibghostty.Terminal; import dev.jlibghostty.TerminalOptions; +import dev.jlibghostty.DeviceAttributes; import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; @@ -32,6 +33,7 @@ public final class TerminalPane implements AutoCloseable { public static TerminalPane create(int columns, int rows) { Terminal terminal = Ghostty.open(TerminalOptions.of(columns, rows)); + terminal.setDeviceAttributesProvider(DeviceAttributes::xtermCompatible); TerminalPane pane = new TerminalPane(terminal, columns, rows); pane.refresh(); return pane; @@ -53,6 +55,13 @@ public final class TerminalPane implements AutoCloseable { public void attach(ShellSession 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) {