Update menus to wrap for smaller screens.

This commit is contained in:
2025-11-29 02:09:54 +00:00
parent 649227b201
commit 518879dc56
3 changed files with 201 additions and 39 deletions
+82 -15
View File
@@ -602,20 +602,82 @@ func (m model) renderMainView() string {
footerStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
keyStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("220"))
footer := fmt.Sprintf("%s/%s: Navigate %s: Toggle %s: New %s: Edit %s: Delete %s: Bench %s: Logs %s: Quit │ Total: %d",
keyStyle.Render("↑↓"),
keyStyle.Render("jk"),
keyStyle.Render("Space"),
keyStyle.Render("n"),
keyStyle.Render("e"),
keyStyle.Render("d"),
keyStyle.Render("b"),
keyStyle.Render("l"),
keyStyle.Render("q"),
len(m.ui.forwardOrder))
// Get terminal width for footer wrapping
termWidth := m.termWidth
if termWidth == 0 {
termWidth = 120
}
// Fill space to push footer to bottom (reserve 2 lines: 1 for spacing, 1 for footer)
footerHeight := 2
// Define key bindings as structured data for flexible rendering
type keyBinding struct {
key string
desc string
}
bindings := []keyBinding{
{"↑↓/jk", "Navigate"},
{"Space", "Toggle"},
{"n", "New"},
{"e", "Edit"},
{"d", "Delete"},
{"b", "Bench"},
{"l", "Logs"},
{"q", "Quit"},
}
// Build footer lines that fit within terminal width
var footerLines []string
var currentLine strings.Builder
currentLineVisualLen := 0
// Calculate how much space we need for the total count suffix
totalSuffix := fmt.Sprintf(" │ Total: %d", len(m.ui.forwardOrder))
totalSuffixLen := len(totalSuffix)
// Available width (account for some margin)
availableWidth := termWidth - 4
for i, binding := range bindings {
// Build this binding's text
keyRendered := keyStyle.Render(binding.key)
bindingText := keyRendered + ": " + binding.desc
// Visual length without ANSI codes
bindingVisualLen := len(binding.key) + 2 + len(binding.desc)
// Add separator if not first item on line
separator := ""
separatorLen := 0
if currentLine.Len() > 0 {
separator = " "
separatorLen = 2
}
// Check if this binding fits on current line
// For the last binding, also need to fit the total suffix
neededWidth := currentLineVisualLen + separatorLen + bindingVisualLen
if i == len(bindings)-1 {
neededWidth += totalSuffixLen
}
if neededWidth > availableWidth && currentLine.Len() > 0 {
// Start a new line
footerLines = append(footerLines, currentLine.String())
currentLine.Reset()
currentLineVisualLen = 0
separator = ""
separatorLen = 0
}
currentLine.WriteString(separator)
currentLine.WriteString(bindingText)
currentLineVisualLen += separatorLen + bindingVisualLen
}
// Add total count to the last line
currentLine.WriteString(totalSuffix)
footerLines = append(footerLines, currentLine.String())
// Calculate footer height
footerHeight := len(footerLines) + 1 // +1 for the blank line before footer
remainingLines := termHeight - currentLines - footerHeight
if remainingLines > 0 {
b.WriteString(strings.Repeat("\n", remainingLines))
@@ -623,7 +685,12 @@ func (m model) renderMainView() string {
// Add footer at bottom
b.WriteString("\n")
b.WriteString(footerStyle.Render(footer))
for i, line := range footerLines {
if i > 0 {
b.WriteString("\n")
}
b.WriteString(footerStyle.Render(line))
}
return b.String()
}
@@ -736,7 +803,7 @@ func (m model) renderDeleteConfirmation() string {
}
b.WriteString("\n\n")
b.WriteString(helpStyle.Render("←/→: Navigate Enter: Confirm Esc: Cancel"))
b.WriteString(wrapHelpText("←/→: Navigate Enter: Confirm Esc: Cancel", wizardHelpWidth(m.termWidth)))
// Wrap in a box using wizard style
boxStyle := lipgloss.NewStyle().
+90
View File
@@ -194,6 +194,96 @@ func renderTextInput(label, value string, valid bool) string {
return b.String()
}
// wizardHelpWidth returns an appropriate width for wizard help text
// based on terminal width. For modals, we use a sensible maximum.
func wizardHelpWidth(termWidth int) int {
if termWidth == 0 {
termWidth = 80
}
// Wizard modals shouldn't be wider than 70 chars typically
// but on narrow terminals, use available space minus padding
maxWidth := 70
available := termWidth - 10 // account for modal borders and padding
if available < maxWidth {
return available
}
return maxWidth
}
// wrapHelpText wraps help text to fit within the given width.
// Help text is expected to be in the format "key: action key: action ..."
// separated by double spaces. On smaller screens, it wraps to multiple lines.
func wrapHelpText(text string, width int) string {
if width <= 0 {
width = 80 // Default width
}
// Account for some padding/margin
availableWidth := width - 4
if availableWidth < 20 {
availableWidth = 20
}
// If text fits, return as-is
if len(text) <= availableWidth {
return helpStyle.Render(text)
}
// Split by double-space separator (common in help text)
parts := strings.Split(text, " ")
if len(parts) <= 1 {
// No double-space separators, just truncate
if len(text) > availableWidth-3 {
return helpStyle.Render(text[:availableWidth-3] + "...")
}
return helpStyle.Render(text)
}
var lines []string
var currentLine strings.Builder
for i, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
// Check if adding this part would exceed width
addition := part
if currentLine.Len() > 0 {
addition = " " + part
}
if currentLine.Len()+len(addition) > availableWidth && currentLine.Len() > 0 {
// Start new line
lines = append(lines, currentLine.String())
currentLine.Reset()
currentLine.WriteString(part)
} else {
if currentLine.Len() > 0 {
currentLine.WriteString(" ")
}
currentLine.WriteString(part)
}
// Handle last part
if i == len(parts)-1 && currentLine.Len() > 0 {
lines = append(lines, currentLine.String())
}
}
// Join with newlines and apply style to each line
var result strings.Builder
for i, line := range lines {
if i > 0 {
result.WriteString("\n")
}
result.WriteString(helpStyle.Render(line))
}
return result.String()
}
// overlayContent overlays modal content centered on the base view
// Note: base parameter is kept for API compatibility but not used since
// lipgloss.Place provides cleaner centering without background artifacts
+29 -24
View File
@@ -109,10 +109,11 @@ func (m model) renderSelectContext() string {
}
b.WriteString("\n")
helpWidth := wizardHelpWidth(m.termWidth)
if wizard.searchFilter != "" {
b.WriteString(helpStyle.Render(fmt.Sprintf("↑/↓: Navigate Enter: Select Backspace: Clear filter (%d/%d) Esc: Cancel", len(wizard.getFilteredContexts()), len(wizard.contexts))))
b.WriteString(wrapHelpText(fmt.Sprintf("↑/↓: Navigate Enter: Select Backspace: Clear filter (%d/%d) Esc: Cancel", len(wizard.getFilteredContexts()), len(wizard.contexts)), helpWidth))
} else {
b.WriteString(helpStyle.Render("Type to filter ↑/↓: Navigate Enter: Select Esc/Ctrl+C: Cancel"))
b.WriteString(wrapHelpText("Type to filter ↑/↓: Navigate Enter: Select Esc/Ctrl+C: Cancel", helpWidth))
}
return b.String()
@@ -150,10 +151,11 @@ func (m model) renderSelectNamespace() string {
}
b.WriteString("\n")
helpWidth := wizardHelpWidth(m.termWidth)
if wizard.searchFilter != "" {
b.WriteString(helpStyle.Render(fmt.Sprintf("↑/↓: Navigate Enter: Select Backspace: Clear filter (%d/%d) Esc: Back", len(wizard.getFilteredNamespaces()), len(wizard.namespaces))))
b.WriteString(wrapHelpText(fmt.Sprintf("↑/↓: Navigate Enter: Select Backspace: Clear filter (%d/%d) Esc: Back", len(wizard.getFilteredNamespaces()), len(wizard.namespaces)), helpWidth))
} else {
b.WriteString(helpStyle.Render("Type to filter ↑/↓: Navigate Enter: Select Esc: Back Ctrl+C: Cancel"))
b.WriteString(wrapHelpText("Type to filter ↑/↓: Navigate Enter: Select Esc: Back Ctrl+C: Cancel", helpWidth))
}
return b.String()
@@ -192,7 +194,7 @@ func (m model) renderSelectResourceType() string {
}
b.WriteString("\n")
b.WriteString(helpStyle.Render("↑/↓: Navigate Enter: Select Esc: Back Ctrl+C: Cancel"))
b.WriteString(wrapHelpText("↑/↓: Navigate Enter: Select Esc: Back Ctrl+C: Cancel", wizardHelpWidth(m.termWidth)))
return b.String()
}
@@ -305,14 +307,15 @@ func (m model) renderEnterResource() string {
b.WriteString("\n")
// Show appropriate help text based on resource type and filter state
helpWidth := wizardHelpWidth(m.termWidth)
if wizard.selectedResourceType == ResourceTypeService {
if wizard.searchFilter != "" {
b.WriteString(helpStyle.Render(fmt.Sprintf("↑/↓: Navigate Enter: Select Backspace: Clear filter (%d/%d) Esc: Back", len(wizard.getFilteredServices()), len(wizard.services))))
b.WriteString(wrapHelpText(fmt.Sprintf("↑/↓: Navigate Enter: Select Backspace: Clear filter (%d/%d) Esc: Back", len(wizard.getFilteredServices()), len(wizard.services)), helpWidth))
} else {
b.WriteString(helpStyle.Render("Type to filter ↑/↓: Navigate Enter: Select Esc: Back Ctrl+C: Cancel"))
b.WriteString(wrapHelpText("Type to filter ↑/↓: Navigate Enter: Select Esc: Back Ctrl+C: Cancel", helpWidth))
}
} else {
b.WriteString(helpStyle.Render("Enter: Continue Esc: Back Ctrl+C: Cancel"))
b.WriteString(wrapHelpText("Enter: Continue Esc: Back Ctrl+C: Cancel", helpWidth))
}
return b.String()
@@ -403,7 +406,7 @@ func (m model) renderEnterRemotePort() string {
}
b.WriteString("\n")
b.WriteString(helpStyle.Render("↑/↓: Navigate Enter: Select Esc: Back Ctrl+C: Cancel"))
b.WriteString(wrapHelpText("↑/↓: Navigate Enter: Select Esc: Back Ctrl+C: Cancel", wizardHelpWidth(m.termWidth)))
} else {
// Text input mode (no detected ports or user chose manual entry)
if len(wizard.detectedPorts) > 0 {
@@ -436,7 +439,7 @@ func (m model) renderEnterRemotePort() string {
}
b.WriteString("\n")
b.WriteString(helpStyle.Render("Enter: Continue Esc: Back Ctrl+C: Cancel"))
b.WriteString(wrapHelpText("Enter: Continue Esc: Back Ctrl+C: Cancel", wizardHelpWidth(m.termWidth)))
}
return b.String()
@@ -477,7 +480,7 @@ func (m model) renderEnterLocalPort() string {
}
b.WriteString("\n")
b.WriteString(helpStyle.Render("Enter: Continue Esc: Back Ctrl+C: Cancel"))
b.WriteString(wrapHelpText("Enter: Continue Esc: Back Ctrl+C: Cancel", wizardHelpWidth(m.termWidth)))
return b.String()
}
@@ -535,7 +538,7 @@ func (m model) renderConfirmation() string {
}
b.WriteString("\n")
b.WriteString(helpStyle.Render("↑/↓/Tab: Navigate Enter: Confirm Esc: Back"))
b.WriteString(wrapHelpText("↑/↓/Tab: Navigate Enter: Confirm Esc: Back", wizardHelpWidth(m.termWidth)))
return b.String()
}
@@ -578,7 +581,7 @@ func (m model) renderSuccess() string {
}
b.WriteString("\n")
b.WriteString(helpStyle.Render("↑/↓: Navigate Enter: Select"))
b.WriteString(wrapHelpText("↑/↓: Navigate Enter: Select", wizardHelpWidth(m.termWidth)))
return b.String()
}
@@ -641,7 +644,7 @@ func (m model) renderRemoveSelection() string {
selectedCount := wizard.getSelectedCount()
b.WriteString(fmt.Sprintf("%d of %d selected\n\n", selectedCount, len(wizard.forwards)))
b.WriteString(helpStyle.Render("Space: Toggle a: All n: None Enter: Remove Esc: Cancel"))
b.WriteString(wrapHelpText("Space: Toggle a: All n: None Enter: Remove Esc: Cancel", wizardHelpWidth(m.termWidth)))
return b.String()
}
@@ -676,7 +679,7 @@ func (m model) renderRemoveConfirmation() string {
}
b.WriteString("\n")
b.WriteString(helpStyle.Render("↑/↓: Navigate Enter: Confirm Esc: Cancel"))
b.WriteString(wrapHelpText("↑/↓: Navigate Enter: Confirm Esc: Cancel", wizardHelpWidth(m.termWidth)))
return b.String()
}
@@ -740,7 +743,7 @@ func (m model) renderBenchmarkConfig() string {
b.WriteString("\n")
b.WriteString(mutedStyle.Render(fmt.Sprintf("Will send %d requests with %d concurrent workers", state.requests, state.concurrency)))
b.WriteString("\n\n")
b.WriteString(helpStyle.Render("↑/↓/Tab: Navigate Type to edit Enter: Run Esc: Cancel"))
b.WriteString(wrapHelpText("↑/↓/Tab: Navigate Type to edit Enter: Run Esc: Cancel", wizardHelpWidth(m.termWidth)))
return b.String()
}
@@ -780,7 +783,7 @@ func (m model) renderBenchmarkRunning() string {
b.WriteString(mutedStyle.Render(fmt.Sprintf("Method: %s Concurrency: %d", state.method, state.concurrency)))
b.WriteString("\n\n")
b.WriteString(helpStyle.Render("Please wait..."))
b.WriteString(wrapHelpText("Please wait...", wizardHelpWidth(m.termWidth)))
return b.String()
}
@@ -796,14 +799,14 @@ func (m model) renderBenchmarkResults() string {
if state.error != nil {
b.WriteString(errorStyle.Render(fmt.Sprintf("✗ Error: %v", state.error)))
b.WriteString("\n\n")
b.WriteString(helpStyle.Render("Press Enter or Esc to return"))
b.WriteString(wrapHelpText("Press Enter or Esc to return", wizardHelpWidth(m.termWidth)))
return b.String()
}
if state.results == nil {
b.WriteString(mutedStyle.Render("No results available"))
b.WriteString("\n\n")
b.WriteString(helpStyle.Render("Press Enter or Esc to return"))
b.WriteString(wrapHelpText("Press Enter or Esc to return", wizardHelpWidth(m.termWidth)))
return b.String()
}
@@ -872,7 +875,7 @@ func (m model) renderBenchmarkResults() string {
}
b.WriteString("\n")
b.WriteString(helpStyle.Render("Press Enter or Esc to return"))
b.WriteString(wrapHelpText("Press Enter or Esc to return", wizardHelpWidth(m.termWidth)))
return b.String()
}
@@ -1076,8 +1079,10 @@ func (m model) renderHTTPLog() string {
}
b.WriteString("\n")
// Help line at bottom
b.WriteString(helpStyle.Render(" ↑/↓: Navigate Enter: Details a: Auto-scroll f: Filter /: Search c: Clear q: Close"))
// Help line at bottom (wrap for smaller screens)
helpText := "↑/↓: Navigate Enter: Details a: Auto-scroll f: Filter /: Search c: Clear q: Close"
b.WriteString(" ")
b.WriteString(wrapHelpText(helpText, termWidth-4))
return b.String()
}
@@ -1273,9 +1278,9 @@ func (m model) renderHTTPLogDetail(entry HTTPLogEntry, termWidth, termHeight int
if state.copyMessage != "" {
b.WriteString(successStyle.Render(state.copyMessage))
b.WriteString(" ")
b.WriteString(helpStyle.Render("↑/↓: Scroll c: Copy Esc: Back"))
b.WriteString(wrapHelpText("↑/↓: Scroll c: Copy Esc: Back", termWidth-10))
} else {
b.WriteString(helpStyle.Render("↑/↓/PgUp/PgDn: Scroll g: Top c: Copy response Esc: Back"))
b.WriteString(wrapHelpText("↑/↓/PgUp/PgDn: Scroll g: Top c: Copy response Esc: Back", termWidth-10))
}
return b.String()