all repos — todo.txt-go @ 601f4a83a9da7611413da0b9ac67f720ee77b709

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() 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}