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});