all repos — todo.txt-go @ a3a0af12af18ee20ca24340a6684f489dfcf206a

feat: :sparkles: Improve style
Tim Izzo tim@octree.ch
Fri, 21 Mar 2025 17:09:34 +0100
commit

a3a0af12af18ee20ca24340a6684f489dfcf206a

parent

b96964548e9f9e5011e04321998d0f30e527f0ce

6 files changed, 117 insertions(+), 30 deletions(-)

jump to
M README.mdREADME.md

@@ -1,5 +1,10 @@

# TODO.txt in Go +## TODO + +- [ ] Use *todo.txt* file targeted by todo-txt.sh config +- [ ] Implement archive mecanism + ## Ressources - <https://github.com/charmbracelet/bubbletea>
M go.modgo.mod

@@ -15,6 +15,7 @@

require ( github.com/1set/todotxt v0.0.4 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/catppuccin/go v0.3.0 github.com/charmbracelet/lipgloss v1.0.0 github.com/charmbracelet/x/ansi v0.8.0 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect
M go.sumgo.sum

@@ -6,6 +6,8 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=

github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= +github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
M help.gokeymap.go

@@ -7,16 +7,18 @@ Up key.Binding

Down key.Binding Priority key.Binding Help key.Binding + SaveQuit key.Binding Quit key.Binding Check key.Binding Clean key.Binding Add key.Binding + Sort key.Binding } // ShortHelp returns keybindings to be shown in the mini help view. It's part // of the key.Map interface. func (k keyMap) ShortHelp() []key.Binding { - return []key.Binding{k.Check, k.Add, k.Quit, k.Help} + return []key.Binding{k.Check, k.Add, k.Sort, k.SaveQuit, k.Help} } // FullHelp returns keybindings for the expanded help view. It's part of the

@@ -25,7 +27,7 @@ func (k keyMap) FullHelp() [][]key.Binding {

return [][]key.Binding{ {k.Check, k.Priority, k.Up}, {k.Add, k.Clean, k.Down}, - {k.Quit, k.Help}, + {k.Sort, k.SaveQuit, k.Quit}, } }

@@ -46,13 +48,13 @@ Add: key.NewBinding(

key.WithKeys("A", "+"), key.WithHelp("A|+", "Add new task"), ), + Sort: key.NewBinding( + key.WithKeys("s"), + key.WithHelp("s", "Sort tasks"), + ), Help: key.NewBinding( key.WithKeys("?"), - key.WithHelp("?", "Show help"), - ), - Quit: key.NewBinding( - key.WithKeys("q", "esc", "ctrl+c"), - key.WithHelp("q", "Quit"), + key.WithHelp("?", "More help"), ), Check: key.NewBinding( key.WithKeys(" ", "enter"),

@@ -61,5 +63,13 @@ ),

Clean: key.NewBinding( key.WithKeys("u"), key.WithHelp("u", "Clean completed tasks"), + ), + SaveQuit: key.NewBinding( + key.WithKeys("q", "esc"), + key.WithHelp("q", "Save & quit"), + ), + Quit: key.NewBinding( + key.WithKeys("ctrl+c"), + key.WithHelp("ctrl+c", "Cancel & quit"), ), }
M main.gomain.go

@@ -49,9 +49,9 @@ keys: keys,

help: help.New(), } } else { - // tasks := tasklist.Filter(todo.FilterNotCompleted) tasks := tasklist - _ = tasks.Sort(todo.SortPriorityAsc, todo.SortProjectAsc) + tasks.Sort(todo.SortCompletedDateAsc, todo.SortPriorityAsc) + selected := make(map[int]struct{}) for i, t := range tasks {

@@ -101,7 +101,7 @@ m.cursor--

} case key.Matches(msg, m.keys.Down): - if m.cursor < len(m.tasks) { + if m.cursor < len(m.tasks)-1 { m.cursor++ }

@@ -119,6 +119,15 @@ m.selected[m.cursor] = struct{}{}

m.tasks[m.cursor].Complete() } + case key.Matches(msg, m.keys.Sort): + 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.Clean): m.tasks = m.tasks.Filter(todo.FilterNotCompleted) m.selected = make(map[int]struct{})

@@ -132,10 +141,13 @@ // Interface

case key.Matches(msg, m.keys.Add): m.interfaceState = Add - case key.Matches(msg, m.keys.Quit): + case key.Matches(msg, m.keys.SaveQuit): m.tasks.WriteToPath("todo.txt") 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

