@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
package/tui/ui/model.go
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
// Package tui implements the terminal user interface using bubbletea.
|
|
2
|
+
package tui
|
|
3
|
+
|
|
4
|
+
import (
|
|
5
|
+
"fmt"
|
|
6
|
+
"strings"
|
|
7
|
+
|
|
8
|
+
tea "github.com/charmbracelet/bubbletea"
|
|
9
|
+
"github.com/charmbracelet/lipgloss"
|
|
10
|
+
"github.com/eisber/remux/tui/client"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
// View mode for the TUI.
|
|
14
|
+
type viewMode int
|
|
15
|
+
|
|
16
|
+
const (
|
|
17
|
+
viewTerminal viewMode = iota
|
|
18
|
+
viewSessionPicker
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
// Model is the bubbletea model for the remux TUI.
|
|
22
|
+
type Model struct {
|
|
23
|
+
manager *client.HostManager
|
|
24
|
+
activeHost string
|
|
25
|
+
activeSession string
|
|
26
|
+
mode viewMode
|
|
27
|
+
width, height int
|
|
28
|
+
|
|
29
|
+
// Terminal output buffer (raw bytes from the remote PTY).
|
|
30
|
+
terminalOutput strings.Builder
|
|
31
|
+
|
|
32
|
+
// Session picker state.
|
|
33
|
+
pickerCursor int
|
|
34
|
+
|
|
35
|
+
// Status messages.
|
|
36
|
+
statusMsg string
|
|
37
|
+
errMsg string
|
|
38
|
+
|
|
39
|
+
// Whether we've received terminal data.
|
|
40
|
+
connected bool
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// terminalDataMsg is sent when terminal output arrives from the server.
|
|
44
|
+
type terminalDataMsg struct {
|
|
45
|
+
data []byte
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// stateUpdateMsg is sent when the session list changes.
|
|
49
|
+
type stateUpdateMsg struct{}
|
|
50
|
+
|
|
51
|
+
// attachedMsg is sent when we've attached to a session.
|
|
52
|
+
type attachedMsg struct {
|
|
53
|
+
session string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// errMsg is sent on connection errors.
|
|
57
|
+
type connectionErrMsg struct {
|
|
58
|
+
err error
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// NewModel creates a new TUI model.
|
|
62
|
+
func NewModel(manager *client.HostManager) Model {
|
|
63
|
+
return Model{
|
|
64
|
+
manager: manager,
|
|
65
|
+
mode: viewTerminal,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Init implements tea.Model.
|
|
70
|
+
func (m Model) Init() tea.Cmd {
|
|
71
|
+
return tea.SetWindowTitle("remux")
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Update implements tea.Model.
|
|
75
|
+
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
76
|
+
switch msg := msg.(type) {
|
|
77
|
+
case tea.KeyMsg:
|
|
78
|
+
return m.handleKey(msg)
|
|
79
|
+
|
|
80
|
+
case tea.WindowSizeMsg:
|
|
81
|
+
m.width = msg.Width
|
|
82
|
+
m.height = msg.Height
|
|
83
|
+
// Resize the active terminal session.
|
|
84
|
+
if m.activeHost != "" {
|
|
85
|
+
conn := m.manager.GetConnection(m.activeHost)
|
|
86
|
+
if conn != nil {
|
|
87
|
+
_ = conn.SendResize(msg.Width, msg.Height-2) // Reserve 2 lines for status bar
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return m, nil
|
|
91
|
+
|
|
92
|
+
case terminalDataMsg:
|
|
93
|
+
m.terminalOutput.Write(msg.data)
|
|
94
|
+
m.connected = true
|
|
95
|
+
return m, nil
|
|
96
|
+
|
|
97
|
+
case stateUpdateMsg:
|
|
98
|
+
return m, nil
|
|
99
|
+
|
|
100
|
+
case attachedMsg:
|
|
101
|
+
m.activeSession = msg.session
|
|
102
|
+
m.statusMsg = fmt.Sprintf("attached to %s", msg.session)
|
|
103
|
+
m.mode = viewTerminal
|
|
104
|
+
return m, nil
|
|
105
|
+
|
|
106
|
+
case connectionErrMsg:
|
|
107
|
+
m.errMsg = msg.err.Error()
|
|
108
|
+
return m, nil
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return m, nil
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
func (m Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|
115
|
+
switch m.mode {
|
|
116
|
+
case viewTerminal:
|
|
117
|
+
return m.handleTerminalKey(msg)
|
|
118
|
+
case viewSessionPicker:
|
|
119
|
+
return m.handlePickerKey(msg)
|
|
120
|
+
}
|
|
121
|
+
return m, nil
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
func (m Model) handleTerminalKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|
125
|
+
switch msg.String() {
|
|
126
|
+
case "ctrl+o":
|
|
127
|
+
// Open session picker.
|
|
128
|
+
m.mode = viewSessionPicker
|
|
129
|
+
m.pickerCursor = 0
|
|
130
|
+
return m, nil
|
|
131
|
+
|
|
132
|
+
case "ctrl+d":
|
|
133
|
+
// Detach — quit the TUI (sessions keep running).
|
|
134
|
+
return m, tea.Quit
|
|
135
|
+
|
|
136
|
+
case "ctrl+c":
|
|
137
|
+
return m, tea.Quit
|
|
138
|
+
|
|
139
|
+
default:
|
|
140
|
+
// Forward all other input to the active terminal.
|
|
141
|
+
if m.activeHost != "" {
|
|
142
|
+
conn := m.manager.GetConnection(m.activeHost)
|
|
143
|
+
if conn != nil {
|
|
144
|
+
_ = conn.SendInput(msg.String())
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return m, nil
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
func (m Model) handlePickerKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|
152
|
+
sessions := m.manager.Sessions()
|
|
153
|
+
|
|
154
|
+
switch msg.String() {
|
|
155
|
+
case "up", "k":
|
|
156
|
+
if m.pickerCursor > 0 {
|
|
157
|
+
m.pickerCursor--
|
|
158
|
+
}
|
|
159
|
+
case "down", "j":
|
|
160
|
+
if m.pickerCursor < len(sessions)-1 {
|
|
161
|
+
m.pickerCursor++
|
|
162
|
+
}
|
|
163
|
+
case "enter":
|
|
164
|
+
if m.pickerCursor < len(sessions) {
|
|
165
|
+
selected := sessions[m.pickerCursor]
|
|
166
|
+
m.activeHost = selected.HostName
|
|
167
|
+
conn := m.manager.GetConnection(selected.HostName)
|
|
168
|
+
if conn != nil {
|
|
169
|
+
_ = conn.SelectSession(selected.Name)
|
|
170
|
+
}
|
|
171
|
+
m.mode = viewTerminal
|
|
172
|
+
m.terminalOutput.Reset()
|
|
173
|
+
m.statusMsg = fmt.Sprintf("switching to %s/%s...", selected.HostName, selected.Name)
|
|
174
|
+
}
|
|
175
|
+
case "escape", "ctrl+o":
|
|
176
|
+
m.mode = viewTerminal
|
|
177
|
+
case "ctrl+c", "ctrl+d":
|
|
178
|
+
return m, tea.Quit
|
|
179
|
+
}
|
|
180
|
+
return m, nil
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// View implements tea.Model.
|
|
184
|
+
func (m Model) View() string {
|
|
185
|
+
if m.width == 0 {
|
|
186
|
+
return "initializing..."
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
switch m.mode {
|
|
190
|
+
case viewSessionPicker:
|
|
191
|
+
return m.renderPicker()
|
|
192
|
+
default:
|
|
193
|
+
return m.renderTerminal()
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
func (m Model) renderTerminal() string {
|
|
198
|
+
// Terminal content fills the screen minus the status bar.
|
|
199
|
+
contentHeight := m.height - 2
|
|
200
|
+
if contentHeight < 1 {
|
|
201
|
+
contentHeight = 1
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Get terminal output and take the last N lines.
|
|
205
|
+
output := m.terminalOutput.String()
|
|
206
|
+
lines := strings.Split(output, "\n")
|
|
207
|
+
if len(lines) > contentHeight {
|
|
208
|
+
lines = lines[len(lines)-contentHeight:]
|
|
209
|
+
}
|
|
210
|
+
content := strings.Join(lines, "\n")
|
|
211
|
+
|
|
212
|
+
// Status bar.
|
|
213
|
+
statusStyle := lipgloss.NewStyle().
|
|
214
|
+
Background(lipgloss.Color("236")).
|
|
215
|
+
Foreground(lipgloss.Color("252")).
|
|
216
|
+
Width(m.width).
|
|
217
|
+
Padding(0, 1)
|
|
218
|
+
|
|
219
|
+
var statusLeft string
|
|
220
|
+
if m.activeHost != "" && m.activeSession != "" {
|
|
221
|
+
statusLeft = fmt.Sprintf("⬤ %s/%s", m.activeHost, m.activeSession)
|
|
222
|
+
} else if m.errMsg != "" {
|
|
223
|
+
statusLeft = fmt.Sprintf("✗ %s", m.errMsg)
|
|
224
|
+
} else {
|
|
225
|
+
statusLeft = m.statusMsg
|
|
226
|
+
}
|
|
227
|
+
statusRight := "Ctrl-O: sessions │ Ctrl-D: detach"
|
|
228
|
+
|
|
229
|
+
padding := m.width - lipgloss.Width(statusLeft) - lipgloss.Width(statusRight) - 2
|
|
230
|
+
if padding < 1 {
|
|
231
|
+
padding = 1
|
|
232
|
+
}
|
|
233
|
+
statusBar := statusStyle.Render(statusLeft + strings.Repeat(" ", padding) + statusRight)
|
|
234
|
+
|
|
235
|
+
return content + "\n" + statusBar
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
func (m Model) renderPicker() string {
|
|
239
|
+
sessions := m.manager.Sessions()
|
|
240
|
+
|
|
241
|
+
titleStyle := lipgloss.NewStyle().
|
|
242
|
+
Bold(true).
|
|
243
|
+
Foreground(lipgloss.Color("212")).
|
|
244
|
+
Padding(1, 2)
|
|
245
|
+
|
|
246
|
+
itemStyle := lipgloss.NewStyle().Padding(0, 2)
|
|
247
|
+
selectedStyle := itemStyle.Background(lipgloss.Color("236")).Foreground(lipgloss.Color("212"))
|
|
248
|
+
|
|
249
|
+
var b strings.Builder
|
|
250
|
+
b.WriteString(titleStyle.Render("Sessions (↑/↓ select, Enter attach, Esc cancel)"))
|
|
251
|
+
b.WriteString("\n\n")
|
|
252
|
+
|
|
253
|
+
if len(sessions) == 0 {
|
|
254
|
+
b.WriteString(itemStyle.Render(" No sessions available"))
|
|
255
|
+
return b.String()
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
currentHost := ""
|
|
259
|
+
for i, s := range sessions {
|
|
260
|
+
// Group header for host.
|
|
261
|
+
if s.HostName != currentHost {
|
|
262
|
+
currentHost = s.HostName
|
|
263
|
+
headerStyle := lipgloss.NewStyle().
|
|
264
|
+
Bold(true).
|
|
265
|
+
Foreground(lipgloss.Color("245")).
|
|
266
|
+
Padding(0, 2)
|
|
267
|
+
b.WriteString(headerStyle.Render(fmt.Sprintf("── %s ──", currentHost)))
|
|
268
|
+
b.WriteString("\n")
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
prefix := " "
|
|
272
|
+
style := itemStyle
|
|
273
|
+
if i == m.pickerCursor {
|
|
274
|
+
prefix = "▸ "
|
|
275
|
+
style = selectedStyle
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
attached := ""
|
|
279
|
+
if s.Attached {
|
|
280
|
+
attached = " (attached)"
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
line := fmt.Sprintf("%s%s%s", prefix, s.Name, attached)
|
|
284
|
+
b.WriteString(style.Render(line))
|
|
285
|
+
b.WriteString("\n")
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return b.String()
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// SetupCallbacks wires the HostManager callbacks to bubbletea messages.
|
|
292
|
+
func SetupCallbacks(manager *client.HostManager, p *tea.Program) {
|
|
293
|
+
manager.OnChange(func() {
|
|
294
|
+
p.Send(stateUpdateMsg{})
|
|
295
|
+
})
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// SetupConnectionCallbacks wires a Connection's callbacks to bubbletea messages.
|
|
299
|
+
func SetupConnectionCallbacks(conn *client.Connection, p *tea.Program) {
|
|
300
|
+
conn.OnTerminalData(func(data []byte) {
|
|
301
|
+
p.Send(terminalDataMsg{data: data})
|
|
302
|
+
})
|
|
303
|
+
conn.OnAttached(func(session string) {
|
|
304
|
+
p.Send(attachedMsg{session: session})
|
|
305
|
+
})
|
|
306
|
+
conn.OnError(func(err error) {
|
|
307
|
+
p.Send(connectionErrMsg{err: err})
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
|
package/vitest.config.js
ADDED