package cli import ( "fmt" "os" "time" "github.com/spf13/cobra" "golang.org/x/term" "github.com/humdrum-tiv/dots-cli/internal/day" ) // timeNow is a seam so command tests can pin the clock. var timeNow = func() time.Time { return time.Now() } func stdinIsTTY() bool { return term.IsTerminal(int(os.Stdin.Fd())) } func typeNames(types []day.ActivityType) map[string]string { m := make(map[string]string, len(types)) for _, t := range types { m[t.ID] = t.Name } return m } // pickActivity resolves a query to exactly one type or errors. func pickActivity(query string, types []day.ActivityType) (*day.ActivityType, error) { matches := day.MatchActivity(query, types) switch len(matches) { case 1: return &matches[0], nil case 0: names := make([]string, len(types)) for i, t := range types { names[i] = t.Name } return nil, fmt.Errorf("no activity matches %q — have: %v", query, names) default: if !stdinIsTTY() { return nil, fmt.Errorf("%q is ambiguous: %v", query, namesOf(matches)) } fmt.Println("Which one?") for i, m := range matches { fmt.Printf(" %d) %s\n", i+1, m.Name) } fmt.Print("> ") var n int if _, err := fmt.Scanln(&n); err != nil || n < 1 || n > len(matches) { return nil, fmt.Errorf("no selection") } return &matches[n-1], nil } } func namesOf(ts []day.ActivityType) []string { out := make([]string, len(ts)) for i, t := range ts { out[i] = t.Name } return out } func NewLogCmd() *cobra.Command { var date string var yes bool cmd := &cobra.Command{ Use: "log [time|range] [activity]", Short: "Paint logged block(s); no activity = fill from plan", Args: cobra.MaximumNArgs(2), RunE: func(cmd *cobra.Command, args []string) error { client, err := RequireClient() if err != nil { return err } // Positional parsing: first arg is a time only if it looks like one. target, activityQuery := "", "" switch len(args) { case 1: if day.IsTimeSyntax(args[0]) { target = args[0] } else { activityQuery = args[0] } case 2: if !day.IsTimeSyntax(args[0]) { return fmt.Errorf("first argument %q is not a time — usage: dots log [time|range] [activity]", args[0]) } target, activityQuery = args[0], args[1] } if date == "" { date = Today() } d, err := client.GetDay(date, TZ()) if err != nil { return err } targets, err := day.ParseTarget(target, d.BlockSize, timeNow()) if err != nil { return err } var activityID *string if activityQuery != "" { t, err := pickActivity(activityQuery, d.Types) if err != nil { return err } activityID = &t.ID } assignments, overwrites, err := day.ResolveLog(targets, activityID, d.Blocks, d.PlanBlocks, d.BlockSize) if err != nil { return err } if len(overwrites) > 0 { fmt.Print(FormatChanges(overwrites, typeNames(d.Types), d.BlockSize)) ok, err := Confirm("Overwrite?", yes, os.Stdin, os.Stdout, stdinIsTTY()) if err != nil { return err } if !ok { fmt.Println("Aborted, nothing written.") return nil } } if err := client.WriteBlocks(date, assignments); err != nil { return err } fmt.Printf("Logged %d block(s).\n", len(assignments)) return nil }, } cmd.Flags().StringVar(&date, "date", "", "day to write (YYYY-MM-DD, default today)") cmd.Flags().BoolVarP(&yes, "yes", "y", false, "skip confirmation prompts") return cmd } func NewClearCmd() *cobra.Command { var date string var yes bool cmd := &cobra.Command{ Use: "clear ", Short: "Clear logged block(s)", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { client, err := RequireClient() if err != nil { return err } if !day.IsTimeSyntax(args[0]) { return fmt.Errorf("%q is not a time or range", args[0]) } if date == "" { date = Today() } d, err := client.GetDay(date, TZ()) if err != nil { return err } targets, err := day.ParseTarget(args[0], d.BlockSize, timeNow()) if err != nil { return err } assignments, cleared := day.ResolveClear(targets, d.Blocks) if len(cleared) == 0 { fmt.Println("Nothing logged there.") return nil } fmt.Print(FormatChanges(cleared, typeNames(d.Types), d.BlockSize)) ok, err := Confirm("Clear?", yes, os.Stdin, os.Stdout, stdinIsTTY()) if err != nil { return err } if !ok { fmt.Println("Aborted, nothing cleared.") return nil } if err := client.WriteBlocks(date, assignments); err != nil { return err } fmt.Printf("Cleared %d block(s).\n", len(cleared)) return nil }, } cmd.Flags().StringVar(&date, "date", "", "day to write (YYYY-MM-DD, default today)") cmd.Flags().BoolVarP(&yes, "yes", "y", false, "skip confirmation prompts") return cmd }