Commit 85b045c

Eric Bower  ·  2026-08-01 12:41:18 -0400 EDT
parent 3697982
feat: track cwd

We now update the session's cwd whenever it changes based on OSC7
ansi escape codes.

Now when switching to session B from within session A, we now correctly
set the dir to where session A left off.

Because we are tracking cwd based on OSC7, embedded within cwd is the
hostname (`std.Uri`) so we know not to try to chdir when the session
is ssh'd into a remote machine.

BREAKING CHANGE: static `start_cwd` has been replaced by dynamic `cwd`
3 files changed,  +91, -20
+73, -14
  1@@ -163,7 +163,16 @@ pub fn clientLoop(client_sock_fd: i32) !ClientResult {
  2                     },
  3                     .Switch => {
  4                         std.log.info("switch session", .{});
  5-                        return ClientResult{ .kind = .switch_session, .session_name = try gpa.dupe(u8, msg.payload) };
  6+                        // Payload format: "session_name\ncwd" from the daemon
  7+                        const newline_idx = std.mem.indexOfScalar(u8, msg.payload, '\n') orelse {
  8+                            // No cwd provided (backward compat or old daemon)
  9+                            return ClientResult{ .kind = .switch_session, .session_name = try gpa.dupe(u8, msg.payload) };
 10+                        };
 11+                        return ClientResult{
 12+                            .kind = .switch_session,
 13+                            .session_name = try gpa.dupe(u8, msg.payload[0..newline_idx]),
 14+                            .cwd = if (newline_idx + 1 < msg.payload.len) try gpa.dupe(u8, msg.payload[newline_idx + 1 ..]) else null,
 15+                        };
 16                     },
 17                     else => {},
 18                 }
 19@@ -326,6 +335,7 @@ fn daemonLoop(daemon: *Daemon, gpa: std.mem.Allocator, io: std.Io, server_sock_f
 20                 } else {
 21                     // Feed PTY output to terminal emulator for state tracking
 22                     vt_stream.nextSlice(buf[0..n]);
 23+                    daemon.setPwd(&term);
 24                     daemon.has_pty_output = true;
 25 
 26                     // When no real terminal client has attached yet, respond to
 27@@ -442,7 +452,7 @@ fn daemonLoop(daemon: *Daemon, gpa: std.mem.Allocator, io: std.Io, server_sock_f
 28                     switch (msg.header.tag) {
 29                         .Input => try daemon.handleInput(gpa, client, msg.payload),
 30                         .Send => daemon.handleSend(gpa, msg.payload),
 31-                        .Output => try daemon.handleOutput(gpa, msg.payload, &vt_stream),
 32+                        .Output => try daemon.handleOutput(gpa, msg.payload, &term, &vt_stream),
 33                         .Init => try daemon.handleInit(gpa, client, pty_fd, &term, msg.payload),
 34                         .Switch => try daemon.handleSwitch(gpa, msg.payload),
 35                         .Resize => try daemon.handleResize(gpa, client, pty_fd, &term, msg.payload),
 36@@ -506,6 +516,7 @@ const ClientResult = struct {
 37         switch_session,
 38     },
 39     session_name: ?[]const u8,
 40+    cwd: ?[]const u8 = null,
 41 };
 42 
 43 /// Client represents each terminal that has connected to a session.
 44@@ -676,6 +687,35 @@ pub const Daemon = struct {
 45 
 46         var keep_fds_open = [_]i32{ server_sock_fd, dir.handle, log_fd };
 47         const cmd = try daemonize.createCmdZ(self.shell, self.is_task_mode, self.command);
 48+
 49+        // format will look like file://{host}{path}
 50+        std.log.info("checking pwd={s}", .{self.cwd});
 51+        const uri_opt = std.Uri.parse(self.cwd) catch |err| blk: {
 52+            std.log.warn("uri parse failed err={s}", .{@errorName(err)});
 53+            break :blk null;
 54+        };
 55+        if (uri_opt) |uri| {
 56+            var host_buf: [255]u8 = undefined;
 57+            const pwd_host = if (uri.getHost(&host_buf) catch null) |host| host.bytes else "unknown";
 58+            var buf: [std.posix.HOST_NAME_MAX]u8 = undefined;
 59+            const hostname = try std.posix.gethostname(&buf);
 60+            std.log.info("pwd_host={s} hostname={s}", .{ pwd_host, hostname });
 61+            if (std.mem.eql(u8, pwd_host, hostname)) {
 62+                const path_str = switch (uri.path) {
 63+                    .raw, .percent_encoded => |s| s,
 64+                };
 65+                const pwd_dir = std.Io.Dir.openDirAbsolute(io, path_str, .{}) catch |err| blk: {
 66+                    std.log.warn("failed to open dir={s} err={s}", .{ path_str, @errorName(err) });
 67+                    break :blk null;
 68+                };
 69+                if (pwd_dir) |pdir| {
 70+                    defer std.Io.Dir.close(pdir, io);
 71+                    std.log.info("set directory dir={s}", .{path_str});
 72+                    try std.process.setCurrentDir(io, pdir);
 73+                }
 74+            }
 75+        }
 76+
 77         const pty_info = daemonize.daemonize(
 78             sesh_name,
 79             cmd,
 80@@ -818,17 +858,27 @@ pub const Daemon = struct {
 81     pub fn handleSwitch(self: *Daemon, gpa: std.mem.Allocator, session_name: []const u8) !void {
 82         for (self.clients.items) |client| {
 83             if (self.leader_client_fd == client.socket_fd) {
 84-                ipc.appendMessage(
 85-                    gpa,
 86-                    &client.write_buf,
 87-                    .Switch,
 88-                    session_name,
 89-                ) catch |err| {
 90-                    std.log.warn(
 91-                        "failed to buffer terminal state for client err={s}",
 92-                        .{@errorName(err)},
 93-                    );
 94-                };
 95+                // Include the daemon's current cwd so the new session can start in the right directory
 96+                if (self.cwd.len > 0) {
 97+                    var payload = gpa.alloc(u8, session_name.len + 1 + self.cwd.len) catch return;
 98+                    defer gpa.free(payload);
 99+                    @memcpy(payload[0..session_name.len], session_name);
100+                    payload[session_name.len] = '\n';
101+                    @memcpy(payload[session_name.len + 1 ..], self.cwd);
102+                    ipc.appendMessage(gpa, &client.write_buf, .Switch, payload) catch |err| {
103+                        std.log.warn(
104+                            "failed to buffer terminal state for client err={s}",
105+                            .{@errorName(err)},
106+                        );
107+                    };
108+                } else {
109+                    ipc.appendMessage(gpa, &client.write_buf, .Switch, session_name) catch |err| {
110+                        std.log.warn(
111+                            "failed to buffer terminal state for client err={s}",
112+                            .{@errorName(err)},
113+                        );
114+                    };
115+                }
116                 client.has_pending_output = true;
117                 return;
118             }
119@@ -1078,8 +1128,17 @@ pub const Daemon = struct {
120         std.log.debug("run command len={d}", .{payload.len});
121     }
122 
123-    pub fn handleOutput(self: *Daemon, gpa: std.mem.Allocator, payload: []const u8, vt_stream: anytype) !void {
124+    fn setPwd(self: *Daemon, term: *ghostty_vt.Terminal) void {
125+        const pwd_opt = term.getPwd();
126+        if (pwd_opt) |pwd| {
127+            std.log.info("setting pwd to ghostty term pwd={s}", .{pwd});
128+            self.cwd = pwd;
129+        }
130+    }
131+
132+    pub fn handleOutput(self: *Daemon, gpa: std.mem.Allocator, payload: []const u8, term: *ghostty_vt.Terminal, vt_stream: anytype) !void {
133         vt_stream.nextSlice(payload);
134+        self.setPwd(term);
135         self.has_pty_output = true;
136         for (self.clients.items) |client| {
137             try ipc.appendMessage(gpa, &client.write_buf, .Output, payload);
+5, -4
 1@@ -1351,9 +1351,6 @@ fn attach(gpa: std.mem.Allocator, io: std.Io, daemon: *Daemon) !void {
 2                 const restore_seq = "\x1bc";
 3                 _ = lib_posix.write(lib_posix.STDOUT_FILENO, restore_seq) catch {};
 4 
 5-                var cwd_buf: [std.fs.max_path_bytes]u8 = undefined;
 6-                const cwd_len = std.process.currentPath(io, &cwd_buf) catch 0;
 7-                const cwd = cwd_buf[0..cwd_len];
 8                 const target_path = socket.getSocketPath(
 9                     gpa,
10                     daemon.cfg.socket_dir,
11@@ -1368,7 +1365,11 @@ fn attach(gpa: std.mem.Allocator, io: std.Io, daemon: *Daemon) !void {
12                 };
13 
14                 var target_daemon = Daemon.init(io, daemon.cfg, session_name, target_path);
15-                target_daemon.cwd = cwd;
16+                // Use the cwd from the previous daemon if available (sent by the daemon),
17+                // otherwise fall back to the client's original cwd
18+                const switch_cwd = looper.cwd orelse daemon.cwd;
19+                std.log.info("switching to new session cwd={s}", .{switch_cwd});
20+                target_daemon.cwd = switch_cwd;
21                 target_daemon.shell = daemon.shell;
22                 return attach(gpa, io, &target_daemon);
23             }
+13, -2
 1@@ -35,7 +35,6 @@ pub fn get_session_entries(
 2     io: std.Io,
 3     socket_dir: []const u8,
 4 ) !std.ArrayList(SessionEntry) {
 5-    std.log.info("get session entries socket_dir={s}", .{socket_dir});
 6     var dir = try std.Io.Dir.openDirAbsolute(io, socket_dir, .{ .iterate = true });
 7     defer dir.close(io);
 8     var iter = dir.iterate();
 9@@ -114,6 +113,18 @@ pub fn get_session_entries(
10     return sessions;
11 }
12 
13+/// getCwd get the current working directory in a std.Uri format.
14+/// Caller is responsible for releasing memory.
15+pub fn getCwd(gpa: std.mem.Allocator, io: std.Io) ![]u8 {
16+    const cur_path = try std.process.currentPathAlloc(io, gpa);
17+    defer gpa.free(cur_path);
18+
19+    var buf: [std.posix.HOST_NAME_MAX]u8 = undefined;
20+    const hostname = try std.posix.gethostname(&buf);
21+
22+    return std.fmt.allocPrint(gpa, "file://{s}{s}", .{ hostname, cur_path });
23+}
24+
25 pub fn shellNeedsQuoting(arg: []const u8) bool {
26     if (arg.len == 0) return true;
27     for (arg) |ch| {
28@@ -820,7 +831,7 @@ pub fn writeSessionLine(
29         session.created_at,
30     });
31     if (session.cwd) |cwd| {
32-        try writer.print("\tstart_dir={s}", .{cwd});
33+        try writer.print("\tcwd={s}", .{cwd});
34     }
35     if (session.cmd) |cmd| {
36         try writer.print("\tcmd={s}", .{cmd});