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
......@@ -163,7 +163,16 @@ pub fn clientLoop(client_sock_fd: i32) !ClientResult {
163163 },
164164 .Switch => {
165165 std.log.info("switch session", .{});
166- return ClientResult{ .kind = .switch_session, .session_name = try gpa.dupe(u8, msg.payload) };
166+ // Payload format: "session_name\ncwd" from the daemon
167+ const newline_idx = std.mem.indexOfScalar(u8, msg.payload, '\n') orelse {
168+ // No cwd provided (backward compat or old daemon)
169+ return ClientResult{ .kind = .switch_session, .session_name = try gpa.dupe(u8, msg.payload) };
170+ };
171+ return ClientResult{
172+ .kind = .switch_session,
173+ .session_name = try gpa.dupe(u8, msg.payload[0..newline_idx]),
174+ .cwd = if (newline_idx + 1 < msg.payload.len) try gpa.dupe(u8, msg.payload[newline_idx + 1 ..]) else null,
175+ };
167176 },
168177 else => {},
169178 }
......@@ -326,6 +335,7 @@ fn daemonLoop(daemon: *Daemon, gpa: std.mem.Allocator, io: std.Io, server_sock_f
326335 } else {
327336 // Feed PTY output to terminal emulator for state tracking
328337 vt_stream.nextSlice(buf[0..n]);
338+ daemon.setPwd(&term);
329339 daemon.has_pty_output = true;
330340
331341 // When no real terminal client has attached yet, respond to
......@@ -442,7 +452,7 @@ fn daemonLoop(daemon: *Daemon, gpa: std.mem.Allocator, io: std.Io, server_sock_f
442452 switch (msg.header.tag) {
443453 .Input => try daemon.handleInput(gpa, client, msg.payload),
444454 .Send => daemon.handleSend(gpa, msg.payload),
445- .Output => try daemon.handleOutput(gpa, msg.payload, &vt_stream),
455+ .Output => try daemon.handleOutput(gpa, msg.payload, &term, &vt_stream),
446456 .Init => try daemon.handleInit(gpa, client, pty_fd, &term, msg.payload),
447457 .Switch => try daemon.handleSwitch(gpa, msg.payload),
448458 .Resize => try daemon.handleResize(gpa, client, pty_fd, &term, msg.payload),
......@@ -506,6 +516,7 @@ const ClientResult = struct {
506516 switch_session,
507517 },
508518 session_name: ?[]const u8,
519+ cwd: ?[]const u8 = null,
509520 };
510521
511522 /// Client represents each terminal that has connected to a session.
......@@ -676,6 +687,35 @@ pub const Daemon = struct {
676687
677688 var keep_fds_open = [_]i32{ server_sock_fd, dir.handle, log_fd };
678689 const cmd = try daemonize.createCmdZ(self.shell, self.is_task_mode, self.command);
690+
691+ // format will look like file://{host}{path}
692+ std.log.info("checking pwd={s}", .{self.cwd});
693+ const uri_opt = std.Uri.parse(self.cwd) catch |err| blk: {
694+ std.log.warn("uri parse failed err={s}", .{@errorName(err)});
695+ break :blk null;
696+ };
697+ if (uri_opt) |uri| {
698+ var host_buf: [255]u8 = undefined;
699+ const pwd_host = if (uri.getHost(&host_buf) catch null) |host| host.bytes else "unknown";
700+ var buf: [std.posix.HOST_NAME_MAX]u8 = undefined;
701+ const hostname = try std.posix.gethostname(&buf);
702+ std.log.info("pwd_host={s} hostname={s}", .{ pwd_host, hostname });
703+ if (std.mem.eql(u8, pwd_host, hostname)) {
704+ const path_str = switch (uri.path) {
705+ .raw, .percent_encoded => |s| s,
706+ };
707+ const pwd_dir = std.Io.Dir.openDirAbsolute(io, path_str, .{}) catch |err| blk: {
708+ std.log.warn("failed to open dir={s} err={s}", .{ path_str, @errorName(err) });
709+ break :blk null;
710+ };
711+ if (pwd_dir) |pdir| {
712+ defer std.Io.Dir.close(pdir, io);
713+ std.log.info("set directory dir={s}", .{path_str});
714+ try std.process.setCurrentDir(io, pdir);
715+ }
716+ }
717+ }
718+
679719 const pty_info = daemonize.daemonize(
680720 sesh_name,
681721 cmd,
......@@ -818,17 +858,27 @@ pub const Daemon = struct {
818858 pub fn handleSwitch(self: *Daemon, gpa: std.mem.Allocator, session_name: []const u8) !void {
819859 for (self.clients.items) |client| {
820860 if (self.leader_client_fd == client.socket_fd) {
821- ipc.appendMessage(
822- gpa,
823- &client.write_buf,
824- .Switch,
825- session_name,
826- ) catch |err| {
827- std.log.warn(
828- "failed to buffer terminal state for client err={s}",
829- .{@errorName(err)},
830- );
831- };
861+ // Include the daemon's current cwd so the new session can start in the right directory
862+ if (self.cwd.len > 0) {
863+ var payload = gpa.alloc(u8, session_name.len + 1 + self.cwd.len) catch return;
864+ defer gpa.free(payload);
865+ @memcpy(payload[0..session_name.len], session_name);
866+ payload[session_name.len] = '\n';
867+ @memcpy(payload[session_name.len + 1 ..], self.cwd);
868+ ipc.appendMessage(gpa, &client.write_buf, .Switch, payload) catch |err| {
869+ std.log.warn(
870+ "failed to buffer terminal state for client err={s}",
871+ .{@errorName(err)},
872+ );
873+ };
874+ } else {
875+ ipc.appendMessage(gpa, &client.write_buf, .Switch, session_name) catch |err| {
876+ std.log.warn(
877+ "failed to buffer terminal state for client err={s}",
878+ .{@errorName(err)},
879+ );
880+ };
881+ }
832882 client.has_pending_output = true;
833883 return;
834884 }
......@@ -1078,8 +1128,17 @@ pub const Daemon = struct {
10781128 std.log.debug("run command len={d}", .{payload.len});
10791129 }
10801130
1081- pub fn handleOutput(self: *Daemon, gpa: std.mem.Allocator, payload: []const u8, vt_stream: anytype) !void {
1131+ fn setPwd(self: *Daemon, term: *ghostty_vt.Terminal) void {
1132+ const pwd_opt = term.getPwd();
1133+ if (pwd_opt) |pwd| {
1134+ std.log.info("setting pwd to ghostty term pwd={s}", .{pwd});
1135+ self.cwd = pwd;
1136+ }
1137+ }
1138+
1139+ pub fn handleOutput(self: *Daemon, gpa: std.mem.Allocator, payload: []const u8, term: *ghostty_vt.Terminal, vt_stream: anytype) !void {
10821140 vt_stream.nextSlice(payload);
1141+ self.setPwd(term);
10831142 self.has_pty_output = true;
10841143 for (self.clients.items) |client| {
10851144 try ipc.appendMessage(gpa, &client.write_buf, .Output, payload);
+5, -4
......@@ -1351,9 +1351,6 @@ fn attach(gpa: std.mem.Allocator, io: std.Io, daemon: *Daemon) !void {
13511351 const restore_seq = "\x1bc";
13521352 _ = lib_posix.write(lib_posix.STDOUT_FILENO, restore_seq) catch {};
13531353
1354- var cwd_buf: [std.fs.max_path_bytes]u8 = undefined;
1355- const cwd_len = std.process.currentPath(io, &cwd_buf) catch 0;
1356- const cwd = cwd_buf[0..cwd_len];
13571354 const target_path = socket.getSocketPath(
13581355 gpa,
13591356 daemon.cfg.socket_dir,
......@@ -1368,7 +1365,11 @@ fn attach(gpa: std.mem.Allocator, io: std.Io, daemon: *Daemon) !void {
13681365 };
13691366
13701367 var target_daemon = Daemon.init(io, daemon.cfg, session_name, target_path);
1371- target_daemon.cwd = cwd;
1368+ // Use the cwd from the previous daemon if available (sent by the daemon),
1369+ // otherwise fall back to the client's original cwd
1370+ const switch_cwd = looper.cwd orelse daemon.cwd;
1371+ std.log.info("switching to new session cwd={s}", .{switch_cwd});
1372+ target_daemon.cwd = switch_cwd;
13721373 target_daemon.shell = daemon.shell;
13731374 return attach(gpa, io, &target_daemon);
13741375 }
+13, -2
......@@ -35,7 +35,6 @@ pub fn get_session_entries(
3535 io: std.Io,
3636 socket_dir: []const u8,
3737 ) !std.ArrayList(SessionEntry) {
38- std.log.info("get session entries socket_dir={s}", .{socket_dir});
3938 var dir = try std.Io.Dir.openDirAbsolute(io, socket_dir, .{ .iterate = true });
4039 defer dir.close(io);
4140 var iter = dir.iterate();
......@@ -114,6 +113,18 @@ pub fn get_session_entries(
114113 return sessions;
115114 }
116115
116+/// getCwd get the current working directory in a std.Uri format.
117+/// Caller is responsible for releasing memory.
118+pub fn getCwd(gpa: std.mem.Allocator, io: std.Io) ![]u8 {
119+ const cur_path = try std.process.currentPathAlloc(io, gpa);
120+ defer gpa.free(cur_path);
121+
122+ var buf: [std.posix.HOST_NAME_MAX]u8 = undefined;
123+ const hostname = try std.posix.gethostname(&buf);
124+
125+ return std.fmt.allocPrint(gpa, "file://{s}{s}", .{ hostname, cur_path });
126+}
127+
117128 pub fn shellNeedsQuoting(arg: []const u8) bool {
118129 if (arg.len == 0) return true;
119130 for (arg) |ch| {
......@@ -820,7 +831,7 @@ pub fn writeSessionLine(
820831 session.created_at,
821832 });
822833 if (session.cwd) |cwd| {
823- try writer.print("\tstart_dir={s}", .{cwd});
834+ try writer.print("\tcwd={s}", .{cwd});
824835 }
825836 if (session.cmd) |cmd| {
826837 try writer.print("\tcmd={s}", .{cmd});