sshm/internal/ui/view.go

160 lines
4.2 KiB
Go

package ui
import (
"fmt"
"strings"
"github.com/charmbracelet/lipgloss"
)
// View renders the complete user interface
func (m Model) View() string {
if !m.ready {
return "Loading..."
}
// Handle different view modes
switch m.viewMode {
case ViewAdd:
if m.addForm != nil {
return m.addForm.View()
}
case ViewEdit:
if m.editForm != nil {
return m.editForm.View()
}
case ViewInfo:
if m.infoForm != nil {
return m.infoForm.View()
}
case ViewPortForward:
if m.portForwardForm != nil {
return m.portForwardForm.View()
}
case ViewHelp:
if m.helpForm != nil {
return m.helpForm.View()
}
case ViewFileSelector:
if m.fileSelectorForm != nil {
return m.fileSelectorForm.View()
}
case ViewList:
return m.renderListView()
}
return m.renderListView()
}
// renderListView renders the main list interface
func (m Model) renderListView() string {
// Build the interface components
components := []string{}
// Add the ASCII title
components = append(components, m.styles.Header.Render(asciiTitle))
// Add the search bar with the appropriate style based on focus
searchPrompt := "Search (/ to focus): "
if m.searchMode {
components = append(components, m.styles.SearchFocused.Render(searchPrompt+m.searchInput.View()))
} else {
components = append(components, m.styles.SearchUnfocused.Render(searchPrompt+m.searchInput.View()))
}
// Add the table with the appropriate style based on focus
if m.searchMode {
// The table is not focused, use the unfocused style
components = append(components, m.styles.TableUnfocused.Render(m.table.View()))
} else {
// The table is focused, use the focused style with the primary color
components = append(components, m.styles.TableFocused.Render(m.table.View()))
}
// Add the help text
var helpText string
if !m.searchMode {
helpText = " ↑/↓: navigate • Enter: connect • p: ping all • i: info • h: help • q: quit"
} else {
helpText = " Type to filter • Enter: validate • Tab: switch • ESC: quit"
}
components = append(components, m.styles.HelpText.Render(helpText))
// Join all components vertically with appropriate spacing
mainView := m.styles.App.Render(
lipgloss.JoinVertical(
lipgloss.Left,
components...,
),
)
// If in delete mode, overlay the confirmation dialog
if m.deleteMode {
// Combine the main view with the confirmation dialog overlay
confirmation := m.renderDeleteConfirmation()
// Center the confirmation dialog on the screen
centeredConfirmation := lipgloss.Place(
m.width,
m.height,
lipgloss.Center,
lipgloss.Center,
confirmation,
)
return centeredConfirmation
}
return mainView
}
// renderDeleteConfirmation renders a clean delete confirmation dialog
func (m Model) renderDeleteConfirmation() string {
// Remove emojis (uncertain width depending on terminal) to stabilize the frame
title := "DELETE SSH HOST"
question := fmt.Sprintf("Are you sure you want to delete host '%s'?", m.deleteHost)
action := "This action cannot be undone."
help := "Enter: confirm • Esc: cancel"
// Individual styles (do not affect width via internal centering)
titleStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("196"))
questionStyle := lipgloss.NewStyle()
actionStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("203"))
helpStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("241"))
lines := []string{
titleStyle.Render(title),
"",
questionStyle.Render(question),
"",
actionStyle.Render(action),
"",
helpStyle.Render(help),
}
// Compute the real maximum width (ANSI-safe via lipgloss.Width)
maxw := 0
for _, ln := range lines {
w := lipgloss.Width(ln)
if w > maxw {
maxw = w
}
}
// Minimal width for aesthetics
if maxw < 40 {
maxw = 40
}
// Build the raw text block (without centering) then apply the container style
raw := strings.Join(lines, "\n")
// Container style: wider horizontal padding, stable border
box := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("196")).
PaddingTop(1).PaddingBottom(1).PaddingLeft(2).PaddingRight(2).
Width(maxw + 4) // +4 = internal margin (2 spaces of left/right padding)
return box.Render(raw)
}