mirror of
https://github.com/taigrr/teaqlite.git
synced 2026-04-02 04:59:03 -07:00
add ability to use editing modes
This commit is contained in:
32
README.md
32
README.md
@@ -63,16 +63,21 @@ go run main.go sample.db
|
|||||||
- `q` or `Ctrl+C`: Quit
|
- `q` or `Ctrl+C`: Quit
|
||||||
|
|
||||||
### Cell Edit Mode
|
### 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
|
- **Text Wrapping**: Long values are automatically wrapped for better visibility
|
||||||
- `Enter`: Save changes to database
|
- `Enter`: Save changes to database
|
||||||
- `Esc`: Cancel editing and return to row detail
|
- `Esc`: Cancel editing and return to row detail
|
||||||
- `Backspace`: Delete characters
|
|
||||||
|
|
||||||
### SQL Query Mode
|
### 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
|
- `Enter`: Execute query
|
||||||
- `Backspace`: Delete characters
|
|
||||||
- `Esc`: Return to table list
|
- `Esc`: Return to table list
|
||||||
- `q` or `Ctrl+C`: Quit
|
- `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
|
5. **Data Search**: Search within table data across all columns
|
||||||
6. **Row Detail Modal**: 2-column view showing Column | Value for selected row
|
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
|
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
|
8. **Readline-style Editing**: Full cursor control with word navigation, line navigation, and advanced deletion
|
||||||
9. **Primary Key Detection**: Uses primary keys for reliable row updates
|
9. **Text Wrapping**: Long values are automatically wrapped in edit and detail views
|
||||||
10. **Screen-Aware Display**: Content automatically fits terminal size
|
10. **Primary Key Detection**: Uses primary keys for reliable row updates
|
||||||
11. **SQL Query Execution**: Execute custom SQL queries and view results (all keys work as input)
|
11. **Screen-Aware Display**: Content automatically fits terminal size
|
||||||
12. **Error Handling**: Displays database errors gracefully
|
12. **SQL Query Execution**: Execute custom SQL queries with advanced text editing
|
||||||
13. **Responsive UI**: Clean, styled interface that adapts to terminal size
|
13. **Error Handling**: Displays database errors gracefully
|
||||||
14. **Column Information**: Shows column names and handles NULL values
|
14. **Responsive UI**: Clean, styled interface that adapts to terminal size
|
||||||
15. **Navigation**: Intuitive keyboard shortcuts for all operations
|
15. **Column Information**: Shows column names and handles NULL values
|
||||||
16. **Dynamic Column Width**: Columns adjust to terminal width
|
16. **Navigation**: Intuitive keyboard shortcuts for all operations
|
||||||
|
17. **Dynamic Column Width**: Columns adjust to terminal width
|
||||||
|
|
||||||
## Navigation Flow
|
## Navigation Flow
|
||||||
|
|
||||||
|
|||||||
112
edit_cell.go
112
edit_cell.go
@@ -14,6 +14,7 @@ type editCellModel struct {
|
|||||||
colIndex int
|
colIndex int
|
||||||
editingValue string
|
editingValue string
|
||||||
originalValue string
|
originalValue string
|
||||||
|
cursorPos int
|
||||||
}
|
}
|
||||||
|
|
||||||
func newEditCellModel(shared *sharedData, rowIndex, colIndex int) *editCellModel {
|
func newEditCellModel(shared *sharedData, rowIndex, colIndex int) *editCellModel {
|
||||||
@@ -28,6 +29,7 @@ func newEditCellModel(shared *sharedData, rowIndex, colIndex int) *editCellModel
|
|||||||
colIndex: colIndex,
|
colIndex: colIndex,
|
||||||
editingValue: originalValue,
|
editingValue: originalValue,
|
||||||
originalValue: 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":
|
// Cursor movement
|
||||||
if len(m.editingValue) > 0 {
|
case "left":
|
||||||
m.editingValue = m.editingValue[:len(m.editingValue)-1]
|
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:
|
default:
|
||||||
|
// Insert character at cursor position
|
||||||
if len(msg.String()) == 1 {
|
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
|
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 {
|
func (m *editCellModel) View() string {
|
||||||
var content strings.Builder
|
var content strings.Builder
|
||||||
|
|
||||||
@@ -98,17 +185,28 @@ func (m *editCellModel) View() string {
|
|||||||
|
|
||||||
content.WriteString("\n")
|
content.WriteString("\n")
|
||||||
|
|
||||||
// Wrap new value
|
// Wrap new value with cursor
|
||||||
content.WriteString("New:")
|
content.WriteString("New:")
|
||||||
content.WriteString("\n")
|
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 {
|
for _, line := range newLines {
|
||||||
content.WriteString(" " + line)
|
content.WriteString(" " + line)
|
||||||
content.WriteString("\n")
|
content.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
return content.String()
|
||||||
}
|
}
|
||||||
122
query.go
122
query.go
@@ -9,10 +9,11 @@ import (
|
|||||||
|
|
||||||
// Query Model
|
// Query Model
|
||||||
type queryModel struct {
|
type queryModel struct {
|
||||||
shared *sharedData
|
shared *sharedData
|
||||||
queryInput string
|
queryInput string
|
||||||
results [][]string
|
cursorPos int
|
||||||
columns []string
|
results [][]string
|
||||||
|
columns []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newQueryModel(shared *sharedData) *queryModel {
|
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
|
// Handle error - could set an error field
|
||||||
}
|
}
|
||||||
|
|
||||||
case "backspace":
|
// Cursor movement
|
||||||
if len(m.queryInput) > 0 {
|
case "left":
|
||||||
m.queryInput = m.queryInput[:len(m.queryInput)-1]
|
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:
|
default:
|
||||||
// In query mode, all single characters should be treated as input
|
// Insert character at cursor position
|
||||||
if len(msg.String()) == 1 {
|
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
|
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 {
|
func (m *queryModel) executeQuery() error {
|
||||||
if strings.TrimSpace(m.queryInput) == "" {
|
if strings.TrimSpace(m.queryInput) == "" {
|
||||||
return nil
|
return nil
|
||||||
@@ -113,9 +202,18 @@ func (m *queryModel) View() string {
|
|||||||
content.WriteString(titleStyle.Render("SQL Query"))
|
content.WriteString(titleStyle.Render("SQL Query"))
|
||||||
content.WriteString("\n\n")
|
content.WriteString("\n\n")
|
||||||
|
|
||||||
|
// Display query with cursor
|
||||||
content.WriteString("Query: ")
|
content.WriteString("Query: ")
|
||||||
content.WriteString(m.queryInput)
|
if m.cursorPos <= len(m.queryInput) {
|
||||||
content.WriteString("_") // cursor
|
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")
|
content.WriteString("\n\n")
|
||||||
|
|
||||||
if len(m.results) > 0 {
|
if len(m.results) > 0 {
|
||||||
@@ -169,7 +267,7 @@ func (m *queryModel) View() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
content.WriteString("\n")
|
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()
|
return content.String()
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user