From 4e2ed7f99fbd15efd22269d3977d4310b082bfb2 Mon Sep 17 00:00:00 2001 From: Gregor Lohaus Date: Fri, 19 Jun 2026 16:02:06 +0200 Subject: [PATCH 1/5] multiple worktree creation --- README.md | 6 +++-- config.example.toml | 1 + .../java/com/gregor/jprototerm/AppConfig.java | 7 ++++- .../com/gregor/jprototerm/TerminalWindow.java | 26 ++++++++++++++++--- 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 60558a9..a8fdeac 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,7 @@ editor_command = "vi {file}" [worktree] relative_worktree_path = "./.worktrees" +split_regex = "," [env.override] ZELLIJ_SESSION_NAME = "" @@ -188,8 +189,9 @@ paste = "CTRL+SHIFT+V" - `Alt+Shift+h` / `Alt+Shift+l`: previous / next tab - `Alt+t`: open the font selector - `Alt+s`: open the active pane scrollback in `$EDITOR` -- `Alt+w`: edit a worktree name, then run `git worktree add /` - from the previously focused pane's working directory +- `Alt+w`: edit one or more worktree names, split by `worktree.split_regex`, then run + `git worktree add /` for each name from the previously focused + pane's working directory - `Alt+y`: enter pane-sync selection mode, commit the selection, or stop an active pane sync - `Space`: toggle the focused pane in the sync set while pane-sync selection mode is active - Once committed, input typed or pasted into any synced pane is mirrored to the other synced panes diff --git a/config.example.toml b/config.example.toml index a3875f6..98b8f4c 100644 --- a/config.example.toml +++ b/config.example.toml @@ -21,6 +21,7 @@ editor_command = "vi {file}" [worktree] relative_worktree_path = "./.worktrees" +split_regex = "," [env.override] ZELLIJ_SESSION_NAME = "" diff --git a/src/main/java/com/gregor/jprototerm/AppConfig.java b/src/main/java/com/gregor/jprototerm/AppConfig.java index 876c875..882ec5e 100644 --- a/src/main/java/com/gregor/jprototerm/AppConfig.java +++ b/src/main/java/com/gregor/jprototerm/AppConfig.java @@ -30,6 +30,7 @@ public record AppConfig( boolean kittyGraphics, String scrollbackEditorCommand, String worktreeRelativePath, + String worktreeSplitRegex, String closeSignal, Map envOverride, Map keybindings @@ -77,6 +78,7 @@ public record AppConfig( booleanValue(document, "kitty_graphics.enabled", defaults.kittyGraphics), stringValue(document, "scrollback.editor_command", defaults.scrollbackEditorCommand), stringValue(document, "worktree.relative_worktree_path", defaults.worktreeRelativePath), + stringValue(document, "worktree.split_regex", defaults.worktreeSplitRegex), closeSignalValue(document, defaults.closeSignal), envOverride(document, defaults.envOverride), keybindings(document, defaults) @@ -100,6 +102,7 @@ public record AppConfig( true, defaultScrollbackEditorCommand(), "./.worktrees", + ",", "SIGTERM", Map.of(), Map.ofEntries( @@ -138,6 +141,7 @@ public record AppConfig( kittyGraphics, scrollbackEditorCommand, worktreeRelativePath, + worktreeSplitRegex, closeSignal, envOverride, keybindings @@ -241,7 +245,8 @@ public record AppConfig( builder.append("[scrollback]\n"); builder.append("editor_command = ").append(quoted(scrollbackEditorCommand)).append("\n\n"); builder.append("[worktree]\n"); - builder.append("relative_worktree_path = ").append(quoted(worktreeRelativePath)).append("\n\n"); + builder.append("relative_worktree_path = ").append(quoted(worktreeRelativePath)).append('\n'); + builder.append("split_regex = ").append(quoted(worktreeSplitRegex)).append("\n\n"); builder.append("[env.override]\n"); for (Map.Entry entry : envOverride.entrySet()) { builder.append(entry.getKey()).append(" = ").append(quoted(entry.getValue())).append('\n'); diff --git a/src/main/java/com/gregor/jprototerm/TerminalWindow.java b/src/main/java/com/gregor/jprototerm/TerminalWindow.java index 0601b5a..678cab7 100644 --- a/src/main/java/com/gregor/jprototerm/TerminalWindow.java +++ b/src/main/java/com/gregor/jprototerm/TerminalWindow.java @@ -313,14 +313,32 @@ final class TerminalWindow { if (relativePath == null || relativePath.isBlank()) { relativePath = "./.worktrees"; } + String splitRegex = config.worktreeSplitRegex(); + if (splitRegex == null || splitRegex.isBlank()) { + splitRegex = ","; + } return editorCommand(file) + "; editor_status=$?" - + "; name=$(cat " + quotedFile + ")" - + "; if [ \"$editor_status\" -eq 0 ] && [ -n \"$name\" ]; then" + + "; git_status=$editor_status" + + "; if [ \"$editor_status\" -eq 0 ]; then" + + " if names_file=$(mktemp); then" + + " if awk -v re=" + shellQuote(splitRegex) + + " '{ text = text $0 \"\\n\" }" + + " END { n = split(text, names, re); for (i = 1; i <= n; i++)" + + " { name = names[i]; sub(/^[[:space:]]+/, \"\", name);" + + " sub(/[[:space:]]+$/, \"\", name); if (name != \"\") print name; } }'" + + " " + quotedFile + " > \"$names_file\"; then" + + " git_status=0" + + "; while IFS= read -r name; do" + " git worktree add " + shellQuote(relativePath) + "/\"$name\"" - + "; git_status=$?" - + "; else git_status=$editor_status" + + " || { git_status=$?; break; }" + + "; done < \"$names_file\"" + + "; else git_status=$?" + + "; fi" + + "; rm -f \"$names_file\"" + + "; else git_status=$?" + + "; fi" + "; fi" + "; rm -f " + quotedFile + "; exit \"$git_status\""; From 7b3a370371c91626d331969b09e24fd6c63fb047 Mon Sep 17 00:00:00 2001 From: Gregor Lohaus Date: Fri, 19 Jun 2026 16:08:17 +0200 Subject: [PATCH 2/5] post worktree creation actions --- README.md | 6 +- config.example.toml | 2 + .../java/com/gregor/jprototerm/AppConfig.java | 7 ++- .../com/gregor/jprototerm/Compositor.java | 8 +++ src/main/java/com/gregor/jprototerm/Tab.java | 22 +++++-- .../com/gregor/jprototerm/TerminalPane.java | 22 +++++-- .../com/gregor/jprototerm/TerminalWindow.java | 60 ++++++++++++++++++- 7 files changed, 111 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index a8fdeac..900bc35 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,8 @@ editor_command = "vi {file}" [worktree] relative_worktree_path = "./.worktrees" split_regex = "," +# One of: "", "cd", "create_panes", "create_panes_floating". +post_create_action = "" [env.override] ZELLIJ_SESSION_NAME = "" @@ -191,7 +193,9 @@ paste = "CTRL+SHIFT+V" - `Alt+s`: open the active pane scrollback in `$EDITOR` - `Alt+w`: edit one or more worktree names, split by `worktree.split_regex`, then run `git worktree add /` for each name from the previously focused - pane's working directory + pane's working directory. `worktree.post_create_action` can then `cd` the previously active pane + to the last created worktree, create one tiled pane per worktree with `create_panes`, or create + one floating pane per worktree with `create_panes_floating`. - `Alt+y`: enter pane-sync selection mode, commit the selection, or stop an active pane sync - `Space`: toggle the focused pane in the sync set while pane-sync selection mode is active - Once committed, input typed or pasted into any synced pane is mirrored to the other synced panes diff --git a/config.example.toml b/config.example.toml index 98b8f4c..b5313c1 100644 --- a/config.example.toml +++ b/config.example.toml @@ -22,6 +22,8 @@ editor_command = "vi {file}" [worktree] relative_worktree_path = "./.worktrees" split_regex = "," +# One of: "", "cd", "create_panes", "create_panes_floating". +post_create_action = "" [env.override] ZELLIJ_SESSION_NAME = "" diff --git a/src/main/java/com/gregor/jprototerm/AppConfig.java b/src/main/java/com/gregor/jprototerm/AppConfig.java index 882ec5e..a60606a 100644 --- a/src/main/java/com/gregor/jprototerm/AppConfig.java +++ b/src/main/java/com/gregor/jprototerm/AppConfig.java @@ -31,6 +31,7 @@ public record AppConfig( String scrollbackEditorCommand, String worktreeRelativePath, String worktreeSplitRegex, + String worktreePostCreateAction, String closeSignal, Map envOverride, Map keybindings @@ -79,6 +80,7 @@ public record AppConfig( stringValue(document, "scrollback.editor_command", defaults.scrollbackEditorCommand), stringValue(document, "worktree.relative_worktree_path", defaults.worktreeRelativePath), stringValue(document, "worktree.split_regex", defaults.worktreeSplitRegex), + stringValue(document, "worktree.post_create_action", defaults.worktreePostCreateAction), closeSignalValue(document, defaults.closeSignal), envOverride(document, defaults.envOverride), keybindings(document, defaults) @@ -103,6 +105,7 @@ public record AppConfig( defaultScrollbackEditorCommand(), "./.worktrees", ",", + "", "SIGTERM", Map.of(), Map.ofEntries( @@ -142,6 +145,7 @@ public record AppConfig( scrollbackEditorCommand, worktreeRelativePath, worktreeSplitRegex, + worktreePostCreateAction, closeSignal, envOverride, keybindings @@ -246,7 +250,8 @@ public record AppConfig( builder.append("editor_command = ").append(quoted(scrollbackEditorCommand)).append("\n\n"); builder.append("[worktree]\n"); builder.append("relative_worktree_path = ").append(quoted(worktreeRelativePath)).append('\n'); - builder.append("split_regex = ").append(quoted(worktreeSplitRegex)).append("\n\n"); + builder.append("split_regex = ").append(quoted(worktreeSplitRegex)).append('\n'); + builder.append("post_create_action = ").append(quoted(worktreePostCreateAction)).append("\n\n"); builder.append("[env.override]\n"); for (Map.Entry entry : envOverride.entrySet()) { builder.append(entry.getKey()).append(" = ").append(quoted(entry.getValue())).append('\n'); diff --git a/src/main/java/com/gregor/jprototerm/Compositor.java b/src/main/java/com/gregor/jprototerm/Compositor.java index f53ef55..deaf644 100644 --- a/src/main/java/com/gregor/jprototerm/Compositor.java +++ b/src/main/java/com/gregor/jprototerm/Compositor.java @@ -196,6 +196,14 @@ public final class Compositor { mutateCurrentTab(() -> currentTab().createPane()); } + public void createTiledPane(String workingDirectory) { + mutateCurrentTab(() -> currentTab().createTiledPane(workingDirectory)); + } + + public void createFloatingPaneInDirectory(String workingDirectory) { + mutateCurrentTab(() -> currentTab().createFloatingPaneInDirectory(workingDirectory)); + } + /** * Opens a floating pane running {@code command} directly (auto-closing when it exits), makes it * active, and returns it (null when no tab exists). diff --git a/src/main/java/com/gregor/jprototerm/Tab.java b/src/main/java/com/gregor/jprototerm/Tab.java index 3824d6e..639402f 100644 --- a/src/main/java/com/gregor/jprototerm/Tab.java +++ b/src/main/java/com/gregor/jprototerm/Tab.java @@ -237,12 +237,20 @@ final class Tab implements AutoCloseable { if (floatingVisible) { createFloatingPane(); } else { - TerminalPane pane = openPane(false); - tiled.add(pane); - setActive(pane); + createTiledPane(paneWorkingDirectory()); } } + void createTiledPane(String workingDirectory) { + TerminalPane pane = openPane(false, workingDirectory); + tiled.add(pane); + setActive(pane); + } + + void createFloatingPaneInDirectory(String workingDirectory) { + addFloating(openPane(true, workingDirectory)); + } + void nextFloatingPane() { if (floating.isEmpty()) { createFloatingPane(); @@ -334,7 +342,7 @@ final class Tab implements AutoCloseable { } private void createFloatingPane() { - addFloating(openPane(true)); + addFloating(openPane(true, paneWorkingDirectory())); } /** @@ -386,9 +394,13 @@ final class Tab implements AutoCloseable { } private TerminalPane openPane(boolean asFloating) { + return openPane(asFloating, paneWorkingDirectory()); + } + + private TerminalPane openPane(boolean asFloating, String workingDirectory) { double[] size = paneSize(asFloating); return register(TerminalPane.create( - config, metrics, this::markContentChanged, size[0], size[1], paneWorkingDirectory())); + config, metrics, this::markContentChanged, size[0], size[1], workingDirectory)); } private double[] paneSize(boolean asFloating) { diff --git a/src/main/java/com/gregor/jprototerm/TerminalPane.java b/src/main/java/com/gregor/jprototerm/TerminalPane.java index 32ebbd6..ab4aa63 100644 --- a/src/main/java/com/gregor/jprototerm/TerminalPane.java +++ b/src/main/java/com/gregor/jprototerm/TerminalPane.java @@ -16,6 +16,8 @@ import javafx.application.Platform; import javafx.scene.canvas.GraphicsContext; import javafx.scene.shape.Shape; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Supplier; @@ -42,8 +44,8 @@ public final class TerminalPane implements AutoCloseable, RenderTarget { private RenderStateSnapshot cachedSnapshot; private volatile ShellSession session; // Run once (on the FX thread) when this pane's process exits on its own, so the owning tab can - // remove it. Set by the Tab that creates the pane; null until then. - private Runnable onExit; + // remove it and command panes can run follow-up actions. + private final List onExitHandlers = new ArrayList<>(); private boolean exited; // Clip region for rendering (rect minus the panes covering this one), set at layout time; // null means clip to the plain bounds. See RenderTarget#clip(). @@ -125,12 +127,20 @@ public final class TerminalPane implements AutoCloseable, RenderTarget { /** Sets the callback run when this pane's process exits on its own (see {@link #handleSessionExit}). */ public void setOnExit(Runnable onExit) { - this.onExit = onExit; + onExitHandlers.clear(); + addOnExit(onExit); + } + + /** Adds a callback run when this pane's process exits on its own (see {@link #handleSessionExit}). */ + public void addOnExit(Runnable onExit) { + if (onExit != null) { + onExitHandlers.add(onExit); + } } /** * Called from the shell reader thread when the pty stream ends without us closing it (the - * process exited). Hops to the FX thread and fires {@link #onExit} once, so tab/compositor + * process exited). Hops to the FX thread and fires the exit handlers once, so tab/compositor * mutation happens on the thread that owns the layout. */ void handleSessionExit() { @@ -139,8 +149,8 @@ public final class TerminalPane implements AutoCloseable, RenderTarget { return; } exited = true; - if (onExit != null) { - onExit.run(); + for (Runnable handler : List.copyOf(onExitHandlers)) { + handler.run(); } }); } diff --git a/src/main/java/com/gregor/jprototerm/TerminalWindow.java b/src/main/java/com/gregor/jprototerm/TerminalWindow.java index 678cab7..f799420 100644 --- a/src/main/java/com/gregor/jprototerm/TerminalWindow.java +++ b/src/main/java/com/gregor/jprototerm/TerminalWindow.java @@ -20,8 +20,10 @@ import javafx.stage.Stage; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; /** @@ -287,9 +289,15 @@ final class TerminalWindow { } try { Path file = Files.createTempFile("jprototerm-worktree-", ".txt"); + Path createdFile = Files.createTempFile("jprototerm-worktree-created-", ".txt"); Files.writeString(file, ""); - compositor.openFloatingPane(worktreeEditorCommand(file)); + TerminalPane commandPane = compositor.openFloatingPane(worktreeEditorCommand(file, createdFile)); + if (commandPane != null) { + commandPane.addOnExit(() -> runPostWorktreeAction(active, createdFile)); + } else { + Files.deleteIfExists(createdFile); + } } catch (IOException ex) { System.err.println("Could not create worktree from editor input: " + ex.getMessage()); } @@ -307,8 +315,9 @@ final class TerminalWindow { return command + " " + quotedFile; } - private String worktreeEditorCommand(Path file) { + private String worktreeEditorCommand(Path file, Path createdFile) { String quotedFile = shellQuote(file.toString()); + String quotedCreatedFile = shellQuote(createdFile.toString()); String relativePath = config.worktreeRelativePath(); if (relativePath == null || relativePath.isBlank()) { relativePath = "./.worktrees"; @@ -331,7 +340,12 @@ final class TerminalWindow { + " " + quotedFile + " > \"$names_file\"; then" + " git_status=0" + "; while IFS= read -r name; do" - + " git worktree add " + shellQuote(relativePath) + "/\"$name\"" + + " worktree_path=" + shellQuote(relativePath) + "/\"$name\"" + + "; git worktree add \"$worktree_path\"" + + " || { git_status=$?; break; }" + + "; created_path=$(cd \"$worktree_path\" && pwd -P)" + + " || { git_status=$?; break; }" + + "; printf '%s\\n' \"$created_path\" >> " + quotedCreatedFile + " || { git_status=$?; break; }" + "; done < \"$names_file\"" + "; else git_status=$?" @@ -344,6 +358,46 @@ final class TerminalWindow { + "; exit \"$git_status\""; } + private void runPostWorktreeAction(TerminalPane lastActivePane, Path createdFile) { + List worktreePaths = readCreatedWorktreePaths(createdFile); + try { + Files.deleteIfExists(createdFile); + } catch (IOException ex) { + System.err.println("Could not remove worktree result file " + createdFile + ": " + ex.getMessage()); + } + if (worktreePaths.isEmpty()) { + return; + } + + String action = config.worktreePostCreateAction(); + if (action == null || action.isBlank()) { + return; + } + + switch (action.trim().toLowerCase(Locale.ROOT)) { + case "cd" -> lastActivePane.send("cd " + shellQuote(worktreePaths.get(worktreePaths.size() - 1)) + "\r"); + case "create_panes" -> worktreePaths.forEach(compositor::createTiledPane); + case "create_panes_floating" -> worktreePaths.forEach(compositor::createFloatingPaneInDirectory); + default -> System.err.println("Unknown worktree.post_create_action '" + action + "'"); + } + } + + private List readCreatedWorktreePaths(Path createdFile) { + try { + List paths = new ArrayList<>(); + for (String line : Files.readAllLines(createdFile)) { + String path = line.trim(); + if (!path.isEmpty()) { + paths.add(path); + } + } + return paths; + } catch (IOException ex) { + System.err.println("Could not read created worktree paths from " + createdFile + ": " + ex.getMessage()); + return List.of(); + } + } + private static String shellQuote(String value) { return "'" + value.replace("'", "'\"'\"'") + "'"; } From 27b7ba69041908884b6164d39b25f6f229364213 Mon Sep 17 00:00:00 2001 From: Gregor Lohaus Date: Fri, 19 Jun 2026 16:15:10 +0200 Subject: [PATCH 3/5] post create none action --- README.md | 10 +++++----- config.example.toml | 4 ++-- src/main/java/com/gregor/jprototerm/AppConfig.java | 2 +- .../java/com/gregor/jprototerm/TerminalWindow.java | 1 + 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 900bc35..980a9e8 100644 --- a/README.md +++ b/README.md @@ -152,8 +152,8 @@ editor_command = "vi {file}" [worktree] relative_worktree_path = "./.worktrees" split_regex = "," -# One of: "", "cd", "create_panes", "create_panes_floating". -post_create_action = "" +# One of: "none", "cd", "create_panes", "create_panes_floating". +post_create_action = "none" [env.override] ZELLIJ_SESSION_NAME = "" @@ -193,9 +193,9 @@ paste = "CTRL+SHIFT+V" - `Alt+s`: open the active pane scrollback in `$EDITOR` - `Alt+w`: edit one or more worktree names, split by `worktree.split_regex`, then run `git worktree add /` for each name from the previously focused - pane's working directory. `worktree.post_create_action` can then `cd` the previously active pane - to the last created worktree, create one tiled pane per worktree with `create_panes`, or create - one floating pane per worktree with `create_panes_floating`. + pane's working directory. `worktree.post_create_action` can then do nothing with `none`, `cd` the + previously active pane to the last created worktree, create one tiled pane per worktree with + `create_panes`, or create one floating pane per worktree with `create_panes_floating`. - `Alt+y`: enter pane-sync selection mode, commit the selection, or stop an active pane sync - `Space`: toggle the focused pane in the sync set while pane-sync selection mode is active - Once committed, input typed or pasted into any synced pane is mirrored to the other synced panes diff --git a/config.example.toml b/config.example.toml index b5313c1..72c2e90 100644 --- a/config.example.toml +++ b/config.example.toml @@ -22,8 +22,8 @@ editor_command = "vi {file}" [worktree] relative_worktree_path = "./.worktrees" split_regex = "," -# One of: "", "cd", "create_panes", "create_panes_floating". -post_create_action = "" +# One of: "none", "cd", "create_panes", "create_panes_floating". +post_create_action = "none" [env.override] ZELLIJ_SESSION_NAME = "" diff --git a/src/main/java/com/gregor/jprototerm/AppConfig.java b/src/main/java/com/gregor/jprototerm/AppConfig.java index a60606a..93163c2 100644 --- a/src/main/java/com/gregor/jprototerm/AppConfig.java +++ b/src/main/java/com/gregor/jprototerm/AppConfig.java @@ -105,7 +105,7 @@ public record AppConfig( defaultScrollbackEditorCommand(), "./.worktrees", ",", - "", + "none", "SIGTERM", Map.of(), Map.ofEntries( diff --git a/src/main/java/com/gregor/jprototerm/TerminalWindow.java b/src/main/java/com/gregor/jprototerm/TerminalWindow.java index f799420..9836079 100644 --- a/src/main/java/com/gregor/jprototerm/TerminalWindow.java +++ b/src/main/java/com/gregor/jprototerm/TerminalWindow.java @@ -375,6 +375,7 @@ final class TerminalWindow { } switch (action.trim().toLowerCase(Locale.ROOT)) { + case "none" -> { } case "cd" -> lastActivePane.send("cd " + shellQuote(worktreePaths.get(worktreePaths.size() - 1)) + "\r"); case "create_panes" -> worktreePaths.forEach(compositor::createTiledPane); case "create_panes_floating" -> worktreePaths.forEach(compositor::createFloatingPaneInDirectory); From 5a50ce62f3de839ed95c2203412c42c073232dac Mon Sep 17 00:00:00 2001 From: Gregor Lohaus Date: Fri, 19 Jun 2026 16:41:31 +0200 Subject: [PATCH 4/5] post create run commands in panes --- README.md | 3 +++ config.example.toml | 1 + .../java/com/gregor/jprototerm/AppConfig.java | 7 ++++++- .../com/gregor/jprototerm/Compositor.java | 18 ++++++++++++++---- src/main/java/com/gregor/jprototerm/Tab.java | 7 ++++--- .../com/gregor/jprototerm/TerminalWindow.java | 19 +++++++++++++++++-- 6 files changed, 45 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 980a9e8..9729f1b 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,7 @@ relative_worktree_path = "./.worktrees" split_regex = "," # One of: "none", "cd", "create_panes", "create_panes_floating". post_create_action = "none" +commands = [] [env.override] ZELLIJ_SESSION_NAME = "" @@ -196,6 +197,8 @@ paste = "CTRL+SHIFT+V" pane's working directory. `worktree.post_create_action` can then do nothing with `none`, `cd` the previously active pane to the last created worktree, create one tiled pane per worktree with `create_panes`, or create one floating pane per worktree with `create_panes_floating`. + `worktree.commands`, for example `["npm install", "git status"]`, can run commands in created + panes; commands are assigned in order and repeat when fewer commands than panes are created. - `Alt+y`: enter pane-sync selection mode, commit the selection, or stop an active pane sync - `Space`: toggle the focused pane in the sync set while pane-sync selection mode is active - Once committed, input typed or pasted into any synced pane is mirrored to the other synced panes diff --git a/config.example.toml b/config.example.toml index 72c2e90..e620901 100644 --- a/config.example.toml +++ b/config.example.toml @@ -24,6 +24,7 @@ relative_worktree_path = "./.worktrees" split_regex = "," # One of: "none", "cd", "create_panes", "create_panes_floating". post_create_action = "none" +commands = [] [env.override] ZELLIJ_SESSION_NAME = "" diff --git a/src/main/java/com/gregor/jprototerm/AppConfig.java b/src/main/java/com/gregor/jprototerm/AppConfig.java index 93163c2..e8c634d 100644 --- a/src/main/java/com/gregor/jprototerm/AppConfig.java +++ b/src/main/java/com/gregor/jprototerm/AppConfig.java @@ -32,6 +32,7 @@ public record AppConfig( String worktreeRelativePath, String worktreeSplitRegex, String worktreePostCreateAction, + List worktreeCommands, String closeSignal, Map envOverride, Map keybindings @@ -81,6 +82,7 @@ public record AppConfig( stringValue(document, "worktree.relative_worktree_path", defaults.worktreeRelativePath), stringValue(document, "worktree.split_regex", defaults.worktreeSplitRegex), stringValue(document, "worktree.post_create_action", defaults.worktreePostCreateAction), + stringListValue(document, "worktree.commands", defaults.worktreeCommands), closeSignalValue(document, defaults.closeSignal), envOverride(document, defaults.envOverride), keybindings(document, defaults) @@ -106,6 +108,7 @@ public record AppConfig( "./.worktrees", ",", "none", + List.of(), "SIGTERM", Map.of(), Map.ofEntries( @@ -146,6 +149,7 @@ public record AppConfig( worktreeRelativePath, worktreeSplitRegex, worktreePostCreateAction, + worktreeCommands, closeSignal, envOverride, keybindings @@ -251,7 +255,8 @@ public record AppConfig( builder.append("[worktree]\n"); builder.append("relative_worktree_path = ").append(quoted(worktreeRelativePath)).append('\n'); builder.append("split_regex = ").append(quoted(worktreeSplitRegex)).append('\n'); - builder.append("post_create_action = ").append(quoted(worktreePostCreateAction)).append("\n\n"); + builder.append("post_create_action = ").append(quoted(worktreePostCreateAction)).append('\n'); + builder.append("commands = ").append(quotedList(worktreeCommands)).append("\n\n"); builder.append("[env.override]\n"); for (Map.Entry entry : envOverride.entrySet()) { builder.append(entry.getKey()).append(" = ").append(quoted(entry.getValue())).append('\n'); diff --git a/src/main/java/com/gregor/jprototerm/Compositor.java b/src/main/java/com/gregor/jprototerm/Compositor.java index deaf644..15a4f67 100644 --- a/src/main/java/com/gregor/jprototerm/Compositor.java +++ b/src/main/java/com/gregor/jprototerm/Compositor.java @@ -196,12 +196,22 @@ public final class Compositor { mutateCurrentTab(() -> currentTab().createPane()); } - public void createTiledPane(String workingDirectory) { - mutateCurrentTab(() -> currentTab().createTiledPane(workingDirectory)); + public TerminalPane createTiledPane(String workingDirectory) { + if (isEmpty()) { + return null; + } + TerminalPane pane = currentTab().createTiledPane(workingDirectory); + layoutVersion++; + return pane; } - public void createFloatingPaneInDirectory(String workingDirectory) { - mutateCurrentTab(() -> currentTab().createFloatingPaneInDirectory(workingDirectory)); + public TerminalPane createFloatingPaneInDirectory(String workingDirectory) { + if (isEmpty()) { + return null; + } + TerminalPane pane = currentTab().createFloatingPaneInDirectory(workingDirectory); + layoutVersion++; + return pane; } /** diff --git a/src/main/java/com/gregor/jprototerm/Tab.java b/src/main/java/com/gregor/jprototerm/Tab.java index 639402f..b70546c 100644 --- a/src/main/java/com/gregor/jprototerm/Tab.java +++ b/src/main/java/com/gregor/jprototerm/Tab.java @@ -241,14 +241,15 @@ final class Tab implements AutoCloseable { } } - void createTiledPane(String workingDirectory) { + TerminalPane createTiledPane(String workingDirectory) { TerminalPane pane = openPane(false, workingDirectory); tiled.add(pane); setActive(pane); + return pane; } - void createFloatingPaneInDirectory(String workingDirectory) { - addFloating(openPane(true, workingDirectory)); + TerminalPane createFloatingPaneInDirectory(String workingDirectory) { + return addFloating(openPane(true, workingDirectory)); } void nextFloatingPane() { diff --git a/src/main/java/com/gregor/jprototerm/TerminalWindow.java b/src/main/java/com/gregor/jprototerm/TerminalWindow.java index 9836079..804f490 100644 --- a/src/main/java/com/gregor/jprototerm/TerminalWindow.java +++ b/src/main/java/com/gregor/jprototerm/TerminalWindow.java @@ -377,12 +377,27 @@ final class TerminalWindow { switch (action.trim().toLowerCase(Locale.ROOT)) { case "none" -> { } case "cd" -> lastActivePane.send("cd " + shellQuote(worktreePaths.get(worktreePaths.size() - 1)) + "\r"); - case "create_panes" -> worktreePaths.forEach(compositor::createTiledPane); - case "create_panes_floating" -> worktreePaths.forEach(compositor::createFloatingPaneInDirectory); + case "create_panes" -> createWorktreePanes(worktreePaths, false); + case "create_panes_floating" -> createWorktreePanes(worktreePaths, true); default -> System.err.println("Unknown worktree.post_create_action '" + action + "'"); } } + private void createWorktreePanes(List worktreePaths, boolean floating) { + List commands = config.worktreeCommands(); + for (int i = 0; i < worktreePaths.size(); i++) { + TerminalPane pane = floating + ? compositor.createFloatingPaneInDirectory(worktreePaths.get(i)) + : compositor.createTiledPane(worktreePaths.get(i)); + if (pane != null && !commands.isEmpty()) { + String command = commands.get(i % commands.size()); + if (command != null && !command.isBlank()) { + pane.send(command + "\r"); + } + } + } + } + private List readCreatedWorktreePaths(Path createdFile) { try { List paths = new ArrayList<>(); From cff4fba81c3fc5fe1377edb779ad33cd190104c9 Mon Sep 17 00:00:00 2001 From: Gregor Lohaus Date: Fri, 19 Jun 2026 16:44:16 +0200 Subject: [PATCH 5/5] post create pane sync --- README.md | 2 ++ config.example.toml | 1 + src/main/java/com/gregor/jprototerm/AppConfig.java | 7 ++++++- src/main/java/com/gregor/jprototerm/Compositor.java | 13 +++++++++++++ .../java/com/gregor/jprototerm/TerminalWindow.java | 7 +++++++ 5 files changed, 29 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9729f1b..0ba70e8 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,7 @@ split_regex = "," # One of: "none", "cd", "create_panes", "create_panes_floating". post_create_action = "none" commands = [] +sync_panes = false [env.override] ZELLIJ_SESSION_NAME = "" @@ -199,6 +200,7 @@ paste = "CTRL+SHIFT+V" `create_panes`, or create one floating pane per worktree with `create_panes_floating`. `worktree.commands`, for example `["npm install", "git status"]`, can run commands in created panes; commands are assigned in order and repeat when fewer commands than panes are created. + `worktree.sync_panes = true` syncs those created panes after the configured commands are sent. - `Alt+y`: enter pane-sync selection mode, commit the selection, or stop an active pane sync - `Space`: toggle the focused pane in the sync set while pane-sync selection mode is active - Once committed, input typed or pasted into any synced pane is mirrored to the other synced panes diff --git a/config.example.toml b/config.example.toml index e620901..0811f36 100644 --- a/config.example.toml +++ b/config.example.toml @@ -25,6 +25,7 @@ split_regex = "," # One of: "none", "cd", "create_panes", "create_panes_floating". post_create_action = "none" commands = [] +sync_panes = false [env.override] ZELLIJ_SESSION_NAME = "" diff --git a/src/main/java/com/gregor/jprototerm/AppConfig.java b/src/main/java/com/gregor/jprototerm/AppConfig.java index e8c634d..2c70f5f 100644 --- a/src/main/java/com/gregor/jprototerm/AppConfig.java +++ b/src/main/java/com/gregor/jprototerm/AppConfig.java @@ -33,6 +33,7 @@ public record AppConfig( String worktreeSplitRegex, String worktreePostCreateAction, List worktreeCommands, + boolean worktreeSyncPanes, String closeSignal, Map envOverride, Map keybindings @@ -83,6 +84,7 @@ public record AppConfig( stringValue(document, "worktree.split_regex", defaults.worktreeSplitRegex), stringValue(document, "worktree.post_create_action", defaults.worktreePostCreateAction), stringListValue(document, "worktree.commands", defaults.worktreeCommands), + booleanValue(document, "worktree.sync_panes", defaults.worktreeSyncPanes), closeSignalValue(document, defaults.closeSignal), envOverride(document, defaults.envOverride), keybindings(document, defaults) @@ -109,6 +111,7 @@ public record AppConfig( ",", "none", List.of(), + false, "SIGTERM", Map.of(), Map.ofEntries( @@ -150,6 +153,7 @@ public record AppConfig( worktreeSplitRegex, worktreePostCreateAction, worktreeCommands, + worktreeSyncPanes, closeSignal, envOverride, keybindings @@ -256,7 +260,8 @@ public record AppConfig( builder.append("relative_worktree_path = ").append(quoted(worktreeRelativePath)).append('\n'); builder.append("split_regex = ").append(quoted(worktreeSplitRegex)).append('\n'); builder.append("post_create_action = ").append(quoted(worktreePostCreateAction)).append('\n'); - builder.append("commands = ").append(quotedList(worktreeCommands)).append("\n\n"); + builder.append("commands = ").append(quotedList(worktreeCommands)).append('\n'); + builder.append("sync_panes = ").append(worktreeSyncPanes).append("\n\n"); builder.append("[env.override]\n"); for (Map.Entry entry : envOverride.entrySet()) { builder.append(entry.getKey()).append(" = ").append(quoted(entry.getValue())).append('\n'); diff --git a/src/main/java/com/gregor/jprototerm/Compositor.java b/src/main/java/com/gregor/jprototerm/Compositor.java index 15a4f67..c74b683 100644 --- a/src/main/java/com/gregor/jprototerm/Compositor.java +++ b/src/main/java/com/gregor/jprototerm/Compositor.java @@ -188,6 +188,19 @@ public final class Compositor { .toList(); } + public void syncPanes(List panes) { + paneSyncSelectMode = false; + paneSyncSelection.clear(); + paneSyncPanes.clear(); + for (TerminalPane pane : panes) { + if (pane != null) { + paneSyncPanes.add(pane); + } + } + prunePaneSyncState(); + layoutVersion++; + } + public void toggleFloating() { mutateCurrentTab(() -> currentTab().toggleFloating()); } diff --git a/src/main/java/com/gregor/jprototerm/TerminalWindow.java b/src/main/java/com/gregor/jprototerm/TerminalWindow.java index 804f490..f72356e 100644 --- a/src/main/java/com/gregor/jprototerm/TerminalWindow.java +++ b/src/main/java/com/gregor/jprototerm/TerminalWindow.java @@ -385,10 +385,14 @@ final class TerminalWindow { private void createWorktreePanes(List worktreePaths, boolean floating) { List commands = config.worktreeCommands(); + List createdPanes = new ArrayList<>(); for (int i = 0; i < worktreePaths.size(); i++) { TerminalPane pane = floating ? compositor.createFloatingPaneInDirectory(worktreePaths.get(i)) : compositor.createTiledPane(worktreePaths.get(i)); + if (pane != null) { + createdPanes.add(pane); + } if (pane != null && !commands.isEmpty()) { String command = commands.get(i % commands.size()); if (command != null && !command.isBlank()) { @@ -396,6 +400,9 @@ final class TerminalWindow { } } } + if (config.worktreeSyncPanes() && !createdPanes.isEmpty()) { + compositor.syncPanes(createdPanes); + } } private List readCreatedWorktreePaths(Path createdFile) {