From fd0a1c45e70f131ae8540cde491cf974f69900f9 Mon Sep 17 00:00:00 2001 From: Calmcacil Date: Mon, 12 Jan 2026 22:36:40 +0100 Subject: [PATCH] feat: replace clipboard copy with config display for SSH sessions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Created ConfigDisplay component that shows full client configuration in a scrollable modal window, replacing non-functional clipboard copy. Benefits: - Works over SSH sessions (no clipboard API needed) - Shows complete configuration, not just public key - Scrollable for long configs with keyboard navigation - Users can select and copy text directly in terminal Changes: - Created internal/tui/components/config-display.go - Updated detail.go to replace copyPublicKey with loadConfig - Removed clipboard-related fields and message type - Updated help text: 'c' now shows config - Key bindings for scrolling: ↑↓, pgup/pgdn, g/G, Esc/q to close Fixes: wg-admin-qtb --- .beads/issues.jsonl | 1 + internal/tui/components/config-display.go | 149 ++++++++++++++++++++++ internal/tui/screens/detail.go | 83 ++++++------ 3 files changed, 196 insertions(+), 37 deletions(-) create mode 100644 internal/tui/components/config-display.go diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index ad5be30..1083516 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -41,6 +41,7 @@ {"id":"wg-admin-p6q","title":"Test theme switching functionality","description":"Test theme switching by setting THEME environment variable to different values (dracula, everforest, default, dark, light) and verify that the TUI renders with correct colors.","status":"closed","priority":1,"issue_type":"task","owner":"Calmcacil@Raion","created_at":"2026-01-12T19:07:40.33610722+01:00","created_by":"Calmcacil","updated_at":"2026-01-12T19:10:22.227831273+01:00","closed_at":"2026-01-12T19:10:22.227831273+01:00","close_reason":"Documentation updated with theme options, build successful, theme switching logic verified"} {"id":"wg-admin-q0x","title":"Research and document Dracula/Everforest color schemes","description":"Research and document the official color specifications for Dracula and Everforest color schemes. Extract hex values for primary, success, warning, error, and muted colors to use in the TUI theme implementation.","status":"closed","priority":2,"issue_type":"task","owner":"Calmcacil@Raion","created_at":"2026-01-12T19:07:40.305301935+01:00","created_by":"Calmcacil","updated_at":"2026-01-12T19:08:01.204915996+01:00","closed_at":"2026-01-12T19:08:01.204915996+01:00","close_reason":"Research completed - documented Dracula and Everforest color specifications with hex values for implementation"} {"id":"wg-admin-qpy","title":"Refactor installation into wg-install.sh","description":"Extract install logic from wireguard.sh into dedicated wg-install.sh script. Handle: dependency checks, package installation, firewall setup (nftables), server key generation, interface initialization, systemd service setup. Use interactive 'read' prompts for settings with 'WGI_' prefixed environment variable overrides.","status":"closed","priority":2,"issue_type":"task","owner":"Calmcacil@Raion","created_at":"2026-01-12T16:27:53.151817177+01:00","created_by":"Calmcacil","updated_at":"2026-01-12T16:50:42.168393277+01:00","closed_at":"2026-01-12T16:50:42.168393277+01:00","close_reason":"Created wg-install.sh script with complete installation logic extracted from wireguard.sh. Script includes dependency checks, package installation, nftables firewall setup, server key generation, interface initialization, and systemd service setup. Uses interactive prompts with WGI_ prefixed environment variable overrides. All validation and error handling maintained with atomic operations and proper cleanup. Test suite (test-wg-install.sh) created with 35 tests all passing.","dependencies":[{"issue_id":"wg-admin-qpy","depends_on_id":"wg-admin-37o","type":"blocks","created_at":"2026-01-12T16:28:20.30398105+01:00","created_by":"Calmcacil"},{"issue_id":"wg-admin-qpy","depends_on_id":"wg-admin-wsk","type":"blocks","created_at":"2026-01-12T16:28:20.305872992+01:00","created_by":"Calmcacil"},{"issue_id":"wg-admin-qpy","depends_on_id":"wg-admin-0wc","type":"blocks","created_at":"2026-01-12T16:28:27.88358441+01:00","created_by":"Calmcacil"},{"issue_id":"wg-admin-qpy","depends_on_id":"wg-admin-cwb","type":"blocks","created_at":"2026-01-12T16:28:27.890595849+01:00","created_by":"Calmcacil"},{"issue_id":"wg-admin-qpy","depends_on_id":"wg-admin-2pl","type":"blocks","created_at":"2026-01-12T16:28:27.948214112+01:00","created_by":"Calmcacil"}]} +{"id":"wg-admin-qtb","title":"Replace clipboard copy with config display window for SSH sessions","description":"# Replace Clipboard Copy with Configuration Display\n\n## Problem\n\nThe TUI application currently has a \"Copy Public Key\" feature (press 'c' in detail screen) that doesn't work over SSH sessions. When running the application remotely via SSH:\n\n1. **Clipboard access is not available** - SSH sessions don't provide clipboard functionality\n2. **Feature is non-functional** - Users cannot copy public keys or configuration\n3. **Workaround required** - Users must manually locate and read config files\n4. **Poor UX** - No easy way to get client configuration for use\n\n## Current Implementation\n\nThe detail screen has a `copyPublicKey()` function that:\n- Returns `clipboardCopiedMsg` message\n- Shows \"Public key copied to clipboard!\" feedback\n- Uses no actual clipboard library (not implemented)\n- Is completely non-functional over SSH\n\n## Proposed Solution\n\nReplace the clipboard copy feature with a **Configuration Display Modal** that:\n\n### 1. Shows Full Client Configuration\n- Displays complete WireGuard client configuration file\n- Includes all sections: [Interface] and [Peer]\n- Shows keys, IPs, endpoints, and all configuration options\n- Useful for:\n - Copying configuration to other devices\n - Manual configuration setup\n - Troubleshooting and debugging\n - Sharing with other users\n\n### 2. Scrollable Content\n- Uses Bubble Tea viewport component for scrolling\n- Supports keyboard navigation:\n - ↑/k - Line up\n - ↓/j - Line down\n - pgup/b - Page up\n - pgdown/f - Page down\n - g - Go to top\n - G - Go to bottom\n- Handles long configurations gracefully\n\n### 3. SSH-Friendly\n- Modal displays text directly on screen\n- Users can select and copy text with mouse in terminal\n- Works in any terminal emulator over SSH\n- No clipboard API required\n\n### 4. Easy to Close\n- Esc key closes modal\n- q key closes modal\n- Returns to detail screen\n\n## Implementation Details\n\n### New Component: ConfigDisplayModal\n\nCreated `internal/tui/components/config-display.go` with:\n\n**Features:**\n- Displays full client configuration in styled modal\n- Scrollable viewport for long content\n- Rounded border with title \"šŸ“‹ Client Configuration\"\n- Help text at bottom with navigation instructions\n- Centered on screen with proper positioning\n\n**Key Bindings:**\n```\n↑/j - Scroll down\n↓/k - Scroll up\npgup/b - Page up\npgdn/f - Page down\ng - Go to top\nG - Go to bottom\nEsc/q - Close modal\n```\n\n### Updated Detail Screen\n\nChanged `internal/tui/screens/detail.go`:\n\n**Removed:**\n- `clipboardCopied` field from DetailScreen struct\n- `clipboardTimer` field from DetailScreen struct\n- `copyPublicKey()` function\n- `clipboardCopiedMsg` type\n- Clipboard timeout handling logic\n- \"āœ“ Public key copied to clipboard!\" display\n\n**Added:**\n- `configDisplay *components.ConfigDisplayModel` field\n- `showConfig bool` field\n- `loadConfig()` function that:\n - Loads client configuration via `wireguard.GetClientConfigContent()`\n - Creates config display modal\n - Shows modal with loaded content\n- Config display modal handling in Update() method\n- Config display modal rendering in View() method\n\n**Changed:**\n- Key handler for 'c': Now calls `loadConfig()` instead of `copyPublicKey()`\n- Help text: Changed from \"[c] Copy Public Key\" to \"[c] View Config\"\n\n## Benefits\n\n1. **Works over SSH** - No clipboard API needed\n2. **More useful** - Shows full configuration, not just public key\n3. **Better UX** - Scrollable, easy to read and select\n4. **Flexible** - Users can copy any part of the configuration\n5. **No external dependencies** - Pure terminal-based solution\n\n## Use Cases\n\n### Scenario 1: User needs config for manual setup\n1. Open client details\n2. Press 'c' to view config\n3. Select and copy full configuration text\n4. Paste into configuration file on target device\n\n### Scenario 2: Troubleshooting\n1. Open client details\n2. Press 'c' to view config\n3. Compare with working configuration\n4. Identify differences or issues\n\n### Scenario 3: Sharing configuration\n1. Open client details\n2. Press 'c' to view config\n3. Select config content\n4. Copy and share with other users/admins\n\n## Files Created\n\n- `internal/tui/components/config-display.go` - Configuration display modal component\n\n## Files Modified\n\n- `internal/tui/screens/detail.go`\n - Updated DetailScreen struct\n - Replaced copyPublicKey with loadConfig\n - Added config display handling\n - Updated help text\n\n## Technical Notes\n\n- Uses Bubble Tea viewport for scrolling\n- Leverages existing `wireguard.GetClientConfigContent()` function\n- Modal styled with lipgloss (rounded border, consistent colors)\n- Viewport dimensions: 76x24 (content area)\n- Modal dimensions: 80x24 (with borders and padding)\n\n## Testing Considerations\n\n- Test with long configuration files (ensure scrolling works)\n- Test with various terminal sizes (ensure modal centers correctly)\n- Test over SSH session (ensure text is selectable/copyable)\n- Test with mouse-enabled terminals (verify selection works)","status":"closed","priority":1,"issue_type":"feature","owner":"Calmcacil@Raion","created_at":"2026-01-12T22:30:26.291472155+01:00","created_by":"Calmcacil","updated_at":"2026-01-12T22:36:25.862565024+01:00","closed_at":"2026-01-12T22:36:25.862565024+01:00","close_reason":"Implemented config display modal to replace non-functional clipboard copy. Users can now view full client configuration in a scrollable window that works over SSH sessions. Press 'c' in detail screen to view config."} {"id":"wg-admin-rfo","title":"CRITICAL: Cannot exit client details screen - navigation broken","description":"# CRITICAL BUG - Cannot exit screens\n\n## Problem\n\nUser is unable to navigate back from certain screens in the TUI. When opening client details or restore screens, pressing the back key ('b' or 'esc') does nothing, forcing the user to kill the process from a separate terminal.\n\n## Root Cause\n\nThe screen Update() method returns `(Screen, tea.Cmd)` tuple. When a screen wants to signal \"go back to previous screen\", it should return `(nil, tea.Cmd)` as the first value.\n\nHowever, both `detail.go` and `restore.go` were incorrectly returning `(s, nil)` instead of `(nil, nil)` when the back key was pressed. This caused the main model's navigation logic to never detect the screen change:\n\n```go\n// In main.go line 107:\nif newScreen == nil { // This check fails because newScreen == s, not nil\n // Go back to previous screen\n m.currentScreen = m.previousScreen\n m.previousScreen = nil\n}\n```\n\n## Issues Found\n\n### detail.go (line 116)\n**Before:**\n```go\ncase \"b\", \"esc\":\n // Return to list screen - signal parent to switch screens\n return s, nil // BUG: Returns current screen, not nil\n```\n\n**After:**\n```go\ncase \"b\", \"esc\":\n // Return to list screen - signal parent to switch screens\n return nil, nil // FIXED: Returns nil to signal screen change\n```\n\n### restore.go (line 101)\n**Before:**\n```go\ncase \"q\", \"esc\":\n // Return to list screen - signal parent to switch screens\n return s, nil // BUG: Returns current screen, not nil\n```\n\n**After:**\n```go\ncase \"q\", \"esc\":\n // Return to list screen - signal parent to switch screens\n return nil, nil // FIXED: Returns nil to signal screen change\n```\n\n## Impact\n\n- Users get trapped in detail and restore screens\n- Cannot return to main list screen\n- Must kill process and restart application\n- Critical usability issue\n\n## Fix Applied\n\nChanged both occurrences from `return s, nil` to `return nil, nil` to properly signal the main model to switch back to the previous screen.\n\n## Verification\n\n- Build successful with no errors\n- Navigation logic now correctly detects nil return value\n- Screen switching works as expected\n\n## Files Modified\n\n- `internal/tui/screens/detail.go` - Line 116\n- `internal/tui/screens/restore.go` - Line 101","status":"closed","priority":0,"issue_type":"bug","owner":"Calmcacil@Raion","created_at":"2026-01-12T22:22:51.43957087+01:00","created_by":"Calmcacil","updated_at":"2026-01-12T22:27:03.641284763+01:00","closed_at":"2026-01-12T22:27:03.641284763+01:00","close_reason":"Fixed navigation bug by changing 'return s, nil' to 'return nil, nil' in detail.go and restore.go. Both screens now correctly signal main model to switch back to previous screen when back key is pressed."} {"id":"wg-admin-slj","title":"Refactor WireGuard scripts into modular architecture","description":"Refactor monolithic wireguard.sh into two separate scripts: wg-install.sh for initial setup, wg-client-manager for client operations. Use interactive 'read' prompts with 'WGI_' prefixed environment variable overrides. Add validation functions, security hardening, and remove all hardcoded sensitive information from repository.","status":"closed","priority":2,"issue_type":"task","owner":"Calmcacil@Raion","created_at":"2026-01-12T16:27:18.232667092+01:00","created_by":"Calmcacil","updated_at":"2026-01-12T17:11:02.639140093+01:00","closed_at":"2026-01-12T17:11:02.639140093+01:00","close_reason":"Refactoring complete: wg-install.sh (921 lines) and wg-client-manager (545 lines) scripts have been created and are functional. wireguard.sh retained for backwards compatibility.","dependencies":[{"issue_id":"wg-admin-slj","depends_on_id":"wg-admin-abw","type":"blocks","created_at":"2026-01-12T16:28:21.930404739+01:00","created_by":"Calmcacil"},{"issue_id":"wg-admin-slj","depends_on_id":"wg-admin-qpy","type":"blocks","created_at":"2026-01-12T16:28:21.936380993+01:00","created_by":"Calmcacil"},{"issue_id":"wg-admin-slj","depends_on_id":"wg-admin-0wc","type":"blocks","created_at":"2026-01-12T16:28:21.983754904+01:00","created_by":"Calmcacil"}]} {"id":"wg-admin-ti0","title":"Investigate q key behavior and parseHandshake bug","description":"Investigate q key behavior causing app to not return properly from client details. parseHandshake fix has been committed separately.","status":"open","priority":1,"issue_type":"bug","owner":"Calmcacil@Raion","created_at":"2026-01-12T19:26:35.435240285+01:00","created_by":"Calmcacil","updated_at":"2026-01-12T19:27:21.412723919+01:00"} diff --git a/internal/tui/components/config-display.go b/internal/tui/components/config-display.go new file mode 100644 index 0000000..48df80d --- /dev/null +++ b/internal/tui/components/config-display.go @@ -0,0 +1,149 @@ +package components + +import ( + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +// ConfigDisplayModel represents a configuration display modal +type ConfigDisplayModel struct { + config string + viewport viewport.Model + Visible bool + Width int + Height int + scrollPos int +} + +// Styles +var ( + configModalStyle = lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("62")). + Padding(1, 2). + Background(lipgloss.Color("235")) + configTitleStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("62")). + Bold(true). + MarginBottom(1) + configHelpStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("241")). + MarginTop(1) + configContentStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("255")). + Width(76) +) + +// NewConfigDisplay creates a new configuration display modal +func NewConfigDisplay(config string, width, height int) *ConfigDisplayModel { + vp := viewport.New(76, 24) + vp.SetContent(config) + + return &ConfigDisplayModel{ + config: config, + viewport: vp, + Visible: true, + Width: width, + Height: height, + scrollPos: 0, + } +} + +// Init initializes the configuration display modal +func (m *ConfigDisplayModel) Init() tea.Cmd { + return nil +} + +// Update handles messages for the configuration display modal +func (m *ConfigDisplayModel) Update(msg tea.Msg) (*ConfigDisplayModel, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "esc", "q": + m.Visible = false + return m, nil + case "up", "k": + m.viewport.LineUp(1) + case "down", "j": + m.viewport.LineDown(1) + case "pgup", "b": + m.viewport.HalfViewUp() + case "pgdown", "f", " ": + m.viewport.HalfViewDown() + case "g", "home": + m.viewport.GotoTop() + case "G", "end": + m.viewport.GotoBottom() + } + case tea.WindowSizeMsg: + // Update modal dimensions on resize + m.Width = msg.Width + m.Height = msg.Height + } + + var cmd tea.Cmd + m.viewport, cmd = m.viewport.Update(msg) + return m, cmd +} + +// View renders the configuration display modal +func (m *ConfigDisplayModel) View() string { + if !m.Visible { + return "" + } + + // Build modal content + title := configTitleStyle.Render("šŸ“‹ Client Configuration") + content := m.viewport.View() + help := configHelpStyle.Render("↑/j: down • ↓/k: up • pgup/pgdn: page • g/G: top/bottom • Esc: close") + + fullContent := lipgloss.JoinVertical( + lipgloss.Left, + title, + "", + content, + help, + ) + + // Apply modal style + modal := configModalStyle.Render(fullContent) + + // Center modal on screen + modalWidth := lipgloss.Width(modal) + modalHeight := lipgloss.Height(modal) + + x := (m.Width - modalWidth) / 2 + if x < 0 { + x = 0 + } + y := (m.Height - modalHeight) / 2 + if y < 0 { + y = 0 + } + + return lipgloss.Place(m.Width, m.Height, + lipgloss.Left, lipgloss.Top, + modal, + lipgloss.WithWhitespaceChars(" "), + lipgloss.WithWhitespaceForeground(lipgloss.Color("235")), + ) +} + +// IsVisible returns true if the modal is visible +func (m *ConfigDisplayModel) IsVisible() bool { + return m.Visible +} + +// Hide hides the modal +func (m *ConfigDisplayModel) Hide() { + m.Visible = false +} + +// Show shows the modal +func (m *ConfigDisplayModel) Show(config string) { + m.config = config + m.viewport.SetContent(config) + m.viewport.GotoTop() + m.Visible = true +} diff --git a/internal/tui/screens/detail.go b/internal/tui/screens/detail.go index 11a8261..8658b29 100644 --- a/internal/tui/screens/detail.go +++ b/internal/tui/screens/detail.go @@ -12,15 +12,15 @@ import ( // DetailScreen displays detailed information about a single WireGuard client type DetailScreen struct { - client wireguard.Client - status string - lastHandshake time.Time - transferRx string - transferTx string - confirmModal *components.DeleteConfirmModel - showConfirm bool - clipboardCopied bool - clipboardTimer int + client wireguard.Client + status string + lastHandshake time.Time + transferRx string + transferTx string + confirmModal *components.DeleteConfirmModel + showConfirm bool + configDisplay *components.ConfigDisplayModel + showConfig bool } // Styles @@ -71,15 +71,6 @@ func (s *DetailScreen) Init() tea.Cmd { func (s *DetailScreen) Update(msg tea.Msg) (Screen, tea.Cmd) { var cmd tea.Cmd - // Handle clipboard copy timeout - if s.clipboardCopied { - s.clipboardTimer++ - if s.clipboardTimer > 2 { - s.clipboardCopied = false - s.clipboardTimer = 0 - } - } - // Handle confirmation modal if s.showConfirm && s.confirmModal != nil { _, cmd = s.confirmModal.Update(msg) @@ -100,10 +91,21 @@ func (s *DetailScreen) Update(msg tea.Msg) (Screen, tea.Cmd) { return s, cmd } + // Handle config display modal + if s.showConfig && s.configDisplay != nil { + _, cmd = s.configDisplay.Update(msg) + + // Handle modal close + if !s.configDisplay.IsVisible() { + s.showConfig = false + return s, nil + } + + return s, cmd + } + // Handle normal screen messages switch msg := msg.(type) { - case clipboardCopiedMsg: - s.clipboardCopied = true case clientStatusLoadedMsg: s.status = msg.status s.lastHandshake = msg.lastHandshake @@ -123,8 +125,8 @@ func (s *DetailScreen) Update(msg tea.Msg) (Screen, tea.Cmd) { ) s.showConfirm = true case "c": - // Copy public key to clipboard - return s, s.copyPublicKey() + // Show client configuration + return s, s.loadConfig() } } @@ -133,6 +135,12 @@ func (s *DetailScreen) Update(msg tea.Msg) (Screen, tea.Cmd) { // View renders the detail screen func (s *DetailScreen) View() string { + // Handle config display modal + if s.showConfig && s.configDisplay != nil { + return s.configDisplay.View() + } + + // Handle confirmation modal if s.showConfirm && s.confirmModal != nil { // Render underlying content dimmed content := s.renderContent() @@ -186,14 +194,9 @@ func (s *DetailScreen) renderContent() string { ) // Add help text - helpText := detailHelpStyle.Render("Actions: [d] Delete • [c] Copy Public Key • [b] Back") + helpText := detailHelpStyle.Render("Actions: [d] Delete • [c] View Config • [b] Back") content = lipgloss.JoinVertical(lipgloss.Left, content, helpText) - // Show clipboard confirmation - if s.clipboardCopied { - content += "\n" + lipgloss.NewStyle().Foreground(lipgloss.Color("46")).Render("āœ“ Public key copied to clipboard!") - } - return content } @@ -252,13 +255,22 @@ func (s *DetailScreen) loadClientStatus() tea.Msg { } } -// copyPublicKey copies the public key to clipboard -func (s *DetailScreen) copyPublicKey() tea.Cmd { +// loadConfig loads and displays the client configuration +func (s *DetailScreen) loadConfig() tea.Cmd { return func() tea.Msg { - // Note: In a real implementation, you would use a clipboard library like - // github.com/atotto/clipboard or implement platform-specific clipboard access - // For now, we'll just simulate the action - return clipboardCopiedMsg{} + config, err := wireguard.GetClientConfigContent(s.client.Name) + if err != nil { + return errMsg{fmt.Errorf("failed to load client config: %w", err)} + } + + // Create or update config display modal + if s.configDisplay == nil { + s.configDisplay = components.NewConfigDisplay(config, 80, 24) + } else { + s.configDisplay.Show(config) + } + s.showConfig = true + return nil } } @@ -284,6 +296,3 @@ type clientStatusLoadedMsg struct { transferRx string transferTx string } - -// clipboardCopiedMsg is sent when public key is copied to clipboard -type clipboardCopiedMsg struct{}