mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-09-05 19:08:17 +00:00
macos: InvokeCommandPaletteIntent and CommandEntity
This commit is contained in:
@@ -126,6 +126,8 @@
|
||||
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 */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
@@ -252,6 +254,8 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -616,14 +620,24 @@
|
||||
A5E4082C2E0237270035FEAC /* App Intents */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A5E408312E02FEDC0035FEAC /* TerminalEntity.swift */,
|
||||
A5E408412E0453370035FEAC /* Entities */,
|
||||
A5E4082D2E0237410035FEAC /* NewTerminalIntent.swift */,
|
||||
A5E408332E03200F0035FEAC /* GetTerminalDetailsIntent.swift */,
|
||||
A5E408422E047D060035FEAC /* CommandPaletteIntent.swift */,
|
||||
A5E4082F2E0271320035FEAC /* GhosttyIntentError.swift */,
|
||||
);
|
||||
path = "App Intents";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A5E408412E0453370035FEAC /* Entities */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A5E408312E02FEDC0035FEAC /* TerminalEntity.swift */,
|
||||
A5E4083F2E04532A0035FEAC /* CommandEntity.swift */,
|
||||
);
|
||||
path = Entities;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@@ -750,6 +764,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A5AEB1652D5BE7D000513529 /* LastWindowPosition.swift in Sources */,
|
||||
A5E408432E047D0B0035FEAC /* CommandPaletteIntent.swift in Sources */,
|
||||
A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */,
|
||||
A54B0CEB2D0CFB4C00CBEFF8 /* NSImage+Extension.swift in Sources */,
|
||||
A5874D9D2DAD786100E83852 /* NSWindow+Extension.swift in Sources */,
|
||||
@@ -827,6 +842,7 @@
|
||||
A599CDB02CF103F60049FA26 /* NSAppearance+Extension.swift in Sources */,
|
||||
A52FFF572CA90484000C6A5B /* QuickTerminalScreen.swift in Sources */,
|
||||
A5CC36132C9CD72D004D6760 /* SecureInputOverlay.swift in Sources */,
|
||||
A5E408402E04532C0035FEAC /* CommandEntity.swift in Sources */,
|
||||
A5E4082A2E022E9E0035FEAC /* TabGroupCloseCoordinator.swift in Sources */,
|
||||
A535B9DA299C569B0017E2E4 /* ErrorView.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)
|
||||
}
|
||||
}
|
128
macos/Sources/Features/App Intents/Entities/CommandEntity.swift
Normal file
128
macos/Sources/Features/App Intents/Entities/CommandEntity.swift
Normal file
@@ -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 }
|
||||
}
|
||||
|
||||
@MainActor
|
||||
var surfaceModel: Ghostty.Surface? {
|
||||
surfaceView?.surfaceModel
|
||||
}
|
||||
|
||||
static var defaultQuery = TerminalQuery()
|
||||
|
||||
init(_ view: Ghostty.SurfaceView) {
|
Reference in New Issue
Block a user