diff --git a/cmd/gime/helpers.go b/cmd/gime/helpers.go new file mode 100644 index 0000000..263302c --- /dev/null +++ b/cmd/gime/helpers.go @@ -0,0 +1,53 @@ +package main + +import ( + "errors" + "strconv" + + "git.bullercodeworks.com/brian/gime" +) + +func TimerToString(t *gime.TimeEntry) string { + var ret string + if t.StartsToday() { + ret = t.GetStart().Format("15:04 - ") + } else { + ret = t.GetStart().Format("2006/01/02 15:04:05 - ") + } + if !t.GetEnd().IsZero() { + if t.EndsToday() { + ret += t.GetEnd().Format("15:04") + } else { + ret += t.GetEnd().Format("2006/01/02 15:04:05") + } + } else { + ret += "**:**:**" + } + tags := t.GetTags() + if tags.Length() > 0 { + ret += " [ " + for i := 0; i < tags.Length(); i++ { + ret += tags.Get(i) + " " + } + ret += "]" + } + return ret +} + +// findTimerById takes a timer id and returns the TimeEntry and the type string +// of the entry corresponding to that id +// It searches TypeCurrent -> TypeRecent -> TypeArchive +func findTimerById(tmrId int) (*gime.TimeEntry, string, error) { + // Find the timer for this tmrId + var prevNum, numLoaded int + for i := range gdb.AllTypes { + timeCollection := gdb.LoadTimeEntryCollection(gdb.AllTypes[i]) + numLoaded += timeCollection.Length() + if numLoaded > tmrId { + // This is the set that it's in, the index we need is tmrId - prevNum + return timeCollection.Get(tmrId - prevNum), gdb.AllTypes[i], nil + } + prevNum = numLoaded + } + return nil, gime.TypeUnknown, errors.New("Unable to find timer with id: " + strconv.Itoa(tmrId)) +} diff --git a/cmd/gime/main.go b/cmd/gime/main.go index 6d7a01c..a5893c9 100644 --- a/cmd/gime/main.go +++ b/cmd/gime/main.go @@ -8,16 +8,20 @@ import ( "time" "git.bullercodeworks.com/brian/gime" + userConfig "github.com/br0xen/user-config" ) const ( - AppName = "gime" - AppVersion = 1 + AppName = "gime" + AppVersion = 1 + DefDBName = "gime.db" + DefArchDBName = "gimearch.db" ) var validOperations map[string][]string var activeTimeEntries *gime.TimeEntryCollection var gdb *gime.GimeDB +var cfg *userConfig.Config func main() { var ret int @@ -33,14 +37,20 @@ func main() { } switch parms[0] { + case "config": + ret = cmdDoConfig(parms[1:]) + case "delete", "remove": + ret = cmdDeleteTimer(parms[1:]) case "help": - ret = printHelp() + ret = cmdPrintHelp() case "status": - ret = printStatus() + ret = cmdPrintStatus() case "start": - ret = startTimer(parms[1:]) + ret = cmdStartTimer(parms[1:]) case "end", "stop": - ret = stopTimer(parms[1:]) + ret = cmdStopTimer(parms[1:]) + case "list", "ls": + ret = cmdPrintList(parms[1:]) default: fmt.Println("Unknown command") ret = 1 @@ -48,7 +58,50 @@ func main() { os.Exit(ret) } -func printHelp() int { +func cmdDoConfig(args []string) int { + if len(args) == 0 { + fmt.Println("Invalid configuration options passed") + return 1 + } + + for _, opt := range args { + if strings.Index(opt, "=") == -1 { + // Single word triggers + switch opt { + case "reset": + fmt.Println("Resetting Configuration...") + cfg.Set("dbdir", cfg.GetConfigPath()+"/") + cfg.Set("dbname", DefDBName) + cfg.Set("dbarchname", DefArchDBName) + case "list": + fmt.Println("Current " + AppName + " config") + for _, v := range cfg.GetKeyList() { + fmt.Println(" " + v + ": " + cfg.Get(v)) + } + } + } 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] != '/' { + val = val + "/" + } + cfg.Set("dbdir", val) + case "dbname": + cfg.Set("dbname", pts[1]) + case "dbarchname": + cfg.Set("dbarchname", pts[1]) + } + } + } + } + return 0 +} + +func cmdPrintHelp() int { fmt.Println("gime - A simple timekeeping application\n") fmt.Println("Usage: gime [@timerID] [operation] [tags...]") for _, v := range validOperations { @@ -64,20 +117,34 @@ func loadActiveTimeEntries() { activeTimeEntries = gdb.LoadTimeEntryCollection(gime.TypeCurrent) } -func printStatus() int { +func cmdPrintList(args []string) int { + /* + var err error + loadActiveTimeEntries() + // By default, list all entries for today + currTime := time.Now() + dur := currTime.Hour()*time.Hour + currTime.Minute()*time.Minute + if len(args) < 1 { + + } + */ + return 0 +} + +func cmdPrintStatus() int { loadActiveTimeEntries() curr := time.Now() fmt.Println("Current Time:", curr.Format(time.Stamp)) if activeTimeEntries.Length() == 0 { fmt.Println("No timer running") } else { - fmt.Print("Currently Active Timers\n") + fmt.Print("Active Timers (", activeTimeEntries.Length(), ")\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"), " ") + fmt.Print(" @"+strconv.Itoa(i)+" ", v.GetStart().Format("15:04"), " ") } else { - fmt.Print("@"+strconv.Itoa(i)+" ", v.GetStart().Format(time.Stamp), " ") + fmt.Print(" @"+strconv.Itoa(i)+" ", v.GetStart().Format(time.Stamp), " ") } since := time.Since(v.GetStart()).String() since = strings.Split(since, ".")[0] @@ -99,78 +166,44 @@ func printStatus() int { 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() - tagStart := 0 - if len(args) > 0 { - // Check if the first argument looks like a date/time - tm, err = time.Parse("15:04", args[0]) - if err != nil { - tm, err = time.Parse(time.Kitchen, args[0]) - } - } - if 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:] - } - fmt.Println(tm) - 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 - } - return 0 -} - -func stopTimer(args []string) int { - var err error - var tm time.Time - st := time.Now() - if len(args) > 0 { - // Check if the first argument looks like a date/time - tm, err = time.Parse("15:04", args[0]) - if err != nil { - tm, err = time.Parse(time.Kitchen, args[0]) - } - } - _, _ = tm, st - return 0 -} - func initialize() { var err error - if gdb, err = gime.LoadDatabase("./", "gime.db", "gimearch.db"); err != nil { - fmt.Println("Error loading the database") - os.Exit(1) - } - validOperations = make(map[string][]string) + 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]", + } + validOperations["detail"] = []string{ + "detail @id - Print details about a timer", + } + validOperations["delete"] = []string{ + "delete @id - Delete a timer", + } + validOperations["end"] = []string{ + "end - The same as stop", + } + validOperations["help"] = []string{ + "help - Print this", + } + 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", + " To list entries by tag, preceed the tags with a +", + } + validOperations["ls"] = []string{ + "ls [duration] [+tags] - The same as list", + } validOperations["status"] = []string{ - "status - Print the current status of the timer", + "status - Print the status of all active timers", } validOperations["start"] = []string{ "start [time] [tags ...] - Start a timer with the given tags (space separated)", @@ -183,11 +216,29 @@ func initialize() { " If the first sub-argument given looks like a time,", " the timer will be stopped then (past or future).", } - validOperations["end"] = []string{ - "end - The same as stop", + + // Load the Config + cfg, err = userConfig.NewConfig(AppName) + if err != nil { + fmt.Println(err.Error()) + fmt.Println("Creating new config") + cfg.Save() } - validOperations["help"] = []string{ - "help - Print this", + // 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 dbarchivename isn't set, set it to the default archive database filename + if cfg.Get("dbarchname") == "" { + cfg.Set("dbarchname", DefArchDBName) + } + 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) } } diff --git a/cmd/gime/timer_operations.go b/cmd/gime/timer_operations.go new file mode 100644 index 0000000..1dad07a --- /dev/null +++ b/cmd/gime/timer_operations.go @@ -0,0 +1,128 @@ +package main + +import ( + "fmt" + "strconv" + "time" + + "git.bullercodeworks.com/brian/gime" +) + +// switchTimer performs a stop on any currently running timers +// and starts a new timer with the given arguments +func switchTimer(args []string) int { + loadActiveTimeEntries() + return 0 +} + +// 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 + tm := time.Now() + tagStart := 0 + if len(args) > 0 { + // Check if the first argument looks like a date/time + tm, err = time.Parse("15:04", args[0]) + if err != nil { + tm, err = time.Parse(time.Kitchen, args[0]) + } + } + if 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:] + } + fmt.Println(tm) + 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 + } + return 0 +} + +// cmdStopTimer takes parameters that describe which times to stop +func cmdStopTimer(args []string) int { + var err error + tm := time.Now() + actTimers := gdb.LoadTimeEntryCollection(gime.TypeCurrent) + if actTimers.Length() != 1 && (len(args) < 1 || args[0][0] != '@') { + fmt.Println("Couldn't determine which timer(s) to stop") + return 1 + } + var tmr *gime.TimeEntry + if actTimers.Length() == 1 { + // only one timer running + tmr = actTimers.Get(0) + } else { + // We've got a timer id to delete + timerId, err := strconv.Atoi(args[0][1:]) + if err != nil { + fmt.Println("Error parsing timer id: " + err.Error()) + return 1 + } + tmr, _, err = findTimerById(timerId) + if err != nil { + fmt.Println(err.Error()) + return 1 + } + } + + if len(args) > 0 { + // Check if the first argument looks like a date/time + tm, err = time.Parse("15:04", args[0]) + if err != nil { + tm, err = time.Parse(time.Kitchen, args[0]) + } + } + + tmr.SetEnd(tm) + if err = gdb.UpdateTimeEntry(tmr); err != nil { + fmt.Println(err.Error()) + return 1 + } + fmt.Println(TimerToString(tmr)) + return 0 +} + +// cmdDeleteTimer takes parameters that describe the timers to be deleted. +func cmdDeleteTimer(args []string) int { + var err error + if len(args) < 1 || args[0][0] != '@' { + fmt.Println("Couldn't determine which timer(s) to delete") + return 1 + } + + // We've got a timer id to delete + timerId, err := strconv.Atoi(args[0][1:]) + if err != nil { + fmt.Println("Error parsing timer id: " + err.Error()) + return 1 + } + tmr, tp, err := findTimerById(timerId) + if err != nil { + fmt.Println(err.Error()) + return 1 + } + if gdb.RemoveTimeEntry(tmr.GetUUID()) != nil { + fmt.Println("Error removing entry " + tp + "." + tmr.GetUUID()) + return 1 + } + fmt.Println("Deleted Time Entry: " + TimerToString(tmr)) + return 0 +} diff --git a/model.go b/model.go index 9a3df49..5a51c4e 100644 --- a/model.go +++ b/model.go @@ -2,6 +2,7 @@ package gime import ( "errors" + "fmt" "github.com/br0xen/boltease" ) @@ -13,6 +14,8 @@ type GimeDB struct { dbArchOpened int path string filename, arch string + + AllTypes []string } const ( @@ -22,8 +25,6 @@ const ( 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) { @@ -34,8 +35,12 @@ func LoadDatabase(path, name, archName string) (*GimeDB, error) { path: path, filename: name, arch: archName, + AllTypes: []string{TypeCurrent, TypeRecent, TypeArchive}, + } + if err := gdb.initDatabase(); err != nil { + fmt.Println(err.Error()) + return nil, err } - gdb.initDatabase() return &gdb, nil } diff --git a/model_timeentry.go b/model_timeentry.go index 6babff4..afb3032 100644 --- a/model_timeentry.go +++ b/model_timeentry.go @@ -8,28 +8,19 @@ 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 +// FindTimeEntryByUUID searches all entries +// for the time entry with the given uuid, return the TimeEntry, the type, and/or and error +// Types are searched TypeCurrent -> TypeRecent -> TypeArchive +func (gdb *GimeDB) FindTimeEntryByUUID(uuid string) (*TimeEntry, string, error) { + for i := range gdb.AllTypes { + timeCollection := gdb.LoadTimeEntryCollection(gdb.AllTypes[i]) + for j := 0; j < timeCollection.Length(); j++ { + if timeCollection.Get(j).GetUUID() == uuid { + return timeCollection.Get(j), gdb.AllTypes[i], 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) + return nil, TypeUnknown, errors.New("Unable to find time entry with uuid " + uuid) } // SaveTimeEntry creates a time entry in the database @@ -41,10 +32,6 @@ func (gdb *GimeDB) SaveTimeEntry(te *TimeEntry) error { if te.end.IsZero() { // Currently running tp = TypeCurrent - useDb, err = gdb.openDBType(tp) - if err != nil { - return err - } } else { // We have an end time. Does this entry go in 'recent' or 'archive' // We shove times that happened 30 days ago into 'archive' @@ -52,6 +39,10 @@ func (gdb *GimeDB) SaveTimeEntry(te *TimeEntry) error { tp = TypeArchive } } + useDb, err = gdb.openDBType(tp) + if err != nil { + return err + } tePath := []string{tp, te.start.Format(time.RFC3339)} if err = useDb.SetValue(tePath, "uuid", te.uuid); err != nil { return err @@ -75,40 +66,28 @@ func (gdb *GimeDB) UpdateTimeEntry(te *TimeEntry) error { if te.uuid == "" { return errors.New("Given time entry has no uuid") } - oldTe, err := gdb.FindTimeEntryByUUID(TypeUnknown, te.uuid) - if err != nil { + if err = gdb.RemoveTimeEntry(te.uuid); err != nil { return err } - + return gdb.SaveTimeEntry(te) } -// 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) - } +// RemoveTimeEntry removes a time entry with the given uuid from the database +func (gdb *GimeDB) RemoveTimeEntry(uuid string) error { + fndEntry, tp, err := gdb.FindTimeEntryByUUID(uuid) + if err != 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 { - return ret + return err } defer gdb.closeDBType(tp) - - return nil, errors.New("Unable to find time entry with uuid " + uuid) + if err != nil { + return err + } + return useDb.DeleteBucket([]string{tp}, fndEntry.start.Format(time.RFC3339)) } // dbGetAllTimeEntries gets all time entries of a specific type @@ -121,8 +100,8 @@ func (gdb *GimeDB) RemoveTimeEntry(tp, uuid string) error { func (gdb *GimeDB) dbGetAllTimeEntries(tp string) []TimeEntry { var ret []TimeEntry if tp == TypeUnknown { - for _, v := range AllTypes { - ret = append(ret, gdb.dbGetAllTimeEntries(v)) + for _, v := range gdb.AllTypes { + ret = append(ret, gdb.dbGetAllTimeEntries(v)...) } return ret } @@ -152,13 +131,13 @@ func (gdb *GimeDB) dbGetTimeEntry(tp, sttm string) (*TimeEntry, error) { var err error if tp == TypeUnknown { // Loop through the types and return the first entry found that matches - for _, v := range AllTypes { + for _, v := range gdb.AllTypes { ret, _ = gdb.dbGetTimeEntry(v, sttm) if ret != nil { return ret, nil } } - return nil, errors.Error("Couldn't find time entry") + return nil, errors.New("Couldn't find time entry") } var useDb *boltease.DB if useDb, err = gdb.openDBType(tp); err != nil { diff --git a/timeentry.go b/timeentry.go index 28591e1..918075b 100644 --- a/timeentry.go +++ b/timeentry.go @@ -37,6 +37,11 @@ func CreateTimeEntry(s, e time.Time, t *TagCollection) (*TimeEntry, error) { return ret, nil } +// GetUUID returns the UUID for the entry +func (t *TimeEntry) GetUUID() string { + return t.uuid +} + // GetStart returns the start time for the entry func (t *TimeEntry) GetStart() time.Time { return t.start @@ -95,3 +100,40 @@ func (t *TimeEntry) RemoveTag(s string) { func (t *TimeEntry) Equals(tst *TimeEntry) bool { return t.uuid == tst.uuid } + +func (t *TimeEntry) StartsToday() bool { + if time.Since(t.GetStart()).Hours() > 24 { + return false + } + return t.GetStart().Day() == time.Now().Day() +} + +func (t *TimeEntry) EndsToday() bool { + if time.Since(t.GetEnd()).Hours() > 24 { + return false + } + return t.GetEnd().Day() == time.Now().Day() +} + +func (t *TimeEntry) String() string { + var ret string + ret = t.GetStart().Format(time.RFC3339) + if !t.GetEnd().IsZero() { + ret += " - " + t.GetEnd().Format(time.RFC3339) + } + tags := t.GetTags() + if tags.Length() > 0 { + ret += " [ " + for i := 0; i < tags.Length(); i++ { + ret += tags.Get(i) + " " + } + ret += "]" + } + if t.GetEnd().IsZero() { + ret += " Running" + } + if t.GetUUID() != "" { + ret += " (" + t.GetUUID() + ")" + } + return ret +}