package main import ( "fmt" "log" "os" "strings" todo "github.com/1set/todotxt" "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" ) type InterfaceState int const ( List InterfaceState = iota Add ) type model struct { interfaceState InterfaceState config config keys keyMap help help.Model tasks todo.TaskList cursor int // which to-do list item our cursor is pointing at selected map[int]struct{} // which to-do items are selected textInput textinput.Model } func initialModel() model { config := NewConfig() // New task input ti := textinput.New() ti.Focus() ti.CharLimit = 200 ti.Width = 200 if tasklist, err := todo.LoadFromPath(config.taskFilePath); err != nil { log.Fatal(err) return model{} } else { tasks := tasklist tasks.Sort(todo.SortCompletedDateAsc, todo.SortDueDateAsc) selected := make(map[int]struct{}) interfaceState := List if len(tasks) == 0 { interfaceState = Add } for i, t := range tasks { if t.Completed { selected[i] = struct{}{} } } return model{ interfaceState: interfaceState, config: config, tasks: tasks, selected: selected, textInput: ti, keys: keys, help: help.New(), } } } func (m model) Init() tea.Cmd { return textinput.Blink } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { case tea.WindowSizeMsg: // If we set a width on the help menu it can gracefully truncate // its view as needed. m.help.Width = msg.Width // Is it a key press? case tea.KeyMsg: switch m.interfaceState { case List: switch { // Navigation case key.Matches(msg, m.keys.Up): if m.cursor > 0 { m.cursor-- } case key.Matches(msg, m.keys.Down): if m.cursor < len(m.tasks)-1 { m.cursor++ } // Tasks management case key.Matches(msg, m.keys.Priority): m.tasks[m.cursor].Priority = strings.ToUpper(msg.String()) case key.Matches(msg, m.keys.ClearPriority): m.tasks[m.cursor].Priority = "" case key.Matches(msg, m.keys.Check): _, ok := m.selected[m.cursor] if ok { delete(m.selected, m.cursor) m.tasks[m.cursor].Reopen() } else { m.selected[m.cursor] = struct{}{} m.tasks[m.cursor].Complete() } case key.Matches(msg, m.keys.SortByPriority): m.tasks.Sort(todo.SortCompletedDateAsc, todo.SortPriorityAsc) m.selected = make(map[int]struct{}) for i, t := range m.tasks { if t.Completed { m.selected[i] = struct{}{} } } case key.Matches(msg, m.keys.SortByDate): m.tasks.Sort(todo.SortCompletedDateAsc, todo.SortDueDateAsc) m.selected = make(map[int]struct{}) for i, t := range m.tasks { if t.Completed { m.selected[i] = struct{}{} } } case key.Matches(msg, m.keys.Clean): m.selected = make(map[int]struct{}) doneTasks := m.tasks.Filter(todo.FilterCompleted) doneFile, err := os.OpenFile(m.config.doneFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { log.Fatal("Can't write done tasks to " + m.config.doneFilePath) } defer doneFile.Close() if writeErr := doneTasks.WriteToFile(doneFile); writeErr != nil { log.Fatal(writeErr) } m.tasks = m.tasks.Filter(todo.FilterNotCompleted) m.selected = make(map[int]struct{}) for i, t := range m.tasks { if t.Completed { m.selected[i] = struct{}{} } } // Interface case key.Matches(msg, m.keys.Add): m.interfaceState = Add case key.Matches(msg, m.keys.SaveQuit): m.tasks.WriteToPath(m.config.taskFilePath) return m, tea.Quit case key.Matches(msg, m.keys.Quit): return m, tea.Quit case key.Matches(msg, m.keys.Help): m.help.ShowAll = !m.help.ShowAll } return m, nil case Add: switch msg.Type { case tea.KeyCtrlC, tea.KeyEsc: return m, tea.Quit case tea.KeyEnter: inputValue := m.textInput.Value() if inputValue != "" { newTask, _ := todo.ParseTask(inputValue) m.tasks.AddTask(newTask) } m.textInput.Reset() m.interfaceState = List } m.textInput, cmd = m.textInput.Update(msg) return m, cmd } } return m, nil } func (m model) View() string { output := "\n" switch m.interfaceState { case List: if len(m.tasks) > 0 { for i, task := range m.tasks { cursor := " " if m.cursor == i { cursor = "→" } // Render the row _, checked := m.selected[i] styles := NewTextStyle() output += fmt.Sprintf("%s %s\n", cursor, styles.getTaskStyle(task, checked)) } } else { output += "No tasks in file" } output += "\n" output += m.help.View(m.keys) case Add: output += fmt.Sprintf("Nouvelle tâche:\n\n%s", m.textInput.View()) output += "\n\n Press ESC to quit.\n" } return output } func main() { p := tea.NewProgram(initialModel(), tea.WithAltScreen()) if _, err := p.Run(); err != nil { fmt.Printf("Alas, there's been an error: %v", err) os.Exit(1) } }