add ability to use editing modes

This commit is contained in:
2025-07-11 23:19:49 -07:00
parent b1018b7fa8
commit e79d20f7ce
3 changed files with 234 additions and 32 deletions

View File

@@ -63,16 +63,21 @@ go run main.go sample.db
- `q` or `Ctrl+C`: Quit
### Cell Edit Mode
- Type new value for the selected cell
- **Readline-style Editing**: Full cursor control and advanced editing
- **Cursor Movement**: `←/→` arrows, `Ctrl+←/→` for word navigation
- **Line Navigation**: `Home`/`Ctrl+A` (start), `End`/`Ctrl+E` (end)
- **Deletion**: `Backspace`, `Delete`/`Ctrl+D`, `Ctrl+W` (word), `Ctrl+K` (to end), `Ctrl+U` (to start)
- **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 (all keys including r, s, h, j, k, l work as input)
- **Advanced Text Editing**: Full readline-style editing controls
- **Cursor Movement**: `←/→` arrows, `Ctrl+←/→` for word navigation
- **Line Navigation**: `Home`/`Ctrl+A` (start), `End`/`Ctrl+E` (end)
- **Deletion**: `Backspace`, `Delete`/`Ctrl+D`, `Ctrl+W` (word), `Ctrl+K` (to end), `Ctrl+U` (to start)
- Type your SQL query (all keys work as input, no conflicts with navigation)
- `Enter`: Execute query
- `Backspace`: Delete characters
- `Esc`: Return to table list
- `q` or `Ctrl+C`: Quit
@@ -85,15 +90,16 @@ 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. **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
8. **Readline-style Editing**: Full cursor control with word navigation, line navigation, and advanced deletion
9. **Text Wrapping**: Long values are automatically wrapped in edit and detail views
10. **Primary Key Detection**: Uses primary keys for reliable row updates
11. **Screen-Aware Display**: Content automatically fits terminal size
12. **SQL Query Execution**: Execute custom SQL queries with advanced text editing
13. **Error Handling**: Displays database errors gracefully
14. **Responsive UI**: Clean, styled interface that adapts to terminal size
15. **Column Information**: Shows column names and handles NULL values
16. **Navigation**: Intuitive keyboard shortcuts for all operations
17. **Dynamic Column Width**: Columns adjust to terminal width
## Navigation Flow

View File

