Initial Commit
This commit is contained in:
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					gime-flat
 | 
				
			||||||
							
								
								
									
										184
									
								
								helpers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										184
									
								
								helpers.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,184 @@
 | 
				
			|||||||
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func GetRoundToDuration() time.Duration {
 | 
				
			||||||
 | 
						var dur time.Duration
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						if dur, err = time.ParseDuration(cfg.Get("roundto")); err != nil {
 | 
				
			||||||
 | 
							cfg.Set("roundto", DefRoundTo)
 | 
				
			||||||
 | 
							dur, _ = time.ParseDuration(DefRoundTo)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return dur
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func DurationToDecimal(dur time.Duration) float64 {
 | 
				
			||||||
 | 
						mins := dur.Minutes() - (dur.Hours() * 60)
 | 
				
			||||||
 | 
						return dur.Hours() + (mins / 60)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// pullRemoveTagsFromArgs takes a list of arguments, removes all 'remove tags' from them
 | 
				
			||||||
 | 
					// then returns the tags and the remaining args
 | 
				
			||||||
 | 
					func pullRemoveTagsFromArgs(args []string) ([]string, []string) {
 | 
				
			||||||
 | 
						var tags, rem []string
 | 
				
			||||||
 | 
						for _, opt := range args {
 | 
				
			||||||
 | 
							if opt[0] == '-' {
 | 
				
			||||||
 | 
								tags = append(tags, opt[1:])
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								rem = append(rem, opt)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return tags, rem
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// pullTagsFromArgs takes a list of arguments, removes all tags from them
 | 
				
			||||||
 | 
					// then returns the tags and the remaining args
 | 
				
			||||||
 | 
					func pullTagsFromArgs(args []string) ([]string, []string) {
 | 
				
			||||||
 | 
						var tags, rem []string
 | 
				
			||||||
 | 
						for _, opt := range args {
 | 
				
			||||||
 | 
							if opt[0] == '+' {
 | 
				
			||||||
 | 
								tags = append(tags, opt[1:])
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								rem = append(rem, opt)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return tags, rem
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func parseFuzzyTime(t string) (time.Time, error) {
 | 
				
			||||||
 | 
						var ret time.Time
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						for i := range fuzzyFormats {
 | 
				
			||||||
 | 
							ret, err = time.Parse(fuzzyFormats[i], t)
 | 
				
			||||||
 | 
							if err == nil {
 | 
				
			||||||
 | 
								// Make sure it's in the local timezone
 | 
				
			||||||
 | 
								tz := time.Now().Format("Z07:00")
 | 
				
			||||||
 | 
								t = ret.Format("2006-01-02T15:04:05") + tz
 | 
				
			||||||
 | 
								if ret, err = time.Parse(time.RFC3339, t); err != nil {
 | 
				
			||||||
 | 
									return ret, err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								// Check for zero on year/mo/day
 | 
				
			||||||
 | 
								if ret.Year() == 0 && ret.Month() == time.January && ret.Day() == 1 {
 | 
				
			||||||
 | 
									ret = ret.AddDate(time.Now().Year(), int(time.Now().Month())-1, time.Now().Day()-1)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return ret, nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return time.Time{}, errors.New("Unable to parse time: " + t)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func friendlyFormatForTime(t time.Time) string {
 | 
				
			||||||
 | 
						nowTime := time.Now()
 | 
				
			||||||
 | 
						if t.Year() != nowTime.Year() || t.Month() != nowTime.Month() {
 | 
				
			||||||
 | 
							return "2006-01-02 15:04"
 | 
				
			||||||
 | 
						} else if t.Day() != nowTime.Day() {
 | 
				
			||||||
 | 
							return "01/02 15:04"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return "15:04"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// timeToFriendlyString returns an easier to read version of the time
 | 
				
			||||||
 | 
					// giving enough details that the user should be fine inferring the rest
 | 
				
			||||||
 | 
					func timeToFriendlyString(t time.Time) string {
 | 
				
			||||||
 | 
						return t.Format(friendlyFormatForTime(t))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func sinceToString(tm time.Time) string {
 | 
				
			||||||
 | 
						return diffToString(tm, time.Now())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func diffToString(tm1, tm2 time.Time) string {
 | 
				
			||||||
 | 
						ret := ""
 | 
				
			||||||
 | 
						yr, mo, dy, hr, mn, sc := diff(tm1, tm2)
 | 
				
			||||||
 | 
						higher := false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if yr > 0 {
 | 
				
			||||||
 | 
							ret += fmt.Sprintf("%4dy ", yr)
 | 
				
			||||||
 | 
							higher = true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if mo > 0 || higher {
 | 
				
			||||||
 | 
							ret += fmt.Sprintf("%2dm ", mo)
 | 
				
			||||||
 | 
							higher = true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if dy > 0 || higher {
 | 
				
			||||||
 | 
							ret += fmt.Sprintf("%2dd ", dy)
 | 
				
			||||||
 | 
							higher = true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if hr > 0 || higher {
 | 
				
			||||||
 | 
							ret += fmt.Sprintf("%2dh ", hr)
 | 
				
			||||||
 | 
							higher = true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if mn > 0 || higher {
 | 
				
			||||||
 | 
							ret += fmt.Sprintf("%2dm ", mn)
 | 
				
			||||||
 | 
							higher = true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if sc > 0 || higher {
 | 
				
			||||||
 | 
							ret += fmt.Sprintf("%2ds", sc)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return ret
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func padRight(st string, l int) string {
 | 
				
			||||||
 | 
						for len(st) < l {
 | 
				
			||||||
 | 
							st = st + " "
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return st
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func padLeft(st string, l int) string {
 | 
				
			||||||
 | 
						for len(st) < l {
 | 
				
			||||||
 | 
							st = " " + st
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return st
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func diff(a, b time.Time) (year, month, day, hour, min, sec int) {
 | 
				
			||||||
 | 
						if a.Location() != b.Location() {
 | 
				
			||||||
 | 
							b = b.In(a.Location())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if a.After(b) {
 | 
				
			||||||
 | 
							a, b = b, a
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						y1, M1, d1 := a.Date()
 | 
				
			||||||
 | 
						y2, M2, d2 := b.Date()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						h1, m1, s1 := a.Clock()
 | 
				
			||||||
 | 
						h2, m2, s2 := b.Clock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						year = int(y2 - y1)
 | 
				
			||||||
 | 
						month = int(M2 - M1)
 | 
				
			||||||
 | 
						day = int(d2 - d1)
 | 
				
			||||||
 | 
						hour = int(h2 - h1)
 | 
				
			||||||
 | 
						min = int(m2 - m1)
 | 
				
			||||||
 | 
						sec = int(s2 - s1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Normalize negative values
 | 
				
			||||||
 | 
						if sec < 0 {
 | 
				
			||||||
 | 
							sec += 60
 | 
				
			||||||
 | 
							min--
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if min < 0 {
 | 
				
			||||||
 | 
							min += 60
 | 
				
			||||||
 | 
							hour--
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if hour < 0 {
 | 
				
			||||||
 | 
							hour += 24
 | 
				
			||||||
 | 
							day--
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if day < 0 {
 | 
				
			||||||
 | 
							// days in month:
 | 
				
			||||||
 | 
							t := time.Date(y1, M1, 32, 0, 0, 0, 0, time.UTC)
 | 
				
			||||||
 | 
							day += 32 - t.Day()
 | 
				
			||||||
 | 
							month--
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if month < 0 {
 | 
				
			||||||
 | 
							month += 12
 | 
				
			||||||
 | 
							year--
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										339
									
								
								main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										339
									
								
								main.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,339 @@
 | 
				
			|||||||
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bufio"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						userConfig "github.com/br0xen/user-config"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						AppName    = "gime-flat"
 | 
				
			||||||
 | 
						AppVersion = 0.1
 | 
				
			||||||
 | 
						DefDBName  = "./gime"
 | 
				
			||||||
 | 
						DefRoundTo = "1m0s"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var validOperations map[string][]string
 | 
				
			||||||
 | 
					var opFuncs map[string]func([]string) int
 | 
				
			||||||
 | 
					var cfg *userConfig.Config
 | 
				
			||||||
 | 
					var roundTo time.Duration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var fuzzyFormats []string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func main() {
 | 
				
			||||||
 | 
						var ret int
 | 
				
			||||||
 | 
						initialize()
 | 
				
			||||||
 | 
						var parms []string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(os.Args) > 1 {
 | 
				
			||||||
 | 
							parms = os.Args[1:]
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							// If no parameters were passed, just print the status
 | 
				
			||||||
 | 
							parms = append(parms, "status")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if fn, ok := opFuncs[parms[0]]; ok {
 | 
				
			||||||
 | 
							ret = fn(parms[1:])
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							fmt.Println("Unknown command")
 | 
				
			||||||
 | 
							ret = 1
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						os.Exit(ret)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func cmdDoConfig(args []string) int {
 | 
				
			||||||
 | 
						if len(args) == 0 {
 | 
				
			||||||
 | 
							fmt.Println("Invalid configuration options passed")
 | 
				
			||||||
 | 
							return 1
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, opt := range args {
 | 
				
			||||||
 | 
							if !strings.Contains(opt, "=") {
 | 
				
			||||||
 | 
								// Single word triggers
 | 
				
			||||||
 | 
								switch opt {
 | 
				
			||||||
 | 
								case "reset":
 | 
				
			||||||
 | 
									fmt.Println("Are you sure you want to reset your configuration? (Y/[N])")
 | 
				
			||||||
 | 
									reader := bufio.NewReader(os.Stdin)
 | 
				
			||||||
 | 
									conf, _ := reader.ReadString('\n')
 | 
				
			||||||
 | 
									conf = strings.TrimSpace(conf)
 | 
				
			||||||
 | 
									if conf == "Y" {
 | 
				
			||||||
 | 
										fmt.Println("Resetting Configuration...")
 | 
				
			||||||
 | 
										cfg.Set("dbdir", cfg.GetConfigPath()+string(os.PathSeparator))
 | 
				
			||||||
 | 
										cfg.Set("dbname", DefDBName)
 | 
				
			||||||
 | 
										cfg.Set("roundto", DefRoundTo)
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										fmt.Println("Done.")
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									return 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								case "list":
 | 
				
			||||||
 | 
									fmt.Println("Current " + AppName + " config")
 | 
				
			||||||
 | 
									for _, v := range cfg.GetKeyList() {
 | 
				
			||||||
 | 
										fmt.Println("  " + v + ": " + cfg.Get(v))
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								case "dbpath":
 | 
				
			||||||
 | 
									fmt.Println(cfg.Get("dbdir"))
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								// Key=Value options
 | 
				
			||||||
 | 
								pts := strings.Split(opt, "=")
 | 
				
			||||||
 | 
								if len(pts) == 2 {
 | 
				
			||||||
 | 
									switch pts[0] {
 | 
				
			||||||
 | 
									case "dbdir":
 | 
				
			||||||
 | 
										val := pts[1]
 | 
				
			||||||
 | 
										if val[len(val)-1] != os.PathSeparator {
 | 
				
			||||||
 | 
											val = val + string(os.PathSeparator)
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										cfg.Set("dbdir", val)
 | 
				
			||||||
 | 
									case "dbname":
 | 
				
			||||||
 | 
										cfg.Set("dbname", pts[1])
 | 
				
			||||||
 | 
									case "dbarchname":
 | 
				
			||||||
 | 
										cfg.Set("dbarchname", pts[1])
 | 
				
			||||||
 | 
									case "roundto":
 | 
				
			||||||
 | 
										// Make sure that we can parse it
 | 
				
			||||||
 | 
										durStr := pts[1]
 | 
				
			||||||
 | 
										_, err := time.ParseDuration(durStr)
 | 
				
			||||||
 | 
										if err != nil {
 | 
				
			||||||
 | 
											fmt.Println("Unable to parse duration:", durStr)
 | 
				
			||||||
 | 
											durStr = DefRoundTo
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										cfg.Set("roundto", durStr)
 | 
				
			||||||
 | 
										fmt.Println("Rounding set to", durStr)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return 0
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func cmdPrintHelp(args []string) int {
 | 
				
			||||||
 | 
						if len(args) == 0 {
 | 
				
			||||||
 | 
							fmt.Println("gime - A simple timekeeping application\n")
 | 
				
			||||||
 | 
							fmt.Println("Usage: gime [@timerID] [operation] [tags...]")
 | 
				
			||||||
 | 
							for _, v := range validOperations {
 | 
				
			||||||
 | 
								for vi := range v {
 | 
				
			||||||
 | 
									fmt.Println("  ", v[vi])
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								fmt.Println("")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							switch args[0] {
 | 
				
			||||||
 | 
							case "formats":
 | 
				
			||||||
 | 
								fmt.Println("Supported date/time formats:")
 | 
				
			||||||
 | 
								for i := range fuzzyFormats {
 | 
				
			||||||
 | 
									fmt.Println(" ", fuzzyFormats[i])
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							fmt.Println("")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return 0
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func cmdDoFuzzyParse(args []string) int {
 | 
				
			||||||
 | 
						if len(args) == 0 {
 | 
				
			||||||
 | 
							return 1
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						var t time.Time
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						if t, err = parseFuzzyTime(args[0]); err != nil {
 | 
				
			||||||
 | 
							fmt.Println(err.Error())
 | 
				
			||||||
 | 
							return 1
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						fmt.Println(t.Format(time.RFC3339))
 | 
				
			||||||
 | 
						return 0
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func initialize() {
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						validOperations = make(map[string][]string)
 | 
				
			||||||
 | 
						opFuncs = make(map[string]func([]string) int)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						opFuncs["add"] = cmdAddTimer
 | 
				
			||||||
 | 
						validOperations["add"] = []string{
 | 
				
			||||||
 | 
							"add [duration] [+tags]  - Add a timer for the given duration",
 | 
				
			||||||
 | 
							"                          with the given tags",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						opFuncs["cont"] = cmdContinueTimer
 | 
				
			||||||
 | 
						validOperations["cont"] = []string{
 | 
				
			||||||
 | 
							"cont [time]             - Continue the last stopped timer",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						opFuncs["config"] = cmdDoConfig
 | 
				
			||||||
 | 
						validOperations["config"] = []string{
 | 
				
			||||||
 | 
							"config [command]        - Perform configuration",
 | 
				
			||||||
 | 
							"       list             - List current configuration",
 | 
				
			||||||
 | 
							"       reset            - Reset current configuration",
 | 
				
			||||||
 | 
							"       Configuration Options:",
 | 
				
			||||||
 | 
							"         dbdir=[database directory]",
 | 
				
			||||||
 | 
							"         dbname=[database filename]",
 | 
				
			||||||
 | 
							"         dbarchname=[archive database filename]",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						opFuncs["detail"] = cmdPrintDetail
 | 
				
			||||||
 | 
						validOperations["detail"] = []string{
 | 
				
			||||||
 | 
							"detail @id              - Print details about a timer",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						opFuncs["delete"] = cmdDeleteTimer
 | 
				
			||||||
 | 
						validOperations["delete"] = []string{
 | 
				
			||||||
 | 
							"delete uuid             - Delete a timer",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						opFuncs["end"] = cmdStopTimer
 | 
				
			||||||
 | 
						validOperations["end"] = []string{
 | 
				
			||||||
 | 
							"end                     - The same as stop",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						opFuncs["help"] = cmdPrintHelp
 | 
				
			||||||
 | 
						validOperations["help"] = []string{
 | 
				
			||||||
 | 
							"help                    - Print this",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						opFuncs["list"] = cmdPrintList
 | 
				
			||||||
 | 
						validOperations["list"] = []string{
 | 
				
			||||||
 | 
							"list [duration] [+tags] - List time entries",
 | 
				
			||||||
 | 
							"                          valid values of [duration] include:",
 | 
				
			||||||
 | 
							"                            :day   - List all entries for the current day",
 | 
				
			||||||
 | 
							"                            :week  - List all entries for the current week",
 | 
				
			||||||
 | 
							"                            :month - List all entries for the current month",
 | 
				
			||||||
 | 
							"                            :year  - List all entries for the current year",
 | 
				
			||||||
 | 
							"                            Or other date values, we'll try to parse it.",
 | 
				
			||||||
 | 
							"                          To list entries by tag, preceed the tags with a +",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						opFuncs["ls"] = cmdPrintList
 | 
				
			||||||
 | 
						validOperations["ls"] = []string{
 | 
				
			||||||
 | 
							"ls [duration] [+tags]   - The same as list",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						opFuncs["modify"] = cmdModifyTimer
 | 
				
			||||||
 | 
						validOperations["modify"] = []string{
 | 
				
			||||||
 | 
							"modify [+tags]          - Modify a timer",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						opFuncs["mod"] = cmdModifyTimer
 | 
				
			||||||
 | 
						validOperations["mod"] = []string{
 | 
				
			||||||
 | 
							"mod [+tags]             - Modify a timer",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						opFuncs["remove"] = cmdDeleteTimer
 | 
				
			||||||
 | 
						validOperations["remove"] = []string{
 | 
				
			||||||
 | 
							"remove uuid             - See 'delete'",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						opFuncs["rm"] = cmdDeleteTimer
 | 
				
			||||||
 | 
						validOperations["rm"] = []string{
 | 
				
			||||||
 | 
							"rm uuid                 - See 'delete'",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						opFuncs["status"] = cmdPrintStatus
 | 
				
			||||||
 | 
						validOperations["status"] = []string{
 | 
				
			||||||
 | 
							"status                  - Print the status of all active timers",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						opFuncs["start"] = cmdStartTimer
 | 
				
			||||||
 | 
						validOperations["start"] = []string{
 | 
				
			||||||
 | 
							"start [time] [+tags] - Start a timer with the given tags (space separated)",
 | 
				
			||||||
 | 
							"                          If the first sub-argument given looks like a time,",
 | 
				
			||||||
 | 
							"                          the timer will be started then (past or future).",
 | 
				
			||||||
 | 
							"                          If a timer is already running it'll be stopped",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						opFuncs["stop"] = cmdStopTimer
 | 
				
			||||||
 | 
						validOperations["stop"] = []string{
 | 
				
			||||||
 | 
							"stop [time]             - Stops the current timer",
 | 
				
			||||||
 | 
							"                          If the first sub-argument given looks like a time,",
 | 
				
			||||||
 | 
							"                          the timer will be stopped then (past or future).",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						opFuncs["switch"] = cmdSwitchTimer
 | 
				
			||||||
 | 
						validOperations["switch"] = []string{
 | 
				
			||||||
 | 
							"switch [+tags]       - Stop all currently running timers and start a new",
 | 
				
			||||||
 | 
							"                          one with the given tags",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						opFuncs["fuzzyparse"] = cmdDoFuzzyParse
 | 
				
			||||||
 | 
						validOperations["fuzzyparse"] = []string{
 | 
				
			||||||
 | 
							"fuzzyparse              - Parse the next argument as a date/time and print",
 | 
				
			||||||
 | 
							"                          the RFC3339 result. (Basically for testing)",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						opFuncs["tags"] = cmdManageTag
 | 
				
			||||||
 | 
						validOperations["tags"] = []string{
 | 
				
			||||||
 | 
							"tags                    - Same as 'tag'",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						opFuncs["tag"] = cmdManageTag
 | 
				
			||||||
 | 
						validOperations["tag"] = []string{
 | 
				
			||||||
 | 
							"tag [+name [:[non]bill] [+newname]] - Manage tags",
 | 
				
			||||||
 | 
							"																	 If no tag is requested, list all tags",
 | 
				
			||||||
 | 
							"                                  If tag is requested and:",
 | 
				
			||||||
 | 
							"                                    newname is given, rename the tag",
 | 
				
			||||||
 | 
							"                                    :non[[bill]able] - set tag to non-billable",
 | 
				
			||||||
 | 
							"                                    :bill[able]    - set tag to billable",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Load the Config
 | 
				
			||||||
 | 
						cfg, err = userConfig.NewConfig(AppName)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							fmt.Println(err.Error())
 | 
				
			||||||
 | 
							fmt.Println("Creating new config")
 | 
				
			||||||
 | 
							cfg.Save()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// If dbdir isn't set, set it to the config directory
 | 
				
			||||||
 | 
						if cfg.Get("dbdir") == "" {
 | 
				
			||||||
 | 
							cfg.Set("dbdir", cfg.GetConfigPath()+"/")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// If dbname isn't set, set it to the default database filename
 | 
				
			||||||
 | 
						if cfg.Get("dbname") == "" {
 | 
				
			||||||
 | 
							cfg.Set("dbname", DefDBName)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						/*
 | 
				
			||||||
 | 
							if gdb, err = gime.LoadDatabase(cfg.Get("dbdir"), cfg.Get("dbname"), cfg.Get("dbarchname")); err != nil {
 | 
				
			||||||
 | 
								fmt.Println("Error loading the database")
 | 
				
			||||||
 | 
								os.Exit(1)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						*/
 | 
				
			||||||
 | 
						if _, err := time.ParseDuration(cfg.Get("roundto")); err != nil {
 | 
				
			||||||
 | 
							cfg.Set("roundto", DefRoundTo)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fuzzyFormats = []string{
 | 
				
			||||||
 | 
							"1504",
 | 
				
			||||||
 | 
							"15:04", // Kitchen, 24hr
 | 
				
			||||||
 | 
							time.Kitchen,
 | 
				
			||||||
 | 
							time.RFC3339,
 | 
				
			||||||
 | 
							"2006-01-02T15:04:05", // RFC3339 without timezone
 | 
				
			||||||
 | 
							"2006-01-02T15:04",    // RFC3339 without seconds or timezone
 | 
				
			||||||
 | 
							time.Stamp,
 | 
				
			||||||
 | 
							"02 Jan 06 15:04:05", // RFC822 with second
 | 
				
			||||||
 | 
							time.RFC822,
 | 
				
			||||||
 | 
							"01/02/2006 15:04",    // U.S. Format
 | 
				
			||||||
 | 
							"01/02/2006 15:04:05", // U.S. Format with seconds
 | 
				
			||||||
 | 
							"01/02/06 15:04",      // U.S. Format, short year
 | 
				
			||||||
 | 
							"01/02/06 15:04:05",   // U.S. Format, short year, with seconds
 | 
				
			||||||
 | 
							"2006-01-02",
 | 
				
			||||||
 | 
							"2006-01-02 15:04",
 | 
				
			||||||
 | 
							"2006-01-02 15:04:05",
 | 
				
			||||||
 | 
							"20060102",
 | 
				
			||||||
 | 
							"20060102 15:04",
 | 
				
			||||||
 | 
							"20060102 15:04:05",
 | 
				
			||||||
 | 
							"20060102 1504",
 | 
				
			||||||
 | 
							"20060102 150405",
 | 
				
			||||||
 | 
							"20060102T15:04",
 | 
				
			||||||
 | 
							"20060102T15:04:05",
 | 
				
			||||||
 | 
							"20060102T1504",
 | 
				
			||||||
 | 
							"20060102T150405",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func assertError(err error) {
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							fmt.Println(err.Error())
 | 
				
			||||||
 | 
							os.Exit(1)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										127
									
								
								model.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								model.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,127 @@
 | 
				
			|||||||
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						scribble "github.com/nanobox-io/golang-scribble"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func SaveTimer(entry *Timer) error {
 | 
				
			||||||
 | 
						db, err := scribble.New(cfg.Get("dbdir"), nil)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							fmt.Println(err.Error())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						yr := strconv.Itoa(entry.GetStart().Year())
 | 
				
			||||||
 | 
						key := entry.GetUUID()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Make sure that this entry isn't saved anywhere
 | 
				
			||||||
 | 
						db.Delete("active", key)
 | 
				
			||||||
 | 
						db.Delete(yr, key)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Now write it
 | 
				
			||||||
 | 
						if entry.IsActive() {
 | 
				
			||||||
 | 
							if err = db.Write("active", key, entry); err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							if err = db.Write(yr, key, entry); err != nil {
 | 
				
			||||||
 | 
								// Error writing the time entry, don't edit metadata
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var m *Meta
 | 
				
			||||||
 | 
						m, err = LoadMetadata()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							m = new(Meta)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						m.AddYear(entry.GetStart().Year())
 | 
				
			||||||
 | 
						m.AddTags(entry.GetTags())
 | 
				
			||||||
 | 
						return SaveMetadata(m)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func LoadTimer(uuid string) *Timer {
 | 
				
			||||||
 | 
						db, err := scribble.New(cfg.Get("dbdir"), nil)
 | 
				
			||||||
 | 
						timer := Timer{}
 | 
				
			||||||
 | 
						// Find the timer, see if it's active first
 | 
				
			||||||
 | 
						if err := db.Read("active", uuid, &timer); err == nil {
 | 
				
			||||||
 | 
							return &timer
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// Ok, we have to check the years
 | 
				
			||||||
 | 
						var m *Meta
 | 
				
			||||||
 | 
						if m, err = LoadMetadata(); err != nil {
 | 
				
			||||||
 | 
							// Couldn't load the metadata...
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for _, v := range m.Years {
 | 
				
			||||||
 | 
							if err = db.Read(strconv.Itoa(v), uuid, &timer); err == nil {
 | 
				
			||||||
 | 
								return &timer
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// Didn't find it
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func DeleteTimer(uuid string) error {
 | 
				
			||||||
 | 
						db, err := scribble.New(cfg.Get("dbdir"), nil)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						tmr := LoadTimer(uuid)
 | 
				
			||||||
 | 
						if tmr.IsActive() {
 | 
				
			||||||
 | 
							return db.Delete("active", uuid)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						yr := strconv.Itoa(tmr.GetStart().Year())
 | 
				
			||||||
 | 
						return db.Delete(yr, uuid)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func LoadActiveTimers() []Timer {
 | 
				
			||||||
 | 
						return LoadTimersFromDb("active")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func LoadTimers(st, end time.Time) []Timer {
 | 
				
			||||||
 | 
						var ret []Timer
 | 
				
			||||||
 | 
						stYr := st.Year()
 | 
				
			||||||
 | 
						endYr := end.Year()
 | 
				
			||||||
 | 
						m, err := LoadMetadata()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return ret
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for i := stYr; i <= endYr; i++ {
 | 
				
			||||||
 | 
							if m.HasYear(i) {
 | 
				
			||||||
 | 
								tmrs := LoadTimersFromDb(strconv.Itoa(i))
 | 
				
			||||||
 | 
								for _, v := range tmrs {
 | 
				
			||||||
 | 
									if v.GetStart().After(st) && v.GetStart().Before(end) {
 | 
				
			||||||
 | 
										ret = append(ret, v)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return ret
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func LoadTimersFromDb(coll string) []Timer {
 | 
				
			||||||
 | 
						db, _ := scribble.New(cfg.Get("dbdir"), nil)
 | 
				
			||||||
 | 
						raw, _ := db.ReadAll(coll)
 | 
				
			||||||
 | 
						var timers []Timer
 | 
				
			||||||
 | 
						for _, tmr := range raw {
 | 
				
			||||||
 | 
							t := Timer{}
 | 
				
			||||||
 | 
							json.Unmarshal([]byte(tmr), &t)
 | 
				
			||||||
 | 
							timers = append(timers, t)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return timers
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func FilterTimers(tmrs []Timer, filter func(t *Timer) bool) []Timer {
 | 
				
			||||||
 | 
						var ret []Timer
 | 
				
			||||||
 | 
						for _, v := range tmrs {
 | 
				
			||||||
 | 
							if filter(&v) {
 | 
				
			||||||
 | 
								ret = append(ret, v)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return ret
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										109
									
								
								model_meta.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								model_meta.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,109 @@
 | 
				
			|||||||
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"sort"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						scribble "github.com/nanobox-io/golang-scribble"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Meta struct {
 | 
				
			||||||
 | 
						Years []int
 | 
				
			||||||
 | 
						Tags  []Tag
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func LoadMetadata() (*Meta, error) {
 | 
				
			||||||
 | 
						db, err := scribble.New(cfg.Get("dbdir"), nil)
 | 
				
			||||||
 | 
						var m *Meta
 | 
				
			||||||
 | 
						err = db.Read("meta", "data", &m)
 | 
				
			||||||
 | 
						return m, err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func SaveMetadata(m *Meta) error {
 | 
				
			||||||
 | 
						db, err := scribble.New(cfg.Get("dbdir"), nil)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return db.Write("meta", "data", m)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (m *Meta) AddYear(yr int) {
 | 
				
			||||||
 | 
						for _, v := range m.Years {
 | 
				
			||||||
 | 
							if v == yr {
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						m.Years = append(m.Years, yr)
 | 
				
			||||||
 | 
						// Sort the years
 | 
				
			||||||
 | 
						sort.Slice(m.Years, func(i, j int) bool {
 | 
				
			||||||
 | 
							return m.Years[i] > m.Years[j]
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (m *Meta) HasYear(yr int) bool {
 | 
				
			||||||
 | 
						for _, v := range m.Years {
 | 
				
			||||||
 | 
							if v == yr {
 | 
				
			||||||
 | 
								return true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (m *Meta) AddTags(tgs []string) {
 | 
				
			||||||
 | 
						for _, tg := range tgs {
 | 
				
			||||||
 | 
							if tag := m.GetTag(tg); tag == nil {
 | 
				
			||||||
 | 
								m.SaveTag(NewTag(tg, false))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// sort the tags
 | 
				
			||||||
 | 
						sort.Slice(m.Tags, func(i, j int) bool {
 | 
				
			||||||
 | 
							return m.Tags[i].Name > m.Tags[j].Name
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (m *Meta) SaveTag(t *Tag) {
 | 
				
			||||||
 | 
						for i, v := range m.Tags {
 | 
				
			||||||
 | 
							if v.Name == t.Name {
 | 
				
			||||||
 | 
								m.Tags[i].Billable = t.Billable
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						m.Tags = append(m.Tags, *t)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (m *Meta) GetTag(uuid string) *Tag {
 | 
				
			||||||
 | 
						for _, v := range m.Tags {
 | 
				
			||||||
 | 
							if v.UUID == uuid {
 | 
				
			||||||
 | 
								return &v
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (m *Meta) GetTags(uuids []string) []Tag {
 | 
				
			||||||
 | 
						var ret []Tag
 | 
				
			||||||
 | 
						for _, v := range uuids {
 | 
				
			||||||
 | 
							if t := m.GetTag(v); t != nil {
 | 
				
			||||||
 | 
								ret = append(ret, *t)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return ret
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (m *Meta) GetTagByName(nm string) *Tag {
 | 
				
			||||||
 | 
						for _, v := range m.Tags {
 | 
				
			||||||
 | 
							if v.Name == nm {
 | 
				
			||||||
 | 
								return &v
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (m *Meta) GetTagsByName(nms []string) []Tag {
 | 
				
			||||||
 | 
						var ret []Tag
 | 
				
			||||||
 | 
						for _, v := range nms {
 | 
				
			||||||
 | 
							if t := m.GetTagByName(v); t != nil {
 | 
				
			||||||
 | 
								ret = append(ret, *t)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return ret
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										29
									
								
								model_tag.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								model_tag.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
				
			|||||||
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import "github.com/pborman/uuid"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Tag struct {
 | 
				
			||||||
 | 
						UUID     string
 | 
				
			||||||
 | 
						Name     string
 | 
				
			||||||
 | 
						Billable bool
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func NewTag(nm string, bill bool) *Tag {
 | 
				
			||||||
 | 
						t := Tag{UUID: uuid.New(), Name: nm, Billable: bill}
 | 
				
			||||||
 | 
						return &t
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (t *Tag) String() string {
 | 
				
			||||||
 | 
						ret := "{ " + t.Name + ", "
 | 
				
			||||||
 | 
						if t.Billable {
 | 
				
			||||||
 | 
							ret += "Billable"
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							ret += "Nonbillable"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						ret += " (" + t.UUID + ") }"
 | 
				
			||||||
 | 
						return ret
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (t *Tag) SetBillable(v bool) {
 | 
				
			||||||
 | 
						t.Billable = v
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										66
									
								
								ops_tag.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								ops_tag.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,66 @@
 | 
				
			|||||||
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func cmdManageTag(args []string) int {
 | 
				
			||||||
 | 
						if len(args) == 0 {
 | 
				
			||||||
 | 
							return cmdListTags(args)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return cmdEditTag(args)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func cmdListTags(args []string) int {
 | 
				
			||||||
 | 
						m, err := LoadMetadata()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							fmt.Println("Error loading tags")
 | 
				
			||||||
 | 
							return 1
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fmt.Println("[")
 | 
				
			||||||
 | 
						for _, v := range m.Tags {
 | 
				
			||||||
 | 
							fmt.Println("  " + v.String())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						fmt.Println("]")
 | 
				
			||||||
 | 
						return 0
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func cmdEditTag(args []string) int {
 | 
				
			||||||
 | 
						m, err := LoadMetadata()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							fmt.Println("Error loading tags")
 | 
				
			||||||
 | 
							return 1
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// The first argument should be the tag name that we want to edit
 | 
				
			||||||
 | 
						tagName := args[0]
 | 
				
			||||||
 | 
						if tagName[0] == '+' {
 | 
				
			||||||
 | 
							tagName = tagName[1:]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						tg := m.GetTag(tagName)
 | 
				
			||||||
 | 
						if tg == nil {
 | 
				
			||||||
 | 
							tg = NewTag(tagName, false)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for _, v := range args[1:] {
 | 
				
			||||||
 | 
							switch v[0] {
 | 
				
			||||||
 | 
							case '+':
 | 
				
			||||||
 | 
								// Altering tag name
 | 
				
			||||||
 | 
								tg.Name = v[1:]
 | 
				
			||||||
 | 
							case ':':
 | 
				
			||||||
 | 
								// Altering billable flag
 | 
				
			||||||
 | 
								if strings.HasPrefix(v, ":non") {
 | 
				
			||||||
 | 
									tg.SetBillable(false)
 | 
				
			||||||
 | 
								} else if strings.HasPrefix(v, ":bill") {
 | 
				
			||||||
 | 
									tg.SetBillable(true)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						m.SaveTag(tg)
 | 
				
			||||||
 | 
						if err = SaveMetadata(m); err != nil {
 | 
				
			||||||
 | 
							fmt.Println("Error saving metadata: " + err.Error())
 | 
				
			||||||
 | 
							return 1
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						fmt.Println(tg.String())
 | 
				
			||||||
 | 
						return 0
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										306
									
								
								ops_timer.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										306
									
								
								ops_timer.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,306 @@
 | 
				
			|||||||
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/pborman/uuid"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func cmdStartTimer(args []string) int {
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						var entry *Timer
 | 
				
			||||||
 | 
						// By default we start the timer now
 | 
				
			||||||
 | 
						tm := time.Now()
 | 
				
			||||||
 | 
						tags, rem := pullTagsFromArgs(args)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(rem) > 0 {
 | 
				
			||||||
 | 
							// Check if the first argument looks like a date/time
 | 
				
			||||||
 | 
							tm, err = parseFuzzyTime(rem[0])
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						entry = NewTimer(tm, time.Time{})
 | 
				
			||||||
 | 
						entry.SetTags(tags)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err = SaveTimer(entry); err != nil {
 | 
				
			||||||
 | 
							fmt.Println(err)
 | 
				
			||||||
 | 
							return 1
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fmt.Println("Started:", entry.FriendlyString())
 | 
				
			||||||
 | 
						return 0
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func cmdAddTimer(args []string) int {
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						var entry *Timer
 | 
				
			||||||
 | 
						// By default we're starting the time now
 | 
				
			||||||
 | 
						tags, rem := pullTagsFromArgs(args)
 | 
				
			||||||
 | 
						var beg, end time.Time
 | 
				
			||||||
 | 
						for _, opt := range rem {
 | 
				
			||||||
 | 
							var tmpBeg, tmpEnd time.Time
 | 
				
			||||||
 | 
							if strings.Contains(opt, "-") {
 | 
				
			||||||
 | 
								pts := strings.Split(opt, "-")
 | 
				
			||||||
 | 
								if len(pts[0]) > 0 {
 | 
				
			||||||
 | 
									// This should be the starting date
 | 
				
			||||||
 | 
									tmpBeg, err = parseFuzzyTime(pts[0])
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										continue
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if len(pts[1]) > 0 {
 | 
				
			||||||
 | 
									// This should be the ending date
 | 
				
			||||||
 | 
									tmpEnd, err = parseFuzzyTime(pts[1])
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										continue
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if !tmpBeg.IsZero() || !tmpEnd.IsZero() {
 | 
				
			||||||
 | 
								beg, end = tmpBeg, tmpEnd
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if end.IsZero() {
 | 
				
			||||||
 | 
							end = time.Now()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						entry = NewTimer(beg, end)
 | 
				
			||||||
 | 
						fmt.Println("Adding Timer:", entry.FriendlyString())
 | 
				
			||||||
 | 
						entry.Tags = tags
 | 
				
			||||||
 | 
						if err = SaveTimer(entry); err != nil {
 | 
				
			||||||
 | 
							fmt.Println(err)
 | 
				
			||||||
 | 
							return 1
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						fmt.Println("Added Time Entry:", entry.FriendlyString())
 | 
				
			||||||
 | 
						return 0
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func cmdContinueTimer(args []string) int {
 | 
				
			||||||
 | 
						// Get the last running timer and start a new one with the same tags
 | 
				
			||||||
 | 
						return 0
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func cmdStopTimer(args []string) int {
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
						tm := time.Now()
 | 
				
			||||||
 | 
						actTimers := LoadActiveTimers()
 | 
				
			||||||
 | 
						var tmr *Timer
 | 
				
			||||||
 | 
						stopId := "@0" // By default, stop the first timer
 | 
				
			||||||
 | 
						for i := range args {
 | 
				
			||||||
 | 
							if args[i][0] == '@' {
 | 
				
			||||||
 | 
								stopId = args[i]
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							tmpTm, err := parseFuzzyTime(args[i])
 | 
				
			||||||
 | 
							if err == nil {
 | 
				
			||||||
 | 
								// We found a time
 | 
				
			||||||
 | 
								tm = tmpTm
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if stopId != "@all" {
 | 
				
			||||||
 | 
							// Find the timer that we're stopping
 | 
				
			||||||
 | 
							timerId, err := strconv.Atoi(stopId[1:])
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								fmt.Println("Error parsing timer id:", err.Error())
 | 
				
			||||||
 | 
								return 1
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							tmr = &actTimers[timerId]
 | 
				
			||||||
 | 
							if timerId >= len(actTimers) || timerId < 0 || tmr == nil {
 | 
				
			||||||
 | 
								fmt.Println("Error finding timer with id:", timerId)
 | 
				
			||||||
 | 
								return 1
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						stopTimer := func(tmr *Timer, at time.Time) int {
 | 
				
			||||||
 | 
							tmr.SetEnd(at)
 | 
				
			||||||
 | 
							if err = SaveTimer(tmr); err != nil {
 | 
				
			||||||
 | 
								fmt.Println(err.Error())
 | 
				
			||||||
 | 
								return 1
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							fmt.Println("Stopped:", tmr.InferDetailString())
 | 
				
			||||||
 | 
							return 0
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if stopId == "@all" {
 | 
				
			||||||
 | 
							var ret int
 | 
				
			||||||
 | 
							for _, v := range actTimers {
 | 
				
			||||||
 | 
								ret += stopTimer(&v, tm)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if ret > 0 {
 | 
				
			||||||
 | 
								return 1
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return 0
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return stopTimer(tmr, tm)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func cmdDeleteTimer(args []string) int {
 | 
				
			||||||
 | 
						if len(args) < 1 {
 | 
				
			||||||
 | 
							fmt.Println("Delete requires a timer UUID")
 | 
				
			||||||
 | 
							return 1
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// Try to parse args[0] as a UUID
 | 
				
			||||||
 | 
						if uuid.Parse(args[0]) == nil {
 | 
				
			||||||
 | 
							// Not a UUID, bail for now
 | 
				
			||||||
 | 
							fmt.Print("Invalid UUID given (", args[0], ")\n")
 | 
				
			||||||
 | 
							return 1
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						tmr := LoadTimer(args[0])
 | 
				
			||||||
 | 
						if tmr == nil {
 | 
				
			||||||
 | 
							fmt.Println("Couldn't find timer with UUID (", args[0], ")\n")
 | 
				
			||||||
 | 
							return 1
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if DeleteTimer(args[0]) != nil {
 | 
				
			||||||
 | 
							fmt.Println("Error deleting timer")
 | 
				
			||||||
 | 
							return 1
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return 0
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func cmdModifyTimer(args []string) int {
 | 
				
			||||||
 | 
						return 0
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func cmdSwitchTimer(args []string) int {
 | 
				
			||||||
 | 
						return 0
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func cmdPrintStatus(args []string) int {
 | 
				
			||||||
 | 
						tmrs := LoadActiveTimers()
 | 
				
			||||||
 | 
						curr := time.Now()
 | 
				
			||||||
 | 
						fmt.Println("Current Time:", curr.Format(time.Stamp))
 | 
				
			||||||
 | 
						if len(tmrs) == 0 {
 | 
				
			||||||
 | 
							fmt.Println("No timer running")
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							fmt.Print("Active Timers (", len(tmrs), ")\n")
 | 
				
			||||||
 | 
							// Find the longest start time & longest duration
 | 
				
			||||||
 | 
							short := true
 | 
				
			||||||
 | 
							for _, v := range tmrs {
 | 
				
			||||||
 | 
								if v.GetStart().Day() != curr.Day() {
 | 
				
			||||||
 | 
									short = false
 | 
				
			||||||
 | 
									break
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for i, v := range tmrs {
 | 
				
			||||||
 | 
								if short {
 | 
				
			||||||
 | 
									fmt.Printf("  @%d %s\n", i, v.DetailString())
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									fmt.Printf("  @%d %s\n", i, v.LongDetailString())
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return 0
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func cmdPrintList(args []string) int {
 | 
				
			||||||
 | 
						useDefaultFilter := true
 | 
				
			||||||
 | 
						var showIds bool
 | 
				
			||||||
 | 
						var beg, end time.Time
 | 
				
			||||||
 | 
						tags, rem := pullTagsFromArgs(args)
 | 
				
			||||||
 | 
						_ = tags
 | 
				
			||||||
 | 
						for _, opt := range rem {
 | 
				
			||||||
 | 
							var tmpBeg, tmpEnd time.Time
 | 
				
			||||||
 | 
							// Check for command modifiers
 | 
				
			||||||
 | 
							if strings.HasPrefix(opt, ":") {
 | 
				
			||||||
 | 
								switch opt {
 | 
				
			||||||
 | 
								case ":ids":
 | 
				
			||||||
 | 
									showIds = true
 | 
				
			||||||
 | 
								case ":day":
 | 
				
			||||||
 | 
									beg, _ = parseFuzzyTime("00:00")
 | 
				
			||||||
 | 
									end, _ = parseFuzzyTime("23:59")
 | 
				
			||||||
 | 
								case ":week":
 | 
				
			||||||
 | 
									currDoW := time.Now().Weekday()
 | 
				
			||||||
 | 
									beg = time.Now().AddDate(0, 0, int(currDoW)*-1)
 | 
				
			||||||
 | 
									beg = time.Date(beg.Year(), beg.Month(), beg.Day(), 0, 0, 0, 0, beg.Location())
 | 
				
			||||||
 | 
								case ":month":
 | 
				
			||||||
 | 
									currDoM := time.Now().Day()
 | 
				
			||||||
 | 
									beg = time.Now().AddDate(0, 0, int(currDoM)*-1)
 | 
				
			||||||
 | 
									beg = time.Date(beg.Year(), beg.Month(), beg.Day(), 0, 0, 0, 0, beg.Location())
 | 
				
			||||||
 | 
								case ":year":
 | 
				
			||||||
 | 
									yr := strconv.Itoa(time.Now().Year())
 | 
				
			||||||
 | 
									beg, _ = parseFuzzyTime(yr + "0101T00:00")
 | 
				
			||||||
 | 
									end, _ = parseFuzzyTime(yr + "1231T23:59")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Do our best to figure out what timers the user wants to list
 | 
				
			||||||
 | 
							var err error
 | 
				
			||||||
 | 
							if strings.Contains(opt, "-") {
 | 
				
			||||||
 | 
								useDefaultFilter = false
 | 
				
			||||||
 | 
								pts := strings.Split(opt, "-")
 | 
				
			||||||
 | 
								if len(pts[0]) > 0 {
 | 
				
			||||||
 | 
									// This should be the starting date
 | 
				
			||||||
 | 
									tmpBeg, err = parseFuzzyTime(pts[0])
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										continue
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if len(pts[1]) > 0 {
 | 
				
			||||||
 | 
									// This should be the ending date
 | 
				
			||||||
 | 
									tmpEnd, err = parseFuzzyTime(pts[1])
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										continue
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if !tmpBeg.IsZero() || !tmpEnd.IsZero() {
 | 
				
			||||||
 | 
								beg, end = tmpBeg, tmpEnd
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if end.IsZero() {
 | 
				
			||||||
 | 
							end = time.Now()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// By default, list all entries ending today or still running
 | 
				
			||||||
 | 
						dayStr := ""
 | 
				
			||||||
 | 
						tmrs := LoadActiveTimers()
 | 
				
			||||||
 | 
						tmrs = append(tmrs, LoadTimers(beg, end)...)
 | 
				
			||||||
 | 
						var str string
 | 
				
			||||||
 | 
						if len(tmrs) == 0 {
 | 
				
			||||||
 | 
							if useDefaultFilter {
 | 
				
			||||||
 | 
								fmt.Println("No timers found for today")
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								begFmt := friendlyFormatForTime(beg)
 | 
				
			||||||
 | 
								endFmt := friendlyFormatForTime(end)
 | 
				
			||||||
 | 
								useFmt := endFmt
 | 
				
			||||||
 | 
								if len(begFmt) > len(endFmt) {
 | 
				
			||||||
 | 
									useFmt = begFmt
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								fmt.Println("No timers found in period " + beg.Format(useFmt) + " - " + end.Format(useFmt))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return 0
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Get day totals
 | 
				
			||||||
 | 
						dayTotals := make(map[string]time.Duration)
 | 
				
			||||||
 | 
						for _, v := range tmrs {
 | 
				
			||||||
 | 
							dur := v.GetEnd().Sub(v.GetStart())
 | 
				
			||||||
 | 
							if v.GetEnd().IsZero() {
 | 
				
			||||||
 | 
								dur = time.Now().Sub(v.GetStart())
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							dayTotals[v.GetStart().Format("2006/01/02")] += dur
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for _, v := range tmrs {
 | 
				
			||||||
 | 
							oldDayStr := dayStr
 | 
				
			||||||
 | 
							dayStr = v.GetStart().Format("2006/01/02")
 | 
				
			||||||
 | 
							if dayStr != oldDayStr {
 | 
				
			||||||
 | 
								vDur := dayTotals[dayStr].Round(GetRoundToDuration())
 | 
				
			||||||
 | 
								fmtStr := dayStr + " ( %.2f )\n"
 | 
				
			||||||
 | 
								str += fmt.Sprintf(fmtStr, DurationToDecimal(vDur))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							_ = showIds
 | 
				
			||||||
 | 
							id := ""
 | 
				
			||||||
 | 
							str += fmt.Sprintf("  %s %s\n", id, v.FriendlyString())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						fmt.Println(str)
 | 
				
			||||||
 | 
						return 0
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func cmdPrintDetail(args []string) int {
 | 
				
			||||||
 | 
						fmt.Println("Not implemented yet.")
 | 
				
			||||||
 | 
						return 1
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										228
									
								
								timer.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										228
									
								
								timer.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,228 @@
 | 
				
			|||||||
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/pborman/uuid"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Timer struct {
 | 
				
			||||||
 | 
						Uuid   string    `json:"uuid"`
 | 
				
			||||||
 | 
						Start  time.Time `json:"start"`
 | 
				
			||||||
 | 
						End    time.Time `json:"end"`
 | 
				
			||||||
 | 
						Tags   []string  `json:"tags"`
 | 
				
			||||||
 | 
						Notes  string    `json:"notes"`
 | 
				
			||||||
 | 
						Billed bool      `json:"billed"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewTimer returns a new timer with the given start and end
 | 
				
			||||||
 | 
					func NewTimer(st, end time.Time) *Timer {
 | 
				
			||||||
 | 
						ret := Timer{
 | 
				
			||||||
 | 
							Uuid:  uuid.New(),
 | 
				
			||||||
 | 
							Start: st,
 | 
				
			||||||
 | 
							End:   end,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return &ret
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetUUID returns the uuid of the timer
 | 
				
			||||||
 | 
					func (t *Timer) GetUUID() string {
 | 
				
			||||||
 | 
						return t.Uuid
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetStart returns the start time of the timer
 | 
				
			||||||
 | 
					func (t *Timer) GetStart() time.Time {
 | 
				
			||||||
 | 
						return t.Start
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SetStart sets the starting time of the timer
 | 
				
			||||||
 | 
					func (t *Timer) SetStart(st time.Time) {
 | 
				
			||||||
 | 
						t.Start = st
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetEnd returns the end time of the timer
 | 
				
			||||||
 | 
					func (t *Timer) GetEnd() time.Time {
 | 
				
			||||||
 | 
						return t.End
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SetEnd sets the ending time of the timer
 | 
				
			||||||
 | 
					func (t *Timer) SetEnd(end time.Time) {
 | 
				
			||||||
 | 
						t.End = end
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IsActive returns if the timer is still active
 | 
				
			||||||
 | 
					func (t *Timer) IsActive() bool {
 | 
				
			||||||
 | 
						return time.Now().After(t.Start) && t.End.IsZero()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SetTags sets the timer's tags
 | 
				
			||||||
 | 
					func (t *Timer) SetTags(tags []string) {
 | 
				
			||||||
 | 
						t.Tags = tags
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetTags returns all of the tags for the timer
 | 
				
			||||||
 | 
					func (t *Timer) GetTags() []string {
 | 
				
			||||||
 | 
						return t.Tags
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// AddTag adds the given tag to the timer
 | 
				
			||||||
 | 
					func (t *Timer) AddTag(tg string) {
 | 
				
			||||||
 | 
						for _, v := range t.Tags {
 | 
				
			||||||
 | 
							if v == tg {
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						t.Tags = append(t.Tags, tg)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// HasTag returns if the timer has the given tag
 | 
				
			||||||
 | 
					func (t *Timer) HasTag(s string) bool {
 | 
				
			||||||
 | 
						for _, v := range t.Tags {
 | 
				
			||||||
 | 
							if v == s {
 | 
				
			||||||
 | 
								return true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// RemoveTag will remove the given tag from the timer
 | 
				
			||||||
 | 
					func (t *Timer) RemoveTag(tg string) {
 | 
				
			||||||
 | 
						var tgs []string
 | 
				
			||||||
 | 
						for _, v := range t.Tags {
 | 
				
			||||||
 | 
							if v != tg {
 | 
				
			||||||
 | 
								tgs = append(tgs, v)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						t.Tags = tgs
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// SetNotes sets the timer's notes to the given string
 | 
				
			||||||
 | 
					func (t *Timer) SetNotes(nt string) {
 | 
				
			||||||
 | 
						t.Notes = nt
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// StartsToday returns if the timer's end time is today
 | 
				
			||||||
 | 
					func (t *Timer) StartsToday() bool {
 | 
				
			||||||
 | 
						currTime := time.Now()
 | 
				
			||||||
 | 
						dur := int64(currTime.Hour())*int64(time.Hour) + int64(currTime.Minute())*int64(time.Minute)
 | 
				
			||||||
 | 
						return int64(time.Since(t.GetStart())) < dur
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// EndsToday returns if the timer's end time is today
 | 
				
			||||||
 | 
					func (t *Timer) EndsToday() bool {
 | 
				
			||||||
 | 
						currTime := time.Now()
 | 
				
			||||||
 | 
						dur := int64(currTime.Hour())*int64(time.Hour) + int64(currTime.Minute())*int64(time.Minute)
 | 
				
			||||||
 | 
						return int64(time.Since(t.GetEnd())) < dur
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IsRunning returns if the timer is still running
 | 
				
			||||||
 | 
					func (t *Timer) IsRunning() bool {
 | 
				
			||||||
 | 
						return t.GetEnd().IsZero()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IsBillable checks if any of the tags on the timer are flagged as billable
 | 
				
			||||||
 | 
					func (t *Timer) IsBillable() bool {
 | 
				
			||||||
 | 
						m, err := LoadMetadata()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for _, v := range m.GetTags(t.Tags) {
 | 
				
			||||||
 | 
							if v.Billable {
 | 
				
			||||||
 | 
								return true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// String formats a string of the time entry
 | 
				
			||||||
 | 
					func (t *Timer) String() string {
 | 
				
			||||||
 | 
						var ret string
 | 
				
			||||||
 | 
						ret = t.GetStart().Format(time.RFC3339)
 | 
				
			||||||
 | 
						if !t.GetEnd().IsZero() {
 | 
				
			||||||
 | 
							ret += " - " + t.GetEnd().Format(time.RFC3339)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if len(t.GetTags()) > 0 {
 | 
				
			||||||
 | 
							ret += " [ "
 | 
				
			||||||
 | 
							for _, v := range t.GetTags() {
 | 
				
			||||||
 | 
								ret += v + " "
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							ret += "]"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if t.GetEnd().IsZero() {
 | 
				
			||||||
 | 
							ret += " Running"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return ret
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (t *Timer) FriendlyString() string {
 | 
				
			||||||
 | 
						var ret string
 | 
				
			||||||
 | 
						var end string
 | 
				
			||||||
 | 
						if t.StartsToday() {
 | 
				
			||||||
 | 
							ret = t.GetStart().Format("15:04 - ")
 | 
				
			||||||
 | 
							end = "**:**"
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							ret = t.GetStart().Format("2006/01/02 15:04:05 - ")
 | 
				
			||||||
 | 
							end = "**:**:**"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if !t.GetEnd().IsZero() {
 | 
				
			||||||
 | 
							if t.EndsToday() {
 | 
				
			||||||
 | 
								end = t.GetEnd().Format("15:04")
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								end = t.GetEnd().Format("2006/01/02 15:04:05")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						ret += end
 | 
				
			||||||
 | 
						if len(t.GetTags()) > 0 {
 | 
				
			||||||
 | 
							ret += " [ "
 | 
				
			||||||
 | 
							for _, v := range t.GetTags() {
 | 
				
			||||||
 | 
								ret += v + " "
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							ret += "]"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return ret
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (t *Timer) InferDetailString() string {
 | 
				
			||||||
 | 
						diffEnd := time.Now()
 | 
				
			||||||
 | 
						if !t.GetEnd().IsZero() {
 | 
				
			||||||
 | 
							diffEnd = t.GetEnd()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if int(diffEnd.Sub(t.GetStart())) >= (int(time.Hour) * diffEnd.Hour()) {
 | 
				
			||||||
 | 
							return t.LongDetailString()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return t.DetailString()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (t *Timer) DetailString() string {
 | 
				
			||||||
 | 
						ret := t.GetStart().Format("15:04") + " - "
 | 
				
			||||||
 | 
						if t.GetEnd().IsZero() {
 | 
				
			||||||
 | 
							ret += "**:** (" + padLeft(sinceToString(t.GetStart()), len("00h 00m 00s")) + ") "
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							ret += t.GetEnd().Format("15:04") + " (" + padLeft(diffToString(t.GetStart(), t.GetEnd()), len("00h 00m 00s")) + ") "
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if len(t.GetTags()) > 0 {
 | 
				
			||||||
 | 
							ret += " [ "
 | 
				
			||||||
 | 
							for _, v := range t.GetTags() {
 | 
				
			||||||
 | 
								ret += v + " "
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							ret += "] "
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return ret
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (t *Timer) LongDetailString() string {
 | 
				
			||||||
 | 
						ret := t.GetStart().Format(time.Stamp)
 | 
				
			||||||
 | 
						if t.GetEnd().IsZero() {
 | 
				
			||||||
 | 
							ret += " (" + padLeft(sinceToString(t.GetStart()), len("0000y 00m 00d 00h 00m 00s")) + ") "
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							ret += " (" + padLeft(diffToString(t.GetStart(), t.GetEnd()), len("0000y 00m 00d 00h 00m 00s")) + ") "
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if len(t.GetTags()) > 0 {
 | 
				
			||||||
 | 
							ret += " [ "
 | 
				
			||||||
 | 
							for _, v := range t.GetTags() {
 | 
				
			||||||
 | 
								ret += v + " "
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							ret += "] "
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return ret
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user