From bbb69c8f27273247cc8e838aef5075cc258575d9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 18 Jun 2025 19:50:05 -0700 Subject: [PATCH] macos: NewTerminalIntent returns Terminal, can split --- .../App Intents/NewTerminalIntent.swift | 58 +++++++++++++++-- .../Terminal/BaseTerminalController.swift | 65 ++++++++++++------- 2 files changed, 93 insertions(+), 30 deletions(-) diff --git a/macos/Sources/Features/App Intents/NewTerminalIntent.swift b/macos/Sources/Features/App Intents/NewTerminalIntent.swift index 51b037cca..55f33bd46 100644 --- a/macos/Sources/Features/App Intents/NewTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/NewTerminalIntent.swift @@ -1,5 +1,6 @@ import AppKit import AppIntents +import GhosttyKit /// App intent that allows creating a new terminal window or tab. /// @@ -16,6 +17,12 @@ struct NewTerminalIntent: AppIntent { ) var location: NewTerminalLocation + @Parameter( + title: "Command", + description: "Command to execute instead of the default shell." + ) + var command: String? + @Parameter( title: "Working Directory", description: "The working directory to open in the terminal.", @@ -36,12 +43,14 @@ struct NewTerminalIntent: AppIntent { static var openAppWhenRun = true @MainActor - func perform() async throws -> some IntentResult { + func perform() async throws -> some IntentResult & ReturnsValue { guard let appDelegate = NSApp.delegate as? AppDelegate else { throw GhosttyIntentError.appUnavailable } + let ghostty = appDelegate.ghostty var config = Ghostty.SurfaceConfiguration() + config.command = command // If we were given a working directory then open that directory if let url = workingDirectory?.fileURL { @@ -65,19 +74,38 @@ struct NewTerminalIntent: AppIntent { switch location { case .window: - _ = TerminalController.newWindow( - appDelegate.ghostty, + let newController = TerminalController.newWindow( + ghostty, withBaseConfig: config, withParent: parent?.window) + if let view = newController.surfaceTree.root?.leftmostLeaf() { + return .result(value: TerminalEntity(view)) + } case .tab: - _ = TerminalController.newTab( - appDelegate.ghostty, + let newController = TerminalController.newTab( + ghostty, from: parent?.window, withBaseConfig: config) + if let view = newController?.surfaceTree.root?.leftmostLeaf() { + return .result(value: TerminalEntity(view)) + } + + case .splitLeft, .splitRight, .splitUp, .splitDown: + guard let parent, + let controller = parent.window?.windowController as? BaseTerminalController else { + throw GhosttyIntentError.surfaceNotFound + } + + if let view = controller.newSplit( + at: parent, + direction: location.splitDirection! + ) { + return .result(value: TerminalEntity(view)) + } } - return .result() + return .result(value: .none) } } @@ -86,6 +114,20 @@ struct NewTerminalIntent: AppIntent { enum NewTerminalLocation: String { case tab case window + case splitLeft = "split:left" + case splitRight = "split:right" + case splitUp = "split:up" + case splitDown = "split:down" + + var splitDirection: SplitTree.NewDirection? { + switch self { + case .splitLeft: return .left + case .splitRight: return .right + case .splitUp: return .up + case .splitDown: return .down + default: return nil + } + } } extension NewTerminalLocation: AppEnum { @@ -94,5 +136,9 @@ extension NewTerminalLocation: AppEnum { static var caseDisplayRepresentations: [Self: DisplayRepresentation] = [ .tab: .init(title: "Tab"), .window: .init(title: "Window"), + .splitLeft: .init(title: "Split Left"), + .splitRight: .init(title: "Split Right"), + .splitUp: .init(title: "Split Up"), + .splitDown: .init(title: "Split Down"), ] } diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index bc91b920e..81b7d32b6 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -193,6 +193,46 @@ class BaseTerminalController: NSWindowController, } } + // MARK: Methods + + /// Create a new split. + @discardableResult + func newSplit( + at oldView: Ghostty.SurfaceView, + direction: SplitTree.NewDirection, + baseConfig config: Ghostty.SurfaceConfiguration? = nil + ) -> Ghostty.SurfaceView? { + // We can only create new splits for surfaces in our tree. + guard surfaceTree.root?.node(view: oldView) != nil else { return nil } + + // Create a new surface view + guard let ghostty_app = ghostty.app else { return nil } + let newView = Ghostty.SurfaceView(ghostty_app, baseConfig: config) + + // Do the split + let newTree: SplitTree + do { + newTree = try surfaceTree.insert( + view: newView, + at: oldView, + direction: direction) + } catch { + // If splitting fails for any reason (it should not), then we just log + // and return. The new view we created will be deinitialized and its + // no big deal. + Ghostty.logger.warning("failed to insert split: \(error)") + return nil + } + + replaceSurfaceTree( + newTree, + moveFocusTo: newView, + moveFocusFrom: oldView, + undoAction: "New Split") + + return newView + } + /// Called when the surfaceTree variable changed. /// /// Subclasses should call super first. @@ -477,30 +517,7 @@ class BaseTerminalController: NSWindowController, default: return } - // Create a new surface view - guard let ghostty_app = ghostty.app else { return } - let newView = Ghostty.SurfaceView(ghostty_app, baseConfig: config) - - // Do the split - let newTree: SplitTree - do { - newTree = try surfaceTree.insert( - view: newView, - at: oldView, - direction: splitDirection) - } catch { - // If splitting fails for any reason (it should not), then we just log - // and return. The new view we created will be deinitialized and its - // no big deal. - Ghostty.logger.warning("failed to insert split: \(error)") - return - } - - replaceSurfaceTree( - newTree, - moveFocusTo: newView, - moveFocusFrom: oldView, - undoAction: "New Split") + newSplit(at: oldView, direction: splitDirection, baseConfig: config) } @objc private func ghosttyDidEqualizeSplits(_ notification: Notification) {