// Package menu is a native-Charm guided menu over the tock CLI. It collects // input with huh and shells to tock; it never stores anything. package menu import ( "fmt" "os" "os/exec" "github.com/charmbracelet/huh" "ticktock/internal/tui/form" ) type action struct{ key, label string } var actions = []action{ {"start", "▶ start — start an activity"}, {"stop", "⏹ stop — stop the running activity"}, {"current", "⏱ current — what's running now"}, {"continue", "🔁 continue — restart a recent activity"}, {"note", "📝 note — append a note to the last activity"}, {"tag", "🏷 tag — append tags to the last activity"}, {"report", "📊 report — time report"}, {"list", "📅 list — calendar view"}, {"last", "🕘 last — recent unique activities"}, {"watch", "⏲ watch — full-screen stopwatch"}, {"analyze", "🧠 analyze — productivity patterns"}, } // runTock execs tock inheriting stdio so full-screen commands work and output // prints normally. func runTock(args []string) error { cmd := exec.Command("tock", args...) cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr return cmd.Run() } func input(title string, value *string) error { return huh.NewForm(huh.NewGroup( huh.NewInput().Title(title).Value(value), )).Run() } // Run shows the menu and performs the chosen action. func Run(sug form.Suggest) error { var key string opts := make([]huh.Option[string], 0, len(actions)) for _, a := range actions { opts = append(opts, huh.NewOption(a.label, a.key)) } err := huh.NewForm(huh.NewGroup( huh.NewSelect[string]().Title("tock — pick an action"). Options(opts...).Value(&key).Filtering(true), )).Run() if err != nil || key == "" { return nil // cancelled } switch key { case "start": var project, desc, tags, note string if err := huh.NewForm(huh.NewGroup( huh.NewInput().Title("project").Value(&project).Suggestions(sug.Projects), huh.NewInput().Title("description").Value(&desc).Suggestions(sug.Descriptions), huh.NewInput().Title("tags (comma-sep, blank=none)").Value(&tags).Suggestions(sug.Tags), huh.NewInput().Title("note (blank=none)").Value(¬e), )).Run(); err != nil { return nil } if project == "" { return fmt.Errorf("project is required") } return runTock(startArgs(project, desc, splitTags(tags), note)) case "stop": var note string if err := input("closing note (blank=none)", ¬e); err != nil { return nil } return runTock(stopArgs(note)) case "continue": _ = runTock([]string{"last"}) // show recent for reference var num string if err := input("number to continue (blank=last)", &num); err != nil { return nil } return runTock(continueArgs(num)) case "note": var text string if err := input("note text", &text); err != nil || text == "" { return nil } return runTock(noteArgs(text)) case "tag": var tags string if err := input("tags (comma-sep)", &tags); err != nil { return nil } t := splitTags(tags) if len(t) == 0 { return nil } return runTock(tagArgs(t)) case "report": var period string if err := huh.NewForm(huh.NewGroup( huh.NewSelect[string]().Title("which period?").Options( huh.NewOption("today", "today"), huh.NewOption("yesterday", "yesterday"), huh.NewOption("specific date", "date"), huh.NewOption("all", "all"), ).Value(&period), )).Run(); err != nil { return nil } date := "" if period == "date" { if err := input("date (YYYY-MM-DD)", &date); err != nil || date == "" { return nil } } return runTock(reportArgs(period, date)) default: // current, list, last, watch, analyze — direct passthrough return runTock([]string{key}) } }