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