package main import ( "fmt" "math" "os" engine "git.pablu.de/pablu/sqv-engine" "github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/textarea" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss/table" "github.com/charmbracelet/log" ) type Focused int const ( EDITOR Focused = iota TABLE PICKER ) type mainModel struct { width, height int manager *engine.Manager lastFocused, focused Focused table *table.Table editor textarea.Model picker list.Model debugMsgs []string } var ( defaultStyle = lipgloss.NewStyle(). Align(lipgloss.Center). BorderStyle(lipgloss.NormalBorder()) focusedStyle = defaultStyle.BorderForeground(lipgloss.Color("202")) ) type item struct { title string } func (i item) Title() string { return i.title } func (i item) Description() string { return i.title } func (i item) FilterValue() string { return i.title } func newMainModerl(manager *engine.Manager) mainModel { ed := textarea.New() ed.Placeholder = "Try \"SELECT * FROM ?;\"" ed.ShowLineNumbers = false ta := table.New().Border(lipgloss.NormalBorder()) li := list.New(nil, list.NewDefaultDelegate(), 0, 0) li.Title = "Table Picker" return mainModel{ editor: ed, table: ta, picker: li, manager: manager, } } func (m mainModel) Init() tea.Cmd { return m.GetTableDefinitions } func (m mainModel) GetTableDefinitions() tea.Msg { tables := m.manager.GetTables() tableNames := make([]list.Item, len(tables)) for i, table := range tables { tableNames[i] = item{table.Name} } return tableDefinitionsMsg{ tableNames, } } type tableDefinitionsMsg struct { Tables []list.Item } func (m mainModel) GetFirstTable() tea.Msg { t := m.manager.GetTables()[0] cmd := m.GetTable(t.Name) return cmd() } func (m mainModel) GetTable(name string) tea.Cmd { return func() tea.Msg { t, ok := m.manager.GetTable(name) if !ok { return nil } m.manager.LoadTable(&t) msg := tableMsg{ Columns: make([]string, len(t.Columns)), Rows: make([][]string, len(t.Rows)), } for i, r := range t.Rows { msg.Rows[i] = r.Values } for i, c := range t.Columns { msg.Columns[i] = c.Name } return msg } } type tableMsg struct { Columns []string Rows [][]string } func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var ( edCmd tea.Cmd pickerCmd tea.Cmd cmds tea.Cmd ) switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "ctrl+c": return m, tea.Quit case "tab": if m.focused == EDITOR { m.focused = TABLE m.editor.Blur() } else { m.focused = EDITOR } case "j": m.table.Offset(50) case "ctrl+e": if m.focused == PICKER { m.focused = m.lastFocused } else { // Maybe blur is not even needed m.editor.Blur() m.lastFocused = m.focused m.focused = PICKER } case "enter": log.Debug("Enter was pressed") if m.focused == PICKER { i, ok := m.picker.SelectedItem().(item) if ok { log.Debug("Selected item okay and batched") cmds = tea.Batch(cmds, m.GetTable(i.title)) } m.focused = m.lastFocused if m.focused == EDITOR { m.editor.Focus() } } } case tea.WindowSizeMsg: m = m.updateStyles(msg.Width, msg.Height) cmds = tea.Batch(cmds, m.GetFirstTable) case tableMsg: m.table.Offset(0) m.table.ClearRows() m.table.Headers(msg.Columns...) m.table.Rows(msg.Rows...) case tableDefinitionsMsg: pickerCmd = m.picker.SetItems(msg.Tables) } switch m.focused { case EDITOR: m.editor.Focus() m.editor, edCmd = m.editor.Update(msg) case PICKER: m.picker, pickerCmd = m.picker.Update(msg) } return m, tea.Batch(edCmd, cmds, pickerCmd) } func (m mainModel) updateStyles(width, height int) mainModel { m.width = width m.height = height h, v := defaultStyle.GetFrameSize() topHeight := math.Ceil(float64(m.height)*3/4 - float64(h)) editorHeight := math.Ceil(float64(m.height)*1/4 - float64(h)) m.editor.SetWidth(m.width - v) m.editor.SetHeight(int(editorHeight)) m.table.Width(m.width - v) m.table.Height(int(topHeight) - h*2) m.picker.SetSize(m.width*7/10, m.height*7/10) return m } func (m mainModel) View() string { var ( view, editor string ) h, _ := defaultStyle.GetFrameSize() topHeight := (m.height * 3 / 4) - h editorHeight := (m.height * 1 / 4) - h switch m.focused { case EDITOR: view = defaultStyle. Height(topHeight). Render(m.table.Render()) editor = focusedStyle. Height(editorHeight). Render(m.editor.View()) case TABLE: view = focusedStyle. Height(topHeight). Render(m.table.Render()) editor = defaultStyle. Height(editorHeight). Render(m.editor.View()) case PICKER: view = defaultStyle. Height(topHeight). Render(m.table.Render()) editor = defaultStyle. Height(editorHeight). Render(m.editor.View()) } main := lipgloss.JoinVertical(lipgloss.Top, view, editor) if m.focused == PICKER { x := (m.width / 2) - m.picker.Width()/2 y := (m.height / 2) - m.picker.Height()/2 return PlaceOverlay(x, y, focusedStyle.Render(m.picker.View()), main, false) } return main } func main() { f, err := tea.LogToFile("debug.log", "debug") if err != nil { fmt.Println("fatal:", err) os.Exit(1) } defer f.Close() log.SetLevel(log.DebugLevel) log.SetOutput(f) m, err := engine.NewManager("../vdcmp/db.sqlite") if err != nil { fmt.Println("fatal:", err) os.Exit(1) } err = m.Start() if err != nil { fmt.Println("fatal:", err) os.Exit(1) } table.New() p := tea.NewProgram(newMainModerl(m), tea.WithAltScreen()) if _, err := p.Run(); err != nil { fmt.Println("fatal:", err) os.Exit(1) } }