package main import ( "fmt" "os" engine "git.pablu.de/pablu/sqv-engine" "github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/table" "github.com/charmbracelet/bubbles/textarea" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/log" ) type mainModel struct { width, height int pickerFocused bool manager *engine.Manager lastFocused int viewStyle lipgloss.Style editorStyle lipgloss.Style pickerStyle lipgloss.Style table table.Model editor textarea.Model picker list.Model debugMsgs []string } var ( defaultStyle = lipgloss.NewStyle(). Align(lipgloss.Center). BorderStyle(lipgloss.NormalBorder()) focusedStyle = defaultStyle.BorderForeground(lipgloss.Color("202")) baseTableStyle = lipgloss.NewStyle(). BorderStyle(lipgloss.NormalBorder()) ) 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 ed.Focus() columns := []table.Column{ {Title: "id", Width: 4}, {Title: "name", Width: 4}, {Title: "family_name", Width: 4}, } rows := []table.Row{ {"0", "Conrad", "Adenauer"}, {"1", "Anna", "Aachen"}, {"2", "Karli", "Columbus"}, {"3", "Max", "Mustermann"}, {"4", "Bernd", "Brot"}, } ta := table.New( table.WithColumns(columns), table.WithRows(rows), ) items := []list.Item{ item{ title: "user", // desc: "users table", }, item{ title: "job", // desc: "jobs table", }, item{ title: "user_has_job", // desc: "user has a job", }, } li := list.New(items, list.NewDefaultDelegate(), 0, 0) li.Title = "Table Picker" return mainModel{ pickerFocused: false, viewStyle: baseTableStyle, editorStyle: defaultStyle, pickerStyle: defaultStyle.BorderStyle(lipgloss.RoundedBorder()).BorderForeground(lipgloss.Color("67")), 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([]table.Column, len(t.Columns)), Rows: make([]table.Row, len(t.Rows)), } lRow := make([]int, len(t.Columns)) lRowC := 0 for i, r := range t.Rows { msg.Rows[i] = r.Values // Pure stupid estemation of how long each row should be if i == 0 { for j, v := range r.Values { vLen := len(v) vLen = max(vLen, len(t.Columns[j].Name)) lRow[j] = vLen lRowC += vLen } } } h, _ := m.viewStyle.GetFrameSize() width := m.viewStyle.GetWidth() - h width -= (len(t.Columns) + 1) * 2 for i, c := range t.Columns { columnWidth := float64(width) * (float64(lRow[i]) / float64(lRowC)) msg.Columns[i] = table.Column{ Title: c.Name, Width: int(columnWidth), } } return msg } } type tableMsg struct { Columns []table.Column Rows []table.Row } func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var ( edCmd tea.Cmd taCmd tea.Cmd liCmd 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.table.Focused() { m.table.Blur() m.editor.Focus() m.lastFocused = 0 } else { m.editor.Blur() m.table.Focus() m.lastFocused = 1 } case "ctrl+e": m.pickerFocused = !m.pickerFocused // Picker is now focused if m.pickerFocused { m.table.Blur() m.editor.Blur() } else { if m.lastFocused == 0 { m.editor.Focus() } else { m.table.Focus() } } case "enter": log.Debug("Enter was pressed") if m.pickerFocused { i, ok := m.picker.SelectedItem().(item) if ok { log.Debug("Selected item okay and batched") cmds = tea.Batch(cmds, m.GetTable(i.title)) } m.pickerFocused = !m.pickerFocused if m.lastFocused == 0 { m.editor.Focus() } else { m.table.Focus() } } } case tea.WindowSizeMsg: m = m.updateStyles(msg.Width, msg.Height) cmds = tea.Batch(cmds, m.GetFirstTable) case tableMsg: // log.Debug("Setting new table view", "columns", msg.Columns, "rows", msg.Rows) m.table = table.New( table.WithColumns(msg.Columns), table.WithRows(msg.Rows), ) m.table.SetWidth(m.viewStyle.GetWidth() - 2) m.table.SetHeight(m.viewStyle.GetHeight() - 2) // m.table.SetColumns(msg.Columns) // m.table.SetRows(msg.Rows) case tableDefinitionsMsg: pickerCmd = m.picker.SetItems(msg.Tables) } m.editor, edCmd = m.editor.Update(msg) m.table, taCmd = m.table.Update(msg) if m.pickerFocused { m.picker, liCmd = m.picker.Update(msg) } return m, tea.Batch(edCmd, taCmd, liCmd, cmds, pickerCmd) } func (m mainModel) updateStyles(width, height int) mainModel { h, v := defaultStyle.GetFrameSize() bh, bv := baseTableStyle.GetFrameSize() log.Debug("Update styles", "bh", bh, "bv", bv, "h", h, "v", v) topHeight := (height * 3 / 4) - bh editorHeight := (height * 1 / 4) - h m.editorStyle = defaultStyle. Width(width - v). Height(editorHeight) m.viewStyle = baseTableStyle. Width(width - bv). Height(topHeight) m.editor.SetWidth(m.editorStyle.GetWidth()) m.editor.SetHeight(m.editorStyle.GetHeight()) m.table.SetWidth(m.viewStyle.GetWidth()) m.table.SetHeight(m.viewStyle.GetHeight()) columns := m.table.Columns() colLen := len(columns) colWidth := m.table.Width() / colLen for i := range columns { columns[i].Width = colWidth } m.table.SetColumns(columns) m.pickerStyle = m.pickerStyle. Width((width * 7 / 10) - v). Height((height * 7 / 10) - h) m.picker.SetSize(m.pickerStyle.GetWidth(), m.pickerStyle.GetHeight()) m.width = width m.height = height return m } func (m mainModel) View() string { view := m.viewStyle. Render(m.table.View()) editor := m.editorStyle. Render(m.editor.View()) main := lipgloss.JoinVertical(lipgloss.Top, view, editor) if m.pickerFocused { x := (m.width / 2) - m.picker.Width()/2 y := (m.height / 2) - m.picker.Height()/2 return PlaceOverlay(x, y, m.pickerStyle.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) } p := tea.NewProgram(newMainModerl(m), tea.WithAltScreen()) if _, err := p.Run(); err != nil { fmt.Println("fatal:", err) os.Exit(1) } }