@lattices/cli 0.4.2 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/app/Info.plist +2 -2
- package/app/Lattices.app/Contents/Info.plist +2 -2
- package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/app/Package.swift +6 -0
- package/app/Sources/AppShell/App.swift +20 -0
- package/app/Sources/{AppDelegate.swift → AppShell/AppDelegate.swift} +94 -34
- package/app/Sources/{AppShellView.swift → AppShell/AppShellView.swift} +12 -1
- package/app/Sources/AppShell/AppUpdater.swift +92 -0
- package/app/Sources/AppShell/CliActionLauncher.swift +50 -0
- package/app/Sources/{HomeDashboardView.swift → AppShell/HomeDashboardView.swift} +18 -10
- package/app/Sources/AppShell/LatticesRuntime.swift +61 -0
- package/app/Sources/{MainView.swift → AppShell/MainView.swift} +351 -191
- package/app/Sources/{OnboardingView.swift → AppShell/OnboardingView.swift} +30 -16
- package/app/Sources/{Preferences.swift → AppShell/Preferences.swift} +78 -0
- package/app/Sources/{SettingsView.swift → AppShell/SettingsView.swift} +869 -152
- package/app/Sources/{HotkeyStore.swift → Core/Actions/HotkeyStore.swift} +9 -5
- package/app/Sources/{IntentEngine.swift → Core/Actions/IntentEngine.swift} +51 -27
- package/app/Sources/Core/Actions/IntentSchema.swift +94 -0
- package/app/Sources/{Intents → Core/Actions/Intents}/LatticeIntent.swift +0 -25
- package/app/Sources/{PaletteCommand.swift → Core/Actions/PaletteCommand.swift} +26 -6
- package/app/Sources/{VoiceIntentResolver.swift → Core/Actions/VoiceIntentResolver.swift} +46 -4
- package/app/Sources/Core/Companion/CompanionActivityLog.swift +70 -0
- package/app/Sources/Core/Companion/CompanionKeyboardController.swift +141 -0
- package/app/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +438 -0
- package/app/Sources/Core/Companion/LatticesCompanionCockpit.swift +555 -0
- package/app/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +594 -0
- package/app/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +204 -0
- package/app/Sources/Core/Companion/LatticesDeckHost.swift +1463 -0
- package/app/Sources/{LatticesApi.swift → Core/Daemon/LatticesApi.swift} +125 -4
- package/app/Sources/{AppTypeClassifier.swift → Core/Desktop/AppTypeClassifier.swift} +36 -0
- package/app/Sources/{DesktopModel.swift → Core/Desktop/DesktopModel.swift} +6 -8
- package/app/Sources/Core/Desktop/MouseFinder.swift +527 -0
- package/app/Sources/Core/Desktop/SessionWindowLocator.swift +139 -0
- package/app/Sources/Core/Desktop/WindowDragSnapController.swift +628 -0
- package/app/Sources/Core/Desktop/WindowPreviewCard.swift +100 -0
- package/app/Sources/Core/Desktop/WindowPreviewStore.swift +113 -0
- package/app/Sources/Core/Desktop/WindowSelectionStore.swift +76 -0
- package/app/Sources/{WindowTiler.swift → Core/Desktop/WindowTiler.swift} +351 -172
- package/app/Sources/Core/Input/MouseGestureConfig.swift +364 -0
- package/app/Sources/Core/Input/MouseGestureController.swift +1203 -0
- package/app/Sources/Core/Input/MouseInputDeviceStore.swift +98 -0
- package/app/Sources/Core/Input/MouseInputEventViewer.swift +272 -0
- package/app/Sources/Core/Input/MouseShortcutStore.swift +107 -0
- package/app/Sources/{CommandModeState.swift → Core/Overlays/CommandMode/CommandModeState.swift} +127 -24
- package/app/Sources/{CommandModeView.swift → Core/Overlays/CommandMode/CommandModeView.swift} +492 -79
- package/app/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +67 -0
- package/app/Sources/{CheatSheetHUD.swift → Core/Overlays/HUD/CheatSheetHUD.swift} +1 -0
- package/app/Sources/{HUDRightBar.swift → Core/Overlays/HUD/HUDRightBar.swift} +23 -201
- package/app/Sources/{LauncherHUD.swift → Core/Overlays/HUD/LauncherHUD.swift} +12 -26
- package/app/Sources/{OmniSearchView.swift → Core/Overlays/OmniSearch/OmniSearchView.swift} +136 -2
- package/app/Sources/{OmniSearchWindow.swift → Core/Overlays/OmniSearch/OmniSearchWindow.swift} +21 -32
- package/app/Sources/Core/Overlays/OverlayPanelShell.swift +241 -0
- package/app/Sources/{ScreenMapState.swift → Core/Overlays/ScreenMap/ScreenMapState.swift} +116 -32
- package/app/Sources/{ScreenMapView.swift → Core/Overlays/ScreenMap/ScreenMapView.swift} +510 -524
- package/app/Sources/{ScreenMapWindowController.swift → Core/Overlays/ScreenMap/ScreenMapWindowController.swift} +12 -4
- package/app/Sources/{VoiceCommandWindow.swift → Core/Overlays/Voice/VoiceCommandWindow.swift} +46 -53
- package/app/Sources/Core/Pi/PiAuthNextStepCard.swift +148 -0
- package/app/Sources/Core/Pi/PiAuthPromptCard.swift +90 -0
- package/app/Sources/{PiChatDock.swift → Core/Pi/PiChatDock.swift} +137 -74
- package/app/Sources/{PiChatSession.swift → Core/Pi/PiChatSession.swift} +608 -108
- package/app/Sources/Core/Pi/PiInstallCallout.swift +86 -0
- package/app/Sources/Core/Pi/PiProviderSetupCallout.swift +99 -0
- package/app/Sources/{PiWorkspaceView.swift → Core/Pi/PiWorkspaceView.swift} +174 -77
- package/app/Sources/{PermissionChecker.swift → Core/System/PermissionChecker.swift} +76 -2
- package/app/Sources/Core/System/SystemTelemetryMonitor.swift +273 -0
- package/app/Sources/{HandsOffSession.swift → Core/Voice/HandsOffSession.swift} +15 -4
- package/app/Sources/{WorkspaceManager.swift → Core/Workspace/WorkspaceManager.swift} +288 -0
- package/bin/assistant-intelligence.ts +874 -0
- package/bin/handsoff-infer.ts +16 -209
- package/bin/handsoff-worker.ts +45 -258
- package/bin/lattices-app.ts +62 -0
- package/bin/lattices-dev +4 -0
- package/bin/lattices.ts +125 -14
- package/docs/agents.md +14 -0
- package/docs/api.md +55 -0
- package/docs/app.md +3 -0
- package/docs/companion-deck.md +180 -0
- package/docs/component-extraction-roadmap.md +392 -0
- package/docs/config.md +25 -0
- package/docs/tiling-reference.md +55 -0
- package/docs/voice-error-model.md +73 -0
- package/package.json +4 -1
- package/app/Sources/App.swift +0 -10
- package/app/Sources/CommandPaletteWindow.swift +0 -134
- package/app/Sources/MouseFinder.swift +0 -222
- /package/app/Sources/{KeyRecorderView.swift → AppShell/KeyRecorderView.swift} +0 -0
- /package/app/Sources/{MainWindow.swift → AppShell/MainWindow.swift} +0 -0
- /package/app/Sources/{SettingsWindow.swift → AppShell/SettingsWindow.swift} +0 -0
- /package/app/Sources/{HotkeyManager.swift → Core/Actions/HotkeyManager.swift} +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/CreateLayerIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/DistributeIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/FocusIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/HelpIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/KillIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/LaunchIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/ListSessionsIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/ListWindowsIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/ScanIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/SearchIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/SwitchLayerIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/TileIntent.swift +0 -0
- /package/app/Sources/{DaemonProtocol.swift → Core/Daemon/DaemonProtocol.swift} +0 -0
- /package/app/Sources/{DaemonServer.swift → Core/Daemon/DaemonServer.swift} +0 -0
- /package/app/Sources/{AccessibilityTextExtractor.swift → Core/Desktop/AccessibilityTextExtractor.swift} +0 -0
- /package/app/Sources/{DesktopModelTypes.swift → Core/Desktop/DesktopModelTypes.swift} +0 -0
- /package/app/Sources/{InventoryManager.swift → Core/Desktop/InventoryManager.swift} +0 -0
- /package/app/Sources/{InventoryPath.swift → Core/Desktop/InventoryPath.swift} +0 -0
- /package/app/Sources/{OcrModel.swift → Core/Desktop/OcrModel.swift} +0 -0
- /package/app/Sources/{OcrStore.swift → Core/Desktop/OcrStore.swift} +0 -0
- /package/app/Sources/{PlacementSpec.swift → Core/Desktop/PlacementSpec.swift} +0 -0
- /package/app/Sources/{TilePickerView.swift → Core/Desktop/TilePickerView.swift} +0 -0
- /package/app/Sources/{AppWindowShell.swift → Core/Overlays/AppWindowShell.swift} +0 -0
- /package/app/Sources/{CommandModeWindow.swift → Core/Overlays/CommandMode/CommandModeWindow.swift} +0 -0
- /package/app/Sources/{CommandPaletteView.swift → Core/Overlays/CommandPalette/CommandPaletteView.swift} +0 -0
- /package/app/Sources/{HUDBottomBar.swift → Core/Overlays/HUD/HUDBottomBar.swift} +0 -0
- /package/app/Sources/{HUDController.swift → Core/Overlays/HUD/HUDController.swift} +0 -0
- /package/app/Sources/{HUDLeftBar.swift → Core/Overlays/HUD/HUDLeftBar.swift} +0 -0
- /package/app/Sources/{HUDMinimap.swift → Core/Overlays/HUD/HUDMinimap.swift} +0 -0
- /package/app/Sources/{HUDState.swift → Core/Overlays/HUD/HUDState.swift} +0 -0
- /package/app/Sources/{HUDTopBar.swift → Core/Overlays/HUD/HUDTopBar.swift} +0 -0
- /package/app/Sources/{LayerBezel.swift → Core/Overlays/HUD/LayerBezel.swift} +0 -0
- /package/app/Sources/{OmniSearchState.swift → Core/Overlays/OmniSearch/OmniSearchState.swift} +0 -0
- /package/app/Sources/{DiagnosticLog.swift → Core/System/DiagnosticLog.swift} +0 -0
- /package/app/Sources/{EventBus.swift → Core/System/EventBus.swift} +0 -0
- /package/app/Sources/{ProcessModel.swift → Core/System/ProcessModel.swift} +0 -0
- /package/app/Sources/{ProcessQuery.swift → Core/System/ProcessQuery.swift} +0 -0
- /package/app/Sources/{AdvisorLearningStore.swift → Core/Voice/AdvisorLearningStore.swift} +0 -0
- /package/app/Sources/{AgentSession.swift → Core/Voice/AgentSession.swift} +0 -0
- /package/app/Sources/{AudioProvider.swift → Core/Voice/AudioProvider.swift} +0 -0
- /package/app/Sources/{VoiceChatView.swift → Core/Voice/VoiceChatView.swift} +0 -0
- /package/app/Sources/{VoxClient.swift → Core/Voice/VoxClient.swift} +0 -0
- /package/app/Sources/{Project.swift → Core/Workspace/Project.swift} +0 -0
- /package/app/Sources/{ProjectScanner.swift → Core/Workspace/ProjectScanner.swift} +0 -0
- /package/app/Sources/{SessionLayerStore.swift → Core/Workspace/SessionLayerStore.swift} +0 -0
- /package/app/Sources/{SessionManager.swift → Core/Workspace/SessionManager.swift} +0 -0
- /package/app/Sources/{Terminal.swift → Core/Workspace/Terminal/Terminal.swift} +0 -0
- /package/app/Sources/{TerminalQuery.swift → Core/Workspace/Terminal/TerminalQuery.swift} +0 -0
- /package/app/Sources/{TerminalSynthesizer.swift → Core/Workspace/Terminal/TerminalSynthesizer.swift} +0 -0
- /package/app/Sources/{TmuxModel.swift → Core/Workspace/Tmux/TmuxModel.swift} +0 -0
- /package/app/Sources/{TmuxQuery.swift → Core/Workspace/Tmux/TmuxQuery.swift} +0 -0
- /package/app/Sources/{ActionRow.swift → UI/ActionRow.swift} +0 -0
- /package/app/Sources/{OrphanRow.swift → UI/OrphanRow.swift} +0 -0
- /package/app/Sources/{ProjectRow.swift → UI/ProjectRow.swift} +0 -0
- /package/app/Sources/{TabGroupRow.swift → UI/TabGroupRow.swift} +0 -0
- /package/app/Sources/{Theme.swift → UI/Theme.swift} +0 -0
|
@@ -31,7 +31,7 @@ enum HotkeyAction: String, CaseIterable, Codable {
|
|
|
31
31
|
// Tiling
|
|
32
32
|
case tileLeft, tileRight, tileMaximize, tileCenter
|
|
33
33
|
case tileTopLeft, tileTopRight, tileBottomLeft, tileBottomRight
|
|
34
|
-
case tileTop, tileBottom, tileDistribute
|
|
34
|
+
case tileTop, tileBottom, tileDistribute, tileTypeGrid
|
|
35
35
|
case tileLeftThird, tileCenterThird, tileRightThird
|
|
36
36
|
|
|
37
37
|
var label: String {
|
|
@@ -40,11 +40,11 @@ enum HotkeyAction: String, CaseIterable, Codable {
|
|
|
40
40
|
case .screenMap: return "Screen Map"
|
|
41
41
|
case .bezel: return "Window Bezel"
|
|
42
42
|
case .cheatSheet: return "Cheat Sheet"
|
|
43
|
-
case .desktopInventory: return "
|
|
44
|
-
case .omniSearch: return "
|
|
43
|
+
case .desktopInventory: return "Window Selector"
|
|
44
|
+
case .omniSearch: return "Search"
|
|
45
45
|
case .voiceCommand: return "Voice Command"
|
|
46
46
|
case .handsOff: return "Hands-Off Mode"
|
|
47
|
-
case .unifiedWindow: return "
|
|
47
|
+
case .unifiedWindow: return "Workspace Home"
|
|
48
48
|
case .hud: return "HUD"
|
|
49
49
|
case .mouseFinder: return "Find Mouse"
|
|
50
50
|
case .layer1: return "Layer 1"
|
|
@@ -70,6 +70,7 @@ enum HotkeyAction: String, CaseIterable, Codable {
|
|
|
70
70
|
case .tileTop: return "Top Half"
|
|
71
71
|
case .tileBottom: return "Bottom Half"
|
|
72
72
|
case .tileDistribute: return "Distribute"
|
|
73
|
+
case .tileTypeGrid: return "Grid Type"
|
|
73
74
|
case .tileLeftThird: return "Left Third"
|
|
74
75
|
case .tileCenterThird: return "Center Third"
|
|
75
76
|
case .tileRightThird: return "Right Third"
|
|
@@ -122,6 +123,7 @@ enum HotkeyAction: String, CaseIterable, Codable {
|
|
|
122
123
|
case .tileTop: return 308
|
|
123
124
|
case .tileBottom: return 309
|
|
124
125
|
case .tileDistribute: return 310
|
|
126
|
+
case .tileTypeGrid: return 314
|
|
125
127
|
case .tileLeftThird: return 311
|
|
126
128
|
case .tileCenterThird: return 312
|
|
127
129
|
case .tileRightThird: return 313
|
|
@@ -226,9 +228,10 @@ class HotkeyStore: ObservableObject {
|
|
|
226
228
|
|
|
227
229
|
// App
|
|
228
230
|
bind(.palette, 46, cmdShift) // Cmd+Shift+M
|
|
229
|
-
bind(.unifiedWindow, 18, hyper) // Hyper+1 (
|
|
231
|
+
bind(.unifiedWindow, 18, hyper) // Hyper+1 (Workspace Home)
|
|
230
232
|
bind(.bezel, 19, hyper) // Hyper+2
|
|
231
233
|
bind(.hud, 20, hyper) // Hyper+3 (HUD overlay)
|
|
234
|
+
bind(.desktopInventory, 5, hyper) // Hyper+G
|
|
232
235
|
bind(.voiceCommand, 21, hyper) // Hyper+4 (moved from Hyper+3)
|
|
233
236
|
let cmdCtrl = UInt32(cmdKey | controlKey)
|
|
234
237
|
bind(.handsOff, 46, cmdCtrl) // Ctrl+Cmd+M
|
|
@@ -259,6 +262,7 @@ class HotkeyStore: ObservableObject {
|
|
|
259
262
|
bind(.tileBottomLeft, 38, ctrlOpt) // Ctrl+Opt+J
|
|
260
263
|
bind(.tileBottomRight, 40, ctrlOpt) // Ctrl+Opt+K
|
|
261
264
|
bind(.tileDistribute, 2, ctrlOpt) // Ctrl+Opt+D
|
|
265
|
+
bind(.tileTypeGrid, 5, hyper) // Hyper+G
|
|
262
266
|
bind(.tileLeftThird, 18, ctrlOpt) // Ctrl+Opt+1
|
|
263
267
|
bind(.tileCenterThird, 19, ctrlOpt) // Ctrl+Opt+2
|
|
264
268
|
bind(.tileRightThird, 20, ctrlOpt) // Ctrl+Opt+3
|
|
@@ -1,31 +1,5 @@
|
|
|
1
1
|
import AppKit
|
|
2
2
|
|
|
3
|
-
// MARK: - Intent Definition
|
|
4
|
-
|
|
5
|
-
struct IntentDef {
|
|
6
|
-
let name: String
|
|
7
|
-
let description: String
|
|
8
|
-
let examples: [String] // Example phrases that map to this intent
|
|
9
|
-
let slots: [IntentSlot] // Named parameters extracted from the utterance
|
|
10
|
-
let handler: (IntentRequest) throws -> JSON
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
struct IntentSlot {
|
|
14
|
-
let name: String
|
|
15
|
-
let type: String // "string", "int", "position", "query"
|
|
16
|
-
let required: Bool
|
|
17
|
-
let description: String
|
|
18
|
-
let enumValues: [String]? // For constrained slots like tile positions
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
struct IntentRequest {
|
|
22
|
-
let intent: String
|
|
23
|
-
let slots: [String: JSON]
|
|
24
|
-
let rawText: String? // Original transcription, for fallback matching
|
|
25
|
-
let confidence: Double? // Transcription confidence from voice service
|
|
26
|
-
let source: String? // "vox", "siri", "cli", etc.
|
|
27
|
-
}
|
|
28
|
-
|
|
29
3
|
// MARK: - Intent Engine
|
|
30
4
|
|
|
31
5
|
final class IntentEngine {
|
|
@@ -133,6 +107,8 @@ final class IntentEngine {
|
|
|
133
107
|
description: "Target window ID", enumValues: nil),
|
|
134
108
|
IntentSlot(name: "session", type: "string", required: false,
|
|
135
109
|
description: "Target session name", enumValues: nil),
|
|
110
|
+
IntentSlot(name: "selection", type: "bool", required: false,
|
|
111
|
+
description: "Apply to the active multi-window selection instead of a single window", enumValues: nil),
|
|
136
112
|
],
|
|
137
113
|
handler: { req in
|
|
138
114
|
guard let posStr = req.slots["position"]?.stringValue else {
|
|
@@ -176,6 +152,34 @@ final class IntentEngine {
|
|
|
176
152
|
throw IntentError.targetNotFound("No window found for app '\(app)'")
|
|
177
153
|
}
|
|
178
154
|
|
|
155
|
+
if req.slots["selection"]?.boolValue == true {
|
|
156
|
+
let selectionIds = WindowSelectionStore.shared.windowIds
|
|
157
|
+
guard !selectionIds.isEmpty else {
|
|
158
|
+
throw IntentError.targetNotFound("No active window selection")
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if selectionIds.count == 1,
|
|
162
|
+
let wid = selectionIds.first,
|
|
163
|
+
let entry = DesktopModel.shared.windows[wid] {
|
|
164
|
+
tileEntry(entry)
|
|
165
|
+
return .object([
|
|
166
|
+
"ok": .bool(true),
|
|
167
|
+
"target": .string("selection"),
|
|
168
|
+
"wid": .int(Int(wid)),
|
|
169
|
+
"position": .string(posStr)
|
|
170
|
+
])
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return try LatticesApi.shared.dispatch(
|
|
174
|
+
method: "space.optimize",
|
|
175
|
+
params: .object([
|
|
176
|
+
"scope": .string("selection"),
|
|
177
|
+
"windowIds": .array(selectionIds.map { .int(Int($0)) }),
|
|
178
|
+
"region": .string(posStr)
|
|
179
|
+
])
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
179
183
|
// Default: tile frontmost window
|
|
180
184
|
DispatchQueue.main.async {
|
|
181
185
|
WindowTiler.tileFrontmostViaAX(to: placement)
|
|
@@ -381,7 +385,7 @@ final class IntentEngine {
|
|
|
381
385
|
|
|
382
386
|
register(IntentDef(
|
|
383
387
|
name: "distribute",
|
|
384
|
-
description: "Distribute windows evenly in a grid, optionally filtered by app and constrained to a screen region",
|
|
388
|
+
description: "Distribute windows evenly in a grid, optionally filtered by app or window type and constrained to a screen region",
|
|
385
389
|
examples: [
|
|
386
390
|
"spread out the windows",
|
|
387
391
|
"distribute everything",
|
|
@@ -394,19 +398,39 @@ final class IntentEngine {
|
|
|
394
398
|
slots: [
|
|
395
399
|
IntentSlot(name: "app", type: "string", required: false,
|
|
396
400
|
description: "Filter to windows of this app (e.g. 'iTerm2', 'Google Chrome')", enumValues: nil),
|
|
401
|
+
IntentSlot(name: "type", type: "string", required: false,
|
|
402
|
+
description: "Filter to a window type (e.g. 'terminal', 'browser', 'editor')",
|
|
403
|
+
enumValues: AppType.allCases.map(\.rawValue)),
|
|
397
404
|
IntentSlot(name: "region", type: "position", required: false,
|
|
398
405
|
description: "Constrain the grid to a screen region. Uses tile position names.",
|
|
399
406
|
enumValues: ["left", "right", "top", "bottom", "top-left", "top-right", "bottom-left", "bottom-right",
|
|
400
407
|
"left-third", "center-third", "right-third"]),
|
|
408
|
+
IntentSlot(name: "selection", type: "bool", required: false,
|
|
409
|
+
description: "Use the active selected windows instead of all visible windows", enumValues: nil),
|
|
401
410
|
],
|
|
402
411
|
handler: { req in
|
|
403
412
|
var params: [String: JSON] = [:]
|
|
404
413
|
if let app = req.slots["app"]?.stringValue {
|
|
405
414
|
params["app"] = .string(app)
|
|
406
415
|
}
|
|
416
|
+
if let type = req.slots["type"]?.stringValue {
|
|
417
|
+
params["type"] = .string(type)
|
|
418
|
+
}
|
|
407
419
|
if let region = req.slots["region"]?.stringValue {
|
|
408
420
|
params["region"] = .string(region)
|
|
409
421
|
}
|
|
422
|
+
if req.slots["selection"]?.boolValue == true {
|
|
423
|
+
let selectionIds = WindowSelectionStore.shared.windowIds
|
|
424
|
+
guard !selectionIds.isEmpty else {
|
|
425
|
+
throw IntentError.targetNotFound("No active window selection")
|
|
426
|
+
}
|
|
427
|
+
params["scope"] = .string("selection")
|
|
428
|
+
params["windowIds"] = .array(selectionIds.map { .int(Int($0)) })
|
|
429
|
+
return try LatticesApi.shared.dispatch(
|
|
430
|
+
method: "space.optimize",
|
|
431
|
+
params: .object(params)
|
|
432
|
+
)
|
|
433
|
+
}
|
|
410
434
|
return try LatticesApi.shared.dispatch(
|
|
411
435
|
method: "layout.distribute",
|
|
412
436
|
params: params.isEmpty ? nil : .object(params)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
struct IntentDef {
|
|
4
|
+
let name: String
|
|
5
|
+
let description: String
|
|
6
|
+
let examples: [String]
|
|
7
|
+
let slots: [IntentSlot]
|
|
8
|
+
let handler: (IntentRequest) throws -> JSON
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
struct IntentSlot {
|
|
12
|
+
let name: String
|
|
13
|
+
let type: String
|
|
14
|
+
let required: Bool
|
|
15
|
+
let description: String
|
|
16
|
+
let enumValues: [String]?
|
|
17
|
+
let defaultValue: JSON?
|
|
18
|
+
|
|
19
|
+
init(
|
|
20
|
+
name: String,
|
|
21
|
+
type: String,
|
|
22
|
+
required: Bool,
|
|
23
|
+
description: String,
|
|
24
|
+
enumValues: [String]? = nil,
|
|
25
|
+
defaultValue: JSON? = nil
|
|
26
|
+
) {
|
|
27
|
+
self.name = name
|
|
28
|
+
self.type = type
|
|
29
|
+
self.required = required
|
|
30
|
+
self.description = description
|
|
31
|
+
self.enumValues = enumValues
|
|
32
|
+
self.defaultValue = defaultValue
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
struct IntentRequest {
|
|
37
|
+
let intent: String
|
|
38
|
+
let slots: [String: JSON]
|
|
39
|
+
let rawText: String?
|
|
40
|
+
let confidence: Double?
|
|
41
|
+
let source: String?
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
enum SlotType {
|
|
45
|
+
case string
|
|
46
|
+
case int
|
|
47
|
+
case bool
|
|
48
|
+
case position
|
|
49
|
+
case query
|
|
50
|
+
case app
|
|
51
|
+
case session
|
|
52
|
+
case layer
|
|
53
|
+
case enumerated([String])
|
|
54
|
+
|
|
55
|
+
var typeLabel: String {
|
|
56
|
+
switch self {
|
|
57
|
+
case .string: return "string"
|
|
58
|
+
case .int: return "int"
|
|
59
|
+
case .bool: return "bool"
|
|
60
|
+
case .position: return "position"
|
|
61
|
+
case .query: return "query"
|
|
62
|
+
case .app: return "app"
|
|
63
|
+
case .session: return "session"
|
|
64
|
+
case .layer: return "layer"
|
|
65
|
+
case .enumerated: return "string"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
var enumValues: [String]? {
|
|
70
|
+
guard case .enumerated(let values) = self else { return nil }
|
|
71
|
+
return values
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
typealias SlotDef = IntentSlot
|
|
76
|
+
|
|
77
|
+
extension IntentSlot {
|
|
78
|
+
init(
|
|
79
|
+
name: String,
|
|
80
|
+
type: SlotType,
|
|
81
|
+
required: Bool = true,
|
|
82
|
+
description: String = "",
|
|
83
|
+
defaultValue: JSON? = nil
|
|
84
|
+
) {
|
|
85
|
+
self.init(
|
|
86
|
+
name: name,
|
|
87
|
+
type: type.typeLabel,
|
|
88
|
+
required: required,
|
|
89
|
+
description: description,
|
|
90
|
+
enumValues: type.enumValues,
|
|
91
|
+
defaultValue: defaultValue
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -12,31 +12,6 @@ protocol LatticeIntent {
|
|
|
12
12
|
func perform(slots: [String: JSON]) throws -> JSON
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
// MARK: - Slot Definition
|
|
16
|
-
|
|
17
|
-
enum SlotType {
|
|
18
|
-
case string // Free-form text
|
|
19
|
-
case position // Tile position (left, right, maximize, etc.)
|
|
20
|
-
case app // Running app name
|
|
21
|
-
case session // Active tmux session
|
|
22
|
-
case layer // Layer name
|
|
23
|
-
case enumerated([String]) // Fixed set of values
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
struct SlotDef {
|
|
27
|
-
let name: String
|
|
28
|
-
let type: SlotType
|
|
29
|
-
let required: Bool
|
|
30
|
-
let defaultValue: JSON?
|
|
31
|
-
|
|
32
|
-
init(name: String, type: SlotType, required: Bool = true, defaultValue: JSON? = nil) {
|
|
33
|
-
self.name = name
|
|
34
|
-
self.type = type
|
|
35
|
-
self.required = required
|
|
36
|
-
self.defaultValue = defaultValue
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
15
|
// MARK: - Compiled Phrase Template
|
|
41
16
|
|
|
42
17
|
struct CompiledPhrase {
|
|
@@ -364,24 +364,44 @@ enum CommandBuilder {
|
|
|
364
364
|
}
|
|
365
365
|
))
|
|
366
366
|
|
|
367
|
+
commands.append(PaletteCommand(
|
|
368
|
+
id: "app-home",
|
|
369
|
+
title: "Home",
|
|
370
|
+
subtitle: "Open the workspace home view",
|
|
371
|
+
icon: "house",
|
|
372
|
+
category: .app,
|
|
373
|
+
badge: nil,
|
|
374
|
+
action: { ScreenMapWindowController.shared.showPage(.home) }
|
|
375
|
+
))
|
|
376
|
+
|
|
367
377
|
commands.append(PaletteCommand(
|
|
368
378
|
id: "app-windows-list",
|
|
369
|
-
title: "
|
|
370
|
-
subtitle: "Browse
|
|
371
|
-
icon: "
|
|
379
|
+
title: "Search",
|
|
380
|
+
subtitle: "Browse windows, displays, spaces, and screen text",
|
|
381
|
+
icon: "magnifyingglass",
|
|
372
382
|
category: .app,
|
|
373
383
|
badge: nil,
|
|
374
|
-
action: {
|
|
384
|
+
action: { ScreenMapWindowController.shared.showPage(.desktopInventory) }
|
|
375
385
|
))
|
|
376
386
|
|
|
377
387
|
commands.append(PaletteCommand(
|
|
378
388
|
id: "app-screen-map",
|
|
379
|
-
title: "
|
|
389
|
+
title: "Layout",
|
|
380
390
|
subtitle: "Visual window editor",
|
|
381
391
|
icon: "rectangle.3.group",
|
|
382
392
|
category: .app,
|
|
383
393
|
badge: nil,
|
|
384
|
-
action: { ScreenMapWindowController.shared.
|
|
394
|
+
action: { ScreenMapWindowController.shared.showPage(.screenMap) }
|
|
395
|
+
))
|
|
396
|
+
|
|
397
|
+
commands.append(PaletteCommand(
|
|
398
|
+
id: "app-workspace-chat",
|
|
399
|
+
title: "Workspace Chat",
|
|
400
|
+
subtitle: "Open the longer-form assistant surface",
|
|
401
|
+
icon: "bubble.left.and.bubble.right",
|
|
402
|
+
category: .app,
|
|
403
|
+
badge: nil,
|
|
404
|
+
action: { ScreenMapWindowController.shared.showPage(.pi) }
|
|
385
405
|
))
|
|
386
406
|
|
|
387
407
|
commands.append(PaletteCommand(
|
|
@@ -141,10 +141,43 @@ final class VoiceIntentResolver {
|
|
|
141
141
|
private func extractSlots(for intentName: String, input: String) -> ExtractedSlots {
|
|
142
142
|
switch intentName {
|
|
143
143
|
case "tile_window":
|
|
144
|
-
|
|
144
|
+
var slots: [String: JSON] = [:]
|
|
145
|
+
var boost = 0.0
|
|
146
|
+
|
|
147
|
+
if let position = resolvePosition(in: input) {
|
|
148
|
+
slots["position"] = .string(position)
|
|
149
|
+
boost += 0.28
|
|
150
|
+
} else {
|
|
145
151
|
return ExtractedSlots(slots: [:], boost: 0)
|
|
146
152
|
}
|
|
147
|
-
|
|
153
|
+
|
|
154
|
+
if refersToSelection(in: input) {
|
|
155
|
+
slots["selection"] = .bool(true)
|
|
156
|
+
boost += 0.08
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return ExtractedSlots(slots: slots, boost: boost)
|
|
160
|
+
|
|
161
|
+
case "distribute":
|
|
162
|
+
var slots: [String: JSON] = [:]
|
|
163
|
+
var boost = 0.0
|
|
164
|
+
|
|
165
|
+
if let region = resolvePosition(in: input) {
|
|
166
|
+
slots["region"] = .string(region)
|
|
167
|
+
boost += 0.18
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if let app = detectKnownApp(in: input) {
|
|
171
|
+
slots["app"] = .string(app)
|
|
172
|
+
boost += 0.14
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if refersToSelection(in: input) {
|
|
176
|
+
slots["selection"] = .bool(true)
|
|
177
|
+
boost += 0.12
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return ExtractedSlots(slots: slots, boost: boost)
|
|
148
181
|
|
|
149
182
|
case "focus":
|
|
150
183
|
if let app = detectKnownApp(in: input) ?? extractEntity(in: input, prefixes: focusPrefixes) {
|
|
@@ -426,6 +459,15 @@ final class VoiceIntentResolver {
|
|
|
426
459
|
return nil
|
|
427
460
|
}
|
|
428
461
|
|
|
462
|
+
private func refersToSelection(in input: String) -> Bool {
|
|
463
|
+
let markers = [
|
|
464
|
+
"grid that", "grid these", "grid those",
|
|
465
|
+
"tile that", "tile these", "tile those",
|
|
466
|
+
"selected windows", "selection", "selected", "these windows", "those windows", "them"
|
|
467
|
+
]
|
|
468
|
+
return markers.contains(where: input.contains)
|
|
469
|
+
}
|
|
470
|
+
|
|
429
471
|
private func detectKnownApp(in input: String) -> String? {
|
|
430
472
|
for app in knownApps() {
|
|
431
473
|
let lower = app.lowercased()
|
|
@@ -601,7 +643,7 @@ final class VoiceIntentResolver {
|
|
|
601
643
|
"search": ["find", "search", "look for", "where is", "where d", "locate", "lost", "show me all", "windows"],
|
|
602
644
|
"list_windows": ["what s open", "list windows", "which windows", "what do i have open"],
|
|
603
645
|
"list_sessions": ["list sessions", "what s running", "which projects", "show my sessions"],
|
|
604
|
-
"distribute": ["distribute", "spread", "organize", "arrange", "tidy", "clean up", "grid"],
|
|
646
|
+
"distribute": ["distribute", "spread", "organize", "arrange", "tidy", "clean up", "grid", "selected", "selection"],
|
|
605
647
|
"create_layer": ["create layer", "save layout", "snapshot", "remember this layout"],
|
|
606
648
|
"kill": ["kill", "stop", "shut down", "close", "terminate", "end"],
|
|
607
649
|
"scan": ["scan", "rescan", "ocr", "read the screen", "what s on my screen", "screen text"],
|
|
@@ -662,7 +704,7 @@ final class VoiceIntentResolver {
|
|
|
662
704
|
"search": ["where d my slack go", "pull up everything with dewey in it", "show me all the chrome windows", "dewey"],
|
|
663
705
|
"list_windows": ["what do i have open", "what windows do i have"],
|
|
664
706
|
"list_sessions": ["show me my sessions", "which projects are active"],
|
|
665
|
-
"distribute": ["tidy up", "line everything up", "clean up the windows"],
|
|
707
|
+
"distribute": ["tidy up", "line everything up", "clean up the windows", "grid that in the bottom half", "arrange the selected windows"],
|
|
666
708
|
"create_layer": ["snapshot this", "remember this layout"],
|
|
667
709
|
"kill": ["close the dewey session", "stop my session"],
|
|
668
710
|
"scan": ["what s on my screen", "read the screen", "give me a fresh scan"],
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import DeckKit
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
final class CompanionActivityLog {
|
|
5
|
+
static let shared = CompanionActivityLog()
|
|
6
|
+
|
|
7
|
+
private let lock = NSLock()
|
|
8
|
+
private var entries: [DeckActivityLogEntry] = []
|
|
9
|
+
private let maxEntries = 120
|
|
10
|
+
|
|
11
|
+
private init() {
|
|
12
|
+
EventBus.shared.subscribe { [weak self] event in
|
|
13
|
+
self?.record(event)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
func record(tag: String, tint: String?, text: String) {
|
|
18
|
+
let entry = DeckActivityLogEntry(
|
|
19
|
+
id: UUID().uuidString,
|
|
20
|
+
createdAt: Date(),
|
|
21
|
+
tag: tag,
|
|
22
|
+
tint: tint,
|
|
23
|
+
text: text
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
lock.lock()
|
|
27
|
+
entries.append(entry)
|
|
28
|
+
if entries.count > maxEntries {
|
|
29
|
+
entries.removeFirst(entries.count - maxEntries)
|
|
30
|
+
}
|
|
31
|
+
lock.unlock()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
func snapshot(limit: Int = 80) -> [DeckActivityLogEntry] {
|
|
35
|
+
lock.lock()
|
|
36
|
+
let copy = entries
|
|
37
|
+
lock.unlock()
|
|
38
|
+
|
|
39
|
+
return Array(copy.suffix(limit).reversed())
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private extension CompanionActivityLog {
|
|
44
|
+
func record(_ event: ModelEvent) {
|
|
45
|
+
switch event {
|
|
46
|
+
case .windowsChanged(let windows, let added, let removed):
|
|
47
|
+
let delta = [added.isEmpty ? nil : "+\(added.count)", removed.isEmpty ? nil : "-\(removed.count)"]
|
|
48
|
+
.compactMap { $0 }
|
|
49
|
+
.joined(separator: " ")
|
|
50
|
+
let suffix = delta.isEmpty ? "" : " (\(delta))"
|
|
51
|
+
record(tag: "WIN", tint: "blue", text: "\(windows.count) desktop windows\(suffix)")
|
|
52
|
+
|
|
53
|
+
case .tmuxChanged(let sessions):
|
|
54
|
+
record(tag: "TMUX", tint: "green", text: "\(sessions.count) tmux sessions indexed")
|
|
55
|
+
|
|
56
|
+
case .layerSwitched(let index):
|
|
57
|
+
record(tag: "LAYER", tint: "violet", text: "Switched workspace layer \(index + 1)")
|
|
58
|
+
|
|
59
|
+
case .processesChanged(let interesting):
|
|
60
|
+
record(tag: "PROC", tint: "amber", text: "\(interesting.count) terminal processes changed")
|
|
61
|
+
|
|
62
|
+
case .ocrScanComplete(let windowCount, let totalBlocks):
|
|
63
|
+
record(tag: "OCR", tint: "teal", text: "Scanned \(totalBlocks) text blocks across \(windowCount) windows")
|
|
64
|
+
|
|
65
|
+
case .voiceCommand(let text, let confidence):
|
|
66
|
+
let pct = Int((confidence * 100).rounded())
|
|
67
|
+
record(tag: "VOICE", tint: "red", text: "\"\(text)\" · \(pct)%")
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
enum CompanionKeyboardError: LocalizedError {
|
|
5
|
+
case unknownKey(String)
|
|
6
|
+
case eventSourceUnavailable
|
|
7
|
+
|
|
8
|
+
var errorDescription: String? {
|
|
9
|
+
switch self {
|
|
10
|
+
case .unknownKey(let key):
|
|
11
|
+
return "Unsupported key for forwarding: \(key)"
|
|
12
|
+
case .eventSourceUnavailable:
|
|
13
|
+
return "Unable to create a keyboard event source."
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
final class CompanionKeyboardController {
|
|
19
|
+
static let shared = CompanionKeyboardController()
|
|
20
|
+
|
|
21
|
+
private init() {}
|
|
22
|
+
|
|
23
|
+
func send(key rawKey: String, modifiers rawModifiers: [String]) throws -> String {
|
|
24
|
+
let parsed = parse(key: rawKey, modifiers: rawModifiers)
|
|
25
|
+
guard let keyCode = keyCode(for: parsed.key) else {
|
|
26
|
+
throw CompanionKeyboardError.unknownKey(rawKey)
|
|
27
|
+
}
|
|
28
|
+
guard let source = CGEventSource(stateID: .combinedSessionState) else {
|
|
29
|
+
throw CompanionKeyboardError.eventSourceUnavailable
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let flags = eventFlags(for: parsed.modifiers)
|
|
33
|
+
guard
|
|
34
|
+
let down = CGEvent(keyboardEventSource: source, virtualKey: keyCode, keyDown: true),
|
|
35
|
+
let up = CGEvent(keyboardEventSource: source, virtualKey: keyCode, keyDown: false)
|
|
36
|
+
else {
|
|
37
|
+
throw CompanionKeyboardError.eventSourceUnavailable
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
down.flags = flags
|
|
41
|
+
up.flags = flags
|
|
42
|
+
down.post(tap: .cghidEventTap)
|
|
43
|
+
usleep(12_000)
|
|
44
|
+
up.post(tap: .cghidEventTap)
|
|
45
|
+
|
|
46
|
+
return displayName(key: parsed.key, modifiers: parsed.modifiers)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private extension CompanionKeyboardController {
|
|
51
|
+
func parse(key rawKey: String, modifiers rawModifiers: [String]) -> (key: String, modifiers: Set<String>) {
|
|
52
|
+
var modifiers = Set(rawModifiers.map(normalizeModifier).filter { !$0.isEmpty })
|
|
53
|
+
var key = rawKey
|
|
54
|
+
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
55
|
+
.lowercased()
|
|
56
|
+
|
|
57
|
+
let symbolModifiers: [(String, String)] = [
|
|
58
|
+
("⌘", "command"),
|
|
59
|
+
("cmd", "command"),
|
|
60
|
+
("command", "command"),
|
|
61
|
+
("⌥", "option"),
|
|
62
|
+
("option", "option"),
|
|
63
|
+
("alt", "option"),
|
|
64
|
+
("⌃", "control"),
|
|
65
|
+
("ctrl", "control"),
|
|
66
|
+
("control", "control"),
|
|
67
|
+
("⇧", "shift"),
|
|
68
|
+
("shift", "shift"),
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
for (symbol, modifier) in symbolModifiers where key.contains(symbol) {
|
|
72
|
+
modifiers.insert(modifier)
|
|
73
|
+
key = key.replacingOccurrences(of: symbol, with: "")
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
key = key
|
|
77
|
+
.replacingOccurrences(of: "+", with: "")
|
|
78
|
+
.replacingOccurrences(of: " ", with: "")
|
|
79
|
+
.replacingOccurrences(of: "⎋", with: "escape")
|
|
80
|
+
.replacingOccurrences(of: "⇥", with: "tab")
|
|
81
|
+
.replacingOccurrences(of: "↩", with: "enter")
|
|
82
|
+
.replacingOccurrences(of: "⏎", with: "enter")
|
|
83
|
+
.replacingOccurrences(of: "return", with: "enter")
|
|
84
|
+
.replacingOccurrences(of: "←", with: "left")
|
|
85
|
+
.replacingOccurrences(of: "→", with: "right")
|
|
86
|
+
.replacingOccurrences(of: "↑", with: "up")
|
|
87
|
+
.replacingOccurrences(of: "↓", with: "down")
|
|
88
|
+
|
|
89
|
+
if key == "esc" {
|
|
90
|
+
key = "escape"
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return (key.isEmpty ? rawKey.lowercased() : key, modifiers)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
func normalizeModifier(_ modifier: String) -> String {
|
|
97
|
+
switch modifier.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() {
|
|
98
|
+
case "cmd", "command", "meta", "⌘":
|
|
99
|
+
return "command"
|
|
100
|
+
case "opt", "option", "alt", "⌥":
|
|
101
|
+
return "option"
|
|
102
|
+
case "ctrl", "control", "⌃":
|
|
103
|
+
return "control"
|
|
104
|
+
case "shift", "⇧":
|
|
105
|
+
return "shift"
|
|
106
|
+
default:
|
|
107
|
+
return ""
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
func eventFlags(for modifiers: Set<String>) -> CGEventFlags {
|
|
112
|
+
var flags: CGEventFlags = []
|
|
113
|
+
if modifiers.contains("command") { flags.insert(.maskCommand) }
|
|
114
|
+
if modifiers.contains("option") { flags.insert(.maskAlternate) }
|
|
115
|
+
if modifiers.contains("control") { flags.insert(.maskControl) }
|
|
116
|
+
if modifiers.contains("shift") { flags.insert(.maskShift) }
|
|
117
|
+
return flags
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
func keyCode(for key: String) -> CGKeyCode? {
|
|
121
|
+
let codes: [String: CGKeyCode] = [
|
|
122
|
+
"a": 0, "s": 1, "d": 2, "f": 3, "h": 4, "g": 5, "z": 6, "x": 7,
|
|
123
|
+
"c": 8, "v": 9, "b": 11, "q": 12, "w": 13, "e": 14, "r": 15,
|
|
124
|
+
"y": 16, "t": 17, "1": 18, "2": 19, "3": 20, "4": 21, "6": 22,
|
|
125
|
+
"5": 23, "=": 24, "9": 25, "7": 26, "-": 27, "8": 28, "0": 29,
|
|
126
|
+
"]": 30, "o": 31, "u": 32, "[": 33, "i": 34, "p": 35, "enter": 36,
|
|
127
|
+
"l": 37, "j": 38, "'": 39, "k": 40, ";": 41, "\\": 42, ",": 43,
|
|
128
|
+
"/": 44, "n": 45, "m": 46, ".": 47, "tab": 48, "space": 49,
|
|
129
|
+
"`": 50, "delete": 51, "backspace": 51, "escape": 53,
|
|
130
|
+
"command": 55, "cmd": 55, "shift": 56, "capslock": 57, "option": 58,
|
|
131
|
+
"alt": 58, "control": 59, "left": 123, "right": 124, "down": 125,
|
|
132
|
+
"up": 126,
|
|
133
|
+
]
|
|
134
|
+
return codes[key]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
func displayName(key: String, modifiers: Set<String>) -> String {
|
|
138
|
+
let ordered = ["control", "option", "shift", "command"].filter { modifiers.contains($0) }
|
|
139
|
+
return (ordered + [key]).joined(separator: "+")
|
|
140
|
+
}
|
|
141
|
+
}
|