macos: NewTerminalIntent returns Terminal, can split

pull/7634/head
Mitchell Hashimoto 2025-06-18 19:50:05 -07:00
parent 683b38f62c
commit bbb69c8f27
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
2 changed files with 93 additions and 30 deletions

View File

@ -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<TerminalEntity?> {
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<Ghostty.SurfaceView>.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"),
]
}

View File

@ -193,6 +193,46 @@ class BaseTerminalController: NSWindowController,
}
}
// MARK: Methods
/// Create a new split.
@discardableResult
func newSplit(
at oldView: Ghostty.SurfaceView,
direction: SplitTree<Ghostty.SurfaceView>.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<Ghostty.SurfaceView>
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<Ghostty.SurfaceView>
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) {