summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Bütow <1224732+snonux@users.noreply.github.com>2025-06-19 23:59:05 +0300
committerPaul Bütow <1224732+snonux@users.noreply.github.com>2025-06-19 23:59:05 +0300
commit81fbc1b949f4c5e394e8fc20fa5d4801f338f36e (patch)
tree8bdd31825f08b70b604f5160aad6fdaf024e7acc
parentbf39c17221d1df1815c9cd78fa33b144c9d5dfb6 (diff)
Add table-based UI and extend task fields
-rw-r--r--cmd/tasksamurai/main.go66
-rw-r--r--go.mod4
-rw-r--r--go.sum6
-rw-r--r--internal/task/task.go22
-rw-r--r--internal/ui/table.go55
-rw-r--r--internal/ui/ui.go47
6 files changed, 124 insertions, 76 deletions
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()
-}