diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index e3518cd2b..2fe835303 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -35,6 +35,7 @@ class AppDelegate: NSObject, @IBOutlet private var menuCopy: NSMenuItem? @IBOutlet private var menuPaste: NSMenuItem? + @IBOutlet private var menuPasteSelection: NSMenuItem? @IBOutlet private var menuSelectAll: NSMenuItem? @IBOutlet private var menuToggleVisibility: NSMenuItem? @@ -353,6 +354,7 @@ class AppDelegate: NSObject, syncMenuShortcut(config, action: "copy_to_clipboard", menuItem: self.menuCopy) syncMenuShortcut(config, action: "paste_from_clipboard", menuItem: self.menuPaste) + syncMenuShortcut(config, action: "paste_from_selection", menuItem: self.menuPasteSelection) syncMenuShortcut(config, action: "select_all", menuItem: self.menuSelectAll) syncMenuShortcut(config, action: "toggle_split_zoom", menuItem: self.menuZoomSplit) diff --git a/macos/Sources/App/macOS/MainMenu.xib b/macos/Sources/App/macOS/MainMenu.xib index 7a8e0d894..0a197fe65 100644 --- a/macos/Sources/App/macOS/MainMenu.xib +++ b/macos/Sources/App/macOS/MainMenu.xib @@ -1,8 +1,8 @@ - + - + @@ -31,6 +31,7 @@ + @@ -185,6 +186,12 @@ + + + + + + diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 393c6ef4d..bda6d62bf 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -389,9 +389,9 @@ class BaseTerminalController: NSWindowController, } switch (request) { - case .osc_52_write: + case let .osc_52_write(pasteboard): guard case .confirm = action else { break } - let pb = NSPasteboard.general + let pb = pasteboard ?? NSPasteboard.general pb.declareTypes([.string], owner: nil) pb.setString(cc.contents, forType: .string) case .osc_52_read, .paste: diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index ed140dcd5..3a2510e3b 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -62,7 +62,7 @@ extension Ghostty { // uses to interface with the application runtime environment. var runtime_cfg = ghostty_runtime_config_s( userdata: Unmanaged.passUnretained(self).toOpaque(), - supports_selection_clipboard: false, + supports_selection_clipboard: true, wakeup_cb: { userdata in App.wakeup(userdata) }, action_cb: { app, target, action in App.action(app!, target: target, action: action) }, read_clipboard_cb: { userdata, loc, state in App.readClipboard(userdata, location: loc, state: state) }, @@ -320,13 +320,13 @@ extension Ghostty { let surfaceView = self.surfaceUserdata(from: userdata) guard let surface = surfaceView.surface else { return } - // We only support the standard clipboard - if (location != GHOSTTY_CLIPBOARD_STANDARD) { + // Get our pasteboard + guard let pasteboard = NSPasteboard.ghostty(location) else { return completeClipboardRequest(surface, data: "", state: state) } // Get our string - let str = NSPasteboard.general.getOpinionatedStringContents() ?? "" + let str = pasteboard.getOpinionatedStringContents() ?? "" completeClipboardRequest(surface, data: str, state: state) } @@ -364,14 +364,12 @@ extension Ghostty { static func writeClipboard(_ userdata: UnsafeMutableRawPointer?, string: UnsafePointer?, location: ghostty_clipboard_e, confirm: Bool) { let surface = self.surfaceUserdata(from: userdata) - // We only support the standard clipboard - if (location != GHOSTTY_CLIPBOARD_STANDARD) { return } + guard let pasteboard = NSPasteboard.ghostty(location) else { return } guard let valueStr = String(cString: string!, encoding: .utf8) else { return } if !confirm { - let pb = NSPasteboard.general - pb.declareTypes([.string], owner: nil) - pb.setString(valueStr, forType: .string) + pasteboard.declareTypes([.string], owner: nil) + pasteboard.setString(valueStr, forType: .string) return } @@ -380,7 +378,7 @@ extension Ghostty { object: surface, userInfo: [ Notification.ConfirmClipboardStrKey: valueStr, - Notification.ConfirmClipboardRequestKey: Ghostty.ClipboardRequest.osc_52_write, + Notification.ConfirmClipboardRequestKey: Ghostty.ClipboardRequest.osc_52_write(pasteboard), ] ) } diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index d09100212..deca8f89d 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -159,7 +159,7 @@ extension Ghostty { case osc_52_read /// An application is attempting to write to the clipboard using OSC 52 - case osc_52_write + case osc_52_write(OSPasteboard?) /// The text to show in the clipboard confirmation prompt for a given request type func text() -> String { @@ -188,7 +188,7 @@ extension Ghostty { case GHOSTTY_CLIPBOARD_REQUEST_OSC_52_READ: return .osc_52_read case GHOSTTY_CLIPBOARD_REQUEST_OSC_52_WRITE: - return .osc_52_write + return .osc_52_write(nil) default: return nil } diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index cf4357a8c..c933eb9bf 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -1127,6 +1127,14 @@ extension Ghostty { } } + @IBAction func pasteSelection(_ sender: Any?) { + guard let surface = self.surface else { return } + let action = "paste_from_selection" + if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) { + AppDelegate.logger.warning("action failed action=\(action)") + } + } + @IBAction override func selectAll(_ sender: Any?) { guard let surface = self.surface else { return } let action = "select_all" @@ -1448,3 +1456,19 @@ extension Ghostty.SurfaceView: NSServicesMenuRequestor { return true } } + +// MARK: NSMenuItemValidation + +extension Ghostty.SurfaceView: NSMenuItemValidation { + func validateMenuItem(_ item: NSMenuItem) -> Bool { + switch item.action { + case #selector(pasteSelection): + let pb = NSPasteboard.ghosttySelection + guard let str = pb.getOpinionatedStringContents() else { return false } + return !str.isEmpty + + default: + return true + } + } +} diff --git a/macos/Sources/Helpers/CrossKit.swift b/macos/Sources/Helpers/CrossKit.swift index 5a69b45a3..690e811bb 100644 --- a/macos/Sources/Helpers/CrossKit.swift +++ b/macos/Sources/Helpers/CrossKit.swift @@ -10,6 +10,7 @@ import AppKit typealias OSView = NSView typealias OSColor = NSColor typealias OSSize = NSSize +typealias OSPasteboard = NSPasteboard protocol OSViewRepresentable: NSViewRepresentable where NSViewType == OSViewType { associatedtype OSViewType: NSView @@ -34,6 +35,7 @@ import UIKit typealias OSView = UIView typealias OSColor = UIColor typealias OSSize = CGSize +typealias OSPasteboard = UIPasteboard protocol OSViewRepresentable: UIViewRepresentable { associatedtype OSViewType: UIView diff --git a/macos/Sources/Helpers/NSPasteboard+Extension.swift b/macos/Sources/Helpers/NSPasteboard+Extension.swift index b1755fea0..7794946f4 100644 --- a/macos/Sources/Helpers/NSPasteboard+Extension.swift +++ b/macos/Sources/Helpers/NSPasteboard+Extension.swift @@ -1,6 +1,12 @@ import AppKit +import GhosttyKit extension NSPasteboard { + /// The pasteboard to used for Ghostty selection. + static var ghosttySelection: NSPasteboard = { + NSPasteboard(name: .init("com.mitchellh.ghostty.selection")) + }() + /// Gets the contents of the pasteboard as a string following a specific set of semantics. /// Does these things in order: /// - Tries to get the absolute filesystem path of the file in the pasteboard if there is one. @@ -14,4 +20,18 @@ extension NSPasteboard { } return self.string(forType: .string) } + + /// The pasteboard for the Ghostty enum type. + static func ghostty(_ clipboard: ghostty_clipboard_e) -> NSPasteboard? { + switch (clipboard) { + case GHOSTTY_CLIPBOARD_STANDARD: + return Self.general + + case GHOSTTY_CLIPBOARD_SELECTION: + return Self.ghosttySelection + + default: + return nil + } + } } diff --git a/src/config/Config.zig b/src/config/Config.zig index 2f38676c5..b14f83f64 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1389,13 +1389,10 @@ keybind: Keybinds = .{}, /// and the system clipboard on macOS. Middle-click paste is always enabled /// even if this is `false`. /// -/// The default value is true on Linux and false on macOS. macOS copy on -/// select behavior is not typical for applications so it is disabled by -/// default. On Linux, this is a standard behavior so it is enabled by -/// default. +/// The default value is true on Linux and macOS. @"copy-on-select": CopyOnSelect = switch (builtin.os.tag) { .linux => .true, - .macos => .false, + .macos => .true, else => .false, }, @@ -2749,6 +2746,13 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config { .{ .toggle_fullscreen = {} }, ); + // Selection clipboard paste, matches Terminal.app + try result.keybind.set.put( + alloc, + .{ .key = .{ .translated = .v }, .mods = .{ .super = true, .shift = true } }, + .{ .paste_from_selection = {} }, + ); + // "Natural text editing" keybinds. This forces these keys to go back // to legacy encoding (not fixterms). It seems macOS users more than // others are used to these keys so we set them as defaults. If