diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 1506d0e..402642d 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -5,7 +5,7 @@ "Deps": [ { "ImportPath": "git.bullercodeworks.com/brian/gime-lib", - "Rev": "579fc6c62aa8f5452e02d3fe3334e2d035b0b712" + "Rev": "2a2aea8641e8b327544452f900ea4f5fcc43fc7e" }, { "ImportPath": "github.com/BurntSushi/toml", diff --git a/helpers.go b/helpers.go index 5b7f814..05801ec 100644 --- a/helpers.go +++ b/helpers.go @@ -145,6 +145,20 @@ func findIdOfTimer(tmr *gime.TimeEntry) (int, error) { return -1, errors.New("Unable to find timer") } +// 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 @@ -167,6 +181,22 @@ func parseFuzzyTime(t string) (time.Time, error) { 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()) } diff --git a/main.go b/main.go index d5c1ab0..5c16277 100644 --- a/main.go +++ b/main.go @@ -244,10 +244,11 @@ func initialize() { 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", + " :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 +", } diff --git a/timer_operations.go b/timer_operations.go index f7287b4..8aed424 100644 --- a/timer_operations.go +++ b/timer_operations.go @@ -9,6 +9,39 @@ import ( "git.bullercodeworks.com/brian/gime-lib" ) +// cmdStartTimer takes a list of arguments and returns the return code +// to be passed along to os.Exit +func cmdStartTimer(args []string) int { + var err error + var entry *gime.TimeEntry + // 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]) + } + + tc := new(gime.TagCollection) + for i := range tags { + tc.Push(tags[i]) + } + + if entry, err = gime.CreateTimeEntry(tm, time.Time{}, tc); err != nil { + fmt.Println(err) + return 1 + } + + if err = gdb.SaveTimeEntry(entry); err != nil { + fmt.Println(err) + return 1 + } + + fmt.Println(" " + TimerToString(entry)) + return 0 +} + func cmdContinueTimer(args []string) int { // Get the last running timer and start a new one with the same tags te, err := getMostRecentTimeEntry() @@ -19,7 +52,7 @@ func cmdContinueTimer(args []string) int { tagColl := te.GetTags() var tags []string for i := 0; i < tagColl.Length(); i++ { - tags = append(tags, tagColl.Get(i)) + tags = append(tags, "+"+tagColl.Get(i)) } args = append(args, tags...) return cmdStartTimer(args) @@ -28,73 +61,40 @@ func cmdContinueTimer(args []string) int { // switchTimer performs a stop on any currently running timers // and starts a new timer with the given arguments func cmdSwitchTimer(args []string) int { - loadActiveTimeEntries() - tm := time.Now() - if timeEntries.Length() > 0 { - fmt.Println("Stopped Timers:") - } - for i := 0; i < timeEntries.Length(); i++ { - tmr := timeEntries.Get(i) - tmr.SetEnd(tm) - if err := gdb.UpdateTimeEntry(tmr); err != nil { - fmt.Println(err.Error()) - return 1 + var foundId bool + for i := range args { + // see if we have a timer id in the args + if args[i][0] == '@' { + foundId = true } - fmt.Println(" " + TimerToString(tmr)) + } + stopArgs := make([]string, len(args)) + copy(stopArgs, args) + if !foundId { + // We didn't find one, so make sure we stop _all_ timers + stopArgs = append(stopArgs, "@all") + } + fmt.Println(stopArgs) + fmt.Println(args) + return 0 + if cmdStopTimer(args) != 0 { + // Error while stopping timers + return 1 } fmt.Println("Started Timer:") return cmdStartTimer(args) } -// cmdStartTimer takes a list of arguments and returns the return code -// to be passed along to os.Exit -func cmdStartTimer(args []string) int { - var err error - var tm time.Time - tagStart := 0 - if len(args) > 0 { - // Check if the first argument looks like a date/time - tm, err = parseFuzzyTime(args[0]) - } - - if len(args) == 0 || err != nil { - // Just start it now - tm = time.Now() - } else { - tagStart = 1 - } - var entry *gime.TimeEntry - - var timerArgs []string - if tagStart < len(args) { - timerArgs = args[tagStart:] - } - tc := new(gime.TagCollection) - for i := range timerArgs { - tc.Push(timerArgs[i]) - } - if entry, err = gime.CreateTimeEntry(tm, time.Time{}, tc); err != nil { - fmt.Println(err) - return 1 - } - if err = gdb.SaveTimeEntry(entry); err != nil { - fmt.Println(err) - return 1 - } - fmt.Println(" " + TimerToString(entry)) - return 0 -} - // cmdStopTimer takes parameters that describe which times to stop func cmdStopTimer(args []string) int { - // args[0] should either be a timer id (starting with '@') or 'all' + // args[0] should be a timer id (starting with '@') var err error tm := time.Now() actTimers := gdb.LoadTimeEntryCollection(gime.TypeCurrent) var tmr *gime.TimeEntry stopId := "@0" // By default, stop the first timer for i := range args { - if args[i] == "all" || args[i][0] == '@' { + if args[i][0] == '@' { stopId = args[i] continue } @@ -105,7 +105,7 @@ func cmdStopTimer(args []string) int { continue } } - if stopId != "all" { + if stopId != "@all" { // Find the timer that we're stopping timerId, err := strconv.Atoi(stopId[1:]) if err != nil { @@ -128,7 +128,7 @@ func cmdStopTimer(args []string) int { fmt.Println("Stopped:", InferTimerDetailString(tmr)) return 0 } - if stopId == "all" { + if stopId == "@all" { var ret int for i := 0; i < actTimers.Length(); i++ { ret += stopTimer(actTimers.Get(i), tm) @@ -161,7 +161,7 @@ func cmdDeleteTimer(args []string) int { fmt.Println(err.Error()) return 1 } - if gdb.RemoveTimeEntry(tmr.GetUUID()) != nil { + if gdb.RemoveTimeEntry(tmr) != nil { fmt.Println("Error removing entry " + gime.TypeToString(tp) + "." + tmr.GetUUID()) return 1 } @@ -179,12 +179,35 @@ func cmdPrintList(args []string) int { var tmpBeg, tmpEnd time.Time // Check for command modifiers if strings.HasPrefix(opt, ":") { - if opt == ":ids" { + switch opt { + case ":ids": showIds = true + // Special durations + 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 } + // Find tags + if strings.HasPrefix(opt, "+") { + searchTags = append(searchTags, opt[1:]) + continue + } + // Do our best to figure out what timers the user wants to list var err error if strings.Contains(opt, "-") { @@ -194,9 +217,6 @@ func cmdPrintList(args []string) int { // This should be the starting date tmpBeg, err = parseFuzzyTime(pts[0]) if err != nil { - // We couldn't parse it as a time, - // Probably this is just a tag - searchTags = append(searchTags, opt) continue } } @@ -204,13 +224,9 @@ func cmdPrintList(args []string) int { // This should be the ending date tmpEnd, err = parseFuzzyTime(pts[1]) if err != nil { - searchTags = append(searchTags, opt) continue } } - } else { - // Tag filters - searchTags = append(searchTags, opt) } if !tmpBeg.IsZero() || !tmpEnd.IsZero() { beg, end = tmpBeg, tmpEnd @@ -248,10 +264,19 @@ func cmdPrintList(args []string) int { dayStr := "" timers := filterTimerCollection(timeEntries, compoundFilter) - _ = dayStr var str string var currId int var err error + if timers.Length() == 0 { + 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 + } for i := 0; i < timers.Length(); i++ { wrk := timers.Get(i) oldDayStr := dayStr @@ -283,19 +308,49 @@ func cmdDoArchive(args []string) int { fmt.Println("Nothing to do") return 1 } - bef, err := parseFuzzyTime(args[0]) - if err != nil { - fmt.Println("Error parsing time") + + var tags []string + tags, args = pullTagsFromArgs(args) + var bef time.Time + var err error + if len(args) > 0 { + bef, err = parseFuzzyTime(args[0]) + } + if bef.IsZero() && len(tags) == 0 { + fmt.Println("Couldn't figure out what to archive") return 1 } ret := 0 - fmt.Print("Archive all timers before ", bef) loadActiveAndRecentTimeEntries() + tagFilter := func(t *gime.TimeEntry) bool { + for i := range tags { + if !t.HasTag(tags[i]) { + return false + } + } + return true + } + if len(tags) > 0 { + timeEntries = filterTimerCollection(timeEntries, tagFilter) + } + fmt.Print("Archive all timers ") + if !bef.IsZero() { + fmt.Print("before ", bef, " ") + } + if len(tags) > 0 { + fmt.Print("with tags ", tags) + } for i := 0; i < timeEntries.Length(); i++ { tst := timeEntries.Get(i) - if tst.GetEnd().Before(bef) { + archIt := false + if !bef.IsZero() { + archIt = tst.GetEnd().Before(bef) + } else { + archIt = true + } + if archIt { fmt.Print(".") - if err = gdb.ArchiveTimeEntry(tst.GetUUID()); err != nil { + if err = gdb.ArchiveTimeEntry(tst); err != nil { fmt.Print("Error archiving entry (", tst.GetUUID(), ")", err.Error()) ret = 1 } diff --git a/vendor/git.bullercodeworks.com/brian/gime-lib/model.go b/vendor/git.bullercodeworks.com/brian/gime-lib/model.go index d23222c..7f95acc 100644 --- a/vendor/git.bullercodeworks.com/brian/gime-lib/model.go +++ b/vendor/git.bullercodeworks.com/brian/gime-lib/model.go @@ -25,6 +25,7 @@ const ( TypeNoArchive = 3 // 011 TypeArchive = 4 // 100 TypeAll = 7 // 111 + TypeError = 8 //1000 ArchiveDays = time.Hour * 24 * 90 // Archive anything older than 90 days ) diff --git a/vendor/git.bullercodeworks.com/brian/gime-lib/model_timeentry.go b/vendor/git.bullercodeworks.com/brian/gime-lib/model_timeentry.go index ae46874..0fcbb7d 100644 --- a/vendor/git.bullercodeworks.com/brian/gime-lib/model_timeentry.go +++ b/vendor/git.bullercodeworks.com/brian/gime-lib/model_timeentry.go @@ -13,21 +13,25 @@ import ( func (gdb *GimeDB) GetLatestTimeEntry() (*TimeEntry, error) { var ret *TimeEntry var err error - tc := gdb.LoadTimeEntryCollection(TypeRecent) - for i := 0; i < tc.Length(); i++ { - te := tc.Get(i) - if ret == nil { - ret = te - } else { - if te.GetEnd().After(ret.GetEnd()) { - ret = te - } - } + + var useDb *boltease.DB + if useDb, err = gdb.openDBType(TypeRecent); err != nil { + return ret, err } - if ret == nil { - err = errors.New("Could not find latest time entry") + defer gdb.closeDBType(TypeRecent) + + var sttimes []string + if sttimes, err = useDb.GetBucketList([]string{TypeToString(TypeRecent)}); err != nil { + return ret, err } - return ret, err + sort.Slice(sttimes, func(i, j int) bool { + return sttimes[j] < sttimes[i] + }) + // The first entry should be the most recent + if len(sttimes) > 0 { + return gdb.dbGetTimeEntry(TypeRecent, sttimes[0]) + } + return nil, errors.New("No recent time entries found") } // findTimeEntryAndTypeByUUID searches all entries @@ -87,13 +91,20 @@ func (gdb *GimeDB) SaveTimeEntryType(tp int, te *TimeEntry) error { return nil } +func (gdb *GimeDB) ArchiveTimeEntry(te *TimeEntry) error { + if err := gdb.RemoveTimeEntryFromCategory(te, TypeRecent); err != nil { + return err + } + return gdb.SaveTimeEntryType(TypeArchive, te) +} + // ArchiveTimeEntry takes a time from TypeRecent and moves it to TypeArchive -func (gdb *GimeDB) ArchiveTimeEntry(uuid string) error { +func (gdb *GimeDB) ArchiveTimeEntryByUUID(uuid string) error { archTime, tp, err := gdb.findTimeEntryAndTypeByUUID(uuid) if tp != TypeRecent { return errors.New("Couldn't find timer to archive in the 'Recent' bucket") } - if err = gdb.RemoveTimeEntry(archTime.uuid); err != nil { + if err = gdb.RemoveTimeEntry(archTime); err != nil { return err } return gdb.SaveTimeEntryType(TypeArchive, archTime) @@ -106,14 +117,36 @@ func (gdb *GimeDB) UpdateTimeEntry(te *TimeEntry) error { if te.uuid == "" { return errors.New("Given time entry has no uuid") } - if err = gdb.RemoveTimeEntry(te.uuid); err != nil { + if err = gdb.RemoveTimeEntry(te); err != nil { return err } return gdb.SaveTimeEntry(te) } // RemoveTimeEntry removes a time entry with the given uuid from the database -func (gdb *GimeDB) RemoveTimeEntry(uuid string) error { +func (gdb *GimeDB) RemoveTimeEntry(te *TimeEntry) error { + for _, v := range gdb.AllTypes { + if gdb.RemoveTimeEntryFromCategory(te, v) == nil { + return nil + } + } + return errors.New("Couldn't find Time Entry to Remove") +} + +func (gdb *GimeDB) RemoveTimeEntryFromCategory(te *TimeEntry, tp int) error { + if !gdb.dbTimeEntryIsInCategory(te, tp) { + return errors.New("Couldn't find timer to remove in the given bucket") + } + var err error + var useDb *boltease.DB + if useDb, err = gdb.openDBType(tp); err != nil { + return err + } + defer gdb.closeDBType(tp) + return useDb.DeleteBucket([]string{TypeToString(tp)}, te.start.Format(time.RFC3339)) +} + +func (gdb *GimeDB) RemoveTimeEntryByUUID(uuid string) error { fndEntry, tp, err := gdb.findTimeEntryAndTypeByUUID(uuid) if err != nil { return errors.New("Unable to find time entry with uuid " + uuid) @@ -193,6 +226,20 @@ func (gdb *GimeDB) dbGetAllTimeEntries(tp int) []TimeEntry { return ret } +func (gdb *GimeDB) dbFindTimeEntryCategory(te *TimeEntry) int { + for _, v := range gdb.AllTypes { + if gdb.dbTimeEntryIsInCategory(te, v) { + return v + } + } + return TypeError +} + +func (gdb *GimeDB) dbTimeEntryIsInCategory(te *TimeEntry, tp int) bool { + _, err := gdb.dbGetTimeEntry(tp, te.start.Format(time.RFC3339)) + return err == nil +} + // dbGetTimeEntry pulls a time entry of type tp with the given start time // from the db and returns it. func (gdb *GimeDB) dbGetTimeEntry(tp int, sttm string) (*TimeEntry, error) {