mirror of
https://github.com/Gu1llaum-3/sshm.git
synced 2026-01-27 03:04:21 +01:00
refactor(ui): split TUI logic into multiple files and improve styling
This commit is contained in:
147
internal/ui/view.go
Normal file
147
internal/ui/view.go
Normal file
@@ -0,0 +1,147 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user