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
17 changes: 12 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,11 +176,13 @@ All user data is stored locally in:
```text
~/.tmpo/ # Production (default)
├── tmpo.db # SQLite database
└── config.yaml # Global configuration (optional)
├── config.yaml # Global configuration (optional)
└── projects.yaml # Global projects registry (optional)

~/.tmpo-dev/ # Development (when TMPO_DEV=1)
├── tmpo.db # SQLite database
└── config.yaml # Global configuration (optional)
├── config.yaml # Global configuration (optional)
└── projects.yaml # Global projects registry (optional)
```

The database schema includes:
Expand All @@ -200,9 +202,14 @@ The database schema includes:

When a user runs `tmpo start`, the project name is detected in this priority order:

1. **`.tmporc` file** - Searches current directory and all parent directories
2. **Git repository** - Uses `git rev-parse --show-toplevel` to find repo root
3. **Directory name** - Falls back to current directory name
1. **`--project` flag** - Explicitly specified global project (e.g., `tmpo start --project "My Project"`)
2. **`.tmporc` file** - Searches current directory and all parent directories
3. **Git repository** - Uses `git rev-parse --show-toplevel` to find repo root
4. **Directory name** - Falls back to current directory name

**Global Projects:**

Users can create global projects with `tmpo init --global`, which stores project configurations in `~/.tmpo/projects.yaml`. These projects can be tracked from any directory using the `--project` flag.

This logic lives in `internal/project/detect.go`.

Expand Down
32 changes: 30 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

- **🚀 Fast & Lightweight** - Built in Go, tmpo starts instantly and uses minimal resources
- **🎯 Automatic Project Detection** - Detects project names from Git repos or `.tmporc` configuration files
- **🌐 Global Projects** - Track time for any project from any directory without configuration files
- **🎯 Milestone Tracking** - Organize time entries into sprints, releases, or project phases
- **💾 Local & Private Storage** - All data stored locally in SQLite - your time tracking stays private
- **📊 Rich Reporting** - View stats, export to CSV/JSON, and track hourly rates
Expand Down Expand Up @@ -58,6 +59,23 @@ tmpo stats
tmpo milestone start "Sprint 1"
```

### Track Projects From Anywhere

Create global projects to track time from any directory:

```bash
# Create a global project
tmpo init --global

# Track time from anywhere on your system
cd /tmp
tmpo start --project "Client Work" "Implementing new feature"
tmpo stop

# View entries from anywhere
tmpo log --project "Client Work"
```

For detailed usage and all commands, see the [Usage Guide](docs/usage.md).

## Configuration
Expand All @@ -80,16 +98,26 @@ This opens an interactive wizard to configure:

### Per-Project Settings

Optionally create a `.tmporc` file in your project to customize settings:
**Local Projects (directory-specific):**

```bash
# Interactive form (prompts for name, rate, description)
# Create a .tmporc file in your project directory
tmpo init

# Or skip prompts and use defaults
tmpo init --accept-defaults
```

**Global Projects (track from anywhere):**

```bash
# Create a global project you can use from any directory
tmpo init --global

