1package main
2
3import (
4 "database/sql"
5 "encoding/base64"
6 "fmt"
7 "log"
8 "os"
9
10 mast "mast/db"
11 parser "mast/parser"
12
13 "github.com/charmbracelet/bubbles/textinput"
14 tea "github.com/charmbracelet/bubbletea"
15 uuid "github.com/gofrs/uuid"
16 "github.com/gorilla/websocket"
17 sqlite3 "github.com/mattn/go-sqlite3"
18)
19
20type model struct {
21 db *sql.DB
22 conn *websocket.Conn
23 todos []string
24 cursor int
25 textInput textinput.Model
26 inputting bool
27}
28
29func initialModel() model {
30 sql.Register("sqlite3_with_extensions",
31 &sqlite3.SQLiteDriver{
32 Extensions: []string{
33 "./db/crsqlite.so",
34 },
35 })
36 db, err := sql.Open("sqlite3_with_extensions", "db.sqlite")
37 if err != nil {
38 panic(err)
39 }
40
41 err = mast.RunMigrations(db)
42 if err != nil {
43 panic(err)
44 }
45
46 ti := textinput.New()
47 ti.Placeholder = "Add a new todo"
48 ti.Focus()
49
50 conn, _, err := websocket.DefaultDialer.Dial("ws://localhost:8080/sync", nil)
51 if err != nil {
52 log.Println("Error connecting to WebSocket:", err)
53 panic(err)
54 }
55
56 m := model{
57 db: db,
58 conn: conn,
59 todos: []string{},
60 cursor: 0,
61 textInput: ti,
62 inputting: false,
63 }
64 m.loadTodos()
65 return m
66}
67
68type CRSQLChange struct {
69 TableName string
70 PK string
71 ColumnName string
72 Value string
73 ColVersion int64
74 DBVersion int64
75 SiteID string
76 CL int64
77 Seq int64
78}
79
80func (m *model) addTodo(todo string) {
81 id, err := uuid.NewV7()
82 if err != nil {
83 panic("Error generating new UUID")
84 }
85 _, err = m.db.Exec(`
86 INSERT INTO todos (id, content)
87 VALUES (?, ?)
88 `, id.String(), todo)
89 if err != nil {
90 panic(err)
91 }
92
93 // TODO:
94 // func sync_changes
95 rows, err := m.db.Query("SELECT * FROM crsql_changes")
96 if err != nil {
97 log.Println("Error querying crsql_changes:", err)
98 return
99 }
100 defer rows.Close()
101
102 var changes []CRSQLChange
103 for rows.Next() {
104 var change CRSQLChange
105 var pkRaw []byte
106 var valueRaw []byte
107 var siteIDRaw []byte
108 err := rows.Scan(&change.TableName, &pkRaw, &change.ColumnName, &valueRaw,
109 &change.ColVersion, &change.DBVersion, &siteIDRaw, &change.CL, &change.Seq)
110
111 if err != nil {
112 log.Println("Error scanning change:", err)
113 return
114 }
115 change.PK = base64.StdEncoding.EncodeToString(pkRaw)
116 change.Value = base64.StdEncoding.EncodeToString(valueRaw)
117 change.SiteID = base64.StdEncoding.EncodeToString(siteIDRaw)
118
119 changes = append(changes, change)
120 fmt.Println(change)
121 }
122
123 if len(changes) > 0 {
124 m.sendToWebSocket(changes)
125 }
126
127 m.loadTodos()
128}
129
130func (m *model) sendToWebSocket(changes []CRSQLChange) {
131 message := struct {
132 Type string `json:"type"`
133 Data []CRSQLChange `json:"data"`
134 }{
135 Type: "changes",
136 Data: changes,
137 }
138
139 err := m.conn.WriteJSON(message)
140 if err != nil {
141 log.Println("Error sending changes to WebSocket:", err)
142 panic(err)
143 }
144}
145
146func (m *model) loadTodos() {
147 rows, err := m.db.Query("SELECT content FROM todos")
148 if err != nil {
149 panic(err)
150 }
151 defer rows.Close()
152
153 m.todos = []string{}
154 for rows.Next() {
155 var content string
156 if err := rows.Scan(&content); err != nil {
157 panic(err)
158 }
159 m.todos = append(m.todos, content)
160 }
161}
162
163func (m model) Init() tea.Cmd {
164 return nil
165}
166
167func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
168 var cmd tea.Cmd
169
170 switch msg := msg.(type) {
171 case tea.KeyMsg:
172 switch msg.String() {
173 case "ctrl+c", "q":
174 return m, m.quit()
175 case "j", "down":
176 m.cursor = (m.cursor + 1) % len(m.todos)
177 case "k", "up":
178 m.cursor = (m.cursor - 1 + len(m.todos)) % len(m.todos)
179 case "a":
180 if !m.inputting {
181 m.inputting = true
182 return m, textinput.Blink
183 }
184 case "enter":
185 if m.inputting {
186 m.addTodo(m.textInput.Value())
187 m.textInput.Reset()
188 m.inputting = false
189 }
190 }
191 }
192
193 if m.inputting {
194 m.textInput, cmd = m.textInput.Update(msg)
195 return m, cmd
196 }
197
198 return m, nil
199}
200
201func (m model) quit() tea.Cmd {
202 m.db.Close()
203 m.conn.Close()
204 return tea.Quit
205}
206
207func (m model) View() string {
208 s := "Todo List:\n\n"
209 for i, todo := range m.todos {
210 cursor := " "
211 if m.cursor == i {
212 cursor = ">"
213 }
214 s += fmt.Sprintf("%s %s\n", cursor, todo)
215 }
216 s += "\nPress 'a' to add a new todo, 'q' to quit.\n"
217
218 if m.inputting {
219 s += "\n" + m.textInput.View()
220 }
221
222 return s
223}
224
225// TODO:
226// Remove the parser test and call it when adding a todo
227func main() {
228 if len(os.Args) < 2 {
229 fmt.Println("Please provide a Taskwarrior add command to parse")
230 return
231 }
232
233 input := os.Args[1]
234 result, err := parser.Parse("", []byte(input))
235 if err != nil {
236 fmt.Printf("Error parsing input: %v\n", err)
237 return
238 }
239
240 fmt.Printf("Parsed result: %+v\n", result)
241 // p := tea.NewProgram(initialModel())
242 // if _, err := p.Run(); err != nil {
243 // fmt.Printf("Alas, there's been an error: %v", err)
244 // os.Exit(1)
245 // }
246}