@@ -14,6 +14,7 @@ type editCellModel struct {
colIndex int
editingValue string
originalValue string
cursorPos int
}
func newEditCellModel(shared *sharedData, rowIndex, colIndex int) *editCellModel {
@@ -28,6 +29,7 @@ func newEditCellModel(shared *sharedData, rowIndex, colIndex int) *editCellModel
colIndex: colIndex,
editingValue: originalValue,
originalValue: originalValue,
cursorPos: len(originalValue), // Start cursor at end
}
}
@@ -59,19 +61,104 @@ func (m *editCellModel) handleInput(msg tea.KeyMsg) (subModel, tea.Cmd) {
}
}
case "backspace":
if len(m.editingValue) > 0 {
m.editingValue = m.editingValue[:len(m.editingValue)-1]
// Cursor movement
case "left":
if m.cursorPos > 0 {
m.cursorPos--
}
case "right":
if m.cursorPos < len(m.editingValue) {
m.cursorPos++
}
case "ctrl+left":
m.cursorPos = m.wordLeft(m.cursorPos)
case "ctrl+right":
m.cursorPos = m.wordRight(m.cursorPos)
case "home", "ctrl+a":
m.cursorPos = 0
case "end", "ctrl+e":
m.cursorPos = len(m.editingValue)
// Deletion
case "backspace":
if m.cursorPos > 0 {
m.editingValue = m.editingValue[:m.cursorPos-1] + m.editingValue[m.cursorPos:]
m.cursorPos--
}
case "delete", "ctrl+d":
if m.cursorPos < len(m.editingValue) {
m.editingValue = m.editingValue[:m.cursorPos] + m.editingValue[m.cursorPos+1:]
}
case "ctrl+w":
// Delete word backward
newPos := m.wordLeft(m.cursorPos)
m.editingValue = m.editingValue[:newPos] + m.editingValue[m.cursorPos:]
m.cursorPos = newPos
case "ctrl+k":
// Delete from cursor to end of line
m.editingValue = m.editingValue[:m.cursorPos]
case "ctrl+u":
// Delete from beginning of line to cursor
m.editingValue = m.editingValue[m.cursorPos:]
m.cursorPos = 0
default:
// Insert character at cursor position
if len(msg.String()) == 1 {
m.editingValue += msg.String()
m.editingValue = m.editingValue[:m.cursorPos] + msg.String() + m.editingValue[m.cursorPos:]
m.cursorPos++
}
}
return m, nil
}
// Helper functions for word navigation (same as query model)
func (m *editCellModel) wordLeft(pos int) int {
if pos == 0 {
return 0
}
// Skip whitespace
for pos > 0 && isWhitespace(m.editingValue[pos-1]) {
pos--
}
// Skip non-whitespace
for pos > 0 && !isWhitespace(m.editingValue[pos-1]) {
pos--
}
return pos
}
func (m *editCellModel) wordRight(pos int) int {
length := len(m.editingValue)
if pos >= length {
return length
}
// Skip non-whitespace
for pos < length && !isWhitespace(m.editingValue[pos]) {
pos++
}
// Skip whitespace
for pos < length && isWhitespace(m.editingValue[pos]) {
pos++
}
return pos
}
func (m *editCellModel) View() string {
var content strings.Builder
@@ -98,17 +185,28 @@ func (m *editCellModel) View() string {
content.WriteString("\n")
// Wrap new value
// Wrap new value with cursor
content.WriteString("New:")
content.WriteString("\n")
newLines := wrapText(m.editingValue+"_", textWidth) // Add cursor
// Display editing value with cursor
valueWithCursor := ""
if m.cursorPos <= len(m.editingValue) {
before := m.editingValue[:m.cursorPos]
after := m.editingValue[m.cursorPos:]
valueWithCursor = before + "█" + after
} else {
valueWithCursor = m.editingValue + "█"
}
newLines := wrapText(valueWithCursor, textWidth)
for _, line := range newLines {
content.WriteString(" " + line)
content.WriteString("\n")
}
content.WriteString("\n")
content.WriteString(helpStyle.Render("Type new value • enter: save • esc: cancel"))
content.WriteString(helpStyle.Render("←/→: move cursor • ctrl+←/→: word nav • home/end: line nav • ctrl+w/k/u: delete • enter: save • esc: cancel"))
return content.String()
}

122
query.go
View File

@@ -9,10 +9,11 @@ import (
// Query Model
type queryModel struct {
shared *sharedData
queryInput string
results [][]string
columns []string
shared *sharedData
queryInput string
cursorPos int
results [][]string
columns []string
}
func newQueryModel(shared *sharedData) *queryModel {
@@ -43,20 +44,108 @@ func (m *queryModel) handleInput(msg tea.KeyMsg) (subModel, tea.Cmd) {
// Handle error - could set an error field
}
case "backspace":
if len(m.queryInput) > 0 {
m.queryInput = m.queryInput[:len(m.queryInput)-1]
// Cursor movement
case "left":
if m.cursorPos > 0 {
m.cursorPos--
}
case "right":
if m.cursorPos < len(m.queryInput) {
m.cursorPos++
}
case "ctrl+left":
m.cursorPos = m.wordLeft(m.cursorPos)
case "ctrl+right":
m.cursorPos = m.wordRight(m.cursorPos)
case "home", "ctrl+a":
m.cursorPos = 0
case "end", "ctrl+e":
m.cursorPos = len(m.queryInput)
// Deletion
case "backspace":
if m.cursorPos > 0 {
m.queryInput = m.queryInput[:m.cursorPos-1] + m.queryInput[m.cursorPos:]
m.cursorPos--
}
case "delete", "ctrl+d":
if m.cursorPos < len(m.queryInput) {
m.queryInput = m.queryInput[:m.cursorPos] + m.queryInput[m.cursorPos+1:]
}
case "ctrl+w":
// Delete word backward
newPos := m.wordLeft(m.cursorPos)
m.queryInput = m.queryInput[:newPos] + m.queryInput[m.cursorPos:]
m.cursorPos = newPos
case "ctrl+k":
// Delete from cursor to end of line
m.queryInput = m.queryInput[:m.cursorPos]
case "ctrl+u":
// Delete from beginning of line to cursor
m.queryInput = m.queryInput[m.cursorPos:]
m.cursorPos = 0
default:
// In query mode, all single characters should be treated as input
// Insert character at cursor position
if len(msg.String()) == 1 {
m.queryInput += msg.String()
m.queryInput = m.queryInput[:m.cursorPos] + msg.String() + m.queryInput[m.cursorPos:]
m.cursorPos++
}
}
return m, nil
}
// Helper functions for word navigation
func (m *queryModel) wordLeft(pos int) int {
if pos == 0 {
return 0
}
// Skip whitespace
for pos > 0 && isWhitespace(m.queryInput[pos-1]) {
pos--
}
// Skip non-whitespace
for pos > 0 && !isWhitespace(m.queryInput[pos-1]) {
pos--
}
return pos
}
func (m *queryModel) wordRight(pos int) int {
length := len(m.queryInput)
if pos >= length {
return length
}
// Skip non-whitespace
for pos < length && !isWhitespace(m.queryInput[pos]) {
pos++
}
// Skip whitespace
for pos < length && isWhitespace(m.queryInput[pos]) {
pos++
}
return pos
}
func isWhitespace(r byte) bool {
return r == ' ' || r == '\t' || r == '\n' || r == '\r'
}
func (m *queryModel) executeQuery() error {
if strings.TrimSpace(m.queryInput) == "" {
return nil
@@ -113,9 +202,18 @@ func (m *queryModel) View() string {
content.WriteString(titleStyle.Render("SQL Query"))
content.WriteString("\n\n")
// Display query with cursor
content.WriteString("Query: ")
content.WriteString(m.queryInput)
content.WriteString("_") // cursor
if m.cursorPos <= len(m.queryInput) {
before := m.queryInput[:m.cursorPos]
after := m.queryInput[m.cursorPos:]
content.WriteString(before)
content.WriteString("█") // Block cursor
content.WriteString(after)
} else {
content.WriteString(m.queryInput)
content.WriteString("█")
}
content.WriteString("\n\n")
if len(m.results) > 0 {
@@ -169,7 +267,7 @@ func (m *queryModel) View() string {
}
content.WriteString("\n")
content.WriteString(helpStyle.Render("enter: execute query • esc: back • q: quit"))
content.WriteString(helpStyle.Render("enter: execute • ←/→: move cursor • ctrl+←/→: word nav • home/end: line nav • ctrl+w/k/u: delete • esc: back"))
return content.String()
}