@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,70 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,141 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,454 +0,0 @@
|
|
|
1
|
-
import Darwin
|
|
2
|
-
import DeckKit
|
|
3
|
-
import Foundation
|
|
4
|
-
|
|
5
|
-
final class LatticesCompanionBridgeServer: NSObject {
|
|
6
|
-
static let shared = LatticesCompanionBridgeServer()
|
|
7
|
-
|
|
8
|
-
static let bonjourType = "_lattices-companion._tcp."
|
|
9
|
-
static let defaultPort: UInt16 = 5287
|
|
10
|
-
static let protocolVersion = "1"
|
|
11
|
-
static let maxBodyBytes = 512 * 1024
|
|
12
|
-
|
|
13
|
-
private let queue = DispatchQueue(label: "lattices.companion.bridge", qos: .userInitiated)
|
|
14
|
-
private let encoder = JSONEncoder()
|
|
15
|
-
private let decoder = JSONDecoder()
|
|
16
|
-
|
|
17
|
-
private var serverFd: Int32 = -1
|
|
18
|
-
private var acceptSource: DispatchSourceRead?
|
|
19
|
-
private var service: NetService?
|
|
20
|
-
|
|
21
|
-
private override init() {
|
|
22
|
-
super.init()
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
func start() {
|
|
26
|
-
guard acceptSource == nil else { return }
|
|
27
|
-
|
|
28
|
-
let diag = DiagnosticLog.shared
|
|
29
|
-
serverFd = socket(AF_INET, SOCK_STREAM, 0)
|
|
30
|
-
guard serverFd >= 0 else {
|
|
31
|
-
diag.error("CompanionBridge: socket() failed — errno \(errno)")
|
|
32
|
-
return
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
var yes: Int32 = 1
|
|
36
|
-
setsockopt(serverFd, SOL_SOCKET, SO_REUSEADDR, &yes, socklen_t(MemoryLayout<Int32>.size))
|
|
37
|
-
|
|
38
|
-
var addr = sockaddr_in()
|
|
39
|
-
addr.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
|
|
40
|
-
addr.sin_family = sa_family_t(AF_INET)
|
|
41
|
-
addr.sin_port = Self.defaultPort.bigEndian
|
|
42
|
-
addr.sin_addr.s_addr = INADDR_ANY.bigEndian
|
|
43
|
-
|
|
44
|
-
let bindResult = withUnsafePointer(to: &addr) {
|
|
45
|
-
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
|
|
46
|
-
Darwin.bind(serverFd, $0, socklen_t(MemoryLayout<sockaddr_in>.size))
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
guard bindResult == 0 else {
|
|
50
|
-
diag.error("CompanionBridge: bind() failed — errno \(errno)")
|
|
51
|
-
close(serverFd)
|
|
52
|
-
serverFd = -1
|
|
53
|
-
return
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
guard listen(serverFd, 8) == 0 else {
|
|
57
|
-
diag.error("CompanionBridge: listen() failed — errno \(errno)")
|
|
58
|
-
close(serverFd)
|
|
59
|
-
serverFd = -1
|
|
60
|
-
return
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
let flags = fcntl(serverFd, F_GETFL)
|
|
64
|
-
_ = fcntl(serverFd, F_SETFL, flags | O_NONBLOCK)
|
|
65
|
-
|
|
66
|
-
let source = DispatchSource.makeReadSource(fileDescriptor: serverFd, queue: queue)
|
|
67
|
-
source.setEventHandler { [weak self] in
|
|
68
|
-
self?.acceptConnection()
|
|
69
|
-
}
|
|
70
|
-
source.setCancelHandler { [weak self] in
|
|
71
|
-
guard let self else { return }
|
|
72
|
-
if self.serverFd >= 0 {
|
|
73
|
-
close(self.serverFd)
|
|
74
|
-
self.serverFd = -1
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
source.resume()
|
|
78
|
-
acceptSource = source
|
|
79
|
-
|
|
80
|
-
publishBonjour()
|
|
81
|
-
diag.success("CompanionBridge: listening on http://0.0.0.0:\(Self.defaultPort)")
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
func stop() {
|
|
85
|
-
acceptSource?.cancel()
|
|
86
|
-
acceptSource = nil
|
|
87
|
-
service?.stop()
|
|
88
|
-
service = nil
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
private extension LatticesCompanionBridgeServer {
|
|
93
|
-
struct HTTPRequest {
|
|
94
|
-
let method: String
|
|
95
|
-
let path: String
|
|
96
|
-
let headers: [String: String]
|
|
97
|
-
let body: Data
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
struct HealthResponse: Codable {
|
|
101
|
-
let ok: Bool
|
|
102
|
-
let name: String
|
|
103
|
-
let serviceType: String
|
|
104
|
-
let hostName: String
|
|
105
|
-
let port: UInt16
|
|
106
|
-
let protocolVersion: String
|
|
107
|
-
let version: String
|
|
108
|
-
let mode: String
|
|
109
|
-
let bridgePublicKey: String
|
|
110
|
-
let bridgeFingerprint: String
|
|
111
|
-
let requestSigningRequired: Bool
|
|
112
|
-
let payloadEncryptionRequired: Bool
|
|
113
|
-
let capabilities: [String]
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
func publishBonjour() {
|
|
117
|
-
let advertisedName = Host.current().localizedName ?? "Lattices Companion"
|
|
118
|
-
let service = NetService(
|
|
119
|
-
domain: "local.",
|
|
120
|
-
type: Self.bonjourType,
|
|
121
|
-
name: advertisedName,
|
|
122
|
-
port: Int32(Self.defaultPort)
|
|
123
|
-
)
|
|
124
|
-
service.includesPeerToPeer = true
|
|
125
|
-
service.setTXTRecord(NetService.data(fromTXTRecord: [
|
|
126
|
-
"v": Data(Self.protocolVersion.utf8),
|
|
127
|
-
"mode": Data("local-network-secure".utf8),
|
|
128
|
-
"fp": Data(LatticesCompanionSecurityCoordinator.shared.bridgeFingerprint.utf8),
|
|
129
|
-
"sec": Data("signed,encrypted".utf8),
|
|
130
|
-
"cap": Data(DeckBridgeCapability.defaultCompanionCapabilities.joined(separator: ",").utf8),
|
|
131
|
-
]))
|
|
132
|
-
service.publish()
|
|
133
|
-
self.service = service
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
func acceptConnection() {
|
|
137
|
-
while true {
|
|
138
|
-
var clientAddr = sockaddr_in()
|
|
139
|
-
var addrLen = socklen_t(MemoryLayout<sockaddr_in>.size)
|
|
140
|
-
|
|
141
|
-
let clientFd = withUnsafeMutablePointer(to: &clientAddr) {
|
|
142
|
-
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
|
|
143
|
-
accept(serverFd, $0, &addrLen)
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if clientFd < 0 {
|
|
148
|
-
if errno != EAGAIN && errno != EWOULDBLOCK {
|
|
149
|
-
DiagnosticLog.shared.error("CompanionBridge: accept() failed — errno \(errno)")
|
|
150
|
-
}
|
|
151
|
-
return
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
let clientFlags = fcntl(clientFd, F_GETFL)
|
|
155
|
-
if clientFlags >= 0 {
|
|
156
|
-
_ = fcntl(clientFd, F_SETFL, clientFlags & ~O_NONBLOCK)
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
queue.async { [weak self] in
|
|
160
|
-
self?.handleClient(fd: clientFd)
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
func handleClient(fd: Int32) {
|
|
166
|
-
defer { close(fd) }
|
|
167
|
-
|
|
168
|
-
guard let request = readRequest(from: fd) else {
|
|
169
|
-
sendError(status: 400, message: "Invalid HTTP request", to: fd)
|
|
170
|
-
return
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
do {
|
|
174
|
-
try route(request, to: fd)
|
|
175
|
-
} catch let error as LatticesCompanionSecurityError {
|
|
176
|
-
let status: Int
|
|
177
|
-
switch error {
|
|
178
|
-
case .untrustedDevice, .insufficientCapability:
|
|
179
|
-
status = 403
|
|
180
|
-
case .missingHeader, .staleRequest, .replayedRequest, .invalidSignature, .invalidEnvelope, .invalidDeviceKey:
|
|
181
|
-
status = 401
|
|
182
|
-
}
|
|
183
|
-
sendError(status: status, message: error.localizedDescription, to: fd)
|
|
184
|
-
} catch {
|
|
185
|
-
sendError(status: 500, message: error.localizedDescription, to: fd)
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
func route(_ request: HTTPRequest, to fd: Int32) throws {
|
|
190
|
-
switch (request.method, request.path) {
|
|
191
|
-
case ("GET", "/health"):
|
|
192
|
-
let security = LatticesDeckHost.shared.securityConfiguration
|
|
193
|
-
let response = HealthResponse(
|
|
194
|
-
ok: true,
|
|
195
|
-
name: Host.current().localizedName ?? "Lattices Companion",
|
|
196
|
-
serviceType: Self.bonjourType,
|
|
197
|
-
hostName: localHostName(),
|
|
198
|
-
port: Self.defaultPort,
|
|
199
|
-
protocolVersion: Self.protocolVersion,
|
|
200
|
-
version: LatticesRuntime.appVersion,
|
|
201
|
-
mode: "local-network-secure",
|
|
202
|
-
bridgePublicKey: LatticesCompanionSecurityCoordinator.shared.bridgePublicKeyBase64,
|
|
203
|
-
bridgeFingerprint: LatticesCompanionSecurityCoordinator.shared.bridgeFingerprint,
|
|
204
|
-
requestSigningRequired: security.requestSigningRequired,
|
|
205
|
-
payloadEncryptionRequired: security.payloadEncryptionRequired,
|
|
206
|
-
capabilities: DeckBridgeCapability.defaultCompanionCapabilities
|
|
207
|
-
)
|
|
208
|
-
try sendJSON(status: 200, value: response, to: fd)
|
|
209
|
-
|
|
210
|
-
case ("GET", "/deck/manifest"):
|
|
211
|
-
try sendJSON(status: 200, value: LatticesDeckHost.shared.manifestSync(), to: fd)
|
|
212
|
-
|
|
213
|
-
case ("POST", "/pairing/request"):
|
|
214
|
-
let pairingRequest = try decoder.decode(DeckPairingRequest.self, from: request.body)
|
|
215
|
-
let response = LatticesCompanionSecurityCoordinator.shared.handlePairingRequest(pairingRequest)
|
|
216
|
-
let status = response.disposition == .denied ? 403 : 200
|
|
217
|
-
try sendJSON(status: status, value: response, to: fd)
|
|
218
|
-
|
|
219
|
-
case ("GET", "/deck/snapshot"):
|
|
220
|
-
let auth = try authorizeProtectedRequest(request, requiredCapability: DeckBridgeCapability.deckRead)
|
|
221
|
-
let snapshot = try LatticesDeckHost.shared.runtimeSnapshotSync()
|
|
222
|
-
let response = try LatticesCompanionSecurityCoordinator.shared.encodeProtectedResponse(
|
|
223
|
-
snapshot,
|
|
224
|
-
auth: auth,
|
|
225
|
-
status: 200,
|
|
226
|
-
path: request.path
|
|
227
|
-
)
|
|
228
|
-
try sendJSON(status: 200, value: response, to: fd)
|
|
229
|
-
|
|
230
|
-
case ("POST", "/deck/perform"):
|
|
231
|
-
let auth = try authorizeProtectedRequest(request, requiredCapability: DeckBridgeCapability.deckPerform)
|
|
232
|
-
let action = try LatticesCompanionSecurityCoordinator.shared.decodeProtectedBody(
|
|
233
|
-
DeckActionRequest.self,
|
|
234
|
-
body: request.body,
|
|
235
|
-
auth: auth,
|
|
236
|
-
method: request.method,
|
|
237
|
-
path: request.path
|
|
238
|
-
)
|
|
239
|
-
let result = try LatticesDeckHost.shared.performSync(action)
|
|
240
|
-
let response = try LatticesCompanionSecurityCoordinator.shared.encodeProtectedResponse(
|
|
241
|
-
result,
|
|
242
|
-
auth: auth,
|
|
243
|
-
status: 200,
|
|
244
|
-
path: request.path
|
|
245
|
-
)
|
|
246
|
-
try sendJSON(status: 200, value: response, to: fd)
|
|
247
|
-
|
|
248
|
-
case ("POST", "/deck/trackpad"):
|
|
249
|
-
let auth = try authorizeProtectedRequest(request, requiredCapability: DeckBridgeCapability.inputTrackpad)
|
|
250
|
-
let eventRequest = try LatticesCompanionSecurityCoordinator.shared.decodeProtectedBody(
|
|
251
|
-
DeckTrackpadEventRequest.self,
|
|
252
|
-
body: request.body,
|
|
253
|
-
auth: auth,
|
|
254
|
-
method: request.method,
|
|
255
|
-
path: request.path
|
|
256
|
-
)
|
|
257
|
-
let result = LatticesCompanionTrackpadController.shared.perform(eventRequest)
|
|
258
|
-
let response = try LatticesCompanionSecurityCoordinator.shared.encodeProtectedResponse(
|
|
259
|
-
result,
|
|
260
|
-
auth: auth,
|
|
261
|
-
status: 200,
|
|
262
|
-
path: request.path
|
|
263
|
-
)
|
|
264
|
-
try sendJSON(status: 200, value: response, to: fd)
|
|
265
|
-
|
|
266
|
-
default:
|
|
267
|
-
sendError(status: 404, message: "Unknown route", to: fd)
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
func authorizeProtectedRequest(_ request: HTTPRequest, requiredCapability: String) throws -> AuthorizedBridgeRequest {
|
|
272
|
-
let security = LatticesDeckHost.shared.securityConfiguration
|
|
273
|
-
guard security.requestSigningRequired else {
|
|
274
|
-
throw LatticesCompanionSecurityError.untrustedDevice
|
|
275
|
-
}
|
|
276
|
-
let auth = try LatticesCompanionSecurityCoordinator.shared.authorize(
|
|
277
|
-
method: request.method,
|
|
278
|
-
path: request.path,
|
|
279
|
-
headers: request.headers,
|
|
280
|
-
body: request.body
|
|
281
|
-
)
|
|
282
|
-
try LatticesCompanionSecurityCoordinator.shared.requireCapability(requiredCapability, for: auth)
|
|
283
|
-
return auth
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
func readRequest(from fd: Int32) -> HTTPRequest? {
|
|
287
|
-
var buffer = Data()
|
|
288
|
-
let deadline = DispatchTime.now().uptimeNanoseconds + 2_000_000_000
|
|
289
|
-
let delimiterCRLF = Data([13, 10, 13, 10])
|
|
290
|
-
let delimiterLF = Data([10, 10])
|
|
291
|
-
|
|
292
|
-
var headerRange = buffer.range(of: delimiterCRLF) ?? buffer.range(of: delimiterLF)
|
|
293
|
-
while headerRange == nil {
|
|
294
|
-
guard let count = readChunk(from: fd, deadline: deadline, into: &buffer) else {
|
|
295
|
-
return nil
|
|
296
|
-
}
|
|
297
|
-
guard count > 0 else { return nil }
|
|
298
|
-
if buffer.count > 128 * 1024 {
|
|
299
|
-
return nil
|
|
300
|
-
}
|
|
301
|
-
headerRange = buffer.range(of: delimiterCRLF) ?? buffer.range(of: delimiterLF)
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
guard let headerRange else { return nil }
|
|
305
|
-
let headerData = buffer.subdata(in: 0..<headerRange.lowerBound)
|
|
306
|
-
guard let headerText = String(data: headerData, encoding: .utf8) else {
|
|
307
|
-
return nil
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
let lines = headerText
|
|
311
|
-
.replacingOccurrences(of: "\r\n", with: "\n")
|
|
312
|
-
.components(separatedBy: "\n")
|
|
313
|
-
guard let requestLine = lines.first else { return nil }
|
|
314
|
-
let requestParts = requestLine.split(separator: " ", omittingEmptySubsequences: true)
|
|
315
|
-
guard requestParts.count >= 2 else { return nil }
|
|
316
|
-
|
|
317
|
-
let method = String(requestParts[0]).uppercased()
|
|
318
|
-
let rawPath = String(requestParts[1])
|
|
319
|
-
let path = rawPath.split(separator: "?", maxSplits: 1).first.map(String.init) ?? rawPath
|
|
320
|
-
|
|
321
|
-
var headers: [String: String] = [:]
|
|
322
|
-
for line in lines.dropFirst() {
|
|
323
|
-
guard let separator = line.firstIndex(of: ":") else { continue }
|
|
324
|
-
let name = line[..<separator].trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
|
325
|
-
let value = line[line.index(after: separator)...].trimmingCharacters(in: .whitespacesAndNewlines)
|
|
326
|
-
headers[name] = value
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
let contentLength = Int(headers["content-length"] ?? "") ?? 0
|
|
330
|
-
guard contentLength <= Self.maxBodyBytes else { return nil }
|
|
331
|
-
var body = Data(buffer[headerRange.upperBound...])
|
|
332
|
-
while body.count < contentLength {
|
|
333
|
-
guard let count = readChunk(
|
|
334
|
-
from: fd,
|
|
335
|
-
deadline: deadline,
|
|
336
|
-
chunkSize: min(4096, contentLength - body.count),
|
|
337
|
-
into: &body
|
|
338
|
-
) else {
|
|
339
|
-
return nil
|
|
340
|
-
}
|
|
341
|
-
guard count > 0 else { return nil }
|
|
342
|
-
}
|
|
343
|
-
if body.count > contentLength {
|
|
344
|
-
body = body.prefix(contentLength)
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
return HTTPRequest(method: method, path: path, headers: headers, body: body)
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
func readChunk(
|
|
351
|
-
from fd: Int32,
|
|
352
|
-
deadline: UInt64,
|
|
353
|
-
chunkSize: Int = 4096,
|
|
354
|
-
into buffer: inout Data
|
|
355
|
-
) -> Int? {
|
|
356
|
-
var chunk = [UInt8](repeating: 0, count: chunkSize)
|
|
357
|
-
|
|
358
|
-
while true {
|
|
359
|
-
let count = Darwin.read(fd, &chunk, chunk.count)
|
|
360
|
-
if count > 0 {
|
|
361
|
-
buffer.append(contentsOf: chunk[..<count])
|
|
362
|
-
return count
|
|
363
|
-
}
|
|
364
|
-
if count == 0 {
|
|
365
|
-
return 0
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
if errno == EINTR {
|
|
369
|
-
continue
|
|
370
|
-
}
|
|
371
|
-
if (errno == EAGAIN || errno == EWOULDBLOCK) &&
|
|
372
|
-
DispatchTime.now().uptimeNanoseconds < deadline {
|
|
373
|
-
usleep(10_000)
|
|
374
|
-
continue
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
DiagnosticLog.shared.error("CompanionBridge: read() failed — errno \(errno)")
|
|
378
|
-
return nil
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
func sendJSON<T: Encodable>(status: Int, value: T, to fd: Int32) throws {
|
|
383
|
-
let body = try encoder.encode(value)
|
|
384
|
-
sendResponse(status: status, contentType: "application/json; charset=utf-8", body: body, to: fd)
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
func sendError(status: Int, message: String, to fd: Int32) {
|
|
388
|
-
let payload: [String: Any] = ["ok": false, "error": message]
|
|
389
|
-
guard let body = try? JSONSerialization.data(withJSONObject: payload, options: []) else { return }
|
|
390
|
-
sendResponse(status: status, contentType: "application/json; charset=utf-8", body: body, to: fd)
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
func sendResponse(status: Int, contentType: String, body: Data, to fd: Int32) {
|
|
394
|
-
let reason = reasonPhrase(for: status)
|
|
395
|
-
let header = [
|
|
396
|
-
"HTTP/1.1 \(status) \(reason)",
|
|
397
|
-
"Content-Type: \(contentType)",
|
|
398
|
-
"Content-Length: \(body.count)",
|
|
399
|
-
"Connection: close",
|
|
400
|
-
"",
|
|
401
|
-
""
|
|
402
|
-
].joined(separator: "\r\n")
|
|
403
|
-
writeAll(Data(header.utf8), to: fd)
|
|
404
|
-
writeAll(body, to: fd)
|
|
405
|
-
_ = shutdown(fd, SHUT_WR)
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
func writeAll(_ data: Data, to fd: Int32) {
|
|
409
|
-
data.withUnsafeBytes { rawBuffer in
|
|
410
|
-
guard var pointer = rawBuffer.bindMemory(to: UInt8.self).baseAddress else { return }
|
|
411
|
-
var remaining = data.count
|
|
412
|
-
while remaining > 0 {
|
|
413
|
-
let written = write(fd, pointer, remaining)
|
|
414
|
-
guard written > 0 else { return }
|
|
415
|
-
remaining -= written
|
|
416
|
-
pointer = pointer.advanced(by: written)
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
func reasonPhrase(for status: Int) -> String {
|
|
422
|
-
switch status {
|
|
423
|
-
case 200: return "OK"
|
|
424
|
-
case 400: return "Bad Request"
|
|
425
|
-
case 401: return "Unauthorized"
|
|
426
|
-
case 403: return "Forbidden"
|
|
427
|
-
case 404: return "Not Found"
|
|
428
|
-
default: return "Internal Server Error"
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
func localHostName() -> String {
|
|
433
|
-
let process = Process()
|
|
434
|
-
process.executableURL = URL(fileURLWithPath: "/usr/sbin/scutil")
|
|
435
|
-
process.arguments = ["--get", "LocalHostName"]
|
|
436
|
-
|
|
437
|
-
let pipe = Pipe()
|
|
438
|
-
process.standardOutput = pipe
|
|
439
|
-
process.standardError = Pipe()
|
|
440
|
-
|
|
441
|
-
do {
|
|
442
|
-
try process.run()
|
|
443
|
-
process.waitUntilExit()
|
|
444
|
-
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
|
445
|
-
if let name = String(data: data, encoding: .utf8)?
|
|
446
|
-
.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
447
|
-
!name.isEmpty {
|
|
448
|
-
return "\(name).local"
|
|
449
|
-
}
|
|
450
|
-
} catch { }
|
|
451
|
-
|
|
452
|
-
return Host.current().localizedName ?? "localhost"
|
|
453
|
-
}
|
|
454
|
-
}
|