all repos — todo.txt-go @ 9a7f2eff7040a7900e3775f9e03bcb88c4c4e2e1

CLI tool for todo.txt files written in Go

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