feat: responsive TUI layout — mobile/tablet/desktop support

- NEW responsive.go: wrapFooter, adaptiveSidePad, clampWidth, truncateStr
- Footer auto-wraps to multi-line on narrow terminals (full labels preserved)
- Box width clamped to terminal width across all tabs
- Host/key/snippet rows truncated with ellipsis; compact format <35 cols
- SFTP panes stack vertically when terminal < 50 cols
- Tab bar truncates names on overflow
- Form contentW minimum lowered 50->30 for mobile
- Fixed: key_list_tab & snippet_list_tab dropped last data row (off-by-one bug)
This commit is contained in:
swanadiva
2026-06-23 19:08:44 +07:00
parent d359688b75
commit d75621e9b4
11 changed files with 302 additions and 113 deletions
+45 -41
View File
@@ -104,7 +104,7 @@ func (t *HostListTab) Update(msg tea.Msg) (Tab, tea.Cmd) {
return t, nil
}
// View renders the host list — exactly like tamagosh
// View renders the host list — responsive layout
func (t *HostListTab) View() string {
var b strings.Builder
@@ -121,13 +121,6 @@ func (t *HostListTab) View() string {
return b.String()
}
// Build rows (plain + styled)
type styledRow struct {
text string
plain string
}
var rows []styledRow
// Scrolling
availH := t.height - 10
if availH < 1 {
@@ -146,11 +139,45 @@ func (t *HostListTab) View() string {
start = len(t.hosts) - maxHosts
}
// Responsive width calculation
sidePad := adaptiveSidePad(t.width)
titlePlain := "Connection List"
// Measure content rows to determine natural box width
widestContent := lipgloss.Width(titlePlain)
for i := start; i < start+maxHosts; i++ {
host := t.hosts[i]
// tamagosh format: " %-12s %-18s :%d"
raw := fmt.Sprintf(" %-12s %-18s :%d", host.Name, host.Hostname, host.Port)
if w := lipgloss.Width(raw); w > widestContent {
widestContent = w
}
}
// Clamp box to terminal width
targetW := clampWidth(widestContent+sidePad*2, t.width)
innerW := targetW - sidePad*2
if innerW < 1 {
innerW = 1
}
// Build rows with adaptive format
type styledRow struct {
text string
plain string
}
var rows []styledRow
for i := start; i < start+maxHosts; i++ {
host := t.hosts[i]
var raw string
if innerW >= 35 {
raw = fmt.Sprintf(" %-12s %-18s :%d", host.Name, host.Hostname, host.Port)
} else {
raw = fmt.Sprintf(" %s %s:%d", host.Name, host.Hostname, host.Port)
}
if lipgloss.Width(raw) > innerW {
raw = truncateStr(raw, innerW)
}
var styled string
if i == t.selectedIndex {
@@ -165,54 +192,31 @@ func (t *HostListTab) View() string {
rows = append(rows, styledRow{text: styled, plain: raw})
}
// Footer
var footer styledRow
// Footer (wrapped to fit innerW)
footerText := "Ctrl+Tab:switch Ctrl+Q:close ↑↓:nav Enter:SSH Ctrl+N:add Ctrl+E:edit Ctrl+F:SFTP Ctrl+K:keys Ctrl+P:snippets q:quit"
footer = styledRow{text: SubtitleStyle.Render(footerText), plain: footerText}
rows = append(rows, footer) // for widest measurement
footerWrapped := wrapFooter(footerText, innerW)
// Title
title := lipgloss.NewStyle().Bold(true).Foreground(gbYellow).Render("Connection List")
titlePlain := "Connection List"
// tamagosh: widest content line drives the box's natural width
widestRow := 0
for _, r := range rows {
if w := lipgloss.Width(r.plain); w > widestRow {
widestRow = w
}
}
contentW := widestRow
if w := lipgloss.Width(footer.plain); w > contentW {
contentW = w
}
if w := lipgloss.Width(titlePlain); w > contentW {
contentW = w
}
const sidePad = 6
targetW := contentW + sidePad*2
title := lipgloss.NewStyle().Bold(true).Foreground(gbYellow).Render(titlePlain)
var content strings.Builder
// Centered title
content.WriteString(lipgloss.PlaceHorizontal(targetW, lipgloss.Center, title))
content.WriteString("\n\n")
// Center each row independently within the box
for _, r := range rows[:len(rows)-1] { // exclude footer, added below
for _, r := range rows {
line := lipgloss.PlaceHorizontal(targetW, lipgloss.Center, r.text)
content.WriteString(line)
content.WriteString("\n")
}
content.WriteString("\n")
// Centered footer
content.WriteString(lipgloss.PlaceHorizontal(targetW, lipgloss.Center, footer.text))
for _, line := range strings.Split(footerWrapped, "\n") {
content.WriteString(lipgloss.PlaceHorizontal(targetW, lipgloss.Center, SubtitleStyle.Render(line)))
content.WriteString("\n")
}
// Wrap in a rounded border box
box := BorderStyle.Render(content.String())
// Center the whole box horizontally
b.WriteString(lipgloss.PlaceHorizontal(t.width, lipgloss.Center, box))
return b.String()