main.go (view raw)
1package main
2
3import (
4 "fmt"
5 "log"
6 "os"
7 "strings"
8
9 todo "github.com/1set/todotxt"
10 "github.com/charmbracelet/bubbles/help"
11 "github.com/charmbracelet/bubbles/key"
12 "github.com/charmbracelet/bubbles/textinput"
13 tea "github.com/charmbracelet/bubbletea"
14)
15
16type InterfaceState int
17
18const (
19 List InterfaceState = iota
20 Add
21 Edit
22)
23
24type model struct {
25 interfaceState InterfaceState
26 config config
27 keys keyMap
28 help help.Model
29
30 tasks todo.TaskList
31 cursor int // which to-do list item our cursor is pointing at
32 selected map[int]struct{} // which to-do items are selected
33
34 textInput textinput.Model
35}
36
37func initialModel() model {
38
39 config := NewConfig()
40
41 // New task input
42 ti := textinput.New()
43 ti.Focus()
44 ti.CharLimit = 200
45 ti.Width = 200
46
47 if tasklist, err := todo.LoadFromPath(config.taskFilePath); err != nil {
48 log.Fatal(err)
49 return model{}
50 } else {
51 tasks := tasklist
52 tasks.Sort(todo.SortCompletedDateAsc, todo.SortDueDateAsc)
53 selected := make(map[int]struct{})
54
55 interfaceState := List
56 if len(tasks) == 0 {
57 interfaceState = Add
58 }
59
60 for i, t := range tasks {
61 if t.Completed {
62 selected[i] = struct{}{}
63 }
64 }
65
66 return model{
67 interfaceState: interfaceState,
68 config: config,
69 tasks: tasks,
70 selected: selected,
71 textInput: ti,
72 keys: keys,
73 help: help.New(),
74 }
75
76 }
77
78}
79
80func (m model) Init() tea.Cmd {
81 return textinput.Blink
82}
83
84func (m model) SaveTasks() {
85 m.tasks.WriteToPath(m.config.taskFilePath)
86}
87
88func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
89 var cmd tea.Cmd
90
91 switch msg := msg.(type) {
92
93 case tea.WindowSizeMsg:
94 // If we set a width on the help menu it can gracefully truncate
95 // its view as needed.
96 m.help.Width = msg.Width
97
98 // Is it a key press?
99 case tea.KeyMsg:
100
101 switch m.interfaceState {
102
103 case List:
104
105 switch {
106 // Navigation
107 case key.Matches(msg, m.keys.Up):
108 if m.cursor > 0 {
109 m.cursor--
110 }
111
112 case key.Matches(msg, m.keys.Down):
113 if m.cursor < len(m.tasks)-1 {
114 m.cursor++
115 }
116
117 // Tasks management
118 case key.Matches(msg, m.keys.Priority):
119 m.tasks[m.cursor].Priority = strings.ToUpper(msg.String())
120 m.SaveTasks()
121
122 case key.Matches(msg, m.keys.ClearPriority):
123 m.tasks[m.cursor].Priority = ""
124 m.SaveTasks()
125
126 case key.Matches(msg, m.keys.Check):
127 _, ok := m.selected[m.cursor]
128
129 if ok {
130 delete(m.selected, m.cursor)
131 m.tasks[m.cursor].Reopen()
132 } else {
133 m.selected[m.cursor] = struct{}{}
134 m.tasks[m.cursor].Complete()
135 }
136
137 m.SaveTasks()
138
139 case key.Matches(msg, m.keys.SortByPriority):
140 m.tasks.Sort(todo.SortCompletedDateAsc, todo.SortPriorityAsc)
141 m.selected = make(map[int]struct{})
142 for i, t := range m.tasks {
143 if t.Completed {
144 m.selected[i] = struct{}{}
145 }
146 }
147
148 case key.Matches(msg, m.keys.SortByDate):
149 m.tasks.Sort(todo.SortCompletedDateAsc, todo.SortDueDateAsc)
150 m.selected = make(map[int]struct{})
151 for i, t := range m.tasks {
152 if t.Completed {
153 m.selected[i] = struct{}{}
154 }
155 }
156
157 case key.Matches(msg, m.keys.Clean):
158 m.selected = make(map[int]struct{})
159 doneTasks := m.tasks.Filter(todo.FilterCompleted)
160 doneFile, err := os.OpenFile(m.config.doneFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
161 if err != nil {
162 log.Fatal("Can't write done tasks to " + m.config.doneFilePath)
163 }
164 defer doneFile.Close()
165 if writeErr := doneTasks.WriteToFile(doneFile); writeErr != nil {
166 log.Fatal(writeErr)
167 }
168 m.SaveTasks()
169 m.tasks = m.tasks.Filter(todo.FilterNotCompleted)
170 m.selected = make(map[int]struct{})
171 for i, t := range m.tasks {
172 if t.Completed {
173 m.selected[i] = struct{}{}
174 }
175 }
176
177 // Interface
178 case key.Matches(msg, m.keys.Add):
179 m.interfaceState = Add
180
181 case key.Matches(msg, m.keys.Edit):
182 m.textInput.SetValue(m.tasks[m.cursor].Original)
183 m.interfaceState = Edit
184
185 case key.Matches(msg, m.keys.Quit):
186 m.SaveTasks()
187 return m, tea.Quit
188
189 case key.Matches(msg, m.keys.Help):
190 m.help.ShowAll = !m.help.ShowAll
191
192 }
193
194 return m, nil
195
196 case Add:
197 switch msg.Type {
198 case tea.KeyCtrlC, tea.KeyEsc:
199 m.interfaceState = List
200 case tea.KeyEnter:
201 inputValue := m.textInput.Value()
202 if inputValue != "" {
203 newTask, _ := todo.ParseTask(inputValue)
204 m.tasks.AddTask(newTask)
205 m.SaveTasks()
206 }
207 m.textInput.Reset()
208 m.interfaceState = List
209 }
210
211 m.textInput, cmd = m.textInput.Update(msg)
212
213 return m, cmd
214
215 case Edit:
216 switch msg.Type {
217 case tea.KeyCtrlC, tea.KeyEsc:
218 m.interfaceState = List
219
220 case tea.KeyEnter:
221 inputValue := m.textInput.Value()
222 if inputValue != "" {
223 editedTask, _ := todo.ParseTask(inputValue)
224 m.tasks[m.cursor] = *editedTask
225 }
226 m.textInput.Reset()
227 m.interfaceState = List
228 }
229
230 m.textInput, cmd = m.textInput.Update(msg)
231
232 return m, cmd
233 }
234
235 }
236
237 return m, nil
238}
239
240func (m model) View() string {
241 output := "\n"
242
243 switch m.interfaceState {
244
245 case List:
246 if len(m.tasks) > 0 {
247
248 for i, task := range m.tasks {
249
250 cursor := " "
251 if m.cursor == i {
252 cursor = "→"
253 }
254
255 // Render the row
256 _, checked := m.selected[i]
257 styles := NewTextStyle()
258 output += fmt.Sprintf("%s %s\n", cursor, styles.getTaskStyle(task, checked))
259 }
260
261 } else {
262 output += "No tasks in file"
263 }
264
265 output += "\n"
266 output += m.help.View(m.keys)
267 case Add:
268 output += fmt.Sprintf("New task:\n\n%s", m.textInput.View())
269 output += "\n\n Press ESC to go back.\n"
270
271 case Edit:
272 output += fmt.Sprintf("Edit task:\n\n%s", m.textInput.View())
273 output += "\n\n Press ESC to go back.\n"
274 }
275
276 return output
277
278}
279
280func main() {
281 p := tea.NewProgram(initialModel(), tea.WithAltScreen())
282 if _, err := p.Run(); err != nil {
283 fmt.Printf("Alas, there's been an error: %v", err)
284 os.Exit(1)
285 }
286}