mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 08:46:08 +03:00
Merge pull request #1351 from mitchellh/macos-svc
macOS: implement read/write terminal text for services
This commit is contained in:
@ -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);
|
||||
|
@ -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?
|
||||
@ -108,9 +109,11 @@ 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
|
||||
let actions = [
|
||||
|
@ -40,6 +40,7 @@
|
||||
<outlet property="menuSelectSplitBelow" destination="QDz-d9-CBr" id="FsH-Dq-jij"/>
|
||||
<outlet property="menuSelectSplitLeft" destination="cTK-oy-KuV" id="Jpr-5q-dqz"/>
|
||||
<outlet property="menuSelectSplitRight" destination="upj-mc-L7X" id="nLY-o1-lky"/>
|
||||
<outlet property="menuServices" destination="aQe-vS-j8Q" id="uWQ-Wo-T1L"/>
|
||||
<outlet property="menuSplitHorizontal" destination="VUR-Ld-nLx" id="RxO-Zw-ovb"/>
|
||||
<outlet property="menuSplitVertical" destination="UDZ-4y-6xL" id="fgZ-Wb-8OR"/>
|
||||
<outlet property="menuTerminalInspector" destination="QwP-M5-fvh" id="wJi-Dh-S9f"/>
|
||||
@ -76,6 +77,11 @@
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
|
||||
<menuItem title="Services" id="rJe-5J-bwL">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Services" id="aQe-vS-j8Q"/>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="qno-yg-pur"/>
|
||||
<menuItem title="Hide Ghostty" keyEquivalent="h" id="Olw-nP-bQN">
|
||||
<connections>
|
||||
<action selector="hide:" target="-1" id="PnN-Uc-m68"/>
|
||||
|
@ -953,3 +953,60 @@ 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
|
||||
if ((self.surface != nil && ghostty_surface_has_selection(self.surface)) &&
|
||||
(sendType == nil || accepted.contains(sendType!))
|
||||
) {
|
||||
return self
|
||||
}
|
||||
|
||||
return super.validRequestor(forSendType: sendType, returnType: returnType)
|
||||
}
|
||||
|
||||
func writeSelection(
|
||||
to pboard: NSPasteboard,
|
||||
types: [NSPasteboard.PasteboardType]
|
||||
) -> Bool {
|
||||
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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
Reference in New Issue
Block a user