@wangyaoshen/remux 0.3.8-dev.bab6c95

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.
Files changed (166) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +47 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +38 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +28 -0
  4. package/.github/dependabot.yml +33 -0
  5. package/.github/workflows/ci.yml +65 -0
  6. package/.github/workflows/deploy.yml +65 -0
  7. package/.github/workflows/publish.yml +138 -0
  8. package/.github/workflows/release-please.yml +21 -0
  9. package/.gitmodules +3 -0
  10. package/.nvmrc +1 -0
  11. package/.release-please-manifest.json +3 -0
  12. package/CLAUDE.md +104 -0
  13. package/Dockerfile +23 -0
  14. package/LICENSE +21 -0
  15. package/README.md +120 -0
  16. package/apps/ios/Config/signing.xcconfig +4 -0
  17. package/apps/ios/Package.swift +26 -0
  18. package/apps/ios/Remux.xcodeproj/project.pbxproj +456 -0
  19. package/apps/ios/Remux.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  20. package/apps/ios/Sources/Remux/Extensions/FaceIDManager.swift +29 -0
  21. package/apps/ios/Sources/Remux/Extensions/InspectCache.swift +66 -0
  22. package/apps/ios/Sources/Remux/MainTabView.swift +32 -0
  23. package/apps/ios/Sources/Remux/Remux.entitlements +8 -0
  24. package/apps/ios/Sources/Remux/RemuxiOSApp.swift +14 -0
  25. package/apps/ios/Sources/Remux/RootView.swift +130 -0
  26. package/apps/ios/Sources/Remux/Views/Control/ControlView.swift +102 -0
  27. package/apps/ios/Sources/Remux/Views/Inspect/InspectView.swift +98 -0
  28. package/apps/ios/Sources/Remux/Views/Live/LiveTerminalView.swift +132 -0
  29. package/apps/ios/Sources/Remux/Views/Now/NowView.swift +173 -0
  30. package/apps/ios/Sources/Remux/Views/Onboarding/ManualConnectView.swift +55 -0
  31. package/apps/ios/Sources/Remux/Views/Onboarding/OnboardingView.swift +70 -0
  32. package/apps/ios/Sources/Remux/Views/Onboarding/QRScannerView.swift +92 -0
  33. package/apps/ios/Sources/Remux/Views/Settings/MeView.swift +136 -0
  34. package/apps/macos/Package.swift +37 -0
  35. package/apps/macos/Resources/shell-integration/bash/bash-preexec.sh +382 -0
  36. package/apps/macos/Resources/shell-integration/bash/ghostty.bash +315 -0
  37. package/apps/macos/Resources/shell-integration/elvish/lib/ghostty-integration.elv +191 -0
  38. package/apps/macos/Resources/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +246 -0
  39. package/apps/macos/Resources/shell-integration/nushell/vendor/autoload/ghostty.nu +110 -0
  40. package/apps/macos/Resources/shell-integration/zsh/.zshenv +61 -0
  41. package/apps/macos/Resources/shell-integration/zsh/ghostty-integration +458 -0
  42. package/apps/macos/Resources/terminfo/67/ghostty +0 -0
  43. package/apps/macos/Resources/terminfo/78/xterm-ghostty +0 -0
  44. package/apps/macos/Sources/Remux/AppDelegate.swift +257 -0
  45. package/apps/macos/Sources/Remux/CrashReporter.swift +210 -0
  46. package/apps/macos/Sources/Remux/FinderIntegration.swift +117 -0
  47. package/apps/macos/Sources/Remux/GhosttyConfig.swift +311 -0
  48. package/apps/macos/Sources/Remux/KeyboardShortcuts/ShortcutAction.swift +115 -0
  49. package/apps/macos/Sources/Remux/KeyboardShortcuts/ShortcutSettingsView.swift +271 -0
  50. package/apps/macos/Sources/Remux/KeyboardShortcuts/StoredShortcut.swift +149 -0
  51. package/apps/macos/Sources/Remux/MainContentView.swift +308 -0
  52. package/apps/macos/Sources/Remux/MenuBarManager.swift +275 -0
  53. package/apps/macos/Sources/Remux/NotificationManager.swift +145 -0
  54. package/apps/macos/Sources/Remux/PortScanner.swift +152 -0
  55. package/apps/macos/Sources/Remux/RemuxApp.swift +13 -0
  56. package/apps/macos/Sources/Remux/SSHDetector.swift +151 -0
  57. package/apps/macos/Sources/Remux/SessionPersistence.swift +226 -0
  58. package/apps/macos/Sources/Remux/SocketController.swift +258 -0
  59. package/apps/macos/Sources/Remux/UpdateChecker.swift +152 -0
  60. package/apps/macos/Sources/Remux/Views/CommandPalette.swift +198 -0
  61. package/apps/macos/Sources/Remux/Views/ConnectionView.swift +84 -0
  62. package/apps/macos/Sources/Remux/Views/InspectView.swift +127 -0
  63. package/apps/macos/Sources/Remux/Views/SettingsView.swift +77 -0
  64. package/apps/macos/Sources/Remux/Views/Sidebar/SidebarView.swift +410 -0
  65. package/apps/macos/Sources/Remux/Views/SplitTree/BrowserPanel.swift +193 -0
  66. package/apps/macos/Sources/Remux/Views/SplitTree/MarkdownPanel.swift +277 -0
  67. package/apps/macos/Sources/Remux/Views/SplitTree/PanelProtocol.swift +14 -0
  68. package/apps/macos/Sources/Remux/Views/SplitTree/SplitNode.swift +149 -0
  69. package/apps/macos/Sources/Remux/Views/SplitTree/SplitView.swift +234 -0
  70. package/apps/macos/Sources/Remux/Views/SplitTree/TerminalPanel.swift +26 -0
  71. package/apps/macos/Sources/Remux/Views/TabBarView.swift +94 -0
  72. package/apps/macos/Sources/Remux/Views/Terminal/ClipboardHelper.swift +101 -0
  73. package/apps/macos/Sources/Remux/Views/Terminal/CopyModeOverlay.swift +325 -0
  74. package/apps/macos/Sources/Remux/Views/Terminal/GhosttyNativeTerminalView.swift +39 -0
  75. package/apps/macos/Sources/Remux/Views/Terminal/GhosttyNativeView.swift +559 -0
  76. package/apps/macos/Sources/Remux/Views/Terminal/SurfaceSearchOverlay.swift +109 -0
  77. package/apps/macos/Sources/Remux/Views/Terminal/TerminalContainerView.swift +95 -0
  78. package/apps/macos/Sources/Remux/Views/Terminal/TerminalRelay.swift +117 -0
  79. package/build.mjs +33 -0
  80. package/native/android/DecodeGoldenPayloads.kt +487 -0
  81. package/native/android/ProtocolModels.kt +188 -0
  82. package/native/ios/DecodeGoldenPayloads.swift +711 -0
  83. package/native/ios/ProtocolModels.swift +200 -0
  84. package/package.json +45 -0
  85. package/packages/RemuxKit/Package.swift +27 -0
  86. package/packages/RemuxKit/Sources/RemuxKit/Device/DeviceManager.swift +27 -0
  87. package/packages/RemuxKit/Sources/RemuxKit/Models/ProtocolModels.swift +206 -0
  88. package/packages/RemuxKit/Sources/RemuxKit/Networking/MessageRouter.swift +108 -0
  89. package/packages/RemuxKit/Sources/RemuxKit/Networking/RemuxConnection.swift +395 -0
  90. package/packages/RemuxKit/Sources/RemuxKit/State/RemuxState.swift +188 -0
  91. package/packages/RemuxKit/Sources/RemuxKit/Storage/KeychainStore.swift +142 -0
  92. package/packages/RemuxKit/Sources/RemuxKit/Terminal/GhosttyBridge.swift +145 -0
  93. package/packages/RemuxKit/Sources/RemuxKit/Terminal/GhosttyTerminalView.swift +35 -0
  94. package/packages/RemuxKit/Sources/RemuxKit/Terminal/Resources/ghostty-terminal.html +91 -0
  95. package/packages/RemuxKit/Tests/RemuxKitTests/ConnectionIntegrationTest.swift +74 -0
  96. package/packages/RemuxKit/Tests/RemuxKitTests/KeychainStoreTests.swift +81 -0
  97. package/packages/RemuxKit/Tests/RemuxKitTests/ProtocolModelsTests.swift +179 -0
  98. package/packages/RemuxKit/Tests/RemuxKitTests/RemuxStateTests.swift +62 -0
  99. package/playwright.config.ts +17 -0
  100. package/pnpm-lock.yaml +1588 -0
  101. package/pty-daemon.js +303 -0
  102. package/release-please-config.json +14 -0
  103. package/scripts/auto-deploy.sh +46 -0
  104. package/scripts/build-dmg.sh +121 -0
  105. package/scripts/build-ghostty-kit.sh +43 -0
  106. package/scripts/check-active-terminology.mjs +132 -0
  107. package/scripts/sync-ghostty-web.sh +28 -0
  108. package/server.js +7074 -0
  109. package/src/adapters/agent-events.ts +246 -0
  110. package/src/adapters/claude-code.ts +158 -0
  111. package/src/adapters/codex.ts +210 -0
  112. package/src/adapters/generic-shell.ts +58 -0
  113. package/src/adapters/index.ts +15 -0
  114. package/src/adapters/registry.ts +99 -0
  115. package/src/adapters/types.ts +41 -0
  116. package/src/auth.ts +174 -0
  117. package/src/e2ee.ts +236 -0
  118. package/src/git-service.ts +168 -0
  119. package/src/message-buffer.ts +137 -0
  120. package/src/pty-daemon.ts +357 -0
  121. package/src/push.ts +127 -0
  122. package/src/renderers.ts +455 -0
  123. package/src/server.ts +2407 -0
  124. package/src/service.ts +226 -0
  125. package/src/session.ts +978 -0
  126. package/src/store.ts +1422 -0
  127. package/src/team.ts +123 -0
  128. package/src/tunnel.ts +126 -0
  129. package/src/types.d.ts +50 -0
  130. package/src/vt-tracker.ts +188 -0
  131. package/src/workspace-head.ts +144 -0
  132. package/src/workspace.ts +153 -0
  133. package/src/ws-handler.ts +1526 -0
  134. package/start.ps1 +83 -0
  135. package/tests/adapters.test.js +171 -0
  136. package/tests/auth.test.js +243 -0
  137. package/tests/codex-adapter.test.js +535 -0
  138. package/tests/durable-stream.test.js +153 -0
  139. package/tests/e2e/app.spec.js +530 -0
  140. package/tests/e2ee.test.js +325 -0
  141. package/tests/message-buffer.test.js +245 -0
  142. package/tests/message-routing.test.js +305 -0
  143. package/tests/pty-daemon.test.js +346 -0
  144. package/tests/push.test.js +281 -0
  145. package/tests/renderers.test.js +391 -0
  146. package/tests/search-shell.test.js +499 -0
  147. package/tests/server.test.js +882 -0
  148. package/tests/service.test.js +267 -0
  149. package/tests/store.test.js +369 -0
  150. package/tests/tunnel.test.js +67 -0
  151. package/tests/workspace-head.test.js +116 -0
  152. package/tests/workspace.test.js +417 -0
  153. package/tsconfig.backend.json +11 -0
  154. package/tsconfig.json +15 -0
  155. package/tui/client/client_test.go +125 -0
  156. package/tui/client/connection.go +342 -0
  157. package/tui/client/host_manager.go +141 -0
  158. package/tui/config/cache.go +81 -0
  159. package/tui/config/config.go +53 -0
  160. package/tui/config/config_test.go +89 -0
  161. package/tui/go.mod +32 -0
  162. package/tui/go.sum +50 -0
  163. package/tui/main.go +261 -0
  164. package/tui/tests/integration_test.go +283 -0
  165. package/tui/ui/model.go +310 -0
  166. package/vitest.config.js +10 -0
