diff --git a/internal/tui/modals.go b/internal/tui/modals.go index 8055d3f..6539a78 100644 --- a/internal/tui/modals.go +++ b/internal/tui/modals.go @@ -301,6 +301,12 @@ func (m *DashboardModel) renderModalStatusBar() string { if m.aiClient != nil { statusItems = append(statusItems, "i: AI Analysis") } + // Add wrapping toggle for log details modal + if m.attributeWrappingEnabled { + statusItems = append(statusItems, "w: Disable wrapping") + } else { + statusItems = append(statusItems, "w: Enable wrapping") + } statusItems = append(statusItems, "↑↓/Wheel: Scroll", "PgUp/PgDn: Page") } } else { diff --git a/internal/tui/model.go b/internal/tui/model.go index 4abee84..abe21bd 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -111,6 +111,9 @@ type DashboardModel struct { logAutoScroll bool // Auto-scroll to latest logs in log viewer instructionsScrollOffset int // Scroll position for instructions/filter status screen + // Modal display options + attributeWrappingEnabled bool // Whether to wrap attribute values instead of truncating them + // Update interval management availableIntervals []time.Duration currentIntervalIdx int @@ -283,6 +286,7 @@ func NewDashboardModel(maxLogBuffer int, updateInterval time.Duration, aiModel s logAutoScroll: true, // Start with auto-scroll enabled showColumns: true, // Show Host/Service columns by default instructionsScrollOffset: 0, // Start at top of instructions + attributeWrappingEnabled: false, // Default to truncating (not wrapping) // Initialize statistics tracking statsStartTime: time.Now(), statsTotalBytes: 0, diff --git a/internal/tui/navigation.go b/internal/tui/navigation.go index 8f67d69..b95c68a 100644 --- a/internal/tui/navigation.go +++ b/internal/tui/navigation.go @@ -861,6 +861,16 @@ func (m *DashboardModel) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } return m, nil } + case "w": + // Toggle attribute wrapping - only when not in chat mode + if !m.chatActive { + m.attributeWrappingEnabled = !m.attributeWrappingEnabled + // Refresh the modal content with new wrapping setting + if m.currentLogEntry != nil { + m.modalContent = m.formatLogDetails(*m.currentLogEntry, 60) + } + return m, nil + } case "escape", "esc": // escape to close modal (only if not in chat mode) m.showModal = false m.modalContent = "" @@ -899,6 +909,14 @@ func (m *DashboardModel) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { case "pgdown": m.infoViewport.HalfPageDown() return m, nil + case "w": + // Toggle attribute wrapping + m.attributeWrappingEnabled = !m.attributeWrappingEnabled + // Refresh the modal content with new wrapping setting + if m.currentLogEntry != nil { + m.modalContent = m.formatLogDetails(*m.currentLogEntry, 60) + } + return m, nil case "escape", "esc": m.showModal = false m.modalContent = "" diff --git a/internal/tui/tables.go b/internal/tui/tables.go index f2d0fef..ccbcfe3 100644 --- a/internal/tui/tables.go +++ b/internal/tui/tables.go @@ -44,25 +44,45 @@ func (m *DashboardModel) formatAttributesTable(attributes map[string]string, max // Create table rows rows := []table.Row{} + totalRows := 0 for _, key := range keys { value := attributes[key] - // Truncate long values to fit - if len(value) > valueWidth-3 { - value = value[:valueWidth-3] + "..." - } - // Truncate long keys to fit + + // Always truncate long keys to fit (keys are less important to see in full) displayKey := key if len(displayKey) > keyWidth-3 { displayKey = displayKey[:keyWidth-3] + "..." } - rows = append(rows, table.Row{displayKey, value}) + + // Handle value display based on wrapping setting + if m.attributeWrappingEnabled && len(value) > valueWidth { + // Wrap long values across multiple rows + wrappedLines := wrapText(value, valueWidth) + for i, line := range wrappedLines { + if i == 0 { + // First line shows the key + rows = append(rows, table.Row{displayKey, line}) + } else { + // Subsequent lines have empty key column + rows = append(rows, table.Row{"", line}) + } + totalRows++ + } + } else { + // Truncate long values to fit (default behavior) + if len(value) > valueWidth-3 { + value = value[:valueWidth-3] + "..." + } + rows = append(rows, table.Row{displayKey, value}) + totalRows++ + } } // Create and configure table t := table.New( table.WithColumns(columns), table.WithRows(rows), - table.WithHeight(len(rows)+2), // +2 for header and padding + table.WithHeight(totalRows+2), // +2 for header and padding table.WithWidth(maxWidth), table.WithFocused(false), // Disable focus to prevent selection ) @@ -155,6 +175,43 @@ func (m *DashboardModel) formatLogDetails(entry LogEntry, maxWidth int) string { return details.String() } +// wrapText wraps text to fit within the specified width +func wrapText(text string, width int) []string { + if len(text) <= width { + return []string{text} + } + + var lines []string + for len(text) > width { + // Find the best break point (prefer spaces) + breakPoint := width + + // Look for a space near the end to break on word boundary + for i := width - 1; i > width/2 && i < len(text); i-- { + if text[i] == ' ' { + breakPoint = i + break + } + } + + // Add the line (excluding the space if we broke on one) + if breakPoint < len(text) && text[breakPoint] == ' ' { + lines = append(lines, text[:breakPoint]) + text = text[breakPoint+1:] // Skip the space + } else { + lines = append(lines, text[:breakPoint]) + text = text[breakPoint:] + } + } + + // Add the remaining text + if len(text) > 0 { + lines = append(lines, text) + } + + return lines +} + // formatAttributeValuesModal formats the attribute values modal showing individual values and their counts with full width layout func (m *DashboardModel) formatAttributeValuesModal(entry *memory.AttributeStatsEntry, maxWidth int) string { var modal strings.Builder