all repos — todo.txt-go @ ecc9536e6199c2e91e6f6f1e6316afcc915f7bbf

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