summaryrefslogtreecommitdiff
path: root/internal/ui/table.go
diff options
context:
space:
mode:
authorPaul Bütow <1224732+snonux@users.noreply.github.com>2025-06-20 09:40:56 +0300
committerPaul Bütow <1224732+snonux@users.noreply.github.com>2025-06-20 09:40:56 +0300
commit13f7678a9fd092ac20eec10e4d2196f5bd1ae107 (patch)
tree512a6b9f8017f8cf0cc24158acc154034c121225 /internal/ui/table.go
parent13ec0a6ec615b4c7e7ddc461a7a6a623109826f9 (diff)
Add cell navigation, editor hotkey, stats and filter
Diffstat (limited to 'internal/ui/table.go')
-rw-r--r--internal/ui/table.go126
1 files changed, 123 insertions, 3 deletions
diff --git a/internal/ui/table.go b/internal/ui/table.go
index 4300034..4293500 100644
--- a/internal/ui/table.go
+++ b/internal/ui/table.go
@@ -1,19 +1,43 @@
package ui
import (
+ "fmt"
+ "strconv"
+ "strings"
+ "time"
+
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
+
atable "tasksamurai/internal/atable"
+ "tasksamurai/internal/task"
)
// Model wraps a Bubble Tea table.Model to display tasks.
type Model struct {
tbl atable.Model
showHelp bool
+
+ filter string
+ tasks []task.Task
+
+ total int
+ inProgress int
+ due int
}
// New creates a new UI model with the provided rows.
-func New(rows []atable.Row) Model {
+func New(filter string) (Model, error) {
+ m := Model{filter: filter}
+
+ if err := m.reload(); err != nil {
+ return Model{}, err
+ }
+
+ return m, nil
+}
+
+func newTable(rows []atable.Row) atable.Model {
cols := []atable.Column{
{Title: "ID", Width: 4},
{Title: "Task", Width: 30},
@@ -36,7 +60,36 @@ func New(rows []atable.Row) Model {
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}
+ return t
+}
+
+func (m *Model) reload() error {
+ tasks, err := task.Export(strings.Fields(m.filter)...)
+ if err != nil {
+ return err
+ }
+
+ var rows []atable.Row
+ var filtered []task.Task
+ for _, tsk := range tasks {
+ if tsk.Status == "completed" {
+ continue
+ }
+ filtered = append(filtered, tsk)
+ rows = append(rows, taskToRow(tsk))
+ }
+
+ m.tasks = filtered
+ m.total = task.TotalTasks(filtered)
+ m.inProgress = task.InProgressTasks(filtered)
+ m.due = task.DueTasks(filtered, time.Now())
+
+ if m.tbl.Columns() == nil {
+ m.tbl = newTable(rows)
+ } else {
+ m.tbl.SetRows(rows)
+ }
+ return nil
}
// Init implements tea.Model.
@@ -60,6 +113,14 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
return m, tea.Quit
+ case "E":
+ if row := m.tbl.SelectedRow(); row != nil {
+ id, err := strconv.Atoi(row[0])
+ if err == nil {
+ _ = task.Edit(id)
+ m.reload()
+ }
+ }
}
}
@@ -81,5 +142,64 @@ func (m Model) View() string {
"?: help", // show help toggle line
)
}
- return m.tbl.View()
+ return lipgloss.JoinVertical(lipgloss.Left,
+ m.tbl.View(),
+ m.statusLine(),
+ )
+}
+
+func (m Model) statusLine() string {
+ return fmt.Sprintf("Total:%d InProgress:%d Due:%d", m.total, m.inProgress, m.due)
+}
+
+func taskToRow(t task.Task) atable.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)
+ }
+
+ return atable.Row{
+ strconv.Itoa(t.ID),
+ t.Description,
+ active,
+ age,
+ t.Priority,
+ tags,
+ t.Recur,
+ formatDue(t.Due),
+ urg,
+ strings.Join(anns, "; "),
+ }
+}
+
+func formatDue(s string) string {
+ if s == "" {
+ return ""
+ }
+ ts, err := time.Parse("20060102T150405Z", s)
+ if err != nil {
+ return s
+ }
+
+ days := int(time.Until(ts).Hours() / 24)
+ val := fmt.Sprintf("%dd", days)
+ style := lipgloss.NewStyle()
+ if days < 0 {
+ style = style.Background(lipgloss.Color("1"))
+ }
+ return style.Render(val)
}