Style forms; [WIP] Draw table
This commit is contained in:
366
common/styles.go
366
common/styles.go
@ -1,6 +1,13 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"log/slog"
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/huh"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
|
||||||
"tasksquire/taskwarrior"
|
"tasksquire/taskwarrior"
|
||||||
@ -9,17 +16,360 @@ import (
|
|||||||
type Styles struct {
|
type Styles struct {
|
||||||
Main lipgloss.Style
|
Main lipgloss.Style
|
||||||
|
|
||||||
ActiveTask lipgloss.Style
|
Form *huh.Theme
|
||||||
InactiveTask lipgloss.Style
|
|
||||||
NormalTask lipgloss.Style
|
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 {
|
||||||
return &Styles{
|
styles := parseColors(config.GetConfig())
|
||||||
Main: lipgloss.NewStyle().Foreground(lipgloss.Color("241")),
|
styles.Main = lipgloss.NewStyle()
|
||||||
|
|
||||||
ActiveTask: lipgloss.NewStyle().Foreground(lipgloss.Color("255")).Background(lipgloss.Color("236")),
|
formTheme := huh.ThemeBase()
|
||||||
InactiveTask: lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Background(lipgloss.Color("236")),
|
formTheme.Focused.Card = formTheme.Focused.Card.BorderStyle(lipgloss.RoundedBorder()).BorderBottom(true).BorderTop(true)
|
||||||
NormalTask: lipgloss.NewStyle().Foreground(lipgloss.Color("241")),
|
formTheme.Focused.Title = formTheme.Focused.Title.Bold(true)
|
||||||
|
formTheme.Focused.SelectSelector = formTheme.Focused.SelectSelector.SetString("> ")
|
||||||
|
formTheme.Focused.SelectedOption = formTheme.Focused.SelectedOption.Bold(true)
|
||||||
|
formTheme.Focused.MultiSelectSelector = formTheme.Focused.MultiSelectSelector.SetString("> ")
|
||||||
|
formTheme.Focused.SelectedPrefix = formTheme.Focused.SelectedPrefix.SetString("✓ ")
|
||||||
|
formTheme.Focused.UnselectedPrefix = formTheme.Focused.SelectedPrefix.SetString("• ")
|
||||||
|
formTheme.Blurred.SelectSelector = formTheme.Blurred.SelectSelector.SetString(" ")
|
||||||
|
formTheme.Blurred.SelectedOption = formTheme.Blurred.SelectedOption.Bold(true)
|
||||||
|
formTheme.Blurred.MultiSelectSelector = formTheme.Blurred.MultiSelectSelector.SetString(" ")
|
||||||
|
formTheme.Blurred.SelectedPrefix = formTheme.Blurred.SelectedPrefix.SetString("✓ ")
|
||||||
|
formTheme.Blurred.UnselectedPrefix = formTheme.Blurred.SelectedPrefix.SetString("• ")
|
||||||
|
|
||||||
|
styles.Form = formTheme
|
||||||
|
|
||||||
|
return styles
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseColors(config map[string]string) *Styles {
|
||||||
|
styles := Styles{}
|
||||||
|
|
||||||
|
for key, value := range config {
|
||||||
|
if strings.HasPrefix(key, "color.") {
|
||||||
|
if value != "" {
|
||||||
|
color := strings.Split(key, ".")[1]
|
||||||
|
switch color {
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &styles
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseColorString(color string) lipgloss.Style {
|
||||||
|
style := lipgloss.NewStyle()
|
||||||
|
if strings.Contains(color, "on") {
|
||||||
|
fgbg := strings.Split(color, "on")
|
||||||
|
fg := strings.TrimSpace(fgbg[0])
|
||||||
|
bg := strings.TrimSpace(fgbg[1])
|
||||||
|
if fg != "" {
|
||||||
|
style = style.Foreground(parseColor(fg))
|
||||||
|
}
|
||||||
|
if bg != "" {
|
||||||
|
style = style.Background(parseColor(bg))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
style = style.Foreground(parseColor(strings.TrimSpace(color)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return style
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseColor(color string) lipgloss.Color {
|
||||||
|
if strings.HasPrefix(color, "rgb") {
|
||||||
|
rgb, err := parseRGBString(strings.TrimPrefix(color, "rgb"))
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Invalid RGB color format")
|
||||||
|
return lipgloss.Color("0")
|
||||||
|
}
|
||||||
|
return lipgloss.Color(rgbToAnsi(rgb))
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(color, "color") {
|
||||||
|
return lipgloss.Color(strings.TrimPrefix(color, "color"))
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(color, "gray") {
|
||||||
|
gray, err := strconv.Atoi(strings.TrimPrefix(color, "gray"))
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Invalid gray color format")
|
||||||
|
return lipgloss.Color("0")
|
||||||
|
}
|
||||||
|
return lipgloss.Color(strconv.Itoa(gray + 232))
|
||||||
|
}
|
||||||
|
if ansi, okcolor := colorStrings[color]; okcolor {
|
||||||
|
return lipgloss.Color(strconv.Itoa(ansi))
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Error("Invalid color format")
|
||||||
|
return lipgloss.Color("0")
|
||||||
|
}
|
||||||
|
|
||||||
|
type RGB struct {
|
||||||
|
r int
|
||||||
|
g int
|
||||||
|
b int
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRGBString(rgbString string) (RGB, error) {
|
||||||
|
var err error
|
||||||
|
rgb := RGB{}
|
||||||
|
|
||||||
|
if len(rgbString) != 3 {
|
||||||
|
return rgb, errors.New("invalid RGB format")
|
||||||
|
}
|
||||||
|
|
||||||
|
rgb.r, err = strconv.Atoi(string(rgbString[0]))
|
||||||
|
if err != nil {
|
||||||
|
return rgb, errors.New("invalid value for R")
|
||||||
|
}
|
||||||
|
|
||||||
|
rgb.g, err = strconv.Atoi(string(rgbString[1]))
|
||||||
|
if err != nil {
|
||||||
|
return rgb, errors.New("invalid value for G")
|
||||||
|
}
|
||||||
|
|
||||||
|
rgb.b, err = strconv.Atoi(string(rgbString[2]))
|
||||||
|
if err != nil {
|
||||||
|
return rgb, errors.New("invalid value for B")
|
||||||
|
}
|
||||||
|
|
||||||
|
return rgb, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var colorStrings = map[string]int{
|
||||||
|
"black": 0,
|
||||||
|
"red": 1,
|
||||||
|
"green": 2,
|
||||||
|
"yellow": 3,
|
||||||
|
"blue": 4,
|
||||||
|
"magenta": 5,
|
||||||
|
"cyan": 6,
|
||||||
|
"white": 7,
|
||||||
|
"bright black": 8,
|
||||||
|
"bright red": 9,
|
||||||
|
"bright green": 10,
|
||||||
|
"bright yellow": 11,
|
||||||
|
"bright blue": 12,
|
||||||
|
"bright magenta": 13,
|
||||||
|
"bright cyan": 14,
|
||||||
|
"bright white": 15,
|
||||||
|
}
|
||||||
|
|
||||||
|
var baseColors = []RGB{
|
||||||
|
{0, 0, 0}, // Black
|
||||||
|
{128, 0, 0}, // Red
|
||||||
|
{0, 128, 0}, // Green
|
||||||
|
{128, 128, 0}, // Yellow
|
||||||
|
{0, 0, 128}, // Blue
|
||||||
|
{128, 0, 128}, // Magenta
|
||||||
|
{0, 128, 128}, // Cyan
|
||||||
|
{192, 192, 192}, // White
|
||||||
|
}
|
||||||
|
|
||||||
|
var highIntensityColors = []RGB{
|
||||||
|
{128, 128, 128}, // Bright Black (Gray)
|
||||||
|
{255, 0, 0}, // Bright Red
|
||||||
|
{0, 255, 0}, // Bright Green
|
||||||
|
{255, 255, 0}, // Bright Yellow
|
||||||
|
{0, 0, 255}, // Bright Blue
|
||||||
|
{255, 0, 255}, // Bright Magenta
|
||||||
|
{0, 255, 255}, // Bright Cyan
|
||||||
|
{255, 255, 255}, // Bright White
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the Euclidean distance between two colors
|
||||||
|
func colorDistance(c1, c2 RGB) float64 {
|
||||||
|
return math.Sqrt(float64((c1.r-c2.r)*(c1.r-c2.r) + (c1.g-c2.g)*(c1.g-c2.g) + (c1.b-c2.b)*(c1.b-c2.b)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert RGB to the nearest ANSI color code
|
||||||
|
func rgbToAnsi(rgb RGB) string {
|
||||||
|
// Check standard and high-intensity colors
|
||||||
|
allColors := append(baseColors, highIntensityColors...)
|
||||||
|
bestIndex := 0
|
||||||
|
minDist := colorDistance(rgb, allColors[0])
|
||||||
|
|
||||||
|
for i := 1; i < len(allColors); i++ {
|
||||||
|
dist := colorDistance(rgb, allColors[i])
|
||||||
|
if dist < minDist {
|
||||||
|
bestIndex = i
|
||||||
|
minDist = dist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bestIndex < 8 {
|
||||||
|
return strconv.Itoa(bestIndex)
|
||||||
|
} else if bestIndex < 16 {
|
||||||
|
return strconv.Itoa(bestIndex + 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check 6x6x6 color cube
|
||||||
|
for i := 0; i < 216; i++ {
|
||||||
|
cubeColor := RGB{
|
||||||
|
(rgb.r / 51) * 51,
|
||||||
|
(rgb.g / 51) * 51,
|
||||||
|
(rgb.b / 51) * 51,
|
||||||
|
}
|
||||||
|
dist := colorDistance(rgb, cubeColor)
|
||||||
|
if dist < minDist {
|
||||||
|
bestIndex = i + 16
|
||||||
|
minDist = dist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check grayscale colors
|
||||||
|
for i := 0; i < 24; i++ {
|
||||||
|
gray := i*10 + 8
|
||||||
|
grayColor := RGB{gray, gray, gray}
|
||||||
|
dist := colorDistance(rgb, grayColor)
|
||||||
|
if dist < minDist {
|
||||||
|
bestIndex = i + 232
|
||||||
|
minDist = dist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strconv.Itoa(bestIndex)
|
||||||
|
}
|
||||||
|
|||||||
@ -35,9 +35,12 @@ func NewContextPickerPage(common *common.Common) *ContextPickerPage {
|
|||||||
selected := common.TW.GetActiveContext().Name
|
selected := common.TW.GetActiveContext().Name
|
||||||
options := make([]string, 0)
|
options := make([]string, 0)
|
||||||
for _, c := range p.contexts {
|
for _, c := range p.contexts {
|
||||||
|
if c.Name != "none" {
|
||||||
options = append(options, c.Name)
|
options = append(options, c.Name)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
slices.Sort(options)
|
slices.Sort(options)
|
||||||
|
options = append([]string{"(none)"}, options...)
|
||||||
|
|
||||||
p.form = huh.NewForm(
|
p.form = huh.NewForm(
|
||||||
huh.NewGroup(
|
huh.NewGroup(
|
||||||
@ -50,7 +53,8 @@ func NewContextPickerPage(common *common.Common) *ContextPickerPage {
|
|||||||
),
|
),
|
||||||
).
|
).
|
||||||
WithShowHelp(false).
|
WithShowHelp(false).
|
||||||
WithShowErrors(true)
|
WithShowErrors(true).
|
||||||
|
WithTheme(p.common.Styles.Form)
|
||||||
|
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
@ -99,7 +103,11 @@ func (p *ContextPickerPage) View() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *ContextPickerPage) updateContextCmd() tea.Msg {
|
func (p *ContextPickerPage) updateContextCmd() tea.Msg {
|
||||||
return UpdateContextMsg(p.common.TW.GetContext(p.form.GetString("context")))
|
context := p.form.GetString("context")
|
||||||
|
if context == "(none)" {
|
||||||
|
context = "none"
|
||||||
|
}
|
||||||
|
return UpdateContextMsg(p.common.TW.GetContext(context))
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateContextMsg *taskwarrior.Context
|
type UpdateContextMsg *taskwarrior.Context
|
||||||
|
|||||||
@ -1,9 +1,6 @@
|
|||||||
package pages
|
package pages
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"tasksquire/common"
|
"tasksquire/common"
|
||||||
"tasksquire/taskwarrior"
|
"tasksquire/taskwarrior"
|
||||||
|
|
||||||
@ -43,13 +40,12 @@ func NewReportPage(com *common.Common, report *taskwarrior.Report) *ReportPage {
|
|||||||
s := table.DefaultStyles()
|
s := table.DefaultStyles()
|
||||||
s.Header = s.Header.
|
s.Header = s.Header.
|
||||||
BorderStyle(lipgloss.NormalBorder()).
|
BorderStyle(lipgloss.NormalBorder()).
|
||||||
BorderForeground(lipgloss.Color("240")).
|
BorderForeground(com.Styles.Active.GetForeground()).
|
||||||
BorderBottom(true).
|
BorderBottom(true).
|
||||||
Bold(false)
|
Bold(true)
|
||||||
s.Selected = s.Selected.
|
s.Selected = s.Selected.
|
||||||
Foreground(lipgloss.Color("229")).
|
Reverse(true).
|
||||||
Background(lipgloss.Color("57")).
|
Bold(true)
|
||||||
Bold(false)
|
|
||||||
|
|
||||||
keys := ReportKeys{
|
keys := ReportKeys{
|
||||||
Quit: key.NewBinding(
|
Quit: key.NewBinding(
|
||||||
@ -91,6 +87,9 @@ func (p ReportPage) Init() tea.Cmd {
|
|||||||
func (p ReportPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (p ReportPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
var cmds []tea.Cmd
|
var cmds []tea.Cmd
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
p.taskTable.SetWidth(msg.Width - 2)
|
||||||
|
p.taskTable.SetHeight(msg.Height - 4)
|
||||||
case BackMsg:
|
case BackMsg:
|
||||||
p.subpageActive = false
|
p.subpageActive = false
|
||||||
case TaskMsg:
|
case TaskMsg:
|
||||||
@ -107,9 +106,6 @@ func (p ReportPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
cmds = append(cmds, p.getTasks())
|
cmds = append(cmds, p.getTasks())
|
||||||
case AddedTaskMsg:
|
case AddedTaskMsg:
|
||||||
cmds = append(cmds, p.getTasks())
|
cmds = append(cmds, p.getTasks())
|
||||||
case tea.WindowSizeMsg:
|
|
||||||
p.taskTable.SetWidth(msg.Width - 2)
|
|
||||||
p.taskTable.SetHeight(msg.Height - 4)
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
switch {
|
switch {
|
||||||
case key.Matches(msg, p.common.Keymap.Quit):
|
case key.Matches(msg, p.common.Keymap.Quit):
|
||||||
@ -157,24 +153,39 @@ func (p ReportPage) View() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *ReportPage) populateTaskTable(tasks []*taskwarrior.Task) {
|
func (p *ReportPage) populateTaskTable(tasks []*taskwarrior.Task) {
|
||||||
columns := []table.Column{
|
nCols := len(p.activeReport.Columns)
|
||||||
{Title: "ID", Width: 4},
|
columns := make([]table.Column, 0)
|
||||||
{Title: "Project", Width: 10},
|
columnSizes := make([]int, nCols)
|
||||||
{Title: "Tags", Width: 10},
|
fullRows := make([]table.Row, len(tasks))
|
||||||
{Title: "Prio", Width: 2},
|
rows := make([]table.Row, len(tasks))
|
||||||
{Title: "Due", Width: 10},
|
|
||||||
{Title: "Task", Width: 50},
|
for i, task := range tasks {
|
||||||
|
row := table.Row{}
|
||||||
|
for i, col := range p.activeReport.Columns {
|
||||||
|
field := task.Get(col)
|
||||||
|
columnSizes[i] = max(columnSizes[i], len(field))
|
||||||
|
row = append(row, field)
|
||||||
}
|
}
|
||||||
var rows []table.Row
|
fullRows[i] = row
|
||||||
for _, task := range tasks {
|
}
|
||||||
rows = append(rows, table.Row{
|
|
||||||
strconv.FormatInt(task.Id, 10),
|
for i, r := range fullRows {
|
||||||
task.Project,
|
row := table.Row{}
|
||||||
strings.Join(task.Tags, ", "),
|
for j, size := range columnSizes {
|
||||||
task.Priority,
|
if size == 0 {
|
||||||
task.Due,
|
continue
|
||||||
task.Description,
|
}
|
||||||
})
|
row = append(row, r[j])
|
||||||
|
}
|
||||||
|
rows[i] = row
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, label := range p.activeReport.Labels {
|
||||||
|
if columnSizes[i] == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
columns = append(columns, table.Column{Title: label, Width: max(columnSizes[i], len(label))})
|
||||||
}
|
}
|
||||||
|
|
||||||
p.taskTable = table.New(
|
p.taskTable = table.New(
|
||||||
|
|||||||
@ -85,7 +85,8 @@ func NewTaskEditorPage(common *common.Common, task taskwarrior.Task) *TaskEditor
|
|||||||
),
|
),
|
||||||
).
|
).
|
||||||
WithShowHelp(false).
|
WithShowHelp(false).
|
||||||
WithShowErrors(false)
|
WithShowErrors(false).
|
||||||
|
WithTheme(p.common.Styles.Form)
|
||||||
|
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,10 @@ func NewConfig(config []string) *TWConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (tc *TWConfig) GetConfig() map[string]string {
|
||||||
|
return tc.config
|
||||||
|
}
|
||||||
|
|
||||||
func (tc *TWConfig) Get(key string) string {
|
func (tc *TWConfig) Get(key string) string {
|
||||||
if _, ok := tc.config[key]; !ok {
|
if _, ok := tc.config[key]; !ok {
|
||||||
slog.Debug(fmt.Sprintf("Key not found in config: %s", key))
|
slog.Debug(fmt.Sprintf("Key not found in config: %s", key))
|
||||||
|
|||||||
@ -1,5 +1,13 @@
|
|||||||
package taskwarrior
|
package taskwarrior
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
type Task struct {
|
type Task struct {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
Uuid string `json:"uuid"`
|
Uuid string `json:"uuid"`
|
||||||
@ -8,13 +16,69 @@ type Task struct {
|
|||||||
Priority string `json:"priority"`
|
Priority string `json:"priority"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Tags []string `json:"tags"`
|
Tags []string `json:"tags"`
|
||||||
|
Depends []string `json:"depends"`
|
||||||
Urgency float32 `json:"urgency"`
|
Urgency float32 `json:"urgency"`
|
||||||
Due string `json:"due"`
|
Due string `json:"due"`
|
||||||
Wait string `json:"wait"`
|
Wait string `json:"wait"`
|
||||||
Scheduled string `json:"scheduled"`
|
Scheduled string `json:"scheduled"`
|
||||||
|
Until string `json:"until"`
|
||||||
|
Recur string `json:"recur"`
|
||||||
|
Start string `json:"start"`
|
||||||
End string `json:"end"`
|
End string `json:"end"`
|
||||||
Entry string `json:"entry"`
|
Entry string `json:"entry"`
|
||||||
Modified string `json:"modified"`
|
Modified string `json:"modified"`
|
||||||
|
Parent string `json:"parent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Task) Get(field string) string {
|
||||||
|
switch field {
|
||||||
|
case "id":
|
||||||
|
return strconv.FormatInt(t.Id, 10)
|
||||||
|
case "uuid":
|
||||||
|
return t.Uuid
|
||||||
|
case "description":
|
||||||
|
return t.Description
|
||||||
|
case "project":
|
||||||
|
return t.Project
|
||||||
|
case "priority":
|
||||||
|
return t.Priority
|
||||||
|
case "status":
|
||||||
|
return t.Status
|
||||||
|
case "tags":
|
||||||
|
return strings.Join(t.Tags, ", ")
|
||||||
|
case "urgency":
|
||||||
|
return fmt.Sprintf("%.2f", t.Urgency)
|
||||||
|
case "due":
|
||||||
|
return t.Due
|
||||||
|
case "wait":
|
||||||
|
return t.Wait
|
||||||
|
case "scheduled":
|
||||||
|
return t.Scheduled
|
||||||
|
case "end":
|
||||||
|
return t.End
|
||||||
|
case "entry":
|
||||||
|
return t.Entry
|
||||||
|
case "modified":
|
||||||
|
return t.Modified
|
||||||
|
// TODO: implement these fields
|
||||||
|
case "start.age":
|
||||||
|
return formatTime(t.Start)
|
||||||
|
case "depends":
|
||||||
|
return strings.Join(t.Depends, ", ")
|
||||||
|
case "entry.age":
|
||||||
|
return formatTime(t.Entry)
|
||||||
|
case "scheduled.countdown":
|
||||||
|
return formatTime(t.Scheduled)
|
||||||
|
case "until.remaining":
|
||||||
|
return formatTime(t.Until)
|
||||||
|
case "due.relative":
|
||||||
|
return formatTime(t.Due)
|
||||||
|
case "recur":
|
||||||
|
return t.Recur
|
||||||
|
default:
|
||||||
|
slog.Error(fmt.Sprintf("Field not implemented: %s", field))
|
||||||
|
return ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Tasks []*Task
|
type Tasks []*Task
|
||||||
@ -39,3 +103,16 @@ type Report struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Reports map[string]*Report
|
type Reports map[string]*Report
|
||||||
|
|
||||||
|
func formatTime(timeStr string) string {
|
||||||
|
if timeStr == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
format := "20060102T150405Z"
|
||||||
|
t, err := time.Parse(format, timeStr)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to parse time:", err)
|
||||||
|
return timeStr
|
||||||
|
}
|
||||||
|
return t.Format("2006-01-02 15:04")
|
||||||
|
}
|
||||||
|
|||||||
@ -16,6 +16,22 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
reportBlacklist = map[string]struct{}{
|
||||||
|
"burndown.daily": {},
|
||||||
|
"burndown.monthly": {},
|
||||||
|
"burndown.weekly": {},
|
||||||
|
"calendar": {},
|
||||||
|
"colors": {},
|
||||||
|
"export": {},
|
||||||
|
"ghistory.annual": {},
|
||||||
|
"ghistory.monthly": {},
|
||||||
|
"history.annual": {},
|
||||||
|
"history.monthly": {},
|
||||||
|
"information": {},
|
||||||
|
"summary": {},
|
||||||
|
"timesheet": {},
|
||||||
|
}
|
||||||
|
|
||||||
tagBlacklist = map[string]struct{}{
|
tagBlacklist = map[string]struct{}{
|
||||||
"ACTIVE": {},
|
"ACTIVE": {},
|
||||||
"ANNOTATED": {},
|
"ANNOTATED": {},
|
||||||
@ -330,6 +346,9 @@ func (ts *TaskSquire) extractReports() Reports {
|
|||||||
reports := make(Reports)
|
reports := make(Reports)
|
||||||
|
|
||||||
for _, report := range availableReports {
|
for _, report := range availableReports {
|
||||||
|
if _, ok := reportBlacklist[report]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
reports[report] = &Report{
|
reports[report] = &Report{
|
||||||
Name: report,
|
Name: report,
|
||||||
Description: ts.config.Get(fmt.Sprintf("report.%s.description", report)),
|
Description: ts.config.Get(fmt.Sprintf("report.%s.description", report)),
|
||||||
|
|||||||
Binary file not shown.
10
test/taskrc
10
test/taskrc
@ -1,4 +1,6 @@
|
|||||||
data.location=/Users/moustachioed/projects/tasksquire/test
|
include light-256.theme
|
||||||
context.test1.read=+test
|
|
||||||
context.test1.write=+test
|
context.test.read=+test
|
||||||
context=test1
|
context.test.write=+test
|
||||||
|
context.home.read=+home
|
||||||
|
context.home.write=+home
|
||||||
|
|||||||
Reference in New Issue
Block a user