@lattices/cli 0.4.14 → 0.5.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 +5 -7
- package/apps/mac/Info.plist +2 -2
- package/apps/mac/Lattices.app/Contents/Info.plist +4 -12
- package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/bin/lattices-app.ts +110 -17
- package/bin/lattices-build +125 -0
- package/bin/lattices-dev +89 -16
- package/bin/lattices.ts +977 -16
- package/docs/agents.md +81 -4
- package/docs/ai-chat-ux-review.md +416 -0
- package/docs/api.md +135 -3
- package/docs/app.md +30 -8
- package/docs/config.md +4 -0
- package/docs/mouse-gestures.md +60 -1
- package/docs/proposals/LAT-004-interactive-overlay-actors.md +1 -1
- package/docs/proposals/LAT-005-action-runtime-product-spine.md +914 -0
- package/docs/proposals/LAT-006-mira-in-lattices.md +553 -0
- package/docs/reference/dewey.config.ts +2 -2
- package/docs/release.md +171 -0
- package/docs/repo-structure.md +4 -5
- package/docs/voice.md +11 -27
- package/package.json +9 -10
- package/apps/mac/Package.swift +0 -27
- package/apps/mac/Sources/AppShell/App.swift +0 -26
- package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +0 -27
- package/apps/mac/Sources/AppShell/AppDelegate.swift +0 -189
- package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +0 -25
- package/apps/mac/Sources/AppShell/AppShellView.swift +0 -171
- package/apps/mac/Sources/AppShell/AppUpdater.swift +0 -305
- package/apps/mac/Sources/AppShell/CliActionLauncher.swift +0 -50
- package/apps/mac/Sources/AppShell/HomeDashboardView.swift +0 -133
- package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +0 -87
- package/apps/mac/Sources/AppShell/KeyRecorderView.swift +0 -210
- package/apps/mac/Sources/AppShell/LatticesRuntime.swift +0 -104
- package/apps/mac/Sources/AppShell/MainView.swift +0 -847
- package/apps/mac/Sources/AppShell/MainWindow.swift +0 -83
- package/apps/mac/Sources/AppShell/MenuBarController.swift +0 -177
- package/apps/mac/Sources/AppShell/OnboardingView.swift +0 -483
- package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +0 -366
- package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +0 -70
- package/apps/mac/Sources/AppShell/Preferences.swift +0 -297
- package/apps/mac/Sources/AppShell/SettingsView.swift +0 -3163
- package/apps/mac/Sources/AppShell/SettingsWindow.swift +0 -34
- package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +0 -13
- package/apps/mac/Sources/Core/Actions/HotkeyManager.swift +0 -256
- package/apps/mac/Sources/Core/Actions/HotkeyStore.swift +0 -399
- package/apps/mac/Sources/Core/Actions/IntentEngine.swift +0 -988
- package/apps/mac/Sources/Core/Actions/IntentSchema.swift +0 -94
- package/apps/mac/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -54
- package/apps/mac/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -56
- package/apps/mac/Sources/Core/Actions/Intents/FocusIntent.swift +0 -69
- package/apps/mac/Sources/Core/Actions/Intents/HelpIntent.swift +0 -41
- package/apps/mac/Sources/Core/Actions/Intents/KillIntent.swift +0 -47
- package/apps/mac/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -53
- package/apps/mac/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -67
- package/apps/mac/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -32
- package/apps/mac/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -30
- package/apps/mac/Sources/Core/Actions/Intents/ScanIntent.swift +0 -52
- package/apps/mac/Sources/Core/Actions/Intents/SearchIntent.swift +0 -190
- package/apps/mac/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -50
- package/apps/mac/Sources/Core/Actions/Intents/TileIntent.swift +0 -61
- package/apps/mac/Sources/Core/Actions/PaletteCommand.swift +0 -439
- package/apps/mac/Sources/Core/Actions/VoiceIntentResolver.swift +0 -713
- package/apps/mac/Sources/Core/Companion/CompanionActivityLog.swift +0 -70
- package/apps/mac/Sources/Core/Companion/CompanionKeyboardController.swift +0 -141
- package/apps/mac/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -454
- package/apps/mac/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -555
- package/apps/mac/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -629
- package/apps/mac/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -204
- package/apps/mac/Sources/Core/Companion/LatticesDeckHost.swift +0 -1463
- package/apps/mac/Sources/Core/Daemon/DaemonProtocol.swift +0 -114
- package/apps/mac/Sources/Core/Daemon/DaemonServer.swift +0 -427
- package/apps/mac/Sources/Core/Daemon/LatticesApi.swift +0 -2965
- package/apps/mac/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -111
- package/apps/mac/Sources/Core/Desktop/AppTypeClassifier.swift +0 -106
- package/apps/mac/Sources/Core/Desktop/DesktopModel.swift +0 -331
- package/apps/mac/Sources/Core/Desktop/DesktopModelTypes.swift +0 -73
- package/apps/mac/Sources/Core/Desktop/InventoryManager.swift +0 -35
- package/apps/mac/Sources/Core/Desktop/InventoryPath.swift +0 -43
- package/apps/mac/Sources/Core/Desktop/MouseFinder.swift +0 -527
- package/apps/mac/Sources/Core/Desktop/OcrModel.swift +0 -467
- package/apps/mac/Sources/Core/Desktop/OcrStore.swift +0 -329
- package/apps/mac/Sources/Core/Desktop/PlacementSpec.swift +0 -195
- package/apps/mac/Sources/Core/Desktop/SessionWindowLocator.swift +0 -139
- package/apps/mac/Sources/Core/Desktop/TilePickerView.swift +0 -209
- package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +0 -33
- package/apps/mac/Sources/Core/Desktop/WindowDragSnapController.swift +0 -429
- package/apps/mac/Sources/Core/Desktop/WindowPreviewCard.swift +0 -100
- package/apps/mac/Sources/Core/Desktop/WindowPreviewStore.swift +0 -112
- package/apps/mac/Sources/Core/Desktop/WindowSelectionStore.swift +0 -76
- package/apps/mac/Sources/Core/Desktop/WindowTiler.swift +0 -2222
- package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +0 -124
- package/apps/mac/Sources/Core/Input/EventTapThread.swift +0 -54
- package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +0 -20
- package/apps/mac/Sources/Core/Input/KeyboardRemapConfig.swift +0 -69
- package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +0 -346
- package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +0 -141
- package/apps/mac/Sources/Core/Input/MouseGestureConfig.swift +0 -499
- package/apps/mac/Sources/Core/Input/MouseGestureController.swift +0 -2583
- package/apps/mac/Sources/Core/Input/MouseInputDeviceStore.swift +0 -98
- package/apps/mac/Sources/Core/Input/MouseInputEventViewer.swift +0 -272
- package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +0 -170
- package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +0 -39
- package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +0 -624
- package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +0 -56
- package/apps/mac/Sources/Core/Overlays/AppWindowShell.swift +0 -63
- package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -1566
- package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -1927
- package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -196
- package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -307
- package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -67
- package/apps/mac/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -576
- package/apps/mac/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -279
- package/apps/mac/Sources/Core/Overlays/HUD/HUDController.swift +0 -1158
- package/apps/mac/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -849
- package/apps/mac/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -179
- package/apps/mac/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -596
- package/apps/mac/Sources/Core/Overlays/HUD/HUDState.swift +0 -367
- package/apps/mac/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -243
- package/apps/mac/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -334
- package/apps/mac/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -203
- package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -280
- package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -422
- package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -94
- package/apps/mac/Sources/Core/Overlays/OverlayPanelShell.swift +0 -241
- package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +0 -3135
- package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +0 -3977
- package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -119
- package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +0 -1217
- package/apps/mac/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +0 -1575
- package/apps/mac/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -148
- package/apps/mac/Sources/Core/Pi/PiAuthPromptCard.swift +0 -90
- package/apps/mac/Sources/Core/Pi/PiChatDock.swift +0 -564
- package/apps/mac/Sources/Core/Pi/PiChatSession.swift +0 -1948
- package/apps/mac/Sources/Core/Pi/PiInstallCallout.swift +0 -86
- package/apps/mac/Sources/Core/Pi/PiProviderSetupCallout.swift +0 -99
- package/apps/mac/Sources/Core/Pi/PiWorkspaceView.swift +0 -510
- package/apps/mac/Sources/Core/System/Capability.swift +0 -79
- package/apps/mac/Sources/Core/System/DiagnosticLog.swift +0 -373
- package/apps/mac/Sources/Core/System/EventBus.swift +0 -31
- package/apps/mac/Sources/Core/System/PermissionChecker.swift +0 -224
- package/apps/mac/Sources/Core/System/ProcessModel.swift +0 -199
- package/apps/mac/Sources/Core/System/ProcessQuery.swift +0 -151
- package/apps/mac/Sources/Core/System/SystemTelemetryMonitor.swift +0 -273
- package/apps/mac/Sources/Core/Voice/AdvisorLearningStore.swift +0 -90
- package/apps/mac/Sources/Core/Voice/AgentSession.swift +0 -377
- package/apps/mac/Sources/Core/Voice/AudioProvider.swift +0 -555
- package/apps/mac/Sources/Core/Voice/HandsOffSession.swift +0 -839
- package/apps/mac/Sources/Core/Voice/VoiceChatView.swift +0 -192
- package/apps/mac/Sources/Core/Voice/VoxClient.swift +0 -454
- package/apps/mac/Sources/Core/Workspace/Project.swift +0 -28
- package/apps/mac/Sources/Core/Workspace/ProjectScanner.swift +0 -141
- package/apps/mac/Sources/Core/Workspace/SessionLayerStore.swift +0 -285
- package/apps/mac/Sources/Core/Workspace/SessionManager.swift +0 -75
- package/apps/mac/Sources/Core/Workspace/Terminal/Terminal.swift +0 -259
- package/apps/mac/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -156
- package/apps/mac/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -200
- package/apps/mac/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -60
- package/apps/mac/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -105
- package/apps/mac/Sources/Core/Workspace/WorkspaceManager.swift +0 -1027
- package/apps/mac/Sources/UI/ActionRow.swift +0 -78
- package/apps/mac/Sources/UI/OrphanRow.swift +0 -129
- package/apps/mac/Sources/UI/ProjectRow.swift +0 -368
- package/apps/mac/Sources/UI/TabGroupRow.swift +0 -178
- package/apps/mac/Sources/UI/Theme.swift +0 -164
- package/apps/mac/Tests/StageDragTests.swift +0 -333
- package/apps/mac/Tests/StageJoinTests.swift +0 -313
- package/apps/mac/Tests/StageManagerTests.swift +0 -280
- package/apps/mac/Tests/StageTileTests.swift +0 -353
- package/swift/Package.swift +0 -20
- package/swift/Sources/DeckKit/DeckAction.swift +0 -51
- package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +0 -152
- package/swift/Sources/DeckKit/DeckCockpit.swift +0 -82
- package/swift/Sources/DeckKit/DeckHost.swift +0 -7
- package/swift/Sources/DeckKit/DeckManifest.swift +0 -145
- package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +0 -533
- package/swift/Sources/DeckKit/DeckTrackpad.swift +0 -63
- package/swift/Sources/DeckKit/DeckValue.swift +0 -93
- package/swift/Sources/DeckKit/DeckVoiceError.swift +0 -88
- package/swift/Tests/DeckKitTests/DeckKitTests.swift +0 -286
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
import AppKit
|
|
2
|
-
|
|
3
|
-
enum Terminal: String, CaseIterable, Identifiable {
|
|
4
|
-
case terminal = "Terminal"
|
|
5
|
-
case iterm2 = "iTerm2"
|
|
6
|
-
case warp = "Warp"
|
|
7
|
-
case ghostty = "Ghostty"
|
|
8
|
-
case kitty = "Kitty"
|
|
9
|
-
case alacritty = "Alacritty"
|
|
10
|
-
|
|
11
|
-
var id: String { rawValue }
|
|
12
|
-
|
|
13
|
-
var bundleId: String {
|
|
14
|
-
switch self {
|
|
15
|
-
case .terminal: return "com.apple.Terminal"
|
|
16
|
-
case .iterm2: return "com.googlecode.iterm2"
|
|
17
|
-
case .warp: return "dev.warp.Warp-Stable"
|
|
18
|
-
case .ghostty: return "com.mitchellh.ghostty"
|
|
19
|
-
case .kitty: return "net.kovidgoyal.kitty"
|
|
20
|
-
case .alacritty: return "org.alacritty"
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
var isInstalled: Bool {
|
|
25
|
-
NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleId) != nil
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
static var installed: [Terminal] {
|
|
29
|
-
allCases.filter(\.isInstalled)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/// Launch a command in this terminal
|
|
33
|
-
func launch(command: String, in directory: String) {
|
|
34
|
-
// Use single quotes for the shell command to avoid AppleScript escaping issues
|
|
35
|
-
let dir = directory.replacingOccurrences(of: "'", with: "'\\''")
|
|
36
|
-
let cmd = command.replacingOccurrences(of: "'", with: "'\\''")
|
|
37
|
-
let fullCmd = "cd '\(dir)' && \(cmd)"
|
|
38
|
-
|
|
39
|
-
switch self {
|
|
40
|
-
case .terminal:
|
|
41
|
-
runOsascript(
|
|
42
|
-
"tell application \"Terminal\"",
|
|
43
|
-
"activate",
|
|
44
|
-
"do script \"\(fullCmd)\"",
|
|
45
|
-
"end tell"
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
case .iterm2:
|
|
49
|
-
runOsascript(
|
|
50
|
-
"tell application \"iTerm2\"",
|
|
51
|
-
"activate",
|
|
52
|
-
"set newWindow to (create window with default profile)",
|
|
53
|
-
"tell current session of newWindow",
|
|
54
|
-
"write text \"\(fullCmd)\"",
|
|
55
|
-
"end tell",
|
|
56
|
-
"end tell"
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
case .warp:
|
|
60
|
-
let task = Process()
|
|
61
|
-
task.executableURL = URL(fileURLWithPath: "/usr/bin/open")
|
|
62
|
-
task.arguments = ["-a", "Warp", directory]
|
|
63
|
-
try? task.run()
|
|
64
|
-
task.waitUntilExit()
|
|
65
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
|
66
|
-
runOsascript(
|
|
67
|
-
"tell application \"System Events\"",
|
|
68
|
-
"tell process \"Warp\"",
|
|
69
|
-
"keystroke \"\(cmd)\"",
|
|
70
|
-
"keystroke return",
|
|
71
|
-
"end tell",
|
|
72
|
-
"end tell"
|
|
73
|
-
)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
case .ghostty:
|
|
77
|
-
let task = Process()
|
|
78
|
-
task.executableURL = URL(fileURLWithPath: "/usr/bin/open")
|
|
79
|
-
task.arguments = ["-a", "Ghostty"]
|
|
80
|
-
task.environment = ["GHOSTTY_SHELL_COMMAND": fullCmd]
|
|
81
|
-
try? task.run()
|
|
82
|
-
|
|
83
|
-
case .kitty:
|
|
84
|
-
if let appUrl = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleId) {
|
|
85
|
-
let kittyBin = appUrl.appendingPathComponent("Contents/MacOS/kitty").path
|
|
86
|
-
let task = Process()
|
|
87
|
-
task.executableURL = URL(fileURLWithPath: kittyBin)
|
|
88
|
-
task.arguments = ["--single-instance", "--directory", directory, "sh", "-c", command]
|
|
89
|
-
try? task.run()
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
case .alacritty:
|
|
93
|
-
if let appUrl = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleId) {
|
|
94
|
-
let bin = appUrl.appendingPathComponent("Contents/MacOS/alacritty").path
|
|
95
|
-
let task = Process()
|
|
96
|
-
task.executableURL = URL(fileURLWithPath: bin)
|
|
97
|
-
task.arguments = ["--working-directory", directory, "-e", "sh", "-c", command]
|
|
98
|
-
try? task.run()
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/// Launch a command in a new tab of the current terminal window
|
|
104
|
-
func launchTab(command: String, in directory: String, tabName: String? = nil) {
|
|
105
|
-
let dir = directory.replacingOccurrences(of: "'", with: "'\\''")
|
|
106
|
-
let cmd = command.replacingOccurrences(of: "'", with: "'\\''")
|
|
107
|
-
let fullCmd = "cd '\(dir)' && \(cmd)"
|
|
108
|
-
|
|
109
|
-
switch self {
|
|
110
|
-
case .iterm2:
|
|
111
|
-
var lines = [
|
|
112
|
-
"tell application \"iTerm2\"",
|
|
113
|
-
"activate",
|
|
114
|
-
"if (count of windows) = 0 then",
|
|
115
|
-
" create window with default profile",
|
|
116
|
-
"else",
|
|
117
|
-
" tell current window to create tab with default profile",
|
|
118
|
-
"end if",
|
|
119
|
-
"tell current session of current tab of current window",
|
|
120
|
-
" write text \"\(fullCmd)\"",
|
|
121
|
-
]
|
|
122
|
-
if let name = tabName {
|
|
123
|
-
let escaped = name.replacingOccurrences(of: "\"", with: "\\\"")
|
|
124
|
-
lines.append(" set name to \"\(escaped)\"")
|
|
125
|
-
}
|
|
126
|
-
lines.append("end tell")
|
|
127
|
-
lines.append("end tell")
|
|
128
|
-
runOsascriptLines(lines)
|
|
129
|
-
|
|
130
|
-
case .terminal:
|
|
131
|
-
var lines = [
|
|
132
|
-
"tell application \"Terminal\"",
|
|
133
|
-
"activate",
|
|
134
|
-
"if (count of windows) = 0 then",
|
|
135
|
-
" do script \"\(fullCmd)\"",
|
|
136
|
-
"else",
|
|
137
|
-
" do script \"\(fullCmd)\" in front window",
|
|
138
|
-
"end if",
|
|
139
|
-
]
|
|
140
|
-
if let name = tabName {
|
|
141
|
-
let escaped = name.replacingOccurrences(of: "\"", with: "\\\"")
|
|
142
|
-
lines.append("set custom title of selected tab of front window to \"\(escaped)\"")
|
|
143
|
-
}
|
|
144
|
-
lines.append("end tell")
|
|
145
|
-
runOsascriptLines(lines)
|
|
146
|
-
|
|
147
|
-
default:
|
|
148
|
-
// Terminals without AppleScript tab support: fall back to new window
|
|
149
|
-
launch(command: command, in: directory)
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/// Rename the current/frontmost tab in this terminal
|
|
154
|
-
func nameTab(_ name: String) {
|
|
155
|
-
let escaped = name.replacingOccurrences(of: "\"", with: "\\\"")
|
|
156
|
-
switch self {
|
|
157
|
-
case .iterm2:
|
|
158
|
-
runOsascript(
|
|
159
|
-
"tell application \"iTerm2\"",
|
|
160
|
-
"tell current session of current tab of current window",
|
|
161
|
-
"set name to \"\(escaped)\"",
|
|
162
|
-
"end tell",
|
|
163
|
-
"end tell"
|
|
164
|
-
)
|
|
165
|
-
case .terminal:
|
|
166
|
-
runOsascript(
|
|
167
|
-
"tell application \"Terminal\"",
|
|
168
|
-
"set custom title of selected tab of front window to \"\(escaped)\"",
|
|
169
|
-
"end tell"
|
|
170
|
-
)
|
|
171
|
-
default:
|
|
172
|
-
break
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/// The tag we put in the terminal window title via tmux set-titles
|
|
177
|
-
static func windowTag(for session: String) -> String {
|
|
178
|
-
"[lattices:\(session)]"
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/// Find and focus the existing terminal window by its [lattices:name] tag, or open a new attach
|
|
182
|
-
func focusOrAttach(session: String) {
|
|
183
|
-
let tag = Terminal.windowTag(for: session)
|
|
184
|
-
|
|
185
|
-
switch self {
|
|
186
|
-
case .terminal:
|
|
187
|
-
runOsascript(
|
|
188
|
-
"tell application \"Terminal\"",
|
|
189
|
-
"activate",
|
|
190
|
-
"set found to false",
|
|
191
|
-
"repeat with w in windows",
|
|
192
|
-
" if name of w contains \"\(tag)\" then",
|
|
193
|
-
" set index of w to 1",
|
|
194
|
-
" set found to true",
|
|
195
|
-
" exit repeat",
|
|
196
|
-
" end if",
|
|
197
|
-
"end repeat",
|
|
198
|
-
"if not found then do script \"tmux attach -t \(session)\"",
|
|
199
|
-
"end tell"
|
|
200
|
-
)
|
|
201
|
-
|
|
202
|
-
case .iterm2:
|
|
203
|
-
// Search through all sessions in all tabs of all windows
|
|
204
|
-
runOsascript(
|
|
205
|
-
"tell application \"iTerm2\"",
|
|
206
|
-
"activate",
|
|
207
|
-
"set found to false",
|
|
208
|
-
"repeat with w in windows",
|
|
209
|
-
" repeat with t in tabs of w",
|
|
210
|
-
" repeat with s in sessions of t",
|
|
211
|
-
" if name of s contains \"\(tag)\" then",
|
|
212
|
-
" select w",
|
|
213
|
-
" tell w to set current tab to t",
|
|
214
|
-
" set found to true",
|
|
215
|
-
" exit repeat",
|
|
216
|
-
" end if",
|
|
217
|
-
" end repeat",
|
|
218
|
-
" if found then exit repeat",
|
|
219
|
-
" end repeat",
|
|
220
|
-
" if found then exit repeat",
|
|
221
|
-
"end repeat",
|
|
222
|
-
"if not found then",
|
|
223
|
-
" if (count of windows) = 0 then",
|
|
224
|
-
" create window with default profile",
|
|
225
|
-
" else",
|
|
226
|
-
" tell current window to create tab with default profile",
|
|
227
|
-
" end if",
|
|
228
|
-
" tell current session of current tab of current window",
|
|
229
|
-
" write text \"tmux attach -t \(session)\"",
|
|
230
|
-
" end tell",
|
|
231
|
-
"end if",
|
|
232
|
-
"end tell"
|
|
233
|
-
)
|
|
234
|
-
|
|
235
|
-
default:
|
|
236
|
-
// For terminals without good AppleScript support, just activate and attach
|
|
237
|
-
let task = Process()
|
|
238
|
-
task.executableURL = URL(fileURLWithPath: "/usr/bin/open")
|
|
239
|
-
task.arguments = ["-a", rawValue]
|
|
240
|
-
try? task.run()
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/// Run an AppleScript by joining lines into a single -e script block
|
|
246
|
-
private func runOsascript(_ lines: String...) {
|
|
247
|
-
runOsascriptLines(lines)
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/// Run an AppleScript from an array of lines
|
|
251
|
-
private func runOsascriptLines(_ lines: [String]) {
|
|
252
|
-
let script = lines.joined(separator: "\n")
|
|
253
|
-
let task = Process()
|
|
254
|
-
task.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
|
|
255
|
-
task.arguments = ["-e", script]
|
|
256
|
-
task.standardOutput = FileHandle.nullDevice
|
|
257
|
-
task.standardError = FileHandle.nullDevice
|
|
258
|
-
try? task.run()
|
|
259
|
-
}
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
import AppKit
|
|
2
|
-
|
|
3
|
-
// MARK: - Data Model
|
|
4
|
-
|
|
5
|
-
struct TerminalTab {
|
|
6
|
-
let app: Terminal
|
|
7
|
-
let windowIndex: Int
|
|
8
|
-
let tabIndex: Int
|
|
9
|
-
let tty: String // normalized: "ttys003"
|
|
10
|
-
let isActiveTab: Bool
|
|
11
|
-
let title: String
|
|
12
|
-
let sessionId: String? // iTerm2 unique ID only
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// MARK: - Query
|
|
16
|
-
|
|
17
|
-
enum TerminalQuery {
|
|
18
|
-
|
|
19
|
-
/// Normalize TTY strings: strip "/dev/" prefix if present.
|
|
20
|
-
/// iTerm2 returns "/dev/ttys003", Terminal.app and `ps` return "ttys003".
|
|
21
|
-
static func normalizeTTY(_ raw: String) -> String {
|
|
22
|
-
if raw.hasPrefix("/dev/") {
|
|
23
|
-
return String(raw.dropFirst(5))
|
|
24
|
-
}
|
|
25
|
-
return raw
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/// Query all running terminal emulators for tab info.
|
|
29
|
-
/// Only queries apps that are currently running (won't auto-launch).
|
|
30
|
-
static func queryAll() -> [TerminalTab] {
|
|
31
|
-
var results: [TerminalTab] = []
|
|
32
|
-
if isAppRunning("iTerm2") {
|
|
33
|
-
results.append(contentsOf: queryITerm2())
|
|
34
|
-
}
|
|
35
|
-
if isAppRunning("Terminal") {
|
|
36
|
-
results.append(contentsOf: queryTerminalApp())
|
|
37
|
-
}
|
|
38
|
-
// Future: queryWarp(), queryGhostty(), etc.
|
|
39
|
-
return results
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// MARK: - iTerm2
|
|
43
|
-
|
|
44
|
-
static func queryITerm2() -> [TerminalTab] {
|
|
45
|
-
let script = """
|
|
46
|
-
tell application "iTerm2"
|
|
47
|
-
set output to ""
|
|
48
|
-
set winIdx to 0
|
|
49
|
-
repeat with w in windows
|
|
50
|
-
set tabIdx to 0
|
|
51
|
-
repeat with t in tabs of w
|
|
52
|
-
repeat with s in sessions of t
|
|
53
|
-
set output to output & winIdx & "\t" & tabIdx & "\t" & (tty of s) & "\t" & (name of s) & "\t" & (unique ID of s) & linefeed
|
|
54
|
-
end repeat
|
|
55
|
-
set tabIdx to tabIdx + 1
|
|
56
|
-
end repeat
|
|
57
|
-
set winIdx to winIdx + 1
|
|
58
|
-
end repeat
|
|
59
|
-
return output
|
|
60
|
-
end tell
|
|
61
|
-
"""
|
|
62
|
-
|
|
63
|
-
let raw = osascript(script)
|
|
64
|
-
guard !raw.isEmpty else { return [] }
|
|
65
|
-
|
|
66
|
-
var tabs: [TerminalTab] = []
|
|
67
|
-
for line in raw.split(separator: "\n", omittingEmptySubsequences: true) {
|
|
68
|
-
let cols = line.split(separator: "\t", maxSplits: 4, omittingEmptySubsequences: false)
|
|
69
|
-
guard cols.count >= 5 else { continue }
|
|
70
|
-
|
|
71
|
-
guard let winIdx = Int(cols[0]),
|
|
72
|
-
let tabIdx = Int(cols[1]) else { continue }
|
|
73
|
-
|
|
74
|
-
let tty = normalizeTTY(String(cols[2]))
|
|
75
|
-
guard tty.hasPrefix("ttys") else { continue }
|
|
76
|
-
|
|
77
|
-
let title = String(cols[3])
|
|
78
|
-
let sessionId = String(cols[4])
|
|
79
|
-
|
|
80
|
-
tabs.append(TerminalTab(
|
|
81
|
-
app: .iterm2,
|
|
82
|
-
windowIndex: winIdx,
|
|
83
|
-
tabIndex: tabIdx,
|
|
84
|
-
tty: tty,
|
|
85
|
-
isActiveTab: false, // iTerm2 doesn't expose this easily in a single call
|
|
86
|
-
title: title,
|
|
87
|
-
sessionId: sessionId
|
|
88
|
-
))
|
|
89
|
-
}
|
|
90
|
-
return tabs
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// MARK: - Terminal.app
|
|
94
|
-
|
|
95
|
-
static func queryTerminalApp() -> [TerminalTab] {
|
|
96
|
-
let script = """
|
|
97
|
-
tell application "Terminal"
|
|
98
|
-
set output to ""
|
|
99
|
-
set winIdx to 0
|
|
100
|
-
repeat with w in windows
|
|
101
|
-
set selTab to selected tab of w
|
|
102
|
-
set tabIdx to 0
|
|
103
|
-
repeat with t in tabs of w
|
|
104
|
-
set isSel to (t = selTab)
|
|
105
|
-
set output to output & winIdx & "\t" & tabIdx & "\t" & (tty of t) & "\t" & (custom title of t) & "\t" & isSel & linefeed
|
|
106
|
-
set tabIdx to tabIdx + 1
|
|
107
|
-
end repeat
|
|
108
|
-
set winIdx to winIdx + 1
|
|
109
|
-
end repeat
|
|
110
|
-
return output
|
|
111
|
-
end tell
|
|
112
|
-
"""
|
|
113
|
-
|
|
114
|
-
let raw = osascript(script)
|
|
115
|
-
guard !raw.isEmpty else { return [] }
|
|
116
|
-
|
|
117
|
-
var tabs: [TerminalTab] = []
|
|
118
|
-
for line in raw.split(separator: "\n", omittingEmptySubsequences: true) {
|
|
119
|
-
let cols = line.split(separator: "\t", maxSplits: 4, omittingEmptySubsequences: false)
|
|
120
|
-
guard cols.count >= 5 else { continue }
|
|
121
|
-
|
|
122
|
-
guard let winIdx = Int(cols[0]),
|
|
123
|
-
let tabIdx = Int(cols[1]) else { continue }
|
|
124
|
-
|
|
125
|
-
let tty = normalizeTTY(String(cols[2]))
|
|
126
|
-
guard tty.hasPrefix("ttys") else { continue }
|
|
127
|
-
|
|
128
|
-
let title = String(cols[3])
|
|
129
|
-
let isActive = String(cols[4]).lowercased() == "true"
|
|
130
|
-
|
|
131
|
-
tabs.append(TerminalTab(
|
|
132
|
-
app: .terminal,
|
|
133
|
-
windowIndex: winIdx,
|
|
134
|
-
tabIndex: tabIdx,
|
|
135
|
-
tty: tty,
|
|
136
|
-
isActiveTab: isActive,
|
|
137
|
-
title: title,
|
|
138
|
-
sessionId: nil
|
|
139
|
-
))
|
|
140
|
-
}
|
|
141
|
-
return tabs
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// MARK: - Helpers
|
|
145
|
-
|
|
146
|
-
/// Check if a named app is already running (prevents AppleScript from auto-launching it).
|
|
147
|
-
private static func isAppRunning(_ name: String) -> Bool {
|
|
148
|
-
NSWorkspace.shared.runningApplications.contains { $0.localizedName == name }
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/// Run an AppleScript and capture stdout.
|
|
152
|
-
/// Uses ProcessQuery.shell to avoid Process.waitUntilExit() deadlocks on macOS 26.
|
|
153
|
-
private static func osascript(_ source: String) -> String {
|
|
154
|
-
ProcessQuery.shell(["/usr/bin/osascript", "-e", source])
|
|
155
|
-
}
|
|
156
|
-
}
|
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
|
|
3
|
-
// MARK: - Unified Output
|
|
4
|
-
|
|
5
|
-
struct TerminalInstance {
|
|
6
|
-
// Join key
|
|
7
|
-
let tty: String
|
|
8
|
-
|
|
9
|
-
// Tab info (from AppleScript)
|
|
10
|
-
let app: Terminal?
|
|
11
|
-
let windowIndex: Int?
|
|
12
|
-
let tabIndex: Int?
|
|
13
|
-
let isActiveTab: Bool
|
|
14
|
-
let tabTitle: String?
|
|
15
|
-
let terminalSessionId: String? // iTerm2 unique ID
|
|
16
|
-
|
|
17
|
-
// Process info (from ps)
|
|
18
|
-
let processes: [ProcessEntry]
|
|
19
|
-
let shellPid: Int?
|
|
20
|
-
let cwd: String?
|
|
21
|
-
|
|
22
|
-
// Tmux info
|
|
23
|
-
let tmuxSession: String?
|
|
24
|
-
let tmuxPaneId: String?
|
|
25
|
-
|
|
26
|
-
// Window info (from CGWindowList)
|
|
27
|
-
let windowId: UInt32?
|
|
28
|
-
let windowTitle: String?
|
|
29
|
-
|
|
30
|
-
// Computed
|
|
31
|
-
var hasClaude: Bool {
|
|
32
|
-
processes.contains { $0.comm == "claude" }
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
var displayName: String {
|
|
36
|
-
if let session = tmuxSession { return session }
|
|
37
|
-
if let title = tabTitle, !title.isEmpty { return title }
|
|
38
|
-
if let title = windowTitle, !title.isEmpty { return title }
|
|
39
|
-
return tty
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// MARK: - Synthesizer
|
|
44
|
-
|
|
45
|
-
enum TerminalSynthesizer {
|
|
46
|
-
|
|
47
|
-
/// Pure-function merge: joins 5 slices by TTY into unified TerminalInstances.
|
|
48
|
-
///
|
|
49
|
-
/// - Parameters:
|
|
50
|
-
/// - processTable: Full process table from ProcessQuery.snapshot()
|
|
51
|
-
/// - interesting: Filtered interesting processes
|
|
52
|
-
/// - tmuxSessions: Current tmux sessions with panes
|
|
53
|
-
/// - terminalTabs: AppleScript-enumerated tabs
|
|
54
|
-
/// - windows: CGWindowList entries
|
|
55
|
-
static func synthesize(
|
|
56
|
-
processTable: [Int: ProcessEntry],
|
|
57
|
-
interesting: [ProcessEntry],
|
|
58
|
-
tmuxSessions: [TmuxSession],
|
|
59
|
-
terminalTabs: [TerminalTab],
|
|
60
|
-
windows: [UInt32: WindowEntry]
|
|
61
|
-
) -> [TerminalInstance] {
|
|
62
|
-
|
|
63
|
-
// 1. Single pass: index ALL processes by normalized TTY
|
|
64
|
-
// This avoids O(TTYs × processes) re-scans later.
|
|
65
|
-
var allProcessesByTTY: [String: [ProcessEntry]] = [:]
|
|
66
|
-
for entry in processTable.values {
|
|
67
|
-
let tty = TerminalQuery.normalizeTTY(entry.tty)
|
|
68
|
-
guard tty != "??" else { continue }
|
|
69
|
-
allProcessesByTTY[tty, default: []].append(entry)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// 2. Group interesting processes by TTY (subset of above)
|
|
73
|
-
var interestingByTTY: [String: [ProcessEntry]] = [:]
|
|
74
|
-
for entry in interesting {
|
|
75
|
-
let tty = TerminalQuery.normalizeTTY(entry.tty)
|
|
76
|
-
guard tty != "??" else { continue }
|
|
77
|
-
interestingByTTY[tty, default: []].append(entry)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// 3. Build tmux pane → TTY lookup
|
|
81
|
-
var tmuxByTTY: [String: (session: String, paneId: String)] = [:]
|
|
82
|
-
for session in tmuxSessions {
|
|
83
|
-
for pane in session.panes {
|
|
84
|
-
if let entry = processTable[pane.pid] {
|
|
85
|
-
let tty = TerminalQuery.normalizeTTY(entry.tty)
|
|
86
|
-
if tty != "??" {
|
|
87
|
-
tmuxByTTY[tty] = (session: session.name, paneId: pane.id)
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// 4. Index terminal tabs by TTY
|
|
94
|
-
var tabByTTY: [String: TerminalTab] = [:]
|
|
95
|
-
for tab in terminalTabs {
|
|
96
|
-
tabByTTY[tab.tty] = tab
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// 5. Collect all known TTYs (union of all maps)
|
|
100
|
-
var allTTYs = Set(interestingByTTY.keys)
|
|
101
|
-
allTTYs.formUnion(tmuxByTTY.keys)
|
|
102
|
-
allTTYs.formUnion(tabByTTY.keys)
|
|
103
|
-
|
|
104
|
-
// 6. Build window lookup for positional matching
|
|
105
|
-
let windowsByApp = buildWindowsByApp(windows)
|
|
106
|
-
|
|
107
|
-
// 7. For each TTY, merge all slices
|
|
108
|
-
var instances: [TerminalInstance] = []
|
|
109
|
-
for tty in allTTYs {
|
|
110
|
-
let procs = interestingByTTY[tty] ?? []
|
|
111
|
-
let tab = tabByTTY[tty]
|
|
112
|
-
let tmux = tmuxByTTY[tty]
|
|
113
|
-
|
|
114
|
-
// Shell PID: process on this TTY whose parent is NOT on this TTY
|
|
115
|
-
let ttyProcs = allProcessesByTTY[tty] ?? []
|
|
116
|
-
let ttyPids = Set(ttyProcs.map(\.pid))
|
|
117
|
-
let shellPid = ttyProcs.first { !ttyPids.contains($0.ppid) }?.pid
|
|
118
|
-
|
|
119
|
-
// CWD: deepest interesting process's cwd, or shell's cwd
|
|
120
|
-
let cwd = procs.last(where: { $0.cwd != nil })?.cwd
|
|
121
|
-
?? (shellPid.flatMap { processTable[$0]?.cwd })
|
|
122
|
-
|
|
123
|
-
// Window: try lattices tag match first, then positional
|
|
124
|
-
let windowMatch = resolveWindow(
|
|
125
|
-
tmuxSession: tmux?.session,
|
|
126
|
-
tab: tab,
|
|
127
|
-
windowsByApp: windowsByApp,
|
|
128
|
-
allWindows: windows
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
instances.append(TerminalInstance(
|
|
132
|
-
tty: tty,
|
|
133
|
-
app: tab?.app,
|
|
134
|
-
windowIndex: tab?.windowIndex,
|
|
135
|
-
tabIndex: tab?.tabIndex,
|
|
136
|
-
isActiveTab: tab?.isActiveTab ?? false,
|
|
137
|
-
tabTitle: tab?.title,
|
|
138
|
-
terminalSessionId: tab?.sessionId,
|
|
139
|
-
processes: procs,
|
|
140
|
-
shellPid: shellPid,
|
|
141
|
-
cwd: cwd,
|
|
142
|
-
tmuxSession: tmux?.session,
|
|
143
|
-
tmuxPaneId: tmux?.paneId,
|
|
144
|
-
windowId: windowMatch?.wid,
|
|
145
|
-
windowTitle: windowMatch?.title
|
|
146
|
-
))
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// 7. Sort: Claude first, active tabs first, then by TTY
|
|
150
|
-
instances.sort { a, b in
|
|
151
|
-
if a.hasClaude != b.hasClaude { return a.hasClaude }
|
|
152
|
-
if a.isActiveTab != b.isActiveTab { return a.isActiveTab }
|
|
153
|
-
return a.tty < b.tty
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return instances
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// MARK: - Private Helpers
|
|
160
|
-
|
|
161
|
-
/// Group windows by app name for positional matching.
|
|
162
|
-
private static func buildWindowsByApp(_ windows: [UInt32: WindowEntry]) -> [String: [WindowEntry]] {
|
|
163
|
-
var result: [String: [WindowEntry]] = [:]
|
|
164
|
-
for w in windows.values {
|
|
165
|
-
result[w.app, default: []].append(w)
|
|
166
|
-
}
|
|
167
|
-
// Sort each app's windows for consistent positional matching
|
|
168
|
-
for key in result.keys {
|
|
169
|
-
result[key]?.sort { $0.wid < $1.wid }
|
|
170
|
-
}
|
|
171
|
-
return result
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/// Resolve a window for this TTY. Try lattices tag match first, then positional.
|
|
175
|
-
private static func resolveWindow(
|
|
176
|
-
tmuxSession: String?,
|
|
177
|
-
tab: TerminalTab?,
|
|
178
|
-
windowsByApp: [String: [WindowEntry]],
|
|
179
|
-
allWindows: [UInt32: WindowEntry]
|
|
180
|
-
) -> WindowEntry? {
|
|
181
|
-
// Strategy 1: lattices session tag match
|
|
182
|
-
if let session = tmuxSession {
|
|
183
|
-
let tag = Terminal.windowTag(for: session)
|
|
184
|
-
if let match = allWindows.values.first(where: { $0.title.contains(tag) }) {
|
|
185
|
-
return match
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Strategy 2: positional match by app + window index
|
|
190
|
-
if let tab = tab {
|
|
191
|
-
let appName = tab.app.rawValue
|
|
192
|
-
if let appWindows = windowsByApp[appName],
|
|
193
|
-
tab.windowIndex < appWindows.count {
|
|
194
|
-
return appWindows[tab.windowIndex]
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return nil
|
|
199
|
-
}
|
|
200
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
|
|
3
|
-
final class TmuxModel: ObservableObject {
|
|
4
|
-
static let shared = TmuxModel()
|
|
5
|
-
|
|
6
|
-
@Published private(set) var sessions: [TmuxSession] = []
|
|
7
|
-
@Published private(set) var isAvailable: Bool = TmuxQuery.isAvailable
|
|
8
|
-
private var timer: Timer?
|
|
9
|
-
|
|
10
|
-
func start(interval: TimeInterval = 3.0) {
|
|
11
|
-
guard timer == nil else { return }
|
|
12
|
-
|
|
13
|
-
if !isAvailable {
|
|
14
|
-
DiagnosticLog.shared.warn("TmuxModel: tmux not found — session features disabled")
|
|
15
|
-
return
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
DiagnosticLog.shared.info("TmuxModel: starting (interval=\(interval)s)")
|
|
19
|
-
poll()
|
|
20
|
-
timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in
|
|
21
|
-
self?.poll()
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
func stop() {
|
|
26
|
-
timer?.invalidate()
|
|
27
|
-
timer = nil
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
func poll() {
|
|
31
|
-
let fresh = TmuxQuery.listSessions()
|
|
32
|
-
let changed = sessionsChanged(old: sessions, new: fresh)
|
|
33
|
-
|
|
34
|
-
DispatchQueue.main.async {
|
|
35
|
-
self.sessions = fresh
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if changed {
|
|
39
|
-
EventBus.shared.post(.tmuxChanged(sessions: fresh))
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
func isRunning(_ name: String) -> Bool {
|
|
44
|
-
sessions.contains { $0.name == name }
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
private func sessionsChanged(old: [TmuxSession], new: [TmuxSession]) -> Bool {
|
|
48
|
-
guard old.count == new.count else { return true }
|
|
49
|
-
let oldNames = Set(old.map(\.name))
|
|
50
|
-
let newNames = Set(new.map(\.name))
|
|
51
|
-
if oldNames != newNames { return true }
|
|
52
|
-
// Check pane counts changed
|
|
53
|
-
for newSession in new {
|
|
54
|
-
guard let oldSession = old.first(where: { $0.name == newSession.name }) else { return true }
|
|
55
|
-
if oldSession.panes.count != newSession.panes.count { return true }
|
|
56
|
-
if oldSession.attached != newSession.attached { return true }
|
|
57
|
-
}
|
|
58
|
-
return false
|
|
59
|
-
}
|
|
60
|
-
}
|