summaryrefslogtreecommitdiff
path: root/macos/Sources/Features/App Intents/Entities/CommandEntity.swift
blob: f7abcc6de73b7ee701609a71ea4b517efa313984 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
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) }
    }
}