diff options
| author | Paul Buetow <paul@buetow.org> | 2025-09-28 23:56:05 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-09-28 23:56:05 +0300 |
| commit | ad21f1b972063d2543f402c04a20f32b263e60e1 (patch) | |
| tree | 6b983ad85c84c5dcc5eb6d70d46c7f8363f9b78e | |
| parent | 6c10d06cb7a0bbdb7e94243e27df676e8998f9a0 (diff) | |
fix output
| -rw-r--r-- | CLAUDE.md | 57 | ||||
| -rw-r--r-- | cmd/timr/main.go | 14 | ||||
| -rw-r--r-- | internal/timer/operations.go | 11 | ||||
| -rw-r--r-- | internal/timer/operations_test.go | 54 | ||||
| -rw-r--r-- | internal/version.go | 2 |
5 files changed, 47 insertions, 91 deletions
diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index f78d779..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,57 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Overview - -`timr` is a simple command-line time tracking tool written in Go. It provides a minimalist stopwatch-style timer that persists state across sessions, allowing users to track time spent on tasks. - -## Common Development Commands - -### Build & Run -- `task` or `go build -o timr ./cmd/timr` - Build the executable -- `task run` or `go run ./cmd/timr` - Run without building -- `task install` or `go install ./cmd/timr` - Install to GOPATH/bin - -### Testing -- `task test` or `go test ./...` - Run all tests -- `go test ./internal/timer -v` - Run specific package tests with verbose output - -## Architecture - -The codebase follows a clean modular structure: - -- **Entry Point**: `/cmd/timr/main.go` - CLI command handling -- **Core Logic**: `/internal/timer/` - Timer state management and operations - - `timer.go` - State persistence (JSON file at `~/.config/timr/.timr_state`) - - `operations.go` - Core timer operations (start, stop, status, reset) -- **Interactive UI**: `/internal/live/` - Full-screen timer using Bubble Tea TUI framework -- **Version Info**: `/internal/version.go` - Version constants - -## Key Implementation Details - -1. **State Persistence**: Timer state is stored as JSON in `~/.config/timr/.timr_state`, containing: - - `running`: boolean indicating if timer is active - - `startTime`: timestamp when timer was last started - - `elapsedTime`: accumulated time in nanoseconds - -2. **Command Structure**: The CLI supports these subcommands: - - `start` - Start/resume timer - - `stop`/`pause` - Stop timer (synonyms) - - `status` - Show formatted status - - `status raw` - Show elapsed seconds (raw number) - - `status rawm` - Show elapsed minutes (raw number) - - `reset` - Reset timer to zero - - `prompt` - Special format for shell prompt integration (returns empty if not running) - - `live` - Interactive TUI mode with keyboard controls - -3. **TUI Framework**: Uses Bubble Tea for the interactive live mode, with dependencies on various charmbracelet libraries for terminal styling. - -4. **Error Handling**: Operations that modify state (start, stop, reset) save state immediately and return errors if persistence fails. - -## Development Guidelines - -- Timer operations should maintain atomic state updates -- All state modifications must persist immediately to handle unexpected exits -- The live mode runs in a separate Bubble Tea program and doesn't modify timer state directly -- Shell prompt integration returns empty string when timer is not running to avoid cluttering prompts
\ No newline at end of file diff --git a/cmd/timr/main.go b/cmd/timr/main.go index 4bd6025..d5b3004 100644 --- a/cmd/timr/main.go +++ b/cmd/timr/main.go @@ -6,9 +6,9 @@ import ( "strconv" "strings" - "github.com/charmbracelet/bubbletea" "codeberg.org/snonux/timr/internal/live" "codeberg.org/snonux/timr/internal/timer" + tea "github.com/charmbracelet/bubbletea" ) func main() { @@ -31,7 +31,15 @@ func runCommand(args []string) (string, error) { switch args[1] { case "start": - output, err = timer.StartTimer() + rawStatus, err := timer.GetRawStatus() + if err != nil { + return "", err + } + status, err := strconv.ParseFloat(rawStatus, 64) + if err != nil { + return "", err + } + output, err = timer.StartTimer(status > 0) case "continue": rawStatus, err := timer.GetRawStatus() if err != nil { @@ -42,7 +50,7 @@ func runCommand(args []string) (string, error) { return "", err } if status > 0 { - output, err = timer.StartTimer() + output, err = timer.StartTimer(true) } else { output = "Timer is at 0, cannot continue." } diff --git a/internal/timer/operations.go b/internal/timer/operations.go index 97be22d..52e5d3e 100644 --- a/internal/timer/operations.go +++ b/internal/timer/operations.go @@ -7,7 +7,7 @@ import ( "time" ) -func StartTimer() (string, error) { +func StartTimer(continued bool) (string, error) { state, err := LoadState() if err != nil { return "", fmt.Errorf("error loading state: %w", err) @@ -22,6 +22,11 @@ func StartTimer() (string, error) { if err := state.Save(); err != nil { return "", fmt.Errorf("error saving state: %w", err) } + + if continued { + return "Timer continued.", nil + } + return "Timer started.", nil } @@ -141,7 +146,7 @@ func TrackTime(description string) (string, error) { // Convert to minutes minutes := int(elapsed.Minutes()) - + // If timer was running, stop it if state.Running { state.Running = false @@ -154,7 +159,7 @@ func TrackTime(description string) (string, error) { // Build and execute the task command taskDescription := fmt.Sprintf("%dmin %s", minutes, description) cmd := exec.Command("task", "add", "+track", taskDescription) - + // Execute the command and capture output output, err := cmd.CombinedOutput() if err != nil { diff --git a/internal/timer/operations_test.go b/internal/timer/operations_test.go index 0cb86fc..557631c 100644 --- a/internal/timer/operations_test.go +++ b/internal/timer/operations_test.go @@ -23,12 +23,12 @@ func TestStartTimer(t *testing.T) { setup(t) // Start the timer - msg, err := StartTimer() + msg, err := StartTimer(false) if err != nil { - t.Fatalf("StartTimer() error = %v", err) + t.Fatalf("StartTimer(false) error = %v", err) } if msg != "Timer started." { - t.Errorf("StartTimer() msg = %v, want %v", msg, "Timer started.") + t.Errorf("StartTimer(false) msg = %v, want %v", msg, "Timer started.") } // Check the state @@ -41,12 +41,12 @@ func TestStartTimer(t *testing.T) { } // Try to start again - msg, err = StartTimer() + msg, err = StartTimer(false) if err != nil { - t.Fatalf("StartTimer() error = %v", err) + t.Fatalf("StartTimer(false) error = %v", err) } if msg != "Timer is already running." { - t.Errorf("StartTimer() msg = %v, want %v", msg, "Timer is already running.") + t.Errorf("StartTimer(false) msg = %v, want %v", msg, "Timer is already running.") } } @@ -63,7 +63,7 @@ func TestStopTimer(t *testing.T) { } // Start and then stop the timer - _, _ = StartTimer() + _, _ = StartTimer(false) time.Sleep(10 * time.Millisecond) // Simulate work msg, err = StopTimer() if err != nil { @@ -100,7 +100,7 @@ func TestGetStatus(t *testing.T) { } // Status when running - _, _ = StartTimer() + _, _ = StartTimer(false) msg, err = GetStatus() if err != nil { t.Fatalf("GetStatus() error = %v", err) @@ -131,7 +131,7 @@ func TestGetRawStatus(t *testing.T) { } state.Running = true state.StartTime = time.Now().Add(-2 * time.Second) // Set start time 2 seconds ago - state.ElapsedTime = 0 // Reset elapsed time for this specific test + state.ElapsedTime = 0 // Reset elapsed time for this specific test if err := state.Save(); err != nil { t.Fatalf("Save() error = %v", err) } @@ -170,7 +170,7 @@ func TestGetRawMinutesStatus(t *testing.T) { } state.Running = true state.StartTime = time.Now().Add(-2 * time.Minute) // Set start time 2 minutes ago - state.ElapsedTime = 0 // Reset elapsed time for this specific test + state.ElapsedTime = 0 // Reset elapsed time for this specific test if err := state.Save(); err != nil { t.Fatalf("Save() error = %v", err) } @@ -189,7 +189,7 @@ func TestResetTimer(t *testing.T) { setup(t) // Start timer to create a state file - _, _ = StartTimer() + _, _ = StartTimer(false) // Reset the timer msg, err := ResetTimer() @@ -219,7 +219,7 @@ func TestTrackTime(t *testing.T) { // Helper to create a mock task command createMockTaskCommand := func(t *testing.T, shouldSucceed bool) { t.Helper() - + // Create a mock script that simulates the task command mockScript := `#!/bin/sh if [ "$1" = "add" ] && [ "$2" = "+timrtest" ]; then @@ -235,16 +235,16 @@ echo "Invalid command" exit 1 ` scriptContent := fmt.Sprintf(mockScript, shouldSucceed) - + // Create temp directory for our mock tempDir := t.TempDir() mockPath := filepath.Join(tempDir, "task") - + // Write the mock script - if err := os.WriteFile(mockPath, []byte(scriptContent), 0755); err != nil { + if err := os.WriteFile(mockPath, []byte(scriptContent), 0o755); err != nil { t.Fatalf("Failed to create mock script: %v", err) } - + // Update PATH to use our mock oldPath := os.Getenv("PATH") os.Setenv("PATH", tempDir+":"+oldPath) @@ -256,22 +256,22 @@ exit 1 t.Run("TrackWithRunningTimer", func(t *testing.T) { setup(t) createMockTaskCommand(t, true) - + // Start timer and let it run for a bit state, _ := LoadState() state.Running = true state.StartTime = time.Now().Add(-5 * time.Minute) state.ElapsedTime = 0 state.Save() - + // We'll modify TrackTime to use +timrtest for testing // For now, test with the actual implementation // In a real scenario, we'd want to make the tag configurable - + // Since we can't easily test the actual command execution, // we'll test the error case when task command is not found msg, err := TrackTime("test description") - + // We expect an error because 'task' command likely doesn't exist // or our mock won't match the exact command if err == nil { @@ -280,7 +280,7 @@ exit 1 t.Error("TrackTime() returned empty message on success") } } - + // Verify timer was stopped state, _ = LoadState() if state.Running { @@ -291,16 +291,16 @@ exit 1 t.Run("TrackWithStoppedTimer", func(t *testing.T) { setup(t) createMockTaskCommand(t, true) - + // Set up a stopped timer with some elapsed time state, _ := LoadState() state.Running = false state.ElapsedTime = 10 * time.Minute state.Save() - + // Try to track time _, err := TrackTime("another test") - + // We expect an error because task command likely doesn't exist // but we can verify the state handling if err == nil { @@ -314,16 +314,16 @@ exit 1 t.Run("TrackWithZeroTime", func(t *testing.T) { setup(t) - + // Fresh timer with no elapsed time state, _ := LoadState() state.Running = false state.ElapsedTime = 0 state.Save() - + // Try to track with zero time _, err := TrackTime("zero time test") - + // Even with zero time, the command should be attempted // We just verify no panic occurs _ = err diff --git a/internal/version.go b/internal/version.go index 8826dc9..10a66c4 100644 --- a/internal/version.go +++ b/internal/version.go @@ -1,3 +1,3 @@ package internal -const Version = "v0.1.0" +const Version = "v0.1.1" |
