From a581955b9b7622f07e94b1736d2a26511a12e0fa Mon Sep 17 00:00:00 2001 From: Aswin M Prabhu Date: Wed, 1 Jan 2025 01:01:42 +0530 Subject: [PATCH] Add tab title rename feature to macos --- include/ghostty.h | 1 + macos/Sources/App/macOS/AppDelegate.swift | 2 + macos/Sources/App/macOS/MainMenu.xib | 8 +++ macos/Sources/Ghostty/Ghostty.App.swift | 23 ++++++++ .../Sources/Ghostty/SurfaceView_AppKit.swift | 55 +++++++++++++++++++ src/Surface.zig | 6 ++ src/apprt/action.zig | 7 ++- src/apprt/glfw.zig | 1 + src/apprt/gtk/App.zig | 1 + src/input/Binding.zig | 5 ++ 10 files changed, 108 insertions(+), 1 deletion(-) diff --git a/include/ghostty.h b/include/ghostty.h index c71831efe..86de4266d 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -586,6 +586,7 @@ typedef enum { GHOSTTY_ACTION_RENDER_INSPECTOR, GHOSTTY_ACTION_DESKTOP_NOTIFICATION, GHOSTTY_ACTION_SET_TITLE, + GHOSTTY_ACTION_PROMPT_TITLE, GHOSTTY_ACTION_PWD, GHOSTTY_ACTION_MOUSE_SHAPE, GHOSTTY_ACTION_MOUSE_VISIBILITY, diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 09a86de1f..dc3a03e9d 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -52,6 +52,7 @@ class AppDelegate: NSObject, @IBOutlet private var menuIncreaseFontSize: NSMenuItem? @IBOutlet private var menuDecreaseFontSize: NSMenuItem? @IBOutlet private var menuResetFontSize: NSMenuItem? + @IBOutlet private var menuChangeTitle: NSMenuItem? @IBOutlet private var menuQuickTerminal: NSMenuItem? @IBOutlet private var menuTerminalInspector: NSMenuItem? @@ -384,6 +385,7 @@ class AppDelegate: NSObject, syncMenuShortcut(config, action: "increase_font_size:1", menuItem: self.menuIncreaseFontSize) syncMenuShortcut(config, action: "decrease_font_size:1", menuItem: self.menuDecreaseFontSize) syncMenuShortcut(config, action: "reset_font_size", menuItem: self.menuResetFontSize) + syncMenuShortcut(config, action: "change_title_prompt", menuItem: self.menuChangeTitle) syncMenuShortcut(config, action: "toggle_quick_terminal", menuItem: self.menuQuickTerminal) syncMenuShortcut(config, action: "toggle_visibility", menuItem: self.menuToggleVisibility) syncMenuShortcut(config, action: "inspector:toggle", menuItem: self.menuTerminalInspector) diff --git a/macos/Sources/App/macOS/MainMenu.xib b/macos/Sources/App/macOS/MainMenu.xib index 4a01d5c62..22211cabe 100644 --- a/macos/Sources/App/macOS/MainMenu.xib +++ b/macos/Sources/App/macOS/MainMenu.xib @@ -14,6 +14,7 @@ + @@ -232,6 +233,13 @@ + + + + + + + diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 968debd09..ba249e3d1 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -484,6 +484,9 @@ extension Ghostty { case GHOSTTY_ACTION_SET_TITLE: setTitle(app, target: target, v: action.action.set_title) + case GHOSTTY_ACTION_PROMPT_TITLE: + return promptTitle(app, target: target) + case GHOSTTY_ACTION_PWD: pwdChanged(app, target: target, v: action.action.pwd) @@ -1007,6 +1010,26 @@ extension Ghostty { } } + private static func promptTitle( + _ app: ghostty_app_t, + target: ghostty_target_s) -> Bool { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("set title prompt does nothing with an app target") + return false + + case GHOSTTY_TARGET_SURFACE: + guard let surface = target.target.surface else { return false } + guard let surfaceView = self.surfaceView(from: surface) else { return false } + surfaceView.promptTitle() + + default: + assertionFailure() + } + + return true + } + private static func pwdChanged( _ app: ghostty_app_t, target: ghostty_target_s, diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 0adc11fa4..daa2ccfde 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -124,6 +124,11 @@ extension Ghostty { // A timer to fallback to ghost emoji if no title is set within the grace period private var titleFallbackTimer: Timer? + // This is the title from the terminal. This is nil if we're currently using + // the terminal title as the main title property. If the title is set manually + // by the user, this is set to the prior value (which may be empty, but non-nil). + private var titleFromTerminal: String? + /// Event monitor (see individual events for why) private var eventMonitor: Any? = nil @@ -380,6 +385,45 @@ extension Ghostty { NSCursor.setHiddenUntilMouseMoves(!visible) } + /// Set the title by prompting the user. + func promptTitle() { + // Create an alert dialog + let alert = NSAlert() + alert.messageText = "Change Terminal Title" + alert.informativeText = "Leave blank to restore the default." + alert.alertStyle = .informational + + // Add a text field to the alert + let textField = NSTextField(frame: NSRect(x: 0, y: 0, width: 250, height: 24)) + textField.stringValue = title + alert.accessoryView = textField + + // Add buttons + alert.addButton(withTitle: "OK") + alert.addButton(withTitle: "Cancel") + + let response = alert.runModal() + + // Check if the user clicked "OK" + if response == .alertFirstButtonReturn { + // Get the input text + let newTitle = textField.stringValue + + if newTitle.isEmpty { + // Empty means that user wants the title to be set automatically + // We also need to reload the config for the "title" property to be + // used again by this tab. + let prevTitle = titleFromTerminal ?? "👻" + titleFromTerminal = nil + setTitle(prevTitle) + } else { + // Set the title and prevent it from being changed automatically + titleFromTerminal = title + title = newTitle + } + } + } + func setTitle(_ title: String) { // This fixes an issue where very quick changes to the title could // cause an unpleasant flickering. We set a timer so that we can @@ -390,6 +434,11 @@ extension Ghostty { withTimeInterval: 0.075, repeats: false ) { [weak self] _ in + // Set the title if it wasn't manually set. + guard self?.titleFromTerminal == nil else { + self?.titleFromTerminal = title + return + } self?.title = title } } @@ -1117,6 +1166,8 @@ extension Ghostty { menu.addItem(.separator()) menu.addItem(withTitle: "Reset Terminal", action: #selector(resetTerminal(_:)), keyEquivalent: "") menu.addItem(withTitle: "Toggle Terminal Inspector", action: #selector(toggleTerminalInspector(_:)), keyEquivalent: "") + menu.addItem(.separator()) + menu.addItem(withTitle: "Change Title...", action: #selector(changeTitle(_:)), keyEquivalent: "") return menu } @@ -1189,6 +1240,10 @@ extension Ghostty { AppDelegate.logger.warning("action failed action=\(action)") } } + + @IBAction func changeTitle(_ sender: Any) { + promptTitle() + } /// Show a user notification and associate it with this surface func showUserNotification(title: String, body: String) { diff --git a/src/Surface.zig b/src/Surface.zig index 4f340ee41..b81a45ecb 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -4024,6 +4024,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool try self.setFontSize(size); }, + .prompt_surface_title => return try self.rt_app.performAction( + .{ .surface = self }, + .prompt_title, + {}, + ), + .clear_screen => { // This is a duplicate of some of the logic in termio.clearScreen // but we need to do this here so we can know the answer before diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 78f55c8a5..20b86707e 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -158,9 +158,13 @@ pub const Action = union(Key) { /// Show a desktop notification. desktop_notification: DesktopNotification, - /// Set the title of the target. + /// Set the title of the target to the requested value. set_title: SetTitle, + /// Set the title of the target to a prompted value. It is up to + /// the apprt to prompt. + prompt_title, + /// The current working directory has changed for the target terminal. pwd: Pwd, @@ -254,6 +258,7 @@ pub const Action = union(Key) { render_inspector, desktop_notification, set_title, + prompt_title, pwd, mouse_shape, mouse_visibility, diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 39c6e058c..531269ee1 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -239,6 +239,7 @@ pub const App = struct { .pwd, .config_change, .toggle_maximize, + .prompt_title, => { log.info("unimplemented action={}", .{action}); return false; diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index f9a3ab160..985ce92b3 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -560,6 +560,7 @@ pub fn performAction( .render_inspector, .renderer_health, .color_change, + .prompt_title, => { log.warn("unimplemented action={}", .{action}); return false; diff --git a/src/input/Binding.zig b/src/input/Binding.zig index bef2ef613..f91967293 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -348,6 +348,10 @@ pub const Action = union(enum) { /// This only works with libadwaita enabled currently. toggle_tab_overview: void, + /// Change the title of the current focused surface via a prompt. + /// This only works on macOS currently. + prompt_surface_title: void, + /// Create a new split in the given direction. /// /// Arguments: @@ -748,6 +752,7 @@ pub const Action = union(enum) { .increase_font_size, .decrease_font_size, .reset_font_size, + .prompt_surface_title, .clear_screen, .select_all, .scroll_to_top,