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 TW taskwarrior.TaskWarrior
Keymap *Keymap Keymap *Keymap
Styles *Styles Styles *Styles
Udas []string Udas []taskwarrior.Uda
pageStack *Stack[Component] pageStack *Stack[Component]
width int width int
@ -54,5 +54,11 @@ func (c *Common) PushPage(page Component) {
} }
func (c *Common) PopPage() (Component, error) { 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 { type Styles struct {
Colors map[string]*lipgloss.Style
Base lipgloss.Style Base lipgloss.Style
Form *huh.Theme Form *huh.Theme
@ -28,61 +30,22 @@ type Styles struct {
ColumnFocused lipgloss.Style ColumnFocused lipgloss.Style
ColumnBlurred lipgloss.Style ColumnBlurred lipgloss.Style
ColumnInsert 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 { 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.Base = lipgloss.NewStyle()
styles.TableStyle = TableStyle{ styles.TableStyle = TableStyle{
@ -94,11 +57,12 @@ func NewStyles(config *taskwarrior.TWConfig) *Styles {
formTheme := huh.ThemeBase() formTheme := huh.ThemeBase()
formTheme.Focused.Title = formTheme.Focused.Title.Bold(true) 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.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.SelectedPrefix = formTheme.Focused.SelectedPrefix.SetString("✓ ")
formTheme.Focused.UnselectedPrefix = 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.SelectSelector = formTheme.Blurred.SelectSelector.SetString(" ")
formTheme.Blurred.SelectedOption = formTheme.Blurred.SelectedOption.Bold(true) formTheme.Blurred.SelectedOption = formTheme.Blurred.SelectedOption.Bold(true)
formTheme.Blurred.MultiSelectSelector = formTheme.Blurred.MultiSelectSelector.SetString(" ") formTheme.Blurred.MultiSelectSelector = formTheme.Blurred.MultiSelectSelector.SetString(" ")
@ -107,131 +71,23 @@ func NewStyles(config *taskwarrior.TWConfig) *Styles {
styles.Form = formTheme styles.Form = formTheme
styles.ColumnFocused = lipgloss.NewStyle().Width(50).Height(30).Border(lipgloss.DoubleBorder(), true) styles.ColumnFocused = lipgloss.NewStyle().Border(lipgloss.RoundedBorder(), true).Padding(1)
styles.ColumnBlurred = lipgloss.NewStyle().Width(50).Height(30).Border(lipgloss.HiddenBorder(), true) styles.ColumnBlurred = lipgloss.NewStyle().Border(lipgloss.HiddenBorder(), true).Padding(1)
styles.ColumnInsert = lipgloss.NewStyle().Width(50).Height(30).Border(lipgloss.DoubleBorder(), true).BorderForeground(styles.Active.GetForeground()) styles.ColumnInsert = lipgloss.NewStyle().Border(lipgloss.RoundedBorder(), true).Padding(1)
if styles.Colors["active"] != nil {
return styles styles.ColumnInsert = styles.ColumnInsert.BorderForeground(styles.Colors["active"].GetForeground())
} }
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.Colors = colors
return &styles return &styles
} }
func parseColorString(color string) lipgloss.Style { func parseColorString(color string) *lipgloss.Style {
style := lipgloss.NewStyle()
if color == "" { if color == "" {
return style return nil
} }
style := lipgloss.NewStyle()
if strings.Contains(color, "on") { if strings.Contains(color, "on") {
fgbg := strings.Split(color, "on") fgbg := strings.Split(color, "on")
fg := strings.TrimSpace(fgbg[0]) fg := strings.TrimSpace(fgbg[0])
@ -246,7 +102,7 @@ func parseColorString(color string) lipgloss.Style {
style = style.Foreground(parseColor(strings.TrimSpace(color))) style = style.Foreground(parseColor(strings.TrimSpace(color)))
} }
return style return &style
} }
func parseColor(color string) lipgloss.Color { 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 { if len(rows) == 0 {
return styles return styles
} }
taskstyle:
for i, task := range rows { for i, task := range rows {
if task.Status == "deleted" { if task.Status == "deleted" {
styles[i] = m.common.Styles.Deleted.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding()) 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 continue
} }
}
if task.Status == "completed" { if task.Status == "completed" {
styles[i] = m.common.Styles.Completed.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding()) 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 continue
} }
}
if task.Status == "pending" && task.Start != "" { 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()) 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 continue
} }
}
// TODO: implement keyword // TODO: implement keyword
// TODO: implement tag // 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 // TODO: implement project
if !task.GetDate("due").IsZero() && task.GetDate("due").Before(time.Now()) { 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()) 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 continue
} }
}
if task.Scheduled != "" { if task.Scheduled != "" {
styles[i] = m.common.Styles.Scheduled.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding()) 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 continue
} }
}
if !task.GetDate("due").IsZero() && task.GetDate("due").Truncate(24*time.Hour).Equal(time.Now().Truncate(24*time.Hour)) { 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()) 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 continue
} }
}
if task.Due != "" { if task.Due != "" {
styles[i] = m.common.Styles.Due.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding()) 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 continue
} }
}
if len(task.Depends) > 0 { 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()) 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 continue
} }
}
// TODO implement blocking // TODO implement blocking
if task.Recur != "" { if task.Recur != "" {
styles[i] = m.common.Styles.Recurring.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding()) 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 continue
} }
}
// TODO: make styles optional and discard if empty // TODO: make styles optional and discard if empty
if len(task.Tags) > 0 { 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()) if c, ok := m.common.Styles.Colors["tagged"]; ok && c != nil {
taskIteration: styles[i] = c.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding())
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
}
}
continue continue
} }
}
if len(m.common.Udas) > 0 { if len(m.common.Udas) > 0 {
for _, uda := range m.common.Udas { for _, uda := range m.common.Udas {
if u, ok := task.Udas[uda]; ok { if u, ok := task.Udas[uda.Name]; ok {
if style, ok := m.common.Styles.Colors[fmt.Sprintf("uda.%s.%s", uda, u)]; ok { if c, ok := m.common.Styles.Colors[fmt.Sprintf("uda.%s.%s", uda.Name, u)]; ok {
styles[i] = style.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding()) styles[i] = c.Inherit(m.styles.Cell).Margin(m.styles.Cell.GetMargin()).Padding(m.styles.Cell.GetPadding())
continue continue taskstyle
} }
} }
} }

10
go.mod
View File

@ -5,6 +5,7 @@ go 1.22.2
require ( require (
github.com/charmbracelet/bubbles v0.18.0 github.com/charmbracelet/bubbles v0.18.0
github.com/charmbracelet/bubbletea v0.26.1 github.com/charmbracelet/bubbletea v0.26.1
github.com/charmbracelet/glamour v0.7.0
github.com/charmbracelet/huh v0.3.0 github.com/charmbracelet/huh v0.3.0
github.com/charmbracelet/lipgloss v0.10.1-0.20240506202754-3ee5dcab73cb github.com/charmbracelet/lipgloss v0.10.1-0.20240506202754-3ee5dcab73cb
github.com/mattn/go-runewidth v0.0.15 github.com/mattn/go-runewidth v0.0.15
@ -12,20 +13,29 @@ require (
) )
require ( require (
github.com/alecthomas/chroma/v2 v2.8.0 // indirect
github.com/atotto/clipboard v0.1.4 // indirect github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // 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/catppuccin/go v0.2.0 // indirect
github.com/charmbracelet/x/exp/term v0.0.0-20240506152644-8135bef4e495 // 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/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/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // 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/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.2 // 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/rivo/uniseg v0.4.7 // indirect
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // 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/sync v0.7.0 // indirect
golang.org/x/sys v0.20.0 // indirect golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.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 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= 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 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 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 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA=
github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= 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 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=
github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= 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 h1:xujcQeF73rh4jwu3+zhfQsvV18x+7zIjlw7/CYbzGJ0=
github.com/charmbracelet/bubbletea v0.26.1/go.mod h1:FzKr7sKoO8iFVcdIBM9J0sJOcQv5nDQaYwsee3kpbgo= 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 h1:CxPplWkgW2yUTDDG0Z4S5HH8SJOosWHd4LxCvi0XsKE=
github.com/charmbracelet/huh v0.3.0/go.mod h1:fujUdKX8tC45CCSaRQdw789O6uaCRwx8l2NDyKfC4jA= 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 h1:Hs3xzxHuruNT2Iuo87iS40c0PhLqpnUKBI6Xw6Ad3wQ=
github.com/charmbracelet/lipgloss v0.10.1-0.20240506202754-3ee5dcab73cb/go.mod h1:EPP2QJ0ectp3zo6gx9f8oJGq8keirqPJ3XpYEI8wrrs= 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 h1:+0U9qX8Pv8KiYgRxfBvORRjgBzLgHMjtElP4O0PyKYA=
github.com/charmbracelet/x/exp/term v0.0.0-20240506152644-8135bef4e495/go.mod h1:qeR6w1zITbkF7vEhcx0CqX5GfnIiQloJWQghN6HfP+c= 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 h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= 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 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0 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-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 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= 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.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 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/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 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= 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= 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/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 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= 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.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.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 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 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 h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y=
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= 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 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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= 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 func() tea.Msg {
return changeAreaMsg(a) return changeAreaMsg(a)
} }

View File

@ -7,6 +7,7 @@ import (
"tasksquire/taskwarrior" "tasksquire/taskwarrior"
"github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/key"
// "github.com/charmbracelet/bubbles/table" // "github.com/charmbracelet/bubbles/table"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
) )
@ -24,7 +25,7 @@ type ReportPage struct {
taskTable table.Model taskTable table.Model
subpage tea.Model subpage common.Component
} }
func NewReportPage(com *common.Common, report *taskwarrior.Report) *ReportPage { 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) { func (p *ReportPage) SetSize(width int, height int) {
p.common.SetSize(width, height) p.common.SetSize(width, height)
p.taskTable.SetWidth(width - p.common.Styles.Base.GetVerticalFrameSize()) p.taskTable.SetWidth(width - p.common.Styles.Base.GetHorizontalFrameSize())
p.taskTable.SetHeight(height - p.common.Styles.Base.GetHorizontalFrameSize()) p.taskTable.SetHeight(height - p.common.Styles.Base.GetVerticalFrameSize())
} }
func (p *ReportPage) Init() tea.Cmd { func (p *ReportPage) Init() tea.Cmd {

View File

@ -5,12 +5,15 @@ import (
"log/slog" "log/slog"
"strings" "strings"
"tasksquire/common" "tasksquire/common"
"time"
"tasksquire/taskwarrior" "tasksquire/taskwarrior"
"github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/glamour"
"github.com/charmbracelet/huh" "github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
) )
@ -26,13 +29,16 @@ type TaskEditorPage struct {
common *common.Common common *common.Common
task taskwarrior.Task task taskwarrior.Task
colWidth int
colHeight int
mode mode mode mode
columnCursor int columnCursor int
area area area int
areaPicker *areaPicker areaPicker *areaPicker
areas map[area]tea.Model areas []area
} }
func NewTaskEditorPage(com *common.Common, task taskwarrior.Task) *TaskEditorPage { func NewTaskEditorPage(com *common.Common, task taskwarrior.Task) *TaskEditorPage {
@ -41,21 +47,17 @@ func NewTaskEditorPage(com *common.Common, task taskwarrior.Task) *TaskEditorPag
task: task, task: task,
} }
if p.task.Priority == "" {
p.task.Priority = "(none)"
}
if p.task.Project == "" { if p.task.Project == "" {
p.task.Project = "(none)" 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() tagOptions := p.common.TW.GetTags()
p.areas = map[area]tea.Model{ p.areas = []area{
areaTask: NewTaskEdit(p.common, &p.task.Description, &p.task.Priority, &p.task.Project, priorityOptions, projectOptions), NewTaskEdit(p.common, &p.task),
areaTags: NewTagEdit(p.common, &p.task.Tags, tagOptions), NewTagEdit(p.common, &p.task.Tags, tagOptions),
areaTime: NewTimeEdit(p.common, &p.task.Due, &p.task.Scheduled, &p.task.Wait, &p.task.Until), 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) // p.areaList = NewAreaList(common, areaItems)
@ -66,17 +68,30 @@ func NewTaskEditorPage(com *common.Common, task taskwarrior.Task) *TaskEditorPag
p.columnCursor = 1 p.columnCursor = 1
if p.task.Uuid == "" { if p.task.Uuid == "" {
// p.mode = modeInsert
p.mode = modeInsert p.mode = modeInsert
} else { } else {
p.mode = modeNormal p.mode = modeNormal
} }
p.SetSize(com.Width(), com.Height())
return &p return &p
} }
func (p *TaskEditorPage) SetSize(width, height int) { func (p *TaskEditorPage) SetSize(width, height int) {
p.common.SetSize(width, height) 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 { 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) { func (p *TaskEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.WindowSizeMsg:
p.SetSize(msg.Width, msg.Height)
case changeAreaMsg: case changeAreaMsg:
p.area = area(msg) p.area = int(msg)
case changeModeMsg: case changeModeMsg:
p.mode = mode(msg) p.mode = mode(msg)
case prevColumnMsg: case prevColumnMsg:
@ -102,13 +119,15 @@ func (p *TaskEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case prevAreaMsg: case prevAreaMsg:
p.area-- p.area--
if p.area < 0 { if p.area < 0 {
p.area = 2 p.area = len(p.areas) - 1
} }
p.areas[p.area].SetCursor(-1)
case nextAreaMsg: case nextAreaMsg:
p.area++ p.area++
if p.area > 2 { if p.area > len(p.areas)-1 {
p.area = 0 p.area = 0
} }
p.areas[p.area].SetCursor(0)
} }
switch p.mode { 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): case key.Matches(msg, p.common.Keymap.Right):
return p, nextColumn() return p, nextColumn()
case key.Matches(msg, p.common.Keymap.Up): case key.Matches(msg, p.common.Keymap.Up):
var cmd tea.Cmd
if p.columnCursor == 0 { if p.columnCursor == 0 {
picker, cmd := p.areaPicker.Update(msg) picker, cmd := p.areaPicker.Update(msg)
p.areaPicker = picker.(*areaPicker) p.areaPicker = picker.(*areaPicker)
return p, cmd return p, cmd
} else { } 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 return p, cmd
} }
case key.Matches(msg, p.common.Keymap.Down): case key.Matches(msg, p.common.Keymap.Down):
var cmd tea.Cmd
if p.columnCursor == 0 { if p.columnCursor == 0 {
picker, cmd := p.areaPicker.Update(msg) picker, cmd := p.areaPicker.Update(msg)
p.areaPicker = picker.(*areaPicker) p.areaPicker = picker.(*areaPicker)
return p, cmd return p, cmd
} else { } 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 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): case key.Matches(msg, p.common.Keymap.Back):
return p, changeMode(modeNormal) return p, changeMode(modeNormal)
case key.Matches(msg, p.common.Keymap.Prev): case key.Matches(msg, p.common.Keymap.Prev):
var cmd tea.Cmd
if p.columnCursor == 0 { if p.columnCursor == 0 {
picker, cmd := p.areaPicker.Update(msg) picker, cmd := p.areaPicker.Update(msg)
p.areaPicker = picker.(*areaPicker) p.areaPicker = picker.(*areaPicker)
return p, cmd return p, cmd
} else { } 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 return p, cmd
} }
case key.Matches(msg, p.common.Keymap.Next): case key.Matches(msg, p.common.Keymap.Next):
var cmd tea.Cmd
if p.columnCursor == 0 { if p.columnCursor == 0 {
picker, cmd := p.areaPicker.Update(msg) picker, cmd := p.areaPicker.Update(msg)
p.areaPicker = picker.(*areaPicker) p.areaPicker = picker.(*areaPicker)
return p, cmd return p, cmd
} else { } 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 return p, cmd
} }
case key.Matches(msg, p.common.Keymap.Ok): case key.Matches(msg, p.common.Keymap.Ok):
area, cmd := p.areas[p.area].Update(msg) model, cmd := p.areas[p.area].Update(msg)
p.areas[p.area] = area p.areas[p.area] = model.(area)
return p, tea.Batch(cmd, nextField()) return p, tea.Batch(cmd, nextField())
} }
} }
var cmd tea.Cmd
if p.columnCursor == 0 { if p.columnCursor == 0 {
picker, cmd := p.areaPicker.Update(msg) picker, cmd := p.areaPicker.Update(msg)
p.areaPicker = picker.(*areaPicker) p.areaPicker = picker.(*areaPicker)
return p, cmd return p, cmd
} else { } 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 return p, cmd
} }
} }
@ -215,143 +234,45 @@ func (p *TaskEditorPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
func (p *TaskEditorPage) View() string { func (p *TaskEditorPage) View() string {
var focusStyle lipgloss.Style var focusedStyle, blurredStyle lipgloss.Style
if p.mode == modeInsert { if p.mode == modeInsert {
focusStyle = p.common.Styles.ColumnInsert focusedStyle = p.common.Styles.ColumnInsert.Width(p.colWidth).Height(p.colHeight)
} else { } 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 { if p.columnCursor == 0 {
picker = focusStyle.Render(p.areaPicker.View()) // picker = focusedStyle.Render(p.areaPicker.View())
area = p.common.Styles.ColumnBlurred.Render(p.areas[p.area].View()) area = blurredStyle.Render(p.areas[p.area].View())
} else { } else {
picker = p.common.Styles.ColumnBlurred.Render(p.areaPicker.View()) // picker = blurredStyle.Render(p.areaPicker.View())
area = focusStyle.Render(p.areas[p.area].View()) area = focusedStyle.Render(p.areas[p.area].View())
} }
return lipgloss.JoinHorizontal( if p.task.Uuid != "" {
lipgloss.Center, area = lipgloss.JoinHorizontal(
picker, lipgloss.Top,
area, area,
p.common.Styles.ColumnFocused.Render(p.common.TW.GetInformation(&p.task)),
) )
} }
// import ( // return lipgloss.Place(p.common.Width(), p.common.Height(), lipgloss.Center, lipgloss.Center, lipgloss.JoinHorizontal(
// "fmt" // lipgloss.Center,
// "io" // picker,
// "log/slog" // area,
// "strings" // ))
// "tasksquire/common" return lipgloss.Place(p.common.Width(), p.common.Height(), lipgloss.Center, lipgloss.Center, area)
// "tasksquire/taskwarrior" }
// "time"
// "github.com/charmbracelet/bubbles/list" type area interface {
// "github.com/charmbracelet/bubbles/textinput" tea.Model
// tea "github.com/charmbracelet/bubbletea" SetCursor(c int)
// "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 areaPicker struct { type areaPicker struct {
common *common.Common common *common.Common
@ -383,17 +304,18 @@ func NewAreaPicker(common *common.Common, items []string) *areaPicker {
} }
} }
func (a *areaPicker) Area() area { func (a *areaPicker) Area() int {
switch a.list.SelectedItem() { // switch a.list.SelectedItem() {
case item("Task"): // case item("Task"):
return areaTask // return areaTask
case item("Tags"): // case item("Tags"):
return areaTags // return areaTags
case item("Dates"): // case item("Dates"):
return areaTime // return areaTime
default: // default:
return areaTask // return areaTask
} // }
return 0
} }
func (a *areaPicker) Init() tea.Cmd { func (a *areaPicker) Init() tea.Cmd {
@ -429,19 +351,23 @@ type taskEdit struct {
cursor int cursor int
newProjectName *string 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 := "" newProject := ""
projectOptions := append([]string{"(none)"}, com.TW.GetProjects()...)
if task.Project == "" {
task.Project = "(none)"
}
defaultKeymap := huh.NewDefaultKeyMap() defaultKeymap := huh.NewDefaultKeyMap()
t := taskEdit{ fields := []huh.Field{
common: common,
fields: []huh.Field{
huh.NewInput(). huh.NewInput().
Title("Task"). Title("Task").
Value(description). Value(&task.Description).
Validate(func(desc string) error { Validate(func(desc string) error {
if desc == "" { if desc == "" {
return fmt.Errorf("task description is required") return fmt.Errorf("task description is required")
@ -449,30 +375,114 @@ func NewTaskEdit(common *common.Common, description *string, priority *string, p
return nil return nil
}). }).
Inline(true). Inline(true).
WithTheme(common.Styles.Form), Prompt(": ").
WithTheme(com.Styles.Form),
huh.NewSelect[string]().
Options(huh.NewOptions(priorityOptions...)...).
Title("Priority").
Key("priority").
Value(priority).
WithKeyMap(defaultKeymap).
WithTheme(common.Styles.Form),
huh.NewSelect[string](). huh.NewSelect[string]().
Options(huh.NewOptions(projectOptions...)...). Options(huh.NewOptions(projectOptions...)...).
Title("Project"). Title("Project").
Value(project). Value(&task.Project).
WithKeyMap(defaultKeymap). WithKeyMap(defaultKeymap).
WithTheme(common.Styles.Form), WithTheme(com.Styles.Form),
huh.NewInput(). huh.NewInput().
Title("New Project"). Title("New Project").
Value(&newProject). Value(&newProject).
WithTheme(common.Styles.Form), 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).
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
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))
}
}
newAnnotation := ""
fields = append(fields, huh.NewInput().
Title("New Annotation").
Value(&newAnnotation).
Inline(true).
Prompt(": ").
WithTheme(com.Styles.Form))
t := taskEdit{
common: com,
fields: fields,
udaValues: udaValues,
newProjectName: &newProject, newProjectName: &newProject,
newAnnotation: &newAnnotation,
} }
t.fields[0].Focus() t.fields[0].Focus()
@ -480,6 +490,16 @@ func NewTaskEdit(common *common.Common, description *string, priority *string, p
return &t 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 { func (t *taskEdit) Init() tea.Cmd {
return nil return nil
} }
@ -515,6 +535,9 @@ func (t *taskEdit) View() string {
views := make([]string, len(t.fields)) views := make([]string, len(t.fields))
for i, field := range t.fields { for i, field := range t.fields {
views[i] = field.View() views[i] = field.View()
if i < len(t.fields)-1 {
views[i] += "\n"
}
} }
return lipgloss.JoinVertical( return lipgloss.JoinVertical(
lipgloss.Left, lipgloss.Left,
@ -551,16 +574,25 @@ func NewTagEdit(common *common.Common, selected *[]string, options []string) *ta
Title("New Tags"). Title("New Tags").
Value(&newTags). Value(&newTags).
Inline(true). Inline(true).
Prompt(": ").
WithTheme(common.Styles.Form), WithTheme(common.Styles.Form),
}, },
newTagsValue: &newTags, newTagsValue: &newTags,
} }
t.fields[0].Focus()
return &t 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 { func (t *tagEdit) Init() tea.Cmd {
return nil return nil
} }
@ -619,33 +651,45 @@ func NewTimeEdit(common *common.Common, due *string, scheduled *string, wait *st
Value(due). Value(due).
Validate(taskwarrior.ValidateDate). Validate(taskwarrior.ValidateDate).
Inline(true). Inline(true).
Prompt(": ").
WithTheme(common.Styles.Form), WithTheme(common.Styles.Form),
huh.NewInput(). huh.NewInput().
Title("Scheduled"). Title("Scheduled").
Value(scheduled). Value(scheduled).
Validate(taskwarrior.ValidateDate). Validate(taskwarrior.ValidateDate).
Inline(true). Inline(true).
Prompt(": ").
WithTheme(common.Styles.Form), WithTheme(common.Styles.Form),
huh.NewInput(). huh.NewInput().
Title("Wait"). Title("Wait").
Value(wait). Value(wait).
Validate(taskwarrior.ValidateDate). Validate(taskwarrior.ValidateDate).
Inline(true). Inline(true).
Prompt(": ").
WithTheme(common.Styles.Form), WithTheme(common.Styles.Form),
huh.NewInput(). huh.NewInput().
Title("Until"). Title("Until").
Value(until). Value(until).
Validate(taskwarrior.ValidateDate). Validate(taskwarrior.ValidateDate).
Inline(true). Inline(true).
Prompt(": ").
WithTheme(common.Styles.Form), WithTheme(common.Styles.Form),
}, },
} }
t.fields[0].Focus()
return &t 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 { func (t *timeEdit) Init() tea.Cmd {
return nil 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) { // func (p *TaskEditorPage) SetSize(width, height int) {
// p.common.SetSize(width, height) // p.common.SetSize(width, height)
// } // }
@ -813,26 +926,36 @@ func (p *TaskEditorPage) updateTasksCmd() tea.Msg {
if p.task.Project == "(none)" { if p.task.Project == "(none)" {
p.task.Project = "" 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) != "" { if *(p.areas[0].(*taskEdit).newProjectName) != "" {
p.task.Project = *p.areas[areaTask].(*taskEdit).newProjectName p.task.Project = *p.areas[0].(*taskEdit).newProjectName
} }
if *(p.areas[areaTags].(*tagEdit).newTagsValue) != "" { if *(p.areas[1].(*tagEdit).newTagsValue) != "" {
newTags := strings.Split(*p.areas[areaTags].(*tagEdit).newTagsValue, " ") newTags := strings.Split(*p.areas[1].(*tagEdit).newTagsValue, " ")
if len(newTags) > 0 { if len(newTags) > 0 {
p.task.Tags = append(p.task.Tags, newTags...) p.task.Tags = append(p.task.Tags, newTags...)
} }
} }
// if p.additionalProject != "" { if *(p.areas[0].(*taskEdit).newAnnotation) != "" {
// p.task.Project = p.additionalProject p.task.Annotations = append(p.task.Annotations, taskwarrior.Annotation{
// } Entry: time.Now().Format("20060102T150405Z"),
// tags := p.form.Get("tags").([]string) Description: *(p.areas[0].(*taskEdit).newAnnotation),
// p.task.Tags = tags })
// p.common.TW.AddTaskAnnotation(p.task.Uuid, *p.areas[0].(*taskEdit).newAnnotation)
}
p.common.TW.ImportTask(&p.task) p.common.TW.ImportTask(&p.task)
return UpdatedTasksMsg{} return UpdatedTasksMsg{}
} }

View File

@ -1,6 +1,7 @@
package taskwarrior package taskwarrior
import ( import (
"encoding/json"
"fmt" "fmt"
"log/slog" "log/slog"
"math" "math"
@ -13,6 +14,23 @@ const (
dtformat = "20060102T150405Z" 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 { type Annotation struct {
Entry string `json:"entry,omitempty"` Entry string `json:"entry,omitempty"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
@ -22,12 +40,14 @@ func (a Annotation) String() string {
return fmt.Sprintf("%s %s", a.Entry, a.Description) return fmt.Sprintf("%s %s", a.Entry, a.Description)
} }
type Tasks []*Task
type Task struct { type Task struct {
Id int64 `json:"id,omitempty"` Id int64 `json:"id,omitempty"`
Uuid string `json:"uuid,omitempty"` Uuid string `json:"uuid,omitempty"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
Project string `json:"project"` Project string `json:"project"`
Priority string `json:"priority"` // Priority string `json:"priority"`
Status string `json:"status,omitempty"` Status string `json:"status,omitempty"`
Tags []string `json:"tags"` Tags []string `json:"tags"`
VirtualTags []string `json:"-"` VirtualTags []string `json:"-"`
@ -114,8 +134,8 @@ func (t *Task) GetString(fieldWFormat string) string {
} }
return t.Project return t.Project
case "priority": // case "priority":
return t.Priority // return t.Priority
case "status": case "status":
return t.Status return t.Status
@ -186,6 +206,7 @@ func (t *Task) GetString(fieldWFormat string) string {
return t.Recur return t.Recur
default: default:
// TODO: format according to UDA type
if val, ok := t.Udas[field]; ok { if val, ok := t.Udas[field]; ok {
if strVal, ok := val.(string); ok { if strVal, ok := val.(string); ok {
return strVal 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 { type Context struct {
Name string Name string
@ -418,3 +501,15 @@ func ValidateDate(s string) error {
return fmt.Errorf("invalid date") 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 GetReport(report string) *Report
GetReports() Reports GetReports() Reports
GetUdas() []string GetUdas() []Uda
GetTasks(report *Report, filter ...string) Tasks GetTasks(report *Report, filter ...string) Tasks
AddTask(task *Task) error // AddTask(task *Task) error
ImportTask(task *Task) ImportTask(task *Task)
SetTaskDone(task *Task) SetTaskDone(task *Task)
DeleteTask(task *Task) DeleteTask(task *Task)
StartTask(task *Task) StartTask(task *Task)
StopTask(task *Task) StopTask(task *Task)
GetInformation(task *Task) string
AddTaskAnnotation(uuid string, annotation string)
Undo() Undo()
} }
@ -171,15 +173,7 @@ func (ts *TaskSquire) GetTasks(report *Report, filter ...string) Tasks {
return nil return nil
} }
unstructuredTasks := make([]map[string]any, 0) for _, task := range tasks {
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]
if task.Depends != nil && len(task.Depends) > 0 { if task.Depends != nil && len(task.Depends) > 0 {
ids := make([]string, len(task.Depends)) ids := make([]string, len(task.Depends))
for i, dependUuid := range task.Depends { for i, dependUuid := range task.Depends {
@ -325,7 +319,7 @@ func (ts *TaskSquire) GetReports() Reports {
return ts.reports return ts.reports
} }
func (ts *TaskSquire) GetUdas() []string { func (ts *TaskSquire) GetUdas() []Uda {
ts.mutex.Lock() ts.mutex.Lock()
defer ts.mutex.Unlock() defer ts.mutex.Unlock()
@ -336,9 +330,27 @@ func (ts *TaskSquire) GetUdas() []string {
return nil return nil
} }
udas := make([]string, 0) udas := make([]Uda, 0)
for _, uda := range strings.Split(string(output), "\n") { for _, uda := range strings.Split(string(output), "\n") {
if uda != "" { 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) udas = append(udas, uda)
} }
} }
@ -367,42 +379,42 @@ func (ts *TaskSquire) SetContext(context *Context) error {
return nil return nil
} }
func (ts *TaskSquire) AddTask(task *Task) error { // func (ts *TaskSquire) AddTask(task *Task) error {
ts.mutex.Lock() // ts.mutex.Lock()
defer ts.mutex.Unlock() // defer ts.mutex.Unlock()
addArgs := []string{"add"} // addArgs := []string{"add"}
if task.Description == "" { // if task.Description == "" {
slog.Error("Task description is required") // slog.Error("Task description is required")
return nil // return nil
} else { // } else {
addArgs = append(addArgs, task.Description) // addArgs = append(addArgs, task.Description)
} // }
if task.Priority != "" && task.Priority != "(none)" { // if task.Priority != "" && task.Priority != "(none)" {
addArgs = append(addArgs, fmt.Sprintf("priority:%s", task.Priority)) // addArgs = append(addArgs, fmt.Sprintf("priority:%s", task.Priority))
} // }
if task.Project != "" && task.Project != "(none)" { // if task.Project != "" && task.Project != "(none)" {
addArgs = append(addArgs, fmt.Sprintf("project:%s", task.Project)) // addArgs = append(addArgs, fmt.Sprintf("project:%s", task.Project))
} // }
if task.Tags != nil { // if task.Tags != nil {
for _, tag := range task.Tags { // for _, tag := range task.Tags {
addArgs = append(addArgs, fmt.Sprintf("+%s", tag)) // addArgs = append(addArgs, fmt.Sprintf("+%s", tag))
} // }
} // }
if task.Due != "" { // if task.Due != "" {
addArgs = append(addArgs, fmt.Sprintf("due:%s", task.Due)) // addArgs = append(addArgs, fmt.Sprintf("due:%s", task.Due))
} // }
cmd := exec.Command(twBinary, append(ts.defaultArgs, addArgs...)...) // cmd := exec.Command(twBinary, append(ts.defaultArgs, addArgs...)...)
err := cmd.Run() // err := cmd.Run()
if err != nil { // if err != nil {
slog.Error("Failed adding task:", err) // slog.Error("Failed adding task:", err)
} // }
// TODO remove error? // // TODO remove error?
return nil // return nil
} // }
// TODO error handling // TODO error handling
func (ts *TaskSquire) ImportTask(task *Task) { func (ts *TaskSquire) ImportTask(task *Task) {
@ -410,7 +422,6 @@ func (ts *TaskSquire) ImportTask(task *Task) {
defer ts.mutex.Unlock() defer ts.mutex.Unlock()
tasks, err := json.Marshal(Tasks{task}) tasks, err := json.Marshal(Tasks{task})
if err != nil { if err != nil {
slog.Error("Failed marshalling task:", err) 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 { func (ts *TaskSquire) extractConfig() *TWConfig {
cmd := exec.Command(twBinary, append(ts.defaultArgs, []string{"_show"}...)...) cmd := exec.Command(twBinary, append(ts.defaultArgs, []string{"_show"}...)...)
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()

Binary file not shown.

View File

@ -1,13 +1,25 @@
include light-256.theme include light-256.theme
uda.priority.values=H,M,,L
context.test.read=+test context.test.read=+test
context.test.write=+test context.test.write=+test
context.home.read=+home context.home.read=+home
context.home.write=+home context.home.write=+home
uda.testuda.type=string uda.testuda.type=string
uda.testuda.label=testuda uda.testuda.label=Testuda
uda.testuda.values=eins,zwei,drei 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.columns=id,testuda,start.age,entry.age,depends,priority,project,tags,recur,scheduled.countdown,due.relative,until.remaining,description,urgency
report.next.context=1 report.next.context=1