diff --git a/internal/tui/app.go b/internal/tui/app.go index d420db3..b19db2b 100644 --- a/internal/tui/app.go +++ b/internal/tui/app.go @@ -920,8 +920,15 @@ func (m *Model) View() string { currentContent := sb.String() currentLines := strings.Count(currentContent, "\n") + 1 - // Fill space to push footer to bottom (reserve 3 lines for footer) - footerHeight := 3 + // Calculate footer height dynamically (help bar lines + status bar + spacing) + footerHeight := 2 // status bar + newline before it + var helpBarContent string + if m.mode == ViewList { + helpBarContent = m.helpBar() + helpBarLines := strings.Count(helpBarContent, "\n") + 1 + footerHeight += helpBarLines + 1 // +1 for newline before help bar + } + remainingLines := m.height - currentLines - footerHeight if remainingLines > 0 { sb.WriteString(strings.Repeat("\n", remainingLines)) @@ -930,7 +937,7 @@ func (m *Model) View() string { // Footer (help bar + status bar) if m.mode == ViewList { sb.WriteString("\n") - sb.WriteString(m.helpBar()) + sb.WriteString(helpBarContent) } sb.WriteString("\n") sb.WriteString(m.statusBar()) @@ -939,19 +946,67 @@ func (m *Model) View() string { } func (m *Model) helpBar() string { - return helpBarStyle.Render(fmt.Sprintf("%s/%s: Navigate %s: Toggle %s: New %s: Edit %s: Delete %s: Presets %s: Groups %s: Backups %s: Search %s: Help %s: Quit", - helpKeyStyle.Render("↑↓"), - helpKeyStyle.Render("jk"), - helpKeyStyle.Render("Space"), - helpKeyStyle.Render("n"), - helpKeyStyle.Render("e"), - helpKeyStyle.Render("d"), - helpKeyStyle.Render("p"), - helpKeyStyle.Render("g"), - helpKeyStyle.Render("b"), - helpKeyStyle.Render("/"), - helpKeyStyle.Render("?"), - helpKeyStyle.Render("q"))) + // Define help items with their display widths (without ANSI codes) + type helpItem struct { + key string + desc string + rawWidth int // width without ANSI escape codes + } + + items := []helpItem{ + {"↑↓/jk", "Navigate", 13}, + {"Space", "Toggle", 13}, + {"n", "New", 6}, + {"e", "Edit", 7}, + {"d", "Delete", 9}, + {"p", "Presets", 10}, + {"g", "Groups", 9}, + {"b", "Backups", 10}, + {"/", "Search", 9}, + {"?", "Help", 7}, + {"q", "Quit", 7}, + } + + separator := " " + sepWidth := 2 + + var lines []string + var currentLine string + var currentWidth int + + for i, item := range items { + rendered := helpKeyStyle.Render(item.key) + ": " + item.desc + + // Check if adding this item would exceed width + newWidth := currentWidth + item.rawWidth + if currentWidth > 0 { + newWidth += sepWidth + } + + if m.width > 0 && newWidth > m.width && currentWidth > 0 { + // Start a new line + lines = append(lines, currentLine) + currentLine = rendered + currentWidth = item.rawWidth + } else { + // Add to current line + if currentWidth > 0 { + currentLine += separator + } + currentLine += rendered + currentWidth = newWidth + if i == 0 { + currentWidth = item.rawWidth + } + } + } + + // Add the last line + if currentLine != "" { + lines = append(lines, currentLine) + } + + return helpBarStyle.Render(strings.Join(lines, "\n")) } func (m *Model) statusBar() string { diff --git a/internal/tui/backups.go b/internal/tui/backups.go index 21ac2aa..2c4b943 100644 --- a/internal/tui/backups.go +++ b/internal/tui/backups.go @@ -174,7 +174,7 @@ func (b *BackupPicker) selectView() string { } leftSb.WriteString("\n") - leftSb.WriteString(helpDescStyle.Render("↑↓ navigate • Enter restore • Esc cancel")) + leftSb.WriteString(WrapHelpText("↑↓ navigate • Enter restore • Esc cancel", 40)) // Build right panel (preview) var rightSb strings.Builder @@ -219,7 +219,8 @@ func (b *BackupPicker) selectView() string { // Show scroll indicator if len(lines) > previewHeight { rightSb.WriteString("\n") - rightSb.WriteString(helpDescStyle.Render(fmt.Sprintf("Lines %d-%d of %d (Shift+↑↓ scroll)", b.previewScroll+1, endLine, len(lines)))) + scrollText := fmt.Sprintf("%d-%d of %d (↑↓ scroll)", b.previewScroll+1, endLine, len(lines)) + rightSb.WriteString(helpDescStyle.Render(scrollText)) } } diff --git a/internal/tui/form.go b/internal/tui/form.go index 70b66d0..b9b54f7 100644 --- a/internal/tui/form.go +++ b/internal/tui/form.go @@ -291,7 +291,7 @@ func (f *Form) View() string { sb.WriteString("\n\n") sb.WriteString("\n") - sb.WriteString(helpDescStyle.Render("Tab/↓ next • Shift+Tab/↑ prev • ←→ select group • Enter save • Esc cancel")) + sb.WriteString(WrapHelpText("Tab/↓ next • Shift+Tab/↑ prev • ←→ select group • Enter save • Esc cancel", f.width-6)) return dialogStyle.Render(sb.String()) } diff --git a/internal/tui/groups.go b/internal/tui/groups.go index 55463e9..79da20f 100644 --- a/internal/tui/groups.go +++ b/internal/tui/groups.go @@ -190,7 +190,7 @@ func (g *GroupPicker) selectView() string { } sb.WriteString("\n\n") - sb.WriteString(helpDescStyle.Render("↑↓ navigate • n new • r rename • d delete • Esc back")) + sb.WriteString(WrapHelpText("↑↓ navigate • n new • r rename • d delete • Esc back", g.width-6)) return dialogStyle.Render(sb.String()) } diff --git a/internal/tui/presets.go b/internal/tui/presets.go index 13b7498..dda707d 100644 --- a/internal/tui/presets.go +++ b/internal/tui/presets.go @@ -411,7 +411,7 @@ func (p *PresetPicker) selectView() string { } sb.WriteString("\n") - sb.WriteString(helpDescStyle.Render("↑↓ navigate • Enter apply • n new • e edit • d delete • Esc cancel")) + sb.WriteString(WrapHelpText("↑↓ navigate • Enter apply • n new • e edit • d delete • Esc cancel", p.width-6)) return dialogStyle.Render(sb.String()) } @@ -483,7 +483,7 @@ func (p *PresetPicker) formView() string { } sb.WriteString("\n\n") - sb.WriteString(helpDescStyle.Render("Tab/↓ next • Enter select/save • Esc cancel")) + sb.WriteString(WrapHelpText("Tab/↓ next • Enter select/save • Esc cancel", p.width-6)) return dialogStyle.Render(sb.String()) } @@ -536,7 +536,7 @@ func (p *PresetPicker) pickerView() string { sb.WriteString(titleStyle.Render(title)) sb.WriteString("\n") - sb.WriteString(helpDescStyle.Render("Space to toggle • Enter to confirm • Esc to cancel")) + sb.WriteString(WrapHelpText("Space to toggle • Enter to confirm • Esc to cancel", p.width-6)) sb.WriteString("\n\n") filtered := p.getFilteredAliases() diff --git a/internal/tui/styles.go b/internal/tui/styles.go index ffa2bb6..b59d42b 100644 --- a/internal/tui/styles.go +++ b/internal/tui/styles.go @@ -2,6 +2,8 @@ package tui import ( + "strings" + "github.com/charmbracelet/lipgloss" ) @@ -148,3 +150,71 @@ func StatusText(enabled bool, pending bool, hasError bool) string { func HelpItem(key, desc string) string { return helpKeyStyle.Render(key) + " " + helpDescStyle.Render(desc) } + +// WrapHelpText wraps help text to fit within maxWidth, splitting on bullet separators. +// If maxWidth is 0 or negative, returns the original text. +func WrapHelpText(text string, maxWidth int) string { + if maxWidth <= 0 { + return helpDescStyle.Render(text) + } + + separator := " • " + parts := splitOnSeparator(text, separator) + + var lines []string + var currentLine string + var currentWidth int + + for i, part := range parts { + partWidth := len(part) + sepWidth := 3 // len(" • ") + + newWidth := currentWidth + partWidth + if currentWidth > 0 { + newWidth += sepWidth + } + + if newWidth > maxWidth && currentWidth > 0 { + lines = append(lines, currentLine) + currentLine = part + currentWidth = partWidth + } else { + if currentWidth > 0 { + currentLine += separator + } + currentLine += part + if i == 0 { + currentWidth = partWidth + } else { + currentWidth = newWidth + } + } + } + + if currentLine != "" { + lines = append(lines, currentLine) + } + + // Apply style to each line and join + var result []string + for _, line := range lines { + result = append(result, helpDescStyle.Render(line)) + } + + return strings.Join(result, "\n") +} + +// splitOnSeparator splits a string on the given separator. +func splitOnSeparator(s, sep string) []string { + var parts []string + for { + idx := strings.Index(s, sep) + if idx == -1 { + parts = append(parts, s) + break + } + parts = append(parts, s[:idx]) + s = s[idx+len(sep):] + } + return parts +}