diff --git a/include/ghostty.h b/include/ghostty.h index f4836f210..41e3b3fe2 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -430,6 +430,11 @@ typedef struct { const char* title; } ghostty_action_set_title_s; +// apprt.action.Pwd.C +typedef struct { + const char* pwd; +} ghostty_action_pwd_s; + // terminal.MouseShape typedef enum { GHOSTTY_MOUSE_SHAPE_DEFAULT, @@ -552,6 +557,7 @@ typedef enum { GHOSTTY_ACTION_RENDER_INSPECTOR, GHOSTTY_ACTION_DESKTOP_NOTIFICATION, GHOSTTY_ACTION_SET_TITLE, + GHOSTTY_ACTION_PWD, GHOSTTY_ACTION_MOUSE_SHAPE, GHOSTTY_ACTION_MOUSE_VISIBILITY, GHOSTTY_ACTION_MOUSE_OVER_LINK, @@ -576,6 +582,7 @@ typedef union { ghostty_action_inspector_e inspector; ghostty_action_desktop_notification_s desktop_notification; ghostty_action_set_title_s set_title; + ghostty_action_pwd_s pwd; ghostty_action_mouse_shape_e mouse_shape; ghostty_action_mouse_visibility_e mouse_visibility; ghostty_action_mouse_over_link_s mouse_over_link; @@ -705,7 +712,6 @@ void ghostty_surface_complete_clipboard_request(ghostty_surface_t, const char*, void*, bool); -uintptr_t ghostty_surface_pwd(ghostty_surface_t, char*, uintptr_t); bool ghostty_surface_has_selection(ghostty_surface_t); uintptr_t ghostty_surface_selection(ghostty_surface_t, char*, uintptr_t); diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index 768f57d30..8fc1ae1ec 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -50,6 +50,7 @@ struct TerminalView: View { // Various state values sent back up from the currently focused terminals. @FocusedValue(\.ghosttySurfaceView) private var focusedSurface @FocusedValue(\.ghosttySurfaceTitle) private var surfaceTitle + @FocusedValue(\.ghosttySurfacePwd) private var surfacePwd @FocusedValue(\.ghosttySurfaceZoomed) private var zoomedSplit @FocusedValue(\.ghosttySurfaceCellSize) private var cellSize @@ -65,14 +66,11 @@ struct TerminalView: View { return title } - - // The proxy icon URL for our window - private var proxyIconURL: URL? { - guard let proxyURLString = focusedSurface?.pwd else { - return nil - } - // Use fileURLWithPath initializer for file paths - return URL(fileURLWithPath: proxyURLString) + + // The pwd of the focused surface as a URL + private var pwdURL: URL? { + guard let surfacePwd else { return nil } + return URL(fileURLWithPath: surfacePwd) } var body: some View { @@ -88,7 +86,7 @@ struct TerminalView: View { if (Ghostty.info.mode == GHOSTTY_BUILD_MODE_DEBUG || Ghostty.info.mode == GHOSTTY_BUILD_MODE_RELEASE_SAFE) { DebugBuildWarningView() } - + Ghostty.TerminalSplit(node: $viewModel.surfaceTree) .environmentObject(ghostty) .focused($focused) @@ -99,7 +97,7 @@ struct TerminalView: View { .onChange(of: title) { newValue in self.delegate?.titleDidChange(to: newValue) } - .onChange(of: proxyIconURL) { newValue in + .onChange(of: pwdURL) { newValue in self.delegate?.pwdDidChange(to: newValue) } .onChange(of: cellSize) { newValue in diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 07acf0f91..489493ad3 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -488,6 +488,9 @@ extension Ghostty { case GHOSTTY_ACTION_SET_TITLE: setTitle(app, target: target, v: action.action.set_title) + case GHOSTTY_ACTION_PWD: + pwdChanged(app, target: target, v: action.action.pwd) + case GHOSTTY_ACTION_OPEN_CONFIG: ghostty_config_open() @@ -943,6 +946,26 @@ extension Ghostty { } } + private static func pwdChanged( + _ app: ghostty_app_t, + target: ghostty_target_s, + v: ghostty_action_pwd_s) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("pwd change does nothing with an app target") + return + + case GHOSTTY_TARGET_SURFACE: + guard let surface = target.target.surface else { return } + guard let surfaceView = self.surfaceView(from: surface) else { return } + guard let pwd = String(cString: v.pwd!, encoding: .utf8) else { return } + surfaceView.pwd = pwd + + default: + assertionFailure() + } + } + private static func setMouseShape( _ app: ghostty_app_t, target: ghostty_target_s, diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index 738c6331f..25fa99a2e 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -92,6 +92,7 @@ extension Ghostty { Surface(view: surfaceView, size: geo.size) .focused($surfaceFocus) .focusedValue(\.ghosttySurfaceTitle, surfaceView.title) + .focusedValue(\.ghosttySurfacePwd, surfaceView.pwd) .focusedValue(\.ghosttySurfaceView, surfaceView) .focusedValue(\.ghosttySurfaceCellSize, surfaceView.cellSize) #if canImport(AppKit) @@ -512,9 +513,7 @@ extension FocusedValues { struct FocusedGhosttySurface: FocusedValueKey { typealias Value = Ghostty.SurfaceView } -} -extension FocusedValues { var ghosttySurfaceTitle: String? { get { self[FocusedGhosttySurfaceTitle.self] } set { self[FocusedGhosttySurfaceTitle.self] = newValue } @@ -523,9 +522,16 @@ extension FocusedValues { struct FocusedGhosttySurfaceTitle: FocusedValueKey { typealias Value = String } -} -extension FocusedValues { + var ghosttySurfacePwd: String? { + get { self[FocusedGhosttySurfacePwd.self] } + set { self[FocusedGhosttySurfacePwd.self] = newValue } + } + + struct FocusedGhosttySurfacePwd: FocusedValueKey { + typealias Value = String + } + var ghosttySurfaceZoomed: Bool? { get { self[FocusedGhosttySurfaceZoomed.self] } set { self[FocusedGhosttySurfaceZoomed.self] = newValue } @@ -534,9 +540,7 @@ extension FocusedValues { struct FocusedGhosttySurfaceZoomed: FocusedValueKey { typealias Value = Bool } -} -extension FocusedValues { var ghosttySurfaceCellSize: OSSize? { get { self[FocusedGhosttySurfaceCellSize.self] } set { self[FocusedGhosttySurfaceCellSize.self] = newValue } diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 512a5239b..6f72f74cc 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -14,6 +14,10 @@ extension Ghostty { // to the app level and it is set from there. @Published var title: String = "👻" + // The current pwd of the surface as defined by the pty. This can be + // changed with escape codes. + @Published var pwd: String? = nil + // The cell size of this surface. This is set by the core when the // surface is first created and any time the cell size changes (i.e. // when the font size changes). This is used to allow windows to be @@ -71,17 +75,6 @@ extension Ghostty { return ghostty_surface_needs_confirm_quit(surface) } - /// Returns the pwd of the surface if it has one. - var pwd: String? { - guard let surface = self.surface else { return nil } - let v = String(unsafeUninitializedCapacity: 1024) { - Int(ghostty_surface_pwd(surface, $0.baseAddress, UInt($0.count))) - } - - if (v.count == 0) { return nil } - return v - } - // Returns the inspector instance for this surface, or nil if the // surface has been closed. var inspector: ghostty_inspector_t? { diff --git a/macos/Sources/Ghostty/SurfaceView_UIKit.swift b/macos/Sources/Ghostty/SurfaceView_UIKit.swift index 1c2357960..8ac08d0bd 100644 --- a/macos/Sources/Ghostty/SurfaceView_UIKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_UIKit.swift @@ -12,6 +12,9 @@ extension Ghostty { // to the app level and it is set from there. @Published var title: String = "👻" + // The current pwd of the surface. + @Published var pwd: String? = nil + // The cell size of this surface. This is set by the core when the // surface is first created and any time the cell size changes (i.e. // when the font size changes). This is used to allow windows to be diff --git a/src/Surface.zig b/src/Surface.zig index 10ecfd8f1..cd23a81da 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -856,6 +856,20 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { }, }, + .pwd_change => |w| { + defer w.deinit(); + + // We always allocate for this because we need to null-terminate. + const str = try self.alloc.dupeZ(u8, w.slice()); + defer self.alloc.free(str); + + try self.rt_app.performAction( + .{ .surface = self }, + .pwd, + .{ .pwd = str }, + ); + }, + .close => self.close(), // Close without confirmation. diff --git a/src/apprt/action.zig b/src/apprt/action.zig index feb2e2ba4..1f954c37c 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -151,6 +151,9 @@ pub const Action = union(Key) { /// Set the title of the target. set_title: SetTitle, + /// The current working directory has changed for the target terminal. + pwd: Pwd, + /// Set the mouse cursor shape. mouse_shape: terminal.MouseShape, @@ -215,6 +218,7 @@ pub const Action = union(Key) { render_inspector, desktop_notification, set_title, + pwd, mouse_shape, mouse_visibility, mouse_over_link, @@ -417,6 +421,21 @@ pub const SetTitle = struct { } }; +pub const Pwd = struct { + pwd: [:0]const u8, + + // Sync with: ghostty_action_set_pwd_s + pub const C = extern struct { + pwd: [*:0]const u8, + }; + + pub fn cval(self: Pwd) C { + return .{ + .pwd = self.pwd.ptr, + }; + } +}; + /// The desktop notification to show. pub const DesktopNotification = struct { title: [:0]const u8, diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 4d45166aa..9f0b2a2a4 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -1428,25 +1428,6 @@ pub const CAPI = struct { return selection.len; } - /// Copies the surface working directory into the provided buffer and - /// returns the copied size. If the buffer is too small, there is no pwd, - /// or there is an error, then 0 is returned. - export fn ghostty_surface_pwd(surface: *Surface, buf: [*]u8, cap: usize) usize { - const pwd_ = surface.core_surface.pwd(global.alloc) catch |err| { - log.warn("error getting pwd err={}", .{err}); - return 0; - }; - const pwd = pwd_ orelse return 0; - defer global.alloc.free(pwd); - - // If the buffer is too small, return no pwd. - if (pwd.len > cap) return 0; - - // Copy into the buffer and return the length - @memcpy(buf[0..pwd.len], pwd); - return pwd.len; - } - /// Tell the surface that it needs to schedule a render export fn ghostty_surface_refresh(surface: *Surface) void { surface.refresh(); diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 638f52bab..54c53139c 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -224,6 +224,7 @@ pub const App = struct { .cell_size, .renderer_health, .color_change, + .pwd, => log.info("unimplemented action={}", .{action}), } } diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index fa73c2436..2f76d8064 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -486,6 +486,7 @@ pub fn performAction( .render_inspector, .renderer_health, .color_change, + .pwd, => log.warn("unimplemented action={}", .{action}), } } diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index 07eb1d466..58faa9633 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -76,6 +76,9 @@ pub const Message = union(enum) { color: terminal.color.RGB, }, + /// The terminal has reported a change in the working directory. + pwd_change: WriteReq, + pub const ReportTitleStyle = enum { csi_21_t, diff --git a/src/termio/message.zig b/src/termio/message.zig index 22b72235b..c88a12f14 100644 --- a/src/termio/message.zig +++ b/src/termio/message.zig @@ -194,9 +194,9 @@ pub fn MessageData(comptime Elem: type, comptime small_size: comptime_int) type } /// Returns a const slice of the data pointed to by this request. - pub fn slice(self: Self) []const Elem { - return switch (self) { - .small => |v| v.data[0..v.len], + pub fn slice(self: *const Self) []const Elem { + return switch (self.*) { + .small => |*v| v.data[0..v.len], .stable => |v| v, .alloc => |v| v.data, }; diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index fa887932a..66c8359bd 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1139,6 +1139,14 @@ pub const StreamHandler = struct { log.debug("terminal pwd: {s}", .{path}); try self.terminal.setPwd(path); + // Report it to the surface. If creating our write request fails + // then we just ignore it. + if (apprt.surface.Message.WriteReq.init(self.alloc, path)) |req| { + self.surfaceMessageWriter(.{ .pwd_change = req }); + } else |err| { + log.warn("error notifying surface of pwd change err={}", .{err}); + } + // If we haven't seen a title, use our pwd as the title. if (!self.seen_title) { try self.changeWindowTitle(path);