From bafd8958d4b9b169c4414d244278117bbe4aa717 Mon Sep 17 00:00:00 2001 From: Martin Date: Sun, 9 Jun 2024 17:55:56 +0200 Subject: [PATCH] Handle UDAs for editing; Fix layout; Add annotations --- common/common.go | 10 +- common/styles.go | 200 ++------------ components/table/table.go | 80 ++++-- go.mod | 10 + go.sum | 28 ++ pages/messaging.go | 4 +- pages/report.go | 7 +- pages/taskEditor.go | 547 +++++++++++++++++++++++-------------- taskwarrior/models.go | 111 +++++++- taskwarrior/taskwarrior.go | 128 +++++---- test/taskchampion.sqlite3 | Bin 196608 -> 225280 bytes test/taskrc | 14 +- 12 files changed, 663 insertions(+), 476 deletions(-) diff --git a/common/common.go b/common/common.go index cde38ca..29aab3d 100644 --- a/common/common.go +++ b/common/common.go @@ -15,7 +15,7 @@ type Common struct { TW taskwarrior.TaskWarrior Keymap *Keymap Styles *Styles - Udas []string + Udas []taskwarrior.Uda pageStack *Stack[Component] width int @@ -54,5 +54,11 @@ func (c *Common) PushPage(page Component) { } func (c *Common) PopPage() (Component, error) { - return c.pageStack.Pop() + component, err := c.pageStack.Pop() + if err != nil { + return nil, err + } + + component.SetSize(c.width, c.height) + return component, nil } diff --git a/common/styles.go b/common/styles.go index eeffda5..f9aec89 100644 --- a/common/styles.go +++ b/common/styles.go @@ -20,6 +20,8 @@ type TableStyle struct { } type Styles struct { + Colors map[string]*lipgloss.Style + Base lipgloss.Style Form *huh.Theme @@ -28,61 +30,22 @@ type Styles struct { ColumnFocused lipgloss.Style ColumnBlurred lipgloss.Style ColumnInsert lipgloss.Style - - Colors map[string]lipgloss.Style - - // TODO: make color config completely dynamic to account for keyword., project., tag. and uda. colors - Active lipgloss.Style - Alternate lipgloss.Style - Blocked lipgloss.Style - Blocking lipgloss.Style - BurndownDone lipgloss.Style - BurndownPending lipgloss.Style - BurndownStarted lipgloss.Style - CalendarDue lipgloss.Style - CalendarDueToday lipgloss.Style - CalendarHoliday lipgloss.Style - CalendarOverdue lipgloss.Style - CalendarScheduled lipgloss.Style - CalendarToday lipgloss.Style - CalendarWeekend lipgloss.Style - CalendarWeeknumber lipgloss.Style - Completed lipgloss.Style - Debug lipgloss.Style - Deleted lipgloss.Style - Due lipgloss.Style - DueToday lipgloss.Style - Error lipgloss.Style - Footnote lipgloss.Style - Header lipgloss.Style - HistoryAdd lipgloss.Style - HistoryDelete lipgloss.Style - HistoryDone lipgloss.Style - Label lipgloss.Style - LabelSort lipgloss.Style - Overdue lipgloss.Style - ProjectNone lipgloss.Style - Recurring lipgloss.Style - Scheduled lipgloss.Style - SummaryBackground lipgloss.Style - SummaryBar lipgloss.Style - SyncAdded lipgloss.Style - SyncChanged lipgloss.Style - SyncRejected lipgloss.Style - TagNext lipgloss.Style - TagNone lipgloss.Style - Tagged lipgloss.Style - UdaPriorityH lipgloss.Style - UdaPriorityL lipgloss.Style - UdaPriorityM lipgloss.Style - UndoAfter lipgloss.Style - UndoBefore lipgloss.Style - Until lipgloss.Style - Warning lipgloss.Style } func NewStyles(config *taskwarrior.TWConfig) *Styles { - styles := parseColors(config.GetConfig()) + styles := Styles{} + + colors := make(map[string]*lipgloss.Style) + + for key, value := range config.GetConfig() { + if strings.HasPrefix(key, "color.") { + _, color, _ := strings.Cut(key, ".") + colors[color] = parseColorString(value) + } + } + + styles.Colors = colors + styles.Base = lipgloss.NewStyle() styles.TableStyle = TableStyle{ @@ -94,11 +57,12 @@ func NewStyles(config *taskwarrior.TWConfig) *Styles { formTheme := huh.ThemeBase() formTheme.Focused.Title = formTheme.Focused.Title.Bold(true) - formTheme.Focused.SelectSelector = formTheme.Focused.SelectSelector.SetString("> ") + formTheme.Focused.SelectSelector = formTheme.Focused.SelectSelector.SetString("→ ") formTheme.Focused.SelectedOption = formTheme.Focused.SelectedOption.Bold(true) - formTheme.Focused.MultiSelectSelector = formTheme.Focused.MultiSelectSelector.SetString("> ") + formTheme.Focused.MultiSelectSelector = formTheme.Focused.MultiSelectSelector.SetString("→ ") formTheme.Focused.SelectedPrefix = formTheme.Focused.SelectedPrefix.SetString("✓ ") formTheme.Focused.UnselectedPrefix = formTheme.Focused.SelectedPrefix.SetString("• ") + formTheme.Blurred.Title = formTheme.Blurred.Title.Bold(true) formTheme.Blurred.SelectSelector = formTheme.Blurred.SelectSelector.SetString(" ") formTheme.Blurred.SelectedOption = formTheme.Blurred.SelectedOption.Bold(true) formTheme.Blurred.MultiSelectSelector = formTheme.Blurred.MultiSelectSelector.SetString(" ") @@ -107,131 +71,23 @@ func NewStyles(config *taskwarrior.TWConfig) *Styles { styles.Form = formTheme - styles.ColumnFocused = lipgloss.NewStyle().Width(50).Height(30).Border(lipgloss.DoubleBorder(), true) - styles.ColumnBlurred = lipgloss.NewStyle().Width(50).Height(30).Border(lipgloss.HiddenBorder(), true) - styles.ColumnInsert = lipgloss.NewStyle().Width(50).Height(30).Border(lipgloss.DoubleBorder(), true).BorderForeground(styles.Active.GetForeground()) - - return styles -} - -func parseColors(config map[string]string) *Styles { - styles := Styles{} - colors := make(map[string]lipgloss.Style) - - for key, value := range config { - if strings.HasPrefix(key, "color.") { - _, colorValue, _ := strings.Cut(key, ".") - colors[colorValue] = parseColorString(value) - switch colorValue { - case "active": - styles.Active = parseColorString(value) - case "alternate": - styles.Alternate = parseColorString(value) - case "blocked": - styles.Blocked = parseColorString(value) - case "blocking": - styles.Blocking = parseColorString(value) - case "burndown.done": - styles.BurndownDone = parseColorString(value) - case "burndown.pending": - styles.BurndownPending = parseColorString(value) - case "burndown.started": - styles.BurndownStarted = parseColorString(value) - case "calendar.due": - styles.CalendarDue = parseColorString(value) - case "calendar.due.today": - styles.CalendarDueToday = parseColorString(value) - case "calendar.holiday": - styles.CalendarHoliday = parseColorString(value) - case "calendar.overdue": - styles.CalendarOverdue = parseColorString(value) - case "calendar.scheduled": - styles.CalendarScheduled = parseColorString(value) - case "calendar.today": - styles.CalendarToday = parseColorString(value) - case "calendar.weekend": - styles.CalendarWeekend = parseColorString(value) - case "calendar.weeknumber": - styles.CalendarWeeknumber = parseColorString(value) - case "completed": - styles.Completed = parseColorString(value) - case "debug": - styles.Debug = parseColorString(value) - case "deleted": - styles.Deleted = parseColorString(value) - case "due": - styles.Due = parseColorString(value) - case "due.today": - styles.DueToday = parseColorString(value) - case "error": - styles.Error = parseColorString(value) - case "footnote": - styles.Footnote = parseColorString(value) - case "header": - styles.Header = parseColorString(value) - case "history.add": - styles.HistoryAdd = parseColorString(value) - case "history.delete": - styles.HistoryDelete = parseColorString(value) - case "history.done": - styles.HistoryDone = parseColorString(value) - case "label": - styles.Label = parseColorString(value) - case "label.sort": - styles.LabelSort = parseColorString(value) - case "overdue": - styles.Overdue = parseColorString(value) - case "project.none": - styles.ProjectNone = parseColorString(value) - case "recurring": - styles.Recurring = parseColorString(value) - case "scheduled": - styles.Scheduled = parseColorString(value) - case "summary.background": - styles.SummaryBackground = parseColorString(value) - case "summary.bar": - styles.SummaryBar = parseColorString(value) - case "sync.added": - styles.SyncAdded = parseColorString(value) - case "sync.changed": - styles.SyncChanged = parseColorString(value) - case "sync.rejected": - styles.SyncRejected = parseColorString(value) - case "tag.next": - styles.TagNext = parseColorString(value) - case "tag.none": - styles.TagNone = parseColorString(value) - case "tagged": - styles.Tagged = parseColorString(value) - case "uda.priority.H": - styles.UdaPriorityH = parseColorString(value) - case "uda.priority.L": - styles.UdaPriorityL = parseColorString(value) - case "uda.priority.M": - styles.UdaPriorityM = parseColorString(value) - case "undo.after": - styles.UndoAfter = parseColorString(value) - case "undo.before": - styles.UndoBefore = parseColorString(value) - case "until": - styles.Until = parseColorString(value) - case "warning": - styles.Warning = parseColorString(value) - } - } + styles.ColumnFocused = lipgloss.NewStyle().Border(lipgloss.RoundedBorder(), true).Padding(1) + styles.ColumnBlurred = lipgloss.NewStyle().Border(lipgloss.HiddenBorder(), true).Padding(1) + styles.ColumnInsert = lipgloss.NewStyle().Border(lipgloss.RoundedBorder(), true).Padding(1) + if styles.Colors["active"] != nil { + styles.ColumnInsert = styles.ColumnInsert.BorderForeground(styles.Colors["active"].GetForeground()) } - styles.Colors = colors - return &styles } -func parseColorString(color string) lipgloss.Style { - style := lipgloss.NewStyle() +func parseColorString(color string) *lipgloss.Style { if color == "" { - return style + return nil } + style := lipgloss.NewStyle() + if strings.Contains(color, "on") { fgbg := strings.Split(color, "on") fg := strings.TrimSpace(fgbg[0]) @@ -246,7 +102,7 @@ func parseColorString(color string) lipgloss.Style { style = style.Foreground(parseColor(strings.TrimSpace(color))) } - return style + return &style } func parseColor(color string) lipgloss.Color { diff --git a/components/table/table.go b/components/table/table.go index 3160d1a..17a54a6 100644 --- a/components/table/table.go +++ b/components/table/table.go @@ -149,65 +149,85 @@ func (m *Model) parseRowStyles(rows taskwarrior.Tasks) []lipgloss.Style { if len(rows) == 0 { return styles } +taskstyle: for i, task := range rows { if task.Status == "deleted" { - styles[i] = m.common.Styles.Deleted.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding()) - continue + if c, ok := m.common.Styles.Colors["deleted"]; ok && c != nil { + styles[i] = c.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding()) + continue + } } if task.Status == "completed" { - styles[i] = m.common.Styles.Completed.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding()) - continue + if c, ok := m.common.Styles.Colors["completed"]; ok && c != nil { + styles[i] = c.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding()) + continue + } } if task.Status == "pending" && task.Start != "" { - styles[i] = m.common.Styles.Active.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding()) - continue + if c, ok := m.common.Styles.Colors["active"]; ok && c != nil { + styles[i] = c.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding()) + continue + } } // TODO: implement keyword // TODO: implement tag + if task.HasTag("next") { + if c, ok := m.common.Styles.Colors["tag.next"]; ok && c != nil { + styles[i] = c.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding()) + continue + } + } // TODO: implement project if !task.GetDate("due").IsZero() && task.GetDate("due").Before(time.Now()) { - styles[i] = m.common.Styles.Overdue.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding()) - continue + if c, ok := m.common.Styles.Colors["overdue"]; ok && c != nil { + styles[i] = c.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding()) + continue + } } if task.Scheduled != "" { - styles[i] = m.common.Styles.Scheduled.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding()) - continue + if c, ok := m.common.Styles.Colors["scheduled"]; ok && c != nil { + styles[i] = c.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding()) + continue + } } if !task.GetDate("due").IsZero() && task.GetDate("due").Truncate(24*time.Hour).Equal(time.Now().Truncate(24*time.Hour)) { - styles[i] = m.common.Styles.DueToday.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding()) - continue + if c, ok := m.common.Styles.Colors["due.today"]; ok && c != nil { + styles[i] = c.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding()) + continue + } } if task.Due != "" { - styles[i] = m.common.Styles.Due.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding()) - continue + if c, ok := m.common.Styles.Colors["due"]; ok && c != nil { + styles[i] = c.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding()) + continue + } } if len(task.Depends) > 0 { - styles[i] = m.common.Styles.Blocked.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding()) - continue + if c, ok := m.common.Styles.Colors["blocked"]; ok && c != nil { + styles[i] = c.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding()) + continue + } } // TODO implement blocking if task.Recur != "" { - styles[i] = m.common.Styles.Recurring.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding()) - continue + if c, ok := m.common.Styles.Colors["recurring"]; ok && c != nil { + styles[i] = c.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding()) + continue + } } // TODO: make styles optional and discard if empty if len(task.Tags) > 0 { - styles[i] = m.common.Styles.Tagged.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding()) - taskIteration: - for _, tag := range task.Tags { - if tag == "next" { - styles[i] = m.common.Styles.TagNext.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding()) - break taskIteration - } + if c, ok := m.common.Styles.Colors["tagged"]; ok && c != nil { + styles[i] = c.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding()) + continue } - continue } if len(m.common.Udas) > 0 { for _, uda := range m.common.Udas { - if u, ok := task.Udas[uda]; ok { - if style, ok := m.common.Styles.Colors[fmt.Sprintf("uda.%s.%s", uda, u)]; ok { - styles[i] = style.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding()) - continue + if u, ok := task.Udas[uda.Name]; ok { + if c, ok := m.common.Styles.Colors[fmt.Sprintf("uda.%s.%s", uda.Name, u)]; ok { + styles[i] = c.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding()) + continue taskstyle } } } diff --git a/go.mod b/go.mod index f716149..af473f5 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.22.2 require ( github.com/charmbracelet/bubbles v0.18.0 github.com/charmbracelet/bubbletea v0.26.1 + github.com/charmbracelet/glamour v0.7.0 github.com/charmbracelet/huh v0.3.0 github.com/charmbracelet/lipgloss v0.10.1-0.20240506202754-3ee5dcab73cb github.com/mattn/go-runewidth v0.0.15 @@ -12,20 +13,29 @@ require ( ) require ( + github.com/alecthomas/chroma/v2 v2.8.0 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/aymerick/douceur v0.2.0 // indirect github.com/catppuccin/go v0.2.0 // indirect github.com/charmbracelet/x/exp/term v0.0.0-20240506152644-8135bef4e495 // indirect + github.com/dlclark/regexp2 v1.4.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/gorilla/css v1.0.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect + github.com/microcosm-cc/bluemonday v1.0.25 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.2 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect + github.com/yuin/goldmark v1.5.4 // indirect + github.com/yuin/goldmark-emoji v1.0.2 // indirect + golang.org/x/net v0.17.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.20.0 // indirect golang.org/x/text v0.15.0 // indirect diff --git a/go.sum b/go.sum index 22a7ee2..02d4238 100644 --- a/go.sum +++ b/go.sum @@ -1,21 +1,37 @@ +github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink= +github.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= +github.com/alecthomas/chroma/v2 v2.8.0 h1:w9WJUjFFmHHB2e8mRpL9jjy3alYDlU0QLDezj1xE264= +github.com/alecthomas/chroma/v2 v2.8.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw= +github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= +github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 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/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= github.com/charmbracelet/bubbletea v0.26.1 h1:xujcQeF73rh4jwu3+zhfQsvV18x+7zIjlw7/CYbzGJ0= github.com/charmbracelet/bubbletea v0.26.1/go.mod h1:FzKr7sKoO8iFVcdIBM9J0sJOcQv5nDQaYwsee3kpbgo= +github.com/charmbracelet/glamour v0.7.0 h1:2BtKGZ4iVJCDfMF229EzbeR1QRKLWztO9dMtjmqZSng= +github.com/charmbracelet/glamour v0.7.0/go.mod h1:jUMh5MeihljJPQbJ/wf4ldw2+yBP59+ctV36jASy7ps= github.com/charmbracelet/huh v0.3.0 h1:CxPplWkgW2yUTDDG0Z4S5HH8SJOosWHd4LxCvi0XsKE= github.com/charmbracelet/huh v0.3.0/go.mod h1:fujUdKX8tC45CCSaRQdw789O6uaCRwx8l2NDyKfC4jA= github.com/charmbracelet/lipgloss v0.10.1-0.20240506202754-3ee5dcab73cb h1:Hs3xzxHuruNT2Iuo87iS40c0PhLqpnUKBI6Xw6Ad3wQ= github.com/charmbracelet/lipgloss v0.10.1-0.20240506202754-3ee5dcab73cb/go.mod h1:EPP2QJ0ectp3zo6gx9f8oJGq8keirqPJ3XpYEI8wrrs= github.com/charmbracelet/x/exp/term v0.0.0-20240506152644-8135bef4e495 h1:+0U9qX8Pv8KiYgRxfBvORRjgBzLgHMjtElP4O0PyKYA= github.com/charmbracelet/x/exp/term v0.0.0-20240506152644-8135bef4e495/go.mod h1:qeR6w1zITbkF7vEhcx0CqX5GfnIiQloJWQghN6HfP+c= +github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= +github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= 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/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 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= @@ -24,9 +40,12 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg= +github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= @@ -35,12 +54,21 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 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-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y= github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU= +github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s= +github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/pages/messaging.go b/pages/messaging.go index f7165a1..fb705ae 100644 --- a/pages/messaging.go +++ b/pages/messaging.go @@ -52,9 +52,9 @@ func prevArea() tea.Cmd { } } -type changeAreaMsg area +type changeAreaMsg int -func changeArea(a area) tea.Cmd { +func changeArea(a int) tea.Cmd { return func() tea.Msg { return changeAreaMsg(a) } diff --git a/pages/report.go b/pages/report.go index b18dfb0..5471c8d 100644 --- a/pages/report.go +++ b/pages/report.go @@ -7,6 +7,7 @@ import ( "tasksquire/taskwarrior" "github.com/charmbracelet/bubbles/key" + // "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" ) @@ -24,7 +25,7 @@ type ReportPage struct { taskTable table.Model - subpage tea.Model + subpage common.Component } func NewReportPage(com *common.Common, report *taskwarrior.Report) *ReportPage { @@ -54,8 +55,8 @@ func NewReportPage(com *common.Common, report *taskwarrior.Report) *ReportPage { func (p *ReportPage) SetSize(width int, height int) { p.common.SetSize(width, height) - p.taskTable.SetWidth(width - p.common.Styles.Base.GetVerticalFrameSize()) - p.taskTable.SetHeight(height - p.common.Styles.Base.GetHorizontalFrameSize()) + p.taskTable.SetWidth(width - p.common.Styles.Base.GetHorizontalFrameSize()) + p.taskTable.SetHeight(height - p.common.Styles.Base.GetVerticalFrameSize()) } func (p *ReportPage) Init() tea.Cmd { diff --git a/pages/taskEditor.go b/pages/taskEditor.go index ebdf78d..5581556 100644 --- a/pages/taskEditor.go +++ b/pages/taskEditor.go @@ -5,12 +5,15 @@ import ( "log/slog" "strings" "tasksquire/common" + "time" "tasksquire/taskwarrior" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" + "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/glamour" "github.com/charmbracelet/huh" "github.com/charmbracelet/lipgloss" ) @@ -26,13 +29,16 @@ type TaskEditorPage struct { common *common.Common task taskwarrior.Task + colWidth int + colHeight int + mode mode columnCursor int - area area + area int areaPicker *areaPicker - areas map[area]tea.Model + areas []area } func NewTaskEditorPage(com *common.Common, task taskwarrior.Task) *TaskEditorPage { @@ -41,21 +47,17 @@ func NewTaskEditorPage(com *common.Common, task taskwarrior.Task) *TaskEditorPag task: task, } - if p.task.Priority == "" { - p.task.Priority = "(none)" - } if p.task.Project == "" { p.task.Project = "(none)" } - priorityOptions := append([]string{"(none)"}, p.common.TW.GetPriorities()...) - projectOptions := append([]string{"(none)"}, p.common.TW.GetProjects()...) tagOptions := p.common.TW.GetTags() - p.areas = map[area]tea.Model{ - areaTask: NewTaskEdit(p.common, &p.task.Description, &p.task.Priority, &p.task.Project, priorityOptions, projectOptions), - areaTags: NewTagEdit(p.common, &p.task.Tags, tagOptions), - areaTime: NewTimeEdit(p.common, &p.task.Due, &p.task.Scheduled, &p.task.Wait, &p.task.Until), + p.areas = []area{ + NewTaskEdit(p.common, &p.task), + NewTagEdit(p.common, &p.task.Tags, tagOptions), + NewTimeEdit(p.common, &p.task.Due, &p.task.Scheduled, &p.task.Wait, &p.task.Until), + NewDetailsEdit(p.common, &p.task), } // p.areaList = NewAreaList(common, areaItems) @@ -66,17 +68,30 @@ func NewTaskEditorPage(com *common.Common, task taskwarrior.Task) *TaskEditorPag p.columnCursor = 1 if p.task.Uuid == "" { - // p.mode = modeInsert p.mode = modeInsert } else { p.mode = modeNormal } + p.SetSize(com.Width(), com.Height()) + return &p } func (p *TaskEditorPage) SetSize(width, height int) { p.common.SetSize(width, height) + + if width >= 70 { + p.colWidth = 70 - p.common.Styles.ColumnFocused.GetVerticalFrameSize() + } else { + p.colWidth = width - p.common.Styles.ColumnFocused.GetVerticalFrameSize() + } + + if height >= 40 { + p.colHeight = 40 - p.common.Styles.ColumnFocused.GetVerticalFrameSize() + } else { + p.colHeight = height - p.common.Styles.ColumnFocused.GetVerticalFrameSize() + } } func (p *TaskEditorPage) Init() tea.Cmd { @@ -85,8 +100,10 @@ func (p *TaskEditorPage) Init() tea.Cmd { func (p *TaskEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { + case tea.WindowSizeMsg: + p.SetSize(msg.Width, msg.Height) case changeAreaMsg: - p.area = area(msg) + p.area = int(msg) case changeModeMsg: p.mode = mode(msg) case prevColumnMsg: @@ -102,13 +119,15 @@ func (p *TaskEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case prevAreaMsg: p.area-- if p.area < 0 { - p.area = 2 + p.area = len(p.areas) - 1 } + p.areas[p.area].SetCursor(-1) case nextAreaMsg: p.area++ - if p.area > 2 { + if p.area > len(p.areas)-1 { p.area = 0 } + p.areas[p.area].SetCursor(0) } switch p.mode { @@ -137,23 +156,23 @@ func (p *TaskEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case key.Matches(msg, p.common.Keymap.Right): return p, nextColumn() case key.Matches(msg, p.common.Keymap.Up): - var cmd tea.Cmd if p.columnCursor == 0 { picker, cmd := p.areaPicker.Update(msg) p.areaPicker = picker.(*areaPicker) return p, cmd } else { - p.areas[p.area], cmd = p.areas[p.area].Update(prevFieldMsg{}) + model, cmd := p.areas[p.area].Update(prevFieldMsg{}) + p.areas[p.area] = model.(area) return p, cmd } case key.Matches(msg, p.common.Keymap.Down): - var cmd tea.Cmd if p.columnCursor == 0 { picker, cmd := p.areaPicker.Update(msg) p.areaPicker = picker.(*areaPicker) return p, cmd } else { - p.areas[p.area], cmd = p.areas[p.area].Update(nextFieldMsg{}) + model, cmd := p.areas[p.area].Update(nextFieldMsg{}) + p.areas[p.area] = model.(area) return p, cmd } } @@ -175,39 +194,39 @@ func (p *TaskEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case key.Matches(msg, p.common.Keymap.Back): return p, changeMode(modeNormal) case key.Matches(msg, p.common.Keymap.Prev): - var cmd tea.Cmd if p.columnCursor == 0 { picker, cmd := p.areaPicker.Update(msg) p.areaPicker = picker.(*areaPicker) return p, cmd } else { - p.areas[p.area], cmd = p.areas[p.area].Update(prevFieldMsg{}) + model, cmd := p.areas[p.area].Update(prevFieldMsg{}) + p.areas[p.area] = model.(area) return p, cmd } case key.Matches(msg, p.common.Keymap.Next): - var cmd tea.Cmd if p.columnCursor == 0 { picker, cmd := p.areaPicker.Update(msg) p.areaPicker = picker.(*areaPicker) return p, cmd } else { - p.areas[p.area], cmd = p.areas[p.area].Update(nextFieldMsg{}) + model, cmd := p.areas[p.area].Update(nextFieldMsg{}) + p.areas[p.area] = model.(area) return p, cmd } case key.Matches(msg, p.common.Keymap.Ok): - area, cmd := p.areas[p.area].Update(msg) - p.areas[p.area] = area + model, cmd := p.areas[p.area].Update(msg) + p.areas[p.area] = model.(area) return p, tea.Batch(cmd, nextField()) } } - var cmd tea.Cmd if p.columnCursor == 0 { picker, cmd := p.areaPicker.Update(msg) p.areaPicker = picker.(*areaPicker) return p, cmd } else { - p.areas[p.area], cmd = p.areas[p.area].Update(msg) + model, cmd := p.areas[p.area].Update(msg) + p.areas[p.area] = model.(area) return p, cmd } } @@ -215,143 +234,45 @@ func (p *TaskEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (p *TaskEditorPage) View() string { - var focusStyle lipgloss.Style + var focusedStyle, blurredStyle lipgloss.Style if p.mode == modeInsert { - focusStyle = p.common.Styles.ColumnInsert + focusedStyle = p.common.Styles.ColumnInsert.Width(p.colWidth).Height(p.colHeight) } else { - focusStyle = p.common.Styles.ColumnFocused + focusedStyle = p.common.Styles.ColumnFocused.Width(p.colWidth).Height(p.colHeight) } - var picker, area string + blurredStyle = p.common.Styles.ColumnBlurred.Width(p.colWidth).Height(p.colHeight) + // var picker, area string + var area string if p.columnCursor == 0 { - picker = focusStyle.Render(p.areaPicker.View()) - area = p.common.Styles.ColumnBlurred.Render(p.areas[p.area].View()) + // picker = focusedStyle.Render(p.areaPicker.View()) + area = blurredStyle.Render(p.areas[p.area].View()) } else { - picker = p.common.Styles.ColumnBlurred.Render(p.areaPicker.View()) - area = focusStyle.Render(p.areas[p.area].View()) + // picker = blurredStyle.Render(p.areaPicker.View()) + area = focusedStyle.Render(p.areas[p.area].View()) } - return lipgloss.JoinHorizontal( - lipgloss.Center, - picker, - area, - ) + if p.task.Uuid != "" { + area = lipgloss.JoinHorizontal( + lipgloss.Top, + area, + p.common.Styles.ColumnFocused.Render(p.common.TW.GetInformation(&p.task)), + ) + + } + + // return lipgloss.Place(p.common.Width(), p.common.Height(), lipgloss.Center, lipgloss.Center, lipgloss.JoinHorizontal( + // lipgloss.Center, + // picker, + // area, + // )) + return lipgloss.Place(p.common.Width(), p.common.Height(), lipgloss.Center, lipgloss.Center, area) } -// import ( -// "fmt" -// "io" -// "log/slog" -// "strings" -// "tasksquire/common" -// "tasksquire/taskwarrior" -// "time" - -// "github.com/charmbracelet/bubbles/list" -// "github.com/charmbracelet/bubbles/textinput" -// tea "github.com/charmbracelet/bubbletea" -// "github.com/charmbracelet/huh" -// "github.com/charmbracelet/lipgloss" -// ) - -// type Field int - -// const ( -// FieldDescription Field = iota -// FieldPriority -// FieldProject -// FieldNewProject -// FieldTags -// FieldNewTags -// FieldDue -// FieldScheduled -// FieldWait -// FieldUntil -// ) - -// type column int - -// const ( -// column1 column = iota -// column2 -// column3 -// ) - -// func changeColumn(c column) tea.Cmd { -// return func() tea.Msg { -// return changeColumnMsg(c) -// } -// } - -// type changeColumnMsg column - -// type mode int - -// const ( -// modeNormal mode = iota -// modeInsert -// modeAddTag -// modeAddProject -// ) - -// type TaskEditorPage struct { -// common *common.Common -// task taskwarrior.Task -// areaList tea.Model -// mode mode -// statusline tea.Model - -// // TODO: rework support for adding tags and projects -// additionalTags string -// additionalProject string - -// columnCursor int -// columns []tea.Model - -// areas map[area][]tea.Model -// selectedArea area -// } - -// type TaskEditorKeys struct { -// Quit key.Binding -// Up key.Binding -// Down key.Binding -// Select key.Binding -// ToggleFocus key.Binding -// } - -// func NewTaskEditorPage(common *common.Common, task taskwarrior.Task) *TaskEditorPage { -// p := &TaskEditorPage{ -// common: common, -// task: task, -// } - -// if p.task.Uuid == "" { -// p.mode = modeInsert -// } else { -// p.mode = modeNormal -// } - -// areaItems := []list.Item{ -// item("Task"), -// item("Tags"), -// item("Time"), -// } - -// } - -// p.statusline = NewStatusLine(common, p.mode) - -// return p -// } - -type area int - -const ( - areaTask area = iota - areaTags - areaTime -) +type area interface { + tea.Model + SetCursor(c int) +} type areaPicker struct { common *common.Common @@ -383,17 +304,18 @@ func NewAreaPicker(common *common.Common, items []string) *areaPicker { } } -func (a *areaPicker) Area() area { - switch a.list.SelectedItem() { - case item("Task"): - return areaTask - case item("Tags"): - return areaTags - case item("Dates"): - return areaTime - default: - return areaTask - } +func (a *areaPicker) Area() int { + // switch a.list.SelectedItem() { + // case item("Task"): + // return areaTask + // case item("Tags"): + // return areaTags + // case item("Dates"): + // return areaTime + // default: + // return areaTask + // } + return 0 } func (a *areaPicker) Init() tea.Cmd { @@ -429,50 +351,138 @@ type taskEdit struct { cursor int newProjectName *string + newAnnotation *string + udaValues map[string]*string } -func NewTaskEdit(common *common.Common, description *string, priority *string, project *string, priorityOptions []string, projectOptions []string) *taskEdit { +func NewTaskEdit(com *common.Common, task *taskwarrior.Task) *taskEdit { newProject := "" + projectOptions := append([]string{"(none)"}, com.TW.GetProjects()...) + if task.Project == "" { + task.Project = "(none)" + } defaultKeymap := huh.NewDefaultKeyMap() - t := taskEdit{ - common: common, - fields: []huh.Field{ - huh.NewInput(). - Title("Task"). - Value(description). - Validate(func(desc string) error { - if desc == "" { - return fmt.Errorf("task description is required") - } - return nil - }). + fields := []huh.Field{ + huh.NewInput(). + Title("Task"). + Value(&task.Description). + Validate(func(desc string) error { + if desc == "" { + return fmt.Errorf("task description is required") + } + return nil + }). + Inline(true). + Prompt(": "). + WithTheme(com.Styles.Form), + + huh.NewSelect[string](). + Options(huh.NewOptions(projectOptions...)...). + Title("Project"). + Value(&task.Project). + WithKeyMap(defaultKeymap). + WithTheme(com.Styles.Form), + + huh.NewInput(). + Title("New Project"). + Value(&newProject). + Inline(true). + Prompt(": "). + WithTheme(com.Styles.Form), + } + + udaValues := make(map[string]*string) + for _, uda := range com.Udas { + switch uda.Type { + case taskwarrior.UdaTypeNumeric: + val := "" + udaValues[uda.Name] = &val + fields = append(fields, huh.NewInput(). + Title(uda.Label). + Value(udaValues[uda.Name]). + Validate(taskwarrior.ValidateNumeric). Inline(true). - WithTheme(common.Styles.Form), + Prompt(": "). + WithTheme(com.Styles.Form)) + case taskwarrior.UdaTypeString: + if len(uda.Values) > 0 { + var val string + values := make([]string, len(uda.Values)) + for i, v := range uda.Values { + values[i] = v + if v == "" { + values[i] = "(none)" + } + if v == uda.Default { + val = values[i] + } + } + if val == "" { + val = values[0] + } + if v, ok := task.Udas[uda.Name]; ok { + //TODO: handle uda types correctly + val = v.(string) + } + udaValues[uda.Name] = &val - huh.NewSelect[string](). - Options(huh.NewOptions(priorityOptions...)...). - Title("Priority"). - Key("priority"). - Value(priority). - WithKeyMap(defaultKeymap). - WithTheme(common.Styles.Form), + fields = append(fields, huh.NewSelect[string](). + Options(huh.NewOptions(values...)...). + Title(uda.Label). + Value(udaValues[uda.Name]). + WithKeyMap(defaultKeymap). + WithTheme(com.Styles.Form)) + } else { + val := "" + udaValues[uda.Name] = &val + fields = append(fields, huh.NewInput(). + Title(uda.Label). + Value(udaValues[uda.Name]). + Inline(true). + Prompt(": "). + WithTheme(com.Styles.Form)) + } + case taskwarrior.UdaTypeDate: + val := "" + udaValues[uda.Name] = &val + fields = append(fields, huh.NewInput(). + Title(uda.Label). + Value(udaValues[uda.Name]). + Validate(taskwarrior.ValidateDate). + Inline(true). + Prompt(": "). + WithTheme(com.Styles.Form)) + case taskwarrior.UdaTypeDuration: + val := "" + udaValues[uda.Name] = &val + fields = append(fields, huh.NewInput(). + Title(uda.Label). + Value(udaValues[uda.Name]). + Validate(taskwarrior.ValidateDuration). + Inline(true). + Prompt(": "). + WithTheme(com.Styles.Form)) + } + } - huh.NewSelect[string](). - Options(huh.NewOptions(projectOptions...)...). - Title("Project"). - Value(project). - WithKeyMap(defaultKeymap). - WithTheme(common.Styles.Form), + newAnnotation := "" + fields = append(fields, huh.NewInput(). + Title("New Annotation"). + Value(&newAnnotation). + Inline(true). + Prompt(": "). + WithTheme(com.Styles.Form)) - huh.NewInput(). - Title("New Project"). - Value(&newProject). - WithTheme(common.Styles.Form), - }, + t := taskEdit{ + common: com, + fields: fields, + + udaValues: udaValues, newProjectName: &newProject, + newAnnotation: &newAnnotation, } t.fields[0].Focus() @@ -480,6 +490,16 @@ func NewTaskEdit(common *common.Common, description *string, priority *string, p return &t } +func (t *taskEdit) SetCursor(c int) { + t.fields[t.cursor].Blur() + if c < 0 { + t.cursor = len(t.fields) - 1 + } else { + t.cursor = c + } + t.fields[t.cursor].Focus() +} + func (t *taskEdit) Init() tea.Cmd { return nil } @@ -515,6 +535,9 @@ func (t *taskEdit) View() string { views := make([]string, len(t.fields)) for i, field := range t.fields { views[i] = field.View() + if i < len(t.fields)-1 { + views[i] += "\n" + } } return lipgloss.JoinVertical( lipgloss.Left, @@ -551,16 +574,25 @@ func NewTagEdit(common *common.Common, selected *[]string, options []string) *ta Title("New Tags"). Value(&newTags). Inline(true). + Prompt(": "). WithTheme(common.Styles.Form), }, newTagsValue: &newTags, } - t.fields[0].Focus() - return &t } +func (t *tagEdit) SetCursor(c int) { + t.fields[t.cursor].Blur() + if c < 0 { + t.cursor = len(t.fields) - 1 + } else { + t.cursor = c + } + t.fields[t.cursor].Focus() +} + func (t *tagEdit) Init() tea.Cmd { return nil } @@ -619,33 +651,45 @@ func NewTimeEdit(common *common.Common, due *string, scheduled *string, wait *st Value(due). Validate(taskwarrior.ValidateDate). Inline(true). + Prompt(": "). WithTheme(common.Styles.Form), huh.NewInput(). Title("Scheduled"). Value(scheduled). Validate(taskwarrior.ValidateDate). Inline(true). + Prompt(": "). WithTheme(common.Styles.Form), huh.NewInput(). Title("Wait"). Value(wait). Validate(taskwarrior.ValidateDate). Inline(true). + Prompt(": "). WithTheme(common.Styles.Form), huh.NewInput(). Title("Until"). Value(until). Validate(taskwarrior.ValidateDate). Inline(true). + Prompt(": "). WithTheme(common.Styles.Form), }, } - t.fields[0].Focus() - return &t } +func (t *timeEdit) SetCursor(c int) { + t.fields[t.cursor].Blur() + if c < 0 { + t.cursor = len(t.fields) - 1 + } else { + t.cursor = c + } + t.fields[t.cursor].Focus() +} + func (t *timeEdit) Init() tea.Cmd { return nil } @@ -687,6 +731,75 @@ func (t *timeEdit) View() string { ) } +type detailsEdit struct { + com *common.Common + renderer *glamour.TermRenderer + vp viewport.Model +} + +func NewDetailsEdit(com *common.Common, task *taskwarrior.Task) *detailsEdit { + renderer, err := glamour.NewTermRenderer( + // glamour.WithStandardStyle("light"), + glamour.WithAutoStyle(), + glamour.WithWordWrap(40), + ) + if err != nil { + slog.Error(err.Error()) + return nil + } + + vp := viewport.New(40, 30) + d := detailsEdit{ + com: com, + renderer: renderer, + vp: vp, + } + + return &d +} + +func (d *detailsEdit) SetCursor(c int) { +} + +func (d *detailsEdit) Init() tea.Cmd { + return nil +} + +func (d *detailsEdit) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg.(type) { + case nextFieldMsg: + return d, nextArea() + case prevFieldMsg: + return d, prevArea() + default: + var cmd tea.Cmd + d.vp, cmd = d.vp.Update(msg) + return d, cmd + } +} + +func (d *detailsEdit) View() string { + dtls := ` + # Cool Details! + ## Things I need + - [ ] A thing + - [x] Done thing + + ## People + - pe1 + - pe2 + ` + + details, err := d.renderer.Render(dtls) + if err != nil { + slog.Error(err.Error()) + return "Could not parse markdown" + } + + d.vp.SetContent(details) + return d.vp.View() +} + // func (p *TaskEditorPage) SetSize(width, height int) { // p.common.SetSize(width, height) // } @@ -813,26 +926,36 @@ func (p *TaskEditorPage) updateTasksCmd() tea.Msg { if p.task.Project == "(none)" { p.task.Project = "" } - if p.task.Priority == "(none)" { - p.task.Priority = "" + + for _, uda := range p.common.Udas { + if val, ok := p.areas[0].(*taskEdit).udaValues[uda.Name]; ok { + if *val == "(none)" { + *val = "" + } + p.task.Udas[uda.Name] = *val + } } - if *(p.areas[areaTask].(*taskEdit).newProjectName) != "" { - p.task.Project = *p.areas[areaTask].(*taskEdit).newProjectName + if *(p.areas[0].(*taskEdit).newProjectName) != "" { + p.task.Project = *p.areas[0].(*taskEdit).newProjectName } - if *(p.areas[areaTags].(*tagEdit).newTagsValue) != "" { - newTags := strings.Split(*p.areas[areaTags].(*tagEdit).newTagsValue, " ") + if *(p.areas[1].(*tagEdit).newTagsValue) != "" { + newTags := strings.Split(*p.areas[1].(*tagEdit).newTagsValue, " ") if len(newTags) > 0 { p.task.Tags = append(p.task.Tags, newTags...) } } - // if p.additionalProject != "" { - // p.task.Project = p.additionalProject - // } - // tags := p.form.Get("tags").([]string) - // p.task.Tags = tags + if *(p.areas[0].(*taskEdit).newAnnotation) != "" { + p.task.Annotations = append(p.task.Annotations, taskwarrior.Annotation{ + Entry: time.Now().Format("20060102T150405Z"), + Description: *(p.areas[0].(*taskEdit).newAnnotation), + }) + + // p.common.TW.AddTaskAnnotation(p.task.Uuid, *p.areas[0].(*taskEdit).newAnnotation) + } + p.common.TW.ImportTask(&p.task) return UpdatedTasksMsg{} } diff --git a/taskwarrior/models.go b/taskwarrior/models.go index 0c54433..7efd877 100644 --- a/taskwarrior/models.go +++ b/taskwarrior/models.go @@ -1,6 +1,7 @@ package taskwarrior import ( + "encoding/json" "fmt" "log/slog" "math" @@ -13,6 +14,23 @@ const ( dtformat = "20060102T150405Z" ) +type UdaType string + +const ( + UdaTypeString UdaType = "string" + UdaTypeDate UdaType = "date" + UdaTypeNumeric UdaType = "numeric" + UdaTypeDuration UdaType = "duration" +) + +type Uda struct { + Name string + Type UdaType + Label string + Values []string + Default string +} + type Annotation struct { Entry string `json:"entry,omitempty"` Description string `json:"description,omitempty"` @@ -22,12 +40,14 @@ func (a Annotation) String() string { return fmt.Sprintf("%s %s", a.Entry, a.Description) } +type Tasks []*Task + type Task struct { - Id int64 `json:"id,omitempty"` - Uuid string `json:"uuid,omitempty"` - Description string `json:"description,omitempty"` - Project string `json:"project"` - Priority string `json:"priority"` + Id int64 `json:"id,omitempty"` + Uuid string `json:"uuid,omitempty"` + Description string `json:"description,omitempty"` + Project string `json:"project"` + // Priority string `json:"priority"` Status string `json:"status,omitempty"` Tags []string `json:"tags"` VirtualTags []string `json:"-"` @@ -114,8 +134,8 @@ func (t *Task) GetString(fieldWFormat string) string { } return t.Project - case "priority": - return t.Priority + // case "priority": + // return t.Priority case "status": return t.Status @@ -186,6 +206,7 @@ func (t *Task) GetString(fieldWFormat string) string { return t.Recur default: + // TODO: format according to UDA type if val, ok := t.Udas[field]; ok { if strVal, ok := val.(string); ok { return strVal @@ -230,7 +251,69 @@ func (t *Task) RemoveTag(tag string) { } } -type Tasks []*Task +func (t *Task) UnmarshalJSON(data []byte) error { + type Alias Task + task := Alias{} + + if err := json.Unmarshal(data, &task); err != nil { + return err + } + + *t = Task(task) + + m := make(map[string]any) + + if err := json.Unmarshal(data, &m); err != nil { + return err + } + + delete(m, "id") + delete(m, "uuid") + delete(m, "description") + delete(m, "project") + // delete(m, "priority") + delete(m, "status") + delete(m, "tags") + delete(m, "depends") + delete(m, "urgency") + delete(m, "parent") + delete(m, "due") + delete(m, "wait") + delete(m, "scheduled") + delete(m, "until") + delete(m, "start") + delete(m, "end") + delete(m, "entry") + delete(m, "modified") + delete(m, "recur") + delete(m, "annotations") + t.Udas = m + + return nil +} + +func (t *Task) MarshalJSON() ([]byte, error) { + type Alias Task + task := Alias(*t) + + knownFields, err := json.Marshal(task) + if err != nil { + return nil, err + } + + var knownMap map[string]any + if err := json.Unmarshal(knownFields, &knownMap); err != nil { + return nil, err + } + + for key, value := range t.Udas { + if value != nil && value != "" { + knownMap[key] = value + } + } + + return json.Marshal(knownMap) +} type Context struct { Name string @@ -418,3 +501,15 @@ func ValidateDate(s string) error { return fmt.Errorf("invalid date") } + +func ValidateNumeric(s string) error { + if _, err := strconv.ParseFloat(s, 64); err != nil { + return fmt.Errorf("invalid number") + } + return nil +} + +func ValidateDuration(s string) error { + // TODO: implement duration validation + return nil +} diff --git a/taskwarrior/taskwarrior.go b/taskwarrior/taskwarrior.go index 196f0f7..f908f35 100644 --- a/taskwarrior/taskwarrior.go +++ b/taskwarrior/taskwarrior.go @@ -89,15 +89,17 @@ type TaskWarrior interface { GetReport(report string) *Report GetReports() Reports - GetUdas() []string + GetUdas() []Uda GetTasks(report *Report, filter ...string) Tasks - AddTask(task *Task) error + // AddTask(task *Task) error ImportTask(task *Task) SetTaskDone(task *Task) DeleteTask(task *Task) StartTask(task *Task) StopTask(task *Task) + GetInformation(task *Task) string + AddTaskAnnotation(uuid string, annotation string) Undo() } @@ -171,15 +173,7 @@ func (ts *TaskSquire) GetTasks(report *Report, filter ...string) Tasks { return nil } - unstructuredTasks := make([]map[string]any, 0) - err = json.Unmarshal(output, &unstructuredTasks) - if err != nil { - slog.Error("Failed unmarshalling tasks:", err) - return nil - } - - for i, task := range tasks { - task.Udas = unstructuredTasks[i] + for _, task := range tasks { if task.Depends != nil && len(task.Depends) > 0 { ids := make([]string, len(task.Depends)) for i, dependUuid := range task.Depends { @@ -325,7 +319,7 @@ func (ts *TaskSquire) GetReports() Reports { return ts.reports } -func (ts *TaskSquire) GetUdas() []string { +func (ts *TaskSquire) GetUdas() []Uda { ts.mutex.Lock() defer ts.mutex.Unlock() @@ -336,9 +330,27 @@ func (ts *TaskSquire) GetUdas() []string { return nil } - udas := make([]string, 0) + udas := make([]Uda, 0) for _, uda := range strings.Split(string(output), "\n") { if uda != "" { + udatype := UdaType(ts.config.Get(fmt.Sprintf("uda.%s.type", uda))) + if udatype == "" { + slog.Error(fmt.Sprintf("UDA type not found: %s", uda)) + continue + } + + label := ts.config.Get(fmt.Sprintf("uda.%s.label", uda)) + values := strings.Split(ts.config.Get(fmt.Sprintf("uda.%s.values", uda)), ",") + def := ts.config.Get(fmt.Sprintf("uda.%s.default", uda)) + + uda := Uda{ + Name: uda, + Label: label, + Type: udatype, + Values: values, + Default: def, + } + udas = append(udas, uda) } } @@ -367,42 +379,42 @@ func (ts *TaskSquire) SetContext(context *Context) error { return nil } -func (ts *TaskSquire) AddTask(task *Task) error { - ts.mutex.Lock() - defer ts.mutex.Unlock() +// func (ts *TaskSquire) AddTask(task *Task) error { +// ts.mutex.Lock() +// defer ts.mutex.Unlock() - addArgs := []string{"add"} +// addArgs := []string{"add"} - if task.Description == "" { - slog.Error("Task description is required") - return nil - } else { - addArgs = append(addArgs, task.Description) - } - if task.Priority != "" && task.Priority != "(none)" { - addArgs = append(addArgs, fmt.Sprintf("priority:%s", task.Priority)) - } - if task.Project != "" && task.Project != "(none)" { - addArgs = append(addArgs, fmt.Sprintf("project:%s", task.Project)) - } - if task.Tags != nil { - for _, tag := range task.Tags { - addArgs = append(addArgs, fmt.Sprintf("+%s", tag)) - } - } - if task.Due != "" { - addArgs = append(addArgs, fmt.Sprintf("due:%s", task.Due)) - } +// if task.Description == "" { +// slog.Error("Task description is required") +// return nil +// } else { +// addArgs = append(addArgs, task.Description) +// } +// if task.Priority != "" && task.Priority != "(none)" { +// addArgs = append(addArgs, fmt.Sprintf("priority:%s", task.Priority)) +// } +// if task.Project != "" && task.Project != "(none)" { +// addArgs = append(addArgs, fmt.Sprintf("project:%s", task.Project)) +// } +// if task.Tags != nil { +// for _, tag := range task.Tags { +// addArgs = append(addArgs, fmt.Sprintf("+%s", tag)) +// } +// } +// if task.Due != "" { +// addArgs = append(addArgs, fmt.Sprintf("due:%s", task.Due)) +// } - cmd := exec.Command(twBinary, append(ts.defaultArgs, addArgs...)...) - err := cmd.Run() - if err != nil { - slog.Error("Failed adding task:", err) - } +// cmd := exec.Command(twBinary, append(ts.defaultArgs, addArgs...)...) +// err := cmd.Run() +// if err != nil { +// slog.Error("Failed adding task:", err) +// } - // TODO remove error? - return nil -} +// // TODO remove error? +// return nil +// } // TODO error handling func (ts *TaskSquire) ImportTask(task *Task) { @@ -410,7 +422,6 @@ func (ts *TaskSquire) ImportTask(task *Task) { defer ts.mutex.Unlock() tasks, err := json.Marshal(Tasks{task}) - if err != nil { slog.Error("Failed marshalling task:", err) } @@ -479,6 +490,31 @@ func (ts *TaskSquire) StopTask(task *Task) { } } +func (ts *TaskSquire) GetInformation(task *Task) string { + ts.mutex.Lock() + defer ts.mutex.Unlock() + + cmd := exec.Command(twBinary, append(ts.defaultArgs, []string{fmt.Sprintf("uuid:%s", task.Uuid), "information"}...)...) + output, err := cmd.CombinedOutput() + if err != nil { + slog.Error("Failed getting task information:", err) + return "" + } + + return string(output) +} + +func (ts *TaskSquire) AddTaskAnnotation(uuid string, annotation string) { + ts.mutex.Lock() + defer ts.mutex.Unlock() + + cmd := exec.Command(twBinary, append(ts.defaultArgs, []string{uuid, "annotate", annotation}...)...) + err := cmd.Run() + if err != nil { + slog.Error("Failed adding annotation:", err) + } +} + func (ts *TaskSquire) extractConfig() *TWConfig { cmd := exec.Command(twBinary, append(ts.defaultArgs, []string{"_show"}...)...) output, err := cmd.CombinedOutput() diff --git a/test/taskchampion.sqlite3 b/test/taskchampion.sqlite3 index f53a80188384efbc0a177db37399207f40044e45..8cd282e94ee5c3e8a908cbc72d375d4f4c9a7660 100644 GIT binary patch delta 14997 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 F{|C7`bbSB- delta 1275 zcmX|xjUV0w==V4%i4#qtxHQwExW}wrnOLBQ66erTLgJ@fzpU*TGXZ? zm~Lm=-EEBtGu+5ECXzOi6hkTt6LJv)*)M8Hv?&RRA56?P;gk4S5~7jB>Y4rEeE8*@ zGxz-Of6keSTZ7`j#=|`Gjii} z8S>fmD6%Pi5V;|}8QGYwMK+{8$n~j{$of<_vM$wvT$d_GhLdNI&m<2aL&>ekwaGAY zO|l62^witP>Zw6-AJMU)&y6uZ8?pZ~kofQ{E9G^B$51S?Xl}Wi!kiQGzqsL*k=2RT|oQtQ1sH|!?ouZLJjcprW2VS;id_7fVsoxd$)EBuC z>U!!GH!|*El6a9R78Nwn^PCg>yH<<){^}gehA@#~7FBX5` z&8R)fxF@4;po{Wjt%uA*qtsjUPU)EMvTZi1W?!loMNYUg7u~DKC&1o3+#upU`6hVk z@y$th2jcr6u|PyC$!*}<%g8dciC?QofT}qCo&THQXrh9eds0LEqCpz@N|Eg45B;Q= ze^xN25OtjZ diff --git a/test/taskrc b/test/taskrc index 1d62a28..eca1d95 100644 --- a/test/taskrc +++ b/test/taskrc @@ -1,13 +1,25 @@ include light-256.theme +uda.priority.values=H,M,,L + context.test.read=+test context.test.write=+test context.home.read=+home context.home.write=+home uda.testuda.type=string -uda.testuda.label=testuda +uda.testuda.label=Testuda uda.testuda.values=eins,zwei,drei +uda.testuda.default=eins + +uda.testuda2.type=numeric +uda.testuda2.label=TESTUDA2 + +uda.testuda3.type=date +uda.testuda3.label=Ttttuda + +uda.testuda4.type=duration +uda.testuda4.label=TtttudaDURUD report.next.columns=id,testuda,start.age,entry.age,depends,priority,project,tags,recur,scheduled.countdown,due.relative,until.remaining,description,urgency report.next.context=1