feat: centered card-style host list layout

- Each host rendered as a bordered card (HostCardStyle / HostCardActiveStyle)
- Title centered horizontally
- Content constrained to 80 columns max
- Vertical centering padding
- Selected host card gets green border (gbGreen #a9b665)
- Empty state message centered
- Removed old plain-text renderHost/renderSelectedHost functions
This commit is contained in:
swanadiva
2026-06-23 15:42:03 +07:00
parent c957a97cdb
commit 53db03814e
2 changed files with 74 additions and 30 deletions
+68 -30
View File
@@ -5,6 +5,7 @@ import (
"strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"git.tukangketik.id/swanadiva/hostkeeper/internal/models"
)
@@ -105,27 +106,54 @@ func (t *HostListTab) Update(msg tea.Msg) (Tab, tea.Cmd) {
// View renders the host list
func (t *HostListTab) View() string {
availW := t.width
if availW < 40 {
availW = 80
}
// Constrain content width so cards don't stretch too wide
contentW := availW
if contentW > 80 {
contentW = 80
}
var b strings.Builder
b.WriteString(AppTitleStyle.Render("HOSTKEEPER - SSH Manager"))
// Centered title
title := AppTitleStyle.Render(" HOSTKEEPER - SSH Manager ")
b.WriteString(lipgloss.Place(contentW, 1, lipgloss.Center, lipgloss.Center, title))
b.WriteString("\n\n")
if len(t.hosts) == 0 {
b.WriteString(SubtitleStyle.Render("No hosts found. Add your first host with: hostkeeper add"))
b.WriteString("\n\n")
return b.String()
}
for i, host := range t.hosts {
if i == t.selectedIndex {
b.WriteString(renderSelectedHost(host))
} else {
b.WriteString(renderHost(host))
b.WriteString(lipgloss.Place(contentW, 1, lipgloss.Center, lipgloss.Center,
SubtitleStyle.Render("No hosts found. Add with Ctrl+N")))
} else {
cardW := contentW - 4
if cardW < 30 {
cardW = 30
}
for i, host := range t.hosts {
entry := renderHostEntry(host, i == t.selectedIndex, cardW)
b.WriteString(entry)
b.WriteString("\n")
}
b.WriteString("\n")
}
return b.String()
// Center horizontally within the full terminal width
content := lipgloss.Place(availW, 1, lipgloss.Center, lipgloss.Top, b.String())
// Add vertical padding to push content toward center of screen
availH := t.height - 5
if availH < 0 {
availH = 0
}
contentLines := strings.Count(content, "\n") + 1
if contentLines < availH {
topPad := (availH - contentLines) / 2
content = strings.Repeat("\n", topPad) + content
}
return content
}
// Close is a no-op for host list tab
@@ -159,28 +187,40 @@ func FindHostListTab(tabs []Tab) *HostListTab {
return nil
}
// renderHost renders a single host
func renderHost(host *models.Host) string {
var b strings.Builder
// 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
b.WriteString(HostNameStyle.Render(host.Name))
b.WriteString("\n")
details := fmt.Sprintf(" %s@%s:%d", host.Username, host.Hostname, host.Port)
b.WriteString(HostDetailStyle.Render(details))
// 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 {
tags := formatTagsForTUI(host.Tags)
b.WriteString(" " + TagStyle.Render(tags))
addr += " " + formatTagsForTUI(host.Tags)
}
inner.WriteString(HostDetailStyle.Render(" " + truncateString(addr, width-2)))
cardStyle := HostCardStyle
if selected {
cardStyle = HostCardActiveStyle
}
return b.String()
return cardStyle.Width(width).Render(inner.String())
}
// renderSelectedHost renders the selected host with highlight
func renderSelectedHost(host *models.Host) string {
hostText := renderHost(host)
return SelectedStyle.Render(hostText)
// 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
@@ -188,11 +228,9 @@ 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, " ")
}