Skip to content

Commit

Permalink
Merge pull request #26 from drewstinnett/feature-lipgloass-tables
Browse files Browse the repository at this point in the history
Feature: using lipgloss style table instead of homemade
  • Loading branch information
drewstinnett authored Oct 10, 2023
2 parents af1295d + ae9ce8f commit 8991578
Show file tree
Hide file tree
Showing 8 changed files with 182 additions and 143 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ require (
github.com/bxcodec/faker v2.0.1+incompatible
github.com/charmbracelet/bubbles v0.16.1
github.com/charmbracelet/bubbletea v0.24.2
github.com/charmbracelet/lipgloss v0.8.0
github.com/charmbracelet/lipgloss v0.9.0
github.com/charmbracelet/log v0.2.5
github.com/dustin/go-humanize v1.0.1
github.com/gin-contrib/cors v1.4.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06
github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v0.8.0 h1:IS00fk4XAHcf8uZKc3eHeMUTCxUH6NkaTrdyCQk84RU=
github.com/charmbracelet/lipgloss v0.8.0/go.mod h1:p4eYUZZJ/0oXTuCQKFF8mqyKCz0ja6y+7DniDDw5KKU=
github.com/charmbracelet/lipgloss v0.9.0 h1:BHIM7U4vX77xGEld8GrTKspBMtSv7j0wxPCH73nrdxE=
github.com/charmbracelet/lipgloss v0.9.0/go.mod h1:h8KDyaivONasw1Bhb4nWiKlk4P1wHPly+3+3v6EFMmA=
github.com/charmbracelet/log v0.2.5 h1:1yVvyKCKVV639RR4LIq1iy1Cs1AKxuNO+Hx2LJtk7Wc=
github.com/charmbracelet/log v0.2.5/go.mod h1:nQGK8tvc4pS9cvVEH/pWJiZ50eUq1aoXUOjGpXvdD0k=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
Expand Down
102 changes: 102 additions & 0 deletions taskpoet/durations.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,105 @@ func parseDuration(s string) (*time.Duration, error) {
return nil, fmt.Errorf("invalid unit: %v", unit)
}
}

// Synonym is a shorthand expression for a specific datetime
type Synonym func() *time.Time

// Calendar is a little helper function that can work with synonym times
type Calendar struct {
present time.Time
}

// WithPresent sets the present time to an arbitrary datetime
func WithPresent(t *time.Time) func(*Calendar) {
return func(c *Calendar) {
c.present = *t
}
}

// Synonym returns a time.Time from a string
// Reference: https://taskwarrior.org/docs/dates/
/*
monday, tuesday, … - Local date for the specified day, after today, with time 00:00:00. Can be shortened, e.g. mon, tue 2.6.0 Can be capitalized, e.g. Monday, Tue
january, february, … - Local date for the specified month, 1st day, with time 00:00:00. Can be shortened, e.g. jan, feb. 2.6.0 Can be capitalized, e.g. January, Feb.
later, someday - Local 2038-01-18, with time 00:00:00. A date far away, with semantically meaningful to GTD users.
soy - Local date for the next year, January 1st, with time 00:00:00.
eoy - Local date for this year, December 31st, with time 00:00:00.
soq - Local date for the start of the next quarter (January, April, July, October), 1st, with time 00:00:00.
eoq - Local date for the end of the current quarter (March, June, September, December), last day of the month, with time 23:59:59.
som - Local date for the 1st day of the next month, with time 00:00:00.
socm - Local date for the 1st day of the current month, with time 00:00:00.
eom, eocm - Local date for the last day of the current month, with time 23:59:59.
sow - Local date for the next Sunday, with time 00:00:00.
socw - Local date for the last Sunday, with time 00:00:00.
eow, eocw - Local date for the end of the week, Saturday night, with time 00:00:00.
soww - Local date for the start of the work week, next Monday, with time 00:00:00.
eoww - Local date for the end of the work week, Friday night, with time 23:59:59.
1st, 2nd, … - Local date for the next Nth day, with time 00:00:00.
goodfriday - Local date for the next Good Friday, with time 00:00:00.
easter - Local date for the next Easter Sunday, with time 00:00:00.
eastermonday - Local date for the next Easter Monday, with time 00:00:00.
ascension - Local date for the next Ascension (39 days after Easter Sunday), with time 00:00:00.
pentecost - Local date for the next Pentecost (40 days after Easter Sunday), with time 00:00:00.
midsommar - Local date for the Saturday after June 20th, with time 00:00:00. Swedish.
midsommarafton - Local date for the Friday after June 19th, with time 00:00:00. Swedish.
*/
func (c Calendar) Synonym(s string) (*time.Time, error) {
switch s {
case "now":
return &c.present, nil
case "today":
year, month, day := c.present.Date()
d := time.Date(year, month, day, 0, 0, 0, 0, c.present.Location())
return &d, nil
case "tomorrow", "sod":
year, month, day := c.present.Add(24 * time.Hour).Date()
d := time.Date(year, month, day, 0, 0, 0, 0, c.present.Location())
return &d, nil
case "yesterday":
year, month, day := c.present.Add(-24 * time.Hour).Date()
d := time.Date(year, month, day, 0, 0, 0, 0, c.present.Location())
return &d, nil
case "eod":
year, month, day := c.present.Date()
d := time.Date(year, month, day, 23, 59, 59, 999, c.present.Location())
return &d, nil
case "monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday":
twd := daysOfWeek[s]
cwd := c.present.Weekday()
if twd == cwd {
// year month, day := c.present.Add(24 * 7 * time.Hour).Date()
next := c.present.Add(24 * 7 * time.Hour)
year, month, day := next.Date()
d := time.Date(year, month, day, 0, 0, 0, 0, c.present.Location())
return &d, nil
}
_ = c.present.Weekday()

default:
return nil, fmt.Errorf("unknown synonym: %v", s)
}
return nil, nil
}

