all repos — todo.txt-go @ main

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