diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 224e7ca..8b9f0dd 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -10,7 +10,7 @@ }, { "ImportPath": "github.com/br0xen/boltease", - "Rev": "50a16b90567dacab0e724ba3620174a19d80489f" + "Rev": "69e69463f4b6e07bd4405fa507333ae55dc32791" }, { "ImportPath": "github.com/pborman/uuid", diff --git a/cmd/gime/main.go b/cmd/gime/main.go index a5b7282..6d7a01c 100644 --- a/cmd/gime/main.go +++ b/cmd/gime/main.go @@ -3,6 +3,8 @@ package main import ( "fmt" "os" + "strconv" + "strings" "time" "git.bullercodeworks.com/brian/gime" @@ -14,7 +16,7 @@ const ( ) var validOperations map[string][]string -var activeTimeEntry *gime.TimeEntry +var activeTimeEntries *gime.TimeEntryCollection var gdb *gime.GimeDB func main() { @@ -48,6 +50,7 @@ func main() { func printHelp() int { 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]) @@ -57,31 +60,56 @@ func printHelp() int { return 0 } +func loadActiveTimeEntries() { + activeTimeEntries = gdb.LoadTimeEntryCollection(gime.TypeCurrent) +} + func printStatus() int { - if activeTimeEntry == nil { + loadActiveTimeEntries() + curr := time.Now() + fmt.Println("Current Time:", curr.Format(time.Stamp)) + if activeTimeEntries.Length() == 0 { fmt.Println("No timer running") } else { - fmt.Print("Timer started at ") - curr := time.Now() - if activeTimeEntry.GetStart().Day() == curr.Day() { - fmt.Println(activeTimeEntry.GetStart().Format("15:04")) - } else { - fmt.Println(activeTimeEntry.GetStart().Format(time.Stamp)) + fmt.Print("Currently Active Timers\n") + for i := 0; i < activeTimeEntries.Length(); i++ { + v := activeTimeEntries.Get(i) + if v.GetStart().Day() == curr.Day() { + fmt.Print("@"+strconv.Itoa(i)+" ", v.GetStart().Format("15:04"), " ") + } else { + fmt.Print("@"+strconv.Itoa(i)+" ", v.GetStart().Format(time.Stamp), " ") + } + since := time.Since(v.GetStart()).String() + since = strings.Split(since, ".")[0] + fmt.Print("(" + since + "s) ") + fmt.Print(v.GetTags().Length()) + if v.GetTags().Length() > 0 { + fmt.Print(" [ ") + for j := 0; j < v.GetTags().Length(); j++ { + fmt.Print(v.GetTags().Get(j)) + if j < v.GetTags().Length()-1 { + fmt.Print(", ") + } + } + fmt.Print(" ] ") + } + fmt.Println("") } - fmt.Println(time.Since(activeTimeEntry.GetStart()).String()) } return 0 } +// switchTimer performs a stop on any currently running timers +// and starts a new timer with the given arguments +func switchTimer(args []string) int { + return 0 +} + // startTimer takes a list of arguments and returns the return code // to be passed along to os.Exit func startTimer(args []string) int { var err error tm := time.Now() - curr := gdb.LoadTimeEntryCollection(gime.TypeCurrent) - if curr.Length() > 0 { - // TODO: Stop all Timers in curr - } tagStart := 0 if len(args) > 0 { // Check if the first argument looks like a date/time @@ -103,7 +131,11 @@ func startTimer(args []string) int { timerArgs = args[tagStart:] } fmt.Println(tm) - if entry, err = gime.CreateTimeEntry(tm, time.Time{}, timerArgs); err != nil { + 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 } @@ -124,7 +156,6 @@ func stopTimer(args []string) int { if err != nil { tm, err = time.Parse(time.Kitchen, args[0]) } - } _, _ = tm, st return 0 diff --git a/model.go b/model.go index ef2e60f..9a3df49 100644 --- a/model.go +++ b/model.go @@ -19,8 +19,11 @@ const ( TypeCurrent = "current" TypeRecent = "recent" TypeArchive = "archive" + TypeUnknown = "unknown" // Not really a type, used for searches and stuff ) +const AllTypes = []string{TypeCurrent, TypeRecent, TypeArchive} + // Load Database returns a database loaded off the files given // name and archName located in path func LoadDatabase(path, name, archName string) (*GimeDB, error) { @@ -36,6 +39,16 @@ func LoadDatabase(path, name, archName string) (*GimeDB, error) { return &gdb, nil } +// Load a TimeEntry collection from a database +func (gdb *GimeDB) LoadTimeEntryCollection(tp string) *TimeEntryCollection { + ret := new(TimeEntryCollection) + entries := gdb.dbGetAllTimeEntries(tp) + for i := range entries { + ret.Push(&entries[i]) + } + return ret +} + func (gdb *GimeDB) openDatabase() error { gdb.dbOpened += 1 if gdb.dbOpened == 1 { diff --git a/model_timeentry.go b/model_timeentry.go index a9640e7..6babff4 100644 --- a/model_timeentry.go +++ b/model_timeentry.go @@ -8,6 +8,30 @@ import ( "github.com/br0xen/boltease" ) +// FindTimeEntryByUUID searches all entries of type tp (can be TypeUnknown) +// for the time entry with the given uuid +func (gdb *GimeDB) FindTimeEntryByUUID(tp, uuid string) (*TimeEntry, error) { + var ret *TimeEntry + var err error + if tp == TypeUnknown { + for _, v := range AllTypes { + ret, err = gdb.FindTimeEntryByUUID(v, uuid) + if ret != nil { + return ret, nil + } + } + return nil, errors.New("Unable to find time entry with uuid " + uuid) + } + tstEntries := gdb.dbGetAllTimeEntries(tp) + for i := range tstEntries { + if tstEntries[i].uuid == uuid { + // Found it! + return tstEntries[i], nil + } + } + return nil, errors.New("Unable to find time entry with uuid " + uuid) +} + // SaveTimeEntry creates a time entry in the database // If TimeEntry.end is zero, then it puts it in TypeCurrent func (gdb *GimeDB) SaveTimeEntry(te *TimeEntry) error { @@ -28,27 +52,55 @@ func (gdb *GimeDB) SaveTimeEntry(te *TimeEntry) error { tp = TypeArchive } } - tePath := []string{tp, te.uuid} + tePath := []string{tp, te.start.Format(time.RFC3339)} + if err = useDb.SetValue(tePath, "uuid", te.uuid); err != nil { + return err + } if err = useDb.SetTimestamp(tePath, "start", te.start); err != nil { return err } if err = useDb.SetTimestamp(tePath, "end", te.end); err != nil { return err } - for i := range te.tags { - err = useDb.SetValue(append(tePath, "tags"), strconv.Itoa(i), te.tags[i]) + for i := 0; i < te.tags.Length(); i++ { + err = useDb.SetValue(append(tePath, "tags"), strconv.Itoa(i), te.tags.Get(i)) } return nil } -// dbGetAllTimeEntries gets all time entries of a specific type -// tp can be: -// TypeCurrent = "current" -// TypeRecent = "recent" -// TypeArchive = "archive" -// Getting all archived time entries has the potential to be a lot of data -func (gdb *GimeDB) dbGetAllTimeEntries(tp string) []TimeEntry { - var ret []TimeEntry +// UpdateTimeEntry updates the time entry in the database +// It first finds the current entry in the database by the uuid +func (gdb *GimeDB) UpdateTimeEntry(te *TimeEntry) error { + var err error + if te.uuid == "" { + return errors.New("Given time entry has no uuid") + } + oldTe, err := gdb.FindTimeEntryByUUID(TypeUnknown, te.uuid) + if err != nil { + return err + } + +} + +// RemoveTimeEntry removes a time entry of type tp (can be TypeUnknown) +// with the given uuid from the database +func (gdb *GimeDB) RemoveTimeEntry(tp, uuid string) error { + var fndEntry *TimeEntry + var err error + // For TypeUnknown, we want to manually loop, so we know where we found it + if tp == TypeUnknown { + for _, v := range AllTypes { + fndEntry, err = gdb.FindTimeEntryByUUID(v, uuid) + if err == nil { + tp = v + break + } + } + if fndEntry == nil { + return errors.New("Unable to find time entry with uuid " + uuid) + } + } + var useDb *boltease.DB var err error if useDb, err = gdb.openDBType(tp); err != nil { @@ -56,11 +108,36 @@ func (gdb *GimeDB) dbGetAllTimeEntries(tp string) []TimeEntry { } defer gdb.closeDBType(tp) - var uuids []string - if uuids, err = useDb.GetBucketList([]string{tp}); err != nil { + return nil, errors.New("Unable to find time entry with uuid " + uuid) +} + +// dbGetAllTimeEntries gets all time entries of a specific type +// tp can be: +// TypeCurrent = "current" +// TypeRecent = "recent" +// TypeArchive = "archive" +// TypeUnknown = "unknown" - Returns _ALL_ entries +// Getting all archived time entries has the potential to be a lot of data +func (gdb *GimeDB) dbGetAllTimeEntries(tp string) []TimeEntry { + var ret []TimeEntry + if tp == TypeUnknown { + for _, v := range AllTypes { + ret = append(ret, gdb.dbGetAllTimeEntries(v)) + } return ret } - for _, v := range uuids { + var useDb *boltease.DB + var err error + if useDb, err = gdb.openDBType(tp); err != nil { + return ret + } + defer gdb.closeDBType(tp) + + var sttimes []string + if sttimes, err = useDb.GetBucketList([]string{tp}); err != nil { + return ret + } + for _, v := range sttimes { if te, _ := gdb.dbGetTimeEntry(tp, v); te != nil { ret = append(ret, *te) } @@ -70,19 +147,30 @@ func (gdb *GimeDB) dbGetAllTimeEntries(tp string) []TimeEntry { // dbGetTimeEntry pulls a time entry of type tp with the given id // from the db and returns it. -func (gdb *GimeDB) dbGetTimeEntry(tp, id string) (*TimeEntry, error) { +func (gdb *GimeDB) dbGetTimeEntry(tp, sttm string) (*TimeEntry, error) { var ret *TimeEntry var err error + if tp == TypeUnknown { + // Loop through the types and return the first entry found that matches + for _, v := range AllTypes { + ret, _ = gdb.dbGetTimeEntry(v, sttm) + if ret != nil { + return ret, nil + } + } + return nil, errors.Error("Couldn't find time entry") + } var useDb *boltease.DB - if useDb, err = gdb.openDBType(tp); err != nil { return ret, err } defer gdb.closeDBType(tp) - entryPath := []string{tp, id} + entryPath := []string{tp, sttm} ret = new(TimeEntry) - ret.uuid = id + if ret.uuid, err = useDb.GetValue(entryPath, "uuid"); err != nil { + return nil, errors.New("Unable to read entry uuid") + } if ret.start, err = useDb.GetTimestamp(entryPath, "start"); err != nil { return nil, errors.New("Unable to read entry start time") } @@ -94,10 +182,11 @@ func (gdb *GimeDB) dbGetTimeEntry(tp, id string) (*TimeEntry, error) { if keys, err = useDb.GetKeyList(entryTagsPath); err != nil { keys = []string{} } + ret.tags = new(TagCollection) for i := range keys { var val string - if val, err = useDb.GetValue(entryTagsPath, keys[i]); err != nil { - ret.tags = append(ret.tags, val) + if val, err = useDb.GetValue(entryTagsPath, keys[i]); err == nil { + ret.tags.Push(val) } } return ret, nil diff --git a/tag_collection.go b/tag_collection.go new file mode 100644 index 0000000..68342ca --- /dev/null +++ b/tag_collection.go @@ -0,0 +1,80 @@ +package gime + +import "fmt" + +// TagCollection is a collection of strings for gomobile bindings +type TagCollection struct { + list []string +} + +// Length returns the number of tags in the collection +func (tc *TagCollection) Length() int { + return len(tc.list) +} + +// Get returns the string at idx or an empty string +func (tc *TagCollection) Get(idx int) string { + if idx < tc.Length() { + return tc.list[idx] + } + return "" +} + +// Clear empties the list of Tags +func (tc *TagCollection) Clear() { + tc.list = tc.list[:0] +} + +// Index finds the index of the given string or -1 if not found +func (tc *TagCollection) Index(t string) int { + for i, tst := range tc.list { + if tst == t { + return i + } + } + return -1 +} + +// Insert inserts a string into the collection at i +func (tc *TagCollection) Insert(i int, t string) { + if i < 0 || i > tc.Length() { + fmt.Println("gime: Attempted to insert string at invalid index") + } + tc.list = append(tc.list, "") + copy(tc.list[i+1:], tc.list[i:]) + tc.list[i] = t +} + +// Remove removes the string at i from the collection +func (tc *TagCollection) Remove(i int) { + if i < 0 || i >= tc.Length() { + fmt.Println("gime: Attempted to remove tag at invalid index") + } + copy(tc.list[i:], tc.list[i+1:]) + tc.list[len(tc.list)-1] = "" + tc.list = tc.list[:len(tc.list)-1] +} + +// Push adds an element to the end of the collection +func (tc *TagCollection) Push(t string) { + tc.Insert(tc.Length(), t) +} + +// Pop removes the last element from the collection +func (tc *TagCollection) Pop() string { + ret := tc.list[tc.Length()-1] + tc.Remove(tc.Length() - 1) + return ret +} + +// Unshift adds an element to the front of the collection +func (tc *TagCollection) Unshift(t string) { + tc.Insert(0, t) +} + +// Shift removes an element from the front of the collection +func (tc *TagCollection) Shift() string { + ret := tc.list[0] + tc.Remove(0) + return ret +} diff --git a/timeentry.go b/timeentry.go index f264e2b..28591e1 100644 --- a/timeentry.go +++ b/timeentry.go @@ -12,26 +12,29 @@ type TimeEntry struct { uuid string start time.Time end time.Time - tags []string + tags *TagCollection } // CreateTimeEntry creates an entry with the given parameters and returns it. // An error is returned if no start time is given or if the end time given is // non-zero and earlier than the start time. -func CreateTimeEntry(s, e time.Time, t []string) (*TimeEntry, error) { +func CreateTimeEntry(s, e time.Time, t *TagCollection) (*TimeEntry, error) { ret := new(TimeEntry) ret.uuid = uuid.New() if s.IsZero() { // No start time given, return error return ret, errors.New("No start time given") } + ret.start = s if !e.IsZero() { if !e.After(s) { // Given end time is earlier than start time return ret, errors.New("End time is before start time") } } - return &TimeEntry{start: s, end: e, tags: t}, nil + ret.end = e + ret.tags = t + return ret, nil } // GetStart returns the start time for the entry @@ -60,14 +63,14 @@ func (t *TimeEntry) IsActive() bool { } // GetTags returns all of the tags associated with this time entry -func (t *TimeEntry) GetTags() []string { +func (t *TimeEntry) GetTags() *TagCollection { return t.tags } // HasTag returns if the time entry contains a specific tag func (t *TimeEntry) HasTag(s string) bool { - for i := range t.tags { - if t.tags[i] == s { + for i := 0; i < t.tags.Length(); i++ { + if t.tags.Get(i) == s { return true } } @@ -77,22 +80,15 @@ func (t *TimeEntry) HasTag(s string) bool { // AddTag adds a tag to the time entry (if it isn't already there) func (t *TimeEntry) AddTag(s string) { if !t.HasTag(s) { - t.tags = append(t.tags, s) + t.tags.Push(s) } } // RemoveTag removes a tag from the time entry func (t *TimeEntry) RemoveTag(s string) { - var i int - var fnd bool - for i = range t.tags { - if t.tags[i] == s { - fnd = true - break - } - } - if fnd { - t.tags = append(t.tags[:i], t.tags[i+1:]...) + i := t.tags.Index(s) + if i != -1 { + t.tags.Remove(i) } } diff --git a/timeentry_collection.go b/timeentry_collection.go index 33f623a..46c659f 100644 --- a/timeentry_collection.go +++ b/timeentry_collection.go @@ -7,16 +7,6 @@ type TimeEntryCollection struct { list []TimeEntry } -// Load a TimeEntry collection from a database -func (gdb *GimeDB) LoadTimeEntryCollection(tp string) *TimeEntryCollection { - ret := new(TimeEntryCollection) - entries := gdb.dbGetAllTimeEntries(tp) - for i := range entries { - ret.Push(&entries[i]) - } - return ret -} - // Length returns how many time entries are in the collection func (tc *TimeEntryCollection) Length() int { return len(tc.list) @@ -24,7 +14,7 @@ func (tc *TimeEntryCollection) Length() int { // Get returns the TimeEntry at idx or a nil entry func (tc *TimeEntryCollection) Get(idx int) *TimeEntry { - if idx <= tc.Length() { + if idx < tc.Length() { return &tc.list[idx] } return nil @@ -83,7 +73,7 @@ func (tc *TimeEntryCollection) Unshift(t *TimeEntry) { } // Shift removes an element from the front of the collection -func (tc *TimeEntryCollection) Shift(t *TimeEntry) *TimeEntry { +func (tc *TimeEntryCollection) Shift() *TimeEntry { ret := tc.list[0] tc.Remove(0) return &ret diff --git a/vendor/github.com/br0xen/boltease/boltease.go b/vendor/github.com/br0xen/boltease/boltease.go index ee08170..75645e5 100644 --- a/vendor/github.com/br0xen/boltease/boltease.go +++ b/vendor/github.com/br0xen/boltease/boltease.go @@ -242,7 +242,7 @@ func (b *DB) GetBucketList(path []string) ([]string, error) { } } - // newBkt should have the last bucket in the path + // bkt should have the last bucket in the path berr = bkt.ForEach(func(k, v []byte) error { if v == nil { // Must be a bucket @@ -271,20 +271,20 @@ func (b *DB) GetKeyList(path []string) ([]string, error) { if bkt == nil { return fmt.Errorf("Couldn't find bucket " + path[0]) } + var berr error if len(path) > 1 { - var newBkt *bolt.Bucket for idx := 1; idx < len(path); idx++ { - newBkt = bkt.Bucket([]byte(path[idx])) - if newBkt == nil { - return fmt.Errorf("Couldn't find bucket " + strings.Join(path[:idx], "/")) + bkt = bkt.Bucket([]byte(path[idx])) + if bkt == nil { + return fmt.Errorf("Couldn't find bucket " + strings.Join(path[:idx], " / ")) } } - bkt = newBkt } - // newBkt should have the last bucket in the path - berr := bkt.ForEach(func(k, v []byte) error { + + // bkt should have the last bucket in the path + berr = bkt.ForEach(func(k, v []byte) error { if v != nil { - // Not a bucket + // Must be a key ret = append(ret, string(k)) } return nil