diff --git a/README.md b/README.md index 79bb5d5..d09b4b2 100644 --- a/README.md +++ b/README.md @@ -64,12 +64,13 @@ go run main.go sample.db ### Cell Edit Mode - Type new value for the selected cell +- **Text Wrapping**: Long values are automatically wrapped for better visibility - `Enter`: Save changes to database - `Esc`: Cancel editing and return to row detail - `Backspace`: Delete characters ### SQL Query Mode -- Type your SQL query +- Type your SQL query (all keys including r, s, h, j, k, l work as input) - `Enter`: Execute query - `Backspace`: Delete characters - `Esc`: Return to table list @@ -84,14 +85,15 @@ go run main.go sample.db 5. **Data Search**: Search within table data across all columns 6. **Row Detail Modal**: 2-column view showing Column | Value for selected row 7. **Cell Editing**: Live editing of individual cell values with database updates -8. **Primary Key Detection**: Uses primary keys for reliable row updates -9. **Screen-Aware Display**: Content automatically fits terminal size -10. **SQL Query Execution**: Execute custom SQL queries and view results -11. **Error Handling**: Displays database errors gracefully -12. **Responsive UI**: Clean, styled interface that adapts to terminal size -13. **Column Information**: Shows column names and handles NULL values -14. **Navigation**: Intuitive keyboard shortcuts for all operations -15. **Dynamic Column Width**: Columns adjust to terminal width +8. **Text Wrapping**: Long values are automatically wrapped in edit and detail views +9. **Primary Key Detection**: Uses primary keys for reliable row updates +10. **Screen-Aware Display**: Content automatically fits terminal size +11. **SQL Query Execution**: Execute custom SQL queries and view results (all keys work as input) +12. **Error Handling**: Displays database errors gracefully +13. **Responsive UI**: Clean, styled interface that adapts to terminal size +14. **Column Information**: Shows column names and handles NULL values +15. **Navigation**: Intuitive keyboard shortcuts for all operations +16. **Dynamic Column Width**: Columns adjust to terminal width ## Navigation Flow diff --git a/main.go b/main.go index 1d6867c..3618777 100644 --- a/main.go +++ b/main.go @@ -546,6 +546,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.selectedCol > 0 { m.selectedCol-- } + case modeQuery: + // In query mode, these should be treated as input + if len(msg.String()) == 1 { + m.queryInput += msg.String() + m.query = m.queryInput + } } case "down", "j": @@ -567,6 +573,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.selectedCol < len(m.columns)-1 { m.selectedCol++ } + case modeQuery: + // In query mode, these should be treated as input + if len(msg.String()) == 1 { + m.queryInput += msg.String() + m.query = m.queryInput + } } case "left", "h": @@ -584,6 +596,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { visibleCount := m.getVisibleTableCount() m.selectedTable = m.tableListPage * visibleCount } + case modeQuery: + // In query mode, these should be treated as input + if len(msg.String()) == 1 { + m.queryInput += msg.String() + m.query = m.queryInput + } } case "right", "l": @@ -606,6 +624,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.selectedTable = len(m.filteredTables) - 1 } } + case modeQuery: + // In query mode, these should be treated as input + if len(msg.String()) == 1 { + m.queryInput += msg.String() + m.query = m.queryInput + } } case "s": @@ -614,6 +638,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.queryInput = "" m.query = "" m.cursor = 0 + } else if m.mode == modeQuery { + // In query mode, 's' should be treated as input + m.queryInput += msg.String() + m.query = m.queryInput } case "r": @@ -624,6 +652,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.loadTableData() case modeRowDetail: m.loadTableData() + case modeQuery: + // In query mode, 'r' should be treated as input + m.queryInput += msg.String() + m.query = m.queryInput } case "backspace": @@ -824,16 +856,50 @@ func (m model) View() string { col := m.columns[i] val := row[i] - dataRow := fmt.Sprintf("%-*s | %-*s", - colWidth, truncateString(col, colWidth), - valueWidth, truncateString(val, valueWidth)) - - if i == m.selectedCol { - content.WriteString(selectedStyle.Render(dataRow)) + // For long values, show them wrapped on multiple lines + if len(val) > valueWidth { + // First line with column name + firstLine := fmt.Sprintf("%-*s | %-*s", + colWidth, truncateString(col, colWidth), + valueWidth, truncateString(val, valueWidth)) + + if i == m.selectedCol { + content.WriteString(selectedStyle.Render(firstLine)) + } else { + content.WriteString(normalStyle.Render(firstLine)) + } + content.WriteString("\n") + + // Additional lines for wrapped text (if there's space) + if len(val) > valueWidth && visibleRows > displayRows { + wrappedLines := wrapText(val, valueWidth) + for j, wrappedLine := range wrappedLines[1:] { // Skip first line already shown + if j >= 2 { // Limit to 3 total lines per field + break + } + continuationLine := fmt.Sprintf("%-*s | %-*s", + colWidth, "", valueWidth, wrappedLine) + if i == m.selectedCol { + content.WriteString(selectedStyle.Render(continuationLine)) + } else { + content.WriteString(normalStyle.Render(continuationLine)) + } + content.WriteString("\n") + } + } } else { - content.WriteString(normalStyle.Render(dataRow)) + // Normal single line + dataRow := fmt.Sprintf("%-*s | %-*s", + colWidth, truncateString(col, colWidth), + valueWidth, val) + + if i == m.selectedCol { + content.WriteString(selectedStyle.Render(dataRow)) + } else { + content.WriteString(normalStyle.Render(dataRow)) + } + content.WriteString("\n") } - content.WriteString("\n") } } @@ -850,11 +916,30 @@ func (m model) View() string { content.WriteString(titleStyle.Render(fmt.Sprintf("Edit: %s.%s", tableName, columnName))) content.WriteString("\n\n") - content.WriteString("Original: " + m.originalValue) - content.WriteString("\n") - content.WriteString("New: " + m.editingValue + "_") - content.WriteString("\n\n") + // Calculate available width for text (leave some margin) + textWidth := max(20, m.width-4) + // Wrap original value + content.WriteString("Original:") + content.WriteString("\n") + originalLines := wrapText(m.originalValue, textWidth) + for _, line := range originalLines { + content.WriteString(" " + line) + content.WriteString("\n") + } + + content.WriteString("\n") + + // Wrap new value + content.WriteString("New:") + content.WriteString("\n") + newLines := wrapText(m.editingValue+"_", textWidth) // Add cursor + for _, line := range newLines { + content.WriteString(" " + line) + content.WriteString("\n") + } + + content.WriteString("\n") content.WriteString(helpStyle.Render("Type new value • enter: save • esc: cancel")) case modeQuery: @@ -938,6 +1023,48 @@ func truncateString(s string, maxLen int) string { return s[:maxLen-3] + "..." } +func wrapText(text string, width int) []string { + if width <= 0 { + return []string{text} + } + + var lines []string + words := strings.Fields(text) + if len(words) == 0 { + return []string{text} + } + + currentLine := "" + for _, word := range words { + // If adding this word would exceed the width + if len(currentLine)+len(word)+1 > width { + if currentLine != "" { + lines = append(lines, currentLine) + currentLine = word + } else { + // Word is longer than width, break it + for len(word) > width { + lines = append(lines, word[:width]) + word = word[width:] + } + currentLine = word + } + } else { + if currentLine != "" { + currentLine += " " + word + } else { + currentLine = word + } + } + } + + if currentLine != "" { + lines = append(lines, currentLine) + } + + return lines +} + func min(a, b int) int { if a < b { return a