diff --git a/cmd/gotime-cui/gotime-cui b/cmd/gotime-cui/gotime-cui index eb582a7..49e53a9 100755 Binary files a/cmd/gotime-cui/gotime-cui and b/cmd/gotime-cui/gotime-cui differ diff --git a/cmd/gotime-cui/main.go b/cmd/gotime-cui/main.go index be1939d..9d42a36 100644 --- a/cmd/gotime-cui/main.go +++ b/cmd/gotime-cui/main.go @@ -3,12 +3,12 @@ package main import ( "fmt" "os" + "strconv" "gogs.bullercodeworks.com/brian/gotime" ) func main() { - var dir string /* err := termbox.Init() if err != nil { @@ -16,23 +16,38 @@ func main() { } defer termbox.Close() */ + dir := "/home/brbuller/.timewarrior/" - if len(os.Args) > 1 { - dir = os.Args[1] + op := os.Args[1] + var id int + var err error + + if len(os.Args) > 2 { + id, err = strconv.Atoi(os.Args[2]) + if err != nil { + panic(err) + } } + got := gotime.Create(dir) - t := got.GetAllTimers() - for i := range t { - fmt.Println(t[i].ToJsonString()) - } - fmt.Println("=== Start Timer ===") - ts := gotime.CreateStartTimerTxns(&t[len(t)-1]) - for i := range ts { - fmt.Println(ts[i].ToString()) - } - fmt.Println("=== Stop Timer ===") - ts = gotime.CreateStopTimerTxns(&t[len(t)-1]) - for i := range ts { - fmt.Println(ts[i].ToString()) + + switch op { + case "ls": + tmrs := got.GetAllTimers() + for i := range tmrs { + fmt.Println("@" + strconv.Itoa(tmrs[i].Id) + ": " + tmrs[i].ToString()) + } + case "tag": + if _, err := got.AddTagsToTimer(id, os.Args[3:]); err != nil { + panic(err) + } + case "untag": + if _, err := got.RemoveTagsFromTimer(id, os.Args[3:]); err != nil { + panic(err) + } + case "start": + got.StartTimer() + case "stop": + got.StopTimer() } } diff --git a/gotime.go b/gotime.go index a5b8505..6392f9a 100644 --- a/gotime.go +++ b/gotime.go @@ -1,18 +1,22 @@ package gotime import ( + "errors" "fmt" + "io" "io/ioutil" + "os" "strings" "time" ) type GoTime struct { - Dir string - timers []Timer - files []string - timeFormat string - Tags []string + Dir string + timers []Timer + files []string + timeFormat string + Tags []string + CurrDataFile string } // Create creates a new instance of GoTime @@ -21,6 +25,8 @@ func Create(dir string) *GoTime { g := &GoTime{Dir: dir} g.timeFormat = "20060102T150405Z" + g.CurrDataFile = time.Now().Format("2006-01.data") + g.files = g.getTimerFiles() for _, f := range g.files { g.addTimersFromFile(f) @@ -64,6 +70,13 @@ func (g *GoTime) GetAllTimers() []Timer { return g.timers } +func (g *GoTime) GetTimer(id int) (*Timer, error) { + if len(g.timers) >= id { + return &g.timers[len(g.timers)-id], nil + } + return nil, errors.New("Invalid Timer Id") +} + // getTimerFiles Returns a string slice of all of the data file names func (g *GoTime) getTimerFiles() []string { var ret []string @@ -79,61 +92,27 @@ func (g *GoTime) getTimerFiles() []string { // addTimersFromFile Adds all timer lines from g.Dir/data/f and adds a timer for it func (g *GoTime) addTimersFromFile(f string) error { - content, err := ioutil.ReadFile(g.Dir + "/data/" + f) + lines, err := g.readDataFile(f) if err != nil { - fmt.Println(err) return err } - cntString := strings.TrimSpace(string(content)) - lines := strings.Split(cntString, "\n") for i := range lines { - if err = g.AddTimerFromString(lines[i]); err != nil { + t, err := CreateTimerFromString(lines[i]) + if err != nil { return err } - } - return err -} - -// AddTimerFromString takes a string in the format of the lines from the data file -// and builds a timer from it. -func (g *GoTime) AddTimerFromString(st string) error { - var err error - t := new(Timer) - flds := strings.Fields(st) - if len(flds) > 1 && flds[0] == "inc" { - // Start Time - if len(flds) >= 2 { - if t.Beg, err = time.Parse(g.timeFormat, flds[1]); err != nil { - return err - } - } - // End Time - if len(flds) >= 4 && flds[2] == "-" { - if t.End, err = time.Parse(g.timeFormat, flds[3]); err != nil { - return err - } - } - var inTags bool - for i := range flds { - if flds[i] == "#" { - inTags = true - continue - } - if inTags { - tg := strings.Trim(flds[i], "\"") - t.Tags = append(t.Tags, tg) - g.AddTag(tg) - } - } if !t.Beg.IsZero() { g.timers = append(g.timers, *t) + for i := range t.Tags { + g.AddTag(t.Tags[i]) + } } } return err } -// AddTag adds a tag to list of all used tags +// AddTag adds a tag to the list of all used tags func (g *GoTime) AddTag(tg string) { if !g.HasTag(tg) { g.Tags = append(g.Tags, tg) @@ -158,10 +137,10 @@ func (g *GoTime) StartTimer() *Timer { t := new(Timer) t.Beg = time.Now() g.timers = append(g.timers, *t) - // TODO: Add a line to the YYYY-MM.data file - // TODO: Update the Undo File - g.ResetIds() + g.AddTimerToCurrentDataFile(t) + + g.ResetIds() return g.Status() } @@ -170,16 +149,247 @@ func (g *GoTime) StopTimer() *Timer { if g.IsRunning() { g.Status().Stop() } - // TODO: Add a stop time to the last line in the YYYY-MM.data file - // TODO: Update the Undo File - return g.Status() -} - -// AddTag adds a new tag to the most recent timer -func (g *GoTime) AddTagToCurrentTimer(tg string) *Timer { - g.Status().AddTag(tg) - // TODO: Add the tag to the last line in the YYYY-MM.data file - // TODO: Update the Undo File + g.UpdateTimerInDataFile(1, g.Status()) return g.Status() } + +func (g *GoTime) RemoveTagsFromTimer(id int, tg []string) (*Timer, error) { + tmr, err := g.GetTimer(id) + if err != nil { + return nil, err + } + for i := range tg { + tmr.RemoveTag(tg[i]) + } + err = g.UpdateTimerInDataFile(id, tmr) + if err != nil { + return nil, err + } + + return tmr, nil +} + +func (g *GoTime) AddTagsToTimer(id int, tg []string) (*Timer, error) { + tmr, err := g.GetTimer(id) + if err != nil { + return nil, err + } + for i := range tg { + tmr.AddTag(tg[i]) + } + err = g.UpdateTimerInDataFile(id, tmr) + if err != nil { + return nil, err + } + + return tmr, nil +} + +// AddTag adds new tags to the most recent timer +func (g *GoTime) AddTagsToCurrentTimer(tgs []string) (*Timer, error) { + return g.AddTagsToTimer(1, tgs) +} + +func (g *GoTime) AddTimerToCurrentDataFile(tmr *Timer) error { + f, err := os.OpenFile(g.Dir+"/data/"+g.CurrDataFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644) + if err != nil { + return err + } + defer f.Close() + + _, err = f.WriteString(tmr.ToString()) + return err +} + +func (g *GoTime) UpdateTimerInDataFile(id int, tmr *Timer) error { + var err error + var datFileNm string + var txn *Transaction + + // We have to step back through the files to find this id + var lines []string + var linesCnt int + + for i := len(g.files) - 1; i >= 0; i-- { + lines, err = g.readDataFile(g.files[i]) + if err != nil { + return err + } else { + if linesCnt+len(lines) >= id { + datFileNm = g.files[i] + } + } + if datFileNm != "" { + break + } + linesCnt += len(lines) + } + + // datFileNm should be the right data file, linesCnt should be the starting index in that file + // We have to find the line in the file that is (id-linesCnt) lines from the bottom + // First back the file up, in case something messes up + if err = g.backupDataFile(datFileNm); err != nil { + return err + } + + var f *os.File + f, err = os.OpenFile(g.Dir+"/data/"+datFileNm, os.O_RDWR|os.O_TRUNC, 0666) + + for i := range lines { + if linesCnt+(len(lines)-i) == id { + // Found the timer line + if _, err = f.WriteString(tmr.ToString() + "\n"); err != nil { + break + } + var oldTmr *Timer + oldTmr, err = CreateTimerFromString(lines[i]) + txn = CreateTxn(oldTmr, tmr) + } else { + if _, err = f.WriteString(lines[i] + "\n"); err != nil { + break + } + } + } + + f.Close() + + if err != nil { + // An error occurred, restore the backup + if restErr := g.restoreBackupFile(datFileNm); restErr != nil { + return errors.New(err.Error() + " (Backup Restore Error: " + restErr.Error() + ")") + } + return err + } + // No error, add the transaction to the undo file + if undoErr := g.writeTxnToUndoFile(txn); undoErr != nil { + return undoErr + } + + return err +} + +func (g *GoTime) readDataFile(fn string) ([]string, error) { + var lines []string + content, err := ioutil.ReadFile(g.Dir + "/data/" + fn) + if err != nil { + fmt.Println(err) + return lines, err + } + cntString := strings.TrimSpace(string(content)) + + lines = strings.Split(cntString, "\n") + return lines, err +} + +func (g *GoTime) backupDataFile(fn string) error { + var err error + if _, err = os.Stat(g.Dir + "/data-backups"); err != nil { + if err = os.Mkdir(g.Dir+"/data-backups", 0755); err != nil { + return err + } + } + return g.copyFile(g.Dir+"/data/"+fn, g.Dir+"/data-backups/"+fn) +} + +func (g *GoTime) restoreBackupFile(fn string) error { + var err error + if _, err = os.Stat(g.Dir + "/data-backups"); err != nil { + if err = os.Mkdir(g.Dir+"/data-backups", 0755); err != nil { + return err + } + } + return g.copyFile(g.Dir+"/data-backups/"+fn, g.Dir+"/data/"+fn) +} + +func (g *GoTime) writeTxnToUndoFile(txn *Transaction) error { + // TODO: For now we're ignoring the undo file. TimeWarrior doesn't support it yet + /* + // Backup the undo file + var err error + var f *os.File + err = g.backupDataFile("undo.data") + if err != nil { + // Couldn't backup undo file... + return err + } + f, err = os.OpenFile(g.Dir+"/data/undo.data", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644) + if err != nil { + return err + } + + f.Close() + + if _, err = f.WriteString(txn.ToString()); err != nil { + undoErr := g.restoreBackupFile("undo.data") + if undoErr != nil { + return errors.New(err.Error() + " (Backup Restore Error: " + undoErr.Error() + ")") + } + return err + } + */ + return nil +} + +func (g *GoTime) copyFile(src, dst string) (err error) { + sfi, err := os.Stat(src) + if err != nil { + return + } + if !sfi.Mode().IsRegular() { + // cannot copy non-regular files (e.g., directories, + // symlinks, devices, etc.) + return fmt.Errorf("CopyFile: non-regular source file %s (%q)", sfi.Name(), sfi.Mode().String()) + } + dfi, err := os.Stat(dst) + if err != nil { + if !os.IsNotExist(err) { + return + } + } else { + if !(dfi.Mode().IsRegular()) { + return fmt.Errorf("CopyFile: non-regular destination file %s (%q)", dfi.Name(), dfi.Mode().String()) + } + if os.SameFile(sfi, dfi) { + return + } + } + if err = os.Link(src, dst); err == nil { + return + } + err = g.copyFileContents(src, dst) + return +} + +func (g *GoTime) copyFileContents(src, dst string) (err error) { + in, err := os.Open(src) + if err != nil { + return + } + defer in.Close() + out, err := os.Create(dst) + if err != nil { + return + } + defer func() { + cerr := out.Close() + if err == nil { + err = cerr + } + }() + if _, err = io.Copy(out, in); err != nil { + return + } + err = out.Sync() + return +} + +func TagIsMultiword(tg string) bool { + if strings.Contains(tg, " ") { + return true + } + if strings.Contains(tg, "-") { + return true + } + return false +} diff --git a/timer.go b/timer.go index cf35bfc..35cf067 100644 --- a/timer.go +++ b/timer.go @@ -7,10 +7,46 @@ import ( ) type Timer struct { - Id int - Beg time.Time - End time.Time - Tags []string + Id int + Beg time.Time + End time.Time + Tags []string + timeFormat string +} + +// CreateTimerFromString takes a string in the format of the lines from the data file +// and builds a timer from it. +func CreateTimerFromString(st string) (*Timer, error) { + var err error + t := new(Timer) + t.timeFormat = "20060102T150405Z" + flds := strings.Fields(st) + if len(flds) > 1 && flds[0] == "inc" { + // Start Time + if len(flds) >= 2 { + if t.Beg, err = time.Parse(t.timeFormat, flds[1]); err != nil { + return t, err + } + } + // End Time + if len(flds) >= 4 && flds[2] == "-" { + if t.End, err = time.Parse(t.timeFormat, flds[3]); err != nil { + return t, err + } + } + var inTags bool + for i := range flds { + if flds[i] == "#" { + inTags = true + continue + } + if inTags { + tg := strings.Trim(flds[i], "\"") + t.AddTag(tg) + } + } + } + return t, err } func (t *Timer) Start() { @@ -88,8 +124,12 @@ func (t *Timer) ToString() string { if len(t.Tags) > 0 { ret += " # " for i := range t.Tags { - if strings.Contains(t.Tags[i], " ") { - ret += "\"" + t.Tags[i] + "\"" + if TagIsMultiword(t.Tags[i]) { + ret += "\"" + } + ret += t.Tags[i] + if TagIsMultiword(t.Tags[i]) { + ret += "\"" } ret += " " } diff --git a/transaction.go b/transaction.go index c754ac5..89df269 100644 --- a/transaction.go +++ b/transaction.go @@ -1,7 +1,5 @@ package gotime -import "time" - type Transaction struct { tp string before *Timer @@ -16,8 +14,9 @@ func CreateStartTimerTxns(tmr *Timer) []Transaction { func CreateStopTimerTxns(tmr *Timer) []Transaction { t1 := CreateIntervalTxn() - t1.before = tmr - t1.before.End = *new(time.Time) + t1.before = new(Timer) + t1.before.Beg = tmr.Beg + t1.before.Tags = tmr.Tags t2 := CreateIntervalTxn() t2.after = tmr @@ -25,6 +24,26 @@ func CreateStopTimerTxns(tmr *Timer) []Transaction { return []Transaction{*t1, *t2} } +func CreateTagChangeTxns(tmr *Timer, oldTags []string) []Transaction { + t := CreateIntervalTxn() + t.before = new(Timer) + t.before.Beg = tmr.Beg + t.before.End = tmr.End + t.before.Tags = oldTags + t.after = tmr + + return []Transaction{*t} +} + +func CreateTxn(oldTmr, newTmr *Timer) *Transaction { + t := Transaction{ + tp: "interval", + before: oldTmr, + after: newTmr, + } + return &t +} + func CreateIntervalTxn() *Transaction { t := new(Transaction) t.tp = "interval" @@ -70,4 +89,12 @@ txn: before: after: {"start":"20170111T201826Z","end":"20170111T221747Z","tags":["bcw","bcw-gotime","work"]} + * + * + * TAG Transaction + +txn: + type: interval + before: {"start":"20170112T123633Z","tags":["bcw","bcw-gotime","work"]} + after: {"start":"20170112T123633Z","tags":["bcw","bcw-gotime","test","work"]} */