@lattices/cli 0.3.0
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 +155 -0
- package/app/Lattices.app/Contents/Info.plist +24 -0
- package/app/Package.swift +13 -0
- package/app/Sources/AccessibilityTextExtractor.swift +111 -0
- package/app/Sources/ActionRow.swift +61 -0
- package/app/Sources/App.swift +10 -0
- package/app/Sources/AppDelegate.swift +242 -0
- package/app/Sources/AppShellView.swift +62 -0
- package/app/Sources/AppTypeClassifier.swift +70 -0
- package/app/Sources/AppWindowShell.swift +63 -0
- package/app/Sources/CheatSheetHUD.swift +332 -0
- package/app/Sources/CommandModeState.swift +1362 -0
- package/app/Sources/CommandModeView.swift +1405 -0
- package/app/Sources/CommandModeWindow.swift +192 -0
- package/app/Sources/CommandPaletteView.swift +307 -0
- package/app/Sources/CommandPaletteWindow.swift +134 -0
- package/app/Sources/DaemonProtocol.swift +101 -0
- package/app/Sources/DaemonServer.swift +414 -0
- package/app/Sources/DesktopModel.swift +149 -0
- package/app/Sources/DesktopModelTypes.swift +71 -0
- package/app/Sources/DiagnosticLog.swift +271 -0
- package/app/Sources/EventBus.swift +30 -0
- package/app/Sources/HotkeyManager.swift +254 -0
- package/app/Sources/HotkeyStore.swift +338 -0
- package/app/Sources/InventoryManager.swift +35 -0
- package/app/Sources/InventoryPath.swift +43 -0
- package/app/Sources/KeyRecorderView.swift +210 -0
- package/app/Sources/LatticesApi.swift +1234 -0
- package/app/Sources/LayerBezel.swift +203 -0
- package/app/Sources/MainView.swift +479 -0
- package/app/Sources/MainWindow.swift +83 -0
- package/app/Sources/OcrModel.swift +430 -0
- package/app/Sources/OcrStore.swift +329 -0
- package/app/Sources/OmniSearchState.swift +283 -0
- package/app/Sources/OmniSearchView.swift +288 -0
- package/app/Sources/OmniSearchWindow.swift +105 -0
- package/app/Sources/OrphanRow.swift +129 -0
- package/app/Sources/PaletteCommand.swift +419 -0
- package/app/Sources/PermissionChecker.swift +125 -0
- package/app/Sources/Preferences.swift +99 -0
- package/app/Sources/ProcessModel.swift +199 -0
- package/app/Sources/ProcessQuery.swift +151 -0
- package/app/Sources/Project.swift +28 -0
- package/app/Sources/ProjectRow.swift +368 -0
- package/app/Sources/ProjectScanner.swift +128 -0
- package/app/Sources/ScreenMapState.swift +2387 -0
- package/app/Sources/ScreenMapView.swift +2820 -0
- package/app/Sources/ScreenMapWindowController.swift +89 -0
- package/app/Sources/SessionManager.swift +72 -0
- package/app/Sources/SettingsView.swift +1064 -0
- package/app/Sources/SettingsWindow.swift +20 -0
- package/app/Sources/TabGroupRow.swift +178 -0
- package/app/Sources/Terminal.swift +259 -0
- package/app/Sources/TerminalQuery.swift +156 -0
- package/app/Sources/TerminalSynthesizer.swift +200 -0
- package/app/Sources/Theme.swift +163 -0
- package/app/Sources/TilePickerView.swift +209 -0
- package/app/Sources/TmuxModel.swift +53 -0
- package/app/Sources/TmuxQuery.swift +81 -0
- package/app/Sources/WindowTiler.swift +1778 -0
- package/app/Sources/WorkspaceManager.swift +575 -0
- package/bin/client.js +4 -0
- package/bin/daemon-client.js +187 -0
- package/bin/lattices-app.js +221 -0
- package/bin/lattices.js +1551 -0
- package/docs/api.md +924 -0
- package/docs/app.md +297 -0
- package/docs/concepts.md +135 -0
- package/docs/config.md +245 -0
- package/docs/layers.md +410 -0
- package/docs/ocr.md +185 -0
- package/docs/overview.md +94 -0
- package/docs/quickstart.md +75 -0
- package/package.json +42 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import Carbon
|
|
2
|
+
import AppKit
|
|
3
|
+
import Combine
|
|
4
|
+
|
|
5
|
+
// MARK: - HotkeyGroup
|
|
6
|
+
|
|
7
|
+
enum HotkeyGroup: String, CaseIterable {
|
|
8
|
+
case app
|
|
9
|
+
case layers
|
|
10
|
+
case tiling
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// MARK: - HotkeyAction
|
|
14
|
+
|
|
15
|
+
enum HotkeyAction: String, CaseIterable, Codable {
|
|
16
|
+
// App
|
|
17
|
+
case palette
|
|
18
|
+
case screenMap
|
|
19
|
+
case bezel
|
|
20
|
+
case cheatSheet
|
|
21
|
+
case desktopInventory
|
|
22
|
+
case omniSearch
|
|
23
|
+
// Layers
|
|
24
|
+
case layer1, layer2, layer3, layer4, layer5, layer6, layer7, layer8, layer9
|
|
25
|
+
// Tiling
|
|
26
|
+
case tileLeft, tileRight, tileMaximize, tileCenter
|
|
27
|
+
case tileTopLeft, tileTopRight, tileBottomLeft, tileBottomRight
|
|
28
|
+
case tileTop, tileBottom, tileDistribute
|
|
29
|
+
case tileLeftThird, tileCenterThird, tileRightThird
|
|
30
|
+
|
|
31
|
+
var label: String {
|
|
32
|
+
switch self {
|
|
33
|
+
case .palette: return "Command Palette"
|
|
34
|
+
case .screenMap: return "Screen Map"
|
|
35
|
+
case .bezel: return "Window Bezel"
|
|
36
|
+
case .cheatSheet: return "Cheat Sheet"
|
|
37
|
+
case .desktopInventory: return "Desktop Inventory"
|
|
38
|
+
case .omniSearch: return "Omni Search"
|
|
39
|
+
case .layer1: return "Layer 1"
|
|
40
|
+
case .layer2: return "Layer 2"
|
|
41
|
+
case .layer3: return "Layer 3"
|
|
42
|
+
case .layer4: return "Layer 4"
|
|
43
|
+
case .layer5: return "Layer 5"
|
|
44
|
+
case .layer6: return "Layer 6"
|
|
45
|
+
case .layer7: return "Layer 7"
|
|
46
|
+
case .layer8: return "Layer 8"
|
|
47
|
+
case .layer9: return "Layer 9"
|
|
48
|
+
case .tileLeft: return "Tile Left"
|
|
49
|
+
case .tileRight: return "Tile Right"
|
|
50
|
+
case .tileMaximize: return "Maximize"
|
|
51
|
+
case .tileCenter: return "Center"
|
|
52
|
+
case .tileTopLeft: return "Top Left"
|
|
53
|
+
case .tileTopRight: return "Top Right"
|
|
54
|
+
case .tileBottomLeft: return "Bottom Left"
|
|
55
|
+
case .tileBottomRight: return "Bottom Right"
|
|
56
|
+
case .tileTop: return "Top Half"
|
|
57
|
+
case .tileBottom: return "Bottom Half"
|
|
58
|
+
case .tileDistribute: return "Distribute"
|
|
59
|
+
case .tileLeftThird: return "Left Third"
|
|
60
|
+
case .tileCenterThird: return "Center Third"
|
|
61
|
+
case .tileRightThird: return "Right Third"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
var group: HotkeyGroup {
|
|
66
|
+
switch self {
|
|
67
|
+
case .palette, .screenMap, .bezel, .cheatSheet, .desktopInventory, .omniSearch: return .app
|
|
68
|
+
case .layer1, .layer2, .layer3, .layer4, .layer5,
|
|
69
|
+
.layer6, .layer7, .layer8, .layer9: return .layers
|
|
70
|
+
default: return .tiling
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
var carbonID: UInt32 {
|
|
75
|
+
switch self {
|
|
76
|
+
case .palette: return 1
|
|
77
|
+
case .screenMap: return 200
|
|
78
|
+
case .bezel: return 201
|
|
79
|
+
case .cheatSheet: return 202
|
|
80
|
+
case .desktopInventory: return 203
|
|
81
|
+
case .omniSearch: return 204
|
|
82
|
+
case .layer1: return 101
|
|
83
|
+
case .layer2: return 102
|
|
84
|
+
case .layer3: return 103
|
|
85
|
+
case .layer4: return 104
|
|
86
|
+
case .layer5: return 105
|
|
87
|
+
case .layer6: return 106
|
|
88
|
+
case .layer7: return 107
|
|
89
|
+
case .layer8: return 108
|
|
90
|
+
case .layer9: return 109
|
|
91
|
+
case .tileLeft: return 300
|
|
92
|
+
case .tileRight: return 301
|
|
93
|
+
case .tileMaximize: return 302
|
|
94
|
+
case .tileCenter: return 303
|
|
95
|
+
case .tileTopLeft: return 304
|
|
96
|
+
case .tileTopRight: return 305
|
|
97
|
+
case .tileBottomLeft: return 306
|
|
98
|
+
case .tileBottomRight: return 307
|
|
99
|
+
case .tileTop: return 308
|
|
100
|
+
case .tileBottom: return 309
|
|
101
|
+
case .tileDistribute: return 310
|
|
102
|
+
case .tileLeftThird: return 311
|
|
103
|
+
case .tileCenterThird: return 312
|
|
104
|
+
case .tileRightThird: return 313
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
static var layerActions: [HotkeyAction] {
|
|
109
|
+
[.layer1, .layer2, .layer3, .layer4, .layer5, .layer6, .layer7, .layer8, .layer9]
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// MARK: - KeyBinding
|
|
114
|
+
|
|
115
|
+
struct KeyBinding: Codable, Equatable {
|
|
116
|
+
let keyCode: UInt32
|
|
117
|
+
let carbonModifiers: UInt32
|
|
118
|
+
var displayParts: [String]
|
|
119
|
+
|
|
120
|
+
static func carbonModifiers(from flags: NSEvent.ModifierFlags) -> UInt32 {
|
|
121
|
+
var mods: UInt32 = 0
|
|
122
|
+
if flags.contains(.command) { mods |= UInt32(cmdKey) }
|
|
123
|
+
if flags.contains(.shift) { mods |= UInt32(shiftKey) }
|
|
124
|
+
if flags.contains(.option) { mods |= UInt32(optionKey) }
|
|
125
|
+
if flags.contains(.control) { mods |= UInt32(controlKey) }
|
|
126
|
+
return mods
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
static func displayParts(keyCode: UInt32, carbonModifiers: UInt32) -> [String] {
|
|
130
|
+
var parts: [String] = []
|
|
131
|
+
if carbonModifiers & UInt32(controlKey) != 0 { parts.append("Ctrl") }
|
|
132
|
+
if carbonModifiers & UInt32(optionKey) != 0 { parts.append("Option") }
|
|
133
|
+
if carbonModifiers & UInt32(shiftKey) != 0 { parts.append("Shift") }
|
|
134
|
+
if carbonModifiers & UInt32(cmdKey) != 0 { parts.append("Cmd") }
|
|
135
|
+
parts.append(keyName(for: keyCode))
|
|
136
|
+
return parts
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
static func keyName(for keyCode: UInt32) -> String {
|
|
140
|
+
let names: [UInt32: String] = [
|
|
141
|
+
0: "A", 1: "S", 2: "D", 3: "F", 4: "H", 5: "G", 6: "Z", 7: "X",
|
|
142
|
+
8: "C", 9: "V", 11: "B", 12: "Q", 13: "W", 14: "E", 15: "R",
|
|
143
|
+
16: "Y", 17: "T", 18: "1", 19: "2", 20: "3", 21: "4", 22: "6",
|
|
144
|
+
23: "5", 24: "=", 25: "9", 26: "7", 27: "-", 28: "8", 29: "0",
|
|
145
|
+
30: "]", 31: "O", 32: "U", 33: "[", 34: "I", 35: "P",
|
|
146
|
+
36: "Return", 37: "L", 38: "J", 39: "'", 40: "K", 41: ";",
|
|
147
|
+
42: "\\", 43: ",", 44: "/", 45: "N", 46: "M", 47: ".",
|
|
148
|
+
48: "Tab", 49: "Space", 50: "`", 51: "Delete",
|
|
149
|
+
53: "Escape",
|
|
150
|
+
65: ".", // numpad
|
|
151
|
+
67: "*", // numpad
|
|
152
|
+
69: "+", // numpad
|
|
153
|
+
71: "Clear", // numpad
|
|
154
|
+
75: "/", // numpad
|
|
155
|
+
76: "Enter", // numpad
|
|
156
|
+
78: "-", // numpad
|
|
157
|
+
82: "0", 83: "1", 84: "2", 85: "3", 86: "4", // numpad
|
|
158
|
+
87: "5", 88: "6", 89: "7", 91: "8", 92: "9", // numpad
|
|
159
|
+
96: "F5", 97: "F6", 98: "F7", 99: "F3", 100: "F8",
|
|
160
|
+
101: "F9", 103: "F11", 105: "F13", 107: "F14",
|
|
161
|
+
109: "F10", 111: "F12", 113: "F15", 114: "Help",
|
|
162
|
+
115: "Home", 116: "PgUp", 117: "Del", 118: "F4",
|
|
163
|
+
119: "End", 120: "F2", 121: "PgDn", 122: "F1",
|
|
164
|
+
123: "\u{2190}", // left arrow
|
|
165
|
+
124: "\u{2192}", // right arrow
|
|
166
|
+
125: "\u{2193}", // down arrow
|
|
167
|
+
126: "\u{2191}", // up arrow
|
|
168
|
+
]
|
|
169
|
+
return names[keyCode] ?? "Key\(keyCode)"
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// MARK: - HotkeyStore
|
|
174
|
+
|
|
175
|
+
class HotkeyStore: ObservableObject {
|
|
176
|
+
static let shared = HotkeyStore()
|
|
177
|
+
|
|
178
|
+
@Published var bindings: [HotkeyAction: KeyBinding]
|
|
179
|
+
private var callbacks: [HotkeyAction: () -> Void] = [:]
|
|
180
|
+
|
|
181
|
+
static let defaultBindings: [HotkeyAction: KeyBinding] = {
|
|
182
|
+
var d = [HotkeyAction: KeyBinding]()
|
|
183
|
+
let hyper = UInt32(cmdKey | controlKey | optionKey | shiftKey)
|
|
184
|
+
let cmdShift = UInt32(cmdKey | shiftKey)
|
|
185
|
+
let cmdOpt = UInt32(cmdKey | optionKey)
|
|
186
|
+
let ctrlOpt = UInt32(controlKey | optionKey)
|
|
187
|
+
|
|
188
|
+
func bind(_ action: HotkeyAction, _ keyCode: UInt32, _ mods: UInt32) {
|
|
189
|
+
d[action] = KeyBinding(
|
|
190
|
+
keyCode: keyCode,
|
|
191
|
+
carbonModifiers: mods,
|
|
192
|
+
displayParts: KeyBinding.displayParts(keyCode: keyCode, carbonModifiers: mods)
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// App
|
|
197
|
+
bind(.palette, 46, cmdShift) // Cmd+Shift+M
|
|
198
|
+
bind(.screenMap, 18, hyper) // Hyper+1
|
|
199
|
+
bind(.bezel, 19, hyper) // Hyper+2
|
|
200
|
+
bind(.cheatSheet, 20, hyper) // Hyper+3
|
|
201
|
+
bind(.desktopInventory, 21, hyper) // Hyper+4
|
|
202
|
+
bind(.omniSearch, 23, hyper) // Hyper+5
|
|
203
|
+
|
|
204
|
+
// Layers: Cmd+Option+1-9
|
|
205
|
+
let layerKeyCodes: [UInt32] = [18, 19, 20, 21, 23, 22, 26, 28, 25]
|
|
206
|
+
for (i, action) in HotkeyAction.layerActions.enumerated() {
|
|
207
|
+
bind(action, layerKeyCodes[i], cmdOpt)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Tiling: Ctrl+Option for all
|
|
211
|
+
bind(.tileLeft, 123, ctrlOpt) // Ctrl+Opt+←
|
|
212
|
+
bind(.tileRight, 124, ctrlOpt) // Ctrl+Opt+→
|
|
213
|
+
bind(.tileTop, 126, ctrlOpt) // Ctrl+Opt+↑
|
|
214
|
+
bind(.tileBottom, 125, ctrlOpt) // Ctrl+Opt+↓
|
|
215
|
+
bind(.tileMaximize, 36, ctrlOpt) // Ctrl+Opt+Return
|
|
216
|
+
bind(.tileCenter, 8, ctrlOpt) // Ctrl+Opt+C
|
|
217
|
+
bind(.tileTopLeft, 32, ctrlOpt) // Ctrl+Opt+U
|
|
218
|
+
bind(.tileTopRight, 34, ctrlOpt) // Ctrl+Opt+I
|
|
219
|
+
bind(.tileBottomLeft, 38, ctrlOpt) // Ctrl+Opt+J
|
|
220
|
+
bind(.tileBottomRight, 40, ctrlOpt) // Ctrl+Opt+K
|
|
221
|
+
bind(.tileDistribute, 2, ctrlOpt) // Ctrl+Opt+D
|
|
222
|
+
bind(.tileLeftThird, 18, ctrlOpt) // Ctrl+Opt+1
|
|
223
|
+
bind(.tileCenterThird, 19, ctrlOpt) // Ctrl+Opt+2
|
|
224
|
+
bind(.tileRightThird, 20, ctrlOpt) // Ctrl+Opt+3
|
|
225
|
+
|
|
226
|
+
return d
|
|
227
|
+
}()
|
|
228
|
+
|
|
229
|
+
private init() {
|
|
230
|
+
// Start with defaults
|
|
231
|
+
var merged = Self.defaultBindings
|
|
232
|
+
|
|
233
|
+
// Layer 2: UserDefaults overrides
|
|
234
|
+
let ud = UserDefaults.standard
|
|
235
|
+
for action in HotkeyAction.allCases {
|
|
236
|
+
let key = "hotkey.\(action.rawValue)"
|
|
237
|
+
if let data = ud.data(forKey: key),
|
|
238
|
+
let binding = try? JSONDecoder().decode(KeyBinding.self, from: data) {
|
|
239
|
+
merged[action] = binding
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Layer 3: ~/.lattices/hotkeys.json overrides
|
|
244
|
+
let jsonPath = NSHomeDirectory() + "/.lattices/hotkeys.json"
|
|
245
|
+
if let data = FileManager.default.contents(atPath: jsonPath),
|
|
246
|
+
let overrides = try? JSONDecoder().decode([String: KeyBinding].self, from: data) {
|
|
247
|
+
for (rawValue, binding) in overrides {
|
|
248
|
+
if let action = HotkeyAction(rawValue: rawValue) {
|
|
249
|
+
merged[action] = binding
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
self.bindings = merged
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// MARK: - Registration
|
|
258
|
+
|
|
259
|
+
func register(action: HotkeyAction, callback: @escaping () -> Void) {
|
|
260
|
+
callbacks[action] = callback
|
|
261
|
+
guard let binding = bindings[action] else { return }
|
|
262
|
+
HotkeyManager.shared.registerSingle(
|
|
263
|
+
id: action.carbonID,
|
|
264
|
+
keyCode: binding.keyCode,
|
|
265
|
+
modifiers: binding.carbonModifiers,
|
|
266
|
+
callback: callback
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// MARK: - Update
|
|
271
|
+
|
|
272
|
+
func updateBinding(for action: HotkeyAction, to binding: KeyBinding) {
|
|
273
|
+
bindings[action] = binding
|
|
274
|
+
|
|
275
|
+
// Persist to UserDefaults
|
|
276
|
+
if let data = try? JSONEncoder().encode(binding) {
|
|
277
|
+
UserDefaults.standard.set(data, forKey: "hotkey.\(action.rawValue)")
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Re-register if we have a callback
|
|
281
|
+
if let callback = callbacks[action] {
|
|
282
|
+
HotkeyManager.shared.registerSingle(
|
|
283
|
+
id: action.carbonID,
|
|
284
|
+
keyCode: binding.keyCode,
|
|
285
|
+
modifiers: binding.carbonModifiers,
|
|
286
|
+
callback: callback
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// MARK: - Reset
|
|
292
|
+
|
|
293
|
+
func resetBinding(for action: HotkeyAction) {
|
|
294
|
+
guard let defaultBinding = Self.defaultBindings[action] else { return }
|
|
295
|
+
UserDefaults.standard.removeObject(forKey: "hotkey.\(action.rawValue)")
|
|
296
|
+
bindings[action] = defaultBinding
|
|
297
|
+
|
|
298
|
+
if let callback = callbacks[action] {
|
|
299
|
+
HotkeyManager.shared.registerSingle(
|
|
300
|
+
id: action.carbonID,
|
|
301
|
+
keyCode: defaultBinding.keyCode,
|
|
302
|
+
modifiers: defaultBinding.carbonModifiers,
|
|
303
|
+
callback: callback
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
func resetAll() {
|
|
309
|
+
for action in HotkeyAction.allCases {
|
|
310
|
+
UserDefaults.standard.removeObject(forKey: "hotkey.\(action.rawValue)")
|
|
311
|
+
}
|
|
312
|
+
bindings = Self.defaultBindings
|
|
313
|
+
|
|
314
|
+
// Re-register all with stored callbacks
|
|
315
|
+
for (action, callback) in callbacks {
|
|
316
|
+
guard let binding = bindings[action] else { continue }
|
|
317
|
+
HotkeyManager.shared.registerSingle(
|
|
318
|
+
id: action.carbonID,
|
|
319
|
+
keyCode: binding.keyCode,
|
|
320
|
+
modifiers: binding.carbonModifiers,
|
|
321
|
+
callback: callback
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// MARK: - Conflict detection
|
|
327
|
+
|
|
328
|
+
func conflicts(for action: HotkeyAction, with binding: KeyBinding) -> HotkeyAction? {
|
|
329
|
+
for (existingAction, existingBinding) in bindings {
|
|
330
|
+
if existingAction != action &&
|
|
331
|
+
existingBinding.keyCode == binding.keyCode &&
|
|
332
|
+
existingBinding.carbonModifiers == binding.carbonModifiers {
|
|
333
|
+
return existingAction
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return nil
|
|
337
|
+
}
|
|
338
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
class InventoryManager: ObservableObject {
|
|
4
|
+
static let shared = InventoryManager()
|
|
5
|
+
|
|
6
|
+
@Published var orphans: [TmuxSession] = []
|
|
7
|
+
@Published var allSessions: [TmuxSession] = []
|
|
8
|
+
|
|
9
|
+
func refresh() {
|
|
10
|
+
// Always query fresh — this is called on explicit user refresh
|
|
11
|
+
let sessions = TmuxQuery.listSessions()
|
|
12
|
+
|
|
13
|
+
// Build set of managed session names
|
|
14
|
+
var managed = Set<String>()
|
|
15
|
+
|
|
16
|
+
// From scanned projects
|
|
17
|
+
for project in ProjectScanner.shared.projects {
|
|
18
|
+
managed.insert(project.sessionName)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// From workspace tab groups
|
|
22
|
+
if let groups = WorkspaceManager.shared.config?.groups {
|
|
23
|
+
for group in groups {
|
|
24
|
+
for tab in group.tabs {
|
|
25
|
+
managed.insert(WorkspaceManager.sessionName(for: tab.path))
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
DispatchQueue.main.async {
|
|
31
|
+
self.allSessions = sessions
|
|
32
|
+
self.orphans = sessions.filter { !managed.contains($0.name) }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
|
|
3
|
+
struct InventoryPath: Equatable {
|
|
4
|
+
let display: String
|
|
5
|
+
let space: String
|
|
6
|
+
let appType: String
|
|
7
|
+
let appName: String
|
|
8
|
+
let windowTitle: String
|
|
9
|
+
|
|
10
|
+
var description: String {
|
|
11
|
+
[display, space, appType, appName, windowTitle]
|
|
12
|
+
.map { sanitize($0) }
|
|
13
|
+
.joined(separator: ".")
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
func matches(pattern: String) -> Bool {
|
|
17
|
+
let segments = description.split(separator: ".").map(String.init)
|
|
18
|
+
let patternSegments = pattern.lowercased().split(separator: ".").map(String.init)
|
|
19
|
+
|
|
20
|
+
for (i, pat) in patternSegments.enumerated() {
|
|
21
|
+
guard i < segments.count else { return false }
|
|
22
|
+
if pat == "*" { continue }
|
|
23
|
+
if !segments[i].hasPrefix(pat) { return false }
|
|
24
|
+
}
|
|
25
|
+
return true
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static func displayName(for screen: NSScreen, isMain: Bool) -> String {
|
|
29
|
+
if isMain { return "main" }
|
|
30
|
+
return sanitizeStatic(screen.localizedName)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private func sanitize(_ s: String) -> String {
|
|
34
|
+
Self.sanitizeStatic(s)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private static func sanitizeStatic(_ s: String) -> String {
|
|
38
|
+
s.lowercased()
|
|
39
|
+
.replacingOccurrences(of: "[\\s./\\\\]+", with: "-", options: .regularExpression)
|
|
40
|
+
.replacingOccurrences(of: "[^a-z0-9_-]", with: "", options: .regularExpression)
|
|
41
|
+
.trimmingCharacters(in: CharacterSet(charactersIn: "-"))
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import Carbon
|
|
3
|
+
|
|
4
|
+
// MARK: - KeyRecorderView
|
|
5
|
+
|
|
6
|
+
struct KeyRecorderView: View {
|
|
7
|
+
let action: HotkeyAction
|
|
8
|
+
@ObservedObject var store: HotkeyStore
|
|
9
|
+
|
|
10
|
+
@State private var isCapturing = false
|
|
11
|
+
@State private var conflictAction: HotkeyAction?
|
|
12
|
+
@State private var pendingBinding: KeyBinding?
|
|
13
|
+
@State private var showConflict = false
|
|
14
|
+
|
|
15
|
+
private var binding: KeyBinding? { store.bindings[action] }
|
|
16
|
+
private var isModified: Bool {
|
|
17
|
+
binding != HotkeyStore.defaultBindings[action]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
var body: some View {
|
|
21
|
+
HStack(spacing: 8) {
|
|
22
|
+
// Action label
|
|
23
|
+
Text(action.label)
|
|
24
|
+
.font(Typo.caption(11))
|
|
25
|
+
.foregroundColor(Palette.textDim)
|
|
26
|
+
.frame(minWidth: 60, idealWidth: 90, alignment: .trailing)
|
|
27
|
+
.lineLimit(1)
|
|
28
|
+
|
|
29
|
+
// Key badges or capture prompt
|
|
30
|
+
if isCapturing {
|
|
31
|
+
Text("Press shortcut...")
|
|
32
|
+
.font(Typo.mono(11))
|
|
33
|
+
.foregroundColor(Palette.running)
|
|
34
|
+
.frame(minWidth: 80, alignment: .leading)
|
|
35
|
+
} else if let binding = binding {
|
|
36
|
+
HStack(spacing: 4) {
|
|
37
|
+
ForEach(binding.displayParts, id: \.self) { part in
|
|
38
|
+
keyBadge(part)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
.frame(minWidth: 80, alignment: .leading)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
Spacer()
|
|
45
|
+
|
|
46
|
+
// Edit button
|
|
47
|
+
Button {
|
|
48
|
+
if isCapturing {
|
|
49
|
+
isCapturing = false
|
|
50
|
+
} else {
|
|
51
|
+
isCapturing = true
|
|
52
|
+
}
|
|
53
|
+
} label: {
|
|
54
|
+
Text(isCapturing ? "Cancel" : "Edit")
|
|
55
|
+
.font(Typo.caption(10))
|
|
56
|
+
.foregroundColor(isCapturing ? Palette.kill : Palette.textDim)
|
|
57
|
+
.padding(.horizontal, 8)
|
|
58
|
+
.padding(.vertical, 3)
|
|
59
|
+
.background(
|
|
60
|
+
RoundedRectangle(cornerRadius: 3)
|
|
61
|
+
.fill(Palette.surface)
|
|
62
|
+
.overlay(
|
|
63
|
+
RoundedRectangle(cornerRadius: 3)
|
|
64
|
+
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
.buttonStyle(.plain)
|
|
69
|
+
|
|
70
|
+
// Reset link (only when modified)
|
|
71
|
+
if isModified {
|
|
72
|
+
Button {
|
|
73
|
+
store.resetBinding(for: action)
|
|
74
|
+
} label: {
|
|
75
|
+
Text("Reset")
|
|
76
|
+
.font(Typo.caption(10))
|
|
77
|
+
.foregroundColor(Palette.detach)
|
|
78
|
+
}
|
|
79
|
+
.buttonStyle(.plain)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
.padding(.vertical, 2)
|
|
83
|
+
.background(
|
|
84
|
+
isCapturing
|
|
85
|
+
? KeyCaptureOverlay(onCapture: handleCapture, onCancel: { isCapturing = false })
|
|
86
|
+
: nil
|
|
87
|
+
)
|
|
88
|
+
.alert("Shortcut Conflict", isPresented: $showConflict) {
|
|
89
|
+
Button("Replace") {
|
|
90
|
+
if let pending = pendingBinding, let conflict = conflictAction {
|
|
91
|
+
// Remove conflicting binding by resetting it
|
|
92
|
+
store.resetBinding(for: conflict)
|
|
93
|
+
store.updateBinding(for: action, to: pending)
|
|
94
|
+
}
|
|
95
|
+
pendingBinding = nil
|
|
96
|
+
conflictAction = nil
|
|
97
|
+
isCapturing = false
|
|
98
|
+
}
|
|
99
|
+
Button("Cancel", role: .cancel) {
|
|
100
|
+
pendingBinding = nil
|
|
101
|
+
conflictAction = nil
|
|
102
|
+
}
|
|
103
|
+
} message: {
|
|
104
|
+
if let conflict = conflictAction {
|
|
105
|
+
Text("This shortcut is already assigned to \"\(conflict.label)\". Replace it?")
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private func handleCapture(_ binding: KeyBinding) {
|
|
111
|
+
if let conflict = store.conflicts(for: action, with: binding) {
|
|
112
|
+
pendingBinding = binding
|
|
113
|
+
conflictAction = conflict
|
|
114
|
+
showConflict = true
|
|
115
|
+
} else {
|
|
116
|
+
store.updateBinding(for: action, to: binding)
|
|
117
|
+
isCapturing = false
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private func keyBadge(_ key: String) -> some View {
|
|
122
|
+
Text(key)
|
|
123
|
+
.font(Typo.geistMonoBold(10))
|
|
124
|
+
.foregroundColor(Palette.text)
|
|
125
|
+
.padding(.horizontal, 6)
|
|
126
|
+
.padding(.vertical, 3)
|
|
127
|
+
.background(
|
|
128
|
+
RoundedRectangle(cornerRadius: 3)
|
|
129
|
+
.fill(Palette.surface)
|
|
130
|
+
.overlay(
|
|
131
|
+
RoundedRectangle(cornerRadius: 3)
|
|
132
|
+
.strokeBorder(Palette.border, lineWidth: 0.5)
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// MARK: - KeyCaptureOverlay (NSViewRepresentable bridge)
|
|
139
|
+
|
|
140
|
+
struct KeyCaptureOverlay: NSViewRepresentable {
|
|
141
|
+
let onCapture: (KeyBinding) -> Void
|
|
142
|
+
let onCancel: () -> Void
|
|
143
|
+
|
|
144
|
+
func makeNSView(context: Context) -> KeyCaptureNSView {
|
|
145
|
+
let view = KeyCaptureNSView()
|
|
146
|
+
view.onCapture = onCapture
|
|
147
|
+
view.onCancel = onCancel
|
|
148
|
+
return view
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
func updateNSView(_ nsView: KeyCaptureNSView, context: Context) {
|
|
152
|
+
nsView.onCapture = onCapture
|
|
153
|
+
nsView.onCancel = onCancel
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
class KeyCaptureNSView: NSView {
|
|
158
|
+
var onCapture: ((KeyBinding) -> Void)?
|
|
159
|
+
var onCancel: (() -> Void)?
|
|
160
|
+
private var monitor: Any?
|
|
161
|
+
|
|
162
|
+
override func viewDidMoveToWindow() {
|
|
163
|
+
super.viewDidMoveToWindow()
|
|
164
|
+
if window != nil {
|
|
165
|
+
startMonitoring()
|
|
166
|
+
} else {
|
|
167
|
+
stopMonitoring()
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private func startMonitoring() {
|
|
172
|
+
stopMonitoring()
|
|
173
|
+
monitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
|
|
174
|
+
guard let self = self else { return event }
|
|
175
|
+
|
|
176
|
+
// Escape cancels
|
|
177
|
+
if event.keyCode == 53 {
|
|
178
|
+
self.onCancel?()
|
|
179
|
+
return nil
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Require at least one modifier
|
|
183
|
+
let mods = event.modifierFlags.intersection([.command, .shift, .option, .control])
|
|
184
|
+
guard !mods.isEmpty else { return nil }
|
|
185
|
+
|
|
186
|
+
let keyCode = UInt32(event.keyCode)
|
|
187
|
+
let carbonMods = KeyBinding.carbonModifiers(from: mods)
|
|
188
|
+
let parts = KeyBinding.displayParts(keyCode: keyCode, carbonModifiers: carbonMods)
|
|
189
|
+
|
|
190
|
+
let binding = KeyBinding(
|
|
191
|
+
keyCode: keyCode,
|
|
192
|
+
carbonModifiers: carbonMods,
|
|
193
|
+
displayParts: parts
|
|
194
|
+
)
|
|
195
|
+
self.onCapture?(binding)
|
|
196
|
+
return nil // swallow the event
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private func stopMonitoring() {
|
|
201
|
+
if let monitor = monitor {
|
|
202
|
+
NSEvent.removeMonitor(monitor)
|
|
203
|
+
self.monitor = nil
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
deinit {
|
|
208
|
+
stopMonitoring()
|
|
209
|
+
}
|
|
210
|
+
}
|