Handle UDAs for editing; Fix layout; Add annotations

This commit is contained in:
Martin
2024-06-09 17:55:56 +02:00
parent 3e1cb9d1bc
commit bafd8958d4
12 changed files with 663 additions and 476 deletions

View File

@ -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
}

View File

@ -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 {

View File

@ -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
}
}
}

10
go.mod
View File

@ -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

28
go.sum
View File

@ -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=

View File

@ -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)
}

View File

@ -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 {

View File

@ -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{}
}

View File

@ -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
}

View File

@ -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()

Binary file not shown.

View File

@ -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