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 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, Tab to switch): " 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 sort mode indicator sortInfo := fmt.Sprintf(" Sort: %s", m.sortMode.String()) components = append(components, m.styles.SortInfo.Render(sortInfo)) // 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 = " Use ↑/↓ to navigate • Enter to connect • (a)dd • (e)dit • (d)elete • / to search • Tab to switch\n Sort: (s)witch • (r)ecent • (n)ame • q/ESC to quit" } else { helpText = " Type to filter hosts • Enter to validate search • Tab to switch to table • ESC to 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) }