refactor config

This commit is contained in:
2026-06-12 11:48:34 +02:00
parent 692511e445
commit 8954482222
5 changed files with 60 additions and 119 deletions

View File

@@ -55,7 +55,7 @@ public record AppConfig(
AppConfig defaults = defaults(); AppConfig defaults = defaults();
Path path = configPath(); Path path = configPath();
if (!Files.isRegularFile(path)) { if (!Files.isRegularFile(path)) {
writeDefaultConfig(path, defaults); save(path, defaults);
return defaults; return defaults;
} }
@@ -195,10 +195,6 @@ public record AppConfig(
return Map.copyOf(parsed); return Map.copyOf(parsed);
} }
private static void writeDefaultConfig(Path path, AppConfig defaults) {
save(path, defaults);
}
private static void save(Path path, AppConfig config) { private static void save(Path path, AppConfig config) {
try { try {
Path parent = path.getParent(); Path parent = path.getParent();
@@ -340,15 +336,7 @@ public record AppConfig(
} }
private static int intValue(TomlTable table, String key, int fallback) { private static int intValue(TomlTable table, String key, int fallback) {
TomlPrimitive primitive = primitive(table, key); return (int) longValue(table, key, fallback);
if (primitive == null) {
return fallback;
}
try {
return primitive.asInteger();
} catch (RuntimeException ex) {
return fallback;
}
} }
private static long longValue(TomlTable table, String key, long fallback) { private static long longValue(TomlTable table, String key, long fallback) {

View File

@@ -134,29 +134,11 @@ public final class Compositor {
} }
public void toggleFloating() { public void toggleFloating() {
if (isEmpty()) { mutateCurrentTab(() -> currentTab().toggleFloating());
return;
}
currentTab().toggleFloating();
layoutVersion++;
} }
public void createPane() { public void createPane() {
if (isEmpty()) { mutateCurrentTab(() -> currentTab().createPane());
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;
} }
/** /**
@@ -173,18 +155,20 @@ public final class Compositor {
} }
public void nextFloatingPane() { public void nextFloatingPane() {
if (isEmpty()) { mutateCurrentTab(() -> currentTab().nextFloatingPane());
return;
}
currentTab().nextFloatingPane();
layoutVersion++;
} }
public void toggleActiveFloating() { 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()) { if (isEmpty()) {
return; return;
} }
currentTab().toggleActiveFloating(); change.run();
layoutVersion++; layoutVersion++;
} }

View File

@@ -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 * 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 * 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() { private void createFloatingPane() {
return addFloating(openPane(true)); addFloating(openPane(true));
} }
/** /**

View File

@@ -41,34 +41,28 @@ abstract class TerminalRenderer {
void release() { 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 * 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 * {@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. * 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) { 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(); gc.beginPath();
if (elements.isEmpty()) { if (region == null) {
gc.rect(x, y, 0.0, 0.0); // fully covered: clip to nothing gc.rect(x, y, width, height);
} } else {
for (PathElement element : elements) { var elements = ((Path) region).getElements();
if (element instanceof MoveTo moveTo) { if (elements.isEmpty()) {
gc.moveTo(moveTo.getX(), moveTo.getY()); gc.rect(x, y, 0.0, 0.0); // fully covered: clip to nothing
} else if (element instanceof LineTo lineTo) { }
gc.lineTo(lineTo.getX(), lineTo.getY()); for (PathElement element : elements) {
} else if (element instanceof ClosePath) { if (element instanceof MoveTo moveTo) {
gc.closePath(); 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(); gc.clip();

View File

@@ -20,7 +20,9 @@ import javafx.stage.Stage;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* One top-level terminal window: its own {@link Stage}, {@link Compositor}, config/metrics, render * 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 Compositor compositor;
private final Stage stage; private final Stage stage;
private final AnimationTimer renderLoop; 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<String, Runnable> keyActions = new LinkedHashMap<>();
private AppConfig config; private AppConfig config;
private boolean closed; private boolean closed;
@@ -51,6 +56,23 @@ final class TerminalWindow {
// The last pane closing closes this window (not the JVM); see teardown(). // The last pane closing closes this window (not the JVM); see teardown().
compositor.setOnEmpty(this::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()); StackPane root = new StackPane(compositor.canvas(), compositor.imageOverlay());
compositor.canvas().widthProperty().bind(root.widthProperty()); compositor.canvas().widthProperty().bind(root.widthProperty());
compositor.canvas().heightProperty().bind(root.heightProperty()); compositor.canvas().heightProperty().bind(root.heightProperty());
@@ -107,59 +129,18 @@ final class TerminalWindow {
} }
private void handlePressed(KeyEvent event) { private void handlePressed(KeyEvent event) {
if (config.keybindings().get("navigate_left").matches(event)) { for (Map.Entry<String, Runnable> action : keyActions.entrySet()) {
compositor.navigate(Direction.LEFT); if (config.keybindings().get(action.getKey()).matches(event)) {
event.consume(); action.getValue().run();
} 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);
event.consume(); event.consume();
return;
} }
} }
String encoded = KeyEncoder.encode(event);
if (encoded != null) {
compositor.activePane().send(encoded);
event.consume();
}
} }
private void handleTyped(KeyEvent event) { private void handleTyped(KeyEvent event) {