@wangyaoshen/remux 0.3.8-dev.a8ceb0c
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 +312 -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 +477 -0
- package/apps/ios/Remux.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/Contents.json +23 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_1024x1024.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_120x120.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_152x152.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_167x167.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_180x180.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_20x20.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_29x29.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_40x40.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_58x58.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_60x60.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_76x76.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_80x80.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_87x87.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/Contents.json +6 -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/setup-ci-secrets.sh +80 -0
- package/scripts/sync-ghostty-web.sh +28 -0
- package/scripts/upload-testflight.sh +100 -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,142 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Security
|
|
3
|
+
|
|
4
|
+
/// Keychain wrapper for storing remux credentials.
|
|
5
|
+
/// Supports multiple servers via kSecAttrAccount differentiation.
|
|
6
|
+
public struct KeychainStore: Sendable {
|
|
7
|
+
|
|
8
|
+
private static let service = "com.remux.credentials"
|
|
9
|
+
|
|
10
|
+
public init() {}
|
|
11
|
+
|
|
12
|
+
// MARK: - Resume Token
|
|
13
|
+
|
|
14
|
+
public func saveResumeToken(_ token: String, forServer server: String) throws {
|
|
15
|
+
try save(key: "resume_token", value: token, account: server)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public func loadResumeToken(forServer server: String) -> String? {
|
|
19
|
+
load(key: "resume_token", account: server)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public func deleteResumeToken(forServer server: String) {
|
|
23
|
+
delete(key: "resume_token", account: server)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// MARK: - Device ID
|
|
27
|
+
|
|
28
|
+
public func saveDeviceId(_ id: String, forServer server: String) throws {
|
|
29
|
+
try save(key: "device_id", value: id, account: server)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public func loadDeviceId(forServer server: String) -> String? {
|
|
33
|
+
load(key: "device_id", account: server)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// MARK: - Server Token (manual token auth)
|
|
37
|
+
|
|
38
|
+
public func saveServerToken(_ token: String, forServer server: String) throws {
|
|
39
|
+
try save(key: "server_token", value: token, account: server)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public func loadServerToken(forServer server: String) -> String? {
|
|
43
|
+
load(key: "server_token", account: server)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public func deleteServerToken(forServer server: String) {
|
|
47
|
+
delete(key: "server_token", account: server)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// MARK: - Server List
|
|
51
|
+
|
|
52
|
+
/// Returns all server URLs that have stored credentials.
|
|
53
|
+
public func savedServers() -> [String] {
|
|
54
|
+
let query: [String: Any] = [
|
|
55
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
56
|
+
kSecAttrService as String: Self.service,
|
|
57
|
+
kSecAttrLabel as String: "resume_token",
|
|
58
|
+
kSecMatchLimit as String: kSecMatchLimitAll,
|
|
59
|
+
kSecReturnAttributes as String: true,
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
var result: AnyObject?
|
|
63
|
+
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
64
|
+
guard status == errSecSuccess, let items = result as? [[String: Any]] else {
|
|
65
|
+
return []
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return items.compactMap { $0[kSecAttrAccount as String] as? String }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// Delete all credentials for a server.
|
|
72
|
+
public func deleteAll(forServer server: String) {
|
|
73
|
+
delete(key: "resume_token", account: server)
|
|
74
|
+
delete(key: "device_id", account: server)
|
|
75
|
+
delete(key: "server_token", account: server)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// MARK: - Low-level helpers
|
|
79
|
+
|
|
80
|
+
private func save(key: String, value: String, account: String) throws {
|
|
81
|
+
let data = Data(value.utf8)
|
|
82
|
+
|
|
83
|
+
// Try update first
|
|
84
|
+
let updateQuery: [String: Any] = [
|
|
85
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
86
|
+
kSecAttrService as String: Self.service,
|
|
87
|
+
kSecAttrLabel as String: key,
|
|
88
|
+
kSecAttrAccount as String: account,
|
|
89
|
+
]
|
|
90
|
+
let updateAttrs: [String: Any] = [
|
|
91
|
+
kSecValueData as String: data,
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
let updateStatus = SecItemUpdate(updateQuery as CFDictionary, updateAttrs as CFDictionary)
|
|
95
|
+
if updateStatus == errSecSuccess { return }
|
|
96
|
+
|
|
97
|
+
// Item doesn't exist — add it
|
|
98
|
+
let addQuery: [String: Any] = [
|
|
99
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
100
|
+
kSecAttrService as String: Self.service,
|
|
101
|
+
kSecAttrLabel as String: key,
|
|
102
|
+
kSecAttrAccount as String: account,
|
|
103
|
+
kSecValueData as String: data,
|
|
104
|
+
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock,
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
|
|
108
|
+
guard addStatus == errSecSuccess else {
|
|
109
|
+
throw KeychainError.saveFailed(status: addStatus)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private func load(key: String, account: String) -> String? {
|
|
114
|
+
let query: [String: Any] = [
|
|
115
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
116
|
+
kSecAttrService as String: Self.service,
|
|
117
|
+
kSecAttrLabel as String: key,
|
|
118
|
+
kSecAttrAccount as String: account,
|
|
119
|
+
kSecReturnData as String: true,
|
|
120
|
+
kSecMatchLimit as String: kSecMatchLimitOne,
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
var result: AnyObject?
|
|
124
|
+
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
125
|
+
guard status == errSecSuccess, let data = result as? Data else { return nil }
|
|
126
|
+
return String(data: data, encoding: .utf8)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private func delete(key: String, account: String) {
|
|
130
|
+
let query: [String: Any] = [
|
|
131
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
132
|
+
kSecAttrService as String: Self.service,
|
|
133
|
+
kSecAttrLabel as String: key,
|
|
134
|
+
kSecAttrAccount as String: account,
|
|
135
|
+
]
|
|
136
|
+
SecItemDelete(query as CFDictionary)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
public enum KeychainError: Error {
|
|
141
|
+
case saveFailed(status: OSStatus)
|
|
142
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import WebKit
|
|
3
|
+
|
|
4
|
+
/// Bridge between Swift and ghostty-web running in WKWebView.
|
|
5
|
+
/// Used by iOS (and macOS WKWebView fallback if needed).
|
|
6
|
+
/// Communication:
|
|
7
|
+
/// Swift → JS: webView.evaluateJavaScript("terminal.write(...)")
|
|
8
|
+
/// JS → Swift: webkit.messageHandlers.remux.postMessage({type, data})
|
|
9
|
+
@MainActor
|
|
10
|
+
public final class GhosttyBridge: NSObject, WKScriptMessageHandler {
|
|
11
|
+
|
|
12
|
+
public let webView: WKWebView
|
|
13
|
+
|
|
14
|
+
public var onInput: ((String) -> Void)?
|
|
15
|
+
public var onResize: ((Int, Int) -> Void)?
|
|
16
|
+
public var onReady: (() -> Void)?
|
|
17
|
+
public var onBell: (() -> Void)?
|
|
18
|
+
|
|
19
|
+
public init(frame: CGRect = .zero) {
|
|
20
|
+
let config = WKWebViewConfiguration()
|
|
21
|
+
config.preferences.setValue(true, forKey: "allowFileAccessFromFileURLs")
|
|
22
|
+
|
|
23
|
+
// Disable zoom on iOS
|
|
24
|
+
let viewportScript = WKUserScript(
|
|
25
|
+
source: "var meta = document.createElement('meta'); meta.name = 'viewport'; meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'; document.head.appendChild(meta);",
|
|
26
|
+
injectionTime: .atDocumentEnd,
|
|
27
|
+
forMainFrameOnly: true
|
|
28
|
+
)
|
|
29
|
+
config.userContentController.addUserScript(viewportScript)
|
|
30
|
+
|
|
31
|
+
webView = WKWebView(frame: frame, configuration: config)
|
|
32
|
+
|
|
33
|
+
#if os(iOS)
|
|
34
|
+
webView.scrollView.isScrollEnabled = false
|
|
35
|
+
webView.scrollView.bounces = false
|
|
36
|
+
#endif
|
|
37
|
+
|
|
38
|
+
super.init()
|
|
39
|
+
|
|
40
|
+
config.userContentController.add(self, name: "remux")
|
|
41
|
+
loadTerminalHTML()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// MARK: - Swift → JS
|
|
45
|
+
|
|
46
|
+
/// Write PTY data to the terminal (base64 encoded to avoid escaping issues)
|
|
47
|
+
public func writeToTerminal(data: Data) {
|
|
48
|
+
let base64 = data.base64EncodedString()
|
|
49
|
+
// Use callAsyncJavaScript with arguments to prevent JS injection
|
|
50
|
+
webView.callAsyncJavaScript(
|
|
51
|
+
"window.terminalBridge.write(b64)",
|
|
52
|
+
arguments: ["b64": base64],
|
|
53
|
+
in: nil,
|
|
54
|
+
in: WKContentWorld.page,
|
|
55
|
+
completionHandler: nil
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// Write string data to the terminal
|
|
60
|
+
public func writeString(_ string: String) {
|
|
61
|
+
if let data = string.data(using: .utf8) {
|
|
62
|
+
writeToTerminal(data: data)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Resize the terminal
|
|
67
|
+
public func resize(cols: Int, rows: Int) {
|
|
68
|
+
webView.callAsyncJavaScript(
|
|
69
|
+
"window.terminalBridge.resize(c, r)",
|
|
70
|
+
arguments: ["c": cols, "r": rows],
|
|
71
|
+
in: nil,
|
|
72
|
+
in: WKContentWorld.page,
|
|
73
|
+
completionHandler: nil
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// Set terminal theme
|
|
78
|
+
public func setTheme(_ config: [String: String]) {
|
|
79
|
+
if let data = try? JSONSerialization.data(withJSONObject: config),
|
|
80
|
+
let json = String(data: data, encoding: .utf8) {
|
|
81
|
+
webView.evaluateJavaScript("window.terminalBridge.setTheme(\(json))") { _, _ in }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// MARK: - JS → Swift (WKScriptMessageHandler)
|
|
86
|
+
|
|
87
|
+
public func userContentController(_ controller: WKUserContentController, didReceive message: WKScriptMessage) {
|
|
88
|
+
guard let body = message.body as? [String: Any],
|
|
89
|
+
let type = body["type"] as? String else { return }
|
|
90
|
+
|
|
91
|
+
switch type {
|
|
92
|
+
case "input":
|
|
93
|
+
if let data = body["data"] as? String {
|
|
94
|
+
onInput?(data)
|
|
95
|
+
}
|
|
96
|
+
case "resize":
|
|
97
|
+
if let cols = body["cols"] as? Int, let rows = body["rows"] as? Int {
|
|
98
|
+
onResize?(cols, rows)
|
|
99
|
+
}
|
|
100
|
+
case "ready":
|
|
101
|
+
onReady?()
|
|
102
|
+
case "bell":
|
|
103
|
+
onBell?()
|
|
104
|
+
default:
|
|
105
|
+
break
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// MARK: - HTML loading
|
|
110
|
+
|
|
111
|
+
private func loadTerminalHTML() {
|
|
112
|
+
// Load from main bundle (app must include ghostty-terminal.html)
|
|
113
|
+
if let resourceURL = Bundle.main.url(forResource: "ghostty-terminal", withExtension: "html") {
|
|
114
|
+
webView.loadFileURL(resourceURL, allowingReadAccessTo: resourceURL.deletingLastPathComponent())
|
|
115
|
+
} else {
|
|
116
|
+
// Fallback: inline HTML for development/testing
|
|
117
|
+
let html = Self.fallbackHTML
|
|
118
|
+
webView.loadHTMLString(html, baseURL: nil)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/// Minimal fallback HTML when bundle resources are not yet available
|
|
123
|
+
private static let fallbackHTML = """
|
|
124
|
+
<!DOCTYPE html>
|
|
125
|
+
<html>
|
|
126
|
+
<head>
|
|
127
|
+
<meta charset="utf-8">
|
|
128
|
+
<style>
|
|
129
|
+
body { margin: 0; background: #1a1a2e; color: #e0e0e0; font-family: monospace; padding: 20px; }
|
|
130
|
+
</style>
|
|
131
|
+
</head>
|
|
132
|
+
<body>
|
|
133
|
+
<p>ghostty-web resources not found. Run scripts/sync-ghostty-web.sh to bundle them.</p>
|
|
134
|
+
<script>
|
|
135
|
+
window.terminalBridge = {
|
|
136
|
+
write: function(b64) {},
|
|
137
|
+
resize: function(c, r) {},
|
|
138
|
+
setTheme: function(t) {}
|
|
139
|
+
};
|
|
140
|
+
window.webkit.messageHandlers.remux.postMessage({type: 'ready', cols: 80, rows: 24});
|
|
141
|
+
</script>
|
|
142
|
+
</body>
|
|
143
|
+
</html>
|
|
144
|
+
"""
|
|
145
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import WebKit
|
|
3
|
+
|
|
4
|
+
/// SwiftUI view wrapping ghostty-web in WKWebView.
|
|
5
|
+
/// Used by iOS. macOS uses GhosttyNativeTerminalView (libghostty) instead.
|
|
6
|
+
#if os(iOS)
|
|
7
|
+
public struct GhosttyTerminalView: UIViewRepresentable {
|
|
8
|
+
let bridge: GhosttyBridge
|
|
9
|
+
|
|
10
|
+
public init(bridge: GhosttyBridge) {
|
|
11
|
+
self.bridge = bridge
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public func makeUIView(context: Context) -> WKWebView {
|
|
15
|
+
bridge.webView
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public func updateUIView(_ uiView: WKWebView, context: Context) {}
|
|
19
|
+
}
|
|
20
|
+
#elseif os(macOS)
|
|
21
|
+
/// macOS fallback using WKWebView (prefer GhosttyNativeTerminalView for production)
|
|
22
|
+
public struct GhosttyTerminalView: NSViewRepresentable {
|
|
23
|
+
let bridge: GhosttyBridge
|
|
24
|
+
|
|
25
|
+
public init(bridge: GhosttyBridge) {
|
|
26
|
+
self.bridge = bridge
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public func makeNSView(context: Context) -> WKWebView {
|
|
30
|
+
bridge.webView
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public func updateNSView(_ nsView: WKWebView, context: Context) {}
|
|
34
|
+
}
|
|
35
|
+
#endif
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
6
|
+
<style>
|
|
7
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
8
|
+
html, body { width: 100%; height: 100%; overflow: hidden; background: transparent; }
|
|
9
|
+
#terminal { width: 100%; height: 100%; }
|
|
10
|
+
</style>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div id="terminal"></div>
|
|
14
|
+
<script src="ghostty-web.js"></script>
|
|
15
|
+
<script>
|
|
16
|
+
// ghostty-web terminal bridge for RemuxKit WKWebView integration
|
|
17
|
+
(function() {
|
|
18
|
+
let terminal = null;
|
|
19
|
+
|
|
20
|
+
function init() {
|
|
21
|
+
if (typeof GhosttyWeb === 'undefined') {
|
|
22
|
+
// ghostty-web not loaded yet — show placeholder
|
|
23
|
+
document.getElementById('terminal').textContent = 'Loading terminal...';
|
|
24
|
+
setTimeout(init, 100);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const container = document.getElementById('terminal');
|
|
29
|
+
terminal = new GhosttyWeb.Terminal(container, {
|
|
30
|
+
// ghostty-web options
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Forward input to Swift
|
|
34
|
+
terminal.onData(function(data) {
|
|
35
|
+
window.webkit.messageHandlers.remux.postMessage({
|
|
36
|
+
type: 'input',
|
|
37
|
+
data: data
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Forward bell to Swift
|
|
42
|
+
terminal.onBell(function() {
|
|
43
|
+
window.webkit.messageHandlers.remux.postMessage({ type: 'bell' });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Watch for container resize
|
|
47
|
+
const resizeObserver = new ResizeObserver(function() {
|
|
48
|
+
if (terminal && terminal.cols && terminal.rows) {
|
|
49
|
+
window.webkit.messageHandlers.remux.postMessage({
|
|
50
|
+
type: 'resize',
|
|
51
|
+
cols: terminal.cols,
|
|
52
|
+
rows: terminal.rows
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
resizeObserver.observe(container);
|
|
57
|
+
|
|
58
|
+
// Notify Swift that terminal is ready
|
|
59
|
+
window.webkit.messageHandlers.remux.postMessage({
|
|
60
|
+
type: 'ready',
|
|
61
|
+
cols: terminal.cols || 80,
|
|
62
|
+
rows: terminal.rows || 24
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Bridge object exposed to Swift
|
|
67
|
+
window.terminalBridge = {
|
|
68
|
+
write: function(base64) {
|
|
69
|
+
if (!terminal) return;
|
|
70
|
+
const raw = atob(base64);
|
|
71
|
+
const bytes = new Uint8Array(raw.length);
|
|
72
|
+
for (let i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i);
|
|
73
|
+
terminal.write(bytes);
|
|
74
|
+
},
|
|
75
|
+
resize: function(cols, rows) {
|
|
76
|
+
if (!terminal) return;
|
|
77
|
+
terminal.resize(cols, rows);
|
|
78
|
+
},
|
|
79
|
+
setTheme: function(config) {
|
|
80
|
+
if (!terminal) return;
|
|
81
|
+
// Apply theme config to terminal
|
|
82
|
+
if (config.background) document.body.style.background = config.background;
|
|
83
|
+
// ghostty-web theme API TBD based on actual ghostty-web version
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
init();
|
|
88
|
+
})();
|
|
89
|
+
</script>
|
|
90
|
+
</body>
|
|
91
|
+
</html>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import Testing
|
|
2
|
+
import Foundation
|
|
3
|
+
@testable import RemuxKit
|
|
4
|
+
|
|
5
|
+
@Suite("Connection Integration")
|
|
6
|
+
struct ConnectionIntegrationTests {
|
|
7
|
+
|
|
8
|
+
@Test("Connect to local remux server and receive state")
|
|
9
|
+
func connectAndReceiveState() async throws {
|
|
10
|
+
let url = URL(string: "http://localhost:8767")!
|
|
11
|
+
|
|
12
|
+
let conn = RemuxConnection(
|
|
13
|
+
serverURL: url,
|
|
14
|
+
credential: .token("test123"),
|
|
15
|
+
cols: 80,
|
|
16
|
+
rows: 24
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
let result: (auth: Bool, state: Bool) = await withCheckedContinuation { continuation in
|
|
20
|
+
final class TestDelegate: RemuxConnectionDelegate, @unchecked Sendable {
|
|
21
|
+
var authed = false
|
|
22
|
+
var gotState = false
|
|
23
|
+
var continuation: CheckedContinuation<(auth: Bool, state: Bool), Never>?
|
|
24
|
+
var timer: DispatchWorkItem?
|
|
25
|
+
|
|
26
|
+
func connectionDidChangeStatus(_ status: ConnectionStatus) {}
|
|
27
|
+
func connectionDidReceiveData(_ data: Data) {}
|
|
28
|
+
|
|
29
|
+
func connectionDidAuthenticate(capabilities: ProtocolCapabilities) {
|
|
30
|
+
authed = true
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func connectionDidFailAuth(reason: String) {
|
|
34
|
+
finish()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func connectionDidReceiveMessage(_ message: String) {
|
|
38
|
+
if message.contains("\"type\":\"state\"") {
|
|
39
|
+
gotState = true
|
|
40
|
+
finish()
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
func finish() {
|
|
45
|
+
timer?.cancel()
|
|
46
|
+
let c = continuation
|
|
47
|
+
continuation = nil
|
|
48
|
+
c?.resume(returning: (auth: authed, state: gotState))
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let delegate = TestDelegate()
|
|
53
|
+
delegate.continuation = continuation
|
|
54
|
+
|
|
55
|
+
// Timeout after 5s
|
|
56
|
+
let timeout = DispatchWorkItem { delegate.finish() }
|
|
57
|
+
delegate.timer = timeout
|
|
58
|
+
DispatchQueue.global().asyncAfter(deadline: .now() + 5, execute: timeout)
|
|
59
|
+
|
|
60
|
+
conn.delegate = delegate
|
|
61
|
+
conn.connect()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
conn.disconnect()
|
|
65
|
+
|
|
66
|
+
if !result.auth {
|
|
67
|
+
print("⚠️ Server not running at localhost:8767, skipping")
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#expect(result.auth == true)
|
|
72
|
+
#expect(result.state == true)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import Testing
|
|
2
|
+
import Foundation
|
|
3
|
+
import Security
|
|
4
|
+
@testable import RemuxKit
|
|
5
|
+
|
|
6
|
+
/// Keychain tests require macOS Keychain access (not available in headless CI).
|
|
7
|
+
/// Tests are conditionally enabled.
|
|
8
|
+
@Suite("KeychainStore")
|
|
9
|
+
struct KeychainStoreTests {
|
|
10
|
+
|
|
11
|
+
let store = KeychainStore()
|
|
12
|
+
let testServer = "test-server-\(UUID().uuidString)"
|
|
13
|
+
|
|
14
|
+
static var keychainAvailable: Bool {
|
|
15
|
+
// CI environments (GitHub Actions, etc.) have partially-broken Keychain:
|
|
16
|
+
// SecItemAdd may succeed but the real store operations fail with -25299.
|
|
17
|
+
// Skip Keychain tests entirely in CI.
|
|
18
|
+
if ProcessInfo.processInfo.environment["CI"] != nil { return false }
|
|
19
|
+
|
|
20
|
+
let query: [String: Any] = [
|
|
21
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
22
|
+
kSecAttrService as String: "com.remux.test-probe",
|
|
23
|
+
kSecAttrAccount as String: UUID().uuidString,
|
|
24
|
+
kSecValueData as String: Data("probe".utf8),
|
|
25
|
+
]
|
|
26
|
+
let status = SecItemAdd(query as CFDictionary, nil)
|
|
27
|
+
if status == errSecSuccess {
|
|
28
|
+
SecItemDelete(query as CFDictionary)
|
|
29
|
+
return true
|
|
30
|
+
}
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@Test("Save and load server token", .enabled(if: keychainAvailable))
|
|
35
|
+
func saveAndLoadToken() throws {
|
|
36
|
+
try store.saveServerToken("abc123", forServer: testServer)
|
|
37
|
+
defer { store.deleteServerToken(forServer: testServer) }
|
|
38
|
+
let loaded = store.loadServerToken(forServer: testServer)
|
|
39
|
+
#expect(loaded == "abc123")
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@Test("Save and load resume token", .enabled(if: keychainAvailable))
|
|
43
|
+
func saveAndLoadResumeToken() throws {
|
|
44
|
+
try store.saveResumeToken("resume-xyz", forServer: testServer)
|
|
45
|
+
defer { store.deleteResumeToken(forServer: testServer) }
|
|
46
|
+
let loaded = store.loadResumeToken(forServer: testServer)
|
|
47
|
+
#expect(loaded == "resume-xyz")
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@Test("Load nonexistent key returns nil")
|
|
51
|
+
func loadMissing() {
|
|
52
|
+
let result = store.loadServerToken(forServer: "nonexistent-\(UUID().uuidString)")
|
|
53
|
+
#expect(result == nil)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@Test("Delete removes key", .enabled(if: keychainAvailable))
|
|
57
|
+
func deleteKey() throws {
|
|
58
|
+
try store.saveServerToken("temp", forServer: testServer)
|
|
59
|
+
store.deleteServerToken(forServer: testServer)
|
|
60
|
+
let loaded = store.loadServerToken(forServer: testServer)
|
|
61
|
+
#expect(loaded == nil)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@Test("Update existing key", .enabled(if: keychainAvailable))
|
|
65
|
+
func updateKey() throws {
|
|
66
|
+
try store.saveServerToken("v1", forServer: testServer)
|
|
67
|
+
try store.saveServerToken("v2", forServer: testServer)
|
|
68
|
+
defer { store.deleteServerToken(forServer: testServer) }
|
|
69
|
+
let loaded = store.loadServerToken(forServer: testServer)
|
|
70
|
+
#expect(loaded == "v2")
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@Test("Delete all for server", .enabled(if: keychainAvailable))
|
|
74
|
+
func deleteAll() throws {
|
|
75
|
+
try store.saveServerToken("tok", forServer: testServer)
|
|
76
|
+
try store.saveDeviceId("dev", forServer: testServer)
|
|
77
|
+
store.deleteAll(forServer: testServer)
|
|
78
|
+
#expect(store.loadServerToken(forServer: testServer) == nil)
|
|
79
|
+
#expect(store.loadDeviceId(forServer: testServer) == nil)
|
|
80
|
+
}
|
|
81
|
+
}
|