mirror of
https://github.com/lukaszraczylo/kportal.git
synced 2026-07-12 07:41:42 +00:00
Optimisation & fixes
This commit is contained in:
+40
-15
@@ -448,6 +448,7 @@ type keyBinding struct {
|
||||
func mainViewKeyBindings() []keyBinding {
|
||||
return []keyBinding{
|
||||
{"↑↓/jk", "Navigate"},
|
||||
{"PgUp/Dn", "Page"},
|
||||
{"Space", "Toggle"},
|
||||
{"n", "New"},
|
||||
{"e", "Edit"},
|
||||
@@ -480,7 +481,7 @@ func (m model) renderMainView() string {
|
||||
|
||||
// Render error section if any errors exist
|
||||
if len(m.ui.errors) > 0 {
|
||||
b.WriteString(m.renderErrorSection())
|
||||
b.WriteString(m.renderErrorSection(termWidth))
|
||||
}
|
||||
|
||||
// Render footer with proper spacing
|
||||
@@ -527,10 +528,14 @@ func (m model) renderTitle(headerColor lipgloss.Color) string {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// renderEmptyMessage renders the message shown when no forwards are configured
|
||||
// renderEmptyMessage renders the message shown when no forwards are configured.
|
||||
// It includes an actionable hint so a first-time user knows how to proceed.
|
||||
func (m model) renderEmptyMessage(mutedColor lipgloss.Color) string {
|
||||
disabledStyle := lipgloss.NewStyle().Foreground(mutedColor)
|
||||
return disabledStyle.Render("No forwards configured\n")
|
||||
mutedStyle := lipgloss.NewStyle().Foreground(mutedColor)
|
||||
hintStyle := lipgloss.NewStyle().Foreground(highlightColor)
|
||||
return mutedStyle.Render("No forwards configured") + "\n\n" +
|
||||
hintStyle.Render(" Press ") + selectedStyle.Render("n") +
|
||||
hintStyle.Render(" to add your first port forward.") + "\n"
|
||||
}
|
||||
|
||||
// renderForwardsTable renders the forwards table with all styling
|
||||
@@ -655,10 +660,12 @@ func (m model) createTableStyleFunc(colors mainViewColors) func(row, col int) li
|
||||
}
|
||||
}
|
||||
|
||||
// renderErrorSection renders the error display section
|
||||
func (m model) renderErrorSection() string {
|
||||
// renderErrorSection renders the error display section, sized to the terminal.
|
||||
func (m model) renderErrorSection(termWidth int) string {
|
||||
var b strings.Builder
|
||||
|
||||
width := errorWidth(termWidth)
|
||||
|
||||
b.WriteString("\n\n")
|
||||
errorHeaderStyle := lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
@@ -669,28 +676,44 @@ func (m model) renderErrorSection() string {
|
||||
|
||||
errorLineStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("196")).
|
||||
Width(ErrorDisplayWidth).
|
||||
MaxWidth(ErrorDisplayWidth)
|
||||
Width(width).
|
||||
MaxWidth(width)
|
||||
|
||||
for id, errMsg := range m.ui.errors {
|
||||
// Find the forward to display its alias
|
||||
if fwd, ok := m.ui.forwards[id]; ok {
|
||||
b.WriteString(m.renderErrorLine(fwd.Alias, errMsg, errorLineStyle))
|
||||
b.WriteString(m.renderErrorLine(fwd.Alias, errMsg, width, errorLineStyle))
|
||||
}
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// errorWidth clamps the error display to the terminal width, falling back to the
|
||||
// default cap so errors never overflow narrow terminals nor sprawl on wide ones.
|
||||
func errorWidth(termWidth int) int {
|
||||
width := ErrorDisplayWidth
|
||||
if termWidth > 0 && termWidth-2 < width {
|
||||
width = termWidth - 2
|
||||
}
|
||||
if width < 20 {
|
||||
width = 20
|
||||
}
|
||||
return width
|
||||
}
|
||||
|
||||
// renderErrorLine renders a single error line with proper wrapping
|
||||
func (m model) renderErrorLine(alias, errMsg string, style lipgloss.Style) string {
|
||||
func (m model) renderErrorLine(alias, errMsg string, width int, style lipgloss.Style) string {
|
||||
var b strings.Builder
|
||||
|
||||
// Format: " • alias: error message"
|
||||
prefix := fmt.Sprintf(" • %s: ", alias)
|
||||
|
||||
// Wrap the error message if it's too long
|
||||
maxErrLen := ErrorDisplayWidth - len(prefix)
|
||||
maxErrLen := width - len(prefix)
|
||||
if maxErrLen < 1 {
|
||||
maxErrLen = 1
|
||||
}
|
||||
wrappedMsg := wrapText(errMsg, maxErrLen)
|
||||
|
||||
// Render first line with prefix
|
||||
@@ -750,9 +773,10 @@ func (m model) buildFooterLines(termWidth int) []string {
|
||||
var currentLine strings.Builder
|
||||
currentLineVisualLen := 0
|
||||
|
||||
// Calculate how much space we need for the total count suffix
|
||||
// Calculate how much space we need for the total count suffix.
|
||||
// Use lipgloss.Width for true display width (the "│" glyph is 3 bytes / 1 col).
|
||||
totalSuffix := fmt.Sprintf(" │ Total: %d", len(m.ui.forwardOrder))
|
||||
totalSuffixLen := len(totalSuffix)
|
||||
totalSuffixLen := lipgloss.Width(totalSuffix)
|
||||
|
||||
// Available width (account for some margin)
|
||||
availableWidth := termWidth - 4
|
||||
@@ -761,8 +785,9 @@ func (m model) buildFooterLines(termWidth int) []string {
|
||||
// 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)
|
||||
// True display width: strips ANSI and counts wide/unicode glyphs (e.g. ↑↓)
|
||||
// correctly, where len() would over-count multibyte runes and wrap early.
|
||||
bindingVisualLen := lipgloss.Width(bindingText)
|
||||
|
||||
// Add separator if not first item on line
|
||||
separator := ""
|
||||
|
||||
@@ -43,3 +43,15 @@ const (
|
||||
// MaxPathWidth is the maximum width for displaying file paths
|
||||
MaxPathWidth = 48
|
||||
)
|
||||
|
||||
// HTTP log table layout
|
||||
const (
|
||||
// HTTPLogRowFormat is the column layout shared by the HTTP-log table header
|
||||
// and its rows (TIME, METHOD, STATUS, LATENCY, PATH) so they stay aligned.
|
||||
HTTPLogRowFormat = "%-10s %-7s %-6s %-8s %s"
|
||||
|
||||
// HTTPLogFixedCols is the width consumed by every column except PATH
|
||||
// (prefix + the four fixed columns and their separators), used to size the
|
||||
// remaining space for the path column responsively.
|
||||
HTTPLogFixedCols = 48
|
||||
)
|
||||
|
||||
+11
-4
@@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/lukaszraczylo/kportal/internal/config"
|
||||
)
|
||||
@@ -195,15 +196,21 @@ func hyperlink(url, text string) string {
|
||||
return fmt.Sprintf("\x1b]8;;%s\x1b\\%s\x1b]8;;\x1b\\", url, text)
|
||||
}
|
||||
|
||||
// truncate truncates a string to maxLen, adding "..." if needed
|
||||
// truncate truncates a string to maxLen runes, adding "..." if needed.
|
||||
// It counts and slices by rune (not byte) so multibyte aliases/resource names
|
||||
// are never cut mid-rune into mojibake.
|
||||
func truncate(s string, maxLen int) string {
|
||||
if len(s) <= maxLen {
|
||||
if maxLen <= 0 {
|
||||
return ""
|
||||
}
|
||||
if utf8.RuneCountInString(s) <= maxLen {
|
||||
return s
|
||||
}
|
||||
r := []rune(s)
|
||||
if maxLen <= 3 {
|
||||
return s[:maxLen]
|
||||
return string(r[:maxLen])
|
||||
}
|
||||
return s[:maxLen-3] + "..."
|
||||
return string(r[:maxLen-3]) + "..."
|
||||
}
|
||||
|
||||
// formatStatusWithIndicator adds color-coded indicator symbols to status
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/lukaszraczylo/kportal/internal/config"
|
||||
@@ -140,10 +141,14 @@ func TestTruncate(t *testing.T) {
|
||||
{"hi!", "hi", 2}, // maxLen <= 3 branch: no ellipsis
|
||||
{"abcd", "abc", 3}, // maxLen <= 3 branch
|
||||
{"", "", 5},
|
||||
{"café-service", "café-...", 8}, // multibyte: count/slice by rune, no mojibake
|
||||
{"日本語ポッド", "日本...", 5}, // CJK runes truncated cleanly
|
||||
{"naïve", "naïve", 5}, // exactly maxLen runes (6 bytes) — unchanged
|
||||
{"abc", "", 0}, // non-positive maxLen
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input+"_"+string(rune('0'+tt.maxLen)), func(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("%s_%d", tt.input, tt.maxLen), func(t *testing.T) {
|
||||
assert.Equal(t, tt.expected, truncate(tt.input, tt.maxLen))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -139,6 +139,18 @@ func renderBreadcrumb(parts ...string) string {
|
||||
return breadcrumbStyle.Render(strings.Join(parts, " / "))
|
||||
}
|
||||
|
||||
// scrollUpIndicator returns the styled "more items above" hint shown when a
|
||||
// viewport is scrolled down past its first item.
|
||||
func scrollUpIndicator() string {
|
||||
return mutedStyle.Render(" ↑ More above ↑") + "\n"
|
||||
}
|
||||
|
||||
// scrollDownIndicator returns the styled "more items below" hint shown when a
|
||||
// viewport has items beyond its last visible row.
|
||||
func scrollDownIndicator() string {
|
||||
return mutedStyle.Render(" ↓ More below ↓") + "\n"
|
||||
}
|
||||
|
||||
// renderList renders a list of items with cursor selection and viewport scrolling
|
||||
func renderList(items []string, cursor int, prefix string, scrollOffset int) string {
|
||||
var b strings.Builder
|
||||
@@ -147,7 +159,7 @@ func renderList(items []string, cursor int, prefix string, scrollOffset int) str
|
||||
|
||||
// Show scroll up indicator if there are items above the viewport
|
||||
if scrollOffset > 0 {
|
||||
b.WriteString(mutedStyle.Render(" ↑ More above ↑") + "\n")
|
||||
b.WriteString(scrollUpIndicator())
|
||||
}
|
||||
|
||||
// Calculate visible range
|
||||
@@ -171,7 +183,7 @@ func renderList(items []string, cursor int, prefix string, scrollOffset int) str
|
||||
|
||||
// Show scroll down indicator if there are items below the viewport
|
||||
if end < totalItems {
|
||||
b.WriteString(mutedStyle.Render(" ↓ More below ↓") + "\n")
|
||||
b.WriteString(scrollDownIndicator())
|
||||
}
|
||||
|
||||
return b.String()
|
||||
|
||||
+62
-93
@@ -9,8 +9,23 @@ import (
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/lukaszraczylo/kportal/internal/k8s"
|
||||
)
|
||||
|
||||
// formatDetectedPort renders a discovered port for the port-selection list,
|
||||
// e.g. "8080", "80 → 8000" (service target differs), optionally "(http)".
|
||||
func formatDetectedPort(port k8s.PortInfo) string {
|
||||
desc := fmt.Sprintf("%d", port.Port)
|
||||
if port.TargetPort > 0 && port.TargetPort != port.Port {
|
||||
desc = fmt.Sprintf("%d → %d", port.Port, port.TargetPort)
|
||||
}
|
||||
if port.Name != "" {
|
||||
desc += fmt.Sprintf(" (%s)", port.Name)
|
||||
}
|
||||
return desc
|
||||
}
|
||||
|
||||
// renderAddWizard renders the appropriate step of the add wizard
|
||||
func (m model) renderAddWizard() string {
|
||||
if m.ui.addWizard == nil {
|
||||
@@ -61,24 +76,25 @@ func (m model) renderSelectContext() string {
|
||||
b.WriteString(spinnerStyle.Render("⣾ Loading contexts..."))
|
||||
} else if wizard.error != nil {
|
||||
b.WriteString(errorStyle.Render(fmt.Sprintf("✗ Error: %v", wizard.error)))
|
||||
b.WriteString(mutedStyle.Render("\n\nCould not read kubeconfig. Press Esc to cancel."))
|
||||
} else if len(wizard.contexts) == 0 {
|
||||
b.WriteString(mutedStyle.Render("No contexts found in kubeconfig"))
|
||||
b.WriteString(mutedStyle.Render("\n\nAdd one with: kubectl config use-context <name>"))
|
||||
} else {
|
||||
filteredContexts := wizard.getFilteredContexts()
|
||||
if len(filteredContexts) == 0 {
|
||||
b.WriteString(mutedStyle.Render("No matching contexts"))
|
||||
} else {
|
||||
const viewportHeight = 20
|
||||
totalItems := len(filteredContexts)
|
||||
|
||||
// Show scroll up indicator if there are items above the viewport
|
||||
if wizard.scrollOffset > 0 {
|
||||
b.WriteString(mutedStyle.Render(" ↑ More above ↑") + "\n")
|
||||
b.WriteString(scrollUpIndicator())
|
||||
}
|
||||
|
||||
// Calculate visible range
|
||||
start := wizard.scrollOffset
|
||||
end := wizard.scrollOffset + viewportHeight
|
||||
end := wizard.scrollOffset + ViewportHeight
|
||||
if end > totalItems {
|
||||
end = totalItems
|
||||
}
|
||||
@@ -103,7 +119,7 @@ func (m model) renderSelectContext() string {
|
||||
|
||||
// Show scroll down indicator if there are items below the viewport
|
||||
if end < totalItems {
|
||||
b.WriteString(mutedStyle.Render(" ↓ More below ↓") + "\n")
|
||||
b.WriteString(scrollDownIndicator())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,7 +140,7 @@ func (m model) renderSelectNamespace() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(renderHeader("Add Port Forward", renderProgress(2, 7)))
|
||||
b.WriteString(fmt.Sprintf("Context: %s\n\n", breadcrumbStyle.Render(wizard.selectedContext)))
|
||||
fmt.Fprintf(&b, "Context: %s\n\n", breadcrumbStyle.Render(wizard.selectedContext))
|
||||
|
||||
b.WriteString("Select Namespace:\n\n")
|
||||
|
||||
@@ -342,39 +358,23 @@ func (m model) renderEnterRemotePort() string {
|
||||
b.WriteString("Select remote port:")
|
||||
b.WriteString("\n\n")
|
||||
|
||||
const viewportHeight = 20
|
||||
totalItems := len(wizard.detectedPorts) + 1 // +1 for manual entry option
|
||||
|
||||
// Show scroll up indicator if there are items above the viewport
|
||||
if wizard.scrollOffset > 0 {
|
||||
b.WriteString(mutedStyle.Render(" ↑ More above ↑") + "\n")
|
||||
b.WriteString(scrollUpIndicator())
|
||||
}
|
||||
|
||||
// Calculate visible range
|
||||
start := wizard.scrollOffset
|
||||
end := wizard.scrollOffset + viewportHeight
|
||||
end := wizard.scrollOffset + ViewportHeight
|
||||
if end > totalItems {
|
||||
end = totalItems
|
||||
}
|
||||
|
||||
// Render detected ports within viewport
|
||||
for i := start; i < end && i < len(wizard.detectedPorts); i++ {
|
||||
port := wizard.detectedPorts[i]
|
||||
// For services, show both service port and target port if they differ
|
||||
var portDesc string
|
||||
if port.TargetPort > 0 && port.TargetPort != port.Port {
|
||||
// Service with different target port: "80 → 8000 (http)"
|
||||
portDesc = fmt.Sprintf("%d → %d", port.Port, port.TargetPort)
|
||||
if port.Name != "" {
|
||||
portDesc += fmt.Sprintf(" (%s)", port.Name)
|
||||
}
|
||||
} else {
|
||||
// Pod port or service with same port
|
||||
portDesc = fmt.Sprintf("%d", port.Port)
|
||||
if port.Name != "" {
|
||||
portDesc += fmt.Sprintf(" (%s)", port.Name)
|
||||
}
|
||||
}
|
||||
portDesc := formatDetectedPort(wizard.detectedPorts[i])
|
||||
|
||||
prefix := " "
|
||||
if i == wizard.cursor {
|
||||
@@ -402,7 +402,7 @@ func (m model) renderEnterRemotePort() string {
|
||||
|
||||
// Show scroll down indicator if there are items below the viewport
|
||||
if end < totalItems {
|
||||
b.WriteString(mutedStyle.Render(" ↓ More below ↓") + "\n")
|
||||
b.WriteString(scrollDownIndicator())
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
@@ -412,19 +412,7 @@ func (m model) renderEnterRemotePort() string {
|
||||
if len(wizard.detectedPorts) > 0 {
|
||||
b.WriteString(mutedStyle.Render("Detected ports:\n"))
|
||||
for _, port := range wizard.detectedPorts {
|
||||
var portDesc string
|
||||
if port.TargetPort > 0 && port.TargetPort != port.Port {
|
||||
portDesc = fmt.Sprintf("%d → %d", port.Port, port.TargetPort)
|
||||
if port.Name != "" {
|
||||
portDesc += fmt.Sprintf(" (%s)", port.Name)
|
||||
}
|
||||
} else {
|
||||
portDesc = fmt.Sprintf("%d", port.Port)
|
||||
if port.Name != "" {
|
||||
portDesc += fmt.Sprintf(" (%s)", port.Name)
|
||||
}
|
||||
}
|
||||
b.WriteString(mutedStyle.Render(fmt.Sprintf(" • %s\n", portDesc)))
|
||||
b.WriteString(mutedStyle.Render(fmt.Sprintf(" • %s\n", formatDetectedPort(port))))
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
@@ -503,18 +491,18 @@ func (m model) renderConfirmation() string {
|
||||
resourceInfo = fmt.Sprintf("service/%s", wizard.resourceValue)
|
||||
}
|
||||
|
||||
b.WriteString(fmt.Sprintf(" Context: %s\n", wizard.selectedContext))
|
||||
b.WriteString(fmt.Sprintf(" Namespace: %s\n", wizard.selectedNamespace))
|
||||
b.WriteString(fmt.Sprintf(" Resource: %s\n", resourceInfo))
|
||||
b.WriteString(fmt.Sprintf(" Remote Port: %d\n", wizard.remotePort))
|
||||
b.WriteString(fmt.Sprintf(" Local Port: %d\n", wizard.localPort))
|
||||
fmt.Fprintf(&b, " Context: %s\n", wizard.selectedContext)
|
||||
fmt.Fprintf(&b, " Namespace: %s\n", wizard.selectedNamespace)
|
||||
fmt.Fprintf(&b, " Resource: %s\n", resourceInfo)
|
||||
fmt.Fprintf(&b, " Remote Port: %d\n", wizard.remotePort)
|
||||
fmt.Fprintf(&b, " Local Port: %d\n", wizard.localPort)
|
||||
b.WriteString(" Protocol: tcp\n")
|
||||
|
||||
httpLogMark := "[ ] disabled"
|
||||
if wizard.httpLog {
|
||||
httpLogMark = "[x] enabled"
|
||||
}
|
||||
b.WriteString(fmt.Sprintf(" HTTP Log: %s\n", httpLogMark))
|
||||
fmt.Fprintf(&b, " HTTP Log: %s\n", httpLogMark)
|
||||
|
||||
b.WriteString("\n")
|
||||
|
||||
@@ -648,7 +636,7 @@ func (m model) renderRemoveSelection() string {
|
||||
}
|
||||
|
||||
selectedCount := wizard.getSelectedCount()
|
||||
b.WriteString(fmt.Sprintf("%d of %d selected\n\n", selectedCount, len(wizard.forwards)))
|
||||
fmt.Fprintf(&b, "%d of %d selected\n\n", selectedCount, len(wizard.forwards))
|
||||
|
||||
b.WriteString(wrapHelpText("Space: Toggle a: All n: None Enter: Remove Esc: Cancel", wizardHelpWidth(m.termWidth)))
|
||||
|
||||
@@ -663,7 +651,7 @@ func (m model) renderRemoveConfirmation() string {
|
||||
b.WriteString("\n")
|
||||
|
||||
selectedCount := wizard.getSelectedCount()
|
||||
b.WriteString(fmt.Sprintf("Remove %d port forward(s)?\n\n", selectedCount))
|
||||
fmt.Fprintf(&b, "Remove %d port forward(s)?\n\n", selectedCount)
|
||||
|
||||
selectedForwards := wizard.getSelectedForwards()
|
||||
for _, fwd := range selectedForwards {
|
||||
@@ -718,7 +706,7 @@ func (m model) renderBenchmarkConfig() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(renderHeader("HTTP Benchmark", ""))
|
||||
b.WriteString(fmt.Sprintf("Target: %s (localhost:%d)", breadcrumbStyle.Render(state.forwardAlias), state.localPort))
|
||||
fmt.Fprintf(&b, "Target: %s (localhost:%d)", breadcrumbStyle.Render(state.forwardAlias), state.localPort)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString("Configure benchmark parameters:")
|
||||
@@ -741,7 +729,7 @@ func (m model) renderBenchmarkConfig() string {
|
||||
b.WriteString(selectedStyle.Render(fmt.Sprintf("%s%-12s", prefix, field.label+":")))
|
||||
b.WriteString(validInputStyle.Render(field.value + "█"))
|
||||
} else {
|
||||
b.WriteString(fmt.Sprintf("%s%-12s %s", prefix, field.label+":", field.value))
|
||||
fmt.Fprintf(&b, "%s%-12s %s", prefix, field.label+":", field.value)
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
@@ -759,7 +747,7 @@ func (m model) renderBenchmarkRunning() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(renderHeader("HTTP Benchmark", ""))
|
||||
b.WriteString(fmt.Sprintf("Target: %s", breadcrumbStyle.Render(state.forwardAlias)))
|
||||
fmt.Fprintf(&b, "Target: %s", breadcrumbStyle.Render(state.forwardAlias))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
// Progress bar
|
||||
@@ -779,7 +767,7 @@ func (m model) renderBenchmarkRunning() string {
|
||||
b.WriteString(spinnerStyle.Render("Running benchmark..."))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
b.WriteString(fmt.Sprintf(" [%s] %d%%", successStyle.Render(bar), percent))
|
||||
fmt.Fprintf(&b, " [%s] %d%%", successStyle.Render(bar), percent)
|
||||
b.WriteString("\n")
|
||||
b.WriteString(mutedStyle.Render(fmt.Sprintf(" %d / %d requests completed", state.progress, state.total)))
|
||||
b.WriteString("\n\n")
|
||||
@@ -799,7 +787,7 @@ func (m model) renderBenchmarkResults() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(renderHeader("Benchmark Results", ""))
|
||||
b.WriteString(fmt.Sprintf("Target: %s", breadcrumbStyle.Render(state.forwardAlias)))
|
||||
fmt.Fprintf(&b, "Target: %s", breadcrumbStyle.Render(state.forwardAlias))
|
||||
b.WriteString("\n\n")
|
||||
|
||||
if state.error != nil {
|
||||
@@ -824,43 +812,43 @@ func (m model) renderBenchmarkResults() string {
|
||||
successRate = 0
|
||||
}
|
||||
|
||||
b.WriteString(fmt.Sprintf("Total Requests: %d", r.TotalRequests))
|
||||
fmt.Fprintf(&b, "Total Requests: %d", r.TotalRequests)
|
||||
b.WriteString("\n")
|
||||
if r.Failed == 0 {
|
||||
b.WriteString(successStyle.Render(fmt.Sprintf("Successful: %d (%.1f%%)", r.Successful, successRate)))
|
||||
} else {
|
||||
b.WriteString(fmt.Sprintf("Successful: %d (%.1f%%)", r.Successful, successRate))
|
||||
fmt.Fprintf(&b, "Successful: %d (%.1f%%)", r.Successful, successRate)
|
||||
}
|
||||
b.WriteString("\n")
|
||||
if r.Failed > 0 {
|
||||
b.WriteString(errorStyle.Render(fmt.Sprintf("Failed: %d", r.Failed)))
|
||||
} else {
|
||||
b.WriteString(fmt.Sprintf("Failed: %d", r.Failed))
|
||||
fmt.Fprintf(&b, "Failed: %d", r.Failed)
|
||||
}
|
||||
b.WriteString("\n\n")
|
||||
|
||||
// Latency stats
|
||||
b.WriteString(breadcrumbStyle.Render("Latency (ms)"))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(fmt.Sprintf(" Min: %.2f", r.MinLatency))
|
||||
fmt.Fprintf(&b, " Min: %.2f", r.MinLatency)
|
||||
b.WriteString("\n")
|
||||
b.WriteString(fmt.Sprintf(" Max: %.2f", r.MaxLatency))
|
||||
fmt.Fprintf(&b, " Max: %.2f", r.MaxLatency)
|
||||
b.WriteString("\n")
|
||||
b.WriteString(fmt.Sprintf(" Avg: %.2f", r.AvgLatency))
|
||||
fmt.Fprintf(&b, " Avg: %.2f", r.AvgLatency)
|
||||
b.WriteString("\n")
|
||||
b.WriteString(fmt.Sprintf(" P50: %.2f", r.P50Latency))
|
||||
fmt.Fprintf(&b, " P50: %.2f", r.P50Latency)
|
||||
b.WriteString("\n")
|
||||
b.WriteString(fmt.Sprintf(" P95: %.2f", r.P95Latency))
|
||||
fmt.Fprintf(&b, " P95: %.2f", r.P95Latency)
|
||||
b.WriteString("\n")
|
||||
b.WriteString(fmt.Sprintf(" P99: %.2f", r.P99Latency))
|
||||
fmt.Fprintf(&b, " P99: %.2f", r.P99Latency)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
// Throughput
|
||||
b.WriteString(breadcrumbStyle.Render("Throughput"))
|
||||
b.WriteString("\n")
|
||||
b.WriteString(fmt.Sprintf(" Requests/sec: %.2f", r.Throughput))
|
||||
fmt.Fprintf(&b, " Requests/sec: %.2f", r.Throughput)
|
||||
b.WriteString("\n")
|
||||
b.WriteString(fmt.Sprintf(" Bytes read: %d", r.BytesRead))
|
||||
fmt.Fprintf(&b, " Bytes read: %d", r.BytesRead)
|
||||
b.WriteString("\n")
|
||||
|
||||
// Status codes if interesting
|
||||
@@ -874,7 +862,7 @@ func (m model) renderBenchmarkResults() string {
|
||||
} else if code >= 400 {
|
||||
b.WriteString(errorStyle.Render(fmt.Sprintf(" %d: %d", code, count)))
|
||||
} else {
|
||||
b.WriteString(fmt.Sprintf(" %d: %d", code, count))
|
||||
fmt.Fprintf(&b, " %d: %d", code, count)
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
@@ -966,7 +954,7 @@ func (m model) renderHTTPLog() string {
|
||||
b.WriteString("\n")
|
||||
|
||||
// Header
|
||||
header := fmt.Sprintf(" %-10s %-7s %-6s %-8s %s",
|
||||
header := " " + fmt.Sprintf(HTTPLogRowFormat,
|
||||
"TIME", "METHOD", "STATUS", "LATENCY", "PATH")
|
||||
b.WriteString(mutedStyle.Render(header))
|
||||
b.WriteString("\n")
|
||||
@@ -1004,8 +992,8 @@ func (m model) renderHTTPLog() string {
|
||||
end = totalEntries
|
||||
}
|
||||
|
||||
// Calculate max path width
|
||||
maxPathWidth := termWidth - 48
|
||||
// Calculate max path width (remaining space after the fixed columns)
|
||||
maxPathWidth := termWidth - HTTPLogFixedCols
|
||||
if maxPathWidth < 10 {
|
||||
maxPathWidth = 10
|
||||
}
|
||||
@@ -1028,14 +1016,11 @@ func (m model) renderHTTPLog() string {
|
||||
}
|
||||
}
|
||||
|
||||
// Truncate path
|
||||
path := entry.Path
|
||||
if len(path) > maxPathWidth {
|
||||
path = path[:maxPathWidth-3] + "..."
|
||||
}
|
||||
// Truncate path (rune-aware, no mid-rune mojibake)
|
||||
path := truncate(entry.Path, maxPathWidth)
|
||||
|
||||
// Build line
|
||||
line := fmt.Sprintf("%-10s %-7s %-6s %-8s %s",
|
||||
line := fmt.Sprintf(HTTPLogRowFormat,
|
||||
entry.Timestamp,
|
||||
entry.Method,
|
||||
statusStr,
|
||||
@@ -1079,7 +1064,7 @@ func (m model) renderHTTPLog() string {
|
||||
// Footer with entry count
|
||||
b.WriteString("\n")
|
||||
if totalEntries != totalUnfiltered {
|
||||
b.WriteString(mutedStyle.Render(fmt.Sprintf(" %d of %d entries (filtered from %d)", totalEntries, totalEntries, totalUnfiltered)))
|
||||
b.WriteString(mutedStyle.Render(fmt.Sprintf(" %d of %d entries (filtered)", totalEntries, totalUnfiltered)))
|
||||
} else {
|
||||
b.WriteString(mutedStyle.Render(fmt.Sprintf(" %d entries", totalEntries)))
|
||||
}
|
||||
@@ -1121,11 +1106,7 @@ func (m model) renderHTTPLogDetail(entry HTTPLogEntry, termWidth, termHeight int
|
||||
}
|
||||
sort.Strings(headerKeys)
|
||||
for _, k := range headerKeys {
|
||||
v := entry.RequestHeaders[k]
|
||||
// Truncate long header values
|
||||
if len(v) > termWidth-20 {
|
||||
v = v[:termWidth-23] + "..."
|
||||
}
|
||||
v := truncate(entry.RequestHeaders[k], termWidth-20)
|
||||
lines = append(lines, fmt.Sprintf(" %s: %s", mutedStyle.Render(k), v))
|
||||
}
|
||||
lines = append(lines, "")
|
||||
@@ -1146,11 +1127,7 @@ func (m model) renderHTTPLogDetail(entry HTTPLogEntry, termWidth, termHeight int
|
||||
reqBody = formatJSONContent(reqBody, entry.RequestHeaders)
|
||||
bodyLines := strings.Split(reqBody, "\n")
|
||||
for _, line := range bodyLines {
|
||||
// Truncate long lines
|
||||
if len(line) > termWidth-6 {
|
||||
line = line[:termWidth-9] + "..."
|
||||
}
|
||||
lines = append(lines, " "+line)
|
||||
lines = append(lines, " "+truncate(line, termWidth-6))
|
||||
}
|
||||
}
|
||||
lines = append(lines, "")
|
||||
@@ -1192,11 +1169,7 @@ func (m model) renderHTTPLogDetail(entry HTTPLogEntry, termWidth, termHeight int
|
||||
}
|
||||
sort.Strings(headerKeys)
|
||||
for _, k := range headerKeys {
|
||||
v := entry.ResponseHeaders[k]
|
||||
// Truncate long header values
|
||||
if len(v) > termWidth-20 {
|
||||
v = v[:termWidth-23] + "..."
|
||||
}
|
||||
v := truncate(entry.ResponseHeaders[k], termWidth-20)
|
||||
lines = append(lines, fmt.Sprintf(" %s: %s", mutedStyle.Render(k), v))
|
||||
}
|
||||
lines = append(lines, "")
|
||||
@@ -1217,11 +1190,7 @@ func (m model) renderHTTPLogDetail(entry HTTPLogEntry, termWidth, termHeight int
|
||||
respBody = formatJSONContent(respBody, entry.ResponseHeaders)
|
||||
bodyLines := strings.Split(respBody, "\n")
|
||||
for _, line := range bodyLines {
|
||||
// Truncate long lines
|
||||
if len(line) > termWidth-6 {
|
||||
line = line[:termWidth-9] + "..."
|
||||
}
|
||||
lines = append(lines, " "+line)
|
||||
lines = append(lines, " "+truncate(line, termWidth-6))
|
||||
}
|
||||
}
|
||||
lines = append(lines, "")
|
||||
|
||||
Reference in New Issue
Block a user