mirror of
https://github.com/taigrr/teaqlite.git
synced 2026-04-02 04:59:03 -07:00
initial project
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
go-sqlite-tui
|
||||||
|
.crush/
|
||||||
|
*.db
|
||||||
66
README.md
Normal file
66
README.md
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# SQLite TUI
|
||||||
|
|
||||||
|
A fully-featured terminal user interface for browsing SQLite databases built with Bubble Tea v2.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Table Browser**: Browse all tables in your SQLite database
|
||||||
|
- **Data Viewer**: View table data with pagination (20 rows per page)
|
||||||
|
- **SQL Query Interface**: Execute custom SQL queries with parameter support
|
||||||
|
- **Navigation**: Intuitive keyboard navigation
|
||||||
|
- **Responsive Design**: Adapts to terminal size
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run main.go <database.db>
|
||||||
|
```
|
||||||
|
|
||||||
|
Example with the included sample database:
|
||||||
|
```bash
|
||||||
|
go run main.go sample.db
|
||||||
|
```
|
||||||
|
|
||||||
|
## Keyboard Controls
|
||||||
|
|
||||||
|
### Table List Mode
|
||||||
|
- `↑/↓` or `k/j`: Navigate between tables
|
||||||
|
- `Enter`: View selected table data
|
||||||
|
- `s`: Switch to SQL query mode
|
||||||
|
- `r`: Refresh table list
|
||||||
|
- `q` or `Ctrl+C`: Quit
|
||||||
|
|
||||||
|
### Table Data Mode
|
||||||
|
- `←/→` or `h/l`: Navigate between pages
|
||||||
|
- `Esc`: Return to table list
|
||||||
|
- `r`: Refresh current table data
|
||||||
|
- `q` or `Ctrl+C`: Quit
|
||||||
|
|
||||||
|
### SQL Query Mode
|
||||||
|
- Type your SQL query
|
||||||
|
- `Enter`: Execute query
|
||||||
|
- `Backspace`: Delete characters
|
||||||
|
- `Esc`: Return to table list
|
||||||
|
- `q` or `Ctrl+C`: Quit
|
||||||
|
|
||||||
|
## Features Implemented
|
||||||
|
|
||||||
|
1. **Table Browsing**: Lists all tables in the database with navigation
|
||||||
|
2. **Paginated Data View**: Shows table data with pagination (20 rows per page)
|
||||||
|
3. **SQL Query Execution**: Execute custom SQL queries and view results
|
||||||
|
4. **Error Handling**: Displays database errors gracefully
|
||||||
|
5. **Responsive UI**: Clean, styled interface that adapts to terminal size
|
||||||
|
6. **Column Information**: Shows column names and handles NULL values
|
||||||
|
7. **Navigation**: Intuitive keyboard shortcuts for all operations
|
||||||
|
|
||||||
|
## Sample Database
|
||||||
|
|
||||||
|
The included `sample.db` contains:
|
||||||
|
- `users` table with id, name, email, age columns
|
||||||
|
- `products` table with id, name, price, category columns
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- [Bubble Tea](https://github.com/charmbracelet/bubbletea) - TUI framework
|
||||||
|
- [Lip Gloss](https://github.com/charmbracelet/lipgloss) - Styling
|
||||||
|
- [go-sqlite3](https://github.com/mattn/go-sqlite3) - SQLite driver
|
||||||
27
go.mod
Normal file
27
go.mod
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
module github.com/taigrr/go-sqlite-tui
|
||||||
|
|
||||||
|
go 1.24.5
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/charmbracelet/bubbletea v1.2.4
|
||||||
|
github.com/charmbracelet/lipgloss v1.0.0
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.24
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
|
github.com/charmbracelet/x/ansi v0.4.5 // indirect
|
||||||
|
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
|
github.com/muesli/termenv v0.15.2 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
golang.org/x/sync v0.9.0 // indirect
|
||||||
|
golang.org/x/sys v0.27.0 // indirect
|
||||||
|
golang.org/x/text v0.3.8 // indirect
|
||||||
|
)
|
||||||
39
go.sum
Normal file
39
go.sum
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
|
github.com/charmbracelet/bubbletea v1.2.4 h1:KN8aCViA0eps9SCOThb2/XPIlea3ANJLUkv3KnQRNCE=
|
||||||
|
github.com/charmbracelet/bubbletea v1.2.4/go.mod h1:Qr6fVQw+wX7JkWWkVyXYk/ZUQ92a6XNekLXa3rR18MM=
|
||||||
|
github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=
|
||||||
|
github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=
|
||||||
|
github.com/charmbracelet/x/ansi v0.4.5 h1:LqK4vwBNaXw2AyGIICa5/29Sbdq58GbGdFngSexTdRM=
|
||||||
|
github.com/charmbracelet/x/ansi v0.4.5/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
||||||
|
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||||
|
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
|
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||||
|
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||||
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
|
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
|
||||||
|
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
||||||
|
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||||
|
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
||||||
|
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
486
main.go
Normal file
486
main.go
Normal file
@@ -0,0 +1,486 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
pageSize = 20
|
||||||
|
)
|
||||||
|
|
||||||
|
type viewMode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
modeTableList viewMode = iota
|
||||||
|
modeTableData
|
||||||
|
modeQuery
|
||||||
|
)
|
||||||
|
|
||||||
|
type model struct {
|
||||||
|
db *sql.DB
|
||||||
|
mode viewMode
|
||||||
|
tables []string
|
||||||
|
selectedTable int
|
||||||
|
tableData [][]string
|
||||||
|
columns []string
|
||||||
|
currentPage int
|
||||||
|
totalRows int
|
||||||
|
query string
|
||||||
|
queryInput string
|
||||||
|
cursor int
|
||||||
|
err error
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
}
|
||||||
|
|
||||||
|
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"))
|
||||||
|
|
||||||
|
tableStyle = lipgloss.NewStyle().
|
||||||
|
Border(lipgloss.RoundedBorder()).
|
||||||
|
BorderForeground(lipgloss.Color("#874BFD")).
|
||||||
|
Padding(1, 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
func initialModel(dbPath string) model {
|
||||||
|
db, err := sql.Open("sqlite3", dbPath)
|
||||||
|
if err != nil {
|
||||||
|
return model{err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
m := model{
|
||||||
|
db: db,
|
||||||
|
mode: modeTableList,
|
||||||
|
currentPage: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
m.loadTables()
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) loadTables() {
|
||||||
|
rows, err := m.db.Query("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
|
||||||
|
if err != nil {
|
||||||
|
m.err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
m.tables = []string{}
|
||||||
|
for rows.Next() {
|
||||||
|
var name string
|
||||||
|
if err := rows.Scan(&name); err != nil {
|
||||||
|
m.err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.tables = append(m.tables, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) loadTableData() {
|
||||||
|
if m.selectedTable >= len(m.tables) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tableName := m.tables[m.selectedTable]
|
||||||
|
|
||||||
|
// Get column info
|
||||||
|
rows, err := m.db.Query(fmt.Sprintf("PRAGMA table_info(%s)", tableName))
|
||||||
|
if err != nil {
|
||||||
|
m.err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
m.columns = []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 {
|
||||||
|
m.err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.columns = append(m.columns, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get total row count
|
||||||
|
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM %s", tableName)
|
||||||
|
err = m.db.QueryRow(countQuery).Scan(&m.totalRows)
|
||||||
|
if err != nil {
|
||||||
|
m.err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get paginated data
|
||||||
|
offset := m.currentPage * pageSize
|
||||||
|
dataQuery := fmt.Sprintf("SELECT * FROM %s LIMIT %d OFFSET %d", tableName, pageSize, offset)
|
||||||
|
|
||||||
|
rows, err = m.db.Query(dataQuery)
|
||||||
|
if err != nil {
|
||||||
|
m.err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
m.tableData = [][]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 {
|
||||||
|
m.err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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.tableData = append(m.tableData, row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) executeQuery() {
|
||||||
|
if strings.TrimSpace(m.query) == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := m.db.Query(m.query)
|
||||||
|
if err != nil {
|
||||||
|
m.err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
// Get column names
|
||||||
|
columns, err := rows.Columns()
|
||||||
|
if err != nil {
|
||||||
|
m.err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.columns = columns
|
||||||
|
|
||||||
|
// Get data
|
||||||
|
m.tableData = [][]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 {
|
||||||
|
m.err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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.tableData = append(m.tableData, row)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.totalRows = len(m.tableData)
|
||||||
|
m.currentPage = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) Init() tea.Cmd {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch msg.String() {
|
||||||
|
case "ctrl+c", "q":
|
||||||
|
return m, tea.Quit
|
||||||
|
|
||||||
|
case "esc":
|
||||||
|
switch m.mode {
|
||||||
|
case modeTableData, modeQuery:
|
||||||
|
m.mode = modeTableList
|
||||||
|
m.err = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
case "enter":
|
||||||
|
switch m.mode {
|
||||||
|
case modeTableList:
|
||||||
|
if len(m.tables) > 0 {
|
||||||
|
m.mode = modeTableData
|
||||||
|
m.currentPage = 0
|
||||||
|
m.loadTableData()
|
||||||
|
}
|
||||||
|
case modeQuery:
|
||||||
|
m.executeQuery()
|
||||||
|
}
|
||||||
|
|
||||||
|
case "up", "k":
|
||||||
|
switch m.mode {
|
||||||
|
case modeTableList:
|
||||||
|
if m.selectedTable > 0 {
|
||||||
|
m.selectedTable--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "down", "j":
|
||||||
|
switch m.mode {
|
||||||
|
case modeTableList:
|
||||||
|
if m.selectedTable < len(m.tables)-1 {
|
||||||
|
m.selectedTable++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "left", "h":
|
||||||
|
switch m.mode {
|
||||||
|
case modeTableData:
|
||||||
|
if m.currentPage > 0 {
|
||||||
|
m.currentPage--
|
||||||
|
m.loadTableData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "right", "l":
|
||||||
|
switch m.mode {
|
||||||
|
case modeTableData:
|
||||||
|
maxPage := (m.totalRows - 1) / pageSize
|
||||||
|
if m.currentPage < maxPage {
|
||||||
|
m.currentPage++
|
||||||
|
m.loadTableData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "s":
|
||||||
|
if m.mode == modeTableList {
|
||||||
|
m.mode = modeQuery
|
||||||
|
m.queryInput = ""
|
||||||
|
m.query = ""
|
||||||
|
m.cursor = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
case "r":
|
||||||
|
if m.mode == modeTableList {
|
||||||
|
m.loadTables()
|
||||||
|
} else if m.mode == modeTableData {
|
||||||
|
m.loadTableData()
|
||||||
|
}
|
||||||
|
|
||||||
|
case "backspace":
|
||||||
|
if m.mode == modeQuery && len(m.queryInput) > 0 {
|
||||||
|
m.queryInput = m.queryInput[:len(m.queryInput)-1]
|
||||||
|
m.query = m.queryInput
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
if m.mode == modeQuery {
|
||||||
|
m.queryInput += msg.String()
|
||||||
|
m.query = m.queryInput
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) View() string {
|
||||||
|
if m.err != nil {
|
||||||
|
return errorStyle.Render(fmt.Sprintf("Error: %v\n\nPress 'esc' to continue or 'q' to quit", m.err))
|
||||||
|
}
|
||||||
|
|
||||||
|
var content strings.Builder
|
||||||
|
|
||||||
|
switch m.mode {
|
||||||
|
case modeTableList:
|
||||||
|
content.WriteString(titleStyle.Render("SQLite TUI - Tables"))
|
||||||
|
content.WriteString("\n\n")
|
||||||
|
|
||||||
|
if len(m.tables) == 0 {
|
||||||
|
content.WriteString("No tables found in database")
|
||||||
|
} else {
|
||||||
|
for i, table := range m.tables {
|
||||||
|
if i == m.selectedTable {
|
||||||
|
content.WriteString(selectedStyle.Render(fmt.Sprintf("> %s", table)))
|
||||||
|
} else {
|
||||||
|
content.WriteString(normalStyle.Render(fmt.Sprintf(" %s", table)))
|
||||||
|
}
|
||||||
|
content.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content.WriteString("\n")
|
||||||
|
content.WriteString(helpStyle.Render("↑/↓: navigate • enter: view table • s: SQL query • r: refresh • q: quit"))
|
||||||
|
|
||||||
|
case modeTableData:
|
||||||
|
tableName := m.tables[m.selectedTable]
|
||||||
|
maxPage := (m.totalRows - 1) / pageSize
|
||||||
|
|
||||||
|
content.WriteString(titleStyle.Render(fmt.Sprintf("Table: %s (Page %d/%d)", tableName, m.currentPage+1, maxPage+1)))
|
||||||
|
content.WriteString("\n\n")
|
||||||
|
|
||||||
|
if len(m.tableData) == 0 {
|
||||||
|
content.WriteString("No data in table")
|
||||||
|
} else {
|
||||||
|
// Create table header
|
||||||
|
var headerRow strings.Builder
|
||||||
|
for i, col := range m.columns {
|
||||||
|
if i > 0 {
|
||||||
|
headerRow.WriteString(" | ")
|
||||||
|
}
|
||||||
|
headerRow.WriteString(fmt.Sprintf("%-15s", truncateString(col, 15)))
|
||||||
|
}
|
||||||
|
content.WriteString(selectedStyle.Render(headerRow.String()))
|
||||||
|
content.WriteString("\n")
|
||||||
|
|
||||||
|
// Add separator
|
||||||
|
var separator strings.Builder
|
||||||
|
for i := range m.columns {
|
||||||
|
if i > 0 {
|
||||||
|
separator.WriteString("-+-")
|
||||||
|
}
|
||||||
|
separator.WriteString(strings.Repeat("-", 15))
|
||||||
|
}
|
||||||
|
content.WriteString(separator.String())
|
||||||
|
content.WriteString("\n")
|
||||||
|
|
||||||
|
// Add data rows
|
||||||
|
for _, row := range m.tableData {
|
||||||
|
var dataRow strings.Builder
|
||||||
|
for i, cell := range row {
|
||||||
|
if i > 0 {
|
||||||
|
dataRow.WriteString(" | ")
|
||||||
|
}
|
||||||
|
dataRow.WriteString(fmt.Sprintf("%-15s", truncateString(cell, 15)))
|
||||||
|
}
|
||||||
|
content.WriteString(normalStyle.Render(dataRow.String()))
|
||||||
|
content.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content.WriteString("\n")
|
||||||
|
content.WriteString(helpStyle.Render(fmt.Sprintf("←/→: prev/next page • Total rows: %d • esc: back • r: refresh • q: quit", m.totalRows)))
|
||||||
|
|
||||||
|
case modeQuery:
|
||||||
|
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.tableData) > 0 {
|
||||||
|
// Show query results
|
||||||
|
var headerRow strings.Builder
|
||||||
|
for i, col := range m.columns {
|
||||||
|
if i > 0 {
|
||||||
|
headerRow.WriteString(" | ")
|
||||||
|
}
|
||||||
|
headerRow.WriteString(fmt.Sprintf("%-15s", truncateString(col, 15)))
|
||||||
|
}
|
||||||
|
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("-", 15))
|
||||||
|
}
|
||||||
|
content.WriteString(separator.String())
|
||||||
|
content.WriteString("\n")
|
||||||
|
|
||||||
|
for _, row := range m.tableData {
|
||||||
|
var dataRow strings.Builder
|
||||||
|
for i, cell := range row {
|
||||||
|
if i > 0 {
|
||||||
|
dataRow.WriteString(" | ")
|
||||||
|
}
|
||||||
|
dataRow.WriteString(fmt.Sprintf("%-15s", truncateString(cell, 15)))
|
||||||
|
}
|
||||||
|
content.WriteString(normalStyle.Render(dataRow.String()))
|
||||||
|
content.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content.WriteString("\n")
|
||||||
|
content.WriteString(helpStyle.Render("enter: execute query • esc: back • q: quit"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return tableStyle.Render(content.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func truncateString(s string, maxLen int) string {
|
||||||
|
if len(s) <= maxLen {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return s[:maxLen-3] + "..."
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
fmt.Println("Usage: go-sqlite-tui <database.db>")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
dbPath := os.Args[1]
|
||||||
|
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
||||||
|
fmt.Printf("Database file '%s' does not exist\n", dbPath)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
m := initialModel(dbPath)
|
||||||
|
if m.err != nil {
|
||||||
|
log.Fatal(m.err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p := tea.NewProgram(m, tea.WithAltScreen())
|
||||||
|
if _, err := p.Run(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user