post worktree creation actions
This commit is contained in:
@@ -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 <relative_worktree_path>/<name>` 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
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -31,6 +31,7 @@ public record AppConfig(
|
||||
String scrollbackEditorCommand,
|
||||
String worktreeRelativePath,
|
||||
String worktreeSplitRegex,
|
||||
String worktreePostCreateAction,
|
||||
String closeSignal,
|
||||
Map<String, String> envOverride,
|
||||
Map<String, KeyBinding> 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<String, String> entry : envOverride.entrySet()) {
|
||||
builder.append(entry.getKey()).append(" = ").append(quoted(entry.getValue())).append('\n');
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -237,10 +237,18 @@ final class Tab implements AutoCloseable {
|
||||
if (floatingVisible) {
|
||||
createFloatingPane();
|
||||
} else {
|
||||
TerminalPane pane = openPane(false);
|
||||
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() {
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<Runnable> 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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<String> 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<String> readCreatedWorktreePaths(Path createdFile) {
|
||||
try {
|
||||
List<String> 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("'", "'\"'\"'") + "'";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user