all repos — todo.txt-go @ 935dbed84d9005c5192cd30f7eafcc1bd9e32cd2

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			case key.Matches(msg, m.keys.Delete):
193				focusedTask := m.tasks[m.cursor]
194				m.tasks.RemoveTaskByID(focusedTask.ID)
195
196			// Interface
197			case key.Matches(msg, m.keys.Add):
198				m.interfaceState = Add
199
200			case key.Matches(msg, m.keys.Edit):
201				m.textInput.SetValue(m.tasks[m.cursor].Original)
202				m.interfaceState = Edit
203
204			case key.Matches(msg, m.keys.Quit):
205				m.SaveTasks()
206				return m, tea.Quit
207
208			case key.Matches(msg, m.keys.Help):
209				m.help.ShowAll = !m.help.ShowAll
210
211			}
212
213			return m, nil
214
215		case Add:
216			switch msg.Type {
217			case tea.KeyCtrlC, tea.KeyEsc:
218				m.interfaceState = List
219			case tea.KeyEnter:
220				inputValue := m.textInput.Value()
221				if inputValue != "" {
222					newTask, _ := todo.ParseTask(inputValue)
223					m.tasks.AddTask(newTask)
224					m.SaveTasks()
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		case Edit:
235			switch msg.Type {
236			case tea.KeyCtrlC, tea.KeyEsc:
237				m.interfaceState = List
238
239			case tea.KeyEnter:
240				inputValue := m.textInput.Value()
241				if inputValue != "" {
242					editedTask, _ := todo.ParseTask(inputValue)
243					m.tasks[m.cursor] = *editedTask
244				}
245				m.textInput.Reset()
246				m.interfaceState = List
247			}
248
249			m.textInput, cmd = m.textInput.Update(msg)
250
251			return m, cmd
252		}
253
254	}
255
256	return m, nil
257}
258
259func (m model) View() string {
260	output := "\n"
261
262	switch m.interfaceState {
263
264	case List:
265		if len(m.tasks) > 0 {
266			for i, task := range m.tasks {
267
268				cursor := " "
269				if m.cursor == i {
270					cursor = "→"
271				}
272
273				// Render the row
274				_, checked := m.selected[i]
275				styles := NewTextStyle()
276				output += fmt.Sprintf("%s %s\n", cursor, styles.getTaskStyle(task, checked))
277			}
278		} else {
279			output += "No tasks in file"
280		}
281
282		output += "\n"
283		output += m.help.View(m.keys)
284	case Add:
285		output += fmt.Sprintf("New task:\n\n%s", m.textInput.View())
286		output += "\n\n Press ESC to go back.\n"
287
288	case Edit:
289		output += fmt.Sprintf("Edit task:\n\n%s", m.textInput.View())
290		output += "\n\n Press ESC to go back.\n"
291	}
292
293	return output
294}
295
296func main() {
297	config := NewConfig()
298	p := tea.NewProgram(initialModel(config), tea.WithAltScreen())
299	if _, err := p.Run(); err != nil {
300		fmt.Printf("Alas, there's been an error: %v", err)
301		os.Exit(1)
302	}
303}