new panes gain active pwd

This commit is contained in:
2026-06-01 13:35:28 +02:00
parent 6cf9afd664
commit a31cf06cbd
4 changed files with 46 additions and 6 deletions

View File

@@ -9,6 +9,9 @@ import java.lang.foreign.MemorySegment;
import java.lang.foreign.SymbolLookup; import java.lang.foreign.SymbolLookup;
import java.lang.foreign.ValueLayout; import java.lang.foreign.ValueLayout;
import java.lang.invoke.MethodHandle; 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.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; 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_ACTIONS_SIZE = 256;
private static final long SPAWN_ATTR_SIZE = 512; 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 POSIX_OPENPT = handle("posix_openpt", FD_INT_INT);
private static final MethodHandle GRANTPT = handle("grantpt", FD_INT_INT); private static final MethodHandle GRANTPT = handle("grantpt", FD_INT_INT);
private static final MethodHandle UNLOCKPT = handle("unlockpt", 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. */ /** Resizes the terminal window. */
public void setWinSize(int columns, int rows) { public void setWinSize(int columns, int rows) {
if (closed) { if (closed) {

View File

@@ -20,7 +20,8 @@ public final class ShellSession implements AutoCloseable {
}); });
} }
public static ShellSession start(String shell, Map<String, String> envOverride, TerminalPane pane, int columns, int rows) { public static ShellSession start(String shell, Map<String, String> envOverride, TerminalPane pane,
int columns, int rows, String workingDirectory) {
try { try {
Map<String, String> environment = new HashMap<>(System.getenv()); Map<String, String> environment = new HashMap<>(System.getenv());
environment.put("TERM", "xterm-kitty"); environment.put("TERM", "xterm-kitty");
@@ -31,7 +32,7 @@ public final class ShellSession implements AutoCloseable {
LinuxPty pty = LinuxPty.spawn( LinuxPty pty = LinuxPty.spawn(
new String[] {shell, "-i"}, new String[] {shell, "-i"},
environment, environment,
System.getProperty("user.home")); workingDirectory != null ? workingDirectory : System.getProperty("user.home"));
ShellSession session = new ShellSession(pty); ShellSession session = new ShellSession(pty);
session.resize(columns, rows); session.resize(columns, rows);
return session; return session;
@@ -69,6 +70,11 @@ public final class ShellSession implements AutoCloseable {
reader.submit(() -> readOutput(pane)); 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) { public void resize(int columns, int rows) {
if (closed) { if (closed) {
return; return;

View File

@@ -308,7 +308,10 @@ final class Tab implements AutoCloseable {
widthPx = lastWidth / (tiled.size() + 1); widthPx = lastWidth / (tiled.size() + 1);
heightPx = availHeight; 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) { private static boolean directionFilter(Direction direction, TerminalPane current, TerminalPane candidate) {

View File

@@ -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 * 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 * 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 * 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 columns = widthPx > 0 ? metrics.columnsFor(widthPx) : config.columns();
int rows = heightPx > 0 ? metrics.rowsFor(heightPx) : config.rows(); int rows = heightPx > 0 ? metrics.rowsFor(heightPx) : config.rows();
Terminal terminal = Ghostty.open(new TerminalOptions(columns, rows, config.maxScrollback())); 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, TerminalPane pane = new TerminalPane(terminal, metrics, config.kittyGraphics(), onContentChange,
new GhosttyTerminalRenderer(metrics), columns, rows); new GhosttyTerminalRenderer(metrics), columns, rows);
pane.refresh(); 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; 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()}). */ /** This pane's own content revision, bumped on every change (see {@link #refresh()}). */
public long contentVersion() { public long contentVersion() {
return contentVersion.get(); return contentVersion.get();