mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-17 01:06: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);
|
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);
|
void ghostty_surface_complete_clipboard_request(ghostty_surface_t, const char *, void *, bool);
|
||||||
uintptr_t ghostty_surface_pwd(ghostty_surface_t, char *, uintptr_t);
|
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);
|
ghostty_inspector_t ghostty_surface_inspector(ghostty_surface_t);
|
||||||
void ghostty_inspector_free(ghostty_surface_t);
|
void ghostty_inspector_free(ghostty_surface_t);
|
||||||
|
@ -17,7 +17,8 @@ class AppDelegate: NSObject,
|
|||||||
category: String(describing: AppDelegate.self)
|
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 menuCheckForUpdates: NSMenuItem?
|
||||||
@IBOutlet private var menuOpenConfig: NSMenuItem?
|
@IBOutlet private var menuOpenConfig: NSMenuItem?
|
||||||
@IBOutlet private var menuReloadConfig: NSMenuItem?
|
@IBOutlet private var menuReloadConfig: NSMenuItem?
|
||||||
@ -108,9 +109,11 @@ class AppDelegate: NSObject,
|
|||||||
// Initial config loading
|
// Initial config loading
|
||||||
configDidReload(ghostty)
|
configDidReload(ghostty)
|
||||||
|
|
||||||
// Register our service provider. This must happen after everything
|
// Register our service provider. This must happen after everything is initialized.
|
||||||
// else is initialized.
|
|
||||||
NSApp.servicesProvider = ServiceProvider()
|
NSApp.servicesProvider = ServiceProvider()
|
||||||
|
|
||||||
|
// This registers the Ghostty => Services menu to exist.
|
||||||
|
NSApp.servicesMenu = menuServices
|
||||||
|
|
||||||
// Configure user notifications
|
// Configure user notifications
|
||||||
let actions = [
|
let actions = [
|
||||||
|
@ -40,6 +40,7 @@
|
|||||||
<outlet property="menuSelectSplitBelow" destination="QDz-d9-CBr" id="FsH-Dq-jij"/>
|
<outlet property="menuSelectSplitBelow" destination="QDz-d9-CBr" id="FsH-Dq-jij"/>
|
||||||
<outlet property="menuSelectSplitLeft" destination="cTK-oy-KuV" id="Jpr-5q-dqz"/>
|
<outlet property="menuSelectSplitLeft" destination="cTK-oy-KuV" id="Jpr-5q-dqz"/>
|
||||||
<outlet property="menuSelectSplitRight" destination="upj-mc-L7X" id="nLY-o1-lky"/>
|
<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="menuSplitHorizontal" destination="VUR-Ld-nLx" id="RxO-Zw-ovb"/>
|
||||||
<outlet property="menuSplitVertical" destination="UDZ-4y-6xL" id="fgZ-Wb-8OR"/>
|
<outlet property="menuSplitVertical" destination="UDZ-4y-6xL" id="fgZ-Wb-8OR"/>
|
||||||
<outlet property="menuTerminalInspector" destination="QwP-M5-fvh" id="wJi-Dh-S9f"/>
|
<outlet property="menuTerminalInspector" destination="QwP-M5-fvh" id="wJi-Dh-S9f"/>
|
||||||
@ -76,6 +77,11 @@
|
|||||||
</connections>
|
</connections>
|
||||||
</menuItem>
|
</menuItem>
|
||||||
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
|
<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">
|
<menuItem title="Hide Ghostty" keyEquivalent="h" id="Olw-nP-bQN">
|
||||||
<connections>
|
<connections>
|
||||||
<action selector="hide:" target="-1" id="PnN-Uc-m68"/>
|
<action selector="hide:" target="-1" id="PnN-Uc-m68"/>
|
||||||
|
@ -953,3 +953,60 @@ extension Ghostty.SurfaceView: NSTextInputClient {
|
|||||||
print("SEL: \(selector)")
|
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
|
/// 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
|
/// the pwd can change at any point from termio. If we are calling from the IO
|
||||||
/// thread you should just check the terminal directly.
|
/// thread you should just check the terminal directly.
|
||||||
|
@ -1438,6 +1438,30 @@ pub const CAPI = struct {
|
|||||||
return surface.core_surface.needsConfirmQuit();
|
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
|
/// Copies the surface working directory into the provided buffer and
|
||||||
/// returns the copied size. If the buffer is too small, there is no pwd,
|
/// returns the copied size. If the buffer is too small, there is no pwd,
|
||||||
/// or there is an error, then 0 is returned.
|
/// or there is an error, then 0 is returned.
|
||||||
|
Reference in New Issue
Block a user