package main import ( "flag" "fmt" "io" "os" "path/filepath" "strings" "time" "git.wow.st/gmp/logbook" "git.wow.st/gmp/logbook/parser" "git.wow.st/gmp/logbook/review" ) func expandPath(path string) string { if strings.HasPrefix(path, "~/") { home, err := os.UserHomeDir() if err == nil { return filepath.Join(home, path[2:]) } } return path } func usage() { fmt.Fprintf(os.Stderr, `Usage: lb [flags] [args] Commands: import [file] Import workouts from DSL file (or stdin if no file) list List all workouts in the database list-measurements List all measurements in the database performances [exercise] [variation] List past performances on specified exercise predict date variable Predict the value of variable on date delete [all][workouts][measurements] delete specified items Flags: -db Path to BoltDB database file (default: ~/.config/logbook/logbook.db) `) } func getPerformances(wr *logbook.WorkoutRepository, mr *logbook.MeasurementRepository) ([]review.Performance, error) { pr := review.NewPerformanceRepository(wr, mr) filter := func(p review.Performance) bool { return true } switch flag.NArg() { case 1: case 2: filter = func(p review.Performance) bool { if p.Exercise == flag.Arg(1) { return true } else { return false } } case 3: filter = func(p review.Performance) bool { if p.Exercise == flag.Arg(1) && p.Type == flag.Arg(2) { return true } else { return false } } default: usage() os.Exit(1) } ps, err := pr.FindPerformances(filter) return ps, err } func main() { defaultDB := "~/.config/logbook/logbook.db" var dbPath string flag.StringVar(&dbPath, "db", defaultDB, "Path to BoltDB database file") flag.Usage = usage flag.Parse() if flag.NArg() == 0 { usage() os.Exit(1) } dbPath = expandPath(dbPath) wr, mr, err := logbook.NewRepositories(dbPath) if err != nil { fmt.Fprintf(os.Stderr, "Error opening repositories: %v\n", err) os.Exit(1) } defer wr.Close() defer mr.Close() cmd := flag.Arg(0) switch cmd { case "import": var input io.Reader if flag.NArg() > 1 { f, err := os.Open(flag.Arg(1)) if err != nil { fmt.Fprintf(os.Stderr, "Error opening file: %v\n", err) os.Exit(1) } defer f.Close() input = f } else { input = os.Stdin } dslBytes, err := io.ReadAll(input) if err != nil { fmt.Fprintf(os.Stderr, "Error reading input: %v\n", err) os.Exit(1) } dslText := string(dslBytes) workouts, measurements, err := parser.ParseDSL(dslText) if err != nil { fmt.Fprintf(os.Stderr, "Parse error: %v\n", err) os.Exit(1) } if err := wr.AddWorkoutsFromIR(workouts); err != nil { fmt.Fprintf(os.Stderr, "Database write error: %v\n", err) os.Exit(1) } if err := mr.AddMeasurementsFromIR(measurements); err != nil { fmt.Fprintf(os.Stderr, "Database write error: %v\n", err) os.Exit(1) } fmt.Printf("Successfully imported %d workout(s) and %d measurement(s).\n", len(workouts), len(measurements)) case "list": workouts, _, err := wr.FindWorkouts(func(w *parser.Workout) bool { return true }) if err != nil { fmt.Fprintf(os.Stderr, "Error reading workouts: %v\n", err) os.Exit(1) } if len(workouts) == 0 { fmt.Println("No workouts found.") return } for _, w := range workouts { dateStr := w.Date.Format("2 Jan 2006") fmt.Printf("Workout: %s (%s) %d SetGroups.", dateStr, w.Type, len(w.SetGroups)) if w.Note != "" { fmt.Printf(" \"%s\"", w.Note) } fmt.Println() for _, sg := range w.SetGroups { fmt.Printf("Setgroup: %s", sg.Exercise) if sg.Type != "" { fmt.Printf(" (%s)", sg.Type) } fmt.Printf(" %d planned sets.", len(sg.PlannedSets)) if sg.Note != "" { fmt.Printf(" \"%s\"", sg.Note) } fmt.Println() for _, s := range sg.PlannedSets { fmt.Printf("Planned Set: %.0fx%d", s.Weight, s.Reps) if s.Note != "" { fmt.Printf(" (%s)", s.Note) } fmt.Println() } if len(sg.ActualSets) > 0 { for _, s := range sg.ActualSets { fmt.Printf("Actual Set: %.0fx%d (%d RIR)", s.Weight, s.Reps, s.RIR) if s.Note != "" { fmt.Printf(" (%s)", s.Note) } fmt.Println() } } } } case "list-measurements": ms, err := mr.FindAll() if err != nil { fmt.Fprintf(os.Stderr, "Error finding measurements: %v\n", err) os.Exit(1) } for _,m := range(ms) { dateStr := m.Date.Format("2 Jan 2006") fmt.Printf("Measurement: %s %s = %0.1f\n", dateStr, m.Variable, m.Value) } case "performances": ps, err := getPerformances(wr, mr) if err != nil { fmt.Printf("Error finding performance: %v\n", err) os.Exit(1) } for _, p := range (ps) { fmt.Printf("Performance: %v\n", p) } case "estimate": // show rep chart for an exercise ps, err := getPerformances(wr, mr) if err != nil { fmt.Printf("Error finding performance: %v\n", err) os.Exit(1) } var weights, reps []float64 var dates []time.Time isbw := review.IsBodyweight(ps[0].Exercise, ps[0].Type) for _, p := range (ps) { for i, w := range (p.Weights) { if isbw { w = w + p.Bodyweight } if p.RIR[i] == 0 { weights = append(weights, w) reps = append(reps, float64(p.Reps[i])) dates = append(dates, p.Times[i].UTC()) fmt.Printf("Set: weight %0.0f, reps %0.0f\n", w, float64(p.Reps[i])) } } } est := review.NewEstimator(review.WithHalfLife(14.0)) err = est.Fit(weights, reps, dates) if err != nil { fmt.Printf("Error estimating performance: %v\n", err) os.Exit(1) } for i := 1; i<=10; i++ { adj := 0.0 if isbw { adj = ps[len(ps)-1].Bodyweight } fmt.Printf("%d: %0.0f\n", i, est.EstimateMaxWeight(float64(i)) - adj) } case "weekly-estimate": ps, err := getPerformances(wr, mr) if err != nil { fmt.Printf("Error finding performance: %v\n", err) os.Exit(1) } var weights, reps []float64 var dates []time.Time isbw := review.IsBodyweight(ps[0].Exercise, ps[0].Type) for _, p := range (ps) { for i, w := range (p.Weights) { if isbw { w = w + p.Bodyweight } if p.RIR[i] == 0 { weights = append(weights, w) reps = append(reps, float64(p.Reps[i])) dates = append(dates, p.Times[i].UTC()) fmt.Printf("Set: weight %0.0f, reps %0.0f\n", w, float64(p.Reps[i])) } } } var adj float64 if isbw { adj = ps[len(ps)-1].Bodyweight } est := review.NewEstimator(review.WithHalfLife(14.0)) err = est.Fit(weights, reps, dates) if err != nil { fmt.Printf("Error estimating performance: %v\n", err) os.Exit(1) } times, vals := est.TimelineEstimate1RM(7*24*time.Hour) for i, t := range times { fmt.Printf("%s: %0.0f\n", t.Format("2006-01-02"), vals[i] - adj) } case "predict": if flag.NArg() != 3 { usage() os.Exit(1) } dateStr := flag.Arg(1) date, err := time.Parse("2006-01-02", dateStr) if err != nil { fmt.Printf("Invalid date: %v", err) os.Exit(1) } variable := flag.Arg(2) fmt.Printf("Predicting %s for date %s\n", variable, date.Format("2006-01-02")) p, err := mr.Predict(variable, date, 0.75) if err != nil { fmt.Printf("Prediction error: %v\n", err) os.Exit(1) } fmt.Printf("%f\n",p) case "delete": if flag.NArg() != 2 { usage() os.Exit(1) } switch flag.Arg(1) { case "workouts": _, ids, err := wr.FindWorkouts(func(*parser.Workout) bool { return true }) if err != nil { fmt.Printf("Error finding workouts: %v\n", err) os.Exit(1) } for _, id := range(ids) { err = wr.DeleteWorkout(id) if err != nil { fmt.Printf("Error deleting workout: %v\n", err) os.Exit(1) } } fmt.Printf("%d workouts deleted.\n", len(ids)) case "measurements": ms, err := mr.FindAll() if err != nil { fmt.Printf("Error finding measurements: %v\n", err) os.Exit(1) } for _, m := range(ms) { err = mr.Delete(m.ID) if err != nil { fmt.Printf("Error deleting workout: %v\n", err) os.Exit(1) } } fmt.Printf("%d measurements deleted.\n", len(ms)) } default: usage() os.Exit(1) } }