From 77fb5e7e11518730ef898bb6ef6c626bf81f2e82 Mon Sep 17 00:00:00 2001 From: Gregor Lohaus Date: Fri, 19 Jun 2026 14:24:35 +0200 Subject: [PATCH 1/3] add worktree shortcut --- README.md | 6 +++ config.example.toml | 10 ++++- .../java/com/gregor/jprototerm/AppConfig.java | 8 ++++ .../com/gregor/jprototerm/TerminalWindow.java | 41 ++++++++++++++++++- 4 files changed, 62 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 23dfc83..5eda1e2 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,9 @@ enabled = true [scrollback] editor_command = "vi {file}" +[worktree] +relative_worktree_path = "./.worktrees" + [env.override] ZELLIJ_SESSION_NAME = "" @@ -163,6 +166,7 @@ previous_tab = "ALT+SHIFT+H" next_tab = "ALT+SHIFT+L" open_font_selector = "ALT+T" open_scrollback = "ALT+S" +open_worktree = "ALT+W" ``` ## Defaults @@ -178,6 +182,8 @@ open_scrollback = "ALT+S" - `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 - Font default: `JetBrainsMono Nerd Font` - Kitty graphics protocol parsing is enabled by default diff --git a/config.example.toml b/config.example.toml index 9eab560..ab18b4b 100644 --- a/config.example.toml +++ b/config.example.toml @@ -19,6 +19,9 @@ enabled = true [scrollback] editor_command = "vi {file}" +[worktree] +relative_worktree_path = "./.worktrees" + [env.override] ZELLIJ_SESSION_NAME = "" @@ -28,9 +31,14 @@ navigate_down = "ALT+J" navigate_up = "ALT+K" navigate_right = "ALT+L" toggle_floating = "ALT+F" -new_floating = "ALT+SHIFT+F" +new_pane = "ALT+N" next_floating = "ALT+F12" +promote_floating = "ALT+P" close_pane = "ALT+X" +new_tab = "ALT+A" +previous_tab = "ALT+SHIFT+H" +next_tab = "ALT+SHIFT+L" open_font_selector = "ALT+T" open_scrollback = "ALT+S" +open_worktree = "ALT+W" paste = "CTRL+SHIFT+V" diff --git a/src/main/java/com/gregor/jprototerm/AppConfig.java b/src/main/java/com/gregor/jprototerm/AppConfig.java index 7453001..0400cdc 100644 --- a/src/main/java/com/gregor/jprototerm/AppConfig.java +++ b/src/main/java/com/gregor/jprototerm/AppConfig.java @@ -29,6 +29,7 @@ public record AppConfig( double windowHeight, boolean kittyGraphics, String scrollbackEditorCommand, + String worktreeRelativePath, String closeSignal, Map envOverride, Map keybindings @@ -48,6 +49,7 @@ public record AppConfig( "next_tab", "open_font_selector", "open_scrollback", + "open_worktree", "paste" ); @@ -72,6 +74,7 @@ public record AppConfig( doubleValue(document, "window.height", defaults.windowHeight), booleanValue(document, "kitty_graphics.enabled", defaults.kittyGraphics), stringValue(document, "scrollback.editor_command", defaults.scrollbackEditorCommand), + stringValue(document, "worktree.relative_worktree_path", defaults.worktreeRelativePath), closeSignalValue(document, defaults.closeSignal), envOverride(document, defaults.envOverride), keybindings(document, defaults) @@ -94,6 +97,7 @@ public record AppConfig( 760.0, true, defaultScrollbackEditorCommand(), + "./.worktrees", "SIGTERM", Map.of(), Map.ofEntries( @@ -111,6 +115,7 @@ public record AppConfig( Map.entry("next_tab", KeyBinding.parse("ALT+SHIFT+L")), Map.entry("open_font_selector", KeyBinding.parse("ALT+T")), Map.entry("open_scrollback", KeyBinding.parse("ALT+S")), + Map.entry("open_worktree", KeyBinding.parse("ALT+W")), Map.entry("paste", KeyBinding.parse("CTRL+SHIFT+V")) ) ); @@ -128,6 +133,7 @@ public record AppConfig( windowHeight, kittyGraphics, scrollbackEditorCommand, + worktreeRelativePath, closeSignal, envOverride, keybindings @@ -230,6 +236,8 @@ public record AppConfig( builder.append("enabled = ").append(kittyGraphics).append("\n\n"); 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("[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 4c38748..7401a42 100644 --- a/src/main/java/com/gregor/jprototerm/TerminalWindow.java +++ b/src/main/java/com/gregor/jprototerm/TerminalWindow.java @@ -71,6 +71,7 @@ final class TerminalWindow { keyActions.put("next_tab", compositor::nextTab); keyActions.put("open_font_selector", this::openFontSelector); keyActions.put("open_scrollback", this::openScrollbackInEditor); + keyActions.put("open_worktree", this::openWorktreeInEditor); keyActions.put("paste", this::pasteFromClipboard); StackPane root = new StackPane(compositor.canvas(), compositor.imageOverlay()); @@ -230,13 +231,30 @@ final class TerminalWindow { // — no shell startup/rc race — and the pane auto-closes when the editor exits. The // trailing rm removes the file (which holds terminal contents) when the editor exits; // deleteOnExit would leak files for the JVM's whole lifetime in daemon mode. - compositor.openFloatingPane(scrollbackEditorCommand(file) + "; rm -f " + shellQuote(file.toString())); + compositor.openFloatingPane(editorCommand(file) + "; rm -f " + shellQuote(file.toString())); } catch (IOException ex) { System.err.println("Could not open scrollback in editor: " + ex.getMessage()); } } - private String scrollbackEditorCommand(Path file) { + private void openWorktreeInEditor() { + // The floating pane command inherits the active pane's cwd at creation time, so the git + // worktree command runs from the pane that was focused before this shortcut opened. + TerminalPane active = compositor.activePane(); + if (active == null) { + return; + } + try { + Path file = Files.createTempFile("jprototerm-worktree-", ".txt"); + Files.writeString(file, ""); + + compositor.openFloatingPane(worktreeEditorCommand(file)); + } catch (IOException ex) { + System.err.println("Could not open worktree editor: " + ex.getMessage()); + } + } + + private String editorCommand(Path file) { String quotedFile = shellQuote(file.toString()); String command = config.scrollbackEditorCommand(); if (command == null || command.isBlank()) { @@ -248,6 +266,25 @@ final class TerminalWindow { return command + " " + quotedFile; } + private String worktreeEditorCommand(Path file) { + String quotedFile = shellQuote(file.toString()); + String relativePath = config.worktreeRelativePath(); + if (relativePath == null || relativePath.isBlank()) { + relativePath = "./.worktrees"; + } + + return editorCommand(file) + + "; editor_status=$?" + + "; name=$(cat " + quotedFile + ")" + + "; if [ \"$editor_status\" -eq 0 ] && [ -n \"$name\" ]; then" + + " git worktree add " + shellQuote(relativePath) + "/\"$name\"" + + "; git_status=$?" + + "; else git_status=$editor_status" + + "; fi" + + "; rm -f " + quotedFile + + "; exit \"$git_status\""; + } + private static String shellQuote(String value) { return "'" + value.replace("'", "'\"'\"'") + "'"; } From f621366b0cfca2164412d646181553fd42d87ea7 Mon Sep 17 00:00:00 2001 From: Gregor Lohaus Date: Fri, 19 Jun 2026 14:27:57 +0200 Subject: [PATCH 2/3] rename action --- README.md | 2 +- config.example.toml | 2 +- src/main/java/com/gregor/jprototerm/AppConfig.java | 4 ++-- src/main/java/com/gregor/jprototerm/TerminalWindow.java | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5eda1e2..26c6688 100644 --- a/README.md +++ b/README.md @@ -166,7 +166,7 @@ previous_tab = "ALT+SHIFT+H" next_tab = "ALT+SHIFT+L" open_font_selector = "ALT+T" open_scrollback = "ALT+S" -open_worktree = "ALT+W" +create_worktree = "ALT+W" ``` ## Defaults diff --git a/config.example.toml b/config.example.toml index ab18b4b..e81eb8b 100644 --- a/config.example.toml +++ b/config.example.toml @@ -40,5 +40,5 @@ previous_tab = "ALT+SHIFT+H" next_tab = "ALT+SHIFT+L" open_font_selector = "ALT+T" open_scrollback = "ALT+S" -open_worktree = "ALT+W" +create_worktree = "ALT+W" paste = "CTRL+SHIFT+V" diff --git a/src/main/java/com/gregor/jprototerm/AppConfig.java b/src/main/java/com/gregor/jprototerm/AppConfig.java index 0400cdc..cbe7380 100644 --- a/src/main/java/com/gregor/jprototerm/AppConfig.java +++ b/src/main/java/com/gregor/jprototerm/AppConfig.java @@ -49,7 +49,7 @@ public record AppConfig( "next_tab", "open_font_selector", "open_scrollback", - "open_worktree", + "create_worktree", "paste" ); @@ -115,7 +115,7 @@ public record AppConfig( Map.entry("next_tab", KeyBinding.parse("ALT+SHIFT+L")), Map.entry("open_font_selector", KeyBinding.parse("ALT+T")), Map.entry("open_scrollback", KeyBinding.parse("ALT+S")), - Map.entry("open_worktree", KeyBinding.parse("ALT+W")), + Map.entry("create_worktree", KeyBinding.parse("ALT+W")), Map.entry("paste", KeyBinding.parse("CTRL+SHIFT+V")) ) ); diff --git a/src/main/java/com/gregor/jprototerm/TerminalWindow.java b/src/main/java/com/gregor/jprototerm/TerminalWindow.java index 7401a42..1630375 100644 --- a/src/main/java/com/gregor/jprototerm/TerminalWindow.java +++ b/src/main/java/com/gregor/jprototerm/TerminalWindow.java @@ -71,7 +71,7 @@ final class TerminalWindow { keyActions.put("next_tab", compositor::nextTab); keyActions.put("open_font_selector", this::openFontSelector); keyActions.put("open_scrollback", this::openScrollbackInEditor); - keyActions.put("open_worktree", this::openWorktreeInEditor); + keyActions.put("create_worktree", this::createWorktreeInEditor); keyActions.put("paste", this::pasteFromClipboard); StackPane root = new StackPane(compositor.canvas(), compositor.imageOverlay()); @@ -237,7 +237,7 @@ final class TerminalWindow { } } - private void openWorktreeInEditor() { + private void createWorktreeInEditor() { // The floating pane command inherits the active pane's cwd at creation time, so the git // worktree command runs from the pane that was focused before this shortcut opened. TerminalPane active = compositor.activePane(); @@ -250,7 +250,7 @@ final class TerminalWindow { compositor.openFloatingPane(worktreeEditorCommand(file)); } catch (IOException ex) { - System.err.println("Could not open worktree editor: " + ex.getMessage()); + System.err.println("Could not create worktree from editor input: " + ex.getMessage()); } } From 64263ee3fb9bfc9514733dddda26649f9a53a8ab Mon Sep 17 00:00:00 2001 From: Gregor Lohaus Date: Fri, 19 Jun 2026 14:32:52 +0200 Subject: [PATCH 3/3] standalone flag --- README.md | 3 +++ src/main/java/com/gregor/jprototerm/Main.java | 9 +++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 26c6688..c877120 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,9 @@ After that, a bare `jprototerm` connects to the daemon and opens a window in the directory. If no daemon is running, `jprototerm` falls back to a standalone in-process window (today's behavior), so it always works. +For development testing, use `jprototerm --standalone` to skip the daemon even when one is +running. + To start the daemon automatically with your graphical session, enable the bundled **user** service (it's a user service, not a system one, because X11 needs a display — which only exists after you log in): diff --git a/src/main/java/com/gregor/jprototerm/Main.java b/src/main/java/com/gregor/jprototerm/Main.java index e42a1a5..938046b 100644 --- a/src/main/java/com/gregor/jprototerm/Main.java +++ b/src/main/java/com/gregor/jprototerm/Main.java @@ -4,7 +4,8 @@ package com.gregor.jprototerm; * Entry point and mode dispatch. A bare invocation is a thin client: it hands the request to a * running {@link Daemon}, or, if none is reachable, opens a single standalone window in this process * (today's behavior). {@code --daemon} runs the long-lived server that hosts every window in one - * JVM, so client launches skip cold JVM/JavaFX/GL startup. + * JVM, so client launches skip cold JVM/JavaFX/GL startup. {@code --standalone} skips daemon client + * mode and always opens an in-process window, which is useful while testing development builds. */ public final class Main { private Main() { @@ -14,15 +15,19 @@ public final class Main { // Match the renderer order the app was tuned for; honor an explicit override if present. System.setProperty("prism.order", System.getProperty("prism.order", "es2,sw")); + boolean standalone = false; for (String arg : args) { if (arg.equals("--daemon")) { Daemon.run(); return; } + if (arg.equals("--standalone")) { + standalone = true; + } } String workingDirectory = System.getProperty("user.dir"); - if (Daemon.tryClient(workingDirectory)) { + if (!standalone && Daemon.tryClient(workingDirectory)) { return; // a running daemon opened the window } // No daemon reachable: fall back to a standalone window; the JVM exits when it closes.