var daysOfWeek = map[string]time.Weekday{
"sunday": time.Sunday,
"monday": time.Monday,
"tuesday": time.Tuesday,
"wednesday": time.Wednesday,
"thursday": time.Thursday,
"friday": time.Friday,
"saturday": time.Saturday,
}

// NewCalendar returns a new calendar object
func NewCalendar(options ...func(*Calendar)) *Calendar {
c := &Calendar{
present: time.Now(),
}
for _, opt := range options {
opt(c)
}

return c
}
28 changes: 28 additions & 0 deletions taskpoet/durations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,31 @@ func TestDurtionErrors(t *testing.T) {
require.EqualError(t, errors.New(expect), err.Error())
}
}

func TestNewCalendar(t *testing.T) {
now := time.Now()
got := NewCalendar(WithPresent(&now))
require.NotNil(t, got)
}

func TestCalendarSynonyms(t *testing.T) {
present := time.Date(1978, 7, 16, 8, 0, 0, 42, time.Local)
cal := NewCalendar(WithPresent(&present))
tests := map[string]time.Time{
"now": time.Date(1978, 7, 16, 8, 0, 0, 42, time.Local),
"today": time.Date(1978, 7, 16, 0, 0, 0, 0, time.Local),
"tomorrow": time.Date(1978, 7, 17, 0, 0, 0, 0, time.Local),
"yesterday": time.Date(1978, 7, 15, 0, 0, 0, 0, time.Local),
"eod": time.Date(1978, 7, 16, 23, 59, 59, 999, time.Local),
}
for given, expect := range tests {
got, err := cal.Synonym(given)
require.NoError(t, err)
require.Equal(t, expect, *got)
}

got, err := cal.Synonym("never-exists")
require.Nil(t, got)
require.Error(t, err)
require.EqualError(t, err, "unknown synonym: never-exists")
}
170 changes: 35 additions & 135 deletions taskpoet/poet.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"time"

"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
"github.com/charmbracelet/log"
"github.com/drewstinnett/taskpoet/themes"
"github.com/mitchellh/go-homedir"
Expand Down Expand Up @@ -181,15 +182,6 @@ const (
)

var docStyle = lipgloss.NewStyle().Padding(0, 2, 0, 2) // subtle = lipgloss.AdaptiveColor{Light: "#f3f4f0", Dark: "#383838"}
// highlight = lipgloss.AdaptiveColor{Light: "#874BFD", Dark: "#7D56F4"}
// special = lipgloss.AdaptiveColor{Light: "#43BF6D", Dark: "#73F59F"}
/*
header = lipgloss.NewStyle().
Foreground(highlight).
Underline(true).
Padding(0, 1, 0, 1)
*/ // entry = lipgloss.NewStyle().Foreground(special).Padding(0, 1, 0, 1)
// entryAlt = lipgloss.NewStyle().Foreground(special).Background(subtle).Padding(0, 1, 0, 1)

