diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index a691ce55f..bbb34820f 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -129,6 +129,7 @@ A5E408402E04532C0035FEAC /* CommandEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E4083F2E04532A0035FEAC /* CommandEntity.swift */; }; A5E408432E047D0B0035FEAC /* CommandPaletteIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408422E047D060035FEAC /* CommandPaletteIntent.swift */; }; A5E408452E0483FD0035FEAC /* KeybindIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408442E0483F80035FEAC /* KeybindIntent.swift */; }; + A5E408472E04852B0035FEAC /* InputIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408462E0485270035FEAC /* InputIntent.swift */; }; A5FEB3002ABB69450068369E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FEB2FF2ABB69450068369E /* main.swift */; }; AEE8B3452B9AA39600260C5E /* NSPasteboard+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */; }; C159E81D2B66A06B00FDFE9C /* OSColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */; }; @@ -258,6 +259,7 @@ A5E4083F2E04532A0035FEAC /* CommandEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandEntity.swift; sourceTree = ""; }; A5E408422E047D060035FEAC /* CommandPaletteIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPaletteIntent.swift; sourceTree = ""; }; A5E408442E0483F80035FEAC /* KeybindIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeybindIntent.swift; sourceTree = ""; }; + A5E408462E0485270035FEAC /* InputIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputIntent.swift; sourceTree = ""; }; A5FEB2FF2ABB69450068369E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSPasteboard+Extension.swift"; sourceTree = ""; }; C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSColor+Extension.swift"; sourceTree = ""; }; @@ -626,6 +628,7 @@ A5E4082D2E0237410035FEAC /* NewTerminalIntent.swift */, A5E408332E03200F0035FEAC /* GetTerminalDetailsIntent.swift */, A5E408422E047D060035FEAC /* CommandPaletteIntent.swift */, + A5E408462E0485270035FEAC /* InputIntent.swift */, A5E408442E0483F80035FEAC /* KeybindIntent.swift */, A5E4082F2E0271320035FEAC /* GhosttyIntentError.swift */, ); @@ -819,6 +822,7 @@ A5B4EA852DFE691B0022C3A2 /* NSMenuItem+Extension.swift in Sources */, A5874D992DAD751B00E83852 /* CGS.swift in Sources */, A586366B2DF0A98C00E04A10 /* Array+Extension.swift in Sources */, + A5E408472E04852B0035FEAC /* InputIntent.swift in Sources */, A51544FE2DFB111C009E85D8 /* TitlebarTabsTahoeTerminalWindow.swift in Sources */, A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */, A56D58862ACDDB4100508D2C /* Ghostty.Shell.swift in Sources */, diff --git a/macos/Sources/Features/App Intents/InputIntent.swift b/macos/Sources/Features/App Intents/InputIntent.swift new file mode 100644 index 000000000..46c849c99 --- /dev/null +++ b/macos/Sources/Features/App Intents/InputIntent.swift @@ -0,0 +1,92 @@ +import AppKit +import AppIntents + +/// App intent to input text in a terminal. +struct InputTextIntent: AppIntent { + static var title: LocalizedStringResource = "Input Text to Terminal" + + @Parameter( + title: "Text", + description: "The text to input to the terminal. The text will be inputted as if it was pasted.", + inputOptions: String.IntentInputOptions( + capitalizationType: .none, + multiline: true, + autocorrect: false, + smartQuotes: false, + smartDashes: false + ) + ) + var text: String + + @Parameter( + title: "Terminal", + description: "The terminal to scope this action to." + ) + var terminal: TerminalEntity + + @available(macOS 26.0, *) + static var supportedModes: IntentModes = [.background, .foreground] + + @MainActor + func perform() async throws -> some IntentResult { + guard let surface = terminal.surfaceModel else { + throw GhosttyIntentError.surfaceNotFound + } + + surface.sendText(text) + return .result() + } +} + +/// App intent to trigger a keyboard event. +struct KeyEventIntent: AppIntent { + static var title: LocalizedStringResource = "Send Keyboard Event to Terminal" + static var description = IntentDescription("Simulate a keyboard event. This will not handle text encoding; use the 'Input Text' action for that.") + + @Parameter( + title: "Text", + description: "The key to send to the terminal." + ) + var key: KeyIntentKey + + @Parameter( + title: "Terminal", + description: "The terminal to scope this action to." + ) + var terminal: TerminalEntity + + @available(macOS 26.0, *) + static var supportedModes: IntentModes = [.background, .foreground] + + @MainActor + func perform() async throws -> some IntentResult { + guard let surface = terminal.surfaceModel else { + throw GhosttyIntentError.surfaceNotFound + } + + surface.sendText(text) + return .result() + } +} + +// MARK: TerminalDetail + +enum KeyIntentKey: String { + case title + case workingDirectory + case allContents + case selectedText + case visibleText +} + +extension KeyIntentKey: AppEnum { + static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Terminal Detail") + + static var caseDisplayRepresentations: [Self: DisplayRepresentation] = [ + .title: .init(title: "Title"), + .workingDirectory: .init(title: "Working Directory"), + .allContents: .init(title: "Full Contents"), + .selectedText: .init(title: "Selected Text"), + .visibleText: .init(title: "Visible Text"), + ] +} diff --git a/macos/Sources/Features/App Intents/KeybindIntent.swift b/macos/Sources/Features/App Intents/KeybindIntent.swift index ddb9c489c..adeb64331 100644 --- a/macos/Sources/Features/App Intents/KeybindIntent.swift +++ b/macos/Sources/Features/App Intents/KeybindIntent.swift @@ -1,13 +1,12 @@ import AppKit import AppIntents -/// App intent that invokes a command palette entry. struct KeybindIntent: AppIntent { static var title: LocalizedStringResource = "Invoke a Keybind Action" @Parameter( title: "Terminal", - description: "The terminal to base available commands from." + description: "The terminal to invoke the action on." ) var terminal: TerminalEntity diff --git a/macos/Sources/Ghostty/Ghostty.Input.swift b/macos/Sources/Ghostty/Ghostty.Input.swift index 942ca5973..e18203f65 100644 --- a/macos/Sources/Ghostty/Ghostty.Input.swift +++ b/macos/Sources/Ghostty/Ghostty.Input.swift @@ -208,4 +208,15 @@ extension Ghostty { 0x43: GHOSTTY_KEY_NUMPAD_MULTIPLY, 0x4E: GHOSTTY_KEY_NUMPAD_SUBTRACT, ]; + + /// `ghostty_input_key_e` + enum Key: String { + case undentified + + var cKey: ghostty_input_key_e { + switch self { + case .undentified: GHOSTTY_KEY_UNIDENTIFIED + } + } + } } diff --git a/macos/Sources/Ghostty/Ghostty.Surface.swift b/macos/Sources/Ghostty/Ghostty.Surface.swift index 5560ff3a8..10e699c1f 100644 --- a/macos/Sources/Ghostty/Ghostty.Surface.swift +++ b/macos/Sources/Ghostty/Ghostty.Surface.swift @@ -35,6 +35,19 @@ extension Ghostty { } } + /// Send text to the terminal as if it was typed. This doesn't send the key events so keyboard + /// shortcuts and other encodings do not take effect. + @MainActor + func sendText(_ text: String) { + let len = text.utf8CString.count + if (len == 0) { return } + + text.withCString { ptr in + // len includes the null terminator so we do len - 1 + ghostty_surface_text(surface, ptr, UInt(len - 1)) + } + } + /// Perform a keybinding action. /// /// The action can be any valid keybind parameter. e.g. `keybind = goto_tab:4` diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index fe0851261..2e7cf499b 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -1700,7 +1700,7 @@ extension Ghostty.SurfaceView: NSTextInputClient { func insertText(_ string: Any, replacementRange: NSRange) { // We must have an associated event guard NSApp.currentEvent != nil else { return } - guard let surface = self.surface else { return } + guard let surfaceModel else { return } // We want the string view of the any value var chars = "" @@ -1724,13 +1724,7 @@ extension Ghostty.SurfaceView: NSTextInputClient { return } - let len = chars.utf8CString.count - if (len == 0) { return } - - chars.withCString { ptr in - // len includes the null terminator so we do len - 1 - ghostty_surface_text(surface, ptr, UInt(len - 1)) - } + surfaceModel.sendText(chars) } /// This function needs to exist for two reasons: