all repos — todo.txt-go @ a3a0af12af18ee20ca24340a6684f489dfcf206a

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