feat: replace clipboard copy with config display for SSH sessions
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
This commit is contained in:
File diff suppressed because one or more lines are too long
149
internal/tui/components/config-display.go
Normal file
149
internal/tui/components/config-display.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -19,8 +19,8 @@ type DetailScreen struct {
|
|||||||
transferTx string
|
transferTx string
|
||||||
confirmModal *components.DeleteConfirmModel
|
confirmModal *components.DeleteConfirmModel
|
||||||
showConfirm bool
|
showConfirm bool
|
||||||
clipboardCopied bool
|
configDisplay *components.ConfigDisplayModel
|
||||||
clipboardTimer int
|
showConfig bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Styles
|
// Styles
|
||||||
@@ -71,15 +71,6 @@ func (s *DetailScreen) Init() tea.Cmd {
|
|||||||
func (s *DetailScreen) Update(msg tea.Msg) (Screen, tea.Cmd) {
|
func (s *DetailScreen) Update(msg tea.Msg) (Screen, tea.Cmd) {
|
||||||
var cmd 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
|
// Handle confirmation modal
|
||||||
if s.showConfirm && s.confirmModal != nil {
|
if s.showConfirm && s.confirmModal != nil {
|
||||||
_, cmd = s.confirmModal.Update(msg)
|
_, cmd = s.confirmModal.Update(msg)
|
||||||
@@ -100,10 +91,21 @@ func (s *DetailScreen) Update(msg tea.Msg) (Screen, tea.Cmd) {
|
|||||||
return s, 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
|
// Handle normal screen messages
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case clipboardCopiedMsg:
|
|
||||||
s.clipboardCopied = true
|
|
||||||
case clientStatusLoadedMsg:
|
case clientStatusLoadedMsg:
|
||||||
s.status = msg.status
|
s.status = msg.status
|
||||||
s.lastHandshake = msg.lastHandshake
|
s.lastHandshake = msg.lastHandshake
|
||||||
@@ -123,8 +125,8 @@ func (s *DetailScreen) Update(msg tea.Msg) (Screen, tea.Cmd) {
|
|||||||
)
|
)
|
||||||
s.showConfirm = true
|
s.showConfirm = true
|
||||||
case "c":
|
case "c":
|
||||||
// Copy public key to clipboard
|
// Show client configuration
|
||||||
return s, s.copyPublicKey()
|
return s, s.loadConfig()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,6 +135,12 @@ func (s *DetailScreen) Update(msg tea.Msg) (Screen, tea.Cmd) {
|
|||||||
|
|
||||||
// View renders the detail screen
|
// View renders the detail screen
|
||||||
func (s *DetailScreen) View() string {
|
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 {
|
if s.showConfirm && s.confirmModal != nil {
|
||||||
// Render underlying content dimmed
|
// Render underlying content dimmed
|
||||||
content := s.renderContent()
|
content := s.renderContent()
|
||||||
@@ -186,14 +194,9 @@ func (s *DetailScreen) renderContent() string {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Add help text
|
// 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)
|
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
|
return content
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,13 +255,22 @@ func (s *DetailScreen) loadClientStatus() tea.Msg {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// copyPublicKey copies the public key to clipboard
|
// loadConfig loads and displays the client configuration
|
||||||
func (s *DetailScreen) copyPublicKey() tea.Cmd {
|
func (s *DetailScreen) loadConfig() tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
// Note: In a real implementation, you would use a clipboard library like
|
config, err := wireguard.GetClientConfigContent(s.client.Name)
|
||||||
// github.com/atotto/clipboard or implement platform-specific clipboard access
|
if err != nil {
|
||||||
// For now, we'll just simulate the action
|
return errMsg{fmt.Errorf("failed to load client config: %w", err)}
|
||||||
return clipboardCopiedMsg{}
|
}
|
||||||
|
|
||||||
|
// 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
|
transferRx string
|
||||||
transferTx string
|
transferTx string
|
||||||
}
|
}
|
||||||
|
|
||||||
// clipboardCopiedMsg is sent when public key is copied to clipboard
|
|
||||||
type clipboardCopiedMsg struct{}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user