diff options
| author | Paul Bütow <1224732+snonux@users.noreply.github.com> | 2025-06-21 20:25:28 +0300 |
|---|---|---|
| committer | Paul Bütow <1224732+snonux@users.noreply.github.com> | 2025-06-21 20:25:28 +0300 |
| commit | 273d493170abe8838252c61a40b558ef4edc3db8 (patch) | |
| tree | b037cd8b3ee8a89060607f10f5dd8dd77715edd4 | |
| parent | d1228ab2c5af89a35929ce313c39db9eb076f3e6 (diff) | |
Add dynamic column widths and overdue sorting
| -rw-r--r-- | internal/task/task.go | 12 | ||||
| -rw-r--r-- | internal/ui/table.go | 171 |
2 files changed, 145 insertions, 38 deletions
diff --git a/internal/task/task.go b/internal/task/task.go index 4acfec9..64dfc0d 100644 --- a/internal/task/task.go +++ b/internal/task/task.go @@ -242,7 +242,8 @@ func Edit(id int) error { // SortTasks orders tasks by start status, priority, due date, tag names and id. // Started tasks are always placed before non-started ones. Tasks without a due -// date are placed after tasks with a due date. +// date are placed after tasks with a due date. Overdue tasks are placed at the +// very top regardless of other properties. func SortTasks(tasks []Task) { joinTags := func(tags []string) string { if len(tags) == 0 { @@ -277,9 +278,18 @@ func SortTasks(tasks []Task) { return t, true } + overdue := func(t Task) bool { + du, ok := parseDue(t.Due) + return ok && time.Now().After(du) + } + sort.Slice(tasks, func(i, j int) bool { ti, tj := tasks[i], tasks[j] + if oi, oj := overdue(ti), overdue(tj); oi != oj { + return oi + } + startedI := ti.Start != "" && ti.Status != "completed" startedJ := tj.Start != "" && tj.Status != "completed" if startedI != startedJ { diff --git a/internal/ui/table.go b/internal/ui/table.go index a78eeb9..4a92876 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -21,17 +21,6 @@ import ( var priorityOptions = []string{"H", "M", "L", ""} -const ( - idWidth = 4 - priWidth = 1 - ageWidth = 6 - urgWidth = 5 - dueWidth = 10 - tagsWidth = 15 - descWidth = 45 - annWidth = 1 -) - func init() { rand.Seed(time.Now().UnixNano()) } @@ -76,6 +65,15 @@ type Model struct { windowHeight int + idWidth int + priWidth int + ageWidth int + urgWidth int + dueWidth int + tagsWidth int + descWidth int + annWidth int + total int inProgress int due int @@ -107,16 +105,16 @@ func New(filters []string) (Model, error) { return m, nil } -func newTable(rows []atable.Row) (atable.Model, atable.Styles) { +func (m *Model) newTable(rows []atable.Row) (atable.Model, atable.Styles) { cols := []atable.Column{ - {Title: "ID", Width: idWidth}, - {Title: "Pri", Width: priWidth}, - {Title: "Age", Width: ageWidth}, - {Title: "Urg", Width: urgWidth}, - {Title: "Due", Width: dueWidth}, - {Title: "Tags", Width: tagsWidth}, - {Title: "Annotations", Width: annWidth}, - {Title: "Description", Width: descWidth}, + {Title: "ID", Width: m.idWidth}, + {Title: "Pri", Width: m.priWidth}, + {Title: "Age", Width: m.ageWidth}, + {Title: "Urg", Width: m.urgWidth}, + {Title: "Due", Width: m.dueWidth}, + {Title: "Tags", Width: m.tagsWidth}, + {Title: "Annotations", Width: m.annWidth}, + {Title: "Description", Width: m.descWidth}, } t := atable.New( atable.WithColumns(cols), @@ -143,10 +141,17 @@ func (m *Model) reload() error { task.SortTasks(tasks) + m.tasks = tasks + m.total = task.TotalTasks(tasks) + m.inProgress = task.InProgressTasks(tasks) + m.due = task.DueTasks(tasks, time.Now()) + + m.computeColumnWidths() + var rows []atable.Row m.searchMatches = nil for i, tsk := range tasks { - rows = append(rows, taskToRowSearch(tsk, m.searchRegex, m.tblStyles, -1)) + rows = append(rows, m.taskToRowSearch(tsk, m.searchRegex, m.tblStyles, -1)) if m.searchRegex != nil { tags := strings.Join(tsk.Tags, " ") if m.searchRegex.MatchString(tags) { @@ -167,15 +172,11 @@ func (m *Model) reload() error { m.searchIndex = 0 } - m.tasks = tasks - m.total = task.TotalTasks(tasks) - m.inProgress = task.InProgressTasks(tasks) - m.due = task.DueTasks(tasks, time.Now()) - if m.tbl.Columns() == nil { - m.tbl, m.tblStyles = newTable(rows) + m.tbl, m.tblStyles = m.newTable(rows) } else { m.tbl.SetRows(rows) + m.applyColumns() } m.updateSelectionHighlight(-1, m.tbl.Cursor(), 0, m.tbl.ColumnCursor()) return nil @@ -190,6 +191,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.WindowSizeMsg: m.tbl.SetWidth(msg.Width) m.windowHeight = msg.Height + m.computeColumnWidths() m.updateTableHeight() return m, nil case editDoneMsg: @@ -557,7 +559,7 @@ func (m Model) topStatusLine() string { Render(line) } -func taskToRow(t task.Task) atable.Row { +func (m Model) taskToRow(t task.Task) atable.Row { style := lipgloss.NewStyle() if t.Start != "" { style = style.Background(lipgloss.Color("6")) @@ -584,10 +586,10 @@ func taskToRow(t task.Task) atable.Row { return atable.Row{ style.Render(strconv.Itoa(t.ID)), - formatPriority(t.Priority, priWidth), + formatPriority(t.Priority, m.priWidth), style.Render(age), style.Render(urg), - formatDue(t.Due, dueWidth), + formatDue(t.Due, m.dueWidth), style.Render(tags), style.Render(annStr), style.Render(t.Description), @@ -689,7 +691,7 @@ func highlightCellMatch(base lipgloss.Style, re *regexp.Regexp, raw, display str return base.Render(display) } -func taskToRowSearch(t task.Task, re *regexp.Regexp, styles atable.Styles, selectedCol int) atable.Row { +func (m Model) taskToRowSearch(t task.Task, re *regexp.Regexp, styles atable.Styles, selectedCol int) atable.Row { rowStyle := lipgloss.NewStyle() if t.Start != "" { rowStyle = rowStyle.Background(lipgloss.Color("6")) @@ -720,9 +722,9 @@ func taskToRowSearch(t task.Task, re *regexp.Regexp, styles atable.Styles, selec } idStr := getStyle(0).Render(strconv.Itoa(t.ID)) - priStr := formatPriority(t.Priority, priWidth) + priStr := formatPriority(t.Priority, m.priWidth) ageStr := getStyle(2).Render(age) - dueStr := formatDue(t.Due, dueWidth) + dueStr := formatDue(t.Due, m.dueWidth) urgStr := getStyle(3).Render(urg) tagStr := highlightCell(getStyle(5), re, tags) @@ -758,7 +760,7 @@ func (m Model) expandedCellView() string { case 0: val = strconv.Itoa(t.ID) case 1: - val = ansi.Strip(formatPriority(t.Priority, priWidth)) + val = ansi.Strip(formatPriority(t.Priority, m.priWidth)) case 2: if ts, err := time.Parse("20060102T150405Z", t.Entry); err == nil { days := int(time.Since(ts).Hours() / 24) @@ -767,7 +769,7 @@ func (m Model) expandedCellView() string { case 3: val = fmt.Sprintf("%.1f", t.Urgency) case 4: - val = ansi.Strip(formatDue(t.Due, dueWidth)) + val = ansi.Strip(formatDue(t.Due, m.dueWidth)) case 5: val = strings.Join(t.Tags, " ") case 6: @@ -797,10 +799,10 @@ func (m *Model) updateSelectionHighlight(prevRow, newRow, prevCol, newCol int) { } rows := m.tbl.Rows() if prevRow >= 0 && prevRow < len(rows) { - rows[prevRow] = taskToRowSearch(m.tasks[prevRow], m.searchRegex, m.tblStyles, -1) + rows[prevRow] = m.taskToRowSearch(m.tasks[prevRow], m.searchRegex, m.tblStyles, -1) } if newRow >= 0 && newRow < len(rows) { - rows[newRow] = taskToRowSearch(m.tasks[newRow], m.searchRegex, m.tblStyles, newCol) + rows[newRow] = m.taskToRowSearch(m.tasks[newRow], m.searchRegex, m.tblStyles, newCol) } m.tbl.SetRows(rows) } @@ -823,3 +825,98 @@ func (m *Model) updateTableHeight() { } m.tbl.SetHeight(h) } + +func dueText(s string) string { + if s == "" { + return "" + } + ts, err := time.Parse("20060102T150405Z", s) + if err != nil { + return s + } + days := int(time.Until(ts).Hours() / 24) + switch days { + case 0: + return "today" + case 1: + return "tomorrow" + case -1: + return "yesterday" + default: + return fmt.Sprintf("%dd", days) + } +} + +func (m *Model) computeColumnWidths() { + maxID := 1 + maxAge := 0 + maxUrg := 0 + maxDue := 0 + maxTags := 0 + maxAnn := 1 + for _, t := range m.tasks { + if l := len(strconv.Itoa(t.ID)); l > maxID { + maxID = l + } + age := "" + if ts, err := time.Parse("20060102T150405Z", t.Entry); err == nil { + age = fmt.Sprintf("%dd", int(time.Since(ts).Hours()/24)) + } + if l := len(age); l > maxAge { + maxAge = l + } + urg := fmt.Sprintf("%.1f", t.Urgency) + if l := len(urg); l > maxUrg { + maxUrg = l + } + due := dueText(t.Due) + if l := len(due); l > maxDue { + maxDue = l + } + tags := strings.Join(t.Tags, " ") + if l := len(tags); l > maxTags { + maxTags = l + } + ann := len(t.Annotations) + if l := len(strconv.FormatInt(int64(ann), 16)); l > maxAnn { + maxAnn = l + } + } + + m.idWidth = maxID + m.priWidth = 1 + m.ageWidth = maxAge + m.urgWidth = maxUrg + m.dueWidth = maxDue + m.tagsWidth = maxTags + m.annWidth = maxAnn + + total := m.tbl.Width() + if total == 0 { + total = 80 + } + base := m.idWidth + m.priWidth + m.ageWidth + m.urgWidth + m.dueWidth + m.tagsWidth + m.annWidth + base += 7 // spaces between columns + m.descWidth = total - base + if m.descWidth < 1 { + m.descWidth = 1 + } + + if m.tbl.Columns() != nil { + m.applyColumns() + } +} + +func (m *Model) applyColumns() { + cols := []atable.Column{ + {Title: "ID", Width: m.idWidth}, + {Title: "Pri", Width: m.priWidth}, + {Title: "Age", Width: m.ageWidth}, + {Title: "Urg", Width: m.urgWidth}, + {Title: "Due", Width: m.dueWidth}, + {Title: "Tags", Width: m.tagsWidth}, + {Title: "Annotations", Width: m.annWidth}, + {Title: "Description", Width: m.descWidth}, + } + m.tbl.SetColumns(cols) +} |
