mirror of
https://github.com/taigrr/teaqlite.git
synced 2026-04-02 04:59:03 -07:00
use cobra/fang
This commit is contained in:
605
internal/app/app.go
Normal file
605
internal/app/app.go
Normal file
@@ -0,0 +1,605 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
_ "modernc.org/sqlite" // Import SQLite driver
|
||||
)
|
||||
|
||||
const (
|
||||
PageSize = 20
|
||||
)
|
||||
|
||||
// Custom message types
|
||||
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 }
|
||||
)
|
||||
|
||||
// Main application model
|
||||
type Model struct {
|
||||
db *sql.DB
|
||||
currentView tea.Model
|
||||
width int
|
||||
height int
|
||||
err error
|
||||
}
|
||||
|
||||
// Shared data that all models need access to
|
||||
type SharedData struct {
|
||||
DB *sql.DB
|
||||
Tables []string
|
||||
FilteredTables []string
|
||||
TableData [][]string
|
||||
FilteredData [][]string
|
||||
Columns []string
|
||||
PrimaryKeys []string
|
||||
SelectedTable int
|
||||
TotalRows int
|
||||
CurrentPage int
|
||||
Width int
|
||||
Height int
|
||||
// Query result context
|
||||
IsQueryResult bool
|
||||
QueryTableName string // For simple queries, store the source table
|
||||
}
|
||||
|
||||
func NewSharedData(db *sql.DB) *SharedData {
|
||||
return &SharedData{
|
||||
DB: db,
|
||||
FilteredTables: []string{},
|
||||
FilteredData: [][]string{},
|
||||
Width: 80,
|
||||
Height: 24,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SharedData) LoadTables() error {
|
||||
query := `SELECT name FROM sqlite_master WHERE type='table' ORDER BY name`
|
||||
rows, err := s.DB.Query(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
s.Tables = []string{}
|
||||
for rows.Next() {
|
||||
var name string
|
||||
if err := rows.Scan(&name); err != nil {
|
||||
return err
|
||||
}
|
||||
s.Tables = append(s.Tables, name)
|
||||
}
|
||||
s.FilteredTables = make([]string, len(s.Tables))
|
||||
copy(s.FilteredTables, s.Tables)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SharedData) LoadTableData() error {
|
||||
if s.SelectedTable >= len(s.FilteredTables) {
|
||||
return fmt.Errorf("invalid table selection")
|
||||
}
|
||||
|
||||
tableName := s.FilteredTables[s.SelectedTable]
|
||||
|
||||
// Get column info and primary keys
|
||||
rows, err := s.DB.Query(fmt.Sprintf("PRAGMA table_info(%s)", tableName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
s.Columns = []string{}
|
||||
s.PrimaryKeys = []string{}
|
||||
for rows.Next() {
|
||||
var cid int
|
||||
var name, dataType string
|
||||
var notNull, pk int
|
||||
var defaultValue sql.NullString
|
||||
|
||||
if err := rows.Scan(&cid, &name, &dataType, ¬Null, &defaultValue, &pk); err != nil {
|
||||
return err
|
||||
}
|
||||
s.Columns = append(s.Columns, name)
|
||||
if pk == 1 {
|
||||
s.PrimaryKeys = append(s.PrimaryKeys, name)
|
||||
}
|
||||
}
|
||||
|
||||
// Get total row count
|
||||
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM %s", tableName)
|
||||
err = s.DB.QueryRow(countQuery).Scan(&s.TotalRows)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get paginated data
|
||||
offset := s.CurrentPage * PageSize
|
||||
dataQuery := fmt.Sprintf("SELECT * FROM %s LIMIT %d OFFSET %d", tableName, PageSize, offset)
|
||||
|
||||
rows, err = s.DB.Query(dataQuery)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
s.TableData = [][]string{}
|
||||
for rows.Next() {
|
||||
values := make([]any, len(s.Columns))
|
||||
valuePtrs := make([]any, len(s.Columns))
|
||||
for i := range values {
|
||||
valuePtrs[i] = &values[i]
|
||||
}
|
||||
|
||||
if err := rows.Scan(valuePtrs...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
row := make([]string, len(s.Columns))
|
||||
for i, val := range values {
|
||||
if val == nil {
|
||||
row[i] = "NULL"
|
||||
} else {
|
||||
row[i] = fmt.Sprintf("%v", val)
|
||||
}
|
||||
}
|
||||
s.TableData = append(s.TableData, row)
|
||||
}
|
||||
|
||||
s.FilteredData = make([][]string, len(s.TableData))
|
||||
copy(s.FilteredData, s.TableData)
|
||||
|
||||
// Reset query result context since this is regular table data
|
||||
s.IsQueryResult = false
|
||||
s.QueryTableName = ""
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SharedData) UpdateCell(rowIndex, colIndex int, newValue string) error {
|
||||
if rowIndex >= len(s.FilteredData) || colIndex >= len(s.Columns) {
|
||||
return fmt.Errorf("invalid row or column index")
|
||||
}
|
||||
|
||||
var tableName string
|
||||
var err error
|
||||
|
||||
if s.IsQueryResult {
|
||||
// For query results, try to determine the source table
|
||||
if s.QueryTableName != "" {
|
||||
tableName = s.QueryTableName
|
||||
} else {
|
||||
// Try to infer table from column names and data
|
||||
tableName, err = s.inferTableFromQueryResult(rowIndex, colIndex)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot determine source table for query result: %v", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For regular table data
|
||||
tableName = s.FilteredTables[s.SelectedTable]
|
||||
}
|
||||
|
||||
columnName := s.Columns[colIndex]
|
||||
|
||||
// Get table info for the target table to find primary keys
|
||||
tableColumns, tablePrimaryKeys, err := s.getTableInfo(tableName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get table info for %s: %v", tableName, err)
|
||||
}
|
||||
|
||||
// Build WHERE clause using primary keys or all columns if no primary key
|
||||
var whereClause strings.Builder
|
||||
var args []any
|
||||
|
||||
if len(tablePrimaryKeys) > 0 {
|
||||
// Use primary keys for WHERE clause
|
||||
for i, pkCol := range tablePrimaryKeys {
|
||||
if i > 0 {
|
||||
whereClause.WriteString(" AND ")
|
||||
}
|
||||
|
||||
// Find the value for this primary key in our data
|
||||
pkValue, err := s.findColumnValue(rowIndex, pkCol, tableColumns)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find primary key value for %s: %v", pkCol, err)
|
||||
}
|
||||
|
||||
whereClause.WriteString(fmt.Sprintf("%s = ?", pkCol))
|
||||
args = append(args, pkValue)
|
||||
}
|
||||
} else {
|
||||
// Use all columns for WHERE clause (less reliable but works)
|
||||
for i, col := range tableColumns {
|
||||
if i > 0 {
|
||||
whereClause.WriteString(" AND ")
|
||||
}
|
||||
|
||||
colValue, err := s.findColumnValue(rowIndex, col, tableColumns)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find column value for %s: %v", col, err)
|
||||
}
|
||||
|
||||
whereClause.WriteString(fmt.Sprintf("%s = ?", col))
|
||||
args = append(args, colValue)
|
||||
}
|
||||
}
|
||||
|
||||
// Execute UPDATE
|
||||
updateQuery := fmt.Sprintf("UPDATE %s SET %s = ? WHERE %s", tableName, columnName, whereClause.String())
|
||||
args = append([]any{newValue}, args...)
|
||||
|
||||
_, err = s.DB.Exec(updateQuery, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update local data
|
||||
s.FilteredData[rowIndex][colIndex] = newValue
|
||||
// Also update the original data if it exists
|
||||
for i, row := range s.TableData {
|
||||
if len(row) > colIndex {
|
||||
match := true
|
||||
for j, cell := range row {
|
||||
if j < len(s.FilteredData[rowIndex]) && cell != s.FilteredData[rowIndex][j] && j != colIndex {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if match {
|
||||
s.TableData[i][colIndex] = newValue
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper function to get table info
|
||||
func (s *SharedData) getTableInfo(tableName string) ([]string, []string, error) {
|
||||
rows, err := s.DB.Query(fmt.Sprintf("PRAGMA table_info(%s)", tableName))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var columns []string
|
||||
var primaryKeys []string
|
||||
|
||||
for rows.Next() {
|
||||
var cid int
|
||||
var name, dataType string
|
||||
var notNull, pk int
|
||||
var defaultValue sql.NullString
|
||||
|
||||
if err := rows.Scan(&cid, &name, &dataType, ¬Null, &defaultValue, &pk); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
columns = append(columns, name)
|
||||
if pk == 1 {
|
||||
primaryKeys = append(primaryKeys, name)
|
||||
}
|
||||
}
|
||||
|
||||
return columns, primaryKeys, nil
|
||||
}
|
||||
|
||||
// Helper function to find a column value in the current row
|
||||
func (s *SharedData) findColumnValue(rowIndex int, columnName string, _ []string) (string, error) {
|
||||
// First try to find it in our current columns (for query results)
|
||||
for i, col := range s.Columns {
|
||||
if col == columnName && i < len(s.FilteredData[rowIndex]) {
|
||||
return s.FilteredData[rowIndex][i], nil
|
||||
}
|
||||
}
|
||||
|
||||
// If not found, this might be a column that's not in the query result
|
||||
// We'll need to query the database to get the current value
|
||||
if s.IsQueryResult && len(s.PrimaryKeys) > 0 {
|
||||
// Build a query to get the missing column value using available primary keys
|
||||
var whereClause strings.Builder
|
||||
var args []any
|
||||
|
||||
for i, pkCol := range s.PrimaryKeys {
|
||||
if i > 0 {
|
||||
whereClause.WriteString(" AND ")
|
||||
}
|
||||
|
||||
// Find primary key value in our data
|
||||
pkIndex := -1
|
||||
for j, col := range s.Columns {
|
||||
if col == pkCol {
|
||||
pkIndex = j
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if pkIndex >= 0 {
|
||||
whereClause.WriteString(fmt.Sprintf("%s = ?", pkCol))
|
||||
args = append(args, s.FilteredData[rowIndex][pkIndex])
|
||||
}
|
||||
}
|
||||
|
||||
if whereClause.Len() > 0 {
|
||||
tableName := s.QueryTableName
|
||||
if tableName == "" {
|
||||
// Try to infer table name
|
||||
tableName, _ = s.inferTableFromQueryResult(rowIndex, 0)
|
||||
}
|
||||
|
||||
query := fmt.Sprintf("SELECT %s FROM %s WHERE %s", columnName, tableName, whereClause.String())
|
||||
var value string
|
||||
err := s.DB.QueryRow(query, args...).Scan(&value)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("column %s not found in current data", columnName)
|
||||
}
|
||||
|
||||
// Helper function to try to infer the source table from query results
|
||||
func (s *SharedData) inferTableFromQueryResult(_, _ int) (string, error) {
|
||||
// This is a simple heuristic - try to find a table that has all our columns
|
||||
for _, tableName := range s.Tables {
|
||||
tableColumns, _, err := s.getTableInfo(tableName)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this table has all our columns
|
||||
hasAllColumns := true
|
||||
for _, queryCol := range s.Columns {
|
||||
found := slices.Contains(tableColumns, queryCol)
|
||||
if !found {
|
||||
hasAllColumns = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if hasAllColumns {
|
||||
// Cache this for future use
|
||||
s.QueryTableName = tableName
|
||||
return tableName, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("could not infer source table from query result")
|
||||
}
|
||||
|
||||
// Styles
|
||||
var (
|
||||
TitleStyle = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#FAFAFA")).
|
||||
Background(lipgloss.Color("#7D56F4")).
|
||||
Padding(0, 1)
|
||||
|
||||
SelectedStyle = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#FAFAFA")).
|
||||
Background(lipgloss.Color("#F25D94"))
|
||||
|
||||
NormalStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FAFAFA"))
|
||||
|
||||
ErrorStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#FF0000")).
|
||||
Bold(true)
|
||||
|
||||
HelpStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#626262"))
|
||||
)
|
||||
|
||||
// Utility functions
|
||||
func TruncateString(s string, maxLen int) string {
|
||||
if len(s) <= maxLen {
|
||||
return s
|
||||
}
|
||||
return s[:maxLen-3] + "..."
|
||||
}
|
||||
|
||||
func WrapText(text string, width int) []string {
|
||||
if width <= 0 {
|
||||
return []string{text}
|
||||
}
|
||||
|
||||
var lines []string
|
||||
words := strings.Fields(text)
|
||||
if len(words) == 0 {
|
||||
return []string{text}
|
||||
}
|
||||
|
||||
currentLine := ""
|
||||
for _, word := range words {
|
||||
if len(currentLine)+len(word)+1 > width {
|
||||
if currentLine != "" {
|
||||
lines = append(lines, currentLine)
|
||||
currentLine = word
|
||||
} else {
|
||||
for len(word) > width {
|
||||
lines = append(lines, word[:width])
|
||||
word = word[width:]
|
||||
}
|
||||
currentLine = word
|
||||
}
|
||||
} else {
|
||||
if currentLine != "" {
|
||||
currentLine += " " + word
|
||||
} else {
|
||||
currentLine = word
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if currentLine != "" {
|
||||
lines = append(lines, currentLine)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
func Min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func Max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func InitialModel(db *sql.DB) *Model {
|
||||
shared := NewSharedData(db)
|
||||
if err := shared.LoadTables(); err != nil {
|
||||
return &Model{err: err}
|
||||
}
|
||||
|
||||
return &Model{
|
||||
db: db,
|
||||
currentView: NewTableListModel(shared),
|
||||
width: 80,
|
||||
height: 24,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) Init() tea.Cmd {
|
||||
return m.currentView.Init()
|
||||
}
|
||||
|
||||
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
m.height = msg.Height
|
||||
// Update current view with new dimensions
|
||||
if tableList, ok := m.currentView.(*TableListModel); ok {
|
||||
tableList.Shared.Width = m.width
|
||||
tableList.Shared.Height = m.height
|
||||
}
|
||||
// Add similar updates for other model types as needed
|
||||
|
||||
case tea.KeyMsg:
|
||||
if msg.String() == "ctrl+c" {
|
||||
return m, tea.Quit
|
||||
}
|
||||
|
||||
case SwitchToTableListMsg:
|
||||
m.currentView = NewTableListModel(m.getSharedData())
|
||||
return m, nil
|
||||
|
||||
case SwitchToTableDataMsg:
|
||||
shared := m.getSharedData()
|
||||
shared.SelectedTable = msg.TableIndex
|
||||
if err := shared.LoadTableData(); err != nil {
|
||||
m.err = err
|
||||
return m, nil
|
||||
}
|
||||
m.currentView = NewTableDataModel(shared)
|
||||
return m, nil
|
||||
|
||||
case SwitchToRowDetailMsg:
|
||||
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
|
||||
|
||||
case SwitchToQueryMsg:
|
||||
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 {
|
||||
m.err = err
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case UpdateCellMsg:
|
||||
shared := m.getSharedData()
|
||||
if err := shared.UpdateCell(msg.RowIndex, msg.ColIndex, msg.Value); err != nil {
|
||||
m.err = err
|
||||
}
|
||||
return m, func() tea.Msg { return SwitchToRowDetailMsg{msg.RowIndex} }
|
||||
}
|
||||
|
||||
if m.err != nil {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
var cmd tea.Cmd
|
||||
m.currentView, cmd = m.currentView.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m *Model) View() string {
|
||||
if m.err != nil {
|
||||
return ErrorStyle.Render(fmt.Sprintf("Error: %v\n\nPress 'ctrl+c' to quit", m.err))
|
||||
}
|
||||
return m.currentView.View()
|
||||
}
|
||||
|
||||
func (m *Model) Err() error {
|
||||
return m.err
|
||||
}
|
||||
|
||||
func (m *Model) getSharedData() *SharedData {
|
||||
// Extract shared data from current view
|
||||
switch v := m.currentView.(type) {
|
||||
case *TableListModel:
|
||||
return v.Shared
|
||||
case *TableDataModel:
|
||||
return v.Shared
|
||||
case *RowDetailModel:
|
||||
return v.Shared
|
||||
case *EditCellModel:
|
||||
return v.Shared
|
||||
case *QueryModel:
|
||||
return v.Shared
|
||||
default:
|
||||
// Fallback - create new shared data
|
||||
return NewSharedData(m.db)
|
||||
}
|
||||
}
|
||||
91
internal/app/edit_cell.go
Normal file
91
internal/app/edit_cell.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
// Edit Cell Model
|
||||
type EditCellModel struct {
|
||||
Shared *SharedData
|
||||
rowIndex int
|
||||
colIndex int
|
||||
value string
|
||||
cursor int
|
||||
}
|
||||
|
||||
func NewEditCellModel(shared *SharedData, rowIndex, colIndex int) *EditCellModel {
|
||||
value := ""
|
||||
if rowIndex < len(shared.FilteredData) && colIndex < len(shared.FilteredData[rowIndex]) {
|
||||
value = shared.FilteredData[rowIndex][colIndex]
|
||||
}
|
||||
|
||||
return &EditCellModel{
|
||||
Shared: shared,
|
||||
rowIndex: rowIndex,
|
||||
colIndex: colIndex,
|
||||
value: value,
|
||||
cursor: len(value),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *EditCellModel) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *EditCellModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
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.value,
|
||||
}
|
||||
}
|
||||
|
||||
case "backspace":
|
||||
if m.cursor > 0 {
|
||||
m.value = m.value[:m.cursor-1] + m.value[m.cursor:]
|
||||
m.cursor--
|
||||
}
|
||||
|
||||
case "left":
|
||||
if m.cursor > 0 {
|
||||
m.cursor--
|
||||
}
|
||||
|
||||
case "right":
|
||||
if m.cursor < len(m.value) {
|
||||
m.cursor++
|
||||
}
|
||||
|
||||
default:
|
||||
if len(msg.String()) == 1 {
|
||||
m.value = m.value[:m.cursor] + msg.String() + m.value[m.cursor:]
|
||||
m.cursor++
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *EditCellModel) View() string {
|
||||
columnName := ""
|
||||
if m.colIndex < len(m.Shared.Columns) {
|
||||
columnName = m.Shared.Columns[m.colIndex]
|
||||
}
|
||||
|
||||
content := TitleStyle.Render(fmt.Sprintf("Edit Cell: %s", columnName)) + "\n\n"
|
||||
content += fmt.Sprintf("Value: %s\n", m.value)
|
||||
content += fmt.Sprintf("Cursor: %d\n\n", m.cursor)
|
||||
content += HelpStyle.Render("enter: save • esc: cancel")
|
||||
|
||||
return content
|
||||
}
|
||||
231
internal/app/query.go
Normal file
231
internal/app/query.go
Normal file
@@ -0,0 +1,231 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
// Query Model
|
||||
type QueryModel struct {
|
||||
Shared *SharedData
|
||||
query string
|
||||
cursor int
|
||||
FocusOnInput bool
|
||||
selectedRow int
|
||||
results [][]string
|
||||
columns []string
|
||||
err error
|
||||
}
|
||||
|
||||
func NewQueryModel(shared *SharedData) *QueryModel {
|
||||
return &QueryModel{
|
||||
Shared: shared,
|
||||
FocusOnInput: true,
|
||||
selectedRow: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *QueryModel) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *QueryModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
if m.FocusOnInput {
|
||||
return m.handleQueryInput(msg)
|
||||
}
|
||||
return m.handleResultsNavigation(msg)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *QueryModel) handleQueryInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "esc":
|
||||
return m, func() tea.Msg { return SwitchToTableListMsg{} }
|
||||
|
||||
case "enter":
|
||||
if strings.TrimSpace(m.query) != "" {
|
||||
return m, m.executeQuery()
|
||||
}
|
||||
|
||||
case "backspace":
|
||||
if m.cursor > 0 {
|
||||
m.query = m.query[:m.cursor-1] + m.query[m.cursor:]
|
||||
m.cursor--
|
||||
}
|
||||
|
||||
case "left":
|
||||
if m.cursor > 0 {
|
||||
m.cursor--
|
||||
}
|
||||
|
||||
case "right":
|
||||
if m.cursor < len(m.query) {
|
||||
m.cursor++
|
||||
}
|
||||
|
||||
default:
|
||||
if len(msg.String()) == 1 {
|
||||
m.query = m.query[:m.cursor] + msg.String() + m.query[m.cursor:]
|
||||
m.cursor++
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *QueryModel) handleResultsNavigation(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "esc", "q":
|
||||
return m, func() tea.Msg { return SwitchToTableListMsg{} }
|
||||
|
||||
case "i":
|
||||
m.FocusOnInput = true
|
||||
return m, nil
|
||||
|
||||
case "enter":
|
||||
if len(m.results) > 0 {
|
||||
return m, func() tea.Msg {
|
||||
return SwitchToRowDetailFromQueryMsg{RowIndex: m.selectedRow}
|
||||
}
|
||||
}
|
||||
|
||||
case "up", "k":
|
||||
if m.selectedRow > 0 {
|
||||
m.selectedRow--
|
||||
}
|
||||
|
||||
case "down", "j":
|
||||
if m.selectedRow < len(m.results)-1 {
|
||||
m.selectedRow++
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *QueryModel) executeQuery() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
rows, err := m.Shared.DB.Query(m.query)
|
||||
if err != nil {
|
||||
m.err = err
|
||||
return nil
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// Get column names
|
||||
columns, err := rows.Columns()
|
||||
if err != nil {
|
||||
m.err = err
|
||||
return nil
|
||||
}
|
||||
m.columns = columns
|
||||
|
||||
// Get results
|
||||
m.results = [][]string{}
|
||||
for rows.Next() {
|
||||
values := make([]any, len(columns))
|
||||
valuePtrs := make([]any, len(columns))
|
||||
for i := range values {
|
||||
valuePtrs[i] = &values[i]
|
||||
}
|
||||
|
||||
if err := rows.Scan(valuePtrs...); err != nil {
|
||||
m.err = err
|
||||
return nil
|
||||
}
|
||||
|
||||
row := make([]string, len(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)
|
||||
}
|
||||
|
||||
// Update shared data for row detail view
|
||||
m.Shared.FilteredData = m.results
|
||||
m.Shared.Columns = m.columns
|
||||
m.Shared.IsQueryResult = true
|
||||
|
||||
m.FocusOnInput = false
|
||||
m.selectedRow = 0
|
||||
m.err = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *QueryModel) View() string {
|
||||
var content strings.Builder
|
||||
|
||||
content.WriteString(TitleStyle.Render("SQL Query"))
|
||||
content.WriteString("\n\n")
|
||||
|
||||
// Query input
|
||||
content.WriteString("Query: ")
|
||||
if m.FocusOnInput {
|
||||
content.WriteString(m.query + "_")
|
||||
} else {
|
||||
content.WriteString(m.query)
|
||||
}
|
||||
content.WriteString("\n\n")
|
||||
|
||||
// Error display
|
||||
if m.err != nil {
|
||||
content.WriteString(ErrorStyle.Render(fmt.Sprintf("Error: %v", m.err)))
|
||||
content.WriteString("\n\n")
|
||||
}
|
||||
|
||||
// Results
|
||||
if len(m.results) > 0 {
|
||||
// Column headers
|
||||
headerRow := ""
|
||||
for i, col := range m.columns {
|
||||
if i > 0 {
|
||||
headerRow += " | "
|
||||
}
|
||||
headerRow += TruncateString(col, 15)
|
||||
}
|
||||
content.WriteString(TitleStyle.Render(headerRow))
|
||||
content.WriteString("\n")
|
||||
|
||||
// Data rows
|
||||
visibleCount := Max(1, m.Shared.Height-10)
|
||||
endIdx := Min(len(m.results), visibleCount)
|
||||
|
||||
for i := 0; i < endIdx; i++ {
|
||||
row := m.results[i]
|
||||
rowStr := ""
|
||||
for j, cell := range row {
|
||||
if j > 0 {
|
||||
rowStr += " | "
|
||||
}
|
||||
rowStr += TruncateString(cell, 15)
|
||||
}
|
||||
|
||||
if i == m.selectedRow && !m.FocusOnInput {
|
||||
content.WriteString(SelectedStyle.Render("> " + rowStr))
|
||||
} else {
|
||||
content.WriteString(NormalStyle.Render(" " + rowStr))
|
||||
}
|
||||
content.WriteString("\n")
|
||||
}
|
||||
|
||||
content.WriteString(fmt.Sprintf("\n%d rows returned\n", len(m.results)))
|
||||
}
|
||||
|
||||
content.WriteString("\n")
|
||||
if m.FocusOnInput {
|
||||
content.WriteString(HelpStyle.Render("enter: execute • esc: back"))
|
||||
} else {
|
||||
content.WriteString(HelpStyle.Render("↑/↓: navigate • enter: details • i: edit query • q: back"))
|
||||
}
|
||||
|
||||
return content.String()
|
||||
}
|
||||
71
internal/app/row_detail.go
Normal file
71
internal/app/row_detail.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
// Row Detail Model
|
||||
type RowDetailModel struct {
|
||||
Shared *SharedData
|
||||
rowIndex int
|
||||
FromQuery bool
|
||||
}
|
||||
|
||||
func NewRowDetailModel(shared *SharedData, rowIndex int) *RowDetailModel {
|
||||
return &RowDetailModel{
|
||||
Shared: shared,
|
||||
rowIndex: rowIndex,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *RowDetailModel) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *RowDetailModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "q", "esc":
|
||||
if m.FromQuery {
|
||||
return m, func() tea.Msg { return ReturnToQueryMsg{} }
|
||||
}
|
||||
return m, func() tea.Msg { return SwitchToTableDataMsg{TableIndex: m.Shared.SelectedTable} }
|
||||
|
||||
case "e":
|
||||
if len(m.Shared.FilteredData) > m.rowIndex && len(m.Shared.Columns) > 0 {
|
||||
return m, func() tea.Msg {
|
||||
return SwitchToEditCellMsg{RowIndex: m.rowIndex, ColIndex: 0}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *RowDetailModel) View() string {
|
||||
var content strings.Builder
|
||||
|
||||
content.WriteString(TitleStyle.Render("Row Details"))
|
||||
content.WriteString("\n\n")
|
||||
|
||||
if m.rowIndex >= len(m.Shared.FilteredData) {
|
||||
content.WriteString("Invalid row index")
|
||||
return content.String()
|
||||
}
|
||||
|
||||
row := m.Shared.FilteredData[m.rowIndex]
|
||||
for i, col := range m.Shared.Columns {
|
||||
if i < len(row) {
|
||||
content.WriteString(fmt.Sprintf("%s: %s\n", col, row[i]))
|
||||
}
|
||||
}
|
||||
|
||||
content.WriteString("\n")
|
||||
content.WriteString(HelpStyle.Render("e: edit • q: back"))
|
||||
|
||||
return content.String()
|
||||
}
|
||||
205
internal/app/table_data.go
Normal file
205
internal/app/table_data.go
Normal file
@@ -0,0 +1,205 @@
|
||||
package app
|
||||
|
||||
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) (tea.Model, 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) (tea.Model, 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) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "q", "esc":
|
||||
return m, func() tea.Msg { return SwitchToTableListMsg{} }
|
||||
|
||||
case "enter":
|
||||
if len(m.Shared.FilteredData) > 0 {
|
||||
return m, func() tea.Msg {
|
||||
return SwitchToRowDetailMsg{RowIndex: m.selectedRow}
|
||||
}
|
||||
}
|
||||
|
||||
case "/":
|
||||
m.searching = true
|
||||
m.searchInput = ""
|
||||
return m, nil
|
||||
|
||||
case "s":
|
||||
return m, func() tea.Msg { return SwitchToQueryMsg{} }
|
||||
|
||||
case "r":
|
||||
if err := m.Shared.LoadTableData(); err == nil {
|
||||
m.filterData()
|
||||
}
|
||||
|
||||
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.Shared.LoadTableData()
|
||||
m.selectedRow = 0
|
||||
}
|
||||
|
||||
case "right", "l":
|
||||
maxPage := (m.Shared.TotalRows - 1) / PageSize
|
||||
if m.Shared.CurrentPage < maxPage {
|
||||
m.Shared.CurrentPage++
|
||||
m.Shared.LoadTableData()
|
||||
m.selectedRow = 0
|
||||
}
|
||||
}
|
||||
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 {
|
||||
for _, cell := range row {
|
||||
if strings.Contains(strings.ToLower(cell), searchLower) {
|
||||
m.Shared.FilteredData = append(m.Shared.FilteredData, row)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if m.selectedRow >= len(m.Shared.FilteredData) {
|
||||
m.selectedRow = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (m *TableDataModel) View() string {
|
||||
var content strings.Builder
|
||||
|
||||
tableName := ""
|
||||
if m.Shared.SelectedTable < len(m.Shared.FilteredTables) {
|
||||
tableName = m.Shared.FilteredTables[m.Shared.SelectedTable]
|
||||
}
|
||||
|
||||
content.WriteString(TitleStyle.Render(fmt.Sprintf("Table: %s", tableName)))
|
||||
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 rows)",
|
||||
m.searchInput, len(m.Shared.FilteredData), len(m.Shared.TableData)))
|
||||
content.WriteString("\n")
|
||||
}
|
||||
|
||||
// Show pagination info
|
||||
totalPages := (m.Shared.TotalRows-1)/PageSize + 1
|
||||
content.WriteString(fmt.Sprintf("Page %d/%d (%d total rows)\n\n",
|
||||
m.Shared.CurrentPage+1, totalPages, m.Shared.TotalRows))
|
||||
|
||||
if len(m.Shared.FilteredData) == 0 {
|
||||
content.WriteString("No data found")
|
||||
} else {
|
||||
// Show column headers
|
||||
headerRow := ""
|
||||
for i, col := range m.Shared.Columns {
|
||||
if i > 0 {
|
||||
headerRow += " | "
|
||||
}
|
||||
headerRow += TruncateString(col, 15)
|
||||
}
|
||||
content.WriteString(TitleStyle.Render(headerRow))
|
||||
content.WriteString("\n")
|
||||
|
||||
// Show data rows
|
||||
visibleCount := Max(1, m.Shared.Height-10)
|
||||
startIdx := 0
|
||||
endIdx := Min(len(m.Shared.FilteredData), visibleCount)
|
||||
|
||||
for i := startIdx; i < endIdx; i++ {
|
||||
row := m.Shared.FilteredData[i]
|
||||
rowStr := ""
|
||||
for j, cell := range row {
|
||||
if j > 0 {
|
||||
rowStr += " | "
|
||||
}
|
||||
rowStr += TruncateString(cell, 15)
|
||||
}
|
||||
|
||||
if i == m.selectedRow {
|
||||
content.WriteString(SelectedStyle.Render("> " + rowStr))
|
||||
} else {
|
||||
content.WriteString(NormalStyle.Render(" " + rowStr))
|
||||
}
|
||||
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("↑/↓: navigate • ←/→: page • /: search • enter: details • s: SQL • r: refresh • q: back"))
|
||||
}
|
||||
|
||||
return content.String()
|
||||
}
|
||||
198
internal/app/table_list.go
Normal file
198
internal/app/table_list.go
Normal file
@@ -0,0 +1,198 @@
|
||||
package app
|
||||
|
||||
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) (tea.Model, 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) (tea.Model, 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) (tea.Model, 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 • ctrl+c: quit"))
|
||||
}
|
||||
|
||||
return content.String()
|
||||
}
|
||||
Reference in New Issue
Block a user