From a87bd355aedeae996ed697a9a99cc3ad073b62a7 Mon Sep 17 00:00:00 2001 From: Drew Stinnett Date: Wed, 7 Feb 2024 18:35:17 -0500 Subject: [PATCH] Fixing form --- .golangci.yml | 1 + cmd/letseat/cmd/edit.go | 64 +++++++++++++++ cmd/letseat/cmd/export.go | 2 +- cmd/letseat/cmd/form.go | 149 +++++++++++++++++++++++++++++++++++ cmd/letseat/cmd/form_test.go | 21 +++++ cmd/letseat/cmd/log.go | 117 +-------------------------- cmd/letseat/cmd/log_test.go | 1 + cmd/letseat/cmd/root.go | 1 + pkg/diary.go | 18 +++++ pkg/diary_test.go | 30 +++++++ 10 files changed, 287 insertions(+), 117 deletions(-) create mode 100644 cmd/letseat/cmd/edit.go create mode 100644 cmd/letseat/cmd/form.go create mode 100644 cmd/letseat/cmd/form_test.go create mode 100644 cmd/letseat/cmd/log_test.go diff --git a/.golangci.yml b/.golangci.yml index 2fbc7fb..9701eea 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -170,6 +170,7 @@ linters-settings: - '$gostd' - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/assert" + - "github.com/charmbracelet/huh" # depguard: # list-type: blacklist # include-go-root: false diff --git a/cmd/letseat/cmd/edit.go b/cmd/letseat/cmd/edit.go new file mode 100644 index 0000000..0deb219 --- /dev/null +++ b/cmd/letseat/cmd/edit.go @@ -0,0 +1,64 @@ +package cmd + +import ( + "fmt" + "sort" + + "github.com/charmbracelet/huh" + letseat "github.com/drewstinnett/letseat/pkg" + "github.com/spf13/cobra" +) + +func newEditCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "edit", + Short: "edit entries", + RunE: runEdit, + } + return cmd +} + +func editEntryOpts(e letseat.Entries) []huh.Option[string] { + sort.Slice(e, func(i, j int) bool { + return e[i].Date.After(*e[j].Date) + }) + ret := make([]huh.Option[string], len(e)) + for idx, entry := range e { + ret[idx] = huh.NewOption(fmt.Sprintf("%v - %v", entry.Date.Format("2006-01-02"), entry.Place), entry.Key()) + } + return ret +} + +func runEdit(cmd *cobra.Command, args []string) error { + diary := letseat.New( + letseat.WithDBFilename(mustGetCmd[string](*cmd, "data")), + ) + defer dclose(diary) + + var editID string + form := huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Title("Which entry would you like to edit?"). + Options( + editEntryOpts(diary.Entries())..., + ). + Value(&editID), + ), + ) + if err := form.Run(); err != nil { + return err + } + + e, err := diary.Get(editID) + if err != nil { + return err + } + + editForm := newEntryForm(e) + if err := editForm.NewForm(diary.Entries()).Run(); err != nil { + return err + } + + return nil +} diff --git a/cmd/letseat/cmd/export.go b/cmd/letseat/cmd/export.go index 0208cb0..fd0a802 100644 --- a/cmd/letseat/cmd/export.go +++ b/cmd/letseat/cmd/export.go @@ -20,11 +20,11 @@ func runExport(cmd *cobra.Command, args []string) error { diary := letseat.New( letseat.WithDBFilename(mustGetCmd[string](*cmd, "data")), ) + defer dclose(diary) out, err := diary.Export() if err != nil { return err } - defer dclose(diary) fmt.Fprint(cmd.OutOrStdout(), string(out)) return nil } diff --git a/cmd/letseat/cmd/form.go b/cmd/letseat/cmd/form.go new file mode 100644 index 0000000..ee38859 --- /dev/null +++ b/cmd/letseat/cmd/form.go @@ -0,0 +1,149 @@ +package cmd + +import ( + "fmt" + "strconv" + "time" + + "github.com/charmbracelet/huh" + letseat "github.com/drewstinnett/letseat/pkg" +) + +func (e entryForm) Entry() letseat.Entry { + d, err := time.Parse("2006-01-02", e.date) + panicIfErr(err) + + cost, err := strconv.Atoi(e.cost) + panicIfErr(err) + + ret := letseat.Entry{ + Place: e.place, + Date: &d, + IsTakeout: e.takeout, + Ratings: make(map[string]int, len(e.ratings)), + Cost: cost, + } + + if e.newPlace != "" { + ret.Place = e.newPlace + } + for person, rating := range e.ratings { + ret.Ratings[person] = *rating + } + return ret +} + +func (e *entryForm) NewForm(entries letseat.Entries) *huh.Form { + placeOpts := newPlaceOpts(entries.UniquePlaceNames()) + + groups := []*huh.Group{ + huh.NewGroup( + huh.NewInput(). + Title("Date"). + Description("When did you go?"). + Validate(validateDate). + Value(&e.date), + huh.NewSelect[string](). + Title("Place"). + Description("What's this place called?"). + Options(placeOpts...). + Value(&e.place), + huh.NewInput(). + Title("Cost"). + Description("Use 0 for unknown cost"). + Placeholder("0"). + Validate(validateNumber). + Prompt("$ "). + Value(&e.cost), + huh.NewConfirm(). + Title("Take Out?"). + Value(&e.takeout), + ), + huh.NewGroup( + huh.NewInput(). + Title("Name"). + Description("What's this new place called??"). + Validate(validatePlace). + Value(&e.newPlace), + ).WithHideFunc(func() bool { + return e.place != "" + }), + } + ri := e.newRatingInputs(entries.PeopleEnhanced()) + if len(ri) > 0 { + groups = append(groups, huh.NewGroup(ri...)) + } + return huh.NewForm(groups...) +} + +var ratingOptions []*huh.Option[int] = []*huh.Option[int]{ + {Key: "🚫 No Rating", Value: 0}, + {Key: "⭐️⭐️⭐️⭐️⭐️", Value: 5}, + {Key: "⭐️⭐️⭐️⭐️", Value: 4}, + {Key: "⭐️⭐️⭐️", Value: 3}, + {Key: "⭐️⭐️", Value: 2}, + {Key: "⭐️", Value: 1}, +} + +func ratingOptionsWithSelected(s int) []huh.Option[int] { + var ret []huh.Option[int] + for _, item := range ratingOptions { + if item.Value == s { + ret = append(ret, item.Selected(true)) + } else { + ret = append(ret, *item) + } + } + return ret +} + +func newPlaceOpts(places []string) []huh.Option[string] { + placeOpts := make([]huh.Option[string], len(places)+1) + placeOpts[0] = huh.Option[string]{ + Key: "Someplace New!", + Value: "", + } + for idx, item := range places { + placeOpts[idx+1] = huh.Option[string]{ + Key: item, + Value: item, + } + } + return placeOpts +} + +// return a new EntryForm using a given entry as a template +func newEntryForm(t *letseat.Entry) entryForm { + if t == nil { + return entryForm{ + date: time.Now().Format("2006-01-02"), + cost: "0", + ratings: map[string]*int{}, + } + } + ratings := map[string]*int{} + for k, v := range t.Ratings { + v := v + ratings[k] = &v + } + return entryForm{ + date: t.Date.Format("2006-01-02"), + cost: fmt.Sprint(t.Cost), + ratings: ratings, + place: t.Place, + takeout: t.IsTakeout, + } +} + +func (e *entryForm) newRatingInputs(people []letseat.Person) []huh.Field { + ratingInputs := make([]huh.Field, len(people)) + for idx, item := range people { + e.ratings[item.Name] = toPTR(0) + ro := ratingOptionsWithSelected(*e.ratings[item.Name]) + ratingInputs[idx] = huh.NewSelect[int](). + Title(fmt.Sprintf("%v's Rating (%v)", item.Name, *e.ratings[item.Name])). + Options(ro...). + Value(e.ratings[item.Name]) + } + return ratingInputs +} diff --git a/cmd/letseat/cmd/form_test.go b/cmd/letseat/cmd/form_test.go new file mode 100644 index 0000000..8382872 --- /dev/null +++ b/cmd/letseat/cmd/form_test.go @@ -0,0 +1,21 @@ +package cmd + +import ( + "testing" + + "github.com/charmbracelet/huh" + "github.com/stretchr/testify/require" +) + +func TestNewPlaceOpts(t *testing.T) { + var target string + got := huh.NewForm(huh.NewGroup( + huh.NewSelect[string](). + Title("Place"). + Options(newPlaceOpts([]string{"Taco Tuesday"})...). + Value(&target), + )).View() + + require.Contains(t, got, "> Someplace New!", "Make sure the default is something new") + require.Contains(t, got, "Taco Tuesday", "Make sure we still have Taco Tuesday") +} diff --git a/cmd/letseat/cmd/log.go b/cmd/letseat/cmd/log.go index 895a917..cd315af 100644 --- a/cmd/letseat/cmd/log.go +++ b/cmd/letseat/cmd/log.go @@ -2,10 +2,7 @@ package cmd import ( "errors" - "fmt" "log/slog" - "strconv" - "time" "github.com/charmbracelet/huh" "github.com/drewstinnett/gout/v2" @@ -32,124 +29,12 @@ type entryForm struct { ratings map[string]*int } -func (e entryForm) Entry() letseat.Entry { - d, err := time.Parse("2006-01-02", e.date) - panicIfErr(err) - - cost, err := strconv.Atoi(e.cost) - panicIfErr(err) - - ret := letseat.Entry{ - Place: e.place, - Date: &d, - IsTakeout: e.takeout, - Ratings: make(map[string]int, len(e.ratings)), - Cost: cost, - } - - if e.newPlace != "" { - ret.Place = e.newPlace - } - for person, rating := range e.ratings { - ret.Ratings[person] = *rating - } - return ret -} - -func (e *entryForm) NewForm(entries letseat.Entries) *huh.Form { - placeOpts := newPlaceOpts(entries.UniquePlaceNames()) - - groups := []*huh.Group{ - huh.NewGroup( - huh.NewInput(). - Title("Date"). - Description("When did you go?"). - Validate(validateDate). - Value(&e.date), - huh.NewSelect[string](). - Title("Place"). - Description("What's this place called?"). - Options(placeOpts...). - Value(&e.place), - huh.NewInput(). - Title("Cost"). - Description("Use 0 for unknown cost"). - Placeholder("0"). - Validate(validateNumber). - Prompt("$ "). - Value(&e.cost), - huh.NewConfirm(). - Title("Take Out?"). - Value(&e.takeout), - ), - huh.NewGroup( - huh.NewInput(). - Title("Name"). - Description("What's this new place called??"). - Validate(validatePlace). - Value(&e.newPlace), - ).WithHideFunc(func() bool { - return e.place != "" - }), - } - ri := newRatingInputs(entries.PeopleEnhanced(), *e) - if len(ri) > 0 { - groups = append(groups, huh.NewGroup(ri...)) - } - return huh.NewForm(groups...) -} - -var ratingOptions []huh.Option[int] = []huh.Option[int]{ - {Key: "🚫 No Rating", Value: 0}, - {Key: "⭐️⭐️⭐️⭐️⭐️", Value: 5}, - {Key: "⭐️⭐️⭐️⭐️", Value: 4}, - {Key: "⭐️⭐️⭐️", Value: 3}, - {Key: "⭐️⭐️", Value: 2}, - {Key: "⭐️", Value: 1}, -} - -func newPlaceOpts(places []string) []huh.Option[string] { - placeOpts := make([]huh.Option[string], len(places)+1) - placeOpts[0] = huh.Option[string]{ - Key: "Someplace New!", - Value: "", - } - for idx, item := range places { - placeOpts[idx+1] = huh.Option[string]{ - Key: item, - Value: item, - } - } - return placeOpts -} - -func newEntryForm() entryForm { - e := entryForm{ - date: time.Now().Format("2006-01-02"), - cost: "0", - ratings: map[string]*int{}, - } - return e -} - -func newRatingInputs(people []letseat.Person, e entryForm) []huh.Field { - ratingInputs := make([]huh.Field, len(people)) - for idx, item := range people { - e.ratings[item.Name] = toPTR(0) - ratingInputs[idx] = huh.NewSelect[int](). - Title(fmt.Sprintf("%v's Rating", item.Name)). - Options(ratingOptions...). - Value(e.ratings[item.Name]) - } - return ratingInputs -} - func runLog(cmd *cobra.Command, args []string) error { diary := letseat.New( letseat.WithFilter(*mustNewEntryFilterWithCmd(cmd)), letseat.WithDBFilename(mustGetCmd[string](*cmd, "data")), ) - e := newEntryForm() + e := newEntryForm(nil) if err := e.NewForm(diary.Entries()).Run(); err != nil { return err diff --git a/cmd/letseat/cmd/log_test.go b/cmd/letseat/cmd/log_test.go new file mode 100644 index 0000000..1d619dd --- /dev/null +++ b/cmd/letseat/cmd/log_test.go @@ -0,0 +1 @@ +package cmd diff --git a/cmd/letseat/cmd/root.go b/cmd/letseat/cmd/root.go index 67e9206..94ccf56 100644 --- a/cmd/letseat/cmd/root.go +++ b/cmd/letseat/cmd/root.go @@ -56,6 +56,7 @@ func newRootCmd() *cobra.Command { newConfigCmd(), newImportCmd(), newExportCmd(), + newEditCmd(), ) return cmd diff --git a/pkg/diary.go b/pkg/diary.go index b4124e7..854e604 100644 --- a/pkg/diary.go +++ b/pkg/diary.go @@ -35,6 +35,24 @@ func (d Diary) Close() error { return d.db.Close() } +// Get returns an entry by it's key +func (d Diary) Get(k string) (*Entry, error) { + var e Entry + if verr := d.db.View(func(tx *bolt.Tx) error { + v := tx.Bucket([]byte(EntriesBucket)).Get([]byte(k)) + if string(v) == "" { + return fmt.Errorf("record not found: %v", k) + } + if err := json.Unmarshal(v, &e); err != nil { + return err + } + return nil + }); verr != nil { + return nil, verr + } + return &e, nil +} + func (d Diary) allEntries() (Entries, error) { ret := Entries{} if verr := d.db.View(func(tx *bolt.Tx) error { diff --git a/pkg/diary_test.go b/pkg/diary_test.go index 89953fe..7198e1d 100644 --- a/pkg/diary_test.go +++ b/pkg/diary_test.go @@ -220,4 +220,34 @@ func TestLogDB(t *testing.T) { }, }, )) + + export, err := diary.Export() + require.NoError(t, err) + require.Equal( + t, + "- place: Mamacitas\n date: 2024-01-15T00:00:00Z\n takeout: true\n ratings:\n drew: 5\n james: 3\n", + string(export), + ) +} + +func TestGet(t *testing.T) { + diary := New(WithDB(newTestDB(t))) + require.NotNil(t, diary) + e := Entry{ + Place: "Mamacitas", + Date: toPTR(time.Date(2024, time.January, 15, 0, 0, 0, 0, time.UTC)), + IsTakeout: true, Ratings: map[string]int{ + "drew": 5, + "james": 3, + }, + } + require.NoError(t, diary.Log(e)) + got, err := diary.Get(e.Key()) + require.NoError(t, err) + require.Equal(t, &e, got) + + // Check for bad keys + got, err = diary.Get("never-exists") + require.EqualError(t, err, "record not found: never-exists") + require.Nil(t, got) }