GoTime Library is Functional
* Start/Stop timer * Add/Remove tags
This commit is contained in:
parent
7fbf90a111
commit
0f40569150
Binary file not shown.
@ -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()
|
||||
}
|
||||
}
|
||||
|
328
gotime.go
328
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
|
||||
}
|
||||
|
52
timer.go
52
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 += " "
|
||||
}
|
||||
|
@ -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"]}
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user