@@ -0,0 +1,173 @@
1
+ import SwiftUI
2
+ import RemuxKit
3
+
4
+ /// Now tab: shows current status, active session, recent activity.
5
+ /// Design ref: hapi's Now first screen — focus on the 3 most important things.
6
+ struct NowView: View {
7
+ @Environment(RemuxState.self) private var state
8
+
9
+ var body: some View {
10
+ NavigationStack {
11
+ ScrollView {
12
+ VStack(spacing: 16) {
13
+ // Connection status card
14
+ ConnectionStatusCard()
15
+
16
+ // Active session card
17
+ if !state.currentSession.isEmpty {
18
+ SessionCard(
19
+ sessionName: state.currentSession,
20
+ tabCount: state.tabs.count,
21
+ activeTab: state.tabs.first { $0.index == state.activeTabIndex }
22
+ )
23
+ }
24
+
25
+ // Client role
26
+ HStack {
27
+ Image(systemName: state.clientRole == "active" ? "hand.raised.fill" : "eye.fill")
28
+ Text(state.clientRole == "active" ? "Active" : "Observer")
29
+ .font(.subheadline)
30
+ Spacer()
31
+ if state.clientRole == "observer" {
32
+ Button("Request Control") {
33
+ state.requestControl()
34
+ }
35
+ .buttonStyle(.bordered)
36
+ .controlSize(.small)
37
+ }
38
+ }
39
+ .padding()
40
+ .background(.regularMaterial)
41
+ .cornerRadius(12)
42
+
43
+ // Quick actions
44
+ QuickActionsSection()
45
+ }
46
+ .padding()
47
+ }
48
+ .navigationTitle("Now")
49
+ .refreshable {
50
+ state.requestInspect(tabIndex: state.activeTabIndex)
51
+ }
52
+ }
53
+ }
54
+ }
55
+
56
+ struct ConnectionStatusCard: View {
57
+ @Environment(RemuxState.self) private var state
58
+
59
+ var body: some View {
60
+ HStack {
61
+ Circle()
62
+ .fill(statusColor)
63
+ .frame(width: 10, height: 10)
64
+ Text(statusText)
65
+ .font(.subheadline.bold())
66
+ Spacer()
67
+ if let url = state.serverURL {
68
+ Text(url.host ?? "")
69
+ .font(.caption)
70
+ .foregroundStyle(.secondary)
71
+ }
72
+ }
73
+ .padding()
74
+ .background(.regularMaterial)
75
+ .cornerRadius(12)
76
+ }
77
+
78
+ private var statusColor: Color {
79
+ switch state.connectionStatus {
80
+ case .connected: .green
81
+ case .reconnecting: .yellow
82
+ case .connecting, .authenticating: .orange
83
+ case .disconnected: .red
84
+ }
85
+ }
86
+
87
+ private var statusText: String {
88
+ switch state.connectionStatus {
89
+ case .connected: "Connected"
90
+ case .reconnecting(let n): "Reconnecting (\(n))..."
91
+ case .connecting: "Connecting..."
92
+ case .authenticating: "Authenticating..."
93
+ case .disconnected: "Disconnected"
94
+ }
95
+ }
96
+ }
97
+
98
+ struct SessionCard: View {
99
+ let sessionName: String
100
+ let tabCount: Int
101
+ let activeTab: WorkspaceTab?
102
+
103
+ var body: some View {
104
+ VStack(alignment: .leading, spacing: 8) {
105
+ HStack {
106
+ Image(systemName: "terminal")
107
+ Text(sessionName)
108
+ .font(.headline)
109
+ Spacer()
110
+ Text("\(tabCount) tab\(tabCount == 1 ? "" : "s")")
111
+ .font(.caption)
112
+ .foregroundStyle(.secondary)
113
+ }
114
+ if let tab = activeTab {
115
+ HStack {
116
+ Text("Active: \(tab.name)")
117
+ .font(.subheadline)
118
+ .foregroundStyle(.secondary)
119
+ if let cwd = tab.panes.first?.cwd {
120
+ Text(cwd)
121
+ .font(.caption)
122
+ .foregroundStyle(.tertiary)
123
+ .lineLimit(1)
124
+ }
125
+ }
126
+ }
127
+ }
128
+ .padding()
129
+ .background(.regularMaterial)
130
+ .cornerRadius(12)
131
+ }
132
+ }
133
+
134
+ struct QuickActionsSection: View {
135
+ @Environment(RemuxState.self) private var state
136
+
137
+ var body: some View {
138
+ VStack(alignment: .leading, spacing: 8) {
139
+ Text("Quick Actions")
140
+ .font(.caption)
141
+ .foregroundStyle(.secondary)
142
+
143
+ LazyVGrid(columns: [.init(.flexible()), .init(.flexible())], spacing: 8) {
144
+ QuickActionButton(icon: "plus", title: "New Tab") {
145
+ state.createTab()
146
+ }
147
+ QuickActionButton(icon: "doc.text.magnifyingglass", title: "Inspect") {
148
+ // Switch to inspect tab — handled by parent TabView
149
+ }
150
+ }
151
+ }
152
+ }
153
+ }
154
+
155
+ struct QuickActionButton: View {
156
+ let icon: String
157
+ let title: String
158
+ let action: () -> Void
159
+
160
+ var body: some View {
161
+ Button(action: action) {
162
+ VStack(spacing: 6) {
163
+ Image(systemName: icon)
164
+ .font(.title3)
165
+ Text(title)
166
+ .font(.caption)
167
+ }
168
+ .frame(maxWidth: .infinity)
169
+ .padding(.vertical, 12)
170
+ }
171
+ .buttonStyle(.bordered)
172
+ }
173
+ }
@@ -0,0 +1,55 @@
1
+ import SwiftUI
2
+ import RemuxKit
3
+
4
+ /// Manual connection entry: URL + token.
5
+ struct ManualConnectView: View {
6
+ @Environment(RemuxState.self) private var state
7
+ @Environment(\.dismiss) private var dismiss
8
+ @State private var serverURL = ""
9
+ @State private var token = ""
10
+ @State private var errorMessage: String?
11
+
12
+ var body: some View {
13
+ NavigationStack {
14
+ Form {
15
+ Section("Server") {
16
+ TextField("URL (e.g. http://192.168.1.100:8767)", text: $serverURL)
17
+ .textContentType(.URL)
18
+ .autocorrectionDisabled()
19
+ .textInputAutocapitalization(.never)
20
+
21
+ SecureField("Token", text: $token)
22
+ }
23
+
24
+ if let error = errorMessage {
25
+ Section {
26
+ Text(error)
27
+ .foregroundStyle(.red)
28
+ }
29
+ }
30
+ }
31
+ .navigationTitle("Connect")
32
+ .navigationBarTitleDisplayMode(.inline)
33
+ .toolbar {
34
+ ToolbarItem(placement: .cancellationAction) {
35
+ Button("Cancel") { dismiss() }
36
+ }
37
+ ToolbarItem(placement: .confirmationAction) {
38
+ Button("Connect") { connect() }
39
+ .disabled(serverURL.isEmpty || token.isEmpty)
40
+ }
41
+ }
42
+ }
43
+ }
44
+
45
+ private func connect() {
46
+ guard let url = URL(string: serverURL) else {
47
+ errorMessage = "Invalid URL"
48
+ return
49
+ }
50
+ let keychain = KeychainStore()
51
+ try? keychain.saveServerToken(token, forServer: serverURL)
52
+ state.connect(url: url, credential: .token(token))
53
+ dismiss()
54
+ }
55
+ }
@@ -0,0 +1,70 @@
1
+ import SwiftUI
2
+ import RemuxKit
3
+
4
+ /// First-launch onboarding: scan QR or manual input.
5
+ struct OnboardingView: View {
6
+ @Environment(RemuxState.self) private var state
7
+ @State private var showScanner = false
8
+ @State private var showManualInput = false
9
+
10
+ var body: some View {
11
+ NavigationStack {
12
+ VStack(spacing: 32) {
13
+ Spacer()
14
+
15
+ Image(systemName: "terminal")
16
+ .font(.system(size: 64))
17
+ .foregroundStyle(.tint)
18
+
19
+ Text("Remux")
20
+ .font(.largeTitle.bold())
21
+
22
+ Text("Remote terminal workspace")
23
+ .font(.subheadline)
24
+ .foregroundStyle(.secondary)
25
+
26
+ Spacer()
27
+
28
+ VStack(spacing: 16) {
29
+ Button(action: { showScanner = true }) {
30
+ Label("Scan QR Code", systemImage: "qrcode.viewfinder")
31
+ .frame(maxWidth: .infinity)
32
+ }
33
+ .buttonStyle(.borderedProminent)
34
+ .controlSize(.large)
35
+
36
+ Button(action: { showManualInput = true }) {
37
+ Label("Enter Manually", systemImage: "keyboard")
38
+ .frame(maxWidth: .infinity)
39
+ }
40
+ .buttonStyle(.bordered)
41
+ .controlSize(.large)
42
+ }
43
+ .padding(.horizontal, 40)
44
+
45
+ Spacer()
46
+ }
47
+ .sheet(isPresented: $showScanner) {
48
+ QRScannerView { payload in
49
+ showScanner = false
50
+ handleQRPayload(payload)
51
+ }
52
+ }
53
+ .sheet(isPresented: $showManualInput) {
54
+ ManualConnectView()
55
+ }
56
+ }
57
+ }
58
+
59
+ private func handleQRPayload(_ json: String) {
60
+ guard let data = json.data(using: .utf8),
61
+ let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
62
+ let urlStr = dict["url"] as? String,
63
+ let token = dict["token"] as? String,
64
+ let url = URL(string: urlStr) else { return }
65
+
66
+ let keychain = KeychainStore()
67
+ try? keychain.saveServerToken(token, forServer: urlStr)
68
+ state.connect(url: url, credential: .token(token))
69
+ }
70
+ }
@@ -0,0 +1,92 @@
1
+ import SwiftUI
2
+ import AVFoundation
3
+
4
+ /// Camera QR code scanner for pairing.
5
+ struct QRScannerView: UIViewControllerRepresentable {
6
+ let onScanned: (String) -> Void
7
+
8
+ func makeUIViewController(context: Context) -> QRScannerController {
9
+ let controller = QRScannerController()
10
+ controller.onScanned = onScanned
11
+ return controller
12
+ }
13
+
14
+ func updateUIViewController(_ uiViewController: QRScannerController, context: Context) {}
15
+ }
16
+
17
+ final class QRScannerController: UIViewController, @preconcurrency AVCaptureMetadataOutputObjectsDelegate {
18
+ var onScanned: ((String) -> Void)?
19
+ private var captureSession: AVCaptureSession?
20
+ private var hasScanned = false
21
+
22
+ override func viewDidLoad() {
23
+ super.viewDidLoad()
24
+ view.backgroundColor = .black
25
+ setupCamera()
26
+ }
27
+
28
+ private func setupCamera() {
29
+ let session = AVCaptureSession()
30
+
31
+ guard let device = AVCaptureDevice.default(for: .video),
32
+ let input = try? AVCaptureDeviceInput(device: device) else {
33
+ showError("Camera not available")
34
+ return
35
+ }
36
+
37
+ session.addInput(input)
38
+
39
+ let output = AVCaptureMetadataOutput()
40
+ session.addOutput(output)
41
+ output.setMetadataObjectsDelegate(self, queue: .main)
42
+ output.metadataObjectTypes = [.qr]
43
+
44
+ let previewLayer = AVCaptureVideoPreviewLayer(session: session)
45
+ previewLayer.frame = view.bounds
46
+ previewLayer.videoGravity = .resizeAspectFill
47
+ view.layer.addSublayer(previewLayer)
48
+
49
+ captureSession = session
50
+
51
+ // Overlay instruction
52
+ let label = UILabel()
53
+ label.text = "Scan the QR code shown by your Remux server"
54
+ label.textColor = .white
55
+ label.font = .systemFont(ofSize: 14)
56
+ label.textAlignment = .center
57
+ label.translatesAutoresizingMaskIntoConstraints = false
58
+ view.addSubview(label)
59
+ NSLayoutConstraint.activate([
60
+ label.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -40),
61
+ label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
62
+ ])
63
+
64
+ DispatchQueue.global(qos: .userInitiated).async {
65
+ session.startRunning()
66
+ }
67
+ }
68
+
69
+ func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
70
+ guard !hasScanned,
71
+ let object = metadataObjects.first as? AVMetadataMachineReadableCodeObject,
72
+ let value = object.stringValue else { return }
73
+ hasScanned = true
74
+ captureSession?.stopRunning()
75
+
76
+ // Haptic feedback
77
+ UIImpactFeedbackGenerator(style: .medium).impactOccurred()
78
+
79
+ dismiss(animated: true) { [weak self] in
80
+ self?.onScanned?(value)
81
+ }
82
+ }
83
+
84
+ private func showError(_ message: String) {
85
+ let label = UILabel()
86
+ label.text = message
87
+ label.textColor = .white
88
+ label.textAlignment = .center
89
+ label.frame = view.bounds
90
+ view.addSubview(label)
91
+ }
92
+ }
@@ -0,0 +1,136 @@
1
+ import SwiftUI
2
+ import RemuxKit
3
+ import LocalAuthentication
4
+
5
+ /// Me tab: server management, devices, settings.
6
+ struct MeView: View {
7
+ @Environment(RemuxState.self) private var state
8
+ @AppStorage("faceIdEnabled") private var faceIdEnabled = false
9
+ private let keychain = KeychainStore()
10
+
11
+ var body: some View {
12
+ NavigationStack {
13
+ List {
14
+ // Connection
15
+ Section("Connection") {
16
+ HStack {
17
+ Circle()
18
+ .fill(state.connectionStatus.color)
19
+ .frame(width: 8, height: 8)
20
+ Text(state.connectionStatus.text)
21
+ Spacer()
22
+ if let url = state.serverURL {
23
+ Text(url.host ?? "")
24
+ .font(.caption)
25
+ .foregroundStyle(.secondary)
26
+ }
27
+ }
28
+
29
+ if case .connected = state.connectionStatus {
30
+ Button("Disconnect", role: .destructive) {
31
+ state.disconnect()
32
+ }
33
+ }
34
+ }
35
+
36
+ // Saved servers
37
+ Section("Servers") {
38
+ ForEach(keychain.savedServers(), id: \.self) { server in
39
+ HStack {
40
+ Image(systemName: "server.rack")
41
+ Text(server)
42
+ .font(.subheadline)
43
+ Spacer()
44
+ if state.serverURL?.absoluteString == server {
45
+ Image(systemName: "checkmark")
46
+ .foregroundStyle(.tint)
47
+ }
48
+ }
49
+ .swipeActions {
50
+ Button(role: .destructive) {
51
+ keychain.deleteAll(forServer: server)
52
+ } label: {
53
+ Label("Delete", systemImage: "trash")
54
+ }
55
+ }
56
+ }
57
+ }
58
+
59
+ // Devices
60
+ if !state.devices.isEmpty {
61
+ Section("Devices") {
62
+ ForEach(state.devices, id: \.id) { device in
63
+ HStack {
64
+ Image(systemName: deviceIcon(device.platform))
65
+ VStack(alignment: .leading) {
66
+ Text(device.name ?? device.id.prefix(8).description)
67
+ .font(.subheadline)
68
+ Text(device.trustLevel)
69
+ .font(.caption)
70
+ .foregroundStyle(device.trustLevel == "trusted" ? .green : .orange)
71
+ }
72
+ Spacer()
73
+ if let lastSeen = device.lastSeen {
74
+ Text(lastSeen.prefix(10).description)
75
+ .font(.caption2)
76
+ .foregroundStyle(.tertiary)
77
+ }
78
+ }
79
+ }
80
+ }
81
+ }
82
+
83
+ // Settings
84
+ Section("Security") {
85
+ Toggle("Face ID / Touch ID", isOn: $faceIdEnabled)
86
+ }
87
+
88
+ // App info
89
+ Section("About") {
90
+ HStack {
91
+ Text("Version")
92
+ Spacer()
93
+ Text(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.3.5")
94
+ .foregroundStyle(.secondary)
95
+ }
96
+ }
97
+ }
98
+ .navigationTitle("Me")
99
+ .onAppear {
100
+ // Request device list
101
+ state.sendJSON(["type": "list_devices"])
102
+ }
103
+ }
104
+ }
105
+
106
+ private func deviceIcon(_ platform: String?) -> String {
107
+ switch platform {
108
+ case "ios": "iphone"
109
+ case "macos": "desktopcomputer"
110
+ case "android": "apps.iphone"
111
+ case "web": "globe"
112
+ default: "questionmark.circle"
113
+ }
114
+ }
115
+ }
116
+
117
+ extension ConnectionStatus {
118
+ var color: Color {
119
+ switch self {
120
+ case .connected: .green
121
+ case .reconnecting: .yellow
122
+ case .connecting, .authenticating: .orange
123
+ case .disconnected: .red
124
+ }
125
+ }
126
+
127
+ var text: String {
128
+ switch self {
129
+ case .connected: "Connected"
130
+ case .reconnecting(let n): "Reconnecting (\(n))..."
131
+ case .connecting: "Connecting..."
132
+ case .authenticating: "Authenticating..."
133
+ case .disconnected: "Disconnected"
134
+ }
135
+ }
136
+ }
@@ -0,0 +1,37 @@
1
+ // swift-tools-version: 6.0
2
+
3
+ import PackageDescription
4
+
5
+ let package = Package(
6
+ name: "Remux",
7
+ platforms: [.macOS(.v14)],
8
+ dependencies: [
9
+ .package(path: "../../packages/RemuxKit"),
10
+ ],
11
+ targets: [
12
+ .executableTarget(
13
+ name: "Remux",
14
+ dependencies: ["RemuxKit", "GhosttyKit"],
15
+ path: "Sources/Remux",
16
+ linkerSettings: [
17
+ .linkedFramework("Cocoa"),
18
+ .linkedFramework("Metal"),
19
+ .linkedFramework("MetalKit"),
20
+ .linkedFramework("QuartzCore"),
21
+ .linkedFramework("Carbon"),
22
+ .linkedFramework("CoreText"),
23
+ .linkedFramework("CoreGraphics"),
24
+ .linkedFramework("Foundation"),
25
+ .linkedFramework("IOKit"),
26
+ .linkedFramework("IOSurface"),
27
+ .linkedFramework("UniformTypeIdentifiers"),
28
+ .linkedLibrary("c++"),
29
+ .linkedLibrary("z"),
30
+ ]
31
+ ),
32
+ .binaryTarget(
33
+ name: "GhosttyKit",
34
+ path: "../../vendor/ghostty/macos/GhosttyKit.xcframework"
35
+ ),
36
+ ]
37
+ )