// TableOpts defines the data displayed in a table
type TableOpts struct {
Expand Down Expand Up @@ -241,54 +233,6 @@ func mustColumnValue(s string, t Task) string {
return got
}

func getTaskColumn(name string, d []Task) (taskColumn, error) {
switch name {
case "ID":
return &shortIDCol{}, nil
case "Age":
return &ageCol{}, nil
case "Due":
return &dueCol{}, nil
case descriptionColumnName:
return &descriptionCol{tasks: d}, nil
case "Completed":
return &completedCol{}, nil
case "Tags":
return &tagsCol{tasks: d}, nil
default:
return nil, fmt.Errorf("unknown columnn: %v", name)
}
}

func iterateColumnHeaders(c []string, d []Task, s lipgloss.Style) []string {
ret := make([]string, len(c))
for idx, item := range c {
cl, err := getTaskColumn(item, d)
panicIfErr(err)
ret[idx] = s.Width(cl.Width()).Render(item)
}
return ret
}

func iterateColumnValues(c []string, t Task, d []Task, s lipgloss.Style) []string {
ret := make([]string, len(c))
for idx, item := range c {
// ret[idx] = s.Width(mustColumnSize(item)).Render(mustColumnValue(item, t))
cl, err := getTaskColumn(item, d)
panicIfErr(err)
ret[idx] = s.Width(cl.Width()).Render(mustColumnValue(item, t))
}
return ret
}

func columnsOrDefault(c []string) []string {
defaultColumns := []string{"ID", "Age", "Description", "Due", "Tags"}
if len(c) == 0 {
return defaultColumns
}
return c
}

// TaskTable returns a table of the given tasks
// func (p *Poet) TaskTable(prefix string, fp FilterParams, filters ...Filter) string {
func (p *Poet) TaskTable(opts TableOpts) string {
Expand All @@ -309,55 +253,59 @@ func (p *Poet) TaskTable(opts TableOpts) string {
tasks = tasks[0:min(len(tasks), opts.FilterParams.Limit)]
}

columns := columnsOrDefault(opts.Columns)
// columns := columnsOrDefault(opts.Columns)
columns := make([]any, len(opts.Columns))
for idx, c := range opts.Columns {
columns[idx] = c
}

row := lipgloss.JoinHorizontal(
lipgloss.Top,
iterateColumnHeaders(columns, tasks, p.styling.RowHeader)...,
)
headerLen := lipgloss.Width(row)
doc := strings.Builder{}
doc.WriteString(row + "\n")

for idx, task := range tasks {
rs := altRowStyle(idx, p.styling.Row, p.styling.RowAlt)
row := lipgloss.JoinHorizontal(
lipgloss.Top,
iterateColumnValues(columns, task, tasks, rs)...,
)
doc.WriteString(row + "\n")
t := table.New().
Border(lipgloss.HiddenBorder()).
BorderStyle(lipgloss.NewStyle()).
StyleFunc(func(row, col int) lipgloss.Style {
switch {
case row == 0:
return p.styling.RowHeader
case row%2 == 0:
return p.styling.RowAlt
default:
return p.styling.Row
}
}).
Headers(columns...)

for _, task := range tasks {
row := make([]string, len(opts.Columns))
for idx, c := range opts.Columns {
row[idx] = mustColumnValue(c, task)
}
t.Row(row...)
}
addLimitWarning(&doc, headerLen-4, opts.FilterParams.Limit, allTasksLen)
doc.WriteString(t.Render())

width := lipgloss.Width(t.Render())
addLimitWarning(&doc, width-4, opts.FilterParams.Limit, allTasksLen)

w, _, _ := term.GetSize(int(os.Stdout.Fd()))
maxW := max(w, headerLen)
log.Debug("setting max window size", "size", maxW)
maxW := min(w, width)
// log.Debug("setting max window size", "size", maxW)
docStyle = docStyle.MaxWidth(maxW)
return docStyle.Render(doc.String())
}

func addLimitWarning(doc io.StringWriter, width, limit, total int) {
if (limit > 0) && limit < total {
_, _ = doc.WriteString("\n")
_, _ = doc.WriteString(
lipgloss.NewStyle().Width(width).Align(lipgloss.Right).Italic(true).Render(
lipgloss.NewStyle().Italic(true).Width(width - 3).Align(lipgloss.Right).Render(
fmt.Sprintf("* %v more records to display, increase the limit to see it",
total-limit)),
)
}
}

func longestDescription(tasks Tasks) int {
// Description is 11 chars long itself, add 2 for padding
r := 13
for _, task := range tasks {
l := len(task.Description)
if l > r {
r = l
}
}
return r
}

// Filter is a filter function applied to a single task
type Filter func(*FilterParams, Task) bool

Expand Down Expand Up @@ -411,13 +359,6 @@ func ApplyFilters(tasks Tasks, p *FilterParams, filters ...Filter) Tasks {
return filteredRecords
}

func altRowStyle(idx int, even, odd lipgloss.Style) lipgloss.Style {
if idx%2 == 0 {
return even
}
return odd
}

// ByDue is the by due date sorter
type ByDue Tasks

Expand Down Expand Up @@ -448,47 +389,6 @@ func (a ByCompleted) Less(i, j int) bool {
}
func (a ByCompleted) Swap(i, j int) { a[i], a[j] = a[j], a[i] }

type taskColumn interface {
String() string
Width() int
}

type shortIDCol struct{}

func (c shortIDCol) String() string { return "ID" }
func (c shortIDCol) Width() int { return 8 }

type ageCol struct {
name string
}

func (a ageCol) String() string { return a.name }
func (a ageCol) Width() int { return 15 }

type dueCol struct{}

func (d dueCol) String() string { return "Due" }
func (d dueCol) Width() int { return 15 }

type descriptionCol struct {
tasks Tasks
}

func (d descriptionCol) String() string { return descriptionColumnName }
func (d descriptionCol) Width() int { return min(55, longestDescription(d.tasks)+3) }

type tagsCol struct {
tasks Tasks
}

func (t tagsCol) String() string { return "Tags" }
func (t tagsCol) Width() int { return 15 }

type completedCol struct{}

func (d completedCol) String() string { return "Completed" }
func (d completedCol) Width() int { return 13 }

func (p *Poet) exists(t *Task) bool {
_, err := p.Task.GetWithID(t.ID, t.PluginID, "")
return err == nil
Expand Down
Loading

0 comments on commit 8991578

Please sign in to comment.