From 80cd318c1c76e468f2235852bc4b48e81fa723bf Mon Sep 17 00:00:00 2001 From: Gregor Lohaus Date: Thu, 28 May 2026 13:53:44 +0200 Subject: [PATCH] scrollback editor shortcut --- .gradle/9.4.1/checksums/checksums.lock | Bin 39 -> 39 bytes .../executionHistory/executionHistory.bin | Bin 49514 -> 49514 bytes .../executionHistory/executionHistory.lock | Bin 39 -> 39 bytes .gradle/9.4.1/fileHashes/fileHashes.bin | Bin 28197 -> 28197 bytes .gradle/9.4.1/fileHashes/fileHashes.lock | Bin 39 -> 39 bytes .../9.4.1/fileHashes/resourceHashesCache.bin | Bin 21557 -> 21761 bytes .../buildOutputCleanup.lock | Bin 39 -> 39 bytes README.md | 5 +++ config.example.toml | 4 ++ .../java/com/gregor/jprototerm/AppConfig.java | 20 +++++++++- src/main/java/com/gregor/jprototerm/Main.java | 35 ++++++++++++++++++ .../com/gregor/jprototerm/TerminalPane.java | 6 +++ 12 files changed, 68 insertions(+), 2 deletions(-) diff --git a/.gradle/9.4.1/checksums/checksums.lock b/.gradle/9.4.1/checksums/checksums.lock index 161de0638dea2da6618fab1f6837065a65ee5310..c0e6caeee22edc21c3ff693f4897386e816a60bb 100644 GIT binary patch delta 22 dcmY#apCBfv^xx&s_6<=M46KGm=EjB$3;d4qeVOE&|9eD~}Uqtg7Oa-+-~(^M1Vg4EQ^jLcLcvm(>9LV@LLUkGcb z)!eFC`^Syn<<65vMhv0J7cyNppU*rZEW~cMS3Ft#&YoF?mJJPIUauIJPG+nZn>;Uq zZ}Z98hm1nS`?VM3A9tzWbmVVMF-Ox4#sy&c`SqNeZ`YSI3LVTlwf%zn)$^~ORVTlZ zJyguNV)DcWxk<5{le-%a2nbIX_nLgCRP>0EuGI3LqXm32)jALq&K1WV3gXYBS(%*@kE&PgmT zW)N8a;_$V**Jfu|CCsymKBb}oQFS+R@{eXUmL*?jex9t|ni zKeXC_Y5g`sbBLBNO-ziFLsE-!GxHL20uu95VQ%!8a+GhC@}s>^MKjJkdg=0g0nk~` zjDJ~7p5CU*vJ~i&$tT+MLso*Fv^koYK_F;yfcMsK3Fq(Y$TqV(eij1?m5FdfRw$S* zG_w(Hvz=7&3n=9KyF^s`kN+Nhwtk6+r)LBMh1}{`SFKOl@WssB>N@kkJ5ie(d4qeVa~A`HeAn!hl>GeCjLO8ytkk@m)Ur(T^sKV-H1qVNT!GfS+hJAT z*9t~?CYl?yJmB%c5Se^2({=NO%p<}=PFuFt9-rg?_}C5SPXgx|7l7p#)N^jWQ(w*~kP-L(kIz}zFvB-5SeKvu z{BW{;gT!QqM#0HFjRyn@AA5WG{@*(BOZ6q;vg@_glVh6NB*a^PBpejq$dgi}{qveZ z=lS!~_kMh4{4-t8%B(TmYIk+)MX;(@o|Atzt8wk$eC^XS<3omwvnM}n5uR+*BFS|S zEWeBm=sA#3QHwHE=qMlKa-fjJZ?$+prB3rPt_4adPxfz>2216(+Jfn| zt&U*&W2+6AHfS?6KMJ&+|0bhoPfl7yAT;c23{hFS-jTbW`r@F-|4j z7qe#+y%Blx=@3w8YH*{c?1rcFY}l^75;r<11{8AXJ^be7k;jKNw?DQ!y)tCa=EgWV z#>rV7LGsFpb*T@YyX-pfT<7QS5Q()n!A^euf{}$0n4YFjUa&J_azZ!95(BHDk-4!U0|Nj7&kHpG literal 39 qcmZSn@Y~A&WN5%61_;=}%)rpFu7}~l>6>$!eHd5`jm(V=85jT#qYN(q diff --git a/.gradle/9.4.1/fileHashes/fileHashes.bin b/.gradle/9.4.1/fileHashes/fileHashes.bin index d7b6ec41a9364206bdb23fd862fcd0b46a1420d9..c9949a885f1bc95a0575a4f72b99f8033be11130 100644 GIT binary patch delta 453 zcmZ2_hjHm0#tq`03ITiacl6zva-iqtruCe6Qr<8yFwV=%VfSPJ1L=avf(gQty*!hH zTzpsL9Dlc@#k!~TP>OitOR&Q1M?WD7)wcj?J!2rD3B-yZEYSSv$r1Uo09SRUAKGhk zmqdVdq;6;iN&#@Y7CmO1ABSeQe-{gbd>da|XOD4be_EA{> z;_$V**Jfu|CCsymKBb}ovj(DA=kDZi9~T9WDM$HMDL>l#R5atvqn9qYT5-YS>z93RE@g&Vvv;$n?@c+NpJz^1&6y1L p`|cclF#j!>o;z7S7sQ`CIWRX7OmBtqpXciH1H~a$vONY_2>@#Io;m;k delta 453 zcmZ2_hjHm0#tq`03eM$+!s9hJ9uFVEy4 zr&SYHmH3ri;=FKwQ}c`Z2Ox!cy88nKg!6?XHf6S`!FStnzAdxQ~m%dYcd}HN}G;8tuD}8fU*+4pszr zgY>D%yL}=}LuZ2A0tC#SKmyH8%87NU51za1I`CZQ=kE}SwNPs!H+%Ztlmq(Nak6U8 nWH7xuM;}bTh0^l55Po28B3OJYl>a1+m8LnCuzLk0!_;x-Df literal 39 rcmZQJN)8L&J8{xS1~6d$$jrddu+BTP{BpvAnTHry4UNo=4H*~!@}Ucq diff --git a/.gradle/9.4.1/fileHashes/resourceHashesCache.bin b/.gradle/9.4.1/fileHashes/resourceHashesCache.bin index 0c7030bec20e8f7b06504bfb4d0b14821ddd4eec..2b8e79cdf11cfab1209db4db9cb5f6ba826bff4e 100644 GIT binary patch delta 365 zcmdnGg0XQG;|3E6#)QeH5-S9z%rLJ08K}3L0SrPaH(!;IU=ld^*O0fyPD8}=Tsy$2P%zgbphBQrnaF8jUOo3=s)!zUk= zZxASQn4a<3>Yp`K&~&n`LJf{n`uikGs@wI`X%s Kn4{?iNCf~Ed1)g6 delta 58 zcmV-A0LA}-ssXjA0kAX}0a}wa7^t)L7!U%p9UBM%lRX?TvxOX~1CvG|cazm1Y61v> Qu_5>uu`rkqvq4Do3t5>I-2eap diff --git a/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/.gradle/buildOutputCleanup/buildOutputCleanup.lock index f866744d1ae52a795a37c06d116a98ac7d1b8a33..f41612dfa36a5d69e6e935a92a4a3755e91eb43d 100644 GIT binary patch literal 39 scmZSHJzwb8v8RV>7{GujiJ5_+VI7<3(-ZA{J5MmM8XB1!8!|8e01TrG$p8QV literal 39 rcmZSHJzwb8v8RV>7{GujfSG}zVV%d_)vOwk9g7)Q4UNo=4H*~!2>T0K diff --git a/README.md b/README.md index a9269bb..0fb3853 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,9 @@ height = 760 [kitty_graphics] enabled = true +[scrollback] +editor_command = "vi {file}" + [keybindings] navigate_left = "ALT+H" navigate_down = "ALT+J" @@ -63,6 +66,7 @@ new_floating = "ALT+SHIFT+F" next_floating = "ALT+F12" close_pane = "ALT+X" open_font_selector = "ALT+T" +open_scrollback = "ALT+S" ``` ## Defaults @@ -73,5 +77,6 @@ open_font_selector = "ALT+T" - `Alt+F12`: cycle floating panes - `Alt+x`: close the active floating pane - `Alt+t`: open the font selector +- `Alt+s`: open the active pane scrollback in `$EDITOR` - Font default: `JetBrainsMono Nerd Font` - Kitty graphics protocol parsing is enabled by default diff --git a/config.example.toml b/config.example.toml index 4f262c9..8e172df 100644 --- a/config.example.toml +++ b/config.example.toml @@ -13,6 +13,9 @@ height = 760 [kitty_graphics] enabled = true +[scrollback] +editor_command = "vi {file}" + [keybindings] navigate_left = "ALT+H" navigate_down = "ALT+J" @@ -23,3 +26,4 @@ new_floating = "ALT+SHIFT+F" next_floating = "ALT+F12" close_pane = "ALT+X" open_font_selector = "ALT+T" +open_scrollback = "ALT+S" diff --git a/src/main/java/com/gregor/jprototerm/AppConfig.java b/src/main/java/com/gregor/jprototerm/AppConfig.java index af6f1e1..745a494 100644 --- a/src/main/java/com/gregor/jprototerm/AppConfig.java +++ b/src/main/java/com/gregor/jprototerm/AppConfig.java @@ -25,6 +25,7 @@ public record AppConfig( double windowWidth, double windowHeight, boolean kittyGraphics, + String scrollbackEditorCommand, Map keybindings ) { private static final List KEYBINDING_KEYS = List.of( @@ -36,7 +37,8 @@ public record AppConfig( "new_floating", "next_floating", "close_pane", - "open_font_selector" + "open_font_selector", + "open_scrollback" ); public static AppConfig load() { @@ -59,6 +61,7 @@ public record AppConfig( doubleValue(document, "window.width", defaults.windowWidth), doubleValue(document, "window.height", defaults.windowHeight), booleanValue(document, "kitty_graphics.enabled", defaults.kittyGraphics), + stringValue(document, "scrollback.editor_command", defaults.scrollbackEditorCommand), keybindings(document, defaults) ); } catch (TomlException ex) { @@ -78,6 +81,7 @@ public record AppConfig( 1200.0, 760.0, true, + defaultScrollbackEditorCommand(), Map.of( "navigate_left", KeyBinding.parse("ALT+H"), "navigate_down", KeyBinding.parse("ALT+J"), @@ -87,7 +91,8 @@ public record AppConfig( "new_floating", KeyBinding.parse("ALT+SHIFT+F"), "next_floating", KeyBinding.parse("ALT+F12"), "close_pane", KeyBinding.parse("ALT+X"), - "open_font_selector", KeyBinding.parse("ALT+T") + "open_font_selector", KeyBinding.parse("ALT+T"), + "open_scrollback", KeyBinding.parse("ALT+S") ) ); } @@ -103,6 +108,7 @@ public record AppConfig( windowWidth, windowHeight, kittyGraphics, + scrollbackEditorCommand, keybindings ); } @@ -123,6 +129,14 @@ public record AppConfig( return "/bin/bash"; } + private static String defaultScrollbackEditorCommand() { + String editor = System.getenv("EDITOR"); + if (editor == null || editor.isBlank()) { + editor = "vi"; + } + return editor.trim() + " {file}"; + } + private static Map keybindings(TomlTable table, AppConfig defaults) { Map parsed = new LinkedHashMap<>(); for (String key : KEYBINDING_KEYS) { @@ -167,6 +181,8 @@ public record AppConfig( builder.append("height = ").append(trimDouble(windowHeight)).append("\n\n"); builder.append("[kitty_graphics]\n"); builder.append("enabled = ").append(kittyGraphics).append("\n\n"); + builder.append("[scrollback]\n"); + builder.append("editor_command = ").append(quoted(scrollbackEditorCommand)).append("\n\n"); builder.append("[keybindings]\n"); for (String key : KEYBINDING_KEYS) { KeyBinding binding = keybindings.get(key); diff --git a/src/main/java/com/gregor/jprototerm/Main.java b/src/main/java/com/gregor/jprototerm/Main.java index c2ac771..a4c8816 100644 --- a/src/main/java/com/gregor/jprototerm/Main.java +++ b/src/main/java/com/gregor/jprototerm/Main.java @@ -15,6 +15,10 @@ import javafx.scene.layout.StackPane; import javafx.scene.text.Font; import javafx.stage.Stage; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + public final class Main extends Application { private TerminalWorkspace workspace; private TerminalCanvasView terminalView; @@ -79,6 +83,9 @@ public final class Main extends Application { } 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 { String encoded = KeyEncoder.encode(event); if (encoded != null) { @@ -143,6 +150,34 @@ public final class Main extends Application { }); } + private void openScrollbackInEditor() { + try { + Path file = Files.createTempFile("jprototerm-scrollback-", ".txt"); + Files.writeString(file, workspace.activePane().scrollbackText()); + file.toFile().deleteOnExit(); + + workspace.activePane().send(scrollbackEditorCommand(file) + "\r"); + } catch (IOException ex) { + System.err.println("Could not open scrollback in editor: " + ex.getMessage()); + } + } + + private String scrollbackEditorCommand(Path file) { + String quotedFile = shellQuote(file.toString()); + String command = config.scrollbackEditorCommand(); + if (command == null || command.isBlank()) { + command = "vi {file}"; + } + if (command.contains("{file}")) { + return command.replace("{file}", quotedFile); + } + return command + " " + quotedFile; + } + + private static String shellQuote(String value) { + return "'" + value.replace("'", "'\"'\"'") + "'"; + } + public static void main(String[] args) { System.setProperty("prism.order", System.getProperty("prism.order", "es2,sw")); launch(Main.class, args); diff --git a/src/main/java/com/gregor/jprototerm/TerminalPane.java b/src/main/java/com/gregor/jprototerm/TerminalPane.java index 3166f10..16fb371 100644 --- a/src/main/java/com/gregor/jprototerm/TerminalPane.java +++ b/src/main/java/com/gregor/jprototerm/TerminalPane.java @@ -115,6 +115,12 @@ public final class TerminalPane implements AutoCloseable { return renderSnapshot.get(); } + public String scrollbackText() { + synchronized (terminal) { + return terminal.text(); + } + } + public long renderVersion() { return renderVersion; }