Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
name: Test

on:
push:
branches: [ main ]
# push:
# branches: [ main ]
pull_request:
branches: [ main ]

Expand Down
119 changes: 119 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

tmpo is a minimal CLI time tracker for developers built with Go. It uses Cobra for CLI commands, SQLite for local storage, and supports automatic project detection via Git or `.tmporc` configuration files.

## Build and Development Commands

**Build the project:**
```bash
go build -o tmpo .
```

**Run tests:**
```bash
go test -v ./...
```

**Test the binary:**
```bash
./tmpo --version
./tmpo --help
```

**Run locally without building:**
```bash
go run main.go [command]
```

**Test release build locally:**
```bash
goreleaser build --snapshot --clean
```

## Architecture

### Core Components

**CLI Layer** (`cmd/`):
- Uses Cobra for command structure
- Each command is a separate file (start.go, stop.go, status.go, etc.)
- All commands registered in `cmd/root.go` via `init()` functions
- Version information is injected via ldflags during build

**Storage Layer** (`internal/storage/`):
- `db.go`: Database wrapper around `*sql.DB` with all query methods
- `models.go`: TimeEntry struct with Duration() and IsRunning() helper methods
- Uses modernc.org/sqlite (pure Go implementation)
- Database location: `$HOME/.tmpo/tmpo.db`
- Schema: time_entries table with id, project_name, start_time, end_time, description, hourly_rate

**Configuration** (`internal/config/`):
- YAML-based config using `.tmporc` files
- Config fields: project_name, hourly_rate, description
- FindAndLoad() searches upward through parent directories for `.tmporc`
- Supports per-project configuration by placing `.tmporc` in project root

**Project Detection** (`internal/project/`):
- Three-tier detection strategy:
1. `.tmporc` file (highest priority)
2. Git repository name via `git rev-parse --show-toplevel`
3. Current directory name (fallback)
- Helper functions: FindTmporc(), GetGitRepoName(), IsInGitRepo(), GetGitRoot()

**Export** (`internal/export/`):
- `csv.go`: Export to CSV format
- `json.go`: Export to JSON format
- Used by `export` command with filtering options

### Key Patterns

**Database Initialization:**
Every command that accesses the database follows this pattern:
```go
db, err := storage.Initialize()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
defer db.Close()
```

**Project Name Detection:**
The `cmd/start.go:DetectProjectName()` function implements the priority: .tmporc config → Git repository → directory name

**Time Entry States:**
- Running: EndTime is nil
- Stopped: EndTime is non-nil
- Methods: TimeEntry.IsRunning(), TimeEntry.Duration()

**Hourly Rate Handling:**
HourlyRate is optional (*float64). Stored as sql.NullFloat64 in database queries and converted to/from pointer for the TimeEntry struct.

**Config Template Pattern:**
The `.tmporc` file generation uses a template-based approach to ensure all fields are visible to users:
- Template is defined as `configTemplate` constant in `internal/config/config.go`
- Located directly below the `Config` struct for easy maintenance
- When adding new fields to `Config`, update both the struct AND the template (marked with IMPORTANT comments)

## Important Notes

**SQLite Driver:**
Uses `modernc.org/sqlite` (pure Go, no CGO) instead of mattn/go-sqlite3. This is important for cross-compilation. macOS builds keep CGO enabled, but Linux/Windows disable it (CGO_ENABLED=0).

**Version Injection:**
Version, Commit, and Date are injected at build time via ldflags:
```
-X github.com/DylanDevelops/tmpo/cmd.Version={{.Version}}
-X github.com/DylanDevelops/tmpo/cmd.Commit={{.Commit}}
-X github.com/DylanDevelops/tmpo/cmd.Date={{.Date}}
```

**Command Registration:**
New commands must be added via `rootCmd.AddCommand()` in their `init()` function.

**Interactive Prompts:**
The `manual` command uses `github.com/manifoldco/promptui` for interactive prompts (date/time input).
2 changes: 1 addition & 1 deletion cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ var initCmd = &cobra.Command{
}
}

err := config.Create(name, hourlyRate)
err := config.CreateWithTemplate(name, hourlyRate)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)

Expand Down
41 changes: 41 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,35 @@ import (
// ProjectName is the human-readable name of the project.
// HourlyRate is the billable hourly rate for the project; when zero it will be omitted from YAML.
// Description is an optional free-form description of the project; when empty it will be omitted from YAML.
//
// ! IMPORTANT When adding new fields to this struct, also update configTemplate below. !
type Config struct {
ProjectName string `yaml:"project_name"`
HourlyRate float64 `yaml:"hourly_rate,omitempty"`
Description string `yaml:"description,omitempty"`
}

// configTemplate is the template used when creating new .tmporc files via CreateWithTemplate.
// It includes all available configuration options with helpful comments.
//
// Format placeholders:
// %s - project name (string)
// %.2f - hourly rate (float64, 2 decimal places)
//
// ! IMPORTANT: When adding new fields to the Config struct above, update this template. !
const configTemplate = `# tmpo project configuration
# This file configures time tracking settings for this project

# Project name (used to identify time entries)
project_name: %s

# [OPTIONAL] Hourly rate for billing calculations (set to 0 to disable)
hourly_rate: %.2f

# [OPTIONAL] Description for this project
description: ""
`

// Load reads a YAML configuration file from the provided path and unmarshals it into a Config.
// It returns a pointer to the populated Config on success. If the file cannot be read or the
// contents cannot be parsed as YAML, Load returns a wrapped error describing the failure.
Expand Down Expand Up @@ -72,6 +95,24 @@ func Create(projectName string, hourlyRate float64) error {
return config.Save(tmporc)
}

// CreateWithTemplate creates a new .tmporc file with a user-friendly format that includes
// all fields (even if empty) and helpful comments. This provides a better user experience
// by showing all available configuration options.
func CreateWithTemplate(projectName string, hourlyRate float64) error {
tmporc := filepath.Join(".", ".tmporc")
if _, err := os.Stat(tmporc); err == nil {
return fmt.Errorf(".tmporc already exists")
}

content := fmt.Sprintf(configTemplate, projectName, hourlyRate)

if err := os.WriteFile(tmporc, []byte(content), 0644); err != nil {
return fmt.Errorf("failed to write config: %w", err)
}

return nil
}

// FindAndLoad searches upward from the current working directory for a file named
// ".tmporc". Starting at os.Getwd(), it ascends parent directories until it either
// finds the file or reaches the filesystem root.
Expand Down
Loading