From ab352b5af9694a7cba8e237d0b1b5a507a6e4226 Mon Sep 17 00:00:00 2001 From: Yasu Flores Date: Sun, 21 Dec 2025 20:26:57 -0600 Subject: [PATCH 1/3] macos: Support native actions to move to beginning of document and move to end of document --- .../Sources/Ghostty/SurfaceView_AppKit.swift | 70 +++++++++++-------- 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 455249ff4..817fca191 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -9,7 +9,7 @@ extension Ghostty { /// The NSView implementation for a terminal surface. class SurfaceView: OSView, ObservableObject, Codable, Identifiable { typealias ID = UUID - + /// Unique ID per surface let id: UUID @@ -44,14 +44,14 @@ extension Ghostty { // The hovered URL string @Published var hoverUrl: String? = nil - + // The progress report (if any) @Published var progressReport: Action.ProgressReport? = nil { didSet { // Cancel any existing timer progressReportTimer?.invalidate() progressReportTimer = nil - + // If we have a new progress report, start a timer to remove it after 15 seconds if progressReport != nil { progressReportTimer = Timer.scheduledTimer(withTimeInterval: 15.0, repeats: false) { [weak self] _ in @@ -101,7 +101,7 @@ extension Ghostty { } } } - + // Cancellable for search state needle changes private var searchNeedleCancellable: AnyCancellable? @@ -219,7 +219,7 @@ extension Ghostty { // A timer to fallback to ghost emoji if no title is set within the grace period private var titleFallbackTimer: Timer? - + // Timer to remove progress report after 15 seconds private var progressReportTimer: Timer? @@ -418,7 +418,7 @@ extension Ghostty { // Remove any notifications associated with this surface let identifiers = Array(self.notificationIdentifiers) UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: identifiers) - + // Cancel progress report timer progressReportTimer?.invalidate() } @@ -555,16 +555,16 @@ extension Ghostty { // Add buttons alert.addButton(withTitle: "OK") alert.addButton(withTitle: "Cancel") - + // Make the text field the first responder so it gets focus alert.window.initialFirstResponder = textField - + let completionHandler: (NSApplication.ModalResponse) -> Void = { [weak self] response in guard let self else { return } - + // Check if the user clicked "OK" guard response == .alertFirstButtonReturn else { return } - + // Get the input text let newTitle = textField.stringValue if newTitle.isEmpty { @@ -988,7 +988,7 @@ extension Ghostty { var x = event.scrollingDeltaX var y = event.scrollingDeltaY let precision = event.hasPreciseScrollingDeltas - + if precision { // We do a 2x speed multiplier. This is subjective, it "feels" better to me. x *= 2; @@ -1350,7 +1350,7 @@ extension Ghostty { var key_ev = event.ghosttyKeyEvent(action, translationMods: translationEvent?.modifierFlags) key_ev.composing = composing - + // For text, we only encode UTF8 if we don't have a single control // character. Control characters are encoded by Ghostty itself. // Without this, `ctrl+enter` does the wrong thing. @@ -1509,7 +1509,7 @@ extension Ghostty { AppDelegate.logger.warning("action failed action=\(action)") } } - + @IBAction func find(_ sender: Any?) { guard let surface = self.surface else { return } let action = "start_search" @@ -1517,7 +1517,7 @@ extension Ghostty { AppDelegate.logger.warning("action failed action=\(action)") } } - + @IBAction func findNext(_ sender: Any?) { guard let surface = self.surface else { return } let action = "search:next" @@ -1533,7 +1533,7 @@ extension Ghostty { AppDelegate.logger.warning("action failed action=\(action)") } } - + @IBAction func findHide(_ sender: Any?) { guard let surface = self.surface else { return } let action = "end_search" @@ -1593,7 +1593,7 @@ extension Ghostty { AppDelegate.logger.warning("action failed action=\(action)") } } - + @IBAction func changeTitle(_ sender: Any) { promptTitle() } @@ -1703,7 +1703,7 @@ extension Ghostty { let isUserSetTitle = try container.decodeIfPresent(Bool.self, forKey: .isUserSetTitle) ?? false self.init(app, baseConfig: config, uuid: uuid) - + // Restore the saved title after initialization if let title = savedTitle { self.title = title @@ -1920,6 +1920,16 @@ extension Ghostty.SurfaceView: NSTextInputClient { return } + // Process MacOS native scroll events + switch selector { + case #selector(moveToBeginningOfDocument(_:)): + surfaceModel!.perform(action: "scroll_to_top") + case #selector(moveToEndOfDocument(_:)): + surfaceModel!.perform(action: "scroll_to_bottom") + default: + break + } + print("SEL: \(selector)") } @@ -1960,14 +1970,14 @@ extension Ghostty.SurfaceView: NSServicesMenuRequestor { // The "COMBINATION" bit is key: we might get sent a string (we can handle that) // but get requested an image (we can't handle that at the time of writing this), // so we must bubble up. - + // Types we can receive let receivable: [NSPasteboard.PasteboardType] = [.string, .init("public.utf8-plain-text")] - + // Types that we can send. Currently the same as receivable but I'm separating // this out so we can modify this in the future. let sendable: [NSPasteboard.PasteboardType] = receivable - + // The sendable types that require a selection (currently all) let sendableRequiresSelection = sendable @@ -1984,7 +1994,7 @@ extension Ghostty.SurfaceView: NSServicesMenuRequestor { return super.validRequestor(forSendType: sendType, returnType: returnType) } } - + return self } @@ -2030,7 +2040,7 @@ extension Ghostty.SurfaceView: NSMenuItemValidation { let pb = NSPasteboard.ghosttySelection guard let str = pb.getOpinionatedStringContents() else { return false } return !str.isEmpty - + case #selector(findHide): return searchState != nil @@ -2135,7 +2145,7 @@ extension Ghostty.SurfaceView { override func accessibilitySelectedTextRange() -> NSRange { return selectedRange() } - + /// Returns the currently selected text as a string. /// This allows assistive technologies to read the selected content. override func accessibilitySelectedText() -> String? { @@ -2149,21 +2159,21 @@ extension Ghostty.SurfaceView { let str = String(cString: text.text) return str.isEmpty ? nil : str } - + /// Returns the number of characters in the terminal content. /// This helps assistive technologies understand the size of the content. override func accessibilityNumberOfCharacters() -> Int { let content = cachedScreenContents.get() return content.count } - + /// Returns the visible character range for the terminal. /// For terminals, we typically show all content as visible. override func accessibilityVisibleCharacterRange() -> NSRange { let content = cachedScreenContents.get() return NSRange(location: 0, length: content.count) } - + /// Returns the line number for a given character index. /// This helps assistive technologies navigate by line. override func accessibilityLine(for index: Int) -> Int { @@ -2171,7 +2181,7 @@ extension Ghostty.SurfaceView { let substring = String(content.prefix(index)) return substring.components(separatedBy: .newlines).count - 1 } - + /// Returns a substring for the given range. /// This allows assistive technologies to read specific portions of the content. override func accessibilityString(for range: NSRange) -> String? { @@ -2179,7 +2189,7 @@ extension Ghostty.SurfaceView { guard let swiftRange = Range(range, in: content) else { return nil } return String(content[swiftRange]) } - + /// Returns an attributed string for the given range. /// /// Note: right now this only applies font information. One day it'd be nice to extend @@ -2190,9 +2200,9 @@ extension Ghostty.SurfaceView { override func accessibilityAttributedString(for range: NSRange) -> NSAttributedString? { guard let surface = self.surface else { return nil } guard let plainString = accessibilityString(for: range) else { return nil } - + var attributes: [NSAttributedString.Key: Any] = [:] - + // Try to get the font from the surface if let fontRaw = ghostty_surface_quicklook_font(surface) { let font = Unmanaged.fromOpaque(fontRaw) From 2215b731da23013324b242fbb28f00b230441770 Mon Sep 17 00:00:00 2001 From: Yasu Flores Date: Sun, 21 Dec 2025 20:47:56 -0600 Subject: [PATCH 2/3] Address warning and add guard clause --- macos/Sources/Ghostty/SurfaceView_AppKit.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 817fca191..c54f674a5 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -1910,6 +1910,7 @@ extension Ghostty.SurfaceView: NSTextInputClient { /// 1. Prevents an audible NSBeep for unimplemented actions. /// 2. Allows us to properly encode super+key input events that we don't handle override func doCommand(by selector: Selector) { + guard let surfaceModel else { return } // If we are being processed by performKeyEquivalent with a command binding, // we send it back through the event system so it can be encoded. if let lastPerformKeyEvent, @@ -1923,9 +1924,9 @@ extension Ghostty.SurfaceView: NSTextInputClient { // Process MacOS native scroll events switch selector { case #selector(moveToBeginningOfDocument(_:)): - surfaceModel!.perform(action: "scroll_to_top") + _ = surfaceModel.perform(action: "scroll_to_top") case #selector(moveToEndOfDocument(_:)): - surfaceModel!.perform(action: "scroll_to_bottom") + _ = surfaceModel.perform(action: "scroll_to_bottom") default: break } From 5bd814adf8b2fad4f7d8ca7c05776c8dcb6cd35a Mon Sep 17 00:00:00 2001 From: Yasu Flores Date: Mon, 22 Dec 2025 08:53:43 -0600 Subject: [PATCH 3/3] move guard down to keep surfaceModel logic together --- macos/Sources/Ghostty/SurfaceView_AppKit.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index c54f674a5..37cc9282e 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -1910,7 +1910,6 @@ extension Ghostty.SurfaceView: NSTextInputClient { /// 1. Prevents an audible NSBeep for unimplemented actions. /// 2. Allows us to properly encode super+key input events that we don't handle override func doCommand(by selector: Selector) { - guard let surfaceModel else { return } // If we are being processed by performKeyEquivalent with a command binding, // we send it back through the event system so it can be encoded. if let lastPerformKeyEvent, @@ -1921,6 +1920,7 @@ extension Ghostty.SurfaceView: NSTextInputClient { return } + guard let surfaceModel else { return } // Process MacOS native scroll events switch selector { case #selector(moveToBeginningOfDocument(_:)):