package main import ( "database/sql" "encoding/base64" "fmt" "log" "os" "mast/db" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" uuid "github.com/gofrs/uuid" "github.com/gorilla/websocket" sqlite3 "github.com/mattn/go-sqlite3" ) type model struct { db *sql.DB conn *websocket.Conn todos []string cursor int textInput textinput.Model inputting bool } func initialModel() model { sql.Register("sqlite3_with_extensions", &sqlite3.SQLiteDriver{ Extensions: []string{ "./db/crsqlite.so", }, }) db, err := sql.Open("sqlite3_with_extensions", "db.sqlite") if err != nil { panic(err) } err = mast.RunMigrations(db) if err != nil { panic(err) } ti := textinput.New() ti.Placeholder = "Add a new todo" ti.Focus() conn, _, err := websocket.DefaultDialer.Dial("ws://localhost:8080/sync", nil) if err != nil { log.Println("Error connecting to WebSocket:", err) panic(err) } m := model{ db: db, conn: conn, todos: []string{}, cursor: 0, textInput: ti, inputting: false, } m.loadTodos() return m } type CRSQLChange struct { TableName string PK string ColumnName string Value string ColVersion int64 DBVersion int64 SiteID string CL int64 Seq int64 } func (m *model) addTodo(todo string) { id, err := uuid.NewV7() if err != nil { panic("Error generating new UUID") } _, err = m.db.Exec(` INSERT INTO todos (id, content) VALUES (?, ?) `, id.String(), todo) if err != nil { panic(err) } rows, err := m.db.Query("SELECT * FROM crsql_changes") if err != nil { log.Println("Error querying crsql_changes:", err) return } defer rows.Close() var changes []CRSQLChange for rows.Next() { var change CRSQLChange var pkRaw []byte var valueRaw []byte var siteIDRaw []byte err := rows.Scan(&change.TableName, &pkRaw, &change.ColumnName, &valueRaw, &change.ColVersion, &change.DBVersion, &siteIDRaw, &change.CL, &change.Seq) if err != nil { log.Println("Error scanning change:", err) return } change.PK = base64.StdEncoding.EncodeToString(pkRaw) change.Value = base64.StdEncoding.EncodeToString(valueRaw) change.SiteID = base64.StdEncoding.EncodeToString(siteIDRaw) changes = append(changes, change) fmt.Println(change) } if len(changes) > 0 { m.sendToWebSocket(changes) } m.loadTodos() } func (m *model) sendToWebSocket(changes []CRSQLChange) { message := struct { Type string `json:"type"` Data []CRSQLChange `json:"data"` }{ Type: "changes", Data: changes, } err := m.conn.WriteJSON(message) if err != nil { log.Println("Error sending changes to WebSocket:", err) panic(err) } } func (m *model) loadTodos() { rows, err := m.db.Query("SELECT content FROM todos") if err != nil { panic(err) } defer rows.Close() m.todos = []string{} for rows.Next() { var content string if err := rows.Scan(&content); err != nil { panic(err) } m.todos = append(m.todos, content) } } func (m model) Init() tea.Cmd { return nil } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "ctrl+c", "q": return m, m.quit() case "j", "down": m.cursor = (m.cursor + 1) % len(m.todos) case "k", "up": m.cursor = (m.cursor - 1 + len(m.todos)) % len(m.todos) case "a": if !m.inputting { m.inputting = true return m, textinput.Blink } case "enter": if m.inputting { m.addTodo(m.textInput.Value()) m.textInput.Reset() m.inputting = false } } } if m.inputting { m.textInput, cmd = m.textInput.Update(msg) return m, cmd } return m, nil } func (m model) quit() tea.Cmd { m.db.Close() m.conn.Close() return tea.Quit } func (m model) View() string { s := "Todo List:\n\n" for i, todo := range m.todos { cursor := " " if m.cursor == i { cursor = ">" } s += fmt.Sprintf("%s %s\n", cursor, todo) } s += "\nPress 'a' to add a new todo, 'q' to quit.\n" if m.inputting { s += "\n" + m.textInput.View() } return s } func main() { p := tea.NewProgram(initialModel()) if _, err := p.Run(); err != nil { fmt.Printf("Alas, there's been an error: %v", err) os.Exit(1) } }