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