@@ -181,19 +193,13 @@ 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! + cursor = "→" } // Render the row + _, checked := m.selected[i] styles := NewTextStyle() - priorityStyle := getPriorityStyle(styles, task.Priority) - output += fmt.Sprintf("%s [%s] %s %s\n", cursor, checked, priorityStyle.Render(task.Priority), task.Todo) + output += fmt.Sprintf("%s %s\n", cursor, styles.getTaskStyle(task, checked)) } output += "\n"

@@ -208,7 +214,7 @@

} func main() { - p := tea.NewProgram(initialModel()) + 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)
M style.gostyle.go

@@ -1,9 +1,28 @@

package main -import "github.com/charmbracelet/lipgloss" +import ( + "fmt" + "regexp" + + todo "github.com/1set/todotxt" + "github.com/charmbracelet/lipgloss" + + catppuccin "github.com/catppuccin/go" +) + +var Catppuccin catppuccin.Flavor = catppuccin.Mocha type Style struct { + Task TaskStyle Priority PriorityStyle + Check lipgloss.Style + Context lipgloss.Style + Project lipgloss.Style +} + +type TaskStyle struct { + Todo lipgloss.Style + Completed lipgloss.Style } type PriorityStyle struct {

@@ -14,27 +33,71 @@ }

func NewTextStyle() Style { + taskStyle := TaskStyle{ + Todo: lipgloss.NewStyle().Foreground(lipgloss.Color(Catppuccin.Text().Hex)), + Completed: lipgloss.NewStyle().Strikethrough(true).Foreground(lipgloss.Color(Catppuccin.Overlay2().Hex)), + } + priorityStyle := PriorityStyle{ - A: lipgloss.NewStyle().Foreground(lipgloss.Color("#f38ba8")), - B: lipgloss.NewStyle().Foreground(lipgloss.Color("#fab387")), - C: lipgloss.NewStyle().Foreground(lipgloss.Color("#94e2d5")), + A: lipgloss.NewStyle().Foreground(lipgloss.Color(Catppuccin.Red().Hex)), + B: lipgloss.NewStyle().Foreground(lipgloss.Color(Catppuccin.Peach().Hex)), + C: lipgloss.NewStyle().Foreground(lipgloss.Color(Catppuccin.Green().Hex)), } style := Style{ Priority: priorityStyle, + Task: taskStyle, + Check: lipgloss.NewStyle().Foreground(lipgloss.Color(Catppuccin.Blue().Hex)), + Context: lipgloss.NewStyle().Foreground(lipgloss.Color(Catppuccin.Blue().Hex)), + Project: lipgloss.NewStyle().Foreground(lipgloss.Color(Catppuccin.Mauve().Hex)), } return style } -func getPriorityStyle(style Style, priority string) lipgloss.Style { +func (s Style) getTaskStyle(task todo.Task, checked bool) string { + + todoText := task.Original + + checkedRegex := regexp.MustCompile(`^x (\d{4}-\d{2}-\d{2} )? ?`) + priorityRegex := regexp.MustCompile(`^\([A-Z]\) `) + contextRegex := regexp.MustCompile(`#\w+`) + projectRegex := regexp.MustCompile(`\+\w+`) + + todoText = checkedRegex.ReplaceAllString(todoText, "") + todoText = priorityRegex.ReplaceAllString(todoText, "") + + checkString := " " + if checked { + checkString = "✓" + } + + switch task.Completed { + case true: + dateStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#6c7086")) + dateString := dateStyle.Render(task.CompletedDate.Local().Format("02-01-2006 15:03")) + return fmt.Sprintf("%s %s %s %s", s.Check.Render(checkString), s.getPriorityStyle(task.Priority), s.Task.Completed.Render(todoText), dateString) + default: + todoText = contextRegex.ReplaceAllStringFunc(todoText, func(match string) string { + return s.Context.Render(match) + }) + todoText = projectRegex.ReplaceAllStringFunc(todoText, func(match string) string { + return s.Project.Render(match) + }) + return fmt.Sprintf("%s %s %s", s.Check.Render(checkString), s.getPriorityStyle(task.Priority), s.Task.Todo.Render(todoText)) + } +} + +func (s Style) getPriorityStyle(priority string) string { switch priority { + case "": + return " " case "A": - return style.Priority.A + return s.Priority.A.Render(priority) case "B": - return style.Priority.B + return s.Priority.B.Render(priority) case "C": - return style.Priority.C + return s.Priority.C.Render(priority) default: - return lipgloss.NewStyle() + return priority } }