# Now track time from anywhere
tmpo start --project "Project Name"
```

See the [Configuration Guide](docs/configuration.md) for details.

## Feedback
Expand Down
10 changes: 7 additions & 3 deletions cmd/entries/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ import (
"github.com/spf13/cobra"
)

var showAllProjects bool
var (
showAllProjects bool
editProjectFlag string
)

func EditCmd() *cobra.Command {
cmd := &cobra.Command{
Expand Down Expand Up @@ -73,8 +76,8 @@ func EditCmd() *cobra.Command {

projectName = selectedProject
} else {
// Use current project
detectedProject, err := project.DetectConfiguredProject()
// use current project or explicit project using --project flag
detectedProject, err := project.DetectConfiguredProjectWithOverride(editProjectFlag)
if err != nil {
ui.PrintError(ui.EmojiError, fmt.Sprintf("detecting project: %v", err))
os.Exit(1)
Expand Down Expand Up @@ -437,6 +440,7 @@ func EditCmd() *cobra.Command {
}

cmd.Flags().BoolVar(&showAllProjects, "show-all-projects", false, "Show project selection before entry selection")
cmd.Flags().StringVarP(&editProjectFlag, "project", "p", "", "Edit entries for a specific global project")

return cmd
}
Expand Down
17 changes: 9 additions & 8 deletions cmd/entries/manual.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import (
"github.com/spf13/cobra"
)

var (
manualProjectFlag string
)

func getDateFormatInfo(configFormat string) (displayFormat, layout string) {
switch configFormat {
case "MM/DD/YYYY":
Expand All @@ -24,7 +28,6 @@ func getDateFormatInfo(configFormat string) (displayFormat, layout string) {
case "YYYY-MM-DD":
return "YYYY-MM-DD", "2006-01-02"
default:
// Default to MM-DD-YYYY
return "MM-DD-YYYY", "01-02-2006"
}
}
Expand All @@ -49,7 +52,7 @@ func ManualCmd() *cobra.Command {
// Get date format for prompts and validation
dateFormatDisplay, dateFormatLayout := getDateFormatInfo(globalCfg.DateFormat)

defaultProject := detectProjectNameWithSource()
defaultProject := detectProjectNameWithSource(manualProjectFlag)

var projectLabel string
if defaultProject != "" {
Expand Down Expand Up @@ -243,6 +246,8 @@ func ManualCmd() *cobra.Command {
},
}

cmd.Flags().StringVarP(&manualProjectFlag, "project", "p", "", "Create entry for a specific global project")

return cmd
}

Expand Down Expand Up @@ -323,12 +328,8 @@ func normalizeAMPM(input string) string {
return strings.ToUpper(input)
}

func detectProjectNameWithSource() (string) {
if cfg, _, err := settings.FindAndLoad(); err == nil && cfg != nil && cfg.ProjectName != "" {
return cfg.ProjectName
}

projectName, err := project.DetectProject()
func detectProjectNameWithSource(explicitProject string) string {
projectName, err := project.DetectConfiguredProjectWithOverride(explicitProject)
if err != nil {
return ""
}
Expand Down
13 changes: 9 additions & 4 deletions cmd/history/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,15 @@ func ExportCmd() *cobra.Command {
var entries []*storage.TimeEntry

if exportMilestone != "" {
projectName, err := project.DetectConfiguredProject()
if err != nil {
ui.PrintError(ui.EmojiError, fmt.Sprintf("detecting project: %v", err))
os.Exit(1)
// if --project flag is used, ensure global project config is used
projectName := exportProject
if projectName == "" {
detectedProject, err := project.DetectConfiguredProject()
if err != nil {
ui.PrintError(ui.EmojiError, fmt.Sprintf("detecting project: %v", err))
os.Exit(1)
}
projectName = detectedProject
}
entries, err = db.GetEntriesByMilestone(projectName, exportMilestone)
} else if exportToday {
Expand Down
23 changes: 14 additions & 9 deletions cmd/history/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ import (
)

var (
logLimit int
logProject string
logMilestone string
logToday bool
logWeek bool
logLimit int
logProject string
logMilestone string
logToday bool
logWeek bool
)

func LogCmd() *cobra.Command {
Expand All @@ -40,10 +40,15 @@ func LogCmd() *cobra.Command {
var entries []*storage.TimeEntry

if logMilestone != "" {
projectName, err := project.DetectConfiguredProject()
if err != nil {
ui.PrintError(ui.EmojiError, fmt.Sprintf("detecting project: %v", err))
os.Exit(1)
// if --project flag is used, ensure global project config is used
projectName := logProject
if projectName == "" {
detectedProject, err := project.DetectConfiguredProject()
if err != nil {
ui.PrintError(ui.EmojiError, fmt.Sprintf("detecting project: %v", err))
os.Exit(1)
}
projectName = detectedProject
}
entries, err = db.GetEntriesByMilestone(projectName, logMilestone)
} else if logToday {
Expand Down
Loading