summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-09-28 23:56:05 +0300
committerPaul Buetow <paul@buetow.org>2025-09-28 23:56:05 +0300
commitad21f1b972063d2543f402c04a20f32b263e60e1 (patch)
tree6b983ad85c84c5dcc5eb6d70d46c7f8363f9b78e
parent6c10d06cb7a0bbdb7e94243e27df676e8998f9a0 (diff)
fix output
-rw-r--r--CLAUDE.md57
-rw-r--r--cmd/timr/main.go14
-rw-r--r--internal/timer/operations.go11
-rw-r--r--internal/timer/operations_test.go54
-rw-r--r--internal/version.go2
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"