From 8b11d20cb0eb5323e224d6233d72cbbc867c8528 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 21 Jan 2024 16:39:52 -0800 Subject: [PATCH 1/4] macos: register a services menu --- macos/Sources/App/macOS/AppDelegate.swift | 4 +++- macos/Sources/App/macOS/MainMenu.xib | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 1aa27387e..f4dd9f1cb 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -17,7 +17,8 @@ class AppDelegate: NSObject, category: String(describing: AppDelegate.self) ) - /// Various menu items so that we can programmatically sync the keyboard shortcut with the Ghostty config. + /// Various menu items so that we can programmatically sync the keyboard shortcut with the Ghostty config + @IBOutlet private var menuServices: NSMenu? @IBOutlet private var menuCheckForUpdates: NSMenuItem? @IBOutlet private var menuOpenConfig: NSMenuItem? @IBOutlet private var menuReloadConfig: NSMenuItem? @@ -111,6 +112,7 @@ class AppDelegate: NSObject, // Register our service provider. This must happen after everything // else is initialized. NSApp.servicesProvider = ServiceProvider() + NSApp.servicesMenu = menuServices // Configure user notifications let actions = [ diff --git a/macos/Sources/App/macOS/MainMenu.xib b/macos/Sources/App/macOS/MainMenu.xib index 2e6041bed..ca9cd1f35 100644 --- a/macos/Sources/App/macOS/MainMenu.xib +++ b/macos/Sources/App/macOS/MainMenu.xib @@ -40,6 +40,7 @@ + @@ -76,6 +77,11 @@ + + + + + From 4c9fc452b6a2053f708617e1c95abe44199e6615 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 21 Jan 2024 17:07:24 -0800 Subject: [PATCH 2/4] macos: register that we accept/send text types for services --- macos/Sources/App/macOS/AppDelegate.swift | 5 ++- .../Sources/Ghostty/SurfaceView_AppKit.swift | 39 +++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index f4dd9f1cb..d3d3e1f4b 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -109,9 +109,10 @@ class AppDelegate: NSObject, // Initial config loading configDidReload(ghostty) - // Register our service provider. This must happen after everything - // else is initialized. + // Register our service provider. This must happen after everything is initialized. NSApp.servicesProvider = ServiceProvider() + + // This registers the Ghostty => Services menu to exist. NSApp.servicesMenu = menuServices // Configure user notifications diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index fb1915fc1..1938ffecd 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -953,3 +953,42 @@ extension Ghostty.SurfaceView: NSTextInputClient { print("SEL: \(selector)") } } + +// MARK: Services + +// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/SysServices/Articles/using.html +extension Ghostty.SurfaceView: NSServicesMenuRequestor { + override func validRequestor( + forSendType sendType: NSPasteboard.PasteboardType?, + returnType: NSPasteboard.PasteboardType? + ) -> Any? { + // Types that we accept sent to us + let accepted: [NSPasteboard.PasteboardType] = [.string, .init("public.utf8-plain-text")] + + // We can always receive the accepted types + if (returnType == nil || accepted.contains(returnType!)) { + return self + } + + // If we have a selection we can send the accepted types too + // TODO selection + if (sendType == nil || accepted.contains(sendType!)) { + return self + } + + return super.validRequestor(forSendType: sendType, returnType: returnType) + } + + func writeSelection( + to pboard: NSPasteboard, + types: [NSPasteboard.PasteboardType] + ) -> Bool { + // TODO + return false + } + + func readSelection(from pboard: NSPasteboard) -> Bool { + // TODO + return false + } +} From 4dbd10c913e0f39daf70dc98c4c0cd5437568922 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 21 Jan 2024 17:22:44 -0800 Subject: [PATCH 3/4] apprt/embedded: support asking for selection text, existence --- include/ghostty.h | 2 ++ .../Sources/Ghostty/SurfaceView_AppKit.swift | 18 ++++++++++---- src/Surface.zig | 15 ++++++++++++ src/apprt/embedded.zig | 24 +++++++++++++++++++ 4 files changed, 55 insertions(+), 4 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index a6283170d..cef635867 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -489,6 +489,8 @@ void ghostty_surface_split_equalize(ghostty_surface_t); bool ghostty_surface_binding_action(ghostty_surface_t, const char *, uintptr_t); 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); ghostty_inspector_t ghostty_surface_inspector(ghostty_surface_t); void ghostty_inspector_free(ghostty_surface_t); diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 1938ffecd..8663c922a 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -971,8 +971,9 @@ extension Ghostty.SurfaceView: NSServicesMenuRequestor { } // If we have a selection we can send the accepted types too - // TODO selection - if (sendType == nil || accepted.contains(sendType!)) { + if ((self.surface != nil && ghostty_surface_has_selection(self.surface)) && + (sendType == nil || accepted.contains(sendType!)) + ) { return self } @@ -983,8 +984,17 @@ extension Ghostty.SurfaceView: NSServicesMenuRequestor { to pboard: NSPasteboard, types: [NSPasteboard.PasteboardType] ) -> Bool { - // TODO - return false + guard let surface = self.surface else { return false } + + // We currently cap the maximum copy size to 1MB. iTerm2 I believe + // caps theirs at 0.1MB (configurable) so this is probably reasonable. + let v = String(unsafeUninitializedCapacity: 1000000) { + Int(ghostty_surface_selection(surface, $0.baseAddress, UInt($0.count))) + } + + pboard.declareTypes([.string], owner: nil) + pboard.setString(v, forType: .string) + return true } func readSelection(from pboard: NSPasteboard) -> Bool { diff --git a/src/Surface.zig b/src/Surface.zig index ada0dfd7f..4b6d151dd 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -865,6 +865,21 @@ fn changeConfig(self: *Surface, config: *const configpkg.Config) !void { }; } +/// Returns true if the terminal has a selection. +pub fn hasSelection(self: *const Surface) bool { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + return self.io.terminal.screen.selection != null; +} + +/// Returns the selected text. This is allocated. +pub fn selectionString(self: *Surface, alloc: Allocator) !?[]const u8 { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + const sel = self.io.terminal.screen.selection orelse return null; + return try self.io.terminal.screen.selectionString(alloc, sel, false); +} + /// Returns the pwd of the terminal, if any. This is always copied because /// the pwd can change at any point from termio. If we are calling from the IO /// thread you should just check the terminal directly. diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 6b631de17..d8e915550 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -1438,6 +1438,30 @@ pub const CAPI = struct { return surface.core_surface.needsConfirmQuit(); } + /// Returns true if the surface has a selection. + export fn ghostty_surface_has_selection(surface: *Surface) bool { + return surface.core_surface.hasSelection(); + } + + /// Copies the surface selection text into the provided buffer and + /// returns the copied size. If the buffer is too small, there is no + /// selection, or there is an error, then 0 is returned. + export fn ghostty_surface_selection(surface: *Surface, buf: [*]u8, cap: usize) usize { + const selection_ = surface.core_surface.selectionString(global.alloc) catch |err| { + log.warn("error getting selection err={}", .{err}); + return 0; + }; + const selection = selection_ orelse return 0; + defer global.alloc.free(selection); + + // If the buffer is too small, return no selection. + if (selection.len > cap) return 0; + + // Copy into the buffer and return the length + @memcpy(buf[0..selection.len], selection); + 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. From 81532c0b5696b75c93e448f289b97260d40dd9d1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 21 Jan 2024 17:26:41 -0800 Subject: [PATCH 4/4] macos: support reading service result text into terminal --- macos/Sources/Ghostty/SurfaceView_AppKit.swift | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 8663c922a..251ddeaa8 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -998,7 +998,15 @@ extension Ghostty.SurfaceView: NSServicesMenuRequestor { } func readSelection(from pboard: NSPasteboard) -> Bool { - // TODO - return false + guard let str = pboard.string(forType: .string) else { return false } + + let len = str.utf8CString.count + if (len == 0) { return true } + str.withCString { ptr in + // len includes the null terminator so we do len - 1 + ghostty_surface_text(surface, ptr, UInt(len - 1)) + } + + return true } }