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:
+45
-41
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user