package main import ( "fmt" "log" "os" todo "github.com/1set/todotxt" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" ) type InterfaceState int const ( List InterfaceState = iota Add ) type model struct { interfaceState InterfaceState 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 { ti := textinput.New() ti.Focus() ti.CharLimit = 200 ti.Width = 200 if tasklist, err := todo.LoadFromPath("todo.txt"); err != nil { log.Fatal(err) // TODO - Handle error, create file if not exists return model{ textInput: ti, } } else { // tasks := tasklist.Filter(todo.FilterNotCompleted) tasks := tasklist _ = tasks.Sort(todo.SortPriorityAsc, todo.SortProjectAsc) selected := make(map[int]struct{}) for i, t := range tasks { if t.Completed { selected[i] = struct{}{} } } return model{ tasks: tasks, selected: selected, textInput: ti, } } } 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) { // Is it a key press? case tea.KeyMsg: switch m.interfaceState { case List: switch msg.String() { // These keys should exit the program. case "ctrl+c", "q": m.tasks.WriteToPath("todo.txt") return m, tea.Quit // Set focused task priority case "A": m.tasks[m.cursor].Priority = "A" case "B": m.tasks[m.cursor].Priority = "B" case "C": m.tasks[m.cursor].Priority = "C" // Remove completed tasks case "u": 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{}{} } } // The "up" and "k" keys move the cursor up case "up", "k": if m.cursor > 0 { m.cursor-- } // The "down" and "j" keys move the cursor down case "down", "j": if m.cursor < len(m.tasks) { m.cursor++ } // The "enter" key and the spacebar (a literal space) toggle // the selected state for the item that the cursor is pointing at. case "enter", " ": _, 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() } // Switch interface to add task interface case "a": m.interfaceState = Add } return m, nil case Add: switch msg.Type { case tea.KeyCtrlC, tea.KeyEsc: return m, tea.Quit case tea.KeyEnter: newTask, _ := todo.ParseTask(m.textInput.Value()) m.tasks.AddTask(newTask) m.textInput.Reset() m.interfaceState = List } m.textInput, cmd = m.textInput.Update(msg) return m, cmd } } // Return the updated model to the Bubble Tea runtime for processing. // Note that we're not returning a command. return m, nil } func (m model) View() string { output := "My tasks list\n\n" switch m.interfaceState { case List: // Iterate over our tasks for i, task := range m.tasks { cursor := " " if m.cursor == i { cursor = ">" } // Is this choice selected? checked := " " // not selected if _, ok := m.selected[i]; ok { checked = "x" // selected! } // Render the row styles := NewTextStyle() priorityStyle := getPriorityStyle(styles, task.Priority) output += fmt.Sprintf("%s [%s] %s %s\n", cursor, checked, priorityStyle.Render(task.Priority), task.Todo) } output += "\nPress q to quit.\n" 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()) if _, err := p.Run(); err != nil { fmt.Printf("Alas, there's been an error: %v", err) os.Exit(1) } }