mirror of
https://github.com/taigrr/teaqlite.git
synced 2026-04-02 04:59:03 -07:00
enable swapping focus into the table
This commit is contained in:
22
README.md
22
README.md
@@ -73,11 +73,18 @@ go run main.go sample.db
|
||||
|
||||
### SQL Query Mode
|
||||
- **Advanced Text Editing**: Full readline-style editing controls
|
||||
- **Dual Focus Mode**: Switch between query input and results with `Tab`
|
||||
- **Query Input Focus**:
|
||||
- **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
|
||||
- `Tab`: Switch focus to results (when available)
|
||||
- **Results Focus**:
|
||||
- `↑/↓` or `k/j`: Navigate between result rows
|
||||
- `Enter`: View selected row in detail modal
|
||||
- `Tab`: Switch focus back to query input
|
||||
- Type your SQL query (all keys work as input, no conflicts with navigation)
|
||||
- `Esc`: Return to table list
|
||||
- `q` or `Ctrl+C`: Quit
|
||||
|
||||
@@ -94,12 +101,13 @@ go run main.go sample.db
|
||||
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
|
||||
12. **SQL Query Execution**: Execute custom SQL queries with advanced text editing and dual-focus mode
|
||||
13. **Query Results Navigation**: Navigate and interact with query results just like table data
|
||||
14. **Error Handling**: Displays database errors gracefully
|
||||
15. **Responsive UI**: Clean, styled interface that adapts to terminal size
|
||||
16. **Column Information**: Shows column names and handles NULL values
|
||||
17. **Navigation**: Intuitive keyboard shortcuts for all operations
|
||||
18. **Dynamic Column Width**: Columns adjust to terminal width
|
||||
|
||||
## Navigation Flow
|
||||
|
||||
|
||||
19
main.go
19
main.go
@@ -21,8 +21,10 @@ type (
|
||||
switchToTableListMsg struct{}
|
||||
switchToTableDataMsg struct{ tableIndex int }
|
||||
switchToRowDetailMsg struct{ rowIndex int }
|
||||
switchToRowDetailFromQueryMsg struct{ rowIndex int }
|
||||
switchToEditCellMsg struct{ rowIndex, colIndex int }
|
||||
switchToQueryMsg struct{}
|
||||
returnToQueryMsg struct{} // Return to query mode from row detail
|
||||
refreshDataMsg struct{}
|
||||
updateCellMsg struct{ rowIndex, colIndex int; value string }
|
||||
executeQueryMsg struct{ query string }
|
||||
@@ -378,6 +380,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.currentView = newRowDetailModel(m.getSharedData(), msg.rowIndex)
|
||||
return m, nil
|
||||
|
||||
case switchToRowDetailFromQueryMsg:
|
||||
rowDetail := newRowDetailModel(m.getSharedData(), msg.rowIndex)
|
||||
rowDetail.fromQuery = true
|
||||
m.currentView = rowDetail
|
||||
return m, nil
|
||||
|
||||
case switchToEditCellMsg:
|
||||
m.currentView = newEditCellModel(m.getSharedData(), msg.rowIndex, msg.colIndex)
|
||||
return m, nil
|
||||
@@ -386,6 +394,17 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.currentView = newQueryModel(m.getSharedData())
|
||||
return m, nil
|
||||
|
||||
case returnToQueryMsg:
|
||||
// Return to query mode, preserving the query state if possible
|
||||
if queryView, ok := m.currentView.(*queryModel); ok {
|
||||
// If we're already in query mode, just switch focus back to results
|
||||
queryView.focusOnInput = false
|
||||
} else {
|
||||
// Create new query model
|
||||
m.currentView = newQueryModel(m.getSharedData())
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case refreshDataMsg:
|
||||
shared := m.getSharedData()
|
||||
if err := shared.loadTableData(); err != nil {
|
||||
|
||||
108
query.go
108
query.go
@@ -14,11 +14,15 @@ type queryModel struct {
|
||||
cursorPos int
|
||||
results [][]string
|
||||
columns []string
|
||||
focusOnInput bool // true = input focused, false = results focused
|
||||
selectedRow int
|
||||
}
|
||||
|
||||
func newQueryModel(shared *sharedData) *queryModel {
|
||||
return &queryModel{
|
||||
shared: shared,
|
||||
focusOnInput: true, // Start with input focused
|
||||
selectedRow: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,11 +43,47 @@ func (m *queryModel) handleInput(msg tea.KeyMsg) (subModel, tea.Cmd) {
|
||||
case "esc":
|
||||
return m, func() tea.Msg { return switchToTableListMsg{} }
|
||||
|
||||
case "tab":
|
||||
// Switch focus between input and results
|
||||
if len(m.results) > 0 {
|
||||
m.focusOnInput = !m.focusOnInput
|
||||
if !m.focusOnInput {
|
||||
// Reset row selection when switching to results
|
||||
m.selectedRow = 0
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case "enter":
|
||||
if m.focusOnInput {
|
||||
// Execute query when input is focused
|
||||
if err := m.executeQuery(); err != nil {
|
||||
// Handle error - could set an error field
|
||||
}
|
||||
} else {
|
||||
// View row detail when results are focused
|
||||
if len(m.results) > 0 && m.selectedRow < len(m.results) {
|
||||
// Convert query results to shared data format for row detail view
|
||||
m.shared.filteredData = m.results
|
||||
m.shared.columns = m.columns
|
||||
return m, func() tea.Msg {
|
||||
return switchToRowDetailFromQueryMsg{rowIndex: m.selectedRow}
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Handle input-specific controls
|
||||
if m.focusOnInput {
|
||||
return m.handleInputControls(msg)
|
||||
} else {
|
||||
return m.handleResultsNavigation(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *queryModel) handleInputControls(msg tea.KeyMsg) (subModel, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
// Cursor movement
|
||||
case "left":
|
||||
if m.cursorPos > 0 {
|
||||
@@ -104,6 +144,36 @@ func (m *queryModel) handleInput(msg tea.KeyMsg) (subModel, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *queryModel) handleResultsNavigation(msg tea.KeyMsg) (subModel, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "up", "k":
|
||||
if m.selectedRow > 0 {
|
||||
m.selectedRow--
|
||||
}
|
||||
|
||||
case "down", "j":
|
||||
if m.selectedRow < len(m.results)-1 {
|
||||
m.selectedRow++
|
||||
}
|
||||
|
||||
case "home":
|
||||
m.selectedRow = 0
|
||||
|
||||
case "end":
|
||||
if len(m.results) > 0 {
|
||||
m.selectedRow = len(m.results) - 1
|
||||
}
|
||||
|
||||
// Page navigation
|
||||
case "page_up":
|
||||
m.selectedRow = max(0, m.selectedRow-10)
|
||||
|
||||
case "page_down":
|
||||
m.selectedRow = min(len(m.results)-1, m.selectedRow+10)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Helper functions for word navigation
|
||||
func (m *queryModel) wordLeft(pos int) int {
|
||||
if pos == 0 {
|
||||
@@ -188,6 +258,11 @@ func (m *queryModel) executeQuery() error {
|
||||
m.results = append(m.results, row)
|
||||
}
|
||||
|
||||
// Reset selection when new results are loaded
|
||||
m.selectedRow = 0
|
||||
// Keep focus on input after executing query
|
||||
m.focusOnInput = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -202,8 +277,14 @@ func (m *queryModel) View() string {
|
||||
content.WriteString(titleStyle.Render("SQL Query"))
|
||||
content.WriteString("\n\n")
|
||||
|
||||
// Display query with cursor
|
||||
// Display query with cursor and focus indicator
|
||||
if m.focusOnInput {
|
||||
content.WriteString("Query: ")
|
||||
} else {
|
||||
content.WriteString(helpStyle.Render("Query: "))
|
||||
}
|
||||
|
||||
if m.focusOnInput {
|
||||
if m.cursorPos <= len(m.queryInput) {
|
||||
before := m.queryInput[:m.cursorPos]
|
||||
after := m.queryInput[m.cursorPos:]
|
||||
@@ -214,11 +295,22 @@ func (m *queryModel) View() string {
|
||||
content.WriteString(m.queryInput)
|
||||
content.WriteString("█")
|
||||
}
|
||||
} else {
|
||||
content.WriteString(m.queryInput)
|
||||
}
|
||||
content.WriteString("\n\n")
|
||||
|
||||
if len(m.results) > 0 {
|
||||
// Show results header with focus indicator
|
||||
if !m.focusOnInput {
|
||||
content.WriteString(titleStyle.Render("Results (focused)"))
|
||||
} else {
|
||||
content.WriteString(helpStyle.Render("Results"))
|
||||
}
|
||||
content.WriteString("\n")
|
||||
|
||||
// Limit rows to fit screen
|
||||
visibleRows := m.getVisibleRowCount()
|
||||
visibleRows := m.getVisibleRowCount() - 2 // Account for results header
|
||||
displayRows := min(len(m.results), visibleRows)
|
||||
|
||||
// Show query results
|
||||
@@ -256,7 +348,13 @@ func (m *queryModel) View() string {
|
||||
}
|
||||
dataRow.WriteString(fmt.Sprintf("%-*s", colWidth, truncateString(cell, colWidth)))
|
||||
}
|
||||
|
||||
// Highlight selected row when results are focused
|
||||
if !m.focusOnInput && i == m.selectedRow {
|
||||
content.WriteString(selectedStyle.Render(dataRow.String()))
|
||||
} else {
|
||||
content.WriteString(normalStyle.Render(dataRow.String()))
|
||||
}
|
||||
content.WriteString("\n")
|
||||
}
|
||||
|
||||
@@ -267,7 +365,11 @@ func (m *queryModel) View() string {
|
||||
}
|
||||
|
||||
content.WriteString("\n")
|
||||
content.WriteString(helpStyle.Render("enter: execute • ←/→: move cursor • ctrl+←/→: word nav • home/end: line nav • ctrl+w/k/u: delete • esc: back"))
|
||||
if m.focusOnInput {
|
||||
content.WriteString(helpStyle.Render("enter: execute • tab: focus results • ←/→: cursor • ctrl+←/→: word nav • home/end: line nav • esc: back"))
|
||||
} else {
|
||||
content.WriteString(helpStyle.Render("↑/↓: select row • enter: view row • tab: focus input • esc: back"))
|
||||
}
|
||||
|
||||
return content.String()
|
||||
}
|
||||
@@ -12,6 +12,7 @@ type rowDetailModel struct {
|
||||
shared *sharedData
|
||||
rowIndex int
|
||||
selectedCol int
|
||||
fromQuery bool // true if came from query results
|
||||
}
|
||||
|
||||
func newRowDetailModel(shared *sharedData, rowIndex int) *rowDetailModel {
|
||||
@@ -19,6 +20,7 @@ func newRowDetailModel(shared *sharedData, rowIndex int) *rowDetailModel {
|
||||
shared: shared,
|
||||
rowIndex: rowIndex,
|
||||
selectedCol: 0,
|
||||
fromQuery: false, // default to false, will be set by caller if needed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,9 +39,13 @@ func (m *rowDetailModel) Update(msg tea.Msg) (subModel, tea.Cmd) {
|
||||
func (m *rowDetailModel) handleNavigation(msg tea.KeyMsg) (subModel, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "esc":
|
||||
if m.fromQuery {
|
||||
return m, func() tea.Msg { return returnToQueryMsg{} }
|
||||
} else {
|
||||
return m, func() tea.Msg {
|
||||
return switchToTableDataMsg{tableIndex: m.shared.selectedTable}
|
||||
}
|
||||
}
|
||||
|
||||
case "enter":
|
||||
if len(m.shared.filteredData) > 0 && m.rowIndex < len(m.shared.filteredData) &&
|
||||
|
||||
Reference in New Issue
Block a user