mirror of
https://github.com/taigrr/teaqlite.git
synced 2026-04-02 04:59:03 -07:00
refactor models out
This commit is contained in:
114
edit_cell.go
Normal file
114
edit_cell.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
// Edit Cell Model
|
||||
type editCellModel struct {
|
||||
shared *sharedData
|
||||
rowIndex int
|
||||
colIndex int
|
||||
editingValue string
|
||||
originalValue string
|
||||
}
|
||||
|
||||
func newEditCellModel(shared *sharedData, rowIndex, colIndex int) *editCellModel {
|
||||
originalValue := ""
|
||||
if rowIndex < len(shared.filteredData) && colIndex < len(shared.filteredData[rowIndex]) {
|
||||
originalValue = shared.filteredData[rowIndex][colIndex]
|
||||
}
|
||||
|
||||
return &editCellModel{
|
||||
shared: shared,
|
||||
rowIndex: rowIndex,
|
||||
colIndex: colIndex,
|
||||
editingValue: originalValue,
|
||||
originalValue: originalValue,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *editCellModel) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *editCellModel) Update(msg tea.Msg) (subModel, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
return m.handleInput(msg)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *editCellModel) handleInput(msg tea.KeyMsg) (subModel, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "esc":
|
||||
return m, func() tea.Msg {
|
||||
return switchToRowDetailMsg{rowIndex: m.rowIndex}
|
||||
}
|
||||
|
||||
case "enter":
|
||||
return m, func() tea.Msg {
|
||||
return updateCellMsg{
|
||||
rowIndex: m.rowIndex,
|
||||
colIndex: m.colIndex,
|
||||
value: m.editingValue,
|
||||
}
|
||||
}
|
||||
|
||||
case "backspace":
|
||||
if len(m.editingValue) > 0 {
|
||||
m.editingValue = m.editingValue[:len(m.editingValue)-1]
|
||||
}
|
||||
|
||||
default:
|
||||
if len(msg.String()) == 1 {
|
||||
m.editingValue += msg.String()
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *editCellModel) View() string {
|
||||
var content strings.Builder
|
||||
|
||||
tableName := m.shared.filteredTables[m.shared.selectedTable]
|
||||
columnName := ""
|
||||
if m.colIndex < len(m.shared.columns) {
|
||||
columnName = m.shared.columns[m.colIndex]
|
||||
}
|
||||
|
||||
content.WriteString(titleStyle.Render(fmt.Sprintf("Edit: %s.%s", tableName, columnName)))
|
||||
content.WriteString("\n\n")
|
||||
|
||||
// Calculate available width for text (leave some margin)
|
||||
textWidth := max(20, m.shared.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"))
|
||||
|
||||
return content.String()
|
||||
}
|
||||
175
query.go
Normal file
175
query.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
// Query Model
|
||||
type queryModel struct {
|
||||
shared *sharedData
|
||||
queryInput string
|
||||
results [][]string
|
||||
columns []string
|
||||
}
|
||||
|
||||
func newQueryModel(shared *sharedData) *queryModel {
|
||||
return &queryModel{
|
||||
shared: shared,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *queryModel) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *queryModel) Update(msg tea.Msg) (subModel, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
return m.handleInput(msg)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *queryModel) handleInput(msg tea.KeyMsg) (subModel, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "esc":
|
||||
return m, func() tea.Msg { return switchToTableListMsg{} }
|
||||
|
||||
case "enter":
|
||||
if err := m.executeQuery(); err != nil {
|
||||
// Handle error - could set an error field
|
||||
}
|
||||
|
||||
case "backspace":
|
||||
if len(m.queryInput) > 0 {
|
||||
m.queryInput = m.queryInput[:len(m.queryInput)-1]
|
||||
}
|
||||
|
||||
default:
|
||||
// In query mode, all single characters should be treated as input
|
||||
if len(msg.String()) == 1 {
|
||||
m.queryInput += msg.String()
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *queryModel) executeQuery() error {
|
||||
if strings.TrimSpace(m.queryInput) == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
rows, err := m.shared.db.Query(m.queryInput)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// Get column names
|
||||
columns, err := rows.Columns()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.columns = columns
|
||||
|
||||
// Get data
|
||||
m.results = [][]string{}
|
||||
for rows.Next() {
|
||||
values := make([]any, len(m.columns))
|
||||
valuePtrs := make([]any, len(m.columns))
|
||||
for i := range values {
|
||||
valuePtrs[i] = &values[i]
|
||||
}
|
||||
|
||||
if err := rows.Scan(valuePtrs...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
row := make([]string, len(m.columns))
|
||||
for i, val := range values {
|
||||
if val == nil {
|
||||
row[i] = "NULL"
|
||||
} else {
|
||||
row[i] = fmt.Sprintf("%v", val)
|
||||
}
|
||||
}
|
||||
m.results = append(m.results, row)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *queryModel) getVisibleRowCount() int {
|
||||
reservedLines := 9
|
||||
return max(1, m.shared.height-reservedLines)
|
||||
}
|
||||
|
||||
func (m *queryModel) View() string {
|
||||
var content strings.Builder
|
||||
|
||||
content.WriteString(titleStyle.Render("SQL Query"))
|
||||
content.WriteString("\n\n")
|
||||
|
||||
content.WriteString("Query: ")
|
||||
content.WriteString(m.queryInput)
|
||||
content.WriteString("_") // cursor
|
||||
content.WriteString("\n\n")
|
||||
|
||||
if len(m.results) > 0 {
|
||||
// Limit rows to fit screen
|
||||
visibleRows := m.getVisibleRowCount()
|
||||
displayRows := min(len(m.results), visibleRows)
|
||||
|
||||
// Show query results
|
||||
colWidth := 10
|
||||
if len(m.columns) > 0 && m.shared.width > 0 {
|
||||
colWidth = max(10, (m.shared.width-len(m.columns)*3)/len(m.columns))
|
||||
}
|
||||
|
||||
var headerRow strings.Builder
|
||||
for i, col := range m.columns {
|
||||
if i > 0 {
|
||||
headerRow.WriteString(" | ")
|
||||
}
|
||||
headerRow.WriteString(fmt.Sprintf("%-*s", colWidth, truncateString(col, colWidth)))
|
||||
}
|
||||
content.WriteString(selectedStyle.Render(headerRow.String()))
|
||||
content.WriteString("\n")
|
||||
|
||||
var separator strings.Builder
|
||||
for i := range m.columns {
|
||||
if i > 0 {
|
||||
separator.WriteString("-+-")
|
||||
}
|
||||
separator.WriteString(strings.Repeat("-", colWidth))
|
||||
}
|
||||
content.WriteString(separator.String())
|
||||
content.WriteString("\n")
|
||||
|
||||
for i := 0; i < displayRows; i++ {
|
||||
row := m.results[i]
|
||||
var dataRow strings.Builder
|
||||
for j, cell := range row {
|
||||
if j > 0 {
|
||||
dataRow.WriteString(" | ")
|
||||
}
|
||||
dataRow.WriteString(fmt.Sprintf("%-*s", colWidth, truncateString(cell, colWidth)))
|
||||
}
|
||||
content.WriteString(normalStyle.Render(dataRow.String()))
|
||||
content.WriteString("\n")
|
||||
}
|
||||
|
||||
if len(m.results) > displayRows {
|
||||
content.WriteString(helpStyle.Render(fmt.Sprintf("... and %d more rows", len(m.results)-displayRows)))
|
||||
content.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
content.WriteString("\n")
|
||||
content.WriteString(helpStyle.Render("enter: execute query • esc: back • q: quit"))
|
||||
|
||||
return content.String()
|
||||
}
|
||||
162
row_detail.go
Normal file
162
row_detail.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
// Row Detail Model
|
||||
type rowDetailModel struct {
|
||||
shared *sharedData
|
||||
rowIndex int
|
||||
selectedCol int
|
||||
}
|
||||
|
||||
func newRowDetailModel(shared *sharedData, rowIndex int) *rowDetailModel {
|
||||
return &rowDetailModel{
|
||||
shared: shared,
|
||||
rowIndex: rowIndex,
|
||||
selectedCol: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *rowDetailModel) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *rowDetailModel) Update(msg tea.Msg) (subModel, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
return m.handleNavigation(msg)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *rowDetailModel) handleNavigation(msg tea.KeyMsg) (subModel, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "esc":
|
||||
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) &&
|
||||
m.selectedCol < len(m.shared.columns) {
|
||||
return m, func() tea.Msg {
|
||||
return switchToEditCellMsg{rowIndex: m.rowIndex, colIndex: m.selectedCol}
|
||||
}
|
||||
}
|
||||
|
||||
case "up", "k":
|
||||
if m.selectedCol > 0 {
|
||||
m.selectedCol--
|
||||
}
|
||||
|
||||
case "down", "j":
|
||||
if m.selectedCol < len(m.shared.columns)-1 {
|
||||
m.selectedCol++
|
||||
}
|
||||
|
||||
case "r":
|
||||
return m, func() tea.Msg { return refreshDataMsg{} }
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *rowDetailModel) getVisibleRowCount() int {
|
||||
reservedLines := 9
|
||||
return max(1, m.shared.height-reservedLines)
|
||||
}
|
||||
|
||||
func (m *rowDetailModel) View() string {
|
||||
var content strings.Builder
|
||||
|
||||
tableName := m.shared.filteredTables[m.shared.selectedTable]
|
||||
content.WriteString(titleStyle.Render(fmt.Sprintf("Row Detail: %s", tableName)))
|
||||
content.WriteString("\n\n")
|
||||
|
||||
if m.rowIndex >= len(m.shared.filteredData) {
|
||||
content.WriteString("Invalid row selection")
|
||||
} else {
|
||||
row := m.shared.filteredData[m.rowIndex]
|
||||
|
||||
// Show as 2-column table: Column | Value
|
||||
colWidth := max(15, m.shared.width/3)
|
||||
valueWidth := max(20, m.shared.width-colWidth-5)
|
||||
|
||||
// Header
|
||||
headerRow := fmt.Sprintf("%-*s | %-*s", colWidth, "Column", valueWidth, "Value")
|
||||
content.WriteString(selectedStyle.Render(headerRow))
|
||||
content.WriteString("\n")
|
||||
|
||||
// Separator
|
||||
separator := strings.Repeat("-", colWidth) + "-+-" + strings.Repeat("-", valueWidth)
|
||||
content.WriteString(separator)
|
||||
content.WriteString("\n")
|
||||
|
||||
// Data rows
|
||||
visibleRows := m.getVisibleRowCount()
|
||||
displayRows := min(len(m.shared.columns), visibleRows)
|
||||
|
||||
for i := 0; i < displayRows; i++ {
|
||||
if i >= len(m.shared.columns) || i >= len(row) {
|
||||
break
|
||||
}
|
||||
|
||||
col := m.shared.columns[i]
|
||||
val := row[i]
|
||||
|
||||
// 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 {
|
||||
// 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")
|
||||
content.WriteString(helpStyle.Render("↑/↓: select field • enter: edit • esc: back • q: quit"))
|
||||
|
||||
return content.String()
|
||||
}
|
||||
228
table_data.go
Normal file
228
table_data.go
Normal file
@@ -0,0 +1,228 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
// Table Data Model
|
||||
type tableDataModel struct {
|
||||
shared *sharedData
|
||||
selectedRow int
|
||||
searchInput string
|
||||
searching bool
|
||||
}
|
||||
|
||||
func newTableDataModel(shared *sharedData) *tableDataModel {
|
||||
return &tableDataModel{
|
||||
shared: shared,
|
||||
selectedRow: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *tableDataModel) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *tableDataModel) Update(msg tea.Msg) (subModel, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
if m.searching {
|
||||
return m.handleSearchInput(msg)
|
||||
}
|
||||
return m.handleNavigation(msg)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *tableDataModel) handleSearchInput(msg tea.KeyMsg) (subModel, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "esc", "enter":
|
||||
m.searching = false
|
||||
m.filterData()
|
||||
case "backspace":
|
||||
if len(m.searchInput) > 0 {
|
||||
m.searchInput = m.searchInput[:len(m.searchInput)-1]
|
||||
m.filterData()
|
||||
}
|
||||
default:
|
||||
if len(msg.String()) == 1 {
|
||||
m.searchInput += msg.String()
|
||||
m.filterData()
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *tableDataModel) handleNavigation(msg tea.KeyMsg) (subModel, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "esc":
|
||||
return m, func() tea.Msg { return switchToTableListMsg{} }
|
||||
|
||||
case "/":
|
||||
m.searching = true
|
||||
m.searchInput = ""
|
||||
return m, nil
|
||||
|
||||
case "enter":
|
||||
if len(m.shared.filteredData) > 0 {
|
||||
return m, func() tea.Msg {
|
||||
return switchToRowDetailMsg{rowIndex: m.selectedRow}
|
||||
}
|
||||
}
|
||||
|
||||
case "r":
|
||||
return m, func() tea.Msg { return refreshDataMsg{} }
|
||||
|
||||
case "up", "k":
|
||||
if m.selectedRow > 0 {
|
||||
m.selectedRow--
|
||||
}
|
||||
|
||||
case "down", "j":
|
||||
if m.selectedRow < len(m.shared.filteredData)-1 {
|
||||
m.selectedRow++
|
||||
}
|
||||
|
||||
case "left", "h":
|
||||
if m.shared.currentPage > 0 {
|
||||
m.shared.currentPage--
|
||||
m.selectedRow = 0
|
||||
return m, func() tea.Msg { return refreshDataMsg{} }
|
||||
}
|
||||
|
||||
case "right", "l":
|
||||
maxPage := max(0, (m.shared.totalRows-1)/pageSize)
|
||||
if m.shared.currentPage < maxPage {
|
||||
m.shared.currentPage++
|
||||
m.selectedRow = 0
|
||||
return m, func() tea.Msg { return refreshDataMsg{} }
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *tableDataModel) filterData() {
|
||||
if m.searchInput == "" {
|
||||
m.shared.filteredData = make([][]string, len(m.shared.tableData))
|
||||
copy(m.shared.filteredData, m.shared.tableData)
|
||||
} else {
|
||||
m.shared.filteredData = [][]string{}
|
||||
searchLower := strings.ToLower(m.searchInput)
|
||||
for _, row := range m.shared.tableData {
|
||||
found := false
|
||||
for _, cell := range row {
|
||||
if strings.Contains(strings.ToLower(cell), searchLower) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if found {
|
||||
m.shared.filteredData = append(m.shared.filteredData, row)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if m.selectedRow >= len(m.shared.filteredData) {
|
||||
m.selectedRow = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (m *tableDataModel) getVisibleRowCount() int {
|
||||
reservedLines := 9
|
||||
if m.searching {
|
||||
reservedLines += 2
|
||||
}
|
||||
return max(1, m.shared.height-reservedLines)
|
||||
}
|
||||
|
||||
func (m *tableDataModel) View() string {
|
||||
var content strings.Builder
|
||||
|
||||
tableName := m.shared.filteredTables[m.shared.selectedTable]
|
||||
maxPage := max(0, (m.shared.totalRows-1)/pageSize)
|
||||
|
||||
content.WriteString(titleStyle.Render(fmt.Sprintf("Table: %s (Page %d/%d)",
|
||||
tableName, m.shared.currentPage+1, maxPage+1)))
|
||||
content.WriteString("\n")
|
||||
|
||||
if m.searching {
|
||||
content.WriteString("\nSearch data: " + m.searchInput + "_")
|
||||
content.WriteString("\n")
|
||||
} else if m.searchInput != "" {
|
||||
content.WriteString(fmt.Sprintf("\nFiltered by: %s (%d/%d rows)",
|
||||
m.searchInput, len(m.shared.filteredData), len(m.shared.tableData)))
|
||||
content.WriteString("\n")
|
||||
}
|
||||
content.WriteString("\n")
|
||||
|
||||
if len(m.shared.filteredData) == 0 {
|
||||
if m.searchInput != "" {
|
||||
content.WriteString("No rows match your search")
|
||||
} else {
|
||||
content.WriteString("No data in table")
|
||||
}
|
||||
} else {
|
||||
visibleRows := m.getVisibleRowCount()
|
||||
displayRows := min(len(m.shared.filteredData), visibleRows)
|
||||
|
||||
// Create table header
|
||||
colWidth := 10
|
||||
if len(m.shared.columns) > 0 && m.shared.width > 0 {
|
||||
colWidth = max(10, (m.shared.width-len(m.shared.columns)*3)/len(m.shared.columns))
|
||||
}
|
||||
|
||||
var headerRow strings.Builder
|
||||
for i, col := range m.shared.columns {
|
||||
if i > 0 {
|
||||
headerRow.WriteString(" | ")
|
||||
}
|
||||
headerRow.WriteString(fmt.Sprintf("%-*s", colWidth, truncateString(col, colWidth)))
|
||||
}
|
||||
content.WriteString(selectedStyle.Render(headerRow.String()))
|
||||
content.WriteString("\n")
|
||||
|
||||
// Add separator
|
||||
var separator strings.Builder
|
||||
for i := range m.shared.columns {
|
||||
if i > 0 {
|
||||
separator.WriteString("-+-")
|
||||
}
|
||||
separator.WriteString(strings.Repeat("-", colWidth))
|
||||
}
|
||||
content.WriteString(separator.String())
|
||||
content.WriteString("\n")
|
||||
|
||||
// Add data rows with highlighting
|
||||
for i := 0; i < displayRows; i++ {
|
||||
if i >= len(m.shared.filteredData) {
|
||||
break
|
||||
}
|
||||
row := m.shared.filteredData[i]
|
||||
var dataRow strings.Builder
|
||||
for j, cell := range row {
|
||||
if j > 0 {
|
||||
dataRow.WriteString(" | ")
|
||||
}
|
||||
dataRow.WriteString(fmt.Sprintf("%-*s", colWidth, truncateString(cell, colWidth)))
|
||||
}
|
||||
if i == m.selectedRow {
|
||||
content.WriteString(selectedStyle.Render(dataRow.String()))
|
||||
} else {
|
||||
content.WriteString(normalStyle.Render(dataRow.String()))
|
||||
}
|
||||
content.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
content.WriteString("\n")
|
||||
if m.searching {
|
||||
content.WriteString(helpStyle.Render("Type to search • enter/esc: finish search"))
|
||||
} else {
|
||||
content.WriteString(helpStyle.Render("↑/↓: select row • ←/→: page • /: search • enter: view row • esc: back • r: refresh • q: quit"))
|
||||
}
|
||||
|
||||
return content.String()
|
||||
}
|
||||
198
table_list.go
Normal file
198
table_list.go
Normal file
@@ -0,0 +1,198 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
// Table List Model
|
||||
type tableListModel struct {
|
||||
shared *sharedData
|
||||
searchInput string
|
||||
searching bool
|
||||
selectedTable int
|
||||
currentPage int
|
||||
}
|
||||
|
||||
func newTableListModel(shared *sharedData) *tableListModel {
|
||||
return &tableListModel{
|
||||
shared: shared,
|
||||
selectedTable: 0,
|
||||
currentPage: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *tableListModel) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *tableListModel) Update(msg tea.Msg) (subModel, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
if m.searching {
|
||||
return m.handleSearchInput(msg)
|
||||
}
|
||||
return m.handleNavigation(msg)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *tableListModel) handleSearchInput(msg tea.KeyMsg) (subModel, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "esc", "enter":
|
||||
m.searching = false
|
||||
m.filterTables()
|
||||
case "backspace":
|
||||
if len(m.searchInput) > 0 {
|
||||
m.searchInput = m.searchInput[:len(m.searchInput)-1]
|
||||
m.filterTables()
|
||||
}
|
||||
default:
|
||||
if len(msg.String()) == 1 {
|
||||
m.searchInput += msg.String()
|
||||
m.filterTables()
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *tableListModel) handleNavigation(msg tea.KeyMsg) (subModel, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "/":
|
||||
m.searching = true
|
||||
m.searchInput = ""
|
||||
return m, nil
|
||||
|
||||
case "enter":
|
||||
if len(m.shared.filteredTables) > 0 {
|
||||
return m, func() tea.Msg {
|
||||
return switchToTableDataMsg{tableIndex: m.selectedTable}
|
||||
}
|
||||
}
|
||||
|
||||
case "s":
|
||||
return m, func() tea.Msg { return switchToQueryMsg{} }
|
||||
|
||||
case "r":
|
||||
if err := m.shared.loadTables(); err == nil {
|
||||
m.filterTables()
|
||||
}
|
||||
|
||||
case "up", "k":
|
||||
if m.selectedTable > 0 {
|
||||
m.selectedTable--
|
||||
m.adjustPage()
|
||||
}
|
||||
|
||||
case "down", "j":
|
||||
if m.selectedTable < len(m.shared.filteredTables)-1 {
|
||||
m.selectedTable++
|
||||
m.adjustPage()
|
||||
}
|
||||
|
||||
case "left", "h":
|
||||
if m.currentPage > 0 {
|
||||
m.currentPage--
|
||||
m.selectedTable = m.currentPage * m.getVisibleCount()
|
||||
}
|
||||
|
||||
case "right", "l":
|
||||
maxPage := (len(m.shared.filteredTables) - 1) / m.getVisibleCount()
|
||||
if m.currentPage < maxPage {
|
||||
m.currentPage++
|
||||
m.selectedTable = m.currentPage * m.getVisibleCount()
|
||||
if m.selectedTable >= len(m.shared.filteredTables) {
|
||||
m.selectedTable = len(m.shared.filteredTables) - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *tableListModel) filterTables() {
|
||||
if m.searchInput == "" {
|
||||
m.shared.filteredTables = make([]string, len(m.shared.tables))
|
||||
copy(m.shared.filteredTables, m.shared.tables)
|
||||
} else {
|
||||
m.shared.filteredTables = []string{}
|
||||
searchLower := strings.ToLower(m.searchInput)
|
||||
for _, table := range m.shared.tables {
|
||||
if strings.Contains(strings.ToLower(table), searchLower) {
|
||||
m.shared.filteredTables = append(m.shared.filteredTables, table)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if m.selectedTable >= len(m.shared.filteredTables) {
|
||||
m.selectedTable = 0
|
||||
m.currentPage = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (m *tableListModel) getVisibleCount() int {
|
||||
reservedLines := 8
|
||||
if m.searching {
|
||||
reservedLines += 2
|
||||
}
|
||||
return max(1, m.shared.height-reservedLines)
|
||||
}
|
||||
|
||||
func (m *tableListModel) adjustPage() {
|
||||
visibleCount := m.getVisibleCount()
|
||||
m.currentPage = m.selectedTable / visibleCount
|
||||
}
|
||||
|
||||
func (m *tableListModel) View() string {
|
||||
var content strings.Builder
|
||||
|
||||
content.WriteString(titleStyle.Render("SQLite TUI - Tables"))
|
||||
content.WriteString("\n")
|
||||
|
||||
if m.searching {
|
||||
content.WriteString("\nSearch: " + m.searchInput + "_")
|
||||
content.WriteString("\n")
|
||||
} else if m.searchInput != "" {
|
||||
content.WriteString(fmt.Sprintf("\nFiltered by: %s (%d/%d tables)",
|
||||
m.searchInput, len(m.shared.filteredTables), len(m.shared.tables)))
|
||||
content.WriteString("\n")
|
||||
}
|
||||
content.WriteString("\n")
|
||||
|
||||
if len(m.shared.filteredTables) == 0 {
|
||||
if m.searchInput != "" {
|
||||
content.WriteString("No tables match your search")
|
||||
} else {
|
||||
content.WriteString("No tables found in database")
|
||||
}
|
||||
} else {
|
||||
visibleCount := m.getVisibleCount()
|
||||
startIdx := m.currentPage * visibleCount
|
||||
endIdx := min(startIdx+visibleCount, len(m.shared.filteredTables))
|
||||
|
||||
for i := startIdx; i < endIdx; i++ {
|
||||
table := m.shared.filteredTables[i]
|
||||
if i == m.selectedTable {
|
||||
content.WriteString(selectedStyle.Render(fmt.Sprintf("> %s", table)))
|
||||
} else {
|
||||
content.WriteString(normalStyle.Render(fmt.Sprintf(" %s", table)))
|
||||
}
|
||||
content.WriteString("\n")
|
||||
}
|
||||
|
||||
if len(m.shared.filteredTables) > visibleCount {
|
||||
totalPages := (len(m.shared.filteredTables) - 1) / visibleCount + 1
|
||||
content.WriteString(fmt.Sprintf("\nPage %d/%d", m.currentPage+1, totalPages))
|
||||
}
|
||||
}
|
||||
|
||||
content.WriteString("\n")
|
||||
if m.searching {
|
||||
content.WriteString(helpStyle.Render("Type to search • enter/esc: finish search"))
|
||||
} else {
|
||||
content.WriteString(helpStyle.Render("↑/↓: navigate • ←/→: page • /: search • enter: view • s: SQL • r: refresh • q: quit"))
|
||||
}
|
||||
|
||||
return content.String()
|
||||
}
|
||||
Reference in New Issue
Block a user