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
30 changes: 30 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
## Pull Request Checklist

<!--
Please follow this checklist and put an x in each of the boxes, like this: [x]. It will ensure that our team takes your pull request seriously.
-->

- [ ] I have read and followed the [contribution guidelines](https://github.com/DylanDevelops/tmpo/blob/main/CONTRIBUTING.md).
- [ ] My pull request targets the `main` branch of tmpo.
- [ ] I have tested these changes locally on my machine.

<!--
What tmpo issue does this PR address (for example, #1234)?
If you have not created an issue for your PR, please search the issue tracker to see if there is an existing issue that aligns with your PR, or open a new issue for discussion.
-->

Closes #[issue number]

## Description

<!--
A summary of the changes made along with any other information that would be helpful to a reviewer such as potential tradeoffs or alternative approaches you considered.
-->

This pull request does x, y, and z...

## Screenshots

<!--
If this PR touches the visual layer of tmpo, please include screenshots or animated gifs to show the changes.
-->
16 changes: 7 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,10 @@ go build -o tmpo .
## Quick Start

```bash
# Initialize an optional configuration file
tmpo init

# Start tracking (auto-detects project)
tmpo start

# Add a description
tmpo start "Fixing auth bug"

# Check current status
# Check status
tmpo status

# Stop tracking
Expand All @@ -64,10 +58,14 @@ For detailed usage and all commands, see the [Usage Guide](docs/usage.md).

## Configuration

Optionally, but highly recommended, create a `.tmporc` file in your project to customize the project name and hourly rate:
Optionally create a `.tmporc` file in your project to customize settings:

```bash
tmpo init --name "My Project" --rate 25.50
# Interactive form (prompts for name, rate, description)
tmpo init

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

See the [Configuration Guide](docs/configuration.md) for details.
Expand Down
128 changes: 109 additions & 19 deletions cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,69 +4,159 @@ import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"

"github.com/DylanDevelops/tmpo/internal/config"
"github.com/DylanDevelops/tmpo/internal/project"
"github.com/manifoldco/promptui"
"github.com/spf13/cobra"
)

var (
hourlyRate float64
projectName string
acceptDefaults bool
)

// initCmd represents the init command
var initCmd = &cobra.Command{
Use: "init",
Short: "Initialize a .tmporc config file",
Long: `Create a .tmporc configuration file in the current directory.`,
Long: `Create a .tmporc configuration file in the current directory using an interactive form.`,
Run: func(cmd *cobra.Command, args []string) {
if _, err := os.Stat(".tmporc"); err == nil {
fmt.Println("Error: .tmporc already exists in this directory")

os.Exit(1)
}

name := projectName
if name == "" {
cwd, err := os.Getwd()
// Detect default project name
defaultName := detectDefaultProjectName()

var name string
var hourlyRate float64
var description string

if acceptDefaults {
// Use all defaults without prompting
name = defaultName
hourlyRate = 0
description = ""
} else {
// Interactive form
fmt.Println("\n[tmpo] Initialize Project Configuration")

// Project Name prompt
namePrompt := promptui.Prompt{
Label: fmt.Sprintf("Project name (%s)", defaultName),
AllowEdit: true,
}

nameInput, err := namePrompt.Run()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}

name = strings.TrimSpace(nameInput)
if name == "" {
name = defaultName
}

// Hourly Rate prompt
ratePrompt := promptui.Prompt{
Label: "Hourly rate (press Enter to skip)",
Validate: validateHourlyRate,
}

rateInput, err := ratePrompt.Run()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}

if project.IsInGitRepo() {
gitName, _ := project.GetGitRoot()
if gitName != "" {
name = filepath.Base(gitName)
rateInput = strings.TrimSpace(rateInput)
if rateInput != "" {
hourlyRate, err = strconv.ParseFloat(rateInput, 64)
if err != nil {
fmt.Fprintf(os.Stderr, "Error parsing hourly rate: %v\n", err)
os.Exit(1)
}
}

if name == "" {
name = filepath.Base(cwd)
// Description prompt
descPrompt := promptui.Prompt{
Label: "Description (press Enter to skip)",
}

descInput, err := descPrompt.Run()
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}

description = strings.TrimSpace(descInput)
}

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

os.Exit(1)
}

fmt.Printf("[tmpo] Created .tmporc for project '%s'\n", name)
fmt.Printf("\n[tmpo] Created .tmporc for project '%s'\n", name)
if hourlyRate > 0 {
fmt.Printf(" Hourly Rate: $%.2f\n", hourlyRate)
}
if description != "" {
fmt.Printf(" Description: %s\n", description)
}

fmt.Println("\nYou can edit .tmporc to customize your project settings.")
},
}

// detectDefaultProjectName returns the auto-detected project name
func detectDefaultProjectName() string {
cwd, err := os.Getwd()
if err != nil {
return "my-project"
}

name := ""
if project.IsInGitRepo() {
gitName, _ := project.GetGitRoot()
if gitName != "" {
name = filepath.Base(gitName)
}
}

if name == "" {
name = filepath.Base(cwd)
}

return name
}

// validateHourlyRate validates that the input is empty or a valid positive number
func validateHourlyRate(input string) error {
input = strings.TrimSpace(input)
if input == "" {
return nil // Allow empty for optional field
}

rate, err := strconv.ParseFloat(input, 64)
if err != nil {
return fmt.Errorf("must be a valid number")
}

if rate < 0 {
return fmt.Errorf("hourly rate cannot be negative")
}

return nil
}

func init() {
rootCmd.AddCommand(initCmd)

initCmd.Flags().Float64VarP(&hourlyRate, "rate", "r", 0, "Hourly rate for this project")
initCmd.Flags().StringVarP(&projectName, "name", "n", "", "Project name (defaults to directory/repo name)")
initCmd.Flags().BoolVarP(&acceptDefaults, "accept-defaults", "a", false, "Accept all defaults and skip interactive prompts")
}
45 changes: 31 additions & 14 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,22 @@ Place a `.tmporc` file in your project root to customize tracking settings for t

### Creating a Configuration File

Use `tmpo init` to create a `.tmporc` file interactively:
Use `tmpo init` to create a `.tmporc` file using an interactive form:

```bash
cd ~/projects/my-project
tmpo init --name "My Project" --rate 150
tmpo init
# You'll be prompted for:
# - Project name (defaults to auto-detected name)
# - Hourly rate (optional, press Enter to skip)
# - Description (optional, press Enter to skip)
```

For quick setup without prompts, use the `--accept-defaults` flag:

```bash
tmpo init --accept-defaults
# Creates .tmporc with auto-detected project name and default values
```

This creates a `.tmporc` file in the current directory.
Expand Down Expand Up @@ -130,28 +141,34 @@ tmpo start

### Separate Projects with Different Rates

Create a `.tmporc` in each project directory:
Create a `.tmporc` in each project directory using `tmpo init`:

```bash
# Client A - $150/hour
cd ~/projects/client-a
cat > .tmporc << EOF
project_name: Client A - Web Development
hourly_rate: 150.00
EOF
tmpo init
# Project name: Client A - Web Development
# Hourly rate: 150
# Description: [press Enter to skip]

# Client B - $175/hour
cd ~/projects/client-b
cat > .tmporc << EOF
project_name: Client B - Game Development
hourly_rate: 175.00
EOF
tmpo init
# Project name: Client B - Game Development
# Hourly rate: 175
# Description: [press Enter to skip]

# Personal project - no billing
cd ~/projects/my-app
cat > .tmporc << EOF
project_name: My App
hourly_rate: 0
tmpo init --accept-defaults # Quick setup with defaults
```

Alternatively, you can manually create `.tmporc` files:

```bash
cat > ~/projects/client-a/.tmporc << EOF
project_name: Client A - Web Development
hourly_rate: 150.00
EOF
```

Expand Down
32 changes: 23 additions & 9 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,22 +77,36 @@ tmpo stats --week # This week's stats

### `tmpo init`

Create a `.tmporc` configuration file for the current project.
Create a `.tmporc` configuration file for the current project using an interactive form. You'll be prompted to enter:

**Options:**
- **Project name** - Defaults to auto-detected name from Git repo or directory
- **Hourly rate** - Optional billing rate (press Enter to skip)
- **Description** - Optional project description (press Enter to skip)

- `--name "Project Name"` - Specify custom project name
- `--rate 150` - Set hourly rate for billing calculations
**Interactive Mode (default):**

**Examples:**
```bash
tmpo init
# [tmpo] Initialize Project Configuration
# Project name (my-project): [Enter custom name or press Enter for default]
# Hourly rate (press Enter to skip): 150
# Description (press Enter to skip): Client website redesign
```

**Quick Mode:**

Use the `--accept-defaults` flag to skip all prompts and use auto-detected defaults:

```bash
tmpo init # Auto-detect project name
tmpo init --name "My Project" # Specify name
tmpo init --name "Client Work" --rate 150 # Set hourly rate
tmpo init --accept-defaults # Creates .tmporc with defaults, no prompts
```

See [Configuration Guide](configuration.md) for details on the `.tmporc` file format.
This creates a `.tmporc` file with:
- Project name from Git repo or directory name
- Hourly rate of 0 (disabled)
- Empty description

See [Configuration Guide](configuration.md) for details on the `.tmporc` file format and manual editing.

## Advanced Features

Expand Down
7 changes: 4 additions & 3 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type Config struct {
// Format placeholders:
// %s - project name (string)
// %.2f - hourly rate (float64, 2 decimal places)
// %s - description (string)
//
// ! IMPORTANT: When adding new fields to the Config struct above, update this template. !
const configTemplate = `# tmpo project configuration
Expand All @@ -40,7 +41,7 @@ project_name: %s
hourly_rate: %.2f

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

// Load reads a YAML configuration file from the provided path and unmarshals it into a Config.
Expand Down Expand Up @@ -98,13 +99,13 @@ func Create(projectName string, hourlyRate float64) error {
// 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 {
func CreateWithTemplate(projectName string, hourlyRate float64, description string) error {
tmporc := filepath.Join(".", ".tmporc")
if _, err := os.Stat(tmporc); err == nil {
return fmt.Errorf(".tmporc already exists")
}

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

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