macos: InvokeCommandPaletteIntent and CommandEntity
parent
5259d0fa55
commit
14e46d0979
|
|
@ -126,6 +126,8 @@
|
||||||
A5E408382E03C7DA0035FEAC /* Ghostty.Surface.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408372E03C7D80035FEAC /* Ghostty.Surface.swift */; };
|
A5E408382E03C7DA0035FEAC /* Ghostty.Surface.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408372E03C7D80035FEAC /* Ghostty.Surface.swift */; };
|
||||||
A5E4083A2E0449BD0035FEAC /* Ghostty.Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408392E0449BB0035FEAC /* Ghostty.Command.swift */; };
|
A5E4083A2E0449BD0035FEAC /* Ghostty.Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408392E0449BB0035FEAC /* Ghostty.Command.swift */; };
|
||||||
A5E4083C2E044DB50035FEAC /* Ghostty.Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E4083B2E044DB40035FEAC /* Ghostty.Error.swift */; };
|
A5E4083C2E044DB50035FEAC /* Ghostty.Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E4083B2E044DB40035FEAC /* Ghostty.Error.swift */; };
|
||||||
|
A5E408402E04532C0035FEAC /* CommandEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E4083F2E04532A0035FEAC /* CommandEntity.swift */; };
|
||||||
|
A5E408432E047D0B0035FEAC /* CommandPaletteIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408422E047D060035FEAC /* CommandPaletteIntent.swift */; };
|
||||||
A5FEB3002ABB69450068369E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FEB2FF2ABB69450068369E /* main.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 */; };
|
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 */; };
|
C159E81D2B66A06B00FDFE9C /* OSColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */; };
|
||||||
|
|
@ -252,6 +254,8 @@
|
||||||
A5E408372E03C7D80035FEAC /* Ghostty.Surface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Surface.swift; sourceTree = "<group>"; };
|
A5E408372E03C7D80035FEAC /* Ghostty.Surface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Surface.swift; sourceTree = "<group>"; };
|
||||||
A5E408392E0449BB0035FEAC /* Ghostty.Command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Command.swift; sourceTree = "<group>"; };
|
A5E408392E0449BB0035FEAC /* Ghostty.Command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Command.swift; sourceTree = "<group>"; };
|
||||||
A5E4083B2E044DB40035FEAC /* Ghostty.Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Error.swift; sourceTree = "<group>"; };
|
A5E4083B2E044DB40035FEAC /* Ghostty.Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Error.swift; sourceTree = "<group>"; };
|
||||||
|
A5E4083F2E04532A0035FEAC /* CommandEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandEntity.swift; sourceTree = "<group>"; };
|
||||||
|
A5E408422E047D060035FEAC /* CommandPaletteIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPaletteIntent.swift; sourceTree = "<group>"; };
|
||||||
A5FEB2FF2ABB69450068369E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
|
A5FEB2FF2ABB69450068369E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
|
||||||
AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSPasteboard+Extension.swift"; sourceTree = "<group>"; };
|
AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSPasteboard+Extension.swift"; sourceTree = "<group>"; };
|
||||||
C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSColor+Extension.swift"; sourceTree = "<group>"; };
|
C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSColor+Extension.swift"; sourceTree = "<group>"; };
|
||||||
|
|
@ -616,14 +620,24 @@
|
||||||
A5E4082C2E0237270035FEAC /* App Intents */ = {
|
A5E4082C2E0237270035FEAC /* App Intents */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
A5E408312E02FEDC0035FEAC /* TerminalEntity.swift */,
|
A5E408412E0453370035FEAC /* Entities */,
|
||||||
A5E4082D2E0237410035FEAC /* NewTerminalIntent.swift */,
|
A5E4082D2E0237410035FEAC /* NewTerminalIntent.swift */,
|
||||||
A5E408332E03200F0035FEAC /* GetTerminalDetailsIntent.swift */,
|
A5E408332E03200F0035FEAC /* GetTerminalDetailsIntent.swift */,
|
||||||
|
A5E408422E047D060035FEAC /* CommandPaletteIntent.swift */,
|
||||||
A5E4082F2E0271320035FEAC /* GhosttyIntentError.swift */,
|
A5E4082F2E0271320035FEAC /* GhosttyIntentError.swift */,
|
||||||
);
|
);
|
||||||
path = "App Intents";
|
path = "App Intents";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
A5E408412E0453370035FEAC /* Entities */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
A5E408312E02FEDC0035FEAC /* TerminalEntity.swift */,
|
||||||
|
A5E4083F2E04532A0035FEAC /* CommandEntity.swift */,
|
||||||
|
);
|
||||||
|
path = Entities;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
|
|
@ -750,6 +764,7 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
A5AEB1652D5BE7D000513529 /* LastWindowPosition.swift in Sources */,
|
A5AEB1652D5BE7D000513529 /* LastWindowPosition.swift in Sources */,
|
||||||
|
A5E408432E047D0B0035FEAC /* CommandPaletteIntent.swift in Sources */,
|
||||||
A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */,
|
A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */,
|
||||||
A54B0CEB2D0CFB4C00CBEFF8 /* NSImage+Extension.swift in Sources */,
|
A54B0CEB2D0CFB4C00CBEFF8 /* NSImage+Extension.swift in Sources */,
|
||||||
A5874D9D2DAD786100E83852 /* NSWindow+Extension.swift in Sources */,
|
A5874D9D2DAD786100E83852 /* NSWindow+Extension.swift in Sources */,
|
||||||
|
|
@ -827,6 +842,7 @@
|
||||||
A599CDB02CF103F60049FA26 /* NSAppearance+Extension.swift in Sources */,
|
A599CDB02CF103F60049FA26 /* NSAppearance+Extension.swift in Sources */,
|
||||||
A52FFF572CA90484000C6A5B /* QuickTerminalScreen.swift in Sources */,
|
A52FFF572CA90484000C6A5B /* QuickTerminalScreen.swift in Sources */,
|
||||||
A5CC36132C9CD72D004D6760 /* SecureInputOverlay.swift in Sources */,
|
A5CC36132C9CD72D004D6760 /* SecureInputOverlay.swift in Sources */,
|
||||||
|
A5E408402E04532C0035FEAC /* CommandEntity.swift in Sources */,
|
||||||
A5E4082A2E022E9E0035FEAC /* TabGroupCloseCoordinator.swift in Sources */,
|
A5E4082A2E022E9E0035FEAC /* TabGroupCloseCoordinator.swift in Sources */,
|
||||||
A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */,
|
A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */,
|
||||||
A53A29882DB69D2F00B6E02C /* TerminalCommandPalette.swift in Sources */,
|
A53A29882DB69D2F00B6E02C /* TerminalCommandPalette.swift in Sources */,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
import AppKit
|
||||||
|
import AppIntents
|
||||||
|
|
||||||
|
/// App intent that invokes a command palette entry.
|
||||||
|
@available(macOS 14.0, *)
|
||||||
|
struct CommandPaletteIntent: AppIntent {
|
||||||
|
static var title: LocalizedStringResource = "Invoke Command Palette Action"
|
||||||
|
|
||||||
|
@Parameter(
|
||||||
|
title: "Terminal",
|
||||||
|
description: "The terminal to base available commands from."
|
||||||
|
)
|
||||||
|
var terminal: TerminalEntity
|
||||||
|
|
||||||
|
@Parameter(
|
||||||
|
title: "Command",
|
||||||
|
description: "The command to invoke.",
|
||||||
|
optionsProvider: CommandQuery()
|
||||||
|
)
|
||||||
|
var command: CommandEntity
|
||||||
|
|
||||||
|
@available(macOS 26.0, *)
|
||||||
|
static var supportedModes: IntentModes = .background
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func perform() async throws -> some IntentResult & ReturnsValue<Bool> {
|
||||||
|
guard let surface = terminal.surfaceModel else {
|
||||||
|
throw GhosttyIntentError.surfaceNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
let performed = surface.perform(action: command.action)
|
||||||
|
return .result(value: performed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
import AppIntents
|
||||||
|
|
||||||
|
// MARK: AppEntity
|
||||||
|
|
||||||
|
@available(macOS 14.0, *)
|
||||||
|
struct CommandEntity: AppEntity {
|
||||||
|
let id: ID
|
||||||
|
|
||||||
|
// Note: for macOS 26 we can move all the properties to @ComputedProperty.
|
||||||
|
|
||||||
|
@Property(title: "Title")
|
||||||
|
var title: String
|
||||||
|
|
||||||
|
@Property(title: "Description")
|
||||||
|
var description: String
|
||||||
|
|
||||||
|
@Property(title: "Action")
|
||||||
|
var action: String
|
||||||
|
|
||||||
|
/// The underlying data model
|
||||||
|
let command: Ghostty.Command
|
||||||
|
|
||||||
|
/// A command identifier is a composite key based on the terminal and action.
|
||||||
|
struct ID: Hashable {
|
||||||
|
let terminalId: TerminalEntity.ID
|
||||||
|
let actionKey: String
|
||||||
|
|
||||||
|
init(terminalId: TerminalEntity.ID, actionKey: String) {
|
||||||
|
self.terminalId = terminalId
|
||||||
|
self.actionKey = actionKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static var typeDisplayRepresentation: TypeDisplayRepresentation {
|
||||||
|
TypeDisplayRepresentation(name: "Command Palette Command")
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayRepresentation: DisplayRepresentation {
|
||||||
|
DisplayRepresentation(
|
||||||
|
title: LocalizedStringResource(stringLiteral: command.title),
|
||||||
|
subtitle: LocalizedStringResource(stringLiteral: command.description),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static var defaultQuery = CommandQuery()
|
||||||
|
|
||||||
|
init(_ command: Ghostty.Command, for terminal: TerminalEntity) {
|
||||||
|
self.id = .init(terminalId: terminal.id, actionKey: command.actionKey)
|
||||||
|
self.command = command
|
||||||
|
self.title = command.title
|
||||||
|
self.description = command.description
|
||||||
|
self.action = command.action
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(macOS 14.0, *)
|
||||||
|
extension CommandEntity.ID: RawRepresentable {
|
||||||
|
var rawValue: String {
|
||||||
|
return "\(terminalId):\(actionKey)"
|
||||||
|
}
|
||||||
|
|
||||||
|
init?(rawValue: String) {
|
||||||
|
let components = rawValue.split(separator: ":", maxSplits: 1)
|
||||||
|
guard components.count == 2 else { return nil }
|
||||||
|
|
||||||
|
guard let terminalId = TerminalEntity.ID(uuidString: String(components[0])) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.terminalId = terminalId
|
||||||
|
self.actionKey = String(components[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Required by AppEntity
|
||||||
|
@available(macOS 14.0, *)
|
||||||
|
extension CommandEntity.ID: EntityIdentifierConvertible {
|
||||||
|
static func entityIdentifier(for entityIdentifierString: String) -> CommandEntity.ID? {
|
||||||
|
.init(rawValue: entityIdentifierString)
|
||||||
|
}
|
||||||
|
|
||||||
|
var entityIdentifierString: String {
|
||||||
|
rawValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: EntityQuery
|
||||||
|
|
||||||
|
@available(macOS 14.0, *)
|
||||||
|
struct CommandQuery: EntityQuery {
|
||||||
|
// Inject our terminal parameter from our command palette intent.
|
||||||
|
@IntentParameterDependency<CommandPaletteIntent>(\.$terminal)
|
||||||
|
var commandPaletteIntent
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func entities(for identifiers: [CommandEntity.ID]) async throws -> [CommandEntity] {
|
||||||
|
// Extract unique terminal IDs to avoid fetching duplicates
|
||||||
|
let terminalIds = Set(identifiers.map(\.terminalId))
|
||||||
|
let terminals = try await TerminalEntity.defaultQuery.entities(for: Array(terminalIds))
|
||||||
|
|
||||||
|
// Build a cache of terminals and their available commands
|
||||||
|
// This avoids repeated command fetching for the same terminal
|
||||||
|
typealias Tuple = (terminal: TerminalEntity, commands: [Ghostty.Command])
|
||||||
|
let commandMap: [TerminalEntity.ID: Tuple] =
|
||||||
|
terminals.reduce(into: [:]) { result, terminal in
|
||||||
|
guard let commands = try? terminal.surfaceModel?.commands() else { return }
|
||||||
|
result[terminal.id] = (terminal: terminal, commands: commands)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map each identifier to its corresponding CommandEntity. If a command doesn't
|
||||||
|
// exist it maps to nil and is removed via compactMap.
|
||||||
|
return identifiers.compactMap { id in
|
||||||
|
guard let (terminal, commands) = commandMap[id.terminalId],
|
||||||
|
let command = commands.first(where: { $0.actionKey == id.actionKey }) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return CommandEntity(command, for: terminal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func suggestedEntities() async throws -> [CommandEntity] {
|
||||||
|
guard let terminal = commandPaletteIntent?.terminal,
|
||||||
|
let surface = terminal.surfaceModel else { return [] }
|
||||||
|
return try surface.commands().map { CommandEntity($0, for: terminal) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -55,6 +55,11 @@ struct TerminalEntity: AppEntity {
|
||||||
Self.defaultQuery.all.first { $0.uuid == self.id }
|
Self.defaultQuery.all.first { $0.uuid == self.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
var surfaceModel: Ghostty.Surface? {
|
||||||
|
surfaceView?.surfaceModel
|
||||||
|
}
|
||||||
|
|
||||||
static var defaultQuery = TerminalQuery()
|
static var defaultQuery = TerminalQuery()
|
||||||
|
|
||||||
init(_ view: Ghostty.SurfaceView) {
|
init(_ view: Ghostty.SurfaceView) {
|
||||||
Loading…
Reference in New Issue