diff --git a/src/main/java/com/gregor/jprototerm/LinuxPty.java b/src/main/java/com/gregor/jprototerm/LinuxPty.java index 2b2eb81..e9dd2a9 100644 --- a/src/main/java/com/gregor/jprototerm/LinuxPty.java +++ b/src/main/java/com/gregor/jprototerm/LinuxPty.java @@ -9,6 +9,9 @@ import java.lang.foreign.MemorySegment; import java.lang.foreign.SymbolLookup; import java.lang.foreign.ValueLayout; import java.lang.invoke.MethodHandle; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -74,6 +77,7 @@ public final class LinuxPty implements AutoCloseable { private static final long SPAWN_ACTIONS_SIZE = 256; private static final long SPAWN_ATTR_SIZE = 512; + private static final MethodHandle TCGETPGRP = handle("tcgetpgrp", FD_INT_INT); private static final MethodHandle POSIX_OPENPT = handle("posix_openpt", FD_INT_INT); private static final MethodHandle GRANTPT = handle("grantpt", FD_INT_INT); private static final MethodHandle UNLOCKPT = handle("unlockpt", FD_INT_INT); @@ -205,6 +209,25 @@ public final class LinuxPty implements AutoCloseable { } } + /** + * Best-effort current working directory of the terminal's foreground process group, read from + * {@code /proc}. This tracks the directory the user is actually in (a {@code cd} in the shell, + * or a child program that changed dir), so a newly opened pane can start there. Falls back to + * the shell's own pid, and returns {@code null} if it cannot be determined. + */ + public String currentWorkingDirectory() { + if (closed) { + return null; + } + int pgid = callInt(TCGETPGRP, masterFd); + int target = pgid > 0 ? pgid : pid; + try { + return Files.readSymbolicLink(Path.of("/proc", Integer.toString(target), "cwd")).toString(); + } catch (IOException | RuntimeException ex) { + return null; + } + } + /** Resizes the terminal window. */ public void setWinSize(int columns, int rows) { if (closed) { diff --git a/src/main/java/com/gregor/jprototerm/ShellSession.java b/src/main/java/com/gregor/jprototerm/ShellSession.java index 09212e2..0b9d3dd 100644 --- a/src/main/java/com/gregor/jprototerm/ShellSession.java +++ b/src/main/java/com/gregor/jprototerm/ShellSession.java @@ -20,7 +20,8 @@ public final class ShellSession implements AutoCloseable { }); } - public static ShellSession start(String shell, Map envOverride, TerminalPane pane, int columns, int rows) { + public static ShellSession start(String shell, Map envOverride, TerminalPane pane, + int columns, int rows, String workingDirectory) { try { Map environment = new HashMap<>(System.getenv()); environment.put("TERM", "xterm-kitty"); @@ -31,7 +32,7 @@ public final class ShellSession implements AutoCloseable { LinuxPty pty = LinuxPty.spawn( new String[] {shell, "-i"}, environment, - System.getProperty("user.home")); + workingDirectory != null ? workingDirectory : System.getProperty("user.home")); ShellSession session = new ShellSession(pty); session.resize(columns, rows); return session; @@ -69,6 +70,11 @@ public final class ShellSession implements AutoCloseable { reader.submit(() -> readOutput(pane)); } + /** Best-effort current working directory of the running shell, or {@code null} if unknown. */ + public String currentWorkingDirectory() { + return closed ? null : pty.currentWorkingDirectory(); + } + public void resize(int columns, int rows) { if (closed) { return; diff --git a/src/main/java/com/gregor/jprototerm/Tab.java b/src/main/java/com/gregor/jprototerm/Tab.java index 1f5c66d..dedeea2 100644 --- a/src/main/java/com/gregor/jprototerm/Tab.java +++ b/src/main/java/com/gregor/jprototerm/Tab.java @@ -308,7 +308,10 @@ final class Tab implements AutoCloseable { widthPx = lastWidth / (tiled.size() + 1); heightPx = availHeight; } - return TerminalPane.create(config, metrics, this::markContentChanged, widthPx, heightPx); + // Open the new pane in the active pane's working directory, so a split/new pane lands + // where the user currently is. null (no active pane yet, or cwd unknown) falls back to home. + String workingDirectory = active != null ? active.currentWorkingDirectory() : null; + return TerminalPane.create(config, metrics, this::markContentChanged, widthPx, heightPx, workingDirectory); } private static boolean directionFilter(Direction direction, TerminalPane current, TerminalPane candidate) { diff --git a/src/main/java/com/gregor/jprototerm/TerminalPane.java b/src/main/java/com/gregor/jprototerm/TerminalPane.java index 592b945..7c00f8a 100644 --- a/src/main/java/com/gregor/jprototerm/TerminalPane.java +++ b/src/main/java/com/gregor/jprototerm/TerminalPane.java @@ -69,9 +69,11 @@ public final class TerminalPane implements AutoCloseable, RenderTarget { * columns and rows fit, and that grid is handed to ghostty and the shell at start-up. A * non-positive size falls back to the configured default grid (used before the first * layout, when no rect is known yet). The pane owns the shell session it starts and runs - * {@code onContentChange} on every content change. + * {@code onContentChange} on every content change. The shell starts in {@code workingDirectory} + * (e.g. the active pane's cwd), or the user's home when {@code null}. */ - public static TerminalPane create(AppConfig config, TerminalMetrics metrics, Runnable onContentChange, double widthPx, double heightPx) { + public static TerminalPane create(AppConfig config, TerminalMetrics metrics, Runnable onContentChange, + double widthPx, double heightPx, String workingDirectory) { int columns = widthPx > 0 ? metrics.columnsFor(widthPx) : config.columns(); int rows = heightPx > 0 ? metrics.rowsFor(heightPx) : config.rows(); Terminal terminal = Ghostty.open(new TerminalOptions(columns, rows, config.maxScrollback())); @@ -79,7 +81,7 @@ public final class TerminalPane implements AutoCloseable, RenderTarget { TerminalPane pane = new TerminalPane(terminal, metrics, config.kittyGraphics(), onContentChange, new GhosttyTerminalRenderer(metrics), columns, rows); pane.refresh(); - pane.attach(ShellSession.start(config.shell(), config.envOverride(), pane, columns, rows)); + pane.attach(ShellSession.start(config.shell(), config.envOverride(), pane, columns, rows, workingDirectory)); return pane; } @@ -205,6 +207,12 @@ public final class TerminalPane implements AutoCloseable, RenderTarget { } } + /** Best-effort current working directory of this pane's shell, or {@code null} if unknown. */ + public String currentWorkingDirectory() { + ShellSession current = session; + return current != null ? current.currentWorkingDirectory() : null; + } + /** This pane's own content revision, bumped on every change (see {@link #refresh()}). */ public long contentVersion() { return contentVersion.get();