From 81fbc1b949f4c5e394e8fc20fa5d4801f338f36e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20B=C3=BCtow?= <1224732+snonux@users.noreply.github.com> Date: Thu, 19 Jun 2025 23:59:05 +0300 Subject: Add table-based UI and extend task fields --- cmd/tasksamurai/main.go | 66 ++++++++++++++++++++++++++++++++++++++----------- go.mod | 4 +-- go.sum | 6 ----- internal/task/task.go | 22 +++++++++++++---- internal/ui/table.go | 55 +++++++++++++++++++++++++++++++++++++++++ internal/ui/ui.go | 47 ----------------------------------- 6 files changed, 124 insertions(+), 76 deletions(-) create mode 100644 internal/ui/table.go delete mode 100644 internal/ui/ui.go diff --git a/cmd/tasksamurai/main.go b/cmd/tasksamurai/main.go index 95aef13..424d0d8 100644 --- a/cmd/tasksamurai/main.go +++ b/cmd/tasksamurai/main.go @@ -3,12 +3,14 @@ package main import ( "fmt" "os" + "strconv" + "strings" + "time" - "tasksamurai/internal" "tasksamurai/internal/task" "tasksamurai/internal/ui" - "github.com/charmbracelet/bubbles/list" + "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" ) @@ -19,19 +21,15 @@ func main() { os.Exit(1) } - var items []list.Item + var rows []table.Row for _, t := range tasks { - if t.Status != "completed" { - items = append(items, taskItem{t}) + if t.Status == "completed" { + continue } + rows = append(rows, taskToRow(t)) } - delegate := list.NewDefaultDelegate() - l := list.New(items, delegate, 0, 0) - l.Title = fmt.Sprintf("tasksamurai %s", internal.Version) - l.SetShowStatusBar(false) - l.SetFilteringEnabled(false) - m := ui.New(l) + m := ui.New(rows) p := tea.NewProgram(&m) if _, err := p.Run(); err != nil { @@ -40,8 +38,46 @@ func main() { } } -type taskItem struct{ t task.Task } +func taskToRow(t task.Task) table.Row { + active := "" + if t.Start != "" { + active = "yes" + } + + age := "" + if ts, err := time.Parse("20060102T150405Z", t.Entry); err == nil { + days := int(time.Since(ts).Hours() / 24) + age = fmt.Sprintf("%dd", days) + } + + tags := strings.Join(t.Tags, ",") + urg := fmt.Sprintf("%.1f", t.Urgency) + + var anns []string + for _, a := range t.Annotations { + anns = append(anns, a.Description) + } -func (i taskItem) Title() string { return i.t.Description } -func (i taskItem) Description() string { return "" } -func (i taskItem) FilterValue() string { return i.t.Description } + return table.Row{ + strconv.Itoa(t.ID), + t.Description, + active, + age, + t.Priority, + tags, + t.Recur, + formatDate(t.Due), + urg, + strings.Join(anns, "; "), + } +} + +func formatDate(s string) string { + if s == "" { + return "" + } + if ts, err := time.Parse("20060102T150405Z", s); err == nil { + return ts.Format("2006-01-02") + } + return s +} diff --git a/go.mod b/go.mod index b60eacc..9ba3205 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,12 @@ go 1.23.8 require ( github.com/charmbracelet/bubbles v0.21.0 github.com/charmbracelet/bubbletea v1.3.5 + github.com/charmbracelet/lipgloss v1.1.0 ) require ( - github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect - github.com/charmbracelet/lipgloss v1.1.0 // indirect github.com/charmbracelet/x/ansi v0.8.0 // indirect github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect github.com/charmbracelet/x/term v0.2.1 // indirect @@ -24,7 +23,6 @@ require ( github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/sahilm/fuzzy v0.1.1 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/sync v0.13.0 // indirect golang.org/x/sys v0.32.0 // indirect diff --git a/go.sum b/go.sum index 5f9d54d..48041bc 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -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/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= @@ -22,8 +20,6 @@ github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQ github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -41,8 +37,6 @@ github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= -github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= diff --git a/internal/task/task.go b/internal/task/task.go index dcb3e9a..a39a7f6 100644 --- a/internal/task/task.go +++ b/internal/task/task.go @@ -8,12 +8,24 @@ import ( ) // Task represents a taskwarrior task as returned by `task export`. +type Annotation struct { + Entry string `json:"entry"` + Description string `json:"description"` +} + type Task struct { - ID int `json:"id"` - UUID string `json:"uuid"` - Description string `json:"description"` - Tags []string `json:"tags"` - Status string `json:"status"` + ID int `json:"id"` + UUID string `json:"uuid"` + Description string `json:"description"` + Tags []string `json:"tags"` + Status string `json:"status"` + Start string `json:"start"` + Entry string `json:"entry"` + Due string `json:"due"` + Priority string `json:"priority"` + Recur string `json:"recur"` + Urgency float64 `json:"urgency"` + Annotations []Annotation `json:"annotations"` } // Add creates a new task with the given description and tags. diff --git a/internal/ui/table.go b/internal/ui/table.go new file mode 100644 index 0000000..682a680 --- /dev/null +++ b/internal/ui/table.go @@ -0,0 +1,55 @@ +package ui + +import ( + "github.com/charmbracelet/bubbles/table" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +// Model wraps a Bubble Tea table.Model to display tasks. +type Model struct{ tbl table.Model } + +// New creates a new UI model with the provided rows. +func New(rows []table.Row) Model { + cols := []table.Column{ + {Title: "ID", Width: 4}, + {Title: "Task", Width: 30}, + {Title: "Active", Width: 6}, + {Title: "Age", Width: 6}, + {Title: "Pri", Width: 4}, + {Title: "Tags", Width: 15}, + {Title: "Recur", Width: 6}, + {Title: "Due", Width: 10}, + {Title: "Urg", Width: 5}, + {Title: "Annotations", Width: 20}, + } + t := table.New( + table.WithColumns(cols), + table.WithRows(rows), + table.WithFocused(true), + ) + styles := table.DefaultStyles() + styles.Header = styles.Header.Foreground(lipgloss.Color("205")) + styles.Selected = styles.Selected.Foreground(lipgloss.Color("229")).Background(lipgloss.Color("57")) + styles.Cell = styles.Cell.Padding(0, 1) + t.SetStyles(styles) + return Model{tbl: t} +} + +// Init implements tea.Model. +func (m Model) Init() tea.Cmd { return nil } + +// Update handles key and window events. +func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.tbl.SetWidth(msg.Width) + m.tbl.SetHeight(msg.Height - 2) + } + var cmd tea.Cmd + m.tbl, cmd = m.tbl.Update(msg) + return m, cmd +} + +// View renders the table UI. +func (m Model) View() string { return m.tbl.View() } diff --git a/internal/ui/ui.go b/internal/ui/ui.go deleted file mode 100644 index 244c191..0000000 --- a/internal/ui/ui.go +++ /dev/null @@ -1,47 +0,0 @@ -package ui - -import ( - "github.com/charmbracelet/bubbles/list" - tea "github.com/charmbracelet/bubbletea" -) - -// Model wraps a Bubble Tea list.Model to display tasks. -type Model struct { - list list.Model -} - -// New creates a new UI model with the provided Bubble Tea list. -func New(l list.Model) Model { - return Model{list: l} -} - -// Init implements tea.Model. -func (m Model) Init() tea.Cmd { return nil } - -// Update handles key and window events. -func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.String() { - case "j": - msg = tea.KeyMsg{Type: tea.KeyDown} - case "k": - msg = tea.KeyMsg{Type: tea.KeyUp} - } - return m.updateList(msg) - case tea.WindowSizeMsg: - m.list.SetSize(msg.Width, msg.Height-2) - } - return m.updateList(msg) -} - -func (m *Model) updateList(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmd tea.Cmd - m.list, cmd = m.list.Update(msg) - return m, cmd -} - -// View renders the list UI. -func (m Model) View() string { - return m.list.View() -} -- cgit v1.2.3