summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd/timr/main.go37
-rw-r--r--go.mod2
-rw-r--r--internal/timer/operations.go75
-rw-r--r--internal/timer/operations_test.go137
-rw-r--r--internal/timer/timer.go65
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>")
}
diff --git a/go.mod b/go.mod
index e0eb885..81196d7 100644
--- a/go.mod
+++ b/go.mod
@@ -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)
+}