├── README.md ├── demo.gif ├── demo.tape ├── go.mod ├── go.sum ├── internal ├── database │ └── database.go ├── help │ └── help.go ├── helpers │ └── helpers.go └── styles │ └── styles.go └── main.go /README.md: -------------------------------------------------------------------------------- 1 | # SQLite Shell 2 | 3 | ![SQLite Shell Demo](./demo.gif) 4 | 5 | **SQLite Shell** is a Terminal User Interface (TUI) application written in Go that provides an interactive shell for executing SQLite commands. It leverages libraries like [Bubble Tea](https://github.com/charmbracelet/bubbletea) to create a rich and user-friendly TUI experience. 6 | 7 | ## Features 8 | 9 | - Execute SQL queries on a specified SQLite database. 10 | - View query results in a tabular format within the terminal. 11 | - Intuitive navigation and input handling. 12 | 13 | ## Installation 14 | 15 | 1. Clone the repository: 16 | 17 | ```sh 18 | git clone https://github.com/iamhectorsosa/sqlite-shell.git 19 | cd sqlite-shell 20 | ``` 21 | 22 | 2. Build the project: 23 | 24 | ```sh 25 | go build -o sqlite-shell 26 | ``` 27 | 28 | ## Usage 29 | 30 | Run the program with the path to an SQLite database file: 31 | 32 | ```sh 33 | ./sqlite-shell 34 | ``` 35 | 36 | ## Work in Progress 37 | 38 | This project is still under development. Expect frequent updates and new features. Contributions and feedback are welcome! 39 | 40 | --- 41 | 42 | Made with Go and ❤️. 43 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamhectorsosa/sqlite-shell/8a7ce1f28fd7213259410282f82bde64b86c8957/demo.gif -------------------------------------------------------------------------------- /demo.tape: -------------------------------------------------------------------------------- 1 | Output demo.gif 2 | 3 | Set Shell "bash" 4 | Set FontSize 20 5 | Set Width 1200 6 | Set Height 600 7 | 8 | Type "cd ~/documents/go-projects/sqlite-shell" Sleep 500ms 9 | Enter Sleep 500ms 10 | 11 | Type "go run . ~/chinook.db" Sleep 500ms 12 | Enter Sleep 4s 13 | 14 | Type 'SELECT BillingCountry, SUM(Total) FROM invoice' Sleep 500ms 15 | Enter Sleep 2s 16 | 17 | Type 's GROUP BY BillingCountry' Sleep 500ms 18 | Enter Sleep 2s 19 | 20 | Down 12 Sleep 2s 21 | 22 | 23 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/iamhectorsosa/sqlite-shell 2 | 3 | go 1.23.2 4 | 5 | require ( 6 | github.com/charmbracelet/bubbles v0.20.0 7 | github.com/charmbracelet/bubbletea v1.2.4 8 | github.com/charmbracelet/lipgloss v1.0.0 9 | ) 10 | 11 | require ( 12 | github.com/atotto/clipboard v0.1.4 // indirect 13 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 14 | github.com/charmbracelet/x/ansi v0.4.5 // indirect 15 | github.com/charmbracelet/x/term v0.2.1 // indirect 16 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect 17 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 18 | github.com/mattn/go-isatty v0.0.20 // indirect 19 | github.com/mattn/go-localereader v0.0.1 // indirect 20 | github.com/mattn/go-runewidth v0.0.16 // indirect 21 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect 22 | github.com/muesli/cancelreader v0.2.2 // indirect 23 | github.com/muesli/termenv v0.15.2 // indirect 24 | github.com/rivo/uniseg v0.4.7 // indirect 25 | golang.org/x/sync v0.9.0 // indirect 26 | golang.org/x/sys v0.27.0 // indirect 27 | golang.org/x/text v0.3.8 // indirect 28 | ) 29 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= 2 | github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= 3 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 4 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 5 | github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= 6 | github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= 7 | github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= 8 | github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= 9 | github.com/charmbracelet/bubbletea v1.2.4 h1:KN8aCViA0eps9SCOThb2/XPIlea3ANJLUkv3KnQRNCE= 10 | github.com/charmbracelet/bubbletea v1.2.4/go.mod h1:Qr6fVQw+wX7JkWWkVyXYk/ZUQ92a6XNekLXa3rR18MM= 11 | github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= 12 | github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= 13 | github.com/charmbracelet/x/ansi v0.4.5 h1:LqK4vwBNaXw2AyGIICa5/29Sbdq58GbGdFngSexTdRM= 14 | github.com/charmbracelet/x/ansi v0.4.5/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= 15 | github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q= 16 | github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= 17 | github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= 18 | github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= 19 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= 20 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= 21 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 22 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 23 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 24 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 25 | github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= 26 | github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= 27 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 28 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 29 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= 30 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= 31 | github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= 32 | github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= 33 | github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= 34 | github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= 35 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 36 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 37 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 38 | golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= 39 | golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 40 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 41 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 42 | golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= 43 | golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 44 | golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= 45 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 46 | -------------------------------------------------------------------------------- /internal/database/database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "bytes" 5 | "encoding/csv" 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "strings" 11 | ) 12 | 13 | func ExecCmd(path, query string) (headers []string, rows [][]string, err error) { 14 | resolvedPath, err := resolvePath(path) 15 | if err != nil { 16 | return nil, nil, formatErrors("resolving path", err) 17 | } 18 | 19 | cmd := exec.Command("sqlite3", "-csv", "-header", resolvedPath, query) 20 | var out, stderr bytes.Buffer 21 | cmd.Stdout, cmd.Stderr = &out, &stderr 22 | if err := cmd.Run(); err != nil { 23 | return nil, nil, formatErrors( 24 | "executing command", 25 | fmt.Errorf("exec: %v", err), 26 | fmt.Errorf("sqlite3: %s", stderr.String()), 27 | ) 28 | } 29 | 30 | headers, rows, err = parseCSV(out.String()) 31 | if err != nil { 32 | return nil, nil, formatErrors("parsing data", err) 33 | } 34 | 35 | return headers, rows, nil 36 | } 37 | 38 | func resolvePath(path string) (string, error) { 39 | path = os.ExpandEnv(path) 40 | 41 | if len(path) > 2 && path[:2] == "~/" { 42 | home, err := os.UserHomeDir() 43 | if err != nil { 44 | return "", fmt.Errorf("reading home dir: %v", err) 45 | } 46 | return filepath.Join(home, path[2:]), nil 47 | } 48 | 49 | if filepath.IsAbs(path) { 50 | return path, nil 51 | } 52 | 53 | return path, nil 54 | } 55 | 56 | func parseCSV(input string) ([]string, [][]string, error) { 57 | reader := csv.NewReader(strings.NewReader(input)) 58 | records, err := reader.ReadAll() 59 | if err != nil { 60 | return nil, nil, fmt.Errorf("reading CSV: %v", err) 61 | } 62 | if len(records) == 0 { 63 | return nil, nil, nil 64 | } 65 | 66 | headers := records[0] 67 | rows := records[1:] 68 | return headers, rows, nil 69 | } 70 | 71 | func formatErrors(context string, errs ...error) error { 72 | errMsgs := make([]string, 0, len(errs)) 73 | for _, err := range errs { 74 | if err != nil { 75 | errMsgs = append(errMsgs, err.Error()) 76 | } 77 | } 78 | if len(errMsgs) > 0 { 79 | return fmt.Errorf("%s: %s", context, strings.Join(errMsgs, "; ")) 80 | } 81 | return nil 82 | } 83 | -------------------------------------------------------------------------------- /internal/help/help.go: -------------------------------------------------------------------------------- 1 | package help 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/help" 5 | "github.com/charmbracelet/bubbles/key" 6 | ) 7 | 8 | type keyMap struct { 9 | Tab key.Binding 10 | Quit key.Binding 11 | } 12 | 13 | var keys = keyMap{ 14 | Tab: key.NewBinding( 15 | key.WithKeys("tab"), 16 | key.WithHelp("tab", "toggle input"), 17 | ), 18 | Quit: key.NewBinding( 19 | key.WithKeys("esc", "ctrl+c"), 20 | key.WithHelp("esc", "quit"), 21 | ), 22 | } 23 | 24 | func New() string { 25 | return help.New().ShortHelpView([]key.Binding{keys.Tab, keys.Quit}) 26 | } 27 | -------------------------------------------------------------------------------- /internal/helpers/helpers.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/charmbracelet/bubbles/table" 7 | ) 8 | 9 | func CreateColumns(columns []string, rows [][]string, viewportWidth int) []table.Column { 10 | maxWidths := make([]int, len(columns)) 11 | for i, col := range columns { 12 | maxWidths[i] = len(col) 13 | } 14 | 15 | for _, row := range rows { 16 | for i, cell := range row { 17 | cellLength := len(cell) 18 | if cellLength > maxWidths[i] { 19 | maxWidths[i] = cellLength 20 | } 21 | } 22 | } 23 | 24 | totalWidth := 0 25 | for _, width := range maxWidths { 26 | totalWidth += width 27 | } 28 | scaleFactor := float64(viewportWidth) / float64(totalWidth) 29 | 30 | cols := make([]table.Column, 0, len(columns)) 31 | for i, title := range columns { 32 | cols = append(cols, table.Column{ 33 | Title: strings.ToUpper(title), 34 | Width: int(float64(maxWidths[i]) * scaleFactor), 35 | }) 36 | } 37 | 38 | return cols 39 | } 40 | 41 | func CreateRows(inputRows [][]string) []table.Row { 42 | rows := make([]table.Row, 0, len(inputRows)) 43 | for _, row := range inputRows { 44 | rows = append(rows, row) 45 | } 46 | return rows 47 | } 48 | -------------------------------------------------------------------------------- /internal/styles/styles.go: -------------------------------------------------------------------------------- 1 | package styles 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/table" 5 | "github.com/charmbracelet/lipgloss" 6 | ) 7 | 8 | var ( 9 | aquamarine = lipgloss.Color("122") 10 | outer_space = lipgloss.Color("238") 11 | dark_charcoal = lipgloss.Color("236") 12 | strong_red = lipgloss.Color("161") 13 | white = lipgloss.Color("231") 14 | ) 15 | 16 | type Styles struct { 17 | Base lipgloss.Style 18 | BoundaryText lipgloss.Style 19 | ErrorText lipgloss.Style 20 | Highlight lipgloss.Style 21 | Background lipgloss.Style 22 | TableHeader lipgloss.Style 23 | TableRow lipgloss.Style 24 | TextBorder lipgloss.Style 25 | WhitespaceStyle lipgloss.WhitespaceOption 26 | WhitespaceBackgroundStyle lipgloss.WhitespaceOption 27 | accent lipgloss.Color 28 | foreground lipgloss.Color 29 | } 30 | 31 | func setStyles(s *Styles) { 32 | s.Base = lipgloss.NewStyle(). 33 | Padding(0, 1) 34 | s.BoundaryText = lipgloss.NewStyle(). 35 | Foreground(s.accent). 36 | Bold(true) 37 | s.ErrorText = lipgloss.NewStyle(). 38 | Border(lipgloss.RoundedBorder()). 39 | BorderForeground(s.accent). 40 | Padding(1, 0). 41 | BorderTop(true). 42 | BorderLeft(true). 43 | BorderRight(true). 44 | BorderBottom(true) 45 | s.Highlight = lipgloss.NewStyle(). 46 | Foreground(s.accent) 47 | s.Background = lipgloss.NewStyle().Foreground(outer_space) 48 | s.TableHeader = table.DefaultStyles().Header. 49 | BorderStyle(lipgloss.NormalBorder()). 50 | BorderForeground(outer_space). 51 | BorderBottom(true). 52 | Bold(false) 53 | s.TableRow = table.DefaultStyles().Selected. 54 | Foreground(s.foreground). 55 | Background(s.accent). 56 | Bold(false) 57 | s.TextBorder = lipgloss.NewStyle(). 58 | PaddingLeft(1). 59 | BorderStyle(lipgloss.ThickBorder()). 60 | BorderLeft(true). 61 | BorderForeground(outer_space) 62 | s.WhitespaceStyle = lipgloss.WithWhitespaceForeground(s.accent) 63 | s.WhitespaceBackgroundStyle = lipgloss.WithWhitespaceForeground(dark_charcoal) 64 | } 65 | 66 | func New() *Styles { 67 | s := Styles{} 68 | 69 | s.accent = aquamarine 70 | s.foreground = outer_space 71 | setStyles(&s) 72 | 73 | return &s 74 | } 75 | 76 | func (s *Styles) Error() { 77 | s.accent = strong_red 78 | s.foreground = white 79 | 80 | setStyles(s) 81 | } 82 | 83 | func (s *Styles) Reset() { 84 | s.accent = aquamarine 85 | s.foreground = outer_space 86 | 87 | setStyles(s) 88 | } 89 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "strings" 8 | 9 | "github.com/charmbracelet/bubbles/key" 10 | "github.com/charmbracelet/bubbles/table" 11 | "github.com/charmbracelet/bubbles/textinput" 12 | tea "github.com/charmbracelet/bubbletea" 13 | "github.com/charmbracelet/lipgloss" 14 | "github.com/iamhectorsosa/sqlite-shell/internal/database" 15 | "github.com/iamhectorsosa/sqlite-shell/internal/help" 16 | "github.com/iamhectorsosa/sqlite-shell/internal/helpers" 17 | "github.com/iamhectorsosa/sqlite-shell/internal/styles" 18 | ) 19 | 20 | func main() { 21 | if len(os.Args) < 2 { 22 | log.Fatal("Required: ") 23 | } 24 | 25 | databasePath := os.Args[1] 26 | 27 | p := tea.NewProgram(initialModel(databasePath), tea.WithAltScreen()) 28 | if _, err := p.Run(); err != nil { 29 | log.Fatal(err) 30 | } 31 | } 32 | 33 | type errMsg error 34 | 35 | type keyMap struct { 36 | Tab key.Binding 37 | Quit key.Binding 38 | } 39 | 40 | var keys = keyMap{ 41 | Tab: key.NewBinding( 42 | key.WithKeys("tab"), 43 | key.WithHelp("tab", "toggle input"), 44 | ), 45 | Quit: key.NewBinding( 46 | key.WithKeys("esc", "ctrl+c"), 47 | key.WithHelp("esc", "quit"), 48 | ), 49 | } 50 | 51 | type model struct { 52 | databasePath string 53 | styles *styles.Styles 54 | help string 55 | err error 56 | textInput textinput.Model 57 | table table.Model 58 | viewportWidth int 59 | viewportHeight int 60 | ready bool 61 | } 62 | 63 | func initialModel(databasePath string) model { 64 | styles := styles.New() 65 | textInput := textinput.New() 66 | textInput.Placeholder = "Write SQL..." 67 | textInput.PromptStyle = styles.Highlight 68 | textInput.Cursor.Style = styles.Highlight 69 | textInput.Focus() 70 | 71 | return model{ 72 | databasePath: databasePath, 73 | styles: styles, 74 | err: nil, 75 | ready: false, 76 | textInput: textInput, 77 | help: help.New(), 78 | } 79 | } 80 | 81 | func (m model) appBoundaryText(text string) string { 82 | return lipgloss.PlaceHorizontal( 83 | m.viewportWidth, 84 | lipgloss.Left, 85 | m.styles.BoundaryText.Render(text+" "), 86 | lipgloss.WithWhitespaceChars("•"), 87 | m.styles.WhitespaceStyle, 88 | ) 89 | } 90 | 91 | func (m model) appErrorText(text string) string { 92 | return lipgloss.Place(m.viewportWidth, m.viewportHeight, 93 | lipgloss.Center, lipgloss.Center, 94 | m.styles.ErrorText.Render(lipgloss.NewStyle(). 95 | Padding(0, 2). 96 | Width(50). 97 | Align(lipgloss.Center). 98 | Render(text), 99 | ), 100 | lipgloss.WithWhitespaceChars("猫咪"), 101 | m.styles.WhitespaceBackgroundStyle, 102 | ) 103 | } 104 | 105 | func (m model) Init() tea.Cmd { 106 | return textinput.Blink 107 | } 108 | 109 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 110 | var cmds []tea.Cmd 111 | 112 | switch msg := msg.(type) { 113 | case tea.WindowSizeMsg: 114 | if !m.ready { 115 | m.ready = true 116 | } 117 | 118 | m.viewportWidth = msg.Width - 2 119 | m.viewportHeight = msg.Height - 8 120 | 121 | case tea.KeyMsg: 122 | switch msg.Type { 123 | case tea.KeyTab: 124 | if m.table.Focused() { 125 | m.table.Blur() 126 | cmd := m.textInput.Focus() 127 | m.textInput.PromptStyle = m.styles.Highlight 128 | cmds = append(cmds, cmd) 129 | } else { 130 | m.table.Focus() 131 | m.textInput.Blur() 132 | m.textInput.PromptStyle = m.styles.Background 133 | } 134 | case tea.KeyEnter: 135 | query := strings.TrimSpace(m.textInput.Value()) 136 | if query == "" { 137 | return m, nil 138 | } 139 | 140 | headers, rows, err := database.ExecCmd(m.databasePath, query) 141 | if err != nil { 142 | m.err = err 143 | m.styles.Error() 144 | m.textInput.PromptStyle = m.styles.Highlight 145 | m.textInput.Cursor.Style = m.styles.Highlight 146 | if len(m.table.Rows()) > 0 { 147 | s := table.DefaultStyles() 148 | s.Header = m.styles.TableHeader 149 | s.Selected = m.styles.TableRow 150 | m.table.SetStyles(s) 151 | } 152 | return m, nil 153 | } else { 154 | m.err = nil 155 | m.styles.Reset() 156 | m.textInput.PromptStyle = m.styles.Highlight 157 | m.textInput.Cursor.Style = m.styles.Highlight 158 | 159 | } 160 | 161 | m.textInput.Blur() 162 | m.textInput.PromptStyle = m.styles.Background 163 | 164 | height := len(rows) + 1 165 | if height > m.viewportHeight { 166 | height = m.viewportHeight 167 | } 168 | 169 | t := table.New( 170 | table.WithColumns(helpers.CreateColumns(headers, rows, m.viewportWidth)), 171 | table.WithRows(helpers.CreateRows(rows)), 172 | table.WithFocused(true), 173 | table.WithHeight(height), 174 | ) 175 | 176 | s := table.DefaultStyles() 177 | s.Header = m.styles.TableHeader 178 | s.Selected = m.styles.TableRow 179 | t.SetStyles(s) 180 | 181 | m.table = t 182 | m.table.Focus() 183 | 184 | case tea.KeyCtrlC, tea.KeyEsc: 185 | return m, tea.Quit 186 | } 187 | 188 | case errMsg: 189 | m.err = msg 190 | return m, nil 191 | } 192 | 193 | var cmdTextInput tea.Cmd 194 | var cmdTable tea.Cmd 195 | m.textInput, cmdTextInput = m.textInput.Update(msg) 196 | m.table, cmdTable = m.table.Update(msg) 197 | cmds = append(cmds, cmdTextInput, cmdTable) 198 | return m, tea.Batch(cmds...) 199 | } 200 | 201 | func (m model) View() string { 202 | if !m.ready { 203 | return "Initializing..." 204 | } 205 | 206 | header := m.appBoundaryText("SQLite Shell") 207 | if m.err != nil { 208 | header = m.appBoundaryText("An error has occured") 209 | } 210 | 211 | content := m.styles.TextBorder.Render(m.textInput.View()) 212 | if len(m.table.Columns()) > 0 && m.err == nil { 213 | content = fmt.Sprintf("%s\n\n%s", content, m.table.View()) 214 | } 215 | 216 | if m.err != nil { 217 | content = fmt.Sprintf("%s\n\n%s", content, m.appErrorText(m.err.Error())) 218 | } 219 | 220 | return m.styles.Base.Render(fmt.Sprintf( 221 | "%s\n\n%s\n\n%s", 222 | header, 223 | content, 224 | m.appBoundaryText(m.help), 225 | )) 226 | } 227 | --------------------------------------------------------------------------------