package tui import ( "fmt" "strings" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "git.tukangketik.id/swanadiva/hostkeeper/internal/models" ) // HostListTab is the host list tab type HostListTab struct { hosts []*models.Host selectedIndex int err error width int height int } // NewHostListTab creates a new host list tab func NewHostListTab() *HostListTab { return &HostListTab{ selectedIndex: 0, } } // Init initializes the tab func (t *HostListTab) Init() tea.Cmd { return nil } // Name returns the tab name func (t *HostListTab) Name() string { return "Hosts" } // Update handles messages for the host list func (t *HostListTab) Update(msg tea.Msg) (Tab, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: t.width = msg.Width t.height = msg.Height case tea.KeyMsg: switch msg.String() { case "up", "k": if t.selectedIndex > 0 { t.selectedIndex-- } case "down", "j": if t.selectedIndex < len(t.hosts)-1 { t.selectedIndex++ } case "enter", " ": if len(t.hosts) > 0 { host := t.hosts[t.selectedIndex] return t, func() tea.Msg { return sshConnectToMsg{host: host} } } case "ctrl+n": return t, func() tea.Msg { return openHostFormMsg{} } case "ctrl+e": if len(t.hosts) > 0 { host := t.hosts[t.selectedIndex] return t, func() tea.Msg { return openHostFormMsg{editing: host} } } case "ctrl+f": if len(t.hosts) > 0 { host := t.hosts[t.selectedIndex] return t, func() tea.Msg { return openSFTPMsg{host: host} } } case "ctrl+k": return t, func() tea.Msg { return openKeyListMsg{} } case "ctrl+p": return t, func() tea.Msg { return openSnippetListMsg{} } case "q", "ctrl+c": return t, func() tea.Msg { return quitMsg{} } } } return t, nil } // View renders the host list func (t *HostListTab) View() string { availW := t.width if availW < 40 { availW = 80 } cardW := availW - 6 if cardW > 74 { cardW = 74 } if cardW < 30 { cardW = 30 } var b strings.Builder // Capped vertical padding b.WriteString(strings.Repeat("\n", 2)) // Title b.WriteString(centerText(AppTitleStyle.Render(" HOSTKEEPER - SSH Manager "), availW)) b.WriteString("\n\n") if len(t.hosts) == 0 { b.WriteString(centerText(SubtitleStyle.Render("No hosts found. Add with Ctrl+N"), availW)) b.WriteString("\n") } else { for i, host := range t.hosts { entry := renderHostEntry(host, i == t.selectedIndex, cardW) // Center the card (which produces 4 lines) horizontally for _, line := range strings.Split(entry, "\n") { b.WriteString(centerText(line, availW)) b.WriteString("\n") } b.WriteString("\n") } } return b.String() } // centerText centers a line of text horizontally within the given width, // respecting ANSI escape codes for accurate display width. func centerText(s string, width int) string { w := lipgloss.Width(s) if w >= width { return s } pad := strings.Repeat(" ", (width-w)/2) return pad + s } // Close is a no-op for host list tab func (t *HostListTab) Close() {} // SetHosts sets the host list func (t *HostListTab) SetHosts(hosts []*models.Host) { t.hosts = hosts if len(hosts) > 0 && t.selectedIndex >= len(hosts) { t.selectedIndex = len(hosts) - 1 } } // Hosts returns the host list func (t *HostListTab) Hosts() []*models.Host { return t.hosts } // SelectedIndex returns the selected index func (t *HostListTab) SelectedIndex() int { return t.selectedIndex } // FindHostListTab finds the first HostListTab in a list of tabs func FindHostListTab(tabs []Tab) *HostListTab { for _, tab := range tabs { if ht, ok := tab.(*HostListTab); ok { return ht } } return nil } // renderHostEntry renders a single host as a card func renderHostEntry(host *models.Host, selected bool, width int) string { // Build the card content var inner strings.Builder // First line: host name (bold) name := truncateString(host.Name, width-4) inner.WriteString(HostNameStyle.Render(" " + name)) inner.WriteString("\n") // Second line: user@host:port with tags addr := fmt.Sprintf("%s@%s:%d", host.Username, host.Hostname, host.Port) if len(host.Tags) > 0 { addr += " " + formatTagsForTUI(host.Tags) } inner.WriteString(HostDetailStyle.Render(" " + truncateString(addr, width-2))) cardStyle := HostCardStyle if selected { cardStyle = HostCardActiveStyle } return cardStyle.Width(width).Render(inner.String()) } // truncateString truncates a string to the given max length, appending "…" if needed func truncateString(s string, maxLen int) string { if maxLen < 1 { return "" } if len(s) <= maxLen { return s } return s[:maxLen-1] + "…" } // formatTagsForTUI formats tags for TUI display func formatTagsForTUI(tags []string) string { if len(tags) == 0 { return "" } var formatted []string for _, tag := range tags { formatted = append(formatted, "["+tag+"]") } return strings.Join(formatted, " ") }