@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,275 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import RemuxKit
|
|
3
|
+
|
|
4
|
+
/// Manages the native macOS menu bar.
|
|
5
|
+
@MainActor
|
|
6
|
+
final class MenuBarManager {
|
|
7
|
+
private weak var state: RemuxState?
|
|
8
|
+
|
|
9
|
+
/// Callback for split operations, wired from AppDelegate.
|
|
10
|
+
var onSplitRight: (() -> Void)?
|
|
11
|
+
var onSplitDown: (() -> Void)?
|
|
12
|
+
var onClosePane: (() -> Void)?
|
|
13
|
+
var onFocusNextPane: (() -> Void)?
|
|
14
|
+
var onFocusPreviousPane: (() -> Void)?
|
|
15
|
+
|
|
16
|
+
/// Callbacks for new features.
|
|
17
|
+
var onNewBrowserPane: (() -> Void)?
|
|
18
|
+
var onNewMarkdownPane: (() -> Void)?
|
|
19
|
+
var onCommandPalette: (() -> Void)?
|
|
20
|
+
var onCopyMode: (() -> Void)?
|
|
21
|
+
var onDetachPane: (() -> Void)?
|
|
22
|
+
|
|
23
|
+
init(state: RemuxState) {
|
|
24
|
+
self.state = state
|
|
25
|
+
setupMenuBar()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private func setupMenuBar() {
|
|
29
|
+
let mainMenu = NSMenu()
|
|
30
|
+
|
|
31
|
+
// App menu
|
|
32
|
+
let appMenu = NSMenu()
|
|
33
|
+
appMenu.addItem(withTitle: "About Remux", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: "")
|
|
34
|
+
appMenu.addItem(.separator())
|
|
35
|
+
appMenu.addItem(withTitle: "Settings...", action: #selector(showSettings), keyEquivalent: ",")
|
|
36
|
+
appMenu.addItem(.separator())
|
|
37
|
+
appMenu.addItem(withTitle: "Hide Remux", action: #selector(NSApplication.hide(_:)), keyEquivalent: "h")
|
|
38
|
+
let hideOthers = appMenu.addItem(withTitle: "Hide Others", action: #selector(NSApplication.hideOtherApplications(_:)), keyEquivalent: "h")
|
|
39
|
+
hideOthers.keyEquivalentModifierMask = [.command, .option]
|
|
40
|
+
appMenu.addItem(withTitle: "Show All", action: #selector(NSApplication.unhideAllApplications(_:)), keyEquivalent: "")
|
|
41
|
+
appMenu.addItem(.separator())
|
|
42
|
+
appMenu.addItem(withTitle: "Quit Remux", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
|
|
43
|
+
let appMenuItem = NSMenuItem()
|
|
44
|
+
appMenuItem.submenu = appMenu
|
|
45
|
+
mainMenu.addItem(appMenuItem)
|
|
46
|
+
|
|
47
|
+
// File menu
|
|
48
|
+
let fileMenu = NSMenu(title: "File")
|
|
49
|
+
let newTab = fileMenu.addItem(withTitle: "New Tab", action: #selector(newTab(_:)), keyEquivalent: "t")
|
|
50
|
+
newTab.target = self
|
|
51
|
+
let closeTab = fileMenu.addItem(withTitle: "Close Tab", action: #selector(closeCurrentTab(_:)), keyEquivalent: "w")
|
|
52
|
+
closeTab.target = self
|
|
53
|
+
fileMenu.addItem(.separator())
|
|
54
|
+
let newWindow = fileMenu.addItem(withTitle: "New Window", action: #selector(newWindow(_:)), keyEquivalent: "n")
|
|
55
|
+
newWindow.target = self
|
|
56
|
+
fileMenu.addItem(.separator())
|
|
57
|
+
let newSession = fileMenu.addItem(withTitle: "New Session", action: #selector(newSession(_:)), keyEquivalent: "n")
|
|
58
|
+
newSession.keyEquivalentModifierMask = [.command, .shift]
|
|
59
|
+
newSession.target = self
|
|
60
|
+
|
|
61
|
+
fileMenu.addItem(.separator())
|
|
62
|
+
|
|
63
|
+
// Open in... submenu
|
|
64
|
+
let openInMenu = NSMenu(title: "Open in...")
|
|
65
|
+
for editor in FinderIntegration.installedEditors {
|
|
66
|
+
let item = openInMenu.addItem(withTitle: editor.name, action: #selector(openInEditor(_:)), keyEquivalent: "")
|
|
67
|
+
item.target = self
|
|
68
|
+
item.representedObject = editor
|
|
69
|
+
item.image = NSImage(systemSymbolName: editor.icon, accessibilityDescription: editor.name)
|
|
70
|
+
}
|
|
71
|
+
if openInMenu.items.isEmpty {
|
|
72
|
+
openInMenu.addItem(withTitle: "No Editors Found", action: nil, keyEquivalent: "")
|
|
73
|
+
}
|
|
74
|
+
let openInMenuItem = NSMenuItem(title: "Open in...", action: nil, keyEquivalent: "")
|
|
75
|
+
openInMenuItem.submenu = openInMenu
|
|
76
|
+
fileMenu.addItem(openInMenuItem)
|
|
77
|
+
|
|
78
|
+
let fileMenuItem = NSMenuItem()
|
|
79
|
+
fileMenuItem.submenu = fileMenu
|
|
80
|
+
mainMenu.addItem(fileMenuItem)
|
|
81
|
+
|
|
82
|
+
// Edit menu
|
|
83
|
+
let editMenu = NSMenu(title: "Edit")
|
|
84
|
+
editMenu.addItem(withTitle: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c")
|
|
85
|
+
editMenu.addItem(withTitle: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v")
|
|
86
|
+
editMenu.addItem(withTitle: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a")
|
|
87
|
+
editMenu.addItem(.separator())
|
|
88
|
+
let findItem = editMenu.addItem(withTitle: "Find...", action: #selector(findInTerminal(_:)), keyEquivalent: "f")
|
|
89
|
+
findItem.target = self
|
|
90
|
+
editMenu.addItem(.separator())
|
|
91
|
+
let copyModeItem = editMenu.addItem(withTitle: "Copy Mode", action: #selector(copyModeAction(_:)), keyEquivalent: "c")
|
|
92
|
+
copyModeItem.keyEquivalentModifierMask = [.command, .shift]
|
|
93
|
+
copyModeItem.target = self
|
|
94
|
+
let editMenuItem = NSMenuItem()
|
|
95
|
+
editMenuItem.submenu = editMenu
|
|
96
|
+
mainMenu.addItem(editMenuItem)
|
|
97
|
+
|
|
98
|
+
// View menu
|
|
99
|
+
let viewMenu = NSMenu(title: "View")
|
|
100
|
+
let toggleSidebar = viewMenu.addItem(withTitle: "Toggle Sidebar", action: #selector(toggleSidebar(_:)), keyEquivalent: "s")
|
|
101
|
+
toggleSidebar.keyEquivalentModifierMask = [.command, .control]
|
|
102
|
+
toggleSidebar.target = self
|
|
103
|
+
|
|
104
|
+
viewMenu.addItem(.separator())
|
|
105
|
+
|
|
106
|
+
// Command Palette
|
|
107
|
+
let cmdPalette = viewMenu.addItem(withTitle: "Command Palette", action: #selector(commandPaletteAction(_:)), keyEquivalent: "p")
|
|
108
|
+
cmdPalette.keyEquivalentModifierMask = [.command, .shift]
|
|
109
|
+
cmdPalette.target = self
|
|
110
|
+
|
|
111
|
+
viewMenu.addItem(.separator())
|
|
112
|
+
|
|
113
|
+
// Split pane items
|
|
114
|
+
let splitRight = viewMenu.addItem(withTitle: "Split Right", action: #selector(splitRightAction(_:)), keyEquivalent: "d")
|
|
115
|
+
splitRight.target = self
|
|
116
|
+
|
|
117
|
+
let splitDown = viewMenu.addItem(withTitle: "Split Down", action: #selector(splitDownAction(_:)), keyEquivalent: "d")
|
|
118
|
+
splitDown.keyEquivalentModifierMask = [.command, .shift]
|
|
119
|
+
splitDown.target = self
|
|
120
|
+
|
|
121
|
+
viewMenu.addItem(.separator())
|
|
122
|
+
|
|
123
|
+
// New panel types
|
|
124
|
+
let browserPane = viewMenu.addItem(withTitle: "New Browser Pane", action: #selector(newBrowserPaneAction(_:)), keyEquivalent: "b")
|
|
125
|
+
browserPane.keyEquivalentModifierMask = [.command, .shift]
|
|
126
|
+
browserPane.target = self
|
|
127
|
+
|
|
128
|
+
let markdownPane = viewMenu.addItem(withTitle: "New Markdown Pane", action: #selector(newMarkdownPaneAction(_:)), keyEquivalent: "m")
|
|
129
|
+
markdownPane.keyEquivalentModifierMask = [.command, .shift]
|
|
130
|
+
markdownPane.target = self
|
|
131
|
+
|
|
132
|
+
viewMenu.addItem(.separator())
|
|
133
|
+
|
|
134
|
+
let closePane = viewMenu.addItem(withTitle: "Close Pane", action: #selector(closePaneAction(_:)), keyEquivalent: "w")
|
|
135
|
+
closePane.keyEquivalentModifierMask = [.command, .shift]
|
|
136
|
+
closePane.target = self
|
|
137
|
+
|
|
138
|
+
viewMenu.addItem(.separator())
|
|
139
|
+
|
|
140
|
+
// Focus navigation
|
|
141
|
+
let focusNext = viewMenu.addItem(withTitle: "Focus Next Pane", action: #selector(focusNextAction(_:)), keyEquivalent: "]")
|
|
142
|
+
focusNext.keyEquivalentModifierMask = [.command, .option]
|
|
143
|
+
focusNext.target = self
|
|
144
|
+
|
|
145
|
+
let focusPrev = viewMenu.addItem(withTitle: "Focus Previous Pane", action: #selector(focusPrevAction(_:)), keyEquivalent: "[")
|
|
146
|
+
focusPrev.keyEquivalentModifierMask = [.command, .option]
|
|
147
|
+
focusPrev.target = self
|
|
148
|
+
|
|
149
|
+
let viewMenuItem = NSMenuItem()
|
|
150
|
+
viewMenuItem.submenu = viewMenu
|
|
151
|
+
mainMenu.addItem(viewMenuItem)
|
|
152
|
+
|
|
153
|
+
// Window menu
|
|
154
|
+
let windowMenu = NSMenu(title: "Window")
|
|
155
|
+
windowMenu.addItem(withTitle: "Minimize", action: #selector(NSWindow.performMiniaturize(_:)), keyEquivalent: "m")
|
|
156
|
+
windowMenu.addItem(withTitle: "Zoom", action: #selector(NSWindow.performZoom(_:)), keyEquivalent: "")
|
|
157
|
+
windowMenu.addItem(.separator())
|
|
158
|
+
|
|
159
|
+
let detachPane = windowMenu.addItem(withTitle: "Detach Pane to Window", action: #selector(detachPaneAction(_:)), keyEquivalent: "\r")
|
|
160
|
+
detachPane.keyEquivalentModifierMask = [.command, .shift]
|
|
161
|
+
detachPane.target = self
|
|
162
|
+
|
|
163
|
+
windowMenu.addItem(.separator())
|
|
164
|
+
windowMenu.addItem(withTitle: "Bring All to Front", action: #selector(NSApplication.arrangeInFront(_:)), keyEquivalent: "")
|
|
165
|
+
let windowMenuItem = NSMenuItem()
|
|
166
|
+
windowMenuItem.submenu = windowMenu
|
|
167
|
+
mainMenu.addItem(windowMenuItem)
|
|
168
|
+
NSApp.windowsMenu = windowMenu
|
|
169
|
+
|
|
170
|
+
// Help menu
|
|
171
|
+
let helpMenu = NSMenu(title: "Help")
|
|
172
|
+
let helpMenuItem = NSMenuItem()
|
|
173
|
+
helpMenuItem.submenu = helpMenu
|
|
174
|
+
mainMenu.addItem(helpMenuItem)
|
|
175
|
+
NSApp.helpMenu = helpMenu
|
|
176
|
+
|
|
177
|
+
NSApp.mainMenu = mainMenu
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@objc private func showSettings(_ sender: Any?) {
|
|
181
|
+
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
@objc private func newWindow(_ sender: Any?) {
|
|
185
|
+
if let appDelegate = NSApp.delegate as? AppDelegate {
|
|
186
|
+
appDelegate.createNewWindow()
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
@objc private func newTab(_ sender: Any?) {
|
|
191
|
+
state?.createTab()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
@objc private func closeCurrentTab(_ sender: Any?) {
|
|
195
|
+
guard let state, !state.tabs.isEmpty else { return }
|
|
196
|
+
let activeTab = state.tabs.first { $0.index == state.activeTabIndex }
|
|
197
|
+
if let pane = activeTab?.panes.first {
|
|
198
|
+
state.closeTab(id: pane.id)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
@objc private func newSession(_ sender: Any?) {
|
|
203
|
+
let name = "session-\(Int.random(in: 1000...9999))"
|
|
204
|
+
state?.createSession(name: name)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
@objc private func toggleSidebar(_ sender: Any?) {
|
|
208
|
+
NSApp.keyWindow?.contentView?.window?.firstResponder?.tryToPerform(
|
|
209
|
+
#selector(NSSplitViewController.toggleSidebar(_:)), with: nil
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
@objc private func findInTerminal(_ sender: Any?) {
|
|
214
|
+
// Search is triggered through the GhosttyNativeView's performKeyEquivalent
|
|
215
|
+
// which intercepts Cmd+F. The menu item provides discoverability.
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// MARK: - Split pane actions
|
|
219
|
+
|
|
220
|
+
@objc private func splitRightAction(_ sender: Any?) {
|
|
221
|
+
onSplitRight?()
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
@objc private func splitDownAction(_ sender: Any?) {
|
|
225
|
+
onSplitDown?()
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
@objc private func closePaneAction(_ sender: Any?) {
|
|
229
|
+
onClosePane?()
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
@objc private func focusNextAction(_ sender: Any?) {
|
|
233
|
+
onFocusNextPane?()
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
@objc private func focusPrevAction(_ sender: Any?) {
|
|
237
|
+
onFocusPreviousPane?()
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// MARK: - New panel actions
|
|
241
|
+
|
|
242
|
+
@objc private func newBrowserPaneAction(_ sender: Any?) {
|
|
243
|
+
onNewBrowserPane?()
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
@objc private func newMarkdownPaneAction(_ sender: Any?) {
|
|
247
|
+
onNewMarkdownPane?()
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
@objc private func commandPaletteAction(_ sender: Any?) {
|
|
251
|
+
onCommandPalette?()
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
@objc private func copyModeAction(_ sender: Any?) {
|
|
255
|
+
onCopyMode?()
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
@objc private func detachPaneAction(_ sender: Any?) {
|
|
259
|
+
onDetachPane?()
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
@objc private func openInEditor(_ sender: Any?) {
|
|
263
|
+
guard let item = sender as? NSMenuItem,
|
|
264
|
+
let editor = item.representedObject as? FinderIntegration.ExternalEditor else { return }
|
|
265
|
+
|
|
266
|
+
// Get CWD from the active tab
|
|
267
|
+
guard let state,
|
|
268
|
+
let tab = state.tabs.first(where: { $0.active }),
|
|
269
|
+
let cwd = tab.panes.first?.cwd, !cwd.isEmpty else {
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
FinderIntegration.openInExternalEditor(path: cwd, editor: editor)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import UserNotifications
|
|
3
|
+
|
|
4
|
+
/// Manages terminal notifications with three-level escalation.
|
|
5
|
+
/// Ref: cmux TerminalNotificationStore.swift (design pattern, not code)
|
|
6
|
+
///
|
|
7
|
+
/// Level 1: Tab badge (red dot + count) — always
|
|
8
|
+
/// Level 2: Sidebar session highlight — always
|
|
9
|
+
/// Level 3: System notification — only when window not focused
|
|
10
|
+
@MainActor
|
|
11
|
+
final class NotificationManager: NSObject {
|
|
12
|
+
|
|
13
|
+
struct TerminalNotification {
|
|
14
|
+
let title: String
|
|
15
|
+
let body: String
|
|
16
|
+
let tabIndex: Int
|
|
17
|
+
let sessionName: String
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// Notification counts per tab index
|
|
21
|
+
private(set) var badgeCounts: [Int: Int] = [:]
|
|
22
|
+
|
|
23
|
+
/// Rate limit: max 1 system notification per 30 seconds
|
|
24
|
+
private var lastSystemNotification = Date.distantPast
|
|
25
|
+
private let systemNotificationCooldown: TimeInterval = 30
|
|
26
|
+
|
|
27
|
+
/// Whether UNUserNotificationCenter is available (requires app bundle context)
|
|
28
|
+
private let notificationsAvailable: Bool
|
|
29
|
+
|
|
30
|
+
override init() {
|
|
31
|
+
// UNUserNotificationCenter.current() crashes if not running inside an app bundle
|
|
32
|
+
notificationsAvailable = Bundle.main.bundleIdentifier != nil
|
|
33
|
+
super.init()
|
|
34
|
+
guard notificationsAvailable else { return }
|
|
35
|
+
UNUserNotificationCenter.current().delegate = self
|
|
36
|
+
requestPermission()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private func requestPermission() {
|
|
40
|
+
guard notificationsAvailable else { return }
|
|
41
|
+
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { _, _ in }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
func handleNotification(_ notification: TerminalNotification) {
|
|
45
|
+
// Level 1: Increment badge count
|
|
46
|
+
badgeCounts[notification.tabIndex, default: 0] += 1
|
|
47
|
+
|
|
48
|
+
// Level 3: System notification if window not focused
|
|
49
|
+
let windowFocused = NSApp.isActive && NSApp.keyWindow != nil
|
|
50
|
+
if !windowFocused {
|
|
51
|
+
sendSystemNotification(notification)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func clearBadge(forTab tabIndex: Int) {
|
|
56
|
+
badgeCounts[tabIndex] = nil
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func clearAllBadges() {
|
|
60
|
+
badgeCounts.removeAll()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private func sendSystemNotification(_ notification: TerminalNotification) {
|
|
64
|
+
guard notificationsAvailable else { return }
|
|
65
|
+
let now = Date()
|
|
66
|
+
guard now.timeIntervalSince(lastSystemNotification) > systemNotificationCooldown else { return }
|
|
67
|
+
lastSystemNotification = now
|
|
68
|
+
|
|
69
|
+
let content = UNMutableNotificationContent()
|
|
70
|
+
content.title = notification.title
|
|
71
|
+
content.body = notification.body
|
|
72
|
+
content.sound = .default
|
|
73
|
+
content.userInfo = [
|
|
74
|
+
"tabIndex": notification.tabIndex,
|
|
75
|
+
"sessionName": notification.sessionName,
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
let request = UNNotificationRequest(
|
|
79
|
+
identifier: UUID().uuidString,
|
|
80
|
+
content: content,
|
|
81
|
+
trigger: nil
|
|
82
|
+
)
|
|
83
|
+
UNUserNotificationCenter.current().add(request)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
extension NotificationManager: @preconcurrency UNUserNotificationCenterDelegate {
|
|
88
|
+
nonisolated func userNotificationCenter(
|
|
89
|
+
_ center: UNUserNotificationCenter,
|
|
90
|
+
didReceive response: UNNotificationResponse,
|
|
91
|
+
withCompletionHandler completionHandler: @escaping () -> Void
|
|
92
|
+
) {
|
|
93
|
+
DispatchQueue.main.async {
|
|
94
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
95
|
+
NSApp.keyWindow?.makeKeyAndOrderFront(nil)
|
|
96
|
+
}
|
|
97
|
+
completionHandler()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
nonisolated func userNotificationCenter(
|
|
101
|
+
_ center: UNUserNotificationCenter,
|
|
102
|
+
willPresent notification: UNNotification,
|
|
103
|
+
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
|
|
104
|
+
) {
|
|
105
|
+
let options: UNNotificationPresentationOptions = NSApp.isActive ? [] : [.banner, .sound]
|
|
106
|
+
completionHandler(options)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// MARK: - OSC Notification Parser
|
|
111
|
+
|
|
112
|
+
/// Parses OSC 9/99/777 notification sequences from PTY data.
|
|
113
|
+
/// Ref: cmux OSC notification detection approach
|
|
114
|
+
struct OSCNotificationParser {
|
|
115
|
+
/// Parse PTY data for notification sequences.
|
|
116
|
+
/// Returns extracted notifications (if any).
|
|
117
|
+
static func parse(_ data: Data) -> [String] {
|
|
118
|
+
guard let text = String(data: data, encoding: .utf8) else { return [] }
|
|
119
|
+
var notifications: [String] = []
|
|
120
|
+
|
|
121
|
+
// OSC 9: iTerm2 notification — ESC ] 9 ; <message> BEL/ST
|
|
122
|
+
let osc9Pattern = "\u{1b}]9;([^\u{07}\u{1b}]+)[\u{07}]"
|
|
123
|
+
if let regex = try? NSRegularExpression(pattern: osc9Pattern) {
|
|
124
|
+
let matches = regex.matches(in: text, range: NSRange(text.startIndex..., in: text))
|
|
125
|
+
for match in matches {
|
|
126
|
+
if let range = Range(match.range(at: 1), in: text) {
|
|
127
|
+
notifications.append(String(text[range]))
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// OSC 777: rxvt-unicode notification — ESC ] 777 ; notify ; <title> ; <body> BEL
|
|
133
|
+
let osc777Pattern = "\u{1b}]777;notify;([^;]*);([^\u{07}\u{1b}]*)[\u{07}]"
|
|
134
|
+
if let regex = try? NSRegularExpression(pattern: osc777Pattern) {
|
|
135
|
+
let matches = regex.matches(in: text, range: NSRange(text.startIndex..., in: text))
|
|
136
|
+
for match in matches {
|
|
137
|
+
if let range = Range(match.range(at: 2), in: text) {
|
|
138
|
+
notifications.append(String(text[range]))
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return notifications
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import RemuxKit
|
|
3
|
+
|
|
4
|
+
/// Detects listening ports from server-reported process info.
|
|
5
|
+
/// Displays detected ports in the sidebar and supports opening
|
|
6
|
+
/// in the browser panel (localhost:port).
|
|
7
|
+
///
|
|
8
|
+
/// Adapted from VS Code port forwarding / detection feature.
|
|
9
|
+
@MainActor
|
|
10
|
+
@Observable
|
|
11
|
+
final class PortScanner {
|
|
12
|
+
|
|
13
|
+
/// A detected listening port.
|
|
14
|
+
struct DetectedPort: Identifiable, Equatable, Hashable, Sendable {
|
|
15
|
+
var id: Int { port }
|
|
16
|
+
let port: Int
|
|
17
|
+
let processName: String
|
|
18
|
+
let pid: Int?
|
|
19
|
+
let detectedAt: Date
|
|
20
|
+
|
|
21
|
+
var localURL: URL? {
|
|
22
|
+
URL(string: "http://localhost:\(port)")
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// Currently detected ports.
|
|
27
|
+
private(set) var ports: [DetectedPort] = []
|
|
28
|
+
|
|
29
|
+
/// Known port patterns (for naming).
|
|
30
|
+
private static let knownPorts: [Int: String] = [
|
|
31
|
+
3000: "Dev Server",
|
|
32
|
+
3001: "Dev Server",
|
|
33
|
+
4200: "Angular",
|
|
34
|
+
5000: "Flask/ASP.NET",
|
|
35
|
+
5173: "Vite",
|
|
36
|
+
5174: "Vite",
|
|
37
|
+
8000: "Django/FastAPI",
|
|
38
|
+
8080: "HTTP Alt",
|
|
39
|
+
8443: "HTTPS Alt",
|
|
40
|
+
8767: "Remux",
|
|
41
|
+
8888: "Jupyter",
|
|
42
|
+
9000: "PHP-FPM",
|
|
43
|
+
9090: "Prometheus",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
/// Parse terminal output text for port listening patterns.
|
|
47
|
+
/// Common patterns:
|
|
48
|
+
/// "listening on port 3000"
|
|
49
|
+
/// "running on http://localhost:8080"
|
|
50
|
+
/// "server started at :5173"
|
|
51
|
+
/// "Local: http://localhost:5173/"
|
|
52
|
+
func parseTerminalOutput(_ text: String) {
|
|
53
|
+
let patterns = [
|
|
54
|
+
// "listening on port NNNN" or "listening at port NNNN"
|
|
55
|
+
"(?:listening|started|running|serving)\\s+(?:on|at)\\s+(?:port\\s+)?(\\d{2,5})",
|
|
56
|
+
// "http://localhost:NNNN" or "http://127.0.0.1:NNNN"
|
|
57
|
+
"https?://(?:localhost|127\\.0\\.0\\.1):(\\d{2,5})",
|
|
58
|
+
// ":NNNN" at word boundary (e.g., "server at :3000")
|
|
59
|
+
"(?:at|on)\\s+:(\\d{2,5})",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
for pattern in patterns {
|
|
63
|
+
guard let regex = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) else {
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
let matches = regex.matches(in: text, range: NSRange(text.startIndex..., in: text))
|
|
67
|
+
for match in matches {
|
|
68
|
+
if let range = Range(match.range(at: 1), in: text),
|
|
69
|
+
let port = Int(text[range]),
|
|
70
|
+
port >= 1024, port <= 65535 {
|
|
71
|
+
addPort(port)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// Add a detected port if not already tracked.
|
|
78
|
+
func addPort(_ port: Int, processName: String? = nil, pid: Int? = nil) {
|
|
79
|
+
guard !ports.contains(where: { $0.port == port }) else { return }
|
|
80
|
+
|
|
81
|
+
let name = processName ?? Self.knownPorts[port] ?? "Port \(port)"
|
|
82
|
+
let detected = DetectedPort(
|
|
83
|
+
port: port,
|
|
84
|
+
processName: name,
|
|
85
|
+
pid: pid,
|
|
86
|
+
detectedAt: Date()
|
|
87
|
+
)
|
|
88
|
+
ports.append(detected)
|
|
89
|
+
ports.sort { $0.port < $1.port }
|
|
90
|
+
NSLog("[remux] Port detected: %d (%@)", port, name)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// Remove a port from the tracked list.
|
|
94
|
+
func removePort(_ port: Int) {
|
|
95
|
+
ports.removeAll { $0.port == port }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// Clear all detected ports.
|
|
99
|
+
func clearAll() {
|
|
100
|
+
ports.removeAll()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// Check if a port is reachable by attempting a TCP connection.
|
|
104
|
+
func isPortReachable(_ port: Int, timeout: TimeInterval = 1.0) -> Bool {
|
|
105
|
+
let fd = socket(AF_INET, SOCK_STREAM, 0)
|
|
106
|
+
guard fd >= 0 else { return false }
|
|
107
|
+
defer { close(fd) }
|
|
108
|
+
|
|
109
|
+
// Set non-blocking
|
|
110
|
+
var flags = fcntl(fd, F_GETFL, 0)
|
|
111
|
+
flags |= O_NONBLOCK
|
|
112
|
+
fcntl(fd, F_SETFL, flags)
|
|
113
|
+
|
|
114
|
+
var addr = sockaddr_in()
|
|
115
|
+
addr.sin_family = sa_family_t(AF_INET)
|
|
116
|
+
addr.sin_port = UInt16(port).bigEndian
|
|
117
|
+
addr.sin_addr.s_addr = inet_addr("127.0.0.1")
|
|
118
|
+
|
|
119
|
+
let result = withUnsafePointer(to: &addr) { ptr in
|
|
120
|
+
ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sockPtr in
|
|
121
|
+
connect(fd, sockPtr, socklen_t(MemoryLayout<sockaddr_in>.size))
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if result == 0 { return true }
|
|
126
|
+
if errno != EINPROGRESS { return false }
|
|
127
|
+
|
|
128
|
+
// Wait for connection with timeout
|
|
129
|
+
var fdSet = fd_set()
|
|
130
|
+
__darwin_fd_zero(&fdSet)
|
|
131
|
+
withUnsafeMutablePointer(to: &fdSet) { ptr in
|
|
132
|
+
let rawPtr = UnsafeMutableRawPointer(ptr)
|
|
133
|
+
let offset = Int(fd / 32)
|
|
134
|
+
let bit = Int(fd % 32)
|
|
135
|
+
rawPtr.advanced(by: offset * MemoryLayout<Int32>.size)
|
|
136
|
+
.assumingMemoryBound(to: Int32.self)
|
|
137
|
+
.pointee |= Int32(1 << bit)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
var tv = timeval(tv_sec: Int(timeout), tv_usec: 0)
|
|
141
|
+
let selectResult = select(fd + 1, nil, &fdSet, nil, &tv)
|
|
142
|
+
return selectResult > 0
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// MARK: - fd_set helpers
|
|
147
|
+
|
|
148
|
+
private func __darwin_fd_zero(_ set: inout fd_set) {
|
|
149
|
+
withUnsafeMutableBytes(of: &set) { rawBuf in
|
|
150
|
+
rawBuf.initializeMemory(as: UInt8.self, repeating: 0)
|
|
151
|
+
}
|
|
152
|
+
}
|