diff --git a/src/main/java/com/gregor/jprototerm/AppConfig.java b/src/main/java/com/gregor/jprototerm/AppConfig.java index 9936b87..7453001 100644 --- a/src/main/java/com/gregor/jprototerm/AppConfig.java +++ b/src/main/java/com/gregor/jprototerm/AppConfig.java @@ -55,7 +55,7 @@ public record AppConfig( AppConfig defaults = defaults(); Path path = configPath(); if (!Files.isRegularFile(path)) { - writeDefaultConfig(path, defaults); + save(path, defaults); return defaults; } @@ -195,10 +195,6 @@ public record AppConfig( 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(); @@ -340,15 +336,7 @@ public record AppConfig( } private static int intValue(TomlTable table, String key, int fallback) { - TomlPrimitive primitive = primitive(table, key); - if (primitive == null) { - return fallback; - } - try { - return primitive.asInteger(); - } catch (RuntimeException ex) { - return fallback; - } + return (int) longValue(table, key, fallback); } private static long longValue(TomlTable table, String key, long fallback) { diff --git a/src/main/java/com/gregor/jprototerm/Compositor.java b/src/main/java/com/gregor/jprototerm/Compositor.java index 88caa8c..882e8f4 100644 --- a/src/main/java/com/gregor/jprototerm/Compositor.java +++ b/src/main/java/com/gregor/jprototerm/Compositor.java @@ -134,29 +134,11 @@ public final class Compositor { } public void toggleFloating() { - if (isEmpty()) { - return; - } - currentTab().toggleFloating(); - layoutVersion++; + mutateCurrentTab(() -> currentTab().toggleFloating()); } public void createPane() { - if (isEmpty()) { - return; - } - currentTab().createPane(); - layoutVersion++; - } - - /** Opens a new floating pane, makes it active, and returns it (null when no tab exists). */ - public TerminalPane openFloatingPane() { - if (isEmpty()) { - return null; - } - TerminalPane pane = currentTab().createFloatingPane(); - layoutVersion++; - return pane; + mutateCurrentTab(() -> currentTab().createPane()); } /** @@ -173,18 +155,20 @@ public final class Compositor { } public void nextFloatingPane() { - if (isEmpty()) { - return; - } - currentTab().nextFloatingPane(); - layoutVersion++; + mutateCurrentTab(() -> currentTab().nextFloatingPane()); } public void toggleActiveFloating() { + mutateCurrentTab(() -> currentTab().toggleActiveFloating()); + } + + // Run a structural change on the current tab and bump the layout version so the next frame + // recomposites. No-op when no tab is left. + private void mutateCurrentTab(Runnable change) { if (isEmpty()) { return; } - currentTab().toggleActiveFloating(); + change.run(); layoutVersion++; } diff --git a/src/main/java/com/gregor/jprototerm/Tab.java b/src/main/java/com/gregor/jprototerm/Tab.java index c02e695..b854952 100644 --- a/src/main/java/com/gregor/jprototerm/Tab.java +++ b/src/main/java/com/gregor/jprototerm/Tab.java @@ -259,12 +259,6 @@ final class Tab implements AutoCloseable { } } - void closeActivePane() { - if (active != null) { - closePane(active); - } - } - /** * Closes {@code closing} (the active pane on a key-bound close, or any pane whose process just * exited) and re-selects the active pane only when the one that closed was active. Returns @@ -329,8 +323,8 @@ final class Tab implements AutoCloseable { } } - TerminalPane createFloatingPane() { - return addFloating(openPane(true)); + private void createFloatingPane() { + addFloating(openPane(true)); } /** diff --git a/src/main/java/com/gregor/jprototerm/TerminalRenderer.java b/src/main/java/com/gregor/jprototerm/TerminalRenderer.java index 70d746e..ca9f4df 100644 --- a/src/main/java/com/gregor/jprototerm/TerminalRenderer.java +++ b/src/main/java/com/gregor/jprototerm/TerminalRenderer.java @@ -41,34 +41,28 @@ abstract class TerminalRenderer { void release() { } - protected static void clipRect(GraphicsContext gc, double x, double y, double width, double height) { - gc.beginPath(); - gc.rect(x, y, width, height); - gc.clip(); - } - /** * Clip to {@code region} if given (the pane's rect minus the panes covering it, computed by * {@code Shape.subtract} at layout), otherwise to the plain rect. The region is a rectilinear * path, so it replays onto the canvas as move/line/close segments. */ protected static void clip(GraphicsContext gc, double x, double y, double width, double height, Shape region) { - if (region == null) { - clipRect(gc, x, y, width, height); - return; - } - var elements = ((Path) region).getElements(); gc.beginPath(); - if (elements.isEmpty()) { - gc.rect(x, y, 0.0, 0.0); // fully covered: clip to nothing - } - for (PathElement element : elements) { - if (element instanceof MoveTo moveTo) { - gc.moveTo(moveTo.getX(), moveTo.getY()); - } else if (element instanceof LineTo lineTo) { - gc.lineTo(lineTo.getX(), lineTo.getY()); - } else if (element instanceof ClosePath) { - gc.closePath(); + if (region == null) { + gc.rect(x, y, width, height); + } else { + var elements = ((Path) region).getElements(); + if (elements.isEmpty()) { + gc.rect(x, y, 0.0, 0.0); // fully covered: clip to nothing + } + for (PathElement element : elements) { + if (element instanceof MoveTo moveTo) { + gc.moveTo(moveTo.getX(), moveTo.getY()); + } else if (element instanceof LineTo lineTo) { + gc.lineTo(lineTo.getX(), lineTo.getY()); + } else if (element instanceof ClosePath) { + gc.closePath(); + } } } gc.clip(); diff --git a/src/main/java/com/gregor/jprototerm/TerminalWindow.java b/src/main/java/com/gregor/jprototerm/TerminalWindow.java index 8c970cb..d69b4bb 100644 --- a/src/main/java/com/gregor/jprototerm/TerminalWindow.java +++ b/src/main/java/com/gregor/jprototerm/TerminalWindow.java @@ -20,7 +20,9 @@ import javafx.stage.Stage; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; /** * One top-level terminal window: its own {@link Stage}, {@link Compositor}, config/metrics, render @@ -34,6 +36,9 @@ final class TerminalWindow { private final Compositor compositor; private final Stage stage; private final AnimationTimer renderLoop; + // Key-bound actions by their config keybinding name, checked in this order on each key press + // (the keys must match AppConfig's keybinding keys). + private final Map keyActions = new LinkedHashMap<>(); private AppConfig config; private boolean closed; @@ -51,6 +56,23 @@ final class TerminalWindow { // The last pane closing closes this window (not the JVM); see teardown(). compositor.setOnEmpty(this::teardown); + keyActions.put("navigate_left", () -> compositor.navigate(Direction.LEFT)); + keyActions.put("navigate_down", () -> compositor.navigate(Direction.DOWN)); + keyActions.put("navigate_up", () -> compositor.navigate(Direction.UP)); + keyActions.put("navigate_right", () -> compositor.navigate(Direction.RIGHT)); + keyActions.put("toggle_floating", compositor::toggleFloating); + keyActions.put("new_pane", compositor::createPane); + keyActions.put("next_floating", compositor::nextFloatingPane); + keyActions.put("promote_floating", compositor::toggleActiveFloating); + // Closing the last pane closes this window, via the compositor's onEmpty hook. + keyActions.put("close_pane", compositor::closeActivePane); + keyActions.put("new_tab", compositor::newTab); + keyActions.put("previous_tab", compositor::previousTab); + keyActions.put("next_tab", compositor::nextTab); + keyActions.put("open_font_selector", this::openFontSelector); + keyActions.put("open_scrollback", this::openScrollbackInEditor); + keyActions.put("paste", this::pasteFromClipboard); + StackPane root = new StackPane(compositor.canvas(), compositor.imageOverlay()); compositor.canvas().widthProperty().bind(root.widthProperty()); compositor.canvas().heightProperty().bind(root.heightProperty()); @@ -107,59 +129,18 @@ final class TerminalWindow { } private void handlePressed(KeyEvent event) { - if (config.keybindings().get("navigate_left").matches(event)) { - compositor.navigate(Direction.LEFT); - event.consume(); - } else if (config.keybindings().get("navigate_down").matches(event)) { - compositor.navigate(Direction.DOWN); - event.consume(); - } else if (config.keybindings().get("navigate_up").matches(event)) { - compositor.navigate(Direction.UP); - event.consume(); - } else if (config.keybindings().get("navigate_right").matches(event)) { - compositor.navigate(Direction.RIGHT); - event.consume(); - } else if (config.keybindings().get("toggle_floating").matches(event)) { - compositor.toggleFloating(); - event.consume(); - } else if (config.keybindings().get("new_pane").matches(event)) { - compositor.createPane(); - event.consume(); - } else if (config.keybindings().get("next_floating").matches(event)) { - compositor.nextFloatingPane(); - event.consume(); - } else if (config.keybindings().get("promote_floating").matches(event)) { - compositor.toggleActiveFloating(); - event.consume(); - } else if (config.keybindings().get("close_pane").matches(event)) { - // Closing the last pane closes this window, via the compositor's onEmpty hook. - compositor.closeActivePane(); - event.consume(); - } else if (config.keybindings().get("new_tab").matches(event)) { - compositor.newTab(); - event.consume(); - } else if (config.keybindings().get("previous_tab").matches(event)) { - compositor.previousTab(); - event.consume(); - } else if (config.keybindings().get("next_tab").matches(event)) { - compositor.nextTab(); - event.consume(); - } else if (config.keybindings().get("open_font_selector").matches(event)) { - openFontSelector(); - event.consume(); - } else if (config.keybindings().get("open_scrollback").matches(event)) { - openScrollbackInEditor(); - event.consume(); - } else if (config.keybindings().get("paste").matches(event)) { - pasteFromClipboard(); - event.consume(); - } else { - String encoded = KeyEncoder.encode(event); - if (encoded != null) { - compositor.activePane().send(encoded); + for (Map.Entry action : keyActions.entrySet()) { + if (config.keybindings().get(action.getKey()).matches(event)) { + action.getValue().run(); event.consume(); + return; } } + String encoded = KeyEncoder.encode(event); + if (encoded != null) { + compositor.activePane().send(encoded); + event.consume(); + } } private void handleTyped(KeyEvent event) {