diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-03 22:48:26 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-03 22:48:26 +0200 |
| commit | 212182216ce42e51da21541f39d485ae11fe5c4c (patch) | |
| tree | cc3572b3d948aa73c1bb5954e7a01ebdcae47eb3 | |
| parent | 0906167aaed5dfab38cefe3fd001187a9c44006e (diff) | |
Task 352: add timer cobra subcommands
| -rw-r--r-- | internal/cli/root.go | 1 | ||||
| -rw-r--r-- | internal/cli/timer.go | 209 | ||||
| -rw-r--r-- | internal/cli/timer_test.go | 84 |
3 files changed, 294 insertions, 0 deletions
diff --git a/internal/cli/root.go b/internal/cli/root.go index 6029e81..53b7758 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -49,6 +49,7 @@ func NewRootCmd() *cobra.Command { cmd.Flags().BoolVar(&showVersion, "version", false, "Print version and exit") cmd.PersistentFlags().StringVar(&configPath, "config", "", "Path to config file") + cmd.AddCommand(newTimerCmd()) return cmd } diff --git a/internal/cli/timer.go b/internal/cli/timer.go new file mode 100644 index 0000000..aafcda0 --- /dev/null +++ b/internal/cli/timer.go @@ -0,0 +1,209 @@ +package cli + +import ( + "errors" + "fmt" + "math/rand/v2" + "strconv" + "strings" + + "codeberg.org/snonux/timr/internal/ascii" + "codeberg.org/snonux/timr/internal/live" + timrTimer "codeberg.org/snonux/timr/internal/timer" + tea "github.com/charmbracelet/bubbletea" + "github.com/spf13/cobra" +) + +func newTimerCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "timer", + Short: "Stopwatch timer operations", + } + + cmd.AddCommand(newTimerStartCmd()) + cmd.AddCommand(newTimerStopCmd()) + cmd.AddCommand(newTimerContinueCmd()) + cmd.AddCommand(newTimerResetCmd()) + cmd.AddCommand(newTimerStatusCmd()) + cmd.AddCommand(newTimerPromptCmd()) + cmd.AddCommand(newTimerTrackCmd()) + cmd.AddCommand(newTimerLiveCmd()) + + return cmd +} + +func newTimerStartCmd() *cobra.Command { + return &cobra.Command{ + Use: "start", + Short: "Start the timer", + RunE: func(cmd *cobra.Command, args []string) error { + rawStatus, err := timrTimer.GetRawStatus() + if err != nil { + return err + } + status, err := strconv.ParseFloat(rawStatus, 64) + if err != nil { + return err + } + + output, err := timrTimer.StartTimer(status > 0) + if err != nil { + return err + } + return printOutput(cmd, output) + }, + } +} + +func newTimerStopCmd() *cobra.Command { + return &cobra.Command{ + Use: "stop", + Short: "Stop the timer", + RunE: func(cmd *cobra.Command, args []string) error { + output, err := timrTimer.StopTimer() + if err != nil { + return err + } + return printOutput(cmd, output) + }, + } +} + +func newTimerContinueCmd() *cobra.Command { + return &cobra.Command{ + Use: "continue", + Short: "Continue a stopped timer", + RunE: func(cmd *cobra.Command, args []string) error { + rawStatus, err := timrTimer.GetRawStatus() + if err != nil { + return err + } + status, err := strconv.ParseFloat(rawStatus, 64) + if err != nil { + return err + } + + output := "Timer is at 0, cannot continue." + if status > 0 { + output, err = timrTimer.StartTimer(true) + if err != nil { + return err + } + } + + return printOutput(cmd, output) + }, + } +} + +func newTimerResetCmd() *cobra.Command { + return &cobra.Command{ + Use: "reset", + Short: "Reset the timer", + RunE: func(cmd *cobra.Command, args []string) error { + output, err := timrTimer.ResetTimer() + if err != nil { + return err + } + return printOutput(cmd, output) + }, + } +} + +func newTimerStatusCmd() *cobra.Command { + var raw bool + var rawMinutes bool + + cmd := &cobra.Command{ + Use: "status", + Short: "Show timer status", + RunE: func(cmd *cobra.Command, args []string) error { + if raw && rawMinutes { + return errors.New("only one of --raw or --raw-minutes can be set") + } + + var ( + output string + err error + ) + + switch { + case raw: + output, err = timrTimer.GetRawStatus() + case rawMinutes: + output, err = timrTimer.GetRawMinutesStatus() + default: + output, err = timrTimer.GetStatus() + } + if err != nil { + return err + } + + return printOutput(cmd, output) + }, + } + + cmd.Flags().BoolVar(&raw, "raw", false, "Show elapsed time in seconds") + cmd.Flags().BoolVar(&rawMinutes, "raw-minutes", false, "Show elapsed time in minutes") + return cmd +} + +func newTimerPromptCmd() *cobra.Command { + return &cobra.Command{ + Use: "prompt", + Short: "Show prompt-friendly timer status", + RunE: func(cmd *cobra.Command, args []string) error { + output, err := timrTimer.GetPromptStatus() + if err != nil { + return err + } + return printOutput(cmd, output) + }, + } +} + +func newTimerTrackCmd() *cobra.Command { + return &cobra.Command{ + Use: "track <description>", + Short: "Track elapsed time to Taskwarrior and reset timer", + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + description := strings.Join(args, " ") + output, err := timrTimer.TrackTime(description) + if err != nil { + return err + } + return printOutput(cmd, output) + }, + } +} + +func newTimerLiveCmd() *cobra.Command { + var font string + + cmd := &cobra.Command{ + Use: "live", + Short: "Launch interactive live timer view", + RunE: func(cmd *cobra.Command, args []string) error { + selectedFont := strings.TrimSpace(font) + if selectedFont == "" { + selectedFont = ascii.AllFonts[rand.IntN(len(ascii.AllFonts))] + } + + program := tea.NewProgram(live.NewModel(selectedFont)) + return program.Start() + }, + } + + cmd.Flags().StringVarP(&font, "font", "f", "", "Font for live timer (doom, mono12, rebel, ansi, ansiShadow)") + return cmd +} + +func printOutput(cmd *cobra.Command, output string) error { + if output == "" { + return nil + } + + _, err := fmt.Fprintln(cmd.OutOrStdout(), output) + return err +} diff --git a/internal/cli/timer_test.go b/internal/cli/timer_test.go new file mode 100644 index 0000000..dead994 --- /dev/null +++ b/internal/cli/timer_test.go @@ -0,0 +1,84 @@ +package cli + +import ( + "bytes" + "path/filepath" + "strings" + "testing" + + timrTimer "codeberg.org/snonux/timr/internal/timer" +) + +func TestTimerStartAndStopCommands(t *testing.T) { + setupTimerState(t) + + var startOut bytes.Buffer + startCmd := NewRootCmd() + startCmd.SetOut(&startOut) + startCmd.SetErr(&startOut) + startCmd.SetArgs([]string{"timer", "start"}) + if err := startCmd.Execute(); err != nil { + t.Fatalf("timer start execute error = %v", err) + } + if !strings.Contains(startOut.String(), "Timer started.") { + t.Fatalf("timer start output = %q", startOut.String()) + } + + var stopOut bytes.Buffer + stopCmd := NewRootCmd() + stopCmd.SetOut(&stopOut) + stopCmd.SetErr(&stopOut) + stopCmd.SetArgs([]string{"timer", "stop"}) + if err := stopCmd.Execute(); err != nil { + t.Fatalf("timer stop execute error = %v", err) + } + if !strings.Contains(stopOut.String(), "Timer stopped.") { + t.Fatalf("timer stop output = %q", stopOut.String()) + } +} + +func TestTimerContinueAtZero(t *testing.T) { + setupTimerState(t) + + var out bytes.Buffer + cmd := NewRootCmd() + cmd.SetOut(&out) + cmd.SetErr(&out) + cmd.SetArgs([]string{"timer", "continue"}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("timer continue execute error = %v", err) + } + + if !strings.Contains(out.String(), "Timer is at 0, cannot continue.") { + t.Fatalf("timer continue output = %q", out.String()) + } +} + +func TestTimerStatusFlagConflict(t *testing.T) { + setupTimerState(t) + + var out bytes.Buffer + cmd := NewRootCmd() + cmd.SetOut(&out) + cmd.SetErr(&out) + cmd.SetArgs([]string{"timer", "status", "--raw", "--raw-minutes"}) + + err := cmd.Execute() + if err == nil { + t.Fatal("timer status conflict error = nil, want error") + } + if !strings.Contains(err.Error(), "--raw") { + t.Fatalf("timer status conflict error = %v", err) + } +} + +func setupTimerState(t *testing.T) { + t.Helper() + + tempDir := t.TempDir() + timrTimer.SetStateFilePathOverride(filepath.Join(tempDir, ".timr_state")) + t.Cleanup(func() { + timrTimer.SetStateFilePathOverride("") + }) +} |
