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
|
### SQL Query Mode
|
||||||
- **Advanced Text Editing**: Full readline-style editing controls
|
- **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
|
- **Cursor Movement**: `←/→` arrows, `Ctrl+←/→` for word navigation
|
||||||
- **Line Navigation**: `Home`/`Ctrl+A` (start), `End`/`Ctrl+E` (end)
|
- **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)
|
- **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
|
||||||
|
- `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
|
- `Esc`: Return to table list
|
||||||
- `q` or `Ctrl+C`: Quit
|
- `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
|
9. **Text Wrapping**: Long values are automatically wrapped in edit and detail views
|
||||||
10. **Primary Key Detection**: Uses primary keys for reliable row updates
|
10. **Primary Key Detection**: Uses primary keys for reliable row updates
|
||||||
11. **Screen-Aware Display**: Content automatically fits terminal size
|
11. **Screen-Aware Display**: Content automatically fits terminal size
|
||||||
12. **SQL Query Execution**: Execute custom SQL queries with advanced text editing
|
12. **SQL Query Execution**: Execute custom SQL queries with advanced text editing and dual-focus mode
|
||||||
13. **Error Handling**: Displays database errors gracefully
|
13. **Query Results Navigation**: Navigate and interact with query results just like table data
|
||||||
14. **Responsive UI**: Clean, styled interface that adapts to terminal size
|
14. **Error Handling**: Displays database errors gracefully
|
||||||
15. **Column Information**: Shows column names and handles NULL values
|
15. **Responsive UI**: Clean, styled interface that adapts to terminal size
|
||||||
16. **Navigation**: Intuitive keyboard shortcuts for all operations
|
16. **Column Information**: Shows column names and handles NULL values
|
||||||
17. **Dynamic Column Width**: Columns adjust to terminal width
|
17. **Navigation**: Intuitive keyboard shortcuts for all operations
|
||||||
|
18. **Dynamic Column Width**: Columns adjust to terminal width
|
||||||
|
|
||||||
## Navigation Flow
|
## Navigation Flow
|
||||||
|
|
||||||
|
|||||||
19
main.go
19
main.go
@@ -21,8 +21,10 @@ type (
|
|||||||
switchToTableListMsg struct{}
|
switchToTableListMsg struct{}
|
||||||
switchToTableDataMsg struct{ tableIndex int }
|
switchToTableDataMsg struct{ tableIndex int }
|
||||||
switchToRowDetailMsg struct{ rowIndex int }
|
switchToRowDetailMsg struct{ rowIndex int }
|
||||||
|
switchToRowDetailFromQueryMsg struct{ rowIndex int }
|
||||||
switchToEditCellMsg struct{ rowIndex, colIndex int }
|
switchToEditCellMsg struct{ rowIndex, colIndex int }
|
||||||
switchToQueryMsg struct{}
|
switchToQueryMsg struct{}
|
||||||
|
returnToQueryMsg struct{} // Return to query mode from row detail
|
||||||
refreshDataMsg struct{}
|
refreshDataMsg struct{}
|
||||||
updateCellMsg struct{ rowIndex, colIndex int; value string }
|
updateCellMsg struct{ rowIndex, colIndex int; value string }
|
||||||
executeQueryMsg struct{ query 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)
|
m.currentView = newRowDetailModel(m.getSharedData(), msg.rowIndex)
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
|
case switchToRowDetailFromQueryMsg:
|
||||||
|
rowDetail := newRowDetailModel(m.getSharedData(), msg.rowIndex)
|
||||||
|
rowDetail.fromQuery = true
|
||||||
|
m.currentView = rowDetail
|
||||||
|
return m, nil
|
||||||
|
|
||||||
case switchToEditCellMsg:
|
case switchToEditCellMsg:
|
||||||
m.currentView = newEditCellModel(m.getSharedData(), msg.rowIndex, msg.colIndex)
|
m.currentView = newEditCellModel(m.getSharedData(), msg.rowIndex, msg.colIndex)
|
||||||
return m, nil
|
return m, nil
|
||||||
@@ -386,6 +394,17 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.currentView = newQueryModel(m.getSharedData())
|
m.currentView = newQueryModel(m.getSharedData())
|
||||||
return m, nil
|
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:
|
case refreshDataMsg:
|
||||||
shared := m.getSharedData()
|
shared := m.getSharedData()
|
||||||
if err := shared.loadTableData(); err != nil {
|
if err := shared.loadTableData(); err != nil {
|
||||||
|
|||||||
108
query.go
108
query.go
@@ -14,11 +14,15 @@ type queryModel struct {
|
|||||||
cursorPos int
|
cursorPos int
|
||||||
results [][]string
|
results [][]string
|
||||||
columns []string
|
columns []string
|
||||||
|
focusOnInput bool // true = input focused, false = results focused
|
||||||
|
selectedRow int
|
||||||
}
|
}
|
||||||
|
|
||||||
func newQueryModel(shared *sharedData) *queryModel {
|
func newQueryModel(shared *sharedData) *queryModel {
|
||||||
return &queryModel{
|
return &queryModel{
|
||||||
shared: shared,
|
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":
|
case "esc":
|
||||||
return m, func() tea.Msg { return switchToTableListMsg{} }
|
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":
|
case "enter":
|
||||||
|
if m.focusOnInput {
|
||||||
|
// Execute query when input is focused
|
||||||
if err := m.executeQuery(); err != nil {
|
if err := m.executeQuery(); err != nil {
|
||||||
// Handle error - could set an error field
|
// 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
|
// Cursor movement
|
||||||
case "left":
|
case "left":
|
||||||
if m.cursorPos > 0 {
|
if m.cursorPos > 0 {
|
||||||
@@ -104,6 +144,36 @@ func (m *queryModel) handleInput(msg tea.KeyMsg) (subModel, tea.Cmd) {
|
|||||||
return m, nil
|
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
|
// Helper functions for word navigation
|
||||||
func (m *queryModel) wordLeft(pos int) int {
|
func (m *queryModel) wordLeft(pos int) int {
|
||||||
if pos == 0 {
|
if pos == 0 {
|
||||||
@@ -188,6 +258,11 @@ func (m *queryModel) executeQuery() error {
|
|||||||
m.results = append(m.results, row)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,8 +277,14 @@ 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
|
// Display query with cursor and focus indicator
|
||||||
|
if m.focusOnInput {
|
||||||
content.WriteString("Query: ")
|
content.WriteString("Query: ")
|
||||||
|
} else {
|
||||||
|
content.WriteString(helpStyle.Render("Query: "))
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.focusOnInput {
|
||||||
if m.cursorPos <= len(m.queryInput) {
|
if m.cursorPos <= len(m.queryInput) {
|
||||||
before := m.queryInput[:m.cursorPos]
|
before := m.queryInput[:m.cursorPos]
|
||||||
after := m.queryInput[m.cursorPos:]
|
after := m.queryInput[m.cursorPos:]
|
||||||
@@ -214,11 +295,22 @@ func (m *queryModel) View() string {
|
|||||||
content.WriteString(m.queryInput)
|
content.WriteString(m.queryInput)
|
||||||
content.WriteString("█")
|
content.WriteString("█")
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
content.WriteString(m.queryInput)
|
||||||
|
}
|
||||||
content.WriteString("\n\n")
|
content.WriteString("\n\n")
|
||||||
|
|
||||||
if len(m.results) > 0 {
|
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
|
// Limit rows to fit screen
|
||||||
visibleRows := m.getVisibleRowCount()
|
visibleRows := m.getVisibleRowCount() - 2 // Account for results header
|
||||||
displayRows := min(len(m.results), visibleRows)
|
displayRows := min(len(m.results), visibleRows)
|
||||||
|
|
||||||
// Show query results
|
// Show query results
|
||||||
@@ -256,7 +348,13 @@ func (m *queryModel) View() string {
|
|||||||
}
|
}
|
||||||
dataRow.WriteString(fmt.Sprintf("%-*s", colWidth, truncateString(cell, colWidth)))
|
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(normalStyle.Render(dataRow.String()))
|
||||||
|
}
|
||||||
content.WriteString("\n")
|
content.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,7 +365,11 @@ func (m *queryModel) View() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
content.WriteString("\n")
|
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()
|
return content.String()
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,7 @@ type rowDetailModel struct {
|
|||||||
shared *sharedData
|
shared *sharedData
|
||||||
rowIndex int
|
rowIndex int
|
||||||
selectedCol int
|
selectedCol int
|
||||||
|
fromQuery bool // true if came from query results
|
||||||
}
|
}
|
||||||
|
|
||||||
func newRowDetailModel(shared *sharedData, rowIndex int) *rowDetailModel {
|
func newRowDetailModel(shared *sharedData, rowIndex int) *rowDetailModel {
|
||||||
@@ -19,6 +20,7 @@ func newRowDetailModel(shared *sharedData, rowIndex int) *rowDetailModel {
|
|||||||
shared: shared,
|
shared: shared,
|
||||||
rowIndex: rowIndex,
|
rowIndex: rowIndex,
|
||||||
selectedCol: 0,
|
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) {
|
func (m *rowDetailModel) handleNavigation(msg tea.KeyMsg) (subModel, tea.Cmd) {
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "esc":
|
case "esc":
|
||||||
|
if m.fromQuery {
|
||||||
|
return m, func() tea.Msg { return returnToQueryMsg{} }
|
||||||
|
} else {
|
||||||
return m, func() tea.Msg {
|
return m, func() tea.Msg {
|
||||||
return switchToTableDataMsg{tableIndex: m.shared.selectedTable}
|
return switchToTableDataMsg{tableIndex: m.shared.selectedTable}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case "enter":
|
case "enter":
|
||||||
if len(m.shared.filteredData) > 0 && m.rowIndex < len(m.shared.filteredData) &&
|
if len(m.shared.filteredData) > 0 && m.rowIndex < len(m.shared.filteredData) &&
|
||||||
|
|||||||
Reference in New Issue
Block a user