@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,149 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
|
|
3
|
+
/// Persisted representation of a keyboard shortcut.
|
|
4
|
+
/// Adapted from ghostty-org/ghostty KeyEquivalent binding storage.
|
|
5
|
+
struct StoredShortcut: Codable, Equatable, Hashable, Sendable {
|
|
6
|
+
var key: String // e.g. "d", "left", "\r"
|
|
7
|
+
var command: Bool
|
|
8
|
+
var shift: Bool
|
|
9
|
+
var option: Bool
|
|
10
|
+
var control: Bool
|
|
11
|
+
|
|
12
|
+
init(
|
|
13
|
+
key: String,
|
|
14
|
+
command: Bool = false,
|
|
15
|
+
shift: Bool = false,
|
|
16
|
+
option: Bool = false,
|
|
17
|
+
control: Bool = false
|
|
18
|
+
) {
|
|
19
|
+
self.key = key
|
|
20
|
+
self.command = command
|
|
21
|
+
self.shift = shift
|
|
22
|
+
self.option = option
|
|
23
|
+
self.control = control
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// Human-readable display string like "⌘D" or "⌃⌘F".
|
|
27
|
+
var displayString: String {
|
|
28
|
+
var parts: [String] = []
|
|
29
|
+
if control { parts.append("⌃") }
|
|
30
|
+
if option { parts.append("⌥") }
|
|
31
|
+
if shift { parts.append("⇧") }
|
|
32
|
+
if command { parts.append("⌘") }
|
|
33
|
+
parts.append(keyDisplayName)
|
|
34
|
+
return parts.joined()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// Display name for the key character.
|
|
38
|
+
private var keyDisplayName: String {
|
|
39
|
+
switch key.lowercased() {
|
|
40
|
+
case "left": return "←"
|
|
41
|
+
case "right": return "→"
|
|
42
|
+
case "up": return "↑"
|
|
43
|
+
case "down": return "↓"
|
|
44
|
+
case "\r", "return", "enter": return "↩"
|
|
45
|
+
case "\t", "tab": return "⇥"
|
|
46
|
+
case " ", "space": return "Space"
|
|
47
|
+
case "\u{1b}", "escape": return "⎋"
|
|
48
|
+
case "\u{7f}", "delete": return "⌫"
|
|
49
|
+
case "[": return "["
|
|
50
|
+
case "]": return "]"
|
|
51
|
+
case "{": return "{"
|
|
52
|
+
case "}": return "}"
|
|
53
|
+
default:
|
|
54
|
+
return key.uppercased()
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// MARK: - NSEvent conversion
|
|
59
|
+
|
|
60
|
+
/// Create a StoredShortcut from an NSEvent (for recording shortcuts).
|
|
61
|
+
@MainActor
|
|
62
|
+
static func from(event: NSEvent) -> StoredShortcut? {
|
|
63
|
+
guard event.type == .keyDown else { return nil }
|
|
64
|
+
|
|
65
|
+
let mods = event.modifierFlags.intersection(.deviceIndependentFlagsMask)
|
|
66
|
+
|
|
67
|
+
// Require at least one modifier key (prevent bare letters from being shortcuts)
|
|
68
|
+
let hasModifier = mods.contains(.command) || mods.contains(.control) ||
|
|
69
|
+
mods.contains(.option)
|
|
70
|
+
guard hasModifier else { return nil }
|
|
71
|
+
|
|
72
|
+
let keyStr: String
|
|
73
|
+
switch Int(event.keyCode) {
|
|
74
|
+
case 123: keyStr = "left"
|
|
75
|
+
case 124: keyStr = "right"
|
|
76
|
+
case 125: keyStr = "down"
|
|
77
|
+
case 126: keyStr = "up"
|
|
78
|
+
case 36: keyStr = "return"
|
|
79
|
+
case 48: keyStr = "tab"
|
|
80
|
+
case 53: keyStr = "escape"
|
|
81
|
+
case 51: keyStr = "delete"
|
|
82
|
+
case 49: keyStr = "space"
|
|
83
|
+
default:
|
|
84
|
+
if let chars = event.charactersIgnoringModifiers, !chars.isEmpty {
|
|
85
|
+
keyStr = chars.lowercased()
|
|
86
|
+
} else {
|
|
87
|
+
return nil
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return StoredShortcut(
|
|
92
|
+
key: keyStr,
|
|
93
|
+
command: mods.contains(.command),
|
|
94
|
+
shift: mods.contains(.shift),
|
|
95
|
+
option: mods.contains(.option),
|
|
96
|
+
control: mods.contains(.control)
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// Check if an NSEvent matches this shortcut.
|
|
101
|
+
@MainActor
|
|
102
|
+
func matches(event: NSEvent) -> Bool {
|
|
103
|
+
guard let recorded = StoredShortcut.from(event: event) else { return false }
|
|
104
|
+
return self == recorded
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// MARK: - Persistence via UserDefaults
|
|
108
|
+
|
|
109
|
+
private static let defaultsKeyPrefix = "shortcut_"
|
|
110
|
+
|
|
111
|
+
/// Get the current shortcut for an action (user-customized or default).
|
|
112
|
+
static func shortcut(for action: ShortcutAction) -> StoredShortcut {
|
|
113
|
+
let key = defaultsKeyPrefix + action.rawValue
|
|
114
|
+
if let data = UserDefaults.standard.data(forKey: key),
|
|
115
|
+
let stored = try? JSONDecoder().decode(StoredShortcut.self, from: data) {
|
|
116
|
+
return stored
|
|
117
|
+
}
|
|
118
|
+
return action.defaultShortcut
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// Save a custom shortcut for an action.
|
|
122
|
+
static func setShortcut(_ shortcut: StoredShortcut, for action: ShortcutAction) {
|
|
123
|
+
let key = defaultsKeyPrefix + action.rawValue
|
|
124
|
+
if let data = try? JSONEncoder().encode(shortcut) {
|
|
125
|
+
UserDefaults.standard.set(data, forKey: key)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/// Reset a shortcut to its default.
|
|
130
|
+
static func resetShortcut(for action: ShortcutAction) {
|
|
131
|
+
let key = defaultsKeyPrefix + action.rawValue
|
|
132
|
+
UserDefaults.standard.removeObject(forKey: key)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// Reset all shortcuts to defaults.
|
|
136
|
+
static func resetAll() {
|
|
137
|
+
for action in ShortcutAction.allCases {
|
|
138
|
+
resetShortcut(for: action)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// Detect conflicts: returns any other actions that share the same shortcut.
|
|
143
|
+
static func conflicts(for action: ShortcutAction) -> [ShortcutAction] {
|
|
144
|
+
let current = shortcut(for: action)
|
|
145
|
+
return ShortcutAction.allCases.filter { other in
|
|
146
|
+
other != action && shortcut(for: other) == current
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import RemuxKit
|
|
3
|
+
|
|
4
|
+
/// Main content view: sidebar + tab bar + split terminal/inspect area.
|
|
5
|
+
/// Includes command palette overlay and copy mode support.
|
|
6
|
+
struct MainContentView: View {
|
|
7
|
+
@Environment(RemuxState.self) private var state
|
|
8
|
+
@State private var showInspect = false
|
|
9
|
+
|
|
10
|
+
// Split pane state
|
|
11
|
+
@State private var splitRoot: SplitNode = .leaf(SplitNode.LeafData(tabIndex: 0))
|
|
12
|
+
@State private var focusedLeafID: UUID?
|
|
13
|
+
|
|
14
|
+
// Command palette
|
|
15
|
+
@State private var showCommandPalette = false
|
|
16
|
+
|
|
17
|
+
// Copy mode
|
|
18
|
+
@State private var showCopyMode = false
|
|
19
|
+
|
|
20
|
+
// Port scanner
|
|
21
|
+
@State private var portScanner = PortScanner()
|
|
22
|
+
|
|
23
|
+
// SSH detector
|
|
24
|
+
@State private var sshDetector = SSHDetector()
|
|
25
|
+
|
|
26
|
+
var body: some View {
|
|
27
|
+
ZStack {
|
|
28
|
+
NavigationSplitView {
|
|
29
|
+
SidebarView(portScanner: portScanner)
|
|
30
|
+
} detail: {
|
|
31
|
+
if case .connected = state.connectionStatus {
|
|
32
|
+
VStack(spacing: 0) {
|
|
33
|
+
TabBarView()
|
|
34
|
+
|
|
35
|
+
ZStack {
|
|
36
|
+
if showInspect {
|
|
37
|
+
HSplitView {
|
|
38
|
+
splitContent
|
|
39
|
+
.frame(minWidth: 300)
|
|
40
|
+
InspectView()
|
|
41
|
+
.frame(minWidth: 250, idealWidth: 350)
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
splitContent
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Copy mode overlay
|
|
48
|
+
CopyModeOverlay(
|
|
49
|
+
isActive: $showCopyMode,
|
|
50
|
+
onRequestContent: {
|
|
51
|
+
// Return terminal lines from inspect snapshot
|
|
52
|
+
if let snapshot = state.inspectSnapshot {
|
|
53
|
+
return snapshot.items.map { $0.content }
|
|
54
|
+
}
|
|
55
|
+
return []
|
|
56
|
+
},
|
|
57
|
+
onCopy: { _ in
|
|
58
|
+
// Text already copied to clipboard in the overlay
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
.toolbar {
|
|
64
|
+
ToolbarItemGroup(placement: .automatic) {
|
|
65
|
+
// SSH upload button
|
|
66
|
+
if sshDetector.hasActiveConnection {
|
|
67
|
+
Button(action: { handleSSHUpload() }) {
|
|
68
|
+
Image(systemName: "square.and.arrow.up")
|
|
69
|
+
}
|
|
70
|
+
.help("Upload File via SCP")
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
Button(action: { showInspect.toggle() }) {
|
|
74
|
+
Image(systemName: showInspect ? "doc.text.fill" : "doc.text")
|
|
75
|
+
}
|
|
76
|
+
.help("Toggle Inspect (\u{2318}I)")
|
|
77
|
+
.keyboardShortcut("i", modifiers: .command)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
ConnectionView()
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
.navigationSplitViewColumnWidth(min: 180, ideal: 220, max: 300)
|
|
85
|
+
|
|
86
|
+
// Command palette overlay
|
|
87
|
+
CommandPalette(
|
|
88
|
+
isPresented: $showCommandPalette,
|
|
89
|
+
commands: buildCommandList()
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
.onAppear {
|
|
93
|
+
// Set initial focused leaf
|
|
94
|
+
if focusedLeafID == nil {
|
|
95
|
+
focusedLeafID = splitRoot.allLeaves.first?.id
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@ViewBuilder
|
|
101
|
+
private var splitContent: some View {
|
|
102
|
+
SplitView(
|
|
103
|
+
node: splitRoot,
|
|
104
|
+
focusedLeafID: $focusedLeafID,
|
|
105
|
+
onSplit: { leafID, orientation in
|
|
106
|
+
splitPane(leafID: leafID, orientation: orientation)
|
|
107
|
+
},
|
|
108
|
+
onClose: { leafID in
|
|
109
|
+
closePane(leafID: leafID)
|
|
110
|
+
},
|
|
111
|
+
onRatioChange: { branchID, ratio in
|
|
112
|
+
splitRoot = splitRoot.updateRatio(branchID: branchID, ratio: ratio)
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// MARK: - Split operations
|
|
118
|
+
|
|
119
|
+
/// Split the given (or focused) pane in the specified direction.
|
|
120
|
+
func splitPane(leafID: UUID? = nil, orientation: SplitNode.Orientation, panelType: PanelType = .terminal) {
|
|
121
|
+
let targetID = leafID ?? focusedLeafID ?? splitRoot.allLeaves.first?.id
|
|
122
|
+
guard let targetID else { return }
|
|
123
|
+
|
|
124
|
+
let newTabIndex = state.activeTabIndex
|
|
125
|
+
splitRoot = splitRoot.split(
|
|
126
|
+
leafID: targetID,
|
|
127
|
+
orientation: orientation,
|
|
128
|
+
newTabIndex: newTabIndex,
|
|
129
|
+
panelType: panelType
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
// Focus the new pane
|
|
133
|
+
if let newLeaves = splitRoot.allLeaves.last {
|
|
134
|
+
focusedLeafID = newLeaves.id
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/// Close a specific pane.
|
|
139
|
+
func closePane(leafID: UUID? = nil) {
|
|
140
|
+
let targetID = leafID ?? focusedLeafID ?? splitRoot.allLeaves.last?.id
|
|
141
|
+
guard let targetID else { return }
|
|
142
|
+
|
|
143
|
+
// Don't close the last pane
|
|
144
|
+
guard splitRoot.allLeaves.count > 1 else { return }
|
|
145
|
+
|
|
146
|
+
// Move focus before removing
|
|
147
|
+
if focusedLeafID == targetID {
|
|
148
|
+
focusedLeafID = splitRoot.previousLeaf(before: targetID)?.id
|
|
149
|
+
?? splitRoot.allLeaves.first?.id
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if let newRoot = splitRoot.removeLeaf(id: targetID) {
|
|
153
|
+
splitRoot = newRoot
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/// Focus the next pane.
|
|
158
|
+
func focusNextPane() {
|
|
159
|
+
guard let current = focusedLeafID,
|
|
160
|
+
let next = splitRoot.nextLeaf(after: current) else { return }
|
|
161
|
+
focusedLeafID = next.id
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/// Focus the previous pane.
|
|
165
|
+
func focusPreviousPane() {
|
|
166
|
+
guard let current = focusedLeafID,
|
|
167
|
+
let prev = splitRoot.previousLeaf(before: current) else { return }
|
|
168
|
+
focusedLeafID = prev.id
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/// Add a browser pane to the split tree.
|
|
172
|
+
func addBrowserPane() {
|
|
173
|
+
splitPane(orientation: .horizontal, panelType: .browser)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/// Add a markdown pane to the split tree.
|
|
177
|
+
func addMarkdownPane() {
|
|
178
|
+
splitPane(orientation: .horizontal, panelType: .markdown)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/// Toggle command palette visibility.
|
|
182
|
+
func toggleCommandPalette() {
|
|
183
|
+
showCommandPalette.toggle()
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/// Toggle copy mode.
|
|
187
|
+
func toggleCopyMode() {
|
|
188
|
+
// Request inspect content first so copy mode has data
|
|
189
|
+
if !showCopyMode {
|
|
190
|
+
state.requestInspect(tabIndex: state.activeTabIndex)
|
|
191
|
+
}
|
|
192
|
+
showCopyMode.toggle()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/// Get the current split layout as a snapshot for persistence.
|
|
196
|
+
var splitLayoutSnapshot: SplitNodeSnapshot {
|
|
197
|
+
splitRoot.toSnapshot()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/// Restore split layout from a snapshot.
|
|
201
|
+
mutating func restoreSplitLayout(_ snapshot: SplitNodeSnapshot) {
|
|
202
|
+
splitRoot = SplitNode.fromSnapshot(snapshot)
|
|
203
|
+
focusedLeafID = splitRoot.allLeaves.first?.id
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// MARK: - Command palette commands
|
|
207
|
+
|
|
208
|
+
private func buildCommandList() -> [PaletteCommand] {
|
|
209
|
+
var commands: [PaletteCommand] = []
|
|
210
|
+
|
|
211
|
+
// Commands from ShortcutAction
|
|
212
|
+
for action in ShortcutAction.allCases {
|
|
213
|
+
let shortcut = StoredShortcut.shortcut(for: action)
|
|
214
|
+
commands.append(PaletteCommand(
|
|
215
|
+
id: action.rawValue,
|
|
216
|
+
name: action.displayName,
|
|
217
|
+
shortcut: shortcut.displayString,
|
|
218
|
+
category: action.category,
|
|
219
|
+
action: { [self] in
|
|
220
|
+
executeShortcutAction(action)
|
|
221
|
+
}
|
|
222
|
+
))
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Additional commands
|
|
226
|
+
commands.append(PaletteCommand(
|
|
227
|
+
id: "newBrowserPane",
|
|
228
|
+
name: "New Browser Pane",
|
|
229
|
+
shortcut: "\u{2318}\u{21E7}B",
|
|
230
|
+
category: "Panels",
|
|
231
|
+
action: { [self] in addBrowserPane() }
|
|
232
|
+
))
|
|
233
|
+
|
|
234
|
+
commands.append(PaletteCommand(
|
|
235
|
+
id: "newMarkdownPane",
|
|
236
|
+
name: "New Markdown Pane",
|
|
237
|
+
shortcut: "\u{2318}\u{21E7}M",
|
|
238
|
+
category: "Panels",
|
|
239
|
+
action: { [self] in addMarkdownPane() }
|
|
240
|
+
))
|
|
241
|
+
|
|
242
|
+
commands.append(PaletteCommand(
|
|
243
|
+
id: "copyMode",
|
|
244
|
+
name: "Copy Mode",
|
|
245
|
+
shortcut: "\u{2318}\u{21E7}C",
|
|
246
|
+
category: "Terminal",
|
|
247
|
+
action: { [self] in toggleCopyMode() }
|
|
248
|
+
))
|
|
249
|
+
|
|
250
|
+
commands.append(PaletteCommand(
|
|
251
|
+
id: "commandPalette",
|
|
252
|
+
name: "Command Palette",
|
|
253
|
+
shortcut: "\u{2318}\u{21E7}P",
|
|
254
|
+
category: "Window",
|
|
255
|
+
action: { /* Already open */ }
|
|
256
|
+
))
|
|
257
|
+
|
|
258
|
+
return commands
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private func executeShortcutAction(_ action: ShortcutAction) {
|
|
262
|
+
switch action {
|
|
263
|
+
case .find: break // Handled by terminal view
|
|
264
|
+
case .clearTerminal: break
|
|
265
|
+
case .newTab: state.createTab()
|
|
266
|
+
case .closeTab:
|
|
267
|
+
if let tab = state.tabs.first(where: { $0.active }),
|
|
268
|
+
let pane = tab.panes.first {
|
|
269
|
+
state.closeTab(id: pane.id)
|
|
270
|
+
}
|
|
271
|
+
case .nextTab:
|
|
272
|
+
let tabs = state.tabs
|
|
273
|
+
let idx = state.activeTabIndex
|
|
274
|
+
if let nextTab = tabs.first(where: { $0.index > idx }) ?? tabs.first,
|
|
275
|
+
let pane = nextTab.panes.first {
|
|
276
|
+
state.switchTab(id: pane.id)
|
|
277
|
+
}
|
|
278
|
+
case .prevTab:
|
|
279
|
+
let tabs = state.tabs
|
|
280
|
+
let idx = state.activeTabIndex
|
|
281
|
+
if let prevTab = tabs.last(where: { $0.index < idx }) ?? tabs.last,
|
|
282
|
+
let pane = prevTab.panes.first {
|
|
283
|
+
state.switchTab(id: pane.id)
|
|
284
|
+
}
|
|
285
|
+
case .splitRight: splitPane(orientation: .horizontal)
|
|
286
|
+
case .splitDown: splitPane(orientation: .vertical)
|
|
287
|
+
case .closePane: closePane()
|
|
288
|
+
case .focusNextPane: focusNextPane()
|
|
289
|
+
case .focusPrevPane: focusPreviousPane()
|
|
290
|
+
case .toggleSidebar: break // Handled by NSSplitViewController
|
|
291
|
+
case .toggleInspect: showInspect.toggle()
|
|
292
|
+
case .toggleFullscreen:
|
|
293
|
+
NSApp.keyWindow?.toggleFullScreen(nil)
|
|
294
|
+
case .focusLeft, .focusRight, .focusUp, .focusDown:
|
|
295
|
+
break // Directional focus — would need spatial awareness
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// MARK: - SSH upload
|
|
300
|
+
|
|
301
|
+
private func handleSSHUpload() {
|
|
302
|
+
guard let url = sshDetector.pickFileForUpload(),
|
|
303
|
+
let cmd = sshDetector.buildUploadCommand(localPath: url.path) else {
|
|
304
|
+
return
|
|
305
|
+
}
|
|
306
|
+
state.sendTerminalInput(cmd + "\n")
|
|
307
|
+
}
|
|
308
|
+
}
|