diff --git a/include/ghostty.h b/include/ghostty.h index 3d3973084..735b1d467 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -904,6 +904,7 @@ typedef enum { GHOSTTY_ACTION_SEARCH_TOTAL, GHOSTTY_ACTION_SEARCH_SELECTED, GHOSTTY_ACTION_READONLY, + GHOSTTY_ACTION_COPY_TITLE, } ghostty_action_tag_e; typedef union { diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 183dca544..7d593d6df 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -647,6 +647,8 @@ extension Ghostty { case GHOSTTY_ACTION_SHOW_CHILD_EXITED: Ghostty.logger.info("known but unimplemented action action=\(action.tag.rawValue)") return false + case GHOSTTY_ACTION_COPY_TITLE: + return copyTitle(app, target: target) default: Ghostty.logger.warning("unknown action action=\(action.tag.rawValue)") return false @@ -1506,6 +1508,25 @@ extension Ghostty { } } + private static func copyTitle( + _ app: ghostty_app_t, + target: ghostty_target_s) -> Bool { + switch (target.tag) { + case GHOSTTY_TARGET_SURFACE: + guard let surface = target.target.surface else { return false } + guard let surfaceView = self.surfaceView(from: surface) else { return false } + let title = surfaceView.title + if title.isEmpty { return false } + let pasteboard = NSPasteboard.general + pasteboard.clearContents() + pasteboard.setString(title, forType: .string) + return true + + default: + return false + } + } + private static func promptTitle( _ app: ghostty_app_t, target: ghostty_target_s, diff --git a/src/Surface.zig b/src/Surface.zig index 64a995265..9a4a8c3b0 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -5398,20 +5398,11 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool return false; }, - .copy_title_to_clipboard => { - const title = self.rt_surface.getTitle() orelse return false; - if (title.len == 0) return false; - - self.rt_surface.setClipboard(.standard, &.{.{ - .mime = "text/plain", - .data = title, - }}, false) catch |err| { - log.err("error copying title to clipboard err={}", .{err}); - return true; - }; - - return true; - }, + .copy_title_to_clipboard => return try self.rt_app.performAction( + .{ .surface = self }, + .copy_title, + {}, + ), .paste_from_clipboard => return try self.startClipboardRequest( .standard, diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 78f4bef54..766ad7bf7 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -330,6 +330,11 @@ pub const Action = union(Key) { /// The readonly state of the surface has changed. readonly: Readonly, + /// Copy the effective title of the surface to the clipboard. + /// The effective title is the user-overridden title if set, + /// otherwise the terminal-set title. + copy_title, + /// Sync with: ghostty_action_tag_e pub const Key = enum(c_int) { quit, @@ -395,6 +400,7 @@ pub const Action = union(Key) { search_total, search_selected, readonly, + copy_title, }; /// Sync with: ghostty_action_u diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index 403f94599..e362e7f00 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -649,6 +649,8 @@ pub const Application = extern struct { .close_tab => return Action.closeTab(target, value), .close_window => return Action.closeWindow(target), + .copy_title => return Action.copyTitle(target), + .config_change => try Action.configChange( self, target, @@ -1896,6 +1898,21 @@ const Action = struct { } } + pub fn copyTitle(target: apprt.Target) bool { + return switch (target) { + .app => false, + .surface => |v| surface: { + const title = v.rt_surface.surface.getEffectiveTitle() orelse break :surface false; + if (title.len == 0) break :surface false; + v.rt_surface.surface.setClipboard(.standard, &.{.{ + .mime = "text/plain", + .data = title, + }}, false); + break :surface true; + }, + }; + } + pub fn configChange( self: *Application, target: apprt.Target, diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index 26009ef79..4ca54c52c 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -1982,6 +1982,13 @@ pub const Surface = extern struct { return self.private().title; } + /// Returns the effective title: the user-overridden title if set, + /// otherwise the terminal-set title. + pub fn getEffectiveTitle(self: *Self) ?[:0]const u8 { + const priv = self.private(); + return priv.title_override orelse priv.title; + } + /// Set the title for this surface, copies the value. This should always /// be the title as set by the terminal program, not any manually set /// title. For manually set titles see `setTitleOverride`.