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 { | |
| 163 | 163 | }, | |
| 164 | 164 | .Switch => { | |
| 165 | 165 | 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 | + | }; | |
| 167 | 176 | }, | |
| 168 | 177 | else => {}, | |
| 169 | 178 | } |
| ... | ... | @@ -326,6 +335,7 @@ fn daemonLoop(daemon: *Daemon, gpa: std.mem.Allocator, io: std.Io, server_sock_f | |
| 326 | 335 | } else { | |
| 327 | 336 | // Feed PTY output to terminal emulator for state tracking | |
| 328 | 337 | vt_stream.nextSlice(buf[0..n]); | |
| 338 | + | daemon.setPwd(&term); | |
| 329 | 339 | daemon.has_pty_output = true; | |
| 330 | 340 | ||
| 331 | 341 | // 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 | |
| 442 | 452 | switch (msg.header.tag) { | |
| 443 | 453 | .Input => try daemon.handleInput(gpa, client, msg.payload), | |
| 444 | 454 | .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), | |
| 446 | 456 | .Init => try daemon.handleInit(gpa, client, pty_fd, &term, msg.payload), | |
| 447 | 457 | .Switch => try daemon.handleSwitch(gpa, msg.payload), | |
| 448 | 458 | .Resize => try daemon.handleResize(gpa, client, pty_fd, &term, msg.payload), |
| ... | ... | @@ -506,6 +516,7 @@ const ClientResult = struct { | |
| 506 | 516 | switch_session, | |
| 507 | 517 | }, | |
| 508 | 518 | session_name: ?[]const u8, | |
| 519 | + | cwd: ?[]const u8 = null, | |
| 509 | 520 | }; | |
| 510 | 521 | ||
| 511 | 522 | /// Client represents each terminal that has connected to a session. |
| ... | ... | @@ -676,6 +687,35 @@ pub const Daemon = struct { | |
| 676 | 687 | ||
| 677 | 688 | var keep_fds_open = [_]i32{ server_sock_fd, dir.handle, log_fd }; | |
| 678 | 689 | 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 | + | ||
| 679 | 719 | const pty_info = daemonize.daemonize( | |
| 680 | 720 | sesh_name, | |
| 681 | 721 | cmd, |
| ... | ... | @@ -818,17 +858,27 @@ pub const Daemon = struct { | |
| 818 | 858 | pub fn handleSwitch(self: *Daemon, gpa: std.mem.Allocator, session_name: []const u8) !void { | |
| 819 | 859 | for (self.clients.items) |client| { | |
| 820 | 860 | 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 | + | } | |
| 832 | 882 | client.has_pending_output = true; | |
| 833 | 883 | return; | |
| 834 | 884 | } |
| ... | ... | @@ -1078,8 +1128,17 @@ pub const Daemon = struct { | |
| 1078 | 1128 | std.log.debug("run command len={d}", .{payload.len}); | |
| 1079 | 1129 | } | |
| 1080 | 1130 | ||
| 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 { | |
| 1082 | 1140 | vt_stream.nextSlice(payload); | |
| 1141 | + | self.setPwd(term); | |
| 1083 | 1142 | self.has_pty_output = true; | |
| 1084 | 1143 | for (self.clients.items) |client| { | |
| 1085 | 1144 | 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 { | |
| 1351 | 1351 | const restore_seq = "\x1bc"; | |
| 1352 | 1352 | _ = lib_posix.write(lib_posix.STDOUT_FILENO, restore_seq) catch {}; | |
| 1353 | 1353 | ||
| 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]; | |
| 1357 | 1354 | const target_path = socket.getSocketPath( | |
| 1358 | 1355 | gpa, | |
| 1359 | 1356 | daemon.cfg.socket_dir, |
| ... | ... | @@ -1368,7 +1365,11 @@ fn attach(gpa: std.mem.Allocator, io: std.Io, daemon: *Daemon) !void { | |
| 1368 | 1365 | }; | |
| 1369 | 1366 | ||
| 1370 | 1367 | 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; | |
| 1372 | 1373 | target_daemon.shell = daemon.shell; | |
| 1373 | 1374 | return attach(gpa, io, &target_daemon); | |
| 1374 | 1375 | } |
+13,
-2
| ... | ... | @@ -35,7 +35,6 @@ pub fn get_session_entries( | |
| 35 | 35 | io: std.Io, | |
| 36 | 36 | socket_dir: []const u8, | |
| 37 | 37 | ) !std.ArrayList(SessionEntry) { | |
| 38 | - | std.log.info("get session entries socket_dir={s}", .{socket_dir}); | |
| 39 | 38 | var dir = try std.Io.Dir.openDirAbsolute(io, socket_dir, .{ .iterate = true }); | |
| 40 | 39 | defer dir.close(io); | |
| 41 | 40 | var iter = dir.iterate(); |
| ... | ... | @@ -114,6 +113,18 @@ pub fn get_session_entries( | |
| 114 | 113 | return sessions; | |
| 115 | 114 | } | |
| 116 | 115 | ||
| 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 | + | ||
| 117 | 128 | pub fn shellNeedsQuoting(arg: []const u8) bool { | |
| 118 | 129 | if (arg.len == 0) return true; | |
| 119 | 130 | for (arg) |ch| { |
| ... | ... | @@ -820,7 +831,7 @@ pub fn writeSessionLine( | |
| 820 | 831 | session.created_at, | |
| 821 | 832 | }); | |
| 822 | 833 | if (session.cwd) |cwd| { | |
| 823 | - | try writer.print("\tstart_dir={s}", .{cwd}); | |
| 834 | + | try writer.print("\tcwd={s}", .{cwd}); | |
| 824 | 835 | } | |
| 825 | 836 | if (session.cmd) |cmd| { | |
| 826 | 837 | try writer.print("\tcmd={s}", .{cmd}); |