all repos — todo.txt-go @ b96964548e9f9e5011e04321998d0f30e527f0ce

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.Filter(todo.FilterNotCompleted)
 53		tasks := tasklist
 54		_ = tasks.Sort(todo.SortPriorityAsc, todo.SortProjectAsc)
 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) {
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.Clean):
123				m.tasks = m.tasks.Filter(todo.FilterNotCompleted)
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			// Interface
132			case key.Matches(msg, m.keys.Add):
133				m.interfaceState = Add
134
135			case key.Matches(msg, m.keys.Quit):
136				m.tasks.WriteToPath("todo.txt")
137				return m, tea.Quit
138
139			case key.Matches(msg, m.keys.Help):
140				m.help.ShowAll = !m.help.ShowAll
141
142			}
143
144			return m, nil
145
146		case Add:
147			switch msg.Type {
148			case tea.KeyCtrlC, tea.KeyEsc:
149				return m, tea.Quit
150			case tea.KeyEnter:
151				inputValue := m.textInput.Value()
152				if inputValue != "" {
153					newTask, _ := todo.ParseTask(inputValue)
154					m.tasks.AddTask(newTask)
155				}
156				m.textInput.Reset()
157				m.interfaceState = List
158			}
159
160			m.textInput, cmd = m.textInput.Update(msg)
161
162			return m, cmd
163
164		}
165	}
166
167	// Return the updated model to the Bubble Tea runtime for processing.
168	// Note that we're not returning a command.
169	return m, nil
170}
171
172func (m model) View() string {
173	output := "\n"
174
175	switch m.interfaceState {
176
177	case List:
178
179		// Iterate over our tasks
180		for i, task := range m.tasks {
181
182			cursor := " "
183			if m.cursor == i {
184				cursor = ">"
185			}
186
187			// Is this choice selected?
188			checked := " " // not selected
189			if _, ok := m.selected[i]; ok {
190				checked = "x" // selected!
191			}
192
193			// Render the row
194			styles := NewTextStyle()
195			priorityStyle := getPriorityStyle(styles, task.Priority)
196			output += fmt.Sprintf("%s [%s] %s %s\n", cursor, checked, priorityStyle.Render(task.Priority), task.Todo)
197		}
198
199		output += "\n"
200		output += m.help.View(m.keys)
201	case Add:
202		output += fmt.Sprintf("Nouvelle tâche:\n\n%s", m.textInput.View())
203		output += "\n\n Press ESC to quit.\n"
204	}
205
206	return output
207
208}
209
210func main() {
211	p := tea.NewProgram(initialModel())
212	if _, err := p.Run(); err != nil {
213		fmt.Printf("Alas, there's been an error: %v", err)
214		os.Exit(1)
215	}
216}