@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.
- package/.github/ISSUE_TEMPLATE/bug_report.md +47 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +38 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +28 -0
- package/.github/dependabot.yml +33 -0
- package/.github/workflows/ci.yml +65 -0
- package/.github/workflows/deploy.yml +65 -0
- package/.github/workflows/publish.yml +138 -0
- package/.github/workflows/release-please.yml +21 -0
- package/.gitmodules +3 -0
- package/.nvmrc +1 -0
- package/.release-please-manifest.json +3 -0
- package/CLAUDE.md +104 -0
- package/Dockerfile +23 -0
- package/LICENSE +21 -0
- package/README.md +120 -0
- package/apps/ios/Config/signing.xcconfig +4 -0
- package/apps/ios/Package.swift +26 -0
- package/apps/ios/Remux.xcodeproj/project.pbxproj +456 -0
- package/apps/ios/Remux.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/apps/ios/Sources/Remux/Extensions/FaceIDManager.swift +29 -0
- package/apps/ios/Sources/Remux/Extensions/InspectCache.swift +66 -0
- package/apps/ios/Sources/Remux/MainTabView.swift +32 -0
- package/apps/ios/Sources/Remux/Remux.entitlements +8 -0
- package/apps/ios/Sources/Remux/RemuxiOSApp.swift +14 -0
- package/apps/ios/Sources/Remux/RootView.swift +130 -0
- package/apps/ios/Sources/Remux/Views/Control/ControlView.swift +102 -0
- package/apps/ios/Sources/Remux/Views/Inspect/InspectView.swift +98 -0
- package/apps/ios/Sources/Remux/Views/Live/LiveTerminalView.swift +132 -0
- package/apps/ios/Sources/Remux/Views/Now/NowView.swift +173 -0
- package/apps/ios/Sources/Remux/Views/Onboarding/ManualConnectView.swift +55 -0
- package/apps/ios/Sources/Remux/Views/Onboarding/OnboardingView.swift +70 -0
- package/apps/ios/Sources/Remux/Views/Onboarding/QRScannerView.swift +92 -0
- package/apps/ios/Sources/Remux/Views/Settings/MeView.swift +136 -0
- package/apps/macos/Package.swift +37 -0
- package/apps/macos/Resources/shell-integration/bash/bash-preexec.sh +382 -0
- package/apps/macos/Resources/shell-integration/bash/ghostty.bash +315 -0
- package/apps/macos/Resources/shell-integration/elvish/lib/ghostty-integration.elv +191 -0
- package/apps/macos/Resources/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +246 -0
- package/apps/macos/Resources/shell-integration/nushell/vendor/autoload/ghostty.nu +110 -0
- package/apps/macos/Resources/shell-integration/zsh/.zshenv +61 -0
- package/apps/macos/Resources/shell-integration/zsh/ghostty-integration +458 -0
- package/apps/macos/Resources/terminfo/67/ghostty +0 -0
- package/apps/macos/Resources/terminfo/78/xterm-ghostty +0 -0
- package/apps/macos/Sources/Remux/AppDelegate.swift +257 -0
- package/apps/macos/Sources/Remux/CrashReporter.swift +210 -0
- package/apps/macos/Sources/Remux/FinderIntegration.swift +117 -0
- package/apps/macos/Sources/Remux/GhosttyConfig.swift +311 -0
- package/apps/macos/Sources/Remux/KeyboardShortcuts/ShortcutAction.swift +115 -0
- package/apps/macos/Sources/Remux/KeyboardShortcuts/ShortcutSettingsView.swift +271 -0
- package/apps/macos/Sources/Remux/KeyboardShortcuts/StoredShortcut.swift +149 -0
- package/apps/macos/Sources/Remux/MainContentView.swift +308 -0
- package/apps/macos/Sources/Remux/MenuBarManager.swift +275 -0
- package/apps/macos/Sources/Remux/NotificationManager.swift +145 -0
- package/apps/macos/Sources/Remux/PortScanner.swift +152 -0
- package/apps/macos/Sources/Remux/RemuxApp.swift +13 -0
- package/apps/macos/Sources/Remux/SSHDetector.swift +151 -0
- package/apps/macos/Sources/Remux/SessionPersistence.swift +226 -0
- package/apps/macos/Sources/Remux/SocketController.swift +258 -0
- package/apps/macos/Sources/Remux/UpdateChecker.swift +152 -0
- package/apps/macos/Sources/Remux/Views/CommandPalette.swift +198 -0
- package/apps/macos/Sources/Remux/Views/ConnectionView.swift +84 -0
- package/apps/macos/Sources/Remux/Views/InspectView.swift +127 -0
- package/apps/macos/Sources/Remux/Views/SettingsView.swift +77 -0
- package/apps/macos/Sources/Remux/Views/Sidebar/SidebarView.swift +410 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/BrowserPanel.swift +193 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/MarkdownPanel.swift +277 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/PanelProtocol.swift +14 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/SplitNode.swift +149 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/SplitView.swift +234 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/TerminalPanel.swift +26 -0
- package/apps/macos/Sources/Remux/Views/TabBarView.swift +94 -0
- package/apps/macos/Sources/Remux/Views/Terminal/ClipboardHelper.swift +101 -0
- package/apps/macos/Sources/Remux/Views/Terminal/CopyModeOverlay.swift +325 -0
- package/apps/macos/Sources/Remux/Views/Terminal/GhosttyNativeTerminalView.swift +39 -0
- package/apps/macos/Sources/Remux/Views/Terminal/GhosttyNativeView.swift +559 -0
- package/apps/macos/Sources/Remux/Views/Terminal/SurfaceSearchOverlay.swift +109 -0
- package/apps/macos/Sources/Remux/Views/Terminal/TerminalContainerView.swift +95 -0
- package/apps/macos/Sources/Remux/Views/Terminal/TerminalRelay.swift +117 -0
- package/build.mjs +33 -0
- package/native/android/DecodeGoldenPayloads.kt +487 -0
- package/native/android/ProtocolModels.kt +188 -0
- package/native/ios/DecodeGoldenPayloads.swift +711 -0
- package/native/ios/ProtocolModels.swift +200 -0
- package/package.json +45 -0
- package/packages/RemuxKit/Package.swift +27 -0
- package/packages/RemuxKit/Sources/RemuxKit/Device/DeviceManager.swift +27 -0
- package/packages/RemuxKit/Sources/RemuxKit/Models/ProtocolModels.swift +206 -0
- package/packages/RemuxKit/Sources/RemuxKit/Networking/MessageRouter.swift +108 -0
- package/packages/RemuxKit/Sources/RemuxKit/Networking/RemuxConnection.swift +395 -0
- package/packages/RemuxKit/Sources/RemuxKit/State/RemuxState.swift +188 -0
- package/packages/RemuxKit/Sources/RemuxKit/Storage/KeychainStore.swift +142 -0
- package/packages/RemuxKit/Sources/RemuxKit/Terminal/GhosttyBridge.swift +145 -0
- package/packages/RemuxKit/Sources/RemuxKit/Terminal/GhosttyTerminalView.swift +35 -0
- package/packages/RemuxKit/Sources/RemuxKit/Terminal/Resources/ghostty-terminal.html +91 -0
- package/packages/RemuxKit/Tests/RemuxKitTests/ConnectionIntegrationTest.swift +74 -0
- package/packages/RemuxKit/Tests/RemuxKitTests/KeychainStoreTests.swift +81 -0
- package/packages/RemuxKit/Tests/RemuxKitTests/ProtocolModelsTests.swift +179 -0
- package/packages/RemuxKit/Tests/RemuxKitTests/RemuxStateTests.swift +62 -0
- package/playwright.config.ts +17 -0
- package/pnpm-lock.yaml +1588 -0
- package/pty-daemon.js +303 -0
- package/release-please-config.json +14 -0
- package/scripts/auto-deploy.sh +46 -0
- package/scripts/build-dmg.sh +121 -0
- package/scripts/build-ghostty-kit.sh +43 -0
- package/scripts/check-active-terminology.mjs +132 -0
- package/scripts/sync-ghostty-web.sh +28 -0
- package/server.js +7074 -0
- package/src/adapters/agent-events.ts +246 -0
- package/src/adapters/claude-code.ts +158 -0
- package/src/adapters/codex.ts +210 -0
- package/src/adapters/generic-shell.ts +58 -0
- package/src/adapters/index.ts +15 -0
- package/src/adapters/registry.ts +99 -0
- package/src/adapters/types.ts +41 -0
- package/src/auth.ts +174 -0
- package/src/e2ee.ts +236 -0
- package/src/git-service.ts +168 -0
- package/src/message-buffer.ts +137 -0
- package/src/pty-daemon.ts +357 -0
- package/src/push.ts +127 -0
- package/src/renderers.ts +455 -0
- package/src/server.ts +2407 -0
- package/src/service.ts +226 -0
- package/src/session.ts +978 -0
- package/src/store.ts +1422 -0
- package/src/team.ts +123 -0
- package/src/tunnel.ts +126 -0
- package/src/types.d.ts +50 -0
- package/src/vt-tracker.ts +188 -0
- package/src/workspace-head.ts +144 -0
- package/src/workspace.ts +153 -0
- package/src/ws-handler.ts +1526 -0
- package/start.ps1 +83 -0
- package/tests/adapters.test.js +171 -0
- package/tests/auth.test.js +243 -0
- package/tests/codex-adapter.test.js +535 -0
- package/tests/durable-stream.test.js +153 -0
- package/tests/e2e/app.spec.js +530 -0
- package/tests/e2ee.test.js +325 -0
- package/tests/message-buffer.test.js +245 -0
- package/tests/message-routing.test.js +305 -0
- package/tests/pty-daemon.test.js +346 -0
- package/tests/push.test.js +281 -0
- package/tests/renderers.test.js +391 -0
- package/tests/search-shell.test.js +499 -0
- package/tests/server.test.js +882 -0
- package/tests/service.test.js +267 -0
- package/tests/store.test.js +369 -0
- package/tests/tunnel.test.js +67 -0
- package/tests/workspace-head.test.js +116 -0
- package/tests/workspace.test.js +417 -0
- package/tsconfig.backend.json +11 -0
- package/tsconfig.json +15 -0
- package/tui/client/client_test.go +125 -0
- package/tui/client/connection.go +342 -0
- package/tui/client/host_manager.go +141 -0
- package/tui/config/cache.go +81 -0
- package/tui/config/config.go +53 -0
- package/tui/config/config_test.go +89 -0
- package/tui/go.mod +32 -0
- package/tui/go.sum +50 -0
- package/tui/main.go +261 -0
- package/tui/tests/integration_test.go +283 -0
- package/tui/ui/model.go +310 -0
- 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
|
+
)
|