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();
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) {

View File

@@ -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++;
}

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
* 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));
}
/**

View File

@@ -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();

View File

@@ -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<String, Runnable> 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<String, Runnable> 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) {