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