Skip to content

Commit

Permalink
feat: allow time tracking via the CLI
Browse files Browse the repository at this point in the history
  • Loading branch information
dhth committed Jan 19, 2025
1 parent 3ee6da5 commit 7ea594a
Show file tree
Hide file tree
Showing 19 changed files with 586 additions and 166 deletions.
2 changes: 1 addition & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,4 @@ linters-settings:
- name: confusing-naming
- name: unused-receiver
- name: unhandled-error
arguments: ["fmt.Print", "fmt.Printf", "fmt.Fprintf", "fmt.Fprint"]
arguments: ["fmt.Print", "fmt.Printf", "fmt.Fprintf", "fmt.Fprint", "fmt.Fprintln"]
15 changes: 15 additions & 0 deletions cmd/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package cmd

import (
"errors"
)

var (
errCouldntUnmarshalToJSON = errors.New("couldn't unmarshal data to JSON")
errCouldntFetchDataFromDB = errors.New("couldn't fetch data from hours' DB")
errCouldntUpdateDataInDB = errors.New("couldn't update data in hours' DB")
errCouldntParseTaskID = errors.New("couldn't parse the argument for task ID as an integer")
errTaskAlreadyBeingTracked = errors.New("task is already being tracked")
errCouldntParseBeginTS = errors.New("couldn't parse begin timestamp")
errCouldntParseEndTS = errors.New("couldn't parse end timestamp")
)
149 changes: 149 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import (
"math/rand"
"os"
"path/filepath"
"strconv"
"strings"
"time"

c "github.com/dhth/hours/internal/common"
pers "github.com/dhth/hours/internal/persistence"
Expand All @@ -21,6 +23,7 @@ const (
defaultDBName = "hours.db"
numDaysThreshold = 30
numTasksThreshold = 20
numTasksLimit = 50
)

var (
Expand Down Expand Up @@ -109,6 +112,10 @@ func NewRootCommand() (*cobra.Command, error) {
genNumDays uint8
genNumTasks uint8
genSkipConfirmation bool
tasksLimit uint
trackTaskComment string
trackTaskBeginTS string
trackTaskEndTS string
)

rootCmd := &cobra.Command{
Expand Down Expand Up @@ -366,6 +373,130 @@ eg. hours active -t ' {{task}} ({{time}}) '
},
}

tasksCmd := &cobra.Command{
Use: "tasks",
Short: "List tasks tracked by hours",
Args: cobra.MaximumNArgs(0),
RunE: func(_ *cobra.Command, _ []string) error {
return renderTasks(db, os.Stdout, tasksLimit)
},
}

trackCmd := &cobra.Command{
Use: "track",
Short: "Interact with hours' time tracking",
Args: cobra.ExactArgs(0),
}

startTrackCmd := &cobra.Command{
Use: "start <TASK_ID>",
Short: "Start tracking time for a task",
Args: cobra.ExactArgs(1),
RunE: func(_ *cobra.Command, args []string) error {
taskID, err := strconv.Atoi(args[0])
if err != nil {
return fmt.Errorf("%w: %s", errCouldntParseTaskID, err.Error())
}

var comment *string
commentTrimmed := strings.TrimSpace(trackTaskComment)
if commentTrimmed != "" {
comment = &commentTrimmed
}

return startTracking(db, os.Stdout, taskID, comment)
},
}

updateTrackCmd := &cobra.Command{
Use: "update",
Short: "Update active task log",
Args: cobra.ExactArgs(0),
RunE: func(_ *cobra.Command, _ []string) error {
var beginTS time.Time
var err error
if trackTaskBeginTS != "" {
beginTS, err = time.ParseInLocation(c.TimeFormat, trackTaskBeginTS, time.Local)
if err != nil {
return fmt.Errorf("%w: %s", errCouldntParseBeginTS, err.Error())
}
}

if beginTS.After(time.Now()) {
return c.ErrBeginTsCannotBeInTheFuture
}

var comment *string
commentTrimmed := strings.TrimSpace(trackTaskComment)
if commentTrimmed != "" {
comment = &commentTrimmed
}

return updateTracking(db, os.Stdout, beginTS, comment)
},
}

stopTrackCmd := &cobra.Command{
Use: "stop",
Short: "Stop active task log",
Args: cobra.ExactArgs(0),
RunE: func(_ *cobra.Command, _ []string) error {
errs := &c.MultiError{}

now := time.Now()
var beginTS *time.Time
if trackTaskBeginTS != "" {
ts, err := time.ParseInLocation(c.TimeFormat, trackTaskBeginTS, time.Local)
if err != nil {
errs.Add(fmt.Errorf("%w: %s", errCouldntParseBeginTS, err.Error()))
} else {
if ts.After(now) {
errs.Add(c.ErrBeginTsCannotBeInTheFuture)
} else {
beginTS = &ts
}
}
}

var endTS *time.Time
if trackTaskBeginTS != "" {
ts, err := time.ParseInLocation(c.TimeFormat, trackTaskEndTS, time.Local)
if err != nil {
errs.Add(fmt.Errorf("%w: %s", errCouldntParseEndTS, err.Error()))
} else {
if ts.After(now) {
errs.Add(c.ErrEndTsCannotBeInTheFuture)
} else {
endTS = &ts
}
}
}

if errs.NumErrors() > 1 {
return errs
} else if errs.NumErrors() == 1 {
return errs.Unwrap()
}

var comment *string
commentTrimmed := strings.TrimSpace(trackTaskComment)
if commentTrimmed != "" {
comment = &commentTrimmed
}

return stopTracking(db, os.Stdout, beginTS, endTS, comment)
},
}

activeTrackCmd := &cobra.Command{
Use: "active",
Short: "Show details about the active task log",
Args: cobra.ExactArgs(0),
RunE: func(_ *cobra.Command, _ []string) error {
return renderActiveTLDetails(db, os.Stdout)
},
}

var err error
userHomeDir, err = os.UserHomeDir()
if err != nil {
Expand All @@ -391,11 +522,29 @@ eg. hours active -t ' {{task}} ({{time}}) '

activeCmd.Flags().StringVarP(&activeTemplate, "template", "t", ui.ActiveTaskPlaceholder, "string template to use for outputting active task")

tasksCmd.Flags().UintVarP(&tasksLimit, "limit", "l", numTasksLimit, "number of tasks to output")

startTrackCmd.Flags().StringVarP(&trackTaskComment, "comment", "c", "", "task log comment")
startTrackCmd.Flags().StringVarP(&trackTaskBeginTS, "begin-ts", "b", "", "task log begin timestamp")

updateTrackCmd.Flags().StringVarP(&trackTaskComment, "comment", "c", "", "task log comment")
updateTrackCmd.Flags().StringVarP(&trackTaskBeginTS, "begin-ts", "b", "", "task log begin timestamp")

stopTrackCmd.Flags().StringVarP(&trackTaskComment, "comment", "c", "", "task log comment")
stopTrackCmd.Flags().StringVarP(&trackTaskBeginTS, "begin-ts", "b", "", "task log begin timestamp")
stopTrackCmd.Flags().StringVarP(&trackTaskEndTS, "end-ts", "e", "", "task log end timestamp")

rootCmd.AddCommand(generateCmd)
rootCmd.AddCommand(reportCmd)
rootCmd.AddCommand(logCmd)
rootCmd.AddCommand(statsCmd)
rootCmd.AddCommand(activeCmd)
rootCmd.AddCommand(tasksCmd)
trackCmd.AddCommand(startTrackCmd)
trackCmd.AddCommand(updateTrackCmd)
trackCmd.AddCommand(stopTrackCmd)
trackCmd.AddCommand(activeTrackCmd)
rootCmd.AddCommand(trackCmd)

rootCmd.CompletionOptions.DisableDefaultCmd = true

Expand Down
39 changes: 39 additions & 0 deletions cmd/tasks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package cmd

import (
"database/sql"
"encoding/json"
"fmt"
"io"

pers "github.com/dhth/hours/internal/persistence"
)

const (
tasksLimit = 500
)

func renderTasks(db *sql.DB, writer io.Writer, limit uint) error {
limitToUse := limit
if limit > tasksLimit {
limitToUse = tasksLimit
}

tasks, err := pers.FetchTasks(db, true, limitToUse)
if err != nil {
return err
}

if len(tasks) == 0 {
return nil
}

result, err := json.MarshalIndent(tasks, "", " ")
if err != nil {
return fmt.Errorf("%w: %s", errCouldntUnmarshalToJSON, err.Error())
}

fmt.Fprintln(writer, string(result))

return nil
}
124 changes: 124 additions & 0 deletions cmd/track.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package cmd

import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"io"
"time"

pers "github.com/dhth/hours/internal/persistence"
)

func renderActiveTLDetails(db *sql.DB, writer io.Writer) error {
details, err := pers.FetchActiveTaskDetails(db)
if errors.Is(err, pers.ErrNoTaskActive) {
return err
} else if err != nil {
return fmt.Errorf("%w: %w", errCouldntFetchDataFromDB, err)
}

result, err := json.MarshalIndent(details, "", " ")
if err != nil {
return fmt.Errorf("%w: %s", errCouldntUnmarshalToJSON, err.Error())
}

fmt.Fprintln(writer, string(result))

return nil
}

func startTracking(db *sql.DB, writer io.Writer, taskID int, comment *string) error {
details, err := pers.FetchActiveTaskDetails(db)
var noTaskActive bool
if errors.Is(err, pers.ErrNoTaskActive) {
noTaskActive = true
} else if err != nil {
return fmt.Errorf("%w: %w", errCouldntFetchDataFromDB, err)
}

now := time.Now()
switch noTaskActive {
case true:
_, err := pers.InsertNewTL(db, taskID, now, comment)
if err != nil {
return fmt.Errorf("%w: %w", errCouldntUpdateDataInDB, err)
}
case false:
if details.TaskID == taskID {
return errTaskAlreadyBeingTracked
}
_, err := pers.QuickSwitchActiveTL(db, taskID, now, comment)
if err != nil {
return fmt.Errorf("%w: %w", errCouldntUpdateDataInDB, err)
}
}

return renderActiveTLDetails(db, writer)
}

func updateTracking(db *sql.DB, writer io.Writer, beginTS time.Time, comment *string) error {
_, err := pers.FetchActiveTaskDetails(db)
if errors.Is(err, pers.ErrNoTaskActive) {
return err
} else if err != nil {
return fmt.Errorf("%w: %w", errCouldntFetchDataFromDB, err)
}

err = pers.EditActiveTL(db, beginTS, comment)
if err != nil {
return fmt.Errorf("%w: %w", errCouldntUpdateDataInDB, err)
}

return renderActiveTLDetails(db, writer)
}

func stopTracking(db *sql.DB, writer io.Writer, beginTS, endTS *time.Time, comment *string) error {
details, err := pers.FetchActiveTaskDetails(db)
if errors.Is(err, pers.ErrNoTaskActive) {
return err
} else if err != nil {
return fmt.Errorf("%w: %w", errCouldntFetchDataFromDB, err)
}

var bTS time.Time
if beginTS == nil {
bTS = details.CurrentLogBeginTS
} else {
bTS = *beginTS
}

var eTS time.Time
if endTS == nil {
eTS = time.Now()
} else {
eTS = *endTS
}

var commentToUse *string
if comment == nil {
commentToUse = details.CurrentLogComment
} else {
commentToUse = comment
}

err = pers.FinishActiveTL(db, details.TaskID, bTS, eTS, commentToUse)
if err != nil {
return fmt.Errorf("%w: %w", errCouldntUpdateDataInDB, err)
}

tlDetails, err := pers.FetchTLByID(db, details.TLID)
if err != nil {
return fmt.Errorf("%w: %w", errCouldntFetchDataFromDB, err)
}

result, err := json.MarshalIndent(tlDetails, "", " ")
if err != nil {
return fmt.Errorf("%w: %s", errCouldntUnmarshalToJSON, err.Error())
}

fmt.Fprintln(writer, string(result))

return nil
}
1 change: 1 addition & 0 deletions internal/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ package common
const (
Author = "@dhth"
RepoIssuesURL = "https://github.com/dhth/hours/issues"
TimeFormat = "2006/01/02 15:04"
)
Loading

0 comments on commit 7ea594a

Please sign in to comment.