diff options
| -rw-r--r-- | cmd/timr/main.go | 37 | ||||
| -rw-r--r-- | go.mod | 2 | ||||
| -rw-r--r-- | internal/timer/operations.go | 75 | ||||
| -rw-r--r-- | internal/timer/operations_test.go | 137 | ||||
| -rw-r--r-- | internal/timer/timer.go | 65 |
5 files changed, 308 insertions, 8 deletions
diff --git a/cmd/timr/main.go b/cmd/timr/main.go index 554d1ca..fc3407a 100644 --- a/cmd/timr/main.go +++ b/cmd/timr/main.go @@ -1,19 +1,42 @@ package main import ( - "flag" "fmt" "os" - "codeberg.org/snonux/timr/internal" + "timr/internal/timer" ) func main() { - versionFlag := flag.Bool("version", false, "Print version and exit") - flag.Parse() + if len(os.Args) < 2 { + printUsage() + os.Exit(1) + } + + var err error + var output string + + switch os.Args[1] { + case "start": + output, err = timer.StartTimer() + case "stop", "pause": + output, err = timer.StopTimer() + case "status": + output, err = timer.GetStatus() + case "reset": + output, err = timer.ResetTimer() + default: + printUsage() + os.Exit(1) + } - if *versionFlag { - fmt.Println(internal.Version) - os.Exit(0) + if err != nil { + fmt.Println("Error:", err) + os.Exit(1) } + fmt.Println(output) +} + +func printUsage() { + fmt.Println("Usage: timr <start|stop|pause|status|reset>") } @@ -1,3 +1,3 @@ -module codeberg.org/snonux/timr +module timr go 1.24.3 diff --git a/internal/timer/operations.go b/internal/timer/operations.go new file mode 100644 index 0000000..3035b58 --- /dev/null +++ b/internal/timer/operations.go @@ -0,0 +1,75 @@ +package timer + +import ( + "fmt" + "os" + "time" +) + +func StartTimer() (string, error) { + state, err := LoadState() + if err != nil { + return "", fmt.Errorf("error loading state: %w", err) + } + + if state.Running { + return "Timer is already running.", nil + } + + state.Running = true + state.StartTime = time.Now() + if err := state.Save(); err != nil { + return "", fmt.Errorf("error saving state: %w", err) + } + return "Timer started.", nil +} + +func StopTimer() (string, error) { + state, err := LoadState() + if err != nil { + return "", fmt.Errorf("error loading state: %w", err) + } + + if !state.Running { + return "Timer is not running.", nil + } + + state.Running = false + state.ElapsedTime += time.Since(state.StartTime) + if err := state.Save(); err != nil { + return "", fmt.Errorf("error saving state: %w", err) + } + return "Timer stopped.", nil +} + +func GetStatus() (string, error) { + state, err := LoadState() + if err != nil { + return "", fmt.Errorf("error loading state: %w", err) + } + + if state.Running { + elapsed := (state.ElapsedTime + time.Since(state.StartTime)).Round(time.Second) + return fmt.Sprintf("Status: Running\nElapsed Time: %s", elapsed), nil + } else { + elapsed := state.ElapsedTime.Round(time.Second) + return fmt.Sprintf("Status: Stopped\nElapsed Time: %s", elapsed), nil + } +} + +func ResetTimer() (string, error) { + stateFile, err := GetStateFile() + if err != nil { + return "", fmt.Errorf("error getting state file path: %w", err) + } + if err := os.Remove(stateFile); err != nil { + if !os.IsNotExist(err) { + return "", fmt.Errorf("error resetting timer: %w", err) + } + } + state := State{} + if err := state.Save(); err != nil { + return "", fmt.Errorf("error saving state: %w", err) + } + return "Timer reset.", nil +}
\ No newline at end of file diff --git a/internal/timer/operations_test.go b/internal/timer/operations_test.go new file mode 100644 index 0000000..39029b0 --- /dev/null +++ b/internal/timer/operations_test.go @@ -0,0 +1,137 @@ +package timer + +import ( + "path/filepath" + "testing" + "time" +) + +// setup sets up a temporary state file for testing. +func setup(t *testing.T) { + t.Helper() + tempDir := t.TempDir() + stateFilePathOverride = filepath.Join(tempDir, ".timr_state") + t.Cleanup(func() { + stateFilePathOverride = "" + }) +} + +func TestStartTimer(t *testing.T) { + setup(t) + + // Start the timer + msg, err := StartTimer() + if err != nil { + t.Fatalf("StartTimer() error = %v", err) + } + if msg != "Timer started." { + t.Errorf("StartTimer() msg = %v, want %v", msg, "Timer started.") + } + + // Check the state + state, err := LoadState() + if err != nil { + t.Fatalf("LoadState() error = %v", err) + } + if !state.Running { + t.Error("state.Running = false, want true") + } + + // Try to start again + msg, err = StartTimer() + if err != nil { + t.Fatalf("StartTimer() error = %v", err) + } + if msg != "Timer is already running." { + t.Errorf("StartTimer() msg = %v, want %v", msg, "Timer is already running.") + } +} + +func TestStopTimer(t *testing.T) { + setup(t) + + // Stop before starting + msg, err := StopTimer() + if err != nil { + t.Fatalf("StopTimer() error = %v", err) + } + if msg != "Timer is not running." { + t.Errorf("StopTimer() msg = %v, want %v", msg, "Timer is not running.") + } + + // Start and then stop the timer + _, _ = StartTimer() + time.Sleep(10 * time.Millisecond) // Simulate work + msg, err = StopTimer() + if err != nil { + t.Fatalf("StopTimer() error = %v", err) + } + if msg != "Timer stopped." { + t.Errorf("StopTimer() msg = %v, want %v", msg, "Timer stopped.") + } + + // Check the state + state, err := LoadState() + if err != nil { + t.Fatalf("LoadState() error = %v", err) + } + if state.Running { + t.Error("state.Running = true, want false") + } + if state.ElapsedTime == 0 { + t.Error("state.ElapsedTime = 0, want > 0") + } +} + +func TestGetStatus(t *testing.T) { + setup(t) + + // Status when stopped + msg, err := GetStatus() + if err != nil { + t.Fatalf("GetStatus() error = %v", err) + } + want := "Status: Stopped\nElapsed Time: 0s" + if msg != want { + t.Errorf("GetStatus() msg = %q, want %q", msg, want) + } + + // Status when running + _, _ = StartTimer() + msg, err = GetStatus() + if err != nil { + t.Fatalf("GetStatus() error = %v", err) + } + want = "Status: Running\nElapsed Time: 0s" + if msg != want { + t.Errorf("GetStatus() msg = %q, want %q", msg, want) + } +} + +func TestResetTimer(t *testing.T) { + setup(t) + + // Start timer to create a state file + _, _ = StartTimer() + + // Reset the timer + msg, err := ResetTimer() + if err != nil { + t.Fatalf("ResetTimer() error = %v", err) + } + if msg != "Timer reset." { + t.Errorf("ResetTimer() msg = %v, want %v", msg, "Timer reset.") + } + + // Check the state + state, err := LoadState() + if err != nil { + t.Fatalf("LoadState() error = %v", err) + } + if state.Running { + t.Error("state.Running = true, want false") + } + if state.ElapsedTime != 0 { + t.Errorf("state.ElapsedTime = %v, want 0", state.ElapsedTime) + } +} diff --git a/internal/timer/timer.go b/internal/timer/timer.go new file mode 100644 index 0000000..3f2e3f5 --- /dev/null +++ b/internal/timer/timer.go @@ -0,0 +1,65 @@ +package timer + +import ( + "encoding/json" + "os" + "path/filepath" + "time" +) + +const ( + stateFile = ".timr_state" +) + +// stateFilePathOverride is used by tests to override the state file path. +var stateFilePathOverride string + +type State struct { + StartTime time.Time + ElapsedTime time.Duration + Running bool +} + +func GetStateFile() (string, error) { + if stateFilePathOverride != "" { + return stateFilePathOverride, nil + } + configDir, err := os.UserConfigDir() + if err != nil { + return "", err + } + return filepath.Join(configDir, "timr", stateFile), nil +} + +func LoadState() (State, error) { + var state State + stateFilePath, err := GetStateFile() + if err != nil { + return state, err + } + + data, err := os.ReadFile(stateFilePath) + if err != nil { + if os.IsNotExist(err) { + return State{}, nil + } + return state, err + } + + err = json.Unmarshal(data, &state) + return state, err +} + +func (s *State) Save() error { + data, err := json.Marshal(s) + if err != nil { + return err + } + + stateFilePath, err := GetStateFile() + if err != nil { + return err + } + + return os.WriteFile(stateFilePath, data, 0644) +} |
